From 05267ee8bf57c599d37a20b5df8c2f93d731da8c Mon Sep 17 00:00:00 2001 From: John Blum Date: Wed, 23 May 2018 18:42:40 -0700 Subject: [PATCH] Add integration tests for Function implementation and execution auto-configuration. --- .../FunctionExecutionAutoConfiguration.java | 9 +- ...onExecutionAutoConfigurationExtension.java | 79 ++++++++ ...onExecutionAutoConfigurationRegistrar.java | 36 ++++ .../execution/AbstractResultCollector.java | 89 +++++++++ .../SingleResultReturningCollector.java | 58 ++++++ ...redFunctionExecutionsIntegrationTests.java | 179 ++++++++++++++++++ .../geode/function/executions/Calculator.java | 47 +++++ 7 files changed, 495 insertions(+), 2 deletions(-) create mode 100644 geode-spring-boot-starter/src/main/java/org/springframework/data/gemfire/function/config/AbstractFunctionExecutionAutoConfigurationExtension.java create mode 100644 geode-spring-boot-starter/src/main/java/org/springframework/data/gemfire/function/config/GemFireFunctionExecutionAutoConfigurationRegistrar.java create mode 100644 geode-spring-boot-starter/src/main/java/org/springframework/data/gemfire/function/execution/AbstractResultCollector.java create mode 100644 geode-spring-boot-starter/src/main/java/org/springframework/data/gemfire/function/execution/SingleResultReturningCollector.java create mode 100644 geode-spring-boot-starter/src/test/java/org/springframework/boot/data/geode/function/AutoConfiguredFunctionExecutionsIntegrationTests.java create mode 100644 geode-spring-boot-starter/src/test/java/org/springframework/boot/data/geode/function/executions/Calculator.java diff --git a/geode-spring-boot-starter/src/main/java/org/springframework/boot/data/geode/autoconfigure/FunctionExecutionAutoConfiguration.java b/geode-spring-boot-starter/src/main/java/org/springframework/boot/data/geode/autoconfigure/FunctionExecutionAutoConfiguration.java index 59ba5aae..6b99747a 100644 --- a/geode-spring-boot-starter/src/main/java/org/springframework/boot/data/geode/autoconfigure/FunctionExecutionAutoConfiguration.java +++ b/geode-spring-boot-starter/src/main/java/org/springframework/boot/data/geode/autoconfigure/FunctionExecutionAutoConfiguration.java @@ -22,7 +22,9 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.context.annotation.Configuration; -import org.springframework.data.gemfire.function.config.EnableGemfireFunctionExecutions; +import org.springframework.context.annotation.Import; +import org.springframework.data.gemfire.function.config.EnableGemfireFunctions; +import org.springframework.data.gemfire.function.config.GemFireFunctionExecutionAutoConfigurationRegistrar; import org.springframework.data.gemfire.function.execution.GemfireFunctionOperations; /** @@ -33,7 +35,9 @@ import org.springframework.data.gemfire.function.execution.GemfireFunctionOperat * @see org.apache.geode.cache.GemFireCache * @see org.springframework.boot.data.geode.autoconfigure.ClientCacheAutoConfiguration * @see org.springframework.context.annotation.Configuration + * @see org.springframework.data.gemfire.function.config.EnableGemfireFunctions * @see org.springframework.data.gemfire.function.config.EnableGemfireFunctionExecutions + * @see org.springframework.data.gemfire.function.config.GemFireFunctionExecutionAutoConfigurationRegistrar * @see org.springframework.data.gemfire.function.execution.GemfireFunctionOperations * @since 1.0.0 */ @@ -41,7 +45,8 @@ import org.springframework.data.gemfire.function.execution.GemfireFunctionOperat @ConditionalOnBean(GemFireCache.class) @ConditionalOnClass({ GemfireFunctionOperations.class, GemFireCache.class }) @AutoConfigureAfter(ClientCacheAutoConfiguration.class) -@EnableGemfireFunctionExecutions +@EnableGemfireFunctions +@Import(GemFireFunctionExecutionAutoConfigurationRegistrar.class) @SuppressWarnings("unused") public class FunctionExecutionAutoConfiguration { diff --git a/geode-spring-boot-starter/src/main/java/org/springframework/data/gemfire/function/config/AbstractFunctionExecutionAutoConfigurationExtension.java b/geode-spring-boot-starter/src/main/java/org/springframework/data/gemfire/function/config/AbstractFunctionExecutionAutoConfigurationExtension.java new file mode 100644 index 00000000..a5d84344 --- /dev/null +++ b/geode-spring-boot-starter/src/main/java/org/springframework/data/gemfire/function/config/AbstractFunctionExecutionAutoConfigurationExtension.java @@ -0,0 +1,79 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed 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.springframework.data.gemfire.function.config; + +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException; + +import java.util.Optional; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.boot.autoconfigure.AutoConfigurationPackages; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.StandardAnnotationMetadata; + +/** + * The AbstractFunctionExecutionAutoConfigurationExtension class... + * + * @author John Blum + * @since 1.0.0 + */ +// TODO replace this class once SD Lovelace is GA and SBDG is rebased on SD Lovelace +public abstract class AbstractFunctionExecutionAutoConfigurationExtension + extends FunctionExecutionBeanDefinitionRegistrar implements BeanFactoryAware { + + private BeanFactory beanFactory; + + @Override + public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) { + registerBeanDefinitions(newAnnotationBasedFunctionExecutionConfigurationSource(annotationMetadata), registry); + } + + @SuppressWarnings("unused") + private AbstractFunctionExecutionConfigurationSource newAnnotationBasedFunctionExecutionConfigurationSource( + AnnotationMetadata annotationMetadata) { + + StandardAnnotationMetadata metadata = + new StandardAnnotationMetadata(getConfiguration(), true); + + return new AnnotationFunctionExecutionConfigurationSource(metadata) { + + @Override + public Iterable getBasePackages() { + return AutoConfigurationPackages.get(getBeanFactory()); + } + }; + } + + @Override + @SuppressWarnings("all") + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + @SuppressWarnings("all") + protected BeanFactory getBeanFactory() { + + return Optional.ofNullable(this.beanFactory) + .orElseThrow(() -> newIllegalStateException("BeanFactory was not properly configured")); + } + + protected abstract Class getConfiguration(); + +} diff --git a/geode-spring-boot-starter/src/main/java/org/springframework/data/gemfire/function/config/GemFireFunctionExecutionAutoConfigurationRegistrar.java b/geode-spring-boot-starter/src/main/java/org/springframework/data/gemfire/function/config/GemFireFunctionExecutionAutoConfigurationRegistrar.java new file mode 100644 index 00000000..d067a3b7 --- /dev/null +++ b/geode-spring-boot-starter/src/main/java/org/springframework/data/gemfire/function/config/GemFireFunctionExecutionAutoConfigurationRegistrar.java @@ -0,0 +1,36 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed 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.springframework.data.gemfire.function.config; + +/** + * The GemFireFunctionExecutionAutoConfigurationRegistrar class... + * + * @author John Blum + * @since 1.0.0 + */ +public class GemFireFunctionExecutionAutoConfigurationRegistrar + extends AbstractFunctionExecutionAutoConfigurationExtension { + + @Override + protected Class getConfiguration() { + return EnableGemfireFunctionExecutionsConfiguration.class; + } + + @EnableGemfireFunctionExecutions + private static class EnableGemfireFunctionExecutionsConfiguration { } + +} diff --git a/geode-spring-boot-starter/src/main/java/org/springframework/data/gemfire/function/execution/AbstractResultCollector.java b/geode-spring-boot-starter/src/main/java/org/springframework/data/gemfire/function/execution/AbstractResultCollector.java new file mode 100644 index 00000000..f7cf6d84 --- /dev/null +++ b/geode-spring-boot-starter/src/main/java/org/springframework/data/gemfire/function/execution/AbstractResultCollector.java @@ -0,0 +1,89 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed 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.springframework.data.gemfire.function.execution; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.geode.cache.execute.FunctionException; +import org.apache.geode.cache.execute.ResultCollector; + +/** + * The AbstractResultCollector class... + * + * @author John Blum + * @since 1.0.0 + */ +@SuppressWarnings("unused") +public abstract class AbstractResultCollector implements ResultCollector { + + protected static final String NOT_IMPLEMENTED = "Not Implemented"; + + protected static final TimeUnit DEFAULT_TIME_UNIT = TimeUnit.MILLISECONDS; + + private AtomicBoolean resultsEnded = new AtomicBoolean(false); + + private S result = null; + + @Override + public synchronized S getResult() throws FunctionException { + return this.result; + } + + @Override + public S getResult(long duration, TimeUnit unit) throws FunctionException, InterruptedException { + + unit = resolveTimeUnit(unit); + + long durationInMilliseconds = unit.toMillis(duration); + long timeout = System.currentTimeMillis() + unit.toMillis(duration); + long waitInMilliseconds = Math.max(durationInMilliseconds, + Math.min(durationInMilliseconds / 5, durationInMilliseconds)); + + while (getResult() == null && System.currentTimeMillis() < timeout) { + unit.timedWait(this, waitInMilliseconds); + } + + return getResult(); + } + + protected synchronized void setResult(S result) { + this.result = result; + } + + protected TimeUnit resolveTimeUnit(TimeUnit unit) { + return unit != null ? unit : DEFAULT_TIME_UNIT; + } + + @Override + public void clearResults() { + this.result = null; + } + + @Override + public void endResults() { + this.resultsEnded.set(true); + } + + protected boolean hasResultsEnded() { + return this.resultsEnded.get(); + } + + protected boolean hasResultsNotEnded() { + return !this.resultsEnded.get(); + } +} diff --git a/geode-spring-boot-starter/src/main/java/org/springframework/data/gemfire/function/execution/SingleResultReturningCollector.java b/geode-spring-boot-starter/src/main/java/org/springframework/data/gemfire/function/execution/SingleResultReturningCollector.java new file mode 100644 index 00000000..e5ea232d --- /dev/null +++ b/geode-spring-boot-starter/src/main/java/org/springframework/data/gemfire/function/execution/SingleResultReturningCollector.java @@ -0,0 +1,58 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed 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.springframework.data.gemfire.function.execution; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Optional; + +import org.apache.geode.distributed.DistributedMember; + +/** + * The SingleResultReturningCollector class... + * + * @author John Blum + * @since 1.0.0 + */ +public class SingleResultReturningCollector extends AbstractResultCollector { + + @Override + public void addResult(DistributedMember memberID, T resultOfSingleExecution) { + setResult(extractSingleResult(resultOfSingleExecution)); + } + + @SuppressWarnings("unchecked") + private T extractSingleResult(Object result) { + + return (T) Optional.ofNullable(result) + .filter(this::isInstanceOfIterable) + .map(it -> (Iterable) it) + .map(this::toIterator) + .filter(Iterator::hasNext) + .map(Iterator::next) + .map(this::extractSingleResult) + .orElse(result); + } + + private boolean isInstanceOfIterable(Object obj) { + return obj instanceof Iterable; + } + + private Iterator toIterator(Iterable obj) { + return obj != null ? obj.iterator() : Collections.emptyIterator(); + } +} diff --git a/geode-spring-boot-starter/src/test/java/org/springframework/boot/data/geode/function/AutoConfiguredFunctionExecutionsIntegrationTests.java b/geode-spring-boot-starter/src/test/java/org/springframework/boot/data/geode/function/AutoConfiguredFunctionExecutionsIntegrationTests.java new file mode 100644 index 00000000..246ad79c --- /dev/null +++ b/geode-spring-boot-starter/src/test/java/org/springframework/boot/data/geode/function/AutoConfiguredFunctionExecutionsIntegrationTests.java @@ -0,0 +1,179 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed 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.springframework.boot.data.geode.function; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Optional; + +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.cache.RegionAttributes; +import org.apache.geode.cache.Scope; +import org.apache.geode.cache.client.ClientRegionShortcut; +import org.apache.geode.cache.execute.FunctionService; +import org.apache.shiro.util.Assert; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.data.geode.function.executions.Calculator; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.data.gemfire.RegionAttributesFactoryBean; +import org.springframework.data.gemfire.client.ClientRegionFactoryBean; +import org.springframework.data.gemfire.config.annotation.EnableLogging; +import org.springframework.data.gemfire.function.annotation.GemfireFunction; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * The AutoConfiguredFunctionExecutionsIntegrationTests class... + * + * @author John Blum + * @since 1.0.0 + */ +@RunWith(SpringRunner.class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@SuppressWarnings("unused") +public class AutoConfiguredFunctionExecutionsIntegrationTests { + + private static final String GEMFIRE_LOG_LEVEL = "error"; + + @Autowired + private Calculator calculator; + + @Test + public void firstFunctionsMustBeRegistered() { + + Arrays.stream(CalculatorFunctions.class.getMethods()) + .filter(method -> !Object.class.equals(method.getDeclaringClass())) + .map(Method::getName) + .forEach(methodName -> assertThat(FunctionService.isRegistered(methodName)) + .describedAs("Function [%s] was not registered", methodName) + .isTrue()); + } + + @Test + public void thenCalculationsAreCorrect() { + + assertThat(this.calculator).isNotNull(); + assertThat(extractResult(this.calculator.add(8.0d, 8.0d))).isEqualTo(16.0d); + assertThat(extractResult(this.calculator.divide(16.0d, 4.0d))).isEqualTo(4.0d); + assertThat(extractResult(this.calculator.factorial(5L))).isEqualTo(120L); + assertThat(extractResult(this.calculator.multiply(4.0d, 4.0d))).isEqualTo(16.0d); + assertThat(extractResult(this.calculator.squared(4.0d))).isEqualTo(16.0d); + assertThat(extractResult(this.calculator.squareRoot(16.0d))).isEqualTo(4.0d); + assertThat(extractResult(this.calculator.subtract(16.0d, 8.0d))).isEqualTo(8.0d); + } + + private Object extractResult(Object result) { + + return Optional.ofNullable(result) + .filter(it -> it instanceof Iterable) + .map(it -> ((Iterable) it).iterator()) + .filter(Iterator::hasNext) + .map(Iterator::next) + .map(this::extractResult) + .orElse(result); + } + + @SpringBootApplication + @EnableLogging(logLevel = GEMFIRE_LOG_LEVEL) + static class TestConfiguration { + + @Bean("Calculations") + public ClientRegionFactoryBean calculationsRegion(GemFireCache gemfireCache, + RegionAttributes calculationsRegionAttributes) { + + ClientRegionFactoryBean calculations = new ClientRegionFactoryBean<>(); + + calculations.setAttributes(calculationsRegionAttributes); + calculations.setCache(gemfireCache); + calculations.setClose(false); + calculations.setShortcut(ClientRegionShortcut.LOCAL_PERSISTENT); + + return calculations; + } + + @Bean + public RegionAttributesFactoryBean calculationsRegionAttributes() { + + RegionAttributesFactoryBean calculationsRegionAttributes = new RegionAttributesFactoryBean(); + + calculationsRegionAttributes.setScope(Scope.LOCAL); + + return calculationsRegionAttributes; + } + + @Bean + public CalculatorFunctions calculatorFunctions() { + return new CalculatorFunctions(); + } + } + + public static class CalculatorFunctions { + + @GemfireFunction(id = "add", hasResult = true) + public double add(double operandOne, double operandTwo) { + return operandOne + operandTwo; + } + + @GemfireFunction(id = "divide", hasResult = true) + public double divide(double numerator, double divisor) { + return numerator / divisor; + } + + @GemfireFunction(id = "factorial", hasResult = true) + public long factorial(long number) { + + Assert.isTrue(number > -1, "Number be greater than -1"); + + long result = number == 2 ? 2 : 1; + + while (number > 1) { + result *= number--; + } + + return result; + } + + @GemfireFunction(id = "multiply", hasResult = true) + public double multiply(double operandOne, double operandTwo) { + return operandOne * operandTwo; + } + + @GemfireFunction(id = "squareRoot", hasResult = true) + public double squareRoot(double number) { + return Math.sqrt(number); + } + + @GemfireFunction(id = "squared", hasResult = true) + public double squared(double number) { + return number * number; + } + + @GemfireFunction(id = "subtract", hasResult = true) + public double subtract(double operandOne, double operandTwo) { + return operandOne - operandTwo; + } + } +} diff --git a/geode-spring-boot-starter/src/test/java/org/springframework/boot/data/geode/function/executions/Calculator.java b/geode-spring-boot-starter/src/test/java/org/springframework/boot/data/geode/function/executions/Calculator.java new file mode 100644 index 00000000..701632de --- /dev/null +++ b/geode-spring-boot-starter/src/test/java/org/springframework/boot/data/geode/function/executions/Calculator.java @@ -0,0 +1,47 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed 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.springframework.boot.data.geode.function.executions; + +import org.junit.experimental.theories.suppliers.TestedOn; +import org.springframework.data.gemfire.function.annotation.OnRegion; + +/** + * The Calculator class... + * + * @author John Blum + * @since 1.0.0 + */ +@SuppressWarnings("all") +@OnRegion(region = "Calculations") +// TODO change Function returns type when SDG properly handles Function method return types/values +public interface Calculator { + + Object add(double operandOne, double operandTwo); + + Object divide(double numerator, double divisor); + + Object factorial(long number); + + Object multiply(double operandOne, double operandTwo); + + Object squareRoot(double number); + + Object squared(double number); + + Object subtract(double operandOne, double operandTwo); + +}