diff --git a/build.gradle.kts b/build.gradle.kts index 0776aaf..36a33d4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -160,7 +160,7 @@ tasks.check { } tasks.register("buildTestIncludes") { - description = "Build include files needed to generate the Jekyll website"; + description = "Build include files needed to generate the Jekyll website" dependsOn(runFunctionalTests, runBenchmarkSmokeTest, extractImplementations) } diff --git a/config/spotbugs/suppressions.xml b/config/spotbugs/suppressions.xml index 12be0f1..5acc1e0 100644 --- a/config/spotbugs/suppressions.xml +++ b/config/spotbugs/suppressions.xml @@ -19,11 +19,4 @@ - - - - - - - \ No newline at end of file diff --git a/docs/_docs/1. implementations.md b/docs/_docs/1. implementations.md index d6c5edd..2e1ff92 100644 --- a/docs/_docs/1. implementations.md +++ b/docs/_docs/1. implementations.md @@ -37,8 +37,9 @@ against the underlying [  GitHub Repo](http "Supported Schema Versions", "Language", "Licence", + "Version tested", "Minimum Java Version", - "Jar size", + "Jar size", "Project activity" ], "data": implData.filter(row => row.shortName !== "Jackson").map(row => [ @@ -47,6 +48,7 @@ against the underlying [  GitHub Repo](http row.supported.join(', '), row.language, row.licence, + row.version, row.minJavaVersion, Math.ceil(row.jarSize / 1024) + ' KB', row.inactive ?? 'Active' diff --git a/docs/_docs/4. other considerations.md b/docs/_docs/4. other considerations.md index 7591612..de387d6 100644 --- a/docs/_docs/4. other considerations.md +++ b/docs/_docs/4. other considerations.md @@ -12,14 +12,15 @@ While this micro-site focuses on the functionality and performance of the valida Things to also consider are: - Is the project in active development? When was its last release? + The `Project activity` column on the [Libraries under test table]({% link _docs/1. implementations.md %}) attempts to show this, + but relies on someone updating the site if a project beings inactive. Projects that aren't active come with their own set of issues, especially around bug & security fixes, or dependency updates. - The following projects under test seem to be inactive at the time of writing: - - `Justify`: Last released Nov, 2020. - - `Medeia`: Last released Jun, 2019. - - `Everit`: Deprecated. Replaced by the `Skema` implementation. - What dependencies does it bring in? Less being more. For example, Vert.x brings in Netty as a dependency, which seems unnecessary. - Size of the library's jar file and its dependencies. + The `Jar size` column on the [Libraries under test table]({% link _docs/1. implementations.md %}) shows the size of the library's primary jar, + but does not _yet_ include the size of any other dependencies this brings in. - Is the implementation fit for purpose. For example, the `Snow` implementation documents itself as a reference implementation. (This may go some way to explain its poor performance). + Another example is the `Vertx` implementation, which doesn't seem to provide anyway to control how remote references are loaded. diff --git a/src/main/java/org/creekservice/kafka/test/perf/implementations/Implementation.java b/src/main/java/org/creekservice/kafka/test/perf/implementations/Implementation.java index d993386..93d29db 100644 --- a/src/main/java/org/creekservice/kafka/test/perf/implementations/Implementation.java +++ b/src/main/java/org/creekservice/kafka/test/perf/implementations/Implementation.java @@ -87,7 +87,7 @@ public String toString() { } } - class MetaData { + final class MetaData { public static final String ACTIVE_PROJECT = ""; public static final Pattern SHORT_NAME_PATTERN = Pattern.compile("[A-Za-z0-9]+"); @@ -99,8 +99,7 @@ class MetaData { private final Set supported; private final URL url; private final Color color; - private final long jarSize; - private final String minJavaVersion; + private final ImplJarFile implJarFile; private final String inactiveMsg; /** @@ -143,10 +142,7 @@ public MetaData( throw new RuntimeException(e); } this.color = requireNonNull(color, "color"); - this.jarSize = - ImplJarFile.jarSizeForClass( - requireNonNull(typeFromImplementation, "typeFromImplementation")); - this.minJavaVersion = ImplJarFile.jarMinJavaVersion(typeFromImplementation); + this.implJarFile = new ImplJarFile(typeFromImplementation); this.inactiveMsg = requireNonNull(inactiveMsg, "inactiveMsg").trim(); if (longName.isBlank()) { @@ -200,12 +196,17 @@ public String color() { @JsonProperty("jarSize") public long jarSize() { - return jarSize; + return implJarFile.jarSize(); + } + + @JsonProperty("version") + public String version() { + return implJarFile.jarVersion(); } @JsonProperty("minJavaVersion") public String minJavaVersion() { - return minJavaVersion; + return implJarFile.minJavaVersion(); } @JsonProperty("inactive") diff --git a/src/main/java/org/creekservice/kafka/test/perf/implementations/JustifyImplementation.java b/src/main/java/org/creekservice/kafka/test/perf/implementations/JustifyImplementation.java index f2c8e7c..490a54b 100644 --- a/src/main/java/org/creekservice/kafka/test/perf/implementations/JustifyImplementation.java +++ b/src/main/java/org/creekservice/kafka/test/perf/implementations/JustifyImplementation.java @@ -151,4 +151,9 @@ private SpecVersion schemaVersion(final SchemaSpec spec) { } return ver; } + + // Final, empty finalize method stops spotbugs CT_CONSTRUCTOR_THROW + @Override + @SuppressWarnings({"deprecation", "Finalize"}) + protected final void finalize() {} } diff --git a/src/main/java/org/creekservice/kafka/test/perf/performance/JsonSerdeBenchmark.java b/src/main/java/org/creekservice/kafka/test/perf/performance/JsonSerdeBenchmark.java index fd0120a..6c6cda7 100644 --- a/src/main/java/org/creekservice/kafka/test/perf/performance/JsonSerdeBenchmark.java +++ b/src/main/java/org/creekservice/kafka/test/perf/performance/JsonSerdeBenchmark.java @@ -228,11 +228,6 @@ private static class ImplementationState { SchemaSpec.DRAFT_2020_12, new AdditionalSchemas(Map.of(), Path.of(""))) : null; - - if (validator07 == null && validator2020 == null) { - throw new UnsupportedOperationException( - "Benchmark code needs enhancing to cover this case."); - } } public TestModel roundTrip(final ModelState model, final SchemaSpec version) { @@ -244,8 +239,16 @@ public TestModel roundTrip(final ModelState model, final SchemaSpec version) { private Implementation.JsonValidator validator(final SchemaSpec version) { switch (version) { case DRAFT_07: + if (validator07 == null) { + throw new UnsupportedOperationException( + "Implementation does not support " + version); + } return validator07; case DRAFT_2020_12: + if (validator2020 == null) { + throw new UnsupportedOperationException( + "Implementation does not support " + version); + } return validator2020; default: throw new UnsupportedOperationException( diff --git a/src/main/java/org/creekservice/kafka/test/perf/util/ImplJarFile.java b/src/main/java/org/creekservice/kafka/test/perf/util/ImplJarFile.java index 4b8d1fc..c123741 100644 --- a/src/main/java/org/creekservice/kafka/test/perf/util/ImplJarFile.java +++ b/src/main/java/org/creekservice/kafka/test/perf/util/ImplJarFile.java @@ -20,6 +20,7 @@ import com.google.common.annotations.VisibleForTesting; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; @@ -28,162 +29,173 @@ import java.nio.file.Paths; import java.security.CodeSource; import java.util.Enumeration; +import java.util.Optional; import java.util.function.Function; import java.util.jar.JarEntry; import java.util.jar.JarFile; import org.objectweb.asm.ClassReader; -/** Utility for determining the size of a jar file a class is loaded from. */ +/** Grabs metadata about an Implementation's jar file. */ public final class ImplJarFile { - /** - * Determine the size of the jar the supplied type is loaded from. - * - * @param type a class loaded from the jar. - * @return the size of the jar, in bytes. - * @throws IllegalArgumentException if the supplied {@code type} isn't loaded from an accessible - * jar. - */ - public static long jarSizeForClass(final Class type) { - return new JarSize(JarTaskBase::sourceCode).size(type); - } + private static final String GRADLE_CACHE_PATH = ".gradle" + File.separator + "caches"; + + private final long jarSize; + private final String jarVersion; + private final String minJavaVersion; /** - * Determine the minimum supported Java version for the jar the supplied type is loaded from. + * Grab metadata about an implementations jar file. * - * @param type a class loaded from the jar. - * @return the minimum supported Java version + * @param type a single type from the implementation's main jar. * @throws IllegalArgumentException if the supplied {@code type} isn't loaded from an accessible * jar. */ - public static String jarMinJavaVersion(final Class type) { - return new JarMinJavaVersion(JarTaskBase::sourceCode).minJavaVersion(type); + public ImplJarFile(final Class type) { + this(type, ImplJarFile::sourceCode); } - private ImplJarFile() {} - @VisibleForTesting - static class JarTaskBase { - - final Function, CodeSource> sourceAccessor; - - JarTaskBase(final Function, CodeSource> locator) { - this.sourceAccessor = requireNonNull(locator, "locator"); - } - - @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "Location supplied by JVM") - Path pathToJar(final Class type) { - final URL location = location(type); - - try { - return Paths.get(location.toURI()); - } catch (final Exception e) { - throw new IllegalArgumentException( - "Type not loaded from accessible jar file. location: " + location, e); - } - } + ImplJarFile(final Class type, final Function, CodeSource> locator) { + final Path location = location(requireNonNull(type, "type"), locator); + this.jarSize = jarSize(location); + this.jarVersion = jarVersion(type, location); + this.minJavaVersion = minJavaVersion(type, location); + } - private URL location(final Class type) { - final CodeSource codeSource = sourceAccessor.apply(type); - if (codeSource == null) { - throw new IllegalArgumentException("Type not loaded from a jar file: " + type); - } + public long jarSize() { + return jarSize; + } - return codeSource.getLocation(); - } + public String jarVersion() { + return jarVersion; + } - private static CodeSource sourceCode(final Class type) { - return type.getProtectionDomain().getCodeSource(); - } + public String minJavaVersion() { + return minJavaVersion; } - @VisibleForTesting - static final class JarSize extends JarTaskBase { + private static CodeSource sourceCode(final Class type) { + return type.getProtectionDomain().getCodeSource(); + } - JarSize(final Function, CodeSource> locator) { - super(locator); + @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "Location supplied by JVM") + private static Path location( + final Class type, final Function, CodeSource> locator) { + final CodeSource codeSource = locator.apply(type); + if (codeSource == null) { + throw new IllegalArgumentException("Type not loaded from a jar file: " + type); } - long size(final Class type) { - final Path path = pathToJar(type); + final URL location = codeSource.getLocation(); - try { - return Files.size(path); - } catch (final Exception e) { - throw new IllegalArgumentException( - "Failed to read file size. location: " + path, e); - } + try { + return Paths.get(location.toURI()); + } catch (final Exception e) { + throw new IllegalArgumentException( + "Type not loaded from accessible jar file. location: " + location, e); } } - @VisibleForTesting - static final class JarMinJavaVersion extends JarTaskBase { - - JarMinJavaVersion(final Function, CodeSource> locator) { - super(locator); + private static long jarSize(final Path location) { + try { + return Files.size(location); + } catch (final Exception e) { + throw new IllegalArgumentException( + "Failed to read file size. location: " + location, e); } + } - String minJavaVersion(final Class type) { - final Path path = pathToJar(type); + /** + * Extract the version number from the path within the Gradle cache. + * + *

Dependencies are in the Gradle cache. This stores jars in a directory structure that + * contains the version number. For example {@code + * .gradle/caches/modules-2/files-2.1/com.damnhandy/handy-uri-templates/ + * 2.1.8/170102d8e1d6fcc5e8f9bef45de923285dd3a80f/handy-uri-templates-2.1.8.jar} Where the + * version is {@code 2.1.8}. + * + * @param type a type loaded from the jar file. + * @return the version of the jar file. + */ + @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "Path provided by JVM") + private static String jarVersion(final Class type, final Path location) { + final String textPath = location.toString(); + final int idx = textPath.indexOf(GRADLE_CACHE_PATH); + if (idx < 0) { + throw new IllegalArgumentException( + "Jar not loaded from the Gradle cache. jar: " + location); + } + final Path startPath = Paths.get(textPath.substring(0, idx + GRADLE_CACHE_PATH.length())); + final Path relative = startPath.relativize(location); + + return Optional.of(relative) + .map(Path::getParent) + .map(Path::getParent) + .map(Path::getFileName) + .map(Path::toString) + .orElseThrow( + () -> + new IllegalArgumentException( + "Could not decode version from path. jar: " + location)); + } - try (JarFile jarFile = new JarFile(path.toFile())) { - final Enumeration entries = jarFile.entries(); + private static String minJavaVersion(final Class type, final Path location) { + try (JarFile jarFile = new JarFile(location.toFile())) { + final Enumeration entries = jarFile.entries(); - while (entries.hasMoreElements()) { - final JarEntry entry = entries.nextElement(); + while (entries.hasMoreElements()) { + final JarEntry entry = entries.nextElement(); - if (entry.getName().endsWith(".class")) { - try (InputStream classInputStream = jarFile.getInputStream(entry)) { - return minJavaVersion(classInputStream); - } + if (entry.getName().endsWith(".class")) { + try (InputStream classInputStream = jarFile.getInputStream(entry)) { + return minJavaVersion(classInputStream); } } - } catch (IOException e) { - throw new IllegalArgumentException( - "Failed to extract the min Java version the jar supports. Path: " + path, - e); } - - return "Unknown"; + } catch (IOException e) { + throw new IllegalArgumentException( + "Failed to extract the min Java version the jar supports. Path: " + location, + e); } - public static String minJavaVersion(final InputStream classFile) throws IOException { - final ClassReader classReader = new ClassReader(classFile); - final VersionClassVisitor versionClassVisitor = new VersionClassVisitor(); - classReader.accept(versionClassVisitor, 0); - - final int majorVersion = versionClassVisitor.majorVersion; - if (majorVersion < 0) { - throw new IllegalArgumentException("Failed to determine Java version"); - } + return "Unknown"; + } - final String javaVersion = - majorVersion < 49 - ? "1." + (majorVersion - 44) - : String.valueOf(majorVersion - 44); + private static String minJavaVersion(final InputStream classFile) throws IOException { + final ClassReader classReader = new ClassReader(classFile); + final VersionClassVisitor versionClassVisitor = new VersionClassVisitor(); + classReader.accept(versionClassVisitor, 0); - return "Java " + javaVersion; + final int majorVersion = versionClassVisitor.majorVersion; + if (majorVersion < 0) { + throw new IllegalArgumentException("Failed to determine Java version"); } - private static class VersionClassVisitor extends org.objectweb.asm.ClassVisitor { + final String javaVersion = + majorVersion < 49 ? "1." + (majorVersion - 44) : String.valueOf(majorVersion - 44); + + return "Java " + javaVersion; + } - private int majorVersion = -1; + private static class VersionClassVisitor extends org.objectweb.asm.ClassVisitor { - VersionClassVisitor() { - super(org.objectweb.asm.Opcodes.ASM9); - } + private int majorVersion = -1; - @Override - public void visit( - final int version, - final int access, - final String name, - final String signature, - final String superName, - final String[] interfaces) { - if (version > majorVersion) { - majorVersion = version; - } + VersionClassVisitor() { + super(org.objectweb.asm.Opcodes.ASM9); + } + + @Override + public void visit( + final int version, + final int access, + final String name, + final String signature, + final String superName, + final String[] interfaces) { + if (version > majorVersion) { + majorVersion = version; } } } diff --git a/src/test/java/org/creekservice/kafka/test/perf/util/ImplJarFileTest.java b/src/test/java/org/creekservice/kafka/test/perf/util/ImplJarFileTest.java index a3c7219..cda1c2b 100644 --- a/src/test/java/org/creekservice/kafka/test/perf/util/ImplJarFileTest.java +++ b/src/test/java/org/creekservice/kafka/test/perf/util/ImplJarFileTest.java @@ -17,22 +17,20 @@ package org.creekservice.kafka.test.perf.util; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThan; +import static org.hamcrest.Matchers.matchesPattern; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mock.Strictness.LENIENT; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.io.File; import java.net.URL; -import java.nio.file.Path; import java.security.CodeSource; import java.util.function.Function; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -54,89 +52,74 @@ void setUp() { .thenReturn(Test.class.getProtectionDomain().getCodeSource().getLocation()); } - @Nested - class JarBaseTaskTest { - - private ExampleTask task; - - @BeforeEach - void setUp() { - task = new ExampleTask(sourceAccessor); - } - - @Test - void shouldPassSuppliedTypeToLocator() { - // When: - task.getPath(Test.class); - - // Then: - verify(sourceAccessor).apply(Test.class); - } - - @Test - void shouldThrowIfSourceCodeNotAvailable() { - // Given: - when(sourceAccessor.apply(any())).thenReturn(null); - - // When: - final Exception e = - assertThrows(IllegalArgumentException.class, () -> task.getPath(Test.class)); - - // Then: - assertThat(e.getMessage(), is("Type not loaded from a jar file: " + Test.class)); - } - - @Test - void shouldThrowIfNotLoadedFromFile() throws Exception { - // Given: - when(codeSource.getLocation()).thenReturn(new URL("ftp:/localhost/something")); - - // When: - final Exception e = - assertThrows(IllegalArgumentException.class, () -> task.getPath(Test.class)); - - // Then: - assertThat( - e.getMessage(), - is( - "Type not loaded from accessible jar file. location:" - + " ftp:/localhost/something")); - } - - @Test - void shouldGetPathOfJar() { - // When: - final Path path = task.getPath(Test.class); - - // Then: - assertThat(path.toString(), containsString(".gradle" + File.separator + "caches")); - } - - private final class ExampleTask extends ImplJarFile.JarTaskBase { - - ExampleTask(final Function, CodeSource> locator) { - super(locator); - } - - Path getPath(final Class testClass) { - return pathToJar(testClass); - } - } + @Test + void shouldPassSuppliedTypeToLocator() { + // When: + new ImplJarFile(Test.class, sourceAccessor); + + // Then: + verify(sourceAccessor).apply(Test.class); + } + + @Test + void shouldThrowIfSourceCodeNotAvailable() { + // Given: + when(sourceAccessor.apply(any())).thenReturn(null); + + // When: + final Exception e = + assertThrows( + IllegalArgumentException.class, + () -> new ImplJarFile(Test.class, sourceAccessor)); + + // Then: + assertThat(e.getMessage(), is("Type not loaded from a jar file: " + Test.class)); + } + + @Test + void shouldThrowIfNotLoadedFromFile() throws Exception { + // Given: + when(codeSource.getLocation()).thenReturn(new URL("ftp:/localhost/something")); + + // When: + final Exception e = + assertThrows( + IllegalArgumentException.class, + () -> new ImplJarFile(Test.class, sourceAccessor)); + + // Then: + assertThat( + e.getMessage(), + is( + "Type not loaded from accessible jar file. location:" + + " ftp:/localhost/something")); + } + + @Test + void shouldReturnJarSize() { + // Given: + final ImplJarFile jarFile = new ImplJarFile(Test.class); + + // Then: + assertThat(jarFile.jarSize(), is(greaterThan(1_000L))); + assertThat(jarFile.jarSize(), is(lessThan(1_000_000L))); } - @Nested - class JarSizeTest { - @Test - void shouldReturnJarSize() { - assertThat(ImplJarFile.jarSizeForClass(Test.class), is(greaterThan(1000L))); - } + @Test + void shouldReturnJarVersion() { + // Given: + final ImplJarFile jarFile = new ImplJarFile(Test.class); + + // Then: + assertThat(jarFile.jarVersion(), matchesPattern("\\d+\\.\\d+\\.\\d+")); } - @Nested - class JarMinJavaVersionTest { - @Test - void shouldReturnJarMinJavaVersion() { - assertThat(ImplJarFile.jarMinJavaVersion(Test.class), is("Java 9")); - } + @Test + void shouldReturnJarMinJavaVersion() { + // Given: + final ImplJarFile jarFile = new ImplJarFile(Test.class); + + // Then: + assertThat(jarFile.minJavaVersion(), is("Java 9")); } } diff --git a/src/test/java/org/creekservice/kafka/test/perf/util/ImplsJsonFormatterTest.java b/src/test/java/org/creekservice/kafka/test/perf/util/ImplsJsonFormatterTest.java index c29e0d7..24ea085 100644 --- a/src/test/java/org/creekservice/kafka/test/perf/util/ImplsJsonFormatterTest.java +++ b/src/test/java/org/creekservice/kafka/test/perf/util/ImplsJsonFormatterTest.java @@ -132,6 +132,15 @@ void shouldIncludeJarSize() { assertThat(json, matchesPattern(".*\"jarSize\":\\d+[,}].*")); } + @Test + void shouldIncludeJarVersion() { + // When: + final String json = ImplsJsonFormatter.implDetailsAsJson(List.of(implA)); + + // Then: + assertThat(json, matchesPattern(".*\"version\":\"\\d+\\.\\d+\\.\\d+\".*")); + } + @Test void shouldIncludeMinJavaVersion() { // When: