TraceVerifier.java

package org.djutils.traceverifier;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

/**
 * Create or verify identity of a trace of states of a system. <br>
 * E.g., A simulation that is run with the same initial conditions (including random seed) should behave in a 100% predictable
 * manner. If it does not, there is some source of randomness that probably should be eliminated. This package can help locate
 * where the various runs of the simulation start to deviate from one another. <br>
 * The simulation must be instrumented with calls to the <code>sample</code> method. The sample method either records these
 * samples in a file, or compares these samples with the values that were stored in the file in a previous run. When a
 * difference occurs, the sample method throws an exception. <br>
 * <br>
 * Copyright (c) 2020-2024 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
 * for project information <a href="https://djutils.org" target="_blank"> https://djutils.org</a>. The DJUTILS project is
 * distributed under a three-clause BSD-style license, which can be found at
 * <a href="https://djutils.org/docs/license.html" target="_blank"> https://djutils.org/docs/license.html</a>. <br>
 * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
 * @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
 */
public class TraceVerifier implements Closeable
{
    /** Name of the output file (when in writing mode). */
    private final String outputFileName;

    /** Reader for existing trace file. */
    private final BufferedReader reader;

    /**
     * Create a new TraceVerifier.
     * @param fileName String; name of the file for the trace
     * @throws IOException when reading or writing fails
     */
    public TraceVerifier(final String fileName) throws IOException
    {
        // System.out.println("Creating TraceVerifier; file name is \"" + fileName + "\"");
        File traceFile = new File(fileName);
        if (traceFile.exists())
        {
            // Verify mode
            this.reader = new BufferedReader(new FileReader(fileName));
            this.outputFileName = null;
        }
        else
        {
            // Record mode
            this.outputFileName = fileName;
            BufferedWriter writer = new BufferedWriter(new FileWriter(fileName));
            writer.close();
            this.reader = null;
        }
    }

    /**
     * Add or compare one sample.
     * @param description String; some kind of description of the sample (usually some kind of time stamp).
     * @param state String; summary of the state of the process that is sampled.
     * @throws IOException when reading or writing fails
     * @throws TraceVerifierException on detection of a sample discrepancy
     */
    public void sample(final String description, final String state) throws IOException
    {
        String got = String.format("%s: %s", description, state);
        if (this.reader != null)
        {
            String expected = this.reader.readLine();
            if (expected.equals(got))
            {
                return;
            }
            int indexOfFirstDifference = 0;
            while (got.charAt(indexOfFirstDifference) == expected.charAt(indexOfFirstDifference))
            {
                indexOfFirstDifference++;
            }
            String format =
                    indexOfFirstDifference == 0 ? "Discrepancy found.\n%%-8.8s: \"%%s\"\n%%-8.8s: \"%%s\"\n%%-8.8s:  %%s^"
                            : String.format("Discrepancy found.\n%%-8.8s: \"%%s\"\n%%-8.8s: \"%%s\"\n%%-8.8s:  %%%d.%ds^",
                                    indexOfFirstDifference, indexOfFirstDifference);
            String error = String.format(format, "Got", got, "Expected", expected, "1st diff", "");
            throw new TraceVerifierException(error);
        }
        BufferedWriter writer = new BufferedWriter(new FileWriter(this.outputFileName, true));
        writer.append(got);
        writer.append('\n');
        writer.close();
    }

    @Override
    public void close() throws IOException
    {
        if (null != this.reader)
        {
            this.reader.close();
        }
    }

    @Override
    public String toString()
    {
        return "TraceVerifier [reader=" + this.reader + ", outputFileName=" + this.outputFileName + "]";
    }

}