Files
flatrender/services/node-agent/internal/runner/remotion_test.go
T

164 lines
4.8 KiB
Go
Raw Normal View History

package runner
import (
"context"
"os"
"os/exec"
"path/filepath"
"testing"
"time"
)
// remotionProjectDir resolves the repo's services/remotion directory relative to
// this test package (services/node-agent/internal/runner), or skips the test when
// it (or npx) is unavailable — keeps the test green on CI nodes without the
// Remotion project checked out.
func remotionProjectDir(t *testing.T) string {
t.Helper()
if v := os.Getenv("REMOTION_PROJECT_DIR"); v != "" {
return v
}
dir, err := filepath.Abs(filepath.Join("..", "..", "..", "remotion"))
if err != nil {
t.Fatalf("abs: %v", err)
}
if _, err := os.Stat(filepath.Join(dir, "package.json")); err != nil {
t.Skipf("remotion project not found at %s (skipping)", dir)
}
return dir
}
func TestRemotionProps(t *testing.T) {
job := &Job{Bindings: []Binding{
{Key: "logoText", Value: "HELLO"},
{Key: "accentColor", Value: "#22d3ee"},
{Key: "", Value: "ignored"}, // empty keys are dropped
}}
got, err := remotionProps(job)
if err != nil {
t.Fatalf("remotionProps: %v", err)
}
want := `{"accentColor":"#22d3ee","logoText":"HELLO"}`
if got != want {
t.Fatalf("props = %s, want %s", got, want)
}
}
func TestRemotionProgress(t *testing.T) {
cases := []struct {
phase int32
cur, total, stch, stchTot int64
wantMin, wantMax int
}{
{0, 0, 0, 0, 0, 5, 5}, // bundling
{1, 90, 180, 0, 0, 30, 45}, // half the frames rendered
{2, 90, 180, 90, 180, 80, 90}, // half stitched
}
for _, c := range cases {
pct, _ := remotionProgress(c.phase, c.cur, c.total, c.stch, c.stchTot)
if pct < c.wantMin || pct > c.wantMax {
t.Errorf("phase %d: pct %d not in [%d,%d]", c.phase, pct, c.wantMin, c.wantMax)
}
}
}
// TestRunRemotion_EndToEnd renders a real composition through the engine and
// asserts an MP4 lands on disk. Slow (spawns Chrome) — run with `go test -run
// RunRemotion -timeout 6m`. Skipped automatically without the project or npx.
func TestRunRemotion_EndToEnd(t *testing.T) {
if testing.Short() {
t.Skip("skipping end-to-end render in -short mode")
}
remDir := remotionProjectDir(t)
if _, err := exec.LookPath(npxCmd()); err != nil {
t.Skipf("%s not on PATH (skipping)", npxCmd())
}
out := filepath.Join(t.TempDir(), "engine-out.mp4")
job := &Job{
JobID: "test-remotion-e2e",
Engine: EngineRemotion,
CompName: "KineticQuote",
Quality: "free",
Resolution: "360p", // exercises the height tier mapping
Bindings: []Binding{
{Key: "quote", Value: "Two engines, one output."},
{Key: "author", Value: "Engine Test"},
{Key: "accentColor", Value: "#22d3ee"},
},
}
var lastPct int
onProgress := func(_ context.Context, pct int, _ string) error { lastPct = pct; return nil }
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Minute)
defer cancel()
got, err := RunRemotion(ctx, remDir, job, out, onProgress, nil)
if err != nil {
t.Fatalf("RunRemotion: %v", err)
}
st, err := os.Stat(got)
if err != nil {
t.Fatalf("stat output: %v", err)
}
if st.Size() == 0 {
t.Fatal("output file is empty")
}
if lastPct < 90 {
t.Errorf("final progress only reached %d%%", lastPct)
}
t.Logf("rendered %s (%d bytes), final progress %d%%", got, st.Size(), lastPct)
}
// TestRun_RemotionEngine exercises the real integration point the node-agent uses:
// runner.Run() dispatching on Job.Engine. With Engine=Remotion and an empty AE path
// (which would otherwise trigger the AE mock), it must route to the Remotion engine
// and produce a real MP4.
func TestRun_RemotionEngine(t *testing.T) {
if testing.Short() {
t.Skip("skipping end-to-end render in -short mode")
}
remDir := remotionProjectDir(t)
if _, err := exec.LookPath(npxCmd()); err != nil {
t.Skipf("%s not on PATH (skipping)", npxCmd())
}
job := &Job{
JobID: "test-run-dispatch",
Engine: EngineRemotion,
RemotionDir: remDir,
CompName: "KineticQuote",
Quality: "free",
Resolution: "360p",
Bindings: []Binding{{Key: "author", Value: "Dispatch Test"}},
}
noop := func(context.Context, int, string) error { return nil }
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Minute)
defer cancel()
// aePath empty: an AE job would mock here; a Remotion job must still render for real.
got, err := Run(ctx, "", t.TempDir(), job, noop, nil)
if err != nil {
t.Fatalf("Run (remotion engine): %v", err)
}
st, err := os.Stat(got)
if err != nil || st.Size() == 0 {
t.Fatalf("no output from Run: %v", err)
}
if string(mustRead(t, got)[:4]) == "mock" {
t.Fatal("Run produced the AE mock output instead of a real Remotion render")
}
t.Logf("Run dispatched to Remotion → %s (%d bytes)", got, st.Size())
}
func mustRead(t *testing.T, path string) []byte {
t.Helper()
b, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
return b
}