Add tests for main package, manager, and various components
- Introduced unit tests for the main package to ensure compilation. - Added tests for the manager, including validation of upload sessions and handling of Blender binary paths. - Implemented tests for job token generation and validation, ensuring security and integrity. - Created tests for configuration management and database schema to verify functionality. - Added tests for logger and runner components to enhance overall test coverage and reliability.
This commit is contained in:
44
internal/runner/api/jobconn_test.go
Normal file
44
internal/runner/api/jobconn_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func TestJobConnection_ConnectAndClose(t *testing.T) {
|
||||
upgrader := websocket.Upgrader{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
var msg map[string]interface{}
|
||||
if err := conn.ReadJSON(&msg); err != nil {
|
||||
return
|
||||
}
|
||||
if msg["type"] == "auth" {
|
||||
_ = conn.WriteJSON(map[string]string{"type": "auth_ok"})
|
||||
}
|
||||
// Keep open briefly so client can mark connected.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
jc := NewJobConnection()
|
||||
managerURL := strings.Replace(server.URL, "http://", "http://", 1)
|
||||
if err := jc.Connect(managerURL, "/job/1", "token123"); err != nil {
|
||||
t.Fatalf("Connect failed: %v", err)
|
||||
}
|
||||
if !jc.IsConnected() {
|
||||
t.Fatal("expected connection to be marked connected")
|
||||
}
|
||||
jc.Close()
|
||||
}
|
||||
|
||||
45
internal/runner/api/manager_test.go
Normal file
45
internal/runner/api/manager_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewManagerClient_TrimsTrailingSlash(t *testing.T) {
|
||||
c := NewManagerClient("http://example.com/")
|
||||
if c.GetBaseURL() != "http://example.com" {
|
||||
t.Fatalf("unexpected base url: %q", c.GetBaseURL())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoRequest_SetsAuthorizationHeader(t *testing.T) {
|
||||
var authHeader string
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader = r.Header.Get("Authorization")
|
||||
_ = json.NewEncoder(w).Encode(map[string]bool{"ok": true})
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
c := NewManagerClient(ts.URL)
|
||||
c.SetCredentials(1, "abc123")
|
||||
|
||||
resp, err := c.Request(http.MethodGet, "/x", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if authHeader != "Bearer abc123" {
|
||||
t.Fatalf("unexpected Authorization header: %q", authHeader)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequest_RequiresAuth(t *testing.T) {
|
||||
c := NewManagerClient("http://example.com")
|
||||
if _, err := c.Request(http.MethodGet, "/x", nil); err == nil {
|
||||
t.Fatal("expected auth error when api key is missing")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -44,8 +45,12 @@ func (m *Manager) GetBinaryPath(version string) (string, error) {
|
||||
if binaryInfo, err := os.Stat(binaryPath); err == nil {
|
||||
// Verify it's actually a file (not a directory)
|
||||
if !binaryInfo.IsDir() {
|
||||
log.Printf("Found existing Blender %s installation at %s", version, binaryPath)
|
||||
return binaryPath, nil
|
||||
absBinaryPath, err := ResolveBinaryPath(binaryPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
log.Printf("Found existing Blender %s installation at %s", version, absBinaryPath)
|
||||
return absBinaryPath, nil
|
||||
}
|
||||
}
|
||||
// Version folder exists but binary is missing - might be incomplete installation
|
||||
@@ -72,20 +77,50 @@ func (m *Manager) GetBinaryPath(version string) (string, error) {
|
||||
return "", fmt.Errorf("blender binary not found after extraction")
|
||||
}
|
||||
|
||||
log.Printf("Blender %s installed at %s", version, binaryPath)
|
||||
return binaryPath, nil
|
||||
absBinaryPath, err := ResolveBinaryPath(binaryPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Printf("Blender %s installed at %s", version, absBinaryPath)
|
||||
return absBinaryPath, nil
|
||||
}
|
||||
|
||||
// GetBinaryForJob returns the Blender binary path for a job.
|
||||
// Uses the version from metadata or falls back to system blender.
|
||||
func (m *Manager) GetBinaryForJob(version string) (string, error) {
|
||||
if version == "" {
|
||||
return "blender", nil // System blender
|
||||
return ResolveBinaryPath("blender")
|
||||
}
|
||||
|
||||
return m.GetBinaryPath(version)
|
||||
}
|
||||
|
||||
// ResolveBinaryPath resolves a Blender executable to an absolute path.
|
||||
func ResolveBinaryPath(blenderBinary string) (string, error) {
|
||||
if blenderBinary == "" {
|
||||
return "", fmt.Errorf("blender binary path is empty")
|
||||
}
|
||||
|
||||
if strings.Contains(blenderBinary, string(filepath.Separator)) {
|
||||
absPath, err := filepath.Abs(blenderBinary)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to resolve blender binary path %q: %w", blenderBinary, err)
|
||||
}
|
||||
return absPath, nil
|
||||
}
|
||||
|
||||
resolvedPath, err := exec.LookPath(blenderBinary)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to locate blender binary %q in PATH: %w", blenderBinary, err)
|
||||
}
|
||||
absPath, err := filepath.Abs(resolvedPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to resolve blender binary path %q: %w", resolvedPath, err)
|
||||
}
|
||||
return absPath, nil
|
||||
}
|
||||
|
||||
// TarballEnv returns a copy of baseEnv with LD_LIBRARY_PATH set so that a
|
||||
// tarball Blender installation can find its bundled libs (e.g. lib/python3.x).
|
||||
// If blenderBinary is the system "blender" or has no path component, baseEnv is
|
||||
|
||||
34
internal/runner/blender/binary_test.go
Normal file
34
internal/runner/blender/binary_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package blender
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResolveBinaryPath_AbsoluteLikePath(t *testing.T) {
|
||||
got, err := ResolveBinaryPath("./blender")
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveBinaryPath failed: %v", err)
|
||||
}
|
||||
if !filepath.IsAbs(got) {
|
||||
t.Fatalf("expected absolute path, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveBinaryPath_Empty(t *testing.T) {
|
||||
if _, err := ResolveBinaryPath(""); err == nil {
|
||||
t.Fatal("expected error for empty blender binary")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTarballEnv_SetsAndExtendsLDLibraryPath(t *testing.T) {
|
||||
bin := filepath.Join(string(os.PathSeparator), "tmp", "blender", "blender")
|
||||
got := TarballEnv(bin, []string{"A=B", "LD_LIBRARY_PATH=/old"})
|
||||
joined := strings.Join(got, "\n")
|
||||
if !strings.Contains(joined, "LD_LIBRARY_PATH=/tmp/blender/lib:/old") {
|
||||
t.Fatalf("expected LD_LIBRARY_PATH to include blender lib, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
32
internal/runner/blender/detect_test.go
Normal file
32
internal/runner/blender/detect_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package blender
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIsDRMCardNode(t *testing.T) {
|
||||
tests := map[string]bool{
|
||||
"card0": true,
|
||||
"card12": true,
|
||||
"card": false,
|
||||
"card0-DP-1": false,
|
||||
"renderD128": false,
|
||||
"foo": false,
|
||||
}
|
||||
for in, want := range tests {
|
||||
if got := isDRMCardNode(in); got != want {
|
||||
t.Fatalf("isDRMCardNode(%q) = %v, want %v", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsGPUControllerLine(t *testing.T) {
|
||||
if !isGPUControllerLine("vga compatible controller: nvidia corp") {
|
||||
t.Fatal("expected VGA controller line to match")
|
||||
}
|
||||
if !isGPUControllerLine("3d controller: amd") {
|
||||
t.Fatal("expected 3d controller line to match")
|
||||
}
|
||||
if isGPUControllerLine("audio device: something") {
|
||||
t.Fatal("audio line should not match")
|
||||
}
|
||||
}
|
||||
|
||||
34
internal/runner/blender/logfilter_test.go
Normal file
34
internal/runner/blender/logfilter_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package blender
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"jiggablend/pkg/types"
|
||||
)
|
||||
|
||||
func TestFilterLog_FiltersNoise(t *testing.T) {
|
||||
cases := []string{
|
||||
"",
|
||||
"--------------------------------------------------------------------",
|
||||
"Failed to add relation foo",
|
||||
"BKE_modifier_set_error",
|
||||
"Depth Type Name",
|
||||
}
|
||||
for _, in := range cases {
|
||||
filtered, level := FilterLog(in)
|
||||
if !filtered {
|
||||
t.Fatalf("expected filtered for %q", in)
|
||||
}
|
||||
if level != types.LogLevelInfo {
|
||||
t.Fatalf("unexpected level for %q: %s", in, level)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterLog_KeepsNormalLine(t *testing.T) {
|
||||
filtered, _ := FilterLog("Rendering done.")
|
||||
if filtered {
|
||||
t.Fatal("normal line should not be filtered")
|
||||
}
|
||||
}
|
||||
|
||||
10
internal/runner/blender/version_test.go
Normal file
10
internal/runner/blender/version_test.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package blender
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestVersionString(t *testing.T) {
|
||||
if got := VersionString(4, 2); got != "4.2" {
|
||||
t.Fatalf("VersionString() = %q, want %q", got, "4.2")
|
||||
}
|
||||
}
|
||||
|
||||
40
internal/runner/runner_test.go
Normal file
40
internal/runner/runner_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewRunner_InitializesFields(t *testing.T) {
|
||||
r := New("http://localhost:8080", "runner-a", "host-a", false)
|
||||
if r == nil {
|
||||
t.Fatal("New should return a runner")
|
||||
}
|
||||
if r.name != "runner-a" || r.hostname != "host-a" {
|
||||
t.Fatalf("unexpected runner identity: %q %q", r.name, r.hostname)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunner_GPUFlagsSetters(t *testing.T) {
|
||||
r := New("http://localhost:8080", "runner-a", "host-a", false)
|
||||
r.SetGPULockedOut(true)
|
||||
if !r.IsGPULockedOut() {
|
||||
t.Fatal("expected GPU lockout to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateFingerprint_PopulatesValue(t *testing.T) {
|
||||
r := New("http://localhost:8080", "runner-a", "host-a", false)
|
||||
r.generateFingerprint()
|
||||
fp := r.GetFingerprint()
|
||||
if fp == "" {
|
||||
t.Fatal("fingerprint should not be empty")
|
||||
}
|
||||
if len(fp) != 64 {
|
||||
t.Fatalf("fingerprint should be sha256 hex, got %q", fp)
|
||||
}
|
||||
if _, err := hex.DecodeString(fp); err != nil {
|
||||
t.Fatalf("fingerprint should be valid hex: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,7 +385,7 @@ func (p *EncodeProcessor) Process(ctx *Context) error {
|
||||
func detectAlphaChannel(ctx *Context, filePath string) bool {
|
||||
// Use ffprobe to check pixel format and stream properties
|
||||
// EXR files with alpha will have formats like gbrapf32le (RGBA) vs gbrpf32le (RGB)
|
||||
cmd := exec.Command("ffprobe",
|
||||
cmd := execCommand("ffprobe",
|
||||
"-v", "error",
|
||||
"-select_streams", "v:0",
|
||||
"-show_entries", "stream=pix_fmt:stream=codec_name",
|
||||
@@ -418,7 +418,7 @@ func detectAlphaChannel(ctx *Context, filePath string) bool {
|
||||
// detectHDR checks if an EXR file contains HDR content using ffprobe
|
||||
func detectHDR(ctx *Context, filePath string) bool {
|
||||
// First, check if the pixel format supports HDR (32-bit float)
|
||||
cmd := exec.Command("ffprobe",
|
||||
cmd := execCommand("ffprobe",
|
||||
"-v", "error",
|
||||
"-select_streams", "v:0",
|
||||
"-show_entries", "stream=pix_fmt",
|
||||
@@ -446,7 +446,7 @@ func detectHDR(ctx *Context, filePath string) bool {
|
||||
// For 32-bit float EXR, sample pixels to check if values exceed SDR range (> 1.0)
|
||||
// Use ffmpeg to extract pixel statistics - check max pixel values
|
||||
// This is more efficient than sampling individual pixels
|
||||
cmd = exec.Command("ffmpeg",
|
||||
cmd = execCommand("ffmpeg",
|
||||
"-v", "error",
|
||||
"-i", filePath,
|
||||
"-vf", "signalstats",
|
||||
@@ -489,7 +489,7 @@ func detectHDRBySampling(ctx *Context, filePath string) bool {
|
||||
}
|
||||
|
||||
for _, region := range sampleRegions {
|
||||
cmd := exec.Command("ffmpeg",
|
||||
cmd := execCommand("ffmpeg",
|
||||
"-v", "error",
|
||||
"-i", filePath,
|
||||
"-vf", fmt.Sprintf("%s,scale=1:1", region),
|
||||
|
||||
120
internal/runner/tasks/encode_test.go
Normal file
120
internal/runner/tasks/encode_test.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFloat32FromBytes(t *testing.T) {
|
||||
got := float32FromBytes([]byte{0x00, 0x00, 0x80, 0x3f}) // 1.0 little-endian
|
||||
if got != 1.0 {
|
||||
t.Fatalf("float32FromBytes() = %v, want 1.0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMax(t *testing.T) {
|
||||
if got := max(1, 2); got != 2 {
|
||||
t.Fatalf("max() = %v, want 2", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractFrameNumber(t *testing.T) {
|
||||
if got := extractFrameNumber("render_0042.png"); got != 42 {
|
||||
t.Fatalf("extractFrameNumber() = %d, want 42", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckFFmpegSizeError(t *testing.T) {
|
||||
err := checkFFmpegSizeError("hardware does not support encoding at size ... constraints: width 128-4096 height 128-4096")
|
||||
if err == nil {
|
||||
t.Fatal("expected a size error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectAlphaChannel_UsesExecSeam(t *testing.T) {
|
||||
orig := execCommand
|
||||
execCommand = fakeExecCommand
|
||||
defer func() { execCommand = orig }()
|
||||
|
||||
if !detectAlphaChannel(&Context{}, "/tmp/frame.exr") {
|
||||
t.Fatal("expected alpha channel detection via mocked ffprobe output")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectHDR_UsesExecSeam(t *testing.T) {
|
||||
orig := execCommand
|
||||
execCommand = fakeExecCommand
|
||||
defer func() { execCommand = orig }()
|
||||
|
||||
if !detectHDR(&Context{}, "/tmp/frame.exr") {
|
||||
t.Fatal("expected HDR detection via mocked ffmpeg sampling output")
|
||||
}
|
||||
}
|
||||
|
||||
func fakeExecCommand(command string, args ...string) *exec.Cmd {
|
||||
cs := []string{"-test.run=TestExecHelperProcess", "--", command}
|
||||
cs = append(cs, args...)
|
||||
cmd := exec.Command(os.Args[0], cs...)
|
||||
cmd.Env = append(os.Environ(), "GO_WANT_HELPER_PROCESS=1")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func TestExecHelperProcess(t *testing.T) {
|
||||
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
|
||||
return
|
||||
}
|
||||
|
||||
idx := 0
|
||||
for i, a := range os.Args {
|
||||
if a == "--" {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx == 0 || idx+1 >= len(os.Args) {
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
cmdName := os.Args[idx+1]
|
||||
cmdArgs := os.Args[idx+2:]
|
||||
|
||||
switch cmdName {
|
||||
case "ffprobe":
|
||||
if containsArg(cmdArgs, "stream=pix_fmt:stream=codec_name") {
|
||||
_, _ = os.Stdout.WriteString("pix_fmt=gbrapf32le\ncodec_name=exr\n")
|
||||
os.Exit(0)
|
||||
}
|
||||
_, _ = os.Stdout.WriteString("gbrpf32le\n")
|
||||
os.Exit(0)
|
||||
case "ffmpeg":
|
||||
if containsArg(cmdArgs, "signalstats") {
|
||||
_, _ = os.Stderr.WriteString("signalstats failed")
|
||||
os.Exit(1)
|
||||
}
|
||||
if containsArg(cmdArgs, "rawvideo") {
|
||||
buf := make([]byte, 12)
|
||||
binary.LittleEndian.PutUint32(buf[0:4], math.Float32bits(1.5))
|
||||
binary.LittleEndian.PutUint32(buf[4:8], math.Float32bits(0.2))
|
||||
binary.LittleEndian.PutUint32(buf[8:12], math.Float32bits(0.1))
|
||||
_, _ = os.Stdout.Write(buf)
|
||||
os.Exit(0)
|
||||
}
|
||||
os.Exit(0)
|
||||
default:
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
func containsArg(args []string, target string) bool {
|
||||
for _, a := range args {
|
||||
if strings.Contains(a, target) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
7
internal/runner/tasks/exec_seams.go
Normal file
7
internal/runner/tasks/exec_seams.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package tasks
|
||||
|
||||
import "os/exec"
|
||||
|
||||
// execCommand is a seam for process execution in tests.
|
||||
var execCommand = exec.Command
|
||||
|
||||
42
internal/runner/tasks/processor_test.go
Normal file
42
internal/runner/tasks/processor_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"jiggablend/pkg/types"
|
||||
)
|
||||
|
||||
func TestNewContext_NormalizesFrameEnd(t *testing.T) {
|
||||
ctx := NewContext(1, 2, "job", 10, 1, "render", "/tmp", "tok", nil, nil, nil, nil, nil, nil, nil, false, false, false, false, false, false, nil)
|
||||
if ctx.FrameEnd != 10 {
|
||||
t.Fatalf("expected FrameEnd to be normalized to Frame, got %d", ctx.FrameEnd)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext_GetOutputFormat_Default(t *testing.T) {
|
||||
ctx := &Context{}
|
||||
if got := ctx.GetOutputFormat(); got != "PNG" {
|
||||
t.Fatalf("GetOutputFormat() = %q, want PNG", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext_ShouldForceCPU(t *testing.T) {
|
||||
ctx := &Context{ForceCPURendering: true}
|
||||
if !ctx.ShouldForceCPU() {
|
||||
t.Fatal("expected force cpu when runner-level flag is set")
|
||||
}
|
||||
|
||||
force := true
|
||||
ctx = &Context{Metadata: &types.BlendMetadata{RenderSettings: types.RenderSettings{EngineSettings: map[string]interface{}{"force_cpu": force}}}}
|
||||
if !ctx.ShouldForceCPU() {
|
||||
t.Fatal("expected force cpu when metadata requests it")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrJobCancelled_IsSentinel(t *testing.T) {
|
||||
if !errors.Is(ErrJobCancelled, ErrJobCancelled) {
|
||||
t.Fatal("sentinel error should be self-identical")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,11 @@ func (p *RenderProcessor) Process(ctx *Context) error {
|
||||
ctx.Info("No Blender version specified, using system blender")
|
||||
}
|
||||
|
||||
blenderBinary, err = blender.ResolveBinaryPath(blenderBinary)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve blender binary: %w", err)
|
||||
}
|
||||
|
||||
// Create output directory
|
||||
outputDir := filepath.Join(ctx.WorkDir, "output")
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
@@ -202,8 +207,16 @@ func (p *RenderProcessor) createRenderScript(ctx *Context, renderFormat string)
|
||||
|
||||
func (p *RenderProcessor) runBlender(ctx *Context, blenderBinary, blendFile, outputDir, renderFormat, blenderHome string) error {
|
||||
scriptPath := filepath.Join(ctx.WorkDir, "enable_gpu.py")
|
||||
blendFileAbs, err := filepath.Abs(blendFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve blend file path: %w", err)
|
||||
}
|
||||
scriptPathAbs, err := filepath.Abs(scriptPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve blender script path: %w", err)
|
||||
}
|
||||
|
||||
args := []string{"-b", blendFile, "--python", scriptPath}
|
||||
args := []string{"-b", blendFileAbs, "--python", scriptPathAbs}
|
||||
if ctx.ShouldEnableExecution() {
|
||||
args = append(args, "--enable-autoexec")
|
||||
}
|
||||
@@ -220,7 +233,7 @@ func (p *RenderProcessor) runBlender(ctx *Context, blenderBinary, blendFile, out
|
||||
args = append(args, "-f", fmt.Sprintf("%d", ctx.Frame))
|
||||
}
|
||||
|
||||
cmd := exec.Command(blenderBinary, args...)
|
||||
cmd := execCommand(blenderBinary, args...)
|
||||
cmd.Dir = ctx.WorkDir
|
||||
|
||||
// Set up environment: LD_LIBRARY_PATH for tarball Blender, then custom HOME
|
||||
|
||||
28
internal/runner/tasks/render_test.go
Normal file
28
internal/runner/tasks/render_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package tasks
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCheckGPUErrorLine_TriggersCallback(t *testing.T) {
|
||||
p := NewRenderProcessor()
|
||||
triggered := false
|
||||
ctx := &Context{
|
||||
OnGPUError: func() { triggered = true },
|
||||
}
|
||||
p.checkGPUErrorLine(ctx, "Fatal: Illegal address in HIP kernel execution")
|
||||
if !triggered {
|
||||
t.Fatal("expected GPU error callback to be triggered")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckGPUErrorLine_IgnoresNormalLine(t *testing.T) {
|
||||
p := NewRenderProcessor()
|
||||
triggered := false
|
||||
ctx := &Context{
|
||||
OnGPUError: func() { triggered = true },
|
||||
}
|
||||
p.checkGPUErrorLine(ctx, "Render completed successfully")
|
||||
if triggered {
|
||||
t.Fatal("did not expect GPU callback for normal line")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,3 +99,27 @@ func TestExtractTar_PathTraversal(t *testing.T) {
|
||||
t.Fatal("expected error for path traversal, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractTarFile(t *testing.T) {
|
||||
destDir := t.TempDir()
|
||||
tarPath := filepath.Join(t.TempDir(), "archive.tar")
|
||||
|
||||
buf := createTarBuffer(map[string]string{
|
||||
"hello.txt": "world",
|
||||
})
|
||||
if err := os.WriteFile(tarPath, buf.Bytes(), 0644); err != nil {
|
||||
t.Fatalf("write tar file: %v", err)
|
||||
}
|
||||
|
||||
if err := ExtractTarFile(tarPath, destDir); err != nil {
|
||||
t.Fatalf("ExtractTarFile: %v", err)
|
||||
}
|
||||
|
||||
got, err := os.ReadFile(filepath.Join(destDir, "hello.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("read extracted file: %v", err)
|
||||
}
|
||||
if string(got) != "world" {
|
||||
t.Fatalf("unexpected file content: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
40
internal/runner/workspace/workspace_test.go
Normal file
40
internal/runner/workspace/workspace_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package workspace
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSanitizeName_ReplacesUnsafeChars(t *testing.T) {
|
||||
got := sanitizeName("runner / with\\bad:chars")
|
||||
if strings.ContainsAny(got, " /\\:") {
|
||||
t.Fatalf("sanitizeName did not sanitize input: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindBlendFiles_IgnoresBlendSaveFiles(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "scene.blend"), []byte("x"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "scene.blend1"), []byte("x"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
files, err := FindBlendFiles(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("FindBlendFiles failed: %v", err)
|
||||
}
|
||||
if len(files) != 1 || files[0] != "scene.blend" {
|
||||
t.Fatalf("unexpected files: %#v", files)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindFirstBlendFile_ReturnsErrorWhenMissing(t *testing.T) {
|
||||
_, err := FindFirstBlendFile(t.TempDir())
|
||||
if err == nil {
|
||||
t.Fatal("expected error when no blend file exists")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user