detached 8d9898d

fix(media): bound encoder memory and report short or broken writes

commit
8d9898dcc55ec8aab1119ae4e18ee5cf13a73e9f
author
brice · · 2026-09-30 08:16 UTC
committer
brice · · 2026-09-30 14:48 UTC
parent
ca02ed9
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +224 -9
M .mockery.yml +7 -0
@@ -19,3 +19,10 @@ packages:
1919 ScreenshotWriter:
2020 VideoWriter:
2121 Selector:
22+ io:
23+ config:
24+ dir: internal/adapters/ffmpeg
25+ pkgname: ffmpeg
26+ filename: io_mock_test.go
27+ interfaces:
28+ Writer:
@@ -19,3 +19,10 @@ packages:
19 ScreenshotWriter:19 ScreenshotWriter:
20 VideoWriter:20 VideoWriter:
21 Selector:21 Selector:
22+ io:
23+ config:
24+ dir: internal/adapters/ffmpeg
25+ pkgname: ffmpeg
26+ filename: io_mock_test.go
27+ interfaces:
28+ Writer:
M internal/adapters/ffmpeg/ffmpeg.go +33 -8
@@ -1,5 +1,13 @@
11 // Package ffmpeg encodes silent SDR video by streaming raw frames to an FFmpeg
22 // child process that muxes fragmented MP4 into a reserved output file.
3+//
4+// Encoder settings favour a small footprint over compression: libx264 veryfast
5+// with -tune zerolatency (no B-frames, no lookahead, sliced threads), two
6+// encoder threads and one filter thread. Measured on 60 paced 30 fps 1080p
7+// frames, against the earlier "fast" preset with 10-frame lookahead, this cut
8+// peak RSS from about 335 MB to about 141 MB and CPU time by more than half,
9+// and made the file about 43% larger (3.8 MB to 5.4 MB) at the same CRF 23.
10+// ultrafast would save only about 8 MB more but grow files a further 37%.
311 package ffmpeg
412
513 import (
@@ -49,7 +57,13 @@ const (
4957 stateClosed
5058 )
5159
52-type failure struct{ err error }
60+// failure is the sticky first error. fatal marks a real stream failure (as
61+// opposed to a caller's cancellation or abort), which Close must report even
62+// if the encoder itself exits cleanly.
63+type failure struct {
64+ err error
65+ fatal bool
66+}
5367
5468 // Writer implements ports.VideoWriter. Start, Write and Close have one owner;
5569 // Abort may be called concurrently with Write. A Writer is single-use.
@@ -144,8 +158,7 @@ func buildArgs(f ports.Frame, outW, outH, fps int) []string {
144158 "-video_size", strconv.Itoa(f.Width) + "x" + strconv.Itoa(f.Height),
145159 "-framerate", rate, "-i", "pipe:0",
146160 "-filter_threads", "1", "-an", "-vf", filter,
147- "-c:v", "libx264", "-threads", "2", "-preset", "fast", "-crf", "23",
148- "-x264-params", "rc-lookahead=10:sync-lookahead=0",
161+ "-c:v", "libx264", "-threads", "2", "-preset", "veryfast", "-tune", "zerolatency", "-crf", "23",
149162 "-g", strconv.Itoa(fps * 2), "-r", rate,
150163 "-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709", "-color_range", "tv",
151164 "-movflags", "frag_keyframe+empty_moov+default_base_moof",
@@ -236,7 +249,7 @@ func (w *Writer) Start(ctx context.Context, first ports.Frame, path string, s po
236249
237250 // interrupt records the first failure and unblocks a pending pipe write.
238251 func (w *Writer) interrupt(err error) {
239- w.cause.CompareAndSwap(nil, &failure{err})
252+ w.cause.CompareAndSwap(nil, &failure{err: err})
240253 _ = w.stdin.SetWriteDeadline(past)
241254 }
242255
@@ -317,7 +330,7 @@ func (w *Writer) writeFailed(err error) error {
317330 case <-t.C:
318331 }
319332 msg := fmt.Errorf("ffmpeg: write frame: %w%s", err, w.diagnostics())
320- w.cause.CompareAndSwap(nil, &failure{msg})
333+ w.cause.CompareAndSwap(nil, &failure{err: msg, fatal: true})
321334 return w.cause.Load().err
322335 }
323336
@@ -344,17 +357,25 @@ func (w *Writer) diagnostics() string {
344357 func writeRows(dst io.Writer, f ports.Frame) error {
345358 rowLen := f.Width * ports.BytesPerPixel
346359 if !f.YInvert && f.Stride == rowLen {
347- _, err := dst.Write(f.Pixels[:rowLen*f.Height])
348- return err
360+ return writeFull(dst, f.Pixels[:rowLen*f.Height])
349361 }
350362 for y := 0; y < f.Height; y++ {
351- if _, err := dst.Write(f.Row(y)); err != nil {
363+ if err := writeFull(dst, f.Row(y)); err != nil {
352364 return err
353365 }
354366 }
355367 return nil
356368 }
357369
370+// writeFull enforces the io.Writer contract: n < len(p) needs a non-nil error.
371+func writeFull(dst io.Writer, p []byte) error {
372+ n, err := dst.Write(p)
373+ if err == nil && n < len(p) {
374+ return io.ErrShortWrite
375+ }
376+ return err
377+}
378+
358379 // Close signals EOF, waits for the encoder to finalize the file (bounded, then
359380 // kills it) and reports the outcome. The video is kept when the encoder exits
360381 // cleanly after at least one frame, including after a cancelled context; it is
@@ -446,6 +467,10 @@ func (w *Writer) shutdown(abort bool) error {
446467 } else {
447468 err = fmt.Errorf("%w%s", err, w.diagnostics())
448469 }
470+ if c := w.cause.Load(); err == nil && c != nil && c.fatal {
471+ // The stream broke mid-recording even though the encoder exited cleanly.
472+ err = c.err
473+ }
449474 if err == nil && w.frames.Load() == 0 {
450475 err = ErrNoFrames
451476 }
@@ -1,5 +1,13 @@
1 // Package ffmpeg encodes silent SDR video by streaming raw frames to an FFmpeg1 // Package ffmpeg encodes silent SDR video by streaming raw frames to an FFmpeg
2 // child process that muxes fragmented MP4 into a reserved output file.2 // child process that muxes fragmented MP4 into a reserved output file.
3+//
4+// Encoder settings favour a small footprint over compression: libx264 veryfast
5+// with -tune zerolatency (no B-frames, no lookahead, sliced threads), two
6+// encoder threads and one filter thread. Measured on 60 paced 30 fps 1080p
7+// frames, against the earlier "fast" preset with 10-frame lookahead, this cut
8+// peak RSS from about 335 MB to about 141 MB and CPU time by more than half,
9+// and made the file about 43% larger (3.8 MB to 5.4 MB) at the same CRF 23.
10+// ultrafast would save only about 8 MB more but grow files a further 37%.
3 package ffmpeg11 package ffmpeg
4 12
5 import (13 import (
@@ -49,7 +57,13 @@ const (
49 stateClosed57 stateClosed
50 )58 )
51 59
52-type failure struct{ err error }60+// failure is the sticky first error. fatal marks a real stream failure (as
61+// opposed to a caller's cancellation or abort), which Close must report even
62+// if the encoder itself exits cleanly.
63+type failure struct {
64+ err error
65+ fatal bool
66+}
53 67
54 // Writer implements ports.VideoWriter. Start, Write and Close have one owner;68 // Writer implements ports.VideoWriter. Start, Write and Close have one owner;
55 // Abort may be called concurrently with Write. A Writer is single-use.69 // Abort may be called concurrently with Write. A Writer is single-use.
@@ -144,8 +158,7 @@ func buildArgs(f ports.Frame, outW, outH, fps int) []string {
144 "-video_size", strconv.Itoa(f.Width) + "x" + strconv.Itoa(f.Height),158 "-video_size", strconv.Itoa(f.Width) + "x" + strconv.Itoa(f.Height),
145 "-framerate", rate, "-i", "pipe:0",159 "-framerate", rate, "-i", "pipe:0",
146 "-filter_threads", "1", "-an", "-vf", filter,160 "-filter_threads", "1", "-an", "-vf", filter,
147- "-c:v", "libx264", "-threads", "2", "-preset", "fast", "-crf", "23",161+ "-c:v", "libx264", "-threads", "2", "-preset", "veryfast", "-tune", "zerolatency", "-crf", "23",
148- "-x264-params", "rc-lookahead=10:sync-lookahead=0",
149 "-g", strconv.Itoa(fps * 2), "-r", rate,162 "-g", strconv.Itoa(fps * 2), "-r", rate,
150 "-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709", "-color_range", "tv",163 "-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709", "-color_range", "tv",
151 "-movflags", "frag_keyframe+empty_moov+default_base_moof",164 "-movflags", "frag_keyframe+empty_moov+default_base_moof",
@@ -236,7 +249,7 @@ func (w *Writer) Start(ctx context.Context, first ports.Frame, path string, s po
236 249
237 // interrupt records the first failure and unblocks a pending pipe write.250 // interrupt records the first failure and unblocks a pending pipe write.
238 func (w *Writer) interrupt(err error) {251 func (w *Writer) interrupt(err error) {
239- w.cause.CompareAndSwap(nil, &failure{err})252+ w.cause.CompareAndSwap(nil, &failure{err: err})
240 _ = w.stdin.SetWriteDeadline(past)253 _ = w.stdin.SetWriteDeadline(past)
241 }254 }
242 255
@@ -317,7 +330,7 @@ func (w *Writer) writeFailed(err error) error {
317 case <-t.C:330 case <-t.C:
318 }331 }
319 msg := fmt.Errorf("ffmpeg: write frame: %w%s", err, w.diagnostics())332 msg := fmt.Errorf("ffmpeg: write frame: %w%s", err, w.diagnostics())
320- w.cause.CompareAndSwap(nil, &failure{msg})333+ w.cause.CompareAndSwap(nil, &failure{err: msg, fatal: true})
321 return w.cause.Load().err334 return w.cause.Load().err
322 }335 }
323 336
@@ -344,17 +357,25 @@ func (w *Writer) diagnostics() string {
344 func writeRows(dst io.Writer, f ports.Frame) error {357 func writeRows(dst io.Writer, f ports.Frame) error {
345 rowLen := f.Width * ports.BytesPerPixel358 rowLen := f.Width * ports.BytesPerPixel
346 if !f.YInvert && f.Stride == rowLen {359 if !f.YInvert && f.Stride == rowLen {
347- _, err := dst.Write(f.Pixels[:rowLen*f.Height])360+ return writeFull(dst, f.Pixels[:rowLen*f.Height])
348- return err
349 }361 }
350 for y := 0; y < f.Height; y++ {362 for y := 0; y < f.Height; y++ {
351- if _, err := dst.Write(f.Row(y)); err != nil {363+ if err := writeFull(dst, f.Row(y)); err != nil {
352 return err364 return err
353 }365 }
354 }366 }
355 return nil367 return nil
356 }368 }
357 369
370+// writeFull enforces the io.Writer contract: n < len(p) needs a non-nil error.
371+func writeFull(dst io.Writer, p []byte) error {
372+ n, err := dst.Write(p)
373+ if err == nil && n < len(p) {
374+ return io.ErrShortWrite
375+ }
376+ return err
377+}
378+
358 // Close signals EOF, waits for the encoder to finalize the file (bounded, then379 // Close signals EOF, waits for the encoder to finalize the file (bounded, then
359 // kills it) and reports the outcome. The video is kept when the encoder exits380 // kills it) and reports the outcome. The video is kept when the encoder exits
360 // cleanly after at least one frame, including after a cancelled context; it is381 // cleanly after at least one frame, including after a cancelled context; it is
@@ -446,6 +467,10 @@ func (w *Writer) shutdown(abort bool) error {
446 } else {467 } else {
447 err = fmt.Errorf("%w%s", err, w.diagnostics())468 err = fmt.Errorf("%w%s", err, w.diagnostics())
448 }469 }
470+ if c := w.cause.Load(); err == nil && c != nil && c.fatal {
471+ // The stream broke mid-recording even though the encoder exited cleanly.
472+ err = c.err
473+ }
449 if err == nil && w.frames.Load() == 0 {474 if err == nil && w.frames.Load() == 0 {
450 err = ErrNoFrames475 err = ErrNoFrames
451 }476 }
M internal/adapters/ffmpeg/ffmpeg_test.go +79 -1
@@ -14,6 +14,8 @@ import (
1414 "testing"
1515 "time"
1616
17+ "github.com/stretchr/testify/mock"
18+
1719 "github.com/bnema/nefercap/internal/ports"
1820 )
1921
@@ -107,7 +109,7 @@ func TestStartValidation(t *testing.T) {
107109 func TestBuildArgs(t *testing.T) {
108110 f := newFrame(63, 47, 252, 0)
109111 args := strings.Join(buildArgs(f, 62, 46, 30), " ")
110- for _, want := range []string{"-pix_fmt bgr0", "-video_size 63x47", "-vf scale=62:46:", "-preset fast", "libx264", "-threads 2", "-filter_threads 1", "pipe:3", "-an"} {
112+ for _, want := range []string{"-pix_fmt bgr0", "-video_size 63x47", "-vf scale=62:46:", "-preset veryfast", "-tune zerolatency", "libx264", "-threads 2", "-filter_threads 1", "pipe:3", "-an"} {
111113 if !strings.Contains(args, want) {
112114 t.Errorf("args missing %q: %s", want, args)
113115 }
@@ -694,3 +696,79 @@ func TestWriteSteadyStateAllocations(t *testing.T) {
694696 t.Fatal(err)
695697 }
696698 }
699+
700+func TestWriteRowsShortWrite(t *testing.T) {
701+ packed := newFrame(2, 2, 8, 1)
702+ padded := newFrame(2, 2, 12, 1)
703+ for name, tc := range map[string]struct {
704+ f ports.Frame
705+ calls int
706+ }{"packed": {packed, 1}, "rows": {padded, 1}} {
707+ t.Run(name, func(t *testing.T) {
708+ m := NewMockWriter(t)
709+ m.EXPECT().Write(mock.Anything).Return(3, nil).Times(tc.calls)
710+ if err := writeRows(m, tc.f); !errors.Is(err, io.ErrShortWrite) {
711+ t.Fatalf("got %v, want io.ErrShortWrite", err)
712+ }
713+ })
714+ }
715+ t.Run("error passes through", func(t *testing.T) {
716+ boom := errors.New("boom")
717+ m := NewMockWriter(t)
718+ m.EXPECT().Write(mock.Anything).Return(0, boom).Once()
719+ if err := writeRows(m, padded); !errors.Is(err, boom) {
720+ t.Fatalf("got %v", err)
721+ }
722+ })
723+}
724+
725+func TestCloseReportsStreamFailureEvenIfEncoderExitsCleanly(t *testing.T) {
726+ w := helperWriter(t, "drain")
727+ w.closeTimeout = 10 * time.Second
728+ path := filepath.Join(t.TempDir(), "v.mp4")
729+ f := newFrame(4, 4, 16, 0)
730+ ctx := context.Background()
731+ if err := w.Start(ctx, f, path, settings()); err != nil {
732+ t.Fatal(err)
733+ }
734+ if err := w.Write(ctx, f); err != nil {
735+ t.Fatal(err)
736+ }
737+ // Break the pipe under the writer: the encoder sees EOF and exits cleanly.
738+ _ = w.stdin.Close()
739+ werr := w.Write(ctx, f)
740+ if werr == nil {
741+ t.Fatal("Write to a broken pipe succeeded")
742+ }
743+ err := w.Close()
744+ if err == nil || !errors.Is(err, os.ErrClosed) {
745+ t.Fatalf("Close = %v, want the stream failure", err)
746+ }
747+ if _, serr := os.Stat(path); !errors.Is(serr, os.ErrNotExist) {
748+ t.Fatalf("broken recording remains: %v", serr)
749+ }
750+}
751+
752+func TestCloseKeepsVideoAfterCancelledWrite(t *testing.T) {
753+ w := helperWriter(t, "drain")
754+ w.closeTimeout = 10 * time.Second
755+ path := filepath.Join(t.TempDir(), "v.mp4")
756+ f := newFrame(4, 4, 16, 0)
757+ ctx, cancel := context.WithCancel(context.Background())
758+ if err := w.Start(ctx, f, path, settings()); err != nil {
759+ t.Fatal(err)
760+ }
761+ if err := w.Write(ctx, f); err != nil {
762+ t.Fatal(err)
763+ }
764+ cancel()
765+ if err := w.Write(ctx, f); !errors.Is(err, context.Canceled) {
766+ t.Fatalf("got %v", err)
767+ }
768+ if err := w.Close(); err != nil {
769+ t.Fatalf("cancellation must not fail Close: %v", err)
770+ }
771+ if got, _ := os.ReadFile(path); string(got) != "bytes=64" {
772+ t.Fatalf("encoder output %q", got)
773+ }
774+}
@@ -14,6 +14,8 @@ import (
14 "testing"14 "testing"
15 "time"15 "time"
16 16
17+ "github.com/stretchr/testify/mock"
18+
17 "github.com/bnema/nefercap/internal/ports"19 "github.com/bnema/nefercap/internal/ports"
18 )20 )
19 21
@@ -107,7 +109,7 @@ func TestStartValidation(t *testing.T) {
107 func TestBuildArgs(t *testing.T) {109 func TestBuildArgs(t *testing.T) {
108 f := newFrame(63, 47, 252, 0)110 f := newFrame(63, 47, 252, 0)
109 args := strings.Join(buildArgs(f, 62, 46, 30), " ")111 args := strings.Join(buildArgs(f, 62, 46, 30), " ")
110- for _, want := range []string{"-pix_fmt bgr0", "-video_size 63x47", "-vf scale=62:46:", "-preset fast", "libx264", "-threads 2", "-filter_threads 1", "pipe:3", "-an"} {112+ for _, want := range []string{"-pix_fmt bgr0", "-video_size 63x47", "-vf scale=62:46:", "-preset veryfast", "-tune zerolatency", "libx264", "-threads 2", "-filter_threads 1", "pipe:3", "-an"} {
111 if !strings.Contains(args, want) {113 if !strings.Contains(args, want) {
112 t.Errorf("args missing %q: %s", want, args)114 t.Errorf("args missing %q: %s", want, args)
113 }115 }
@@ -694,3 +696,79 @@ func TestWriteSteadyStateAllocations(t *testing.T) {
694 t.Fatal(err)696 t.Fatal(err)
695 }697 }
696 }698 }
699+
700+func TestWriteRowsShortWrite(t *testing.T) {
701+ packed := newFrame(2, 2, 8, 1)
702+ padded := newFrame(2, 2, 12, 1)
703+ for name, tc := range map[string]struct {
704+ f ports.Frame
705+ calls int
706+ }{"packed": {packed, 1}, "rows": {padded, 1}} {
707+ t.Run(name, func(t *testing.T) {
708+ m := NewMockWriter(t)
709+ m.EXPECT().Write(mock.Anything).Return(3, nil).Times(tc.calls)
710+ if err := writeRows(m, tc.f); !errors.Is(err, io.ErrShortWrite) {
711+ t.Fatalf("got %v, want io.ErrShortWrite", err)
712+ }
713+ })
714+ }
715+ t.Run("error passes through", func(t *testing.T) {
716+ boom := errors.New("boom")
717+ m := NewMockWriter(t)
718+ m.EXPECT().Write(mock.Anything).Return(0, boom).Once()
719+ if err := writeRows(m, padded); !errors.Is(err, boom) {
720+ t.Fatalf("got %v", err)
721+ }
722+ })
723+}
724+
725+func TestCloseReportsStreamFailureEvenIfEncoderExitsCleanly(t *testing.T) {
726+ w := helperWriter(t, "drain")
727+ w.closeTimeout = 10 * time.Second
728+ path := filepath.Join(t.TempDir(), "v.mp4")
729+ f := newFrame(4, 4, 16, 0)
730+ ctx := context.Background()
731+ if err := w.Start(ctx, f, path, settings()); err != nil {
732+ t.Fatal(err)
733+ }
734+ if err := w.Write(ctx, f); err != nil {
735+ t.Fatal(err)
736+ }
737+ // Break the pipe under the writer: the encoder sees EOF and exits cleanly.
738+ _ = w.stdin.Close()
739+ werr := w.Write(ctx, f)
740+ if werr == nil {
741+ t.Fatal("Write to a broken pipe succeeded")
742+ }
743+ err := w.Close()
744+ if err == nil || !errors.Is(err, os.ErrClosed) {
745+ t.Fatalf("Close = %v, want the stream failure", err)
746+ }
747+ if _, serr := os.Stat(path); !errors.Is(serr, os.ErrNotExist) {
748+ t.Fatalf("broken recording remains: %v", serr)
749+ }
750+}
751+
752+func TestCloseKeepsVideoAfterCancelledWrite(t *testing.T) {
753+ w := helperWriter(t, "drain")
754+ w.closeTimeout = 10 * time.Second
755+ path := filepath.Join(t.TempDir(), "v.mp4")
756+ f := newFrame(4, 4, 16, 0)
757+ ctx, cancel := context.WithCancel(context.Background())
758+ if err := w.Start(ctx, f, path, settings()); err != nil {
759+ t.Fatal(err)
760+ }
761+ if err := w.Write(ctx, f); err != nil {
762+ t.Fatal(err)
763+ }
764+ cancel()
765+ if err := w.Write(ctx, f); !errors.Is(err, context.Canceled) {
766+ t.Fatalf("got %v", err)
767+ }
768+ if err := w.Close(); err != nil {
769+ t.Fatalf("cancellation must not fail Close: %v", err)
770+ }
771+ if got, _ := os.ReadFile(path); string(got) != "bytes=64" {
772+ t.Fatalf("encoder output %q", got)
773+ }
774+}
A internal/adapters/ffmpeg/io_mock_test.go +105 -0
@@ -0,0 +1,105 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package ffmpeg
6+
7+import (
8+ mock "github.com/stretchr/testify/mock"
9+)
10+
11+// NewMockWriter creates a new instance of MockWriter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
12+// The first argument is typically a *testing.T value.
13+func NewMockWriter(t interface {
14+ mock.TestingT
15+ Cleanup(func())
16+}) *MockWriter {
17+ if helper, ok := t.(interface{ Helper() }); ok {
18+ helper.Helper()
19+ }
20+
21+ mock := &MockWriter{}
22+ mock.Mock.Test(t)
23+
24+ t.Cleanup(func() {
25+ if helper, ok := t.(interface{ Helper() }); ok {
26+ helper.Helper()
27+ }
28+ mock.AssertExpectations(t)
29+ })
30+
31+ return mock
32+}
33+
34+// MockWriter is an autogenerated mock type for the Writer type
35+type MockWriter struct {
36+ mock.Mock
37+}
38+
39+type MockWriter_Expecter struct {
40+ mock *mock.Mock
41+}
42+
43+func (_m *MockWriter) EXPECT() *MockWriter_Expecter {
44+ return &MockWriter_Expecter{mock: &_m.Mock}
45+}
46+
47+// Write provides a mock function for the type MockWriter
48+func (_mock *MockWriter) Write(p []byte) (int, error) {
49+ ret := _mock.Called(p)
50+
51+ if len(ret) == 0 {
52+ panic("no return value specified for Write")
53+ }
54+
55+ var r0 int
56+ var r1 error
57+ if returnFunc, ok := ret.Get(0).(func([]byte) (int, error)); ok {
58+ return returnFunc(p)
59+ }
60+ if returnFunc, ok := ret.Get(0).(func([]byte) int); ok {
61+ r0 = returnFunc(p)
62+ } else {
63+ r0 = ret.Get(0).(int)
64+ }
65+ if returnFunc, ok := ret.Get(1).(func([]byte) error); ok {
66+ r1 = returnFunc(p)
67+ } else {
68+ r1 = ret.Error(1)
69+ }
70+ return r0, r1
71+}
72+
73+// MockWriter_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write'
74+type MockWriter_Write_Call struct {
75+ *mock.Call
76+}
77+
78+// Write is a helper method to define mock.On call
79+// - p []byte
80+func (_e *MockWriter_Expecter) Write(p any) *MockWriter_Write_Call {
81+ return &MockWriter_Write_Call{Call: _e.mock.On("Write", p)}
82+}
83+
84+func (_c *MockWriter_Write_Call) Run(run func(p []byte)) *MockWriter_Write_Call {
85+ _c.Call.Run(func(args mock.Arguments) {
86+ var arg0 []byte
87+ if args[0] != nil {
88+ arg0 = args[0].([]byte)
89+ }
90+ run(
91+ arg0,
92+ )
93+ })
94+ return _c
95+}
96+
97+func (_c *MockWriter_Write_Call) Return(n int, err error) *MockWriter_Write_Call {
98+ _c.Call.Return(n, err)
99+ return _c
100+}
101+
102+func (_c *MockWriter_Write_Call) RunAndReturn(run func(p []byte) (int, error)) *MockWriter_Write_Call {
103+ _c.Call.Return(run)
104+ return _c
105+}
@@ -0,0 +1,105 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package ffmpeg
6+
7+import (
8+ mock "github.com/stretchr/testify/mock"
9+)
10+
11+// NewMockWriter creates a new instance of MockWriter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
12+// The first argument is typically a *testing.T value.
13+func NewMockWriter(t interface {
14+ mock.TestingT
15+ Cleanup(func())
16+}) *MockWriter {
17+ if helper, ok := t.(interface{ Helper() }); ok {
18+ helper.Helper()
19+ }
20+
21+ mock := &MockWriter{}
22+ mock.Mock.Test(t)
23+
24+ t.Cleanup(func() {
25+ if helper, ok := t.(interface{ Helper() }); ok {
26+ helper.Helper()
27+ }
28+ mock.AssertExpectations(t)
29+ })
30+
31+ return mock
32+}
33+
34+// MockWriter is an autogenerated mock type for the Writer type
35+type MockWriter struct {
36+ mock.Mock
37+}
38+
39+type MockWriter_Expecter struct {
40+ mock *mock.Mock
41+}
42+
43+func (_m *MockWriter) EXPECT() *MockWriter_Expecter {
44+ return &MockWriter_Expecter{mock: &_m.Mock}
45+}
46+
47+// Write provides a mock function for the type MockWriter
48+func (_mock *MockWriter) Write(p []byte) (int, error) {
49+ ret := _mock.Called(p)
50+
51+ if len(ret) == 0 {
52+ panic("no return value specified for Write")
53+ }
54+
55+ var r0 int
56+ var r1 error
57+ if returnFunc, ok := ret.Get(0).(func([]byte) (int, error)); ok {
58+ return returnFunc(p)
59+ }
60+ if returnFunc, ok := ret.Get(0).(func([]byte) int); ok {
61+ r0 = returnFunc(p)
62+ } else {
63+ r0 = ret.Get(0).(int)
64+ }
65+ if returnFunc, ok := ret.Get(1).(func([]byte) error); ok {
66+ r1 = returnFunc(p)
67+ } else {
68+ r1 = ret.Error(1)
69+ }
70+ return r0, r1
71+}
72+
73+// MockWriter_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write'
74+type MockWriter_Write_Call struct {
75+ *mock.Call
76+}
77+
78+// Write is a helper method to define mock.On call
79+// - p []byte
80+func (_e *MockWriter_Expecter) Write(p any) *MockWriter_Write_Call {
81+ return &MockWriter_Write_Call{Call: _e.mock.On("Write", p)}
82+}
83+
84+func (_c *MockWriter_Write_Call) Run(run func(p []byte)) *MockWriter_Write_Call {
85+ _c.Call.Run(func(args mock.Arguments) {
86+ var arg0 []byte
87+ if args[0] != nil {
88+ arg0 = args[0].([]byte)
89+ }
90+ run(
91+ arg0,
92+ )
93+ })
94+ return _c
95+}
96+
97+func (_c *MockWriter_Write_Call) Return(n int, err error) *MockWriter_Write_Call {
98+ _c.Call.Return(n, err)
99+ return _c
100+}
101+
102+func (_c *MockWriter_Write_Call) RunAndReturn(run func(p []byte) (int, error)) *MockWriter_Write_Call {
103+ _c.Call.Return(run)
104+ return _c
105+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 9

↑↓ moveenter openesc close