| /* |
| * 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.jackrabbit.filevault.maven.packaging.it; |
| |
| import static org.hamcrest.CoreMatchers.notNullValue; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertThat; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| |
| import java.io.File; |
| import java.io.FileReader; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.io.Reader; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.Files; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Enumeration; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.Set; |
| import java.util.jar.JarEntry; |
| import java.util.jar.JarFile; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipException; |
| import java.util.zip.ZipFile; |
| |
| import org.apache.commons.io.IOUtils; |
| import org.apache.commons.io.input.BoundedReader; |
| import org.apache.maven.it.VerificationException; |
| import org.apache.maven.it.Verifier; |
| import org.apache.maven.model.Model; |
| import org.apache.maven.model.io.xpp3.MavenXpp3Reader; |
| import org.apache.maven.shared.utils.StringUtils; |
| import org.apache.maven.shared.utils.io.FileUtils; |
| import org.apache.maven.shared.utils.io.IOUtil; |
| import org.codehaus.plexus.util.xml.pull.XmlPullParserException; |
| import org.hamcrest.Description; |
| import org.hamcrest.Matchers; |
| import org.hamcrest.TypeSafeMatcher; |
| import org.junit.Assert; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import aQute.bnd.header.Attrs; |
| import aQute.bnd.header.Parameters; |
| |
| /** |
| * Helper class to build and verify a maven project. |
| */ |
| public class ProjectBuilder { |
| |
| /** |
| * default logger |
| */ |
| private static final Logger log = LoggerFactory.getLogger(ProjectBuilder.class); |
| |
| private static final Set<String> IGNORED_MANIFEST_ENTRIES = new HashSet<>(Arrays.asList("Build-Jdk-Spec", "Created-By")); |
| |
| public static final String TEST_PROJECTS_ROOT = "target/test-classes/test-projects"; |
| |
| static final String TEST_PACKAGE_DEFAULT_NAME = "target/package-plugin-test-pkg-1.0.0-SNAPSHOT.zip"; |
| |
| static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("%(\\d*)%"); |
| |
| private File testProjectsRoot; |
| |
| private File testProjectDir; |
| |
| private File testPackageFile; |
| |
| private Properties testProperties; |
| |
| private String[] testGoals = {"clean", "package"}; |
| |
| private List<String> pkgZipEntries; |
| |
| private File expectedFilesFile; |
| |
| private File expectedOrderFile; |
| |
| private File expectedManifestFile; |
| |
| private File expectedFilterFile; |
| |
| private File expectedFilesWithChecksumsFile; |
| |
| private File expectedLogLinesFile; |
| |
| private File logTxtFile; |
| |
| private boolean buildExpectedToFail; |
| |
| private boolean verifyPackageContents = true; |
| |
| public ProjectBuilder() { |
| testProjectsRoot = new File(TEST_PROJECTS_ROOT); |
| testProperties = new Properties(); |
| |
| testProperties.put("plugin.version", getPluginVersion()); |
| testProperties.put("testcontent.directory", new File("target/test-classes/test-content").getAbsolutePath()); |
| testProperties.put("maven.compiler.source", "1.8"); |
| testProperties.put("maven.compiler.target", "1.8"); |
| } |
| |
| /** |
| * Retrieves the version of the {@code filevault-package-maven-plugin} of the current project. The version is used in the |
| * test poms so that the cli build uses the current plugin. Usually the version is set via the system property |
| * {@code plugin.version} via the failsafe plugin. If the property is missing the method tries to read it from the |
| * {@code pom.xml} of the project. this is useful when running the tests in an IDE. |
| * |
| * @return the version of the current {@code filevault-package-maven-plugin} |
| * @throws IllegalArgumentException if the version cannot be determined. |
| */ |
| private String getPluginVersion() { |
| String pluginVersion = System.getProperty("plugin.version"); |
| if (pluginVersion == null) { |
| try (FileReader fileReader = new FileReader("pom.xml")) { |
| // try to read from project |
| MavenXpp3Reader reader = new MavenXpp3Reader(); |
| Model model = reader.read(fileReader); |
| pluginVersion = model.getVersion(); |
| } catch (IOException | XmlPullParserException e) { |
| log.error("Unable to read version from pom", e); |
| } |
| } |
| if (pluginVersion == null) { |
| throw new IllegalArgumentException("Unable to detect plugin.version"); |
| } |
| return pluginVersion; |
| } |
| |
| public ProjectBuilder setTestProjectsRoot(File testProjectsRoot) { |
| if (testProjectDir != null) { |
| throw new IllegalArgumentException("projects root can't be changed after projects dir is set."); |
| } |
| this.testProjectsRoot = testProjectsRoot; |
| return this; |
| } |
| |
| public ProjectBuilder setTestProjectDir(File testProjectDir) { |
| this.testProjectDir = testProjectDir; |
| this.testPackageFile = new File(testProjectDir, TEST_PACKAGE_DEFAULT_NAME); |
| |
| // if we ever need different files, just create the setters. |
| this.expectedFilesFile = new File(testProjectDir, "expected-files.txt"); |
| this.expectedOrderFile = new File(testProjectDir, "expected-file-order.txt"); |
| this.expectedManifestFile = new File(testProjectDir, "expected-manifest.txt"); |
| this.expectedFilterFile = new File(testProjectDir, "expected-filter.xml"); |
| this.expectedFilesWithChecksumsFile = new File(testProjectDir, "expected-files-with-checksums.txt"); |
| this.expectedLogLinesFile = new File(testProjectDir, "expected-log-lines.txt"); |
| this.logTxtFile = new File(testProjectDir, "log.txt"); |
| |
| return this; |
| } |
| |
| public ProjectBuilder setTestProjectDir(String relPath) { |
| return setTestProjectDir(new File(testProjectsRoot, relPath)); |
| } |
| |
| |
| public File getTestProjectDir() { |
| return testProjectDir; |
| } |
| |
| public ProjectBuilder setTestPackageFile(File testPackageFile) { |
| this.testPackageFile = testPackageFile; |
| return this; |
| } |
| |
| public ProjectBuilder setTestPackageFile(String testPackageFileName) { |
| this.setTestPackageFile(new File(testProjectDir, testPackageFileName)); |
| return this; |
| } |
| |
| public File getTestPackageFile() { |
| return testPackageFile; |
| } |
| |
| public ProjectBuilder setTestGoals(String ... testGoals) { |
| if (testGoals != null && testGoals.length != 0) { |
| this.testGoals = testGoals; |
| } |
| return this; |
| } |
| |
| public ProjectBuilder setBuildExpectedToFail(boolean buildExpectedToFail) { |
| this.buildExpectedToFail = buildExpectedToFail; |
| return this; |
| } |
| |
| public ProjectBuilder setVerifyPackageContents(boolean verifyPackageContents) { |
| this.verifyPackageContents = verifyPackageContents; |
| return this; |
| } |
| |
| public ProjectBuilder setProperty(String name, String value) { |
| testProperties.put(name, value); |
| return this; |
| } |
| |
| public ProjectBuilder setExpectedFilesWithChecksumsFile(String expectedFilesWithChecksumsFile) { |
| this.expectedFilesWithChecksumsFile = new File(testProjectDir, expectedFilesWithChecksumsFile); |
| return this; |
| } |
| |
| public ProjectBuilder build() throws VerificationException, IOException { |
| Verifier verifier = new Verifier(testProjectDir.getAbsolutePath()); |
| verifier.setSystemProperties(testProperties); |
| verifier.setDebug(true); |
| verifier.setAutoclean(false); |
| // verifier.setDebugJvm(true); |
| //verifier.setMavenDebug(true); |
| try { |
| verifier.executeGoals(Arrays.asList(testGoals)); |
| assertFalse("Build expected to fail in project " + testProjectDir.getAbsolutePath(), buildExpectedToFail); |
| } catch (VerificationException e) { |
| if (buildExpectedToFail) { |
| return this; |
| } |
| throw e; |
| } finally { |
| verifier.resetStreams(); |
| } |
| verifier.verify(true); |
| |
| if (!verifyPackageContents) { |
| return this; |
| } |
| |
| // read zip |
| pkgZipEntries = verifyPackageZipEntries(testPackageFile); |
| return this; |
| } |
| |
| static List<String> verifyPackageZipEntries(File packageFile) throws IOException { |
| assertTrue("Project generates package file at " + packageFile, packageFile.exists()); |
| |
| List<String> pkgZipEntries = new ArrayList<>(); |
| try (JarFile jar = new JarFile(packageFile)) { |
| Enumeration<JarEntry> e = jar.entries(); |
| while (e.hasMoreElements()) { |
| pkgZipEntries.add(e.nextElement().getName()); |
| } |
| } |
| // ensure that MANIFEST.MF is first entry |
| String first = pkgZipEntries.get(0); |
| if ("META-INF/".equals(first)) { |
| first = pkgZipEntries.get(1); |
| } |
| assertEquals("MANIFEST.MF must be first entry in package " + packageFile, "META-INF/MANIFEST.MF", first); |
| |
| // ensure that there is a jcr_root directory |
| assertTrue("Package does not contain mandatory 'jcr_root' folder in package " + packageFile, pkgZipEntries.contains("jcr_root/")); |
| return pkgZipEntries; |
| } |
| public ProjectBuilder verifyPackageProperty(String key, String value) throws IOException { |
| if (buildExpectedToFail) { |
| return this; |
| } |
| assertEquals("Property '" + key + "' has correct value", value, getPackageProperty(key)); |
| return this; |
| } |
| |
| public String getPackageProperty(String key) throws ZipException, IOException { |
| Properties properties; |
| try (ZipFile zip = new ZipFile(testPackageFile)) { |
| ZipEntry propertiesFile = zip.getEntry("META-INF/vault/properties.xml"); |
| assertThat(propertiesFile, notNullValue()); |
| |
| properties = new Properties(); |
| properties.loadFromXML(zip.getInputStream(propertiesFile)); |
| } |
| return properties.getProperty(key); |
| } |
| |
| public ProjectBuilder verifyExpectedManifest() throws IOException { |
| final String expected = FileUtils.fileRead(expectedManifestFile); |
| List<String> entries; |
| String result; |
| try (JarFile jar = new JarFile(testPackageFile)) { |
| entries = new ArrayList<>(); |
| for (Map.Entry<Object, Object> e : jar.getManifest().getMainAttributes().entrySet()) { |
| String key = e.getKey().toString(); |
| if (IGNORED_MANIFEST_ENTRIES.contains(key)) { |
| continue; |
| } |
| if ("Import-Package".equals(key)) { |
| // split export package so that we have a sorted set |
| Parameters params = new Parameters(e.getValue().toString()); |
| for (Map.Entry<String, Attrs> entry : params.entrySet()) { |
| entries.add(key + ":" + entry.getKey() + ";" + entry.getValue()); |
| } |
| continue; |
| } |
| entries.add(e.getKey() + ":" + e.getValue()); |
| } |
| } |
| Collections.sort(entries); |
| result = StringUtils.join(entries.iterator(), "\n"); |
| assertEquals("Manifest", normalizeWhitespace(expected), normalizeWhitespace(result)); |
| return this; |
| } |
| |
| public ProjectBuilder verifyExpectedFiles() throws IOException { |
| verifyExpectedFiles(expectedFilesFile, pkgZipEntries); |
| return this; |
| } |
| |
| public ProjectBuilder verifyExpectedFiles(File expectedFilesFile, List<String> pkgZipEntries) throws IOException { |
| // first check that only the expected entries are there in the package (regardless of the order) |
| List<String> expectedEntries = Files.readAllLines(expectedFilesFile.toPath(), StandardCharsets.UTF_8); |
| assertEquals("Package contains the expected entry names", |
| toTidyString(expectedEntries), |
| toTidyString(pkgZipEntries)); |
| return this; |
| } |
| |
| public ProjectBuilder verifyExpectedFilesChecksum() throws IOException { |
| List<String> expectedEntriesWithChecksums = Files.readAllLines(expectedFilesWithChecksumsFile.toPath(), StandardCharsets.UTF_8); |
| for (String expectedEntryWithChecksum : expectedEntriesWithChecksums) { |
| // split name and checksum |
| String[] parts = expectedEntryWithChecksum.split(" ", 2); |
| verifyExpectedFileChecksum(parts[0], parts[1]); |
| } |
| return this; |
| } |
| |
| public ProjectBuilder verifyExpectedFileChecksum(String name, String checksum) throws IOException { |
| // the second part must be a hexadecimal CRC32 checksum |
| final long expectedChecksum = Long.parseLong(checksum, 16); |
| try (JarFile jar = new JarFile(testPackageFile)) { |
| JarEntry entry = jar.getJarEntry(name); |
| if (entry == null) { |
| fail("Could not find entry with name " + name + " in package " + testPackageFile); |
| } |
| Assert.assertThat(entry, new JarEntryMatcher(name, jar, expectedChecksum)); |
| } |
| return this; |
| } |
| |
| private final static class JarEntryMatcher extends TypeSafeMatcher<JarEntry> { |
| |
| private final String name; |
| private final long expectedCrc; |
| private final JarFile jarFile; |
| public JarEntryMatcher(String name, JarFile jarFile, long expectedCrc) { |
| this.name = name; |
| this.jarFile = jarFile; |
| this.expectedCrc = expectedCrc; |
| } |
| |
| @Override |
| protected void describeMismatchSafely(JarEntry item, Description mismatchDescription) { |
| mismatchDescription.appendText("was Jar entry with name ").appendValue(item.getName()).appendText(" having the CRC ").appendValue(Long.toHexString(item.getCrc())); |
| try (Reader reader = new BoundedReader(new InputStreamReader(jarFile.getInputStream(item), StandardCharsets.UTF_8), 8000)) { |
| String content = IOUtils.toString(reader); |
| // make new line visible |
| content = content.replaceAll("\r", Matcher.quoteReplacement("\r")).replaceAll("\n", Matcher.quoteReplacement("\n")); |
| mismatchDescription.appendText(" (").appendValue(content).appendText(")"); |
| } catch (IOException e) { |
| mismatchDescription.appendText(" (Could not extract value due to exception ").appendValue(e).appendText(")"); |
| } |
| } |
| |
| @Override |
| protected boolean matchesSafely(JarEntry item) { |
| return expectedCrc == item.getCrc(); |
| } |
| |
| @Override |
| public void describeTo(Description description) { |
| description.appendText("Jar entry with name ").appendValue(name).appendText(" having the CRC ").appendValue(Long.toHexString(expectedCrc)); |
| } |
| } |
| |
| public ProjectBuilder verifyExpectedFilesOrder() throws IOException { |
| List<String> expectedEntriesInOrder= Files.readAllLines(expectedOrderFile.toPath(), StandardCharsets.UTF_8); |
| assertThat("Order of entries within package", pkgZipEntries, Matchers.containsInRelativeOrder(expectedEntriesInOrder.toArray())); |
| return this; |
| } |
| |
| public ProjectBuilder verifyExpectedFilter() throws IOException { |
| if (buildExpectedToFail) { |
| return this; |
| } |
| try (ZipFile zip = new ZipFile(testPackageFile)) { |
| ZipEntry entry = zip.getEntry("META-INF/vault/filter.xml"); |
| assertNotNull("package has a filter.xml", entry); |
| String result = IOUtil.toString(zip.getInputStream(entry), "utf-8"); |
| String expected = FileUtils.fileRead(expectedFilterFile); |
| assertEquals("filter.xml is correct", normalizeWhitespace(expected), normalizeWhitespace(result)); |
| } |
| return this; |
| } |
| |
| public ProjectBuilder verifyExpectedFilterInWorkDirectory(final String workDirectory) throws IOException { |
| if (buildExpectedToFail) { |
| return this; |
| } |
| File workDirFile = new File(testProjectDir, workDirectory); |
| assertTrue("workDirectory should exist: " + workDirFile.toString(), workDirFile.isDirectory()); |
| File filterFile = new File(workDirFile, "META-INF/vault/filter.xml"); |
| assertTrue("filterFile should exist: " + filterFile.toString(), filterFile.isFile()); |
| String result = FileUtils.fileRead(filterFile); |
| String expected = FileUtils.fileRead(expectedFilterFile); |
| assertEquals("filter.xml is incorrect", normalizeWhitespace(expected), normalizeWhitespace(result)); |
| return this; |
| } |
| |
| public ProjectBuilder verifyExpectedLogLines(String... placeholderValues) throws IOException { |
| List<String> expectedLogLines = Files.readAllLines(expectedLogLinesFile.toPath()); |
| List<String> actualLogLines = getBuildOutput(); |
| for (String expectedLogLine : expectedLogLines) { |
| // do placeholder replacement |
| |
| Matcher matcher = PLACEHOLDER_PATTERN.matcher(expectedLogLine); |
| while (matcher.find()) { |
| int placeholderIndex = Integer.parseInt(matcher.group(1)); |
| if (placeholderIndex >= placeholderValues.length) { |
| throw new IllegalArgumentException("At least " + placeholderIndex + " placeholder values need to be given, but only "+ placeholderValues.length + " received."); |
| } |
| // replace current item in iterator with the new value |
| expectedLogLine = matcher.replaceAll(Matcher.quoteReplacement(placeholderValues[placeholderIndex])); |
| } |
| // update list |
| assertThat("Could not find the expected log line in the output '" + logTxtFile +"'", actualLogLines, Matchers.hasItem(expectedLogLine)); |
| } |
| // support not and exists |
| return this; |
| } |
| public List<String> getBuildOutput() throws IOException { |
| return Files.readAllLines(logTxtFile.toPath(), StandardCharsets.UTF_8); |
| } |
| |
| private String toTidyString(List<String> lines) { |
| String[] copy = lines.toArray(new String[lines.size()]); |
| Arrays.sort(copy); |
| StringBuilder buf = new StringBuilder(); |
| for (String line: copy) { |
| buf.append(line).append("\n"); |
| } |
| return buf.toString(); |
| } |
| |
| /** |
| * Eliminates differences in line separators when executing tests on different platform (*nix / windows) |
| */ |
| private String normalizeWhitespace(String s) { |
| return s.replaceAll("[\r\n]+", "\n"); |
| } |
| } |