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

488 lines
12 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// Integration tests: create DBF with various field types,
// insert 100 records, verify read-back, test memo fields.
package dbf
import (
"five/hbrt"
"five/hbrdd"
"fmt"
"math"
"os"
"path/filepath"
"testing"
)
// TestDBFNTX_Create100 creates a DBFNTX-style DBF with 100 records.
func TestDBFNTX_Create100(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "ntx_test")
drv := &DBFDriver{}
area, err := drv.Create(hbrdd.CreateParams{
Path: path,
Fields: []hbrdd.FieldInfo{
{Name: "ID", Type: 'N', Len: 10, Dec: 0},
{Name: "NAME", Type: 'C', Len: 30},
{Name: "SALARY", Type: 'N', Len: 12, Dec: 2},
{Name: "ACTIVE", Type: 'L', Len: 1},
{Name: "HIREDATE", Type: 'D', Len: 8},
},
})
if err != nil {
t.Fatal(err)
}
// Verify DBF version byte (NTX style = standard DBF3)
dbfArea := area.(*DBFArea)
if dbfArea.header.Version != VersionDBF3 {
t.Errorf("DBFNTX version = 0x%02X, want 0x%02X", dbfArea.header.Version, VersionDBF3)
}
// Insert 100 records
names := []string{"Kim", "Lee", "Park", "Choi", "Jung", "Kang", "Cho", "Yoon", "Jang", "Lim"}
for i := 1; i <= 100; i++ {
area.Append()
area.PutValue(0, hbrt.MakeInt(i))
area.PutValue(1, hbrt.MakeString(fmt.Sprintf("%s_%03d", names[i%10], i)))
area.PutValue(2, hbrt.MakeDouble(30000.0+float64(i)*500.50, 12, 2))
area.PutValue(3, hbrt.MakeBool(i%3 != 0))
area.PutValue(4, hbrt.MakeDate(dateToJulian(2020, (i%12)+1, (i%28)+1)))
area.Flush()
}
area.Close()
// Reopen and verify
area2, err := drv.Open(hbrdd.OpenParams{Path: path})
if err != nil {
t.Fatal(err)
}
defer area2.Close()
rc, _ := area2.RecCount()
if rc != 100 {
t.Fatalf("reccount = %d, want 100", rc)
}
// Verify field count and types
if area2.FieldCount() != 5 {
t.Fatalf("fieldcount = %d, want 5", area2.FieldCount())
}
// Verify record 1
area2.GoTo(1)
id, _ := area2.GetValue(0)
name, _ := area2.GetValue(1)
salary, _ := area2.GetValue(2)
active, _ := area2.GetValue(3)
hdate, _ := area2.GetValue(4)
if id.AsNumInt() != 1 {
t.Errorf("rec1 ID = %d, want 1", id.AsNumInt())
}
if name.AsString()[:7] != "Lee_001" {
t.Errorf("rec1 NAME = %q", name.AsString())
}
if math.Abs(salary.AsDouble()-30500.50) > 0.01 {
t.Errorf("rec1 SALARY = %f, want 30500.50", salary.AsDouble())
}
if !active.AsBool() {
t.Error("rec1 ACTIVE should be .T.")
}
if !hdate.IsDate() {
t.Error("rec1 HIREDATE should be date type")
}
// Verify record 50
area2.GoTo(50)
id, _ = area2.GetValue(0)
if id.AsNumInt() != 50 {
t.Errorf("rec50 ID = %d, want 50", id.AsNumInt())
}
// Verify record 100
area2.GoTo(100)
id, _ = area2.GetValue(0)
if id.AsNumInt() != 100 {
t.Errorf("rec100 ID = %d, want 100", id.AsNumInt())
}
// Verify sequential scan
area2.GoTop()
count := 0
for !area2.EOF() {
count++
area2.Skip(1)
}
if count != 100 {
t.Errorf("scan count = %d, want 100", count)
}
// Verify backward scan
area2.GoBottom()
count = 0
for !area2.BOF() {
count++
area2.Skip(-1)
}
if count != 100 {
t.Errorf("backward scan = %d, want 100", count)
}
// Verify file extension
if _, err := os.Stat(path + ".dbf"); err != nil {
t.Error("DBF file should exist")
}
}
// TestDBFCDX_Create100 creates a DBFCDX-style DBF (FPT version byte).
func TestDBFCDX_Create100(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "cdx_test")
// Create with FPT version (CDX style)
drv := &DBFDriver{}
area, err := drv.Create(hbrdd.CreateParams{
Path: path,
Fields: []hbrdd.FieldInfo{
{Name: "CODE", Type: 'C', Len: 10},
{Name: "AMOUNT", Type: 'N', Len: 15, Dec: 4},
{Name: "FLAG", Type: 'L', Len: 1},
{Name: "TXDATE", Type: 'D', Len: 8},
},
})
if err != nil {
t.Fatal(err)
}
// Insert 100 records
for i := 1; i <= 100; i++ {
area.Append()
area.PutValue(0, hbrt.MakeString(fmt.Sprintf("TX%06d", i)))
area.PutValue(1, hbrt.MakeDouble(float64(i)*123.4567, 15, 4))
area.PutValue(2, hbrt.MakeBool(i%2 == 0))
area.PutValue(3, hbrt.MakeDate(dateToJulian(2025, (i%12)+1, (i%28)+1)))
area.Flush()
}
area.Close()
// Reopen and verify
area2, err := drv.Open(hbrdd.OpenParams{Path: path})
if err != nil {
t.Fatal(err)
}
defer area2.Close()
rc, _ := area2.RecCount()
if rc != 100 {
t.Fatalf("reccount = %d, want 100", rc)
}
// Verify record 1
area2.GoTo(1)
code, _ := area2.GetValue(0)
if code.AsString()[:8] != "TX000001" {
t.Errorf("rec1 CODE = %q", code.AsString())
}
amount, _ := area2.GetValue(1)
if math.Abs(amount.AsDouble()-123.4567) > 0.001 {
t.Errorf("rec1 AMOUNT = %f", amount.AsDouble())
}
// Verify record 100
area2.GoTo(100)
code, _ = area2.GetValue(0)
if code.AsString()[:8] != "TX000100" {
t.Errorf("rec100 CODE = %q", code.AsString())
}
}
// TestDBF_MemoField tests memo field with FPT file.
func TestDBF_MemoField(t *testing.T) {
dir := t.TempDir()
dbfPath := filepath.Join(dir, "memo_test")
fptPath := dbfPath + ".fpt"
// Create DBF with memo field
drv := &DBFDriver{}
area, err := drv.Create(hbrdd.CreateParams{
Path: dbfPath,
Fields: []hbrdd.FieldInfo{
{Name: "ID", Type: 'N', Len: 5},
{Name: "TITLE", Type: 'C', Len: 50},
{Name: "NOTES", Type: 'M', Len: 10}, // Memo field
},
})
if err != nil {
t.Fatal(err)
}
// Create FPT file alongside DBF
fpt, err := CreateFPT(fptPath, 64)
if err != nil {
area.Close()
t.Fatal(err)
}
// Insert 100 records with memo data
for i := 1; i <= 100; i++ {
area.Append()
area.PutValue(0, hbrt.MakeInt(i))
area.PutValue(1, hbrt.MakeString(fmt.Sprintf("Item %d", i)))
// Write memo to FPT and store block number in DBF
memoText := fmt.Sprintf("This is memo #%d with some longer text for testing. Record number: %d. "+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt.", i, i)
blockNo, err := fpt.WriteMemo([]byte(memoText))
if err != nil {
t.Fatalf("write memo %d: %v", i, err)
}
area.PutValue(2, hbrt.MakeLong(int64(blockNo)))
area.Flush()
}
area.Close()
fpt.Close()
// Reopen and verify
area2, err := drv.Open(hbrdd.OpenParams{Path: dbfPath})
if err != nil {
t.Fatal(err)
}
defer area2.Close()
fpt2, err := OpenFPT(fptPath)
if err != nil {
t.Fatal(err)
}
defer fpt2.Close()
// Verify FPT file exists (CDX uses FPT, not DBT)
if _, err := os.Stat(fptPath); err != nil {
t.Error("FPT memo file should exist")
}
// Verify no DBT file (CDX doesn't use DBT)
dbtPath := dbfPath + ".dbt"
if _, err := os.Stat(dbtPath); err == nil {
t.Error("DBT file should NOT exist for CDX/FPT")
}
rc, _ := area2.RecCount()
if rc != 100 {
t.Fatalf("reccount = %d, want 100", rc)
}
// Read and verify memo for record 1
area2.GoTo(1)
memoBlockVal, _ := area2.GetValue(2)
blockNo := uint32(memoBlockVal.AsNumInt())
if blockNo == 0 {
t.Fatal("rec1 memo block should not be 0")
}
memoData, err := fpt2.ReadMemo(blockNo)
if err != nil {
t.Fatal(err)
}
memoStr := string(memoData)
if len(memoStr) < 10 {
t.Errorf("rec1 memo too short: %d bytes", len(memoStr))
}
if memoStr[:16] != "This is memo #1 " {
t.Errorf("rec1 memo start = %q", memoStr[:20])
}
// Read and verify memo for record 50
area2.GoTo(50)
memoBlockVal, _ = area2.GetValue(2)
blockNo = uint32(memoBlockVal.AsNumInt())
memoData, err = fpt2.ReadMemo(blockNo)
if err != nil {
t.Fatal(err)
}
memoStr = string(memoData)
if memoStr[:17] != "This is memo #50 " {
t.Errorf("rec50 memo start = %q", memoStr[:20])
}
// Read and verify memo for record 100
area2.GoTo(100)
memoBlockVal, _ = area2.GetValue(2)
blockNo = uint32(memoBlockVal.AsNumInt())
memoData, err = fpt2.ReadMemo(blockNo)
if err != nil {
t.Fatal(err)
}
memoStr = string(memoData)
if memoStr[:18] != "This is memo #100 " {
t.Errorf("rec100 memo start = %q", memoStr[:20])
}
// Verify FPT block size
if fpt2.blockSize != 64 {
t.Errorf("FPT block size = %d, want 64", fpt2.blockSize)
}
t.Logf("Memo test: 100 records with FPT memo verified")
t.Logf("FPT block size: %d, next block: %d", fpt2.blockSize, fpt2.header.NextBlock)
}
// TestDBFCDX_MemoIsFPT confirms that DBFCDX uses FPT format, not DBT.
func TestDBFCDX_MemoIsFPT(t *testing.T) {
dir := t.TempDir()
dbfPath := filepath.Join(dir, "cdx_memo_test")
fptPath := dbfPath + ".fpt"
dbtPath := dbfPath + ".dbt"
// Create FPT (what DBFCDX would use)
fpt, err := CreateFPT(fptPath, 64)
if err != nil {
t.Fatal(err)
}
// Write some memo data
block1, _ := fpt.WriteMemo([]byte("FPT memo data for DBFCDX"))
block2, _ := fpt.WriteMemo([]byte("Second memo entry"))
fpt.Close()
// Verify FPT exists
if _, err := os.Stat(fptPath); err != nil {
t.Fatal("FPT file must exist for DBFCDX")
}
// Verify DBT does NOT exist
if _, err := os.Stat(dbtPath); err == nil {
t.Fatal("DBT file must NOT exist for DBFCDX (uses FPT)")
}
// Reopen and read back
fpt2, err := OpenFPT(fptPath)
if err != nil {
t.Fatal(err)
}
defer fpt2.Close()
data1, _ := fpt2.ReadMemo(block1)
if string(data1) != "FPT memo data for DBFCDX" {
t.Errorf("memo 1 = %q", string(data1))
}
data2, _ := fpt2.ReadMemo(block2)
if string(data2) != "Second memo entry" {
t.Errorf("memo 2 = %q", string(data2))
}
// Verify Big-Endian header (FPT characteristic)
f, _ := os.Open(fptPath)
hdr := make([]byte, 8)
f.ReadAt(hdr, 0)
f.Close()
// FPT header: nextBlock(4 BE), reserved(2), blockSize(2 BE)
// Block size should be 64 in Big-Endian
blockSizeBE := uint16(hdr[6])<<8 | uint16(hdr[7])
if blockSizeBE != 64 {
t.Errorf("FPT block size (BE) = %d, want 64", blockSizeBE)
}
t.Logf("DBFCDX memo format: FPT confirmed (Big-Endian header, block size %d)", blockSizeBE)
t.Logf("Block 1 at %d, Block 2 at %d", block1, block2)
}
// TestDBF_AllFieldTypes tests all common DBF field types.
func TestDBF_AllFieldTypes(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "alltypes")
drv := &DBFDriver{}
area, err := drv.Create(hbrdd.CreateParams{
Path: path,
Fields: []hbrdd.FieldInfo{
{Name: "CHAR", Type: 'C', Len: 20},
{Name: "NUM", Type: 'N', Len: 12, Dec: 2},
{Name: "INT", Type: 'N', Len: 10, Dec: 0},
{Name: "LOGIC", Type: 'L', Len: 1},
{Name: "DATE", Type: 'D', Len: 8},
{Name: "MEMO", Type: 'M', Len: 10},
},
})
if err != nil {
t.Fatal(err)
}
// Insert test records
area.Append()
area.PutValue(0, hbrt.MakeString("Hello World"))
area.PutValue(1, hbrt.MakeDouble(-12345.67, 12, 2))
area.PutValue(2, hbrt.MakeInt(999999))
area.PutValue(3, hbrt.MakeBool(true))
area.PutValue(4, hbrt.MakeDate(dateToJulian(2026, 3, 28)))
area.PutValue(5, hbrt.MakeLong(0)) // empty memo
area.Flush()
area.Append()
area.PutValue(0, hbrt.MakeString("")) // empty string
area.PutValue(1, hbrt.MakeDouble(0.0, 12, 2)) // zero
area.PutValue(2, hbrt.MakeInt(-1)) // negative
area.PutValue(3, hbrt.MakeBool(false))
area.PutValue(4, hbrt.MakeDate(0)) // empty date
area.PutValue(5, hbrt.MakeLong(0))
area.Flush()
area.Close()
// Reopen and verify
area2, err := drv.Open(hbrdd.OpenParams{Path: path})
if err != nil {
t.Fatal(err)
}
defer area2.Close()
// Record 1
area2.GoTo(1)
v, _ := area2.GetValue(0)
if v.AsString()[:11] != "Hello World" {
t.Errorf("CHAR = %q", v.AsString())
}
v, _ = area2.GetValue(1)
if math.Abs(v.AsDouble()-(-12345.67)) > 0.01 {
t.Errorf("NUM = %f, want -12345.67", v.AsDouble())
}
v, _ = area2.GetValue(2)
if v.AsNumInt() != 999999 {
t.Errorf("INT = %d, want 999999", v.AsNumInt())
}
v, _ = area2.GetValue(3)
if !v.AsBool() {
t.Error("LOGIC should be TRUE")
}
v, _ = area2.GetValue(4)
y, m, d := julianToDate(v.AsJulian())
if y != 2026 || m != 3 || d != 28 {
t.Errorf("DATE = %d-%d-%d, want 2026-3-28", y, m, d)
}
// Record 2 — edge cases
area2.GoTo(2)
v, _ = area2.GetValue(1)
if v.AsDouble() != 0.0 {
t.Errorf("zero NUM = %f", v.AsDouble())
}
v, _ = area2.GetValue(2)
if v.AsNumInt() != -1 {
t.Errorf("negative INT = %d", v.AsNumInt())
}
v, _ = area2.GetValue(3)
if v.AsBool() {
t.Error("LOGIC should be FALSE")
}
v, _ = area2.GetValue(4)
if v.AsJulian() != 0 {
t.Error("empty date should be julian 0")
}
t.Log("All field types test passed")
}