/* * Copyright 2017-2020 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 * * https://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.cloud.function.deployer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.function.Function; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.util.function.Tuple2; import reactor.util.function.Tuples; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.function.context.FunctionCatalog; import org.springframework.context.ApplicationContext; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; import static org.assertj.core.api.Assertions.assertThat; /** * * @author Oleg Zhurakousky * @since 3.0 */ public class FunctionDeployerTests { @BeforeEach public void before() { System.clearProperty("spring.cloud.function.definition"); } /* * Target function `class UpperCaseFunction implements Function<String, String>` * Main/Start class present, no Spring configuration */ @Test public void testWithMainAndStartClassNoSpringConfiguration() throws Exception { String[] args = new String[] { "--spring.cloud.function.location=target/it/bootjar/target/bootjar-1.0.0.RELEASE-exec.jar", "--spring.cloud.function.function-class=function.example.UpperCaseFunction;function.example.ReverseFunction" }; ApplicationContext context = SpringApplication.run(DeployerApplication.class, args); FunctionCatalog catalog = context.getBean(FunctionCatalog.class); Function<String, String> function = catalog.lookup("upperCaseFunction"); assertThat(function.apply("bob")).isEqualTo("BOB"); assertThat(function.apply("stacy")).isEqualTo("STACY"); function = catalog.lookup("reverseFunction"); assertThat(function.apply("bob")).isEqualTo("bob"); assertThat(function.apply("stacy")).isEqualTo("ycats"); Function<Flux<String>, Flux<String>> functionAsFlux = catalog.lookup("upperCaseFunction"); List<String> results = functionAsFlux.apply(Flux.just("bob", "stacy")).collectList().block(); assertThat(results.get(0)).isEqualTo("BOB"); assertThat(results.get(1)).isEqualTo("STACY"); functionAsFlux = catalog.lookup("reverseFunction"); results = functionAsFlux.apply(Flux.just("bob", "stacy")).collectList().block(); assertThat(results.get(0)).isEqualTo("bob"); assertThat(results.get(1)).isEqualTo("ycats"); } @Test public void testWithSimplestJar() throws Exception { String[] args = new String[] { "--spring.cloud.function.location=target/it/simplestjar/target/simplestjar-1.0.0.RELEASE.jar", "--spring.cloud.function.function-class=function.example.UpperCaseFunction" }; ApplicationContext context = SpringApplication.run(DeployerApplication.class, args); FunctionCatalog catalog = context.getBean(FunctionCatalog.class); Function<String, String> function = catalog.lookup("upperCaseFunction"); assertThat(function.apply("bob")).isEqualTo("BOB"); assertThat(function.apply("stacy")).isEqualTo("STACY"); Function<Flux<String>, Flux<String>> functionAsFlux = catalog.lookup("upperCaseFunction"); List<String> results = functionAsFlux.apply(Flux.just("bob", "stacy")).collectList().block(); assertThat(results.get(0)).isEqualTo("BOB"); assertThat(results.get(1)).isEqualTo("STACY"); } @Test public void testWithSimplestJarExploaded() throws Exception { String[] args = new String[] { "--spring.cloud.function.location=target/it/simplestjar/target/classes", "--spring.cloud.function.function-class=function.example.UpperCaseFunction" }; ApplicationContext context = SpringApplication.run(DeployerApplication.class, args); FunctionCatalog catalog = context.getBean(FunctionCatalog.class); Function<String, String> function = catalog.lookup("upperCaseFunction"); assertThat(function.apply("bob")).isEqualTo("BOB"); assertThat(function.apply("stacy")).isEqualTo("STACY"); Function<Flux<String>, Flux<String>> functionAsFlux = catalog.lookup("upperCaseFunction"); List<String> results = functionAsFlux.apply(Flux.just("bob", "stacy")).collectList().block(); assertThat(results.get(0)).isEqualTo("BOB"); assertThat(results.get(1)).isEqualTo("STACY"); } /* * Target function `class UpperCaseFunction implements Function<String, String>` * No Main/Start class present, no Spring configuration */ @Test public void testNoMainAndNoStartClassAndNoSpringConfiguration() throws Exception { String[] args = new String[] { "--spring.cloud.function.location=target/it/bootjarnostart/target/bootjarnostart-1.0.0.RELEASE-exec.jar", "--spring.cloud.function.function-class=function.example.UpperCaseFunction" }; ApplicationContext context = SpringApplication.run(DeployerApplication.class, args); FunctionCatalog catalog = context.getBean(FunctionCatalog.class); Function<String, String> function = catalog.lookup("upperCaseFunction"); assertThat(function.apply("bob")).isEqualTo("BOB"); assertThat(function.apply("stacy")).isEqualTo("STACY"); Function<Flux<String>, Flux<String>> functionAsFlux = catalog.lookup("upperCaseFunction"); List<String> results = functionAsFlux.apply(Flux.just("bob", "stacy")).collectList().block(); assertThat(results.get(0)).isEqualTo("BOB"); assertThat(results.get(1)).isEqualTo("STACY"); } /* * Target function `class UpperCaseFunction implements Function<String, String>` * No Main/Start class present, no Spring configuration * * Function class is discovered via 'Function-Class` manifest entry */ @Test public void testNoMainAndNoStartClassAndNoSpringConfigurationDiscoverClassFromManifest() throws Exception { String[] args = new String[] { "--spring.cloud.function.location=target/it/bootjarnostart/target/bootjarnostart-1.0.0.RELEASE-exec.jar" }; ApplicationContext context = SpringApplication.run(DeployerApplication.class, args); FunctionCatalog catalog = context.getBean(FunctionCatalog.class); Function<String, String> function = catalog.lookup("upperCaseFunction"); assertThat(function.apply("bob")).isEqualTo("BOB"); assertThat(function.apply("stacy")).isEqualTo("STACY"); Function<Flux<String>, Flux<String>> functionAsFlux = catalog.lookup("upperCaseFunction"); List<String> results = functionAsFlux.apply(Flux.just("bob", "stacy")).collectList().block(); assertThat(results.get(0)).isEqualTo("BOB"); assertThat(results.get(1)).isEqualTo("STACY"); } /* * Target function: * * @Bean public Function<String, String> uppercase() */ @Test public void testWithMainAndStartClassAndSpringConfiguration() throws Exception { String[] args = new String[] { "--spring.cloud.function.location=target/it/bootapp/target/bootapp-1.0.0.RELEASE-exec.jar", "--spring.cloud.function.definition=uppercase" }; ApplicationContext context = SpringApplication.run(DeployerApplication.class, args); FunctionCatalog catalog = context.getBean(FunctionCatalog.class); Function<Message<byte[]>, Message<byte[]>> function = catalog.lookup("uppercase", "application/json"); Message<byte[]> result = function .apply(MessageBuilder.withPayload("\"bob\"".getBytes(StandardCharsets.UTF_8)).build()); assertThat(new String(result.getPayload(), StandardCharsets.UTF_8)).isEqualTo("\"BOB\""); } @Test public void testWithLegacyProperties() throws Exception { String[] args = new String[] { "--function.location=target/it/bootapp/target/bootapp-1.0.0.RELEASE-exec.jar", "--function.name=uppercase" }; ApplicationContext context = SpringApplication.run(DeployerApplication.class, args); FunctionCatalog catalog = context.getBean(FunctionCatalog.class); Function<Message<byte[]>, Message<byte[]>> function = catalog.lookup("uppercase", "application/json"); Message<byte[]> result = function .apply(MessageBuilder.withPayload("\"bob\"".getBytes(StandardCharsets.UTF_8)).build()); assertThat(new String(result.getPayload(), StandardCharsets.UTF_8)).isEqualTo("\"BOB\""); } /* * Same as above but: * Given that Java 11 does not include 'javax' packages, this test simply validates that * the delegation will be made to archive loader where it is available */ @Test public void testWithMainAndStartClassAndSpringConfigurationJavax() throws Exception { String[] args = new String[] { "--spring.cloud.function.location=target/it/bootapp-with-javax/target/bootapp-with-javax-1.0.0.RELEASE-exec.jar", "--spring.cloud.function.function-name=uppercase" }; ApplicationContext context = SpringApplication.run(DeployerApplication.class, args); FunctionCatalog catalog = context.getBean(FunctionCatalog.class); Function<Message<byte[]>, Message<byte[]>> function = catalog.lookup("uppercase", "application/json"); Message<byte[]> result = function .apply(MessageBuilder.withPayload("\"[email protected]\"".getBytes(StandardCharsets.UTF_8)).build()); assertThat(new String(result.getPayload(), StandardCharsets.UTF_8)).isEqualTo("\"[email protected]\""); } /* * Target function: * * @Bean public Function<String, String> uppercase() * * this contains SCF on classpath */ @Test public void testWithMainAndStartClassAndSpringConfigurationAndSCFOnClasspath() throws Exception { String[] args = new String[] { "--spring.cloud.function.location=target/it/bootapp-with-scf/target/bootapp-with-scf-1.0.0.RELEASE-exec.jar", "--spring.cloud.function.function-name=uppercase" }; ApplicationContext context = SpringApplication.run(DeployerApplication.class, args); FunctionCatalog catalog = context.getBean(FunctionCatalog.class); Function<Message<byte[]>, Message<byte[]>> function = catalog.lookup("uppercase", "application/json"); Message<byte[]> result = function .apply(MessageBuilder.withPayload("\"bob\"".getBytes(StandardCharsets.UTF_8)).build()); assertThat(new String(result.getPayload(), StandardCharsets.UTF_8)).isEqualTo("\"BOB\""); } /* * Target function: * * @Bean public Function<Person, Person> uppercasePerson() */ @Test public void testWithMainAndStartClassAndSpringConfigurationAndTypeConversion() throws Exception { String[] args = new String[] { "--spring.cloud.function.location=target/it/bootapp/target/bootapp-1.0.0.RELEASE-exec.jar", "--spring.cloud.function.function-name=uppercasePerson" }; ApplicationContext context = SpringApplication.run(DeployerApplication.class, args); FunctionCatalog catalog = context.getBean(FunctionCatalog.class); Function<Message<byte[]>, Message<byte[]>> function = catalog.lookup("uppercasePerson", "application/json"); Message<byte[]> result = function.apply( MessageBuilder.withPayload("{\"name\":\"bob\",\"id\":1}".getBytes(StandardCharsets.UTF_8)).build()); assertThat(new String(result.getPayload(), StandardCharsets.UTF_8)).isEqualTo("{\"name\":\"BOB\",\"id\":1}"); } /* * Target Function * * @Bean Function<Tuple2<Flux<String>, Flux<Integer>>, Tuple2<Flux<Double>, Flux<String>>> */ @Test public void testBootAppWithMultipleInputOutput() { String[] args = new String[] { "--spring.cloud.function.location=target/it/bootapp-multi/target/bootapp-multi-1.0.0.RELEASE-exec.jar", "--spring.cloud.function.function-name=fn" }; ApplicationContext context = SpringApplication.run(DeployerApplication.class, args); FunctionCatalog catalog = context.getBean(FunctionCatalog.class); Function<Tuple2<Flux<Message<byte[]>>, Flux<Message<byte[]>>>, Flux<Message<byte[]>>> multiInputFunction = catalog .lookup("fn", "application/json"); Message<byte[]> carEventMessage = MessageBuilder.withPayload("{\"carEvent\":\"CAR IS BUILT\"}".getBytes()).build(); Message<byte[]> checkoutEventMessage = MessageBuilder.withPayload("{\"checkoutEvent\":\"CAR IS CHECKED OUT\"}".getBytes()).build(); Flux<Message<byte[]>> carEventStream = Flux.just(carEventMessage); Flux<Message<byte[]>> checkoutEventStream = Flux.just(checkoutEventMessage); Flux<Message<byte[]>> result = multiInputFunction.apply(Tuples.of(carEventStream, checkoutEventStream)); byte[] resutBytes = result.blockFirst().getPayload(); assertThat(resutBytes).isEqualTo("{\"orderEvent\":\"CartEvent: CAR IS BUILT- CheckoutEvent: CAR IS CHECKED OUT\"}".getBytes()); } /* * Target Function * * Function<Tuple2<Flux<String>, Flux<Integer>>, Tuple2<Flux<Double>, Flux<String>>> */ @Test public void testBootJarWithMultipleInputOutput() { String[] args = new String[] { "--spring.cloud.function.location=target/it/bootjar-multi/target/bootjar-multi-1.0.0.RELEASE-exec.jar", "--spring.cloud.function.function-class=function.example.Repeater" }; ApplicationContext context = SpringApplication.run(DeployerApplication.class, args); FunctionCatalog catalog = context.getBean(FunctionCatalog.class); Function<Tuple2<Flux<Message<byte[]>>, Flux<Message<byte[]>>>, Tuple2<Flux<Message<byte[]>>, Flux<Message<byte[]>>>> function = catalog.lookup("repeater", "application/json", "application/json"); Message<byte[]> msg1 = MessageBuilder.withPayload("\"one\"".getBytes()).build(); Message<byte[]> msg2 = MessageBuilder.withPayload("\"two\"".getBytes()).build(); Flux<Message<byte[]>> inputOne = Flux.just(msg1, msg2); Message<byte[]> msgInt1 = MessageBuilder.withPayload("\"1\"".getBytes()).build(); Message<byte[]> msgInt2 = MessageBuilder.withPayload("\"2\"".getBytes()).build(); Flux<Message<byte[]>> inputTwo = Flux.just(msgInt1, msgInt2); Tuple2<Flux<Message<byte[]>>, Flux<Message<byte[]>>> result = function.apply(Tuples.of(inputOne, inputTwo)); List<String> result1 = new ArrayList<>(); List<String> result2 = new ArrayList<>(); result.getT1().subscribe(message -> { result1.add(new String(message.getPayload())); }); result.getT2().subscribe(message -> { result2.add(new String(message.getPayload())); }); assertThat(result1.get(0)).isEqualTo("\"one\""); assertThat(result1.get(1)).isEqualTo("\"two\""); assertThat(result2.get(0)).isEqualTo("3"); assertThat(result2.get(1)).isEqualTo("2"); } // same as previous test, but lookup is empty @Test public void testBootJarWithMultipleInputOutputEmptyLookup() { String[] args = new String[] { "--spring.cloud.function.location=target/it/bootjar-multi/target/bootjar-multi-1.0.0.RELEASE-exec.jar", "--spring.cloud.function.function-class=function.example.Repeater" }; ApplicationContext context = SpringApplication.run(DeployerApplication.class, args); FunctionCatalog catalog = context.getBean(FunctionCatalog.class); Function<Tuple2<Flux<Message<byte[]>>, Flux<Message<byte[]>>>, Tuple2<Flux<Message<byte[]>>, Flux<Message<byte[]>>>> function = catalog.lookup("", "application/json", "application/json"); Message<byte[]> msg1 = MessageBuilder.withPayload("\"one\"".getBytes()).build(); Message<byte[]> msg2 = MessageBuilder.withPayload("\"two\"".getBytes()).build(); Flux<Message<byte[]>> inputOne = Flux.just(msg1, msg2); Message<byte[]> msgInt1 = MessageBuilder.withPayload("\"1\"".getBytes()).build(); Message<byte[]> msgInt2 = MessageBuilder.withPayload("\"2\"".getBytes()).build(); Flux<Message<byte[]>> inputTwo = Flux.just(msgInt1, msgInt2); Tuple2<Flux<Message<byte[]>>, Flux<Message<byte[]>>> result = function.apply(Tuples.of(inputOne, inputTwo)); List<String> result1 = new ArrayList<>(); List<String> result2 = new ArrayList<>(); result.getT1().subscribe(message -> { result1.add(new String(message.getPayload())); }); result.getT2().subscribe(message -> { result2.add(new String(message.getPayload())); }); assertThat(result1.get(0)).isEqualTo("\"one\""); assertThat(result1.get(1)).isEqualTo("\"two\""); assertThat(result2.get(0)).isEqualTo("3"); assertThat(result2.get(1)).isEqualTo("2"); } @SpringBootApplication(proxyBeanMethods = false) private static class DeployerApplication { } }