Sfoglia il codice sorgente

Add module descriptor test matchers (#86324)

This change adds test-only matchers for various aspect of module descriptors
Chris Hegarty 3 anni fa
parent
commit
e692dfeea4

+ 223 - 0
test/framework/src/main/java/org/elasticsearch/test/hamcrest/ModuleDescriptorMatchers.java

@@ -0,0 +1,223 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.test.hamcrest;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+import java.lang.module.ModuleDescriptor.Exports;
+import java.lang.module.ModuleDescriptor.Opens;
+import java.lang.module.ModuleDescriptor.Provides;
+import java.lang.module.ModuleDescriptor.Requires;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+public class ModuleDescriptorMatchers {
+
+    private ModuleDescriptorMatchers() {}
+
+    public static Matcher<Exports> exportsOf(String pkg) {
+        return new ExportsMatcher(pkg, Set.of());
+    }
+
+    public static Matcher<Exports> exportsOf(String pkg, Set<String> targets) {
+        return new ExportsMatcher(pkg, targets);
+    }
+
+    public static Matcher<Opens> opensOf(String pkg) {
+        return new OpensMatcher(pkg, Set.of());
+    }
+
+    public static Matcher<Opens> opensOf(String pkg, Set<String> targets) {
+        return new OpensMatcher(pkg, targets);
+    }
+
+    public static Matcher<Requires> requiresOf(String mn) {
+        return new RequiresNameMatcher(mn);
+    }
+
+    public static Matcher<Provides> providesOf(String service, List<String> provides) {
+        return new ProvidesMatcher(service, provides);
+    }
+
+    /**
+     * Matcher that matches the <i>source</i> and <i>targets</i> of a {@code Requires}.
+     * The matcher is agnostic of the {@code Requires} modifiers.
+     */
+    static class ExportsMatcher extends TypeSafeMatcher<Exports> {
+
+        private final String source;
+        private final Set<String> targets;
+
+        ExportsMatcher(String source, Set<String> targets) {
+            this.source = source;
+            this.targets = Set.copyOf(targets);
+        }
+
+        @Override
+        protected boolean matchesSafely(final Exports item) {
+            return item != null && Objects.equals(item.source(), source) && Objects.equals(item.targets(), targets);
+        }
+
+        @Override
+        public void describeTo(final Description description) {
+            description.appendText("Exports[%s]".formatted(exportsToString(source, targets)));
+        }
+
+        @Override
+        protected void describeMismatchSafely(final Exports item, final Description mismatchDescription) {
+            describeTo(mismatchDescription);
+            if (item == null) {
+                mismatchDescription.appendText("was null");
+            } else {
+                mismatchDescription.appendText(", actual Exports[%s]".formatted(exportsToString(item)));
+            }
+        }
+
+        private static String exportsToString(String source, Set<String> targets) {
+            if (targets.isEmpty()) {
+                return source;
+            } else {
+                return source + " to " + targets;
+            }
+        }
+
+        private static String exportsToString(Exports exports) {
+            if (exports.targets().isEmpty()) {
+                return exports.source();
+            } else {
+                return exports.source() + " to " + exports.targets();
+            }
+        }
+    }
+
+    /**
+     * Matcher that matches the <i>name</i> of a {@code Requires}.
+     * The matcher is agnostic of other {@code Requires} state, like the modifiers.
+     */
+    static class RequiresNameMatcher extends TypeSafeMatcher<Requires> {
+
+        private final String mn;
+
+        RequiresNameMatcher(String mn) {
+            this.mn = mn;
+        }
+
+        @Override
+        protected boolean matchesSafely(final Requires item) {
+            return item != null && Objects.equals(item.name(), mn);
+        }
+
+        @Override
+        public void describeTo(final Description description) {
+            description.appendText("Requires with name " + mn);
+        }
+
+        @Override
+        protected void describeMismatchSafely(final Requires item, final Description mismatchDescription) {
+            describeTo(mismatchDescription);
+            if (item == null) {
+                mismatchDescription.appendText("was null");
+            } else {
+                mismatchDescription.appendText(", actual Requires with name " + item);
+            }
+        }
+    }
+
+    /**
+     * Matcher that matches the <i>source</i> and <i>targets</i> of an {@code Opens}.
+     * The matcher is agnostic of the modifiers.
+     */
+    static class OpensMatcher extends TypeSafeMatcher<Opens> {
+
+        private final String source;
+        private final Set<String> targets;
+
+        OpensMatcher(String source, Set<String> targets) {
+            this.source = source;
+            this.targets = Set.copyOf(targets);
+        }
+
+        @Override
+        protected boolean matchesSafely(final Opens item) {
+            return item != null && Objects.equals(item.source(), source) && Objects.equals(item.targets(), targets);
+        }
+
+        @Override
+        public void describeTo(final Description description) {
+            description.appendText("Opens[%s]".formatted(opensToString(source, targets)));
+        }
+
+        @Override
+        protected void describeMismatchSafely(final Opens item, final Description mismatchDescription) {
+            describeTo(mismatchDescription);
+            if (item == null) {
+                mismatchDescription.appendText("was null");
+            } else {
+                mismatchDescription.appendText(", actual Opens[%s]".formatted(opensToString(item)));
+            }
+        }
+
+        private static String opensToString(String source, Set<String> targets) {
+            if (targets.isEmpty()) {
+                return source;
+            } else {
+                return source + " to " + targets;
+            }
+        }
+
+        private static String opensToString(Opens opens) {
+            if (opens.targets().isEmpty()) {
+                return opens.source();
+            } else {
+                return opens.source() + " to " + opens.targets();
+            }
+        }
+    }
+
+    /**
+     * Matcher that matches the <i>service</i> and <i>providers</i> of a {@code Provides}.
+     */
+    static class ProvidesMatcher extends TypeSafeMatcher<Provides> {
+
+        private final String service;
+        private final List<String> providers;
+
+        ProvidesMatcher(String service, List<String> providers) {
+            this.service = service;
+            this.providers = List.copyOf(providers);
+        }
+
+        @Override
+        protected boolean matchesSafely(final Provides item) {
+            return item != null && item.service().equals(service) && item.providers().equals(providers);
+        }
+
+        @Override
+        public void describeTo(final Description description) {
+            description.appendText("Provides[%s]".formatted(providesToString(service, providers)));
+        }
+
+        @Override
+        protected void describeMismatchSafely(final Provides item, final Description mismatchDescription) {
+            describeTo(mismatchDescription);
+            if (item == null) {
+                mismatchDescription.appendText("was null");
+            } else {
+                mismatchDescription.appendText(", actual Provides[%s]".formatted(item));
+            }
+        }
+
+        private static String providesToString(String service, List<String> provides) {
+            return service + " with " + provides;
+        }
+    }
+}

+ 214 - 0
test/framework/src/test/java/org/elasticsearch/test/hamcrest/ModuleDescriptorMatchersTests.java

@@ -0,0 +1,214 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.test.hamcrest;
+
+import org.elasticsearch.test.ESTestCase;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.StringDescription;
+
+import java.lang.module.ModuleDescriptor;
+import java.lang.module.ModuleDescriptor.Exports;
+import java.lang.module.ModuleDescriptor.Opens;
+import java.lang.module.ModuleDescriptor.Provides;
+import java.lang.module.ModuleDescriptor.Requires;
+import java.util.List;
+import java.util.Set;
+
+import static org.elasticsearch.test.hamcrest.ModuleDescriptorMatchers.exportsOf;
+import static org.elasticsearch.test.hamcrest.ModuleDescriptorMatchers.opensOf;
+import static org.elasticsearch.test.hamcrest.ModuleDescriptorMatchers.providesOf;
+import static org.elasticsearch.test.hamcrest.ModuleDescriptorMatchers.requiresOf;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+
+public class ModuleDescriptorMatchersTests extends ESTestCase {
+
+    // -- Exports
+
+    public void testExportsPackage() {
+        var exports = createExports("p");
+        assertMatch(exports, exportsOf("p"));
+    }
+
+    public void testExportsPackage2() {
+        var exports = createExports("foo.bar");
+        assertMatch(exports, exportsOf("foo.bar"));
+    }
+
+    public void testExportsQualifiedExport() {
+        var exports = createExports("p.q", Set.of("mod", "mod1", "mod2"));
+        assertMatch(exports, exportsOf("p.q", Set.of("mod", "mod1", "mod2")));
+    }
+
+    public void testExportsQualifiedExportMismatch() {
+        var exports = createExports("p", Set.of("m"));
+        assertMismatch(exports, exportsOf("p.foo", Set.of("mod")), equalTo("Exports[p.foo to [mod]], actual Exports[p to [m]]"));
+    }
+
+    public void testExportsQualifiedExportMismatch2() {
+        var exports = createExports("p", Set.of("m"));
+        assertMismatch(exports, exportsOf("p", Set.of("mod")), equalTo("Exports[p to [mod]], actual Exports[p to [m]]"));
+    }
+
+    public void testExportsNull() {
+        assertMismatch(null, exportsOf("p"), equalTo("was null"));
+    }
+
+    static Exports createExports(String pkg) {
+        var md = ModuleDescriptor.newModule("m").exports(pkg).build();
+        return md.exports().stream().findFirst().orElseThrow();
+    }
+
+    static Exports createExports(String pkg, Set<String> targets) {
+        var builder = ModuleDescriptor.newModule("m");
+        builder.exports(pkg, Set.copyOf(targets));
+        return builder.build().exports().stream().findFirst().orElseThrow();
+    }
+
+    // -- Opens
+
+    public void testOpensPackage() {
+        var opens = createOpens("p");
+        assertMatch(opens, opensOf("p"));
+    }
+
+    public void testOpensPackage2() {
+        var opens = createOpens("larry.curly.moe");
+        assertMatch(opens, opensOf("larry.curly.moe"));
+    }
+
+    public void testOpensQualifiedOpens() {
+        var opens = createOpens("p.q", Set.of("mod", "mod1", "mod2"));
+        assertMatch(opens, opensOf("p.q", Set.of("mod", "mod1", "mod2")));
+    }
+
+    public void testOpensQualifiedExportMismatch() {
+        var opens = createOpens("p", Set.of("m"));
+        assertMismatch(opens, opensOf("p.foo", Set.of("mod")), equalTo("Opens[p.foo to [mod]], actual Opens[p to [m]]"));
+    }
+
+    public void testOpensQualifiedExportMismatch2() {
+        var opens = createOpens("p", Set.of("m"));
+        assertMismatch(opens, opensOf("p", Set.of("mod")), equalTo("Opens[p to [mod]], actual Opens[p to [m]]"));
+    }
+
+    public void testOpensNull() {
+        assertMismatch(null, opensOf("p"), equalTo("was null"));
+    }
+
+    static Opens createOpens(String pkg) {
+        var md = ModuleDescriptor.newModule("m").opens(pkg).build();
+        return md.opens().stream().findFirst().orElseThrow();
+    }
+
+    static Opens createOpens(String pkg, Set<String> targets) {
+        var builder = ModuleDescriptor.newModule("m");
+        builder.opens(pkg, Set.copyOf(targets));
+        return builder.build().opens().stream().findFirst().orElseThrow();
+    }
+
+    // -- Requires
+
+    public void testRequiresModule() {
+        var requires = createRequires("m");
+        assertMatch(requires, requiresOf("m"));
+    }
+
+    public void testRequiresModule1() {
+        var requires = createRequires("m1");
+        assertMatch(requires, requiresOf("m1"));
+    }
+
+    public void testRequiresMismatch() {
+        var requires = createRequires("m");
+        assertMismatch(requires, requiresOf("foo"), equalTo("Requires with name foo, actual Requires with name m"));
+    }
+
+    public void testRequiresMismatch2() {
+        var requires = createRequires("o.e.server");
+        assertMismatch(requires, requiresOf("bar.baz"), equalTo("Requires with name bar.baz, actual Requires with name o.e.server"));
+    }
+
+    public void testRequiresNull() {
+        assertMismatch(null, requiresOf("m"), equalTo("was null"));
+    }
+
+    static Requires createRequires(String mn) {
+        var md = ModuleDescriptor.newModule("mmm").requires(mn).build();
+        return md.requires().stream().filter(r -> r.name().equals(mn)).findFirst().orElseThrow();
+    }
+
+    // --- provides
+
+    public void testProvides() {
+        var provides = createProvides("p.FooService", List.of("q.FooServiceImpl"));
+        assertMatch(provides, providesOf("p.FooService", List.of("q.FooServiceImpl")));
+    }
+
+    public void testProvidesMismatch() {
+        var provides = createProvides("p.F", List.of("q.FI"));
+        assertMismatch(
+            provides,
+            providesOf("p.F", List.of("q.BI")),
+            equalTo("Provides[p.F with [q.BI]], actual Provides[p.F with [q.FI]]")
+        );
+    }
+
+    public void testProvidesMismatch2() {
+        var provides = createProvides("p.F", List.of("q.FI"));
+        assertMismatch(
+            provides,
+            providesOf("p.B", List.of("q.FI")),
+            equalTo("Provides[p.B with [q.FI]], actual Provides[p.F with [q.FI]]")
+        );
+    }
+
+    public void testProvidesMismatch3() {
+        var provides = createProvides("p.F", List.of("q.FI"));
+        assertMismatch(
+            provides,
+            providesOf("p.F", List.of("q.FI", "q.BI")),
+            equalTo("Provides[p.F with [q.FI, q.BI]], actual Provides[p.F with [q.FI]]")
+        );
+    }
+
+    public void testProvidesNull() {
+        assertMismatch(null, providesOf("p.Foo", List.of("p.FooImpl")), equalTo("was null"));
+    }
+
+    static Provides createProvides(String service, List<String> providers) {
+        var md = ModuleDescriptor.newModule("m").provides(service, List.copyOf(providers)).build();
+        return md.provides().stream().findFirst().orElseThrow();
+    }
+
+    // -- infra
+
+    static <T> void assertMatch(T actual, Matcher<? super T> matcher) {
+        assertMatch("", actual, matcher);
+    }
+
+    static <T> void assertMatch(String reason, T actual, Matcher<? super T> matcher) {
+        if (matcher.matches(actual)) {
+            return;
+        }
+        Description description = new StringDescription();
+        description.appendText("expected ");
+        matcher.describeMismatch(actual, description);
+        throw new AssertionError(description.toString());
+    }
+
+    static <T> void assertMismatch(T actual, Matcher<? super T> matcher, Matcher<String> mismatchDescriptionMatcher) {
+        assertThat(matcher.matches(actual), is(false));
+
+        StringDescription description = new StringDescription();
+        matcher.describeMismatch(actual, description);
+        assertThat(description.toString(), mismatchDescriptionMatcher);
+    }
+}