- Introduced a new script testing mode to allow testing of agent handler scripts (hooks, tools, etc.) using a Go-like interface, enabling better unit testing of TypeScript/JavaScript code. - Enhanced the `LoadScripts` function to skip test files during script loading, ensuring only relevant scripts are processed. - Refactored the test context creation to support custom context configurations via a JSON file, allowing for flexible authorization and metadata management during tests. - Updated the test runner to handle script tests, including the ability to filter tests using regex patterns and manage custom context data. - Improved documentation to include details on script testing usage, input formats, and available assertions, enhancing developer experience and clarity.
1000 lines
26 KiB
Go
1000 lines
26 KiB
Go
package test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/yaoapp/gou/runtime/v8/bridge"
|
|
"rogchap.com/v8go"
|
|
)
|
|
|
|
// TestingT represents the testing object passed to test functions
|
|
// It provides assertion methods and test control flow
|
|
type TestingT struct {
|
|
mu sync.Mutex
|
|
name string
|
|
failed bool
|
|
skipped bool
|
|
logs []string
|
|
errors []string
|
|
|
|
// Assertion failure details (for the first failure)
|
|
assertionInfo *ScriptAssertionInfo
|
|
}
|
|
|
|
// NewTestingT creates a new TestingT instance
|
|
func NewTestingT(name string) *TestingT {
|
|
return &TestingT{
|
|
name: name,
|
|
logs: make([]string, 0),
|
|
errors: make([]string, 0),
|
|
}
|
|
}
|
|
|
|
// Name returns the test name
|
|
func (t *TestingT) Name() string {
|
|
return t.name
|
|
}
|
|
|
|
// Failed returns whether the test has failed
|
|
func (t *TestingT) Failed() bool {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.failed
|
|
}
|
|
|
|
// Skipped returns whether the test was skipped
|
|
func (t *TestingT) Skipped() bool {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.skipped
|
|
}
|
|
|
|
// Logs returns all log messages
|
|
func (t *TestingT) Logs() []string {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return append([]string{}, t.logs...)
|
|
}
|
|
|
|
// Errors returns all error messages
|
|
func (t *TestingT) Errors() []string {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return append([]string{}, t.errors...)
|
|
}
|
|
|
|
// AssertionInfo returns the first assertion failure info
|
|
func (t *TestingT) AssertionInfo() *ScriptAssertionInfo {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.assertionInfo
|
|
}
|
|
|
|
// log adds a log message
|
|
func (t *TestingT) log(msg string) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
t.logs = append(t.logs, msg)
|
|
}
|
|
|
|
// fail marks the test as failed with an error message
|
|
func (t *TestingT) fail(msg string, info *ScriptAssertionInfo) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
t.failed = true
|
|
t.errors = append(t.errors, msg)
|
|
if t.assertionInfo == nil && info != nil {
|
|
t.assertionInfo = info
|
|
}
|
|
}
|
|
|
|
// skip marks the test as skipped
|
|
func (t *TestingT) skip(reason string) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
t.skipped = true
|
|
if reason != "" {
|
|
t.logs = append(t.logs, "SKIP: "+reason)
|
|
}
|
|
}
|
|
|
|
// NewTestingTObject creates a JavaScript testing.T object for V8
|
|
func NewTestingTObject(v8ctx *v8go.Context, t *TestingT) (*v8go.Value, error) {
|
|
iso := v8ctx.Isolate()
|
|
|
|
// Create the main testing object
|
|
testObj := v8go.NewObjectTemplate(iso)
|
|
|
|
// Set name property
|
|
testObj.Set("name", t.name)
|
|
|
|
// Create assert object
|
|
assertObj, err := newAssertObject(v8ctx, t)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Set methods
|
|
testObj.Set("log", t.logMethod(iso))
|
|
testObj.Set("error", t.errorMethod(iso))
|
|
testObj.Set("skip", t.skipMethod(iso))
|
|
testObj.Set("fail", t.failMethod(iso))
|
|
testObj.Set("fatal", t.fatalMethod(iso))
|
|
|
|
// Create instance
|
|
instance, err := testObj.NewInstance(v8ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
obj, err := instance.Value.AsObject()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Set assert object
|
|
obj.Set("assert", assertObj)
|
|
|
|
// Set failed getter (dynamic)
|
|
obj.Set("failed", t.failed)
|
|
|
|
return instance.Value, nil
|
|
}
|
|
|
|
// logMethod implements t.log(...args)
|
|
func (t *TestingT) logMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
args := info.Args()
|
|
parts := make([]string, len(args))
|
|
for i, arg := range args {
|
|
parts[i] = arg.String()
|
|
}
|
|
t.log(strings.Join(parts, " "))
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// errorMethod implements t.error(...args)
|
|
func (t *TestingT) errorMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
args := info.Args()
|
|
parts := make([]string, len(args))
|
|
for i, arg := range args {
|
|
parts[i] = arg.String()
|
|
}
|
|
msg := strings.Join(parts, " ")
|
|
t.fail(msg, nil)
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// skipMethod implements t.skip(reason?)
|
|
func (t *TestingT) skipMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
reason := ""
|
|
if len(info.Args()) > 0 {
|
|
reason = info.Args()[0].String()
|
|
}
|
|
t.skip(reason)
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// failMethod implements t.fail(reason?)
|
|
func (t *TestingT) failMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
reason := "test failed"
|
|
if len(info.Args()) > 0 {
|
|
reason = info.Args()[0].String()
|
|
}
|
|
t.fail(reason, nil)
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// fatalMethod implements t.fatal(reason?)
|
|
// Same as fail but intended to stop execution (in JS, this is handled by throwing)
|
|
func (t *TestingT) fatalMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
v8ctx := info.Context()
|
|
reason := "fatal error"
|
|
if len(info.Args()) > 0 {
|
|
reason = info.Args()[0].String()
|
|
}
|
|
t.fail(reason, nil)
|
|
// Return exception to stop execution
|
|
return bridge.JsException(v8ctx, reason)
|
|
})
|
|
}
|
|
|
|
// newAssertObject creates the assert object with all assertion methods
|
|
func newAssertObject(v8ctx *v8go.Context, t *TestingT) (*v8go.Value, error) {
|
|
iso := v8ctx.Isolate()
|
|
|
|
assertObj := v8go.NewObjectTemplate(iso)
|
|
|
|
// Boolean assertions
|
|
assertObj.Set("True", assertTrueMethod(iso, t))
|
|
assertObj.Set("False", assertFalseMethod(iso, t))
|
|
|
|
// Equality assertions
|
|
assertObj.Set("Equal", assertEqualMethod(iso, t))
|
|
assertObj.Set("NotEqual", assertNotEqualMethod(iso, t))
|
|
|
|
// Nil assertions
|
|
assertObj.Set("Nil", assertNilMethod(iso, t))
|
|
assertObj.Set("NotNil", assertNotNilMethod(iso, t))
|
|
|
|
// String assertions
|
|
assertObj.Set("Contains", assertContainsMethod(iso, t))
|
|
assertObj.Set("NotContains", assertNotContainsMethod(iso, t))
|
|
|
|
// Length assertion
|
|
assertObj.Set("Len", assertLenMethod(iso, t))
|
|
|
|
// Comparison assertions
|
|
assertObj.Set("Greater", assertGreaterMethod(iso, t))
|
|
assertObj.Set("GreaterOrEqual", assertGreaterOrEqualMethod(iso, t))
|
|
assertObj.Set("Less", assertLessMethod(iso, t))
|
|
assertObj.Set("LessOrEqual", assertLessOrEqualMethod(iso, t))
|
|
|
|
// Error assertions
|
|
assertObj.Set("Error", assertErrorMethod(iso, t))
|
|
assertObj.Set("NoError", assertNoErrorMethod(iso, t))
|
|
|
|
// Panic assertions
|
|
assertObj.Set("Panic", assertPanicMethod(iso, t))
|
|
assertObj.Set("NoPanic", assertNoPanicMethod(iso, t))
|
|
|
|
// Regex assertions
|
|
assertObj.Set("Match", assertMatchMethod(iso, t))
|
|
assertObj.Set("NotMatch", assertNotMatchMethod(iso, t))
|
|
|
|
// Type assertion
|
|
assertObj.Set("Type", assertTypeMethod(iso, t))
|
|
|
|
// JSON path assertion
|
|
assertObj.Set("JSONPath", assertJSONPathMethod(iso, t))
|
|
|
|
// Create instance
|
|
instance, err := assertObj.NewInstance(v8ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return instance.Value, nil
|
|
}
|
|
|
|
// Helper function to get optional message argument
|
|
func getMessage(args []*v8go.Value, startIdx int) string {
|
|
if len(args) > startIdx && args[startIdx].IsString() {
|
|
return args[startIdx].String()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// assertTrueMethod implements assert.True(value, message?)
|
|
func assertTrueMethod(iso *v8go.Isolate, t *TestingT) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
args := info.Args()
|
|
if len(args) < 1 {
|
|
t.fail("True requires a value argument", &ScriptAssertionInfo{Type: "True"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
value := args[0].Boolean()
|
|
message := getMessage(args, 1)
|
|
|
|
if !value {
|
|
msg := "expected true, got false"
|
|
if message != "" {
|
|
msg = message
|
|
}
|
|
t.fail(msg, &ScriptAssertionInfo{
|
|
Type: "True",
|
|
Expected: true,
|
|
Actual: false,
|
|
Message: message,
|
|
})
|
|
}
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// assertFalseMethod implements assert.False(value, message?)
|
|
func assertFalseMethod(iso *v8go.Isolate, t *TestingT) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
args := info.Args()
|
|
if len(args) < 1 {
|
|
t.fail("False requires a value argument", &ScriptAssertionInfo{Type: "False"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
value := args[0].Boolean()
|
|
message := getMessage(args, 1)
|
|
|
|
if value {
|
|
msg := "expected false, got true"
|
|
if message != "" {
|
|
msg = message
|
|
}
|
|
t.fail(msg, &ScriptAssertionInfo{
|
|
Type: "False",
|
|
Expected: false,
|
|
Actual: true,
|
|
Message: message,
|
|
})
|
|
}
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// assertEqualMethod implements assert.Equal(actual, expected, message?)
|
|
func assertEqualMethod(iso *v8go.Isolate, t *TestingT) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
v8ctx := info.Context()
|
|
args := info.Args()
|
|
if len(args) < 2 {
|
|
t.fail("Equal requires actual and expected arguments", &ScriptAssertionInfo{Type: "Equal"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
actual, _ := bridge.GoValue(args[0], v8ctx)
|
|
expected, _ := bridge.GoValue(args[1], v8ctx)
|
|
message := getMessage(args, 2)
|
|
|
|
if !deepEqual(actual, expected) {
|
|
msg := fmt.Sprintf("expected %v, got %v", expected, actual)
|
|
if message != "" {
|
|
msg = message
|
|
}
|
|
t.fail(msg, &ScriptAssertionInfo{
|
|
Type: "Equal",
|
|
Expected: expected,
|
|
Actual: actual,
|
|
Message: message,
|
|
})
|
|
}
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// assertNotEqualMethod implements assert.NotEqual(actual, expected, message?)
|
|
func assertNotEqualMethod(iso *v8go.Isolate, t *TestingT) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
v8ctx := info.Context()
|
|
args := info.Args()
|
|
if len(args) < 2 {
|
|
t.fail("NotEqual requires actual and expected arguments", &ScriptAssertionInfo{Type: "NotEqual"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
actual, _ := bridge.GoValue(args[0], v8ctx)
|
|
expected, _ := bridge.GoValue(args[1], v8ctx)
|
|
message := getMessage(args, 2)
|
|
|
|
if deepEqual(actual, expected) {
|
|
msg := fmt.Sprintf("expected values to be different, both are %v", actual)
|
|
if message != "" {
|
|
msg = message
|
|
}
|
|
t.fail(msg, &ScriptAssertionInfo{
|
|
Type: "NotEqual",
|
|
Expected: expected,
|
|
Actual: actual,
|
|
Message: message,
|
|
})
|
|
}
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// assertNilMethod implements assert.Nil(value, message?)
|
|
func assertNilMethod(iso *v8go.Isolate, t *TestingT) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
args := info.Args()
|
|
if len(args) < 1 {
|
|
t.fail("Nil requires a value argument", &ScriptAssertionInfo{Type: "Nil"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
isNil := args[0].IsNull() || args[0].IsUndefined()
|
|
message := getMessage(args, 1)
|
|
|
|
if !isNil {
|
|
msg := fmt.Sprintf("expected nil, got %v", args[0].String())
|
|
if message != "" {
|
|
msg = message
|
|
}
|
|
t.fail(msg, &ScriptAssertionInfo{
|
|
Type: "Nil",
|
|
Expected: nil,
|
|
Actual: args[0].String(),
|
|
Message: message,
|
|
})
|
|
}
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// assertNotNilMethod implements assert.NotNil(value, message?)
|
|
func assertNotNilMethod(iso *v8go.Isolate, t *TestingT) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
args := info.Args()
|
|
if len(args) < 1 {
|
|
t.fail("NotNil requires a value argument", &ScriptAssertionInfo{Type: "NotNil"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
isNil := args[0].IsNull() || args[0].IsUndefined()
|
|
message := getMessage(args, 1)
|
|
|
|
if isNil {
|
|
msg := "expected non-nil value, got nil"
|
|
if message != "" {
|
|
msg = message
|
|
}
|
|
t.fail(msg, &ScriptAssertionInfo{
|
|
Type: "NotNil",
|
|
Actual: nil,
|
|
Message: message,
|
|
})
|
|
}
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// assertContainsMethod implements assert.Contains(str, substr, message?)
|
|
func assertContainsMethod(iso *v8go.Isolate, t *TestingT) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
args := info.Args()
|
|
if len(args) < 2 {
|
|
t.fail("Contains requires str and substr arguments", &ScriptAssertionInfo{Type: "Contains"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
str := args[0].String()
|
|
substr := args[1].String()
|
|
message := getMessage(args, 2)
|
|
|
|
if !strings.Contains(str, substr) {
|
|
msg := fmt.Sprintf("expected '%s' to contain '%s'", str, substr)
|
|
if message != "" {
|
|
msg = message
|
|
}
|
|
t.fail(msg, &ScriptAssertionInfo{
|
|
Type: "Contains",
|
|
Expected: substr,
|
|
Actual: str,
|
|
Message: message,
|
|
})
|
|
}
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// assertNotContainsMethod implements assert.NotContains(str, substr, message?)
|
|
func assertNotContainsMethod(iso *v8go.Isolate, t *TestingT) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
args := info.Args()
|
|
if len(args) < 2 {
|
|
t.fail("NotContains requires str and substr arguments", &ScriptAssertionInfo{Type: "NotContains"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
str := args[0].String()
|
|
substr := args[1].String()
|
|
message := getMessage(args, 2)
|
|
|
|
if strings.Contains(str, substr) {
|
|
msg := fmt.Sprintf("expected '%s' to not contain '%s'", str, substr)
|
|
if message != "" {
|
|
msg = message
|
|
}
|
|
t.fail(msg, &ScriptAssertionInfo{
|
|
Type: "NotContains",
|
|
Expected: substr,
|
|
Actual: str,
|
|
Message: message,
|
|
})
|
|
}
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// assertLenMethod implements assert.Len(value, length, message?)
|
|
func assertLenMethod(iso *v8go.Isolate, t *TestingT) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
v8ctx := info.Context()
|
|
args := info.Args()
|
|
if len(args) < 2 {
|
|
t.fail("Len requires value and length arguments", &ScriptAssertionInfo{Type: "Len"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
value, _ := bridge.GoValue(args[0], v8ctx)
|
|
expectedLen := int(args[1].Integer())
|
|
message := getMessage(args, 2)
|
|
|
|
actualLen := getLength(value)
|
|
|
|
if actualLen != expectedLen {
|
|
msg := fmt.Sprintf("expected length %d, got %d", expectedLen, actualLen)
|
|
if message != "" {
|
|
msg = message
|
|
}
|
|
t.fail(msg, &ScriptAssertionInfo{
|
|
Type: "Len",
|
|
Expected: expectedLen,
|
|
Actual: actualLen,
|
|
Message: message,
|
|
})
|
|
}
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// assertGreaterMethod implements assert.Greater(a, b, message?)
|
|
func assertGreaterMethod(iso *v8go.Isolate, t *TestingT) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
args := info.Args()
|
|
if len(args) < 2 {
|
|
t.fail("Greater requires two arguments", &ScriptAssertionInfo{Type: "Greater"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
a := args[0].Number()
|
|
b := args[1].Number()
|
|
message := getMessage(args, 2)
|
|
|
|
if !(a > b) {
|
|
msg := fmt.Sprintf("expected %v > %v", a, b)
|
|
if message != "" {
|
|
msg = message
|
|
}
|
|
t.fail(msg, &ScriptAssertionInfo{
|
|
Type: "Greater",
|
|
Expected: fmt.Sprintf("> %v", b),
|
|
Actual: a,
|
|
Message: message,
|
|
})
|
|
}
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// assertGreaterOrEqualMethod implements assert.GreaterOrEqual(a, b, message?)
|
|
func assertGreaterOrEqualMethod(iso *v8go.Isolate, t *TestingT) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
args := info.Args()
|
|
if len(args) < 2 {
|
|
t.fail("GreaterOrEqual requires two arguments", &ScriptAssertionInfo{Type: "GreaterOrEqual"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
a := args[0].Number()
|
|
b := args[1].Number()
|
|
message := getMessage(args, 2)
|
|
|
|
if !(a >= b) {
|
|
msg := fmt.Sprintf("expected %v >= %v", a, b)
|
|
if message != "" {
|
|
msg = message
|
|
}
|
|
t.fail(msg, &ScriptAssertionInfo{
|
|
Type: "GreaterOrEqual",
|
|
Expected: fmt.Sprintf(">= %v", b),
|
|
Actual: a,
|
|
Message: message,
|
|
})
|
|
}
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// assertLessMethod implements assert.Less(a, b, message?)
|
|
func assertLessMethod(iso *v8go.Isolate, t *TestingT) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
args := info.Args()
|
|
if len(args) < 2 {
|
|
t.fail("Less requires two arguments", &ScriptAssertionInfo{Type: "Less"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
a := args[0].Number()
|
|
b := args[1].Number()
|
|
message := getMessage(args, 2)
|
|
|
|
if !(a < b) {
|
|
msg := fmt.Sprintf("expected %v < %v", a, b)
|
|
if message != "" {
|
|
msg = message
|
|
}
|
|
t.fail(msg, &ScriptAssertionInfo{
|
|
Type: "Less",
|
|
Expected: fmt.Sprintf("< %v", b),
|
|
Actual: a,
|
|
Message: message,
|
|
})
|
|
}
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// assertLessOrEqualMethod implements assert.LessOrEqual(a, b, message?)
|
|
func assertLessOrEqualMethod(iso *v8go.Isolate, t *TestingT) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
args := info.Args()
|
|
if len(args) < 2 {
|
|
t.fail("LessOrEqual requires two arguments", &ScriptAssertionInfo{Type: "LessOrEqual"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
a := args[0].Number()
|
|
b := args[1].Number()
|
|
message := getMessage(args, 2)
|
|
|
|
if !(a <= b) {
|
|
msg := fmt.Sprintf("expected %v <= %v", a, b)
|
|
if message != "" {
|
|
msg = message
|
|
}
|
|
t.fail(msg, &ScriptAssertionInfo{
|
|
Type: "LessOrEqual",
|
|
Expected: fmt.Sprintf("<= %v", b),
|
|
Actual: a,
|
|
Message: message,
|
|
})
|
|
}
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// assertErrorMethod implements assert.Error(err, message?)
|
|
func assertErrorMethod(iso *v8go.Isolate, t *TestingT) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
args := info.Args()
|
|
if len(args) < 1 {
|
|
t.fail("Error requires an argument", &ScriptAssertionInfo{Type: "Error"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
// Check if it's null/undefined (no error)
|
|
isError := !args[0].IsNull() && !args[0].IsUndefined()
|
|
message := getMessage(args, 1)
|
|
|
|
if !isError {
|
|
msg := "expected an error, got nil"
|
|
if message != "" {
|
|
msg = message
|
|
}
|
|
t.fail(msg, &ScriptAssertionInfo{
|
|
Type: "Error",
|
|
Actual: nil,
|
|
Message: message,
|
|
})
|
|
}
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// assertNoErrorMethod implements assert.NoError(err, message?)
|
|
func assertNoErrorMethod(iso *v8go.Isolate, t *TestingT) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
args := info.Args()
|
|
if len(args) < 1 {
|
|
t.fail("NoError requires an argument", &ScriptAssertionInfo{Type: "NoError"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
// Check if it's null/undefined (no error)
|
|
isError := !args[0].IsNull() && !args[0].IsUndefined()
|
|
message := getMessage(args, 1)
|
|
|
|
if isError {
|
|
msg := fmt.Sprintf("expected no error, got %v", args[0].String())
|
|
if message != "" {
|
|
msg = message
|
|
}
|
|
t.fail(msg, &ScriptAssertionInfo{
|
|
Type: "NoError",
|
|
Actual: args[0].String(),
|
|
Message: message,
|
|
})
|
|
}
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// assertPanicMethod implements assert.Panic(fn, message?)
|
|
func assertPanicMethod(iso *v8go.Isolate, t *TestingT) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
v8ctx := info.Context()
|
|
args := info.Args()
|
|
if len(args) < 1 {
|
|
t.fail("Panic requires a function argument", &ScriptAssertionInfo{Type: "Panic"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
if !args[0].IsFunction() {
|
|
t.fail("Panic requires a function argument", &ScriptAssertionInfo{Type: "Panic"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
message := getMessage(args, 1)
|
|
|
|
// Try to call the function and check if it throws
|
|
fn, _ := args[0].AsFunction()
|
|
_, err := fn.Call(v8ctx.Global())
|
|
|
|
if err == nil {
|
|
msg := "expected function to panic, but it didn't"
|
|
if message != "" {
|
|
msg = message
|
|
}
|
|
t.fail(msg, &ScriptAssertionInfo{
|
|
Type: "Panic",
|
|
Message: message,
|
|
})
|
|
}
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// assertNoPanicMethod implements assert.NoPanic(fn, message?)
|
|
func assertNoPanicMethod(iso *v8go.Isolate, t *TestingT) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
v8ctx := info.Context()
|
|
args := info.Args()
|
|
if len(args) < 1 {
|
|
t.fail("NoPanic requires a function argument", &ScriptAssertionInfo{Type: "NoPanic"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
if !args[0].IsFunction() {
|
|
t.fail("NoPanic requires a function argument", &ScriptAssertionInfo{Type: "NoPanic"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
message := getMessage(args, 1)
|
|
|
|
// Try to call the function and check if it throws
|
|
fn, _ := args[0].AsFunction()
|
|
_, err := fn.Call(v8ctx.Global())
|
|
|
|
if err != nil {
|
|
msg := fmt.Sprintf("expected function not to panic, but got: %v", err)
|
|
if message != "" {
|
|
msg = message
|
|
}
|
|
t.fail(msg, &ScriptAssertionInfo{
|
|
Type: "NoPanic",
|
|
Actual: err.Error(),
|
|
Message: message,
|
|
})
|
|
}
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// assertMatchMethod implements assert.Match(value, pattern, message?)
|
|
func assertMatchMethod(iso *v8go.Isolate, t *TestingT) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
args := info.Args()
|
|
if len(args) < 2 {
|
|
t.fail("Match requires value and pattern arguments", &ScriptAssertionInfo{Type: "Match"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
value := args[0].String()
|
|
pattern := args[1].String()
|
|
message := getMessage(args, 2)
|
|
|
|
re, err := regexp.Compile(pattern)
|
|
if err != nil {
|
|
t.fail(fmt.Sprintf("invalid regex pattern: %v", err), &ScriptAssertionInfo{
|
|
Type: "Match",
|
|
Message: message,
|
|
})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
if !re.MatchString(value) {
|
|
msg := fmt.Sprintf("expected '%s' to match pattern '%s'", value, pattern)
|
|
if message != "" {
|
|
msg = message
|
|
}
|
|
t.fail(msg, &ScriptAssertionInfo{
|
|
Type: "Match",
|
|
Expected: pattern,
|
|
Actual: value,
|
|
Message: message,
|
|
})
|
|
}
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// assertNotMatchMethod implements assert.NotMatch(value, pattern, message?)
|
|
func assertNotMatchMethod(iso *v8go.Isolate, t *TestingT) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
args := info.Args()
|
|
if len(args) < 2 {
|
|
t.fail("NotMatch requires value and pattern arguments", &ScriptAssertionInfo{Type: "NotMatch"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
value := args[0].String()
|
|
pattern := args[1].String()
|
|
message := getMessage(args, 2)
|
|
|
|
re, err := regexp.Compile(pattern)
|
|
if err != nil {
|
|
t.fail(fmt.Sprintf("invalid regex pattern: %v", err), &ScriptAssertionInfo{
|
|
Type: "NotMatch",
|
|
Message: message,
|
|
})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
if re.MatchString(value) {
|
|
msg := fmt.Sprintf("expected '%s' to not match pattern '%s'", value, pattern)
|
|
if message != "" {
|
|
msg = message
|
|
}
|
|
t.fail(msg, &ScriptAssertionInfo{
|
|
Type: "NotMatch",
|
|
Expected: pattern,
|
|
Actual: value,
|
|
Message: message,
|
|
})
|
|
}
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// assertTypeMethod implements assert.Type(value, typeName, message?)
|
|
func assertTypeMethod(iso *v8go.Isolate, t *TestingT) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
args := info.Args()
|
|
if len(args) < 2 {
|
|
t.fail("Type requires value and typeName arguments", &ScriptAssertionInfo{Type: "Type"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
value := args[0]
|
|
expectedType := args[1].String()
|
|
message := getMessage(args, 2)
|
|
|
|
actualType := getJsType(value)
|
|
|
|
if actualType != expectedType {
|
|
msg := fmt.Sprintf("expected type '%s', got '%s'", expectedType, actualType)
|
|
if message != "" {
|
|
msg = message
|
|
}
|
|
t.fail(msg, &ScriptAssertionInfo{
|
|
Type: "Type",
|
|
Expected: expectedType,
|
|
Actual: actualType,
|
|
Message: message,
|
|
})
|
|
}
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// assertJSONPathMethod implements assert.JSONPath(obj, path, expected, message?)
|
|
func assertJSONPathMethod(iso *v8go.Isolate, t *TestingT) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
v8ctx := info.Context()
|
|
args := info.Args()
|
|
if len(args) < 3 {
|
|
t.fail("JSONPath requires obj, path, and expected arguments", &ScriptAssertionInfo{Type: "JSONPath"})
|
|
return v8go.Undefined(iso)
|
|
}
|
|
|
|
obj, _ := bridge.GoValue(args[0], v8ctx)
|
|
path := args[1].String()
|
|
expected, _ := bridge.GoValue(args[2], v8ctx)
|
|
message := getMessage(args, 3)
|
|
|
|
// Use the existing extractPath function from assert.go
|
|
asserter := &Asserter{}
|
|
actual := asserter.extractPath(obj, strings.TrimPrefix(path, "$."))
|
|
|
|
if !deepEqual(actual, expected) {
|
|
msg := fmt.Sprintf("path '%s': expected %v, got %v", path, expected, actual)
|
|
if message != "" {
|
|
msg = message
|
|
}
|
|
t.fail(msg, &ScriptAssertionInfo{
|
|
Type: "JSONPath",
|
|
Expected: expected,
|
|
Actual: actual,
|
|
Message: message,
|
|
})
|
|
}
|
|
return v8go.Undefined(iso)
|
|
})
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
// deepEqual performs deep equality comparison
|
|
func deepEqual(a, b interface{}) bool {
|
|
// Handle nil cases
|
|
if a == nil && b == nil {
|
|
return true
|
|
}
|
|
if a == nil || b == nil {
|
|
return false
|
|
}
|
|
|
|
// Try JSON comparison for complex types
|
|
aJSON, errA := json.Marshal(a)
|
|
bJSON, errB := json.Marshal(b)
|
|
if errA == nil && errB == nil {
|
|
return string(aJSON) == string(bJSON)
|
|
}
|
|
|
|
// Fall back to reflect.DeepEqual
|
|
return reflect.DeepEqual(a, b)
|
|
}
|
|
|
|
// getLength returns the length of a value (array, string, map)
|
|
func getLength(v interface{}) int {
|
|
if v == nil {
|
|
return 0
|
|
}
|
|
|
|
switch val := v.(type) {
|
|
case string:
|
|
return len(val)
|
|
case []interface{}:
|
|
return len(val)
|
|
case map[string]interface{}:
|
|
return len(val)
|
|
default:
|
|
rv := reflect.ValueOf(v)
|
|
switch rv.Kind() {
|
|
case reflect.Slice, reflect.Array, reflect.Map, reflect.String:
|
|
return rv.Len()
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// getJsType returns the JavaScript type name of a value
|
|
func getJsType(v *v8go.Value) string {
|
|
if v.IsNull() {
|
|
return "null"
|
|
}
|
|
if v.IsUndefined() {
|
|
return "undefined"
|
|
}
|
|
if v.IsString() {
|
|
return "string"
|
|
}
|
|
if v.IsNumber() {
|
|
return "number"
|
|
}
|
|
if v.IsBoolean() {
|
|
return "boolean"
|
|
}
|
|
if v.IsArray() {
|
|
return "array"
|
|
}
|
|
if v.IsFunction() {
|
|
return "function"
|
|
}
|
|
if v.IsObject() {
|
|
return "object"
|
|
}
|
|
return "unknown"
|
|
}
|