Files
five/hbrdd/dbf/rdd_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

408 lines
9.1 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// RDD Stress Test — comprehensive test of DBF + NTX index integration.
package dbf
import (
"five/hbrt"
"five/hbrdd"
"fmt"
"os"
"path/filepath"
"testing"
)
func createTestDB(t *testing.T, dir string, name string, count int) *DBFArea {
t.Helper()
path := filepath.Join(dir, name)
drv, _ := hbrdd.GetDriver("DBFNTX")
area, err := drv.Create(hbrdd.CreateParams{
Path: path,
Fields: []hbrdd.FieldInfo{
{Name: "ID", Type: 'N', Len: 6, Dec: 0},
{Name: "NAME", Type: 'C', Len: 20, Dec: 0},
{Name: "CITY", Type: 'C', Len: 15, Dec: 0},
{Name: "AGE", Type: 'N', Len: 3, Dec: 0},
{Name: "SALARY", Type: 'N', Len: 10, Dec: 2},
{Name: "ACTIVE", Type: 'L', Len: 1, Dec: 0},
},
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
a := area.(*DBFArea)
cities := []string{"Seoul", "Tokyo", "NewYork", "London", "Paris",
"Berlin", "Sydney", "Toronto", "Mumbai", "Beijing"}
names := []string{"Kim", "Lee", "Park", "Choi", "Jung",
"Kang", "Cho", "Yoon", "Jang", "Lim"}
for i := 0; i < count; i++ {
a.Append()
a.PutValue(0, hbrt.MakeInt(i+1)) // ID (0-based)
a.PutValue(1, hbrt.MakeString(names[i%len(names)])) // NAME
a.PutValue(2, hbrt.MakeString(cities[i%len(cities)])) // CITY
a.PutValue(3, hbrt.MakeInt(20+i%50)) // AGE
a.PutValue(4, hbrt.MakeDouble(float64(30000+i*100), 10, 2)) // SALARY
a.PutValue(5, hbrt.MakeBool(i%3 != 0)) // ACTIVE
}
a.Flush()
return a
}
// === Basic DBF Tests ===
func TestStress_CreateAndCount(t *testing.T) {
dir := t.TempDir()
a := createTestDB(t, dir, "test1", 500)
defer a.Close()
rc, _ := a.RecCount()
if rc != 500 {
t.Errorf("RecCount = %d, want 500", rc)
}
}
func TestStress_NavigateAll(t *testing.T) {
dir := t.TempDir()
a := createTestDB(t, dir, "test2", 1000)
defer a.Close()
// GoTop → Skip through all
a.GoTop()
count := 0
for !a.EOF() {
count++
a.Skip(1)
}
if count != 1000 {
t.Errorf("Forward skip count = %d, want 1000", count)
}
// GoBottom → Skip backward
a.GoBottom()
count = 0
for !a.BOF() {
count++
a.Skip(-1)
}
if count < 999 {
t.Errorf("Backward skip count = %d, want >= 999", count)
}
}
func TestStress_ReadAllFields(t *testing.T) {
dir := t.TempDir()
a := createTestDB(t, dir, "test3", 100)
defer a.Close()
a.GoTo(1)
id, _ := a.GetValue(0)
name, _ := a.GetValue(1)
city, _ := a.GetValue(2)
t.Logf("Record 1: ID=%v NAME=%q CITY=%q", id, name.AsString(), city.AsString())
if id.AsNumInt() != 1 {
t.Errorf("ID = %v (AsNumInt=%d), want 1", id, id.AsNumInt())
}
nameStr := name.AsString()
if len(nameStr) >= 3 && nameStr[:3] != "Kim" {
t.Errorf("NAME = %q, want 'Kim...'", nameStr)
}
cityStr := city.AsString()
if len(cityStr) >= 5 && cityStr[:5] != "Seoul" {
t.Errorf("CITY = %q, want 'Seoul...'", cityStr)
}
// Check last record
a.GoTo(100)
id, _ = a.GetValue(0)
if id.AsNumInt() != 100 {
t.Errorf("Last ID = %v, want 100", id)
}
}
func TestStress_UpdateRecords(t *testing.T) {
dir := t.TempDir()
a := createTestDB(t, dir, "test4", 50)
defer a.Close()
// Update every record
for i := uint32(1); i <= 50; i++ {
a.GoTo(i)
a.PutValue(1, hbrt.MakeString(fmt.Sprintf("Updated_%d", i)))
}
a.Flush()
// Verify
a.GoTo(25)
name, _ := a.GetValue(1)
if name.AsString()[:10] != "Updated_25" {
t.Errorf("Updated NAME = %q", name.AsString())
}
}
func TestStress_DeleteAndPack(t *testing.T) {
dir := t.TempDir()
a := createTestDB(t, dir, "test5", 100)
defer a.Close()
// Delete even records
for i := uint32(2); i <= 100; i += 2 {
a.GoTo(i)
a.Delete()
}
a.Flush()
// Count deleted
deleted := 0
a.GoTop()
for !a.EOF() {
if a.Deleted() {
deleted++
}
a.Skip(1)
}
if deleted != 50 {
t.Errorf("Deleted = %d, want 50", deleted)
}
// Pack
a.Pack()
rc, _ := a.RecCount()
if rc != 50 {
t.Errorf("After Pack, RecCount = %d, want 50", rc)
}
// Verify all remaining are odd IDs
a.GoTo(1)
id, _ := a.GetValue(0)
if id.AsInt() != 1 {
t.Errorf("First after pack: ID = %d, want 1", id.AsInt())
}
}
func TestStress_Zap(t *testing.T) {
dir := t.TempDir()
a := createTestDB(t, dir, "test6", 200)
defer a.Close()
rc, _ := a.RecCount()
if rc != 200 {
t.Fatalf("Before zap: %d", rc)
}
a.Zap()
rc, _ = a.RecCount()
if rc != 0 {
t.Errorf("After zap: %d, want 0", rc)
}
}
// === NTX Index Tests ===
func TestStress_IndexCreate(t *testing.T) {
dir := t.TempDir()
a := createTestDB(t, dir, "idx1", 100)
defer a.Close()
// Create index on NAME field
err := a.OrderCreate(hbrdd.OrderCreateParams{
KeyExpr: "NAME",
FilePath: filepath.Join(dir, "idx1_name.ntx"),
})
if err != nil {
t.Fatalf("OrderCreate failed: %v", err)
}
// Verify index file exists
if _, err := os.Stat(filepath.Join(dir, "idx1_name.ntx")); err != nil {
t.Fatal("NTX file not created")
}
}
func TestStress_IndexSeek(t *testing.T) {
dir := t.TempDir()
a := createTestDB(t, dir, "idx2", 100)
defer a.Close()
err := a.OrderCreate(hbrdd.OrderCreateParams{
KeyExpr: "NAME",
FilePath: filepath.Join(dir, "idx2_name.ntx"),
})
if err != nil {
t.Fatalf("OrderCreate: %v", err)
}
// Seek for "Park" — padded to match key width (20 chars for NAME field)
padded := fmt.Sprintf("%-20s", "Park")
found, err := a.Seek(hbrt.MakeString(padded), false, false)
if err != nil {
t.Fatalf("Seek error: %v", err)
}
t.Logf("Seek result: found=%v, EOF=%v, RecNo=%d", found, a.EOF(), a.RecNo())
if !found {
// Try soft seek
found2, _ := a.Seek(hbrt.MakeString(padded), true, false)
t.Logf("Soft seek: found=%v, EOF=%v, RecNo=%d", found2, a.EOF(), a.RecNo())
t.Error("Seek 'Park' should find a record")
}
if !a.EOF() {
name, _ := a.GetValue(1)
t.Logf("Found record NAME=%q", name.AsString())
if len(name.AsString()) >= 4 && name.AsString()[:4] != "Park" {
t.Errorf("Seek result NAME = %q, want 'Park...'", name.AsString())
}
}
}
func TestStress_IndexOrder(t *testing.T) {
dir := t.TempDir()
a := createTestDB(t, dir, "idx3", 50)
defer a.Close()
err := a.OrderCreate(hbrdd.OrderCreateParams{
KeyExpr: "NAME",
FilePath: filepath.Join(dir, "idx3_name.ntx"),
})
if err != nil {
t.Fatalf("OrderCreate: %v", err)
}
// GoTop in index order — should give alphabetically first name
a.GoTop()
name, _ := a.GetValue(1)
firstName := name.AsString()
// Skip through and verify sorted order, track unique records
prev := firstName
count := 0
seen := make(map[uint32]bool)
for !a.EOF() {
recno := a.RecNo()
if seen[recno] {
t.Logf("Duplicate recNo %d at count %d", recno, count)
}
seen[recno] = true
n, _ := a.GetValue(1)
cur := n.AsString()
if cur < prev {
t.Errorf("Index not sorted: %q < %q at count %d", cur, prev, count)
break
}
prev = cur
count++
a.Skip(1)
}
unique := len(seen)
t.Logf("Index traversal: %d iterations, %d unique records (expected 50)", count, unique)
if unique != 50 {
t.Errorf("Unique records = %d, want 50", unique)
}
}
func TestStress_IndexGoTopBottom(t *testing.T) {
dir := t.TempDir()
a := createTestDB(t, dir, "idx4", 100)
defer a.Close()
a.OrderCreate(hbrdd.OrderCreateParams{
KeyExpr: "NAME",
FilePath: filepath.Join(dir, "idx4_name.ntx"),
})
a.GoTop()
topName, _ := a.GetValue(1)
a.GoBottom()
botName, _ := a.GetValue(1)
if topName.AsString() > botName.AsString() {
t.Errorf("GoTop name %q > GoBottom name %q", topName.AsString(), botName.AsString())
}
}
func TestStress_IndexSwitch(t *testing.T) {
dir := t.TempDir()
a := createTestDB(t, dir, "idx5", 50)
defer a.Close()
// Create two indexes
a.OrderCreate(hbrdd.OrderCreateParams{
KeyExpr: "NAME",
FilePath: filepath.Join(dir, "idx5_name.ntx"),
TagName: "NAME",
})
a.OrderCreate(hbrdd.OrderCreateParams{
KeyExpr: "CITY",
FilePath: filepath.Join(dir, "idx5_city.ntx"),
TagName: "CITY",
})
// Switch to NAME index
a.OrderListFocus("NAME")
a.GoTop()
nameFirst, _ := a.GetValue(1)
// Switch to CITY index
a.OrderListFocus("CITY")
a.GoTop()
cityFirst, _ := a.GetValue(2)
// Switch to natural order
a.OrderListFocus("")
a.GoTop()
natID, _ := a.GetValue(0)
t.Logf("NAME order first: %s", nameFirst.AsString())
t.Logf("CITY order first: %s", cityFirst.AsString())
t.Logf("Natural first ID: %d", natID.AsInt())
if natID.AsInt() != 1 {
t.Errorf("Natural order first ID = %d, want 1", natID.AsInt())
}
}
func TestStress_LargeIndex(t *testing.T) {
dir := t.TempDir()
a := createTestDB(t, dir, "large", 5000)
defer a.Close()
err := a.OrderCreate(hbrdd.OrderCreateParams{
KeyExpr: "NAME",
FilePath: filepath.Join(dir, "large_name.ntx"),
})
if err != nil {
t.Fatalf("Create large index: %v", err)
}
// Seek in large index
padded := "Park "
found, _ := a.Seek(hbrt.MakeString(padded), false, false)
if !found {
t.Error("Seek in 5000-record index should find 'Park'")
}
// Full traversal — count unique records
a.GoTop()
seen := make(map[uint32]bool)
for !a.EOF() {
seen[a.RecNo()] = true
a.Skip(1)
}
t.Logf("Large index: %d unique records traversed", len(seen))
if len(seen) != 5000 {
t.Errorf("Large index unique = %d, want 5000", len(seen))
}
}