blob: 6fe1d3c302192d1ea760da9c48799c1185cc3b57 [file] [log] [blame]
/*
* 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.ignite.internal.configuration.testframework;
import java.lang.reflect.Field;
import java.lang.reflect.Parameter;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigObject;
import org.apache.ignite.configuration.RootKey;
import org.apache.ignite.internal.configuration.DynamicConfigurationChanger;
import org.apache.ignite.internal.configuration.RootInnerNode;
import org.apache.ignite.internal.configuration.SuperRoot;
import org.apache.ignite.internal.configuration.asm.ConfigurationAsmGenerator;
import org.apache.ignite.internal.configuration.hocon.HoconConverter;
import org.apache.ignite.internal.configuration.tree.ConfigurationSource;
import org.apache.ignite.internal.configuration.tree.InnerNode;
import org.apache.ignite.internal.configuration.util.ConfigurationUtil;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.platform.commons.support.AnnotationSupport;
import org.junit.platform.commons.support.HierarchyTraversalMode;
import static java.util.concurrent.CompletableFuture.completedFuture;
import static org.apache.ignite.configuration.annotation.ConfigurationType.LOCAL;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* JUnit extension to inject configuration instances into test classes.
*
* @see InjectConfiguration
*/
public class ConfigurationExtension implements BeforeEachCallback, AfterEachCallback, ParameterResolver {
/** JUnit namespace for the extension. */
private static final Namespace namespace = Namespace.create(ConfigurationExtension.class);
/** Key to store {@link ConfigurationAsmGenerator} in {@link ExtensionContext.Store}. */
private static final Object CGEN_KEY = new Object();
/** {@inheritDoc} */
@Override public void beforeEach(ExtensionContext context) throws Exception {
ConfigurationAsmGenerator cgen = new ConfigurationAsmGenerator();
context.getStore(namespace).put(CGEN_KEY, cgen);
Object testInstance = context.getRequiredTestInstance();
for (Field field : getMatchingFields(testInstance.getClass())) {
field.setAccessible(true);
field.set(testInstance, cfgValue(field.getType(), field.getAnnotation(InjectConfiguration.class), cgen));
}
}
/** {@inheritDoc} */
@Override public void afterEach(ExtensionContext context) throws Exception {
context.getStore(namespace).remove(CGEN_KEY);
}
/** {@inheritDoc} */
@Override public boolean supportsParameter(
ParameterContext parameterContext, ExtensionContext extensionContext
) throws ParameterResolutionException {
return parameterContext.isAnnotated(InjectConfiguration.class)
&& supportType(parameterContext.getParameter().getType());
}
/** {@inheritDoc} */
@Override public Object resolveParameter(
ParameterContext parameterContext, ExtensionContext extensionContext
) throws ParameterResolutionException {
Parameter parameter = parameterContext.getParameter();
ConfigurationAsmGenerator cgen =
extensionContext.getStore(namespace).get(CGEN_KEY, ConfigurationAsmGenerator.class);
try {
return cfgValue(parameter.getType(), parameter.getAnnotation(InjectConfiguration.class), cgen);
}
catch (ClassNotFoundException classNotFoundException) {
throw new ParameterResolutionException(
"Cannot find a configuration schema class that matches " + parameter.getType().getCanonicalName(),
classNotFoundException
);
}
}
/**
* Instantiates a configuration instance for injection.
*
* @param type Type of the field or parameter. Class name must end with {@code Configuration}.
* @param annotation Annotation present on the field or parameter.
* @param cgen Runtime code generator associated with the extension instance.
* @return Mock configuration instance.
* @throws ClassNotFoundException If corresponding configuration schema class is not found.
* @see #supportType(Class)
*/
private static Object cfgValue(
Class<?> type,
InjectConfiguration annotation,
ConfigurationAsmGenerator cgen
) throws ClassNotFoundException {
// Trying to find a schema class using configuration naming convention. This code won't work for inner Java
// classes, extension is designed to mock actual configurations from public API to configure Ignite components.
Class<?> schemaClass = Class.forName(type.getCanonicalName() + "Schema");
// Internal configuration extensions are not yet supported. This will probably be changed in the future.
cgen.compileRootSchema(schemaClass, Map.of());
// RootKey must be mocked, there's no way to instantiate it using a public constructor.
RootKey rootKey = mock(RootKey.class);
when(rootKey.key()).thenReturn("mock");
when(rootKey.type()).thenReturn(LOCAL);
when(rootKey.schemaClass()).thenReturn(schemaClass);
when(rootKey.internal()).thenReturn(false);
SuperRoot superRoot = new SuperRoot(s -> new RootInnerNode(rootKey, cgen.instantiateNode(schemaClass)));
ConfigObject hoconCfg = ConfigFactory.parseString(annotation.value()).root();
HoconConverter.hoconSource(hoconCfg).descend(superRoot);
ConfigurationUtil.addDefaults(superRoot);
// Reference to the super root is required to make DynamicConfigurationChanger#change method atomic.
var superRootRef = new AtomicReference<>(superRoot);
return cgen.instantiateCfg(rootKey, new DynamicConfigurationChanger() {
/** {@inheritDoc} */
@Override public CompletableFuture<Void> change(ConfigurationSource change) {
while (true) {
SuperRoot sr = superRootRef.get();
SuperRoot copy = sr.copy();
change.descend(copy);
if (superRootRef.compareAndSet(sr, copy))
return completedFuture(null);
}
}
/** {@inheritDoc} */
@Override public InnerNode getRootNode(RootKey<?, ?> rk) {
return superRootRef.get().getRoot(rk);
}
});
}
/**
* Looks for the annotated field inside the given test class.
*
* @return Annotated fields.
*/
private static List<Field> getMatchingFields(Class<?> testClass) {
return AnnotationSupport.findAnnotatedFields(
testClass,
InjectConfiguration.class,
field -> supportType(field.getType()),
HierarchyTraversalMode.TOP_DOWN
);
}
/**
* Checks that instance of the given class can be injected by the extension.
*
* @param type Field or parameter type.
* @return {@code true} if value of the given class can be injected.
*/
private static boolean supportType(Class<?> type) {
return type.getCanonicalName().endsWith("Configuration");
}
}