- Compiler: PP → Lexer → Parser → Analyzer → Gengo pipeline - Parser: 232/236 (98%) Harbour compatibility, registry-based dispatch - RTL: 351 Harbour-compatible functions - RDD: DBF/NTX/CDX engines with Rushmore bitmap optimization - Go Interop: IMPORT + pkg.Func() + obj:Method() with FastPath (15M calls/sec) - HB_FUNC API: Full Harbour C API compatible Go bridge - Concurrency: SPAWN/LAUNCH/GOROUTINE, <-, WATCH, PARALLEL FOR, ASYNC/AWAIT - Extensions: Multi-return, DEFER, Slice, f-string, Nil-safe ?:, CONST - Macro Compiler: Runtime AST parsing and evaluation - Debugger: TUI debugger with source display, breakpoints, stepping - FRB: Native + Pcode dual mode runtime binary - Tests: 13 packages ALL PASS Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
478 lines
13 KiB
Go
478 lines
13 KiB
Go
package hbrt
|
||
|
||
import (
|
||
"fmt"
|
||
"math"
|
||
"math/rand"
|
||
"reflect"
|
||
"strings"
|
||
"sync"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// ===== Stress test Go functions =====
|
||
|
||
func goSumInts(a, b, c, d, e int) int { return a + b + c + d + e }
|
||
func goConcatMany(a, b, c, d, e, f string) string { return a + b + c + d + e + f }
|
||
func goReturnLargeSlice(n int) []int {
|
||
s := make([]int, n)
|
||
for i := range s { s[i] = i }
|
||
return s
|
||
}
|
||
func goReturnLargeMap(n int) map[string]int {
|
||
m := make(map[string]int, n)
|
||
for i := 0; i < n; i++ { m[fmt.Sprintf("key_%d", i)] = i }
|
||
return m
|
||
}
|
||
func goNestedSlice() [][]int {
|
||
return [][]int{{1, 2}, {3, 4}, {5, 6}}
|
||
}
|
||
func goReturnNil() *testStruct { return nil }
|
||
func goAcceptNil(s *testStruct) string {
|
||
if s == nil { return "nil" }
|
||
return s.Name
|
||
}
|
||
|
||
type chainObj struct{ Val int }
|
||
func (c *chainObj) Add(n int) *chainObj { return &chainObj{Val: c.Val + n} }
|
||
func (c *chainObj) Mul(n int) *chainObj { return &chainObj{Val: c.Val * n} }
|
||
func (c *chainObj) Result() int { return c.Val }
|
||
func goNewChain(n int) *chainObj { return &chainObj{Val: n} }
|
||
|
||
// ===================================================================
|
||
// 1. VOLUME: Thousands of calls
|
||
// ===================================================================
|
||
|
||
func TestStress_HighVolume_StringCalls(t *testing.T) {
|
||
for i := 0; i < 10000; i++ {
|
||
v := MakeString(fmt.Sprintf("hello_%d", i))
|
||
results := GoCallFunc(strings.ToUpper, v)
|
||
expected := fmt.Sprintf("HELLO_%d", i)
|
||
if results[0].AsString() != expected {
|
||
t.Fatalf("iter %d: got %q want %q", i, results[0].AsString(), expected)
|
||
}
|
||
}
|
||
t.Log("10,000 string calls OK")
|
||
}
|
||
|
||
func TestStress_HighVolume_IntCalls(t *testing.T) {
|
||
for i := 0; i < 10000; i++ {
|
||
results := GoCallFunc(goSumInts,
|
||
MakeInt(i), MakeInt(i*2), MakeInt(i*3), MakeInt(i*4), MakeInt(i*5))
|
||
expected := i * 15 // i+2i+3i+4i+5i
|
||
if results[0].AsInt() != expected {
|
||
t.Fatalf("iter %d: got %d want %d", i, results[0].AsInt(), expected)
|
||
}
|
||
}
|
||
t.Log("10,000 int calls OK")
|
||
}
|
||
|
||
func TestStress_HighVolume_FloatCalls(t *testing.T) {
|
||
for i := 0; i < 10000; i++ {
|
||
v := MakeDouble(float64(i)*0.1, 0, 0)
|
||
results := GoCallFunc(math.Sqrt, v)
|
||
expected := math.Sqrt(float64(i) * 0.1)
|
||
diff := results[0].AsDouble() - expected
|
||
if diff > 0.0001 || diff < -0.0001 {
|
||
t.Fatalf("iter %d: got %f want %f", i, results[0].AsDouble(), expected)
|
||
}
|
||
}
|
||
t.Log("10,000 float calls OK")
|
||
}
|
||
|
||
func TestStress_HighVolume_BoolCalls(t *testing.T) {
|
||
for i := 0; i < 10000; i++ {
|
||
s := fmt.Sprintf("item_%d", i)
|
||
search := fmt.Sprintf("_%d", i)
|
||
results := GoCallFunc(strings.Contains, MakeString(s), MakeString(search))
|
||
if !results[0].AsBool() {
|
||
t.Fatalf("iter %d: expected true", i)
|
||
}
|
||
}
|
||
t.Log("10,000 bool calls OK")
|
||
}
|
||
|
||
// ===================================================================
|
||
// 2. LARGE DATA: big arrays, maps, strings
|
||
// ===================================================================
|
||
|
||
func TestStress_LargeString(t *testing.T) {
|
||
// 1MB string
|
||
big := strings.Repeat("abcdefghij", 100000)
|
||
v := MakeString(big)
|
||
results := GoCallFunc(strings.ToUpper, v)
|
||
got := results[0].AsString()
|
||
if len(got) != 1000000 {
|
||
t.Fatalf("large string: len=%d want 1000000", len(got))
|
||
}
|
||
if got[:10] != "ABCDEFGHIJ" {
|
||
t.Fatalf("large string: prefix=%q", got[:10])
|
||
}
|
||
t.Logf("1MB string roundtrip OK (len=%d)", len(got))
|
||
}
|
||
|
||
func TestStress_LargeArray(t *testing.T) {
|
||
// 10,000 element array Go→PRG
|
||
results := GoCallFunc(goReturnLargeSlice, MakeInt(10000))
|
||
if !results[0].IsArray() {
|
||
t.Fatalf("large array: not array")
|
||
}
|
||
arr := results[0].AsArray()
|
||
if len(arr.Items) != 10000 {
|
||
t.Fatalf("large array: len=%d want 10000", len(arr.Items))
|
||
}
|
||
if arr.Items[0].AsInt() != 0 || arr.Items[9999].AsInt() != 9999 {
|
||
t.Fatalf("large array: first=%d last=%d", arr.Items[0].AsInt(), arr.Items[9999].AsInt())
|
||
}
|
||
|
||
// PRG→Go roundtrip: send array back to Go strings.Join
|
||
strItems := make([]Value, 100)
|
||
for i := range strItems {
|
||
strItems[i] = MakeString(fmt.Sprintf("%d", i))
|
||
}
|
||
arrVal := MakeArrayFrom(strItems)
|
||
joinResults := GoCallFunc(strings.Join, arrVal, MakeString(","))
|
||
joined := joinResults[0].AsString()
|
||
parts := strings.Split(joined, ",")
|
||
if len(parts) != 100 {
|
||
t.Fatalf("array roundtrip: len=%d", len(parts))
|
||
}
|
||
t.Log("10,000 element array + 100 element roundtrip OK")
|
||
}
|
||
|
||
func TestStress_LargeMap(t *testing.T) {
|
||
results := GoCallFunc(goReturnLargeMap, MakeInt(1000))
|
||
if !results[0].IsHash() {
|
||
t.Fatalf("large map: not hash")
|
||
}
|
||
h := results[0].AsHash()
|
||
if len(h.Keys) != 1000 {
|
||
t.Fatalf("large map: len=%d want 1000", len(h.Keys))
|
||
}
|
||
t.Logf("1,000 entry map OK")
|
||
}
|
||
|
||
// ===================================================================
|
||
// 3. TYPE BOUNDARY: edge values
|
||
// ===================================================================
|
||
|
||
func TestStress_IntBoundary(t *testing.T) {
|
||
cases := []int{0, 1, -1, 127, -128, 255, 32767, -32768, 65535,
|
||
2147483647, -2147483648, 100000000}
|
||
for _, n := range cases {
|
||
results := GoCallFunc(goIdentityInt, MakeInt(n))
|
||
if results[0].AsInt() != n {
|
||
t.Errorf("boundary int %d: got %d", n, results[0].AsInt())
|
||
}
|
||
}
|
||
t.Log("Int boundary values OK")
|
||
}
|
||
|
||
func TestStress_Int64Boundary(t *testing.T) {
|
||
cases := []int64{0, 1, -1, math.MaxInt32, math.MinInt32,
|
||
math.MaxInt32 + 1, math.MinInt32 - 1,
|
||
999999999999, -999999999999}
|
||
for _, n := range cases {
|
||
results := GoCallFunc(goIdentityInt64, MakeLong(n))
|
||
if results[0].AsLong() != n {
|
||
t.Errorf("boundary int64 %d: got %d", n, results[0].AsLong())
|
||
}
|
||
}
|
||
t.Log("Int64 boundary values OK")
|
||
}
|
||
|
||
func TestStress_FloatBoundary(t *testing.T) {
|
||
cases := []float64{0, 0.1, -0.1, math.SmallestNonzeroFloat64,
|
||
math.MaxFloat64 / 2, -math.MaxFloat64 / 2,
|
||
math.Pi, math.E, math.Phi}
|
||
for _, f := range cases {
|
||
results := GoCallFunc(goIdentityFloat64, MakeDouble(f, 0, 0))
|
||
diff := math.Abs(results[0].AsDouble() - f)
|
||
if diff > 1e-10 {
|
||
t.Errorf("boundary float %g: got %g", f, results[0].AsDouble())
|
||
}
|
||
}
|
||
t.Log("Float boundary values OK")
|
||
}
|
||
|
||
func TestStress_StringBoundary(t *testing.T) {
|
||
cases := []string{
|
||
"", // empty
|
||
" ", // single space
|
||
"\t\n\r", // whitespace
|
||
"a", // single char
|
||
strings.Repeat("x", 65536), // 64KB
|
||
"Hello 世界 🌍", // unicode
|
||
"line1\nline2\nline3", // newlines
|
||
`"quoted"`, // quotes
|
||
"null\x00byte", // null byte
|
||
}
|
||
for i, s := range cases {
|
||
results := GoCallFunc(goIdentityString, MakeString(s))
|
||
if results[0].AsString() != s {
|
||
t.Errorf("boundary string[%d] len=%d: mismatch", i, len(s))
|
||
}
|
||
}
|
||
t.Log("String boundary values OK (empty, unicode, 64KB, null bytes)")
|
||
}
|
||
|
||
// ===================================================================
|
||
// 4. CONCURRENT: goroutine safety
|
||
// ===================================================================
|
||
|
||
func TestStress_ConcurrentCalls(t *testing.T) {
|
||
var wg sync.WaitGroup
|
||
errors := make(chan string, 100)
|
||
|
||
for g := 0; g < 50; g++ {
|
||
wg.Add(1)
|
||
go func(id int) {
|
||
defer wg.Done()
|
||
for i := 0; i < 200; i++ {
|
||
s := fmt.Sprintf("goroutine_%d_iter_%d", id, i)
|
||
results := GoCallFunc(strings.ToUpper, MakeString(s))
|
||
expected := strings.ToUpper(s)
|
||
if results[0].AsString() != expected {
|
||
errors <- fmt.Sprintf("g%d i%d: %q != %q", id, i, results[0].AsString(), expected)
|
||
return
|
||
}
|
||
}
|
||
}(g)
|
||
}
|
||
|
||
wg.Wait()
|
||
close(errors)
|
||
for e := range errors {
|
||
t.Fatal(e)
|
||
}
|
||
t.Log("50 goroutines × 200 calls = 10,000 concurrent calls OK")
|
||
}
|
||
|
||
func TestStress_ConcurrentObjectMethods(t *testing.T) {
|
||
var wg sync.WaitGroup
|
||
errors := make(chan string, 100)
|
||
|
||
for g := 0; g < 20; g++ {
|
||
wg.Add(1)
|
||
go func(id int) {
|
||
defer wg.Done()
|
||
obj := &testStruct{Name: fmt.Sprintf("obj_%d", id), Value: id}
|
||
v := WrapGo(obj)
|
||
for i := 0; i < 500; i++ {
|
||
results := GoCall(v, "Add", MakeInt(i))
|
||
expected := id + i
|
||
if results[0].AsInt() != expected {
|
||
errors <- fmt.Sprintf("g%d i%d: %d != %d", id, i, results[0].AsInt(), expected)
|
||
return
|
||
}
|
||
}
|
||
}(g)
|
||
}
|
||
|
||
wg.Wait()
|
||
close(errors)
|
||
for e := range errors {
|
||
t.Fatal(e)
|
||
}
|
||
t.Log("20 goroutines × 500 method calls = 10,000 concurrent object calls OK")
|
||
}
|
||
|
||
// ===================================================================
|
||
// 5. OBJECT LIFECYCLE: create, use, chain, stress
|
||
// ===================================================================
|
||
|
||
func TestStress_ObjectChain(t *testing.T) {
|
||
// Chain: obj.Add(5).Mul(3).Add(10).Result() = (0+5)*3+10 = 25
|
||
obj := WrapGo(goNewChain(0))
|
||
|
||
r := GoCall(obj, "Add", MakeInt(5))
|
||
obj = r[0]
|
||
r = GoCall(obj, "Mul", MakeInt(3))
|
||
obj = r[0]
|
||
r = GoCall(obj, "Add", MakeInt(10))
|
||
obj = r[0]
|
||
r = GoCall(obj, "Result")
|
||
|
||
if r[0].AsInt() != 25 {
|
||
t.Fatalf("chain: got %d want 25", r[0].AsInt())
|
||
}
|
||
t.Log("Object method chain OK: (0+5)*3+10 = 25")
|
||
}
|
||
|
||
func TestStress_ManyObjects(t *testing.T) {
|
||
// Create 1000 objects, call methods on each
|
||
objects := make([]Value, 1000)
|
||
for i := 0; i < 1000; i++ {
|
||
objects[i] = WrapGo(&testStruct{Name: fmt.Sprintf("obj_%d", i), Value: i})
|
||
}
|
||
|
||
for i, obj := range objects {
|
||
r := GoCall(obj, "GetValue")
|
||
if r[0].AsInt() != i {
|
||
t.Fatalf("object %d: GetValue=%d", i, r[0].AsInt())
|
||
}
|
||
r = GoCall(obj, "GetName")
|
||
expected := fmt.Sprintf("obj_%d", i)
|
||
if r[0].AsString() != expected {
|
||
t.Fatalf("object %d: GetName=%q", i, r[0].AsString())
|
||
}
|
||
}
|
||
t.Log("1,000 objects created and verified OK")
|
||
}
|
||
|
||
func TestStress_ObjectNilSafety(t *testing.T) {
|
||
// Call method on nil-wrapped object
|
||
results := GoCallFunc(goReturnNil)
|
||
nilObj := results[0]
|
||
if !nilObj.IsNil() {
|
||
t.Fatalf("expected NIL from nil pointer")
|
||
}
|
||
|
||
// GoCall on NIL should not panic
|
||
r := GoCall(nilObj, "GetName")
|
||
if len(r) < 2 {
|
||
t.Fatalf("expected error result")
|
||
}
|
||
t.Log("Nil object safety OK")
|
||
}
|
||
|
||
// ===================================================================
|
||
// 6. TYPE COERCION MATRIX: every PRG→Go combination
|
||
// ===================================================================
|
||
|
||
func goTakeString(s string) string { return s }
|
||
func goTakeInt(n int) int { return n }
|
||
func goTakeInt64(n int64) int64 { return n }
|
||
func goTakeFloat64(f float64) float64 { return f }
|
||
func goTakeBool(b bool) bool { return b }
|
||
func goTakeInterface(v interface{}) string { return fmt.Sprintf("%v", v) }
|
||
|
||
func TestStress_CoercionMatrix(t *testing.T) {
|
||
// Test: every Harbour type sent to every Go type
|
||
values := []struct {
|
||
name string
|
||
v Value
|
||
}{
|
||
{"NIL", MakeNil()},
|
||
{"String", MakeString("hello")},
|
||
{"Int", MakeInt(42)},
|
||
{"Long", MakeLong(9999999999)},
|
||
{"Double", MakeDouble(3.14, 0, 0)},
|
||
{"Bool.T", MakeBool(true)},
|
||
{"Bool.F", MakeBool(false)},
|
||
}
|
||
|
||
targets := []struct {
|
||
name string
|
||
fn interface{}
|
||
}{
|
||
{"→string", goTakeString},
|
||
{"→int", goTakeInt},
|
||
{"→int64", goTakeInt64},
|
||
{"→float64", goTakeFloat64},
|
||
{"→bool", goTakeBool},
|
||
{"→interface{}", goTakeInterface},
|
||
}
|
||
|
||
passed := 0
|
||
for _, v := range values {
|
||
for _, target := range targets {
|
||
func() {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
// Some coercions may panic — that's OK, we just track them
|
||
t.Logf(" %s %s: panic (expected for incompatible)", v.name, target.name)
|
||
}
|
||
}()
|
||
results := GoCallFunc(target.fn, v.v)
|
||
if len(results) > 0 {
|
||
passed++
|
||
}
|
||
}()
|
||
}
|
||
}
|
||
t.Logf("Coercion matrix: %d/42 combinations succeeded", passed)
|
||
}
|
||
|
||
// ===================================================================
|
||
// 7. PERFORMANCE BENCHMARK
|
||
// ===================================================================
|
||
|
||
func TestStress_Performance(t *testing.T) {
|
||
n := 100000
|
||
|
||
// Benchmark: direct Go call
|
||
start := time.Now()
|
||
for i := 0; i < n; i++ {
|
||
_ = strings.ToUpper("hello")
|
||
}
|
||
directTime := time.Since(start)
|
||
|
||
// Benchmark: via GoCallFunc bridge
|
||
start = time.Now()
|
||
v := MakeString("hello")
|
||
for i := 0; i < n; i++ {
|
||
GoCallFunc(strings.ToUpper, v)
|
||
}
|
||
bridgeTime := time.Since(start)
|
||
|
||
ratio := float64(bridgeTime) / float64(directTime)
|
||
t.Logf("Performance: direct=%v bridge=%v ratio=%.1fx",
|
||
directTime, bridgeTime, ratio)
|
||
t.Logf("Bridge throughput: %.0f calls/sec",
|
||
float64(n)/bridgeTime.Seconds())
|
||
|
||
// Benchmark: object method call
|
||
obj := WrapGo(&testStruct{Name: "test", Value: 42})
|
||
arg := MakeInt(1)
|
||
start = time.Now()
|
||
for i := 0; i < n; i++ {
|
||
GoCall(obj, "Add", arg)
|
||
}
|
||
methodTime := time.Since(start)
|
||
t.Logf("Method call: %v (%.0f calls/sec)",
|
||
methodTime, float64(n)/methodTime.Seconds())
|
||
}
|
||
|
||
// ===================================================================
|
||
// 8. RANDOM FUZZ: random types and values
|
||
// ===================================================================
|
||
|
||
func TestStress_RandomFuzz(t *testing.T) {
|
||
rng := rand.New(rand.NewSource(42))
|
||
funcs := []interface{}{
|
||
strings.ToUpper,
|
||
strings.ToLower,
|
||
strings.TrimSpace,
|
||
}
|
||
|
||
for i := 0; i < 5000; i++ {
|
||
s := randomString(rng, rng.Intn(100))
|
||
fn := funcs[rng.Intn(len(funcs))]
|
||
v := MakeString(s)
|
||
|
||
results := GoCallFunc(fn, v)
|
||
if len(results) == 0 {
|
||
t.Fatalf("fuzz iter %d: no results", i)
|
||
}
|
||
|
||
// Verify against direct call
|
||
expected := reflect.ValueOf(fn).Call([]reflect.Value{reflect.ValueOf(s)})
|
||
if results[0].AsString() != expected[0].String() {
|
||
t.Fatalf("fuzz iter %d: %q → got %q want %q",
|
||
i, s, results[0].AsString(), expected[0].String())
|
||
}
|
||
}
|
||
t.Log("5,000 random fuzz calls OK")
|
||
}
|
||
|
||
func randomString(rng *rand.Rand, length int) string {
|
||
chars := "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ\t\n"
|
||
b := make([]byte, length)
|
||
for i := range b {
|
||
b[i] = chars[rng.Intn(len(chars))]
|
||
}
|
||
return string(b)
|
||
}
|