145b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin/*
245b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * Copyright (C) 2016 The Android Open Source Project
345b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin *
445b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * Licensed under the Apache License, Version 2.0 (the "License");
545b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * you may not use this file except in compliance with the License.
645b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * You may obtain a copy of the License at
745b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin *
845b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin *      http://www.apache.org/licenses/LICENSE-2.0
945b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin *
1045b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * Unless required by applicable law or agreed to in writing, software
1145b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * distributed under the License is distributed on an "AS IS" BASIS,
1245b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1345b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * See the License for the specific language governing permissions and
1445b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * limitations under the License.
1545b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin */
1645b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
1745b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffinpackage vogar.target;
1845b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
1945b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffinimport com.google.common.base.Function;
2045b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffinimport com.google.common.base.Functions;
2145b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffinimport java.util.List;
2245b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffinimport java.util.Properties;
2345b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffinimport java.util.concurrent.atomic.AtomicInteger;
2445b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffinimport org.junit.After;
2545b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffinimport org.junit.Before;
2645b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffinimport org.junit.Rule;
2745b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffinimport vogar.Result;
28527980476e5d16cc0eee3ea3ca585be2776e8762Paul Duffinimport vogar.target.junit.JUnitUtils;
2945b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffinimport vogar.testing.InterceptOutputStreams;
3045b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffinimport vogar.testing.InterceptOutputStreams.Stream;
3145b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
3245b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffinimport static org.junit.Assert.assertEquals;
3345b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffinimport static org.junit.Assert.assertTrue;
3445b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
3545b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin/**
3645b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * Provides support for testing {@link TestRunner} class.
3745b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin *
3845b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * <p>Subclasses provide the individual test methods, each test method has the following structure.
3945b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin *
4045b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * <p>It is annotated with {@link TestRunnerProperties @TestRunnerProperties} that specifies the
4145b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * properties that the main Vogar process supplies to the client process that actually runs the
4245b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * tests. It must specify either a {@link TestRunnerProperties#testClass()} or
4345b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * {@link TestRunnerProperties#testClassOrPackage()}, all the remaining properties are optional.
4445b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin *
4545b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * <p>It calls {@code testRunnerRule.createTestRunner(...)} to create a {@link TestRunner}; passing
4645b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * in any additional command line arguments that {@link TestRunner#TestRunner(Properties, List)}
4745b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * accepts.
4845b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin *
4945b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * <p>It calls {@link TestRunner#run()} to actually run the tests.
5045b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin *
5145b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * <p>It calls {@link #expectedResults()} to obtain a {@link ExpectedResults} instance that it uses
5245b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * to specify the expected results for the test. Once it has specified the expected results then it
5345b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * must call either {@link ExpectedResults#completedNormally()} or
5445b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * {@link ExpectedResults#aborted()}. They indicate whether the test process completed normally or
5545b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * would abort (due to a test timing out) and cause the actual results to be checked against the
5645b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin * expected results.
5745b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin */
5845b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffinpublic abstract class AbstractTestRunnerTest {
5945b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
6045b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin    @Rule
6145b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin    public InterceptOutputStreams ios = new InterceptOutputStreams(Stream.OUT);
6245b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
6345b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin    @Rule
6445b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin    public TestRunnerRule testRunnerRule = new TestRunnerRule();
6545b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
6645b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin    /**
6745b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin     * Keeps track of number of times {@link #expectedResults()} has been called without
6845b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin     * {@link ExpectedResults#checkFilteredOutput(String)}
6945b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin     * also being called. If it is {@code > 0} then the test is in error.
7045b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin     */
7145b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin    private AtomicInteger checkCount;
7245b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
7345b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin    @Before
7445b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin    public void beforeTest() {
7545b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        checkCount = new AtomicInteger();
7645b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin    }
7745b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
7845b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin    @After
7945b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin    public void afterTest() {
8045b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        if (checkCount.get() != 0) {
8145b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            throw new IllegalStateException("Test called expectedResults() but failed to call"
8245b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                    + "either aborted() or completedNormally()");
8345b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        }
8445b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin    }
8545b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
8645b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin    protected ExpectedResults expectedResults() {
8745b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        checkCount.incrementAndGet();
8845b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        return new ExpectedResults(testRunnerRule.testClass(), ios,
8945b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                checkCount);
9045b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin    }
9145b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
9245b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin    protected static class ExpectedResults {
9345b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
9445b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        private final StringBuilder builder = new StringBuilder();
9545b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        private final InterceptOutputStreams ios;
9645b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        private final AtomicInteger checkCount;
9745b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        private String testClassName;
9845b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        private Function<String, String> filter;
9945b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
10045b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        private ExpectedResults(
10145b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                Class<?> testClass, InterceptOutputStreams ios, AtomicInteger checkCount) {
10245b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            this.testClassName = testClass.getName();
10345b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            this.checkCount = checkCount;
10445b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            // Automatically strip out methods from a stack trace to avoid making tests dependent
10545b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            // on either the call hierarchy or on source line numbers which would make the tests
10645b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            // incredibly fragile. If a test fails then the unfiltered output containing the full
10745b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            // stack trace will be output so this will not lose information needed to debug errors.
10845b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            filter = new Function<String, String>() {
10945b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                @Override
11045b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                public String apply(String input) {
11145b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                    // Remove stack trace from output.
11245b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                    return input.replaceAll("\\t(at[^\\n]+|\\.\\.\\. [0-9]+ more)\\n", "");
11345b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                }
11445b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            };
11545b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            this.ios = ios;
11645b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        }
11745b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
11845b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        public ExpectedResults text(String message) {
11945b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            builder.append(message);
12045b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            return this;
12145b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        }
12245b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
12345b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        private ExpectedResults addFilter(Function<String, String> function) {
12445b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            filter = Functions.compose(filter, function);
12545b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            return this;
12645b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        }
12745b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
12845b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        public ExpectedResults ensureProfilingWasRequested() {
12945b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            return addFilter(new Function<String, String>() {
13045b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                @Override
13145b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                public String apply(String input) {
13245b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                    // Make sure that profiling is requested (even though it's not supported).
13345b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                    assertTrue("Profiling was not requested",
13445b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                            input.startsWith("Profiling is disabled: "));
13545b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
13645b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                    input = input.replaceAll("^Profiling is disabled:[^\n]+\\n", "");
13745b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                    return input;
13845b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                }
13945b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            });
14045b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        }
14145b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
14245b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        public ExpectedResults forTestClass(Class<?> testClass) {
14345b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            this.testClassName = testClass.getName();
14445b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            return this;
14545b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        }
14645b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
14745b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        public ExpectedResults forTestClass(String testClassName) {
14845b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            this.testClassName = testClassName;
14945b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            return this;
15045b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        }
15145b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
15245b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        public ExpectedResults failure(String methodName, String message) {
15345b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            String output = outcome(testClassName, methodName, message, Result.EXEC_FAILED);
15445b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            return text(output);
15545b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        }
15645b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
15745b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        public ExpectedResults success(String methodName) {
15845b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            String output = outcome(testClassName, methodName, null, Result.SUCCESS);
15945b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            return text(output);
16045b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        }
16145b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
16245b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        public ExpectedResults success(String methodName, String message) {
16345b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            String output = outcome(testClassName, methodName, message, Result.SUCCESS);
16445b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            return text(output);
16545b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        }
16645b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
16745b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
16845b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        public ExpectedResults unsupported() {
16945b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            String output = outcome(
17045b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                    testClassName, null,
17145b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                    "Skipping " + testClassName + ": no associated runner class\n",
17245b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                    Result.UNSUPPORTED);
17345b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            return text(output);
17445b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        }
17545b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
17645b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        public ExpectedResults noRunner() {
17745b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            String message =
17845b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                    String.format("Skipping %s: no associated runner class\n", testClassName);
17945b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            String output = outcome(testClassName, null, message, Result.UNSUPPORTED);
18045b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            return text(output);
18145b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        }
18245b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
18345b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        public void completedNormally() {
18445b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            text("//00xx{\"completedNormally\":true}\n");
18545b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            checkFilteredOutput(builder.toString());
18645b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        }
18745b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
18845b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        public void aborted() {
18945b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            checkFilteredOutput(builder.toString());
19045b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        }
19145b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
19245b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
19345b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        private static String outcome(
19445b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                String testClassName, String methodName, String message, Result result) {
195527980476e5d16cc0eee3ea3ca585be2776e8762Paul Duffin            String testName = JUnitUtils.getTestName(testClassName, methodName);
19645b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
19745b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            return String.format("//00xx{\"outcome\":\"%s\"}\n"
19845b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                            + "%s"
19945b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                            + "//00xx{\"result\":\"%s\"}\n",
20045b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                    testName, message == null ? "" : message, result);
20145b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        }
20245b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin
20345b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        private void checkFilteredOutput(String expected) {
20445b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            checkCount.decrementAndGet();
20545b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            String output = ios.contents(Stream.OUT);
20645b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            String filtered = filter.apply(output);
20745b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            if (!expected.equals(filtered)) {
20845b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin                assertEquals(expected, output);
20945b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin            }
21045b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin        }
21145b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin    }
21245b3ffb2d3d0c310718926c1acc69f5bf946ee3fPaul Duffin}
213