aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--cloud-tenant-cd/pom.xml19
-rw-r--r--cloud-tenant-cd/src/main/java/ai/vespa/hosted/cd/VespaTestRuntime.java57
-rw-r--r--cloud-tenant-cd/src/main/java/ai/vespa/hosted/cd/impl/VespaTestRuntime.java113
-rw-r--r--cloud-tenant-cd/src/main/java/ai/vespa/hosted/cd/impl/http/HttpDeployment.java (renamed from cloud-tenant-cd/src/main/java/ai/vespa/hosted/cd/http/HttpDeployment.java)4
-rw-r--r--cloud-tenant-cd/src/main/java/ai/vespa/hosted/cd/impl/http/HttpEndpoint.java (renamed from cloud-tenant-cd/src/main/java/ai/vespa/hosted/cd/http/HttpEndpoint.java)2
-rw-r--r--cloud-tenant-cd/src/main/java/com/yahoo/vespa/hosted/cd/impl/package-info.java9
-rw-r--r--cloud-tenant-cd/src/main/resources/META-INF/services/ai.vespa.hosted.cd.TestRuntime2
-rw-r--r--cloud-tenant-cd/src/main/resources/configdefinitions/cloud-tenant-cd.def9
-rw-r--r--pom.xml1
-rw-r--r--tenant-cd-api/pom.xml7
-rw-r--r--tenant-cd-api/src/main/java/ai/vespa/hosted/cd/TestRuntime.java19
-rw-r--r--tenant-cd-api/src/main/java/ai/vespa/hosted/cd/internal/TestRuntimeProvider.java29
-rw-r--r--vespa-osgi-testrunner/pom.xml88
-rw-r--r--vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/JunitHandler.java58
-rw-r--r--vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/JunitRunner.java167
-rw-r--r--vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/TestDescriptor.java60
-rw-r--r--vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/package-info.java9
-rw-r--r--vespa-osgi-testrunner/src/test/java/TestDescriptorTest.java70
18 files changed, 660 insertions, 63 deletions
diff --git a/cloud-tenant-cd/pom.xml b/cloud-tenant-cd/pom.xml
index ba4b7d02020..03730daceb8 100644
--- a/cloud-tenant-cd/pom.xml
+++ b/cloud-tenant-cd/pom.xml
@@ -43,6 +43,12 @@
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>container-dev</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
<!-- compile scope -->
<dependency>
@@ -66,6 +72,19 @@
</dependencies>
<build>
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <!-- TODO: Make config class plugin work with other packageprefix. Below does not work-->
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>config-class-plugin</artifactId>
+ <version>${project.version}</version>
+ <configuration>
+ <packagePrefix>ai.vespa</packagePrefix>
+ </configuration>
+ </plugin>
+ </plugins>
+ </pluginManagement>
<plugins>
<plugin>
<groupId>com.yahoo.vespa</groupId>
diff --git a/cloud-tenant-cd/src/main/java/ai/vespa/hosted/cd/VespaTestRuntime.java b/cloud-tenant-cd/src/main/java/ai/vespa/hosted/cd/VespaTestRuntime.java
deleted file mode 100644
index 75aaaec78ba..00000000000
--- a/cloud-tenant-cd/src/main/java/ai/vespa/hosted/cd/VespaTestRuntime.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package ai.vespa.hosted.cd;
-
-import ai.vespa.cloud.Zone;
-import ai.vespa.hosted.api.ControllerHttpClient;
-import ai.vespa.hosted.api.Properties;
-import ai.vespa.hosted.api.TestConfig;
-import ai.vespa.hosted.cd.http.HttpDeployment;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.zone.ZoneId;
-
-import java.nio.file.Files;
-import java.nio.file.Paths;
-
-/**
- * @author mortent
- */
-public class VespaTestRuntime implements TestRuntime {
- private final TestConfig config;
- private final Deployment deploymentToTest;
-
- public VespaTestRuntime() {
- String configPath = System.getProperty("vespa.test.config");
- TestConfig config = configPath != null ? fromFile(configPath) : fromController();
- this.config = config;
- this.deploymentToTest = new HttpDeployment(config.deployments().get(config.zone()), new ai.vespa.hosted.auth.EndpointAuthenticator(config.system()));
- }
-
- @Override
- public Zone zone() {
- return new Zone(
- ai.vespa.cloud.Environment.valueOf(config.zone().environment().name()),
- config.zone().region().value()); }
-
- /** Returns the deployment this is testing. */
- @Override
- public Deployment deploymentToTest() { return deploymentToTest; }
-
- private static TestConfig fromFile(String path) {
- try {
- return TestConfig.fromJson(Files.readAllBytes(Paths.get(path)));
- }
- catch (Exception e) {
- throw new IllegalArgumentException("Failed reading config from '" + path + "'!", e);
- }
- }
-
- private static TestConfig fromController() {
- ControllerHttpClient controller = new ai.vespa.hosted.auth.ApiAuthenticator().controller();
- ApplicationId id = Properties.application();
- Environment environment = Properties.environment().orElse(Environment.dev);
- ZoneId zone = Properties.region().map(region -> ZoneId.from(environment, region))
- .orElseGet(() -> controller.defaultZone(environment));
- return controller.testConfig(id, zone);
- }
-}
diff --git a/cloud-tenant-cd/src/main/java/ai/vespa/hosted/cd/impl/VespaTestRuntime.java b/cloud-tenant-cd/src/main/java/ai/vespa/hosted/cd/impl/VespaTestRuntime.java
new file mode 100644
index 00000000000..d2367d588f6
--- /dev/null
+++ b/cloud-tenant-cd/src/main/java/ai/vespa/hosted/cd/impl/VespaTestRuntime.java
@@ -0,0 +1,113 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.hosted.cd.impl;
+
+import ai.vespa.cloud.Zone;
+import ai.vespa.hosted.api.ControllerHttpClient;
+import ai.vespa.hosted.api.Properties;
+import ai.vespa.hosted.api.TestConfig;
+import ai.vespa.hosted.cd.Deployment;
+import ai.vespa.hosted.cd.TestRuntime;
+import ai.vespa.hosted.cd.impl.http.HttpDeployment;
+import com.google.inject.Inject;
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.SystemName;
+import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.vespa.hosted.cd.impl.CloudTenantCdConfig;
+
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author mortent
+ */
+public class VespaTestRuntime extends AbstractComponent implements TestRuntime {
+ private final TestConfig config;
+ private final Deployment deploymentToTest;
+
+ /*
+ * Used when executing tests locally
+ */
+ public VespaTestRuntime() {
+ this(configFromPropertyOrController());
+ }
+
+ /*
+ * Used when executing tests from using Vespa test framework in container
+ */
+ @Inject
+ public VespaTestRuntime(CloudTenantCdConfig c) {
+
+ this(fromVespaConfig(c));
+ }
+ private VespaTestRuntime(TestConfig config) {
+ this.config = config;
+ this.deploymentToTest = new HttpDeployment(config.deployments().get(config.zone()), new ai.vespa.hosted.auth.EndpointAuthenticator(config.system()));
+ }
+
+ @Override
+ public Zone zone() {
+ return new Zone(
+ ai.vespa.cloud.Environment.valueOf(config.zone().environment().name()),
+ config.zone().region().value()); }
+
+ /** Returns the deployment this is testing. */
+ @Override
+ public Deployment deploymentToTest() { return deploymentToTest; }
+
+ private static TestConfig configFromPropertyOrController() {
+ String configPath = System.getProperty("vespa.test.config");
+ return configPath != null ? fromFile(configPath) : fromController();
+ }
+
+ private static TestConfig fromFile(String path) {
+ try {
+ return TestConfig.fromJson(Files.readAllBytes(Paths.get(path)));
+ }
+ catch (Exception e) {
+ throw new IllegalArgumentException("Failed reading config from '" + path + "'!", e);
+ }
+ }
+
+ private static TestConfig fromController() {
+ ControllerHttpClient controller = new ai.vespa.hosted.auth.ApiAuthenticator().controller();
+ ApplicationId id = Properties.application();
+ Environment environment = Properties.environment().orElse(Environment.dev);
+ ZoneId zone = Properties.region().map(region -> ZoneId.from(environment, region))
+ .orElseGet(() -> controller.defaultZone(environment));
+ return controller.testConfig(id, zone);
+ }
+
+ private static TestConfig fromVespaConfig(CloudTenantCdConfig config) {
+ Map<ZoneId, Map<String, URI>> deployments = new HashMap<>();
+ Map<ZoneId, List<String>> contentClusters = new HashMap<>();
+ for (Map.Entry<String, CloudTenantCdConfig.Zones> entry : config.zones().entrySet()) {
+ ZoneId zoneId = ZoneId.from(entry.getKey());
+
+ Map<String, URI> zoneDeployments = entry.getValue().deployments().entrySet().stream()
+ .collect(Collectors.toMap(Map.Entry::getKey, e -> URI.create(e.getValue())));
+
+ deployments.put(zoneId, zoneDeployments);
+
+ contentClusters.put(zoneId, entry.getValue().contentClusters());
+ }
+ return new TestConfig(
+ ApplicationId.fromFullString(config.application()),
+ ZoneId.from(config.zone()),
+ SystemName.from(config.systemName()),
+ config.isCi(),
+ deployments,
+ contentClusters);
+ }
+
+ @Override
+ public void deconstruct() {
+ super.deconstruct();
+ }
+}
diff --git a/cloud-tenant-cd/src/main/java/ai/vespa/hosted/cd/http/HttpDeployment.java b/cloud-tenant-cd/src/main/java/ai/vespa/hosted/cd/impl/http/HttpDeployment.java
index 80d5416ab34..65210455b85 100644
--- a/cloud-tenant-cd/src/main/java/ai/vespa/hosted/cd/http/HttpDeployment.java
+++ b/cloud-tenant-cd/src/main/java/ai/vespa/hosted/cd/impl/http/HttpDeployment.java
@@ -1,5 +1,5 @@
// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package ai.vespa.hosted.cd.http;
+package ai.vespa.hosted.cd.impl.http;
import ai.vespa.hosted.api.EndpointAuthenticator;
import ai.vespa.hosted.cd.Deployment;
@@ -17,7 +17,7 @@ import java.util.stream.Collectors;
*/
public class HttpDeployment implements Deployment {
- private final Map<String, HttpEndpoint> endpoints;
+ private final Map<String, Endpoint> endpoints;
/** Creates a representation of the given deployment endpoints, using the authenticator for data plane access. */
public HttpDeployment(Map<String, URI> endpoints, EndpointAuthenticator authenticator) {
diff --git a/cloud-tenant-cd/src/main/java/ai/vespa/hosted/cd/http/HttpEndpoint.java b/cloud-tenant-cd/src/main/java/ai/vespa/hosted/cd/impl/http/HttpEndpoint.java
index a803fc3e0e2..f48973b7382 100644
--- a/cloud-tenant-cd/src/main/java/ai/vespa/hosted/cd/http/HttpEndpoint.java
+++ b/cloud-tenant-cd/src/main/java/ai/vespa/hosted/cd/impl/http/HttpEndpoint.java
@@ -1,5 +1,5 @@
// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package ai.vespa.hosted.cd.http;
+package ai.vespa.hosted.cd.impl.http;
import ai.vespa.hosted.api.EndpointAuthenticator;
import ai.vespa.hosted.cd.Endpoint;
diff --git a/cloud-tenant-cd/src/main/java/com/yahoo/vespa/hosted/cd/impl/package-info.java b/cloud-tenant-cd/src/main/java/com/yahoo/vespa/hosted/cd/impl/package-info.java
new file mode 100644
index 00000000000..b5d6f0450ec
--- /dev/null
+++ b/cloud-tenant-cd/src/main/java/com/yahoo/vespa/hosted/cd/impl/package-info.java
@@ -0,0 +1,9 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+/**
+ * @author mortent
+ */
+@ExportPackage
+package com.yahoo.vespa.hosted.cd.impl;
+
+import com.yahoo.osgi.annotation.ExportPackage; \ No newline at end of file
diff --git a/cloud-tenant-cd/src/main/resources/META-INF/services/ai.vespa.hosted.cd.TestRuntime b/cloud-tenant-cd/src/main/resources/META-INF/services/ai.vespa.hosted.cd.TestRuntime
index 695fe363e4e..35cb2ed7c25 100644
--- a/cloud-tenant-cd/src/main/resources/META-INF/services/ai.vespa.hosted.cd.TestRuntime
+++ b/cloud-tenant-cd/src/main/resources/META-INF/services/ai.vespa.hosted.cd.TestRuntime
@@ -1,2 +1,2 @@
# Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-ai.vespa.hosted.cd.VespaTestRuntime \ No newline at end of file
+ai.vespa.hosted.cd.impl.VespaTestRuntime \ No newline at end of file
diff --git a/cloud-tenant-cd/src/main/resources/configdefinitions/cloud-tenant-cd.def b/cloud-tenant-cd/src/main/resources/configdefinitions/cloud-tenant-cd.def
new file mode 100644
index 00000000000..bac21f386be
--- /dev/null
+++ b/cloud-tenant-cd/src/main/resources/configdefinitions/cloud-tenant-cd.def
@@ -0,0 +1,9 @@
+# Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=vespa.hosted.cd.impl
+
+application string
+zone string
+systemName string
+isCi bool
+zones{}.deployments{} string
+zones{}.contentClusters[] string
diff --git a/pom.xml b/pom.xml
index ba9ad4b04a4..b150e33208a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -139,6 +139,7 @@
<module>vespa-hadoop</module>
<module>vespa-http-client</module>
<module>vespa-maven-plugin</module>
+ <module>vespa-osgi-testrunner</module>
<module>vespa-testrunner-components</module>
<module>vespa_feed_perf</module>
<module>vespa_jersey2</module>
diff --git a/tenant-cd-api/pom.xml b/tenant-cd-api/pom.xml
index b19d42d094f..233788a12a9 100644
--- a/tenant-cd-api/pom.xml
+++ b/tenant-cd-api/pom.xml
@@ -40,6 +40,13 @@
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <!-- required to inject component from test implementation -->
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>component</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
<!-- compile -->
<dependency> <!-- TODO(bjorncs): share junit version number with test-runner implementation -->
diff --git a/tenant-cd-api/src/main/java/ai/vespa/hosted/cd/TestRuntime.java b/tenant-cd-api/src/main/java/ai/vespa/hosted/cd/TestRuntime.java
index 08cc0467b71..7aebbc7b7e7 100644
--- a/tenant-cd-api/src/main/java/ai/vespa/hosted/cd/TestRuntime.java
+++ b/tenant-cd-api/src/main/java/ai/vespa/hosted/cd/TestRuntime.java
@@ -2,7 +2,10 @@
package ai.vespa.hosted.cd;
import ai.vespa.cloud.Zone;
+import ai.vespa.hosted.cd.internal.TestRuntimeProvider;
+import org.osgi.framework.BundleReference;
+import java.util.Optional;
import java.util.ServiceLoader;
/**
@@ -13,8 +16,20 @@ import java.util.ServiceLoader;
*/
public interface TestRuntime {
static TestRuntime get() {
- ServiceLoader<TestRuntime> serviceLoader = ServiceLoader.load(TestRuntime.class);
- return serviceLoader.findFirst().orElseThrow(() -> new RuntimeException("No TestRuntime implementation found"));
+ var classloader = TestRuntime.class.getClassLoader();
+
+ System.out.println("classloader.toString() = " + classloader.toString());
+ System.out.println("classloader.getClass().toString() = " + classloader.getClass().toString());
+
+ if (classloader instanceof BundleReference) {
+ System.out.println("Loading Test runtime from osgi component");
+ return Optional.ofNullable(TestRuntimeProvider.getTestRuntime())
+ .orElseThrow(() -> new RuntimeException("Component graph not ready, retrying"));
+ } else {
+ System.out.println("Loading Test runtime from service loader");
+ ServiceLoader<TestRuntime> serviceLoader = ServiceLoader.load(TestRuntime.class, TestRuntime.class.getClassLoader());
+ return serviceLoader.findFirst().orElseThrow(() -> new RuntimeException("No TestRuntime implementation found"));
+ }
}
Deployment deploymentToTest();
diff --git a/tenant-cd-api/src/main/java/ai/vespa/hosted/cd/internal/TestRuntimeProvider.java b/tenant-cd-api/src/main/java/ai/vespa/hosted/cd/internal/TestRuntimeProvider.java
new file mode 100644
index 00000000000..e97fa5faf85
--- /dev/null
+++ b/tenant-cd-api/src/main/java/ai/vespa/hosted/cd/internal/TestRuntimeProvider.java
@@ -0,0 +1,29 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.hosted.cd.internal;
+
+import ai.vespa.hosted.cd.TestRuntime;
+import com.yahoo.component.AbstractComponent;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * @author mortent
+ */
+public class TestRuntimeProvider extends AbstractComponent {
+
+ private static final AtomicReference<TestRuntime> testRuntime = new AtomicReference<>();
+
+ public TestRuntimeProvider(TestRuntime testRuntime) {
+ TestRuntimeProvider.testRuntime.set(testRuntime);
+ }
+
+ public static TestRuntime getTestRuntime() {
+ return testRuntime.get();
+ }
+
+ @Override
+ public void deconstruct() {
+ super.deconstruct();
+ testRuntime.set(null);
+ }
+}
diff --git a/vespa-osgi-testrunner/pom.xml b/vespa-osgi-testrunner/pom.xml
new file mode 100644
index 00000000000..31bf6eba18b
--- /dev/null
+++ b/vespa-osgi-testrunner/pom.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>parent</artifactId>
+ <groupId>com.yahoo.vespa</groupId>
+ <version>7-SNAPSHOT</version>
+ <relativePath>../parent/pom.xml</relativePath>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>vespa-osgi-testrunner</artifactId>
+ <packaging>container-plugin</packaging>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>container-dev</artifactId>
+ <version>7-SNAPSHOT</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <!-- Verify that we need all junit deps -->
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-engine</artifactId>
+ <version>5.6.2</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-api</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.platform</groupId>
+ <artifactId>junit-platform-launcher</artifactId>
+ <version>1.6.2</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-api</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <version>5.6.2</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-api</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>tenant-cd-api</artifactId>
+ <version>7-SNAPSHOT</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>bundle-plugin</artifactId>
+ <version>${project.version}</version>
+ <extensions>true</extensions>
+ <configuration>
+ <attachBundleArtifact>true</attachBundleArtifact>
+ <bundleClassifierName>deploy</bundleClassifierName>
+ <useCommonAssemblyIds>false</useCommonAssemblyIds>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+
+</project> \ No newline at end of file
diff --git a/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/JunitHandler.java b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/JunitHandler.java
new file mode 100644
index 00000000000..5534ea29d13
--- /dev/null
+++ b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/JunitHandler.java
@@ -0,0 +1,58 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.testrunner;
+
+import com.yahoo.container.handler.metrics.JsonResponse;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.jdisc.LoggingRequestHandler;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.restapi.ErrorResponse;
+import com.yahoo.restapi.MessageResponse;
+import org.osgi.framework.Bundle;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.function.Function;
+
+/**
+ * @author mortent
+ */
+public class JunitHandler extends LoggingRequestHandler {
+
+ private final JunitRunner junitRunner;
+
+ public JunitHandler(Executor executor, AccessLog accessLog, JunitRunner junitRunner) {
+ super(executor, accessLog);
+ this.junitRunner = junitRunner;
+ }
+
+ @Override
+ public HttpResponse handle(HttpRequest httpRequest) {
+ String mode = property("mode", "help", httpRequest, String::valueOf);
+ TestDescriptor.TestCategory category = property("category", TestDescriptor.TestCategory.systemtest, httpRequest, TestDescriptor.TestCategory::valueOf);
+
+ if ("help".equalsIgnoreCase(mode)) {
+ return new MessageResponse("Accepted modes: \n help \n list \n execute");
+ }
+
+ if (!"list".equalsIgnoreCase(mode) && !"execute".equalsIgnoreCase(mode)) {
+ return new ErrorResponse(400, "client error", "Unknown mode \"" + mode + "\"");
+ }
+
+ Bundle testBundle = junitRunner.findTestBundle("-tests");
+ TestDescriptor testDescriptor = junitRunner.loadTestDescriptor(testBundle);
+ List<Class<?>> testClasses = junitRunner.loadClasses(testBundle, testDescriptor, category);
+
+ String jsonResponse = junitRunner.executeTests(testClasses);
+
+ return new JsonResponse(200, jsonResponse);
+ }
+
+ private static <VAL> VAL property(String name, VAL defaultValue, HttpRequest request, Function<String, VAL> converter) {
+ final String propertyString = request.getProperty(name);
+ if (propertyString != null) {
+ return converter.apply(propertyString);
+ }
+ return defaultValue;
+ }
+}
diff --git a/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/JunitRunner.java b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/JunitRunner.java
new file mode 100644
index 00000000000..68d3ea4c124
--- /dev/null
+++ b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/JunitRunner.java
@@ -0,0 +1,167 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.testrunner;
+
+import com.google.inject.Inject;
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.exception.ExceptionUtils;
+import com.yahoo.io.IOUtils;
+import com.yahoo.jdisc.application.OsgiFramework;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Slime;
+import com.yahoo.slime.SlimeUtils;
+import com.yahoo.yolean.Exceptions;
+import org.junit.jupiter.engine.JupiterTestEngine;
+import org.junit.platform.engine.discovery.DiscoverySelectors;
+import org.junit.platform.launcher.Launcher;
+import org.junit.platform.launcher.LauncherConstants;
+import org.junit.platform.launcher.LauncherDiscoveryRequest;
+import org.junit.platform.launcher.core.LauncherConfig;
+import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
+import org.junit.platform.launcher.core.LauncherFactory;
+import org.junit.platform.launcher.listeners.LoggingListener;
+import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
+import org.junit.platform.launcher.listeners.TestExecutionSummary;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+
+import java.io.IOException;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author mortent
+ */
+public class JunitRunner extends AbstractComponent {
+ private static final Logger logger = Logger.getLogger(JunitRunner.class.getName());
+
+ private final BundleContext bundleContext;
+
+ @Inject
+ public JunitRunner(OsgiFramework osgiFramework) {
+ // TODO mortent: Find a way to workaround this hack
+ var tmp = osgiFramework.bundleContext();
+ try {
+ var field = tmp.getClass().getDeclaredField("wrapped");
+ field.setAccessible(true);
+ bundleContext = (BundleContext) field.get(tmp);
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public Bundle findTestBundle(String bundleNameSuffix) {
+ return Stream.of(bundleContext.getBundles())
+ .filter(bundle -> bundle.getSymbolicName().endsWith(bundleNameSuffix))
+ .findAny()
+ .orElseThrow(() -> new RuntimeException("No bundle on classpath with name ending on " + bundleNameSuffix));
+ }
+
+ public TestDescriptor loadTestDescriptor(Bundle bundle) {
+ URL resource = bundle.getEntry("META-INF/testClasses.json");
+ TestDescriptor testDescriptor;
+ try {
+ var jsonDescriptor = IOUtils.readAll(resource.openStream(), Charset.defaultCharset()).trim();
+ testDescriptor = TestDescriptor.fromJsonString(jsonDescriptor);
+ logger.info( "Test classes in bundle :" + testDescriptor.toString());
+ return testDescriptor;
+ } catch (IOException e) {
+ throw new RuntimeException("Could not load META-INF/testClasses.json [" + e.getMessage() + "]");
+ }
+ }
+
+ public List<Class<?>> loadClasses(Bundle bundle, TestDescriptor testDescriptor, TestDescriptor.TestCategory testCategory) {
+ List<Class<?>> testClasses = testDescriptor.getConfiguredTests(testCategory).stream()
+ .map(className -> loadClass(bundle, className))
+ .collect(Collectors.toList());
+
+ StringBuffer buffer = new StringBuffer();
+ testClasses.forEach(cl -> buffer.append("\t").append(cl.toString()).append(" / ").append(cl.getClassLoader().toString()).append("\n"));
+ logger.info("Loaded testClasses: \n" + buffer.toString());
+ return testClasses;
+ }
+
+ private Class<?> loadClass(Bundle bundle, String className) {
+ try {
+ return bundle.loadClass(className);
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException("Could not find class: " + className + " in bundle " + bundle.getSymbolicName(), e);
+ }
+ }
+
+ public String executeTests(List<Class<?>> testClasses) {
+ LauncherDiscoveryRequest discoveryRequest = LauncherDiscoveryRequestBuilder.request()
+ .selectors(
+ testClasses.stream().map(DiscoverySelectors::selectClass).collect(Collectors.toList())
+ )
+ .configurationParameter(LauncherConstants.CAPTURE_STDERR_PROPERTY_NAME,"true")
+ .configurationParameter(LauncherConstants.CAPTURE_STDOUT_PROPERTY_NAME,"true")
+ .build();
+
+ var launcherConfig = LauncherConfig.builder()
+ .addTestEngines(new JupiterTestEngine())
+
+ .build();
+ Launcher launcher = LauncherFactory.create(launcherConfig);
+
+ // Create log listener:
+ var logLines = new ArrayList<String>();
+ var logListener = LoggingListener.forBiConsumer((t, m) -> log(logLines, m.get(), t));
+ // Create a summary listener:
+ var summaryListener = new SummaryGeneratingListener();
+ launcher.registerTestExecutionListeners(logListener, summaryListener);
+
+ // Execute request
+ launcher.execute(discoveryRequest);
+
+ var report = summaryListener.getSummary();
+
+ return createJsonTestReport(report, logLines);
+ }
+
+ private String createJsonTestReport(TestExecutionSummary report, List<String> logLines) {
+ var slime = new Slime();
+ var root = slime.setObject();
+ var summary = root.setObject("summary");
+ summary.setLong("Total tests", report.getTestsFoundCount());
+ summary.setLong("Test success", report.getTestsSucceededCount());
+ summary.setLong("Test failed", report.getTestsFailedCount());
+ summary.setLong("Test ignored", report.getTestsSkippedCount());
+ summary.setLong("Test success", report.getTestsAbortedCount());
+ summary.setLong("Test started", report.getTestsStartedCount());
+ var failures = summary.setArray("failures");
+ report.getFailures().forEach(failure -> serializeFailure(failure, failures.addObject()));
+
+ var output = root.setArray("output");
+ logLines.forEach(output::addString);
+
+ return Exceptions.uncheck(() -> new String(SlimeUtils.toJsonBytes(slime), StandardCharsets.UTF_8));
+ }
+
+ private void serializeFailure(TestExecutionSummary.Failure failure, Cursor slime) {
+ var testIdentifier = failure.getTestIdentifier();
+ slime.setString("testName", testIdentifier.getUniqueId());
+ slime.setString("testError",failure.getException().getMessage());
+ slime.setString("exception", ExceptionUtils.getStackTraceAsString(failure.getException()));
+ }
+
+ private void log(List<String> logs, String message, Throwable t) {
+ logs.add(message);
+ if(t != null) {
+ logs.add(t.getMessage());
+ List.of(t.getStackTrace()).stream()
+ .map(StackTraceElement::toString)
+ .forEach(logs::add);
+ }
+ }
+
+ @Override
+ public void deconstruct() {
+ super.deconstruct();
+ }
+}
diff --git a/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/TestDescriptor.java b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/TestDescriptor.java
new file mode 100644
index 00000000000..1c7092de93a
--- /dev/null
+++ b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/TestDescriptor.java
@@ -0,0 +1,60 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.testrunner;
+
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Inspector;
+import com.yahoo.slime.SlimeStream;
+import com.yahoo.slime.SlimeUtils;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author mortent
+ */
+public class TestDescriptor {
+ private final Map<TestCategory, List<String>> configuredTestClasses;
+ private final String version;
+
+ private TestDescriptor(String version, Map<TestCategory, List<String>> configuredTestClasses) {
+ this.version = version;
+ this.configuredTestClasses = configuredTestClasses;
+ }
+
+ public static TestDescriptor fromJsonString(String testDescriptor) {
+ var slime = SlimeUtils.jsonToSlime(testDescriptor);
+ var root = slime.get();
+ var version = root.field("version").asString();
+ var testRoot = root.field("configuredTests");
+ var systemTests = getJsonArray(testRoot, "systemTests");
+ var stagingTests = getJsonArray(testRoot, "stagingTests");
+ var productionTests = getJsonArray(testRoot, "productionTests");
+ return new TestDescriptor(version, Map.of(
+ TestCategory.systemtest, systemTests,
+ TestCategory.stagingtest, stagingTests,
+ TestCategory.productiontest, productionTests
+ ));
+ }
+
+ private static List<String> getJsonArray(Cursor cursor, String field) {
+ return SlimeStream.fromArray(cursor.field(field), Inspector::asString).collect(Collectors.toList());
+ }
+
+ public String version() {
+ return version;
+ }
+
+ public List<String> getConfiguredTests(TestCategory category) {
+ return List.copyOf(configuredTestClasses.get(category));
+ }
+
+ @Override
+ public String toString() {
+ return "TestClassDescriptor{" +
+ "configuredTestClasses=" + configuredTestClasses +
+ '}';
+ }
+
+ public enum TestCategory {systemtest, stagingtest, productiontest}
+}
diff --git a/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/package-info.java b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/package-info.java
new file mode 100644
index 00000000000..2606b3f839d
--- /dev/null
+++ b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/package-info.java
@@ -0,0 +1,9 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+/**
+ * @author mortent
+ */
+@ExportPackage
+package com.yahoo.vespa.testrunner;
+
+import com.yahoo.osgi.annotation.ExportPackage; \ No newline at end of file
diff --git a/vespa-osgi-testrunner/src/test/java/TestDescriptorTest.java b/vespa-osgi-testrunner/src/test/java/TestDescriptorTest.java
new file mode 100644
index 00000000000..9462a52ed9f
--- /dev/null
+++ b/vespa-osgi-testrunner/src/test/java/TestDescriptorTest.java
@@ -0,0 +1,70 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+import com.yahoo.vespa.testrunner.TestDescriptor;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author mortent
+ */
+public class TestDescriptorTest {
+
+ @Test
+ public void parses_system_test_only () {
+ String testDescriptor = "{\n" +
+ " \"version\": \"1.0\",\n" +
+ " \"configuredTests\": {\n" +
+ " \"systemTests\": [\n" +
+ " \"ai.vespa.test.SystemTest1\",\n" +
+ " \"ai.vespa.test.SystemTest2\"\n" +
+ " ]\n" +
+ " " +
+ "}\n" +
+ "}";
+ var testClassDescriptor = TestDescriptor.fromJsonString(testDescriptor);
+
+ var systemTests = testClassDescriptor.getConfiguredTests(TestDescriptor.TestCategory.systemtest);
+ Assertions.assertIterableEquals(List.of("ai.vespa.test.SystemTest1", "ai.vespa.test.SystemTest2"), systemTests);
+
+ var stagingTests = testClassDescriptor.getConfiguredTests(TestDescriptor.TestCategory.stagingtest);
+ Assertions.assertIterableEquals(Collections.emptyList(), stagingTests);
+
+ var productionTests = testClassDescriptor.getConfiguredTests(TestDescriptor.TestCategory.productiontest);
+ Assertions.assertIterableEquals(Collections.emptyList(), productionTests);
+ }
+
+ @Test
+ public void parsesDescriptorFile() {
+ String testDescriptor = "{\n" +
+ " \"version\": \"1.0\",\n" +
+ " \"configuredTests\": {\n" +
+ " \"systemTests\": [\n" +
+ " \"ai.vespa.test.SystemTest1\",\n" +
+ " \"ai.vespa.test.SystemTest2\"\n" +
+ " ],\n" +
+ " \"stagingTests\": [\n" +
+ " \"ai.vespa.test.StagingTest1\",\n" +
+ " \"ai.vespa.test.StagingTest2\"\n" +
+ " ],\n" +
+ " \"productionTests\": [\n" +
+ " \"ai.vespa.test.ProductionTest1\",\n" +
+ " \"ai.vespa.test.ProductionTest2\"\n" +
+ " ]\n" +
+ " " +
+ "}\n" +
+ "}";
+ var testClassDescriptor = TestDescriptor.fromJsonString(testDescriptor);
+
+ var systemTests = testClassDescriptor.getConfiguredTests(TestDescriptor.TestCategory.systemtest);
+ Assertions.assertIterableEquals(List.of("ai.vespa.test.SystemTest1", "ai.vespa.test.SystemTest2"), systemTests);
+
+ var stagingTests = testClassDescriptor.getConfiguredTests(TestDescriptor.TestCategory.stagingtest);
+ Assertions.assertIterableEquals(List.of("ai.vespa.test.StagingTest1", "ai.vespa.test.StagingTest2"), stagingTests);
+
+ var productionTests = testClassDescriptor.getConfiguredTests(TestDescriptor.TestCategory.productiontest);
+ Assertions.assertIterableEquals(List.of("ai.vespa.test.ProductionTest1", "ai.vespa.test.ProductionTest2"), productionTests);
+ }
+}