- Updated benchmark and test functions to utilize node configurations instead of pool configurations for improved clarity and consistency. - Refactored related setup functions and test cases to align with the new node-based architecture. - Adjusted error messages and documentation to reflect the transition from pool to node terminology. Made-with: Cursor
418 lines
11 KiB
Go
418 lines
11 KiB
Go
package jsapi_test
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
v8runtime "github.com/yaoapp/gou/runtime/v8"
|
|
"github.com/yaoapp/yao/config"
|
|
sandbox "github.com/yaoapp/yao/sandbox/v2"
|
|
"github.com/yaoapp/yao/tai"
|
|
"github.com/yaoapp/yao/tai/registry"
|
|
"github.com/yaoapp/yao/test"
|
|
|
|
_ "github.com/yaoapp/yao/sandbox/v2/jsapi"
|
|
)
|
|
|
|
type testMode struct {
|
|
Name string
|
|
Addr string
|
|
TaiID string // filled by setupSandbox
|
|
Options []tai.Option
|
|
}
|
|
|
|
func testModes() []testMode {
|
|
modes := []testMode{{Name: "local", Addr: "local"}}
|
|
if addr := os.Getenv("SANDBOX_TEST_REMOTE_ADDR"); addr != "" {
|
|
modes = append(modes, testMode{Name: "remote", Addr: addr})
|
|
}
|
|
return modes
|
|
}
|
|
|
|
func testImage() string {
|
|
if img := os.Getenv("SANDBOX_TEST_IMAGE"); img != "" {
|
|
return img
|
|
}
|
|
return "alpine:latest"
|
|
}
|
|
|
|
func setupSandbox(t *testing.T, m *testMode) {
|
|
t.Helper()
|
|
test.Prepare(t, config.Conf)
|
|
|
|
reg := registry.Global()
|
|
if reg == nil {
|
|
registry.Init(nil)
|
|
}
|
|
|
|
client, err := tai.New(m.Addr, m.Options...)
|
|
if err != nil {
|
|
t.Fatalf("tai.New: %v", err)
|
|
}
|
|
m.TaiID = client.TaiID()
|
|
|
|
sandbox.Init()
|
|
mgr := sandbox.M()
|
|
t.Cleanup(func() { mgr.Close() })
|
|
}
|
|
|
|
func runJS(t *testing.T, source string) interface{} {
|
|
t.Helper()
|
|
res, err := v8runtime.Call(v8runtime.CallOptions{
|
|
Sid: "test",
|
|
Timeout: 60 * time.Second,
|
|
}, source)
|
|
if err != nil {
|
|
t.Fatalf("JS error: %v", err)
|
|
}
|
|
return res
|
|
}
|
|
|
|
func runJSExpectError(t *testing.T, source string) string {
|
|
t.Helper()
|
|
_, err := v8runtime.Call(v8runtime.CallOptions{
|
|
Sid: "test",
|
|
Timeout: 30 * time.Second,
|
|
}, source)
|
|
if err == nil {
|
|
t.Fatal("expected JS error, got nil")
|
|
}
|
|
return err.Error()
|
|
}
|
|
|
|
func skipIfNoDocker(t *testing.T) {
|
|
t.Helper()
|
|
addr := os.Getenv("SANDBOX_TEST_LOCAL_ADDR")
|
|
if addr == "" {
|
|
addr = "local"
|
|
}
|
|
_ = addr
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// sandbox.Create / sandbox.Get / sandbox.Delete
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestCreate(t *testing.T) {
|
|
skipIfNoDocker(t)
|
|
for _, m := range testModes() {
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
img := testImage()
|
|
res := runJS(t, fmt.Sprintf(`function TestCreate() {
|
|
var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
|
|
if (pc.kind !== "box") throw new Error("kind=" + pc.kind);
|
|
if (!pc.id) throw new Error("no id");
|
|
var id = pc.id;
|
|
sandbox.Delete(id);
|
|
return id;
|
|
}`, img, m.TaiID))
|
|
if res == nil || res == "" {
|
|
t.Error("expected box id")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGet(t *testing.T) {
|
|
skipIfNoDocker(t)
|
|
for _, m := range testModes() {
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
img := testImage()
|
|
res := runJS(t, fmt.Sprintf(`function TestGet() {
|
|
var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
|
|
var id = pc.id;
|
|
var got = sandbox.Get(id);
|
|
if (!got) throw new Error("Get returned null");
|
|
if (got.kind !== "box") throw new Error("kind=" + got.kind);
|
|
sandbox.Delete(id);
|
|
return id;
|
|
}`, img, m.TaiID))
|
|
if res == nil || res == "" {
|
|
t.Error("expected box id")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetNotFound(t *testing.T) {
|
|
for _, m := range testModes() {
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
res := runJS(t, `function TestGetNotFound() {
|
|
var got = sandbox.Get("sb-nonexistent-id");
|
|
return got === null ? "null" : "found";
|
|
}`)
|
|
if res != "null" {
|
|
t.Errorf("expected null, got %v", res)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDelete(t *testing.T) {
|
|
skipIfNoDocker(t)
|
|
for _, m := range testModes() {
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
img := testImage()
|
|
res := runJS(t, fmt.Sprintf(`function TestDelete() {
|
|
var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
|
|
var id = pc.id;
|
|
sandbox.Delete(id);
|
|
var got = sandbox.Get(id);
|
|
return got === null ? "deleted" : "still exists";
|
|
}`, img, m.TaiID))
|
|
if res != "deleted" {
|
|
t.Errorf("expected deleted, got %v", res)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// sandbox.List
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestList(t *testing.T) {
|
|
skipIfNoDocker(t)
|
|
for _, m := range testModes() {
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
img := testImage()
|
|
res := runJS(t, fmt.Sprintf(`function TestList() {
|
|
var a = sandbox.Create({ image: "%s", owner: "list-user", node_id: "%s" });
|
|
var b = sandbox.Create({ image: "%s", owner: "list-user", node_id: "%s" });
|
|
var list = sandbox.List({ owner: "list-user" });
|
|
var count = list.length;
|
|
sandbox.Delete(a.id);
|
|
sandbox.Delete(b.id);
|
|
return count;
|
|
}`, img, m.TaiID, img, m.TaiID))
|
|
n := toInt(res)
|
|
if n < 2 {
|
|
t.Errorf("expected >= 2, got %d", n)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Computer.Exec
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestExec(t *testing.T) {
|
|
skipIfNoDocker(t)
|
|
for _, m := range testModes() {
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
img := testImage()
|
|
res := runJS(t, fmt.Sprintf(`function TestExec() {
|
|
var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
|
|
var r = pc.Exec(["echo", "hello-jsapi"]);
|
|
sandbox.Delete(pc.id);
|
|
return r.stdout;
|
|
}`, img, m.TaiID))
|
|
s := fmt.Sprintf("%v", res)
|
|
if !strings.Contains(s, "hello-jsapi") {
|
|
t.Errorf("stdout = %q, want contain 'hello-jsapi'", s)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExecWithOptions(t *testing.T) {
|
|
skipIfNoDocker(t)
|
|
for _, m := range testModes() {
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
img := testImage()
|
|
res := runJS(t, fmt.Sprintf(`function TestExecWithOptions() {
|
|
var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
|
|
var r = pc.Exec(["pwd"], { workdir: "/tmp" });
|
|
sandbox.Delete(pc.id);
|
|
return r.stdout;
|
|
}`, img, m.TaiID))
|
|
s := fmt.Sprintf("%v", res)
|
|
if !strings.Contains(s, "/tmp") {
|
|
t.Errorf("stdout = %q, want contain '/tmp'", s)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Computer.Stream
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestStream(t *testing.T) {
|
|
skipIfNoDocker(t)
|
|
for _, m := range testModes() {
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
img := testImage()
|
|
res := runJS(t, fmt.Sprintf(`function TestStream() {
|
|
var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
|
|
var chunks = [];
|
|
var exitCode = -1;
|
|
pc.Stream(["echo", "streaming"], function(type, data) {
|
|
if (type === "stdout") chunks.push(data);
|
|
if (type === "exit") exitCode = data;
|
|
});
|
|
sandbox.Delete(pc.id);
|
|
return chunks.join("").trim() + "|" + exitCode;
|
|
}`, img, m.TaiID))
|
|
s := fmt.Sprintf("%v", res)
|
|
if !strings.Contains(s, "streaming|0") {
|
|
t.Errorf("result = %q, want contain 'streaming|0'", s)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Computer.ComputerInfo
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestComputerInfo(t *testing.T) {
|
|
skipIfNoDocker(t)
|
|
for _, m := range testModes() {
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
img := testImage()
|
|
res := runJS(t, fmt.Sprintf(`function TestComputerInfo() {
|
|
var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
|
|
var info = pc.ComputerInfo();
|
|
sandbox.Delete(pc.id);
|
|
return info.kind;
|
|
}`, img, m.TaiID))
|
|
if res != "box" {
|
|
t.Errorf("kind = %q, want 'box'", res)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Computer.Info (box-only)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestBoxInfo(t *testing.T) {
|
|
skipIfNoDocker(t)
|
|
for _, m := range testModes() {
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
img := testImage()
|
|
res := runJS(t, fmt.Sprintf(`function TestBoxInfo() {
|
|
var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
|
|
var info = pc.Info();
|
|
sandbox.Delete(pc.id);
|
|
return info.id ? "ok" : "no-id";
|
|
}`, img, m.TaiID))
|
|
if res != "ok" {
|
|
t.Errorf("expected ok, got %v", res)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Box-only method on host → error
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestHostBoxMethodsThrow(t *testing.T) {
|
|
if os.Getenv("SANDBOX_TEST_REMOTE_ADDR") == "" {
|
|
t.Skip("no remote host configured")
|
|
}
|
|
for _, m := range testModes() {
|
|
if m.Name == "local" {
|
|
continue
|
|
}
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
errMsg := runJSExpectError(t, fmt.Sprintf(`function TestHostBoxMethodsThrow() {
|
|
var host = sandbox.Host("%s");
|
|
host.Info();
|
|
}`, m.TaiID))
|
|
if !strings.Contains(errMsg, "not supported") {
|
|
t.Errorf("expected 'not supported' error, got: %s", errMsg)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Computer.kind property
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestComputerKind(t *testing.T) {
|
|
skipIfNoDocker(t)
|
|
for _, m := range testModes() {
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
img := testImage()
|
|
res := runJS(t, fmt.Sprintf(`function TestComputerKind() {
|
|
var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
|
|
var k = pc.kind;
|
|
sandbox.Delete(pc.id);
|
|
return k;
|
|
}`, img, m.TaiID))
|
|
if res != "box" {
|
|
t.Errorf("kind = %q, want 'box'", res)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// sandbox.Nodes (requires registry)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestNodes(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
registry.Init(nil)
|
|
res := runJS(t, `function TestNodes() {
|
|
var nodes = sandbox.Nodes();
|
|
return Array.isArray(nodes) ? "array" : typeof nodes;
|
|
}`)
|
|
if res != "array" {
|
|
t.Errorf("expected array, got %v", res)
|
|
}
|
|
}
|
|
|
|
func TestGetNodeNotFound(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
registry.Init(nil)
|
|
res := runJS(t, `function TestGetNodeNotFound() {
|
|
var node = sandbox.GetNode("tai-nonexistent");
|
|
return node === null ? "null" : "found";
|
|
}`)
|
|
if res != "null" {
|
|
t.Errorf("expected null, got %v", res)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func toInt(v interface{}) int {
|
|
switch n := v.(type) {
|
|
case int:
|
|
return n
|
|
case int32:
|
|
return int(n)
|
|
case int64:
|
|
return int(n)
|
|
case float64:
|
|
return int(n)
|
|
case float32:
|
|
return int(n)
|
|
default:
|
|
return 0
|
|
}
|
|
}
|