indices = StringUtils.findAllIndices("hello", 'l');
+ assertEquals(2, indices.size());
+ assertEquals(2, indices.get(0));
+ assertEquals(3, indices.get(1));
+
+ indices = StringUtils.findAllIndices("aaa", 'a');
+ assertEquals(3, indices.size());
+
+ indices = StringUtils.findAllIndices("hello", 'z');
+ assertTrue(indices.isEmpty());
+
+ indices = StringUtils.findAllIndices("", 'a');
+ assertTrue(indices.isEmpty());
+
+ indices = StringUtils.findAllIndices(null, 'a');
+ assertTrue(indices.isEmpty());
+ }
+
+ @Test
+ void testIsNumeric() {
+ assertTrue(StringUtils.isNumeric("12345"));
+ assertTrue(StringUtils.isNumeric("0"));
+ assertTrue(StringUtils.isNumeric("007"));
+
+ assertFalse(StringUtils.isNumeric("12.34"));
+ assertFalse(StringUtils.isNumeric("-123"));
+ assertFalse(StringUtils.isNumeric("abc"));
+ assertFalse(StringUtils.isNumeric("12a34"));
+ assertFalse(StringUtils.isNumeric(""));
+ assertFalse(StringUtils.isNumeric(null));
+ }
+
+ @Test
+ void testRepeat() {
+ assertEquals("abcabcabc", StringUtils.repeat("abc", 3));
+ assertEquals("aaa", StringUtils.repeat("a", 3));
+ assertEquals("", StringUtils.repeat("abc", 0));
+ assertEquals("", StringUtils.repeat("abc", -1));
+ assertEquals("", StringUtils.repeat(null, 3));
+ }
+
+ @Test
+ void testTruncate() {
+ assertEquals("hello", StringUtils.truncate("hello", 10));
+ assertEquals("hel...", StringUtils.truncate("hello world", 6));
+ assertEquals("hello...", StringUtils.truncate("hello world", 8));
+ assertEquals("", StringUtils.truncate("hello", 0));
+ assertEquals("", StringUtils.truncate(null, 10));
+ assertEquals("hel", StringUtils.truncate("hello", 3));
+ }
+
+ @Test
+ void testToTitleCase() {
+ assertEquals("Hello", StringUtils.toTitleCase("hello"));
+ assertEquals("Hello", StringUtils.toTitleCase("HELLO"));
+ assertEquals("Hello", StringUtils.toTitleCase("hELLO"));
+ assertEquals("A", StringUtils.toTitleCase("a"));
+ assertEquals("", StringUtils.toTitleCase(""));
+ assertNull(StringUtils.toTitleCase(null));
+ }
+}
diff --git a/codeflash-benchmark/codeflash_benchmark/version.py b/codeflash-benchmark/codeflash_benchmark/version.py
index 18606e8d2..616b1bc71 100644
--- a/codeflash-benchmark/codeflash_benchmark/version.py
+++ b/codeflash-benchmark/codeflash_benchmark/version.py
@@ -1,2 +1,2 @@
# These version placeholders will be replaced by uv-dynamic-versioning during build.
-__version__ = "0.3.0"
+__version__ = "0.20.1.post242.dev0+7c7eeb5b"
diff --git a/codeflash-java-runtime/pom.xml b/codeflash-java-runtime/pom.xml
new file mode 100644
index 000000000..cb95732dd
--- /dev/null
+++ b/codeflash-java-runtime/pom.xml
@@ -0,0 +1,131 @@
+
+
+ 4.0.0
+
+ com.codeflash
+ codeflash-runtime
+ 1.0.0
+ jar
+
+ CodeFlash Java Runtime
+ Runtime library for CodeFlash Java instrumentation and comparison
+
+
+ 11
+ 11
+ UTF-8
+
+
+
+
+
+ com.google.code.gson
+ gson
+ 2.10.1
+
+
+
+
+ com.esotericsoftware
+ kryo
+ 5.6.2
+
+
+
+
+ org.objenesis
+ objenesis
+ 3.4
+
+
+
+
+ org.xerial
+ sqlite-jdbc
+ 3.45.0.0
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.10.1
+ test
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ 11
+ 11
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.0.0
+
+
+ --add-opens java.base/java.util=ALL-UNNAMED
+ --add-opens java.base/java.lang=ALL-UNNAMED
+ --add-opens java.base/java.lang.reflect=ALL-UNNAMED
+ --add-opens java.base/java.math=ALL-UNNAMED
+ --add-opens java.base/java.io=ALL-UNNAMED
+ --add-opens java.base/java.net=ALL-UNNAMED
+ --add-opens java.base/java.time=ALL-UNNAMED
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.1
+
+
+ package
+
+ shade
+
+
+
+
+ com.codeflash.Comparator
+
+
+
+
+ *:*
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-install-plugin
+ 3.1.1
+
+
+
+
diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/BenchmarkContext.java b/codeflash-java-runtime/src/main/java/com/codeflash/BenchmarkContext.java
new file mode 100644
index 000000000..c3699f00c
--- /dev/null
+++ b/codeflash-java-runtime/src/main/java/com/codeflash/BenchmarkContext.java
@@ -0,0 +1,42 @@
+package com.codeflash;
+
+/**
+ * Context object for tracking benchmark timing.
+ *
+ * Created by {@link CodeFlash#startBenchmark(String)} and passed to
+ * {@link CodeFlash#endBenchmark(BenchmarkContext)}.
+ */
+public final class BenchmarkContext {
+
+ private final String methodId;
+ private final long startTime;
+
+ /**
+ * Create a new benchmark context.
+ *
+ * @param methodId Method being benchmarked
+ * @param startTime Start time in nanoseconds
+ */
+ BenchmarkContext(String methodId, long startTime) {
+ this.methodId = methodId;
+ this.startTime = startTime;
+ }
+
+ /**
+ * Get the method ID.
+ *
+ * @return Method identifier
+ */
+ public String getMethodId() {
+ return methodId;
+ }
+
+ /**
+ * Get the start time.
+ *
+ * @return Start time in nanoseconds
+ */
+ public long getStartTime() {
+ return startTime;
+ }
+}
diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/BenchmarkResult.java b/codeflash-java-runtime/src/main/java/com/codeflash/BenchmarkResult.java
new file mode 100644
index 000000000..dfe348e78
--- /dev/null
+++ b/codeflash-java-runtime/src/main/java/com/codeflash/BenchmarkResult.java
@@ -0,0 +1,160 @@
+package com.codeflash;
+
+import java.util.Arrays;
+
+/**
+ * Result of a benchmark run with statistical analysis.
+ *
+ * Provides JMH-style statistics including mean, standard deviation,
+ * and percentiles (p50, p90, p99).
+ */
+public final class BenchmarkResult {
+
+ private final String methodId;
+ private final long[] measurements;
+ private final long mean;
+ private final long stdDev;
+ private final long min;
+ private final long max;
+ private final long p50;
+ private final long p90;
+ private final long p99;
+
+ /**
+ * Create a benchmark result from raw measurements.
+ *
+ * @param methodId Method that was benchmarked
+ * @param measurements Array of timing measurements in nanoseconds
+ */
+ public BenchmarkResult(String methodId, long[] measurements) {
+ this.methodId = methodId;
+ this.measurements = measurements.clone();
+
+ // Sort for percentile calculations
+ long[] sorted = measurements.clone();
+ Arrays.sort(sorted);
+
+ this.min = sorted[0];
+ this.max = sorted[sorted.length - 1];
+ this.mean = calculateMean(sorted);
+ this.stdDev = calculateStdDev(sorted, this.mean);
+ this.p50 = percentile(sorted, 50);
+ this.p90 = percentile(sorted, 90);
+ this.p99 = percentile(sorted, 99);
+ }
+
+ private static long calculateMean(long[] values) {
+ long sum = 0;
+ for (long v : values) {
+ sum += v;
+ }
+ return sum / values.length;
+ }
+
+ private static long calculateStdDev(long[] values, long mean) {
+ if (values.length < 2) {
+ return 0;
+ }
+ long sumSquaredDiff = 0;
+ for (long v : values) {
+ long diff = v - mean;
+ sumSquaredDiff += diff * diff;
+ }
+ return (long) Math.sqrt(sumSquaredDiff / (values.length - 1));
+ }
+
+ private static long percentile(long[] sorted, int percentile) {
+ int index = (int) Math.ceil(percentile / 100.0 * sorted.length) - 1;
+ return sorted[Math.max(0, Math.min(index, sorted.length - 1))];
+ }
+
+ // Getters
+
+ public String getMethodId() {
+ return methodId;
+ }
+
+ public long[] getMeasurements() {
+ return measurements.clone();
+ }
+
+ public int getIterationCount() {
+ return measurements.length;
+ }
+
+ public long getMean() {
+ return mean;
+ }
+
+ public long getStdDev() {
+ return stdDev;
+ }
+
+ public long getMin() {
+ return min;
+ }
+
+ public long getMax() {
+ return max;
+ }
+
+ public long getP50() {
+ return p50;
+ }
+
+ public long getP90() {
+ return p90;
+ }
+
+ public long getP99() {
+ return p99;
+ }
+
+ /**
+ * Get mean in milliseconds.
+ */
+ public double getMeanMs() {
+ return mean / 1_000_000.0;
+ }
+
+ /**
+ * Get standard deviation in milliseconds.
+ */
+ public double getStdDevMs() {
+ return stdDev / 1_000_000.0;
+ }
+
+ /**
+ * Calculate coefficient of variation (CV) as percentage.
+ * CV = (stdDev / mean) * 100
+ * Lower is better (more stable measurements).
+ */
+ public double getCoefficientOfVariation() {
+ if (mean == 0) {
+ return 0;
+ }
+ return (stdDev * 100.0) / mean;
+ }
+
+ /**
+ * Check if measurements are stable (CV < 10%).
+ */
+ public boolean isStable() {
+ return getCoefficientOfVariation() < 10.0;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "BenchmarkResult{method='%s', mean=%.3fms, stdDev=%.3fms, p50=%.3fms, p90=%.3fms, p99=%.3fms, cv=%.1f%%, iterations=%d}",
+ methodId,
+ getMeanMs(),
+ getStdDevMs(),
+ p50 / 1_000_000.0,
+ p90 / 1_000_000.0,
+ p99 / 1_000_000.0,
+ getCoefficientOfVariation(),
+ measurements.length
+ );
+ }
+}
diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/Blackhole.java b/codeflash-java-runtime/src/main/java/com/codeflash/Blackhole.java
new file mode 100644
index 000000000..eeb6d4fd4
--- /dev/null
+++ b/codeflash-java-runtime/src/main/java/com/codeflash/Blackhole.java
@@ -0,0 +1,148 @@
+package com.codeflash;
+
+/**
+ * Utility class to prevent dead code elimination by the JIT compiler.
+ *
+ * Inspired by JMH's Blackhole class. When the JVM detects that a computed
+ * value is never used, it may eliminate the computation entirely. By
+ * "consuming" values through this class, we prevent such optimizations.
+ *
+ * Usage:
+ *
+ * int result = expensiveComputation();
+ * Blackhole.consume(result); // Prevents JIT from eliminating the computation
+ *
+ *
+ * The implementation uses volatile writes which act as memory barriers,
+ * preventing the JIT from optimizing away the computation.
+ */
+public final class Blackhole {
+
+ // Volatile fields act as memory barriers, preventing optimization
+ private static volatile int intSink;
+ private static volatile long longSink;
+ private static volatile double doubleSink;
+ private static volatile Object objectSink;
+
+ private Blackhole() {
+ // Utility class, no instantiation
+ }
+
+ /**
+ * Consume an int value to prevent dead code elimination.
+ *
+ * @param value Value to consume
+ */
+ public static void consume(int value) {
+ intSink = value;
+ }
+
+ /**
+ * Consume a long value to prevent dead code elimination.
+ *
+ * @param value Value to consume
+ */
+ public static void consume(long value) {
+ longSink = value;
+ }
+
+ /**
+ * Consume a double value to prevent dead code elimination.
+ *
+ * @param value Value to consume
+ */
+ public static void consume(double value) {
+ doubleSink = value;
+ }
+
+ /**
+ * Consume a float value to prevent dead code elimination.
+ *
+ * @param value Value to consume
+ */
+ public static void consume(float value) {
+ doubleSink = value;
+ }
+
+ /**
+ * Consume a boolean value to prevent dead code elimination.
+ *
+ * @param value Value to consume
+ */
+ public static void consume(boolean value) {
+ intSink = value ? 1 : 0;
+ }
+
+ /**
+ * Consume a byte value to prevent dead code elimination.
+ *
+ * @param value Value to consume
+ */
+ public static void consume(byte value) {
+ intSink = value;
+ }
+
+ /**
+ * Consume a short value to prevent dead code elimination.
+ *
+ * @param value Value to consume
+ */
+ public static void consume(short value) {
+ intSink = value;
+ }
+
+ /**
+ * Consume a char value to prevent dead code elimination.
+ *
+ * @param value Value to consume
+ */
+ public static void consume(char value) {
+ intSink = value;
+ }
+
+ /**
+ * Consume an Object to prevent dead code elimination.
+ * Works for any reference type including arrays and collections.
+ *
+ * @param value Value to consume
+ */
+ public static void consume(Object value) {
+ objectSink = value;
+ }
+
+ /**
+ * Consume an int array to prevent dead code elimination.
+ *
+ * @param values Array to consume
+ */
+ public static void consume(int[] values) {
+ objectSink = values;
+ if (values != null && values.length > 0) {
+ intSink = values[0];
+ }
+ }
+
+ /**
+ * Consume a long array to prevent dead code elimination.
+ *
+ * @param values Array to consume
+ */
+ public static void consume(long[] values) {
+ objectSink = values;
+ if (values != null && values.length > 0) {
+ longSink = values[0];
+ }
+ }
+
+ /**
+ * Consume a double array to prevent dead code elimination.
+ *
+ * @param values Array to consume
+ */
+ public static void consume(double[] values) {
+ objectSink = values;
+ if (values != null && values.length > 0) {
+ doubleSink = values[0];
+ }
+ }
+}
diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/CodeFlash.java b/codeflash-java-runtime/src/main/java/com/codeflash/CodeFlash.java
new file mode 100644
index 000000000..bde06a335
--- /dev/null
+++ b/codeflash-java-runtime/src/main/java/com/codeflash/CodeFlash.java
@@ -0,0 +1,264 @@
+package com.codeflash;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Main API for CodeFlash runtime instrumentation.
+ *
+ * Provides methods for:
+ * - Capturing function inputs/outputs for behavior verification
+ * - Benchmarking with JMH-inspired best practices
+ * - Preventing dead code elimination
+ *
+ * Usage:
+ *
+ * // Behavior capture
+ * CodeFlash.captureInput("Calculator.add", a, b);
+ * int result = a + b;
+ * return CodeFlash.captureOutput("Calculator.add", result);
+ *
+ * // Benchmarking
+ * BenchmarkContext ctx = CodeFlash.startBenchmark("Calculator.add");
+ * // ... code to benchmark ...
+ * CodeFlash.endBenchmark(ctx);
+ *
+ */
+public final class CodeFlash {
+
+ private static final AtomicLong callIdCounter = new AtomicLong(0);
+ private static volatile ResultWriter resultWriter;
+ private static volatile boolean initialized = false;
+ private static volatile String outputFile;
+
+ // Configuration from environment variables
+ private static final int DEFAULT_WARMUP_ITERATIONS = 10;
+ private static final int DEFAULT_MEASUREMENT_ITERATIONS = 20;
+
+ static {
+ // Register shutdown hook to flush results
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ if (resultWriter != null) {
+ resultWriter.close();
+ }
+ }));
+ }
+
+ private CodeFlash() {
+ // Utility class, no instantiation
+ }
+
+ /**
+ * Initialize CodeFlash with output file path.
+ * Called automatically if CODEFLASH_OUTPUT_FILE env var is set.
+ *
+ * @param outputPath Path to output file (SQLite database)
+ */
+ public static synchronized void initialize(String outputPath) {
+ if (!initialized || !outputPath.equals(outputFile)) {
+ outputFile = outputPath;
+ Path path = Paths.get(outputPath);
+ resultWriter = new ResultWriter(path);
+ initialized = true;
+ }
+ }
+
+ /**
+ * Get or create the result writer, initializing from environment if needed.
+ */
+ private static ResultWriter getWriter() {
+ if (!initialized) {
+ String envPath = System.getenv("CODEFLASH_OUTPUT_FILE");
+ if (envPath != null && !envPath.isEmpty()) {
+ initialize(envPath);
+ } else {
+ // Default to temp file if no env var
+ initialize(System.getProperty("java.io.tmpdir") + "/codeflash_results.db");
+ }
+ }
+ return resultWriter;
+ }
+
+ /**
+ * Capture function input arguments.
+ *
+ * @param methodId Unique identifier for the method (e.g., "Calculator.add")
+ * @param args Input arguments
+ */
+ public static void captureInput(String methodId, Object... args) {
+ long callId = callIdCounter.incrementAndGet();
+ byte[] argsBytes = Serializer.serialize(args);
+ getWriter().recordInput(callId, methodId, argsBytes, System.nanoTime());
+ }
+
+ /**
+ * Capture function output and return it (for chaining in return statements).
+ *
+ * @param methodId Unique identifier for the method
+ * @param result The result value
+ * @param Type of the result
+ * @return The same result (for chaining)
+ */
+ public static T captureOutput(String methodId, T result) {
+ long callId = callIdCounter.get(); // Use same callId as input
+ byte[] resultBytes = Serializer.serialize(result);
+ getWriter().recordOutput(callId, methodId, resultBytes, System.nanoTime());
+ return result;
+ }
+
+ /**
+ * Capture an exception thrown by the function.
+ *
+ * @param methodId Unique identifier for the method
+ * @param error The exception
+ */
+ public static void captureException(String methodId, Throwable error) {
+ long callId = callIdCounter.get();
+ byte[] errorBytes = Serializer.serializeException(error);
+ getWriter().recordError(callId, methodId, errorBytes, System.nanoTime());
+ }
+
+ /**
+ * Start a benchmark context for timing code execution.
+ * Implements JMH-inspired warmup and measurement phases.
+ *
+ * @param methodId Unique identifier for the method being benchmarked
+ * @return BenchmarkContext to pass to endBenchmark
+ */
+ public static BenchmarkContext startBenchmark(String methodId) {
+ return new BenchmarkContext(methodId, System.nanoTime());
+ }
+
+ /**
+ * End a benchmark and record the timing.
+ *
+ * @param ctx The benchmark context from startBenchmark
+ */
+ public static void endBenchmark(BenchmarkContext ctx) {
+ long endTime = System.nanoTime();
+ long duration = endTime - ctx.getStartTime();
+ getWriter().recordBenchmark(ctx.getMethodId(), duration, endTime);
+ }
+
+ /**
+ * Run a benchmark with proper JMH-style warmup and measurement.
+ *
+ * @param methodId Unique identifier for the method
+ * @param runnable Code to benchmark
+ * @return Benchmark result with statistics
+ */
+ public static BenchmarkResult runBenchmark(String methodId, Runnable runnable) {
+ int warmupIterations = getWarmupIterations();
+ int measurementIterations = getMeasurementIterations();
+
+ // Warmup phase - results discarded
+ for (int i = 0; i < warmupIterations; i++) {
+ runnable.run();
+ }
+
+ // Suggest GC before measurement (hint only, not guaranteed)
+ System.gc();
+
+ // Measurement phase
+ long[] measurements = new long[measurementIterations];
+ for (int i = 0; i < measurementIterations; i++) {
+ long start = System.nanoTime();
+ runnable.run();
+ measurements[i] = System.nanoTime() - start;
+ }
+
+ BenchmarkResult result = new BenchmarkResult(methodId, measurements);
+ getWriter().recordBenchmarkResult(methodId, result);
+ return result;
+ }
+
+ /**
+ * Run a benchmark that returns a value (prevents dead code elimination).
+ *
+ * @param methodId Unique identifier for the method
+ * @param supplier Code to benchmark that returns a value
+ * @param Return type
+ * @return Benchmark result with statistics
+ */
+ public static BenchmarkResult runBenchmarkWithResult(String methodId, java.util.function.Supplier supplier) {
+ int warmupIterations = getWarmupIterations();
+ int measurementIterations = getMeasurementIterations();
+
+ // Warmup phase - consume results to prevent dead code elimination
+ for (int i = 0; i < warmupIterations; i++) {
+ Blackhole.consume(supplier.get());
+ }
+
+ // Suggest GC before measurement
+ System.gc();
+
+ // Measurement phase
+ long[] measurements = new long[measurementIterations];
+ for (int i = 0; i < measurementIterations; i++) {
+ long start = System.nanoTime();
+ T result = supplier.get();
+ measurements[i] = System.nanoTime() - start;
+ Blackhole.consume(result); // Prevent dead code elimination
+ }
+
+ BenchmarkResult benchmarkResult = new BenchmarkResult(methodId, measurements);
+ getWriter().recordBenchmarkResult(methodId, benchmarkResult);
+ return benchmarkResult;
+ }
+
+ /**
+ * Get warmup iterations from environment or use default.
+ */
+ private static int getWarmupIterations() {
+ String env = System.getenv("CODEFLASH_WARMUP_ITERATIONS");
+ if (env != null) {
+ try {
+ return Integer.parseInt(env);
+ } catch (NumberFormatException e) {
+ // Use default
+ }
+ }
+ return DEFAULT_WARMUP_ITERATIONS;
+ }
+
+ /**
+ * Get measurement iterations from environment or use default.
+ */
+ private static int getMeasurementIterations() {
+ String env = System.getenv("CODEFLASH_MEASUREMENT_ITERATIONS");
+ if (env != null) {
+ try {
+ return Integer.parseInt(env);
+ } catch (NumberFormatException e) {
+ // Use default
+ }
+ }
+ return DEFAULT_MEASUREMENT_ITERATIONS;
+ }
+
+ /**
+ * Get the current call ID (for correlation).
+ *
+ * @return Current call ID
+ */
+ public static long getCurrentCallId() {
+ return callIdCounter.get();
+ }
+
+ /**
+ * Reset the call ID counter (for testing).
+ */
+ public static void resetCallId() {
+ callIdCounter.set(0);
+ }
+
+ /**
+ * Force flush all pending writes.
+ */
+ public static void flush() {
+ if (resultWriter != null) {
+ resultWriter.flush();
+ }
+ }
+}
diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/Comparator.java b/codeflash-java-runtime/src/main/java/com/codeflash/Comparator.java
new file mode 100644
index 000000000..32d9f6034
--- /dev/null
+++ b/codeflash-java-runtime/src/main/java/com/codeflash/Comparator.java
@@ -0,0 +1,699 @@
+package com.codeflash;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.*;
+
+/**
+ * Deep object comparison for verifying serialization/deserialization correctness.
+ *
+ * This comparator is used to verify that objects survive the serialize-deserialize
+ * cycle correctly. It handles:
+ * - Primitives and wrappers with epsilon tolerance for floats
+ * - Collections, Maps, and Arrays
+ * - Custom objects via reflection
+ * - NaN and Infinity special cases
+ * - Exception comparison
+ * - Placeholder rejection
+ */
+public final class Comparator {
+
+ private static final double EPSILON = 1e-9;
+
+ private Comparator() {
+ // Utility class
+ }
+
+ /**
+ * CLI entry point for comparing test results from two SQLite databases.
+ *
+ * Reads Kryo-serialized BLOBs from the test_results table, deserializes them,
+ * and compares using deep object comparison.
+ *
+ * Outputs JSON to stdout:
+ * {"equivalent": true/false, "totalInvocations": N, "diffs": [...]}
+ *
+ * Exit code: 0 if equivalent, 1 if different.
+ */
+ public static void main(String[] args) {
+ if (args.length != 2) {
+ System.err.println("Usage: java com.codeflash.Comparator ");
+ System.exit(2);
+ return;
+ }
+
+ String originalDbPath = args[0];
+ String candidateDbPath = args[1];
+
+ try {
+ Class.forName("org.sqlite.JDBC");
+ } catch (ClassNotFoundException e) {
+ printError("SQLite JDBC driver not found: " + e.getMessage());
+ System.exit(2);
+ return;
+ }
+
+ Map originalResults;
+ Map candidateResults;
+
+ try {
+ originalResults = readTestResults(originalDbPath);
+ } catch (Exception e) {
+ printError("Failed to read original database: " + e.getMessage());
+ System.exit(2);
+ return;
+ }
+
+ try {
+ candidateResults = readTestResults(candidateDbPath);
+ } catch (Exception e) {
+ printError("Failed to read candidate database: " + e.getMessage());
+ System.exit(2);
+ return;
+ }
+
+ Set allKeys = new LinkedHashSet<>();
+ allKeys.addAll(originalResults.keySet());
+ allKeys.addAll(candidateResults.keySet());
+
+ List diffs = new ArrayList<>();
+ int totalInvocations = allKeys.size();
+
+ for (String key : allKeys) {
+ byte[] origBytes = originalResults.get(key);
+ byte[] candBytes = candidateResults.get(key);
+
+ if (origBytes == null && candBytes == null) {
+ // Both null (void methods) — equivalent
+ continue;
+ }
+
+ if (origBytes == null) {
+ Object candObj = safeDeserialize(candBytes);
+ diffs.add(formatDiff("missing", key, 0, null, safeToString(candObj)));
+ continue;
+ }
+
+ if (candBytes == null) {
+ Object origObj = safeDeserialize(origBytes);
+ diffs.add(formatDiff("missing", key, 0, safeToString(origObj), null));
+ continue;
+ }
+
+ Object origObj = safeDeserialize(origBytes);
+ Object candObj = safeDeserialize(candBytes);
+
+ try {
+ if (!compare(origObj, candObj)) {
+ diffs.add(formatDiff("return_value", key, 0, safeToString(origObj), safeToString(candObj)));
+ }
+ } catch (KryoPlaceholderAccessException e) {
+ // Placeholder detected — skip comparison for this invocation
+ continue;
+ }
+ }
+
+ boolean equivalent = diffs.isEmpty();
+
+ StringBuilder json = new StringBuilder();
+ json.append("{\"equivalent\":").append(equivalent);
+ json.append(",\"totalInvocations\":").append(totalInvocations);
+ json.append(",\"diffs\":[");
+ for (int i = 0; i < diffs.size(); i++) {
+ if (i > 0) json.append(",");
+ json.append(diffs.get(i));
+ }
+ json.append("]}");
+
+ System.out.println(json.toString());
+ System.exit(equivalent ? 0 : 1);
+ }
+
+ private static Map readTestResults(String dbPath) throws Exception {
+ Map results = new LinkedHashMap<>();
+ String url = "jdbc:sqlite:" + dbPath;
+
+ try (Connection conn = DriverManager.getConnection(url);
+ Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(
+ "SELECT iteration_id, return_value FROM test_results WHERE loop_index = 1")) {
+ while (rs.next()) {
+ String iterationId = rs.getString("iteration_id");
+ byte[] returnValue = rs.getBytes("return_value");
+ // Strip the CODEFLASH_TEST_ITERATION suffix (e.g. "7_0" -> "7")
+ // Original runs with _0, candidate with _1, but the test iteration
+ // counter before the underscore is what identifies the invocation.
+ int lastUnderscore = iterationId.lastIndexOf('_');
+ if (lastUnderscore > 0) {
+ iterationId = iterationId.substring(0, lastUnderscore);
+ }
+ results.put(iterationId, returnValue);
+ }
+ }
+ return results;
+ }
+
+ private static Object safeDeserialize(byte[] data) {
+ if (data == null) {
+ return null;
+ }
+ try {
+ return Serializer.deserialize(data);
+ } catch (Exception e) {
+ return java.util.Map.of("__type", "DeserializationError", "error", String.valueOf(e.getMessage()));
+ }
+ }
+
+ private static String safeToString(Object obj) {
+ if (obj == null) {
+ return "null";
+ }
+ try {
+ if (obj.getClass().isArray()) {
+ return java.util.Arrays.deepToString(new Object[]{obj});
+ }
+ return String.valueOf(obj);
+ } catch (Exception e) {
+ return "";
+ }
+ }
+
+ private static String formatDiff(String scope, String methodId, int callId,
+ String originalValue, String candidateValue) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{\"scope\":\"").append(escapeJson(scope)).append("\"");
+ sb.append(",\"methodId\":\"").append(escapeJson(methodId)).append("\"");
+ sb.append(",\"callId\":").append(callId);
+ sb.append(",\"originalValue\":").append(jsonStringOrNull(originalValue));
+ sb.append(",\"candidateValue\":").append(jsonStringOrNull(candidateValue));
+ sb.append("}");
+ return sb.toString();
+ }
+
+ private static String jsonStringOrNull(String value) {
+ if (value == null) {
+ return "null";
+ }
+ return "\"" + escapeJson(value) + "\"";
+ }
+
+ private static String escapeJson(String s) {
+ if (s == null) return "";
+ return s.replace("\\", "\\\\")
+ .replace("\"", "\\\"")
+ .replace("\n", "\\n")
+ .replace("\r", "\\r")
+ .replace("\t", "\\t");
+ }
+
+ private static void printError(String message) {
+ System.out.println("{\"error\":\"" + escapeJson(message) + "\"}");
+ }
+
+ /**
+ * Compare two objects for deep equality.
+ *
+ * @param orig The original object
+ * @param newObj The object to compare against
+ * @return true if objects are equivalent
+ * @throws KryoPlaceholderAccessException if comparison involves a placeholder
+ */
+ public static boolean compare(Object orig, Object newObj) {
+ return compareInternal(orig, newObj, new IdentityHashMap<>());
+ }
+
+ /**
+ * Compare two objects, returning a detailed result.
+ *
+ * @param orig The original object
+ * @param newObj The object to compare against
+ * @return ComparisonResult with details about the comparison
+ */
+ public static ComparisonResult compareWithDetails(Object orig, Object newObj) {
+ try {
+ boolean equal = compareInternal(orig, newObj, new IdentityHashMap<>());
+ return new ComparisonResult(equal, null);
+ } catch (KryoPlaceholderAccessException e) {
+ return new ComparisonResult(false, e.getMessage());
+ }
+ }
+
+ private static boolean compareInternal(Object orig, Object newObj,
+ IdentityHashMap