From 2d23a95a095b3598cb520670babcdd51efd98f71 Mon Sep 17 00:00:00 2001 From: Ryan McNally Date: Fri, 27 Oct 2023 15:22:44 +0100 Subject: [PATCH] classpath banter --- .../test/flow/assrt/AbstractFlocessor.java | 8 +- .../test/flow/assrt/AssertionOptions.java | 8 ++ example/app-itest/pom.xml | 13 --- .../example/app/itest/IntegrationTest.java | 41 +------- pom.xml | 2 +- .../test/flow/report/duct/Duct.java | 99 +++++++++++++++---- .../test/flow/report/duct/Server.java | 19 +++- .../test/flow/report/duct/SystrayGui.java | 8 +- .../main/resources/simplelogger.properties | 2 +- .../test/resources/simplelogger.properties | 5 + 10 files changed, 126 insertions(+), 79 deletions(-) create mode 100644 report/duct/src/test/resources/simplelogger.properties diff --git a/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java b/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java index 41303e0358..b75bb59f4c 100644 --- a/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java +++ b/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java @@ -52,6 +52,7 @@ import com.mastercard.test.flow.report.data.LogEvent; import com.mastercard.test.flow.report.data.ResidueData; import com.mastercard.test.flow.report.data.TransmissionData; +import com.mastercard.test.flow.report.duct.Duct; import com.mastercard.test.flow.util.Dependencies; import com.mastercard.test.flow.util.Flows; @@ -1013,7 +1014,12 @@ private void report( Consumer data, boolean error ) { // also, if appropriate, open a browser to it if( reporting.shouldOpen( error ) ) { - report.browse(); + if( AssertionOptions.DUCT.isTrue() ) { + Duct.serve( report.path() ); + } + else { + report.browse(); + } } } } diff --git a/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AssertionOptions.java b/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AssertionOptions.java index 6f43360807..ee3197cc93 100644 --- a/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AssertionOptions.java +++ b/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AssertionOptions.java @@ -4,6 +4,7 @@ import com.mastercard.test.flow.Flow; import com.mastercard.test.flow.assrt.filter.FilterOptions; +import com.mastercard.test.flow.report.duct.Duct; import com.mastercard.test.flow.util.Option; /** @@ -24,6 +25,13 @@ public enum AssertionOptions implements Option { */ ARTIFACT_DIR(FilterOptions.ARTIFACT_DIR), + /** + * Controls whether we use {@link Duct} or not + */ + DUCT(b -> b.property( "mctf.report.serve" ) + .description( "" + + "Set to `true` to browse reports on a local web server rather than the filesystem" )), + /** * Controls {@link Replay} parameters */ diff --git a/example/app-itest/pom.xml b/example/app-itest/pom.xml index bfc3996bbe..b24cd9ecab 100644 --- a/example/app-itest/pom.xml +++ b/example/app-itest/pom.xml @@ -16,19 +16,6 @@ - - - com.sparkjava - spark-core - - - - org.slf4j - slf4j-api - - - - diff --git a/example/app-itest/src/test/java/com/mastercard/test/flow/example/app/itest/IntegrationTest.java b/example/app-itest/src/test/java/com/mastercard/test/flow/example/app/itest/IntegrationTest.java index 1b39afa890..341e21f538 100644 --- a/example/app-itest/src/test/java/com/mastercard/test/flow/example/app/itest/IntegrationTest.java +++ b/example/app-itest/src/test/java/com/mastercard/test/flow/example/app/itest/IntegrationTest.java @@ -1,6 +1,7 @@ package com.mastercard.test.flow.example.app.itest; +import static com.mastercard.test.flow.assrt.Reporting.ALWAYS; import static com.mastercard.test.flow.assrt.Reporting.FAILURES; import static com.mastercard.test.flow.example.app.model.ExampleSystem.Actors.CORE; import static com.mastercard.test.flow.example.app.model.ExampleSystem.Actors.DB; @@ -14,9 +15,6 @@ import static com.mastercard.test.flow.example.app.model.ExampleSystem.Unpredictables.HOST; import static com.mastercard.test.flow.example.app.model.ExampleSystem.Unpredictables.RNG; -import java.awt.Desktop; -import java.awt.Desktop.Action; -import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.function.Supplier; @@ -47,8 +45,6 @@ import com.mastercard.test.flow.msg.http.HttpReq; import com.mastercard.test.flow.msg.web.WebSequence; -import spark.Service; - /** * Spins up a complete instance of the application and compares its behaviour * against our model @@ -91,40 +87,7 @@ public static void startApp() { */ @AfterAll public static void stopApp() throws Exception { - clusterManager.stopCluster(); - - if( "true".equals( System.getProperty( "mctf.itest.report.serve" ) ) ) { - // serve the report - String reportDir = reportLocation.get().toString(); - System.out.println( "Serving " + reportDir ); - Service service = Service.ignite() - .port( 0 ) - .externalStaticFileLocation( reportDir ); - service.staticFiles.header( "Access-Control-Allow-Origin", "*" ); - service.init(); - service.awaitInitialization(); - - // open the browser - URI uri = new URI( "http://localhost:" + service.port() ); - if( Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported( Action.BROWSE ) ) { - System.out.println( "Opening browser to " + uri ); - Desktop.getDesktop().browse( uri ); - } - else { - System.out.println( "Open your browser to " + uri ); - } - - // wait it till the human is done with it - System.out.println( "Hit enter to shutdown the server" ); - // noinspection ResultOfMethodCallIgnored - System.in.read(); - - // shut it down - service.stop(); - service.awaitStop(); - System.out.println( "Shutdown complete" ); - } } /** @@ -133,7 +96,7 @@ public static void stopApp() throws Exception { @TestFactory Stream flows() { Flocessor f = new Flocessor( "Integration test", ExampleSystem.MODEL ) - .reporting( FAILURES ) + .reporting( AssertionOptions.DUCT.isTrue() ? ALWAYS : FAILURES ) .system( State.FUL, WEB_UI, UI, CORE, QUEUE, HISTOGRAM, STORE, DB ) .masking( BORING, CLOCK, HOST, RNG ) .logs( Util.LOG_CAPTURE ) diff --git a/pom.xml b/pom.xml index 689bd2b6d8..8473840233 100644 --- a/pom.xml +++ b/pom.xml @@ -469,7 +469,7 @@ true - true + true diff --git a/report/duct/src/main/java/com/mastercard/test/flow/report/duct/Duct.java b/report/duct/src/main/java/com/mastercard/test/flow/report/duct/Duct.java index 7d0881054a..5cf2fbe7dc 100644 --- a/report/duct/src/main/java/com/mastercard/test/flow/report/duct/Duct.java +++ b/report/duct/src/main/java/com/mastercard/test/flow/report/duct/Duct.java @@ -9,6 +9,7 @@ import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; +import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -17,6 +18,7 @@ import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -36,6 +38,18 @@ */ public class Duct { + /** + * When true, failures will be printed to stderr. This framework + * does not assume that clients use a logging framework, and it tries to keep + * silent on stdout. This class uses slf4j for the bulk of operations, but those + * will be running in a different process to the test. The interaction between + * the test and the duct process (where we can't use slf4j) does a bunch of + * failure-prone things though, so it's nice to have the option of seeing the + * issues when you're wondering why your report is not being served. + */ + static final boolean NOISY_FAILS = "true" + .equals( System.getProperty( "mctf.duct.noisy" ) ); + /** * The preference name where we save our index directories */ @@ -58,10 +72,15 @@ public static void main( String[] args ) { .map( duct::add ) .forEach( served -> { try { - Desktop.getDesktop().browse( served.toURI() ); + if( Desktop.isDesktopSupported() ) { + Desktop.getDesktop().browse( served.toURI() ); + } } catch( Exception e ) { - System.err.println( "Failed to browse " + served + " due to " + e.getMessage() ); + if( NOISY_FAILS ) { + System.err.println( "Failed to browse " + served ); + e.printStackTrace(); + } } } ); } @@ -79,33 +98,67 @@ public static void serve( Path report ) { if( added != null ) { // there's an existing instance! try { - Desktop.getDesktop().browse( added.toURI() ); + if( Desktop.isDesktopSupported() ) { + Desktop.getDesktop().browse( added.toURI() ); + } } - catch( @SuppressWarnings("unused") IOException | URISyntaxException e ) { - // oh well, we tried + catch( IOException | URISyntaxException e ) { + if( NOISY_FAILS ) { + System.err.println( "Failed to browse " + added ); + e.printStackTrace(); + } } } else { // we'll have to spawn our own instance + ProcessBuilder pb = new ProcessBuilder( + "java", + // re-use the current JVM's classpath. It's running this class, so it should + // also have the dependencies we need. The classpath will be bigger than duct + // strictly needs, but the cost of that is negligible + "-cp", getClassPath(), + // invoke this class's main method + Duct.class.getName(), + // pass the report path on the commandline - the above main method will take + // care of adding and browsing it + report.toAbsolutePath().toString() ); try { // this process will persist after the demise of the current JVM - ProcessBuilder pb = new ProcessBuilder( - "java", - // re-use the current JVM's classpath. It's running this class, so it should - // also have the dependencies we need. The classpath will be bigger than duct - // strictly needs, but the cost of that is negligible - "-cp", System.getProperty( "java.class.path" ), - // invoke this class's main method - Duct.class.getName(), - // pass the report path on the commandline - the above main method will take - // care of adding and browsing it - report.toAbsolutePath().toString() ); pb.start(); } - catch( @SuppressWarnings("unused") IOException e ) { - // oh well, we tried + catch( IOException e ) { + if( NOISY_FAILS ) { + System.err.println( "Failed to launch:\n" + + pb.command().stream().collect( joining( " " ) ) ); + e.printStackTrace(); + } + } + } + } + + /** + * Gets the classpath of the current JVM. Ordinarily you can find the classpath + * just with the java.class.path system property. When running + * tests via maven (a really common use-case for us) that + * just contains plexus-classworlds.jar, which is no use to us. + * Hence we're reduced to this: ascending the classloader chain and pulling out + * any URL that we find. + * + * @return The classpath of the current JVM + */ + static String getClassPath() { + List urls = new ArrayList<>(); + ClassLoader cl = Duct.class.getClassLoader(); + while( cl != null ) { + if( cl instanceof URLClassLoader ) { + Collections.addAll( urls, ((URLClassLoader) cl).getURLs() ); } + cl = cl.getParent(); } + return urls.stream() + .map( URL::getPath ) + .collect( joining( File.pathSeparator ) ); } /** @@ -123,9 +176,17 @@ private static URL tryAdd( Path report ) { b -> new String( b, UTF_8 ) ); return new URL( res.body ); } - catch( @SuppressWarnings("unused") Exception e ) { + catch( Exception e ) { // A failure on this request is not unexpected - it could just be a signal that // we need to start a new instance of duct + if( NOISY_FAILS ) { + System.err.println( String.format( + "Failed to add via http:\n%s %s\n%s", + "http://localhost:" + PORT + "/add", + "POST", + report.toAbsolutePath().toString() ) ); + e.printStackTrace(); + } return null; } } diff --git a/report/duct/src/main/java/com/mastercard/test/flow/report/duct/Server.java b/report/duct/src/main/java/com/mastercard/test/flow/report/duct/Server.java index 6b7a6c05ba..84ba0caf43 100644 --- a/report/duct/src/main/java/com/mastercard/test/flow/report/duct/Server.java +++ b/report/duct/src/main/java/com/mastercard/test/flow/report/duct/Server.java @@ -11,6 +11,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -36,6 +37,17 @@ class Server { private static final ObjectMapper JSON = new ObjectMapper() .enable( SerializationFeature.INDENT_OUTPUT ); + private static final Map MIME_TYPES; + static { + Map mtm = new HashMap<>(); + mtm.put( ".css", "text/css" ); + mtm.put( ".html", "text/html" ); + mtm.put( ".ico", "image/vnd.microsoft.icon" ); + mtm.put( ".js", "text/javascript" ); + mtm.put( ".txt", "text/plain" ); + MIME_TYPES = Collections.unmodifiableMap( mtm ); + } + /** * Restricts our server to only working with local clients. This application * will merrily serve up the contents of directories, so we have to be mindful @@ -44,7 +56,6 @@ class Server { * localhost */ private static final Filter LOCAL_ORIGIN_ONLY = ( request, response ) -> { - LOG.info( "REQUEST TO " + request.pathInfo() ); // SECURITY-CRITICAL BEHAVIOUR try { InetAddress addr = InetAddress.getByName( request.ip() ); @@ -155,6 +166,12 @@ void map( String path, Path dir ) { private static Route respondWithFileBytes( Path f ) { return ( req, res ) -> { + + MIME_TYPES.entrySet().stream() + .filter( e -> f.toString().endsWith( e.getKey() ) ) + .findFirst() + .ifPresent( e -> res.header( "Content-Type", e.getValue() ) ); + byte[] buff = new byte[8192]; try( InputStream is = Files.newInputStream( f ); OutputStream os = res.raw().getOutputStream(); ) { diff --git a/report/duct/src/main/java/com/mastercard/test/flow/report/duct/SystrayGui.java b/report/duct/src/main/java/com/mastercard/test/flow/report/duct/SystrayGui.java index 06477a492d..d947cd95c0 100644 --- a/report/duct/src/main/java/com/mastercard/test/flow/report/duct/SystrayGui.java +++ b/report/duct/src/main/java/com/mastercard/test/flow/report/duct/SystrayGui.java @@ -91,7 +91,7 @@ private static PopupMenu menu( Duct duct ) { } private static MenuItem index( Duct duct ) { - MenuItem index = new MenuItem( "Report index..." ); + MenuItem index = new MenuItem( "Index" ); index.addActionListener( ev -> { try { Desktop.getDesktop().browse( new URI( "http://localhost:" + duct.port() ) ); @@ -104,7 +104,7 @@ private static MenuItem index( Duct duct ) { } private static MenuItem add( Duct duct ) { - MenuItem add = new MenuItem( "Add reports..." ); + MenuItem add = new MenuItem( "Add..." ); add.addActionListener( ev -> { File start = null; try { @@ -138,7 +138,7 @@ private static MenuItem add( Duct duct ) { } private static MenuItem clearIndex( Duct duct ) { - MenuItem item = new MenuItem( "Clear index" ); + MenuItem item = new MenuItem( "Clear" ); item.addActionListener( ev -> { duct.clearIndex(); } ); @@ -146,7 +146,7 @@ private static MenuItem clearIndex( Duct duct ) { } private static MenuItem reindex( Duct duct ) { - MenuItem item = new MenuItem( "Refresh index" ); + MenuItem item = new MenuItem( "Refresh" ); item.addActionListener( ev -> duct.reindex() ); return item; } diff --git a/report/duct/src/main/resources/simplelogger.properties b/report/duct/src/main/resources/simplelogger.properties index 749f6427e4..f3fbe7dc8a 100644 --- a/report/duct/src/main/resources/simplelogger.properties +++ b/report/duct/src/main/resources/simplelogger.properties @@ -1,5 +1,5 @@ # http://www.slf4j.org/api/org/slf4j/impl/SimpleLogger.html -# org.slf4j.simpleLogger.logFile=duct_log.txt set at runtime +# org.slf4j.simpleLogger.logFile=location of log.txt set at runtime org.slf4j.simpleLogger.showDateTime=true org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd'T'HH:mm:ss.SSSZ org.slf4j.simpleLogger.defaultLogLevel=info \ No newline at end of file diff --git a/report/duct/src/test/resources/simplelogger.properties b/report/duct/src/test/resources/simplelogger.properties new file mode 100644 index 0000000000..817d19b6a5 --- /dev/null +++ b/report/duct/src/test/resources/simplelogger.properties @@ -0,0 +1,5 @@ +# http://www.slf4j.org/api/org/slf4j/impl/SimpleLogger.html +org.slf4j.simpleLogger.logFile=target/log.txt +org.slf4j.simpleLogger.showDateTime=true +org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd'T'HH:mm:ss.SSSZ +org.slf4j.simpleLogger.defaultLogLevel=info \ No newline at end of file