summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMorten Tokle <mortent@verizonmedia.com>2020-06-23 14:17:51 +0200
committerGitHub <noreply@github.com>2020-06-23 14:17:51 +0200
commitf1b046dc29c91016d8f0ba65dbb18b49795d1bc0 (patch)
tree91f3465439fe5b942c71eb58517fe026a82c6f3f
parentc0fa17a707e01e336f9d7a2045e9ca5bb86822e9 (diff)
parent62df2892e217b01b6391f4d7239c33363a7e59c5 (diff)
Merge pull request #13663 from vespa-engine/bjorncs/vespa-maven-plugin
Bjorncs/vespa maven plugin
-rw-r--r--hosted-api/pom.xml12
-rw-r--r--hosted-api/src/main/java/ai/vespa/hosted/api/TestDescriptor.java56
-rw-r--r--hosted-api/src/test/java/ai/vespa/hosted/api/TestDescriptorTest.java17
-rw-r--r--vespa-maven-plugin/pom.xml15
-rw-r--r--vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/GenerateTestDescriptorMojo.java61
-rw-r--r--vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/TestAnnotationAnalyzer.java74
6 files changed, 226 insertions, 9 deletions
diff --git a/hosted-api/pom.xml b/hosted-api/pom.xml
index 2a42f890ba4..b066cb158e0 100644
--- a/hosted-api/pom.xml
+++ b/hosted-api/pom.xml
@@ -33,6 +33,12 @@
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>yolean</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
@@ -44,6 +50,12 @@
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>testutil</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
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 37858148ef0..08cd3932ae7 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
@@ -3,18 +3,30 @@ package ai.vespa.hosted.api;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Inspector;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
import com.yahoo.slime.SlimeStream;
import com.yahoo.slime.SlimeUtils;
+import java.io.ByteArrayOutputStream;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
+import static com.yahoo.yolean.Exceptions.uncheck;
+
/**
* @author mortent
*/
public class TestDescriptor {
public static final String DEFAULT_FILENAME = "META-INF/ai.vespa/testDescriptor.json";
+ public static final String CURRENT_VERSION = "1.0";
+
+ private static final String JSON_FIELD_VERSION = "version";
+ 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_PRODUCTION_TESTS = "productionTests";
private final Map<TestCategory, List<String>> configuredTestClasses;
private final String version;
@@ -27,16 +39,26 @@ public class TestDescriptor {
public static TestDescriptor fromJsonString(String testDescriptor) {
var slime = SlimeUtils.jsonToSlime(testDescriptor);
var root = slime.get();
- var version = root.field("version").asString();
- var testRoot = root.field("configuredTests");
- var systemTests = getJsonArray(testRoot, "systemTests");
- var stagingTests = getJsonArray(testRoot, "stagingTests");
- var productionTests = getJsonArray(testRoot, "productionTests");
- return new TestDescriptor(version, Map.of(
+ var version = root.field(JSON_FIELD_VERSION).asString();
+ 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 productionTests = getJsonArray(testRoot, JSON_FIELD_PRODUCTION_TESTS);
+ return new TestDescriptor(version, toMap(systemTests, stagingTests, productionTests));
+ }
+
+ public static TestDescriptor from(
+ String version, List<String> systemTests, List<String> stagingTests, List<String> productionTests) {
+ return new TestDescriptor(version, toMap(systemTests, stagingTests, productionTests));
+ }
+
+ private static Map<TestCategory, List<String>> toMap(
+ List<String> systemTests, List<String> stagingTests, List<String> productionTests) {
+ return Map.of(
TestCategory.systemtest, systemTests,
TestCategory.stagingtest, stagingTests,
TestCategory.productiontest, productionTests
- ));
+ );
}
private static List<String> getJsonArray(Cursor cursor, String field) {
@@ -51,6 +73,26 @@ public class TestDescriptor {
return List.copyOf(configuredTestClasses.get(category));
}
+ public String toJson() {
+ Slime slime = new Slime();
+ Cursor root = slime.setObject();
+ root.setString(JSON_FIELD_VERSION, this.version);
+ Cursor tests = root.setObject(JSON_FIELD_CONFIGURED_TESTS);
+ addJsonArrayForTests(tests, JSON_FIELD_SYSTEM_TESTS, TestCategory.systemtest);
+ addJsonArrayForTests(tests, JSON_FIELD_STAGING_TESTS, TestCategory.stagingtest);
+ addJsonArrayForTests(tests, JSON_FIELD_PRODUCTION_TESTS, TestCategory.productiontest);
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ uncheck(() -> new JsonFormat(/*compact*/false).encode(out, slime));
+ return out.toString();
+ }
+
+ private void addJsonArrayForTests(Cursor testsRoot, String fieldName, TestCategory category) {
+ List<String> tests = configuredTestClasses.get(category);
+ if (tests.isEmpty()) return;
+ Cursor cursor = testsRoot.setArray(fieldName);
+ tests.forEach(cursor::addString);
+ }
+
@Override
public String toString() {
return "TestClassDescriptor{" +
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 2676d9d79da..7e59af9ced8 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
@@ -1,6 +1,7 @@
// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package ai.vespa.hosted.api;
+import com.yahoo.test.json.JsonTestHelper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -67,4 +68,20 @@ public class TestDescriptorTest {
var productionTests = testClassDescriptor.getConfiguredTests(TestDescriptor.TestCategory.productiontest);
Assertions.assertIterableEquals(List.of("ai.vespa.test.ProductionTest1", "ai.vespa.test.ProductionTest2"), productionTests);
}
+
+ @Test
+ public void generatesCorrectJson() {
+ String json = "{\n" +
+ " \"version\": \"1.0\",\n" +
+ " \"configuredTests\": {\n" +
+ " \"systemTests\": [\n" +
+ " \"ai.vespa.test.SystemTest1\",\n" +
+ " \"ai.vespa.test.SystemTest2\"\n" +
+ " ]\n" +
+ " " +
+ " }\n" +
+ "}\n";
+ var descriptor = TestDescriptor.fromJsonString(json);
+ JsonTestHelper.assertJsonEquals(json, descriptor.toJson());
+ }
}
diff --git a/vespa-maven-plugin/pom.xml b/vespa-maven-plugin/pom.xml
index 9f1d6f5ff6b..0910f38d5e5 100644
--- a/vespa-maven-plugin/pom.xml
+++ b/vespa-maven-plugin/pom.xml
@@ -44,10 +44,21 @@
<artifactId>config-application-package</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>tenant-cd-api</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>yolean</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
<dependency>
- <groupId>commons-cli</groupId>
- <artifactId>commons-cli</artifactId>
+ <groupId>org.ow2.asm</groupId>
+ <artifactId>asm</artifactId>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
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
new file mode 100644
index 00000000000..8309b7a8124
--- /dev/null
+++ b/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/GenerateTestDescriptorMojo.java
@@ -0,0 +1,61 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.hosted.plugin;
+
+import ai.vespa.hosted.api.TestDescriptor;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.project.MavenProject;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.stream.Stream;
+
+/**
+ * Generates a test descriptor file based on content of the compiled test classes
+ *
+ * @author bjorncs
+ */
+@Mojo(name = "generateTestDescriptor", threadSafe = true)
+public class GenerateTestDescriptorMojo extends AbstractMojo {
+
+ @Parameter(defaultValue = "${project}", readonly = true)
+ protected MavenProject project;
+
+ @Override
+ public void execute() throws MojoExecutionException {
+ TestAnnotationAnalyzer analyzer = new TestAnnotationAnalyzer();
+ analyzeTestClasses(analyzer);
+ TestDescriptor descriptor = TestDescriptor.from(
+ TestDescriptor.CURRENT_VERSION,
+ analyzer.systemTests(),
+ analyzer.stagingTests(),
+ analyzer.productionTests());
+ writeDescriptorFile(descriptor);
+ }
+
+ private void analyzeTestClasses(TestAnnotationAnalyzer analyzer) throws MojoExecutionException {
+ try (Stream<Path> files = Files.walk(testClassesDirectory())) {
+ files
+ .filter(f -> f.toString().endsWith(".class"))
+ .forEach(analyzer::analyzeClass);
+ } catch (Exception e) {
+ throw new MojoExecutionException("Failed to analyze test classes: " + e.getMessage(), e);
+ }
+ }
+
+ private void writeDescriptorFile(TestDescriptor descriptor) throws MojoExecutionException {
+ try {
+ Path descriptorFile = testClassesDirectory().resolve(TestDescriptor.DEFAULT_FILENAME);
+ Files.createDirectories(descriptorFile.getParent());
+ Files.write(descriptorFile, descriptor.toJson().getBytes());
+ } catch (IOException e) {
+ throw new MojoExecutionException("Failed to write test descriptor file: " + e.getMessage(), e);
+ }
+ }
+
+ private Path testClassesDirectory() { return Paths.get(project.getBuild().getTestOutputDirectory()); }
+}
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
new file mode 100644
index 00000000000..c45ef21bc31
--- /dev/null
+++ b/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/TestAnnotationAnalyzer.java
@@ -0,0 +1,74 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.hosted.plugin;
+
+
+import ai.vespa.hosted.cd.ProductionTest;
+import ai.vespa.hosted.cd.StagingTest;
+import ai.vespa.hosted.cd.SystemTest;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Analyzes test classes and tracks all classes containing hosted Vespa test annotations ({@link ai.vespa.hosted.cd}).
+ *
+ * @author bjorncs
+ */
+class TestAnnotationAnalyzer {
+
+ private final List<String> systemTests = new ArrayList<>();
+ private final List<String> stagingTests = new ArrayList<>();
+ private final List<String> productionTests = new ArrayList<>();
+
+ List<String> systemTests() { return systemTests; }
+ List<String> stagingTests() { return stagingTests; }
+ List<String> productionTests() { return productionTests; }
+
+ void analyzeClass(Path classFile) {
+ try (InputStream in = Files.newInputStream(classFile)) {
+ new ClassReader(in).accept(new AsmClassVisitor(), ClassReader.SKIP_DEBUG);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private class AsmClassVisitor extends ClassVisitor {
+
+ private String className;
+
+ AsmClassVisitor() { super(Opcodes.ASM7); }
+
+ @Override
+ public void visit(
+ int version, int access, String name, String signature, String superName, String[] interfaces) {
+ Type type = Type.getObjectType(name);
+ if (type.getSort() == Type.OBJECT) {
+ this.className = type.getClassName();
+ super.visit(version, access, name, signature, superName, interfaces);
+ }
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
+ String annotationClassName = Type.getType(descriptor).getClassName();
+ if (ProductionTest.class.getName().equals(annotationClassName)) {
+ productionTests.add(className);
+ } else if (StagingTest.class.getName().equals(annotationClassName)) {
+ stagingTests.add(className);
+ } else if (SystemTest.class.getName().equals(annotationClassName)) {
+ systemTests.add(className);
+ }
+ return null;
+ }
+ }
+}