diff --git a/enterprise-update-test/mer.bat b/enterprise-update-test/mer.bat
new file mode 100644
index 0000000000..5626a8ec94
--- /dev/null
+++ b/enterprise-update-test/mer.bat
@@ -0,0 +1 @@
+mvn install -Dthis.installer.location=c:\demo\alfresco-one-installer-20160203-SNAPSHOT-664-win-x64.exe -Dbase.installer.location=c:\demo\alfresco-one-installer-20160203-SNAPSHOT-664-win-x64.exe
diff --git a/enterprise-update-test/pom.xml b/enterprise-update-test/pom.xml
new file mode 100644
index 0000000000..e55b052fa0
--- /dev/null
+++ b/enterprise-update-test/pom.xml
@@ -0,0 +1,273 @@
+
+ 4.0.0
+ alfresco-enterprise-update-test
+ End to end test of the installer and update
+
+ org.alfresco
+ alfresco-full-installation
+ 5.1.1-SNAPSHOT
+
+
+
+
+
+
+
+
+
+ ${project.build.directory}/test-data/base-alf-installation
+ ${project.build.directory}/test-data/this-alf-installation
+
+ --mode unattended --alfresco_admin_password admin --jdbc_username alfresco --jdbc_password alfresco
+ --tomcat_server_domain ${HOSTNAME} --disable-components postgres --enable-components javaalfresco,libreofficecomponent,alfrescosolr,alfrescosolr4,aosmodule,alfrescowcmqs,alfrescogoogledocs
+ --alfrescocustomstack_services_startup demand
+
+
+ ${project.build.directory}/test-data/alfresco-one-update-package
+
+
+
+
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+
+ de.schlichtherle.truezip
+ truezip-driver-zip
+ 7.7.9
+
+
+ de.schlichtherle.truezip
+ truezip-file
+ 7.7.9
+
+
+ commons-io
+ commons-io
+ 2.4
+
+
+ org.apache.logging.log4j
+ log4j-api
+ 2.3
+
+
+ org.apache.logging.log4j
+ log4j-core
+ 2.3
+
+
+
+ org.springframework
+ spring-core
+ 4.2.4.RELEASE
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.5.1
+
+ 1.7
+ 1.7
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+
+
+ unpack-update-package
+ generate-resources
+
+ unpack
+
+
+
+
+ org.alfresco
+ alfresco-one-update-package
+ ${project.version}
+ true
+ ${unpacked.update.package}
+ zip
+
+
+
+
+
+
+
+
+
+ maven-antrun-plugin
+
+
+ fetch-installer
+ pre-integration-test
+
+ run
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Is base.installer.version provided ?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Downloading Base Alfresco installer ${base.installer.version}...
+
+
+
+ Installing the base Version of Alfresco... to ${base.alfresco.instance}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Installing newly generated version of Alfresco...
+
+
+
+
+
+
+
+
+
+
+ org.apache.ant
+ ant-jsch
+ 1.8.2
+
+
+ ant-contrib
+ ant-contrib
+ 1.0b3
+
+
+ ant
+ ant
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ default-test
+ test
+
+ test
+
+
+
+ **/*IntegrationTest.java
+
+ true
+ alphabetical
+
+ ${alfresco.update.package}
+ ${project.build.directory}
+ ${this.alfresco.instance}
+ ${base.alfresco.instance}
+
+
+
+
+ integration-test
+ integration-test
+
+ test
+
+
+ false
+ ${project.build.directory}
+ true
+ alphabetical
+
+ none
+
+
+ **/*IntegrationTest.java
+
+
+ ${project.artifactId}-${installer.version.name}.zip
+ ${project.build.directory}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/CaseSensitivePathComparator.java b/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/CaseSensitivePathComparator.java
new file mode 100644
index 0000000000..56c03633ba
--- /dev/null
+++ b/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/CaseSensitivePathComparator.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2016 Alfresco Software, Ltd. All rights reserved.
+ *
+ * License rights for this program may be obtained from Alfresco Software, Ltd.
+ * pursuant to a written agreement and any use of this program without such an
+ * agreement is prohibited.
+ */
+package org.alfresco.update.tool.dircomp;
+
+import java.nio.file.Path;
+import java.util.Comparator;
+
+/**
+ * Provides a platform agnostic and consistent sorting mechanism
+ * for {@link java.nio.file.Path} objects.
+ *
+ * @author Matt Ward
+ */
+public class CaseSensitivePathComparator implements Comparator
+{
+ @Override
+ public int compare(Path p1, Path p2)
+ {
+ String pathStr1 = p1.toString();
+ String pathStr2 = p2.toString();
+ return pathStr1.compareTo(pathStr2);
+ }
+}
diff --git a/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/FileTreeCompare.java b/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/FileTreeCompare.java
new file mode 100644
index 0000000000..4c164402df
--- /dev/null
+++ b/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/FileTreeCompare.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2016 Alfresco Software, Ltd. All rights reserved.
+ *
+ * License rights for this program may be obtained from Alfresco Software, Ltd.
+ * pursuant to a written agreement and any use of this program without such an
+ * agreement is prohibited.
+ */
+package org.alfresco.update.tool.dircomp;
+
+import java.nio.file.Path;
+import java.util.List;
+
+
+/**
+ * File tree comparison tool interface.
+ *
+ * @author Matt Ward
+ */
+public interface FileTreeCompare
+{
+ ResultSet compare(Path p1, Path p2);
+}
diff --git a/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/FileTreeCompareImpl.java b/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/FileTreeCompareImpl.java
new file mode 100644
index 0000000000..99472447ea
--- /dev/null
+++ b/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/FileTreeCompareImpl.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright 2016 Alfresco Software, Ltd. All rights reserved.
+ *
+ * License rights for this program may be obtained from Alfresco Software, Ltd.
+ * pursuant to a written agreement and any use of this program without such an
+ * agreement is prohibited.
+ */
+package org.alfresco.update.tool.dircomp;
+
+import de.schlichtherle.truezip.file.TArchiveDetector;
+import de.schlichtherle.truezip.file.TConfig;
+import de.schlichtherle.truezip.file.TFile;
+import de.schlichtherle.truezip.file.TVFS;
+import de.schlichtherle.truezip.fs.archive.zip.ZipDriver;
+import de.schlichtherle.truezip.socket.sl.IOPoolLocator;
+import org.alfresco.update.tool.dircomp.exception.FileTreeCompareException;
+import org.apache.commons.io.FileUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.util.AntPathMatcher;
+
+import java.io.*;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Class capable of comparing two trees of files to determine which directories or
+ * files appear in one tree and not the other, or whether a file that appears in
+ * both has differences in its content.
+ *
+ * @author Matt Ward
+ */
+public class FileTreeCompareImpl implements FileTreeCompare
+{
+ private static final Logger log = LogManager.getLogger(FileTreeCompareImpl.class);
+ private final Set ignorePaths = new HashSet<>();
+ private final Set allowedDiffsPaths = new HashSet<>();
+ private final AntPathMatcher pathMatcher = new AntPathMatcher(File.separator);
+
+ public FileTreeCompareImpl()
+ {
+ this(null, null);
+ }
+
+ public FileTreeCompareImpl(Set ignorePaths, Set allowedDiffsPaths)
+ {
+ // This config MUST be present before any TFile objects etc. are created.
+ TConfig config = TConfig.get();
+ config.setArchiveDetector(new TArchiveDetector("war|jar|amp", new ZipDriver(IOPoolLocator.SINGLETON)));
+ if (ignorePaths == null)
+ {
+ // Add default ignores
+ ignorePaths = new HashSet<>();
+ ignorePaths.add(toPlatformPath("alf_data/postgresql/**"));
+ ignorePaths.add(toPlatformPath("alf_data/oouser/user/**"));
+ ignorePaths.add(toPlatformPath("alf_data/solr/*.war"));
+ ignorePaths.add(toPlatformPath("common/**"));
+ ignorePaths.add(toPlatformPath("META-INF/MANIFEST.MF"));
+ ignorePaths.add(toPlatformPath("META-INF/maven/**"));
+ ignorePaths.add(toPlatformPath("licenses/notice.txt"));
+ ignorePaths.add(toPlatformPath("uninstall.app/**"));
+ ignorePaths.add(toPlatformPath("uninstall/**"));
+ ignorePaths.add(toPlatformPath("uninstall.exe"));
+ ignorePaths.add(toPlatformPath("uninstall.dat"));
+ ignorePaths.add(toPlatformPath("libreoffice.app/**"));
+ ignorePaths.add(toPlatformPath("libreoffice/**"));
+ ignorePaths.add(toPlatformPath("java/**"));
+ ignorePaths.add(toPlatformPath("applied-updates/**"));
+ ignorePaths.add(toPlatformPath("~build/**"));
+ ignorePaths.add(toPlatformPath("properties.ini"));
+ ignorePaths.add(toPlatformPath("**/log.txt"));
+ ignorePaths.add(toPlatformPath("**/solrcore.properties"));
+ ignorePaths.add(toPlatformPath("**/modifications.install"));
+ ignorePaths.add(toPlatformPath("tomcat/webapps/ROOT.war"));
+
+ // Ignore for 5.1 MNT-14307
+ ignorePaths.add(toPlatformPath("tomcat/shared/classes/alfresco/web-extension/share-config-custom.xml"));
+
+
+ }
+ if (allowedDiffsPaths == null)
+ {
+ // Add default paths where certain differences are allowed, e.g. absolute path references.
+ allowedDiffsPaths = new HashSet<>();
+ allowedDiffsPaths.add(toPlatformPath("common/bin/**"));
+ allowedDiffsPaths.add(toPlatformPath("common/include/**/*.h"));
+ allowedDiffsPaths.add(toPlatformPath("common/lib/**/*.pc"));
+ allowedDiffsPaths.add(toPlatformPath("common/lib/**/*.la"));
+ allowedDiffsPaths.add(toPlatformPath("libreoffice.app/Contents/Resources/bootstraprc"));
+ allowedDiffsPaths.add(toPlatformPath("postgresql/bin/**"));
+ allowedDiffsPaths.add(toPlatformPath("**/*.sh"));
+ allowedDiffsPaths.add(toPlatformPath("**/*.bat"));
+ allowedDiffsPaths.add(toPlatformPath("**/*.ini"));
+ allowedDiffsPaths.add(toPlatformPath("**/*.properties"));
+ allowedDiffsPaths.add(toPlatformPath("**/*.xml"));
+ allowedDiffsPaths.add(toPlatformPath("**/*.sample"));
+ allowedDiffsPaths.add(toPlatformPath("**/*.txt"));
+ allowedDiffsPaths.add(toPlatformPath("tomcat/conf/Catalina/localhost/solr4.xml"));
+ allowedDiffsPaths.add(toPlatformPath("tomcat/conf/Catalina/localhost/solr.xml"));
+ }
+ this.ignorePaths.addAll(ignorePaths);
+ this.allowedDiffsPaths.addAll(allowedDiffsPaths);
+ }
+
+ private String toPlatformPath(String path)
+ {
+ return path.replace("/", File.separator);
+ }
+
+ @Override
+ public ResultSet compare(Path p1, Path p2)
+ {
+ ResultSet resultSet = new ResultSet();
+ try
+ {
+ compare(resultSet.stats, resultSet.results, p1, p2);
+ }
+ catch (Exception e)
+ {
+ throw new FileTreeCompareException("Unable to compare file trees.", e);
+ }
+ return resultSet;
+ }
+
+ private void compare(ResultSet.Stats stats, List results, Path tree1, Path tree2) throws IOException
+ {
+ SortedPathSet set1 = sortedPaths(tree1);
+ SortedPathSet set2 = sortedPaths(tree2);
+
+ SortedPathSet all = new SortedPathSet();
+ all.addAll(set1);
+ all.addAll(set2);
+
+ for (Path pathToFind : all)
+ {
+ if (pathMatchesIgnorePattern(pathToFind))
+ {
+ // Skip paths that we don't want to examine, e.g. tomcat/temp
+ log.debug("Skipping path: "+pathToFind);
+ stats.ignoredFileCount++;
+ continue;
+ }
+
+ Result result = new Result();
+ results.add(result);
+ stats.resultCount++;
+
+ if (set1.contains(pathToFind) && set2.contains(pathToFind))
+ {
+ log.debug("In both: "+pathToFind);
+ // Set the results, translating paths back to absolute as required.
+ result.p1 = tree1.resolve(pathToFind);
+ result.p2 = tree2.resolve(pathToFind);
+ boolean contentMatches = false;
+ if (Files.isRegularFile(result.p1) && Files.isRegularFile(result.p2))
+ {
+ contentMatches = FileUtils.contentEquals(result.p1.toFile(), result.p2.toFile());
+ if (!contentMatches)
+ {
+ if (pathMatchesAllowedDiffsPattern(pathToFind))
+ {
+ File f1 = preprocessFile(tree1, result.p1.toFile());
+ File f2 = preprocessFile(tree2, result.p2.toFile());
+ contentMatches = FileUtils.contentEquals(f1, f2);
+ // Delete the files now that we no longer need them. The originals are still available.
+ f1.delete();
+ f2.delete();
+ if (contentMatches)
+ {
+ // If the preprocessed files match, then although the files didn't
+ // match when first compared byte-for-byte, they do match as far as we are concerned.
+ // But add to the stats that this is what has happened.
+ stats.suppressedDifferenceCount++;
+ }
+ }
+ else if (isSpecialArchive(pathToFind))
+ {
+ Path archive1 = extract(result.p1);
+ Path archive2 = extract(result.p2);
+ result.subTree1 = archive1;
+ result.subTree2 = archive2;
+ final int diffBefore = stats.differenceCount;
+ compare(stats, result.subResults, archive1, archive2);
+ final int diffAfter = stats.differenceCount;
+ if (diffAfter == diffBefore)
+ {
+ // No significant differences were found in the (recursive) subtree comparison.
+ // We can therefore mark the special archive files matching in both trees.
+ contentMatches = true;
+ }
+ }
+ }
+
+ }
+ else if (Files.isDirectory(result.p1) && Files.isDirectory(result.p2))
+ {
+ // Two directories are counted as the same.
+ contentMatches = true;
+ }
+ result.equal = contentMatches;
+ }
+ else if (set1.contains(pathToFind))
+ {
+ log.debug("In tree1 only: "+pathToFind);
+ result.p1 = tree1.resolve(pathToFind);
+ result.p2 = null;
+ }
+ else if (set2.contains(pathToFind))
+ {
+ log.debug("In tree2 only: "+pathToFind);
+ result.p1 = null;
+ result.p2 = tree2.resolve(pathToFind);
+ }
+ else
+ {
+ throw new IllegalStateException(
+ "Something went wrong. The path is not found in either tree: "+pathToFind);
+ }
+
+ if (!result.equal)
+ {
+ stats.differenceCount++;
+ }
+ }
+ }
+
+ private File preprocessFile(Path tree, File orig) throws IOException
+ {
+ // Create a set of replacements that we intend to make. Replacing them with
+ // a known token allows us to remove differences (that we're not interested in) in the files.
+ Map replacements = new HashMap<>();
+ replacements.put(tree.toRealPath().toString(), replacementToken("comparison_root"));
+
+ // Create a pattern for module.installDate
+ Pattern installDatePattern = Pattern.compile("module.installDate=.*[\n\r\f]*$");
+ Pattern commentPattern = Pattern.compile("^#.*");
+
+ File processed = Files.createTempFile(orig.getName(), ".tmp").toFile();
+ try(Reader r = new FileReader(orig);
+ BufferedReader br = new BufferedReader(r);
+ Writer w = new FileWriter(processed);
+ PrintWriter pw = new PrintWriter(w))
+ {
+ String line;
+
+ while ((line = br.readLine()) != null)
+ {
+ for (String replKey : replacements.keySet())
+ {
+ String replVal = replacements.get(replKey);
+ line = line.replace(replKey, replVal);
+ }
+ Matcher m = installDatePattern.matcher(line);
+ if(m.matches())
+ {
+ // replace module.installDate
+ line = m.replaceFirst("module.installDate=");
+ }
+ Matcher cp = commentPattern.matcher(line);
+ if(cp.matches())
+ {
+ // replace module.installDate
+ line = "# {comment suppressed}\n";
+ }
+
+ pw.println(line);
+ }
+ }
+ return processed;
+ }
+
+ private String replacementToken(String label)
+ {
+ return String.format("@$@$@$@${{TREE_COMPARE_%s}}", label);
+ }
+
+ private boolean isSpecialArchive(Path pathToFind)
+ {
+ return pathToFind.getFileName().toString().toLowerCase().endsWith(".war") ||
+ pathToFind.getFileName().toString().toLowerCase().endsWith(".jar") ||
+ pathToFind.getFileName().toString().toLowerCase().endsWith(".amp");
+ }
+
+ /**
+ * If the set of paths to allow certain differences ({@link #allowedDiffsPaths})
+ * contains a pattern matching the specified path, then true is returned.
+ *
+ * Patterns are ant-style patterns.
+ *
+ * @param path The path to check
+ * @return True if there is a pattern in the allowedDiffsPaths set matching the path.
+ */
+ private boolean pathMatchesAllowedDiffsPattern(String path)
+ {
+ return pathMatchesPattern(path, allowedDiffsPaths);
+ }
+
+ /**
+ * @see #pathMatchesAllowedDiffsPattern(String)
+ */
+ private boolean pathMatchesAllowedDiffsPattern(Path path)
+ {
+ return pathMatchesAllowedDiffsPattern(path.toString());
+ }
+
+ /**
+ * If the set of paths to ignore ({@link #ignorePaths}) contains
+ * a pattern matching the specified path, then true is returned.
+ *
+ * Patterns are ant-style patterns.
+ *
+ * @param path The path to check
+ * @return True if there is a path in the ignorePaths set that is a prefix of the path.
+ */
+ private boolean pathMatchesIgnorePattern(String path)
+ {
+ return pathMatchesPattern(path, ignorePaths);
+ }
+
+ /**
+ * @see #pathMatchesIgnorePattern(String)
+ */
+ private boolean pathMatchesIgnorePattern(Path path)
+ {
+ return pathMatchesIgnorePattern(path.toString());
+ }
+
+ private boolean pathMatchesPattern(String path, Set patterns)
+ {
+ for (String pattern : patterns)
+ {
+ if (pathMatcher.match(pattern, path))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private Path extract(Path archivePath) throws IOException
+ {
+ String destDirName = archivePath.getFileName().toString();
+ Path dest = Files.createTempDirectory(destDirName);
+ extract(archivePath, dest);
+ return dest;
+ }
+
+ private void extract(Path archivePath, Path destPath) throws IOException
+ {
+ TFile archive = new TFile(archivePath.toFile());
+ TFile dest = new TFile(destPath.toFile(), TArchiveDetector.NULL);
+ try
+ {
+ // Unzip the archive.
+ archive.cp_rp(dest);
+ }
+ finally
+ {
+ TVFS.umount(archive);
+ TVFS.umount(dest);
+ }
+ }
+
+ /**
+ * Traverse path and create a {@link SortedPathSet} containing the set
+ * of paths encountered. The {@link Path} instances are relative to
+ * the base path provided as a parameter to this method.
+ *
+ * @param path The path to traverse.
+ * @return SortedPathSet
+ * @throws IOException
+ */
+ protected SortedPathSet sortedPaths(Path path) throws IOException
+ {
+ SortedPathSet sortedPaths = new SortedPathSet();
+ collectPaths(sortedPaths, path, path.toFile());
+ return sortedPaths;
+ }
+
+ private void collectPaths(SortedPathSet sortedSet, Path root, File path) throws IOException
+ {
+ for (File f : path.listFiles())
+ {
+ Path relativePath = root.relativize(f.toPath());
+ sortedSet.add(relativePath);
+ if (f.isDirectory())
+ {
+ collectPaths(sortedSet, root, f);
+ }
+ }
+ }
+}
diff --git a/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/HtmlResultFormatter.java b/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/HtmlResultFormatter.java
new file mode 100644
index 0000000000..ec8fa8ebc1
--- /dev/null
+++ b/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/HtmlResultFormatter.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2016 Alfresco Software, Ltd. All rights reserved.
+ *
+ * License rights for this program may be obtained from Alfresco Software, Ltd.
+ * pursuant to a written agreement and any use of this program without such an
+ * agreement is prohibited.
+ */
+package org.alfresco.update.tool.dircomp;
+
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * Format results as HTML.
+ *
+ * @author Matt Ward
+ */
+public class HtmlResultFormatter implements ResultFormatter
+{
+ private int maxPathDisplayLength = 50;
+ private boolean differencesOnly;
+
+
+ @Override
+ public void format(ResultSet resultSet, OutputStream out)
+ {
+ boolean failed = resultSet.stats.differenceCount > 0;
+
+ try(PrintWriter pw = new PrintWriter(out))
+ {
+ pw.println("");
+ pw.println("");
+ pw.println("");
+ pw.println("File tree comparison results");
+ pw.println("");
+ pw.println("");
+ pw.println("");
+ pw.println("");
+ pw.println("
");
+ pw.println("");
+ pw.println("");
+
+ for (Result r : results)
+ {
+ ++row;
+
+ outputResult(pw, row, r);
+
+ if (!r.subResults.isEmpty())
+ {
+ // Only show the subresults if there are
+ if (!differencesOnly || !r.equal)
+ {
+ pw.println("
");
+ }
+
+ private void outputResult(PrintWriter pw, int row, Result r)
+ {
+ if (differencesOnly && r.equal)
+ {
+ return;
+ }
+ pw.println("
");
+
+ pw.println("
"+row+"
");
+
+ String p1 = (r.p1 == null) ? "" : r.p1.toString();
+ String p1Abbr = abbreviate(p1, maxPathDisplayLength);
+ String p2 = (r.p2 == null) ? "" : r.p2.toString();
+ String p2Abbr = abbreviate(p2, maxPathDisplayLength);
+
+ // TODO: URL/HTML-encode as appropriate
+ String diffClass;
+ if (r.equal)
+ {
+ if (r.subResults.isEmpty())
+ {
+ // Result refers to a normal file or directory.
+ diffClass = "info";
+ }
+ else
+ {
+ // File is a special archive, but no differences in the sub-results are considered important.
+ diffClass = "warning";
+ }
+ }
+ else
+ {
+ // The file/directory appears different in each tree. If it is a special archive, then there
+ // are differences that we care about.
+ diffClass = "danger";
+ }
+
+ pw.println(
+ String.format("
");
+ }
+
+ private String abbreviate(String str, int maxLength)
+ {
+ return (str.length() > maxLength) ? "..."+str.substring(str.length()-maxLength) : str;
+ }
+
+ public void setMaxPathDisplayLength(int maxPathDisplayLength)
+ {
+ this.maxPathDisplayLength = maxPathDisplayLength;
+ }
+
+ public void setDifferencesOnly(boolean differencesOnly)
+ {
+ this.differencesOnly = differencesOnly;
+ }
+}
diff --git a/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/Result.java b/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/Result.java
new file mode 100644
index 0000000000..355cebb239
--- /dev/null
+++ b/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/Result.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2016 Alfresco Software, Ltd. All rights reserved.
+ *
+ * License rights for this program may be obtained from Alfresco Software, Ltd.
+ * pursuant to a written agreement and any use of this program without such an
+ * agreement is prohibited.
+ */
+package org.alfresco.update.tool.dircomp;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Simple struct-style result data class.
+ *
+ * @author Matt Ward
+ */
+public final class Result
+{
+ /**
+ * The path within tree1 being compared (intended to be the updated Alfresco installation).
+ *
+ * This field will be null if the file in question appears only in the tree2.
+ */
+ Path p1;
+ /**
+ * The path within tree2 being compared (intended to be the freshly installed equivalent of {@link #p1}).
+ *
+ * This field will be null if the file in question appears only in the tree1.
+ */
+ Path p2;
+ /**
+ * Are the paths in {@link #p1} and {@link #p2} of identical content? If they refer to a directory, then
+ * equal in this sense is to have the same directory name. If they refer to a plain file, then equal means
+ * that they contain the exact same contents.
+ */
+ boolean equal;
+ /**
+ * The root path of sub-tree1 to which {@link #subResults} refers.
+ * @see #subResults
+ */
+ Path subTree1;
+ /**
+ * The root path of sub-tree2 to which {@link #subResults} refers.
+ * @see #subResults
+ */
+ Path subTree2;
+ /**
+ * If p1 and p2 refer to a special archive with differences, e.g. they refer to alfresco.war,
+ * then a deep comparison of the archives will be performed and the results stored here.
+ *
+ * The paths to the expanded archives will be stored in {@link #subTree1} and {@link #subTree2}
+ * and all the paths in {@link #subResults} will be within those new roots.
+ */
+ List subResults = new ArrayList<>();
+
+ @Override
+ public int hashCode()
+ {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (this.equal ? 1231 : 1237);
+ result = prime * result + ((this.p1 == null) ? 0 : this.p1.hashCode());
+ result = prime * result + ((this.p2 == null) ? 0 : this.p2.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+ Result other = (Result) obj;
+ if (this.equal != other.equal) return false;
+ if (this.p1 == null)
+ {
+ if (other.p1 != null) return false;
+ }
+ else if (!this.p1.equals(other.p1)) return false;
+ if (this.p2 == null)
+ {
+ if (other.p2 != null) return false;
+ }
+ else if (!this.p2.equals(other.p2)) return false;
+ return true;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("Result[p1=%s, p2=%s, equal=%b]", p1, p2, equal);
+ }
+}
diff --git a/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/ResultFormatter.java b/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/ResultFormatter.java
new file mode 100644
index 0000000000..6eddc194a1
--- /dev/null
+++ b/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/ResultFormatter.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2016 Alfresco Software, Ltd. All rights reserved.
+ *
+ * License rights for this program may be obtained from Alfresco Software, Ltd.
+ * pursuant to a written agreement and any use of this program without such an
+ * agreement is prohibited.
+ */
+package org.alfresco.update.tool.dircomp;
+
+import java.io.OutputStream;
+import java.util.Collection;
+
+/**
+ * Format a set of {@link Result} objects.
+ *
+ * @author Matt Ward
+ */
+public interface ResultFormatter
+{
+ /**
+ * Format the result set to the supplied {@link OutputStream}. The caller
+ * must take care of creating and destroying the OutputStream correctly.
+ *
+ * @param results The results to format.
+ * @param out The stream to format the results to.
+ */
+ void format(ResultSet results, OutputStream out);
+}
diff --git a/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/ResultSet.java b/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/ResultSet.java
new file mode 100644
index 0000000000..a96a426569
--- /dev/null
+++ b/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/ResultSet.java
@@ -0,0 +1,44 @@
+package org.alfresco.update.tool.dircomp;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/*
+ * Copyright 2015-2016 Alfresco Software, Ltd. All rights reserved.
+ *
+ * License rights for this program may be obtained from Alfresco Software, Ltd.
+ * pursuant to a written agreement and any use of this program without such an
+ * agreement is prohibited.
+ */
+public class ResultSet
+{
+ final List results = new ArrayList<>();
+ public final Stats stats = new Stats();
+
+ /**
+ * Class for aggregating basic statistics relating to the directory tree comparisons.
+ *
+ * For all counts, unless specified otherwise, if a file appears in both trees, it is
+ * counted only once, as it is relative files we are tracking regardless of which
+ * tree(s) they exist in.
+ */
+ public static class Stats
+ {
+ /**
+ * The number of files (including directories) examined.
+ */
+ public int resultCount;
+ /**
+ * The number of files discovered to have differences that are not allowed or ignored.
+ */
+ public int differenceCount;
+ /**
+ * The number of files discovered to have differences, but the difference is allowed.
+ */
+ public int suppressedDifferenceCount;
+ /**
+ * The number of files that were completely ignored due to being in the ignore list.
+ */
+ public int ignoredFileCount;
+ }
+}
diff --git a/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/SortedPathSet.java b/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/SortedPathSet.java
new file mode 100644
index 0000000000..52c7087cb6
--- /dev/null
+++ b/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/SortedPathSet.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2016 Alfresco Software, Ltd. All rights reserved.
+ *
+ * License rights for this program may be obtained from Alfresco Software, Ltd.
+ * pursuant to a written agreement and any use of this program without such an
+ * agreement is prohibited.
+ */
+package org.alfresco.update.tool.dircomp;
+
+import java.nio.file.Path;
+import java.util.TreeSet;
+
+/**
+ * Sorted set of {@link Path} objects that provides consistent
+ * cross-platform sort order.
+ *
+ * @see java.util.TreeSet
+ * @author Matt Ward
+ */
+public class SortedPathSet extends TreeSet
+{
+ private static final long serialVersionUID = 1L;
+
+ public SortedPathSet()
+ {
+ super(new CaseSensitivePathComparator());
+ }
+}
diff --git a/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/ZipResultFormatter.java b/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/ZipResultFormatter.java
new file mode 100644
index 0000000000..0ec0aac001
--- /dev/null
+++ b/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/ZipResultFormatter.java
@@ -0,0 +1,73 @@
+package org.alfresco.update.tool.dircomp;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Path;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * Format the org.alfresco.update.tool.dircomp.ResultSet as a set of files inside a zip file.
+ *
+ * Files that are equal are not added to the zip file.
+ *
+ * @author mrogers
+ */
+public class ZipResultFormatter implements ResultFormatter
+{
+ @Override
+ public void format(ResultSet resultSet, OutputStream out)
+ {
+ ZipOutputStream zos = (ZipOutputStream)out;
+
+ for(Result result : resultSet.results)
+ {
+ if(!result.equal)
+ {
+ try
+ {
+ putFile(result.p1, zos);
+ putFile(result.p2, zos);
+ }
+ catch (IOException ie)
+ {
+ // Do nothing
+ }
+ }
+ }
+ }
+
+ private void putFile(Path path, ZipOutputStream zos) throws IOException
+ {
+
+ if(path != null)
+ {
+ byte[] buffer = new byte[1024];
+ File file = path.toFile();
+ if(file.isFile())
+ {
+ ZipEntry zipEntry = new ZipEntry(getEntryName(path));
+ zipEntry.setTime(file.lastModified());
+ try (FileInputStream ins = new FileInputStream(file))
+ {
+ zos.putNextEntry(zipEntry);
+ int len;
+ while ((len = ins.read(buffer)) > 0)
+ {
+ zos.write(buffer, 0, len);
+ }
+ zos.closeEntry();
+ }
+ }
+ }
+ }
+
+ private String getEntryName(Path path)
+ {
+ // eg differences/xml-data/foo/bar
+ return "differences" + path.normalize().toString().replace('\\', '/').trim();
+ }
+
+}
diff --git a/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/exception/FileTreeCompareException.java b/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/exception/FileTreeCompareException.java
new file mode 100644
index 0000000000..ebf1e4545f
--- /dev/null
+++ b/enterprise-update-test/src/main/java/org/alfresco/update/tool/dircomp/exception/FileTreeCompareException.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016 Alfresco Software, Ltd. All rights reserved.
+ *
+ * License rights for this program may be obtained from Alfresco Software, Ltd.
+ * pursuant to a written agreement and any use of this program without such an
+ * agreement is prohibited.
+ */
+package org.alfresco.update.tool.dircomp.exception;
+
+import org.alfresco.update.tool.dircomp.FileTreeCompare;
+
+/**
+ * Exception class representing failures during file tree comparison.
+ *
+ * @see FileTreeCompare
+ * @author Matt Ward
+ */
+public class FileTreeCompareException extends RuntimeException
+{
+ private static final long serialVersionUID = 1L;
+
+ public FileTreeCompareException(String message)
+ {
+ super(message);
+ }
+
+ public FileTreeCompareException(String message, Throwable cause)
+ {
+ super(message, cause);
+ }
+}
diff --git a/enterprise-update-test/src/test/java/EndToEndIntegrationTest.java b/enterprise-update-test/src/test/java/EndToEndIntegrationTest.java
new file mode 100644
index 0000000000..53fec9617c
--- /dev/null
+++ b/enterprise-update-test/src/test/java/EndToEndIntegrationTest.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright 2015-2016 Alfresco Software, Ltd. All rights reserved.
+ *
+ * License rights for this program may be obtained from Alfresco Software, Ltd.
+ * pursuant to a written agreement and any use of this program without such an
+ * agreement is prohibited.
+ */
+
+import org.alfresco.update.tool.dircomp.FileTreeCompare;
+import org.alfresco.update.tool.dircomp.FileTreeCompareImpl;
+import org.alfresco.update.tool.dircomp.HtmlResultFormatter;
+import org.alfresco.update.tool.dircomp.Result;
+import org.alfresco.update.tool.dircomp.ResultSet;
+import org.alfresco.update.tool.dircomp.ZipResultFormatter;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.*;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.zip.ZipOutputStream;
+
+import static org.junit.Assert.assertTrue;
+
+public class EndToEndIntegrationTest
+{
+ private final static String JAR_FILE_NAME = "alfresco-update-tool.jar";
+
+ File targetDir;
+
+ private String getBasePath()
+ {
+ String basePath = System.getProperty("base.alfresco.instance");
+ if (basePath == null)
+ {
+ basePath = "./test-data/base-alf-installation";
+ }
+ File base = new File(targetDir, basePath);
+ assertTrue("base instance does not exist :" + base, base.exists());
+
+ return base.getAbsolutePath();
+ }
+
+ private String getThisPath()
+ {
+ String basePath = System.getProperty("this.alfresco.instance");
+ if (basePath == null)
+ {
+ basePath = "./test-data/this-alf-installation";
+ }
+ File base = new File(targetDir, basePath);
+ assertTrue("this instance (the one to update) does not exist :" + base, base.exists());
+
+ return base.getAbsolutePath();
+ }
+
+ private String getUpdatePath()
+ {
+ String basePath = System.getProperty("unpacked.update.package");
+ if (basePath == null)
+ {
+ basePath = "./test-data/alfresco-one-update-package";
+ }
+ File base = new File(targetDir, basePath);
+ assertTrue(base.isDirectory());
+ assertTrue("the update package does not exist :" + base, base.exists());
+ String[] dirs = base.list();
+ assertTrue(dirs.length == 1);
+
+ return new File(base, dirs[0]).getAbsolutePath();
+ }
+
+ private void initTargetDir()
+ {
+ String targetDir = System.getProperty("alfresco.target.dir");
+ if (targetDir == null)
+ {
+ targetDir = "./target"; // test needs to be run in target dir.
+ }
+ this.targetDir = new File(targetDir);
+ assertTrue("target dir does not exist :" + targetDir, this.targetDir.exists());
+ }
+
+ @Before
+ public void setUp() throws Exception
+ {
+ initTargetDir();
+ }
+
+
+ @Test
+ public void testEndToEndUpdate() throws Exception
+ {
+ File updateThisOne = new File(getBasePath());
+
+ File referenceInstance = new File(getThisPath());
+
+ File updatePackage = new File(getUpdatePath());
+
+ // Run the update
+ runUpdateTool(updateThisOne, updatePackage);
+
+ // Run the diff tool
+ compare(referenceInstance, updateThisOne);
+
+ }
+
+ /**
+ * Run the update tool
+ */
+ public void runUpdateTool(File instanceToUpdate, File updatePackage) throws Exception
+ {
+ // expect to find jar at "lib/alfresco-update-tool.jar"
+ File jar = new File(updatePackage, "lib/" + JAR_FILE_NAME);
+ assertTrue("lib/" + JAR_FILE_NAME, jar.exists());
+
+ // expect to find update resources
+
+ String options = " --assumeyes -u " + updatePackage;
+ String cmd = "java -jar " + jar.getAbsolutePath() + options + " " + instanceToUpdate.getPath();
+
+ boolean found = runCommand(
+ targetDir,
+ cmd,
+ null,
+ 0,
+ "The update was successful"
+ );
+
+ assertTrue("The update was successful", found);
+
+ }
+
+ /**
+ * Run the diff tool
+ *
+ * @param freshInstallation
+ * @param updatedInstallation
+ */
+ public void compare(File freshInstallation, File updatedInstallation) throws IOException
+ {
+ FileTreeCompare comparator = new FileTreeCompareImpl();
+ ResultSet resultSet = comparator.compare(updatedInstallation.toPath(), freshInstallation.toPath());
+
+ File dircompDir = new File(targetDir, "installation-diff");
+ dircompDir.mkdirs();
+
+ // Format the results as an HTML report.
+ File file = new File(dircompDir, "installation-diff-report.html");
+ file.createNewFile();
+
+ HtmlResultFormatter formatter = new HtmlResultFormatter();
+ formatter.setDifferencesOnly(true);
+ try(FileOutputStream fos = new FileOutputStream(file);
+ BufferedOutputStream bos = new BufferedOutputStream(fos))
+ {
+ formatter.format(resultSet, bos);
+ }
+
+ File zipFile = new File(dircompDir, "installation-diff-report.zip");
+ zipFile.createNewFile();
+
+ ZipResultFormatter zformatter = new ZipResultFormatter();
+
+ try (FileOutputStream fos = new FileOutputStream(zipFile);
+ ZipOutputStream zos = new ZipOutputStream(fos))
+ {
+ zformatter.format(resultSet, zos);
+ }
+
+
+ assertTrue("update test has found unexpected differences, see the installation-diff-report for further details", resultSet.stats.differenceCount == 0);
+
+ }
+
+ /**
+ * Utility/harness to allow easy testing of {@link #compare(File, File)}.
+ *
+ * Uncomment @Ignore, but do not check in.
+ *
+ * @throws IOException
+ */
+ @Ignore
+ @Test
+ public void bigDiff() throws IOException
+ {
+ Path path1 = Paths.get("/Users/MWard/dev2/alf-installs/alf-5.1-b667");
+ Path path2 = Paths.get("/Users/MWard/dev2/alf-installs/alf-5.1-b669");
+
+ compare(path1.toFile(), path2.toFile());
+ }
+
+ /*
+ * Method to execute a command
+ *
+ * This variant of runCommand takes multiple expected messages.
+ * If a message is repeated twice in expectedMessage then the message must appear in
+ * the command line output at least twice to return true.
+ *
+ * @param targetLocation location for executing the command
+ * @param cmd the command to be executed
+ * @param input - input for command line prompts, may be null if not required
+ * @param expectedMessage... messages to be verified
+ * @param expectedReturnCode 0 for success
+ * @return true the messages are all found
+ * @throws IOException
+ */
+ public boolean runCommand(File targetLocation, String cmd, String[] input, int expectedReturnCode, String... expectedMessage) throws IOException, InterruptedException
+ {
+ Runtime rt = Runtime.getRuntime();
+ String line = null;
+ Process pr = rt.exec(cmd, null, targetLocation);
+ if(input != null && input.length > 0)
+ {
+ Input inputThread = new Input(pr.getOutputStream(), input);
+ inputThread.start();
+ }
+
+ ArrayList toFind = new ArrayList();
+ for(int i = 0; i < expectedMessage.length; i++)
+ {
+ toFind.add(expectedMessage[i]);
+ }
+
+ int found;
+ try (BufferedReader out = new BufferedReader(new InputStreamReader(pr.getInputStream())))
+ {
+ while ((line = out.readLine()) != null)
+ {
+ found = -1;
+ for(int i = 0; i < toFind.size() ; i++)
+ {
+ if (line.contains(toFind.get(i)))
+ {
+ found = i;
+ }
+ }
+ System.out.println(line);
+
+ if(found >= 0)
+ {
+ toFind.remove(found);
+ }
+ }
+ }
+
+ int retCode = pr.waitFor();
+
+ if(retCode != expectedReturnCode)
+ {
+ System.out.println("Not expected return code expected:" + expectedReturnCode + " actual: " + retCode);
+ return false;
+ }
+
+ if(toFind.size() == 0)
+ {
+ return true;
+ }
+
+ System.out.println("Did not find expected message: " + toFind);
+
+ return false;
+ }
+
+ /**
+ *
+ */
+ protected class Input extends Thread
+ {
+ OutputStream is;
+ String[] input;
+
+ Input(OutputStream is, String[] input)
+ {
+ this.is = is;
+ this.input = input;
+ }
+
+ public void run()
+ {
+ try (BufferedWriter in = new BufferedWriter(new OutputStreamWriter(is));)
+ {
+ for (String line : input)
+ {
+ in.write(line);
+ in.newLine();
+ System.out.println("wrote : " + line);
+ }
+ }
+ catch (IOException e)
+ {
+
+ }
+ }
+ }
+
+
+}
diff --git a/enterprise-update-test/src/test/java/org/alfresco/update/tool/dircomp/FileTreeCompareImplTest.java b/enterprise-update-test/src/test/java/org/alfresco/update/tool/dircomp/FileTreeCompareImplTest.java
new file mode 100644
index 0000000000..938ef2722a
--- /dev/null
+++ b/enterprise-update-test/src/test/java/org/alfresco/update/tool/dircomp/FileTreeCompareImplTest.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright 2016 Alfresco Software, Ltd. All rights reserved.
+ *
+ * License rights for this program may be obtained from Alfresco Software, Ltd.
+ * pursuant to a written agreement and any use of this program without such an
+ * agreement is prohibited.
+ */
+package org.alfresco.update.tool.dircomp;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.util.AntPathMatcher;
+
+/**
+ * Tests for the {@link FileTreeCompareImpl} class.
+ *
+ * @author Matt Ward
+ */
+public class FileTreeCompareImplTest
+{
+ FileTreeCompareImpl comparator;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ comparator = new FileTreeCompareImpl(new HashSet(), new HashSet());
+ }
+
+ @Test
+ public void canGetSortedPathSet() throws IOException
+ {
+ Path tree = pathFromClasspath("dir_compare/simple_file_folders/tree1");
+ SortedPathSet paths = comparator.sortedPaths(tree);
+ Iterator it = paths.iterator();
+
+ System.out.println("Paths:");
+ for (Path p : paths)
+ {
+ System.out.println("\t"+p);
+ }
+
+ assertEquals(11, paths.size());
+
+ assertEquals("a", unixPathStr(tree, it.next()));
+ assertEquals("b", unixPathStr(tree, it.next()));
+ assertEquals("b/blah.txt", unixPathStr(tree, it.next()));
+ assertEquals("c", unixPathStr(tree, it.next()));
+ assertEquals("c/c1", unixPathStr(tree, it.next()));
+ assertEquals("c/c1/commands.bat", unixPathStr(tree, it.next()));
+ assertEquals("c/c1/commands.sh", unixPathStr(tree, it.next()));
+ assertEquals("c/c2", unixPathStr(tree, it.next()));
+ assertEquals("c/c2/Aardvark.java", unixPathStr(tree, it.next()));
+ assertEquals("c/c2/Banana.java", unixPathStr(tree, it.next()));
+ assertEquals("d", unixPathStr(tree, it.next()));
+ }
+
+ private String unixPathStr(Path root, Path path)
+ {
+ // Allow test to run on Windows also
+ String pathStr = path.toString();
+ pathStr = pathStr.replace(File.separatorChar, '/');
+ return pathStr;
+ }
+
+ @Test
+ public void canDiffSimpleTreesOfFilesAndFolders()
+ {
+ Path tree1 = pathFromClasspath("dir_compare/simple_file_folders/tree1");
+ Path tree2 = pathFromClasspath("dir_compare/simple_file_folders/tree2");
+
+ ResultSet resultSet = comparator.compare(tree1, tree2);
+
+ System.out.println("Comparison results:");
+ for (Result r : resultSet.results)
+ {
+ System.out.println("\t"+r);
+ }
+
+ // One result for each relative file/folder
+ assertEquals(13, resultSet.results.size());
+ assertEquals(13, resultSet.stats.resultCount);
+
+ Iterator rit = resultSet.results.iterator();
+ // TODO: currently all of the files are in one, other or both but where they
+ // are in both, the file *contents* are identical.
+ // TODO: evolve test data and functionality to cope with different file contents.
+ assertResultEquals(tree1.resolve("a"), tree2.resolve("a"), true, rit.next());
+ assertResultEquals(null, tree2.resolve("a/story.txt"), false, rit.next());
+ assertResultEquals(tree1.resolve("b"), tree2.resolve("b"), true, rit.next());
+ assertResultEquals(tree1.resolve("b/blah.txt"), tree2.resolve("b/blah.txt"), true, rit.next());
+ assertResultEquals(tree1.resolve("c"), tree2.resolve("c"), true, rit.next());
+ assertResultEquals(tree1.resolve("c/c1"), tree2.resolve("c/c1"), true, rit.next());
+ assertResultEquals(tree1.resolve("c/c1/commands.bat"), tree2.resolve("c/c1/commands.bat"), true, rit.next());
+ assertResultEquals(tree1.resolve("c/c1/commands.sh"), tree2.resolve("c/c1/commands.sh"), true, rit.next());
+ assertResultEquals(tree1.resolve("c/c2"), tree2.resolve("c/c2"), true, rit.next());
+ // Aardvark.java appears in both trees but is not the same!
+ assertResultEquals(tree1.resolve("c/c2/Aardvark.java"), tree2.resolve("c/c2/Aardvark.java"), false, rit.next());
+ assertResultEquals(tree1.resolve("c/c2/Banana.java"), null, false, rit.next());
+ assertResultEquals(tree1.resolve("d"), null, false, rit.next());
+ assertResultEquals(null, tree2.resolve("e"), false, rit.next());
+ }
+
+ /**
+ * A "learning test" allowing me to check my assumptions and document the expected behaviour.
+ */
+ @Test
+ public void testAntPathMatcher()
+ {
+ AntPathMatcher matcher = new AntPathMatcher();
+ assertTrue(matcher.match("**/common/lib/**/*.pc", "prefix/common/lib/pkgconfig/ImageMagick++-6.Q16.pc"));
+ assertFalse(matcher.match("**/common/lib/**/*.pc", "/absolute/prefix/common/lib/pkgconfig/ImageMagick++-6.Q16.pc"));
+ assertTrue(matcher.match("/**/common/lib/**/*.pc", "/absolute/prefix/common/lib/pkgconfig/ImageMagick++-6.Q16.pc"));
+ assertTrue(matcher.match("common/lib/**/*.pc", "common/lib/pkgconfig/Wand.pc"));
+ assertTrue(matcher.match("**/*.pc", "common/lib/pkgconfig/Wand.pc"));
+ assertFalse(matcher.match("*.pc", "common/lib/pkgconfig/Wand.pc"));
+
+ assertTrue(matcher.match("libreoffice.app/Contents/Resources/bootstraprc", "libreoffice.app/Contents/Resources/bootstraprc"));
+ assertTrue(matcher.match("*.sh", "alfresco.sh"));
+ assertFalse(matcher.match("*.sh", "a/different/alfresco.sh"));
+
+ // Windows matcher
+ // It seems that changing the path separator on an instance that's already been
+ // used isn't a good idea due to pattern caching.
+ matcher = new AntPathMatcher("\\");
+ assertTrue(matcher.match("**\\common\\lib\\**\\*.pc", "prefix\\common\\lib\\pkgconfig\\ImageMagick++-6.Q16.pc"));
+ assertTrue(matcher.match("\\**\\common\\lib\\**\\*.pc", "\\absolute\\prefix\\common\\lib\\pkgconfig\\ImageMagick++-6.Q16.pc"));
+
+ assertTrue(matcher.match("b\\blah.txt", "b\\blah.txt"));
+ }
+
+ @Test
+ public void canIgnoreSpecifiedPaths()
+ {
+ Path tree1 = pathFromClasspath("dir_compare/simple_file_folders/tree1");
+ Path tree2 = pathFromClasspath("dir_compare/simple_file_folders/tree2");
+
+ Set ignorePaths = new HashSet<>();
+ ignorePaths.add(toPlatformPath("b/blah.txt"));
+ ignorePaths.add(toPlatformPath("c/c2/**"));
+ ignorePaths.add(toPlatformPath("d/**"));
+ ignorePaths.add(toPlatformPath("e/**"));
+ comparator = new FileTreeCompareImpl(ignorePaths, new HashSet());
+
+ // Perform the comparison
+ ResultSet resultSet = comparator.compare(tree1, tree2);
+
+ System.out.println("Comparison results:");
+ for (Result r : resultSet.results)
+ {
+ System.out.println("\t"+r);
+ }
+
+ Iterator rit = resultSet.results.iterator();
+ assertResultEquals(tree1.resolve("a"), tree2.resolve("a"), true, rit.next());
+ assertResultEquals(null, tree2.resolve("a/story.txt"), false, rit.next());
+ assertResultEquals(tree1.resolve("b"), tree2.resolve("b"), true, rit.next());
+ // No b/blah.txt here.
+ assertResultEquals(tree1.resolve("c"), tree2.resolve("c"), true, rit.next());
+ assertResultEquals(tree1.resolve("c/c1"), tree2.resolve("c/c1"), true, rit.next());
+ assertResultEquals(tree1.resolve("c/c1/commands.bat"), tree2.resolve("c/c1/commands.bat"), true, rit.next());
+ assertResultEquals(tree1.resolve("c/c1/commands.sh"), tree2.resolve("c/c1/commands.sh"), true, rit.next());
+ // No c/c2, c/c2/Aardvark.java, c/c2/Banana.java, d or e here.
+
+ List results = resultSet.results;
+ assertResultNotPresent(tree1.resolve("b/blah.txt"), tree2.resolve("b/blah.txt"), true, results);
+ assertResultNotPresent(tree1.resolve("c/c2"), tree2.resolve("c/c2"), true, results);
+ assertResultNotPresent(tree1.resolve("c/c2/Aardvark.java"), tree2.resolve("c/c2/Aardvark.java"), false, results);
+ assertResultNotPresent(tree1.resolve("c/c2/Banana.java"), null, false, results);
+ assertResultNotPresent(tree1.resolve("d"), null, false, results);
+ assertResultNotPresent(null, tree2.resolve("e"), false, results);
+ assertEquals(7, results.size());
+
+ // TODO: What about paths within war/jar/zip files?
+ // ...at the moment, if we specify a path of "mydir/README.txt" to be ignored,
+ // this will be ignored in the main tree, e.g. /mydir/README.txt but also
+ // within sub-trees if there is a match, e.g. /mydir/README.txt
+ }
+
+ @Test
+ public void canSpecifyFilesThatShouldHaveCertainDifferencesAllowed() throws IOException
+ {
+ Path tree1 = pathFromClasspath("dir_compare/allowed_differences/tree1");
+ Path tree2 = pathFromClasspath("dir_compare/allowed_differences/tree2");
+
+ // Check that two identical trees are... identical!
+ ResultSet resultSet = comparator.compare(tree1, tree2);
+
+ System.out.println("Comparison results:");
+ for (Result r : resultSet.results)
+ {
+ System.out.println("\t"+r);
+ }
+ assertEquals(0, resultSet.stats.differenceCount);
+ assertEquals(0, resultSet.stats.ignoredFileCount);
+ assertEquals(4, resultSet.stats.resultCount);
+ assertEquals(4, resultSet.results.size());
+
+ // Now add files that are different only in there use of tree1 and tree2's absolute paths.
+ File t1File = new File(tree1.toFile(), "different.txt");
+ t1File.deleteOnExit();
+ FileUtils.write(t1File, sampleText(tree1.toAbsolutePath().toString()));
+
+ File t2File = new File(tree2.toFile(), "different.txt");
+ t2File.deleteOnExit();
+ FileUtils.write(t2File, sampleText(tree2.toAbsolutePath().toString()));
+
+ // Now add a module.properties that are different in their "installDate" property only.
+ File t3File = new File(tree1.toFile(), "module.properties");
+ t3File.deleteOnExit();
+ Date date = new Date();
+ FileUtils.write(t3File, sampleModuleProperties("2016-02-29T16\\:26\\:18.053Z"));
+
+ File t4File = new File(tree2.toFile(), "module.properties");
+ t4File.deleteOnExit();
+ FileUtils.write(t4File, sampleModuleProperties("2016-02-28T14\\:30\\:14.035Z"));
+
+
+ // Perform the comparison
+ comparator = new FileTreeCompareImpl(new HashSet(), new HashSet());
+ resultSet = comparator.compare(tree1, tree2);
+ System.out.println("Comparison results:");
+ for (Result r : resultSet.results)
+ {
+ System.out.println("\t"+r);
+ }
+
+ // We should see a difference
+ assertEquals(0, resultSet.stats.suppressedDifferenceCount);
+ assertEquals(2, resultSet.stats.differenceCount);
+ assertEquals(0, resultSet.stats.ignoredFileCount);
+ assertEquals(6, resultSet.stats.resultCount);
+ assertEquals(6, resultSet.results.size());
+
+ Iterator rit = resultSet.results.iterator();
+ assertResultEquals(tree1.resolve("different.txt"), tree2.resolve("different.txt"), false, rit.next());
+
+ // Perform the comparison again, but after allowing the files to be different.
+ Set allowedDiffsPaths = new HashSet<>();
+ allowedDiffsPaths.add(toPlatformPath("**/*.txt"));
+ allowedDiffsPaths.add(toPlatformPath("**/module.properties"));
+
+ // Perform the comparison, this time with some allowed differences
+ comparator = new FileTreeCompareImpl(new HashSet(), allowedDiffsPaths);
+ resultSet = comparator.compare(tree1, tree2);
+
+ // We should see a difference - but it is in the 'suppressed' list.
+ assertEquals(2, resultSet.stats.suppressedDifferenceCount);
+ assertEquals(0, resultSet.stats.differenceCount);
+ assertEquals(0, resultSet.stats.ignoredFileCount);
+ assertEquals(6, resultSet.stats.resultCount);
+ assertEquals(6, resultSet.results.size());
+
+ rit = resultSet.results.iterator();
+ assertResultEquals(tree1.resolve("different.txt"), tree2.resolve("different.txt"), true, rit.next());
+ }
+
+ private String sampleText(String absPath)
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append("This is some example text\n");
+ sb.append("...in tree: "+absPath);
+ sb.append(" ...and here is some more text.\n");
+ sb.append("...but wait! here's an absolute path again:"+absPath+", yes.");
+ sb.append("The End.");
+ return sb.toString();
+ }
+
+ private String sampleModuleProperties(String installDateAsString)
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append("# " + installDateAsString + "\n");
+ sb.append("module.id=org.alfresco.integrations.share.google.docs\n");
+ sb.append("module.version=3.0.3\n");
+ sb.append("module.buildnumber=4ent\n");
+ sb.append("module.title=Alfresco / Google Docs Share Module\n");
+ sb.append("module.description=The Share side artifacts of the Alfresco / Google Docs Integration.\n");
+ sb.append("module.repo.version.min=5.0.0\n");
+ sb.append("module.repo.version.max=5.99.99\n");
+ sb.append("module.installState=INSTALLED\n");
+ // this is the problem we are trying to solve
+ sb.append("module.installDate=" + installDateAsString + "\n");
+
+ return sb.toString();
+ }
+
+ @Test
+ public void canDiffTreesContainingWarFiles()
+ {
+ Path tree1 = pathFromClasspath("dir_compare/file_folders_plus_war/tree1");
+ Path tree2 = pathFromClasspath("dir_compare/file_folders_plus_war/tree2");
+
+ ResultSet resultSet = comparator.compare(tree1, tree2);
+
+ System.out.println("Comparison results:");
+ for (Result r : resultSet.results)
+ {
+ System.out.println("\t"+r);
+ }
+
+ // The 14 top-level results + 17 sub-results.
+ assertEquals(31, resultSet.stats.resultCount);
+
+ // One result for each relative file/folder
+ assertEquals(14, resultSet.results.size());
+
+
+ Iterator rit = resultSet.results.iterator();
+ // TODO: currently all of the files are in one, other or both but where they
+ // are in both, the file *contents* are identical.
+ // TODO: evolve test data and functionality to cope with different file contents.
+ assertResultEquals(tree1.resolve("a"), tree2.resolve("a"), true, rit.next());
+ assertResultEquals(null, tree2.resolve("a/story.txt"), false, rit.next());
+ assertResultEquals(tree1.resolve("b"), tree2.resolve("b"), true, rit.next());
+
+ // Examine the results of the war file comparison
+ Result result = rit.next();
+ // The WAR files are different.
+ assertResultEquals(
+ tree1.resolve("b/alfresco-testdata-webapp.war"),
+ tree2.resolve("b/alfresco-testdata-webapp.war"),
+ false,
+ result);
+ List subResults = result.subResults;
+ System.out.println("subResults:");
+ for (Result r : subResults)
+ {
+ System.out.println("\t"+r);
+ }
+ Iterator subIt = subResults.iterator();
+ Path subTree1 = result.subTree1;
+ Path subTree2 = result.subTree2;
+ assertEquals(17, subResults.size());
+ assertResultEquals(subTree1.resolve("META-INF"), subTree2.resolve("META-INF"), true, subIt.next());
+ assertResultEquals(subTree1.resolve("META-INF/MANIFEST.MF"), subTree2.resolve("META-INF/MANIFEST.MF"), false, subIt.next());
+ assertResultEquals(subTree1.resolve("META-INF/maven"), subTree2.resolve("META-INF/maven"), true, subIt.next());
+ assertResultEquals(subTree1.resolve("META-INF/maven/org.alfresco.dummy"), subTree2.resolve("META-INF/maven/org.alfresco.dummy"), true, subIt.next());
+ assertResultEquals(subTree1.resolve("META-INF/maven/org.alfresco.dummy/alfresco-testdata-webapp"), subTree2.resolve("META-INF/maven/org.alfresco.dummy/alfresco-testdata-webapp"), true, subIt.next());
+ assertResultEquals(subTree1.resolve("META-INF/maven/org.alfresco.dummy/alfresco-testdata-webapp/pom.properties"), subTree2.resolve("META-INF/maven/org.alfresco.dummy/alfresco-testdata-webapp/pom.properties"), false, subIt.next());
+ assertResultEquals(subTree1.resolve("META-INF/maven/org.alfresco.dummy/alfresco-testdata-webapp/pom.xml"), subTree2.resolve("META-INF/maven/org.alfresco.dummy/alfresco-testdata-webapp/pom.xml"), true, subIt.next());
+ assertResultEquals(subTree1.resolve("WEB-INF"), subTree2.resolve("WEB-INF"), true, subIt.next());
+ assertResultEquals(subTree1.resolve("WEB-INF/classes"), subTree2.resolve("WEB-INF/classes"), true, subIt.next());
+ assertResultEquals(subTree1.resolve("WEB-INF/classes/org"), subTree2.resolve("WEB-INF/classes/org"), true, subIt.next());
+ assertResultEquals(subTree1.resolve("WEB-INF/classes/org/alfresco"), subTree2.resolve("WEB-INF/classes/org/alfresco"), true, subIt.next());
+ assertResultEquals(subTree1.resolve("WEB-INF/classes/org/alfresco/testdata"), subTree2.resolve("WEB-INF/classes/org/alfresco/testdata"), true, subIt.next());
+ assertResultEquals(subTree1.resolve("WEB-INF/classes/org/alfresco/testdata/webapp"), subTree2.resolve("WEB-INF/classes/org/alfresco/testdata/webapp"), true, subIt.next());
+ assertResultEquals(null, subTree2.resolve("WEB-INF/classes/org/alfresco/testdata/webapp/Another.class"), false, subIt.next());
+ assertResultEquals(subTree1.resolve("WEB-INF/classes/org/alfresco/testdata/webapp/ExampleJavaClass.class"), subTree2.resolve("WEB-INF/classes/org/alfresco/testdata/webapp/ExampleJavaClass.class"), true, subIt.next());
+ assertResultEquals(subTree1.resolve("WEB-INF/web.xml"), subTree2.resolve("WEB-INF/web.xml"), true, subIt.next());
+ assertResultEquals(subTree1.resolve("index.jsp"), subTree2.resolve("index.jsp"), false, subIt.next());
+
+ // Back up to the top-level comparisons
+ assertResultEquals(tree1.resolve("b/blah.txt"), tree2.resolve("b/blah.txt"), true, rit.next());
+ assertResultEquals(tree1.resolve("c"), tree2.resolve("c"), true, rit.next());
+ assertResultEquals(tree1.resolve("c/c1"), tree2.resolve("c/c1"), true, rit.next());
+ assertResultEquals(tree1.resolve("c/c1/commands.bat"), tree2.resolve("c/c1/commands.bat"), true, rit.next());
+ assertResultEquals(tree1.resolve("c/c1/commands.sh"), tree2.resolve("c/c1/commands.sh"), true, rit.next());
+ assertResultEquals(tree1.resolve("c/c2"), tree2.resolve("c/c2"), true, rit.next());
+ // Aardvark.java appears in both trees but is not the same!
+ assertResultEquals(tree1.resolve("c/c2/Aardvark.java"), tree2.resolve("c/c2/Aardvark.java"), false, rit.next());
+ assertResultEquals(tree1.resolve("c/c2/Banana.java"), null, false, rit.next());
+ assertResultEquals(tree1.resolve("d"), null, false, rit.next());
+ assertResultEquals(null, tree2.resolve("e"), false, rit.next());
+ }
+
+ private void assertResultNotPresent(Path p1, Path p2, boolean contentEqual, List results)
+ {
+ Result r = new Result();
+ r.p1 = p1;
+ r.p2 = p2;
+ r.equal = contentEqual;
+ assertFalse("Result should not be present: "+r, results.contains(r));
+ }
+
+ private void assertResultEquals(Path p1, Path p2, boolean contentEqual, Result result)
+ {
+ Result expected = new Result();
+ expected.p1 = p1;
+ expected.p2 = p2;
+ expected.equal = contentEqual;
+ assertEquals(expected, result);
+ }
+
+ private Path pathFromClasspath(String path)
+ {
+ try
+ {
+ return Paths.get(getClass().getClassLoader().getResource(path).toURI());
+ }
+ catch (URISyntaxException error)
+ {
+ throw new RuntimeException("");
+ }
+ }
+
+ private String toPlatformPath(String path)
+ {
+ return path.replace("/", File.separator);
+ }
+}
diff --git a/enterprise-update-test/src/test/java/org/alfresco/update/tool/dircomp/HtmlResultFormatterTest.java b/enterprise-update-test/src/test/java/org/alfresco/update/tool/dircomp/HtmlResultFormatterTest.java
new file mode 100644
index 0000000000..bd387646db
--- /dev/null
+++ b/enterprise-update-test/src/test/java/org/alfresco/update/tool/dircomp/HtmlResultFormatterTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2016 Alfresco Software, Ltd. All rights reserved.
+ *
+ * License rights for this program may be obtained from Alfresco Software, Ltd.
+ * pursuant to a written agreement and any use of this program without such an
+ * agreement is prohibited.
+ */
+package org.alfresco.update.tool.dircomp;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.springframework.util.AntPathMatcher;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for the {@link HtmlResultFormatter} class.
+ *