Files
five/hbrt/gobridge_stress_test.go
Charles KWON OhJun 59568f3301 Five v0.9 — Harbour + Go fusion language
- 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>
2026-03-31 09:41:50 +09:00

478 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}