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 e9fb4cdc50..41303e0358 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 @@ -8,6 +8,9 @@ import static java.util.stream.Collectors.toCollection; import static java.util.stream.Collectors.toList; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; @@ -93,6 +96,7 @@ public enum State { private final String title; private Dependencies dependencies; private Reporting reporting = Reporting.NEVER; + private String[] reportPath = {}; private Writer report; /** @@ -136,11 +140,11 @@ public enum State { * The path to the execution report that we're replaying data from, or * null if we're not replaying */ - private final String replaySource = Replay.source(); + private String replaySource = Replay.source(); /** * If we're replaying, we'll use this as the source of data for assertion */ - private final Replay replay = new Replay( replaySource ); + private Replay replay = new Replay( replaySource ); /** * Test action @@ -190,12 +194,17 @@ protected AbstractFlocessor( String title, Model model ) { /** * Controls report generation * - * @param r Whether or not to generate a report, and whether or not to display - * it at the conclusion of testing + * @param r Whether or not to generate a report, and whether or not to + * display it at the conclusion of testing + * @param path The directory path underneath the flow artifact directory in + * which to write the report * @return this */ - public T reporting( Reporting r ) { + public T reporting( Reporting r, String... path ) { reporting = r; + reportPath = path; + replaySource = Replay.source( path ); + replay = new Replay( replaySource ); return self(); } @@ -207,7 +216,7 @@ public T reporting( Reporting r ) { * @return this */ public T masking( Unpredictable... sources ) { - this.masks = sources.clone(); + masks = sources.clone(); return self(); } @@ -342,8 +351,8 @@ public T filtering( Consumer cfg ) { * @see AssertionOptions#SUPPRESS_FILTER */ public T exercising( Predicate filter, Consumer rejectionLog ) { - this.flowFilter = filter; - this.filterRejectionLog = rejectionLog; + flowFilter = filter; + filterRejectionLog = rejectionLog; return self(); } @@ -937,11 +946,14 @@ private T self() { private void report( Consumer data, boolean error ) { if( reporting.writing() ) { - boolean alreadyBrowsing = true; + Path testDir = null; + Path reportDir = null; if( report == null ) { String testTitle = title; + testDir = Paths.get( AssertionOptions.ARTIFACT_DIR.value(), reportPath ); + // work out what the report directory should be called String name = AssertionOptions.REPORT_NAME.value(); if( name == null ) { @@ -958,14 +970,51 @@ private void report( Consumer data, boolean error ) { // the REPORT_NAME property is the same as the REPLAY property } - report = new Writer( model.title(), testTitle, - Paths.get( AssertionOptions.ARTIFACT_DIR.value() ).resolve( name ) ); - alreadyBrowsing = false; + reportDir = testDir.resolve( name ); + + report = new Writer( model.title(), testTitle, reportDir ); } + data.accept( report ); - if( !alreadyBrowsing && reporting.shouldOpen( error ) ) { - report.browse(); + if( testDir != null && reportDir != null ) { + // We've just created a new report! We should: + + // if required, create a predictably-named link to it + if( !"latest".equals( reportDir.getFileName().toString() ) ) { + try { + Path linkPath = testDir.resolve( "latest" ); + boolean shouldLink; + // we want to delete an existing symlink, but avoid changing any other kind of + // file that might exist at that path + if( Files.exists( linkPath, LinkOption.NOFOLLOW_LINKS ) ) { + if( Files.isSymbolicLink( linkPath ) ) { + Files.delete( linkPath ); + shouldLink = true; + } + else { + shouldLink = false; + } + } + else { + shouldLink = true; + } + + if( shouldLink ) { + Files.createSymbolicLink( linkPath, linkPath.getParent().relativize( reportDir ) ); + } + } + catch( @SuppressWarnings("unused") IOException ioe ) { + // The symlink to the latest report is a nice-to-have. Some platforms (e.g.: + // windows) restrict the ability to create symlinks so we can't count on it + // working. + } + } + + // also, if appropriate, open a browser to it + if( reporting.shouldOpen( error ) ) { + report.browse(); + } } } } diff --git a/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Replay.java b/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Replay.java index aaf54dc26f..5d1f45ce60 100644 --- a/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Replay.java +++ b/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Replay.java @@ -1,10 +1,14 @@ package com.mastercard.test.flow.assrt; +import static java.util.stream.Collectors.joining; + import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; +import java.util.Objects; import java.util.Optional; +import java.util.stream.Stream; import com.mastercard.test.flow.Interaction; import com.mastercard.test.flow.report.Reader; @@ -52,13 +56,15 @@ public static boolean isActive() { /** * Works out the appropriate data source based on system properties * + * @param path The elements of the path from the Flow artifact directory to + * where the test writes reports * @return The path to the report that should be used as a source of data, or * null if there is no such report */ - public static String source() { + public static String source( String... path ) { String src = AssertionOptions.REPLAY.value(); if( LATEST.equals( src ) ) { - src = mostRecent(); + src = mostRecent( path ); } return src; } @@ -69,7 +75,7 @@ public static String source() { /** * @param source The path to the report to use as a source of data, or * null for an empty {@link Replay} - * @see #source() + * @see #source(String...) */ public Replay( String source ) { reader = source != null ? new Reader( Paths.get( source ) ) : null; @@ -131,8 +137,14 @@ public String populate( Assertion t ) { * * @return The most recent execution report */ - private static final String mostRecent() { - Path report = Reader.mostRecent( AssertionOptions.ARTIFACT_DIR.value(), + private static final String mostRecent( String... path ) { + Path report = Reader.mostRecent( + // searching in the test's report directory + Stream.concat( + Stream.of( AssertionOptions.ARTIFACT_DIR.value() ), + Stream.of( path ) ) + .filter( Objects::nonNull ) + .collect( joining( "/" ) ), // let's stick to primary sources of data p -> !p.getFileName().toString().endsWith( REPLAYED_SUFFIX ) ); if( report == null ) { diff --git a/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/ReplayTest.java b/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/ReplayTest.java index 1d370ff949..799ffefe72 100644 --- a/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/ReplayTest.java +++ b/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/ReplayTest.java @@ -34,7 +34,7 @@ class ReplayTest { private static TestFlocessor build( String title, List behaviourLog ) { return new TestFlocessor( title, TestModel.abc() ) .system( State.LESS, Actors.B ) - .reporting( QUIETLY ) + .reporting( QUIETLY, title ) .behaviour( assrt -> { behaviourLog.add( "exercising system with " + assrt.expected().request().assertable() ); @@ -45,7 +45,6 @@ private static TestFlocessor build( String title, List behaviourLog ) { } private static Path generateReport( String title ) { - System.setProperty( AssertionOptions.REPORT_NAME.property(), title ); List behaviourLog = new ArrayList<>(); TestFlocessor tf = build( title, behaviourLog ); tf.execute(); @@ -79,7 +78,6 @@ private static Path generateReport( String title ) { @AfterEach void clearReplayProperty() { System.clearProperty( AssertionOptions.REPLAY.property() ); - System.clearProperty( AssertionOptions.REPORT_NAME.property() ); } /** @@ -125,45 +123,60 @@ void noLatest() { */ @Test void latest() throws Exception { - generateReport( "older" ); - Path dataSource = generateReport( "latest" ); - - // this one was created later but is named as a replay report, so should be - // ignored as a source of data - generateReport( "later_replay" ); - - // this one was also created later but lacks detail files, so should also be - // ignored as a source of data - Path noDetails = generateReport( "no_detail" ); - QuietFiles.recursiveDelete( noDetails.resolve( "detail" ) ); - - // this one was also created later but lacks an index, so should also be - // ignored as a source of data - Path noIndex = generateReport( "no_index" ); - QuietFiles.recursiveDelete( noIndex.resolve( "index.html" ) ); - - // activate replay mode - System.setProperty( AssertionOptions.REPLAY.property(), Replay.LATEST ); - - // run again - TestFlocessor tf = build( "bar", new ArrayList<>() ); - tf.execute(); - - assertTrue( tf.report().toString().endsWith( "_replay" ), - "reports from replay runs (e.g.: " + tf.report() - + ") have an obvious filename suffix" ); - // read the report generated by the replay run - Reader r = new Reader( tf.report() ); - Index index = r.read(); - assertEquals( "bar (replay)", - index.meta.testTitle, - "reports from replay runs are titled as such" ); - FlowData fd = r.detail( index.entries.get( 0 ) ); - - String msg = fd.logs.get( 0 ).message; - assertEquals( "Replaying data from " + dataSource, msg, - "flow log first entry shows the data source" ); - + try { + QuietFiles.recursiveDelete( Paths.get( "target/mctf/latest" ) ); + + // this one will be ignored as it's not the latest valid report + AssertionOptions.REPORT_NAME.set( "older" ); + generateReport( "latest" ); + + // This is the one that will be replayed + AssertionOptions.REPORT_NAME.set( "replay_source" ); + Path dataSource = generateReport( "latest" ); + + // this one was created later but is named as a replay report, so should be + // ignored as a source of data + AssertionOptions.REPORT_NAME.set( "later_replay" ); + generateReport( "latest" ); + + // this one was also created later but lacks detail files, so should also be + // ignored as a source of data + AssertionOptions.REPORT_NAME.set( "no_detail" ); + Path noDetails = generateReport( "latest" ); + QuietFiles.recursiveDelete( noDetails.resolve( "detail" ) ); + + // this one was also created later but lacks an index, so should also be + // ignored as a source of data + AssertionOptions.REPORT_NAME.set( "no_index" ); + Path noIndex = generateReport( "latest" ); + QuietFiles.recursiveDelete( noIndex.resolve( "index.html" ) ); + + // activate replay mode + System.setProperty( AssertionOptions.REPLAY.property(), Replay.LATEST ); + + // run again + TestFlocessor tf = build( "latest", new ArrayList<>() ); + tf.execute(); + + assertTrue( tf.report().toString().endsWith( "_replay" ), + "reports from replay runs (e.g.: " + tf.report() + + ") have an obvious filename suffix" ); + // read the report generated by the replay run + Reader r = new Reader( tf.report() ); + Index index = r.read(); + assertEquals( "latest (replay)", + index.meta.testTitle, + "reports from replay runs are titled as such" ); + FlowData fd = r.detail( index.entries.get( 0 ) ); + + String msg = fd.logs.get( 0 ).message; + assertEquals( "Replaying data from " + dataSource, msg, + "flow log first entry shows the data source" ); + + } + finally { + AssertionOptions.REPORT_NAME.clear(); + } } /** @@ -381,8 +394,11 @@ void missingIndex() throws Exception { // try to run again IllegalStateException ise = assertThrows( IllegalStateException.class, () -> build( "blah", null ) ); - assertEquals( - "No index data found in " + AssertionOptions.ARTIFACT_DIR.value() + "/missingIndexEntry", - ise.getMessage().replace( '\\', '/' ) ); + assertEquals( String.format( + "No index data found in %s/missingIndexEntry/masked_timestamp", + AssertionOptions.ARTIFACT_DIR.value() ), + ise.getMessage() + .replace( '\\', '/' ) + .replaceAll( "/\\d{6}-\\d{6}$", "/masked_timestamp" ) ); } } diff --git a/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/ReportingTest.java b/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/ReportingTest.java index 37ee1304b6..073e7e4a48 100644 --- a/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/ReportingTest.java +++ b/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/ReportingTest.java @@ -11,12 +11,18 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collections; import java.util.EnumSet; import java.util.function.Function; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; import com.fasterxml.jackson.databind.ObjectMapper; import com.mastercard.test.flow.Actor; @@ -25,6 +31,7 @@ import com.mastercard.test.flow.report.data.Entry; import com.mastercard.test.flow.report.data.FlowData; import com.mastercard.test.flow.report.data.Index; +import com.mastercard.test.flow.util.Option.Temporary; /** * Exercises {@link Reporting} values @@ -306,4 +313,41 @@ void exercised() { FlowData fd = r.detail( ie ); assertEquals( "[B]", fd.exercised.toString() ); } + + /** + * Shows that a stably-named symlink is created that points to the latest report + */ + @Test + @DisabledOnOs(OS.WINDOWS) // requires special permissions to create symlinks + void symlink() { + TestFlocessor tf = new TestFlocessor( "symlink", TestModel.abc() ) + .system( State.FUL, B ) + .reporting( QUIETLY, "symlink" ) + .behaviour( assrt -> { + // no assertions made + } ); + try( Temporary t = AssertionOptions.REPORT_NAME.temporarily( "sub/path/report" ) ) { + tf.execute(); + } + + Path writtenPath = tf.report(); + Path linkedPath = Paths.get( "target/mctf/symlink/latest" ); + + assertEquals( "target/mctf/symlink/sub/path/report", writtenPath.toString() ); + assertTrue( Files.exists( linkedPath, LinkOption.NOFOLLOW_LINKS ), + "The expected symlink has been created" ); + assertTrue( Files.isSymbolicLink( linkedPath ), + "It really is a symlink" ); + + Reader dr = new Reader( writtenPath ); + Index direct = dr.read(); + + Reader lr = new Reader( linkedPath ); + Index linked = lr.read(); + + // reading either provides the same data + assertEquals( direct.meta.modelTitle, linked.meta.modelTitle ); + assertEquals( direct.meta.testTitle, linked.meta.testTitle ); + assertEquals( direct.meta.timestamp, linked.meta.timestamp ); + } } diff --git a/doc/src/main/markdown/further.md b/doc/src/main/markdown/further.md index 79fb47ca21..924373376a 100644 --- a/doc/src/main/markdown/further.md +++ b/doc/src/main/markdown/further.md @@ -6,14 +6,14 @@ This guide explores some features that will be useful as the system model grows ## Execution report -Adding a call to [`.reporting()`][AbstractFlocessor.reporting(Reporting)] to the construction chain of the `Flocessor` instance controls whether a HTML report of the test run is produced. The report will detail both the expected and observed system behaviour and the results of comparing the two. +Adding a call to [`.reporting()`][AbstractFlocessor.reporting(Reporting,String...)] to the construction chain of the `Flocessor` instance controls whether a HTML report of the test run is produced. The report will detail both the expected and observed system behaviour and the results of comparing the two. The [`Reporting` enum value][Reporting] that you supply controls whether the report is generated and under what circumstances it is automatically opened in a browser. By default the report will be saved to a timestamped directory under `target/mctf`, but the `mctf.report.dir` system property offers control over the destination directory. -[AbstractFlocessor.reporting(Reporting)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L190-L197,190-197 +[AbstractFlocessor.reporting(Reporting,String...)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L194-L203,194-203 [Reporting]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Reporting.java @@ -35,7 +35,7 @@ You can use [`Replay.isActive()`][Replay.isActive()] in your assertion component -[Replay.isActive()]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Replay.java#L41-L48,41-48 +[Replay.isActive()]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Replay.java#L45-L52,45-52 @@ -78,8 +78,8 @@ Note that only the tag/index-based filtering can be used to avoid flow construct -[AbstractFlocessor.filtering(Consumer)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L306-L314,306-314 -[AbstractFlocessor.exercising(Predicate,Consumer)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L319-L344,319-344 +[AbstractFlocessor.filtering(Consumer)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L315-L323,315-323 +[AbstractFlocessor.exercising(Predicate,Consumer)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L328-L353,328-353 @@ -123,7 +123,7 @@ Note that the assertion components will not make any assumptions about the forma [LogCapture]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/LogCapture.java [Tail]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/log/Tail.java [Merge]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/log/Merge.java -[AbstractFlocessor.logs(LogCapture)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L274-L281,274-281 +[AbstractFlocessor.logs(LogCapture)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L283-L290,283-290 @@ -246,7 +246,7 @@ Consider the following worked example: [flow.Unpredictable]: ../../../../api/src/main/java/com/mastercard/test/flow/Unpredictable.java [AbstractMessage.masking(Unpredictable,UnaryOperator)]: ../../../../message/message-core/src/main/java/com/mastercard/test/flow/msg/AbstractMessage.java#L50-L57,50-57 -[AbstractFlocessor.masking(Unpredictable...)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L202-L209,202-209 +[AbstractFlocessor.masking(Unpredictable...)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L211-L218,211-218 [mask.BenSys]: ../../test/java/com/mastercard/test/flow/doc/mask/BenSys.java [mask.DieSys]: ../../test/java/com/mastercard/test/flow/doc/mask/DieSys.java [mask.Unpredictables]: ../../test/java/com/mastercard/test/flow/doc/mask/Unpredictables.java @@ -256,7 +256,7 @@ Consider the following worked example: [msg.Mask.andThen(Consumer)]: ../../../../message/message-core/src/main/java/com/mastercard/test/flow/msg/Mask.java#L290-L292,290-292 [BenDiceTest?masking]: ../../test/java/com/mastercard/test/flow/doc/mask/BenDiceTest.java#L31,31 [BenTest]: ../../test/java/com/mastercard/test/flow/doc/mask/BenTest.java -[AbstractFlocessor.masking(Unpredictable...)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L202-L209,202-209 +[AbstractFlocessor.masking(Unpredictable...)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L211-L218,211-218 @@ -276,7 +276,7 @@ You can see usage of these types in the example system: [flow.Context]: ../../../../api/src/main/java/com/mastercard/test/flow/Context.java [Builder.context(Context)]: ../../../../builder/src/main/java/com/mastercard/test/flow/builder/Builder.java#L225-L232,225-232 [assrt.Applicator]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Applicator.java -[AbstractFlocessor.applicators(Applicator...)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L248-L254,248-254 +[AbstractFlocessor.applicators(Applicator...)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L257-L263,257-263 [model.ctx.QueueProcessing]: ../../../../example/app-model/src/main/java/com/mastercard/test/flow/example/app/model/ctx/QueueProcessing.java [QueueProcessingApplicator]: ../../../../example/app-assert/src/main/java/com/mastercard/test/flow/example/app/assrt/ctx/QueueProcessingApplicator.java @@ -299,7 +299,7 @@ You can see usage of these types in the example system: [flow.Residue]: ../../../../api/src/main/java/com/mastercard/test/flow/Residue.java [assrt.Checker]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Checker.java -[AbstractFlocessor.checkers(Checker...)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L261-L267,261-267 +[AbstractFlocessor.checkers(Checker...)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L270-L276,270-276 [model.rsd.DBItems]: ../../../../example/app-model/src/main/java/com/mastercard/test/flow/example/app/model/rsd/DBItems.java [DBItemsChecker]: ../../../../example/app-assert/src/main/java/com/mastercard/test/flow/example/app/assrt/rsd/DBItemsChecker.java diff --git a/example/app-store/src/test/java/com/mastercard/test/flow/example/app/store/QueryTest.java b/example/app-store/src/test/java/com/mastercard/test/flow/example/app/store/QueryTest.java index 0e541fab69..ac07d80e67 100644 --- a/example/app-store/src/test/java/com/mastercard/test/flow/example/app/store/QueryTest.java +++ b/example/app-store/src/test/java/com/mastercard/test/flow/example/app/store/QueryTest.java @@ -79,7 +79,7 @@ public static void startService() { if( !Replay.isActive() ) { service.start(); } - reportName = AssertionOptions.REPORT_NAME.temporarily( "query_latest" ); + reportName = AssertionOptions.REPORT_NAME.temporarily( "latest" ); } /** @@ -99,7 +99,7 @@ public static void stopService() { @TestFactory Stream flows() { Flocessor flocessor = new Flocessor( "Query test", ExampleSystem.MODEL ) - .reporting( FAILURES ) + .reporting( FAILURES, "query" ) .exercising( flow -> Flows.intersects( flow, Actors.STORE ), LOG::info ) .system( State.LESS, Actors.STORE ) .masking( BORING, HOST, CLOCK, RNG ) diff --git a/report/report-core/src/main/java/com/mastercard/test/flow/report/QuietFiles.java b/report/report-core/src/main/java/com/mastercard/test/flow/report/QuietFiles.java index d59b0c31c6..212aff0008 100644 --- a/report/report-core/src/main/java/com/mastercard/test/flow/report/QuietFiles.java +++ b/report/report-core/src/main/java/com/mastercard/test/flow/report/QuietFiles.java @@ -2,7 +2,9 @@ import java.io.IOException; import java.io.UncheckedIOException; +import java.nio.file.CopyOption; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.attribute.FileAttribute; @@ -57,10 +59,10 @@ private QuietFiles() { */ public static void recursiveDelete( Path path ) { try { - if( !Files.exists( path ) ) { + if( !Files.exists( path, LinkOption.NOFOLLOW_LINKS ) ) { // we're done here } - else if( Files.isDirectory( path ) ) { + else if( Files.isDirectory( path, LinkOption.NOFOLLOW_LINKS ) ) { try( Stream children = Files.list( path ) ) { children.forEach( QuietFiles::recursiveDelete ); } @@ -130,6 +132,18 @@ public static Stream list( Path dir ) { return wrap( () -> Files.list( dir ) ); } + /** + * @see Files#move(Path, Path, CopyOption...) + * @param source the path to the file to move + * @param target the path to the target file (may be associated with a + * different provider to the source path) + * @param options options specifying how the move should be done + * @return the path to the target file + */ + public static Path move( Path source, Path target, CopyOption... options ) { + return wrap( () -> Files.move( source, target, options ) ); + } + /** * @see Files#readAllBytes(Path) * @param path The path to read diff --git a/report/report-core/src/main/java/com/mastercard/test/flow/report/Reader.java b/report/report-core/src/main/java/com/mastercard/test/flow/report/Reader.java index dc5a5bf94f..9bf16890b7 100644 --- a/report/report-core/src/main/java/com/mastercard/test/flow/report/Reader.java +++ b/report/report-core/src/main/java/com/mastercard/test/flow/report/Reader.java @@ -94,7 +94,7 @@ private static T extract( URI uri, Class type ) { String file = br.lines().collect( joining( "\n" ) ); return Template.extract( file, type ); } - catch( @SuppressWarnings("unused") FileNotFoundException fnfe ) { + catch( FileNotFoundException fnfe ) { if( fnfe.getMessage().contains( "Permission denied" ) ) { // on linux platforms missing read permission results in a // FileNotFoundException. @@ -113,8 +113,8 @@ private static T extract( URI uri, Class type ) { } /** - * Searches the a directory for the most recent execution report that satisfies - * a constraint + * Searches a directory for the most recent execution report that satisfies a + * constraint * * @param dir The directory to search in * @param filter Additional search constraint