diff options
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 >_< |