test(golden): language-neutral fixtures + per-binding parity runners (#255)
**Task 5 — golden-fixture parity for the C-ABI bindings.** Lifts the C#/Go/Java/R tests from *one indicator per archetype* toward reference-value parity, catching FFI wiring bugs (swapped params, wrong multi-output field) the math-only core tests cannot see. ## What's here - **`examples/rust/src/bin/gen_golden.rs`** + **`testdata/golden/*.csv`** — a Rust generator computing a deterministic OHLCV series plus the core's reference outputs for a curated archetype-spanning set: scalar (`Sma`/`Ema`/`Rsi`), candle (`Atr`), scalar multi-output (`MACD`), candle multi-output (`ADX`), pairwise (`Beta`). `nan` marks warmup. Regenerate with `cargo run -p wickra-examples --bin gen_golden`. - **Parity runners** replaying the identical fixtures through each FFI (rel-tol 1e-6), each a standard test in the binding's existing suite (no `ci.yml` change — rides `dotnet test` / `go test` / `mvn install` / `R CMD`+testthat). A walk-up search locates `testdata/golden` regardless of run dir. - **C#** (`bindings/csharp/.../GoldenTests.cs`) — ✅ validated locally, 7/7 pass. - **Go** (`bindings/go/golden_test.go`) — ✅ validated locally, pass. - **Java** (`bindings/java/.../GoldenTests.java`) — modeled on the archetype API; validated by CI (no local mvn). - **R** (`bindings/r/tests/testthat/test-golden.R`) — modeled on the archetype API; validated by CI (no local Rscript). ## Notes - The curated set spans every marshalling archetype; extending the indicator list is mechanical (add to the generator + regenerate). Bars/profile archetypes can be added next. - No new CI jobs.
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
package org.wickra;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Golden-fixture parity: replay the shared {@code testdata/golden} input series
|
||||
* through the Java FFI and assert every value matches the Rust reference output.
|
||||
* Where the archetype tests only check finiteness, this pins exact values, so a
|
||||
* wiring bug (swapped parameter, wrong multi-output field) is caught. Fixtures
|
||||
* are generated by {@code cargo run -p wickra-examples --bin gen_golden}.
|
||||
*/
|
||||
class GoldenTests {
|
||||
private static final double TOL = 1e-6;
|
||||
|
||||
private static Path goldenDir() {
|
||||
File d = new File("").getAbsoluteFile();
|
||||
while (d != null) {
|
||||
File g = new File(d, "testdata/golden");
|
||||
if (g.isDirectory()) {
|
||||
return g.toPath();
|
||||
}
|
||||
d = d.getParentFile();
|
||||
}
|
||||
throw new IllegalStateException("testdata/golden not found from " + new File("").getAbsolutePath());
|
||||
}
|
||||
|
||||
private static List<String[]> readCsv(String name) throws Exception {
|
||||
List<String> lines = Files.readAllLines(goldenDir().resolve(name + ".csv"));
|
||||
List<String[]> rows = new ArrayList<>();
|
||||
for (int i = 1; i < lines.size(); i++) { // skip header
|
||||
if (!lines.get(i).isEmpty()) {
|
||||
rows.add(lines.get(i).split(","));
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static double cell(String s) {
|
||||
return s.equals("nan") ? Double.NaN : Double.parseDouble(s);
|
||||
}
|
||||
|
||||
private static double[][] input() throws Exception {
|
||||
List<String[]> rows = readCsv("input");
|
||||
double[][] out = new double[rows.size()][];
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
String[] r = rows.get(i);
|
||||
double[] v = new double[r.length];
|
||||
for (int j = 0; j < r.length; j++) {
|
||||
v[j] = Double.parseDouble(r[j]);
|
||||
}
|
||||
out[i] = v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static void close(double got, double want, int row, String field) {
|
||||
if (Double.isNaN(want)) {
|
||||
assertTrue(Double.isNaN(got), "row " + row + " " + field + ": expected warmup/NaN, got " + got);
|
||||
return;
|
||||
}
|
||||
double tol = TOL * Math.max(1.0, Math.abs(want));
|
||||
assertTrue(Math.abs(got - want) <= tol, "row " + row + " " + field + ": got " + got + " want " + want);
|
||||
}
|
||||
|
||||
@Test
|
||||
void scalarMatchesGolden() throws Exception {
|
||||
double[][] in = input();
|
||||
try (Sma sma = new Sma(14)) {
|
||||
List<String[]> e = readCsv("sma");
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
close(sma.update(in[i][3]), cell(e.get(i)[0]), i, "sma");
|
||||
}
|
||||
}
|
||||
try (Ema ema = new Ema(14)) {
|
||||
List<String[]> e = readCsv("ema");
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
close(ema.update(in[i][3]), cell(e.get(i)[0]), i, "ema");
|
||||
}
|
||||
}
|
||||
try (Rsi rsi = new Rsi(14)) {
|
||||
List<String[]> e = readCsv("rsi");
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
close(rsi.update(in[i][3]), cell(e.get(i)[0]), i, "rsi");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void candleAtrMatchesGolden() throws Exception {
|
||||
double[][] in = input();
|
||||
List<String[]> e = readCsv("atr");
|
||||
try (Atr atr = new Atr(14)) {
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
close(atr.update(in[i][0], in[i][1], in[i][2], in[i][3], in[i][4], i), cell(e.get(i)[0]), i, "atr");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void pairwiseBetaMatchesGolden() throws Exception {
|
||||
double[][] in = input();
|
||||
List<String[]> e = readCsv("beta");
|
||||
try (Beta beta = new Beta(20)) {
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
// generator fed (close, open)
|
||||
close(beta.update(in[i][3], in[i][0]), cell(e.get(i)[0]), i, "beta");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void macdMatchesGolden() throws Exception {
|
||||
double[][] in = input();
|
||||
List<String[]> e = readCsv("macd");
|
||||
try (MacdIndicator macd = new MacdIndicator(12, 26, 9)) {
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
MacdOutput o = macd.update(in[i][3]);
|
||||
String[] row = e.get(i);
|
||||
if (row[0].equals("nan")) {
|
||||
assertNull(o, "row " + i + " macd: expected warmup");
|
||||
continue;
|
||||
}
|
||||
assertNotNull(o, "row " + i + " macd: expected value");
|
||||
close(o.macd(), cell(row[0]), i, "macd.macd");
|
||||
close(o.signal(), cell(row[1]), i, "macd.signal");
|
||||
close(o.histogram(), cell(row[2]), i, "macd.histogram");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void adxMatchesGolden() throws Exception {
|
||||
double[][] in = input();
|
||||
List<String[]> e = readCsv("adx");
|
||||
try (Adx adx = new Adx(14)) {
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
AdxOutput o = adx.update(in[i][0], in[i][1], in[i][2], in[i][3], in[i][4], i);
|
||||
String[] row = e.get(i);
|
||||
if (row[0].equals("nan")) {
|
||||
assertNull(o, "row " + i + " adx: expected warmup");
|
||||
continue;
|
||||
}
|
||||
assertNotNull(o, "row " + i + " adx: expected value");
|
||||
close(o.plusDi(), cell(row[0]), i, "adx.plus_di");
|
||||
close(o.minusDi(), cell(row[1]), i, "adx.minus_di");
|
||||
close(o.adx(), cell(row[2]), i, "adx.adx");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user