aboutsummaryrefslogtreecommitdiffstats
path: root/vespa-osgi-testrunner/src/test/java/com/yahoo/vespa/testrunner/JunitRunnerTest.java
blob: 09aaeba081f733d1abafcf5fa6cccb09b7d69869 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
package com.yahoo.vespa.testrunner;

import ai.vespa.cloud.ApplicationId;
import ai.vespa.cloud.Environment;
import ai.vespa.cloud.Zone;
import ai.vespa.hosted.cd.Deployment;
import ai.vespa.hosted.cd.Endpoint;
import ai.vespa.hosted.cd.TestRuntime;
import com.yahoo.vespa.testrunner.TestReport.Status;
import com.yahoo.vespa.testrunner.TestRunner.Suite;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.engine.JupiterTestEngine;
import org.junit.platform.engine.EngineExecutionListener;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.TestEngine;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.engine.reporting.ReportEntry;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestIdentifier;
import org.junit.platform.launcher.TestPlan;
import org.junit.platform.launcher.core.EngineDiscoveryOrchestrator;
import org.junit.platform.launcher.core.EngineExecutionOrchestrator;
import org.junit.platform.launcher.core.LauncherDiscoveryResult;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.annotation.Annotation;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

import static ai.vespa.hosted.cd.internal.TestRuntimeProvider.testRuntime;
import static java.util.Objects.requireNonNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import static org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.Phase.EXECUTION;

/**
 * @author jonmv
 */
class JunitRunnerTest {

    static final Clock clock = Clock.fixed(Instant.EPOCH, ZoneId.of("UTC"));

    @TestFactory
    Stream<DynamicTest> runSampleTests() {
        String packageName = "com.yahoo.vespa.test.samples";
        InputStream classes = getClass().getClassLoader().getResourceAsStream(packageName.replace(".", "/"));
        BufferedReader reader = new BufferedReader(new InputStreamReader(requireNonNull(classes, packageName + " should contain sample tests")));
        return reader.lines()
                     .filter(line -> line.endsWith("Test.class"))
                     .map(name -> {
                         try {
                             Class<?> testClass = getClass().getClassLoader().loadClass(packageName + "." + name.replace(".class", ""));
                             return dynamicTest(testClass.getSimpleName(), () -> verify(testClass));
                         }
                         catch (ClassNotFoundException e) {
                             throw new IllegalStateException(e);
                         }
                     });
    }

    static void verify(Class<?> testClass) {
        Expect expected = requireNonNull(testClass.getAnnotation(Expect.class), "sample tests must be annotated with @Expect");
        TestReport report = test(getSuite(testClass), new byte[0], testClass).getReport();
        assertEquals(Status.values()[expected.status()], report.root().status());
        Map<Status, Long> tally = new EnumMap<>(Status.class);
        if (expected.successful() > 0) tally.put(Status.successful, expected.successful());
        if (expected.skipped() > 0) tally.put(Status.skipped, expected.skipped());
        if (expected.aborted() > 0) tally.put(Status.aborted, expected.aborted());
        if (expected.inconclusive() > 0) tally.put(Status.inconclusive, expected.inconclusive());
        if (expected.failed() > 0) tally.put(Status.failed, expected.failed());
        if (expected.error() > 0) tally.put(Status.error, expected.error());
        assertEquals(tally, report.root().tally());
    }

    static Suite getSuite(Class<?> testClass) {
        for (Annotation annotation : testClass.getAnnotations()) {
            switch (annotation.annotationType().getSimpleName()) {
                case "SystemTest": return Suite.SYSTEM_TEST;
                case "StagingSetup": return Suite.STAGING_SETUP_TEST;
                case "StagingTest": return Suite.STAGING_TEST;
                case "ProductionTest": return Suite.PRODUCTION_TEST;
            }
        }
        return null;
    }

    static TestRunner test(Suite suite, byte[] testConfig, Class<?>... testClasses) {
        JunitRunner runner = new JunitRunner(clock,
                                             config -> { assertSame(testConfig, config); testRuntime.set(new MockTestRuntime()); },
                                             __ -> List.of(testClasses),
                                             JunitRunnerTest::execute);
        try {
            runner.test(suite, testConfig).get();
        }
        catch (Exception e) {
            fail(e);
        }
        return runner;
    }


    // For some inane reason, the JUnit test framework makes it impossible to simply launch a new instance of itself
    // from inside a unit test (run by itself) in the standard way, so this kludge is necessary to work around that.
    static void execute(LauncherDiscoveryRequest discoveryRequest, TestExecutionListener... listeners) {
        TestEngine testEngine = new JupiterTestEngine();
        LauncherDiscoveryResult discoveryResult = new EngineDiscoveryOrchestrator(Set.of(testEngine), Set.of()).discover(discoveryRequest, EXECUTION);
        TestDescriptor engineTestDescriptor = discoveryResult.getEngineTestDescriptor(testEngine);
        TestPlan plan = TestPlan.from(List.of(engineTestDescriptor), discoveryRequest.getConfigurationParameters());
        for (TestExecutionListener listener : listeners) listener.testPlanExecutionStarted(plan);
        new EngineExecutionOrchestrator().execute(discoveryResult, new ExecutionListenerAdapter(plan, listeners));
        for (TestExecutionListener listener : listeners) listener.testPlanExecutionFinished(plan);
    }

    static class ExecutionListenerAdapter implements EngineExecutionListener {

        private final TestPlan plan;
        private final List<TestExecutionListener> listeners;

        public ExecutionListenerAdapter(TestPlan plan, TestExecutionListener... listeners) {
            this.plan = plan;
            this.listeners = List.of(listeners);
        }

        private TestIdentifier getTestIdentifier(TestDescriptor testDescriptor) {
            return plan.getTestIdentifier(testDescriptor.getUniqueId().toString());
        }

        @Override public void dynamicTestRegistered(TestDescriptor testDescriptor) {
            TestIdentifier id = TestIdentifier.from(testDescriptor);
            plan.addInternal(id);
            for (TestExecutionListener listener : listeners)
                listener.dynamicTestRegistered(id);
        }

        @Override public void executionSkipped(TestDescriptor testDescriptor, String reason) {
            for (TestExecutionListener listener : listeners)
                listener.executionSkipped(getTestIdentifier(testDescriptor), reason);
        }

        @Override public void executionStarted(TestDescriptor testDescriptor) {
            for (TestExecutionListener listener : listeners)
                listener.executionStarted(getTestIdentifier(testDescriptor));
        }

        @Override public void executionFinished(TestDescriptor testDescriptor, TestExecutionResult testExecutionResult) {
            for (TestExecutionListener listener : listeners)
                listener.executionFinished(getTestIdentifier(testDescriptor), testExecutionResult);
        }

        @Override public void reportingEntryPublished(TestDescriptor testDescriptor, ReportEntry entry) {
            for (TestExecutionListener listener : listeners)
                listener.reportingEntryPublished(getTestIdentifier(testDescriptor), entry);
        }

    }


    static class MockTestRuntime implements TestRuntime {

        @Override
        public Deployment deploymentToTest() {
            return new Deployment() {
                @Override public Endpoint endpoint(String id) { return null; }
                @Override public String platform() { return "1.2.3"; }
                @Override public long revision() { return 321; }
                @Override public Instant deployedAt() { return Instant.ofEpochMilli(1000); }
            };
        }

        @Override
        public Zone zone() {
            return new Zone(Environment.test, "name");
        }

        @Override
        public ApplicationId application() {
            return null;
        }

    }

}