Skip to content

Commit 4b1df35

Browse files
fix: prevent timing out on short/skipped videos (#28643)
1 parent 9e1f1e7 commit 4b1df35

File tree

5 files changed

+86
-53
lines changed

5 files changed

+86
-53
lines changed

cli/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ _Released 1/16/2024 (PENDING)_
55

66
**Bugfixes:**
77

8+
- No longer wait for additional frames when recording a video for a spec that was skipped by the Cloud due to Auto Cancellation. Fixes [#27898](https://github.com/cypress-io/cypress/issues/27898).
89
- Now `node_modules` will not be ignored if a project path or a provided path to spec files contains it. Fixes [#23616](https://github.com/cypress-io/cypress/issues/23616).
910
- Updated display of assertions and commands with a URL argument to escape markdown formatting so that values are displayed as is and assertion values display as bold. Fixes [#24960](https://github.com/cypress-io/cypress/issues/24960) and [#28100](https://github.com/cypress-io/cypress/issues/28100).
1011
- When generating assertions via Cypress Studio, the preview of the generated assertions now correctly displays the past tense of 'expected' instead of 'expect'. Fixed in [#28593](https://github.com/cypress-io/cypress/pull/28593).

packages/server/lib/modes/run.ts

+14-14
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,18 @@ async function waitForTestsToFinishRunning (options: { project: Project, screens
554554

555555
debug('received project end')
556556

557+
_.defaults(results, {
558+
error: null,
559+
hooks: null,
560+
tests: null,
561+
video: null,
562+
screenshots: null,
563+
reporterStats: null,
564+
})
565+
566+
// Cypress Cloud told us to skip this spec
567+
const skippedSpec = results.skippedSpec
568+
557569
// https://github.com/cypress-io/cypress/issues/2370
558570
// delay 1 second if we're recording a video to give
559571
// the browser padding to render the final frames
@@ -562,26 +574,14 @@ async function waitForTestsToFinishRunning (options: { project: Project, screens
562574

563575
debug('received videoController %o', { videoController })
564576

565-
if (videoController) {
577+
if (videoController && !skippedSpec) {
566578
const span = telemetry.startSpan({ name: 'video:capture:delayToLetFinish' })
567579

568580
debug('delaying to extend video %o', { DELAY_TO_LET_VIDEO_FINISH_MS })
569581
await Bluebird.delay(DELAY_TO_LET_VIDEO_FINISH_MS)
570582
span?.end()
571583
}
572584

573-
_.defaults(results, {
574-
error: null,
575-
hooks: null,
576-
tests: null,
577-
video: null,
578-
screenshots: null,
579-
reporterStats: null,
580-
})
581-
582-
// Cypress Cloud told us to skip this spec
583-
const skippedSpec = results.skippedSpec
584-
585585
if (screenshots) {
586586
results.screenshots = screenshots
587587
}
@@ -603,7 +603,7 @@ async function waitForTestsToFinishRunning (options: { project: Project, screens
603603
}
604604

605605
try {
606-
await videoController.endVideoCapture()
606+
await videoController.endVideoCapture(!skippedSpec)
607607
debug('ended video capture')
608608
} catch (err) {
609609
videoCaptureFailed = true

packages/server/lib/video_capture.ts

+22-15
Original file line numberDiff line numberDiff line change
@@ -114,35 +114,36 @@ export type StartOptions = {
114114
// If set, expect input frames as webm chunks.
115115
webmInput?: boolean
116116
// Callback for asynchronous errors in video capturing/compression.
117-
onError?: (err: Error, stdout: string, stderr: string) => void
117+
onError?: (err: Error) => void
118118
}
119119

120120
export function start (options: StartOptions) {
121121
const pt = new stream.PassThrough()
122122
const ended = deferredPromise()
123-
let done = false
123+
let doneCapturing = false
124124
let wantsWrite = true
125-
let skippedChunksCount = 0
126-
let writtenChunksCount = 0
125+
let skippedFramesCount = 0
126+
let writtenFramesCount = 0
127127

128128
_.defaults(options, {
129129
onError () {},
130130
})
131131

132-
const endVideoCapture = function (waitForMoreChunksTimeout = 3000) {
133-
debugFrames('frames written:', writtenChunksCount)
132+
const endVideoCapture = function (waitForMoreFrames = true) {
133+
debugFrames('frames written:', writtenFramesCount)
134134

135135
// in some cases (webm) ffmpeg will crash if fewer than 2 buffers are
136136
// written to the stream, so we don't end capture until we get at least 2
137-
if (writtenChunksCount < 2) {
137+
if (writtenFramesCount < 2 && waitForMoreFrames) {
138138
return new Bluebird((resolve) => {
139139
pt.once('data', resolve)
140140
})
141141
.then(() => endVideoCapture())
142-
.timeout(waitForMoreChunksTimeout)
142+
.timeout(3000)
143+
.catch(() => endVideoCapture(false))
143144
}
144145

145-
done = true
146+
doneCapturing = true
146147

147148
pt.end()
148149

@@ -158,7 +159,7 @@ export function start (options: StartOptions) {
158159
// our stream yet because paint
159160
// events can linger beyond
160161
// finishing the actual video
161-
if (done) {
162+
if (doneCapturing) {
162163
return
163164
}
164165

@@ -185,7 +186,7 @@ export function start (options: StartOptions) {
185186
lengths[data.length] = true
186187
}
187188

188-
writtenChunksCount++
189+
writtenFramesCount++
189190

190191
debugFrames('writing video frame')
191192

@@ -200,9 +201,9 @@ export function start (options: StartOptions) {
200201
})
201202
}
202203
} else {
203-
skippedChunksCount += 1
204+
skippedFramesCount += 1
204205

205-
return debugFrames('skipping video frame %o', { skipped: skippedChunksCount })
206+
return debugFrames('skipping video frame %o', { skipped: skippedFramesCount })
206207
}
207208
}
208209

@@ -228,8 +229,14 @@ export function start (options: StartOptions) {
228229
}).on('error', (err, stdout, stderr) => {
229230
debug('capture errored: %o', { error: err.message, stdout, stderr })
230231

231-
// bubble errors up
232-
options.onError?.(err, stdout, stderr)
232+
if (err.message.includes('ffmpeg exited with code 1: pipe:0')) {
233+
err.message = 'Insufficient frames captured to create video.'
234+
}
235+
236+
// bubble errors up if occurs before endCapture is called
237+
if (!doneCapturing) {
238+
options.onError?.(err)
239+
}
233240

234241
// reject the ended promise
235242
return ended.reject(err)

packages/server/test/integration/video_capture_spec.ts

+48-23
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import path from 'path'
44
import fse from 'fs-extra'
55
import os from 'os'
66

7+
const image1Path = path.join(__dirname, '..', '..', '..', 'icons', 'assets', 'cypress.iconset', 'icon_16x16.png')
8+
const image2Path = path.join(__dirname, '..', '..', '..', 'icons', 'assets', 'cypress.iconset', 'icon_32x32.png')
9+
const image3Path = path.join(__dirname, '..', '..', '..', 'icons', 'assets', 'cypress.iconset', 'icon_128x128.png')
10+
711
async function startSpiedVideoCapture (videoName, options = {}) {
812
const props = await videoCapture.start({ videoName, ...options })
913

@@ -28,7 +32,7 @@ Output file #0 does not contain any stream\n`
2832
}
2933

3034
describe('Video Capture', () => {
31-
context('#start', () => {
35+
context('#start.writeVideoFrame', () => {
3236
let tmpFilename
3337

3438
beforeEach(() => {
@@ -63,27 +67,6 @@ describe('Video Capture', () => {
6367
await expect(endVideoCapture()).rejectedWith(END_OF_FILE_ERROR)
6468
})
6569

66-
it('will eventually timeout on single frame write', async () => {
67-
const { writeVideoFrameAsBuffer, endVideoCapture } = await startSpiedVideoCapture(tmpFilename)
68-
69-
writeVideoFrameAsBuffer('foo')
70-
71-
await expect(endVideoCapture(1)).be.rejectedWith('operation timed out')
72-
})
73-
74-
// https://github.com/cypress-io/cypress/issues/6408
75-
it('waits for at least 2 stream writes before ending', async () => {
76-
const { writeVideoFrameAsBuffer, endVideoCapture, END_OF_FILE_ERROR } = await startSpiedVideoCapture(tmpFilename)
77-
78-
writeVideoFrameAsBuffer('foo')
79-
80-
const endVideoCaptureResult = endVideoCapture()
81-
82-
writeVideoFrameAsBuffer('foobar')
83-
84-
await expect(endVideoCaptureResult).rejectedWith(END_OF_FILE_ERROR)
85-
})
86-
8770
// https://github.com/cypress-io/cypress/issues/16648
8871
context('deduping frames', async () => {
8972
it('does not dedupe when not webminput', async () => {
@@ -94,7 +77,6 @@ describe('Video Capture', () => {
9477
writeVideoFrameAsBuffer('foo')
9578
writeVideoFrameAsBuffer('foo')
9679
expect(_pt.write).callCount(4)
97-
// await expect(endVideoCapture()).rejectedWith(END_OF_FILE_ERROR)
9880
})
9981
})
10082

@@ -108,4 +90,47 @@ describe('Video Capture', () => {
10890
expect(_pt.write).calledOnce
10991
})
11092
})
93+
94+
context('#start.endVideoCapture', () => {
95+
let tmpFilename
96+
97+
beforeEach(() => {
98+
tmpFilename = path.join(fse.mkdtempSync(path.join(os.tmpdir(), 'cy-video-')), 'video.mp4')
99+
})
100+
101+
it('ends immediately if more than two frames written', async () => {
102+
const { writeVideoFrame, endVideoCapture } = await startSpiedVideoCapture(tmpFilename)
103+
104+
writeVideoFrame(fse.readFileSync(image1Path))
105+
writeVideoFrame(fse.readFileSync(image2Path))
106+
writeVideoFrame(fse.readFileSync(image3Path))
107+
108+
const waitForMoreFrames = false
109+
110+
await endVideoCapture(waitForMoreFrames)
111+
})
112+
113+
// https://github.com/cypress-io/cypress/issues/6408
114+
it('waits for at least 2 stream writes before ending if spec not skipped by the cloud', async () => {
115+
const { writeVideoFrame, endVideoCapture } = await startSpiedVideoCapture(tmpFilename)
116+
117+
writeVideoFrame(fse.readFileSync(image1Path))
118+
119+
const waitForMoreFrames = true
120+
const endVideoCaptureResult = endVideoCapture(waitForMoreFrames)
121+
122+
writeVideoFrame(fse.readFileSync(image2Path))
123+
124+
await endVideoCaptureResult
125+
})
126+
127+
it('ends immediately if less than two frames have been written and spec is skipped by the cloud', async () => {
128+
const { writeVideoFrame, endVideoCapture } = await startSpiedVideoCapture(tmpFilename)
129+
130+
writeVideoFrame(fse.readFileSync(image1Path))
131+
const waitForMoreFrames = false
132+
133+
await endVideoCapture(waitForMoreFrames)
134+
})
135+
})
111136
})

packages/types/src/video.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export type BrowserVideoController = {
2525
/**
2626
* A function that resolves once the video is fully captured and flushed to disk.
2727
*/
28-
endVideoCapture: () => Promise<void>
28+
endVideoCapture: (waitForMoreFrames: boolean) => Promise<void>
2929
/**
3030
* Timestamp of when the video capture started - used for chapter timestamps.
3131
*/

0 commit comments

Comments
 (0)