- 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>
408 lines
9.1 KiB
Go
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))
|
|
}
|
|
}
|