summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/AttributeChangeValidator.java2
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/AttributeChangeValidatorTest.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java2
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java12
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java31
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java15
-rw-r--r--controller-server/src/main/resources/configdefinitions/controller.def7
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java25
-rw-r--r--controller-server/src/test/resources/test_runner_services.xml-cd-legacy (renamed from controller-server/src/test/resources/test_runner_services.xml-cd)0
-rw-r--r--controller-server/src/test/resources/test_runner_services.xml-cd-osgi26
-rw-r--r--hosted-api/src/main/java/ai/vespa/hosted/api/TestDescriptor.java14
-rw-r--r--hosted-api/src/test/java/ai/vespa/hosted/api/TestDescriptorTest.java15
-rw-r--r--searchcore/src/vespa/searchcore/proton/attribute/attributesconfigscout.cpp1
-rw-r--r--vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/GenerateTestDescriptorMojo.java1
-rw-r--r--vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/TestAnnotationAnalyzer.java5
-rw-r--r--vespa-osgi-testrunner/pom.xml14
-rw-r--r--vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/JunitHandler.java71
-rw-r--r--vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/JunitRunner.java124
-rw-r--r--vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/TestReport.java55
-rw-r--r--vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/TestRunnerHandler.java211
-rw-r--r--vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/legacy/LegacyTestRunner.java22
-rw-r--r--vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/legacy/TestProfile.java (renamed from vespa-testrunner-components/src/main/java/com/yahoo/vespa/hosted/testrunner/TestProfile.java)11
-rw-r--r--vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/legacy/package-info.java9
-rw-r--r--vespa-testrunner-components/pom.xml18
-rw-r--r--vespa-testrunner-components/src/main/java/com/yahoo/vespa/hosted/testrunner/PomXmlGenerator.java1
-rw-r--r--vespa-testrunner-components/src/main/java/com/yahoo/vespa/hosted/testrunner/TestRunner.java10
-rw-r--r--vespa-testrunner-components/src/main/java/com/yahoo/vespa/hosted/testrunner/TestRunnerHandler.java2
-rw-r--r--vespa-testrunner-components/src/test/java/com/yahoo/vespa/hosted/testrunner/PomXmlGeneratorTest.java1
-rw-r--r--vespa-testrunner-components/src/test/java/com/yahoo/vespa/hosted/testrunner/TestRunnerTest.java2
31 files changed, 544 insertions, 174 deletions
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/AttributeChangeValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/AttributeChangeValidator.java
index 4f3e100ce75..5d790c74f18 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/AttributeChangeValidator.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/AttributeChangeValidator.java
@@ -96,8 +96,8 @@ public class AttributeChangeValidator {
validateAttributeSetting(currAttr, nextAttr, Attribute::densePostingListThreshold, "dense-posting-list-threshold", result);
validateAttributeSetting(currAttr, nextAttr, Attribute::isEnabledOnlyBitVector, "rank: filter", result);
validateAttributeSetting(currAttr, nextAttr, AttributeChangeValidator::hasHnswIndex, "indexing: index", result);
+ validateAttributeSetting(currAttr, nextAttr, Attribute::distanceMetric, "distance-metric", result);
if (hasHnswIndex(currAttr) && hasHnswIndex(nextAttr)) {
- validateAttributeSetting(currAttr, nextAttr, Attribute::distanceMetric, "distance-metric", result);
validateAttributeHnswIndexSetting(currAttr, nextAttr, HnswIndexParams::maxLinksPerNode, "max-links-per-node", result);
validateAttributeHnswIndexSetting(currAttr, nextAttr, HnswIndexParams::neighborsToExploreAtInsert, "neighbors-to-explore-at-insert", result);
}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/AttributeChangeValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/AttributeChangeValidatorTest.java
index 5da36d82a62..e00b34d4a79 100644
--- a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/AttributeChangeValidatorTest.java
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/AttributeChangeValidatorTest.java
@@ -168,11 +168,12 @@ public class AttributeChangeValidatorTest {
}
@Test
- public void changing_distance_metric_without_hnsw_index_enabled_is_ok() throws Exception {
+ public void changing_distance_metric_without_hnsw_index_enabled_requires_restart() throws Exception {
new Fixture("field f1 type tensor(x[2]) { indexing: attribute }",
"field f1 type tensor(x[2]) { indexing: attribute \n attribute { " +
"distance-metric: geodegrees \n } }").
- assertValidation();
+ assertValidation(newRestartAction("Field 'f1' changed: change property " +
+ "'distance-metric' from 'EUCLIDEAN' to 'GEODEGREES'"));
}
@Test
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java
index 24864c03530..2a167ee2962 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java
@@ -18,7 +18,7 @@ public interface BillingController {
/**
* @return String containing error message if something went wrong. Empty otherwise
*/
- PlanResult setPlan(TenantName tenant, PlanId planId, boolean hasApplications);
+ PlanResult setPlan(TenantName tenant, PlanId planId, boolean hasDeployments);
Invoice.Id createInvoiceForPeriod(TenantName tenant, ZonedDateTime startTime, ZonedDateTime endTime, String agent);
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java
index 4f367d6498e..523f20eaef8 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java
@@ -30,7 +30,7 @@ public class MockBillingController implements BillingController {
}
@Override
- public PlanResult setPlan(TenantName tenant, PlanId planId, boolean hasApplications) {
+ public PlanResult setPlan(TenantName tenant, PlanId planId, boolean hasDeployments) {
plans.put(tenant, planId);
return PlanResult.success();
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java
index 15ab14e3241..9e4600a1bdb 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java
@@ -18,6 +18,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.ServiceRegistry;
import com.yahoo.vespa.hosted.controller.api.integration.maven.MavenRepository;
import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
import com.yahoo.vespa.hosted.controller.auditlog.AuditLogger;
+import com.yahoo.vespa.hosted.controller.config.ControllerConfig;
import com.yahoo.vespa.hosted.controller.deployment.JobController;
import com.yahoo.vespa.hosted.controller.dns.NameServiceForwarder;
import com.yahoo.vespa.hosted.controller.metric.ConfigServerMetrics;
@@ -76,6 +77,7 @@ public class Controller extends AbstractComponent {
private final MavenRepository mavenRepository;
private final Metric metric;
private final RoutingController routingController;
+ private final ControllerConfig controllerConfig;
/**
* Creates a controller
@@ -84,14 +86,15 @@ public class Controller extends AbstractComponent {
*/
@Inject
public Controller(CuratorDb curator, RotationsConfig rotationsConfig, AccessControl accessControl, FlagSource flagSource,
- MavenRepository mavenRepository, ServiceRegistry serviceRegistry, Metric metric, SecretStore secretStore) {
+ MavenRepository mavenRepository, ServiceRegistry serviceRegistry, Metric metric, SecretStore secretStore,
+ ControllerConfig controllerConfig) {
this(curator, rotationsConfig, accessControl, com.yahoo.net.HostName::getLocalhost, flagSource,
- mavenRepository, serviceRegistry, metric, secretStore);
+ mavenRepository, serviceRegistry, metric, secretStore, controllerConfig);
}
public Controller(CuratorDb curator, RotationsConfig rotationsConfig, AccessControl accessControl,
Supplier<String> hostnameSupplier, FlagSource flagSource, MavenRepository mavenRepository,
- ServiceRegistry serviceRegistry, Metric metric, SecretStore secretStore) {
+ ServiceRegistry serviceRegistry, Metric metric, SecretStore secretStore, ControllerConfig controllerConfig) {
this.hostnameSupplier = Objects.requireNonNull(hostnameSupplier, "HostnameSupplier cannot be null");
this.curator = Objects.requireNonNull(curator, "Curator cannot be null");
@@ -110,6 +113,7 @@ public class Controller extends AbstractComponent {
routingController = new RoutingController(this, Objects.requireNonNull(rotationsConfig, "RotationsConfig cannot be null"));
auditLogger = new AuditLogger(curator, clock);
jobControl = new JobControl(curator);
+ this.controllerConfig = controllerConfig;
// Record the version of this controller
curator().writeControllerVersion(this.hostname(), ControllerVersion.CURRENT);
@@ -149,6 +153,8 @@ public class Controller extends AbstractComponent {
public MavenRepository mavenRepository() { return mavenRepository; }
+ public ControllerConfig controllerConfig() { return controllerConfig; }
+
public ApplicationView getApplicationView(String tenantName, String applicationName, String instanceName,
String environment, String region) {
return serviceRegistry.configServer().getApplicationView(tenantName, applicationName, instanceName, environment, region);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java
index f08cce57dcb..0ef25d0f613 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java
@@ -45,6 +45,7 @@ import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.Endpoint;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.certificate.EndpointCertificateException;
+import com.yahoo.vespa.hosted.controller.config.ControllerConfig;
import com.yahoo.vespa.hosted.controller.maintenance.JobRunner;
import com.yahoo.vespa.hosted.controller.routing.RoutingPolicyId;
import com.yahoo.yolean.Exceptions;
@@ -793,10 +794,13 @@ public class InternalStepRunner implements StepRunner {
ZoneId zone = id.type().zone(controller.system());
boolean useTesterCertificate = controller.system().isPublic() && id.type().environment().isTest();
+ boolean useOsgiBasedTestRuntime = testerPlatformVersion(id).isAfter(new Version(7, 247, 11));
byte[] servicesXml = servicesXml(! controller.system().isPublic(),
useTesterCertificate,
- testerResourcesFor(zone, spec.requireInstance(id.application().instance())));
+ useOsgiBasedTestRuntime,
+ testerResourcesFor(zone, spec.requireInstance(id.application().instance())),
+ controller.controllerConfig().steprunner().testerapp());
byte[] testPackage = controller.applications().applicationStore().getTester(id.application().tenant(), id.application().application(), version);
byte[] deploymentXml = deploymentXml(id.tester(),
spec.athenzDomain(),
@@ -845,7 +849,9 @@ public class InternalStepRunner implements StepRunner {
}
/** Returns the generated services.xml content for the tester application. */
- static byte[] servicesXml(boolean systemUsesAthenz, boolean useTesterCertificate, NodeResources resources) {
+ static byte[] servicesXml(
+ boolean systemUsesAthenz, boolean useTesterCertificate, boolean useOsgiBasedTestRuntime,
+ NodeResources resources, ControllerConfig.Steprunner.Testerapp config) {
int jdiscMemoryGb = 2; // 2Gb memory for tester application (excessive?).
int jdiscMemoryPct = (int) Math.ceil(100 * jdiscMemoryGb / resources.memoryGb());
@@ -856,6 +862,23 @@ public class InternalStepRunner implements StepRunner {
"<resources vcpu=\"%.2f\" memory=\"%.2fGb\" disk=\"%.2fGb\" disk-speed=\"%s\" storage-type=\"%s\"/>",
resources.vcpu(), resources.memoryGb(), resources.diskGb(), resources.diskSpeed().name(), resources.storageType().name());
+ String runtimeProviderClass = config.runtimeProviderClass();
+ String tenantCdBundle = config.tenantCdBundle();
+
+ String handlerAndExtraComponents = useOsgiBasedTestRuntime
+ ?
+ " <component id=\"" + runtimeProviderClass + "\" bundle=\"" + tenantCdBundle + "\" />\n" +
+ "\n" +
+ " <component id=\"com.yahoo.vespa.testrunner.JunitRunner\" bundle=\"vespa-osgi-testrunner\" />\n" +
+ "\n" +
+ " <handler id=\"com.yahoo.vespa.testrunner.TestRunnerHandler\" bundle=\"vespa-osgi-testrunner\">\n" +
+ " <binding>http://*/tester/v1/*</binding>\n" +
+ " </handler>\n"
+ :
+ " <handler id=\"com.yahoo.vespa.hosted.testrunner.TestRunnerHandler\" bundle=\"vespa-testrunner-components\">\n" +
+ " <binding>http://*/tester/v1/*</binding>\n" +
+ " </handler>\n";
+
String servicesXml =
"<?xml version='1.0' encoding='UTF-8'?>\n" +
"<services xmlns:deploy='vespa' version='1.0'>\n" +
@@ -870,9 +893,7 @@ public class InternalStepRunner implements StepRunner {
" </config>\n" +
" </component>\n" +
"\n" +
- " <handler id=\"com.yahoo.vespa.hosted.testrunner.TestRunnerHandler\" bundle=\"vespa-testrunner-components\">\n" +
- " <binding>http://*/tester/v1/*</binding>\n" +
- " </handler>\n" +
+ handlerAndExtraComponents +
"\n" +
" <nodes count=\"1\" allocated-memory=\"" + jdiscMemoryPct + "%\">\n" +
" " + resourceString + "\n" +
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java
index 0e6f856b115..8fad0db4368 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java
@@ -19,6 +19,7 @@ import com.yahoo.slime.Slime;
import com.yahoo.slime.SlimeUtils;
import com.yahoo.vespa.hosted.controller.ApplicationController;
import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.integration.billing.PaymentInstrument;
import com.yahoo.vespa.hosted.controller.api.integration.billing.Invoice;
@@ -137,9 +138,9 @@ public class BillingApiHandler extends LoggingRequestHandler {
var tenantName = TenantName.from(tenant);
var slime = inspectorOrThrow(request);
var planId = PlanId.from(slime.field("plan").asString());
- var hasApplications = applicationController.asList(tenantName).size() > 0;
- var result = billingController.setPlan(tenantName, planId, hasApplications);
+ var hasDeployments = hasDeployments(tenantName);
+ var result = billingController.setPlan(tenantName, planId, hasDeployments);
if (result.isSuccess())
return new StringResponse("Plan: " + planId.value());
@@ -380,4 +381,14 @@ public class BillingApiHandler extends LoggingRequestHandler {
return LocalDate.parse(until);
}
+ private boolean hasDeployments(TenantName tenantName) {
+ return applicationController.asList(tenantName)
+ .stream()
+ .flatMap(app -> app.instances().values()
+ .stream()
+ .flatMap(instance -> instance.deployments().values().stream())
+ )
+ .count() > 0;
+ }
+
}
diff --git a/controller-server/src/main/resources/configdefinitions/controller.def b/controller-server/src/main/resources/configdefinitions/controller.def
new file mode 100644
index 00000000000..069deaf276d
--- /dev/null
+++ b/controller-server/src/main/resources/configdefinitions/controller.def
@@ -0,0 +1,7 @@
+# Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# Generic config for controller
+namespace=vespa.hosted.controller.config
+
+steprunner.testerapp.tenantCdBundle string default="cloud-tenant-cd"
+
+steprunner.testerapp.runtimeProviderClass string default="ai.vespa.hosted.cd.cloud.impl.VespaTestRuntimeProvider" \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
index 35093c22f42..c0244b9ea17 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
@@ -31,6 +31,7 @@ import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.SystemApplication;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade;
+import com.yahoo.vespa.hosted.controller.config.ControllerConfig;
import com.yahoo.vespa.hosted.controller.integration.ConfigServerMock;
import com.yahoo.vespa.hosted.controller.integration.MetricsMock;
import com.yahoo.vespa.hosted.controller.integration.SecretStoreMock;
@@ -367,7 +368,8 @@ public final class ControllerTester {
new InMemoryFlagSource(),
new MockMavenRepository(),
serviceRegistry,
- new MetricsMock(), new SecretStoreMock());
+ new MetricsMock(), new SecretStoreMock(),
+ new ControllerConfig.Builder().build());
// Calculate initial versions
controller.updateVersionStatus(VersionStatus.compute(controller));
return controller;
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java
index 07c643070a0..02640cf8486 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java
@@ -27,6 +27,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud;
import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMailer;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.SystemApplication;
+import com.yahoo.vespa.hosted.controller.config.ControllerConfig;
import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
import org.junit.Before;
import org.junit.Test;
@@ -485,12 +486,24 @@ public class InternalStepRunnerTest {
}
@Test
- public void generates_correct_services_xml_test() {
- assertFile("test_runner_services.xml-cd",
- new String(InternalStepRunner.servicesXml(
- true,
- false,
- new NodeResources(2, 12, 75, 1, NodeResources.DiskSpeed.fast, NodeResources.StorageType.local))));
+ public void generates_correct_services_xml_using_osgi_based_runtime() {
+ generates_correct_services_xml("test_runner_services.xml-cd-osgi", true);
+ }
+
+ @Test
+ public void generates_correct_services_xml_using_legacy_runtime() {
+ generates_correct_services_xml("test_runner_services.xml-cd-legacy", false);
+ }
+
+ private void generates_correct_services_xml(String filenameExpectedOutput, boolean useOsgiBasedRuntime) {
+ ControllerConfig.Steprunner.Testerapp config = new ControllerConfig.Steprunner.Testerapp.Builder().build();
+ assertFile(filenameExpectedOutput,
+ new String(InternalStepRunner.servicesXml(
+ true,
+ false,
+ useOsgiBasedRuntime,
+ new NodeResources(2, 12, 75, 1, NodeResources.DiskSpeed.fast, NodeResources.StorageType.local),
+ config)));
}
private void assertFile(String resourceName, String actualContent) {
diff --git a/controller-server/src/test/resources/test_runner_services.xml-cd b/controller-server/src/test/resources/test_runner_services.xml-cd-legacy
index 125c5004d25..125c5004d25 100644
--- a/controller-server/src/test/resources/test_runner_services.xml-cd
+++ b/controller-server/src/test/resources/test_runner_services.xml-cd-legacy
diff --git a/controller-server/src/test/resources/test_runner_services.xml-cd-osgi b/controller-server/src/test/resources/test_runner_services.xml-cd-osgi
new file mode 100644
index 00000000000..03277628156
--- /dev/null
+++ b/controller-server/src/test/resources/test_runner_services.xml-cd-osgi
@@ -0,0 +1,26 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<services xmlns:deploy='vespa' version='1.0'>
+ <container version='1.0' id='tester'>
+
+ <component id="com.yahoo.vespa.hosted.testrunner.TestRunner" bundle="vespa-testrunner-components">
+ <config name="com.yahoo.vespa.hosted.testrunner.test-runner">
+ <artifactsPath>artifacts</artifactsPath>
+ <surefireMemoryMb>5120</surefireMemoryMb>
+ <useAthenzCredentials>true</useAthenzCredentials>
+ <useTesterCertificate>false</useTesterCertificate>
+ </config>
+ </component>
+
+ <component id="ai.vespa.hosted.cd.cloud.impl.VespaTestRuntimeProvider" bundle="cloud-tenant-cd" />
+
+ <component id="com.yahoo.vespa.testrunner.JunitRunner" bundle="vespa-osgi-testrunner" />
+
+ <handler id="com.yahoo.vespa.testrunner.TestRunnerHandler" bundle="vespa-osgi-testrunner">
+ <binding>http://*/tester/v1/*</binding>
+ </handler>
+
+ <nodes count="1" allocated-memory="17%">
+ <resources vcpu="2.00" memory="12.00Gb" disk="75.00Gb" disk-speed="fast" storage-type="local"/>
+ </nodes>
+ </container>
+</services>
diff --git a/hosted-api/src/main/java/ai/vespa/hosted/api/TestDescriptor.java b/hosted-api/src/main/java/ai/vespa/hosted/api/TestDescriptor.java
index 08cd3932ae7..6074bd73a20 100644
--- a/hosted-api/src/main/java/ai/vespa/hosted/api/TestDescriptor.java
+++ b/hosted-api/src/main/java/ai/vespa/hosted/api/TestDescriptor.java
@@ -26,6 +26,7 @@ public class TestDescriptor {
private static final String JSON_FIELD_CONFIGURED_TESTS = "configuredTests";
private static final String JSON_FIELD_SYSTEM_TESTS = "systemTests";
private static final String JSON_FIELD_STAGING_TESTS = "stagingTests";
+ private static final String JSON_FIELD_STAGING_SETUP_TESTS = "stagingSetupTests";
private static final String JSON_FIELD_PRODUCTION_TESTS = "productionTests";
private final Map<TestCategory, List<String>> configuredTestClasses;
@@ -43,20 +44,22 @@ public class TestDescriptor {
var testRoot = root.field(JSON_FIELD_CONFIGURED_TESTS);
var systemTests = getJsonArray(testRoot, JSON_FIELD_SYSTEM_TESTS);
var stagingTests = getJsonArray(testRoot, JSON_FIELD_STAGING_TESTS);
+ var stagingSetupTests = getJsonArray(testRoot, JSON_FIELD_STAGING_SETUP_TESTS);
var productionTests = getJsonArray(testRoot, JSON_FIELD_PRODUCTION_TESTS);
- return new TestDescriptor(version, toMap(systemTests, stagingTests, productionTests));
+ return new TestDescriptor(version, toMap(systemTests, stagingTests, stagingSetupTests, productionTests));
}
public static TestDescriptor from(
- String version, List<String> systemTests, List<String> stagingTests, List<String> productionTests) {
- return new TestDescriptor(version, toMap(systemTests, stagingTests, productionTests));
+ String version, List<String> systemTests, List<String> stagingTests, List<String> stagingSetupTests, List<String> productionTests) {
+ return new TestDescriptor(version, toMap(systemTests, stagingTests, stagingSetupTests, productionTests));
}
private static Map<TestCategory, List<String>> toMap(
- List<String> systemTests, List<String> stagingTests, List<String> productionTests) {
+ List<String> systemTests, List<String> stagingTests, List<String> stagingSetupTests, List<String> productionTests) {
return Map.of(
TestCategory.systemtest, systemTests,
TestCategory.stagingtest, stagingTests,
+ TestCategory.stagingsetuptest, stagingSetupTests,
TestCategory.productiontest, productionTests
);
}
@@ -81,6 +84,7 @@ public class TestDescriptor {
addJsonArrayForTests(tests, JSON_FIELD_SYSTEM_TESTS, TestCategory.systemtest);
addJsonArrayForTests(tests, JSON_FIELD_STAGING_TESTS, TestCategory.stagingtest);
addJsonArrayForTests(tests, JSON_FIELD_PRODUCTION_TESTS, TestCategory.productiontest);
+ addJsonArrayForTests(tests, JSON_FIELD_STAGING_SETUP_TESTS, TestCategory.stagingsetuptest);
ByteArrayOutputStream out = new ByteArrayOutputStream();
uncheck(() -> new JsonFormat(/*compact*/false).encode(out, slime));
return out.toString();
@@ -100,5 +104,5 @@ public class TestDescriptor {
'}';
}
- public enum TestCategory {systemtest, stagingtest, productiontest}
+ public enum TestCategory {systemtest, stagingsetuptest, stagingtest, productiontest}
}
diff --git a/hosted-api/src/test/java/ai/vespa/hosted/api/TestDescriptorTest.java b/hosted-api/src/test/java/ai/vespa/hosted/api/TestDescriptorTest.java
index 7e59af9ced8..d78526c500b 100644
--- a/hosted-api/src/test/java/ai/vespa/hosted/api/TestDescriptorTest.java
+++ b/hosted-api/src/test/java/ai/vespa/hosted/api/TestDescriptorTest.java
@@ -33,6 +33,9 @@ public class TestDescriptorTest {
var stagingTests = testClassDescriptor.getConfiguredTests(TestDescriptor.TestCategory.stagingtest);
Assertions.assertIterableEquals(Collections.emptyList(), stagingTests);
+ var stagingSetupTests = testClassDescriptor.getConfiguredTests(TestDescriptor.TestCategory.stagingtest);
+ Assertions.assertIterableEquals(Collections.emptyList(), stagingSetupTests);
+
var productionTests = testClassDescriptor.getConfiguredTests(TestDescriptor.TestCategory.productiontest);
Assertions.assertIterableEquals(Collections.emptyList(), productionTests);
}
@@ -40,7 +43,8 @@ public class TestDescriptorTest {
@Test
public void parsesDescriptorFile() {
String testDescriptor = "{\n" +
- " \"version\": \"1.0\",\n" +
+ " \"" +
+ "version\": \"1.0\",\n" +
" \"configuredTests\": {\n" +
" \"systemTests\": [\n" +
" \"ai.vespa.test.SystemTest1\",\n" +
@@ -50,6 +54,10 @@ public class TestDescriptorTest {
" \"ai.vespa.test.StagingTest1\",\n" +
" \"ai.vespa.test.StagingTest2\"\n" +
" ],\n" +
+ " \"stagingSetupTests\": [\n" +
+ " \"ai.vespa.test.StagingSetupTest1\",\n" +
+ " \"ai.vespa.test.StagingSetupTest2\"\n" +
+ " ],\n" +
" \"productionTests\": [\n" +
" \"ai.vespa.test.ProductionTest1\",\n" +
" \"ai.vespa.test.ProductionTest2\"\n" +
@@ -65,8 +73,13 @@ public class TestDescriptorTest {
var stagingTests = testClassDescriptor.getConfiguredTests(TestDescriptor.TestCategory.stagingtest);
Assertions.assertIterableEquals(List.of("ai.vespa.test.StagingTest1", "ai.vespa.test.StagingTest2"), stagingTests);
+ var stagingSetupTests = testClassDescriptor.getConfiguredTests(TestDescriptor.TestCategory.stagingsetuptest);
+ Assertions.assertIterableEquals(List.of("ai.vespa.test.StagingSetupTest1", "ai.vespa.test.StagingSetupTest2"), stagingSetupTests);
+
var productionTests = testClassDescriptor.getConfiguredTests(TestDescriptor.TestCategory.productiontest);
Assertions.assertIterableEquals(List.of("ai.vespa.test.ProductionTest1", "ai.vespa.test.ProductionTest2"), productionTests);
+
+ JsonTestHelper.assertJsonEquals(testClassDescriptor.toJson(), testDescriptor);
}
@Test
diff --git a/searchcore/src/vespa/searchcore/proton/attribute/attributesconfigscout.cpp b/searchcore/src/vespa/searchcore/proton/attribute/attributesconfigscout.cpp
index 986571d07e1..7f6d47f4ae2 100644
--- a/searchcore/src/vespa/searchcore/proton/attribute/attributesconfigscout.cpp
+++ b/searchcore/src/vespa/searchcore/proton/attribute/attributesconfigscout.cpp
@@ -30,6 +30,7 @@ AttributesConfigScout::adjust(AttributesConfig::Attribute &attr,
attr.huge = liveAttr.huge;
// Note: Predicate attributes only handle changes for the dense-posting-list-threshold config.
attr.densepostinglistthreshold = liveAttr.densepostinglistthreshold;
+ attr.distancemetric = liveAttr.distancemetric;
attr.index = liveAttr.index;
}
diff --git a/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/GenerateTestDescriptorMojo.java b/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/GenerateTestDescriptorMojo.java
index 8309b7a8124..259ae2602c4 100644
--- a/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/GenerateTestDescriptorMojo.java
+++ b/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/GenerateTestDescriptorMojo.java
@@ -33,6 +33,7 @@ public class GenerateTestDescriptorMojo extends AbstractMojo {
TestDescriptor.CURRENT_VERSION,
analyzer.systemTests(),
analyzer.stagingTests(),
+ analyzer.stagingSetupTests(),
analyzer.productionTests());
writeDescriptorFile(descriptor);
}
diff --git a/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/TestAnnotationAnalyzer.java b/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/TestAnnotationAnalyzer.java
index c45ef21bc31..e8b29b2b0f7 100644
--- a/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/TestAnnotationAnalyzer.java
+++ b/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/TestAnnotationAnalyzer.java
@@ -3,6 +3,7 @@ package ai.vespa.hosted.plugin;
import ai.vespa.hosted.cd.ProductionTest;
+import ai.vespa.hosted.cd.StagingSetup;
import ai.vespa.hosted.cd.StagingTest;
import ai.vespa.hosted.cd.SystemTest;
import org.objectweb.asm.AnnotationVisitor;
@@ -28,10 +29,12 @@ class TestAnnotationAnalyzer {
private final List<String> systemTests = new ArrayList<>();
private final List<String> stagingTests = new ArrayList<>();
+ private final List<String> stagingSetupTests = new ArrayList<>();
private final List<String> productionTests = new ArrayList<>();
List<String> systemTests() { return systemTests; }
List<String> stagingTests() { return stagingTests; }
+ List<String> stagingSetupTests() { return stagingSetupTests; }
List<String> productionTests() { return productionTests; }
void analyzeClass(Path classFile) {
@@ -65,6 +68,8 @@ class TestAnnotationAnalyzer {
productionTests.add(className);
} else if (StagingTest.class.getName().equals(annotationClassName)) {
stagingTests.add(className);
+ } else if (StagingSetup.class.getName().equals(annotationClassName)) {
+ stagingTests.add(className);
} else if (SystemTest.class.getName().equals(annotationClassName)) {
systemTests.add(className);
}
diff --git a/vespa-osgi-testrunner/pom.xml b/vespa-osgi-testrunner/pom.xml
index 62ea578f14f..db0dba89b8a 100644
--- a/vespa-osgi-testrunner/pom.xml
+++ b/vespa-osgi-testrunner/pom.xml
@@ -22,7 +22,6 @@
<scope>provided</scope>
</dependency>
- <!-- Verify that we need all junit deps -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
@@ -45,18 +44,7 @@
</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>
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
deleted file mode 100644
index cb7d5b8df6b..00000000000
--- a/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/JunitHandler.java
+++ /dev/null
@@ -1,71 +0,0 @@
-// 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 ai.vespa.hosted.api.TestDescriptor;
-import ai.vespa.hosted.cd.internal.TestRuntimeProvider;
-import com.google.inject.Inject;
-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.io.IOException;
-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;
- private final TestRuntimeProvider testRuntimeProvider;
-
- @Inject
- public JunitHandler(Executor executor, AccessLog accessLog, JunitRunner junitRunner, TestRuntimeProvider testRuntimeProvider) {
- super(executor, accessLog);
- this.junitRunner = junitRunner;
- this.testRuntimeProvider = testRuntimeProvider;
- }
-
- @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);
-
- try {
- testRuntimeProvider.initialize(httpRequest.getData().readAllBytes());
- } catch (IOException e) {
- return new ErrorResponse(500, "testruntime-initialization", "Exception reading test config");
- }
-
- 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
index 69134f86be0..3fc85365084 100644
--- 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
@@ -2,15 +2,12 @@
package com.yahoo.vespa.testrunner;
import ai.vespa.hosted.api.TestDescriptor;
+import ai.vespa.hosted.cd.internal.TestRuntimeProvider;
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 com.yahoo.vespa.testrunner.legacy.LegacyTestRunner;
import org.junit.jupiter.engine.JupiterTestEngine;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.Launcher;
@@ -20,16 +17,19 @@ 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.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -41,10 +41,12 @@ public class JunitRunner extends AbstractComponent {
private static final Logger logger = Logger.getLogger(JunitRunner.class.getName());
private final BundleContext bundleContext;
+ private final TestRuntimeProvider testRuntimeProvider;
+ private Future<TestReport> execution;
@Inject
- public JunitRunner(OsgiFramework osgiFramework) {
- // TODO mortent: Find a way to workaround this hack
+ public JunitRunner(OsgiFramework osgiFramework, TestRuntimeProvider testRuntimeProvider) {
+ this.testRuntimeProvider = testRuntimeProvider;
var tmp = osgiFramework.bundleContext();
try {
var field = tmp.getClass().getDeclaredField("wrapped");
@@ -55,27 +57,54 @@ public class JunitRunner extends AbstractComponent {
}
}
- public Bundle findTestBundle(String bundleNameSuffix) {
+ public void executeTests(TestDescriptor.TestCategory category, byte[] testConfig) {
+ if (execution != null) {
+ throw new RuntimeException("Test execution already in progress");
+ }
+ testRuntimeProvider.initialize(testConfig);
+ Optional<Bundle> testBundle = findTestBundle();
+ if (testBundle.isEmpty()) {
+ throw new RuntimeException("No test bundle available");
+ }
+
+ Optional<TestDescriptor> testDescriptor = loadTestDescriptor(testBundle.get());
+ if (testDescriptor.isEmpty()) {
+ throw new RuntimeException("Could not find test descriptor");
+ }
+ List<Class<?>> testClasses = loadClasses(testBundle.get(), testDescriptor.get(), category);
+
+ execution = CompletableFuture.supplyAsync(() -> launchJunit(testClasses));
+ }
+
+ public boolean isSupported() {
+ return findTestBundle().isPresent();
+ }
+
+ private Optional<Bundle> findTestBundle() {
return Stream.of(bundleContext.getBundles())
- .filter(bundle -> bundle.getSymbolicName().endsWith(bundleNameSuffix))
- .findAny()
- .orElseThrow(() -> new RuntimeException("No bundle on classpath with name ending on " + bundleNameSuffix));
+ .filter(this::isTestBundle)
+ .findAny();
}
- public TestDescriptor loadTestDescriptor(Bundle bundle) {
+ private boolean isTestBundle(Bundle bundle) {
+ var testBundleHeader = bundle.getHeaders().get("X-JDisc-Test-Bundle-Version");
+ return testBundleHeader != null && !testBundleHeader.isBlank();
+ }
+
+ private Optional<TestDescriptor> loadTestDescriptor(Bundle bundle) {
URL resource = bundle.getEntry(TestDescriptor.DEFAULT_FILENAME);
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;
+ return Optional.of(testDescriptor);
} catch (IOException e) {
- throw new RuntimeException("Could not load " + TestDescriptor.DEFAULT_FILENAME + " [" + e.getMessage() + "]");
+ return Optional.empty();
}
}
- public List<Class<?>> loadClasses(Bundle bundle, TestDescriptor testDescriptor, TestDescriptor.TestCategory testCategory) {
+ private 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());
@@ -94,7 +123,7 @@ public class JunitRunner extends AbstractComponent {
}
}
- public String executeTests(List<Class<?>> testClasses) {
+ private TestReport launchJunit(List<Class<?>> testClasses) {
LauncherDiscoveryRequest discoveryRequest = LauncherDiscoveryRequestBuilder.request()
.selectors(
testClasses.stream().map(DiscoverySelectors::selectClass).collect(Collectors.toList())
@@ -116,36 +145,8 @@ public class JunitRunner extends AbstractComponent {
// 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()));
+ return new TestReport(report, logLines);
}
private void log(List<String> logs, String message, Throwable t) {
@@ -162,4 +163,33 @@ public class JunitRunner extends AbstractComponent {
public void deconstruct() {
super.deconstruct();
}
+
+ public LegacyTestRunner.Status getStatus() {
+ if (execution == null) return LegacyTestRunner.Status.NOT_STARTED;
+ if (!execution.isDone()) return LegacyTestRunner.Status.RUNNING;
+ try {
+ TestReport report = execution.get();
+ if (report.isSuccess()) {
+ return LegacyTestRunner.Status.SUCCESS;
+ } else {
+ return LegacyTestRunner.Status.FAILURE;
+ }
+ } catch (InterruptedException|ExecutionException e) {
+ logger.log(Level.WARNING, "Error while getting test report", e);
+ return LegacyTestRunner.Status.ERROR;
+ }
+ }
+
+ public String getReportAsJson() {
+ if (execution.isDone()) {
+ try {
+ return execution.get().toJson();
+ } catch (Exception e) {
+ logger.log(Level.WARNING, "Error getting test report", e);
+ return "";
+ }
+ } else {
+ return "";
+ }
+ }
}
diff --git a/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/TestReport.java b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/TestReport.java
new file mode 100644
index 00000000000..2e45ba96486
--- /dev/null
+++ b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/TestReport.java
@@ -0,0 +1,55 @@
+// 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.exception.ExceptionUtils;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Slime;
+import com.yahoo.slime.SlimeUtils;
+import com.yahoo.yolean.Exceptions;
+import org.junit.platform.launcher.listeners.TestExecutionSummary;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * @author mortent
+ */
+public class TestReport {
+ private final TestExecutionSummary junitReport;
+ private final List<String> logLines;
+
+ public TestReport(TestExecutionSummary junitReport, List<String> logLines) {
+ this.junitReport = junitReport;
+ this.logLines = List.copyOf(logLines);
+ }
+
+ 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()));
+ }
+
+ public String toJson() {
+ var slime = new Slime();
+ var root = slime.setObject();
+ var summary = root.setObject("summary");
+ summary.setLong("Total tests", junitReport.getTestsFoundCount());
+ summary.setLong("Test success", junitReport.getTestsSucceededCount());
+ summary.setLong("Test failed", junitReport.getTestsFailedCount());
+ summary.setLong("Test ignored", junitReport.getTestsSkippedCount());
+ summary.setLong("Test aborted", junitReport.getTestsAbortedCount());
+ summary.setLong("Test started", junitReport.getTestsStartedCount());
+ var failures = summary.setArray("failures");
+ junitReport.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));
+ }
+
+ public boolean isSuccess() {
+ return (junitReport.getTestsFailedCount() + junitReport.getTestsAbortedCount()) == 0;
+ }
+}
diff --git a/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/TestRunnerHandler.java b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/TestRunnerHandler.java
new file mode 100644
index 00000000000..cb337a0c176
--- /dev/null
+++ b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/TestRunnerHandler.java
@@ -0,0 +1,211 @@
+// 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 ai.vespa.hosted.api.TestDescriptor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.inject.Inject;
+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.slime.Cursor;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.testrunner.legacy.LegacyTestRunner;
+import com.yahoo.vespa.testrunner.legacy.TestProfile;
+import com.yahoo.yolean.Exceptions;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+
+import static com.yahoo.jdisc.Response.Status;
+
+/**
+ * @author valerijf
+ * @author jvenstad
+ * @author mortent
+ */
+public class TestRunnerHandler extends LoggingRequestHandler {
+
+ private static final String CONTENT_TYPE_APPLICATION_JSON = "application/json";
+
+ private final JunitRunner junitRunner;
+ private final LegacyTestRunner testRunner;
+ private final boolean useOsgiMode;
+
+ @Inject
+ public TestRunnerHandler(Executor executor, AccessLog accessLog, JunitRunner junitRunner, LegacyTestRunner testRunner) {
+ super(executor, accessLog);
+ this.junitRunner = junitRunner;
+ this.testRunner = testRunner;
+ this.useOsgiMode = junitRunner.isSupported();
+ }
+
+ @Override
+ public HttpResponse handle(HttpRequest request) {
+ try {
+ switch (request.getMethod()) {
+ case GET: return handleGET(request);
+ case POST: return handlePOST(request);
+
+ default: return new Response(Status.METHOD_NOT_ALLOWED, "Method '" + request.getMethod() + "' is not supported");
+ }
+ } catch (IllegalArgumentException e) {
+ return new Response(Status.BAD_REQUEST, Exceptions.toMessageString(e));
+ } catch (Exception e) {
+ log.log(Level.WARNING, "Unexpected error handling '" + request.getUri() + "'", e);
+ return new Response(Status.INTERNAL_SERVER_ERROR, Exceptions.toMessageString(e));
+ }
+ }
+
+ private HttpResponse handleGET(HttpRequest request) {
+ String path = request.getUri().getPath();
+ if (path.equals("/tester/v1/log")) {
+ if (useOsgiMode) {
+ // TODO (mortent): Handle case where log is returned multiple times
+ String report = junitRunner.getReportAsJson();
+ List<LogRecord> logRecords = new ArrayList<>();
+ if (!report.isBlank()) {
+ logRecords.add(new LogRecord(Level.INFO, report));
+ }
+ return new SlimeJsonResponse(logToSlime(logRecords));
+ } else {
+ return new SlimeJsonResponse(logToSlime(testRunner.getLog(request.hasProperty("after")
+ ? Long.parseLong(request.getProperty("after"))
+ : -1)));
+ }
+ } else if (path.equals("/tester/v1/status")) {
+ if (useOsgiMode) {
+ log.info("Responding with status " + junitRunner.getStatus());
+ return new Response(junitRunner.getStatus().name());
+ } else {
+ log.info("Responding with status " + testRunner.getStatus());
+ return new Response(testRunner.getStatus().name());
+ }
+ }
+ return new Response(Status.NOT_FOUND, "Not found: " + request.getUri().getPath());
+ }
+
+ private HttpResponse handlePOST(HttpRequest request) throws IOException {
+ final String path = request.getUri().getPath();
+ if (path.startsWith("/tester/v1/run/")) {
+ String type = lastElement(path);
+ TestProfile testProfile = TestProfile.valueOf(type.toUpperCase() + "_TEST");
+ byte[] config = request.getData().readAllBytes();
+ if (useOsgiMode) {
+ junitRunner.executeTests(categoryFromProfile(testProfile), config);
+ log.info("Started tests of type " + type + " and status is " + junitRunner.getStatus());
+ return new Response("Successfully started " + type + " tests");
+ } else {
+ testRunner.test(testProfile, config);
+ log.info("Started tests of type " + type + " and status is " + testRunner.getStatus());
+ return new Response("Successfully started " + type + " tests");
+ }
+ }
+ return new Response(Status.NOT_FOUND, "Not found: " + request.getUri().getPath());
+ }
+
+ TestDescriptor.TestCategory categoryFromProfile(TestProfile testProfile) {
+ switch(testProfile) {
+ case SYSTEM_TEST: return TestDescriptor.TestCategory.systemtest;
+ case STAGING_SETUP_TEST: return TestDescriptor.TestCategory.stagingsetuptest;
+ case STAGING_TEST: return TestDescriptor.TestCategory.stagingtest;
+ case PRODUCTION_TEST: return TestDescriptor.TestCategory.productiontest;
+ default: throw new RuntimeException("Unknown test profile: " + testProfile.name());
+ }
+ }
+
+ private static String lastElement(String path) {
+ if (path.endsWith("/"))
+ path = path.substring(0, path.length() - 1);
+ int lastSlash = path.lastIndexOf("/");
+ if (lastSlash < 0) return path;
+ return path.substring(lastSlash + 1);
+ }
+
+ static Slime logToSlime(Collection<LogRecord> log) {
+ Slime slime = new Slime();
+ Cursor root = slime.setObject();
+ Cursor recordArray = root.setArray("logRecords");
+ logArrayToSlime(recordArray, log);
+ return slime;
+ }
+
+ static void logArrayToSlime(Cursor recordArray, Collection<LogRecord> log) {
+ log.forEach(record -> {
+ Cursor recordObject = recordArray.addObject();
+ recordObject.setLong("id", record.getSequenceNumber());
+ recordObject.setLong("at", record.getMillis());
+ recordObject.setString("type", typeOf(record.getLevel()));
+ String message = record.getMessage();
+ if (record.getThrown() != null) {
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ record.getThrown().printStackTrace(new PrintStream(buffer));
+ message += "\n" + buffer;
+ }
+ recordObject.setString("message", message);
+ });
+ }
+
+ public static String typeOf(Level level) {
+ return level.getName().equals("html") ? "html"
+ : level.intValue() < Level.INFO.intValue() ? "debug"
+ : level.intValue() < Level.WARNING.intValue() ? "info"
+ : level.intValue() < Level.SEVERE.intValue() ? "warning"
+ : "error";
+ }
+
+ private static class SlimeJsonResponse extends HttpResponse {
+ private final Slime slime;
+
+ private SlimeJsonResponse(Slime slime) {
+ super(200);
+ this.slime = slime;
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ new JsonFormat(true).encode(outputStream, slime);
+ }
+
+ @Override
+ public String getContentType() {
+ return CONTENT_TYPE_APPLICATION_JSON;
+ }
+ }
+
+ private static class Response extends HttpResponse {
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+ private final String message;
+
+ private Response(String response) {
+ this(200, response);
+ }
+
+ private Response(int statusCode, String message) {
+ super(statusCode);
+ this.message = message;
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ ObjectNode objectNode = objectMapper.createObjectNode();
+ objectNode.put("message", message);
+ objectMapper.writeValue(outputStream, objectNode);
+ }
+
+ @Override
+ public String getContentType() {
+ return CONTENT_TYPE_APPLICATION_JSON;
+ }
+ }
+}
diff --git a/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/legacy/LegacyTestRunner.java b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/legacy/LegacyTestRunner.java
new file mode 100644
index 00000000000..9f1a68218f0
--- /dev/null
+++ b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/legacy/LegacyTestRunner.java
@@ -0,0 +1,22 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.testrunner.legacy;
+
+import java.util.Collection;
+import java.util.logging.LogRecord;
+
+/**
+ * @author mortent
+ */
+public interface LegacyTestRunner {
+
+ Collection<LogRecord> getLog(long after);
+
+ Status getStatus();
+
+ void test(TestProfile testProfile, byte[] config);
+
+ // TODO (mortent) : This seems to be duplicated in TesterCloud.Status and expects to have the same values
+ enum Status {
+ NOT_STARTED, RUNNING, FAILURE, ERROR, SUCCESS
+ }
+}
diff --git a/vespa-testrunner-components/src/main/java/com/yahoo/vespa/hosted/testrunner/TestProfile.java b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/legacy/TestProfile.java
index d568b549f9b..59576209043 100644
--- a/vespa-testrunner-components/src/main/java/com/yahoo/vespa/hosted/testrunner/TestProfile.java
+++ b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/legacy/TestProfile.java
@@ -1,11 +1,11 @@
-// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.testrunner;
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.testrunner.legacy;
/**
* @author valerijf
* @author jvenstad
*/
-enum TestProfile {
+public enum TestProfile {
SYSTEM_TEST("system, com.yahoo.vespa.tenant.systemtest.base.SystemTest", true),
STAGING_SETUP_TEST("staging-setup", false),
@@ -20,12 +20,11 @@ enum TestProfile {
this.failIfNoTests = failIfNoTests;
}
- String group() {
+ public String group() {
return group;
}
- boolean failIfNoTests() {
+ public boolean failIfNoTests() {
return failIfNoTests;
}
-
}
diff --git a/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/legacy/package-info.java b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/legacy/package-info.java
new file mode 100644
index 00000000000..49f6cef0c22
--- /dev/null
+++ b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/legacy/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.legacy;
+
+import com.yahoo.osgi.annotation.ExportPackage; \ No newline at end of file
diff --git a/vespa-testrunner-components/pom.xml b/vespa-testrunner-components/pom.xml
index 31568d01fb5..e780da726a1 100644
--- a/vespa-testrunner-components/pom.xml
+++ b/vespa-testrunner-components/pom.xml
@@ -24,6 +24,24 @@
</dependency>
<dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>vespa-osgi-testrunner</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ <!-- junit must be excluded to keep maven-surefire-plugin to be confused -->
+ <exclusions>
+ <exclusion>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-engine</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.junit.platform</groupId>
+ <artifactId>junit-platform-launcher</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <dependency>
<groupId>org.fusesource.jansi</groupId>
<artifactId>jansi</artifactId>
<version>1.11</version>
diff --git a/vespa-testrunner-components/src/main/java/com/yahoo/vespa/hosted/testrunner/PomXmlGenerator.java b/vespa-testrunner-components/src/main/java/com/yahoo/vespa/hosted/testrunner/PomXmlGenerator.java
index e6f402ba563..dd424de5471 100644
--- a/vespa-testrunner-components/src/main/java/com/yahoo/vespa/hosted/testrunner/PomXmlGenerator.java
+++ b/vespa-testrunner-components/src/main/java/com/yahoo/vespa/hosted/testrunner/PomXmlGenerator.java
@@ -2,6 +2,7 @@
package com.yahoo.vespa.hosted.testrunner;
import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.testrunner.legacy.TestProfile;
import java.nio.file.Path;
import java.util.List;
diff --git a/vespa-testrunner-components/src/main/java/com/yahoo/vespa/hosted/testrunner/TestRunner.java b/vespa-testrunner-components/src/main/java/com/yahoo/vespa/hosted/testrunner/TestRunner.java
index cdf320a6304..4308b0bba4c 100644
--- a/vespa-testrunner-components/src/main/java/com/yahoo/vespa/hosted/testrunner/TestRunner.java
+++ b/vespa-testrunner-components/src/main/java/com/yahoo/vespa/hosted/testrunner/TestRunner.java
@@ -3,6 +3,8 @@ package com.yahoo.vespa.hosted.testrunner;
import com.google.inject.Inject;
import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.testrunner.legacy.LegacyTestRunner;
+import com.yahoo.vespa.testrunner.legacy.TestProfile;
import org.fusesource.jansi.AnsiOutputStream;
import org.fusesource.jansi.HtmlAnsiOutputStream;
@@ -30,14 +32,13 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.logging.Level.INFO;
import static java.util.logging.Level.SEVERE;
/**
* @author valerijf
* @author jvenstad
*/
-public class TestRunner {
+public class TestRunner implements LegacyTestRunner {
private static final Logger logger = Logger.getLogger(TestRunner.class.getName());
private static final Level HTML = new Level("html", 1) { };
@@ -203,9 +204,4 @@ public class TestRunner {
}
}
-
- public enum Status {
- NOT_STARTED, RUNNING, FAILURE, ERROR, SUCCESS
- }
-
}
diff --git a/vespa-testrunner-components/src/main/java/com/yahoo/vespa/hosted/testrunner/TestRunnerHandler.java b/vespa-testrunner-components/src/main/java/com/yahoo/vespa/hosted/testrunner/TestRunnerHandler.java
index e92dbcede5a..8f9966a898f 100644
--- a/vespa-testrunner-components/src/main/java/com/yahoo/vespa/hosted/testrunner/TestRunnerHandler.java
+++ b/vespa-testrunner-components/src/main/java/com/yahoo/vespa/hosted/testrunner/TestRunnerHandler.java
@@ -9,10 +9,10 @@ import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.LoggingRequestHandler;
import com.yahoo.container.logging.AccessLog;
import com.yahoo.io.IOUtils;
-import java.util.logging.Level;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.JsonFormat;
import com.yahoo.slime.Slime;
+import com.yahoo.vespa.testrunner.legacy.TestProfile;
import com.yahoo.yolean.Exceptions;
import java.io.ByteArrayOutputStream;
diff --git a/vespa-testrunner-components/src/test/java/com/yahoo/vespa/hosted/testrunner/PomXmlGeneratorTest.java b/vespa-testrunner-components/src/test/java/com/yahoo/vespa/hosted/testrunner/PomXmlGeneratorTest.java
index c7799bff116..823dca4a7a2 100644
--- a/vespa-testrunner-components/src/test/java/com/yahoo/vespa/hosted/testrunner/PomXmlGeneratorTest.java
+++ b/vespa-testrunner-components/src/test/java/com/yahoo/vespa/hosted/testrunner/PomXmlGeneratorTest.java
@@ -1,6 +1,7 @@
// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.testrunner;
+import com.yahoo.vespa.testrunner.legacy.TestProfile;
import org.junit.Test;
import java.io.IOException;
diff --git a/vespa-testrunner-components/src/test/java/com/yahoo/vespa/hosted/testrunner/TestRunnerTest.java b/vespa-testrunner-components/src/test/java/com/yahoo/vespa/hosted/testrunner/TestRunnerTest.java
index 22fd7fddf31..b2c7a77240b 100644
--- a/vespa-testrunner-components/src/test/java/com/yahoo/vespa/hosted/testrunner/TestRunnerTest.java
+++ b/vespa-testrunner-components/src/test/java/com/yahoo/vespa/hosted/testrunner/TestRunnerTest.java
@@ -1,6 +1,7 @@
// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.testrunner;
+import com.yahoo.vespa.testrunner.legacy.TestProfile;
import org.fusesource.jansi.Ansi;
import org.junit.Before;
import org.junit.Rule;
@@ -16,7 +17,6 @@ import java.util.logging.LogRecord;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
/**
* Unit tests relying on a UNIX shell >_<