diff --git a/assert/assert-filter/README.md b/assert/assert-filter/README.md index 054fee123c..c70fd47903 100644 --- a/assert/assert-filter/README.md +++ b/assert/assert-filter/README.md @@ -44,7 +44,7 @@ Calling [`blockForUpdates()`][Filter!.blockForUpdates()] on a `Filter` instance -[Filter!.blockForUpdates()]: src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java#L138-L151,138-151 +[Filter!.blockForUpdates()]: src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java#L158-L171,158-171 @@ -59,7 +59,7 @@ Calling [`load()`][Filter!.load()] on a filter (when system property `mctf.filte -[Filter!.save()]: src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java#L162-L168,162-168 -[Filter!.load()]: src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java#L108-L122,108-122 +[Filter!.save()]: src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java#L182-L188,182-188 +[Filter!.load()]: src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java#L128-L142,128-142 diff --git a/assert/assert-filter/src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java b/assert/assert-filter/src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java index bf6841bc37..daf00e4a4c 100644 --- a/assert/assert-filter/src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java +++ b/assert/assert-filter/src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java @@ -1,6 +1,7 @@ package com.mastercard.test.flow.assrt.filter; +import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toCollection; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; @@ -8,13 +9,17 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Comparator; +import java.util.Deque; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.TreeSet; +import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -55,6 +60,10 @@ public class Filter { private static final Pattern RANGE_PTRN = Pattern.compile( "(\\d+)-(\\d+)" ); private final Set indices = parseIndices( FilterOptions.INDICES.value() ); + private Consumer listener = f -> { + // default to nothing + }; + /** * Extracts numeric indices from a string * @@ -105,6 +114,17 @@ public Filter( Model model ) { this.model = model; } + /** + * Sets the listener on this {@link Filter} + * + * @param l The object that will be notified when this filter is updated + * @return this + */ + public Filter listener( Consumer l ) { + listener = l; + return this; + } + /** * Attempts to configure the filter from historic executions: *
    @@ -196,6 +216,7 @@ public Filter includedTags( Set tags ) { includeTags.clear(); includeTags.addAll( tags ); matchIndexSelection( current ); + listener.accept( this ); } return this; } @@ -252,6 +273,7 @@ public Filter excludedTags( Set tags ) { excludeTags.clear(); excludeTags.addAll( tags ); matchIndexSelection( current ); + listener.accept( this ); } return this; } @@ -284,6 +306,7 @@ public Filter indices( Set idx ) { indices.clear(); indices.addAll( valid ); + listener.accept( this ); return this; } @@ -387,12 +410,14 @@ private boolean matches( Flow flow ) { if( !includeTags.isEmpty() ) { Set includeIntersection = new HashSet<>( flow.meta().tags() ); includeIntersection.retainAll( includeTags ); - included &= !includeIntersection.isEmpty(); + // the flow bears *all* of the included tags + included &= includeIntersection.size() == includeTags.size(); } if( !excludeTags.isEmpty() ) { Set excludeIntersection = new HashSet<>( flow.meta().tags() ); excludeIntersection.retainAll( excludeTags ); + // the flow bears *none* of the excluded tags included &= excludeIntersection.isEmpty(); } @@ -421,6 +446,63 @@ public Stream flows() { return filtered.stream(); } + /** + * Computes the property values that recreate the settings of this + * {@link Filter} + * + * @param option The option + * @return The value for that option that would recreate the current filter + * configuration, or null if the supplied option is not + * relevant for filter configuration + */ + public String property( FilterOptions option ) { + if( option == FilterOptions.INCLUDE_TAGS && !includedTags().isEmpty() ) { + return includedTags().stream().collect( Collectors.joining( "," ) ); + } + if( option == FilterOptions.EXCLUDE_TAGS && !excludedTags().isEmpty() ) { + return excludedTags().stream().collect( Collectors.joining( "," ) ); + } + if( option == FilterOptions.INDICES ) { + return ranges( indices() ); + } + return null; + } + + /** + * Compresses integer values into ranges + * + * @param values The values, e.g.: [1,2,3,5] + * @return A string representing those values, e.g.: 1-3,5 + */ + private static String ranges( Set values ) { + Deque dq = values.stream() + .filter( Objects::nonNull ) + .sorted() + .collect( toCollection( ArrayDeque::new ) ); + + List ranges = new ArrayList<>(); + + while( !dq.isEmpty() ) { + int low = dq.removeFirst().intValue(); + int high = low; + while( !dq.isEmpty() && dq.peek().intValue() == high + 1 ) { + high = dq.removeFirst().intValue(); + } + if( low == high ) { + ranges.add( String.valueOf( low ) ); + } + else if( high - low == 1 ) { + ranges.add( String.valueOf( low ) ); + ranges.add( String.valueOf( high ) ); + } + else { + ranges.add( low + "-" + high ); + } + } + + return ranges.stream().collect( joining( "," ) ); + } + private static class Persistence { private static final Path persistencePath = Paths.get( diff --git a/assert/assert-filter/src/main/java/com/mastercard/test/flow/assrt/filter/gui/FilterGui.java b/assert/assert-filter/src/main/java/com/mastercard/test/flow/assrt/filter/gui/FilterGui.java index 28cfe1832a..6a9a7792df 100644 --- a/assert/assert-filter/src/main/java/com/mastercard/test/flow/assrt/filter/gui/FilterGui.java +++ b/assert/assert-filter/src/main/java/com/mastercard/test/flow/assrt/filter/gui/FilterGui.java @@ -1,7 +1,7 @@ + package com.mastercard.test.flow.assrt.filter.gui; import static java.awt.GridBagConstraints.BOTH; -import static java.awt.event.WindowEvent.WINDOW_CLOSING; import java.awt.BorderLayout; import java.awt.GraphicsEnvironment; @@ -33,7 +33,7 @@ * - this can improve performance by avoiding building flows that will not be * exercised. */ -public class FilterGui extends JFrame { +public class FilterGui { private static final long serialVersionUID = 1L; @@ -54,7 +54,6 @@ public static boolean requested() { * @param filter The filter to control */ public FilterGui( Filter filter ) { - super( "Flow filters" ); this.filter = filter; } @@ -67,8 +66,11 @@ public void blockForInput() { final Object monitor = new Object(); SwingUtilities.invokeLater( () -> { - setDefaultCloseOperation( WindowConstants.DISPOSE_ON_CLOSE ); - addWindowListener( new WindowAdapter() { + JFrame frame = new JFrame( "Flow filters" ); + frame.setName( "flow_filter_frame" ); + frame.setDefaultCloseOperation( WindowConstants.DISPOSE_ON_CLOSE ); + frame.addWindowListener( new WindowAdapter() { + @Override public void windowClosed( WindowEvent e ) { updatesCompleted.set( true ); @@ -77,16 +79,11 @@ public void windowClosed( WindowEvent e ) { } } } ); - JButton build = new JButton( "Build" ); - build.setName( "build_button" ); - JButton run = new JButton( "Run" ); - run.setName( "run_button" ); - run.addActionListener( ac -> dispatchEvent( new WindowEvent( this, WINDOW_CLOSING ) ) ); - getContentPane().add( buildGUI( build, run ) ); - pack(); - setLocationRelativeTo( null ); - getRootPane().setDefaultButton( build ); - setVisible( true ); + frame.getContentPane().add( buildGUI( frame ) ); + frame.setAlwaysOnTop( true ); + frame.pack(); + frame.setLocationRelativeTo( null ); + frame.setVisible( true ); } ); synchronized( monitor ) { @@ -102,8 +99,29 @@ public void windowClosed( WindowEvent e ) { } } - private JPanel buildGUI( JButton build, JButton run ) { + /** + * Builds the filter controls. The supplied frame will be updated to: + *
      + *
    • Control the default button - starting out as "build", but moving to "run" + * after that
    • + *
    • Provoke frame closure when "run" is clicked
    • + *
    + * + * @param frame The frame that will hold the controls + * @return The controls component + */ + public JPanel buildGUI( JFrame frame ) { + + JButton build = new JButton( "Build" ); + build.setName( "build_button" ); + frame.getRootPane().setDefaultButton( build ); + + JButton run = new JButton( "Run" ); + run.setName( "run_button" ); run.setEnabled( false ); + run.addActionListener( ac -> frame.dispatchEvent( + new WindowEvent( frame, WindowEvent.WINDOW_CLOSING ) ) ); + JTextField filterField = new JTextField(); filterField.setName( "filter_textfield" ); filterField.setHorizontalAlignment( SwingConstants.CENTER ); @@ -151,7 +169,7 @@ private JPanel buildGUI( JButton build, JButton run ) { flowBuilding.set( true ); flowPanel.withListener( tagPanel::refreshAndLimitTags ); run.setEnabled( true ); - SwingUtilities.getRootPane( run ).setDefaultButton( run ); + frame.getRootPane().setDefaultButton( run ); panel.remove( buildPanel ); panel.add( flowPanel, diff --git a/assert/assert-filter/src/test/java/com/mastercard/test/flow/assrt/filter/FilterTest.java b/assert/assert-filter/src/test/java/com/mastercard/test/flow/assrt/filter/FilterTest.java index ca006f5872..883cb9618e 100644 --- a/assert/assert-filter/src/test/java/com/mastercard/test/flow/assrt/filter/FilterTest.java +++ b/assert/assert-filter/src/test/java/com/mastercard/test/flow/assrt/filter/FilterTest.java @@ -18,6 +18,7 @@ import java.util.Deque; import java.util.Set; import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; import java.util.stream.Stream; @@ -42,13 +43,175 @@ class FilterTest { */ @BeforeEach @AfterEach - public void clearProperties() { + void clearProperties() { FilterOptions.INCLUDE_TAGS.clear(); FilterOptions.EXCLUDE_TAGS.clear(); FilterOptions.INDICES.clear(); FilterOptions.FILTER_REPEAT.clear(); } + /** + * Tag inclusion filtering behaviour - flows must have all of the included tags + */ + @Test + void tagInclude() { + Model mdl = new Mdl().withFlows( + "abc [a, b, c]", + "bcd [b, c, d]", + "cde [c, d, e]", + "def [d, e, f]", + "efg [e, f, g]" ); + BiConsumer test = ( in, out ) -> assertEquals( out, + new Filter( mdl ) + .includedTags( Stream.of( in.split( "," ) ) + .filter( s -> !s.isEmpty() ) + .collect( toSet() ) ) + .flows() + .map( f -> f.meta().description() ) + .collect( joining( "," ) ), + "for " + in ); + + test.accept( "", "abc,bcd,cde,def,efg" ); + test.accept( "a", "abc" ); + test.accept( "b", "abc,bcd" ); + test.accept( "c", "abc,bcd,cde" ); + test.accept( "d", "bcd,cde,def" ); + + test.accept( "a,b", "abc" ); + } + + /** + * Tag exclusion behaviour - flows must have none of the excluded tags + */ + @Test + void tagExclude() { + Model mdl = new Mdl().withFlows( + "abc [a, b, c]", + "bcd [b, c, d]", + "cde [c, d, e]", + "def [d, e, f]", + "efg [e, f, g]" ); + BiConsumer test = ( in, out ) -> assertEquals( out, + new Filter( mdl ) + .excludedTags( Stream.of( in.split( "," ) ) + .filter( s -> !s.isEmpty() ) + .collect( toSet() ) ) + .flows() + .map( f -> f.meta().description() ) + .collect( joining( "," ) ), + "for " + in ); + + test.accept( "", "abc,bcd,cde,def,efg" ); + test.accept( "a", "bcd,cde,def,efg" ); + test.accept( "b", "cde,def,efg" ); + test.accept( "c", "def,efg" ); + test.accept( "d", "abc,efg" ); + + test.accept( "a,g", "bcd,cde,def" ); + } + + /** + * Combining include and exclude filters + */ + @Test + void tagFilter() { + Model mdl = new Mdl().withFlows( + "abc [a, b, c]", + "bcd [b, c, d]", + "cde [c, d, e]", + "def [d, e, f]", + "efg [e, f, g]" ); + BiConsumer test = ( in, out ) -> assertEquals( out, + new Filter( mdl ) + .includedTags( Stream.of( in.split( "," ) ) + .filter( s -> !s.isEmpty() && s.startsWith( "+" ) ) + .map( s -> s.substring( 1 ) ) + .collect( toSet() ) ) + .excludedTags( Stream.of( in.split( "," ) ) + .filter( s -> !s.isEmpty() && s.startsWith( "-" ) ) + .map( s -> s.substring( 1 ) ) + .collect( toSet() ) ) + .flows() + .map( f -> f.meta().description() ) + .collect( joining( "," ) ), + "for " + in ); + + test.accept( "", "abc,bcd,cde,def,efg" ); + test.accept( "+a", "abc" ); + test.accept( "+b", "abc,bcd" ); + test.accept( "-a", "bcd,cde,def,efg" ); + test.accept( "-b", "cde,def,efg" ); + test.accept( "+a,-b", "" ); + test.accept( "-a,+b", "bcd" ); + test.accept( "+d,-b", "cde,def" ); + } + + /** + * Index filtering + */ + @Test + void indexFilter() { + Model mdl = new Mdl().withFlows( + "abc [a, b, c]", + "bcd [b, c, d]", + "cde [c, d, e]", + "def [d, e, f]", + "efg [e, f, g]" ); + BiConsumer test = ( in, out ) -> assertEquals( out, + new Filter( mdl ) + .indices( Stream.of( in.split( "," ) ) + .filter( s -> !s.isEmpty() ) + .map( Integer::valueOf ) + .collect( toSet() ) ) + .flows() + .map( f -> f.meta().description() ) + .collect( joining( "," ) ), + "for " + in ); + + test.accept( "", "abc,bcd,cde,def,efg" ); + test.accept( "0", "abc" ); + test.accept( "1", "bcd" ); + test.accept( "4", "efg" ); + test.accept( "2,3", "cde,def" ); + } + + /** + * Combining tag and index filtering + */ + @Test + void filter() { + Model mdl = new Mdl().withFlows( + "abc [a, b, c]", + "bcd [b, c, d]", + "cde [c, d, e]", + "def [d, e, f]", + "efg [e, f, g]" ); + BiConsumer test = ( in, out ) -> assertEquals( out, + new Filter( mdl ) + .includedTags( Stream.of( in.split( "," ) ) + .filter( s -> !s.isEmpty() && s.startsWith( "+" ) ) + .map( s -> s.substring( 1 ) ) + .collect( toSet() ) ) + .excludedTags( Stream.of( in.split( "," ) ) + .filter( s -> !s.isEmpty() && s.startsWith( "-" ) ) + .map( s -> s.substring( 1 ) ) + .collect( toSet() ) ) + .indices( Stream.of( in.split( "," ) ) + .filter( s -> s.matches( "\\d+" ) ) + .map( Integer::valueOf ) + .collect( toSet() ) ) + .flows() + .map( f -> f.meta().description() ) + .collect( joining( "," ) ), + "for " + in ); + + test.accept( "", "abc,bcd,cde,def,efg" ); + test.accept( "+c", "abc,bcd,cde" ); + test.accept( "+c,-a", "bcd,cde" ); + test.accept( "+c,-a,0", "bcd" ); + test.accept( "+c,-a,1", "cde" ); + } + /** * Loading filters from properties */ @@ -117,11 +280,11 @@ void persistence() throws IOException { @Test void failures() { Model mdl = new Mdl().withFlows( - new Flw( "abc [a, b, c]" ), - new Flw( "bcd [b, c, d]" ), - new Flw( "cde [c, d, e]" ), - new Flw( "def [d, e, f]" ), - new Flw( "efg [e, f, g]" ) ); + "abc [a, b, c]", + "bcd [b, c, d]", + "cde [c, d, e]", + "def [d, e, f]", + "efg [e, f, g]" ); // ensure there are no reports Path reportDir = Paths.get( "target", "mctf", "FilterTest", "failures" ); @@ -235,7 +398,8 @@ void indexUpdates() { "abc", "bcd" ) .expectIndices( "But the tag filter is just as effective as " - + "the index filter, so the indices are cleared" ); + + "the index filter, so the indices are cleared" ) + .expectListenerEvents( 2 ); new FltrTst( mdl ) .includeIndex( 0, 1 ) @@ -248,7 +412,8 @@ void indexUpdates() { "abc", "bcd" ) .expectIndices( "But the tag filter is just as effective as " - + "the index filter, so the indices are cleared" ); + + "the index filter, so the indices are cleared" ) + .expectListenerEvents( 2 ); new FltrTst( mdl ) .includeIndex( 2 ) @@ -260,7 +425,8 @@ void indexUpdates() { .expectIndices( "Index has changed to account for the tag filter", 1 ) .expectFlows( "Still get the same flow", - "cde" ); + "cde" ) + .expectListenerEvents( 2 ); new FltrTst( mdl ) .includeIndex( 2 ) @@ -272,15 +438,22 @@ void indexUpdates() { .expectIndices( "Index has changed to account for the tag filter", 1 ) .expectFlows( "Still get the same flow", - "cde" ); + "cde" ) + .expectListenerEvents( 2 ); } private static class FltrTst { private final Filter filter; + private AtomicInteger listenerEvents = new AtomicInteger( 0 ); public FltrTst( Model model ) { filter = new Filter( model ); + assertSame( filter, + filter.listener( f -> { + assertSame( filter, f ); + listenerEvents.incrementAndGet(); + } ) ); } public FltrTst includeTag( String tag ) { @@ -342,6 +515,11 @@ public FltrTst expectIndices( String description, int... idxs ) { assertEquals( expect, filter.indices(), description ); return this; } + + public FltrTst expectListenerEvents( int count ) { + assertEquals( count, listenerEvents.get() ); + return this; + } } /** @@ -367,4 +545,49 @@ void parseIndices() { test.accept( "0-4", "[0, 1, 2, 3, 4]" ); test.accept( "0-2,5,blah,8-9", "[0, 1, 2, 5, 8, 9]" ); } + + /** + * Tests index property access + */ + @Test + void indexProperty() { + BiConsumer test = ( in, out ) -> { + Mdl mdl = new Mdl(); + Stream.of( "abcdefghijklmnopqrstuvwxyz".split( "" ) ) + .forEach( a -> mdl.withFlows( a + " []" ) ); + Filter f = new Filter( mdl ); + f.indices( Stream.of( in.split( "," ) ) + .filter( s -> !s.isEmpty() ) + .map( Integer::valueOf ) + .collect( toSet() ) ); + assertEquals( + out, + f.property( FilterOptions.INDICES ), + "for " + in ); + }; + + test.accept( "", "" ); + test.accept( "1", "1" ); + test.accept( "1,2", "1,2" ); + test.accept( "1,3", "1,3" ); + test.accept( "1,2,3", "1-3" ); + test.accept( "1,2,3,4,7,8,9", "1-4,7-9" ); + test.accept( "1,2,3,4,6,8,9,10,11,12", "1-4,6,8-12" ); + } + + /** + * Tests tag property access + */ + @Test + void tagProperty() { + Filter f = new Filter( new Mdl() ); + assertEquals( null, f.property( FilterOptions.INCLUDE_TAGS ) ); + assertEquals( null, f.property( FilterOptions.EXCLUDE_TAGS ) ); + + f.includedTags( Stream.of( "abc", "def" ).collect( toSet() ) ); + f.excludedTags( Stream.of( "ghi", "jkl" ).collect( toSet() ) ); + + assertEquals( "abc,def", f.property( FilterOptions.INCLUDE_TAGS ) ); + assertEquals( "ghi,jkl", f.property( FilterOptions.EXCLUDE_TAGS ) ); + } } diff --git a/assert/assert-filter/src/test/java/com/mastercard/test/flow/assrt/filter/cli/TagPhaseTest.java b/assert/assert-filter/src/test/java/com/mastercard/test/flow/assrt/filter/cli/TagPhaseTest.java index a8c716fa70..f8242274b0 100644 --- a/assert/assert-filter/src/test/java/com/mastercard/test/flow/assrt/filter/cli/TagPhaseTest.java +++ b/assert/assert-filter/src/test/java/com/mastercard/test/flow/assrt/filter/cli/TagPhaseTest.java @@ -1,3 +1,4 @@ + package com.mastercard.test.flow.assrt.filter.cli; import static java.util.stream.Collectors.joining; @@ -55,7 +56,7 @@ void input() { test.accept( "+f", "a" ); test.accept( "+g", "a b" ); test.accept( "-h", "d e" ); - test.accept( "+f +l", "a e" ); + test.accept( "+f +l", "" ); test.accept( "+i -k", "b c" ); test.accept( "+f -f", "a b c d e" ); diff --git a/assert/assert-filter/src/test/java/com/mastercard/test/flow/assrt/filter/gui/FilterGuiHarness.java b/assert/assert-filter/src/test/java/com/mastercard/test/flow/assrt/filter/gui/FilterGuiHarness.java index 879c76e3b5..c01ba6b1ff 100644 --- a/assert/assert-filter/src/test/java/com/mastercard/test/flow/assrt/filter/gui/FilterGuiHarness.java +++ b/assert/assert-filter/src/test/java/com/mastercard/test/flow/assrt/filter/gui/FilterGuiHarness.java @@ -1,3 +1,4 @@ + package com.mastercard.test.flow.assrt.filter.gui; import static com.mastercard.test.flow.assrt.filter.Util.copypasta; @@ -13,8 +14,9 @@ import java.util.regex.Pattern; import java.util.stream.Stream; +import org.assertj.swing.core.BasicRobot; import org.assertj.swing.edt.FailOnThreadViolationRepaintManager; -import org.assertj.swing.edt.GuiActionRunner; +import org.assertj.swing.finder.WindowFinder; import org.assertj.swing.fixture.FrameFixture; import org.assertj.swing.fixture.JListFixture; import org.assertj.swing.fixture.JTextComponentFixture; @@ -42,6 +44,7 @@ public class FilterGuiHarness { * The tag list widgets in the interface */ public enum TagList { + /***/ AVAILABLE, /***/ @@ -153,6 +156,7 @@ public FilterGuiHarness buildFlows() { * The flow list widgets in the interface */ public enum FlowList { + /***/ DISABLED, /***/ @@ -265,8 +269,7 @@ public FilterGuiHarness hitEnter() { */ public FilterGuiHarness on( Mdl model ) { Filter filter = new Filter( model ); - FilterGui gui = GuiActionRunner.execute( () -> new FilterGui( filter ) ); - FrameFixture ff = new FrameFixture( gui ); + FilterGui gui = new FilterGui( filter ); // it is suprisingly disruptive to have the mouse pointer left somewhere // unexpected, so let's make an effort to put it back where we found it @@ -279,21 +282,29 @@ public FilterGuiHarness on( Mdl model ) { guith.setDaemon( true ); guith.start(); - while( !interactions.isEmpty() ) { - interactions.poll().accept( ff, model ); - } + FrameFixture ff = WindowFinder + .findFrame( "flow_filter_frame" ) + .withTimeout( 3000 ) + .using( BasicRobot.robotWithCurrentAwtHierarchy() ); try { - guith.join( 5000 ); - } - catch( InterruptedException e ) { - e.printStackTrace(); - } + while( !interactions.isEmpty() ) { + interactions.poll().accept( ff, model ); + } - Assertions.assertFalse( guith.isAlive(), "cli thread should have stopped!" ); + try { + guith.join( 5000 ); + } + catch( InterruptedException e ) { + e.printStackTrace(); + } - ff.robot().moveMouse( initialMousePosition ); - ff.cleanUp(); + Assertions.assertFalse( guith.isAlive(), "cli thread should have stopped!" ); + } + finally { + ff.robot().moveMouse( initialMousePosition ); + ff.cleanUp(); + } results = copypasta( filter.flows() .map( f -> f.meta().id() ) );