Skip to content

Commit c0ab5ee

Browse files
dfa1claude
andcommitted
fix(test): stream-compare conformance CSVs via temp files to avoid OOM (#254)
StringWriter materialized multi-GB CSV Strings in heap for large corpus files, crashing the JVM before reaching corrupted lines and silently masking data bugs. Replace with CsvExporter.exportCsv(Path,Path,...) + Files.newBufferedWriter for oracle output; assertFilesMatch streams both files line-by-line via BufferedReader — constant heap regardless of size. Also marks bi-euro2016 (gap:252) and covid-world-vaccination-progress (gap:253) as known gaps now that the test actually reaches the bad lines. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 845ebb1 commit c0ab5ee

3 files changed

Lines changed: 79 additions & 46 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- `ScanIterator.sliceArray` now handles `NullArray`; previously threw on Rust-written files where a null-typed column used a coarser chunk grid than the other columns. ([#247](https://github.com/dfa1/vortex-java/issues/247))
1515
- `AlpRdEncodingDecoder` now reads `left_parts_ptype` from metadata and rejects any value other than U16 with a `VortexException`; previously the field was silently ignored and U16 was hardcoded, risking a 2-byte stride over a 1-byte buffer for spec-conformant non-Rust writers. ([#249](https://github.com/dfa1/vortex-java/issues/249))
1616
- `SparseEncodingDecoder` now asserts exactly two children (`patch_indices`, `patch_values`) and throws `VortexException` on any other count; previously a third child was silently accepted. ([#250](https://github.com/dfa1/vortex-java/issues/250))
17+
- `RaincloudConformanceIntegrationTest` now writes both the vortex-java and Parquet oracle CSVs to temp files and streams them line-by-line for comparison; previously it materialized both into heap `String` objects, causing OOM for large corpus files (≥157 MB) that were previously hidden by throwing before reaching the corrupted line. ([#254](https://github.com/dfa1/vortex-java/issues/254))
1718

1819
## [0.12.1] — 2026-07-08
1920

integration/src/test/java/io/github/dfa1/vortex/integration/RaincloudConformanceIntegrationTest.java

Lines changed: 76 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
import org.junit.jupiter.api.TestFactory;
1515
import org.opentest4j.TestAbortedException;
1616

17+
import java.io.BufferedReader;
1718
import java.io.IOException;
18-
import java.io.StringWriter;
1919
import java.io.UncheckedIOException;
20+
import java.io.Writer;
2021
import java.nio.file.Files;
2122
import java.nio.file.Path;
2223
import java.util.HashMap;
@@ -25,7 +26,6 @@
2526
import java.util.stream.Stream;
2627

2728
import static org.assertj.core.api.Assertions.assertThat;
28-
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2929
import static org.junit.jupiter.api.Assumptions.assumeTrue;
3030

3131
/// Real-world cross-implementation conformance over the Raincloud corpus
@@ -83,14 +83,21 @@ Stream<DynamicTest> conformancePerSlug() throws IOException {
8383
}
8484

8585
private static void assertMatchesParquetOracle(Path vortex, Path parquet) throws IOException {
86-
// Given
87-
List<String> oracleLines = oracleLines(parquet);
86+
// Given — write both CSVs to temp files; never hold multi-GB strings in heap
87+
Path oracleCsv = Files.createTempFile("oracle-", ".csv");
88+
Path vortexCsv = Files.createTempFile("vortex-", ".csv");
89+
try {
90+
writeOracle(parquet, oracleCsv);
8891

89-
// When
90-
List<String> result = exportVortex(vortex);
92+
// When
93+
CsvExporter.exportCsv(vortex, vortexCsv, ExportOptions.defaults());
9194

92-
// Then
93-
assertLinesMatch(result, oracleLines);
95+
// Then
96+
assertFilesMatch(vortexCsv, oracleCsv);
97+
} finally {
98+
Files.deleteIfExists(oracleCsv);
99+
Files.deleteIfExists(vortexCsv);
100+
}
94101
}
95102

96103
/// Runs the full conformance check but reports the outcome as an aborted test
@@ -108,64 +115,89 @@ private static void reportUntriaged(Path vortex, Path parquet) {
108115
throw new TestAbortedException("untriaged slug passes — flip its matrix entry to ok");
109116
}
110117

111-
private static void assertStillFails(Path vortex, Path parquet, String status) {
118+
private static void assertStillFails(Path vortex, Path parquet, String status) throws IOException {
112119
// Given / When — a decode gap still reproduces when the export itself throws.
113120
// Only VortexException (the contractual untrusted-input failure) and, narrowly,
114121
// IndexOutOfBoundsException count: the latter is itself a bounds-guard bug (#215
115122
// throws it today) and this arm dies with that fix — a blanket RuntimeException
116123
// catch would green unrelated regressions (NPEs, ...). No oracle needed here
117124
// (the oracle may not even read this parquet, e.g. nested columns).
118-
List<String> result;
125+
Path vortexCsv = Files.createTempFile("vortex-", ".csv");
119126
try {
120-
result = exportVortex(vortex);
121-
} catch (VortexException | IndexOutOfBoundsException e) {
122-
return;
123-
} catch (IOException e) {
124-
throw new UncheckedIOException(e);
125-
}
126-
127-
// Then — the export now succeeds, so the gap must still be a silent-corruption
128-
// one (e.g. #208): values must still mismatch the oracle. A clean pass means the
129-
// gap was fixed: flip the expected-status.csv entry to ok in the same change
130-
List<String> oracleLines = oracleLines(parquet);
131-
assertThatThrownBy(() -> assertLinesMatch(result, oracleLines))
132-
.as("known gap %s no longer reproduces — flip its matrix entry to ok", status)
133-
.isInstanceOf(AssertionError.class);
134-
}
127+
try {
128+
CsvExporter.exportCsv(vortex, vortexCsv, ExportOptions.defaults());
129+
} catch (VortexException | IndexOutOfBoundsException e) {
130+
return; // gap still reproduces as a thrown exception
131+
}
135132

136-
private static void assertLinesMatch(List<String> result, List<String> oracleLines) {
137-
assertThat(result).hasSameSizeAs(oracleLines);
138-
for (int i = 0; i < oracleLines.size(); i++) {
139-
assertThat(result.get(i)).as("line %d", i + 1).isEqualTo(oracleLines.get(i));
133+
// Then — the export now succeeds, so the gap must still be a silent-corruption
134+
// one (e.g. #208): values must still mismatch the oracle. A clean pass means the
135+
// gap was fixed: flip the expected-status.csv entry to ok in the same change
136+
Path oracleCsv = Files.createTempFile("oracle-", ".csv");
137+
try {
138+
writeOracle(parquet, oracleCsv);
139+
boolean stillMismatches = false;
140+
try {
141+
assertFilesMatch(vortexCsv, oracleCsv);
142+
} catch (AssertionError ignored) {
143+
stillMismatches = true;
144+
}
145+
assertThat(stillMismatches)
146+
.as("known gap %s no longer reproduces — flip its matrix entry to ok", status)
147+
.isTrue();
148+
} finally {
149+
Files.deleteIfExists(oracleCsv);
150+
}
151+
} finally {
152+
Files.deleteIfExists(vortexCsv);
140153
}
141154
}
142155

143-
private static List<String> exportVortex(Path vortex) throws IOException {
144-
StringWriter out = new StringWriter();
145-
CsvExporter.exportCsv(vortex, out, ExportOptions.defaults());
146-
return out.toString().lines().toList();
156+
/// Stream-compares two CSV files line by line without loading either into heap.
157+
///
158+
/// @param result the vortex-java export
159+
/// @param oracle the parquet oracle export
160+
/// @throws AssertionError if any line differs or the files have different lengths
161+
/// @throws IOException if either file cannot be read
162+
private static void assertFilesMatch(Path result, Path oracle) throws IOException {
163+
try (BufferedReader r = Files.newBufferedReader(result);
164+
BufferedReader o = Files.newBufferedReader(oracle)) {
165+
long lineNum = 0;
166+
String rLine;
167+
while ((rLine = r.readLine()) != null) {
168+
lineNum++;
169+
String oLine = o.readLine();
170+
assertThat(oLine).as("oracle ended before vortex output at line %d", lineNum).isNotNull();
171+
assertThat(rLine).as("line %d", lineNum).isEqualTo(oLine);
172+
}
173+
assertThat(o.readLine()).as("vortex output ended before oracle at line %d", lineNum + 1).isNull();
174+
}
147175
}
148176

149-
/// Reads the parquet sibling through hardwood; an oracle-side failure (nested
150-
/// columns, unsupported physical type) aborts the slug rather than failing it —
151-
/// it says nothing about vortex-java. Deliberate asymmetry: an `ok` slug whose
152-
/// parquet the oracle cannot read stops being verified (visibly, as skipped) —
153-
/// widen the oracle rather than let unverifiable entries fail the build.
154-
private static List<String> oracleLines(Path parquet) {
155-
StringWriter out = new StringWriter();
156-
try {
157-
writeOracleCsv(parquet, out);
177+
/// Writes the parquet oracle to a file using the same cell rules as `CsvExporter`.
178+
/// An oracle-side failure (nested columns, unsupported physical type) aborts the
179+
/// slug via [TestAbortedException] rather than failing it — it says nothing about
180+
/// vortex-java. An `ok` slug whose parquet cannot be read stops being verified
181+
/// (visibly, as skipped) — widen the oracle rather than let unverifiable entries
182+
/// fail the build.
183+
///
184+
/// @param parquet the parquet sibling
185+
/// @param out the output file to write CSV into
186+
/// @throws TestAbortedException if the oracle cannot read this parquet
187+
/// @throws IOException if writing fails
188+
private static void writeOracle(Path parquet, Path out) throws IOException {
189+
try (Writer writer = Files.newBufferedWriter(out)) {
190+
writeOracleCsv(parquet, writer);
158191
} catch (TestAbortedException e) {
159192
throw e;
160193
} catch (Exception e) {
161194
throw new TestAbortedException("oracle cannot read the parquet sibling: " + e);
162195
}
163-
return out.toString().lines().toList();
164196
}
165197

166198
/// Oracle: hardwood reads the parquet sibling and emits CSV through the same
167199
/// fastcsv writer configuration as `CsvExporter`, using its exact cell rules.
168-
private static void writeOracleCsv(Path parquet, StringWriter out) throws IOException {
200+
private static void writeOracleCsv(Path parquet, Writer out) throws IOException {
169201
try (ParquetFileReader pfr = ParquetFileReader.open(InputFile.of(parquet));
170202
RowReader rows = pfr.rowReader();
171203
CsvWriter csv = CsvWriter.builder().fieldSeparator(',').build(out)) {

integration/src/test/resources/raincloud/expected-status.csv

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ bi-cmsprovider,untriaged
2929
bi-commongovernment,untriaged
3030
bi-corporations,ok
3131
bi-eixo,untriaged
32-
bi-euro2016,ok
32+
bi-euro2016,gap:252
3333
bi-food,ok
3434
bi-generico,untriaged
3535
bi-hashtags,ok
@@ -82,7 +82,7 @@ coig,untriaged
8282
coronahack-chest-xraydataset,untriaged
8383
cosmopedia-stanford,untriaged
8484
countries-of-the-world,ok
85-
covid-world-vaccination-progress,ok
85+
covid-world-vaccination-progress,gap:253
8686
covid19-data-from-john-hopkins-university,untriaged
8787
crimes-in-boston,untriaged
8888
databricks-dolly-15k,untriaged

0 commit comments

Comments
 (0)