DefaultExtensibleEnumRegistryTest.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.impl;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.maven.api.ExtensibleEnum;
import org.apache.maven.api.services.Lookup;
import org.apache.maven.api.spi.ExtensibleEnumProvider;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class DefaultExtensibleEnumRegistryTest {
private static final TestValue FOO = new TestValue("foo");
private static final TestValue BAR = new TestValue("bar");
private static final TestValue CUSTOM = new TestValue("custom");
@Test
void lookupFindsBuiltinValues() {
var registry = new TestRegistry(List.of(), FOO, BAR);
assertSame(FOO, registry.lookup("foo").orElseThrow());
assertSame(BAR, registry.lookup("BAR").orElseThrow());
}
@Test
void lookupFindsConstructorInjectedProviders() {
TestProvider provider = () -> List.of(CUSTOM);
var registry = new TestRegistry(List.of(provider));
assertSame(CUSTOM, registry.lookup("custom").orElseThrow());
}
@Test
void dynamicLookupFindsProjectExtensionProviders() {
Lookup lookup = mock(Lookup.class);
TestProvider provider = () -> List.of(CUSTOM);
when(lookup.lookupList(TestProvider.class)).thenReturn(List.of(provider));
var registry = new TestRegistry(lookup, List.of());
assertSame(CUSTOM, registry.lookup("custom").orElseThrow());
}
@Test
void dynamicLookupTakesPriorityOverConstructorInjected() {
Lookup lookup = mock(Lookup.class);
TestValue builtin = new TestValue("foo");
TestValue override = new TestValue("foo");
TestProvider dynamicProvider = () -> List.of(override);
TestProvider injectedProvider = () -> List.of(builtin);
when(lookup.lookupList(TestProvider.class)).thenReturn(List.of(dynamicProvider));
var registry = new TestRegistry(lookup, List.of(injectedProvider));
assertSame(override, registry.lookup("foo").orElseThrow());
}
@Test
void dynamicLookupDoesNotRetainProviderAcrossProjects() {
Lookup lookup = mock(Lookup.class);
TestValue custom = new TestValue("custom");
TestProvider provider = () -> List.of(custom);
AtomicReference<List<TestProvider>> visible = new AtomicReference<>(List.of(provider));
when(lookup.lookupList(TestProvider.class)).thenAnswer(inv -> visible.get());
var registry = new TestRegistry(lookup, List.of());
assertSame(custom, registry.lookup("custom").orElseThrow());
visible.set(List.of());
assertTrue(registry.lookup("custom").isEmpty());
}
@Test
void fallsBackToBuiltinWhenDynamicLookupFindsNothing() {
Lookup lookup = mock(Lookup.class);
when(lookup.lookupList(TestProvider.class)).thenReturn(List.of());
TestValue foo = new TestValue("foo");
var registry = new TestRegistry(lookup, List.of(), foo);
assertSame(foo, registry.lookup("foo").orElseThrow());
}
// --- isVisibleFrom tests ---
@Test
void visibleFromNullClassLoaderAlwaysReturnsTrue() {
TestProvider provider = () -> List.of(CUSTOM);
assertTrue(ExtensibleEnumRegistries.isVisibleFrom(provider, null));
}
@Test
void visibleFromSameClassLoaderReturnsTrue() {
TestProvider provider = () -> List.of(CUSTOM);
ClassLoader cl = provider.getClass().getClassLoader();
assertTrue(ExtensibleEnumRegistries.isVisibleFrom(provider, cl));
}
@Test
void visibleFromChildOfParentClassLoaderReturnsTrue() throws Exception {
TestProvider provider = () -> List.of(CUSTOM);
ClassLoader parent = provider.getClass().getClassLoader();
try (URLClassLoader child = new URLClassLoader(new URL[0], parent)) {
assertTrue(ExtensibleEnumRegistries.isVisibleFrom(provider, child));
}
}
@Test
void notVisibleFromUnrelatedClassLoader() throws Exception {
TestProvider provider = () -> List.of(CUSTOM);
try (URLClassLoader unrelated = new URLClassLoader(new URL[0], null)) {
assertFalse(ExtensibleEnumRegistries.isVisibleFrom(provider, unrelated));
}
}
@Test
void visibleFromClassRealmThatImportsProviderRealm() throws Exception {
TestProvider provider = () -> List.of(CUSTOM);
ClassLoader providerCl = provider.getClass().getClassLoader();
try (FakeClassRealm realm = new FakeClassRealm(Set.of(providerCl))) {
assertTrue(ExtensibleEnumRegistries.isVisibleFrom(provider, realm));
}
}
@Test
void notVisibleFromClassRealmWithoutMatchingImport() throws Exception {
TestProvider provider = () -> List.of(CUSTOM);
try (URLClassLoader otherCl = new URLClassLoader(new URL[0], null);
FakeClassRealm realm = new FakeClassRealm(Set.of(otherCl))) {
assertFalse(ExtensibleEnumRegistries.isVisibleFrom(provider, realm));
}
}
record TestValue(String id) implements ExtensibleEnum {}
interface TestProvider extends ExtensibleEnumProvider<TestValue> {}
static class TestRegistry extends ExtensibleEnumRegistries.DefaultExtensibleEnumRegistry<TestValue, TestProvider> {
@SafeVarargs
TestRegistry(List<TestProvider> providers, TestValue... builtins) {
super(providers, builtins);
}
@SafeVarargs
TestRegistry(Lookup lookup, List<TestProvider> providers, TestValue... builtins) {
super(lookup, TestProvider.class, providers, builtins);
}
}
/**
* Simulates a ClassRealm with {@code getImportRealms()} so the reflection-based
* import check can be exercised without depending on plexus-classworlds.
*/
static class FakeClassRealm extends URLClassLoader {
private final Collection<ClassLoader> importedRealms;
FakeClassRealm(Collection<ClassLoader> importedRealms) {
super(new URL[0], null);
this.importedRealms = importedRealms;
}
@SuppressWarnings("unused") // called via reflection by isImportedRealm
public Collection<ClassLoader> getImportRealms() {
return importedRealms;
}
}
}