* test: golden-pin the four de-duplicated indicators across all C-ABI bindings Extend gen_golden to emit reference fixtures for AdOscillator (ADOSC), IntradayIntensity, AwesomeOscillatorHistogram and AverageDrawdown, and replay them through the Go / C# / Java / R golden harnesses so their corrected definitions stay bit-identical to the Rust core in every binding. Go suite verified locally (gcc 13 + cgo): all 9 golden tests pass; C#/Java/R use the same fixtures and harness pattern (CI-verified). First step of extending the golden coverage beyond the seven archetype representatives. * test: golden-pin the scalar-output tranche (308 indicators) against Rust Extend gen_golden with a generated emit_scalar that writes reference fixtures for every single-f64-output indicator (scalar / candle / pairwise input) using valid constructor params, and add a manifest-driven generic Python golden replay that reconstructs each by its native name and checks it bit-for-bit against the Rust output. 308 indicators now value-tied to the Rust core in Python (pytest: 308/308). Takes golden coverage from the 7 archetype representatives to 308+ of the catalogue. 22 scalar indicators with non-default constructor constraints are skipped by gen_golden for now (logged), as are non-f64-output ones; multi-output, exotic inputs and the per-indicator arg arities of the C-ABI/Node replays follow. Generated + verified locally with the full toolchain. * test: golden-pin the multi-output tranche (70 indicators) in Python Add a generated emit_multi to gen_golden (per-indicator Output-field access, one CSV column per field) and a manifest-driven generic Python replay that checks every field of each multi-output indicator against the Rust reference. 70 multi-output indicators now value-tied to Rust in Python; combined with the scalar tranche, 378 indicators are golden-pinned. 8 multi with non-default param constraints and 5 with non-f64 Output fields (Option/Vec/i64) are deferred. pytest green. * test(golden): add 30 constraint-tuned indicators to scalar/multi golden suite Emit golden fixtures for 22 scalar-output and 8 multi-output indicators whose constructors need non-default parameters (Alma, Jma, Psar, T3, Mama, DoubleBollinger, ZigZag, ...). All 408 fixtures replay bit-for-bit through the Python binding. * test(golden): cover 36 missed scalar/multi indicators Add 26 single-output (LinearRegression family, HT cycle, Candle volatility estimators, DrawdownDuration) and 10 multi-output (BollingerBands, MACD/MACDEXT/MACDFIX, Camarilla, VWAP bands, ...) indicators to the golden suite. 444 fixtures replay bit-for-bit through the Python binding. * test(golden): cover 50 exotic-input indicators Add deterministic synthetic feeders for the DerivativesTick (17), CrossSection (15), Trade (8), TradeQuote (3) and OrderBook (7) families, derived from the shared OHLCV input series in both gen_golden and a new Python replay harness (test_golden_exotic). All 494 fixtures replay bit-for-bit through the Python binding. * test(golden): complete 514-indicator golden coverage Add the final tranches: 3 mixed multi-output indicators (Ichimoku, WilliamsFractals, LeadLagCrossCorrelation), 6 histogram profiles (time/volume seasonality + TPO/volume price profiles), 10 alt-chart bar builders and the footprint. Every one of the 514 distinct indicators now has a Rust-generated g_<Canonical>.csv fixture and a generic Python replay (scalar/multi/exotic/profile/bars), all passing bit-for-bit. * test(golden): add generic Node replay for all 514 indicators A manifest-driven node:test harness reconstructs every indicator by its native class, feeds the same synthetic stream derived from the shared golden input, and checks output bit-for-bit against the Rust reference fixtures (scalar/multi/exotic/profile/bars). node_manifest.json is generated from index.d.ts plus the Python-side manifests. 514/514 pass. * test(golden): add generated Go replay for all 514 indicators golden_all_test.go (generated by gen_golden_test.py) reconstructs every Go indicator, feeds the shared synthetic stream and checks output bit-for-bit against the Rust reference fixtures. A reflection-based comparator flattens multi-output structs, profiles and bar slices so one path covers all archetypes. This is the first C-ABI binding verified across the full catalogue. 514/514 pass. * test(golden): add generated C# replay for all 514 indicators GoldenAllTests.g.cs (generated by gen_golden_test.py) reconstructs every C# indicator, feeds the shared synthetic stream and checks output bit-for-bit against the Rust reference fixtures via a reflection-based flatten covering scalar/multi/profile/bar archetypes. 514/514 pass. Also add the '#nullable enable' directive the compiler requires to the generated Indicators.g.cs, clearing the four CS8669 warnings on the nullable double[] profile return types. * fix(java): marshal C ABI bool params correctly; add 514 golden replay The Java FFM binding marshalled the cross-section state flags (newHigh, newLow, aboveMa, onBuySignal) as JAVA_DOUBLE arrays, but the C ABI takes them as const bool* (one byte each), so the native side read the low byte of each 8-byte double and saw every flag as false. Add WickraNative. boolSegment and use it across the 15 cross-section indicators. Also pass the MacdExt MaType arguments as byte to match the uint8_t downcall descriptor (was int, throwing WrongMethodTypeException). Add GoldenAllTest.java (generated by gen_golden_test.py): a reflection runner replaying all 514 indicators against the Rust reference fixtures. The bugs above were found by this test; 514/514 now pass. * fix(r): marshal C ABI bool flags correctly; add 514 golden replay The R wrapper passed the cross-section state flags as (bool *)REAL(x), reinterpreting the 8-byte doubles as 1-byte bools so the native side read every flag as false. Add wk_bool_vec to convert each flag vector into a real C bool buffer and use it for all 15 cross-section update wrappers. Add test-golden-all.R + generated golden_specs.R: a reflective runner replaying all 514 indicators against the Rust reference fixtures. The bug above was found by this test; verified 514/514 pass locally. * test(golden): add WASM replay for all 514 indicators A manifest-driven node:test harness loads the nodejs-target wasm-pack build, reconstructs every indicator by its JS class, feeds the shared synthetic stream and checks output bit-for-bit against the Rust reference fixtures. wasm_manifest.json is generated from the wasm .d.ts plus the shared manifests; a recursive flattener covers scalar, multi (Reflect objects), profile and bar shapes. 514/514 pass locally (wasm-pack build --target nodejs, then node --test). * test(golden): add C and C++ replay for all 514 indicators golden_test.c (generated by gen_golden_test.py) drives every indicator through the C ABI (wickra.h) and checks output bit-for-bit against the Rust reference fixtures. golden_test.cpp #includes the same source so the identical runner is compiled and run under both gcc (C) and g++ (C++) via the CMake targets golden_test / golden_test_cpp — proving the extern "C" header is consumable from each language. Both 514/514 (verified via ctest). * test(golden): gofmt the generated Go golden replay * test(golden): make the Node fixture reader CRLF-safe and pin fixtures to LF
338 lines
12 KiB
Python
338 lines
12 KiB
Python
"""Generate golden_all_test.go: a value-parity test that replays the shared
|
|
golden input through every one of the 514 Go indicators and checks output
|
|
bit-for-bit against the Rust-generated g_<Canonical>.csv fixtures.
|
|
|
|
Run from repo root: python bindings/go/gen_golden_test.py
|
|
"""
|
|
import glob
|
|
import json
|
|
import os
|
|
import re
|
|
|
|
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
G = os.path.join(ROOT, "testdata", "golden")
|
|
GEN = open(os.path.join(ROOT, "bindings", "go", "indicators_gen.go"), encoding="utf-8").read()
|
|
|
|
# Go constructor parameter types, keyed by canonical (== Go type name).
|
|
ctor_types = {}
|
|
for m in re.finditer(r"func New(\w+)\(([^)]*)\)\s*\(\*\w+, error\)", GEN):
|
|
name, ps = m.group(1), m.group(2).strip()
|
|
types = []
|
|
if ps:
|
|
for p in ps.split(","):
|
|
p = p.strip()
|
|
_, _, ty = p.partition(" ")
|
|
types.append(ty.strip())
|
|
ctor_types[name] = types
|
|
|
|
# Unified archetype + params, keyed by canonical.
|
|
spec = {} # canon -> dict(arch, params, width?, n?)
|
|
scal = json.load(open(os.path.join(G, "scalar_manifest.json")))
|
|
for e in scal:
|
|
inp = e["input"]
|
|
arch = {"f64": "scalar_f64", "Candle": "scalar_candle", "(f64, f64)": "pairwise"}[inp]
|
|
spec[e["canonical"]] = {"arch": arch, "params": e["params"]}
|
|
for e in json.load(open(os.path.join(G, "multi_manifest.json"))):
|
|
inp = e["input"]
|
|
arch = {"f64": "multi_f64", "Candle": "multi_candle", "(f64, f64)": "multi_pairwise"}[inp]
|
|
spec[e["canonical"]] = {"arch": arch, "params": e["params"], "n": e["n"]}
|
|
ex = json.load(open(os.path.join(G, "exotic_manifest.json")))
|
|
for e in ex["deriv"]:
|
|
spec[e["canonical"]] = {"arch": "deriv_multi" if "n" in e else "deriv", "params": e["params"], "n": e.get("n")}
|
|
for e in ex["cross"]:
|
|
spec[e["canonical"]] = {"arch": "cross", "params": e["params"]}
|
|
for e in ex["trade"]:
|
|
spec[e["canonical"]] = {"arch": "trade", "params": e["params"]}
|
|
for e in ex["trademid"]:
|
|
spec[e["canonical"]] = {"arch": "trademid", "params": e["params"]}
|
|
for e in ex["ob"]:
|
|
spec[e["canonical"]] = {"arch": "ob", "params": e["params"]}
|
|
for e in json.load(open(os.path.join(G, "profile_manifest.json"))):
|
|
spec[e["canonical"]] = {"arch": "profile_" + e["kind"], "params": e["params"], "width": e["width"]}
|
|
for e in json.load(open(os.path.join(G, "bars_manifest.json"))):
|
|
arch = "footprint" if e["canonical"] == "Footprint" else "bars_" + e["feed"]
|
|
spec[e["canonical"]] = {"arch": arch, "params": e["params"]}
|
|
|
|
canons = sorted(os.path.basename(f)[2:-4] for f in glob.glob(os.path.join(G, "g_*.csv")))
|
|
|
|
|
|
def go_param(value, gotype):
|
|
intlike = gotype in ("int", "int32", "int64", "uint", "uintptr", "usize")
|
|
if intlike:
|
|
return str(int(round(value)))
|
|
# float64
|
|
return repr(float(value)) if "." in repr(float(value)) or "e" in repr(float(value)) else f"{float(value)}"
|
|
|
|
|
|
def ctor_call(canon):
|
|
types = ctor_types.get(canon, [])
|
|
vals = spec[canon]["params"]
|
|
args = ", ".join(go_param(v, t) for v, t in zip(vals, types))
|
|
return f"New{canon}({args})"
|
|
|
|
|
|
# Update-call expression + output handling per archetype.
|
|
def block(canon):
|
|
s = spec[canon]
|
|
a = s["arch"]
|
|
ctor = ctor_call(canon)
|
|
lines = [f'\tt.Run("{canon}", func(t *testing.T) {{']
|
|
lines.append(f"\t\tind, err := {ctor}")
|
|
lines.append('\t\tif err != nil {')
|
|
lines.append(f'\t\t\tt.Fatalf("new {canon}: %v", err)')
|
|
lines.append("\t\t}")
|
|
lines.append("\t\tgot := make([][]float64, len(rows))")
|
|
lines.append("\t\tfor i, r := range rows {")
|
|
if a == "scalar_f64":
|
|
upd = "ind.Update(r[3])"
|
|
lines.append(f"\t\t\tgot[i] = []float64{{{upd}}}")
|
|
elif a == "pairwise":
|
|
lines.append("\t\t\tgot[i] = []float64{ind.Update(r[3], r[0])}")
|
|
elif a == "scalar_candle":
|
|
lines.append("\t\t\tgot[i] = []float64{ind.Update(r[0], r[1], r[2], r[3], r[4], int64(i))}")
|
|
elif a == "trade":
|
|
lines.append("\t\t\tgot[i] = []float64{ind.Update(r[3], r[4], r[3] >= r[0], int64(i))}")
|
|
elif a == "trademid":
|
|
lines.append("\t\t\tgot[i] = []float64{ind.Update(r[3], r[4], r[3] >= r[0], int64(i), (r[1]+r[2])/2)}")
|
|
elif a == "ob":
|
|
lines.append("\t\t\tbp, bs, ap, as_ := obLists(r)")
|
|
lines.append("\t\t\tgot[i] = []float64{ind.Update(bp, bs, ap, as_)}")
|
|
elif a == "deriv":
|
|
lines.append("\t\t\td := derivFields(r)")
|
|
lines.append("\t\t\tgot[i] = []float64{ind.Update(d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9], d[10], int64(i))}")
|
|
elif a == "deriv_multi":
|
|
lines.append("\t\t\td := derivFields(r)")
|
|
lines.append("\t\t\tout, ok := ind.Update(d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9], d[10], int64(i))")
|
|
lines.append(f"\t\t\tgot[i] = reflectRow(out, ok, {s['n']})")
|
|
elif a == "cross":
|
|
lines.append("\t\t\tch, vo, nh, nl, am, ob_ := crossLists(r)")
|
|
lines.append("\t\t\tgot[i] = []float64{ind.Update(ch, vo, nh, nl, am, ob_, int64(i))}")
|
|
elif a in ("multi_f64",):
|
|
lines.append("\t\t\tout, ok := ind.Update(r[3])")
|
|
lines.append(f"\t\t\tgot[i] = reflectRow(out, ok, {s['n']})")
|
|
elif a == "multi_pairwise":
|
|
lines.append("\t\t\tout, ok := ind.Update(r[3], r[0])")
|
|
lines.append(f"\t\t\tgot[i] = reflectRow(out, ok, {s['n']})")
|
|
elif a == "multi_candle":
|
|
lines.append("\t\t\tout, ok := ind.Update(r[0], r[1], r[2], r[3], r[4], int64(i))")
|
|
lines.append(f"\t\t\tgot[i] = reflectRow(out, ok, {s['n']})")
|
|
elif a == "profile_bins":
|
|
lines.append("\t\t\tbins, ok := ind.Update(r[0], r[1], r[2], r[3], r[4], int64(i))")
|
|
lines.append(f"\t\t\tif ok {{ got[i] = bins }} else {{ got[i] = nanRow({s['width']}) }}")
|
|
elif a == "profile_pricebins":
|
|
lines.append("\t\t\tout, ok := ind.Update(r[0], r[1], r[2], r[3], r[4], int64(i))")
|
|
lines.append(f"\t\t\tgot[i] = reflectRow(out, ok, {s['width']})")
|
|
elif a == "bars_close":
|
|
lines.append("\t\t\tgot[i] = flattenBars(ind.Update(r[3], r[3], r[3], r[3], 1.0, 0))")
|
|
elif a == "bars_candle4":
|
|
lines.append("\t\t\tgot[i] = flattenBars(ind.Update(r[0], r[1], r[2], r[3], 1.0, 0))")
|
|
elif a == "bars_candle5":
|
|
lines.append("\t\t\tgot[i] = flattenBars(ind.Update(r[0], r[1], r[2], r[3], r[4], 0))")
|
|
elif a == "footprint":
|
|
lines.append("\t\t\tgot[i] = flattenBars(ind.Update(r[3], r[4], r[3] >= r[0], int64(i)))")
|
|
else:
|
|
raise SystemExit("unknown arch " + a)
|
|
lines.append("\t\t}")
|
|
lines.append(f'\t\tcompareGolden(t, "{canon}", got)')
|
|
lines.append("\t})")
|
|
return "\n".join(lines)
|
|
|
|
|
|
HEADER = '''// Code generated by gen_golden_test.py. DO NOT EDIT.
|
|
//
|
|
// Value-parity for every one of the 514 Go indicators: the shared golden input
|
|
// is replayed through each one and checked bit-for-bit against the Rust
|
|
// reference fixtures testdata/golden/g_<Canonical>.csv. Multi-output, profile
|
|
// and bar shapes are flattened by reflection so a single comparator covers all
|
|
// archetypes. Regenerate with: python bindings/go/gen_golden_test.py
|
|
|
|
package wickra
|
|
|
|
import (
|
|
\t"bufio"
|
|
\t"math"
|
|
\t"os"
|
|
\t"reflect"
|
|
\t"strings"
|
|
\t"testing"
|
|
)
|
|
|
|
// readGoldenRaw keeps blank lines (a candle on which no bar closed) so bar rows
|
|
// stay aligned to the input; non-bar fixtures contain no blank lines.
|
|
func readGoldenRaw(t *testing.T, name string) [][]string {
|
|
\tt.Helper()
|
|
\tf, err := os.Open("../../testdata/golden/" + name + ".csv")
|
|
\tif err != nil {
|
|
\t\tt.Fatalf("open %s: %v", name, err)
|
|
\t}
|
|
\tdefer f.Close()
|
|
\tvar rows [][]string
|
|
\tsc := bufio.NewScanner(f)
|
|
\tsc.Buffer(make([]byte, 0, 1024*1024), 1024*1024)
|
|
\tfirst := true
|
|
\tfor sc.Scan() {
|
|
\t\tline := sc.Text()
|
|
\t\tif first {
|
|
\t\t\tfirst = false
|
|
\t\t\tcontinue
|
|
\t\t}
|
|
\t\tif line == "" {
|
|
\t\t\trows = append(rows, []string{})
|
|
\t\t\tcontinue
|
|
\t\t}
|
|
\t\trows = append(rows, strings.Split(line, ","))
|
|
\t}
|
|
\treturn rows
|
|
}
|
|
|
|
func nanRow(n int) []float64 {
|
|
\tr := make([]float64, n)
|
|
\tfor i := range r {
|
|
\t\tr[i] = math.NaN()
|
|
\t}
|
|
\treturn r
|
|
}
|
|
|
|
func reflectRow(out any, ok bool, width int) []float64 {
|
|
\tif !ok {
|
|
\t\treturn nanRow(width)
|
|
\t}
|
|
\tv := reflect.ValueOf(out)
|
|
\trow := make([]float64, 0, width)
|
|
\tfor k := 0; k < v.NumField(); k++ {
|
|
\t\trow = appendField(row, v.Field(k))
|
|
\t}
|
|
\treturn row
|
|
}
|
|
|
|
func appendField(row []float64, f reflect.Value) []float64 {
|
|
\tswitch f.Kind() {
|
|
\tcase reflect.Float64, reflect.Float32:
|
|
\t\treturn append(row, f.Float())
|
|
\tcase reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
\t\treturn append(row, float64(f.Int()))
|
|
\tcase reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
|
\t\treturn append(row, float64(f.Uint()))
|
|
\tcase reflect.Slice:
|
|
\t\tfor j := 0; j < f.Len(); j++ {
|
|
\t\t\trow = appendField(row, f.Index(j))
|
|
\t\t}
|
|
\t\treturn row
|
|
\tdefault:
|
|
\t\treturn row
|
|
\t}
|
|
}
|
|
|
|
func flattenBars(bars any) []float64 {
|
|
\tv := reflect.ValueOf(bars)
|
|
\trow := []float64{}
|
|
\tfor i := 0; i < v.Len(); i++ {
|
|
\t\tbar := v.Index(i)
|
|
\t\tfor k := 0; k < bar.NumField(); k++ {
|
|
\t\t\trow = appendField(row, bar.Field(k))
|
|
\t\t}
|
|
\t}
|
|
\treturn row
|
|
}
|
|
|
|
// Synthetic feeds derived from one OHLCV row, identical to gen_golden's Rust
|
|
// construction (DerivativesTick / CrossSection / OrderBook).
|
|
func derivFields(r []float64) [11]float64 {
|
|
\to, h, l, c, v := r[0], r[1], r[2], r[3], r[4]
|
|
\treturn [11]float64{
|
|
\t\t(c - o) / c * 0.01, // funding_rate
|
|
\t\tc, // mark_price
|
|
\t\tc - 0.5, // index_price
|
|
\t\tc + 1.0, // futures_price
|
|
\t\tv * 10.0, // open_interest
|
|
\t\tv * 0.6, // long_size
|
|
\t\tv * 0.4, // short_size
|
|
\t\tv * 0.55, // taker_buy_volume
|
|
\t\tv * 0.45, // taker_sell_volume
|
|
\t\th - c, // long_liquidation
|
|
\t\tc - l, // short_liquidation
|
|
\t}
|
|
}
|
|
|
|
func crossLists(r []float64) ([]float64, []float64, []bool, []bool, []bool, []bool) {
|
|
\to, c, v := r[0], r[3], r[4]
|
|
\tchange := make([]float64, 5)
|
|
\tvolume := make([]float64, 5)
|
|
\tnewHigh := make([]bool, 5)
|
|
\tnewLow := make([]bool, 5)
|
|
\taboveMa := make([]bool, 5)
|
|
\tonBuy := make([]bool, 5)
|
|
\tfor j := 0; j < 5; j++ {
|
|
\t\tjf := float64(j)
|
|
\t\tchange[j] = (c - o) + jf
|
|
\t\tvolume[j] = v + jf*10.0
|
|
\t\tnewHigh[j] = j%2 == 0
|
|
\t\tnewLow[j] = j%3 == 0
|
|
\t\taboveMa[j] = j%2 == 0
|
|
\t\tonBuy[j] = j%3 == 0
|
|
\t}
|
|
\treturn change, volume, newHigh, newLow, aboveMa, onBuy
|
|
}
|
|
|
|
func obLists(r []float64) ([]float64, []float64, []float64, []float64) {
|
|
\tc, v := r[3], r[4]
|
|
\tbidPx := make([]float64, 5)
|
|
\tbidSz := make([]float64, 5)
|
|
\taskPx := make([]float64, 5)
|
|
\taskSz := make([]float64, 5)
|
|
\tfor k := 0; k < 5; k++ {
|
|
\t\tkf := float64(k + 1)
|
|
\t\tbidPx[k] = c - 0.1*kf
|
|
\t\tbidSz[k] = v / kf
|
|
\t\taskPx[k] = c + 0.1*kf
|
|
\t\taskSz[k] = v * 0.9 / kf
|
|
\t}
|
|
\treturn bidPx, bidSz, askPx, askSz
|
|
}
|
|
|
|
func compareGolden(t *testing.T, name string, got [][]float64) {
|
|
\tt.Helper()
|
|
\texp := readGoldenRaw(t, "g_"+name)
|
|
\tif len(exp) != len(got) {
|
|
\t\tt.Fatalf("%s: %d fixture rows vs %d computed", name, len(exp), len(got))
|
|
\t}
|
|
\tfor i := range exp {
|
|
\t\tif len(exp[i]) != len(got[i]) {
|
|
\t\t\tt.Fatalf("%s row %d: arity %d vs %d", name, i, len(got[i]), len(exp[i]))
|
|
\t\t}
|
|
\t\tfor k := range exp[i] {
|
|
\t\t\twant := goldenCell(exp[i][k])
|
|
\t\t\tg := got[i][k]
|
|
\t\t\tif math.IsNaN(want) {
|
|
\t\t\t\tif !math.IsNaN(g) {
|
|
\t\t\t\t\tt.Fatalf("%s row %d col %d: want NaN got %v", name, i, k, g)
|
|
\t\t\t\t}
|
|
\t\t\t\tcontinue
|
|
\t\t\t}
|
|
\t\t\tif math.IsInf(want, 0) {
|
|
\t\t\t\tif !math.IsInf(g, 0) || (g > 0) != (want > 0) {
|
|
\t\t\t\t\tt.Fatalf("%s row %d col %d: want %v got %v", name, i, k, want, g)
|
|
\t\t\t\t}
|
|
\t\t\t\tcontinue
|
|
\t\t\t}
|
|
\t\t\ttol := goldenTol * math.Max(1.0, math.Abs(want))
|
|
\t\t\tif math.Abs(g-want) > tol {
|
|
\t\t\t\tt.Fatalf("%s row %d col %d: got %v want %v", name, i, k, g, want)
|
|
\t\t\t}
|
|
\t\t}
|
|
\t}
|
|
}
|
|
|
|
func TestGoldenAll(t *testing.T) {
|
|
\trows := goldenInput(t)
|
|
'''
|
|
|
|
# bars need blank-line-preserving fixture reads; reuse readGolden but it skips
|
|
# blanks. We need a raw reader for bars and input.
|
|
out = [HEADER]
|
|
for canon in canons:
|
|
out.append(block(canon))
|
|
out.append("}")
|
|
open(os.path.join(ROOT, "bindings", "go", "golden_all_test.go"), "w", encoding="utf-8").write("\n".join(out) + "\n")
|
|
print("generated golden_all_test.go with", len(canons), "indicators")
|