From 5d2e962ff762b344fedb978f0c507630f2587e39 Mon Sep 17 00:00:00 2001 From: dzou Date: Fri, 3 Apr 2020 11:16:04 -0400 Subject: [PATCH] Add GCF integration tests Add Integration Tests for GcfSpringBootHttpRequestHandler2 fix up fix build cleanup after merge Added process-based server integration test support some more refactoring remove unneeded maven deps address Dmitry and Dans feedback --- .../spring-cloud-function-adapter-gcp/pom.xml | 5 + .../FunctionInvokerIntegrationTests.java | 102 ++++++++++ .../integration/LocalServerTestSupport.java | 190 ++++++++++++++++++ 3 files changed, 297 insertions(+) create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcloud/integration/FunctionInvokerIntegrationTests.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcloud/integration/LocalServerTestSupport.java diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/pom.xml b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/pom.xml index 33d273cef..8f4f335b6 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/pom.xml +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/pom.xml @@ -42,6 +42,11 @@ spring-boot-starter-test test + + org.springframework + spring-web + test + com.google.cloud.functions.invoker java-function-invoker diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcloud/integration/FunctionInvokerIntegrationTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcloud/integration/FunctionInvokerIntegrationTests.java new file mode 100644 index 000000000..dc5f73f02 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcloud/integration/FunctionInvokerIntegrationTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012-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.adapter.gcloud.integration; + +import java.io.IOException; +import java.util.function.Function; + +import org.junit.Test; + +import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import static org.springframework.cloud.function.adapter.gcloud.integration.LocalServerTestSupport.verify; + +/** + * Integration tests for GCF Http Functions. + * + * @author Daniel Zou + * @author Mike Eltsufin + */ +public class FunctionInvokerIntegrationTests { + + @Test + public void testSingular() { + verify(CloudFunctionMainSingular.class, null, "hello", "HELLO"); + } + + @Test + public void testUppercase() throws InterruptedException, IOException { + verify(CloudFunctionMain.class, "uppercase", "hello", "HELLO"); + + } + + @Test + public void testFooBar() { + verify(CloudFunctionMain.class, "foobar", new Foo("Hi"), new Bar("Hi")); + } + + @Configuration + @Import({ ContextFunctionCatalogAutoConfiguration.class }) + static class CloudFunctionMainSingular { + + @Bean + Function uppercase() { + return input -> input.toUpperCase(); + } + + } + + @Configuration + @Import({ ContextFunctionCatalogAutoConfiguration.class }) + static class CloudFunctionMain { + + @Bean + Function uppercase() { + return input -> input.toUpperCase(); + } + + @Bean + Function foobar() { + return input -> new Bar(input.value); + } + + } + + private static class Foo { + + String value; + + Foo(String value) { + this.value = value; + } + + } + + private static class Bar { + + String value; + + Bar(String value) { + this.value = value; + } + + } + +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcloud/integration/LocalServerTestSupport.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcloud/integration/LocalServerTestSupport.java new file mode 100644 index 000000000..4061c5d1f --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcloud/integration/LocalServerTestSupport.java @@ -0,0 +1,190 @@ +/* + * Copyright 2012-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.adapter.gcloud.integration; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import com.google.cloud.functions.invoker.runner.Invoker; +import com.google.gson.Gson; + +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.cloud.function.adapter.gcloud.FunctionInvoker; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test support class for running tests on the local Cloud Function server. + * + * @author Daniel Zou + * @author Mike Eltsufin + */ +public class LocalServerTestSupport { + + private static final Gson gson = new Gson(); + + private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool(); + + private static final String SERVER_READY_STRING = "Started ServerConnector"; + + private static AtomicInteger nextPort = new AtomicInteger(8080); + + /** + * Starts up the Cloud Function Server and executes the test + */ + public static void verify(Class mainClass, String function, I input, O expectedOutput) { + try (ServerProcess serverProcess = LocalServerTestSupport.startServer(mainClass, function)) { + TestRestTemplate testRestTemplate = new TestRestTemplate(); + + HttpHeaders headers = new HttpHeaders(); + + ResponseEntity response = testRestTemplate.postForEntity( + "http://localhost:" + serverProcess.getPort(), new HttpEntity<>(gson.toJson(input), headers), + String.class); + + assertThat(response.getBody()).isEqualTo(gson.toJson(expectedOutput)); + } + catch (Exception e) { + e.printStackTrace(); + } + } + + private static ServerProcess startServer(Class springApplicationMainClass, String function) + throws InterruptedException, IOException { + int port = nextPort.getAndIncrement(); + + String signatureType = "http"; + String target = FunctionInvoker.class.getCanonicalName(); + + File javaHome = new File(System.getProperty("java.home")); + assertThat(javaHome.exists()).isTrue(); + File javaBin = new File(javaHome, "bin"); + File javaCommand = new File(javaBin, "java"); + assertThat(javaCommand.exists()).isTrue(); + String myClassPath = System.getProperty("java.class.path"); + assertThat(myClassPath).isNotNull(); + + List command = new ArrayList<>(); + command.addAll(Arrays.asList(javaCommand.toString(), "-classpath", myClassPath, Invoker.class.getName())); + + ProcessBuilder processBuilder = new ProcessBuilder().command(command).redirectErrorStream(true); + Map environment = new HashMap<>(); + environment.put("PORT", String.valueOf(port)); + environment.put("K_SERVICE", "test-function"); + environment.put("FUNCTION_SIGNATURE_TYPE", signatureType); + environment.put("FUNCTION_TARGET", target); + environment.put("MAIN_CLASS", springApplicationMainClass.getCanonicalName()); + if (function != null) { + environment.put("spring.cloud.function.definition", function); + } + processBuilder.environment().putAll(environment); + Process serverProcess = processBuilder.start(); + CountDownLatch ready = new CountDownLatch(1); + StringBuilder output = new StringBuilder(); + Future outputMonitorResult = EXECUTOR + .submit(() -> monitorOutput(serverProcess.getInputStream(), ready, output)); + boolean serverReady = ready.await(5, TimeUnit.SECONDS); + if (!serverReady) { + serverProcess.destroy(); + throw new AssertionError("Server never became ready"); + } + return new ServerProcess(serverProcess, outputMonitorResult, output, port); + } + + private static void monitorOutput(InputStream processOutput, CountDownLatch ready, StringBuilder output) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(processOutput))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.contains(SERVER_READY_STRING)) { + ready.countDown(); + } + System.out.println(line); + synchronized (output) { + output.append(line).append('\n'); + } + if (line.contains("WARNING")) { + throw new AssertionError("Found warning in server output:\n" + line); + } + } + } + catch (IOException e) { + e.printStackTrace(); + throw new UncheckedIOException(e); + } + } + + private static class ServerProcess implements AutoCloseable { + + private final Process process; + + private final Future outputMonitorResult; + + private final StringBuilder output; + + private final int port; + + ServerProcess(Process process, Future outputMonitorResult, StringBuilder output, int port) { + this.process = process; + this.outputMonitorResult = outputMonitorResult; + this.output = output; + this.port = port; + } + + Process process() { + return process; + } + + Future outputMonitorResult() { + return outputMonitorResult; + } + + String output() { + synchronized (output) { + return output.toString(); + } + } + + @Override + public void close() { + process().destroy(); + } + + public int getPort() { + return port; + } + + } + +}