Add test case comparing CGit vs JGit ignore behavior for random patterns

This test case was developed in the scope of bug 478065.

Bug: 478065
Change-Id: Ibcce1ed375d4a6ba05461e6c6b287d16752fa681
Signed-off-by: Sébastien Arod <sebastien.arod@gmail.com>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
diff --git a/org.eclipse.jgit.test/exttst/org/eclipse/jgit/ignore/CGitVsJGitRandomIgnorePatternTest.java b/org.eclipse.jgit.test/exttst/org/eclipse/jgit/ignore/CGitVsJGitRandomIgnorePatternTest.java
new file mode 100644
index 0000000..db5f1b2
--- /dev/null
+++ b/org.eclipse.jgit.test/exttst/org/eclipse/jgit/ignore/CGitVsJGitRandomIgnorePatternTest.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2015, Sebastien Arod <sebastien.arod@gmail.com>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.ignore;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+
+import org.eclipse.jgit.api.Git;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * This test generates random ignore patterns and random path and compares the
+ * output of Cgit check-ignore to the output of {@link FastIgnoreRule}.
+ */
+public class CGitVsJGitRandomIgnorePatternTest {
+
+	private static class PseudoRandomPatternGenerator {
+
+		private static final int DEFAULT_MAX_FRAGMENTS_PER_PATTERN = 15;
+
+		/**
+		 * Generates 75% Special fragments and 25% "standard" characters
+		 */
+		private static final double DEFAULT_SPECIAL_FRAGMENTS_FREQUENCY = 0.75d;
+
+		private static final List<String> SPECIAL_FRAGMENTS = Arrays.asList(
+				"\\", "!", "#", "[", "]", "|", "/", "*", "?", "{", "}", "(",
+				")", "\\d", "(", "**", "[a\\]]", "\\ ", "+", "-", "^", "$", ".",
+				":", "=", "[[:", ":]]"
+
+		);
+
+		private static final String STANDARD_CHARACTERS = new String(
+				"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
+
+		private final Random random = new Random();
+
+		private final int maxFragmentsPerPattern;
+
+		private final double specialFragmentsFrequency;
+
+		public PseudoRandomPatternGenerator() {
+			this(DEFAULT_MAX_FRAGMENTS_PER_PATTERN,
+					DEFAULT_SPECIAL_FRAGMENTS_FREQUENCY);
+		}
+
+		public PseudoRandomPatternGenerator(int maxFragmentsPerPattern,
+				double specialFragmentsFrequency) {
+			this.maxFragmentsPerPattern = maxFragmentsPerPattern;
+			this.specialFragmentsFrequency = specialFragmentsFrequency;
+		}
+
+		public String nextRandomString() {
+			StringBuilder builder = new StringBuilder();
+			int length = randomFragmentCount();
+			for (int i = 0; i < length; i++) {
+				if (useSpecialFragment()) {
+					builder.append(randomSpecialFragment());
+				} else {
+					builder.append(randomStandardCharacters());
+				}
+
+			}
+			return builder.toString();
+		}
+
+		private int randomFragmentCount() {
+			// We want at least one fragment
+			return 1 + random.nextInt(maxFragmentsPerPattern - 1);
+		}
+
+		private char randomStandardCharacters() {
+			return STANDARD_CHARACTERS
+					.charAt(random.nextInt(STANDARD_CHARACTERS.length()));
+		}
+
+		private boolean useSpecialFragment() {
+			return random.nextDouble() < specialFragmentsFrequency;
+		}
+
+		private String randomSpecialFragment() {
+			return SPECIAL_FRAGMENTS
+					.get(random.nextInt(SPECIAL_FRAGMENTS.size()));
+		}
+	}
+
+	@SuppressWarnings("serial")
+	public static class CgitFatalException extends Exception {
+
+		public CgitFatalException(int cgitExitCode, String pattern, String path,
+				String cgitStdError) {
+			super("CgitFatalException (" + cgitExitCode + ") for pattern:["
+					+ pattern + "] and path:[" + path + "]\n" + cgitStdError);
+		}
+
+	}
+
+	public static class CGitIgnoreRule {
+
+		private File gitDir;
+
+		private String pattern;
+
+		public CGitIgnoreRule(File gitDir, String pattern)
+				throws UnsupportedEncodingException, IOException {
+			this.gitDir = gitDir;
+			this.pattern = pattern;
+			Files.write(new File(gitDir, ".gitignore").toPath(),
+					(pattern + "\n").getBytes("UTF-8"),
+					StandardOpenOption.CREATE,
+					StandardOpenOption.TRUNCATE_EXISTING,
+					StandardOpenOption.WRITE);
+		}
+
+		public boolean isMatch(String path)
+				throws IOException, InterruptedException, CgitFatalException {
+			Process proc = startCgitCheckIgnore(path);
+
+			String cgitStdOutput = readProcessStream(proc.getInputStream());
+			String cgitStdError = readProcessStream(proc.getErrorStream());
+
+			int cgitExitCode = proc.waitFor();
+
+			if (cgitExitCode == 128) {
+				throw new CgitFatalException(cgitExitCode, pattern, path,
+						cgitStdError);
+			}
+			return !cgitStdOutput.startsWith("::");
+		}
+
+		private Process startCgitCheckIgnore(String path) throws IOException {
+			// Use --stdin instead of using argument otherwise paths starting
+			// with "-" were interpreted as
+			// options by git check-ignore
+			String[] command = new String[] { "git", "check-ignore",
+					"--no-index", "-v", "-n", "--stdin" };
+			Process proc = Runtime.getRuntime().exec(command, new String[0],
+					gitDir);
+			OutputStream out = proc.getOutputStream();
+			out.write((path + "\n").getBytes("UTF-8"));
+			out.flush();
+			out.close();
+			return proc;
+		}
+
+		private String readProcessStream(InputStream processStream)
+				throws IOException {
+			try (BufferedReader stdOut = new BufferedReader(
+					new InputStreamReader(processStream))) {
+
+				StringBuilder out = new StringBuilder();
+				String s;
+				while ((s = stdOut.readLine()) != null) {
+					out.append(s);
+				}
+				return out.toString();
+			}
+		}
+	}
+
+	private static final int NB_PATTERN = 1000;
+
+	private static final int PATH_PER_PATTERN = 1000;
+
+	@Test
+	public void testRandomPatterns() throws Exception {
+		// Initialize new git repo
+		File gitDir = Files.createTempDirectory("jgit").toFile();
+		Git.init().setDirectory(gitDir).call();
+		PseudoRandomPatternGenerator generator = new PseudoRandomPatternGenerator();
+
+		// Generate random patterns and paths
+		for (int i = 0; i < NB_PATTERN; i++) {
+			String pattern = generator.nextRandomString();
+
+			FastIgnoreRule jgitIgnoreRule = new FastIgnoreRule(pattern);
+			CGitIgnoreRule cgitIgnoreRule = new CGitIgnoreRule(gitDir, pattern);
+
+			// Test path with pattern as path
+			assertCgitAndJgitMatch(pattern, jgitIgnoreRule, cgitIgnoreRule,
+					pattern);
+
+			for (int p = 0; p < PATH_PER_PATTERN; p++) {
+				String path = generator.nextRandomString();
+				assertCgitAndJgitMatch(pattern, jgitIgnoreRule, cgitIgnoreRule,
+						path);
+			}
+		}
+	}
+
+	@SuppressWarnings({ "boxing" })
+	private void assertCgitAndJgitMatch(String pattern,
+			FastIgnoreRule jgitIgnoreRule, CGitIgnoreRule cgitIgnoreRule,
+			String pathToTest) throws IOException, InterruptedException {
+
+		try {
+			boolean cgitMatch = cgitIgnoreRule.isMatch(pathToTest);
+			boolean jgitMatch = jgitIgnoreRule.isMatch(pathToTest,
+					pathToTest.endsWith("/"));
+			if (cgitMatch != jgitMatch) {
+				System.err.println(
+						buildAssertionToAdd(pattern, pathToTest, cgitMatch));
+			}
+			Assert.assertEquals("jgit:" + jgitMatch + " <> cgit:" + cgitMatch
+					+ " for pattern:[" + pattern + "] and path:[" + pathToTest
+					+ "]", cgitMatch, jgitMatch);
+		} catch (CgitFatalException e) {
+			// Lots of generated patterns or path are rejected by Cgit with a
+			// fatal error. We want to ignore them.
+		}
+	}
+
+	private String buildAssertionToAdd(String pattern, String pathToTest,
+			boolean cgitMatch) {
+		return "assertMatch(" + toJavaString(pattern) + ", "
+				+ toJavaString(pathToTest) + ", " + cgitMatch
+				+ " /*cgit result*/);";
+	}
+
+	private String toJavaString(String pattern2) {
+		return "\"" + pattern2.replace("\\", "\\\\").replace("\"", "\\\"")
+				+ "\"";
+	}
+}