content: /*
   * 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.checkerframework.checker.nullness.qual.Nullable;
  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.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.nio.file.Files;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  import static org.hamcrest.CoreMatchers.is;
  import static org.hamcrest.MatcherAssert.assertThat;
  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 @Nullable File logFile;
  
    /**
     * Name of current .ref file.
     */
    protected @Nullable File refFile;
  
    /**
     * OutputStream for current test log.
     */
    protected @Nullable OutputStream logOutputStream;
  
    /** Diff masks defined so far. */
    private String diffMasks;
    @Nullable Pattern compiledDiffPattern;
    @Nullable Matcher compiledDiffMatcher;
    private String ignorePatterns;
    @Nullable Pattern compiledIgnorePattern;
    @Nullable 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) {
      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()));
      //noinspection ResultOfMethodCallIgnored
      testClassDir.mkdirs();
      File testLogFile =
          new File(
              testClassDir,
              testCaseName);
      return new OutputStreamWriter(
          openTestLogOutputStream(testLogFile), StandardCharsets.UTF_8);
    }
  
    /** Returns the root directory under which test logs should be written. */
    protected abstract File getTestlogRoot();
  
    /**
     * 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 + ".log");
      //noinspection ResultOfMethodCallIgnored
      logFile.delete();
  
      refFile = new File(testFileSansExt + ".ref");
  
      logOutputStream = Files.newOutputStream(logFile.toPath());
      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.
     *
     * <p>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 {
      if (logOutputStream == null) {
        throw new IllegalStateException();
      }
      logOutputStream.close();
      logOutputStream = null;
  
      if (refFile == null) {
        throw new IllegalStateException();
      }
      if (!refFile.exists()) {
        fail("Reference file " + refFile + " does not exist");
      }
      if (logFile == null) {
        throw new IllegalStateException();
      }
      diffFile(logFile, refFile);
    }
  
    /**
     * Compares a log file with its reference log.
     *
     * <p>Usually, the log file and the reference log are in the same directory,
     * one ending with '.log' and the other with '.ref'.
     *
     * <p>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
      //noinspection ResultOfMethodCallIgnored
      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.isEmpty()) {
        diffMasks = mask;
      } else {
        diffMasks = diffMasks + "|" + mask;
      }
      compiledDiffPattern = Pattern.compile(diffMasks);
      compiledDiffMatcher = compiledDiffPattern.matcher("");
    }
  
    protected void addIgnorePattern(String javaPattern) {
      if (ignorePatterns.isEmpty()) {
        ignorePatterns = javaPattern;
      } else {
        ignorePatterns = ignorePatterns + "|" + javaPattern;
      }
      compiledIgnorePattern = Pattern.compile(ignorePatterns);
      compiledIgnoreMatcher = compiledIgnorePattern.matcher("");
    }
  
    private String applyDiffMask(String s) {
      if (compiledDiffMatcher != null) {
        if (compiledDiffPattern == null) {
          throw new AssertionError();
        }
        compiledDiffMatcher.reset(s);
  
        // we assume most 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 (refFile == null) {
          throw new IllegalStateException();
        }
        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.
          assertThat(message, fileContents(logFile),
              is(fileContents(refFile)));
        } 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<String> lines1 = fileLines(file1);
      List<String> lines2 = fileLines(file2);
      return diffLines(lines1, lines2);
    }
  
    /**
     * Returns a string containing the difference between the two sets of lines.
     */
    public static String diffLines(List<String> lines1, List<String> lines2) {
      final Diff<String> differencer = new Diff<>(lines1, lines2);
      final List<Difference> 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: "<ds>,<de>d<as>"
            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: "<ds>a<as,ae>"
            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: "<ds>,<de>c<as>,<ae>
            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<String> fileLines(File file) {
      List<String> 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?)+>");
    }
  }
  