- 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>
379 lines
8.6 KiB
Go
379 lines
8.6 KiB
Go
package hbrtl
|
|
|
|
import (
|
|
"five/hbrt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// === hb_jsonEncode ===
|
|
|
|
func TestJsonEncode_String(t *testing.T) {
|
|
_, th := setupVM()
|
|
th.PushString("hello")
|
|
th.PendingParams2(1)
|
|
HbJsonEncode(th)
|
|
if r := th.GetRetValue().AsString(); r != `"hello"` {
|
|
t.Errorf("encode string = %q, want %q", r, `"hello"`)
|
|
}
|
|
}
|
|
|
|
func TestJsonEncode_Number(t *testing.T) {
|
|
_, th := setupVM()
|
|
th.PushInt(42)
|
|
th.PendingParams2(1)
|
|
HbJsonEncode(th)
|
|
if r := th.GetRetValue().AsString(); r != "42" {
|
|
t.Errorf("encode int = %q, want %q", r, "42")
|
|
}
|
|
}
|
|
|
|
func TestJsonEncode_Bool(t *testing.T) {
|
|
_, th := setupVM()
|
|
th.PushBool(true)
|
|
th.PendingParams2(1)
|
|
HbJsonEncode(th)
|
|
if r := th.GetRetValue().AsString(); r != "true" {
|
|
t.Errorf("encode bool = %q", r)
|
|
}
|
|
}
|
|
|
|
func TestJsonEncode_Nil(t *testing.T) {
|
|
_, th := setupVM()
|
|
th.PushNil()
|
|
th.PendingParams2(1)
|
|
HbJsonEncode(th)
|
|
if r := th.GetRetValue().AsString(); r != "null" {
|
|
t.Errorf("encode nil = %q", r)
|
|
}
|
|
}
|
|
|
|
func TestJsonEncode_Array(t *testing.T) {
|
|
_, th := setupVM()
|
|
arr := hbrt.MakeArrayFrom([]hbrt.Value{
|
|
hbrt.MakeInt(1), hbrt.MakeString("two"), hbrt.MakeBool(false),
|
|
})
|
|
th.PushValue(arr)
|
|
th.PendingParams2(1)
|
|
HbJsonEncode(th)
|
|
r := th.GetRetValue().AsString()
|
|
if r != `[1,"two",false]` {
|
|
t.Errorf("encode array = %q", r)
|
|
}
|
|
}
|
|
|
|
func TestJsonEncode_Hash(t *testing.T) {
|
|
_, th := setupVM()
|
|
h := &hbrt.HbHash{
|
|
Keys: []hbrt.Value{hbrt.MakeString("name"), hbrt.MakeString("age")},
|
|
Values: []hbrt.Value{hbrt.MakeString("Five"), hbrt.MakeInt(1)},
|
|
}
|
|
th.PushValue(hbrt.MakeHashFrom(h))
|
|
th.PendingParams2(1)
|
|
HbJsonEncode(th)
|
|
r := th.GetRetValue().AsString()
|
|
if !strings.Contains(r, `"name":"Five"`) || !strings.Contains(r, `"age":1`) {
|
|
t.Errorf("encode hash = %q", r)
|
|
}
|
|
}
|
|
|
|
func TestJsonEncode_Pretty(t *testing.T) {
|
|
_, th := setupVM()
|
|
h := &hbrt.HbHash{
|
|
Keys: []hbrt.Value{hbrt.MakeString("x")},
|
|
Values: []hbrt.Value{hbrt.MakeInt(1)},
|
|
}
|
|
th.PushValue(hbrt.MakeHashFrom(h))
|
|
th.PushBool(true) // pretty
|
|
th.PendingParams2(2)
|
|
HbJsonEncode(th)
|
|
r := th.GetRetValue().AsString()
|
|
if !strings.Contains(r, "\n") {
|
|
t.Errorf("pretty should have newlines: %q", r)
|
|
}
|
|
}
|
|
|
|
// === hb_jsonDecode ===
|
|
|
|
func TestJsonDecode_Object(t *testing.T) {
|
|
_, th := setupVM()
|
|
th.PushString(`{"name":"Five","version":2}`)
|
|
th.PendingParams2(1)
|
|
HbJsonDecode(th)
|
|
r := th.GetRetValue()
|
|
if !r.IsHash() {
|
|
t.Fatalf("decode object should be hash, got %v", r)
|
|
}
|
|
}
|
|
|
|
func TestJsonDecode_Array(t *testing.T) {
|
|
_, th := setupVM()
|
|
th.PushString(`[1,2,3]`)
|
|
th.PendingParams2(1)
|
|
HbJsonDecode(th)
|
|
r := th.GetRetValue()
|
|
if !r.IsArray() {
|
|
t.Fatalf("decode array should be array")
|
|
}
|
|
arr := r.AsArray()
|
|
if len(arr.Items) != 3 {
|
|
t.Errorf("array len = %d, want 3", len(arr.Items))
|
|
}
|
|
}
|
|
|
|
func TestJsonDecode_Nested(t *testing.T) {
|
|
_, th := setupVM()
|
|
th.PushString(`{"users":[{"name":"Kim"},{"name":"Lee"}]}`)
|
|
th.PendingParams2(1)
|
|
HbJsonDecode(th)
|
|
r := th.GetRetValue()
|
|
if !r.IsHash() {
|
|
t.Fatal("should be hash")
|
|
}
|
|
}
|
|
|
|
func TestJsonDecode_Invalid(t *testing.T) {
|
|
_, th := setupVM()
|
|
th.PushString(`{broken}`)
|
|
th.PendingParams2(1)
|
|
HbJsonDecode(th)
|
|
if !th.GetRetValue().IsNil() {
|
|
t.Error("invalid JSON should return NIL")
|
|
}
|
|
}
|
|
|
|
// === JsonPretty ===
|
|
|
|
func TestJsonPretty(t *testing.T) {
|
|
_, th := setupVM()
|
|
h := &hbrt.HbHash{
|
|
Keys: []hbrt.Value{hbrt.MakeString("a"), hbrt.MakeString("b")},
|
|
Values: []hbrt.Value{hbrt.MakeInt(1), hbrt.MakeInt(2)},
|
|
}
|
|
th.PushValue(hbrt.MakeHashFrom(h))
|
|
th.PendingParams2(1)
|
|
JsonPretty(th)
|
|
r := th.GetRetValue().AsString()
|
|
if !strings.Contains(r, "\n") || !strings.Contains(r, " ") {
|
|
t.Errorf("pretty = %q", r)
|
|
}
|
|
}
|
|
|
|
// === JsonPath ===
|
|
|
|
func TestJsonPath_Simple(t *testing.T) {
|
|
_, th := setupVM()
|
|
data := makeTestHash()
|
|
th.PushValue(data)
|
|
th.PushString("$.name")
|
|
th.PendingParams2(2)
|
|
JsonPath(th)
|
|
if r := th.GetRetValue().AsString(); r != "Five" {
|
|
t.Errorf("path $.name = %q, want 'Five'", r)
|
|
}
|
|
}
|
|
|
|
func TestJsonPath_Nested(t *testing.T) {
|
|
_, th := setupVM()
|
|
// {"user":{"name":"Charles"}}
|
|
inner := &hbrt.HbHash{
|
|
Keys: []hbrt.Value{hbrt.MakeString("name")},
|
|
Values: []hbrt.Value{hbrt.MakeString("Charles")},
|
|
}
|
|
outer := &hbrt.HbHash{
|
|
Keys: []hbrt.Value{hbrt.MakeString("user")},
|
|
Values: []hbrt.Value{hbrt.MakeHashFrom(inner)},
|
|
}
|
|
th.PushValue(hbrt.MakeHashFrom(outer))
|
|
th.PushString("$.user.name")
|
|
th.PendingParams2(2)
|
|
JsonPath(th)
|
|
if r := th.GetRetValue().AsString(); r != "Charles" {
|
|
t.Errorf("path $.user.name = %q", r)
|
|
}
|
|
}
|
|
|
|
func TestJsonPath_Array(t *testing.T) {
|
|
_, th := setupVM()
|
|
arr := hbrt.MakeArrayFrom([]hbrt.Value{
|
|
hbrt.MakeInt(100), hbrt.MakeInt(200), hbrt.MakeInt(300),
|
|
})
|
|
h := &hbrt.HbHash{
|
|
Keys: []hbrt.Value{hbrt.MakeString("scores")},
|
|
Values: []hbrt.Value{arr},
|
|
}
|
|
th.PushValue(hbrt.MakeHashFrom(h))
|
|
th.PushString("$.scores[1]")
|
|
th.PendingParams2(2)
|
|
JsonPath(th)
|
|
if r := th.GetRetValue().AsNumInt(); r != 200 {
|
|
t.Errorf("path $.scores[1] = %d, want 200", r)
|
|
}
|
|
}
|
|
|
|
func TestJsonPath_Missing(t *testing.T) {
|
|
_, th := setupVM()
|
|
th.PushValue(makeTestHash())
|
|
th.PushString("$.nonexistent.deep.path")
|
|
th.PendingParams2(2)
|
|
JsonPath(th)
|
|
if !th.GetRetValue().IsNil() {
|
|
t.Error("missing path should return NIL")
|
|
}
|
|
}
|
|
|
|
// === JsonMerge ===
|
|
|
|
func TestJsonMerge(t *testing.T) {
|
|
_, th := setupVM()
|
|
a := &hbrt.HbHash{
|
|
Keys: []hbrt.Value{hbrt.MakeString("x"), hbrt.MakeString("y")},
|
|
Values: []hbrt.Value{hbrt.MakeInt(1), hbrt.MakeInt(2)},
|
|
}
|
|
b := &hbrt.HbHash{
|
|
Keys: []hbrt.Value{hbrt.MakeString("y"), hbrt.MakeString("z")},
|
|
Values: []hbrt.Value{hbrt.MakeInt(99), hbrt.MakeInt(3)},
|
|
}
|
|
th.PushValue(hbrt.MakeHashFrom(a))
|
|
th.PushValue(hbrt.MakeHashFrom(b))
|
|
th.PendingParams2(2)
|
|
JsonMerge(th)
|
|
r := th.GetRetValue()
|
|
if !r.IsHash() {
|
|
t.Fatal("merge should return hash")
|
|
}
|
|
h := r.AsHash()
|
|
if len(h.Keys) != 3 {
|
|
t.Errorf("merged keys = %d, want 3", len(h.Keys))
|
|
}
|
|
}
|
|
|
|
// === JsonValid ===
|
|
|
|
func TestJsonValid(t *testing.T) {
|
|
_, th := setupVM()
|
|
th.PushString(`{"valid":true}`)
|
|
th.PendingParams2(1)
|
|
JsonValid(th)
|
|
if !th.GetRetValue().AsBool() {
|
|
t.Error("valid JSON should return true")
|
|
}
|
|
|
|
th.PushString(`{broken`)
|
|
th.PendingParams2(1)
|
|
JsonValid(th)
|
|
if th.GetRetValue().AsBool() {
|
|
t.Error("invalid JSON should return false")
|
|
}
|
|
}
|
|
|
|
// === JsonType ===
|
|
|
|
func TestJsonType(t *testing.T) {
|
|
_, th := setupVM()
|
|
cases := map[string]string{
|
|
`{"a":1}`: "object",
|
|
`[1,2]`: "array",
|
|
`"hello"`: "string",
|
|
`42`: "number",
|
|
`true`: "boolean",
|
|
`null`: "null",
|
|
``: "invalid",
|
|
}
|
|
for input, expected := range cases {
|
|
th.PushString(input)
|
|
th.PendingParams2(1)
|
|
JsonType(th)
|
|
if r := th.GetRetValue().AsString(); r != expected {
|
|
t.Errorf("JsonType(%q) = %q, want %q", input, r, expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
// === JsonTo / JsonFrom (file roundtrip) ===
|
|
|
|
func TestJsonFileRoundtrip(t *testing.T) {
|
|
_, th := setupVM()
|
|
dir := t.TempDir()
|
|
fpath := filepath.Join(dir, "test.json")
|
|
|
|
// Write
|
|
h := &hbrt.HbHash{
|
|
Keys: []hbrt.Value{hbrt.MakeString("msg"), hbrt.MakeString("num")},
|
|
Values: []hbrt.Value{hbrt.MakeString("hello"), hbrt.MakeInt(42)},
|
|
}
|
|
th.PushValue(hbrt.MakeHashFrom(h))
|
|
th.PushString(fpath)
|
|
th.PendingParams2(2)
|
|
JsonTo(th)
|
|
if !th.GetRetValue().AsBool() {
|
|
t.Fatal("JsonTo failed")
|
|
}
|
|
|
|
// Verify file exists
|
|
data, err := os.ReadFile(fpath)
|
|
if err != nil {
|
|
t.Fatal("file not created")
|
|
}
|
|
if !strings.Contains(string(data), "hello") {
|
|
t.Errorf("file content: %s", string(data))
|
|
}
|
|
|
|
// Read back
|
|
th.PushString(fpath)
|
|
th.PendingParams2(1)
|
|
JsonFrom(th)
|
|
r := th.GetRetValue()
|
|
if !r.IsHash() {
|
|
t.Fatal("JsonFrom should return hash")
|
|
}
|
|
}
|
|
|
|
// === Encode/Decode roundtrip ===
|
|
|
|
func TestJsonRoundtrip(t *testing.T) {
|
|
_, th := setupVM()
|
|
|
|
// Complex nested structure
|
|
inner := hbrt.MakeArrayFrom([]hbrt.Value{
|
|
hbrt.MakeInt(1), hbrt.MakeString("two"), hbrt.MakeBool(true), hbrt.MakeNil(),
|
|
})
|
|
h := &hbrt.HbHash{
|
|
Keys: []hbrt.Value{hbrt.MakeString("arr"), hbrt.MakeString("val")},
|
|
Values: []hbrt.Value{inner, hbrt.MakeDouble(3.14, 0, 0)},
|
|
}
|
|
|
|
// Encode
|
|
th.PushValue(hbrt.MakeHashFrom(h))
|
|
th.PendingParams2(1)
|
|
HbJsonEncode(th)
|
|
encoded := th.GetRetValue().AsString()
|
|
|
|
// Decode
|
|
th.PushString(encoded)
|
|
th.PendingParams2(1)
|
|
HbJsonDecode(th)
|
|
decoded := th.GetRetValue()
|
|
|
|
// Re-encode and compare
|
|
th.PushValue(decoded)
|
|
th.PendingParams2(1)
|
|
HbJsonEncode(th)
|
|
reEncoded := th.GetRetValue().AsString()
|
|
|
|
if encoded != reEncoded {
|
|
t.Errorf("roundtrip mismatch:\n original: %s\n roundtrip: %s", encoded, reEncoded)
|
|
}
|
|
}
|
|
|
|
// helper
|
|
func makeTestHash() hbrt.Value {
|
|
h := &hbrt.HbHash{
|
|
Keys: []hbrt.Value{hbrt.MakeString("name"), hbrt.MakeString("version")},
|
|
Values: []hbrt.Value{hbrt.MakeString("Five"), hbrt.MakeInt(1)},
|
|
}
|
|
return hbrt.MakeHashFrom(h)
|
|
}
|