diff --git a/jetty-start/pom.xml b/jetty-start/pom.xml index 3ea04f9b8c3..17d2a253668 100644 --- a/jetty-start/pom.xml +++ b/jetty-start/pom.xml @@ -32,41 +32,9 @@ org.eclipse.jetty.start.* - - org.apache.maven.plugins - maven-shade-plugin - - true - false - - - org.eclipse.jetty:jetty-util - - - - - org.eclipse.jetty.util - org.eclipse.jetty.start.util - - - - - - package - - shade - - - - - - org.eclipse.jetty - jetty-util - ${project.version} - org.eclipse.jetty.toolchain jetty-test-helper diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/Modules.java b/jetty-start/src/main/java/org/eclipse/jetty/start/Modules.java index f52bd049122..3fee8802054 100644 --- a/jetty-start/src/main/java/org/eclipse/jetty/start/Modules.java +++ b/jetty-start/src/main/java/org/eclipse/jetty/start/Modules.java @@ -34,8 +34,6 @@ import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.eclipse.jetty.util.TopologicalSort; - /** * Access for all modules declared, as well as what is enabled. */ @@ -205,15 +203,8 @@ public class Modules implements Iterable module.getDepends().forEach(add); module.getOptional().forEach(add); } - try - { - sort.sort(_modules); - } - catch (IllegalStateException e) - { - System.err.println(sort.dump()); - throw e; - } + + sort.sort(_modules); } public List getEnabled() diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/StartLog.java b/jetty-start/src/main/java/org/eclipse/jetty/start/StartLog.java index 1d967793a4a..580ebfbe11c 100644 --- a/jetty-start/src/main/java/org/eclipse/jetty/start/StartLog.java +++ b/jetty-start/src/main/java/org/eclipse/jetty/start/StartLog.java @@ -41,6 +41,7 @@ public class StartLog private final static PrintStream stderr = System.err; private static volatile PrintStream out = System.out; private static volatile PrintStream err = System.err; + private static volatile PrintStream logStream = System.err; private final static StartLog INSTANCE = new StartLog(); public static void debug(String format, Object... args) @@ -74,12 +75,12 @@ public class StartLog public static void log(String type, String msg) { - err.println(type + ": " + msg); + logStream.println(type + ": " + msg); } public static void log(String type, String format, Object... args) { - err.printf(type + ": " + format + "%n",args); + logStream.printf(type + ": " + format + "%n",args); } public static void info(String format, Object... args) @@ -94,7 +95,7 @@ public class StartLog public static void warn(Throwable t) { - t.printStackTrace(err); + t.printStackTrace(logStream); } public static boolean isDebugEnabled() @@ -163,9 +164,10 @@ public class StartLog err.println("StartLog to " + logfile); OutputStream fileout = Files.newOutputStream(startLog,StandardOpenOption.CREATE,StandardOpenOption.APPEND); - PrintStream logger = new PrintStream(fileout); + PrintStream logger = new PrintStream(fileout,true); out=logger; err=logger; + setStream(logger); System.setErr(logger); System.setOut(logger); err.println("StartLog Establishing " + logfile + " on " + new Date()); @@ -189,7 +191,20 @@ public class StartLog err.println("StartLog ended"); stderr.println("StartLog ended"); } + setStream(stderr); System.setErr(stderr); System.setOut(stdout); } + + public static PrintStream getStream() + { + return logStream; + } + + public static PrintStream setStream(PrintStream stream) + { + PrintStream ret = logStream; + logStream = stream; + return ret; + } } diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/TopologicalSort.java b/jetty-start/src/main/java/org/eclipse/jetty/start/TopologicalSort.java new file mode 100644 index 00000000000..5253657b188 --- /dev/null +++ b/jetty-start/src/main/java/org/eclipse/jetty/start/TopologicalSort.java @@ -0,0 +1,185 @@ +// +// ======================================================================== +// Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.start; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * Topological sort a list or array. + *

A Topological sort is used when you have a partial ordering expressed as + * dependencies between elements (also often represented as edges in a directed + * acyclic graph). A Topological sort should not be used when you have a total + * ordering expressed as a {@link Comparator} over the items. The algorithm has + * the additional characteristic that dependency sets are sorted by the original + * list order so that order is preserved when possible.

+ *

+ * The sort algorithm works by recursively visiting every item, once and + * only once. On each visit, the items dependencies are first visited and then the + * item is added to the sorted list. Thus the algorithm ensures that dependency + * items are always added before dependent items.

+ * + * @param The type to be sorted. It must be able to be added to a {@link HashSet} + */ +@SuppressWarnings("Duplicates") +public class TopologicalSort +{ + private final Map> _dependencies = new HashMap<>(); + + /** + * Add a dependency to be considered in the sort. + * @param dependent The dependent item will be sorted after all its dependencies + * @param dependency The dependency item, will be sorted before its dependent item + */ + public void addDependency(T dependent, T dependency) + { + Set set = _dependencies.get(dependent); + if (set==null) + { + set=new HashSet<>(); + _dependencies.put(dependent,set); + } + set.add(dependency); + } + + /** Sort the passed array according to dependencies previously set with + * {@link #addDependency(Object, Object)}. Where possible, ordering will be + * preserved if no dependency + * @param array The array to be sorted. + */ + public void sort(T[] array) + { + List sorted = new ArrayList<>(); + Set visited = new HashSet<>(); + Comparator comparator = new InitialOrderComparator<>(array); + + // Visit all items in the array + for (T t : array) + visit(t,visited,sorted,comparator); + + sorted.toArray(array); + } + + /** Sort the passed list according to dependencies previously set with + * {@link #addDependency(Object, Object)}. Where possible, ordering will be + * preserved if no dependency + * @param list The list to be sorted. + */ + public void sort(Collection list) + { + List sorted = new ArrayList<>(); + Set visited = new HashSet<>(); + Comparator comparator = new InitialOrderComparator<>(list); + + // Visit all items in the list + for (T t : list) + visit(t,visited,sorted,comparator); + + list.clear(); + list.addAll(sorted); + } + + /** Visit an item to be sorted. + * @param item The item to be visited + * @param visited The Set of items already visited + * @param sorted The list to sort items into + * @param comparator A comparator used to sort dependencies. + */ + private void visit(T item, Set visited, List sorted,Comparator comparator) + { + // If the item has not been visited + if(!visited.contains(item)) + { + // We are visiting it now, so add it to the visited set + visited.add(item); + + // Lookup the items dependencies + Set dependencies = _dependencies.get(item); + if (dependencies!=null) + { + // Sort the dependencies + SortedSet ordered_deps = new TreeSet<>(comparator); + ordered_deps.addAll(dependencies); + + // recursively visit each dependency + for (T d:ordered_deps) + visit(d,visited,sorted,comparator); + } + + // Now that we have visited all our dependencies, they and their + // dependencies will have been added to the sorted list. So we can + // now add the current item and it will be after its dependencies + sorted.add(item); + } + else if (!sorted.contains(item)) + // If we have already visited an item, but it has not yet been put in the + // sorted list, then we must be in a cycle! + throw new IllegalStateException("cyclic at "+item); + } + + + /** A comparator that is used to sort dependencies in the order they + * were in the original list. This ensures that dependencies are visited + * in the original order and no needless reordering takes place. + * @param + */ + private static class InitialOrderComparator implements Comparator + { + private final Map _indexes = new HashMap<>(); + InitialOrderComparator(T[] initial) + { + int i=0; + for (T t : initial) + _indexes.put(t,i++); + } + + InitialOrderComparator(Collection initial) + { + int i=0; + for (T t : initial) + _indexes.put(t,i++); + } + + @Override + public int compare(T o1, T o2) + { + Integer i1=_indexes.get(o1); + Integer i2=_indexes.get(o2); + if (i1==null || i2==null || i1.equals(o2)) + return 0; + if (i1 expectedXmls = new ArrayList<>(); for (String line : textFile) @@ -89,10 +103,10 @@ public class ConfigurationAssert List actualXmls = new ArrayList<>(); for (Path xml : args.getXmlFiles()) { - actualXmls.add(shorten(baseHome,xml,testResourcesDir)); + actualXmls.add(shorten(baseHome, xml, testResourcesDir)); } - assertOrdered("XML Resolution Order",expectedXmls,actualXmls); - + assertOrdered("XML Resolution Order", expectedXmls, actualXmls); + // Validate LIBs (order is not important) List expectedLibs = new ArrayList<>(); for (String line : textFile) @@ -105,10 +119,10 @@ public class ConfigurationAssert List actualLibs = new ArrayList<>(); for (File path : args.getClasspath()) { - actualLibs.add(shorten(baseHome,path.toPath(),testResourcesDir)); + actualLibs.add(shorten(baseHome, path.toPath(), testResourcesDir)); } - assertContainsUnordered("Libs",expectedLibs,actualLibs); - + assertContainsUnordered("Libs", expectedLibs, actualLibs); + // Validate PROPERTIES (order is not important) Set expectedProperties = new HashSet<>(); for (String line : textFile) @@ -123,16 +137,16 @@ public class ConfigurationAssert { String name = prop.key; if ("jetty.home".equals(name) || "jetty.base".equals(name) || - "user.dir".equals(name) || prop.origin.equals(Props.ORIGIN_SYSPROP) || - name.startsWith("java.")) + "user.dir".equals(name) || prop.origin.equals(Props.ORIGIN_SYSPROP) || + name.startsWith("java.")) { // strip these out from assertion, to make assertions easier. continue; } actualProperties.add(prop.key + "=" + args.getProperties().expand(prop.value)); } - assertContainsUnordered("Properties",expectedProperties,actualProperties); - + assertContainsUnordered("Properties", expectedProperties, actualProperties); + // Validate Downloads List expectedDownloads = new ArrayList<>(); for (String line : textFile) @@ -147,20 +161,34 @@ public class ConfigurationAssert { if (darg.uri != null) { - actualDownloads.add(String.format("%s|%s",darg.uri,darg.location)); + actualDownloads.add(String.format("%s|%s", darg.uri, darg.location)); } } - assertContainsUnordered("Downloads",expectedDownloads,actualDownloads); - - textFile.stream() - .filter(s->s.startsWith("EXISTS|")).map(f->f.substring(7)).forEach(f-> + assertContainsUnordered("Downloads", expectedDownloads, actualDownloads); + + // File / Path Existence Checks + streamOf(textFile, "EXISTS").forEach(f -> { - Path path=baseHome.getBasePath().resolve(f); - assertTrue(baseHome.toShortForm(path)+" exists?",Files.exists(path)); - assertEquals(baseHome.toShortForm(path)+" isDir?",f.endsWith("/"),Files.isDirectory(path)); + Path path = baseHome.getPath(f); + if (f.endsWith("/")) + { + PathAssert.assertDirExists("Required Directory", path); + } + else + { + PathAssert.assertFileExists("Required File", path); + } + }); + + // Output Validation + streamOf(textFile, "OUTPUT").forEach(regex -> + { + Pattern pat = Pattern.compile(regex); + Matcher mat = pat.matcher(output); + assertTrue("Output [\n" + output + "]\nContains Regex Match: " + pat.pattern(), mat.find()); }); } - + private static String shorten(BaseHome baseHome, Path path, Path testResourcesDir) { String value = baseHome.toShortForm(path); @@ -168,7 +196,7 @@ public class ConfigurationAssert { return value; } - + if (path.startsWith(testResourcesDir)) { int len = testResourcesDir.toString().length(); @@ -181,41 +209,47 @@ public class ConfigurationAssert { try { - Assert.assertEquals(msg,expectedSet.size(),actualSet.size()); + Assert.assertEquals(msg, expectedSet.size(), actualSet.size()); if (!expectedSet.isEmpty()) - Assert.assertThat(msg,actualSet,Matchers.containsInAnyOrder(expectedSet.toArray())); + assertThat(msg, actualSet, Matchers.containsInAnyOrder(expectedSet.toArray())); } - catch(AssertionError e) + catch (AssertionError e) { - System.err.println("Expected: "+expectedSet); - System.err.println("Actual : "+actualSet); + System.err.println("Expected: " + expectedSet); + System.err.println("Actual : " + actualSet); throw e; } } - + public static void assertOrdered(String msg, List expectedList, List actualList) { try { - Assert.assertEquals(msg,expectedList.size(),actualList.size()); + Assert.assertEquals(msg, expectedList.size(), actualList.size()); if (!expectedList.isEmpty()) - Assert.assertThat(msg,actualList,Matchers.contains(expectedList.toArray())); + assertThat(msg, actualList, Matchers.contains(expectedList.toArray())); } - catch(AssertionError e) + catch (AssertionError e) { - System.err.println("Expected: "+expectedList); - System.err.println("Actual : "+actualList); + System.err.println("Expected: " + expectedList); + System.err.println("Actual : " + actualList); throw e; } } - + + private static Stream streamOf(TextFile textFile, String key) + { + return textFile.stream() + .filter(s -> s.startsWith(key + "|")).map(f -> getValue(f)); + } + private static String getValue(String arg) { int idx = arg.indexOf('|'); - Assert.assertThat("Expecting '|' sign in [" + arg + "]",idx,greaterThanOrEqualTo(0)); + assertThat("Expecting '|' sign in [" + arg + "]", idx, greaterThanOrEqualTo(0)); String value = arg.substring(idx + 1).trim(); - Assert.assertThat("Expecting Value after '|' in [" + arg + "]",value.length(),greaterThan(0)); + assertThat("Expecting Value after '|' in [" + arg + "]", value.length(), greaterThan(0)); return value; } } diff --git a/jetty-start/src/test/java/org/eclipse/jetty/start/MainTest.java b/jetty-start/src/test/java/org/eclipse/jetty/start/MainTest.java index 799a5a16963..4705f3d3412 100644 --- a/jetty-start/src/test/java/org/eclipse/jetty/start/MainTest.java +++ b/jetty-start/src/test/java/org/eclipse/jetty/start/MainTest.java @@ -30,7 +30,7 @@ import java.util.List; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.TestTracker; -import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.toolchain.test.IO; import org.junit.Before; import org.junit.Ignore; import org.junit.Rule; diff --git a/jetty-start/src/test/java/org/eclipse/jetty/start/TestUseCases.java b/jetty-start/src/test/java/org/eclipse/jetty/start/TestUseCases.java index 031f52acc1e..8c183e3978a 100644 --- a/jetty-start/src/test/java/org/eclipse/jetty/start/TestUseCases.java +++ b/jetty-start/src/test/java/org/eclipse/jetty/start/TestUseCases.java @@ -20,15 +20,19 @@ package org.eclipse.jetty.start; import static java.util.stream.Collectors.toList; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FilenameFilter; +import java.io.FileReader; import java.io.IOException; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; -import org.eclipse.jetty.toolchain.test.IO; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.hamcrest.Matchers; import org.junit.Assert; @@ -46,107 +50,114 @@ public class TestUseCases { @Parameters(name = "{0}") public static List getCases() throws Exception - { + { File usecases = MavenTestingUtils.getTestResourceDir("usecases/"); - File[] cases=usecases.listFiles(new FilenameFilter() - { - @Override - public boolean accept(File dir, String name) - { - return name.endsWith(".assert.txt"); - } - }); - + File[] cases = usecases.listFiles((dir, name) -> name.endsWith(".assert.txt")); Arrays.sort(cases); List ret = new ArrayList<>(); - for(File assertTxt:cases) + for (File assertTxt : cases) { - String caseName=assertTxt.getName().replace(".assert.txt",""); - String baseName=caseName.split("\\.")[0]; - ret.add(new Object[] {caseName,baseName, assertTxt, lines(new File(usecases,caseName+".prepare.txt")),lines(new File(usecases,caseName+".cmdline.txt"))}); + String caseName = assertTxt.getName().replace(".assert.txt", ""); + ret.add(new Object[]{caseName}); } return ret; } - - static String[] lines(File file) throws IOException - { - if (!file.exists() || !file.canRead()) - return new String[0]; - return IO.readToString(file).split("[\n\r]+"); - } @Parameter(0) public String caseName; - @Parameter(1) - public String baseName; - - @Parameter(2) - public File assertFile; - - @Parameter(3) - public String[] prepare; - - @Parameter(4) - public String[] commandLineArgs; - @Test public void testUseCase() throws Exception { + String baseName = caseName.replaceFirst("\\..*$", ""); + File assertFile = MavenTestingUtils.getTestResourceFile("usecases/" + caseName + ".assert.txt"); + Path homeDir = MavenTestingUtils.getTestResourceDir("dist-home").toPath().toRealPath(); Path baseSrcDir = MavenTestingUtils.getTestResourceDir("usecases/" + baseName).toPath().toRealPath(); Path baseDir = MavenTestingUtils.getTargetTestingPath(caseName); - if (baseDir.toFile().exists()) - org.eclipse.jetty.toolchain.test.FS.cleanDirectory(baseDir); - else - baseDir.toFile().mkdirs(); - org.eclipse.jetty.toolchain.test.IO.copyDir(baseSrcDir.toFile(),baseDir.toFile()); + org.eclipse.jetty.toolchain.test.FS.ensureEmpty(baseDir); + org.eclipse.jetty.toolchain.test.IO.copyDir(baseSrcDir.toFile(), baseDir.toFile()); + System.setProperty("jetty.home", homeDir.toString()); + System.setProperty("jetty.base", baseDir.toString()); - System.setProperty("jetty.home",homeDir.toString()); - System.setProperty("jetty.base",baseDir.toString()); - + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PrintStream originalStream = StartLog.setStream(new PrintStream(out)); try { - if (prepare != null && prepare.length>0) + // If there is a "{caseName}.prepare.txt" then use those + // lines as if you are calling start.jar once to setup + // the base directory. + List prepareArgs = lines(caseName + ".prepare.txt"); + if (!prepareArgs.isEmpty()) { - for (String arg : prepare) - { - Main main = new Main(); - List cmdLine = new ArrayList<>(); - cmdLine.add("--testing-mode"); - cmdLine.addAll(Arrays.asList(arg.split(" "))); - main.start(main.processCommandLine(cmdLine)); - } + Main main = new Main(); + List cmdLine = new ArrayList<>(); + cmdLine.add("--testing-mode"); + cmdLine.addAll(prepareArgs); + + main.start(main.processCommandLine(cmdLine)); } - + Main main = new Main(); List cmdLine = new ArrayList<>(); - cmdLine.add("--debug"); - if (commandLineArgs != null) - { - for (String arg : commandLineArgs) - cmdLine.add(arg); - } - + // cmdLine.add("--debug"); + + // If there is a "{caseName}.cmdline.txt" then these + // entries are extra command line argument to use for + // the actual testcase + cmdLine.addAll(lines(caseName + ".cmdline.txt")); StartArgs args = main.processCommandLine(cmdLine); args.getAllModules().checkEnabledModules(); BaseHome baseHome = main.getBaseHome(); - ConfigurationAssert.assertConfiguration(baseHome,args,assertFile); + + StartLog.setStream(originalStream); + String output = out.toString(StandardCharsets.UTF_8.name()); + ConfigurationAssert.assertConfiguration(baseHome, args, output, assertFile); } catch (Exception e) { - List exceptions = Arrays.asList(lines(assertFile)).stream().filter(s->s.startsWith("EX|")).collect(toList()); + List exceptions = lines(assertFile).stream().filter(s -> s.startsWith("EX|")).collect(toList()); if (exceptions.isEmpty()) throw e; - for (String ex:exceptions) + for (String ex : exceptions) { - ex=ex.substring(3); - Assert.assertThat(e.toString(),Matchers.containsString(ex)); + ex = ex.substring(3); + Assert.assertThat(e.toString(), Matchers.containsString(ex)); } } + finally + { + StartLog.setStream(originalStream); + } + } + + private List lines(String filename) throws IOException + { + return lines(MavenTestingUtils.getTestResourcesPath().resolve("usecases" + File.separator + filename).toFile()); + } + + private List lines(File file) throws IOException + { + if (!file.exists() || !file.canRead()) + return Collections.emptyList(); + List ret = new ArrayList<>(); + try (FileReader reader = new FileReader(file); + BufferedReader buf = new BufferedReader(reader)) + { + String line; + while ((line = buf.readLine()) != null) + { + line = line.trim(); + if (line.length() > 0) + { + ret.add(line); + } + } + } + return ret; } } diff --git a/jetty-start/src/test/resources/usecases/dynamic-loop.0.prepare.txt b/jetty-start/src/test/resources/usecases/dynamic-loop.0.prepare.txt index 99dcff0e0ff..535d63e02ec 100644 --- a/jetty-start/src/test/resources/usecases/dynamic-loop.0.prepare.txt +++ b/jetty-start/src/test/resources/usecases/dynamic-loop.0.prepare.txt @@ -1 +1,2 @@ ---create-startd --add=tom +--create-startd +--add-to-start=tom diff --git a/jetty-start/src/test/resources/usecases/empty.addToStartCreateStartd.assert.txt b/jetty-start/src/test/resources/usecases/empty.addToStartCreateStartd.assert.txt index 0b80f1c57a9..6b2ed3d8921 100644 --- a/jetty-start/src/test/resources/usecases/empty.addToStartCreateStartd.assert.txt +++ b/jetty-start/src/test/resources/usecases/empty.addToStartCreateStartd.assert.txt @@ -19,5 +19,9 @@ PROP|optional.prop=value0 # Files / Directories to create EXISTS|maindir/ -EXISTS|start.d/start.ini +EXISTS|start.d/main.ini +EXISTS|start.d/extra.ini EXISTS|start.d/optional.ini + +# Output Assertions [regex!] (order is irrelevant) +OUTPUT|MKDIR: \$\{jetty.base\}/maindir \ No newline at end of file