package main import ( "bytes" "context" "encoding/json" "errors" "fmt" "net/http" "net/http/httptest" "os" "os/exec" "path/filepath" "strings" "testing" "time" cdplog "github.com/chromedp/cdproto/log" "github.com/chromedp/cdproto/runtime" "github.com/chromedp/chromedp" mb "github.com/localai/motion-bricks.cpp/bindings/go" ) func TestParseDevice(t *testing.T) { for name, expected := range map[string]mb.Device{"auto": mb.DeviceAuto, "cpu": mb.DeviceCPU, "vulkan": mb.DeviceVulkan, "CPU": mb.DeviceCPU} { actual, err := parseDevice(name) if err != nil || actual != expected { t.Fatalf("parseDevice(%q)=(%d,%v), want %d", name, actual, err, expected) } } if _, err := parseDevice("cuda"); err == nil { t.Fatal("unknown device was accepted") } } func TestNativeHTTPFlow(t *testing.T) { libraryPath, modelPath, stylesPath := os.Getenv("MOTIONBRICKS_LIB"), os.Getenv("MOTIONBRICKS_MODEL"), os.Getenv("MOTIONBRICKS_STYLES") if libraryPath == "" || modelPath == "" || stylesPath == "" { t.Skip("native demo paths are not configured") } demo, err := loadDemoServer(libraryPath, modelPath, stylesPath, mb.DeviceCPU) if err != nil { t.Fatal(err) } defer demo.Close() server := httptest.NewServer(demo.routes()) defer server.Close() response, err := http.Get(server.URL + "/api/meta") if err != nil { t.Fatal(err) } var metadata struct { FPS int `json:"fps"` Joints []mb.Joint `json:"joints"` Styles []styleInfo `json:"styles"` } if err = json.NewDecoder(response.Body).Decode(&metadata); err != nil { t.Fatal(err) } response.Body.Close() if response.StatusCode != http.StatusOK || metadata.FPS != 30 || len(metadata.Joints) != 34 || len(metadata.Styles) < 10 { t.Fatalf("invalid metadata: status=%d fps=%d joints=%d styles=%d", response.StatusCode, metadata.FPS, len(metadata.Joints), len(metadata.Styles)) } post := func(path string, value any, output any) { t.Helper() body, _ := json.Marshal(value) reply, postErr := http.Post(server.URL+path, "application/json", bytes.NewReader(body)) if postErr != nil { t.Fatal(postErr) } defer reply.Body.Close() if reply.StatusCode != http.StatusOK { var failure any _ = json.NewDecoder(reply.Body).Decode(&failure) t.Fatalf("%s: status=%d body=%v", path, reply.StatusCode, failure) } if err := json.NewDecoder(reply.Body).Decode(output); err != nil { t.Fatal(err) } } var initial planResponse post("/api/session", sessionRequest{Style: "walk"}, &initial) if initial.Session == "" || initial.Motion == nil || initial.Motion.Joints != 34 || initial.Motion.Frames < 24 || initial.Targets == nil || initial.Targets.Frames != 4 || initial.Targets.Joints != 34 || len(initial.Targets.Roots) != 12 || len(initial.Targets.Rotations) != 4*34*4 { t.Fatalf("invalid initial response: %+v", initial) } turnedStyle := "" for _, style := range metadata.Styles { if style.Name == "walk_zombie" { turnedStyle = style.Name break } if style.Name != "walk" && turnedStyle == "" { turnedStyle = style.Name } } if turnedStyle == "" { t.Fatal("no alternate upstream style is available") } var turned planResponse post("/api/plan", planRequest{Session: initial.Session, Style: turnedStyle, Move: [2]float32{1, 0}, Facing: [2]float32{1, 0}, Advance: 3, Seed: 77}, &turned) if turned.Style != turnedStyle || turned.Motion == nil || turned.Motion.Joints != 34 || len(turned.Motion.Rotations) != int(turned.Motion.Frames*34*4) || turned.Targets == nil || turned.Targets.Frames != 4 || len(turned.Targets.Rotations) != 4*34*4 { t.Fatalf("invalid turned response: style=%q motion=%+v", turned.Style, turned.Motion) } } func TestHeadlessChrome(t *testing.T) { libraryPath, modelPath, stylesPath := os.Getenv("MOTIONBRICKS_LIB"), os.Getenv("MOTIONBRICKS_MODEL"), os.Getenv("MOTIONBRICKS_STYLES") if libraryPath == "" || modelPath == "" || stylesPath == "" { t.Skip("native demo paths are not configured") } chrome := os.Getenv("MOTIONBRICKS_CHROME") if chrome == "" { var err error chrome, err = exec.LookPath("chromium") if err != nil { t.Skip("chromium is not installed") } } demo, err := loadDemoServer(libraryPath, modelPath, stylesPath, mb.DeviceCPU) if err != nil { t.Fatal(err) } defer demo.Close() server := httptest.NewServer(demo.routes()) defer server.Close() options := append([]chromedp.ExecAllocatorOption{}, chromedp.DefaultExecAllocatorOptions[:]...) options = append(options, chromedp.ExecPath(chrome), chromedp.Flag("no-sandbox", true), chromedp.Flag("disable-dev-shm-usage", true), chromedp.Flag("use-angle", "swiftshader"), chromedp.Flag("enable-unsafe-swiftshader", true), chromedp.WindowSize(1280, 800), ) allocator, cancelAllocator := chromedp.NewExecAllocator(context.Background(), options...) defer cancelAllocator() browser, cancelBrowser := chromedp.NewContext(allocator) defer cancelBrowser() chromedp.ListenTarget(browser, func(event any) { switch event := event.(type) { case *cdplog.EventEntryAdded: t.Logf("browser log: %s", event.Entry.Text) case *runtime.EventConsoleAPICalled: t.Logf("browser console: %v", event.Args) case *runtime.EventExceptionThrown: t.Logf("browser exception: %s", event.ExceptionDetails.Text) } }) ctx, cancel := context.WithTimeout(browser, 30*time.Second) defer cancel() waitFor := func(expression, description string) chromedp.Action { return chromedp.ActionFunc(func(ctx context.Context) error { deadline := time.Now().Add(25 * time.Second) for time.Now().Before(deadline) { var ready bool if evaluateErr := chromedp.Evaluate(expression, &ready).Do(ctx); evaluateErr != nil { return evaluateErr } if ready { return nil } time.Sleep(100 * time.Millisecond) } var diagnostic any _ = chromedp.Evaluate(`({status: document.documentElement.dataset.testStatus, sequence: document.documentElement.dataset.planSequence, moveX: document.documentElement.dataset.plannedMoveX, moveZ: document.documentElement.dataset.plannedMoveZ, scripts: [...document.scripts].map(s => ({src:s.src,type:s.type})), resources: performance.getEntriesByType("resource").map(r => r.name)})`, &diagnostic).Do(ctx) return fmt.Errorf("%w for %s: %#v", errors.New("timeout waiting for browser"), description, diagnostic) }) } var initialScreenshot, movingScreenshot, screenshot []byte var message string err = chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error { if enableErr := cdplog.Enable().Do(ctx); enableErr != nil { return enableErr } return runtime.Enable().Do(ctx) }), chromedp.Navigate(server.URL+"/"), waitFor(`document.documentElement.dataset.testStatus === "ready"`, "initial plan"), chromedp.FullScreenshot(&initialScreenshot, 90), chromedp.Click(`.pad button[data-key="w"]`, chromedp.ByQuery), waitFor(`Number(document.documentElement.dataset.planSequence) >= 2 && document.documentElement.dataset.plannedMoveZ === "1"`, "forward pad-button plan"), chromedp.FullScreenshot(&movingScreenshot, 90), chromedp.Click(`.pad button[data-key="w"]`, chromedp.ByQuery), waitFor(`Number(document.documentElement.dataset.planSequence) >= 3 && document.documentElement.dataset.plannedMoveX === "0" && document.documentElement.dataset.plannedMoveZ === "0"`, "pad-button stop plan"), chromedp.Evaluate(`dispatchEvent(new KeyboardEvent("keydown", {key:"d", bubbles:true}))`, nil), waitFor(`Number(document.documentElement.dataset.planSequence) >= 4 && document.documentElement.dataset.plannedMoveX === "1"`, "keyboard-right plan"), chromedp.Evaluate(`dispatchEvent(new KeyboardEvent("keyup", {key:"d", bubbles:true}))`, nil), waitFor(`Number(document.documentElement.dataset.planSequence) >= 5 && document.documentElement.dataset.plannedMoveX === "0" && document.documentElement.dataset.plannedMoveZ === "0"`, "keyboard stop plan"), chromedp.Navigate(server.URL+"/?test=1"), waitFor(`document.documentElement.dataset.testStatus === "passed" || document.documentElement.dataset.testStatus === "failed"`, "style-and-turn self-test"), chromedp.Text("#test-result", &message, chromedp.ByQuery), chromedp.FullScreenshot(&screenshot, 90), ) if err != nil { t.Fatal(err) } var status string if err = chromedp.Run(ctx, chromedp.Evaluate(`document.documentElement.dataset.testStatus`, &status)); err != nil { t.Fatal(err) } if status != "passed" { t.Fatalf("browser self-test status=%q: %s", status, message) } if len(initialScreenshot) < 10_000 || len(movingScreenshot) < 10_000 || len(screenshot) < 10_000 { t.Fatalf("rendered screenshots are unexpectedly small: initial=%d moving=%d final=%d", len(initialScreenshot), len(movingScreenshot), len(screenshot)) } artifact := os.Getenv("MOTIONBRICKS_SCREENSHOT") if artifact == "" { artifact = filepath.Join(t.TempDir(), "motionbricks-demo.png") } extension := filepath.Ext(artifact) if extension == "" { extension = ".png" } base := strings.TrimSuffix(artifact, filepath.Ext(artifact)) artifacts := []struct { path string data []byte }{ {path: base + "-initial" + extension, data: initialScreenshot}, {path: base + "-moving" + extension, data: movingScreenshot}, {path: artifact, data: screenshot}, } for _, item := range artifacts { if err = os.WriteFile(item.path, item.data, 0o600); err != nil { t.Fatal(err) } } t.Logf("%s; screenshots: %s, %s, %s (final %s)", message, artifacts[0].path, artifacts[1].path, artifacts[2].path, fmt.Sprintf("%d bytes", len(screenshot))) }