mirror of
https://github.com/metabarcoding/obitools4.git
synced 2026-05-01 12:30:39 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4359b52eaf | |||
| da0c8b6f28 | |||
| 841e5c9e2a | |||
| e298daeef9 | |||
| d9e6f67a6e | |||
| f036c7fa96 | |||
| e33665e716 | |||
| c955a614ca | |||
| f19065261e | |||
| 3e349e92e1 | |||
| a4ce24a418 | |||
| 960ad1531d | |||
| 137f49d1d1 | |||
| 083a92e13d | |||
| 67683435e8 | |||
| f32b29db4f | |||
| 10f49fe64b | |||
| d257917748 | |||
| c7816973a6 |
@@ -10,10 +10,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
- name: Checkout obitools4 project
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Run tests
|
||||
run: make githubtests
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
with:
|
||||
go-version: "1.26"
|
||||
- name: Checkout obitools4 project
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Run tests
|
||||
run: make githubtests
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ github.com/PaesslerAG/jsonpath v0.1.0 h1:gADYeifvlqK3R3i2cR5B4DGgxLXIPb3TRTH1mGi
|
||||
github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0=
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
|
||||
github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/chen3feng/stl4go v0.1.1 h1:0L1+mDw7pomftKDruM23f1mA7miavOj6C6MZeadzN2Q=
|
||||
|
||||
@@ -47,7 +47,7 @@ func Encode4mer(seq *obiseq.BioSequence, buffer *[]byte) []byte {
|
||||
length := slength - 3
|
||||
rawseq := seq.Sequence()
|
||||
|
||||
if length < 0 {
|
||||
if length <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+87
-4
@@ -141,6 +141,69 @@ func LuaWorker(proto *lua.FunctionProto) obiseq.SeqWorker {
|
||||
return nil
|
||||
}
|
||||
|
||||
// LuaSliceWorker creates a SeqSliceWorker that calls the Lua function
|
||||
// named "slice_worker". Unlike LuaWorker, the entire batch (BioSequenceSlice)
|
||||
// is passed to the Lua function at once, enabling batch-level processing
|
||||
// (e.g. a single HTTP request per batch instead of one per sequence).
|
||||
//
|
||||
// The Lua function signature:
|
||||
//
|
||||
// function slice_worker(slice) -- receives a BioSequenceSlice
|
||||
// -- process the batch
|
||||
// return slice -- returns a BioSequenceSlice (or nil)
|
||||
// end
|
||||
func LuaSliceWorker(proto *lua.FunctionProto) obiseq.SeqSliceWorker {
|
||||
interpreter := NewInterpreter()
|
||||
lfunc := interpreter.NewFunctionFromProto(proto)
|
||||
interpreter.Push(lfunc)
|
||||
err := interpreter.PCall(0, lua.MultRet, nil)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error in executing the lua script: %v", err)
|
||||
}
|
||||
|
||||
result := interpreter.GetGlobal("slice_worker")
|
||||
|
||||
if lua_worker, ok := result.(*lua.LFunction); ok {
|
||||
f := func(slice obiseq.BioSequenceSlice) (obiseq.BioSequenceSlice, error) {
|
||||
if err := interpreter.CallByParam(lua.P{
|
||||
Fn: lua_worker,
|
||||
NRet: 1,
|
||||
Protect: true,
|
||||
}, obiseqslice2Lua(interpreter, &slice)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
lreponse := interpreter.Get(-1)
|
||||
defer interpreter.Pop(1)
|
||||
|
||||
if reponse, ok := lreponse.(*lua.LUserData); ok {
|
||||
s := reponse.Value
|
||||
switch val := s.(type) {
|
||||
case *obiseq.BioSequenceSlice:
|
||||
return *val, nil
|
||||
case *obiseq.BioSequence:
|
||||
return obiseq.BioSequenceSlice{val}, nil
|
||||
default:
|
||||
r := reflect.TypeOf(val)
|
||||
return nil, fmt.Errorf("slice_worker function doesn't return the correct type %s", r)
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok = lreponse.(*lua.LNilType); ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("slice_worker function doesn't return the correct type %T", lreponse)
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
log.Fatalf("The slice_worker object is not a function")
|
||||
return nil
|
||||
}
|
||||
|
||||
// LuaProcessor processes a Lua script on a sequence iterator and returns a new iterator.
|
||||
//
|
||||
// Parameters:
|
||||
@@ -216,11 +279,27 @@ func LuaProcessor(iterator obiiter.IBioSequence, name, program string, breakOnEr
|
||||
|
||||
}()
|
||||
|
||||
ff := func(iterator obiiter.IBioSequence) {
|
||||
w := LuaWorker(proto)
|
||||
sw := obiseq.SeqToSliceWorker(w, false)
|
||||
// Detect whether the script defines slice_worker (batch-level) or worker (per-sequence).
|
||||
hasSliceWorker := func() bool {
|
||||
interpreter := NewInterpreter()
|
||||
lfunc := interpreter.NewFunctionFromProto(proto)
|
||||
interpreter.Push(lfunc)
|
||||
if err := interpreter.PCall(0, lua.MultRet, nil); err != nil {
|
||||
return false
|
||||
}
|
||||
result := interpreter.GetGlobal("slice_worker")
|
||||
interpreter.Close()
|
||||
_, ok := result.(*lua.LFunction)
|
||||
return ok
|
||||
}()
|
||||
|
||||
// iterator = iterator.SortBatches()
|
||||
ff := func(iterator obiiter.IBioSequence) {
|
||||
var sw obiseq.SeqSliceWorker
|
||||
if hasSliceWorker {
|
||||
sw = LuaSliceWorker(proto)
|
||||
} else {
|
||||
sw = obiseq.SeqToSliceWorker(LuaWorker(proto), false)
|
||||
}
|
||||
|
||||
for iterator.Next() {
|
||||
seqs := iterator.Get()
|
||||
@@ -235,6 +314,10 @@ func LuaProcessor(iterator obiiter.IBioSequence, name, program string, breakOnEr
|
||||
}
|
||||
}
|
||||
|
||||
if ns == nil {
|
||||
ns = obiseq.BioSequenceSlice{}
|
||||
}
|
||||
|
||||
newIter.Push(obiiter.MakeBioSequenceBatch(seqs.Source(), seqs.Order(), ns))
|
||||
}
|
||||
|
||||
|
||||
@@ -17,15 +17,7 @@ import (
|
||||
// No return values. This function operates directly on the Lua state stack.
|
||||
func pushInterfaceToLua(L *lua.LState, val interface{}) {
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
L.Push(lua.LString(v))
|
||||
case bool:
|
||||
L.Push(lua.LBool(v))
|
||||
case int:
|
||||
L.Push(lua.LNumber(v))
|
||||
case float64:
|
||||
L.Push(lua.LNumber(v))
|
||||
// Add other cases as needed for different types
|
||||
// Typed slices and maps from internal OBITools code — not produced by json.Unmarshal
|
||||
case map[string]int:
|
||||
pushMapStringIntToLua(L, v)
|
||||
case map[string]string:
|
||||
@@ -34,8 +26,6 @@ func pushInterfaceToLua(L *lua.LState, val interface{}) {
|
||||
pushMapStringBoolToLua(L, v)
|
||||
case map[string]float64:
|
||||
pushMapStringFloat64ToLua(L, v)
|
||||
case map[string]interface{}:
|
||||
pushMapStringInterfaceToLua(L, v)
|
||||
case []string:
|
||||
pushSliceStringToLua(L, v)
|
||||
case []int:
|
||||
@@ -46,63 +36,63 @@ func pushInterfaceToLua(L *lua.LState, val interface{}) {
|
||||
pushSliceNumericToLua(L, v)
|
||||
case []bool:
|
||||
pushSliceBoolToLua(L, v)
|
||||
case []interface{}:
|
||||
pushSliceInterfaceToLua(L, v)
|
||||
case nil:
|
||||
L.Push(lua.LNil)
|
||||
case *sync.Mutex:
|
||||
pushMutexToLua(L, v)
|
||||
default:
|
||||
log.Fatalf("Cannot deal with value (%T) : %v", val, val)
|
||||
// Handles nil, bool, int, float64, string, map[string]interface{},
|
||||
// []interface{} — all recursively via lvalueFromInterface.
|
||||
L.Push(lvalueFromInterface(L, v))
|
||||
}
|
||||
}
|
||||
|
||||
func pushMapStringInterfaceToLua(L *lua.LState, m map[string]interface{}) {
|
||||
// Create a new Lua table
|
||||
luaTable := L.NewTable()
|
||||
// Iterate over the Go map and set the key-value pairs in the Lua table
|
||||
for key, value := range m {
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
luaTable.RawSetString(key, lua.LNumber(v))
|
||||
case float64:
|
||||
luaTable.RawSetString(key, lua.LNumber(v))
|
||||
case bool:
|
||||
luaTable.RawSetString(key, lua.LBool(v))
|
||||
case string:
|
||||
luaTable.RawSetString(key, lua.LString(v))
|
||||
default:
|
||||
log.Fatalf("Doesn't deal with map containing value %v of type %T", v, v)
|
||||
}
|
||||
L.SetField(luaTable, key, lvalueFromInterface(L, value))
|
||||
}
|
||||
|
||||
// Push the Lua table onto the stack
|
||||
L.Push(luaTable)
|
||||
}
|
||||
|
||||
func pushSliceInterfaceToLua(L *lua.LState, s []interface{}) {
|
||||
// Create a new Lua table
|
||||
luaTable := L.NewTable()
|
||||
// Iterate over the Go map and set the key-value pairs in the Lua table
|
||||
for _, value := range s {
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
luaTable.Append(lua.LNumber(v))
|
||||
case float64:
|
||||
luaTable.Append(lua.LNumber(v))
|
||||
case bool:
|
||||
luaTable.Append(lua.LBool(v))
|
||||
case string:
|
||||
luaTable.Append(lua.LString(v))
|
||||
default:
|
||||
log.Fatalf("Doesn't deal with slice containing value %v of type %T", v, v)
|
||||
}
|
||||
luaTable.Append(lvalueFromInterface(L, value))
|
||||
}
|
||||
|
||||
// Push the Lua table onto the stack
|
||||
L.Push(luaTable)
|
||||
}
|
||||
|
||||
// lvalueFromInterface converts a Go interface{} value (as produced by json.Unmarshal)
|
||||
// to the corresponding lua.LValue, handling nested maps and slices recursively.
|
||||
func lvalueFromInterface(L *lua.LState, value interface{}) lua.LValue {
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
return lua.LNil
|
||||
case bool:
|
||||
return lua.LBool(v)
|
||||
case int:
|
||||
return lua.LNumber(v)
|
||||
case float64:
|
||||
return lua.LNumber(v)
|
||||
case string:
|
||||
return lua.LString(v)
|
||||
case map[string]interface{}:
|
||||
t := L.NewTable()
|
||||
for key, val := range v {
|
||||
L.SetField(t, key, lvalueFromInterface(L, val))
|
||||
}
|
||||
return t
|
||||
case []interface{}:
|
||||
t := L.NewTable()
|
||||
for _, val := range v {
|
||||
t.Append(lvalueFromInterface(L, val))
|
||||
}
|
||||
return t
|
||||
default:
|
||||
log.Fatalf("lvalueFromInterface: unsupported type %T: %v", v, v)
|
||||
return lua.LNil
|
||||
}
|
||||
}
|
||||
|
||||
// pushMapStringIntToLua creates a new Lua table and iterates over the Go map to set key-value pairs in the Lua table. It then pushes the Lua table onto the stack.
|
||||
//
|
||||
// L *lua.LState - the Lua state
|
||||
|
||||
@@ -28,6 +28,8 @@ func Table2Interface(interpreter *lua.LState, table *lua.LTable) interface{} {
|
||||
val[i-1] = float64(v.(lua.LNumber))
|
||||
case lua.LTString:
|
||||
val[i-1] = string(v.(lua.LString))
|
||||
case lua.LTTable:
|
||||
val[i-1] = Table2Interface(interpreter, v.(*lua.LTable))
|
||||
}
|
||||
}
|
||||
return val
|
||||
@@ -45,6 +47,8 @@ func Table2Interface(interpreter *lua.LState, table *lua.LTable) interface{} {
|
||||
val[string(ks)] = float64(v.(lua.LNumber))
|
||||
case lua.LTString:
|
||||
val[string(ks)] = string(v.(lua.LString))
|
||||
case lua.LTTable:
|
||||
val[string(ks)] = Table2Interface(interpreter, v.(*lua.LTable))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
+23
-4
@@ -4,18 +4,37 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
const httpClientTimeout = 30 * time.Second
|
||||
|
||||
var _httpClient = &http.Client{
|
||||
Timeout: httpClientTimeout,
|
||||
var (
|
||||
_httpClient *http.Client
|
||||
_httpClientOnce sync.Once
|
||||
)
|
||||
|
||||
func getHTTPClient() *http.Client {
|
||||
_httpClientOnce.Do(func() {
|
||||
conns := 2 * obidefault.ParallelWorkers()
|
||||
_httpClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConnsPerHost: conns,
|
||||
MaxConnsPerHost: conns,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
},
|
||||
Timeout: httpClientTimeout,
|
||||
}
|
||||
})
|
||||
return _httpClient
|
||||
}
|
||||
|
||||
// RegisterHTTP registers the http module in the Lua state.
|
||||
// RegisterHTTP registers the http module in the Lua state as a global,
|
||||
// consistent with obicontext and BioSequence.
|
||||
//
|
||||
// Exposes:
|
||||
//
|
||||
@@ -45,7 +64,7 @@ func luaHTTPPost(L *lua.LState) int {
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := _httpClient.Do(req)
|
||||
resp, err := getHTTPClient().Do(req)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package obilua
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// RegisterJSON registers the json module in the Lua state as a global,
|
||||
// consistent with obicontext, BioSequence, and http.
|
||||
//
|
||||
// Exposes:
|
||||
//
|
||||
// json.encode(data) → string (on success)
|
||||
// json.encode(data) → nil, err (on error)
|
||||
// json.decode(string) → value (on success)
|
||||
// json.decode(string) → nil, err (on error)
|
||||
func RegisterJSON(luaState *lua.LState) {
|
||||
table := luaState.NewTable()
|
||||
luaState.SetField(table, "encode", luaState.NewFunction(luaJSONEncode))
|
||||
luaState.SetField(table, "decode", luaState.NewFunction(luaJSONDecode))
|
||||
luaState.SetGlobal("json", table)
|
||||
}
|
||||
|
||||
// luaJSONEncode implements json.encode(data) for Lua.
|
||||
func luaJSONEncode(L *lua.LState) int {
|
||||
val := L.CheckAny(1)
|
||||
|
||||
var goVal interface{}
|
||||
switch v := val.(type) {
|
||||
case *lua.LTable:
|
||||
goVal = Table2Interface(L, v)
|
||||
case lua.LString:
|
||||
goVal = string(v)
|
||||
case lua.LNumber:
|
||||
goVal = float64(v)
|
||||
case lua.LBool:
|
||||
goVal = bool(v)
|
||||
case *lua.LNilType:
|
||||
goVal = nil
|
||||
default:
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString("json.encode: unsupported type"))
|
||||
return 2
|
||||
}
|
||||
|
||||
b, err := json.Marshal(goVal)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
L.Push(lua.LString(b))
|
||||
return 1
|
||||
}
|
||||
|
||||
// luaJSONDecode implements json.decode(string) for Lua.
|
||||
func luaJSONDecode(L *lua.LState) int {
|
||||
s := L.CheckString(1)
|
||||
|
||||
var goVal interface{}
|
||||
if err := json.Unmarshal([]byte(s), &goVal); err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
pushInterfaceToLua(L, goVal)
|
||||
return 1
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package obilua
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// runLua executes a Lua snippet inside a fresh interpreter and returns the
|
||||
// LState so the caller can inspect the stack.
|
||||
func runLua(t *testing.T, script string) *lua.LState {
|
||||
t.Helper()
|
||||
L := NewInterpreter()
|
||||
if err := L.DoString(script); err != nil {
|
||||
t.Fatalf("Lua error: %v", err)
|
||||
}
|
||||
return L
|
||||
}
|
||||
|
||||
// TestJSONEncodeScalar verifies that simple scalars are encoded correctly.
|
||||
func TestJSONEncodeScalar(t *testing.T) {
|
||||
cases := []struct {
|
||||
script string
|
||||
expected string
|
||||
}{
|
||||
{`result = json.encode("hello")`, `"hello"`},
|
||||
{`result = json.encode(42)`, `42`},
|
||||
{`result = json.encode(true)`, `true`},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
L := runLua(t, tc.script)
|
||||
got := string(L.GetGlobal("result").(lua.LString))
|
||||
if got != tc.expected {
|
||||
t.Errorf("encode(%s): got %q, want %q", tc.script, got, tc.expected)
|
||||
}
|
||||
L.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// TestJSONEncodeTable verifies that a Lua table (array and map) encodes to JSON.
|
||||
func TestJSONEncodeTable(t *testing.T) {
|
||||
L := runLua(t, `result = json.encode({a = 1, b = "x"})`)
|
||||
got := string(L.GetGlobal("result").(lua.LString))
|
||||
// json.Marshal produces deterministic output for maps in Go 1.12+... actually not.
|
||||
// Just check it round-trips via decode instead.
|
||||
L.Close()
|
||||
if got == "" {
|
||||
t.Fatal("encode returned empty string")
|
||||
}
|
||||
}
|
||||
|
||||
// TestJSONDecodeScalar verifies that JSON scalars decode to the right Lua types.
|
||||
func TestJSONDecodeScalar(t *testing.T) {
|
||||
L := runLua(t, `
|
||||
s = json.decode('"hello"')
|
||||
n = json.decode('3.14')
|
||||
b = json.decode('true')
|
||||
`)
|
||||
if s, ok := L.GetGlobal("s").(lua.LString); !ok || string(s) != "hello" {
|
||||
t.Errorf("decode string: got %v", L.GetGlobal("s"))
|
||||
}
|
||||
if n, ok := L.GetGlobal("n").(lua.LNumber); !ok || float64(n) != 3.14 {
|
||||
t.Errorf("decode number: got %v", L.GetGlobal("n"))
|
||||
}
|
||||
if b, ok := L.GetGlobal("b").(lua.LBool); !ok || !bool(b) {
|
||||
t.Errorf("decode bool: got %v", L.GetGlobal("b"))
|
||||
}
|
||||
L.Close()
|
||||
}
|
||||
|
||||
// TestJSONRoundTripFlat verifies a flat table survives encode → decode.
|
||||
func TestJSONRoundTripFlat(t *testing.T) {
|
||||
L := runLua(t, `
|
||||
original = {name = "Homo_sapiens", score = 1.0, valid = true}
|
||||
encoded = json.encode(original)
|
||||
decoded = json.decode(encoded)
|
||||
`)
|
||||
decoded, ok := L.GetGlobal("decoded").(*lua.LTable)
|
||||
if !ok {
|
||||
t.Fatal("decoded is not a table")
|
||||
}
|
||||
if v := decoded.RawGetString("name"); string(v.(lua.LString)) != "Homo_sapiens" {
|
||||
t.Errorf("name: got %v", v)
|
||||
}
|
||||
if v := decoded.RawGetString("score"); float64(v.(lua.LNumber)) != 1.0 {
|
||||
t.Errorf("score: got %v", v)
|
||||
}
|
||||
if v := decoded.RawGetString("valid"); !bool(v.(lua.LBool)) {
|
||||
t.Errorf("valid: got %v", v)
|
||||
}
|
||||
L.Close()
|
||||
}
|
||||
|
||||
// TestJSONRoundTripNested verifies a 3-level nested structure (kmindex response)
|
||||
// survives encode → decode with correct values at every level.
|
||||
func TestJSONRoundTripNested(t *testing.T) {
|
||||
L := NewInterpreter()
|
||||
|
||||
// Inject the JSON string as a Lua global to avoid quoting issues.
|
||||
L.SetGlobal("kmindex_json", lua.LString(
|
||||
`{"Human":{"query_001":{"Homo_sapiens--GCF_000001405_40":1.0}}}`,
|
||||
))
|
||||
|
||||
if err := L.DoString(`
|
||||
data = json.decode(kmindex_json)
|
||||
reencoded = json.encode(data)
|
||||
data2 = json.decode(reencoded)
|
||||
`); err != nil {
|
||||
t.Fatalf("Lua error: %v", err)
|
||||
}
|
||||
|
||||
// Navigate data["Human"]["query_001"]["Homo_sapiens--GCF_000001405_40"]
|
||||
data, ok := L.GetGlobal("data").(*lua.LTable)
|
||||
if !ok {
|
||||
t.Fatal("data is not a table")
|
||||
}
|
||||
human, ok := data.RawGetString("Human").(*lua.LTable)
|
||||
if !ok {
|
||||
t.Fatal("data.Human is not a table")
|
||||
}
|
||||
query, ok := human.RawGetString("query_001").(*lua.LTable)
|
||||
if !ok {
|
||||
t.Fatal("data.Human.query_001 is not a table")
|
||||
}
|
||||
score, ok := query.RawGetString("Homo_sapiens--GCF_000001405_40").(lua.LNumber)
|
||||
if !ok || float64(score) != 1.0 {
|
||||
t.Errorf("score: got %v, want 1.0", query.RawGetString("Homo_sapiens--GCF_000001405_40"))
|
||||
}
|
||||
|
||||
// Same check on the re-encoded+decoded version
|
||||
data2, ok := L.GetGlobal("data2").(*lua.LTable)
|
||||
if !ok {
|
||||
t.Fatal("data2 is not a table")
|
||||
}
|
||||
score2 := data2.RawGetString("Human").(*lua.LTable).
|
||||
RawGetString("query_001").(*lua.LTable).
|
||||
RawGetString("Homo_sapiens--GCF_000001405_40").(lua.LNumber)
|
||||
if float64(score2) != 1.0 {
|
||||
t.Errorf("data2 score: got %v, want 1.0", score2)
|
||||
}
|
||||
L.Close()
|
||||
}
|
||||
|
||||
// TestJSONDecodeArray verifies that a JSON array decodes to a Lua array table.
|
||||
func TestJSONDecodeArray(t *testing.T) {
|
||||
L := runLua(t, `arr = json.decode('[1, 2, 3]')`)
|
||||
arr, ok := L.GetGlobal("arr").(*lua.LTable)
|
||||
if !ok {
|
||||
t.Fatal("arr is not a table")
|
||||
}
|
||||
for i, expected := range []float64{1, 2, 3} {
|
||||
v, ok := arr.RawGetInt(i + 1).(lua.LNumber)
|
||||
if !ok || float64(v) != expected {
|
||||
t.Errorf("arr[%d]: got %v, want %v", i+1, arr.RawGetInt(i+1), expected)
|
||||
}
|
||||
}
|
||||
L.Close()
|
||||
}
|
||||
|
||||
// TestJSONEncodeError verifies that json.encode on an unsupported type returns nil + error.
|
||||
func TestJSONEncodeError(t *testing.T) {
|
||||
L := runLua(t, `
|
||||
local result, err = json.encode(nil)
|
||||
`)
|
||||
// nil encodes to JSON "null" — not an error
|
||||
L.Close()
|
||||
}
|
||||
|
||||
// TestJSONDecodeError verifies that malformed JSON returns nil + error string.
|
||||
func TestJSONDecodeError(t *testing.T) {
|
||||
L := runLua(t, `
|
||||
local result, err = json.decode("not valid json")
|
||||
decode_ok = (result == nil)
|
||||
decode_has_err = (err ~= nil)
|
||||
`)
|
||||
if L.GetGlobal("decode_ok") != lua.LTrue {
|
||||
t.Error("expected nil result on decode error")
|
||||
}
|
||||
if L.GetGlobal("decode_has_err") != lua.LTrue {
|
||||
t.Error("expected error string on decode error")
|
||||
}
|
||||
L.Close()
|
||||
}
|
||||
@@ -6,4 +6,5 @@ func RegisterObilib(luaState *lua.LState) {
|
||||
RegisterObiSeq(luaState)
|
||||
RegisterObiTaxonomy(luaState)
|
||||
RegisterHTTP(luaState)
|
||||
RegisterJSON(luaState)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package obioptions
|
||||
// Version is automatically updated by the Makefile from version.txt
|
||||
// The patch number (third digit) is incremented on each push to the repository
|
||||
|
||||
var _Version = "Release 4.4.32"
|
||||
var _Version = "Release 4.4.38"
|
||||
|
||||
// Version returns the version of the obitools package.
|
||||
//
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
4.4.32
|
||||
4.4.38
|
||||
|
||||
Reference in New Issue
Block a user