- Introduced a trace manager in the context struct for lazy initialization and management of trace operations. - Implemented the Trace method in the context to facilitate trace object creation and management within JavaScript. - Enhanced configuration options for trace drivers, including defaults for local and store drivers, and added path and prefix settings. - Updated related tests to verify the functionality of the new trace management features.
544 lines
15 KiB
Go
544 lines
15 KiB
Go
package jsapi
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
v8 "github.com/yaoapp/gou/runtime/v8"
|
|
"github.com/yaoapp/gou/runtime/v8/bridge"
|
|
"github.com/yaoapp/yao/config"
|
|
"github.com/yaoapp/yao/test"
|
|
"github.com/yaoapp/yao/trace/types"
|
|
"rogchap.com/v8go"
|
|
)
|
|
|
|
// TestTraceNew test creating a new trace from JavaScript
|
|
func TestTraceNew(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
v8.RegisterFunction("testTraceNew", testTraceNewEmbed)
|
|
res, err := v8.Call(v8.CallOptions{}, `
|
|
function test() {
|
|
const trace = new Trace({ driver: "local", path: "/tmp/test-traces" })
|
|
return testTraceNew(trace)
|
|
}`)
|
|
if err != nil {
|
|
t.Fatalf("Call failed: %v", err)
|
|
}
|
|
|
|
// With Internal Field + goMaps, res should now be the manager directly
|
|
manager, ok := res.(types.Manager)
|
|
if !ok {
|
|
t.Fatalf("Expected types.Manager, got %T", res)
|
|
}
|
|
|
|
assert.NotNil(t, manager, "manager should not be nil")
|
|
|
|
// After v8.Call returns, the jsRes should have been released via defer bridge.FreeJsValue(jsRes)
|
|
// This should have triggered __release() on the trace object, cleaning up the goMaps
|
|
// Note: We can't directly check goMaps as it's in the bridge package
|
|
}
|
|
|
|
func testTraceNewEmbed(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, testTraceNewFunction)
|
|
}
|
|
|
|
func testTraceNewFunction(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
args := info.Args()
|
|
if len(args) < 1 {
|
|
return bridge.JsException(info.Context(), "Missing parameters")
|
|
}
|
|
|
|
trace, err := args[0].AsObject()
|
|
if err != nil {
|
|
return bridge.JsException(info.Context(), err)
|
|
}
|
|
|
|
// Verify the trace has id field
|
|
traceID, err := trace.Get("id")
|
|
if err != nil {
|
|
return bridge.JsException(info.Context(), err)
|
|
}
|
|
|
|
if !traceID.IsString() {
|
|
return bridge.JsException(info.Context(), fmt.Errorf("id should be a string"))
|
|
}
|
|
|
|
// Verify the trace has __release function
|
|
release, err := trace.Get("__release")
|
|
if err != nil {
|
|
return bridge.JsException(info.Context(), err)
|
|
}
|
|
|
|
if !release.IsFunction() {
|
|
return bridge.JsException(info.Context(), fmt.Errorf("__release should be a function"))
|
|
}
|
|
|
|
// Return the trace object itself so its __release will be called when v8.Call finishes
|
|
return args[0]
|
|
}
|
|
|
|
// TestTraceAddNode test adding nodes to trace
|
|
func TestTraceAddNode(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
v8.RegisterFunction("testTraceAddNode", testTraceAddNodeEmbed)
|
|
res, err := v8.Call(v8.CallOptions{}, `
|
|
function test() {
|
|
const trace = new Trace({ driver: "local", path: "/tmp/test-traces" })
|
|
const node = trace.Add({ type: "step", content: "Test step" }, { label: "Step 1" })
|
|
// testTraceAddNode returns the trace object itself
|
|
return testTraceAddNode(trace, node)
|
|
}`)
|
|
if err != nil {
|
|
t.Fatalf("Call failed: %v", err)
|
|
}
|
|
|
|
// With Internal Field + goMaps, res should now be the manager directly
|
|
manager, ok := res.(types.Manager)
|
|
if !ok {
|
|
t.Fatalf("Expected types.Manager, got %T", res)
|
|
}
|
|
|
|
assert.NotNil(t, manager, "manager should not be nil")
|
|
// Note: We can't directly check goMaps cleanup as it's in the bridge package
|
|
}
|
|
|
|
func testTraceAddNodeEmbed(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, testTraceAddNodeFunction)
|
|
}
|
|
|
|
func testTraceAddNodeFunction(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
args := info.Args()
|
|
if len(args) < 2 {
|
|
return bridge.JsException(info.Context(), "Missing parameters")
|
|
}
|
|
|
|
trace, err := args[0].AsObject()
|
|
if err != nil {
|
|
return bridge.JsException(info.Context(), err)
|
|
}
|
|
|
|
node, err := args[1].AsObject()
|
|
if err != nil {
|
|
return bridge.JsException(info.Context(), err)
|
|
}
|
|
|
|
// Get trace ID
|
|
traceID, err := trace.Get("id")
|
|
if err != nil {
|
|
return bridge.JsException(info.Context(), err)
|
|
}
|
|
|
|
// Get node ID
|
|
nodeID, err := node.Get("id")
|
|
if err != nil {
|
|
return bridge.JsException(info.Context(), err)
|
|
}
|
|
|
|
// Verify IDs exist
|
|
if traceID.IsUndefined() || nodeID.IsUndefined() {
|
|
return bridge.JsException(info.Context(), fmt.Errorf("missing trace or node ID"))
|
|
}
|
|
|
|
// Return the trace object itself to trigger __release
|
|
return args[0]
|
|
}
|
|
|
|
// TestTraceNodeComplete test completing a node
|
|
func TestTraceNodeComplete(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
res, err := v8.Call(v8.CallOptions{}, `
|
|
function test() {
|
|
const trace = new Trace({ driver: "local", path: "/tmp/test-traces" })
|
|
const node = trace.Add({ type: "step", content: "Test step" }, { label: "Step 1" })
|
|
|
|
// Log some messages
|
|
node.Info("Processing...")
|
|
node.Debug("Debug info")
|
|
|
|
// Complete the node
|
|
node.Complete({ result: "success" })
|
|
|
|
return trace
|
|
}`)
|
|
if err != nil {
|
|
t.Fatalf("Call failed: %v", err)
|
|
}
|
|
|
|
// With Internal Field + goMaps, res should now be the manager directly
|
|
manager, ok := res.(types.Manager)
|
|
if !ok {
|
|
t.Fatalf("Expected types.Manager, got %T", res)
|
|
}
|
|
|
|
assert.NotNil(t, manager, "manager should not be nil")
|
|
// Note: We can't directly check goMaps cleanup as it's in the bridge package
|
|
}
|
|
|
|
// TestTraceSpace test creating and using spaces
|
|
func TestTraceSpace(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
v8.RegisterFunction("testTraceSpace", testTraceSpaceEmbed)
|
|
res, err := v8.Call(v8.CallOptions{}, `
|
|
function test() {
|
|
const trace = new Trace({ driver: "local", path: "/tmp/test-traces" })
|
|
const space = trace.CreateSpace({ label: "Test Space" })
|
|
|
|
// Set some values
|
|
space.Set("key1", "value1")
|
|
space.Set("key2", 123)
|
|
space.Set("key3", { nested: "object" })
|
|
|
|
// Call test function for verification
|
|
testTraceSpace(space)
|
|
|
|
// Return trace object to trigger __release
|
|
return trace
|
|
}`)
|
|
if err != nil {
|
|
t.Fatalf("Call failed: %v", err)
|
|
}
|
|
|
|
// With Internal Field + goMaps, res should now be the manager directly
|
|
manager, ok := res.(types.Manager)
|
|
if !ok {
|
|
t.Fatalf("Expected types.Manager, got %T", res)
|
|
}
|
|
|
|
assert.NotNil(t, manager, "manager should not be nil")
|
|
// Note: We can't directly check goMaps cleanup as it's in the bridge package
|
|
}
|
|
|
|
func testTraceSpaceEmbed(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, testTraceSpaceFunction)
|
|
}
|
|
|
|
func testTraceSpaceFunction(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
args := info.Args()
|
|
if len(args) < 1 {
|
|
return bridge.JsException(info.Context(), "Missing parameters")
|
|
}
|
|
|
|
space, err := args[0].AsObject()
|
|
if err != nil {
|
|
return bridge.JsException(info.Context(), err)
|
|
}
|
|
|
|
// Get space ID
|
|
spaceID, err := space.Get("id")
|
|
if err != nil {
|
|
return bridge.JsException(info.Context(), err)
|
|
}
|
|
|
|
// Get stored values to verify
|
|
getVal := func(obj *v8go.Object, method string, key string) (interface{}, error) {
|
|
getMethod, err := obj.Get(method)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer getMethod.Release()
|
|
getFn, err := getMethod.AsFunction()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
keyVal, err := v8go.NewValue(info.Context().Isolate(), key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer keyVal.Release()
|
|
result, err := getFn.Call(obj.Value, keyVal)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return bridge.GoValue(result, info.Context())
|
|
}
|
|
|
|
key1, _ := getVal(space, "Get", "key1")
|
|
key2, _ := getVal(space, "Get", "key2")
|
|
|
|
// Verify values exist
|
|
if spaceID.IsUndefined() || key1 == nil || key2 == nil {
|
|
return bridge.JsException(info.Context(), fmt.Errorf("missing space data"))
|
|
}
|
|
|
|
// This function is just for verification, we don't need to return a new object
|
|
// Return undefined, the outer JavaScript will return the trace object
|
|
return v8go.Undefined(info.Context().Isolate())
|
|
}
|
|
|
|
// TestTraceConcurrent test concurrent trace operations
|
|
func TestTraceConcurrent(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
// Number of concurrent goroutines
|
|
concurrency := 10
|
|
iterationsPerGoroutine := 5
|
|
|
|
var wg sync.WaitGroup
|
|
errors := make(chan error, concurrency*iterationsPerGoroutine)
|
|
results := make(chan string, concurrency*iterationsPerGoroutine)
|
|
|
|
// Launch concurrent goroutines
|
|
for i := 0; i < concurrency; i++ {
|
|
wg.Add(1)
|
|
go func(routineID int) {
|
|
defer wg.Done()
|
|
|
|
for j := 0; j < iterationsPerGoroutine; j++ {
|
|
script := fmt.Sprintf(`
|
|
function test() {
|
|
const trace = new Trace({ driver: "local", path: "/tmp/test-traces-%d-%d" })
|
|
const node = trace.Add({ type: "step", content: "Test %d-%d" }, { label: "Step" })
|
|
node.Info("Processing")
|
|
node.Complete({ result: "done" })
|
|
return trace
|
|
}`, routineID, j, routineID, j)
|
|
|
|
res, err := v8.Call(v8.CallOptions{}, script)
|
|
if err != nil {
|
|
errors <- fmt.Errorf("routine %d iteration %d failed: %v", routineID, j, err)
|
|
return
|
|
}
|
|
|
|
// With Internal Field + goMaps, res should now be the manager directly
|
|
if _, ok := res.(types.Manager); ok {
|
|
// Successfully got manager, add a result
|
|
results <- fmt.Sprintf("routine-%d-iteration-%d", routineID, j)
|
|
} else {
|
|
errors <- fmt.Errorf("routine %d iteration %d: expected types.Manager, got %T", routineID, j, res)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Wait for all goroutines to complete
|
|
wg.Wait()
|
|
close(errors)
|
|
close(results)
|
|
|
|
// Check for errors
|
|
for err := range errors {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Verify all results
|
|
resultCount := 0
|
|
for range results {
|
|
resultCount++
|
|
}
|
|
|
|
// Verify the correct number of results
|
|
expectedResults := concurrency * iterationsPerGoroutine
|
|
assert.Equal(t, expectedResults, resultCount, "Should have %d results", expectedResults)
|
|
|
|
// Verify all traces are cleaned up
|
|
// Note: We can't directly check goMaps cleanup as it's in the bridge package
|
|
}
|
|
|
|
// TestTraceParallel test parallel node execution
|
|
func TestTraceParallel(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
v8.RegisterFunction("testTraceParallel", testTraceParallelEmbed)
|
|
res, err := v8.Call(v8.CallOptions{}, `
|
|
function test() {
|
|
const trace = new Trace({ driver: "local", path: "/tmp/test-traces" })
|
|
|
|
// Must call Add() first to create root node
|
|
trace.Add({ type: "root", content: "Root" }, { label: "Root" })
|
|
|
|
// Create parallel nodes
|
|
const nodes = trace.Parallel([
|
|
{ input: { type: "task", content: "Task 1" }, option: { label: "Parallel 1" } },
|
|
{ input: { type: "task", content: "Task 2" }, option: { label: "Parallel 2" } },
|
|
{ input: { type: "task", content: "Task 3" }, option: { label: "Parallel 3" } }
|
|
])
|
|
|
|
// Call test function for verification
|
|
testTraceParallel(nodes)
|
|
|
|
// Return trace object to trigger __release
|
|
return trace
|
|
}`)
|
|
if err != nil {
|
|
t.Fatalf("Call failed: %v", err)
|
|
}
|
|
|
|
// With Internal Field + goMaps, res should now be the manager directly
|
|
manager, ok := res.(types.Manager)
|
|
if !ok {
|
|
t.Fatalf("Expected types.Manager, got %T", res)
|
|
}
|
|
|
|
assert.NotNil(t, manager, "manager should not be nil")
|
|
// Note: We can't directly check goMaps cleanup as it's in the bridge package
|
|
}
|
|
|
|
func testTraceParallelEmbed(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, testTraceParallelFunction)
|
|
}
|
|
|
|
func testTraceParallelFunction(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
args := info.Args()
|
|
if len(args) < 1 {
|
|
return bridge.JsException(info.Context(), "Missing parameters")
|
|
}
|
|
|
|
nodes, err := args[0].AsObject()
|
|
if err != nil {
|
|
return bridge.JsException(info.Context(), err)
|
|
}
|
|
|
|
// Get array length
|
|
lengthVal, err := nodes.Get("length")
|
|
if err != nil {
|
|
return bridge.JsException(info.Context(), err)
|
|
}
|
|
|
|
// Verify we got 3 nodes
|
|
if lengthVal.Int32() != 3 {
|
|
return bridge.JsException(info.Context(), fmt.Errorf("expected 3 nodes, got %d", lengthVal.Int32()))
|
|
}
|
|
|
|
// Return undefined, the outer JavaScript will return the trace object
|
|
return v8go.Undefined(info.Context().Isolate())
|
|
}
|
|
|
|
// TestTraceMarkComplete test marking trace as complete
|
|
func TestTraceMarkComplete(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
res, err := v8.Call(v8.CallOptions{}, `
|
|
function test() {
|
|
const trace = new Trace({ driver: "local", path: "/tmp/test-traces" })
|
|
const node = trace.Add({ type: "step", content: "Test step" }, { label: "Step 1" })
|
|
node.Complete({ result: "success" })
|
|
|
|
// Mark entire trace as complete
|
|
trace.MarkComplete()
|
|
|
|
// Check if complete
|
|
const isComplete = trace.IsComplete()
|
|
|
|
return trace
|
|
}`)
|
|
if err != nil {
|
|
t.Fatalf("Call failed: %v", err)
|
|
}
|
|
|
|
// With Internal Field + goMaps, res should now be the manager directly
|
|
manager, ok := res.(types.Manager)
|
|
if !ok {
|
|
t.Fatalf("Expected types.Manager, got %T", res)
|
|
}
|
|
|
|
assert.NotNil(t, manager, "manager should not be nil")
|
|
// Note: We can't directly check goMaps cleanup as it's in the bridge package
|
|
}
|
|
|
|
// TestTracePassAsParameter test passing trace object as parameter to a function
|
|
func TestTracePassAsParameter(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
v8.RegisterFunction("processTrace", processTraceEmbed)
|
|
res, err := v8.Call(v8.CallOptions{}, `
|
|
function test() {
|
|
// Define a function that accepts trace as parameter
|
|
const handleTrace = function(trace) {
|
|
// Add a node using the passed trace
|
|
const node = trace.Add({ type: "step", content: "Step from handler" }, { label: "Handler Step" })
|
|
node.Info("Processing in handler function")
|
|
node.Complete({ result: "handler completed" })
|
|
|
|
// Return some info
|
|
return {
|
|
traceId: trace.id,
|
|
nodeId: node.id
|
|
}
|
|
}
|
|
|
|
// Create trace and pass it to the handler
|
|
const trace = new Trace({ driver: "local", path: "/tmp/test-traces" })
|
|
const result = handleTrace(trace)
|
|
|
|
// Also test passing to a Go function
|
|
processTrace(trace)
|
|
|
|
// Return trace to trigger __release
|
|
return trace
|
|
}`)
|
|
if err != nil {
|
|
t.Fatalf("Call failed: %v", err)
|
|
}
|
|
|
|
// With __govalue function, res should now be the manager directly
|
|
manager, ok := res.(types.Manager)
|
|
if !ok {
|
|
t.Fatalf("Expected types.Manager, got %T", res)
|
|
}
|
|
|
|
assert.NotNil(t, manager, "manager should not be nil")
|
|
// Note: We can't directly check goMaps cleanup as it's in the bridge package
|
|
}
|
|
|
|
func processTraceEmbed(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, processTraceFunction)
|
|
}
|
|
|
|
func processTraceFunction(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
args := info.Args()
|
|
if len(args) < 1 {
|
|
return bridge.JsException(info.Context(), "Missing trace parameter")
|
|
}
|
|
|
|
// Try to get the trace object
|
|
traceValue := args[0]
|
|
|
|
// Convert to Go value - with __govalue function, this should return the manager directly
|
|
goValue, err := bridge.GoValue(traceValue, info.Context())
|
|
if err != nil {
|
|
return bridge.JsException(info.Context(), fmt.Errorf("failed to convert trace: %v", err))
|
|
}
|
|
|
|
fmt.Printf("\n=== Go function received trace ===\n")
|
|
fmt.Printf("Type: %T\n", goValue)
|
|
|
|
// Check if we got the manager directly (new __govalue behavior)
|
|
if manager, ok := goValue.(types.Manager); ok {
|
|
fmt.Printf("✅ SUCCESS: Got manager directly via __govalue function!\n")
|
|
fmt.Printf(" Manager type: %T\n", manager)
|
|
|
|
// Now we can use the manager directly!
|
|
manager.Info("Message from Go function via __govalue")
|
|
return v8go.Undefined(info.Context().Isolate())
|
|
}
|
|
|
|
// Fallback: if we got a map (shouldn't happen with __govalue)
|
|
if traceMap, ok := goValue.(map[string]interface{}); ok {
|
|
fmt.Printf("⚠️ Got map (fallback): %v\n", getMapKeys(traceMap))
|
|
return bridge.JsException(info.Context(), fmt.Errorf("unexpected: got map instead of manager"))
|
|
}
|
|
|
|
return bridge.JsException(info.Context(), fmt.Errorf("unexpected type: %T", goValue))
|
|
}
|
|
|
|
func getMapKeys(m map[string]interface{}) []string {
|
|
keys := make([]string, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
return keys
|
|
}
|