All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.calcite.test.DiffTestCase Maven / Gradle / Ivy

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to you under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.calcite.test;

import org.apache.calcite.util.ReflectUtil;
import org.apache.calcite.util.TestUtil;
import org.apache.calcite.util.Util;

import org.incava.diff.Diff;
import org.incava.diff.Difference;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;

/**
 * DiffTestCase is an abstract base for JUnit tests which produce multi-line
 * output to be verified by diffing against a pre-existing reference file.
 */
public abstract class DiffTestCase {
  //~ Instance fields --------------------------------------------------------

  private final String testCaseName;

  /**
   * Name of current .log file.
   */
  protected File logFile;

  /**
   * Name of current .ref file.
   */
  protected File refFile;

  /**
   * OutputStream for current test log.
   */
  protected OutputStream logOutputStream;

  /** Diff masks defined so far. */
  private String diffMasks;
  Pattern compiledDiffPattern;
  Matcher compiledDiffMatcher;
  private String ignorePatterns;
  Pattern compiledIgnorePattern;
  Matcher compiledIgnoreMatcher;

  /**
   * Whether to give verbose message if diff fails.
   */
  private boolean verbose;

  /**
   * Initializes a new DiffTestCase.
   *
   * @param testCaseName Test case name
   */
  protected DiffTestCase(String testCaseName) throws Exception {
    this.testCaseName = testCaseName;
    // diffMasks = new ArrayList();
    diffMasks = "";
    ignorePatterns = "";
    compiledIgnoreMatcher = null;
    compiledDiffMatcher = null;
    String verboseVal =
        System.getProperty(DiffTestCase.class.getName() + ".verbose");
    if (verboseVal != null) {
      verbose = true;
    }
  }

  //~ Methods ----------------------------------------------------------------

  @BeforeEach
  protected void setUp() {
    // diffMasks.clear();
    diffMasks = "";
    ignorePatterns = "";
    compiledIgnoreMatcher = null;
    compiledDiffMatcher = null;
  }

  @AfterEach
  protected void tearDown() throws IOException {
    if (logOutputStream != null) {
      logOutputStream.close();
      logOutputStream = null;
    }
  }

  /**
   * Initializes a diff-based test. Any existing .log and .dif files
   * corresponding to this test case are deleted, and a new, empty .log file
   * is created. The default log file location is a subdirectory under the
   * result getTestlogRoot(), where the subdirectory name is based on the
   * unqualified name of the test class. The generated log file name will be
   * testMethodName.log, and the expected reference file will be
   * testMethodName.ref.
   *
   * @return Writer for log file, which caller should use as a destination for
   * test output to be diffed
   */
  protected Writer openTestLog() throws Exception {
    File testClassDir =
        new File(
            getTestlogRoot(),
            ReflectUtil.getUnqualifiedClassName(getClass()));
    testClassDir.mkdirs();
    File testLogFile =
        new File(
            testClassDir,
            testCaseName);
    return new OutputStreamWriter(
        openTestLogOutputStream(testLogFile), StandardCharsets.UTF_8);
  }

  /** Returns the root directory under which testlogs should be written. */
  protected abstract File getTestlogRoot() throws Exception;

  /**
   * Initializes a diff-based test, overriding the default log file naming
   * scheme altogether.
   *
   * @param testFileSansExt full path to log filename, without .log/.ref
   *                        extension
   */
  protected OutputStream openTestLogOutputStream(File testFileSansExt)
      throws IOException {
    assert logOutputStream == null;

    logFile = new File(testFileSansExt.toString() + ".log");
    logFile.delete();

    refFile = new File(testFileSansExt.toString() + ".ref");

    logOutputStream = new FileOutputStream(logFile);
    return logOutputStream;
  }

  /**
   * Finishes a diff-based test. Output that was written to the Writer
   * returned by openTestLog is diffed against a .ref file, and if any
   * differences are detected, the test case fails. Note that the diff used is
   * just a boolean test, and does not create any .dif ouput.
   *
   * 

NOTE: if you wrap the Writer returned by openTestLog() (e.g. with a * PrintWriter), be sure to flush the wrapping Writer before calling this * method.

* * @see #diffFile(File, File) */ protected void diffTestLog() throws IOException { assert logOutputStream != null; logOutputStream.close(); logOutputStream = null; if (!refFile.exists()) { fail("Reference file " + refFile + " does not exist"); } diffFile(logFile, refFile); } /** * Compares a log file with its reference log. * *

Usually, the log file and the reference log are in the same directory, * one ending with '.log' and the other with '.ref'. * *

If the files are identical, removes logFile. * * @param logFile Log file * @param refFile Reference log */ protected void diffFile(File logFile, File refFile) throws IOException { BufferedReader logReader = null; BufferedReader refReader = null; try { // NOTE: Use of diff.mask is deprecated, use diff_mask. String diffMask = System.getProperty("diff.mask", null); if (diffMask != null) { addDiffMask(diffMask); } diffMask = System.getProperty("diff_mask", null); if (diffMask != null) { addDiffMask(diffMask); } logReader = Util.reader(logFile); refReader = Util.reader(refFile); LineNumberReader logLineReader = new LineNumberReader(logReader); LineNumberReader refLineReader = new LineNumberReader(refReader); for (;;) { String logLine = logLineReader.readLine(); String refLine = refLineReader.readLine(); while ((logLine != null) && matchIgnorePatterns(logLine)) { // System.out.println("logMatch Line:" + logLine); logLine = logLineReader.readLine(); } while ((refLine != null) && matchIgnorePatterns(refLine)) { // System.out.println("refMatch Line:" + logLine); refLine = refLineReader.readLine(); } if ((logLine == null) || (refLine == null)) { if (logLine != null) { diffFail( logFile, logLineReader.getLineNumber()); } if (refLine != null) { diffFail( logFile, refLineReader.getLineNumber()); } break; } logLine = applyDiffMask(logLine); refLine = applyDiffMask(refLine); if (!logLine.equals(refLine)) { diffFail( logFile, logLineReader.getLineNumber()); } } } finally { if (logReader != null) { logReader.close(); } if (refReader != null) { refReader.close(); } } // no diffs detected, so delete redundant .log file logFile.delete(); } /** * Adds a diff mask. Strings matching the given regular expression will be * masked before diffing. This can be used to suppress spurious diffs on a * case-by-case basis. * * @param mask a regular expression, as per String.replaceAll */ protected void addDiffMask(String mask) { // diffMasks.add(mask); if (diffMasks.length() == 0) { diffMasks = mask; } else { diffMasks = diffMasks + "|" + mask; } compiledDiffPattern = Pattern.compile(diffMasks); compiledDiffMatcher = compiledDiffPattern.matcher(""); } protected void addIgnorePattern(String javaPattern) { if (ignorePatterns.length() == 0) { ignorePatterns = javaPattern; } else { ignorePatterns = ignorePatterns + "|" + javaPattern; } compiledIgnorePattern = Pattern.compile(ignorePatterns); compiledIgnoreMatcher = compiledIgnorePattern.matcher(""); } private String applyDiffMask(String s) { if (compiledDiffMatcher != null) { compiledDiffMatcher.reset(s); // we assume most of lines do not match // so compiled matches will be faster than replaceAll. if (compiledDiffMatcher.find()) { return compiledDiffPattern.matcher(s).replaceAll("XYZZY"); } } return s; } private boolean matchIgnorePatterns(String s) { if (compiledIgnoreMatcher != null) { compiledIgnoreMatcher.reset(s); return compiledIgnoreMatcher.matches(); } return false; } private void diffFail( File logFile, int lineNumber) { final String message = "diff detected at line " + lineNumber + " in " + logFile; if (verbose) { if (inIde()) { // If we're in IntelliJ, it's worth printing the 'expected // <...> actual <...>' string, because IntelliJ can format // this intelligently. Otherwise, use the more concise // diff format. assertEquals(fileContents(refFile), fileContents(logFile), message); } else { String s = diff(refFile, logFile); fail(message + '\n' + s + '\n'); } } fail(message); } /** * Returns whether this test is running inside the IntelliJ IDE. * * @return whether we're running in IntelliJ. */ private static boolean inIde() { Throwable runtimeException = new Throwable(); runtimeException.fillInStackTrace(); final StackTraceElement[] stackTrace = runtimeException.getStackTrace(); StackTraceElement lastStackTraceElement = stackTrace[stackTrace.length - 1]; // Junit test launched from IntelliJ 6.0 if (lastStackTraceElement.getClassName().equals( "com.intellij.rt.execution.junit.JUnitStarter") && lastStackTraceElement.getMethodName().equals("main")) { return true; } // Application launched from IntelliJ 6.0 if (lastStackTraceElement.getClassName().equals( "com.intellij.rt.execution.application.AppMain") && lastStackTraceElement.getMethodName().equals("main")) { return true; } return false; } /** * Returns a string containing the difference between the contents of two * files. The string has a similar format to the UNIX 'diff' utility. */ public static String diff(File file1, File file2) { List lines1 = fileLines(file1); List lines2 = fileLines(file2); return diffLines(lines1, lines2); } /** * Returns a string containing the difference between the two sets of lines. */ public static String diffLines(List lines1, List lines2) { final Diff differencer = new Diff<>(lines1, lines2); final List differences = differencer.execute(); StringWriter sw = new StringWriter(); int offset = 0; for (Difference d : differences) { final int as = d.getAddedStart() + 1; final int ae = d.getAddedEnd() + 1; final int ds = d.getDeletedStart() + 1; final int de = d.getDeletedEnd() + 1; if (ae == 0) { if (de == 0) { // no change } else { // a deletion: ",d" sw.append(String.valueOf(ds)); if (de > ds) { sw.append(",").append(String.valueOf(de)); } sw.append("d").append(String.valueOf(as - 1)).append('\n'); for (int i = ds - 1; i < de; ++i) { sw.append("< ").append(lines1.get(i)).append('\n'); } } } else { if (de == 0) { // an addition: "a" sw.append(String.valueOf(ds - 1)).append("a").append( String.valueOf(as)); if (ae > as) { sw.append(",").append(String.valueOf(ae)); } sw.append('\n'); for (int i = as - 1; i < ae; ++i) { sw.append("> ").append(lines2.get(i)).append('\n'); } } else { // a change: ",c, sw.append(String.valueOf(ds)); if (de > ds) { sw.append(",").append(String.valueOf(de)); } sw.append("c").append(String.valueOf(as)); if (ae > as) { sw.append(",").append(String.valueOf(ae)); } sw.append('\n'); for (int i = ds - 1; i < de; ++i) { sw.append("< ").append(lines1.get(i)).append('\n'); } sw.append("---\n"); for (int i = as - 1; i < ae; ++i) { sw.append("> ").append(lines2.get(i)).append('\n'); } offset = offset + (ae - as) - (de - ds); } } } return sw.toString(); } /** * Returns a list of the lines in a given file. * * @param file File * @return List of lines */ private static List fileLines(File file) { List lines = new ArrayList<>(); try (LineNumberReader r = new LineNumberReader(Util.reader(file))) { String line; while ((line = r.readLine()) != null) { lines.add(line); } return lines; } catch (IOException e) { e.printStackTrace(); throw TestUtil.rethrow(e); } } /** * Returns the contents of a file as a string. * * @param file File * @return Contents of the file */ protected static String fileContents(File file) { byte[] buf = new byte[2048]; try (FileInputStream reader = new FileInputStream(file)) { int readCount; final ByteArrayOutputStream writer = new ByteArrayOutputStream(); while ((readCount = reader.read(buf)) >= 0) { writer.write(buf, 0, readCount); } return writer.toString(StandardCharsets.UTF_8.name()); } catch (IOException e) { throw TestUtil.rethrow(e); } } /** * Sets whether to give verbose message if diff fails. */ protected void setVerbose(boolean verbose) { this.verbose = verbose; } /** * Sets the diff masks that are common to .REF files */ protected void setRefFileDiffMasks() { // mask out source control Id addDiffMask("\\$Id.*\\$"); // NOTE hersker 2006-06-02: // The following two patterns can be used to mask out the // sqlline JDBC URI and continuation prompts. This is useful // during transition periods when URIs are changed, or when // new drivers are deployed which have their own URIs but // should first pass the existing test suite before their // own .ref files get checked in. // // It is not recommended to use these patterns on an everyday // basis. Real differences in the output are difficult to spot // when diff-ing .ref and .log files which have different // sqlline prompts at the start of each line. // mask out sqlline JDBC URI prompt addDiffMask("0: \\bjdbc(:[^:>]+)+:>"); // mask out different-length sqlline continuation prompts addDiffMask("^(\\.\\s?)+>"); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy