168 lines
5.7 KiB
Go
168 lines
5.7 KiB
Go
package main
|
|
|
|
// main_test.go - end-to-end smoke test for the depth-anything-cpp gRPC backend.
|
|
//
|
|
// Spawns the compiled depth-anything-cpp binary on a free local port, dials it
|
|
// via gRPC, and exercises LoadModel + Predict against the test fixtures
|
|
// downloaded by test.sh: the small (vits) f32 GGUF of Depth Anything 3 and a
|
|
// real photo. Asserts that Predict returns a JSON payload with a positive
|
|
// depth-map width/height.
|
|
//
|
|
// The spec Skip()s cleanly if its fixtures (the model, the test image, the
|
|
// built binary, or the fallback .so) are missing, so the test target stays
|
|
// usable on a fresh checkout / on CI runners where the model hasn't been
|
|
// downloaded.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
)
|
|
|
|
func TestDepth(t *testing.T) {
|
|
RegisterFailHandler(Fail)
|
|
RunSpecs(t, "depth-anything-cpp backend smoke suite")
|
|
}
|
|
|
|
// freePort grabs an ephemeral TCP port and immediately releases it so the
|
|
// spawned backend can bind to it. There is a tiny TOCTOU window here but in
|
|
// practice it's adequate for a smoke test on a quiet runner.
|
|
func freePort() int {
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
Expect(err).ToNot(HaveOccurred(), "freePort listen")
|
|
port := l.Addr().(*net.TCPAddr).Port
|
|
Expect(l.Close()).To(Succeed())
|
|
return port
|
|
}
|
|
|
|
// startBackend spawns the depth-anything-cpp binary on the given port and waits
|
|
// until it accepts TCP connections (up to 10s). It mirrors how main.go resolves
|
|
// the purego library: the DEPTHANYTHING_LIBRARY env var points the dlopen at the
|
|
// freshly built fallback .so. The returned cleanup func kills the process.
|
|
func startBackend(port int) func() {
|
|
binary, err := filepath.Abs("./depth-anything-cpp")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
if _, err := os.Stat(binary); err != nil {
|
|
Skip(fmt.Sprintf("backend binary not built: %s (run `make depth-anything-cpp` first)", binary))
|
|
}
|
|
|
|
libPath, err := filepath.Abs("./libdepthanythingcpp-fallback.so")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
if _, err := os.Stat(libPath); err != nil {
|
|
Skip(fmt.Sprintf("fallback library not built: %s (run `make libdepthanythingcpp-fallback.so` first)", libPath))
|
|
}
|
|
|
|
addr := fmt.Sprintf("127.0.0.1:%d", port)
|
|
cmd := exec.Command(binary, "--addr", addr)
|
|
cmd.Env = append(os.Environ(), "DEPTHANYTHING_LIBRARY="+libPath)
|
|
cmd.Stdout = os.Stderr
|
|
cmd.Stderr = os.Stderr
|
|
Expect(cmd.Start()).To(Succeed())
|
|
|
|
cleanup := func() {
|
|
if cmd.Process != nil {
|
|
_ = cmd.Process.Kill()
|
|
_, _ = cmd.Process.Wait()
|
|
}
|
|
}
|
|
|
|
deadline := time.Now().Add(10 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
c, err := net.DialTimeout("tcp", addr, 200*time.Millisecond)
|
|
if err == nil {
|
|
_ = c.Close()
|
|
return cleanup
|
|
}
|
|
time.Sleep(200 * time.Millisecond)
|
|
}
|
|
|
|
cleanup()
|
|
Fail(fmt.Sprintf("backend did not become ready on %s within 10s", addr))
|
|
return func() {}
|
|
}
|
|
|
|
// loadTestImage reads the test image downloaded by test.sh and returns its
|
|
// base64-encoded content (one of the wire formats accepted by Predict).
|
|
func loadTestImage() string {
|
|
imgPath, err := filepath.Abs("test-data/test.jpg")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
imgBytes, err := os.ReadFile(imgPath)
|
|
if err != nil {
|
|
Skip(fmt.Sprintf("test image not present: %s (run test.sh first)", imgPath))
|
|
}
|
|
return base64.StdEncoding.EncodeToString(imgBytes)
|
|
}
|
|
|
|
// dialBackend opens a gRPC client connection to the spawned backend.
|
|
func dialBackend(port int) (pb.BackendClient, func()) {
|
|
addr := fmt.Sprintf("127.0.0.1:%d", port)
|
|
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
|
Expect(err).ToNot(HaveOccurred())
|
|
return pb.NewBackendClient(conn), func() { _ = conn.Close() }
|
|
}
|
|
|
|
// modelPathOrSkip resolves the model file under ./test-models/ and Skip()s the
|
|
// current spec if it's missing (not present on a fresh checkout / on CI runners
|
|
// without the download).
|
|
func modelPathOrSkip(name string) string {
|
|
modelDir, err := filepath.Abs("test-models")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
modelPath := filepath.Join(modelDir, name)
|
|
if _, err := os.Stat(modelPath); err != nil {
|
|
Skip(fmt.Sprintf("model not present: %s (run test.sh first)", modelPath))
|
|
}
|
|
return modelPath
|
|
}
|
|
|
|
var _ = Describe("depth-anything-cpp backend", func() {
|
|
It("runs depth+pose against a known-good image", func() {
|
|
modelPath := modelPathOrSkip("depth-anything-small-f32.gguf")
|
|
imgB64 := loadTestImage()
|
|
|
|
port := freePort()
|
|
cleanup := startBackend(port)
|
|
defer cleanup()
|
|
|
|
client, closeConn := dialBackend(port)
|
|
defer closeConn()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Minute)
|
|
defer cancel()
|
|
|
|
loadResp, err := client.LoadModel(ctx, &pb.ModelOptions{
|
|
Model: "depth-anything-small-f32.gguf",
|
|
ModelFile: modelPath,
|
|
Threads: 4,
|
|
})
|
|
Expect(err).ToNot(HaveOccurred(), "LoadModel")
|
|
Expect(loadResp.GetSuccess()).To(BeTrue(), "LoadModel reported failure: %s", loadResp.GetMessage())
|
|
|
|
// Predict runs depth+pose and returns the JSON depthResult in Reply.Message.
|
|
reply, err := client.Predict(ctx, &pb.PredictOptions{
|
|
Images: []string{imgB64},
|
|
})
|
|
Expect(err).ToNot(HaveOccurred(), "Predict")
|
|
|
|
var res depthResult
|
|
Expect(json.Unmarshal(reply.GetMessage(), &res)).To(Succeed(), "Predict returned non-JSON: %q", string(reply.GetMessage()))
|
|
Expect(res.DepthW).To(BeNumerically(">", 0), "depth width should be positive")
|
|
Expect(res.DepthH).To(BeNumerically(">", 0), "depth height should be positive")
|
|
|
|
_, _ = fmt.Fprintf(GinkgoWriter, "depth OK: %dx%d min=%.3f max=%.3f\n",
|
|
res.DepthW, res.DepthH, res.DepthMin, res.DepthMax)
|
|
})
|
|
})
|