From 88b76f5894df31cb9c95afe5cfe94f52fb3dc863 Mon Sep 17 00:00:00 2001 From: Denis O Date: Mon, 14 Oct 2019 00:40:20 +0300 Subject: [PATCH] Upstream sync (#3) * Updating docs to reflect new CLI syntax (#138) * Updating docs to reflect new CLI syntax * Missed a few * fn-java-fdk: post-1.0.60 version bump [skip ci] * remove printenv * manual version bump * fn-java-fdk: post-1.0.62 version bump [skip ci] * Update FN container name (#139) [skip ci] Update FN container name from functions to fnserver at grabbing internal docker network IP * Updated dependency versions to reduce security problems (#143) Using snyk.io tools we found several dependencies had security vulnerabilities and could be updated. This does not remove all of them, but the remaining CVEs are in transitive dependencies brought in by 3rd party code. CVEs remain in: async-thumbnails example (low priority to fix) through - minio - wiremock fn-spring-cloud-function through - spring-cloud-function-context Additional changes were needed to adjust for API changes in updated dependencies: async-thumbnails example: - new minio client uses chunked file uploads so assertions needed changing fn-spring-cloud-function: - Several breaking API changes, and it seems now that Consumer behaviour is broken. * fn-java-fdk: post-1.0.63 version bump [skip ci] * preparations for the junit5 module - separation of the junit4 and junit5 code into different modules. - core module to share code between different TDD frameworks * cleaning of unused folders/modules * fixed deps in build-image module * fixed integration-tests examples * fixed deps in build-image module * deactivated integration-test 3 and 7 - cycle dependency to cli * fn-java-fdk: post-1.0.64 version bump [skip ci] * pom.xml files - refactoring * switched all versions to the latest available in maven central * switched all plugin-versions to the latest available in maven central * removed comment * will make the google compiler happy... * fn-java-fdk: post-1.0.65 version bump [skip ci] * Add Unix Stream codec, Split flow from FDK, update integration tests (#150) * http input codec * add jetty-core and UDS deps * PoC jetty core UDS event codec * revise FDK contract for initial relase, split flow from fn, support new unix domain socket contract * First pass of refactor * added annotations for for features to support flow * moving flow feature to runtime package * rename flow testing and add docs, fix race in Stream testing * fixing failing tests * Update to reflect FN_ID * more integration test fixing * fix up format name * first stab at home-grown UDS implementation * docker build fix * UDS rework, remove old UDS dep and test new one * adjust permissions on file on start * new integration tests * simlify build and integration tests, add tests for http-stream * fix ci * typo * make igntests standalone * itests run 1.8 * fix itest result * env for itests * add createDir * test failures in circle * fix test interference * Use standard properties in fdk tests, fix versions in itests * hmm no entropy on circle boxes :) * use consistent properties for versions everywhere * add waits for start * this time with feeling * diagnosing timeout * close input, try more aggressive testing * C errors * fixing up accept and twiddling with integration test start * set DOCKER_LOCALHOST * integration test fixing/finessing * cludge docker localhost * no_proxy messing * set fnserver IP correctly * typo * nits in C code * review nits * more C fix nits * Make mandatory headers mandatory again * minus comment * remove Content-Length from stripped input * fix call test * bump version to fix broken release [skip ci] * Initial cut at HTTP gateway prtotocol support * regen sig file * unit tests on HTTPGw Stuff * ign tests code * integration tests * update QR code demo to be HTTP Gateway friendly * tests and docs, organize inputs * Readme typos * fn-java-fdk: post-1.0.67 version bump [skip ci] * Fix typos, minor lints, and a small race condition in stream codec cleanup (#155) * Fix typos, minor lints, and a small race condition in stream codec cleanup * review feedback * debug log * add more logging * mask binary stuff in c debug code * Fix issue with uninitialised addres in accept * default REPO location in ign tests, fix race condition in Flow test tests * fix typo from #154 * bump jetty client to latest * fn-java-fdk: post-1.0.68 version bump [skip ci] * trying new delpoyment setting * fn-java-fdk: post-1.0.69 version bump [skip ci] * Try another new deployment setting (#158) * fn-java-fdk: post-1.0.70 version bump [skip ci] * make FnFeature multi-valued remove use of getAnnotationsByType (#160) * fn-java-fdk: post-1.0.71 version bump [skip ci] * Build and Init Images for substrate VM This adds a a build and init image that builds functions using substrateVM as consolidated binaries. * fn-java-fdk: post-1.0.72 version bump [skip ci] * CI/Release of substrate related images (#161) * first cut at substrate images * fix docker build * bump fdk for release * fn-java-fdk: post-1.0.74 version bump [skip ci] * Rev jackson-databind to deal with latest vulnerabilities. (#163) * Fix Jackson databind to deal with latest CVE * rev minio version used in async thumbnails test to pick up newer jetty version * fix up async thumbnails to test new minio version * fn-java-fdk: post-1.0.75 version bump [skip ci] * [164] Support http-stream protocol in substrate/native java. * This gets the local build.sh working again on the latest JDK8 * Remove comment * Include some CI fixes from @tteggel * Update file permissions on surefire CI fix script * Remove old default and http formats from boilerplate test * Add some logging to boilerplate test * Update FunctionsTest.java * Update FunctionsTest.java * Update FunctionsTest.java * Update FunctionsTest.java * Update FunctionsTest.java * Added some output to help if fn operation fails * Update IntegrationTestRule.java * Update IntegrationTestRule.java * Update IntegrationTestRule.java * Update pom.xml * tweak to surefire version and config * Add specific 8-friendly test and pom for boilerplate test. * Update FunctionsTest.java * Another attempt at logging a failed result...sigh * Rename func.yaml to prevent overwrite * Let's go again... * fn-java-fdk: post-1.0.76 version bump [skip ci] * fn-java-fdk: post-1.0.77 version bump [skip ci] * Add jdk11 build/runtime images (#169) * Add build for J11 images * Remove JDK9 VM memory options * fn-java-fdk: post-1.0.78 version bump [skip ci] * Use JRE instead of JDK for 11 runtime image, and add push of 11 images into CI release.sh * Reinstate SNAPSHOT version in init-native pom * fn-java-fdk: post-1.0.79 version bump [skip ci] * Replace java9 tests with java11 * Ooops... * Add JDK9 test back whilst CLI still supports it. * fn-java-fdk: post-1.0.80 version bump [skip ci] * Bump jackson version * fn-java-fdk: post-1.0.81 version bump [skip ci] * removed the (now) useless Docker & Format entries (#178) * fn-java-fdk: post-1.0.82 version bump [skip ci] * Native java fixes (#179) * Fixed maven surefire classloader. * Fixed SVM problem with max heap size in container. * fn-java-fdk: post-1.0.83 version bump [skip ci] * Removed old format codecs and tests * Remove Java 9 dockerfiles, update builds and tests * Update tests to java11 * Fix broken release * Bump version number past previous broken release * fn-java-fdk: post-1.0.85 version bump [skip ci] * Update HTTPGatewayFunctions.md * removed some legacy stuff (fn call & route) * A first cut at updating and simplifying the Java FDK Readme. Removed user tutorial already included in tutorials. Reorganized and simplified. More work to do reviewing each of the docs linked here. * simplifying the fdk repo readme along with mikes update * Update README.md * add generic link back to official docs * WIP fn user added to runtime image, working on adding user to the init-image * added fn user to runtime java * changes to RUN cmd syntax * trying to run fn user with correct cli flags * added user name * added gid * create app before deploy app * remove --local from create app * create app before deploy in all integ tests * deploy after context created * --create-app change to integ tests * removal of comments * fn-java-fdk: post-1.0.86 version bump [skip ci] * Update Java FDK string-reverse example * post-1.0.87 bump (manual) * fn-java-fdk: post-1.0.88 version bump [skip ci] * WIP adding logframer and tests to fdk-java, before each function invocation * WIP debugging test * Quick fix..? * checking if env variables are logged out to stdout and stderr, stdout gets redirected to stderr hence the duplication in the assertion * removing comments of test in wrong file * fn-java-fdk: post-1.0.89 version bump [skip ci] * adding explicit newline characters in print statements * editing tests * correcting \n * fn-java-fdk: post-1.0.90 version bump [skip ci] * Update pom.xml so circleci test will pass. JDK 11 not allowed currently. * fnproject/functions -> fnproject/fnserver * add fn user to jre11 runtime image * fn-java-fdk: post-1.0.91 version bump [skip ci] * fn-java-fdk: post-1.0.92 version bump [skip ci] * Re-order release artifact publishing By publishing tag to github first we ensure that no artifacts are pushed for a version which isn't tagged. Publishing to bintray last ensures that the CLI won't pick up a version which is not pushed everywhere. * Post-1.0.93 version bump [skip ci] * fn-java-fdk: post-1.0.94 version bump [skip ci] * Skip deployment of example code Do not attempt to publish artifacts for the string-reverse exapmle function * fn-java-fdk: post-1.0.95 version bump [skip ci] * Bump Jackson version * fn-java-fdk: post-1.0.96 version bump [skip ci] * Fix path of java binary in Dockerfile (#210) * fn-java-fdk: post-1.0.97 version bump [skip ci] * Updated to GraalVM 19. (#209) * fn-java-fdk: post-1.0.98 version bump [skip ci] * Add env to test server to allow setting fn limits (#214) work around changed paths in new j11 image * fn-java-fdk: post-1.0.99 version bump [skip ci] * Updated native-image to GraalVM 19.2.0. (#213) * Updated to GraalVM 19.2.0. * Fixed build-native Dockerfile. * fn-java-fdk: post-1.0.100 version bump [skip ci] * Bump Jackson version to 2.9.10 (#216) * fn-java-fdk: post-1.0.101 version bump [skip ci] * Update all names to Function Development Kit for Java per Shaun, per Legal, and for consistency. * Removed hyphen as best practice seems to be no hyphen * Updating abbrev to FDK for Java * fn-java-fdk: post-1.0.102 version bump [skip ci] --- .circleci/config.yml | 101 +-- .circleci/fix-java-for-surefire.sh | 19 + .circleci/install-go.sh | 32 - .circleci/release.sh | 101 +-- .circleci/update-versions.sh | 7 +- .gitignore | 4 + README.md | 331 +-------- api/pom.xml | 28 +- api/src/main/api/snapshot.sigfile | 337 +++------ .../java/com/fnproject/fn/api/FnFeature.java | 19 + .../java/com/fnproject/fn/api/FnFeatures.java | 17 + .../com/fnproject/fn/api/FunctionInvoker.java | 16 + .../java/com/fnproject/fn/api/Headers.java | 268 +++++++- .../java/com/fnproject/fn/api/InputEvent.java | 29 +- .../fnproject/fn/api/InvocationContext.java | 40 ++ .../com/fnproject/fn/api/OutputEvent.java | 150 ++-- .../com/fnproject/fn/api/QueryParameters.java | 2 +- .../com/fnproject/fn/api/RuntimeContext.java | 32 +- .../com/fnproject/fn/api/RuntimeFeature.java | 17 + .../api/httpgateway/HTTPGatewayContext.java | 84 +++ .../com/fnproject/fn/api/HeadersTest.java | 32 + build-image/docker-build.sh | 12 - build.sh | 56 ++ build_in_docker.sh | 2 + docs/ExtendingDataBinding.md | 2 +- docs/FAQ.md | 6 +- docs/FnFlowsAdvancedTopics.md | 5 + docs/FnFlowsUserGuide.md | 35 +- docs/HTTPGatewayFunctions.md | 36 + docs/TestingFunctions.md | 40 +- examples/README.md | 20 +- examples/async-thumbnails/README.md | 84 +-- examples/async-thumbnails/func.yaml | 14 +- examples/async-thumbnails/pom.xml | 40 +- examples/async-thumbnails/run.sh | 12 +- .../setup/resize128/func.yaml | 7 +- .../setup/resize256/func.yaml | 7 +- .../setup/resize512/func.yaml | 7 +- examples/async-thumbnails/setup/setup.sh | 80 +-- .../fn/examples/ThumbnailsFunction.java | 21 +- .../fn/examples/ThumbnailsFunctionTest.java | 107 +-- examples/gradle-build/README.md | 4 +- .../com/example/fn/HelloFunctionTest.java | 3 - examples/pom.xml | 5 - examples/qr-code/README.md | 53 +- examples/qr-code/pom.xml | 14 +- .../java/com/fnproject/fn/examples/QRGen.java | 23 +- .../com/fnproject/fn/examples/QRGenTest.java | 28 +- examples/regex-query/README.md | 4 +- examples/regex-query/pom.xml | 18 +- .../fn/examples/RegexQueryTests.java | 2 - examples/string-reverse/README.md | 60 +- examples/string-reverse/func.yaml | 16 +- examples/string-reverse/pom.xml | 58 +- .../java/com/example/fn/StringReverse.java | 7 + .../fnproject/fn/examples/StringReverse.java | 11 - .../fn/testing}/StringReverseTest.java | 4 +- flow-api/pom.xml | 61 ++ flow-api/src/main/api/snapshot.sigfile | 343 ++++++++++ .../java/com/fnproject/fn/api/flow/Flow.java | 14 +- .../fn/api/flow/FlowCompletionException.java | 0 .../com/fnproject/fn/api/flow/FlowFuture.java | 1 - .../java/com/fnproject/fn/api/flow/Flows.java | 5 +- .../api/flow/FunctionInvocationException.java | 0 .../flow/FunctionInvokeFailedException.java | 0 .../fn/api/flow/FunctionTimeoutException.java | 0 .../com/fnproject/fn/api/flow/HttpMethod.java | 0 .../fnproject/fn/api/flow/HttpRequest.java | 0 .../fnproject/fn/api/flow/HttpResponse.java | 0 .../flow/InvalidStageResponseException.java | 0 .../flow/LambdaSerializationException.java | 0 .../fn/api/flow/PlatformException.java | 0 .../flow/ResultSerializationException.java | 0 .../api/flow/StageInvokeFailedException.java | 0 .../fn/api/flow/StageLostException.java | 0 .../fn/api/flow/StageTimeoutException.java | 0 .../fn/api/flow/WrappedFunctionException.java | 0 .../fnproject/fn/api/flow/package-info.java | 0 .../com/fnproject/fn/api/flow/FlowsTest.java | 4 +- flow-runtime/pom.xml | 76 +++ .../fnproject/fn/runtime/flow/APIModel.java | 2 +- .../fn/runtime/flow/BlobResponse.java | 0 .../fn/runtime/flow/BlobStoreClient.java | 0 .../fn/runtime/flow/CodeLocation.java | 0 .../fn/runtime/flow/CompleterClient.java | 0 .../runtime/flow/CompleterClientFactory.java | 0 .../fn/runtime/flow/CompletionId.java | 0 .../fn/runtime/flow/DefaultHttpResponse.java | 0 .../fn/runtime/flow/EntityReader.java | 0 .../runtime/flow/FlowContinuationInvoker.java | 36 +- .../fn/runtime/flow/FlowFeature.java | 38 ++ .../fn/runtime/flow/FlowFutureSource.java | 0 .../com/fnproject/fn/runtime/flow/FlowId.java | 0 .../fn/runtime/flow/FlowRuntimeGlobals.java | 0 .../fnproject/fn/runtime/flow/HttpClient.java | 0 .../fnproject/fn/runtime/flow/JsonInvoke.java | 2 +- .../runtime/flow/RemoteBlobStoreClient.java | 1 - .../fnproject/fn/runtime/flow/RemoteFlow.java | 0 .../fn/runtime/flow/RemoteFlowApiClient.java | 24 +- .../flow/FlowsContinuationInvokerTest.java | 229 ++++--- .../runtime/flow/RemoteFlowApiClientTest.java | 6 +- .../fn/runtime/flow/TestBlobStore.java | 0 .../testing/flowtestfns}/FnFlowsFunction.java | 5 +- flow-testing/pom.xml | 78 +++ .../fn/testing/flow/FlowTesting.java | 237 +++++++ .../testing/flow}/FnFunctionStubBuilder.java | 23 +- .../fn/testing/flow}/InMemCompleter.java | 55 +- .../fn/testing/flow}/ResultException.java | 2 +- .../testing/flow}/FnTestingRuleFlowsTest.java | 155 +++-- .../fn/testing/flow/IntegrationTest.java | 42 ++ .../fn/testing/flow}/MultipleEventsTest.java | 13 +- .../fn/testing/flow}/WhenCompleteTest.java | 13 +- .../flowtestfns}/ExerciseEverything.java | 28 +- fn-spring-cloud-function/pom.xml | 27 +- .../function/SpringCloudFunctionFeature.java | 21 + .../function/SpringCloudFunctionInvoker.java | 9 +- .../function/SpringCloudFunctionLoader.java | 16 +- .../functions/SpringCloudConsumer.java | 2 +- .../functions/SpringCloudFunction.java | 3 +- .../function/functions/SpringCloudMethod.java | 2 +- .../functions/SpringCloudSupplier.java | 3 +- .../function/SimpleFunctionInspector.java | 13 +- ...ngCloudFunctionInvokerIntegrationTest.java | 22 +- .../SpringCloudFunctionLoaderTest.java | 21 +- .../function/testfns/EmptyFunctionConfig.java | 17 +- .../function/testfns/FunctionConfig.java | 17 +- images/build-native/Dockerfile | 45 ++ images/build-native/README.md | 5 + images/build-native/docker-build.sh | 21 + images/build-native/native.version | 1 + {build-image => images/build}/Dockerfile | 0 .../build/Dockerfile-jdk11 | 4 +- {build-image => images/build}/cache-deps.sh | 0 images/build/docker-build.sh | 21 + {build-image => images/build}/pom.xml | 26 +- .../java/com/example/fn/HelloFunction.java | 0 .../com/example/fn/HelloFunctionTest.java | 24 + images/init-native/Dockerfile | 35 + images/init-native/Dockerfile-init-image | 25 + images/init-native/README.md | 2 + images/init-native/docker-build.sh | 6 + .../init-native/func.init.yaml | 0 images/init-native/pom.xml | 88 +++ images/init-native/src/main/conf/jni.json | 22 + .../init-native/src/main/conf/reflection.json | 9 + .../java/com/example/fn/HelloFunction.java | 11 + .../com/example/fn/HelloFunctionTest.java | 0 {runtime => images/runtime}/Dockerfile | 9 +- images/runtime/Dockerfile-jre11 | 20 + images/runtime/README.md | 3 + infra/update/functions/Jenkinsfile | 2 +- integration-tests/.gitignore | 5 - integration-tests/README.md | 51 +- integration-tests/fnserver.env | 3 + .../flowAllFeatures}/README.md | 0 .../flowAllFeatures}/func.yaml | 9 +- .../funcs/flowAllFeatures/pom.xml | 84 +++ .../fn/integration/ExerciseEverything.java | 25 +- .../com/fnproject/fn/integration/Test.java | 0 .../test-1 => funcs/flowBasic}/func.yaml | 7 +- .../test-1-jdk8 => funcs/flowBasic}/pom.xml | 16 +- .../integration/test_1/CompleterFunction.java | 3 + .../flowBasicJDK8}/func.yaml | 7 +- .../test-1 => funcs/flowBasicJDK8}/pom.xml | 15 +- .../integration/test_1/CompleterFunction.java | 3 + .../funcs/flowExitHooks/func.yaml | 7 + .../test-5 => funcs/flowExitHooks}/pom.xml | 20 +- .../integration/test_5/CompleterFunction.java | 45 ++ .../funcs/flowTimeouts/func.yaml | 7 + .../test-4 => funcs/flowTimeouts}/pom.xml | 26 +- .../integration/test_6/CompleterFunction.java | 11 +- .../funcs/helloFunc/func-proto.yaml | 6 + integration-tests/funcs/helloFunc/pom.xml | 68 ++ .../fn/integration/hello/HelloFunction.java | 8 + integration-tests/funcs/httpgwfunc/func.yaml | 10 + integration-tests/funcs/httpgwfunc/pom.xml | 60 ++ .../java/com/example/fn/TriggerFunction.java | 27 + .../com/example/fn/TriggerFunctionTest.java | 40 ++ integration-tests/funcs/simpleFunc/func.yaml | 6 + .../{main/test-2 => funcs/simpleFunc}/pom.xml | 14 +- .../fn/integration/test2/PlainFunction.java | 3 +- .../integration/test2/PlainFunctionTest.java | 0 integration-tests/lib.sh | 48 -- integration-tests/main/test-1-jdk8/config | 1 - integration-tests/main/test-1-jdk8/expected | 1 - integration-tests/main/test-1-jdk8/input | 1 - integration-tests/main/test-1/config | 1 - integration-tests/main/test-1/expected | 1 - integration-tests/main/test-1/input | 1 - integration-tests/main/test-2/config | 1 - integration-tests/main/test-2/expected | 1 - integration-tests/main/test-2/func.yaml | 5 - integration-tests/main/test-3/.gitignore | 3 - integration-tests/main/test-3/expected | 1 - integration-tests/main/test-3/input | 1 - integration-tests/main/test-3/pre-test.sh | 6 - .../main/test-4/Dockerfile.custom | 4 - integration-tests/main/test-4/config | 1 - integration-tests/main/test-4/expected.sh | 3 - integration-tests/main/test-4/input | 1 - integration-tests/main/test-4/pre-test.sh | 5 - integration-tests/main/test-5/config | 1 - integration-tests/main/test-5/expected.sh | 39 -- integration-tests/main/test-5/func.yaml | 6 - integration-tests/main/test-5/input | 0 .../integration/test_5/CompleterFunction.java | 14 - integration-tests/main/test-6/config | 1 - integration-tests/main/test-6/expected.sh | 32 - integration-tests/main/test-6/func.yaml | 6 - integration-tests/main/test-6/input | 0 integration-tests/main/test-6/pom.xml | 64 -- integration-tests/main/test-7/delete.sh | 3 - integration-tests/main/test-7/deploy.sh | 3 - integration-tests/main/test-7/expected | 1 - integration-tests/main/test-7/pre-test.sh | 6 - integration-tests/main/test-7/run-test.sh | 3 - integration-tests/pom.xml | 68 +- integration-tests/post-configure-hook.sh | 18 - integration-tests/pre-build-hook.sh | 31 - integration-tests/run-all-tests.sh | 100 --- integration-tests/run-local.sh | 139 ---- integration-tests/run-remote.sh | 39 -- integration-tests/run_tests_ci.sh | 95 +++ integration-tests/smoke-test.sh | 95 --- .../integrationtest/IntegrationTestRule.java | 351 ++++++++++ .../fn/integrationtest/FlowTest.java | 100 +++ .../fn/integrationtest/FunctionsTest.java | 97 +++ pom.xml | 194 +++++- release.version | 2 +- runtime/Dockerfile-jdk9 | 18 - runtime/pom.xml | 40 +- runtime/src/main/c/.gitignore | 4 + runtime/src/main/c/CMakeLists.txt | 10 + runtime/src/main/c/Dockerfile-buildimage | 8 + runtime/src/main/c/README.md | 19 + runtime/src/main/c/buildit.sh | 14 + runtime/src/main/c/rebuild_so.sh | 10 + runtime/src/main/c/unix_socket.c | 560 +++++++++++++++ .../fn/runtime/DefaultEventCodec.java | 73 -- .../fn/runtime/DefaultMethodWrapper.java | 17 +- .../com/fnproject/fn/runtime/EntryPoint.java | 116 ++-- .../com/fnproject/fn/runtime/EventCodec.java | 34 +- .../fn/runtime/FunctionConfigurer.java | 5 +- .../runtime/FunctionInvocationCallback.java | 1 - .../fn/runtime/FunctionInvocationContext.java | 57 +- .../fnproject/fn/runtime/FunctionLoader.java | 4 +- .../fn/runtime/FunctionRuntimeContext.java | 93 +-- .../fnproject/fn/runtime/HTTPStreamCodec.java | 352 ++++++++++ .../fnproject/fn/runtime/HttpEventCodec.java | 156 ----- .../fn/runtime/MethodFunctionInvoker.java | 24 +- .../fn/runtime/MethodTypeWrapper.java | 21 +- .../fn/runtime/ParameterWrapper.java | 24 - .../fn/runtime/PrimitiveTypeResolver.java | 2 +- .../fn/runtime/ReadOnceInputEvent.java | 69 +- .../fn/runtime/ReturnTypeWrapper.java | 18 - .../runtime/coercion/ByteArrayCoercion.java | 2 +- .../fn/runtime/coercion/ContextCoercion.java | 28 + .../fn/runtime/coercion/StringCoercion.java | 2 +- .../fn/runtime/coercion/VoidCoercion.java | 2 +- .../coercion/jackson/JacksonCoercion.java | 10 +- .../exception/FunctionIOException.java | 20 + .../FunctionInitializationException.java | 23 + .../InternalFunctionInvocationException.java | 4 +- .../exception/InvalidEntryPointException.java | 2 +- .../PlatformCommunicationException.java | 3 + .../FunctionHTTPGatewayContext.java | 110 +++ .../QueryParametersImpl.java | 9 +- .../QueryParametersParser.java | 4 +- .../fn/runtime/ntv/UnixServerSocket.java | 62 ++ .../fnproject/fn/runtime/ntv/UnixSocket.java | 320 +++++++++ .../fn/runtime/ntv/UnixSocketException.java | 18 + .../fn/runtime/ntv/UnixSocketNative.java | 60 ++ .../fn/runtime/ConfigurationMethodsTest.java | 66 +- .../fnproject/fn/runtime/DataBindingTest.java | 50 +- .../fn/runtime/DefaultEventCodecTest.java | 104 --- .../fn/runtime/EndToEndInvokeTest.java | 167 +++-- .../fn/runtime/ErrorMessagesTest.java | 32 +- .../fnproject/fn/runtime/FnTestHarness.java | 411 ++++------- ...sts.java => FunctionConstructionTest.java} | 26 +- .../fn/runtime/HTTPStreamCodecTest.java | 330 +++++++++ .../fnproject/fn/runtime/HeaderBuilder.java | 8 +- .../fn/runtime/HttpEventCodecTest.java | 287 -------- .../fn/runtime/JacksonCoercionTest.java | 14 +- .../fn/runtime/MethodWrapperTests.java | 35 +- .../fn/runtime/QueryParametersParserTest.java | 8 +- .../fnproject/fn/runtime/flow/FlowsTest.java | 280 -------- .../FunctionHTTPGatewayContextTest.java | 98 +++ .../fn/runtime/ntv/UnixSocketNativeTest.java | 466 +++++++++++++ .../fn/runtime/ntv/UnixSocketTest.java | 93 +++ .../CustomDataBindingFnInputOutput.java | 2 +- .../CustomDataBindingFnWithAnnotation.java | 2 - ...mDataBindingFnWithAnnotationAndConfig.java | 3 +- .../CustomDataBindingFnWithConfig.java | 1 - .../CustomDataBindingFnWithDudCoercion.java | 1 - ...tomDataBindingFnWithMultipleCoercions.java | 3 +- ...stomDataBindingFnWithNoUserCoercions.java} | 4 +- ...stomOutputDataBindingFnWithAnnotation.java | 2 +- .../CustomOutputDataBindingFnWithConfig.java | 3 +- ...tomOutputDataBindingFnWithDudCoercion.java | 3 +- ...putDataBindingFnWithMultipleCoercions.java | 2 +- ...utputDataBindingFnWithNoUserCoercions.java | 2 +- .../fnproject/fn/runtime/testfns/TestFn.java | 18 +- .../TestFnWithConfigurationMethods.java | 2 +- .../coercions/StringReversalCoercion.java | 2 +- .../coercions/StringUpperCaseCoercion.java | 6 +- testing-core/pom.xml | 22 + .../fnproject/fn/testing/FnEventBuilder.java | 59 +- .../fn/testing/FnHttpEventBuilder.java | 63 ++ .../com/fnproject/fn/testing/FnResult.java | 32 + .../fn/testing/FnTestingClassLoader.java | 27 +- .../fnproject/fn/testing/FunctionError.java | 0 .../fnproject/fn/testing/HeaderWriter.java | 0 .../fnproject/fn/testing/PlatformError.java | 0 testing-junit4/pom.xml | 36 + .../fn/testing/FnEventBuilderJUnit4.java | 5 + .../fnproject/fn/testing/FnTestingRule.java | 444 ++++++++++++ .../fn/testing/FnTestingRuleFeature.java | 26 + .../fn/testing/FnTestingRuleTest.java | 340 ++++++++++ testing/pom.xml | 23 +- .../fn/testing/FnHttpEventBuilder.java | 178 ----- .../com/fnproject/fn/testing/FnResult.java | 47 -- .../fnproject/fn/testing/FnTestingRule.java | 640 ------------------ .../fn/testing/FnTestingRuleTest.java | 401 ----------- .../fnproject/fn/testing/IntegrationTest.java | 36 - 324 files changed, 8685 insertions(+), 5447 deletions(-) create mode 100755 .circleci/fix-java-for-surefire.sh delete mode 100755 .circleci/install-go.sh create mode 100644 api/src/main/java/com/fnproject/fn/api/FnFeature.java create mode 100644 api/src/main/java/com/fnproject/fn/api/FnFeatures.java create mode 100644 api/src/main/java/com/fnproject/fn/api/RuntimeFeature.java create mode 100644 api/src/main/java/com/fnproject/fn/api/httpgateway/HTTPGatewayContext.java create mode 100644 api/src/test/java/com/fnproject/fn/api/HeadersTest.java delete mode 100755 build-image/docker-build.sh create mode 100755 build.sh create mode 100644 build_in_docker.sh create mode 100644 docs/HTTPGatewayFunctions.md create mode 100644 examples/string-reverse/src/main/java/com/example/fn/StringReverse.java delete mode 100644 examples/string-reverse/src/main/java/com/fnproject/fn/examples/StringReverse.java rename examples/string-reverse/src/test/java/com/{fnproject/examples => example/fn/testing}/StringReverseTest.java (87%) create mode 100644 flow-api/pom.xml create mode 100644 flow-api/src/main/api/snapshot.sigfile rename {api => flow-api}/src/main/java/com/fnproject/fn/api/flow/Flow.java (95%) rename {api => flow-api}/src/main/java/com/fnproject/fn/api/flow/FlowCompletionException.java (100%) rename {api => flow-api}/src/main/java/com/fnproject/fn/api/flow/FlowFuture.java (99%) rename {api => flow-api}/src/main/java/com/fnproject/fn/api/flow/Flows.java (94%) rename {api => flow-api}/src/main/java/com/fnproject/fn/api/flow/FunctionInvocationException.java (100%) rename {api => flow-api}/src/main/java/com/fnproject/fn/api/flow/FunctionInvokeFailedException.java (100%) rename {api => flow-api}/src/main/java/com/fnproject/fn/api/flow/FunctionTimeoutException.java (100%) rename {api => flow-api}/src/main/java/com/fnproject/fn/api/flow/HttpMethod.java (100%) rename {api => flow-api}/src/main/java/com/fnproject/fn/api/flow/HttpRequest.java (100%) rename {api => flow-api}/src/main/java/com/fnproject/fn/api/flow/HttpResponse.java (100%) rename {api => flow-api}/src/main/java/com/fnproject/fn/api/flow/InvalidStageResponseException.java (100%) rename {api => flow-api}/src/main/java/com/fnproject/fn/api/flow/LambdaSerializationException.java (100%) rename {api => flow-api}/src/main/java/com/fnproject/fn/api/flow/PlatformException.java (100%) rename {api => flow-api}/src/main/java/com/fnproject/fn/api/flow/ResultSerializationException.java (100%) rename {api => flow-api}/src/main/java/com/fnproject/fn/api/flow/StageInvokeFailedException.java (100%) rename {api => flow-api}/src/main/java/com/fnproject/fn/api/flow/StageLostException.java (100%) rename {api => flow-api}/src/main/java/com/fnproject/fn/api/flow/StageTimeoutException.java (100%) rename {api => flow-api}/src/main/java/com/fnproject/fn/api/flow/WrappedFunctionException.java (100%) rename {api => flow-api}/src/main/java/com/fnproject/fn/api/flow/package-info.java (100%) rename {api => flow-api}/src/test/java/com/fnproject/fn/api/flow/FlowsTest.java (99%) create mode 100644 flow-runtime/pom.xml rename {runtime => flow-runtime}/src/main/java/com/fnproject/fn/runtime/flow/APIModel.java (100%) rename {runtime => flow-runtime}/src/main/java/com/fnproject/fn/runtime/flow/BlobResponse.java (100%) rename {runtime => flow-runtime}/src/main/java/com/fnproject/fn/runtime/flow/BlobStoreClient.java (100%) rename {runtime => flow-runtime}/src/main/java/com/fnproject/fn/runtime/flow/CodeLocation.java (100%) rename {runtime => flow-runtime}/src/main/java/com/fnproject/fn/runtime/flow/CompleterClient.java (100%) rename {runtime => flow-runtime}/src/main/java/com/fnproject/fn/runtime/flow/CompleterClientFactory.java (100%) rename {runtime => flow-runtime}/src/main/java/com/fnproject/fn/runtime/flow/CompletionId.java (100%) rename {runtime => flow-runtime}/src/main/java/com/fnproject/fn/runtime/flow/DefaultHttpResponse.java (100%) rename {runtime => flow-runtime}/src/main/java/com/fnproject/fn/runtime/flow/EntityReader.java (100%) rename {runtime => flow-runtime}/src/main/java/com/fnproject/fn/runtime/flow/FlowContinuationInvoker.java (93%) create mode 100644 flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowFeature.java rename {runtime => flow-runtime}/src/main/java/com/fnproject/fn/runtime/flow/FlowFutureSource.java (100%) rename {runtime => flow-runtime}/src/main/java/com/fnproject/fn/runtime/flow/FlowId.java (100%) rename {runtime => flow-runtime}/src/main/java/com/fnproject/fn/runtime/flow/FlowRuntimeGlobals.java (100%) rename {runtime => flow-runtime}/src/main/java/com/fnproject/fn/runtime/flow/HttpClient.java (100%) rename {runtime => flow-runtime}/src/main/java/com/fnproject/fn/runtime/flow/JsonInvoke.java (97%) rename {runtime => flow-runtime}/src/main/java/com/fnproject/fn/runtime/flow/RemoteBlobStoreClient.java (97%) rename {runtime => flow-runtime}/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlow.java (100%) rename {runtime => flow-runtime}/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClient.java (95%) rename {runtime => flow-runtime}/src/test/java/com/fnproject/fn/runtime/flow/FlowsContinuationInvokerTest.java (76%) rename {runtime => flow-runtime}/src/test/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClientTest.java (98%) rename {runtime => flow-runtime}/src/test/java/com/fnproject/fn/runtime/flow/TestBlobStore.java (100%) rename {runtime/src/test/java/com/fnproject/fn/runtime/testfns => flow-runtime/src/test/java/com/fnproject/fn/testing/flowtestfns}/FnFlowsFunction.java (83%) create mode 100644 flow-testing/pom.xml create mode 100644 flow-testing/src/main/java/com/fnproject/fn/testing/flow/FlowTesting.java rename {testing/src/main/java/com/fnproject/fn/testing => flow-testing/src/main/java/com/fnproject/fn/testing/flow}/FnFunctionStubBuilder.java (75%) rename {testing/src/main/java/com/fnproject/fn/testing => flow-testing/src/main/java/com/fnproject/fn/testing/flow}/InMemCompleter.java (94%) rename {testing/src/main/java/com/fnproject/fn/testing => flow-testing/src/main/java/com/fnproject/fn/testing/flow}/ResultException.java (89%) rename {testing/src/test/java/com/fnproject/fn/testing => flow-testing/src/test/java/com/fnproject/fn/testing/flow}/FnTestingRuleFlowsTest.java (74%) create mode 100644 flow-testing/src/test/java/com/fnproject/fn/testing/flow/IntegrationTest.java rename {testing/src/test/java/com/fnproject/fn/testing => flow-testing/src/test/java/com/fnproject/fn/testing/flow}/MultipleEventsTest.java (90%) rename {testing/src/test/java/com/fnproject/fn/testing => flow-testing/src/test/java/com/fnproject/fn/testing/flow}/WhenCompleteTest.java (70%) rename {testing/src/test/java/com/fnproject/fn/testing => flow-testing/src/test/java/com/fnproject/fn/testing/flowtestfns}/ExerciseEverything.java (95%) create mode 100644 fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionFeature.java create mode 100644 images/build-native/Dockerfile create mode 100644 images/build-native/README.md create mode 100755 images/build-native/docker-build.sh create mode 100644 images/build-native/native.version rename {build-image => images/build}/Dockerfile (100%) rename build-image/Dockerfile-jdk9 => images/build/Dockerfile-jdk11 (86%) rename {build-image => images/build}/cache-deps.sh (100%) create mode 100755 images/build/docker-build.sh rename {build-image => images/build}/pom.xml (72%) rename {build-image => images/build}/src/main/java/com/example/fn/HelloFunction.java (100%) create mode 100644 images/build/src/test/java/com/example/fn/HelloFunctionTest.java create mode 100644 images/init-native/Dockerfile create mode 100644 images/init-native/Dockerfile-init-image create mode 100644 images/init-native/README.md create mode 100755 images/init-native/docker-build.sh rename integration-tests/main/test-2/input => images/init-native/func.init.yaml (100%) create mode 100644 images/init-native/pom.xml create mode 100644 images/init-native/src/main/conf/jni.json create mode 100644 images/init-native/src/main/conf/reflection.json create mode 100644 images/init-native/src/main/java/com/example/fn/HelloFunction.java rename {build-image => images/init-native}/src/test/java/com/example/fn/HelloFunctionTest.java (100%) rename {runtime => images/runtime}/Dockerfile (65%) create mode 100644 images/runtime/Dockerfile-jre11 create mode 100644 images/runtime/README.md delete mode 100644 integration-tests/.gitignore create mode 100644 integration-tests/fnserver.env rename integration-tests/{main/test-4 => funcs/flowAllFeatures}/README.md (100%) rename integration-tests/{main/test-4 => funcs/flowAllFeatures}/func.yaml (74%) create mode 100644 integration-tests/funcs/flowAllFeatures/pom.xml rename integration-tests/{main/test-4 => funcs/flowAllFeatures}/src/main/java/com/fnproject/fn/integration/ExerciseEverything.java (94%) rename integration-tests/{main/test-4 => funcs/flowAllFeatures}/src/main/java/com/fnproject/fn/integration/Test.java (100%) rename integration-tests/{main/test-1 => funcs/flowBasic}/func.yaml (57%) rename integration-tests/{main/test-1-jdk8 => funcs/flowBasic}/pom.xml (80%) rename integration-tests/{main/test-1-jdk8 => funcs/flowBasic}/src/main/java/com/fnproject/fn/integration/test_1/CompleterFunction.java (76%) rename integration-tests/{main/test-1-jdk8 => funcs/flowBasicJDK8}/func.yaml (56%) rename integration-tests/{main/test-1 => funcs/flowBasicJDK8}/pom.xml (80%) rename integration-tests/{main/test-1 => funcs/flowBasicJDK8}/src/main/java/com/fnproject/fn/integration/test_1/CompleterFunction.java (76%) create mode 100644 integration-tests/funcs/flowExitHooks/func.yaml rename integration-tests/{main/test-5 => funcs/flowExitHooks}/pom.xml (75%) create mode 100644 integration-tests/funcs/flowExitHooks/src/main/java/com/fnproject/fn/integration/test_5/CompleterFunction.java create mode 100644 integration-tests/funcs/flowTimeouts/func.yaml rename integration-tests/{main/test-4 => funcs/flowTimeouts}/pom.xml (79%) rename integration-tests/{main/test-6 => funcs/flowTimeouts}/src/main/java/com/fnproject/fn/integration/test_6/CompleterFunction.java (67%) create mode 100644 integration-tests/funcs/helloFunc/func-proto.yaml create mode 100644 integration-tests/funcs/helloFunc/pom.xml create mode 100644 integration-tests/funcs/helloFunc/src-proto/main/java/com/fnproject/fn/integration/hello/HelloFunction.java create mode 100644 integration-tests/funcs/httpgwfunc/func.yaml create mode 100644 integration-tests/funcs/httpgwfunc/pom.xml create mode 100644 integration-tests/funcs/httpgwfunc/src/main/java/com/example/fn/TriggerFunction.java create mode 100644 integration-tests/funcs/httpgwfunc/src/test/java/com/example/fn/TriggerFunctionTest.java create mode 100644 integration-tests/funcs/simpleFunc/func.yaml rename integration-tests/{main/test-2 => funcs/simpleFunc}/pom.xml (83%) rename integration-tests/{main/test-2 => funcs/simpleFunc}/src/main/java/com/fnproject/fn/integration/test2/PlainFunction.java (86%) rename integration-tests/{main/test-2 => funcs/simpleFunc}/src/test/java/com/fnproject/fn/integration/test2/PlainFunctionTest.java (100%) delete mode 100644 integration-tests/lib.sh delete mode 100644 integration-tests/main/test-1-jdk8/config delete mode 100644 integration-tests/main/test-1-jdk8/expected delete mode 100644 integration-tests/main/test-1-jdk8/input delete mode 100644 integration-tests/main/test-1/config delete mode 100644 integration-tests/main/test-1/expected delete mode 100644 integration-tests/main/test-1/input delete mode 100644 integration-tests/main/test-2/config delete mode 100644 integration-tests/main/test-2/expected delete mode 100644 integration-tests/main/test-2/func.yaml delete mode 100644 integration-tests/main/test-3/.gitignore delete mode 100644 integration-tests/main/test-3/expected delete mode 100644 integration-tests/main/test-3/input delete mode 100755 integration-tests/main/test-3/pre-test.sh delete mode 100644 integration-tests/main/test-4/Dockerfile.custom delete mode 100644 integration-tests/main/test-4/config delete mode 100755 integration-tests/main/test-4/expected.sh delete mode 100644 integration-tests/main/test-4/input delete mode 100755 integration-tests/main/test-4/pre-test.sh delete mode 100644 integration-tests/main/test-5/config delete mode 100755 integration-tests/main/test-5/expected.sh delete mode 100644 integration-tests/main/test-5/func.yaml delete mode 100644 integration-tests/main/test-5/input delete mode 100644 integration-tests/main/test-5/src/main/java/com/fnproject/fn/integration/test_5/CompleterFunction.java delete mode 100644 integration-tests/main/test-6/config delete mode 100755 integration-tests/main/test-6/expected.sh delete mode 100644 integration-tests/main/test-6/func.yaml delete mode 100644 integration-tests/main/test-6/input delete mode 100644 integration-tests/main/test-6/pom.xml delete mode 100644 integration-tests/main/test-7/delete.sh delete mode 100755 integration-tests/main/test-7/deploy.sh delete mode 100644 integration-tests/main/test-7/expected delete mode 100755 integration-tests/main/test-7/pre-test.sh delete mode 100755 integration-tests/main/test-7/run-test.sh delete mode 100755 integration-tests/post-configure-hook.sh delete mode 100755 integration-tests/pre-build-hook.sh delete mode 100755 integration-tests/run-all-tests.sh delete mode 100755 integration-tests/run-local.sh delete mode 100755 integration-tests/run-remote.sh create mode 100755 integration-tests/run_tests_ci.sh delete mode 100755 integration-tests/smoke-test.sh create mode 100644 integration-tests/src/main/java/com/fnproject/fn/integrationtest/IntegrationTestRule.java create mode 100644 integration-tests/src/test/java/com/fnproject/fn/integrationtest/FlowTest.java create mode 100644 integration-tests/src/test/java/com/fnproject/fn/integrationtest/FunctionsTest.java delete mode 100644 runtime/Dockerfile-jdk9 create mode 100644 runtime/src/main/c/.gitignore create mode 100644 runtime/src/main/c/CMakeLists.txt create mode 100644 runtime/src/main/c/Dockerfile-buildimage create mode 100644 runtime/src/main/c/README.md create mode 100755 runtime/src/main/c/buildit.sh create mode 100755 runtime/src/main/c/rebuild_so.sh create mode 100644 runtime/src/main/c/unix_socket.c delete mode 100644 runtime/src/main/java/com/fnproject/fn/runtime/DefaultEventCodec.java create mode 100644 runtime/src/main/java/com/fnproject/fn/runtime/HTTPStreamCodec.java delete mode 100644 runtime/src/main/java/com/fnproject/fn/runtime/HttpEventCodec.java delete mode 100644 runtime/src/main/java/com/fnproject/fn/runtime/ParameterWrapper.java delete mode 100644 runtime/src/main/java/com/fnproject/fn/runtime/ReturnTypeWrapper.java create mode 100644 runtime/src/main/java/com/fnproject/fn/runtime/coercion/ContextCoercion.java create mode 100644 runtime/src/main/java/com/fnproject/fn/runtime/exception/FunctionIOException.java create mode 100644 runtime/src/main/java/com/fnproject/fn/runtime/exception/FunctionInitializationException.java create mode 100644 runtime/src/main/java/com/fnproject/fn/runtime/httpgateway/FunctionHTTPGatewayContext.java rename runtime/src/main/java/com/fnproject/fn/runtime/{ => httpgateway}/QueryParametersImpl.java (76%) rename runtime/src/main/java/com/fnproject/fn/runtime/{ => httpgateway}/QueryParametersParser.java (97%) create mode 100644 runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixServerSocket.java create mode 100644 runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixSocket.java create mode 100644 runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixSocketException.java create mode 100644 runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixSocketNative.java delete mode 100644 runtime/src/test/java/com/fnproject/fn/runtime/DefaultEventCodecTest.java rename runtime/src/test/java/com/fnproject/fn/runtime/{FunctionConstructionTests.java => FunctionConstructionTest.java} (83%) create mode 100644 runtime/src/test/java/com/fnproject/fn/runtime/HTTPStreamCodecTest.java delete mode 100644 runtime/src/test/java/com/fnproject/fn/runtime/HttpEventCodecTest.java delete mode 100644 runtime/src/test/java/com/fnproject/fn/runtime/flow/FlowsTest.java create mode 100644 runtime/src/test/java/com/fnproject/fn/runtime/httpgateway/FunctionHTTPGatewayContextTest.java create mode 100644 runtime/src/test/java/com/fnproject/fn/runtime/ntv/UnixSocketNativeTest.java create mode 100644 runtime/src/test/java/com/fnproject/fn/runtime/ntv/UnixSocketTest.java rename runtime/src/test/java/com/fnproject/fn/runtime/testfns/{CustomDataBindingFnWithNoUserCoersions.java => CustomDataBindingFnWithNoUserCoercions.java} (83%) create mode 100644 testing-core/pom.xml rename {testing => testing-core}/src/main/java/com/fnproject/fn/testing/FnEventBuilder.java (54%) create mode 100644 testing-core/src/main/java/com/fnproject/fn/testing/FnHttpEventBuilder.java create mode 100644 testing-core/src/main/java/com/fnproject/fn/testing/FnResult.java rename {testing => testing-core}/src/main/java/com/fnproject/fn/testing/FnTestingClassLoader.java (65%) rename {testing => testing-core}/src/main/java/com/fnproject/fn/testing/FunctionError.java (100%) rename {testing => testing-core}/src/main/java/com/fnproject/fn/testing/HeaderWriter.java (100%) rename {testing => testing-core}/src/main/java/com/fnproject/fn/testing/PlatformError.java (100%) create mode 100644 testing-junit4/pom.xml create mode 100644 testing-junit4/src/main/java/com/fnproject/fn/testing/FnEventBuilderJUnit4.java create mode 100644 testing-junit4/src/main/java/com/fnproject/fn/testing/FnTestingRule.java create mode 100644 testing-junit4/src/main/java/com/fnproject/fn/testing/FnTestingRuleFeature.java create mode 100644 testing-junit4/src/test/java/com/fnproject/fn/testing/FnTestingRuleTest.java delete mode 100644 testing/src/main/java/com/fnproject/fn/testing/FnHttpEventBuilder.java delete mode 100644 testing/src/main/java/com/fnproject/fn/testing/FnResult.java delete mode 100644 testing/src/main/java/com/fnproject/fn/testing/FnTestingRule.java delete mode 100644 testing/src/test/java/com/fnproject/fn/testing/FnTestingRuleTest.java delete mode 100644 testing/src/test/java/com/fnproject/fn/testing/IntegrationTest.java diff --git a/.circleci/config.yml b/.circleci/config.yml index 7188b583..e8a142bf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,17 +2,27 @@ version: 2 jobs: build: working_directory: ~/fn-java-fdk - machine: - java: - version: oraclejdk8 + machine: true environment: # store_artifacts doesn't shell substitute so the variable # definitions are duplicated in those steps too. FDK_ARTIFACT_DIR: /tmp/artifacts/fdk TEST_ARTIFACT_DIR: /tmp/artifacts/tests - STAGING_DIR: /tmp/staging-repository + REPOSITORY_LOCATION: /tmp/staging_repo steps: - checkout + - run: + name: Update Docker to latest + command: ./.circleci/install-docker.sh + - run: + name: Install fn binary (as it is needed for the integration tests) + command: ./.circleci/install-fn.sh + - run: + name: Workaround for https://issues.apache.org/jira/browse/SUREFIRE-1588 + command: ./.circleci/fix-java-for-surefire.sh + - run: + name: Install junit-merge + command: npm install -g junit-merge - run: name: Set release to latest branch version command: | @@ -22,83 +32,22 @@ jobs: name: Determine the release version command: ./.circleci/update-versions.sh - run: - name: Build and deploy locally + name: Build and Test FDK command: | - rm -rf $STAGING_DIR - mkdir -p $STAGING_DIR - mvn deploy -DaltDeploymentRepository=localStagingDir::default::file://"$STAGING_DIR" - - store_test_results: - path: runtime/target/surefire-reports - - store_test_results: - path: testing/target/surefire-reports - + export FN_FDK_VERSION=$(cat ./release.version) + ./build.sh - run: - name: Copy FDK artifacts to upload folder + name: Run integration tests command: | - mkdir -p "$FDK_ARTIFACT_DIR" - cp -a api/target/*.jar "$FDK_ARTIFACT_DIR" - cp -a runtime/target/*.jar "$FDK_ARTIFACT_DIR" - - store_artifacts: - name: Upload FDK artifacts - path: /tmp/artifacts/fdk - - - run: - name: Update Docker to latest - command: ./.circleci/install-docker.sh - + export FN_JAVA_FDK_VERSION=$(cat release.version) + ./integration-tests/run_tests_ci.sh + timeout: 1200 - run: name: Login to Docker command: | if [[ "${CIRCLE_BRANCH}" == "master" && -z "${CIRCLE_PR_REPONAME}" ]]; then docker login -u $DOCKER_USER -p $DOCKER_PASS fi - - - run: - name: Build fn-java-fdk Docker build image - command: | - cd build-image - ./docker-build.sh -t fnproject/fn-java-fdk-build:$(cat ../release.version) . - - - run: - name: Build fn-java-fdk-jdk9 Docker build image - command: | - cd build-image - ./docker-build.sh -f Dockerfile-jdk9 -t fnproject/fn-java-fdk-build:jdk9-$(cat ../release.version) . - - run: - name: Build fn-java-fdk Docker runtime image - command: | - cd runtime - docker build -t fnproject/fn-java-fdk:$(cat ../release.version) . - - - run: - name: Build fn-java-fdk-jdk9 Docker runtime image - command: | - cd runtime - docker build -f Dockerfile-jdk9 -t fnproject/fn-java-fdk:jdk9-$(cat ../release.version) . - - - run: - name: Install fn binary (as it is needed for the integration tests) - command: ./.circleci/install-fn.sh - - - run: - name: Run integration tests - command: REPOSITORY_LOCATION="$STAGING_DIR" FN_JAVA_FDK_VERSION=$(cat release.version) ./integration-tests/run-local.sh - timeout: 1200 - - - run: - name: Copy integration test results to test artifact dir - command: | - mkdir -p "$TEST_ARTIFACT_DIR" - for test_dir in ./integration-tests/main/test-*; do - test_name="$(basename "$test_dir")" - mkdir "${TEST_ARTIFACT_DIR}/${test_name}" - cp -a $(find "$test_dir" -maxdepth 1 -type f) "${TEST_ARTIFACT_DIR}/${test_name}" - done - find "${TEST_ARTIFACT_DIR}" - when: always - - store_artifacts: - name: Upload integration test results to artifacts - path: /tmp/artifacts/tests - deploy: name: Release new version command: | @@ -108,4 +57,12 @@ jobs: git branch --set-upstream-to=origin/${CIRCLE_BRANCH} ${CIRCLE_BRANCH} ./.circleci/release.sh fi + - run: + name: Gather test results + when: always + command: | + junit-merge $(find . -wholename "*/target/surefire-reports/*.xml") --createDir -o test_results/merged_results.xml + - store_test_results: + when: always + path: test_results diff --git a/.circleci/fix-java-for-surefire.sh b/.circleci/fix-java-for-surefire.sh new file mode 100755 index 00000000..fb00731c --- /dev/null +++ b/.circleci/fix-java-for-surefire.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + + set -ex + + cat << 'EOF' > $HOME/.m2/settings.xml + + + + SUREFIRE-1588 + + true + + + -Djdk.net.URLClassPath.disableClassPathURLCheck=true + + + + + EOF \ No newline at end of file diff --git a/.circleci/install-go.sh b/.circleci/install-go.sh deleted file mode 100755 index b61512dc..00000000 --- a/.circleci/install-go.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash - -: ${GOVERSION:=1.8.3} -: ${OS:=linux} -: ${ARCH:=amd64} - -set -ex - -go version -go env GOROOT - -# Remove previous Go version -sudo rm -rf /usr/local/go - -# Install Go -BUILD_DIR="/tmp/go-${GOVERSION}.${OS}.${ARCH}" -mkdir -p "$BUILD_DIR" -pushd "$BUILD_DIR" - wget https://storage.googleapis.com/golang/go${GOVERSION}.${OS}-${ARCH}.tar.gz - sudo tar -C /usr/local -xzf go${GOVERSION}.${OS}-${ARCH}.tar.gz - go get -u github.com/golang/dep/... -popd - -mkdir -p "${GOPATH}/bin" - -# Install Glide -if ! type glide 2>&1 > /dev/null; then - curl https://glide.sh/get | sh -fi - -go version -go env GOROOT diff --git a/.circleci/release.sh b/.circleci/release.sh index f4a3983e..7a8b83ad 100755 --- a/.circleci/release.sh +++ b/.circleci/release.sh @@ -6,9 +6,11 @@ USER=fnproject SERVICE=fn-java-fdk RUNTIME_IMAGE=${SERVICE} BUILD_IMAGE=${SERVICE}-build +NATIVE_INIT_IMAGE=fn-java-native-init +NATIVE_BUILD_IMAGE=fn-java-native release_version=$(cat release.version) -if [[ $release_version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] ; then +if [[ ${release_version} =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] ; then echo "Deploying version $release_version" else echo Invalid version $release_version @@ -21,22 +23,21 @@ version_parts=(${release_version//./ }) new_minor=$((${version_parts[2]}+1)) new_version="${version_parts[0]}.${version_parts[1]}.$new_minor" -if [[ $new_version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] ; then +if [[ ${new_version} =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] ; then echo "Next version $new_version" else - echo Invalid new version $new_version + echo Invalid new version ${new_version} exit 1 fi +# Push result to git -# Deploy to bintray -mvn -s ./settings-deploy.xml \ - -DskipTests \ - -DaltDeploymentRepository="fnproject-release-repo::default::$MVN_RELEASE_REPO" \ - -Dfnproject-release-repo.username="$MVN_RELEASE_USER" \ - -Dfnproject-release-repo.password="$MVN_RELEASE_PASSWORD" \ - -DdeployAtEnd=true \ - clean deploy +echo ${new_version} > release.version +git tag -a "$release_version" -m "version $release_version" +git add release.version +git commit -m "$SERVICE: post-$release_version version bump [skip ci]" +git push +git push origin "$release_version" # Regenerate runtime and build images and push them @@ -44,40 +45,56 @@ mvn -s ./settings-deploy.xml \ moving_version=${release_version%.*}-latest ## jdk8 runtime - docker tag $USER/$RUNTIME_IMAGE:${release_version} $USER/$RUNTIME_IMAGE:latest - docker tag $USER/$RUNTIME_IMAGE:${release_version} $USER/$RUNTIME_IMAGE:${moving_version} - docker push $USER/$RUNTIME_IMAGE:latest - docker push $USER/$RUNTIME_IMAGE:${release_version} - docker push $USER/$RUNTIME_IMAGE:${moving_version} + docker tag ${USER}/${RUNTIME_IMAGE}:${release_version} ${USER}/${RUNTIME_IMAGE}:latest + docker tag ${USER}/${RUNTIME_IMAGE}:${release_version} ${USER}/${RUNTIME_IMAGE}:${moving_version} + docker push ${USER}/${RUNTIME_IMAGE}:latest + docker push ${USER}/${RUNTIME_IMAGE}:${release_version} + docker push ${USER}/${RUNTIME_IMAGE}:${moving_version} ## jdk8 build - docker tag $USER/$BUILD_IMAGE:${release_version} $USER/$BUILD_IMAGE:latest - docker tag $USER/$BUILD_IMAGE:${release_version} $USER/$BUILD_IMAGE:${moving_version} - docker push $USER/$BUILD_IMAGE:latest - docker push $USER/$BUILD_IMAGE:${release_version} - docker push $USER/$BUILD_IMAGE:${moving_version} - - ## jdk9 runtime - docker tag $USER/$RUNTIME_IMAGE:jdk9-${release_version} $USER/$RUNTIME_IMAGE:jdk9-latest - docker tag $USER/$RUNTIME_IMAGE:jdk9-${release_version} $USER/$RUNTIME_IMAGE:jdk9-${moving_version} - docker push $USER/$RUNTIME_IMAGE:jdk9-latest - docker push $USER/$RUNTIME_IMAGE:jdk9-${release_version} - docker push $USER/$RUNTIME_IMAGE:jdk9-${moving_version} - - ## jdk9 build - docker tag $USER/$BUILD_IMAGE:jdk9-${release_version} $USER/$BUILD_IMAGE:jdk9-latest - docker tag $USER/$BUILD_IMAGE:jdk9-${release_version} $USER/$BUILD_IMAGE:jdk9-${moving_version} - docker push $USER/$BUILD_IMAGE:jdk9-latest - docker push $USER/$BUILD_IMAGE:jdk9-${release_version} - docker push $USER/$BUILD_IMAGE:jdk9-${moving_version} + docker tag ${USER}/${BUILD_IMAGE}:${release_version} ${USER}/${BUILD_IMAGE}:latest + docker tag ${USER}/${BUILD_IMAGE}:${release_version} ${USER}/${BUILD_IMAGE}:${moving_version} + docker push ${USER}/${BUILD_IMAGE}:latest + docker push ${USER}/${BUILD_IMAGE}:${release_version} + docker push ${USER}/${BUILD_IMAGE}:${moving_version} + + ## jre11 runtime + docker tag ${USER}/${RUNTIME_IMAGE}:jre11-${release_version} ${USER}/${RUNTIME_IMAGE}:jre11-latest + docker tag ${USER}/${RUNTIME_IMAGE}:jre11-${release_version} ${USER}/${RUNTIME_IMAGE}:jre11-${moving_version} + docker push ${USER}/${RUNTIME_IMAGE}:jre11-latest + docker push ${USER}/${RUNTIME_IMAGE}:jre11-${release_version} + docker push ${USER}/${RUNTIME_IMAGE}:jre11-${moving_version} + + ## jdk11 build + docker tag ${USER}/${BUILD_IMAGE}:jdk11-${release_version} ${USER}/${BUILD_IMAGE}:jdk11-latest + docker tag ${USER}/${BUILD_IMAGE}:jdk11-${release_version} ${USER}/${BUILD_IMAGE}:jdk11-${moving_version} + docker push ${USER}/${BUILD_IMAGE}:jdk11-latest + docker push ${USER}/${BUILD_IMAGE}:jdk11-${release_version} + docker push ${USER}/${BUILD_IMAGE}:jdk11-${moving_version} + + ## native init image + docker tag ${USER}/${NATIVE_INIT_IMAGE}:${release_version} ${USER}/${NATIVE_INIT_IMAGE}:latest + docker tag ${USER}/${NATIVE_INIT_IMAGE}:${release_version} ${USER}/${NATIVE_INIT_IMAGE}:${moving_version} + docker push ${USER}/${NATIVE_INIT_IMAGE}:latest + docker push ${USER}/${NATIVE_INIT_IMAGE}:${release_version} + docker push ${USER}/${NATIVE_INIT_IMAGE}:${moving_version} + ) +( + if [ -f images/build-native/native_build.image ] ; then + native_build_image=$(cat images/build-native/native_build.image) + docker tag ${native_build_image} ${USER}/${NATIVE_BUILD_IMAGE}:latest + docker push ${USER}/${NATIVE_BUILD_IMAGE}:latest + docker push ${native_build_image} + fi +) -# Push result to git -echo $new_version > release.version -git tag -a "$release_version" -m "version $release_version" -git add release.version -git commit -m "$SERVICE: post-$release_version version bump [skip ci]" -git push -git push origin "$release_version" +# Deploy to bintray +mvn -s ./settings-deploy.xml \ + -DskipTests \ + -DaltDeploymentRepository="fnproject-release-repo::default::${MVN_RELEASE_REPO}" \ + -Dfnproject-release-repo.username="${MVN_RELEASE_USER}" \ + -Dfnproject-release-repo.password="${MVN_RELEASE_PASSWORD}" \ + clean deploy diff --git a/.circleci/update-versions.sh b/.circleci/update-versions.sh index 1490bd2e..394a3b76 100755 --- a/.circleci/update-versions.sh +++ b/.circleci/update-versions.sh @@ -1,4 +1,9 @@ #!/bin/bash +# +# this is instead for the maven release plugin (which sucks) - this sets the versions across the project before a build +# for branch builds these are ignored (nothing is deployed) +# For master releases this sets the latest version that this branch would be released as +# release_version=$(cat release.version) if [[ $release_version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] ; then @@ -14,6 +19,6 @@ mvn versions:set -D newVersion=${release_version} versions:update-child-modules # We need to replace the example dependency versions also # (sed syntax for portability between MacOS and gnu) find . -name pom.xml | - xargs -n 1 sed -i.bak -e "s|.*|${release_version}|" + xargs -n 1 sed -i.bak -e "s|.*|${release_version}|" find . -name pom.xml.bak -delete diff --git a/.gitignore b/.gitignore index 1fff5367..1e68a65a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,7 @@ logs/ *.versionsBackup .gradle examples/gradle-build/build +**/*.classpath +**/*.project +**/*.settings + diff --git a/README.md b/README.md index 81eb9874..de02a132 100644 --- a/README.md +++ b/README.md @@ -1,331 +1,24 @@ -# Fn Java Functions Developer Kit (FDK) [![CircleCI](https://circleci.com/gh/fnproject/fdk-java.svg?style=svg&circle-token=348bec5610c34421f6c436ab8f6a18e153cb1c01)](https://circleci.com/gh/fnproject/fdk-java) -This project adds support for writing functions in Java on the [Fn -platform](https://github.com/fnproject/fn), with full support for Java 9 -as the default out of the box. +# Function Development Kit for Java (FDK for Java) -# FAQ -Some common questions are answered in [our FAQ](docs/FAQ.md). +The Function Development Kit for Java makes it easy to build and deploy Java functions to Fn with full support for Java 11+ as the default out of the box. -# Quick Start Tutorial +Some of the FDK for Java features include: -By following this step-by-step guide you will learn to create, run and deploy -a simple app written in Java on Fn. +- Parsing input and writing output +- Flexible data binding to Java objects +- Function testing using JUnit rules +- And more! -## Pre-requisites +## Learn about the Fn Project -Before you get started you will need the following things: +New to Fn Project? If you want to learn more about using the Fn Project to power your next project, start with the [official documentation](https://github.com/fnproject/docs). -* The [Fn CLI](https://github.com/fnproject/cli) tool -* [Docker-ce 17.06+ installed locally](https://docs.docker.com/engine/installation/) +## Using the Function Development Kit for Java -### Install the Fn CLI tool +For detailed instructions on using the FDK to build and deploy Java functions to Fn, please see the official FDK developer guide in our docs repo here: https://github.com/fnproject/docs/blob/master/fdks/fdk-java/README.md. -To install the Fn CLI tool, just run the following: - -``` -curl -LSs https://raw.githubusercontent.com/fnproject/cli/master/install | sh -``` - -This will download a shell script and execute it. If the script asks for -a password, that is because it invokes sudo. - -## Your first Function - -### 1. Create your first Java Function: - -```bash -$ mkdir hello-java-function && cd hello-java-function -$ fn init --runtime=java --name your_dockerhub_account/hello -Runtime: java -function boilerplate generated. -func.yaml created -``` - -This creates the boilerplate for a new Java Function based on Maven and Oracle -Java 9. The `pom.xml` includes a dependency on the latest version of the Fn -Java FDK that is useful for developing your Java functions. - -You can now import this project into your favourite IDE as normal. - -### 2. Deep dive into your first Java Function: -We'll now take a look at what makes up our new Java Function. First, lets take -a look at the `func.yaml`: - -```bash -$ cat func.yaml -name: your_dockerhub_account/hello -version: 0.0.1 -runtime: java -cmd: com.example.fn.HelloFunction::handleRequest -``` - -The `cmd` field determines which method is called when your funciton is -invoked. In the generated Function, the `func.yaml` references -`com.example.fn.HelloFunction::handleRequest`. Your functions will likely live -in different classes, and this field should always point to the method to -execute, with the following syntax: - -```text -cmd: :: -``` - -For more information about the fields in `func.yaml`, refer to the [Fn platform -documentation](https://github.com/fnproject/fn/blob/master/docs/function-file.md) -about it. - -Let's also have a brief look at the source: -`src/main/java/com/example/fn/HelloFunction.java`: - -```java -package com.example.fn; - -public class HelloFunction { - - public String handleRequest(String input) { - String name = (input == null || input.isEmpty()) ? "world" : input; - - return "Hello, " + name + "!"; - } - -} -``` - -The function takes some optional input and returns a greeting dependent on it. - -### 3. Run your first Java Function: -You are now ready to run your Function locally using the Fn CLI tool. - -```bash -$ fn build -Building image your_dockerhub_account/hello:0.0.1 -Sending build context to Docker daemon 14.34kB -Step 1/11 : FROM fnproject/fn-java-fdk-build:jdk9-latest as build-stage - ---> 5435658a63ac -Step 2/11 : WORKDIR /function - ---> 37340c5aa451 - -... - -Step 5/11 : RUN mvn package dependency:copy-dependencies -DincludeScope=runtime -DskipTests=true -Dmdep.prependGroupId=true -DoutputDirectory=target --fail-never ----> Running in 58b3b1397ba2 -[INFO] Scanning for projects... -Downloading: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-compiler-plugin/3.3/maven-compiler-plugin-3.3.pom -Downloaded: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-compiler-plugin/3.3/maven-compiler-plugin-3.3.pom (11 kB at 21 kB/s) - -... - -[INFO] ------------------------------------------------------------------------ -[INFO] BUILD SUCCESS -[INFO] ------------------------------------------------------------------------ -[INFO] Total time: 2.228 s -[INFO] Finished at: 2017-06-27T12:06:59Z -[INFO] Final Memory: 18M/143M -[INFO] ------------------------------------------------------------------------ - -... - -Function your_dockerhub_account/hello:0.0.1 built successfully. - -$ fn run -Hello, world! -``` - -The next time you run this, it will execute much quicker as your dependencies -are cached. Try passing in some input this time: - -```bash -$ echo -n "Universe" | fn run -... -Hello, Universe! -``` - -### 4. Testing your function -The Fn Java FDK includes a testing library providing useful [JUnit -4](http://junit.org/junit4/) rules to test functions. Look at the test in -`src/test/java/com/example/fn/HelloFunctionTest.java`: - -```java -package com.example.fn; - -import com.fnproject.fn.testing.*; -import org.junit.*; - -import static org.junit.Assert.*; - -public class HelloFunctionTest { - - @Rule - public final FnTestingRule testing = FnTestingRule.createDefault(); - - @Test - public void shouldReturnGreeting() { - testing.givenEvent().enqueue(); - testing.thenRun(HelloFunction.class, "handleRequest"); - - FnResult result = testing.getOnlyResult(); - assertEquals("Hello, world!", result.getBodyAsString()); - } - -} -``` - -This test is very simple: it just enqueues an event with empty input and then -runs the function, checking its output. Under the hood, the `FnTestingRule` is -actually instantiating the same runtime wrapping function invocations, so that -during the test your function will be invoked in exactly the same way that it -would when deployed. - -There is much more functionality to construct tests in the testing library. -Testing functions is covered in more detail in [Testing -Functions](docs/TestingFunctions.md). - -### 5. Run using HTTP and the local Fn server -The previous example used `fn run` to run a function directly via docker, you -can also use the Fn server locally to test the deployment of your function and -the HTTP calls to your functions. - -Open another terminal and start the Fn server: - -```bash -$ fn start -``` - -Then in your original terminal create an app: - -```bash -$ fn apps create java-app -Successfully created app: java-app -``` - -Now deploy your Function using the `fn deploy` command. This will bump the -function's version up, rebuild it, and push the image to the Docker registry, -ready to be used in the function deployment. Finally it will create a route on -the local Fn server, corresponding to your function. - -We are using the `--local` flag to tell fn to skip pushing the image anywhere -as we are just going to run this on our local fn server that we started with -`fn start` above. - -```bash -$ fn deploy --app java-app --local -... -Bumped to version 0.0.2 -Building image hello:0.0.2 -Sending build context to Docker daemon 14.34kB - -... - -Successfully built bf2b7fa55520 -Successfully tagged your_dockerhub_account/hello:0.0.2 -Updating route /hello-java-function using image your_dockerhub_account/hello:0.0.2... -``` - -Call the Function via the Fn CLI: - -```bash -$ fn call java-app /hello-java-function -Hello, world! -``` - -You can also call the Function via curl: - -```bash -$ curl http://localhost:8080/r/java-app/hello-java-function -Hello, world! -``` - -### 6. Something more interesting -The Fn Java FDK supports [flexible data binding](docs/DataBinding.md) to make -it easier for you to map function input and output data to Java objects. - -Below is an example to of a Function that returns a POJO which will be -serialized to JSON using Jackson: - -```java -package com.example.fn; - -public class PojoFunction { - - public static class Greeting { - public final String name; - public final String salutation; - - public Greeting(String salutation, String name) { - this.salutation = salutation; - this.name = name; - } - } - - public Greeting greet(String name) { - if (name == null || name.isEmpty()) - name = "World"; - - return new Greeting("Hello", name); - } - -} -``` - -Update your `func.yaml` to reference the new method: - -```yaml -cmd: com.example.fn.PojoFunction::greet -``` - -Now run your new function: - -```bash -$ fn run -... -{"name":"World","salutation":"Hello"} - -$ echo -n Michael | fn run -... -{"name":"Michael","salutation":"Hello"} -``` - -## 7. Where do I go from here? - -Learn more about the Fn Java FDK by reading the next tutorials in the series. -Also check out the examples in the [`examples` directory](examples) for some -functions demonstrating different features of the Fn Java FDK. - -### Configuring your function - -If you want to set up the state of your function object before the function is -invoked, and to use external configuration variables that you can set up with -the Fn tool, have a look at the [Function -Configuration](docs/FunctionConfiguration.md) tutorial. - -### Input and output bindings - -You have the option of taking more control of how serialization and -deserialization is performed by defining your own bindings. - -See the [Data Binding](docs/DataBinding.md) tutorial for other out-of-the-box -options and the [Extending Data Binding](docs/ExtendingDataBinding.md) tutorial -for how to define and use your own bindings. - -### Asynchronous workflows - -Suppose you want to call out to some other function from yours - perhaps -a function written in a different language, or even one maintained by -a different team. Maybe you then want to do some processing on the result. Or -even have your function interact asynchronously with a completely different -system. Perhaps you also need to maintain some state for the duration of your -function, but you don't want to pay for execution time while you're waiting for -someone else to do their work. - -If this sounds like you, then have a look at the [Fn Flow -quickstart](docs/FnFlowsUserGuide.md). - -# Get help - - * Come over and chat to us on the [fnproject Slack](https://join.slack.com/t/fnproject/shared_invite/enQtMjIwNzc5MTE4ODg3LTdlYjE2YzU1MjAxODNhNGUzOGNhMmU2OTNhZmEwOTcxZDQxNGJiZmFiMzNiMTk0NjU2NTIxZGEyNjI0YmY4NTA). - * Raise an issue in [our github](https://github.com/fnproject/fn-java-fdk/). - -# Contributing +## Contributing to the Function Development Kit for Java Please see [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/api/pom.xml b/api/pom.xml index 224d57d5..a7a27704 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -9,18 +9,25 @@ 4.0.0 - - UTF-8 - - api + + + junit + junit + test + + + org.assertj + assertj-core + test + + org.apache.maven.plugins maven-javadoc-plugin - 3.0.0-M1 attach-javadocs @@ -33,7 +40,6 @@ org.netbeans.tools sigtest-maven-plugin - 1.0 @@ -44,17 +50,9 @@ src/main/api/snapshot.sigfile strictcheck - com.fnproject.fn.api,com.fnproject.fn.api.exception,com.fnproject.fn.api.flow + com.fnproject.fn.api,com.fnproject.fn.api.exception - - - junit - junit - 4.12 - test - - diff --git a/api/src/main/api/snapshot.sigfile b/api/src/main/api/snapshot.sigfile index 1f743e18..2ed59945 100644 --- a/api/src/main/api/snapshot.sigfile +++ b/api/src/main/api/snapshot.sigfile @@ -6,17 +6,52 @@ CLSS public abstract interface !annotation com.fnproject.fn.api.FnConfiguration anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[METHOD]) intf java.lang.annotation.Annotation +CLSS public abstract interface !annotation com.fnproject.fn.api.FnFeature + anno 0 java.lang.annotation.Repeatable(java.lang.Class value=class com.fnproject.fn.api.FnFeatures) + anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[TYPE]) +intf java.lang.annotation.Annotation +meth public abstract java.lang.Class value() + +CLSS public abstract interface !annotation com.fnproject.fn.api.FnFeatures + anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[TYPE]) +intf java.lang.annotation.Annotation +meth public abstract com.fnproject.fn.api.FnFeature[] value() + CLSS public abstract interface com.fnproject.fn.api.FunctionInvoker +innr public final static !enum Phase meth public abstract java.util.Optional tryInvoke(com.fnproject.fn.api.InvocationContext,com.fnproject.fn.api.InputEvent) +CLSS public final static !enum com.fnproject.fn.api.FunctionInvoker$Phase + outer com.fnproject.fn.api.FunctionInvoker +fld public final static com.fnproject.fn.api.FunctionInvoker$Phase Call +fld public final static com.fnproject.fn.api.FunctionInvoker$Phase PreCall +meth public static com.fnproject.fn.api.FunctionInvoker$Phase valueOf(java.lang.String) +meth public static com.fnproject.fn.api.FunctionInvoker$Phase[] values() +supr java.lang.Enum + CLSS public final com.fnproject.fn.api.Headers -meth public com.fnproject.fn.api.Headers withHeader(java.lang.String,java.lang.String) -meth public java.util.Map getAll() +intf java.io.Serializable +meth public java.util.Map getAll() +meth public !varargs com.fnproject.fn.api.Headers addHeader(java.lang.String,java.lang.String,java.lang.String[]) +meth public !varargs com.fnproject.fn.api.Headers setHeader(java.lang.String,java.lang.String,java.lang.String[]) +meth public boolean equals(java.lang.Object) +meth public com.fnproject.fn.api.Headers removeHeader(java.lang.String) +meth public com.fnproject.fn.api.Headers setHeader(java.lang.String,java.util.Collection) +meth public com.fnproject.fn.api.Headers setHeaders(java.util.Map>) +meth public int hashCode() +meth public java.lang.String toString() +meth public java.util.Collection keys() +meth public java.util.List getAllValues(java.lang.String) +meth public java.util.Map> asMap() meth public java.util.Optional get(java.lang.String) meth public static com.fnproject.fn.api.Headers emptyHeaders() meth public static com.fnproject.fn.api.Headers fromMap(java.util.Map) +meth public static com.fnproject.fn.api.Headers fromMultiHeaderMap(java.util.Map>) +meth public static java.lang.String canonicalKey(java.lang.String) supr java.lang.Object -hfds headers +hfds emptyHeaders,headerName,headers CLSS public abstract interface !annotation com.fnproject.fn.api.InputBinding anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) @@ -31,15 +66,16 @@ CLSS public abstract interface com.fnproject.fn.api.InputEvent intf java.io.Closeable meth public abstract <%0 extends java.lang.Object> {%%0} consumeBody(java.util.function.Function) meth public abstract com.fnproject.fn.api.Headers getHeaders() -meth public abstract com.fnproject.fn.api.QueryParameters getQueryParameters() -meth public abstract java.lang.String getAppName() -meth public abstract java.lang.String getMethod() -meth public abstract java.lang.String getRequestUrl() -meth public abstract java.lang.String getRoute() +meth public abstract java.lang.String getCallID() +meth public abstract java.time.Instant getDeadline() CLSS public abstract interface com.fnproject.fn.api.InvocationContext +meth public abstract !varargs void setResponseHeader(java.lang.String,java.lang.String,java.lang.String[]) +meth public abstract com.fnproject.fn.api.Headers getRequestHeaders() meth public abstract com.fnproject.fn.api.RuntimeContext getRuntimeContext() meth public abstract void addListener(com.fnproject.fn.api.InvocationListener) +meth public abstract void addResponseHeader(java.lang.String,java.lang.String) +meth public void setResponseContentType(java.lang.String) CLSS public abstract interface com.fnproject.fn.api.InvocationListener intf java.util.EventListener @@ -64,16 +100,29 @@ CLSS public abstract interface com.fnproject.fn.api.OutputCoercion meth public abstract java.util.Optional wrapFunctionResult(com.fnproject.fn.api.InvocationContext,com.fnproject.fn.api.MethodWrapper,java.lang.Object) CLSS public abstract interface com.fnproject.fn.api.OutputEvent -fld public final static int FAILURE = 500 -fld public final static int SUCCESS = 200 +fld public final static java.lang.String CONTENT_TYPE_HEADER = "Content-Type" +innr public final static !enum Status meth public abstract com.fnproject.fn.api.Headers getHeaders() -meth public abstract int getStatusCode() -meth public abstract java.util.Optional getContentType() +meth public abstract com.fnproject.fn.api.OutputEvent$Status getStatus() meth public abstract void writeToOutput(java.io.OutputStream) throws java.io.IOException meth public boolean isSuccess() -meth public static com.fnproject.fn.api.OutputEvent emptyResult(int) -meth public static com.fnproject.fn.api.OutputEvent fromBytes(byte[],int,java.lang.String) -meth public static com.fnproject.fn.api.OutputEvent fromBytes(byte[],int,java.lang.String,com.fnproject.fn.api.Headers) +meth public com.fnproject.fn.api.OutputEvent withHeaders(com.fnproject.fn.api.Headers) +meth public java.util.Optional getContentType() +meth public static com.fnproject.fn.api.OutputEvent emptyResult(com.fnproject.fn.api.OutputEvent$Status) +meth public static com.fnproject.fn.api.OutputEvent fromBytes(byte[],com.fnproject.fn.api.OutputEvent$Status,java.lang.String) +meth public static com.fnproject.fn.api.OutputEvent fromBytes(byte[],com.fnproject.fn.api.OutputEvent$Status,java.lang.String,com.fnproject.fn.api.Headers) + +CLSS public final static !enum com.fnproject.fn.api.OutputEvent$Status + outer com.fnproject.fn.api.OutputEvent +fld public final static com.fnproject.fn.api.OutputEvent$Status FunctionError +fld public final static com.fnproject.fn.api.OutputEvent$Status FunctionTimeout +fld public final static com.fnproject.fn.api.OutputEvent$Status InternalError +fld public final static com.fnproject.fn.api.OutputEvent$Status Success +meth public int getCode() +meth public static com.fnproject.fn.api.OutputEvent$Status valueOf(java.lang.String) +meth public static com.fnproject.fn.api.OutputEvent$Status[] values() +supr java.lang.Enum +hfds code CLSS public abstract interface com.fnproject.fn.api.QueryParameters meth public abstract java.util.List getValues(java.lang.String) @@ -83,15 +132,21 @@ meth public abstract java.util.Optional get(java.lang.String) CLSS public abstract interface com.fnproject.fn.api.RuntimeContext meth public abstract <%0 extends java.lang.Object> java.util.Optional<{%%0}> getAttribute(java.lang.String,java.lang.Class<{%%0}>) meth public abstract com.fnproject.fn.api.MethodWrapper getMethod() +meth public abstract java.lang.String getAppID() +meth public abstract java.lang.String getFunctionID() meth public abstract java.util.List getInputCoercions(com.fnproject.fn.api.MethodWrapper,int) meth public abstract java.util.List getOutputCoercions(java.lang.reflect.Method) meth public abstract java.util.Map getConfiguration() meth public abstract java.util.Optional getInvokeInstance() meth public abstract java.util.Optional getConfigurationByKey(java.lang.String) meth public abstract void addInputCoercion(com.fnproject.fn.api.InputCoercion) +meth public abstract void addInvoker(com.fnproject.fn.api.FunctionInvoker,com.fnproject.fn.api.FunctionInvoker$Phase) meth public abstract void addOutputCoercion(com.fnproject.fn.api.OutputCoercion) meth public abstract void setAttribute(java.lang.String,java.lang.Object) -meth public abstract void setInvoker(com.fnproject.fn.api.FunctionInvoker) +meth public void setInvoker(com.fnproject.fn.api.FunctionInvoker) + +CLSS public abstract interface com.fnproject.fn.api.RuntimeFeature +meth public abstract void initialize(com.fnproject.fn.api.RuntimeContext) CLSS public abstract interface com.fnproject.fn.api.TypeWrapper meth public abstract java.lang.Class getParameterClass() @@ -116,201 +171,16 @@ cons public init(java.lang.String) cons public init(java.lang.String,java.lang.Exception) supr java.lang.RuntimeException -CLSS public abstract interface com.fnproject.fn.api.flow.Flow -innr public final static !enum FlowState -intf java.io.Serializable -meth public <%0 extends java.io.Serializable, %1 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> invokeFunction(java.lang.String,{%%1},java.lang.Class<{%%0}>) -meth public <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture invokeFunction(java.lang.String,{%%0}) -meth public abstract !varargs com.fnproject.fn.api.flow.FlowFuture anyOf(com.fnproject.fn.api.flow.FlowFuture[]) -meth public abstract !varargs com.fnproject.fn.api.flow.FlowFuture allOf(com.fnproject.fn.api.flow.FlowFuture[]) -meth public abstract <%0 extends java.io.Serializable, %1 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> invokeFunction(java.lang.String,com.fnproject.fn.api.flow.HttpMethod,com.fnproject.fn.api.Headers,{%%1},java.lang.Class<{%%0}>) -meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture invokeFunction(java.lang.String,com.fnproject.fn.api.flow.HttpMethod,com.fnproject.fn.api.Headers,{%%0}) -meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> completedValue({%%0}) -meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> createFlowFuture() -meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> failedFuture(java.lang.Throwable) -meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> supply(com.fnproject.fn.api.flow.Flows$SerCallable<{%%0}>) -meth public abstract com.fnproject.fn.api.flow.Flow addTerminationHook(com.fnproject.fn.api.flow.Flows$SerConsumer) -meth public abstract com.fnproject.fn.api.flow.FlowFuture invokeFunction(java.lang.String,com.fnproject.fn.api.flow.HttpMethod,com.fnproject.fn.api.Headers,byte[]) -meth public abstract com.fnproject.fn.api.flow.FlowFuture delay(long,java.util.concurrent.TimeUnit) -meth public abstract com.fnproject.fn.api.flow.FlowFuture supply(com.fnproject.fn.api.flow.Flows$SerRunnable) -meth public com.fnproject.fn.api.flow.FlowFuture invokeFunction(java.lang.String,com.fnproject.fn.api.flow.HttpMethod) -meth public com.fnproject.fn.api.flow.FlowFuture invokeFunction(java.lang.String,com.fnproject.fn.api.flow.HttpMethod,com.fnproject.fn.api.Headers) - -CLSS public final static !enum com.fnproject.fn.api.flow.Flow$FlowState - outer com.fnproject.fn.api.flow.Flow -fld public final static com.fnproject.fn.api.flow.Flow$FlowState CANCELLED -fld public final static com.fnproject.fn.api.flow.Flow$FlowState FAILED -fld public final static com.fnproject.fn.api.flow.Flow$FlowState KILLED -fld public final static com.fnproject.fn.api.flow.Flow$FlowState SUCCEEDED -fld public final static com.fnproject.fn.api.flow.Flow$FlowState UNKNOWN -meth public static com.fnproject.fn.api.flow.Flow$FlowState valueOf(java.lang.String) -meth public static com.fnproject.fn.api.flow.Flow$FlowState[] values() -supr java.lang.Enum - -CLSS public com.fnproject.fn.api.flow.FlowCompletionException -cons public init(java.lang.String) -cons public init(java.lang.String,java.lang.Throwable) -cons public init(java.lang.Throwable) -supr java.lang.RuntimeException -CLSS public abstract interface com.fnproject.fn.api.flow.FlowFuture<%0 extends java.lang.Object> -intf java.io.Serializable -meth public abstract <%0 extends java.lang.Object, %1 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%1}> thenCombine(com.fnproject.fn.api.flow.FlowFuture,com.fnproject.fn.api.flow.Flows$SerBiFunction) -meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture thenAcceptBoth(com.fnproject.fn.api.flow.FlowFuture<{%%0}>,com.fnproject.fn.api.flow.Flows$SerBiConsumer<{com.fnproject.fn.api.flow.FlowFuture%0},{%%0}>) -meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> applyToEither(com.fnproject.fn.api.flow.FlowFuture,com.fnproject.fn.api.flow.Flows$SerFunction<{com.fnproject.fn.api.flow.FlowFuture%0},{%%0}>) -meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> handle(com.fnproject.fn.api.flow.Flows$SerBiFunction) -meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> thenApply(com.fnproject.fn.api.flow.Flows$SerFunction<{com.fnproject.fn.api.flow.FlowFuture%0},{%%0}>) -meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> thenCompose(com.fnproject.fn.api.flow.Flows$SerFunction<{com.fnproject.fn.api.flow.FlowFuture%0},com.fnproject.fn.api.flow.FlowFuture<{%%0}>>) -meth public abstract boolean cancel() -meth public abstract boolean complete({com.fnproject.fn.api.flow.FlowFuture%0}) -meth public abstract boolean completeExceptionally(java.lang.Throwable) -meth public abstract com.fnproject.fn.api.flow.FlowFuture acceptEither(com.fnproject.fn.api.flow.FlowFuture,com.fnproject.fn.api.flow.Flows$SerConsumer<{com.fnproject.fn.api.flow.FlowFuture%0}>) -meth public abstract com.fnproject.fn.api.flow.FlowFuture thenAccept(com.fnproject.fn.api.flow.Flows$SerConsumer<{com.fnproject.fn.api.flow.FlowFuture%0}>) -meth public abstract com.fnproject.fn.api.flow.FlowFuture thenRun(com.fnproject.fn.api.flow.Flows$SerRunnable) -meth public abstract com.fnproject.fn.api.flow.FlowFuture<{com.fnproject.fn.api.flow.FlowFuture%0}> exceptionally(com.fnproject.fn.api.flow.Flows$SerFunction) -meth public abstract com.fnproject.fn.api.flow.FlowFuture<{com.fnproject.fn.api.flow.FlowFuture%0}> exceptionallyCompose(com.fnproject.fn.api.flow.Flows$SerFunction>) -meth public abstract com.fnproject.fn.api.flow.FlowFuture<{com.fnproject.fn.api.flow.FlowFuture%0}> whenComplete(com.fnproject.fn.api.flow.Flows$SerBiConsumer<{com.fnproject.fn.api.flow.FlowFuture%0},java.lang.Throwable>) -meth public abstract {com.fnproject.fn.api.flow.FlowFuture%0} get() -meth public abstract {com.fnproject.fn.api.flow.FlowFuture%0} get(long,java.util.concurrent.TimeUnit) throws java.util.concurrent.TimeoutException -meth public abstract {com.fnproject.fn.api.flow.FlowFuture%0} getNow({com.fnproject.fn.api.flow.FlowFuture%0}) - -CLSS public final com.fnproject.fn.api.flow.Flows -innr public abstract interface static FlowSource -innr public abstract interface static SerBiConsumer -innr public abstract interface static SerBiFunction -innr public abstract interface static SerCallable -innr public abstract interface static SerConsumer -innr public abstract interface static SerFunction -innr public abstract interface static SerRunnable -innr public abstract interface static SerSupplier -meth public static com.fnproject.fn.api.flow.Flow currentFlow() -meth public static com.fnproject.fn.api.flow.Flows$FlowSource getCurrentFlowSource() -meth public static void setCurrentFlowSource(com.fnproject.fn.api.flow.Flows$FlowSource) -supr java.lang.Object -hfds flowSource - -CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$FlowSource - outer com.fnproject.fn.api.flow.Flows -meth public abstract com.fnproject.fn.api.flow.Flow currentFlow() - -CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$SerBiConsumer<%0 extends java.lang.Object, %1 extends java.lang.Object> - outer com.fnproject.fn.api.flow.Flows - anno 0 java.lang.FunctionalInterface() -intf java.io.Serializable -intf java.util.function.BiConsumer<{com.fnproject.fn.api.flow.Flows$SerBiConsumer%0},{com.fnproject.fn.api.flow.Flows$SerBiConsumer%1}> - -CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$SerBiFunction<%0 extends java.lang.Object, %1 extends java.lang.Object, %2 extends java.lang.Object> - outer com.fnproject.fn.api.flow.Flows - anno 0 java.lang.FunctionalInterface() -intf java.io.Serializable -intf java.util.function.BiFunction<{com.fnproject.fn.api.flow.Flows$SerBiFunction%0},{com.fnproject.fn.api.flow.Flows$SerBiFunction%1},{com.fnproject.fn.api.flow.Flows$SerBiFunction%2}> - -CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$SerCallable<%0 extends java.lang.Object> - outer com.fnproject.fn.api.flow.Flows - anno 0 java.lang.FunctionalInterface() -intf java.io.Serializable -intf java.util.concurrent.Callable<{com.fnproject.fn.api.flow.Flows$SerCallable%0}> - -CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$SerConsumer<%0 extends java.lang.Object> - outer com.fnproject.fn.api.flow.Flows - anno 0 java.lang.FunctionalInterface() -intf java.io.Serializable -intf java.util.function.Consumer<{com.fnproject.fn.api.flow.Flows$SerConsumer%0}> - -CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$SerFunction<%0 extends java.lang.Object, %1 extends java.lang.Object> - outer com.fnproject.fn.api.flow.Flows - anno 0 java.lang.FunctionalInterface() -intf java.io.Serializable -intf java.util.function.Function<{com.fnproject.fn.api.flow.Flows$SerFunction%0},{com.fnproject.fn.api.flow.Flows$SerFunction%1}> - -CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$SerRunnable - outer com.fnproject.fn.api.flow.Flows - anno 0 java.lang.FunctionalInterface() -intf java.io.Serializable -intf java.lang.Runnable - -CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$SerSupplier<%0 extends java.lang.Object> - outer com.fnproject.fn.api.flow.Flows - anno 0 java.lang.FunctionalInterface() -intf java.io.Serializable -intf java.util.function.Supplier<{com.fnproject.fn.api.flow.Flows$SerSupplier%0}> - -CLSS public com.fnproject.fn.api.flow.FunctionInvocationException -cons public init(com.fnproject.fn.api.flow.HttpResponse) -meth public com.fnproject.fn.api.flow.HttpResponse getFunctionResponse() -supr java.lang.RuntimeException -hfds functionResponse - -CLSS public com.fnproject.fn.api.flow.FunctionInvokeFailedException -cons public init(java.lang.String) -supr com.fnproject.fn.api.flow.PlatformException - -CLSS public com.fnproject.fn.api.flow.FunctionTimeoutException -cons public init(java.lang.String) -supr com.fnproject.fn.api.flow.PlatformException - -CLSS public final !enum com.fnproject.fn.api.flow.HttpMethod -fld public final static com.fnproject.fn.api.flow.HttpMethod DELETE -fld public final static com.fnproject.fn.api.flow.HttpMethod GET -fld public final static com.fnproject.fn.api.flow.HttpMethod HEAD -fld public final static com.fnproject.fn.api.flow.HttpMethod OPTIONS -fld public final static com.fnproject.fn.api.flow.HttpMethod PATCH -fld public final static com.fnproject.fn.api.flow.HttpMethod POST -fld public final static com.fnproject.fn.api.flow.HttpMethod PUT -meth public java.lang.String toString() -meth public static com.fnproject.fn.api.flow.HttpMethod valueOf(java.lang.String) -meth public static com.fnproject.fn.api.flow.HttpMethod[] values() -supr java.lang.Enum -hfds verb - -CLSS public abstract interface com.fnproject.fn.api.flow.HttpRequest -meth public abstract byte[] getBodyAsBytes() -meth public abstract com.fnproject.fn.api.Headers getHeaders() -meth public abstract com.fnproject.fn.api.flow.HttpMethod getMethod() - -CLSS public abstract interface com.fnproject.fn.api.flow.HttpResponse -meth public abstract byte[] getBodyAsBytes() -meth public abstract com.fnproject.fn.api.Headers getHeaders() -meth public abstract int getStatusCode() - -CLSS public com.fnproject.fn.api.flow.InvalidStageResponseException -cons public init(java.lang.String) -supr com.fnproject.fn.api.flow.PlatformException - -CLSS public com.fnproject.fn.api.flow.LambdaSerializationException -cons public init(java.lang.String) -cons public init(java.lang.String,java.lang.Exception) -supr com.fnproject.fn.api.flow.FlowCompletionException - -CLSS public com.fnproject.fn.api.flow.PlatformException -cons public init(java.lang.String) -cons public init(java.lang.String,java.lang.Throwable) -cons public init(java.lang.Throwable) -meth public java.lang.Throwable fillInStackTrace() -supr com.fnproject.fn.api.flow.FlowCompletionException - -CLSS public com.fnproject.fn.api.flow.ResultSerializationException -cons public init(java.lang.String,java.lang.Throwable) -supr com.fnproject.fn.api.flow.FlowCompletionException - -CLSS public com.fnproject.fn.api.flow.StageInvokeFailedException -cons public init(java.lang.String) -supr com.fnproject.fn.api.flow.PlatformException - -CLSS public com.fnproject.fn.api.flow.StageLostException -cons public init(java.lang.String) -supr com.fnproject.fn.api.flow.PlatformException - -CLSS public com.fnproject.fn.api.flow.StageTimeoutException -cons public init(java.lang.String) -supr com.fnproject.fn.api.flow.PlatformException - -CLSS public final com.fnproject.fn.api.flow.WrappedFunctionException -cons public init(java.lang.Throwable) -intf java.io.Serializable -meth public java.lang.Class getOriginalExceptionType() -supr java.lang.RuntimeException -hfds originalExceptionType +CLSS public abstract interface com.fnproject.fn.api.httpgateway.HTTPGatewayContext +meth public abstract InvocationContext getInvocationContext() +meth public abstract Headers getHeaders() +meth public abstract String getRequestURL() +meth public abstract String getMethod() +meth public abstract QueryParameters getQueryParameters() +meth public abstract void addResponseHeader(String key, String value) +meth public abstract void setResponseHeader(String key, String v1, String... vs) +meth public abstract void setStatusCode(int code) CLSS public abstract interface java.io.Closeable intf java.lang.AutoCloseable @@ -350,12 +220,6 @@ cons public init(java.lang.Throwable) supr java.lang.Throwable hfds serialVersionUID -CLSS public abstract interface !annotation java.lang.FunctionalInterface - anno 0 java.lang.annotation.Documented() - anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) - anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[TYPE]) -intf java.lang.annotation.Annotation - CLSS public java.lang.Object cons public init() meth protected java.lang.Object clone() throws java.lang.CloneNotSupportedException @@ -370,10 +234,6 @@ meth public final void wait(long,int) throws java.lang.InterruptedException meth public int hashCode() meth public java.lang.String toString() -CLSS public abstract interface java.lang.Runnable - anno 0 java.lang.FunctionalInterface() -meth public abstract void run() - CLSS public java.lang.RuntimeException cons protected init(java.lang.String,java.lang.Throwable,boolean,boolean) cons public init() @@ -419,6 +279,13 @@ CLSS public abstract interface !annotation java.lang.annotation.Documented anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[ANNOTATION_TYPE]) intf java.lang.annotation.Annotation +CLSS public abstract interface !annotation java.lang.annotation.Repeatable + anno 0 java.lang.annotation.Documented() + anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[ANNOTATION_TYPE]) +intf java.lang.annotation.Annotation +meth public abstract java.lang.Class value() + CLSS public abstract interface !annotation java.lang.annotation.Retention anno 0 java.lang.annotation.Documented() anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) @@ -435,33 +302,3 @@ meth public abstract java.lang.annotation.ElementType[] value() CLSS public abstract interface java.util.EventListener -CLSS public abstract interface java.util.concurrent.Callable<%0 extends java.lang.Object> - anno 0 java.lang.FunctionalInterface() -meth public abstract {java.util.concurrent.Callable%0} call() throws java.lang.Exception - -CLSS public abstract interface java.util.function.BiConsumer<%0 extends java.lang.Object, %1 extends java.lang.Object> - anno 0 java.lang.FunctionalInterface() -meth public abstract void accept({java.util.function.BiConsumer%0},{java.util.function.BiConsumer%1}) -meth public java.util.function.BiConsumer<{java.util.function.BiConsumer%0},{java.util.function.BiConsumer%1}> andThen(java.util.function.BiConsumer) - -CLSS public abstract interface java.util.function.BiFunction<%0 extends java.lang.Object, %1 extends java.lang.Object, %2 extends java.lang.Object> - anno 0 java.lang.FunctionalInterface() -meth public <%0 extends java.lang.Object> java.util.function.BiFunction<{java.util.function.BiFunction%0},{java.util.function.BiFunction%1},{%%0}> andThen(java.util.function.Function) -meth public abstract {java.util.function.BiFunction%2} apply({java.util.function.BiFunction%0},{java.util.function.BiFunction%1}) - -CLSS public abstract interface java.util.function.Consumer<%0 extends java.lang.Object> - anno 0 java.lang.FunctionalInterface() -meth public abstract void accept({java.util.function.Consumer%0}) -meth public java.util.function.Consumer<{java.util.function.Consumer%0}> andThen(java.util.function.Consumer) - -CLSS public abstract interface java.util.function.Function<%0 extends java.lang.Object, %1 extends java.lang.Object> - anno 0 java.lang.FunctionalInterface() -meth public <%0 extends java.lang.Object> java.util.function.Function<{%%0},{java.util.function.Function%1}> compose(java.util.function.Function) -meth public <%0 extends java.lang.Object> java.util.function.Function<{java.util.function.Function%0},{%%0}> andThen(java.util.function.Function) -meth public abstract {java.util.function.Function%1} apply({java.util.function.Function%0}) -meth public static <%0 extends java.lang.Object> java.util.function.Function<{%%0},{%%0}> identity() - -CLSS public abstract interface java.util.function.Supplier<%0 extends java.lang.Object> - anno 0 java.lang.FunctionalInterface() -meth public abstract {java.util.function.Supplier%0} get() - diff --git a/api/src/main/java/com/fnproject/fn/api/FnFeature.java b/api/src/main/java/com/fnproject/fn/api/FnFeature.java new file mode 100644 index 00000000..83f9dd77 --- /dev/null +++ b/api/src/main/java/com/fnproject/fn/api/FnFeature.java @@ -0,0 +1,19 @@ +package com.fnproject.fn.api; + +import java.lang.annotation.*; + +/** + * Annotation to be used in user function classes to enable runtime-wide feature. + * + * Runtime features are initialized at the point that the function class is loaded but prior to the call chain. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Repeatable(FnFeatures.class) +public @interface FnFeature { + /** + * The feature class to load this must have a zero-arg public constructor + * @return feature class + */ + Class value(); +} diff --git a/api/src/main/java/com/fnproject/fn/api/FnFeatures.java b/api/src/main/java/com/fnproject/fn/api/FnFeatures.java new file mode 100644 index 00000000..3db2a4dc --- /dev/null +++ b/api/src/main/java/com/fnproject/fn/api/FnFeatures.java @@ -0,0 +1,17 @@ +package com.fnproject.fn.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to be used in user function classes to enable runtime-wide feature. + * + * Runtime features are initialized at the point that the function class is loaded but prior to the call chain. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface FnFeatures { + FnFeature[] value(); +} diff --git a/api/src/main/java/com/fnproject/fn/api/FunctionInvoker.java b/api/src/main/java/com/fnproject/fn/api/FunctionInvoker.java index 7f1fa29a..70b9fd5d 100644 --- a/api/src/main/java/com/fnproject/fn/api/FunctionInvoker.java +++ b/api/src/main/java/com/fnproject/fn/api/FunctionInvoker.java @@ -6,6 +6,22 @@ * Handles the invocation of a given function call */ public interface FunctionInvoker { + /** + * Phase determines a loose ordering for invocation handler processing + * this should be used with {@link RuntimeContext#addInvoker(FunctionInvoker, Phase)} to add new invoke handlers to a runtime + */ + enum Phase { + /** + * The Pre-Call phase runs before the main function call, all {@link FunctionInvoker} handlers added at this phase are tried prior to calling the {@link Phase#Call} phase + * This phase is typically used for handlers that /may/ intercept the request based on request attributes + */ + PreCall, + /** + * The Call Phase indicates invokers that should handle call values - typically a given runtime will only be handled by one of these + */ + Call + } + /** * Optionally handles an invocation chain for this function *

diff --git a/api/src/main/java/com/fnproject/fn/api/Headers.java b/api/src/main/java/com/fnproject/fn/api/Headers.java index 2e069c03..7029e909 100644 --- a/api/src/main/java/com/fnproject/fn/api/Headers.java +++ b/api/src/main/java/com/fnproject/fn/api/Headers.java @@ -1,16 +1,62 @@ package com.fnproject.fn.api; +import java.io.Serializable; import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Stream; /** - * Represents the headers on an HTTP request or response. Multiple headers with the same key are collapsed into a single - * entry where the values are concatenated by commas as per the HTTP spec (RFC 7230). + * Represents a set of String-String[] header attributes, per HTTP headers. + *

+ * Internally header keys are always canonicalized using HTTP header conventions + *

+ * Headers objects are immutable + *

+ * Keys are are stored and compared in a case-insensitive way and are canonicalised according to RFC 7230 conventions such that : + * + *

    + *
  • a-header
  • + *
  • A-Header
  • + *
  • A-HeaDer
  • + *
+ * are all equivalent - keys are returned in the canonical form (lower cased except for leading characters) + * Where keys do not comply with HTTP header naming they are left as is. */ -public final class Headers { - private Map headers; +public final class Headers implements Serializable { + private static final Headers emptyHeaders = new Headers(Collections.emptyMap()); + private Map> headers; + + private Headers(Map> headersIn) { + this.headers = headersIn; + } + + private static Pattern headerName = Pattern.compile("[A-Za-z0-9!#%&'*+-.^_`|~]+"); + + public Map getAll() { + return headers; + } + + /** + * Calculates the canonical key (cf RFC 7230) for a header + *

+ * If the header contains invalid characters it returns the original header + * + * @param key the header key to canonicalise + * @return a canonical key or the original key if the input contains invalid character + */ + public static String canonicalKey(String key) { + if (!headerName.matcher(key).matches()) { + return key; + } + String parts[] = key.split("-", -1); + for (int i = 0; i < parts.length; i++) { + String p = parts[i]; + if (p.length() > 0) { + parts[i] = p.substring(0, 1).toUpperCase() + p.substring(1).toLowerCase(); + } + } + return String.join("-", parts); - private Headers(Map headers) { - this.headers = headers; } /** @@ -21,6 +67,23 @@ private Headers(Map headers) { * @return {@code Headers} built from headers map */ public static Headers fromMap(Map headers) { + Objects.requireNonNull(headers, "headersIn"); + Map> h = new HashMap<>(); + headers.forEach((k, v) -> h.put(canonicalKey(k), Collections.singletonList(v))); + return new Headers(Collections.unmodifiableMap(new HashMap<>(h))); + } + + /** + * Build a headers object from a map composed of (name, value) entries, we take a copy of the map and + * disallow any further modification + * + * @param headers underlying collection of header entries to copy + * @return {@code Headers} built from headers map + */ + public static Headers fromMultiHeaderMap(Map> headers) { + Map> hm = new HashMap<>(); + + headers.forEach((k, vs) -> hm.put(canonicalKey(k), new ArrayList<>(vs))); return new Headers(Collections.unmodifiableMap(new HashMap<>(Objects.requireNonNull(headers)))); } @@ -30,43 +93,136 @@ public static Headers fromMap(Map headers) { * @return empty headers */ public static Headers emptyHeaders() { - return new Headers(Collections.emptyMap()); + return emptyHeaders; } /** - * Creates a new headers object with the specified header added + * Sets a map of headers, overwriting any headers in the current headers with the respective values * + * @param vals a map of headers + * @return a new headers object with thos headers set + */ + public Headers setHeaders(Map> vals) { + Objects.requireNonNull(vals, "vals"); + Map> nm = new HashMap<>(headers); + vals.forEach((k, vs) -> { + vs.forEach(v -> Objects.requireNonNull(v, "header list contains null entries")); + nm.put(canonicalKey(k), vs); + }); + return new Headers(nm); + } + + /** + * Creates a new headers object with the specified header added - if a header with the same key existed it the new value is appended + *

* This will overwrite an existing header with an exact name match + * * @param key new header key - * @param value new header value + * @param v1 new header value + * @param vs additional header values to set * @return a new headers object with the specified header added */ - public Headers withHeader(String key, String value){ - Map newHeaders = new HashMap<>(); - newHeaders.putAll(getAll()); - newHeaders.put(key,value); - return new Headers(newHeaders); + public Headers addHeader(String key, String v1, String... vs) { + Objects.requireNonNull(key, "key"); + Objects.requireNonNull(key, "value"); + + String canonKey = canonicalKey(key); + + Map> nm = new HashMap<>(headers); + List current = nm.get(canonKey); + + if (current == null) { + List s = new ArrayList<>(); + s.add(v1); + s.addAll(Arrays.asList(vs)); + + nm.put(canonKey, Collections.unmodifiableList(s)); + } else { + List s = new ArrayList<>(current); + s.add(v1); + s.addAll(Arrays.asList(vs)); + nm.put(canonKey, Collections.unmodifiableList(s)); + } + return new Headers(nm); + } /** - * Returns the header matching the specified key. This matches headers in a case-insensitive way and substitutes - * underscore and hyphen characters such that : "CONTENT_TYPE" and "Content-type" are equivalent. If no matching - * header is found then {@code Optional.empty} is returned. + * Creates a new headers object with the specified header set - this overwrites any existin values *

- * Multiple headers are collapsed by {@code fn} into a single header entry delimited by commas (see - * RFC7230 Sec 3.2.2 for details), for example + * This will overwrite an existing header with an exact name match * - *

-     *     Accept: text/html
-     *     Accept: text/plain
-     * 
+ * @param key new header key + * @param v1 new header value + * @param vs more header values to set + * @return a new headers object with the specified header added + */ + public Headers setHeader(String key, String v1, String... vs) { + Objects.requireNonNull(key, "key"); + Objects.requireNonNull(v1, "v1"); + Stream.of(vs).forEach((v) -> Objects.requireNonNull(v, "vs")); + + Map> nm = new HashMap<>(headers); + List s = new ArrayList<>(); + s.add(v1); + s.addAll(Arrays.asList(vs)); + nm.put(canonicalKey(key), Collections.unmodifiableList(s)); + return new Headers(Collections.unmodifiableMap(nm)); + } + + + /** + * Creates a new headers object with the specified headers set - this overwrites any existin values + *

+ * This will overwrite an existing header with an exact name match * - * is collapsed into + * @param key new header key + * @param vs header values to set + * @return a new headers object with the specified header added + */ + public Headers setHeader(String key, Collection vs) { + Objects.requireNonNull(key, "key"); + Objects.requireNonNull(vs, "vs"); + if (vs.size() == 0) { + throw new IllegalArgumentException("can't set keys to an empty list"); + } + vs.forEach((v) -> Objects.requireNonNull(v, "vs")); + + Map> nm = new HashMap<>(headers); + nm.put(canonicalKey(key), Collections.unmodifiableList(new ArrayList<>(vs))); + return new Headers(Collections.unmodifiableMap(nm)); + + } + + /** + * Creates a new headers object with the specified headers remove - this overwrites any existin values + *

+ * This will overwrite an existing header with an exact name match * - *

-     *     Accept: text/html, text/plain
-     * 
+ * @param key new header key + * @return a new headers object with the specified header removed + */ + public Headers removeHeader(String key) { + Objects.requireNonNull(key, "key"); + + String canonKey = canonicalKey(key); + if (!headers.containsKey(canonKey)) { + return this; + } + + Map> nm = new HashMap<>(headers); + nm.remove(canonKey); + return new Headers(Collections.unmodifiableMap(nm)); + + } + + /** + * Returns the header matching the specified key. This matches headers in a case-insensitive way and substitutes + * underscore and hyphen characters such that : "CONTENT_TYPE_HEADER" and "Content-type" are equivalent. If no matching + * header is found then {@code Optional.empty} is returned. + *

+ * When multiple headers are present then the first value is returned- see { #getAllValues(String key)} to get all values for a header * * @param key match key * @return a header matching key or empty if no header matches. @@ -74,20 +230,60 @@ public Headers withHeader(String key, String value){ */ public Optional get(String key) { Objects.requireNonNull(key, "Key cannot be null"); - return getAll().entrySet().stream() - .filter((e) -> e.getKey() - .replaceAll("-", "_") - .equalsIgnoreCase(key.replaceAll("-", "_"))) - .map(Map.Entry::getValue) - .findFirst(); + String canonKey = canonicalKey(key); + + List val = headers.get(canonKey); + if (val == null){ + return Optional.empty(); + } + return Optional.of(val.get(0)); } /** - * The function invocation headers passed on the request + * Returns a collection of current header keys * - * @return a map of Invocation headers. + * @return a collection of keys */ - public Map getAll() { + public Collection keys() { + return headers.keySet(); + } + + /** + * Returns the headers as a map + * + * @return a map of key-values + */ + public Map> asMap() { return headers; } + + /** + * GetAllValues returns all values for a header or an empty list if the header has no values + * @param key the Header key + * @return a possibly empty list of values + */ + public List getAllValues(String key) { + return headers.getOrDefault(canonicalKey(key), Collections.emptyList()); + } + + public int hashCode() { + return headers.hashCode(); + } + + + public boolean equals(Object other) { + if (!(other instanceof Headers)) { + return false; + } + if (other == this) { + return true; + } + return headers.equals(((Headers) other).headers); + } + + @Override + public String toString() { + return Objects.toString(headers); + } + } diff --git a/api/src/main/java/com/fnproject/fn/api/InputEvent.java b/api/src/main/java/com/fnproject/fn/api/InputEvent.java index 2ecab8aa..cda6cca3 100644 --- a/api/src/main/java/com/fnproject/fn/api/InputEvent.java +++ b/api/src/main/java/com/fnproject/fn/api/InputEvent.java @@ -2,6 +2,7 @@ import java.io.Closeable; import java.io.InputStream; +import java.time.Instant; import java.util.function.Function; public interface InputEvent extends Closeable { @@ -17,29 +18,21 @@ public interface InputEvent extends Closeable { */ T consumeBody(Function dest); - /** - * The application name associated with this function - * - * @return an application name - */ - String getAppName(); - /** - * @return The route (including preceding slash) of this function call - */ - String getRoute(); /** - * @return The full request URL of this function invocation + * return the current call ID for this event + * @return a call ID */ - String getRequestUrl(); + String getCallID(); + /** - * The HTTP method used to invoke this function + * The deadline by which this event should be processed - this is information and is intended to help you determine how long you should spend processing your event - if you exceed this deadline Fn will terminate your container. * - * @return an UpperCase HTTP method + * @return a deadline relative to the current system clock that the event must be processed by */ - String getMethod(); + Instant getDeadline(); /** @@ -49,11 +42,5 @@ public interface InputEvent extends Closeable { */ Headers getHeaders(); - /** - * The query parameters of the function invocation - * - * @return an immutable map of query parameters parsed from the request URL - */ - QueryParameters getQueryParameters(); } diff --git a/api/src/main/java/com/fnproject/fn/api/InvocationContext.java b/api/src/main/java/com/fnproject/fn/api/InvocationContext.java index 3b3395a1..080e3bc6 100644 --- a/api/src/main/java/com/fnproject/fn/api/InvocationContext.java +++ b/api/src/main/java/com/fnproject/fn/api/InvocationContext.java @@ -9,6 +9,11 @@ */ public interface InvocationContext { + /** + * Returns the {@link RuntimeContext} associated with this invocation context + * + * @return a runtime context + */ RuntimeContext getRuntimeContext(); /** @@ -19,4 +24,39 @@ public interface InvocationContext { */ void addListener(InvocationListener listener); + + /** + * Returns the current request headers for the invocation + * + * @return the headers passed into the function + */ + Headers getRequestHeaders(); + + /** + * Sets the response content type, this will override the default content type of the output + * + * @param contentType a mime type for the response + */ + default void setResponseContentType(String contentType) { + this.setResponseHeader(OutputEvent.CONTENT_TYPE_HEADER, contentType); + } + + /** + * Adds a response header to the outbound event + * + * @param key header key + * @param value header value + */ + void addResponseHeader(String key, String value); + + /** + * Sets a response header to the outbound event, overriding a previous value. + *

+ * Headers set in this way override any headers returned by the function or any middleware on the function + * + * @param key header key + * @param v1 first value to set + * @param vs other values to set header to + */ + void setResponseHeader(String key, String v1, String... vs); } diff --git a/api/src/main/java/com/fnproject/fn/api/OutputEvent.java b/api/src/main/java/com/fnproject/fn/api/OutputEvent.java index ef13983c..0b93b68f 100644 --- a/api/src/main/java/com/fnproject/fn/api/OutputEvent.java +++ b/api/src/main/java/com/fnproject/fn/api/OutputEvent.java @@ -3,45 +3,82 @@ import java.io.IOException; import java.io.OutputStream; +import java.util.Objects; import java.util.Optional; /** * Wrapper for an outgoing fn event */ public interface OutputEvent { + + String CONTENT_TYPE_HEADER = "Content-Type"; + + /** + * The outcome status of this function event + * This determines how the platform will reflect this error to the customer and how it will treat the container after an error + */ + enum Status { + /** + * The event was successfully processed + */ + Success(200), + /** + * The Function code raised unhandled exception + */ + FunctionError(502), + /** + * The Function code did not respond within a given timeout + */ + FunctionTimeout(504), + /** + * An internal error occurred in the FDK + */ + InternalError(500); + + private final int code; + + Status(int code) { + this.code = code; + } + + public int getCode() { + return this.code; + } + + } + + /** - * Report the HTTP status code of this event. - * For default-format functions, this value is mapped into a success/failure value as follows: - * status codes in the range [100, 400) are considered successful; anything else is a failure. + * Report the outcome status code of this event. * - * @return the status code associated with this event + * @return the status associated with this event */ - int getStatusCode(); + Status getStatus(); - int SUCCESS = 200; - int FAILURE = 500; /** * Report the boolean success of this event. * For default-format functions, this is used to map the HTTP status code into a straight success/failure. + * * @return true if the output event results from a successful invocation. */ default boolean isSuccess() { - return 100 <= getStatusCode() && getStatusCode() < 400; + return getStatus() == Status.Success; } /** - * The indicative content type of the response. + * The content type of the response. *

- * This will only be used when the function format is HTTP * * @return The name of the content type. */ - Optional getContentType(); + default Optional getContentType(){ + return getHeaders().get(CONTENT_TYPE_HEADER); + } /** * Any additional {@link Headers} that should be supplied along with the content - * + *

* These are only used when the function format is HTTP * * @return the headers to add @@ -51,51 +88,79 @@ default boolean isSuccess() { /** * Write the body of the output to a stream * - * @param out an outputstream to emit the body of the event - * @throws IOException OutputStream exceptions percolate up through this method + * @param out an outputstream to emit the body of the event + * @throws IOException OutputStream exceptions percolate up through this method */ void writeToOutput(OutputStream out) throws IOException; + /** + * Creates a new output event based on this one with the headers overriding + * @param headers the headers use in place of this event + * @return a new output event with these set + */ + default OutputEvent withHeaders(Headers headers) { + Objects.requireNonNull(headers, "headers"); + + OutputEvent a = this; + return new OutputEvent() { + + @Override + public Status getStatus() { + return a.getStatus(); + } + + @Override + public Headers getHeaders() { + return headers; + } + + @Override + public void writeToOutput(OutputStream out) throws IOException { + a.writeToOutput(out); + } + }; + } + /** * Create an output event from a byte array * * @param bytes the byte array to write to the output - * @param statusCode the status code to report + * @param status the status code to report * @param contentType the content type to present on HTTP responses * @return a new output event */ - static OutputEvent fromBytes(byte[] bytes, int statusCode, String contentType) { - return fromBytes(bytes, statusCode, contentType, Headers.emptyHeaders()); - } + static OutputEvent fromBytes(byte[] bytes, Status status, String contentType) { + return fromBytes(bytes, status, contentType, Headers.emptyHeaders()); + } /** * Create an output event from a byte array * * @param bytes the byte array to write to the output - * @param statusCode the HTTP status code of this event - * @param contentType the content type to present on HTTP responses + * @param status the status code of this event + * @param contentType the content type to present on HTTP responses or null * @param headers any additional headers to supply with HTTP responses * @return a new output event */ - static OutputEvent fromBytes(byte[] bytes, int statusCode, String contentType, Headers headers) { - if (statusCode < 100 || 600 <= statusCode) { - throw new IllegalArgumentException("Valid status codes must lie in the range [100, 599]"); - } + static OutputEvent fromBytes(final byte[] bytes, final Status status, final String contentType, final Headers headers) { + Objects.requireNonNull(bytes, "bytes"); + Objects.requireNonNull(status, "status"); + Objects.requireNonNull(headers, "headers"); + + final Headers newHeaders = contentType== null?Headers.emptyHeaders():headers.setHeader("Content-Type",contentType); return new OutputEvent() { @Override - public int getStatusCode() { - return statusCode; + public Status getStatus() { + return status; } - @Override - public Optional getContentType() { - return Optional.ofNullable(contentType); - } @Override - public Headers getHeaders() { return headers; } + public Headers getHeaders() { + return newHeaders; + } @Override public void writeToOutput(OutputStream out) throws IOException { @@ -104,24 +169,25 @@ public void writeToOutput(OutputStream out) throws IOException { }; } - static OutputEvent emptyResult(int statusCode) { - if (statusCode < 100 || 600 <= statusCode) { - throw new IllegalArgumentException("Valid status codes must lie in the range [100, 599]"); - } + /** + * Returns an output event with an empty body and a given status + * @param status the status of the event + * @return a new output event + */ + static OutputEvent emptyResult(final Status status) { + Objects.requireNonNull(status, "status"); + return new OutputEvent() { @Override - public int getStatusCode() { - return statusCode; + public Status getStatus() { + return status; } @Override - public Optional getContentType() { - return Optional.empty(); + public Headers getHeaders() { + return Headers.emptyHeaders(); } - @Override - public Headers getHeaders() { return Headers.emptyHeaders(); } - @Override public void writeToOutput(OutputStream out) throws IOException { diff --git a/api/src/main/java/com/fnproject/fn/api/QueryParameters.java b/api/src/main/java/com/fnproject/fn/api/QueryParameters.java index 08514054..f0dacd05 100644 --- a/api/src/main/java/com/fnproject/fn/api/QueryParameters.java +++ b/api/src/main/java/com/fnproject/fn/api/QueryParameters.java @@ -7,7 +7,7 @@ /** * Wrapper for query parameters map parsed from the URL of a function invocation. */ -public interface QueryParameters { +public interface QueryParameters { /** * Find the first entry for {@code key} if it exists otherwise returns {@code Optional.empty} * diff --git a/api/src/main/java/com/fnproject/fn/api/RuntimeContext.java b/api/src/main/java/com/fnproject/fn/api/RuntimeContext.java index 865a6d10..08466cb7 100644 --- a/api/src/main/java/com/fnproject/fn/api/RuntimeContext.java +++ b/api/src/main/java/com/fnproject/fn/api/RuntimeContext.java @@ -13,6 +13,20 @@ * of a function; they will not change between multiple invocations of a hot function. */ public interface RuntimeContext { + + + /** + * The application ID of the application associated with this function + * @return an application ID + */ + String getAppID(); + + /** + * THe function ID of the function + * @return a function ID + */ + String getFunctionID(); + /** * Create an instance of the user specified class on which the target function to invoke is declared. * @@ -77,7 +91,7 @@ public interface RuntimeContext { * * @param targetMethod The user function method * @param param The index of the parameter - * @return a list of configured input coercions to apply to the given parameter + * @return a list of configured input coercions to apply to the given parameter */ List getInputCoercions(MethodWrapper targetMethod, int param); @@ -105,7 +119,21 @@ public interface RuntimeContext { * Set an {@link FunctionInvoker} for this function. The invoker will override * the built in function invoker, although the cloud threads invoker will still * have precedence so that cloud threads can be used from functions using custom invokers. + * * @param invoker The {@link FunctionInvoker} to add. + * @deprecated this is equivalent to {@link #addInvoker(FunctionInvoker, FunctionInvoker.Phase)} with a phase of {@link FunctionInvoker.Phase#Call} + */ + default void setInvoker(FunctionInvoker invoker) { + addInvoker(invoker, FunctionInvoker.Phase.Call); + } + + + /** + * Adds an FunctionInvoker handler to the runtime - new FunctionInvokers are added at the head of the specific phase they apply to so ordering may be important + * + * + * @param invoker an invoker to use to handle a given call + * @param phase the phase at which to add the invoke */ - void setInvoker(FunctionInvoker invoker); + void addInvoker(FunctionInvoker invoker, FunctionInvoker.Phase phase); } diff --git a/api/src/main/java/com/fnproject/fn/api/RuntimeFeature.java b/api/src/main/java/com/fnproject/fn/api/RuntimeFeature.java new file mode 100644 index 00000000..43365794 --- /dev/null +++ b/api/src/main/java/com/fnproject/fn/api/RuntimeFeature.java @@ -0,0 +1,17 @@ +package com.fnproject.fn.api; + +/** + * RuntimeFeatures are classes that configure the Fn Runtime prior to startup and can be loaded by annotating the function class with a {@link FnFeature} annotation + * Created on 10/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public interface RuntimeFeature { + + /** + * Initialize the runtime context for this function + * + * @param context a runtime context to initalize + */ + void initialize(RuntimeContext context); +} diff --git a/api/src/main/java/com/fnproject/fn/api/httpgateway/HTTPGatewayContext.java b/api/src/main/java/com/fnproject/fn/api/httpgateway/HTTPGatewayContext.java new file mode 100644 index 00000000..d97c0edb --- /dev/null +++ b/api/src/main/java/com/fnproject/fn/api/httpgateway/HTTPGatewayContext.java @@ -0,0 +1,84 @@ +package com.fnproject.fn.api.httpgateway; + +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InvocationContext; +import com.fnproject.fn.api.QueryParameters; + +/** + * A context for accessing and setting HTTP Gateway atributes such aas headers and query parameters from a function call + *

+ * Created on 19/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public interface HTTPGatewayContext { + + /** + * Returns the underlying invocation context behind this HTTP context + * + * @return an invocation context related to this function + */ + InvocationContext getInvocationContext(); + + + /** + * Returns the HTTP headers for the request associated with this function call + * If no headers were set this will return an empty headers object + * + * @return the incoming HTTP headers sent in the gateway request + */ + Headers getHeaders(); + + + /** + * Returns the fully qualified request URI that the function was called with, including query parameters + * + * @return the request URI of the function + */ + String getRequestURL(); + + + /** + * Returns the incoming request method for the HTTP + * + * @return the HTTP method set on this call + */ + String getMethod(); + + /** + * Returns the query parameters of the request + * + * @return a query parameters object + */ + QueryParameters getQueryParameters(); + + + /** + * Adds a response header to the outbound event + * + * @param key header key + * @param value header value + */ + void addResponseHeader(String key, String value); + + /** + * Sets a response header to the outbound event, overriding a previous value. + *

+ * Headers set in this way override any headers returned by the function or any middleware on the function + *

+ * Setting the "Content-Type" response header also sets this on the underlying Invocation context + * + * @param key header key + * @param v1 first value to set + * @param vs other values to set header to + */ + void setResponseHeader(String key, String v1, String... vs); + + /** + * Sets the HTTP status code of the response + * + * @param code an HTTP status code + * @throws IllegalArgumentException if the code is < 100 or >l=600 + */ + void setStatusCode(int code); +} diff --git a/api/src/test/java/com/fnproject/fn/api/HeadersTest.java b/api/src/test/java/com/fnproject/fn/api/HeadersTest.java new file mode 100644 index 00000000..946ed305 --- /dev/null +++ b/api/src/test/java/com/fnproject/fn/api/HeadersTest.java @@ -0,0 +1,32 @@ +package com.fnproject.fn.api; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Created on 10/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class HeadersTest { + + @Test + public void shouldCanonicalizeHeaders(){ + for (String[] v : new String[][] { + {"",""}, + {"a","A"}, + {"fn-ID-","Fn-Id-"}, + {"myHeader-VaLue","Myheader-Value"}, + {" Not a Header "," Not a Header "}, + {"-","-"}, + {"--","--"}, + {"a-","A-"}, + {"-a","-A"} + }){ + assertThat(Headers.canonicalKey(v[0])).isEqualTo(v[1]); + } + } + + +} diff --git a/build-image/docker-build.sh b/build-image/docker-build.sh deleted file mode 100755 index d5dc37b1..00000000 --- a/build-image/docker-build.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -ex - -cd /tmp/staging-repository && python -mSimpleHTTPServer 18080 1>>/tmp/http-logs 2>&1 & -SRV_PROCESS=$! - -if [ -n "$DOCKER_LOCALHOST" ]; then - REPO_ENV="--build-arg FN_REPO_URL=http://$DOCKER_LOCALHOST:18080" -fi - -docker build $REPO_ENV $* - -kill $SRV_PROCESS diff --git a/build.sh b/build.sh new file mode 100755 index 00000000..b19ac5a0 --- /dev/null +++ b/build.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +set -e +set -x +mkdir -p /tmp/staging_repo +rm -rf /tmp/staging_repo/* + +BUILD_VERSION=${FN_FDK_VERSION:-1.0.0-SNAPSHOT} +export REPOSITORY_LOCATION=${REPOSITORY_LOCATION:-/tmp/staging_repo} + +while [ $# -ne 0 ] +do + case "$1" in + --build-native-java) + BUILD_NATIVE_JAVA=true + ;; + esac + shift +done + +( + runtime/src/main/c/rebuild_so.sh +) + +mvn -B deploy -DaltDeploymentRepository=localStagingDir::default::file://${REPOSITORY_LOCATION} + +( + cd images/build + ./docker-build.sh -t fnproject/fn-java-fdk-build:${BUILD_VERSION} . +) + +( + cd images/build + ./docker-build.sh -f Dockerfile-jdk11 -t fnproject/fn-java-fdk-build:jdk11-${BUILD_VERSION} . +) + +( + cd runtime + docker build -t fnproject/fn-java-fdk:${BUILD_VERSION} -f ../images/runtime/Dockerfile . +) + +( + cd runtime + docker build -f ../images/runtime/Dockerfile-jre11 -t fnproject/fn-java-fdk:jre11-${BUILD_VERSION} . +) + +( + workdir=$(pwd)/runtime + cd images/build-native + ./docker-build.sh ${workdir} +) + +( + cd images/init-native + ./docker-build.sh +) \ No newline at end of file diff --git a/build_in_docker.sh b/build_in_docker.sh new file mode 100644 index 00000000..c9f5aee9 --- /dev/null +++ b/build_in_docker.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +docker run --rm --name compile -v "$(pwd)":/usr/src/mymaven -w /usr/src/mymaven svenruppert/maven-3.5-jdk-08 mvn clean install diff --git a/docs/ExtendingDataBinding.md b/docs/ExtendingDataBinding.md index bc8195c9..26a8a2d2 100644 --- a/docs/ExtendingDataBinding.md +++ b/docs/ExtendingDataBinding.md @@ -19,7 +19,7 @@ First of all, let's create a new function project. If you haven't done it alread ```shell $ fn start & -$ fn apps create java-app +$ fn create app java-app Successfully created app: java-app ``` diff --git a/docs/FAQ.md b/docs/FAQ.md index 375650b5..2477c4ab 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -12,11 +12,7 @@ The FDK is comprised of: - a build-time Docker image for repeatable builds. ### Is the FDK required in order to run Java on Fn? -No. You can still write Java functions on Fn without using the FDK. However using the FDK will make several things easier for you: - 1. A curated base image for Java 8 and Java 9 means that you don't have to build and maintain your own image. These images contain optimizations for quick JVM startup times. - 1. Accessing configuration from Fn is easy through FDK APIs. - 1. Input and output type coercion reduces the amount of serialization and formatting boilerplate that you have to write. - 1. A JUnit rule provides a realistic test harness for you to test your function in isolation. +Yes - The FDK implements the IO/contract with the FN service and is required to receive events from the platform ### What is Fn Flow? Fn Flow is a [Java API](https://github.com/fnproject/fn-java-fdk/blob/master/docs/FnFlowsUserGuide.md) and [corresponding service](https://github.com/fnproject/flow) that helps you create complex, long-running, fault-tolerant functions using a promises-style asynchronous API. Check out the [Fn Flow docs](https://github.com/fnproject/fn-java-fdk/blob/master/docs/FnFlowsUserGuide.md) for more information. diff --git a/docs/FnFlowsAdvancedTopics.md b/docs/FnFlowsAdvancedTopics.md index ac2f75a5..efd40e6d 100644 --- a/docs/FnFlowsAdvancedTopics.md +++ b/docs/FnFlowsAdvancedTopics.md @@ -61,6 +61,7 @@ An important consideration is that, if your lambda captures fields from your function class, then that class must also be Serializable: ```java +@FnFeature(FlowFeature.class) public class MyFunction{ private String config = "foo"; @@ -82,6 +83,7 @@ E.g. making `MyFunction` serializable will work as the function instance object will be captured alongside the lambda: ```java +@FnFeature(FlowFeature.class) public class MyFunction implements Serializable{ private String config = "foo"; @@ -104,6 +106,7 @@ prior to passing them, removing the need to make the function class serializable. For example: ```java +@FnFeature(FlowFeature.class) public class MyFunction{ private final Database db; // non-serializable object private final String config = "foo"; @@ -128,6 +131,7 @@ Alternatively, you can make non-serializable fields `transient` and construct them on the fly: ```java +@FnFeature(FlowFeature.class) public class MyFunction implements Serialiable{ private final transient Database db; // non-serializable object private final String config = "foo"; @@ -298,6 +302,7 @@ exception. E.g.: ```java +@FnFeature(FlowFeature.class) public class MyFunction{ public static class MyException extends RuntimeException{ public MyException(String message){ diff --git a/docs/FnFlowsUserGuide.md b/docs/FnFlowsUserGuide.md index 91ed4ca4..7834fe0c 100644 --- a/docs/FnFlowsUserGuide.md +++ b/docs/FnFlowsUserGuide.md @@ -60,7 +60,7 @@ $ fn start Similarly, start the Flows server server and point it at the functions server API URL: ``` -$ DOCKER_LOCALHOST=$(docker inspect --type container -f '{{.NetworkSettings.Gateway}}' functions) +$ DOCKER_LOCALHOST=$(docker inspect --type container -f '{{.NetworkSettings.Gateway}}' fnserver) $ docker run --rm \ -p 8081:8081 \ @@ -99,6 +99,19 @@ func.yaml created ``` +### Add the Flow runtime to your function + +In your `pom.xml` add a depdendency on `flow-runtime` : + +```$ml + + com.fnproject.fn + flow-runtime + ${fdk.version} + + +``` + ### Create a Flow within your Function You will create a function that produces the nth prime number and then returns @@ -117,7 +130,10 @@ package com.example.fn; import com.fnproject.fn.api.flow.Flow; import com.fnproject.fn.api.flow.Flows; +import com.fnproject.fn.runtime.flow.FlowFeature; +import com.fnproject.fn.api.FnFeature; +@FnFeature(FlowFeature.class) public class PrimeFunction { public String handleRequest(int nth) { @@ -166,7 +182,7 @@ path: /primes Create your app and deploy your function: ``` -$ fn apps create flows-example +$ fn create app flows-example Successfully created app: flows-example $ fn deploy --app flows-example @@ -178,20 +194,27 @@ Configure your function to talk to the local flow service endpoint: ``` $ DOCKER_LOCALHOST=$(docker inspect --type container -f '{{.NetworkSettings.Gateway}}' functions) -$ fn apps config set flows-example COMPLETER_BASE_URL "http://$DOCKER_LOCALHOST:8081" +$ fn config app flows-example COMPLETER_BASE_URL "http://$DOCKER_LOCALHOST:8081" ``` ### Run your Flow function -You can now run your function using `fn call` or HTTP and curl: +You can now run your function using `fn invoke` or HTTP. ``` -$ echo 10 | fn call flows-example /primes +$ echo 10 | fn invoke flows-example primes The 10th prime number is 29 ``` +To invoke your function via HTTP, you need to know its invocation endpoint (or the function needs to have an HTTP trigger defined). + +``` +$ fn inspect fn flows-examples primes ``` -$ curl -XPOST -d "10" http://localhost:8080/r/flows-example/primes + +Take note of the `fnproject.io/fn/invokeEndpoint` URL and invoke it (ex. using curl). + +$ curl -X POST -d "10" http://localhost:8080/invoke/... The 10th prime number is 29 ``` diff --git a/docs/HTTPGatewayFunctions.md b/docs/HTTPGatewayFunctions.md new file mode 100644 index 00000000..86afd87a --- /dev/null +++ b/docs/HTTPGatewayFunctions.md @@ -0,0 +1,36 @@ +# Accessing HTTP Information From Functions + +Functions can be used to handle events, RPC calls or HTTP requests. When you are writing a function that handles an HTTP request you frequently need access to the HTTP headers of the incoming request or need to set HTTP headers or the status code on the outbound respsonse. + + +In Fn for Java, when your function is being served by an HTTP trigger (or another compatible HTTP gateway) you can get access to both the incoming request headers for your function by adding a 'com.fnproject.fn.api.httpgateway.HTTPGatewayContext' parameter to your function's parameters. + + + Using this allows you to : + + * Read incoming headers + * Access the method and request URL for the trigger + * Write outbound headers to the response + * Set the status code of the response + + + For example this function reads a request header the method and request URL, sets an response header and sets the response status code to perform an HTTP redirect. + +```java +package com.fnproject.fn.examples; +import com.fnproject.fn.api.httpgateway.HTTPGatewayContext; + + +public class RedirectFunction { + + public void redirect(HTTPGatewayContext hctx) { + System.err.println("Request URL is:" + hctx.getRequestURL()); + System.err.println("Trace ID" + hctx.getHeaders().get("My-Trace-ID").orElse("N/A")); + + hctx.setResponseHeader("Location","http://example.com"); + hctx.setStatusCode(302); + + } +} + +``` diff --git a/docs/TestingFunctions.md b/docs/TestingFunctions.md index ffabd74d..9127b16e 100644 --- a/docs/TestingFunctions.md +++ b/docs/TestingFunctions.md @@ -16,7 +16,7 @@ To import the testing library add the following dependency to your Maven project com.fnproject.fn testing - 1.0.0-SNAPSHOT + ${fdk.version} test ``` @@ -155,9 +155,33 @@ You can test that this is all handled correctly as follows: # Testing Fn Flows -You can use `FnTestingRule` to test [Fn Flows](FnFlowsUserGuide.md) within your functions. If flow stages are started by functions within `thenRun` then the testing rule will execute the stages of those flows locally, returning when all spawned flows are complete. +You can use `FlowTesting` to test [Fn Flows](FnFlowsUserGuide.md) within your functions. If flow stages are started by functions within `thenRun` then the testing rule will execute the stages of those flows locally, returning when all spawned flows are complete. -`FnTestingRule` supports mocking the behaviour of Fn functions invoked by the `invokeFunction()` API within flows. +Start by importing the `flow-testing` library into your functino in `test` scope: + +```xml + + com.fnproject.fn + flow-testing + ${fdk.version} + test + +``` + +Then create a `FlowTesting` field in your test class, passing the `FnTesting` rule as a parameter: + +```java +import com.fnproject.fn.testing.FnTestingRule; +import com.fnproject.fn.testing.flow.FlowTesting; + +public class FunctionTest { + @Rule + public final FnTestingRule testing = FnTestingRule.createDefault(); + + private final FlowTesting flowTesting = FlowTesting.create(testing); +``` + +`FlowTesting` supports mocking the behaviour of Fn functions invoked by the `invokeFunction()` API within flows. You can specify that the invocation a function returns a valid value (as a byte array): @@ -165,7 +189,7 @@ You can specify that the invocation a function returns a valid value (as a byte @Test public void callsRemoteFunctionWhichSucceeds() { - testing.givenFn("example/other-function").withResult("blah".getBytes()); + flowTesting.givenFn("example/other-function").withResult("blah".getBytes()); // ... @@ -178,8 +202,8 @@ Or you can specify that the invocation a function will cause a user error or a p @Test public void callsRemoteFunctionWhichCausesAnError() { - testing.givenFn("example/other-function").withFunctionError(); - testing.givenFn("example/other-function-2").withPlatformError(); + flowTesting.givenFn("example/other-function").withFunctionError(); + flowTesting.givenFn("example/other-function-2").withPlatformError(); // ... @@ -196,7 +220,7 @@ used to check some behavior: @Test public void callsRemoteFunction() { - testing.givenFn("example/other-function").withAction( (data) -> { called.set(true); return data; } ); + flowTesting.givenFn("example/other-function").withAction( (data) -> { called.set(true); return data; } ); called.set(false); @@ -221,7 +245,7 @@ If you need to share objects or static data between your test classes and your f ```java testing.addSharedClass(MyClassWithStaticState.class); // Shares only the specific class testing.addSharedPrefix("com.example.MyClassWithStaticState"); // Shares the class and anything under it - testing.addSharedPrefix("com.example.mysubpackage."); // Shares anyhting under a package + testing.addSharedPrefix("com.example.mysubpackage."); // Shares anything under a package ``` While it is possible, it is not generally correct to share the function class itself with the test Class Loader - doing so may result in unexpected (not representative of the real fn platform) initialisation of static fields on the class. With Flows sharing the test class may also result in concurrent access to static data (via `@FnConfiguration` methods). \ No newline at end of file diff --git a/examples/README.md b/examples/README.md index 4995a246..0514760b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,39 +1,39 @@ -# `fn` Java FDK Example Projects +# Fn Java FDK Example Projects In this directory you will find some example projects demonstrating different -features of the `fn` Java FDK: +features of the Fn Java FDK: * Plain java code support (`string-reverse`) * Functional testing of your functions (`regex-query` and `qr-code`) * Built-in JSON coercion (`regex-query`) * [InputEvent and OutputEvent](/docs/DataBinding.md) handling (`qr-code`) -## 1. String reverse +## (1) [String reverse](string-reverse/README.md) This function takes a string and returns the reverse of the string. -The `fn` Java FDK runtime will handle marshalling data into your -functions without the function having to have any knowledge of the FDK API. +The Fn Java FDK handles marshalling data into your +functions without the function having any knowledge of the FDK API. -## 2. Regex query +## (2) Regex query This function takes a JSON object containing a `text` field and a `regex` field and return a JSON object with a list of matches in the `matches` field. It demonstrates the builtin JSON support of the fn Java -wrapper (provided through Jackson) and how the platform handles serialisation +wrapper (provided through Jackson) and how the platform handles serialization of POJO return values. -## 3. QR Code gen +## (3) QR Code gen This function parses the query parameters of a GET request (through the `InputEvent` passed into the function) to generate a QR code. It demonstrates the `InputEvent` and `OutputEvent` interfaces which provide low level access to data entering the `fn` Java FDK. -## 4. Asynchronous thumbnails generation +## (4) Asynchronous thumbnails generation This example showcases the Fn Flow asynchronous execution API, by creating a workflow that takes an image and asynchronously generates three thumbnails for it, then uploads them to an object storage. -## 5. Gradle build +## (5) Gradle build This shows how to use Gradle to build functions using the Java FDK. diff --git a/examples/async-thumbnails/README.md b/examples/async-thumbnails/README.md index 10616498..b3cff27d 100644 --- a/examples/async-thumbnails/README.md +++ b/examples/async-thumbnails/README.md @@ -32,9 +32,8 @@ this example. Run: ``` This will start a local functions service, a local flow completion -service, and will set up a `myapp` application and three routes: `/resize128`, -`/resize256` and `/resize512`. The routes are implemented as Fn functions -which just invoke `imagemagick` to convert the images to the specified sizes. +service, and will set up a `myapp` application and three functions: `resize128`, +`resize256` and `resize512`. These functions just invoke `imagemagick` to convert the images to the specified sizes. The setup script also starts a docker container with an object storage daemon based on `minio` (with access key `alpha` and secret key `betabetabetabeta`). @@ -48,14 +47,9 @@ docker container, so that you can verify when the thumbnails are uploaded. Build the function locally: ```bash -$ fn build +$ fn deploy --local --app myapp ``` -Create a route to host the function: - -```bash -$ fn routes create myapp /async-thumbnails -``` Configure the app. In order to do this you must determine the IP address of the storage server docker container: @@ -68,18 +62,18 @@ $ docker inspect --type container -f '{{range .NetworkSettings.Networks}}{{.IPAd and then use it as the storage host: ```bash -$ fn routes config set myapp /async-thumbnails OBJECT_STORAGE_URL http://172.17.0.4:9000 +$ fn config app myapp OBJECT_STORAGE_URL http://172.17.0.4:9000 myapp /async-thumbnails updated OBJECT_STORAGE_URL with http://172.17.0.4:9000 -$ fn routes config set myapp /async-thumbnails OBJECT_STORAGE_ACCESS alpha +$ fn config app myapp OBJECT_STORAGE_ACCESS alpha myapp /async-thumbnails updated OBJECT_STORAGE_ACCESS with alpha -$ fn routes config set myapp /async-thumbnails OBJECT_STORAGE_SECRET betabetabetabeta +$ fn config app myapp OBJECT_STORAGE_SECRET betabetabetabeta myapp /async-thumbnails updated OBJECT_STORAGE_SECRET with betabetabetabeta ``` Invoke the function by passing the provided test image: ```bash -$ curl -X POST --data-binary @test-image.png -H "Content-type: application/octet-stream" "http://localhost:8080/r/myapp/async-thumbnails" +$ curl -X POST --data-binary @test-image.png -H "Content-type: application/octet-stream" "http://localhost:8080/t/myapp/async-thumbnails" {"imageId":"bd74fff4-0388-4c6f-82f2-8cde9ba9b6fc"} ``` @@ -116,6 +110,13 @@ public class ThumbnailsFunction { .orElseThrow(() -> new RuntimeException("Missing configuration: OBJECT_STORAGE_ACCESS")); storageSecretKey = ctx.getConfigurationByKey("OBJECT_STORAGE_SECRET") .orElseThrow(() -> new RuntimeException("Missing configuration: OBJECT_STORAGE_SECRET")); + + resize128ID = ctx.getConfigurationByKey("RESIZE_128_FN_ID") + .orElseThrow(() -> new RuntimeException("Missing configuration: RESIZE_128_FN_ID")); + resize256ID = ctx.getConfigurationByKey("RESIZE_256_FN_ID") + .orElseThrow(() -> new RuntimeException("Missing configuration: RESIZE_256_FN_ID")); + resize512ID = ctx.getConfigurationByKey("RESIZE_512_FN_ID") + .orElseThrow(() -> new RuntimeException("Missing configuration: RESIZE_512_FN_ID")); } // ... @@ -155,11 +156,11 @@ public class ThumbnailsFunction { Flow runtime = Flows.currentFlow(); runtime.allOf( - runtime.invokeFunction("myapp/resize128", HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) + runtime.invokeFunction(resize128ID, HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) .thenAccept((img) -> objectUpload(img.getBodyAsBytes(), id + "-128.png")), - runtime.invokeFunction("myapp/resize256", HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) + runtime.invokeFunction(resize256ID, HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) .thenAccept((img) -> objectUpload(img.getBodyAsBytes(), id + "-256.png")), - runtime.invokeFunction("myapp/resize512", HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) + runtime.invokeFunction(resize512ID, HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) .thenAccept((img) -> objectUpload(img.getBodyAsBytes(), id + "-512.png")), runtime.supply(() -> objectUpload(imageBuffer, id + ".png")) ); @@ -218,8 +219,9 @@ in [Testing Functions](../../docs/TestingFunctions.md). ```java public class ThumbnailsFunctionTest { - @Rule - public final FnTestingRule testing = FnTestingRule.createDefault(); + @Rule + public final FnTestingRule fn = FnTestingRule.createDefault(); + private final FlowTesting flow = FlowTesting.create(fn); // ... } @@ -259,20 +261,22 @@ public class ThumbnailsFunctionTest { @Test public void testThumbnail() { - testing - .setConfig("OBJECT_STORAGE_URL", "http://localhost:" + mockServer.port()) - .setConfig("OBJECT_STORAGE_ACCESS", "alpha") - .setConfig("OBJECT_STORAGE_SECRET", "betabetabetabeta") + fn.setConfig("OBJECT_STORAGE_URL", "http://localhost:" + mockServer.port()) + .setConfig("OBJECT_STORAGE_ACCESS", "alpha") + .setConfig("OBJECT_STORAGE_SECRET", "betabetabetabeta") + .setConfig("RESIZE_128_FN_ID","myapp/resize128") + .setConfig("RESIZE_256_FN_ID","myapp/resize256") + .setConfig("RESIZE_512_FN_ID","myapp/resize512"); - .givenFn("myapp/resize128") + flow.givenFn("myapp/resize128") .withAction((data) -> "128".getBytes()) .givenFn("myapp/resize256") .withAction((data) -> "256".getBytes()) .givenFn("myapp/resize512") .withAction((data) -> "512".getBytes()) - .givenEvent() + fn.givenEvent() .withBody("testing".getBytes()) .enqueue(); @@ -301,21 +305,23 @@ public class ThumbnailsFunctionTest { @Test public void anExternalFunctionFailure() { - testing - .setConfig("OBJECT_STORAGE_URL", "http://localhost:" + mockServer.port()) - .setConfig("OBJECT_STORAGE_ACCESS", "alpha") - .setConfig("OBJECT_STORAGE_SECRET", "betabetabetabeta") - - .givenFn("myapp/resize128") - .withResult("128".getBytes()) - .givenFn("myapp/resize256") - .withResult("256".getBytes()) - .givenFn("myapp/resize512") - .withFunctionError() - - .givenEvent() - .withBody("testing".getBytes()) - .enqueue(); + fn.setConfig("OBJECT_STORAGE_URL", "http://localhost:" + mockServer.port()) + .setConfig("OBJECT_STORAGE_ACCESS", "alpha") + .setConfig("OBJECT_STORAGE_SECRET", "betabetabetabeta") + .setConfig("RESIZE_128_FN_ID","myapp/resize128") + .setConfig("RESIZE_256_FN_ID","myapp/resize256") + .setConfig("RESIZE_512_FN_ID","myapp/resize512"); + + flow.givenFn("myapp/resize128") + .withResult("128".getBytes()) + .givenFn("myapp/resize256") + .withResult("256".getBytes()) + .givenFn("myapp/resize512") + .withFunctionError(); + + fn.givenEvent() + .withBody("testing".getBytes()) + .enqueue(); // Mock the http endpoint mockMinio(); diff --git a/examples/async-thumbnails/func.yaml b/examples/async-thumbnails/func.yaml index d6003dc4..7bd97826 100644 --- a/examples/async-thumbnails/func.yaml +++ b/examples/async-thumbnails/func.yaml @@ -1,7 +1,11 @@ -name: fn-example/async-thumbnails -version: 0.0.1 +schema_version: 20180708 +name: async-thumbnails +version: 0.0.8 runtime: java cmd: com.fnproject.fn.examples.ThumbnailsFunction::handleRequest -path: /async-thumbnails -format: http -timeout: 30 +format: http-stream +timeout: 120 +triggers: +- name: async-thumbnails + type: http + source: /async-thumbnails diff --git a/examples/async-thumbnails/pom.xml b/examples/async-thumbnails/pom.xml index e554bee8..c65684ac 100644 --- a/examples/async-thumbnails/pom.xml +++ b/examples/async-thumbnails/pom.xml @@ -6,8 +6,11 @@ UTF-8 - 1.0.0-SNAPSHOT + UTF-8 + + 1.0.0-SNAPSHOT 2.8.47 + 2.9.10 com.fnproject.fn.examples @@ -15,25 +18,48 @@ 1.0.0-SNAPSHOT + com.fnproject.fn api - ${fnproject.version} + ${fdk.version} + + + com.fnproject.fn + flow-runtime + ${fdk.version} commons-net commons-net 3.6 + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + io.minio minio - 3.0.5 + 5.0.1 + + + com.fnproject.fn + testing-core + ${fdk.version} + test + + + com.fnproject.fn + flow-testing + ${fdk.version} + test com.fnproject.fn - testing - ${fnproject.version} + testing-junit4 + ${fdk.version} test @@ -46,7 +72,7 @@ com.github.tomakehurst wiremock - 2.7.1 + 2.19.0 @@ -55,7 +81,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.3 + 3.8.0 1.8 1.8 diff --git a/examples/async-thumbnails/run.sh b/examples/async-thumbnails/run.sh index 76315c0f..68bd6574 100755 --- a/examples/async-thumbnails/run.sh +++ b/examples/async-thumbnails/run.sh @@ -1,15 +1,11 @@ #!/bin/bash +set -e -fn build +fn --verbose deploy --app myapp --local -fn routes create myapp /async-thumbnails -STORAGE_SERVER_IP=`docker inspect --type container -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' example-storage-server` -fn routes config set myapp /async-thumbnails OBJECT_STORAGE_URL http://${STORAGE_SERVER_IP}:9000 -fn routes config set myapp /async-thumbnails OBJECT_STORAGE_ACCESS alpha -fn routes config set myapp /async-thumbnails OBJECT_STORAGE_SECRET betabetabetabeta - -curl -X POST --data-binary @test-image.png -H "Content-type: application/octet-stream" "http://localhost:8080/r/myapp/async-thumbnails" +echo "Calling function" +curl -v -X POST --data-binary @test-image.png -H "Content-type: application/octet-stream" "http://localhost:8080/t/myapp/async-thumbnails" echo "Contents of bucket" mc ls -r example-storage-server diff --git a/examples/async-thumbnails/setup/resize128/func.yaml b/examples/async-thumbnails/setup/resize128/func.yaml index b7a5071b..27013369 100644 --- a/examples/async-thumbnails/setup/resize128/func.yaml +++ b/examples/async-thumbnails/setup/resize128/func.yaml @@ -1,4 +1,5 @@ -name: example/resize128 -version: 0.0.1 +schema_version: 20180708 +name: resize128 +version: 0.0.5 entrypoint: convert - -resize 128x128 - -path: /resize128 +format: default diff --git a/examples/async-thumbnails/setup/resize256/func.yaml b/examples/async-thumbnails/setup/resize256/func.yaml index 9261f2f6..cd5b4032 100644 --- a/examples/async-thumbnails/setup/resize256/func.yaml +++ b/examples/async-thumbnails/setup/resize256/func.yaml @@ -1,4 +1,5 @@ -name: example/resize256 -version: 0.0.1 +schema_version: 20180708 +name: resize256 +version: 0.0.5 entrypoint: convert - -resize 256x256 - -path: /resize256 +format: default diff --git a/examples/async-thumbnails/setup/resize512/func.yaml b/examples/async-thumbnails/setup/resize512/func.yaml index 8ee1d02f..82d9a844 100644 --- a/examples/async-thumbnails/setup/resize512/func.yaml +++ b/examples/async-thumbnails/setup/resize512/func.yaml @@ -1,4 +1,5 @@ -name: example/resize512 -version: 0.0.1 +schema_version: 20180708 +name: resize512 +version: 0.0.8 entrypoint: convert - -resize 512x512 - -path: /resize512 +format: default diff --git a/examples/async-thumbnails/setup/setup.sh b/examples/async-thumbnails/setup/setup.sh index 501c0e60..96c3e3a4 100755 --- a/examples/async-thumbnails/setup/setup.sh +++ b/examples/async-thumbnails/setup/setup.sh @@ -51,26 +51,21 @@ fi STORAGE_SERVER_IP=`docker inspect --type container -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' example-storage-server` # Start functions server if not there -if [[ -z `docker ps | grep "functions"` ]]; then - docker run -d --name functions \ - -e NO_PROXY="$STORAGE_SERVER_IP:$NO_PROXY" \ - -p 8080:8080 \ - -v /var/run/docker.sock:/var/run/docker.sock \ - "$FUNCTIONS_IMAGE" - # Give it time to start up +if [[ -z `docker ps | grep "fnserver"` ]]; then + fn start -d sleep 3 else echo "Functions server is already up." fi # Get its IP -FUNCTIONS_SERVER_IP=`docker inspect --type container -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' functions` +FUNCTIONS_SERVER_IP=`docker inspect --type container -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' fnserver` # Start flow service if not there if [[ -z `docker ps | grep "flow-service"` ]]; then docker run -d --name flow-service \ -e LOG_LEVEL=debug \ -e NO_PROXY="$FUNCTIONS_SERVER_IP:$NO_PROXY" \ - -e API_URL=http://$FUNCTIONS_SERVER_IP:8080/r \ + -e API_URL=http://$FUNCTIONS_SERVER_IP:8080/invoke \ -p 8081:8081 \ "$COMPLETER_IMAGE" # Give it time to start up @@ -82,46 +77,41 @@ fi COMPLETER_SERVER_IP=`docker inspect --type container -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' flow-service` # Create app and routes -if [[ `fn apps list` == *"myapp"* ]]; then +if [[ `fn list apps` == *"myapp"* ]]; then echo "App myapp is already there." else - fn apps create myapp - fn apps config set myapp COMPLETER_BASE_URL http://10.167.103.193:8081 + fn create app myapp fi -if [[ `fn routes list myapp` == *"/resize128"* ]]; then - echo "Route /resize128 is already there." -else - # This works around proxy issues - cd $SCRIPT_DIR/resize128 && \ - docker build -t example/resize128:0.0.1 \ - --build-arg http_proxy=$http_proxy \ - --build-arg https_proxy=$https_proxy \ - . && \ - fn routes create myapp /resize128 -fi -if [[ `fn routes list myapp` == *"/resize256"* ]]; then - echo "Route /resize256 is already there." -else - # This works around proxy issues - cd $SCRIPT_DIR/resize256 && \ - docker build -t example/resize256:0.0.1 \ - --build-arg http_proxy=$http_proxy \ - --build-arg https_proxy=$https_proxy \ - . && \ - fn routes create myapp /resize256 -fi -if [[ `fn routes list myapp` == *"/resize512"* ]]; then - echo "Route /resize512 is already there." -else - # This works around proxy issues - cd $SCRIPT_DIR/resize512 && \ - docker build -t example/resize512:0.0.1 \ - --build-arg http_proxy=$http_proxy \ - --build-arg https_proxy=$https_proxy \ - . && \ - fn routes create myapp /resize512 -fi + +fn config app myapp COMPLETER_BASE_URL http://${COMPLETER_SERVER_IP}:8081 +fn config app myapp OBJECT_STORAGE_URL http://${STORAGE_SERVER_IP}:9000 +fn config app myapp OBJECT_STORAGE_ACCESS alpha +fn config app myapp OBJECT_STORAGE_SECRET betabetabetabeta + +( + cd ${SCRIPT_DIR}/resize128 + fn deploy --app myapp --local +) + +fn config app myapp RESIZE_128_FN_ID $(fn list functions myapp | grep resize128 | awk '{print $3}') + +( + cd ${SCRIPT_DIR}/resize256 + fn deploy --app myapp --local +) + +fn config app myapp RESIZE_256_FN_ID $(fn list functions myapp | grep resize256 | awk '{print $3}') + + +( + cd ${SCRIPT_DIR}/resize512 + fn deploy --app myapp --local +) + +fn config app myapp RESIZE_512_FN_ID $(fn list functions myapp | grep resize512 | awk '{print $3}') + + if mc config host list | grep example-storage-server &>/dev/null ; then diff --git a/examples/async-thumbnails/src/main/java/com/fnproject/fn/examples/ThumbnailsFunction.java b/examples/async-thumbnails/src/main/java/com/fnproject/fn/examples/ThumbnailsFunction.java index 7ad601de..5f6c1dee 100644 --- a/examples/async-thumbnails/src/main/java/com/fnproject/fn/examples/ThumbnailsFunction.java +++ b/examples/async-thumbnails/src/main/java/com/fnproject/fn/examples/ThumbnailsFunction.java @@ -1,8 +1,10 @@ package com.fnproject.fn.examples; +import com.fnproject.fn.api.FnFeature; import com.fnproject.fn.api.Headers; import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.api.flow.Flow; +import com.fnproject.fn.runtime.flow.FlowFeature; import com.fnproject.fn.api.flow.Flows; import com.fnproject.fn.api.flow.HttpMethod; import io.minio.MinioClient; @@ -10,12 +12,17 @@ import java.io.ByteArrayInputStream; import java.io.Serializable; +@FnFeature(FlowFeature.class) public class ThumbnailsFunction implements Serializable { private final String storageUrl; private final String storageAccessKey; private final String storageSecretKey; + private final String resize128ID; + private final String resize256ID; + private final String resize512ID; + public ThumbnailsFunction(RuntimeContext ctx) { storageUrl = ctx.getConfigurationByKey("OBJECT_STORAGE_URL") .orElseThrow(() -> new RuntimeException("Missing configuration: OBJECT_STORAGE_URL")); @@ -23,6 +30,14 @@ public ThumbnailsFunction(RuntimeContext ctx) { .orElseThrow(() -> new RuntimeException("Missing configuration: OBJECT_STORAGE_ACCESS")); storageSecretKey = ctx.getConfigurationByKey("OBJECT_STORAGE_SECRET") .orElseThrow(() -> new RuntimeException("Missing configuration: OBJECT_STORAGE_SECRET")); + + resize128ID = ctx.getConfigurationByKey("RESIZE_128_FN_ID") + .orElseThrow(() -> new RuntimeException("Missing configuration: RESIZE_128_FN_ID")); + resize256ID = ctx.getConfigurationByKey("RESIZE_256_FN_ID") + .orElseThrow(() -> new RuntimeException("Missing configuration: RESIZE_256_FN_ID")); + resize512ID = ctx.getConfigurationByKey("RESIZE_512_FN_ID") + .orElseThrow(() -> new RuntimeException("Missing configuration: RESIZE_512_FN_ID")); + } public class Response { @@ -35,11 +50,11 @@ public Response handleRequest(byte[] imageBuffer) { Flow runtime = Flows.currentFlow(); runtime.allOf( - runtime.invokeFunction("myapp/resize128", HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) + runtime.invokeFunction(resize128ID, HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) .thenAccept((img) -> objectUpload(img.getBodyAsBytes(), id + "-128.png")), - runtime.invokeFunction("myapp/resize256", HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) + runtime.invokeFunction(resize256ID, HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) .thenAccept((img) -> objectUpload(img.getBodyAsBytes(), id + "-256.png")), - runtime.invokeFunction("myapp/resize512", HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) + runtime.invokeFunction(resize512ID, HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) .thenAccept((img) -> objectUpload(img.getBodyAsBytes(), id + "-512.png")), runtime.supply(() -> objectUpload(imageBuffer, id + ".png")) ); diff --git a/examples/async-thumbnails/src/test/java/com/fnproject/fn/examples/ThumbnailsFunctionTest.java b/examples/async-thumbnails/src/test/java/com/fnproject/fn/examples/ThumbnailsFunctionTest.java index 675ca691..8179ea10 100644 --- a/examples/async-thumbnails/src/test/java/com/fnproject/fn/examples/ThumbnailsFunctionTest.java +++ b/examples/async-thumbnails/src/test/java/com/fnproject/fn/examples/ThumbnailsFunctionTest.java @@ -1,7 +1,7 @@ package com.fnproject.fn.examples; -import com.fnproject.fn.examples.ThumbnailsFunction; import com.fnproject.fn.testing.FnTestingRule; +import com.fnproject.fn.testing.flow.FlowTesting; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit.WireMockRule; import org.junit.Rule; @@ -12,65 +12,76 @@ public class ThumbnailsFunctionTest { @Rule - public final FnTestingRule testing = FnTestingRule.createDefault(); + public final FnTestingRule fn = FnTestingRule.createDefault(); + private final FlowTesting flow = FlowTesting.create(fn); @Rule public final WireMockRule mockServer = new WireMockRule(0); @Test public void testThumbnail() { - testing - - .setConfig("OBJECT_STORAGE_URL", "http://localhost:" + mockServer.port()) - .setConfig("OBJECT_STORAGE_ACCESS", "alpha") - .setConfig("OBJECT_STORAGE_SECRET", "betabetabetabeta") - - .givenFn("myapp/resize128") - .withAction((data) -> "128".getBytes()) - .givenFn("myapp/resize256") - .withAction((data) -> "256".getBytes()) - .givenFn("myapp/resize512") - .withAction((data) -> "512".getBytes()) - - .givenEvent() - .withBody("testing".getBytes()) - .enqueue(); + fn + .setConfig("OBJECT_STORAGE_URL", "http://localhost:" + mockServer.port()) + .setConfig("OBJECT_STORAGE_ACCESS", "alpha") + .setConfig("OBJECT_STORAGE_SECRET", "betabetabetabeta") + .setConfig("RESIZE_128_FN_ID","myapp/resize128") + .setConfig("RESIZE_256_FN_ID","myapp/resize256") + .setConfig("RESIZE_512_FN_ID","myapp/resize512"); + + + flow + .givenFn("myapp/resize128") + .withAction((data) -> "128".getBytes()) + .givenFn("myapp/resize256") + .withAction((data) -> "256".getBytes()) + .givenFn("myapp/resize512") + .withAction((data) -> "512".getBytes()); + + fn + .givenEvent() + .withBody("fn".getBytes()) + .enqueue(); // Mock the http endpoint mockMinio(); - testing.thenRun(ThumbnailsFunction.class, "handleRequest"); + fn.thenRun(ThumbnailsFunction.class, "handleRequest"); // Check the final image uploads were performed - mockServer.verify(1, putRequestedFor(urlMatching("/alpha/.*\\.png")).withRequestBody(equalTo("testing"))); - mockServer.verify(1, putRequestedFor(urlMatching("/alpha/.*\\.png")).withRequestBody(equalTo("128"))); - mockServer.verify(1, putRequestedFor(urlMatching("/alpha/.*\\.png")).withRequestBody(equalTo("256"))); - mockServer.verify(1, putRequestedFor(urlMatching("/alpha/.*\\.png")).withRequestBody(equalTo("512"))); + mockServer.verify(putRequestedFor(urlMatching("/alpha/.*\\.png")).withRequestBody(containing("fn"))); + mockServer.verify(putRequestedFor(urlMatching("/alpha/.*\\.png")).withRequestBody(containing("128"))); + mockServer.verify(putRequestedFor(urlMatching("/alpha/.*\\.png")).withRequestBody(containing("256"))); + mockServer.verify(putRequestedFor(urlMatching("/alpha/.*\\.png")).withRequestBody(containing("512"))); mockServer.verify(4, putRequestedFor(urlMatching(".*"))); } @Test public void anExternalFunctionFailure() { - testing - .setConfig("OBJECT_STORAGE_URL", "http://localhost:" + mockServer.port()) - .setConfig("OBJECT_STORAGE_ACCESS", "alpha") - .setConfig("OBJECT_STORAGE_SECRET", "betabetabetabeta") - - .givenFn("myapp/resize128") - .withResult("128".getBytes()) - .givenFn("myapp/resize256") - .withResult("256".getBytes()) - .givenFn("myapp/resize512") - .withFunctionError() - - .givenEvent() - .withBody("testing".getBytes()) - .enqueue(); + fn + .setConfig("OBJECT_STORAGE_URL", "http://localhost:" + mockServer.port()) + .setConfig("OBJECT_STORAGE_ACCESS", "alpha") + .setConfig("OBJECT_STORAGE_SECRET", "betabetabetabeta") + .setConfig("RESIZE_128_FN_ID","myapp/resize128") + .setConfig("RESIZE_256_FN_ID","myapp/resize256") + .setConfig("RESIZE_512_FN_ID","myapp/resize512");; + + flow + .givenFn("myapp/resize128") + .withResult("128".getBytes()) + .givenFn("myapp/resize256") + .withResult("256".getBytes()) + .givenFn("myapp/resize512") + .withFunctionError(); + + fn + .givenEvent() + .withBody("fn".getBytes()) + .enqueue(); // Mock the http endpoint mockMinio(); - testing.thenRun(ThumbnailsFunction.class, "handleRequest"); + fn.thenRun(ThumbnailsFunction.class, "handleRequest"); // Confirm that one image upload didn't happen mockServer.verify(0, putRequestedFor(urlMatching("/alpha/.*\\.png")).withRequestBody(equalTo("512"))); @@ -82,15 +93,15 @@ public void anExternalFunctionFailure() { private void mockMinio() { mockServer.stubFor(get(urlMatching("/alpha.*")) - .willReturn(aResponse().withBody( - "\n" + - "\n" + - " alpha\n" + - " \n" + - " 0\n" + - " 100\n" + - " false\n" + - ""))); + .willReturn(aResponse().withBody( + "\n" + + "\n" + + " alpha\n" + + " \n" + + " 0\n" + + " 100\n" + + " false\n" + + ""))); mockServer.stubFor(WireMock.head(urlMatching("/alpha.*")).willReturn(aResponse().withStatus(200))); diff --git a/examples/gradle-build/README.md b/examples/gradle-build/README.md index 269072c7..3214e0b3 100644 --- a/examples/gradle-build/README.md +++ b/examples/gradle-build/README.md @@ -4,11 +4,11 @@ Fn uses Maven by default for builds. This is an example that uses Fn's `docker` The example consists of a `Dockerfile` that builds the function using gradle and copies the function's dependencies to `build/deps` and a func.yaml that uses that `Dockerfile` to build the function. -Note that FDK versions are hard-coded in this example, you may need to update them manually to more recent version. +Note that fdk.versions are hard-coded in this example, you may need to update them manually to more recent version. Key points: * [Dockerfile](Dockerfile) - contains the containerised docker build (based on dockerhub library/gradle images) and image build - this includes the gradle invocation * The `cacheDeps` task in `build.gradle` is invoked within the Dockerfile - The task pulls down dependencies into the container gradle cache to speed up docker builds. * The `copyDeps` task in `build.gradle` copies the functions compile deps -* This uses JDK 8 by default - you can change this to Java 9 by changing : `FROM gradle:4.5.1-jdk8 as build-stage` to `FROM gradle:4.5.1-jdk9 as build-stage` and `FROM fnproject/fn-java-fdk:1.0.56` to `FROM fnproject/fn-java-fdk:jdk9-1.0.56` \ No newline at end of file +* This uses JDK 8 by default - you can change this to Java 11 by changing : `FROM gradle:4.5.1-jdk8 as build-stage` to `FROM gradle:4.5.1-jre11 as build-stage` and `FROM fnproject/fn-java-fdk:1.0.85` to `FROM fnproject/fn-java-fdk:jre11-1.0.85` \ No newline at end of file diff --git a/examples/gradle-build/src/test/java/com/example/fn/HelloFunctionTest.java b/examples/gradle-build/src/test/java/com/example/fn/HelloFunctionTest.java index e6b7a5e3..bd46e52d 100644 --- a/examples/gradle-build/src/test/java/com/example/fn/HelloFunctionTest.java +++ b/examples/gradle-build/src/test/java/com/example/fn/HelloFunctionTest.java @@ -1,8 +1,5 @@ package com.example.fn; -import com.fnproject.fn.testing.*; -import org.junit.*; - import static org.junit.Assert.*; public class HelloFunctionTest { diff --git a/examples/pom.xml b/examples/pom.xml index 219291f4..c00977ea 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -13,10 +13,6 @@ 1.0.0-SNAPSHOT - - UTF-8 - - string-reverse regex-query @@ -31,7 +27,6 @@ org.apache.maven.plugins maven-deploy-plugin - 2.8.2 true diff --git a/examples/qr-code/README.md b/examples/qr-code/README.md index d511c970..eda10787 100644 --- a/examples/qr-code/README.md +++ b/examples/qr-code/README.md @@ -31,8 +31,8 @@ $ fn build Create an app and route to host the function ```bash -$ fn apps create qr-app -$ fn routes create qr-app /qr +$ fn create app qr-app +$ fn create route qr-app /qr ``` Invoke the function to create a QR code @@ -54,21 +54,22 @@ of the example function. The body of the function is shown below: ```java -public OutputEvent create(InputEvent event) throws MalformedURLException, UnsupportedEncodingException { - String decodedUrl = URLDecoder.decode(event.getRequestUrl(), "utf-8"); - QueryParameters params = getParams(decodedUrl); - ImageType type = getFormat(params.getFirst("format").orElse("png")); - String contents = params.getFirst("contents").orElse(""); - - ByteArrayOutputStream stream = QRCode.from(contents).to(type).stream(); - System.err.println("Generated QR Code for contents: " + contents); - return OutputEvent.fromBytes(stream.toByteArray(), true, getMimeType(type)); -} + + public byte[] create(HTTPGatewayContext hctx) { + ImageType type = getFormat(hctx.getQueryParameters().get("format").orElse(defaultFormat)); + System.err.println("Default format: " + type.toString()); + String contents = hctx.getQueryParameters().get("contents").orElseThrow(() -> new RuntimeException("Contents must be provided to the QR code")); + + ByteArrayOutputStream stream = QRCode.from(contents).to(type).stream(); + System.err.println("Generated QR Code for contents: " + contents); + + hctx.setResponseHeader("Content-Type", getMimeType(type)); + return stream.toByteArray(); + } + ``` -The fn Java FDK facilitates access to the internal events representing the -invocation of the function, `InputEvent`, and response of the function, -`OutputEvent`, for more fine grained control of the platform. See +The fn Java FDK facilitates access to the HTTP context of events triggered from HTTP gateways via `HTTPGatewayContext` , and response of the function as a bye array, for more fine grained control of the platform. See [Data Binding](/docs/DataBinding.md) for further information on the types of input that the fn Java FDK provides. @@ -116,17 +117,17 @@ this to handle invocations of functions and retrieving function results ```java ... - @Test - public void textHelloWorld() throws Exception { - fn.givenEvent() - .withRequestUrl("http://www.example.com/qr?contents=" + URLEncoder.encode("hello world", "utf-8")) - .withMethod("GET") - .enqueue(); - fn.thenRun(QRGen.class, "create"); - - assertArrayEquals(readTestFile("qr-code-text-hello-world.png"), fn.getOnlyResult().getBodyAsBytes()); - } -... + @Test + public void textHelloWorld() throws Exception { + String content = "hello world"; + fn.givenEvent() + .withHeader("Fn-Http-Request-Url", "http://www.example.com/qr?contents=hello+world&format=png") + .withHeader("Fn-Http-Method","GET") + .enqueue(); + fn.thenRun(QRGen.class, "create"); + + assertEquals(content, decode(fn.getOnlyResult().getBodyAsBytes())); + } ``` Input events are constructed using `fn.givenEvent()` providing an `FnEventBuilder` diff --git a/examples/qr-code/pom.xml b/examples/qr-code/pom.xml index e7941657..f2a60dfc 100644 --- a/examples/qr-code/pom.xml +++ b/examples/qr-code/pom.xml @@ -7,7 +7,7 @@ UTF-8 - 1.0.0-SNAPSHOT + 1.0.0-SNAPSHOT com.fnproject.fn.examples @@ -24,7 +24,7 @@ com.fnproject.fn api - ${fnproject.version} + ${fdk.version} @@ -36,7 +36,13 @@ com.fnproject.fn testing - ${fnproject.version} + ${fdk.version} + test + + + com.fnproject.fn + testing-junit4 + ${fdk.version} test @@ -45,7 +51,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.3 + 3.8.0 1.8 1.8 diff --git a/examples/qr-code/src/main/java/com/fnproject/fn/examples/QRGen.java b/examples/qr-code/src/main/java/com/fnproject/fn/examples/QRGen.java index a95caa3d..9f233e29 100644 --- a/examples/qr-code/src/main/java/com/fnproject/fn/examples/QRGen.java +++ b/examples/qr-code/src/main/java/com/fnproject/fn/examples/QRGen.java @@ -1,13 +1,11 @@ package com.fnproject.fn.examples; -import com.fnproject.fn.api.InputEvent; -import com.fnproject.fn.api.OutputEvent; import com.fnproject.fn.api.RuntimeContext; +import com.fnproject.fn.api.httpgateway.HTTPGatewayContext; import net.glxn.qrgen.core.image.ImageType; import net.glxn.qrgen.javase.QRCode; -import java.io.*; -import java.net.MalformedURLException; +import java.io.ByteArrayOutputStream; public class QRGen { private final String defaultFormat; @@ -16,19 +14,20 @@ public QRGen(RuntimeContext ctx) { defaultFormat = ctx.getConfigurationByKey("FORMAT").orElse("png"); } - public OutputEvent create(InputEvent event) throws MalformedURLException, UnsupportedEncodingException { - ImageType type = getFormat(event.getQueryParameters().get("format").orElse(defaultFormat)); + public byte[] create(HTTPGatewayContext hctx) { + ImageType type = getFormat(hctx.getQueryParameters().get("format").orElse(defaultFormat)); System.err.println("Default format: " + type.toString()); - String contents = event.getQueryParameters().get("contents").orElseThrow(() -> new RuntimeException("Contents must be provided to the QR code")); + String contents = hctx.getQueryParameters().get("contents").orElseThrow(() -> new RuntimeException("Contents must be provided to the QR code")); ByteArrayOutputStream stream = QRCode.from(contents).to(type).stream(); System.err.println("Generated QR Code for contents: " + contents); - return OutputEvent.fromBytes(stream.toByteArray(), OutputEvent.SUCCESS, getMimeType(type)); + hctx.setResponseHeader("Content-Type", getMimeType(type)); + return stream.toByteArray(); } private ImageType getFormat(String extension) { - switch(extension.toLowerCase()) { + switch (extension.toLowerCase()) { case "png": return ImageType.PNG; case "jpg": @@ -43,8 +42,8 @@ private ImageType getFormat(String extension) { } } - private String getMimeType(ImageType type) { - switch(type) { + private String getMimeType(ImageType type) { + switch (type) { case JPG: return "image/jpeg"; case GIF: @@ -56,5 +55,5 @@ private String getMimeType(ImageType type) { default: throw new RuntimeException("Invalid ImageType: " + type); } - } + } } diff --git a/examples/qr-code/src/test/java/com/fnproject/fn/examples/QRGenTest.java b/examples/qr-code/src/test/java/com/fnproject/fn/examples/QRGenTest.java index d727e941..c23ec6c2 100644 --- a/examples/qr-code/src/test/java/com/fnproject/fn/examples/QRGenTest.java +++ b/examples/qr-code/src/test/java/com/fnproject/fn/examples/QRGenTest.java @@ -1,7 +1,10 @@ package com.fnproject.fn.examples; import com.fnproject.fn.testing.FnTestingRule; -import com.google.zxing.*; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.ChecksumException; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; import com.google.zxing.client.j2se.BufferedImageLuminanceSource; import com.google.zxing.common.HybridBinarizer; import com.google.zxing.qrcode.QRCodeReader; @@ -23,11 +26,9 @@ public class QRGenTest { public void textHelloWorld() throws Exception { String content = "hello world"; fn.givenEvent() - .withRequestUrl("http://www.example.com/qr") - .withQueryParameter("contents", content) - .withQueryParameter("format", "png") - .withMethod("GET") - .enqueue(); + .withHeader("Fn-Http-Request-Url", "http://www.example.com/qr?contents=hello+world&format=png") + .withHeader("Fn-Http-Method","GET") + .enqueue(); fn.thenRun(QRGen.class, "create"); assertEquals(content, decode(fn.getOnlyResult().getBodyAsBytes())); @@ -37,10 +38,9 @@ public void textHelloWorld() throws Exception { public void phoneNumber() throws Exception { String telephoneNumber = "tel:0-12345-67890"; fn.givenEvent() - .withRequestUrl("http://www.example.com/qr") - .withQueryParameter("contents", telephoneNumber) - .withMethod("GET") - .enqueue(); + .withHeader("Fn-Http-Request-Url", "http://www.example.com/qr?contents=tel:0-12345-67890") + .withHeader("Fn-Http-Method","GET") + .enqueue(); fn.thenRun(QRGen.class, "create"); assertEquals(telephoneNumber, decode(fn.getOnlyResult().getBodyAsBytes())); @@ -51,14 +51,14 @@ public void formatConfigurationIsUsedIfNoFormatIsProvided() throws Exception { String contents = "hello world"; fn.setConfig("FORMAT", "jpg"); fn.givenEvent() - .withRequestUrl("http://www.example.com/qr") - .withQueryParameter("contents", contents) - .withMethod("GET") - .enqueue(); + .withHeader("Fn-Http-Request-Url", "http://www.example.com/qr?contents=hello+world") + .withHeader("Fn-Http-Method","GET") + .enqueue(); fn.thenRun(QRGen.class, "create"); assertEquals(contents, decode(fn.getOnlyResult().getBodyAsBytes())); } + private String decode(byte[] imageBytes) throws IOException, NotFoundException, ChecksumException, FormatException { BinaryBitmap bitmap = readToBitmap(imageBytes); return new QRCodeReader().decode(bitmap).getText(); diff --git a/examples/regex-query/README.md b/examples/regex-query/README.md index a9d67155..f7a21c6e 100644 --- a/examples/regex-query/README.md +++ b/examples/regex-query/README.md @@ -33,8 +33,8 @@ $ fn build Create an app and route to host the function ```bash -$ fn apps create regex-query -$ fn routes create regex-query /query +$ fn create app regex-query +$ fn create route regex-query /query ``` Invoke the function to perform a regex search diff --git a/examples/regex-query/pom.xml b/examples/regex-query/pom.xml index 23961362..d72bc766 100644 --- a/examples/regex-query/pom.xml +++ b/examples/regex-query/pom.xml @@ -6,8 +6,10 @@ UTF-8 - 1.0.0-SNAPSHOT - 2.8.9 + UTF-8 + + 1.0.0-SNAPSHOT + 2.9.10 com.fnproject.fn.examples @@ -33,8 +35,14 @@ com.fnproject.fn - testing - ${fnproject.version} + testing-core + ${fdk.version} + test + + + com.fnproject.fn + testing-junit4 + ${fdk.version} test @@ -50,7 +58,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.3 + 3.8.0 1.8 1.8 diff --git a/examples/regex-query/src/test/java/com/fnproject/fn/examples/RegexQueryTests.java b/examples/regex-query/src/test/java/com/fnproject/fn/examples/RegexQueryTests.java index e5a44626..6ccdce56 100644 --- a/examples/regex-query/src/test/java/com/fnproject/fn/examples/RegexQueryTests.java +++ b/examples/regex-query/src/test/java/com/fnproject/fn/examples/RegexQueryTests.java @@ -15,7 +15,6 @@ public void matchingSingleCharacter() throws JSONException { String text = "a"; String regex = "."; fn.givenEvent() - .withMethod("POST") .withBody(String.format("{\"text\": \"%s\", \"regex\": \"%s\"}", text, regex)) .enqueue(); @@ -34,7 +33,6 @@ public void matchingSingleCharacterMultipleTimes() throws JSONException { String text = "abc"; String regex = "."; fn.givenEvent() - .withMethod("POST") .withBody(String.format("{\"text\": \"%s\", \"regex\": \"%s\"}", text, regex)) .enqueue(); diff --git a/examples/string-reverse/README.md b/examples/string-reverse/README.md index 9c52dae4..97eb79a0 100644 --- a/examples/string-reverse/README.md +++ b/examples/string-reverse/README.md @@ -1,71 +1,75 @@ -# Example oFunctions Project: String Reverse +# Example Java Function: String Reverse -This example provides an HTTP endpoint for reversing strings +This example provides an HTTP trigger endpoint for reversing strings. ```bash -$ curl -d "Hello, World!" "http://localhost:8080/r/string-reverse-app/reverse" -!dlroW ,olleH +$ curl -d "Hello World" http://localhost:8080/t/string-reverse-app/string-reverse +dlroW olleH ``` ## Demonstrated FDK features -This example uses **no** features of the fn Java FDK; in fact it doesn't have -a dependency on the fn Java FDK, it just plain old Java code. +This example uses **none** of the Fn Java FDK features, in fact it doesn't have +any dependency on the Fn Java FDK. It is just plain old Java code. ## Step by step -Ensure you have the functions server running using, this will host your -function and provide the HTTP endpoints that invoke it: +Ensure you have the Fn server running to host your +function and provide the HTTP endpoint that invokes it: -```bash +(1) Start the server + +```sh $ fn start ``` -Build the function locally +(2) Create an app for the function -```bash -$ fn build +```sh +$ fn create app string-reverse-app ``` -Create an app and route to host the function +(3) Deploy the function to your app from the `string-reverse` directory. -```bash -$ fn apps create string-reverse-app -$ fn routes create string-reverse-app /reverse +```sh +fn deploy --app string-reverse-app --local +``` + +(4) Invoke the function and reverse the string. + +```sh +echo "Hello World" | fn invoke string-reverse-app string-reverse +dlroW olleH ``` -Invoke the function to reverse a string +(5) Invoke the function using curl and a trigger to reverse a string. ```bash -$ curl -d "Hello, World!" "http://localhost:8080/r/string-reverse-app/reverse" -!dlroW ,olleH +$ curl -d "Hello World" http://localhost:8080/t/string-reverse-app/string-reverse +dlroW olleH ``` ## Code walkthrough The entrypoint to the function is specified in `func.yaml` in the `cmd` key. -It is set this to `com.fnproject.fn.examples.StringReverse::reverse`. The whole class +It is set this to `com.example.fn.StringReverse::reverse`. The whole class `StringReverse` is shown below: ```java -package com.fnproject.fn.examples; +package com.example.fn; public class StringReverse { public String reverse(String str) { - StringBuilder builder = new StringBuilder(); - for (int i = str.length() - 1; i >= 0; i--) { - builder.append(str.charAt(i)); - } - return builder.toString(); + return new StringBuilder(str).reverse().toString(); } } ``` -As you can see, this is plain java with no references to the fn API. The -fn Java FDK handles the marshalling of the HTTP body into the `str` +As you can see, this is plain java with no references to the Fn API. The +Fn Java FDK handles the marshalling of the HTTP body into the `str` parameter as well as the marshalling of the returned reversed string into the HTTP response body (see [Data Binding](/docs/DataBinding.md) for more information on how marshalling is performed). diff --git a/examples/string-reverse/func.yaml b/examples/string-reverse/func.yaml index 197eebe2..4d56d44f 100644 --- a/examples/string-reverse/func.yaml +++ b/examples/string-reverse/func.yaml @@ -1,7 +1,11 @@ -name: fn-example/string-reverse -version: 0.0.1 +schema_version: 20180708 +name: string-reverse +version: 0.0.2 runtime: java -timeout: 30 -format: http -cmd: com.fnproject.fn.examples.StringReverse::reverse -path: /reverse +build_image: fnproject/fn-java-fdk-build:jdk11-1.0.87 +run_image: fnproject/fn-java-fdk:jre11-1.0.87 +cmd: com.example.fn.StringReverse::reverse +triggers: +- name: string-reverse + type: http + source: /string-reverse diff --git a/examples/string-reverse/pom.xml b/examples/string-reverse/pom.xml index 4c380ea1..916d988d 100644 --- a/examples/string-reverse/pom.xml +++ b/examples/string-reverse/pom.xml @@ -3,21 +3,50 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - - UTF-8 + 1.0.87 - - com.fnproject.fn.examples + com.example.fn string-reverse - 1.0.0-SNAPSHOT + 1.0.0 + + + + fn-release-repo + https://dl.bintray.com/fnproject/fnproject + + true + + + false + + + + + com.fnproject.fn + api + ${fdk.version} + + + com.fnproject.fn + testing-core + ${fdk.version} + test + + + com.fnproject.fn + testing-junit4 + ${fdk.version} + test + junit junit 4.12 + test @@ -28,8 +57,8 @@ maven-compiler-plugin 3.3 - 1.8 - 1.8 + 8 + 8 @@ -40,13 +69,14 @@ true + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.1 + + false + + - - - - fn-maven-releases - https://dl.bintray.com/fnproject/fnproject - - diff --git a/examples/string-reverse/src/main/java/com/example/fn/StringReverse.java b/examples/string-reverse/src/main/java/com/example/fn/StringReverse.java new file mode 100644 index 00000000..46d0f5fb --- /dev/null +++ b/examples/string-reverse/src/main/java/com/example/fn/StringReverse.java @@ -0,0 +1,7 @@ +package com.example.fn; + +public class StringReverse { + public String reverse(String str) { + return new StringBuilder(str).reverse().toString(); + } +} diff --git a/examples/string-reverse/src/main/java/com/fnproject/fn/examples/StringReverse.java b/examples/string-reverse/src/main/java/com/fnproject/fn/examples/StringReverse.java deleted file mode 100644 index e95ce08e..00000000 --- a/examples/string-reverse/src/main/java/com/fnproject/fn/examples/StringReverse.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.fnproject.fn.examples; - -public class StringReverse { - public String reverse(String str) { - StringBuilder builder = new StringBuilder(); - for (int i = str.length() - 1; i >= 0; i--) { - builder.append(str.charAt(i)); - } - return builder.toString(); - } -} diff --git a/examples/string-reverse/src/test/java/com/fnproject/examples/StringReverseTest.java b/examples/string-reverse/src/test/java/com/example/fn/testing/StringReverseTest.java similarity index 87% rename from examples/string-reverse/src/test/java/com/fnproject/examples/StringReverseTest.java rename to examples/string-reverse/src/test/java/com/example/fn/testing/StringReverseTest.java index 9f67dd2c..e9b59af0 100644 --- a/examples/string-reverse/src/test/java/com/fnproject/examples/StringReverseTest.java +++ b/examples/string-reverse/src/test/java/com/example/fn/testing/StringReverseTest.java @@ -1,6 +1,6 @@ -package com.fnproject.examples; +package com.example.fn.testing; -import com.fnproject.fn.examples.StringReverse; +import com.example.fn.StringReverse; import org.junit.Test; import static junit.framework.TestCase.assertEquals; diff --git a/flow-api/pom.xml b/flow-api/pom.xml new file mode 100644 index 00000000..b965bdb4 --- /dev/null +++ b/flow-api/pom.xml @@ -0,0 +1,61 @@ + + + + fdk + com.fnproject.fn + 1.0.0-SNAPSHOT + + 4.0.0 + + flow-api + + + com.fnproject.fn + api + + + junit + junit + test + + + org.assertj + assertj-core + test + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + org.netbeans.tools + sigtest-maven-plugin + + + + check + + + + + src/main/api/snapshot.sigfile + strictcheck + com.fnproject.fn.api.flow + + + + + diff --git a/flow-api/src/main/api/snapshot.sigfile b/flow-api/src/main/api/snapshot.sigfile new file mode 100644 index 00000000..9048991c --- /dev/null +++ b/flow-api/src/main/api/snapshot.sigfile @@ -0,0 +1,343 @@ +#Signature file v4.1 +#Version 1.0.0-SNAPSHOT + +CLSS public abstract interface com.fnproject.fn.api.flow.Flow +innr public final static !enum FlowState +intf java.io.Serializable +meth public <%0 extends java.io.Serializable, %1 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> invokeFunction(java.lang.String,{%%1},java.lang.Class<{%%0}>) +meth public <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture invokeFunction(java.lang.String,{%%0}) +meth public abstract !varargs com.fnproject.fn.api.flow.FlowFuture anyOf(com.fnproject.fn.api.flow.FlowFuture[]) +meth public abstract !varargs com.fnproject.fn.api.flow.FlowFuture allOf(com.fnproject.fn.api.flow.FlowFuture[]) +meth public abstract <%0 extends java.io.Serializable, %1 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> invokeFunction(java.lang.String,com.fnproject.fn.api.flow.HttpMethod,com.fnproject.fn.api.Headers,{%%1},java.lang.Class<{%%0}>) +meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture invokeFunction(java.lang.String,com.fnproject.fn.api.flow.HttpMethod,com.fnproject.fn.api.Headers,{%%0}) +meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> completedValue({%%0}) +meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> createFlowFuture() +meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> failedFuture(java.lang.Throwable) +meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> supply(com.fnproject.fn.api.flow.Flows$SerCallable<{%%0}>) +meth public abstract com.fnproject.fn.api.flow.Flow addTerminationHook(com.fnproject.fn.api.flow.Flows$SerConsumer) +meth public abstract com.fnproject.fn.api.flow.FlowFuture invokeFunction(java.lang.String,com.fnproject.fn.api.flow.HttpMethod,com.fnproject.fn.api.Headers,byte[]) +meth public abstract com.fnproject.fn.api.flow.FlowFuture delay(long,java.util.concurrent.TimeUnit) +meth public abstract com.fnproject.fn.api.flow.FlowFuture supply(com.fnproject.fn.api.flow.Flows$SerRunnable) +meth public com.fnproject.fn.api.flow.FlowFuture invokeFunction(java.lang.String,com.fnproject.fn.api.flow.HttpMethod) +meth public com.fnproject.fn.api.flow.FlowFuture invokeFunction(java.lang.String,com.fnproject.fn.api.flow.HttpMethod,com.fnproject.fn.api.Headers) + +CLSS public final static !enum com.fnproject.fn.api.flow.Flow$FlowState + outer com.fnproject.fn.api.flow.Flow +fld public final static com.fnproject.fn.api.flow.Flow$FlowState CANCELLED +fld public final static com.fnproject.fn.api.flow.Flow$FlowState FAILED +fld public final static com.fnproject.fn.api.flow.Flow$FlowState KILLED +fld public final static com.fnproject.fn.api.flow.Flow$FlowState SUCCEEDED +fld public final static com.fnproject.fn.api.flow.Flow$FlowState UNKNOWN +meth public static com.fnproject.fn.api.flow.Flow$FlowState valueOf(java.lang.String) +meth public static com.fnproject.fn.api.flow.Flow$FlowState[] values() +supr java.lang.Enum + +CLSS public com.fnproject.fn.api.flow.FlowCompletionException +cons public init(java.lang.String) +cons public init(java.lang.String,java.lang.Throwable) +cons public init(java.lang.Throwable) +supr java.lang.RuntimeException + +CLSS public abstract interface com.fnproject.fn.api.flow.FlowFuture<%0 extends java.lang.Object> +intf java.io.Serializable +meth public abstract <%0 extends java.lang.Object, %1 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%1}> thenCombine(com.fnproject.fn.api.flow.FlowFuture,com.fnproject.fn.api.flow.Flows$SerBiFunction) +meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture thenAcceptBoth(com.fnproject.fn.api.flow.FlowFuture<{%%0}>,com.fnproject.fn.api.flow.Flows$SerBiConsumer<{com.fnproject.fn.api.flow.FlowFuture%0},{%%0}>) +meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> applyToEither(com.fnproject.fn.api.flow.FlowFuture,com.fnproject.fn.api.flow.Flows$SerFunction<{com.fnproject.fn.api.flow.FlowFuture%0},{%%0}>) +meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> handle(com.fnproject.fn.api.flow.Flows$SerBiFunction) +meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> thenApply(com.fnproject.fn.api.flow.Flows$SerFunction<{com.fnproject.fn.api.flow.FlowFuture%0},{%%0}>) +meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> thenCompose(com.fnproject.fn.api.flow.Flows$SerFunction<{com.fnproject.fn.api.flow.FlowFuture%0},com.fnproject.fn.api.flow.FlowFuture<{%%0}>>) +meth public abstract boolean cancel() +meth public abstract boolean complete({com.fnproject.fn.api.flow.FlowFuture%0}) +meth public abstract boolean completeExceptionally(java.lang.Throwable) +meth public abstract com.fnproject.fn.api.flow.FlowFuture acceptEither(com.fnproject.fn.api.flow.FlowFuture,com.fnproject.fn.api.flow.Flows$SerConsumer<{com.fnproject.fn.api.flow.FlowFuture%0}>) +meth public abstract com.fnproject.fn.api.flow.FlowFuture thenAccept(com.fnproject.fn.api.flow.Flows$SerConsumer<{com.fnproject.fn.api.flow.FlowFuture%0}>) +meth public abstract com.fnproject.fn.api.flow.FlowFuture thenRun(com.fnproject.fn.api.flow.Flows$SerRunnable) +meth public abstract com.fnproject.fn.api.flow.FlowFuture<{com.fnproject.fn.api.flow.FlowFuture%0}> exceptionally(com.fnproject.fn.api.flow.Flows$SerFunction) +meth public abstract com.fnproject.fn.api.flow.FlowFuture<{com.fnproject.fn.api.flow.FlowFuture%0}> exceptionallyCompose(com.fnproject.fn.api.flow.Flows$SerFunction>) +meth public abstract com.fnproject.fn.api.flow.FlowFuture<{com.fnproject.fn.api.flow.FlowFuture%0}> whenComplete(com.fnproject.fn.api.flow.Flows$SerBiConsumer<{com.fnproject.fn.api.flow.FlowFuture%0},java.lang.Throwable>) +meth public abstract {com.fnproject.fn.api.flow.FlowFuture%0} get() +meth public abstract {com.fnproject.fn.api.flow.FlowFuture%0} get(long,java.util.concurrent.TimeUnit) throws java.util.concurrent.TimeoutException +meth public abstract {com.fnproject.fn.api.flow.FlowFuture%0} getNow({com.fnproject.fn.api.flow.FlowFuture%0}) + +CLSS public final com.fnproject.fn.api.flow.Flows +innr public abstract interface static FlowSource +innr public abstract interface static SerBiConsumer +innr public abstract interface static SerBiFunction +innr public abstract interface static SerCallable +innr public abstract interface static SerConsumer +innr public abstract interface static SerFunction +innr public abstract interface static SerRunnable +innr public abstract interface static SerSupplier +meth public static com.fnproject.fn.api.flow.Flow currentFlow() +meth public static com.fnproject.fn.api.flow.Flows$FlowSource getCurrentFlowSource() +meth public static void setCurrentFlowSource(com.fnproject.fn.api.flow.Flows$FlowSource) +supr java.lang.Object +hfds flowSource + +CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$FlowSource + outer com.fnproject.fn.api.flow.Flows +meth public abstract com.fnproject.fn.api.flow.Flow currentFlow() + +CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$SerBiConsumer<%0 extends java.lang.Object, %1 extends java.lang.Object> + outer com.fnproject.fn.api.flow.Flows + anno 0 java.lang.FunctionalInterface() +intf java.io.Serializable +intf java.util.function.BiConsumer<{com.fnproject.fn.api.flow.Flows$SerBiConsumer%0},{com.fnproject.fn.api.flow.Flows$SerBiConsumer%1}> + +CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$SerBiFunction<%0 extends java.lang.Object, %1 extends java.lang.Object, %2 extends java.lang.Object> + outer com.fnproject.fn.api.flow.Flows + anno 0 java.lang.FunctionalInterface() +intf java.io.Serializable +intf java.util.function.BiFunction<{com.fnproject.fn.api.flow.Flows$SerBiFunction%0},{com.fnproject.fn.api.flow.Flows$SerBiFunction%1},{com.fnproject.fn.api.flow.Flows$SerBiFunction%2}> + +CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$SerCallable<%0 extends java.lang.Object> + outer com.fnproject.fn.api.flow.Flows + anno 0 java.lang.FunctionalInterface() +intf java.io.Serializable +intf java.util.concurrent.Callable<{com.fnproject.fn.api.flow.Flows$SerCallable%0}> + +CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$SerConsumer<%0 extends java.lang.Object> + outer com.fnproject.fn.api.flow.Flows + anno 0 java.lang.FunctionalInterface() +intf java.io.Serializable +intf java.util.function.Consumer<{com.fnproject.fn.api.flow.Flows$SerConsumer%0}> + +CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$SerFunction<%0 extends java.lang.Object, %1 extends java.lang.Object> + outer com.fnproject.fn.api.flow.Flows + anno 0 java.lang.FunctionalInterface() +intf java.io.Serializable +intf java.util.function.Function<{com.fnproject.fn.api.flow.Flows$SerFunction%0},{com.fnproject.fn.api.flow.Flows$SerFunction%1}> + +CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$SerRunnable + outer com.fnproject.fn.api.flow.Flows + anno 0 java.lang.FunctionalInterface() +intf java.io.Serializable +intf java.lang.Runnable + +CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$SerSupplier<%0 extends java.lang.Object> + outer com.fnproject.fn.api.flow.Flows + anno 0 java.lang.FunctionalInterface() +intf java.io.Serializable +intf java.util.function.Supplier<{com.fnproject.fn.api.flow.Flows$SerSupplier%0}> + +CLSS public com.fnproject.fn.api.flow.FunctionInvocationException +cons public init(com.fnproject.fn.api.flow.HttpResponse) +meth public com.fnproject.fn.api.flow.HttpResponse getFunctionResponse() +supr java.lang.RuntimeException +hfds functionResponse + +CLSS public com.fnproject.fn.api.flow.FunctionInvokeFailedException +cons public init(java.lang.String) +supr com.fnproject.fn.api.flow.PlatformException + +CLSS public com.fnproject.fn.api.flow.FunctionTimeoutException +cons public init(java.lang.String) +supr com.fnproject.fn.api.flow.PlatformException + +CLSS public final !enum com.fnproject.fn.api.flow.HttpMethod +fld public final static com.fnproject.fn.api.flow.HttpMethod DELETE +fld public final static com.fnproject.fn.api.flow.HttpMethod GET +fld public final static com.fnproject.fn.api.flow.HttpMethod HEAD +fld public final static com.fnproject.fn.api.flow.HttpMethod OPTIONS +fld public final static com.fnproject.fn.api.flow.HttpMethod PATCH +fld public final static com.fnproject.fn.api.flow.HttpMethod POST +fld public final static com.fnproject.fn.api.flow.HttpMethod PUT +meth public java.lang.String toString() +meth public static com.fnproject.fn.api.flow.HttpMethod valueOf(java.lang.String) +meth public static com.fnproject.fn.api.flow.HttpMethod[] values() +supr java.lang.Enum +hfds verb + +CLSS public abstract interface com.fnproject.fn.api.flow.HttpRequest +meth public abstract byte[] getBodyAsBytes() +meth public abstract com.fnproject.fn.api.Headers getHeaders() +meth public abstract com.fnproject.fn.api.flow.HttpMethod getMethod() + +CLSS public abstract interface com.fnproject.fn.api.flow.HttpResponse +meth public abstract byte[] getBodyAsBytes() +meth public abstract com.fnproject.fn.api.Headers getHeaders() +meth public abstract int getStatusCode() + +CLSS public com.fnproject.fn.api.flow.InvalidStageResponseException +cons public init(java.lang.String) +supr com.fnproject.fn.api.flow.PlatformException + +CLSS public com.fnproject.fn.api.flow.LambdaSerializationException +cons public init(java.lang.String) +cons public init(java.lang.String,java.lang.Exception) +supr com.fnproject.fn.api.flow.FlowCompletionException + +CLSS public com.fnproject.fn.api.flow.PlatformException +cons public init(java.lang.String) +cons public init(java.lang.String,java.lang.Throwable) +cons public init(java.lang.Throwable) +meth public java.lang.Throwable fillInStackTrace() +supr com.fnproject.fn.api.flow.FlowCompletionException + +CLSS public com.fnproject.fn.api.flow.ResultSerializationException +cons public init(java.lang.String,java.lang.Throwable) +supr com.fnproject.fn.api.flow.FlowCompletionException + +CLSS public com.fnproject.fn.api.flow.StageInvokeFailedException +cons public init(java.lang.String) +supr com.fnproject.fn.api.flow.PlatformException + +CLSS public com.fnproject.fn.api.flow.StageLostException +cons public init(java.lang.String) +supr com.fnproject.fn.api.flow.PlatformException + +CLSS public com.fnproject.fn.api.flow.StageTimeoutException +cons public init(java.lang.String) +supr com.fnproject.fn.api.flow.PlatformException + +CLSS public final com.fnproject.fn.api.flow.WrappedFunctionException +cons public init(java.lang.Throwable) +intf java.io.Serializable +meth public java.lang.Class getOriginalExceptionType() +supr java.lang.RuntimeException +hfds originalExceptionType + +CLSS public abstract interface java.io.Serializable + +CLSS public abstract interface java.lang.Comparable<%0 extends java.lang.Object> +meth public abstract int compareTo({java.lang.Comparable%0}) + +CLSS public abstract java.lang.Enum<%0 extends java.lang.Enum<{java.lang.Enum%0}>> +cons protected init(java.lang.String,int) +intf java.io.Serializable +intf java.lang.Comparable<{java.lang.Enum%0}> +meth protected final java.lang.Object clone() throws java.lang.CloneNotSupportedException +meth protected final void finalize() +meth public final boolean equals(java.lang.Object) +meth public final int compareTo({java.lang.Enum%0}) +meth public final int hashCode() +meth public final int ordinal() +meth public final java.lang.Class<{java.lang.Enum%0}> getDeclaringClass() +meth public final java.lang.String name() +meth public java.lang.String toString() +meth public static <%0 extends java.lang.Enum<{%%0}>> {%%0} valueOf(java.lang.Class<{%%0}>,java.lang.String) +supr java.lang.Object +hfds name,ordinal + +CLSS public java.lang.Exception +cons protected init(java.lang.String,java.lang.Throwable,boolean,boolean) +cons public init() +cons public init(java.lang.String) +cons public init(java.lang.String,java.lang.Throwable) +cons public init(java.lang.Throwable) +supr java.lang.Throwable +hfds serialVersionUID + +CLSS public abstract interface !annotation java.lang.FunctionalInterface + anno 0 java.lang.annotation.Documented() + anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[TYPE]) +intf java.lang.annotation.Annotation + +CLSS public java.lang.Object +cons public init() +meth protected java.lang.Object clone() throws java.lang.CloneNotSupportedException +meth protected void finalize() throws java.lang.Throwable +meth public boolean equals(java.lang.Object) +meth public final java.lang.Class getClass() +meth public final void notify() +meth public final void notifyAll() +meth public final void wait() throws java.lang.InterruptedException +meth public final void wait(long) throws java.lang.InterruptedException +meth public final void wait(long,int) throws java.lang.InterruptedException +meth public int hashCode() +meth public java.lang.String toString() + +CLSS public abstract interface java.lang.Runnable + anno 0 java.lang.FunctionalInterface() +meth public abstract void run() + +CLSS public java.lang.RuntimeException +cons protected init(java.lang.String,java.lang.Throwable,boolean,boolean) +cons public init() +cons public init(java.lang.String) +cons public init(java.lang.String,java.lang.Throwable) +cons public init(java.lang.Throwable) +supr java.lang.Exception +hfds serialVersionUID + +CLSS public java.lang.Throwable +cons protected init(java.lang.String,java.lang.Throwable,boolean,boolean) +cons public init() +cons public init(java.lang.String) +cons public init(java.lang.String,java.lang.Throwable) +cons public init(java.lang.Throwable) +intf java.io.Serializable +meth public final java.lang.Throwable[] getSuppressed() +meth public final void addSuppressed(java.lang.Throwable) +meth public java.lang.StackTraceElement[] getStackTrace() +meth public java.lang.String getLocalizedMessage() +meth public java.lang.String getMessage() +meth public java.lang.String toString() +meth public java.lang.Throwable fillInStackTrace() +meth public java.lang.Throwable getCause() +meth public java.lang.Throwable initCause(java.lang.Throwable) +meth public void printStackTrace() +meth public void printStackTrace(java.io.PrintStream) +meth public void printStackTrace(java.io.PrintWriter) +meth public void setStackTrace(java.lang.StackTraceElement[]) +supr java.lang.Object +hfds CAUSE_CAPTION,EMPTY_THROWABLE_ARRAY,NULL_CAUSE_MESSAGE,SELF_SUPPRESSION_MESSAGE,SUPPRESSED_CAPTION,SUPPRESSED_SENTINEL,UNASSIGNED_STACK,backtrace,cause,detailMessage,serialVersionUID,stackTrace,suppressedExceptions +hcls PrintStreamOrWriter,SentinelHolder,WrappedPrintStream,WrappedPrintWriter + +CLSS public abstract interface java.lang.annotation.Annotation +meth public abstract boolean equals(java.lang.Object) +meth public abstract int hashCode() +meth public abstract java.lang.Class annotationType() +meth public abstract java.lang.String toString() + +CLSS public abstract interface !annotation java.lang.annotation.Documented + anno 0 java.lang.annotation.Documented() + anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[ANNOTATION_TYPE]) +intf java.lang.annotation.Annotation + +CLSS public abstract interface !annotation java.lang.annotation.Retention + anno 0 java.lang.annotation.Documented() + anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[ANNOTATION_TYPE]) +intf java.lang.annotation.Annotation +meth public abstract java.lang.annotation.RetentionPolicy value() + +CLSS public abstract interface !annotation java.lang.annotation.Target + anno 0 java.lang.annotation.Documented() + anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[ANNOTATION_TYPE]) +intf java.lang.annotation.Annotation +meth public abstract java.lang.annotation.ElementType[] value() + +CLSS public abstract interface java.util.concurrent.Callable<%0 extends java.lang.Object> + anno 0 java.lang.FunctionalInterface() +meth public abstract {java.util.concurrent.Callable%0} call() throws java.lang.Exception + +CLSS public abstract interface java.util.function.BiConsumer<%0 extends java.lang.Object, %1 extends java.lang.Object> + anno 0 java.lang.FunctionalInterface() +meth public abstract void accept({java.util.function.BiConsumer%0},{java.util.function.BiConsumer%1}) +meth public java.util.function.BiConsumer<{java.util.function.BiConsumer%0},{java.util.function.BiConsumer%1}> andThen(java.util.function.BiConsumer) + +CLSS public abstract interface java.util.function.BiFunction<%0 extends java.lang.Object, %1 extends java.lang.Object, %2 extends java.lang.Object> + anno 0 java.lang.FunctionalInterface() +meth public <%0 extends java.lang.Object> java.util.function.BiFunction<{java.util.function.BiFunction%0},{java.util.function.BiFunction%1},{%%0}> andThen(java.util.function.Function) +meth public abstract {java.util.function.BiFunction%2} apply({java.util.function.BiFunction%0},{java.util.function.BiFunction%1}) + +CLSS public abstract interface java.util.function.Consumer<%0 extends java.lang.Object> + anno 0 java.lang.FunctionalInterface() +meth public abstract void accept({java.util.function.Consumer%0}) +meth public java.util.function.Consumer<{java.util.function.Consumer%0}> andThen(java.util.function.Consumer) + +CLSS public abstract interface java.util.function.Function<%0 extends java.lang.Object, %1 extends java.lang.Object> + anno 0 java.lang.FunctionalInterface() +meth public <%0 extends java.lang.Object> java.util.function.Function<{%%0},{java.util.function.Function%1}> compose(java.util.function.Function) +meth public <%0 extends java.lang.Object> java.util.function.Function<{java.util.function.Function%0},{%%0}> andThen(java.util.function.Function) +meth public abstract {java.util.function.Function%1} apply({java.util.function.Function%0}) +meth public static <%0 extends java.lang.Object> java.util.function.Function<{%%0},{%%0}> identity() + +CLSS public abstract interface java.util.function.Supplier<%0 extends java.lang.Object> + anno 0 java.lang.FunctionalInterface() +meth public abstract {java.util.function.Supplier%0} get() + diff --git a/api/src/main/java/com/fnproject/fn/api/flow/Flow.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/Flow.java similarity index 95% rename from api/src/main/java/com/fnproject/fn/api/flow/Flow.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/Flow.java index 22ab2ed8..b366e361 100644 --- a/api/src/main/java/com/fnproject/fn/api/flow/Flow.java +++ b/flow-api/src/main/java/com/fnproject/fn/api/flow/Flow.java @@ -33,7 +33,7 @@ public interface Flow extends Serializable { *

* Function IDs should be of the form "APPID/path/in/app" (without leading slash) where APPID may either be a named application or ".", indicating the appID of the current (calling) function. * - * @param functionId Function ID of function to invoke - this should have the form APPNAME/FUNCTION_PATH (e.g. "myapp/path/to/function" or "./path/to/function"). + * @param functionId Function ID of function to invoke - this should be the function ID returned by `fn inspect function appName fnName` * @param method HTTP method to invoke function * @param headers Headers to add to the HTTP request representing the function invocation * @param data input data to function as a byte array - @@ -45,7 +45,7 @@ public interface Flow extends Serializable { * Invoke a function by ID with headers and an empty body *

* - * @param functionId Function ID of function to invoke - this should have the form APPNAME/FUNCTION_PATH (e.g. "myapp/path/to/function" or "./path/to/function"). + * @param functionId Function ID of function to invoke - this should be the function ID returned by `fn inspect function appName fnName` * @param method HTTP method to invoke function * @param headers Headers to add to the HTTP request representing the function invocation * @return a future which completes normally if the function succeeded and fails if it fails @@ -60,7 +60,7 @@ default FlowFuture invokeFunction(String functionId, HttpMethod me *

* This currently only maps to JSON via the default JSON mapper in the FDK * - * @param functionId Function ID of function to invoke - this should have the form APPNAME/FUNCTION_PATH (e.g. "myapp/path/to/function" or "./path/to/function"). + * @param functionId Function ID of function to invoke - this should be the function ID returned by `fn inspect function appName fnName` * @param input The input object to send to the function input * @param responseType The expected response type of the target function * @param The Response type @@ -77,7 +77,7 @@ default FlowFuture invokeFunction(String function *

* This currently only maps to JSON via the default JSON mapper in the FDK * - * @param functionId Function ID of function to invoke - this should have the form APPNAME/FUNCTION_PATH (e.g. "myapp/path/to/function" or "./path/to/function"). + * @param functionId Function ID of function to invoke - this should be the function ID returned by `fn inspect function appName fnName` * @param method the HTTP method to use for this call * @param headers additional HTTP headers to pass to this function - * @param input The input object to send to the function input @@ -98,7 +98,7 @@ default FlowFuture invokeFunction(String function *

* This currently only maps to JSON via the default JSON mapper in the FDK * - * @param functionId Function ID of function to invoke - this should have the form APPNAME/FUNCTION_PATH (e.g. "myapp/path/to/function" or "./path/to/function"). + * @param functionId Function ID of function to invoke - this should be the function ID returned by `fn inspect function appName fnName` * @param input The input object to send to the function input * @param The Input type of the function * @return a flow future that completes with the result of the function, or an error if the function invocation failed @@ -116,7 +116,7 @@ default FlowFuture invokeFunction(String functionId, U input) *

* This currently only maps to JSON via the default JSON mapper in the FDK * - * @param functionId Function ID of function to invoke - this should have the form APPNAME/FUNCTION_PATH (e.g. "myapp/path/to/function" or "./path/to/function"). + * @param functionId Function ID of function to invoke - this should be the function ID returned by `fn inspect function appName fnName` * @param method the HTTP method to use for this call * @param headers additional HTTP headers to pass to this function - * @param input The input object to send to the function input @@ -130,7 +130,7 @@ default FlowFuture invokeFunction(String functionId, U input) * Invoke a function by ID with no headers *

* - * @param functionId Function ID of function to invoke - this should have the form APPNAME/FUNCTION_PATH (e.g. "myapp/path/to/function" or "./path/to/function"). + * @param functionId Function ID of function to invoke - this should be the function ID returned by `fn inspect function appName fnName` * @param method HTTP method to invoke function * @return a future which completes normally if the function succeeded and fails if it fails * @see #invokeFunction(String, HttpMethod, Headers, byte[]) diff --git a/api/src/main/java/com/fnproject/fn/api/flow/FlowCompletionException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/FlowCompletionException.java similarity index 100% rename from api/src/main/java/com/fnproject/fn/api/flow/FlowCompletionException.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/FlowCompletionException.java diff --git a/api/src/main/java/com/fnproject/fn/api/flow/FlowFuture.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/FlowFuture.java similarity index 99% rename from api/src/main/java/com/fnproject/fn/api/flow/FlowFuture.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/FlowFuture.java index da3d5e60..6dd82fdb 100644 --- a/api/src/main/java/com/fnproject/fn/api/flow/FlowFuture.java +++ b/flow-api/src/main/java/com/fnproject/fn/api/flow/FlowFuture.java @@ -1,7 +1,6 @@ package com.fnproject.fn.api.flow; import java.io.Serializable; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; diff --git a/api/src/main/java/com/fnproject/fn/api/flow/Flows.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/Flows.java similarity index 94% rename from api/src/main/java/com/fnproject/fn/api/flow/Flows.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/Flows.java index c71b58d4..5845671b 100644 --- a/api/src/main/java/com/fnproject/fn/api/flow/Flows.java +++ b/flow-api/src/main/java/com/fnproject/fn/api/flow/Flows.java @@ -1,5 +1,6 @@ package com.fnproject.fn.api.flow; + import java.io.Serializable; import java.util.Objects; import java.util.concurrent.Callable; @@ -19,7 +20,7 @@ private Flows() { * * @return the current supplier of the flow runtime */ - public static FlowSource getCurrentFlowSource() { + public static synchronized FlowSource getCurrentFlowSource() { return flowSource; } @@ -37,7 +38,7 @@ public interface FlowSource { * @return the current flow runtime */ public synchronized static Flow currentFlow() { - Objects.requireNonNull(flowSource, "Flows.flowSource is not set - Flows.currentFlow() should only be called from within a FaaS function invocation"); + Objects.requireNonNull(flowSource, "Flows.flowSource is not set - Flows.currentFlow() is the @FnFeature(FlowFeature.class) annotation set on your function?"); return flowSource.currentFlow(); } diff --git a/api/src/main/java/com/fnproject/fn/api/flow/FunctionInvocationException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/FunctionInvocationException.java similarity index 100% rename from api/src/main/java/com/fnproject/fn/api/flow/FunctionInvocationException.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/FunctionInvocationException.java diff --git a/api/src/main/java/com/fnproject/fn/api/flow/FunctionInvokeFailedException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/FunctionInvokeFailedException.java similarity index 100% rename from api/src/main/java/com/fnproject/fn/api/flow/FunctionInvokeFailedException.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/FunctionInvokeFailedException.java diff --git a/api/src/main/java/com/fnproject/fn/api/flow/FunctionTimeoutException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/FunctionTimeoutException.java similarity index 100% rename from api/src/main/java/com/fnproject/fn/api/flow/FunctionTimeoutException.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/FunctionTimeoutException.java diff --git a/api/src/main/java/com/fnproject/fn/api/flow/HttpMethod.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/HttpMethod.java similarity index 100% rename from api/src/main/java/com/fnproject/fn/api/flow/HttpMethod.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/HttpMethod.java diff --git a/api/src/main/java/com/fnproject/fn/api/flow/HttpRequest.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/HttpRequest.java similarity index 100% rename from api/src/main/java/com/fnproject/fn/api/flow/HttpRequest.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/HttpRequest.java diff --git a/api/src/main/java/com/fnproject/fn/api/flow/HttpResponse.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/HttpResponse.java similarity index 100% rename from api/src/main/java/com/fnproject/fn/api/flow/HttpResponse.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/HttpResponse.java diff --git a/api/src/main/java/com/fnproject/fn/api/flow/InvalidStageResponseException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/InvalidStageResponseException.java similarity index 100% rename from api/src/main/java/com/fnproject/fn/api/flow/InvalidStageResponseException.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/InvalidStageResponseException.java diff --git a/api/src/main/java/com/fnproject/fn/api/flow/LambdaSerializationException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/LambdaSerializationException.java similarity index 100% rename from api/src/main/java/com/fnproject/fn/api/flow/LambdaSerializationException.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/LambdaSerializationException.java diff --git a/api/src/main/java/com/fnproject/fn/api/flow/PlatformException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/PlatformException.java similarity index 100% rename from api/src/main/java/com/fnproject/fn/api/flow/PlatformException.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/PlatformException.java diff --git a/api/src/main/java/com/fnproject/fn/api/flow/ResultSerializationException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/ResultSerializationException.java similarity index 100% rename from api/src/main/java/com/fnproject/fn/api/flow/ResultSerializationException.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/ResultSerializationException.java diff --git a/api/src/main/java/com/fnproject/fn/api/flow/StageInvokeFailedException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/StageInvokeFailedException.java similarity index 100% rename from api/src/main/java/com/fnproject/fn/api/flow/StageInvokeFailedException.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/StageInvokeFailedException.java diff --git a/api/src/main/java/com/fnproject/fn/api/flow/StageLostException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/StageLostException.java similarity index 100% rename from api/src/main/java/com/fnproject/fn/api/flow/StageLostException.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/StageLostException.java diff --git a/api/src/main/java/com/fnproject/fn/api/flow/StageTimeoutException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/StageTimeoutException.java similarity index 100% rename from api/src/main/java/com/fnproject/fn/api/flow/StageTimeoutException.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/StageTimeoutException.java diff --git a/api/src/main/java/com/fnproject/fn/api/flow/WrappedFunctionException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/WrappedFunctionException.java similarity index 100% rename from api/src/main/java/com/fnproject/fn/api/flow/WrappedFunctionException.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/WrappedFunctionException.java diff --git a/api/src/main/java/com/fnproject/fn/api/flow/package-info.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/package-info.java similarity index 100% rename from api/src/main/java/com/fnproject/fn/api/flow/package-info.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/package-info.java diff --git a/api/src/test/java/com/fnproject/fn/api/flow/FlowsTest.java b/flow-api/src/test/java/com/fnproject/fn/api/flow/FlowsTest.java similarity index 99% rename from api/src/test/java/com/fnproject/fn/api/flow/FlowsTest.java rename to flow-api/src/test/java/com/fnproject/fn/api/flow/FlowsTest.java index d8d51d5e..aed5b7eb 100644 --- a/api/src/test/java/com/fnproject/fn/api/flow/FlowsTest.java +++ b/flow-api/src/test/java/com/fnproject/fn/api/flow/FlowsTest.java @@ -1,9 +1,11 @@ package com.fnproject.fn.api.flow; +import org.junit.Test; + import java.lang.reflect.Modifier; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import org.junit.Test; public class FlowsTest { public FlowsTest() { diff --git a/flow-runtime/pom.xml b/flow-runtime/pom.xml new file mode 100644 index 00000000..532686fe --- /dev/null +++ b/flow-runtime/pom.xml @@ -0,0 +1,76 @@ + + + + fdk + com.fnproject.fn + 1.0.0-SNAPSHOT + + 4.0.0 + + flow-runtime + + + + com.fnproject.fn + api + + + com.fnproject.fn + flow-api + + + + com.fnproject.fn + runtime + + + com.fnproject.fn + testing-junit4 + test + + + org.mockito + mockito-core + test + + + junit + junit + + + org.assertj + assertj-core + test + + + org.apache.httpcomponents + httpmime + test + + + + + + + + maven-dependency-plugin + + + copy-dependencies + package + + copy-dependencies + + + ${project.build.directory}/dependency + runtime + true + + + + + + + diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/APIModel.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/APIModel.java similarity index 100% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/APIModel.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/APIModel.java index eea809a9..c5cb2b1b 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/APIModel.java +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/APIModel.java @@ -5,8 +5,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonUnwrapped; import com.fnproject.fn.api.Headers; -import com.fnproject.fn.api.flow.*; import com.fnproject.fn.api.exception.FunctionInputHandlingException; +import com.fnproject.fn.api.flow.*; import com.fnproject.fn.runtime.exception.PlatformCommunicationException; import org.apache.commons.io.IOUtils; diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/BlobResponse.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/BlobResponse.java similarity index 100% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/BlobResponse.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/BlobResponse.java diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/BlobStoreClient.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/BlobStoreClient.java similarity index 100% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/BlobStoreClient.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/BlobStoreClient.java diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/CodeLocation.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/CodeLocation.java similarity index 100% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/CodeLocation.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/CodeLocation.java diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/CompleterClient.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/CompleterClient.java similarity index 100% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/CompleterClient.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/CompleterClient.java diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/CompleterClientFactory.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/CompleterClientFactory.java similarity index 100% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/CompleterClientFactory.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/CompleterClientFactory.java diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/CompletionId.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/CompletionId.java similarity index 100% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/CompletionId.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/CompletionId.java diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/DefaultHttpResponse.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/DefaultHttpResponse.java similarity index 100% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/DefaultHttpResponse.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/DefaultHttpResponse.java diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/EntityReader.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/EntityReader.java similarity index 100% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/EntityReader.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/EntityReader.java diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowContinuationInvoker.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowContinuationInvoker.java similarity index 93% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowContinuationInvoker.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowContinuationInvoker.java index 81d12f1a..177b2e89 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowContinuationInvoker.java +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowContinuationInvoker.java @@ -1,12 +1,11 @@ package com.fnproject.fn.runtime.flow; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fnproject.fn.api.*; +import com.fnproject.fn.api.exception.FunctionInputHandlingException; import com.fnproject.fn.api.flow.Flow; import com.fnproject.fn.api.flow.Flows; import com.fnproject.fn.api.flow.PlatformException; -import com.fnproject.fn.api.exception.FunctionInputHandlingException; import com.fnproject.fn.runtime.exception.InternalFunctionInvocationException; import com.fnproject.fn.runtime.exception.PlatformCommunicationException; @@ -32,6 +31,10 @@ public final class FlowContinuationInvoker implements FunctionInvoker { public static final String FLOW_ID_HEADER = "Fnproject-FlowId"; + FlowContinuationInvoker() { + + } + private static class URLCompleterClientFactory implements CompleterClientFactory { private final String completerBaseUrl; private transient CompleterClient completerClient; @@ -45,7 +48,7 @@ private static class URLCompleterClientFactory implements CompleterClientFactory public synchronized CompleterClient getCompleterClient() { if (this.completerClient == null) { this.completerClient = new RemoteFlowApiClient(completerBaseUrl + "/v1", - getBlobStoreClient(), new HttpClient()); + getBlobStoreClient(), new HttpClient()); } return this.completerClient; } @@ -135,7 +138,7 @@ public synchronized Flow currentFlow() { if (matchingDispatchPattern != null) { if (matchingDispatchPattern.numArguments() != invokeStageRequest.args.size()) { - throw new FunctionInputHandlingException("Number of arguments provided (" + invokeStageRequest.args.size() + ") in .InvokeStageRequest does not match the number required by the function type (" + matchingDispatchPattern.numArguments() + ")"); + throw new FunctionInputHandlingException("Number of arguments provided (" + invokeStageRequest.args.size() + ") in .InvokeStageRequest does not match the number required by the function type (" + matchingDispatchPattern.numArguments() + ")"); } } else { throw new FunctionInputHandlingException("No functional interface type matches the supplied continuation class"); @@ -168,7 +171,7 @@ public synchronized Flow currentFlow() { @Override public synchronized Flow currentFlow() { if (runtime == null) { - String functionId = evt.getAppName() + evt.getRoute(); + String functionId = ctx.getRuntimeContext().getFunctionID(); CompleterClientFactory factory = getOrCreateCompleterClientFactory(completerBaseUrl); final FlowId flowId = factory.getCompleterClient().createFlow(functionId); runtime = new RemoteFlow(flowId); @@ -204,9 +207,9 @@ private OutputEvent invokeContinuation(BlobStoreClient blobStoreClient, FlowId f APIModel.Datum datum = APIModel.datumFromJava(flowId, ite.getCause(), blobStoreClient); throw new InternalFunctionInvocationException( - "Error invoking flows lambda", - ite.getCause(), - constructOutputEvent(datum, false) + "Error invoking flows lambda", + ite.getCause(), + constructOutputEvent(datum, false) ); } catch (Exception ex) { throw new PlatformException(ex); @@ -223,27 +226,18 @@ private OutputEvent invokeContinuation(BlobStoreClient blobStoreClient, FlowId f */ final static class ContinuationOutputEvent implements OutputEvent { private final byte[] body; + private static final Headers headers = Headers.emptyHeaders().setHeader(OutputEvent.CONTENT_TYPE_HEADER, "application/json"); private ContinuationOutputEvent(boolean success, byte[] body) { this.body = body; } - /** - * The completer expects a 200 on the output event. - * - * @return - */ @Override - public int getStatusCode() { - return OutputEvent.SUCCESS; + public Status getStatus() { + return Status.Success; } - @Override - public Optional getContentType() { - return Optional.of("application/json"); - } - @Override public void writeToOutput(OutputStream out) throws IOException { out.write(body); @@ -251,7 +245,7 @@ public void writeToOutput(OutputStream out) throws IOException { @Override public Headers getHeaders() { - return Headers.emptyHeaders(); + return headers; } } diff --git a/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowFeature.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowFeature.java new file mode 100644 index 00000000..27a79f0b --- /dev/null +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowFeature.java @@ -0,0 +1,38 @@ +package com.fnproject.fn.runtime.flow; + +import com.fnproject.fn.api.FunctionInvoker; +import com.fnproject.fn.api.RuntimeContext; +import com.fnproject.fn.api.RuntimeFeature; + +/** + * + * The flow feature enables the Flow Client SDK and runtime behaviour in a Java function in order to use Flow in a function you must add the following to the function class: + * + * + * + * import com.fnproject.fn.api.FnFeature; + * import com.fnproject.fn.runtime.flow.FlowFeature; + * + * @FnFeature(FlowFeature.class) + * public class MyFunction { + * + * + * public void myFunction(String input){ + * Flows.currentFlow().... + * + * } + * } + * + * + * + * Created on 10/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class FlowFeature implements RuntimeFeature { + @Override + public void initialize(RuntimeContext context){ + FunctionInvoker invoker = new FlowContinuationInvoker(); + context.addInvoker(invoker,FunctionInvoker.Phase.PreCall); + } +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowFutureSource.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowFutureSource.java similarity index 100% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowFutureSource.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowFutureSource.java diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowId.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowId.java similarity index 100% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowId.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowId.java diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowRuntimeGlobals.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowRuntimeGlobals.java similarity index 100% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowRuntimeGlobals.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowRuntimeGlobals.java diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/HttpClient.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/HttpClient.java similarity index 100% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/HttpClient.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/HttpClient.java diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/JsonInvoke.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/JsonInvoke.java similarity index 97% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/JsonInvoke.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/JsonInvoke.java index cfb21953..d6e20272 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/JsonInvoke.java +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/JsonInvoke.java @@ -73,7 +73,7 @@ public static FlowFuture invokeFunction(Flow flow, String func String inputString = getObjectMapper().writeValueAsString(input); Headers newHeaders; if (!headers.get("Content-type").isPresent()) { - newHeaders = headers.withHeader("Content-type", "application/json"); + newHeaders = headers.addHeader("Content-type", "application/json"); } else { newHeaders = headers; } diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteBlobStoreClient.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteBlobStoreClient.java similarity index 97% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteBlobStoreClient.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteBlobStoreClient.java index 18f0d644..b4274d93 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteBlobStoreClient.java +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteBlobStoreClient.java @@ -1,6 +1,5 @@ package com.fnproject.fn.runtime.flow; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fnproject.fn.runtime.exception.PlatformCommunicationException; import java.io.IOException; diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlow.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlow.java similarity index 100% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlow.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlow.java diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClient.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClient.java similarity index 95% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClient.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClient.java index 60c96a09..a60bf452 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClient.java +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClient.java @@ -120,12 +120,10 @@ public CompletionId invokeFunction(FlowId flowId, String functionId, byte[] data httpReq.headers = new ArrayList<>(); - headers.getAll().forEach((k, v) -> { - APIModel.HTTPHeader h = new APIModel.HTTPHeader(); - h.key = k; - h.value = v; - httpReq.headers.add(h); - }); + headers.asMap().forEach((k, vs) -> vs.forEach(v -> httpReq.headers.add(APIModel.HTTPHeader.create(k, v)))); + + Map> headersMap = headers.asMap(); + headersMap.forEach((key, values) -> values.forEach(value -> httpReq.headers.add(APIModel.HTTPHeader.create(key, value)))); } httpReq.method = APIModel.HTTPMethod.fromFlow(method); @@ -153,9 +151,9 @@ public CompletionId completedValue(FlowId flowId, boolean success, Object value, APIModel.CompletionResult completionResult = new APIModel.CompletionResult(); completionResult.successful = success; - if(value instanceof RemoteFlow.RemoteFlowFuture) { + if (value instanceof RemoteFlow.RemoteFlowFuture) { APIModel.StageRefDatum stageRefDatum = new APIModel.StageRefDatum(); - stageRefDatum.stageId = ((RemoteFlow.RemoteFlowFuture)value).id(); + stageRefDatum.stageId = ((RemoteFlow.RemoteFlowFuture) value).id(); completionResult.result = stageRefDatum; } else { APIModel.Datum blobDatum = APIModel.datumFromJava(flowId, value, blobStoreClient); @@ -251,14 +249,14 @@ public Object waitForCompletion(FlowId flowId, CompletionId id, ClassLoader igno long remainingTimeout = Math.max(1, start + msTimeout - lastStart); try (HttpClient.HttpResponse response = - httpClient.execute(prepareGet(apiUrlBase + "/flows/" + flowId.getId() + "/stages/" + id.getId() + "/await?timeout_ms=" + remainingTimeout))) { + httpClient.execute(prepareGet(apiUrlBase + "/flows/" + flowId.getId() + "/stages/" + id.getId() + "/await?timeout_ms=" + remainingTimeout))) { if (response.getStatusCode() == 200) { APIModel.AwaitStageResponse resp = FlowRuntimeGlobals.getObjectMapper().readValue(response.getContentStream(), APIModel.AwaitStageResponse.class); if (resp.result.successful) { return resp.result.toJava(flowId, blobStoreClient, getClass().getClassLoader()); } else { - throw new FlowCompletionException((Throwable)resp.result.toJava(flowId, blobStoreClient, getClass().getClassLoader())); + throw new FlowCompletionException((Throwable) resp.result.toJava(flowId, blobStoreClient, getClass().getClassLoader())); } } else if (response.getStatusCode() == 408) { // do nothing go round again @@ -313,10 +311,10 @@ private static PlatformCommunicationException asError(HttpClient.HttpResponse re try { String body = response.entityAsString(); return new PlatformCommunicationException(String.format("Received unexpected response (%d) from " + - "Flow service: %s", response.getStatusCode(), body == null ? "Empty body" : body)); + "Flow service: %s", response.getStatusCode(), body == null ? "Empty body" : body)); } catch (IOException e) { return new PlatformCommunicationException(String.format("Received unexpected response (%d) from " + - "Flow service. Could not read body.", response.getStatusCode()), e); + "Flow service. Could not read body.", response.getStatusCode()), e); } } @@ -342,7 +340,7 @@ private static byte[] serializeClosure(Object data) { private CompletionId addStageWithClosure(APIModel.CompletionOperation operation, FlowId flowId, Serializable supplier, CodeLocation codeLocation, List deps) { byte[] serialized = serializeClosure(supplier); - BlobResponse blobResponse = blobStoreClient.writeBlob(flowId.getId(), serialized, CONTENT_TYPE_JAVA_OBJECT); + BlobResponse blobResponse = blobStoreClient.writeBlob(flowId.getId(), serialized, CONTENT_TYPE_JAVA_OBJECT); return addStage(operation, APIModel.Blob.fromBlobResponse(blobResponse), deps, flowId, codeLocation); diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/flow/FlowsContinuationInvokerTest.java b/flow-runtime/src/test/java/com/fnproject/fn/runtime/flow/FlowsContinuationInvokerTest.java similarity index 76% rename from runtime/src/test/java/com/fnproject/fn/runtime/flow/FlowsContinuationInvokerTest.java rename to flow-runtime/src/test/java/com/fnproject/fn/runtime/flow/FlowsContinuationInvokerTest.java index 389d4e34..e38dfe45 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/flow/FlowsContinuationInvokerTest.java +++ b/flow-runtime/src/test/java/com/fnproject/fn/runtime/flow/FlowsContinuationInvokerTest.java @@ -3,10 +3,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fnproject.fn.api.*; +import com.fnproject.fn.api.exception.FunctionInputHandlingException; import com.fnproject.fn.api.flow.*; -import com.fnproject.fn.runtime.QueryParametersImpl; import com.fnproject.fn.runtime.ReadOnceInputEvent; -import com.fnproject.fn.api.exception.FunctionInputHandlingException; import com.fnproject.fn.runtime.exception.InternalFunctionInvocationException; import org.junit.After; import org.junit.Before; @@ -15,10 +14,8 @@ import org.junit.rules.ExpectedException; import java.io.*; -import java.io.IOException; -import java.io.InputStream; -import java.io.Serializable; import java.lang.reflect.Method; +import java.time.Instant; import java.util.*; import static com.fnproject.fn.runtime.flow.FlowContinuationInvoker.FLOW_ID_HEADER; @@ -62,9 +59,9 @@ public void continuationInvokedWhenGraphHeaderPresent() throws IOException, Clas // Given InputEvent event = newRequest() - .withClosure((Flows.SerFunction) (x) -> x * 2) - .withJavaObjectArgs(10) - .asEvent(); + .withClosure((Flows.SerFunction) (x) -> x * 2) + .withJavaObjectArgs(10) + .asEvent(); // When Optional result = invoker.tryInvoke(new EmptyInvocationContext(), event); @@ -84,8 +81,8 @@ public void continuationNotInvokedWhenHeaderMissing() throws IOException, ClassN // Given InputEvent event = new InputEventBuilder() - .withBody("") - .build(); + .withBody("") + .build(); // When FlowContinuationInvoker invoker = new FlowContinuationInvoker(); @@ -102,8 +99,8 @@ public void failsIfArgMissing() throws IOException, ClassNotFoundException { // Given InputEvent event = newRequest() - .withClosure((Flows.SerFunction) (x) -> x * 2) - .asEvent(); + .withClosure((Flows.SerFunction) (x) -> x * 2) + .asEvent(); invoker.tryInvoke(new EmptyInvocationContext(), event); @@ -121,8 +118,8 @@ public void failsIfUnknownClosureType() { thrown.expect(FunctionInputHandlingException.class); // Given InputEvent event = newRequest() - .withClosure(new TestIf()) - .asEvent(); + .withClosure(new TestIf()) + .asEvent(); invoker.tryInvoke(new EmptyInvocationContext(), event); } @@ -143,25 +140,25 @@ private Tc(Serializable closure, Object result, Object... args) { } Tc[] cases = new Tc[]{ - new Tc((Flows.SerConsumer) (v) -> { - }, null, "hello"), - new Tc((Flows.SerBiFunction) (String::concat), "hello bob", "hello ", "bob"), - new Tc((Flows.SerBiConsumer) (a, b) -> { - }, null, "hello ", "bob"), - new Tc((Flows.SerFunction) (String::toUpperCase), "HELLO BOB", "hello bob"), - new Tc((Flows.SerRunnable) () -> { - }, null), - new Tc((Flows.SerCallable) () -> "hello", "hello"), - new Tc((Flows.SerSupplier) () -> "hello", "hello"), + new Tc((Flows.SerConsumer) (v) -> { + }, null, "hello"), + new Tc((Flows.SerBiFunction) (String::concat), "hello bob", "hello ", "bob"), + new Tc((Flows.SerBiConsumer) (a, b) -> { + }, null, "hello ", "bob"), + new Tc((Flows.SerFunction) (String::toUpperCase), "HELLO BOB", "hello bob"), + new Tc((Flows.SerRunnable) () -> { + }, null), + new Tc((Flows.SerCallable) () -> "hello", "hello"), + new Tc((Flows.SerSupplier) () -> "hello", "hello"), }; for (Tc tc : cases) { InputEvent event = newRequest() - .withClosure(tc.closure) - .withJavaObjectArgs(tc.args) - .asEvent(); + .withClosure(tc.closure) + .withJavaObjectArgs(tc.args) + .asEvent(); Optional result = invoker.tryInvoke(new EmptyInvocationContext(), event); assertThat(result).isPresent(); @@ -180,13 +177,13 @@ private Tc(Serializable closure, Object result, Object... args) { public void emptyValueCorrectlySerialized() throws IOException, ClassNotFoundException { // Given InputEvent event = newRequest() - .withClosure((Flows.SerConsumer) (x) -> { - if (x != null) { - throw new RuntimeException("Not Null"); - } - }) - .withEmptyDatumArg() - .asEvent(); + .withClosure((Flows.SerConsumer) (x) -> { + if (x != null) { + throw new RuntimeException("Not Null"); + } + }) + .withEmptyDatumArg() + .asEvent(); // When Optional result = invoker.tryInvoke(new EmptyInvocationContext(), event); @@ -202,8 +199,8 @@ public void emptyValueCorrectlySerialized() throws IOException, ClassNotFoundExc public void emptyValueCorrectlyDeSerialized() throws IOException, ClassNotFoundException { // Given InputEvent event = newRequest() - .withClosure((Flows.SerSupplier) () -> null) - .asEvent(); + .withClosure((Flows.SerSupplier) () -> null) + .asEvent(); // When Optional result = invoker.tryInvoke(new EmptyInvocationContext(), event); @@ -221,12 +218,12 @@ public void stageRefCorrectlyDeserialized() throws IOException, ClassNotFoundExc // Given InputEvent event = newRequest() - .withClosure((Flows.SerConsumer>) (x) -> { - assertThat(x).isNotNull(); - assertThat(((RemoteFlow.RemoteFlowFuture) x).id()).isEqualTo("newStage"); - }) - .withStageRefArg("newStage") - .asEvent(); + .withClosure((Flows.SerConsumer>) (x) -> { + assertThat(x).isNotNull(); + assertThat(((RemoteFlow.RemoteFlowFuture) x).id()).isEqualTo("newStage"); + }) + .withStageRefArg("newStage") + .asEvent(); // When Optional result = invoker.tryInvoke(new EmptyInvocationContext(), event); @@ -245,8 +242,8 @@ public void stageRefCorrectlySerialized() throws IOException, ClassNotFoundExcep // Given InputEvent event = newRequest() - .withClosure((Flows.SerSupplier>) () -> ff) - .asEvent(); + .withClosure((Flows.SerSupplier>) () -> ff) + .asEvent(); // When Optional result = invoker.tryInvoke(new EmptyInvocationContext(), event); @@ -266,11 +263,11 @@ public void stageRefCorrectlySerialized() throws IOException, ClassNotFoundExcep public void setsCurrentStageId() throws IOException, ClassNotFoundException { InputEvent event = newRequest() - .withClosure((Flows.SerRunnable) () -> { - assertThat(FlowRuntimeGlobals.getCurrentCompletionId()).contains(new CompletionId("myStage")); - }) - .withStageId("myStage") - .asEvent(); + .withClosure((Flows.SerRunnable) () -> { + assertThat(FlowRuntimeGlobals.getCurrentCompletionId()).contains(new CompletionId("myStage")); + }) + .withStageId("myStage") + .asEvent(); // When Optional result = invoker.tryInvoke(new EmptyInvocationContext(), event); @@ -287,13 +284,13 @@ public void httpRespToFn() throws Exception { // Given InputEvent event = newRequest() - .withClosure((Flows.SerConsumer) (x) -> { - assertThat(x.getBodyAsBytes()).isEqualTo("Hello".getBytes()); - assertThat(x.getStatusCode()).isEqualTo(201); - assertThat(x.getHeaders().get("Foo")).contains("Bar"); - }) - .withHttpRespArg(201, "Hello", APIModel.HTTPHeader.create("Foo", "Bar")) - .asEvent(); + .withClosure((Flows.SerConsumer) (x) -> { + assertThat(x.getBodyAsBytes()).isEqualTo("Hello".getBytes()); + assertThat(x.getStatusCode()).isEqualTo(201); + assertThat(x.getHeaders().get("Foo")).contains("Bar"); + }) + .withHttpRespArg(201, "Hello", APIModel.HTTPHeader.create("Foo", "Bar")) + .asEvent(); // When @@ -309,14 +306,14 @@ public void httpRespToFnWithError() throws Exception { // Given InputEvent event = newRequest() - .withClosure((Flows.SerConsumer) (e) -> { - HttpResponse x = e.getFunctionResponse(); - assertThat(x.getBodyAsBytes()).isEqualTo("Hello".getBytes()); - assertThat(x.getStatusCode()).isEqualTo(201); - assertThat(x.getHeaders().get("Foo")).contains("Bar"); - }) - .withHttpRespArg(false, 201, "Hello", APIModel.HTTPHeader.create("Foo", "Bar")) - .asEvent(); + .withClosure((Flows.SerConsumer) (e) -> { + HttpResponse x = e.getFunctionResponse(); + assertThat(x.getBodyAsBytes()).isEqualTo("Hello".getBytes()); + assertThat(x.getStatusCode()).isEqualTo(201); + assertThat(x.getHeaders().get("Foo")).contains("Bar"); + }) + .withHttpRespArg(false, 201, "Hello", APIModel.HTTPHeader.create("Foo", "Bar")) + .asEvent(); // When @@ -341,24 +338,24 @@ class TestCase { } } for (TestCase tc : new TestCase[]{ - new TestCase(APIModel.ErrorType.InvalidStageResponse, InvalidStageResponseException.class), - new TestCase(APIModel.ErrorType.FunctionInvokeFailed, FunctionInvokeFailedException.class), - new TestCase(APIModel.ErrorType.FunctionTimeout, FunctionTimeoutException.class), - new TestCase(APIModel.ErrorType.StageFailed, StageInvokeFailedException.class), - new TestCase(APIModel.ErrorType.StageTimeout, StageTimeoutException.class), - new TestCase(APIModel.ErrorType.StageLost, StageLostException.class) + new TestCase(APIModel.ErrorType.InvalidStageResponse, InvalidStageResponseException.class), + new TestCase(APIModel.ErrorType.FunctionInvokeFailed, FunctionInvokeFailedException.class), + new TestCase(APIModel.ErrorType.FunctionTimeout, FunctionTimeoutException.class), + new TestCase(APIModel.ErrorType.StageFailed, StageInvokeFailedException.class), + new TestCase(APIModel.ErrorType.StageTimeout, StageTimeoutException.class), + new TestCase(APIModel.ErrorType.StageLost, StageLostException.class) }) { Class type = tc.exceptionType; // Given InputEvent event = newRequest() - .withClosure((Flows.SerConsumer) (e) -> { + .withClosure((Flows.SerConsumer) (e) -> { - assertThat(e).hasMessage("My Error"); - assertThat(e).isInstanceOf(type); - }) - .withErrorBody(tc.errorType, "My Error") - .asEvent(); + assertThat(e).hasMessage("My Error"); + assertThat(e).isInstanceOf(type); + }) + .withErrorBody(tc.errorType, "My Error") + .asEvent(); // When @@ -376,8 +373,8 @@ class TestCase { public void functionInvocationExceptionThrownIfStageResultIsNotSerializable() { thrown.expect(ResultSerializationException.class); InputEvent event = newRequest() - .withClosure((Flows.SerSupplier) Object::new) - .asEvent(); + .withClosure((Flows.SerSupplier) Object::new) + .asEvent(); invoker.tryInvoke(new EmptyInvocationContext(), event); @@ -395,10 +392,10 @@ public void functionInvocationExceptionThrownIfStageThrowsException() { InputEvent event = newRequest() - .withClosure((Flows.SerRunnable) () -> { - throw new MyRuntimeException(); - }) - .asEvent(); + .withClosure((Flows.SerRunnable) () -> { + throw new MyRuntimeException(); + }) + .asEvent(); invoker.tryInvoke(new EmptyInvocationContext(), event); @@ -433,7 +430,7 @@ public FlowRequestBuilder withClosure(Serializable closure) { public FlowRequestBuilder withJavaObjectArgs(Object... args) { Arrays.stream(args).forEach((arg) -> - req.args.add(blobStore.withResult(flowId, arg, true))); + req.args.add(blobStore.withResult(flowId, arg, true))); return this; } @@ -449,10 +446,10 @@ public InputEvent asEvent() { System.err.println("Req:" + new String(body)); return new InputEventBuilder() - .withHeader(FLOW_ID_HEADER, flowId) - .withHeader("Content-type", "application/json") - .withBody(new ByteArrayInputStream(body)) - .build(); + .withHeader(FLOW_ID_HEADER, flowId) + .withHeader("Content-type", "application/json") + .withBody(new ByteArrayInputStream(body)) + .build(); } public FlowRequestBuilder withEmptyDatumArg() { @@ -550,43 +547,39 @@ public InputEventBuilder withBody(String body) { return this; } - private Map currentHeaders() { - return new HashMap<>(headers.getAll()); + private Map> currentHeaders() { + return new HashMap<>(headers.asMap()); } - public InputEventBuilder withHeaders(Map headers) { - Map updated = currentHeaders(); - updated.putAll(headers); - this.headers = Headers.fromMap(updated); - return this; - } public InputEventBuilder withHeader(String name, String value) { - Map updated = currentHeaders(); - updated.put(name, value); - this.headers = Headers.fromMap(updated); + this.headers = headers.setHeader(name, value); return this; } private String getHeader(String key) { - for (Map.Entry entry : currentHeaders().entrySet()) { - if (key.equalsIgnoreCase(entry.getKey())) { - return entry.getValue(); - } - } - return null; + return headers.get(key).orElse(null); } public InputEvent build() { return new ReadOnceInputEvent( - "", "", "", "", - body, - headers, new QueryParametersImpl()); + body, + headers, "callID", Instant.now()); } } class EmptyRuntimeContext implements RuntimeContext { + @Override + public String getAppID() { + return "appID"; + } + + @Override + public String getFunctionID() { + return "fnID"; + } + @Override public Optional getInvokeInstance() { return Optional.empty(); @@ -637,6 +630,12 @@ public void setInvoker(FunctionInvoker invoker) { throw new RuntimeException("You can't modify the empty runtime context in the tests, sorry."); } + @Override + public void addInvoker(FunctionInvoker invoker, FunctionInvoker.Phase phase) { + throw new RuntimeException("You can't modify the empty runtime context in the tests, sorry."); + + } + @Override public MethodWrapper getMethod() { return null; @@ -655,5 +654,21 @@ public RuntimeContext getRuntimeContext() { public void addListener(InvocationListener listener) { } + @Override + public Headers getRequestHeaders() { + return Headers.emptyHeaders(); + } + + + @Override + public void addResponseHeader(String key, String value) { + + } + + @Override + public void setResponseHeader(String key, String value, String... vs) { + + } + } } diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClientTest.java b/flow-runtime/src/test/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClientTest.java similarity index 98% rename from runtime/src/test/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClientTest.java rename to flow-runtime/src/test/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClientTest.java index 3063a77e..72388243 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClientTest.java +++ b/flow-runtime/src/test/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClientTest.java @@ -6,6 +6,7 @@ import com.fnproject.fn.api.flow.Flows; import com.fnproject.fn.api.flow.HttpMethod; import com.fnproject.fn.runtime.exception.PlatformCommunicationException; +import com.fnproject.fn.runtime.flow.*; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.junit.Before; @@ -21,6 +22,7 @@ import java.io.ObjectOutputStream; import java.util.Collections; import java.util.List; +import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -129,7 +131,9 @@ public void invokeFunctionWithInvalidFunctionId() throws Exception { thrown.expectMessage("Failed to add stage"); // When - completerClient.invokeFunction(new FlowId(testFlowId), testFunctionId, invokeBody, HttpMethod.POST, Headers.fromMap(Collections.singletonMap("Content-type", contentType)), locationFn()); + Map headersMap = Collections.singletonMap("Content-type", contentType); + Headers headers = Headers.fromMap(headersMap); + completerClient.invokeFunction(new FlowId(testFlowId), testFunctionId, invokeBody, HttpMethod.POST, headers, locationFn()); } @Test diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/flow/TestBlobStore.java b/flow-runtime/src/test/java/com/fnproject/fn/runtime/flow/TestBlobStore.java similarity index 100% rename from runtime/src/test/java/com/fnproject/fn/runtime/flow/TestBlobStore.java rename to flow-runtime/src/test/java/com/fnproject/fn/runtime/flow/TestBlobStore.java diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/FnFlowsFunction.java b/flow-runtime/src/test/java/com/fnproject/fn/testing/flowtestfns/FnFlowsFunction.java similarity index 83% rename from runtime/src/test/java/com/fnproject/fn/runtime/testfns/FnFlowsFunction.java rename to flow-runtime/src/test/java/com/fnproject/fn/testing/flowtestfns/FnFlowsFunction.java index 59cc2de1..23ad312d 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/FnFlowsFunction.java +++ b/flow-runtime/src/test/java/com/fnproject/fn/testing/flowtestfns/FnFlowsFunction.java @@ -1,10 +1,13 @@ -package com.fnproject.fn.runtime.testfns; +package com.fnproject.fn.testing.flowtestfns; +import com.fnproject.fn.api.FnFeature; import com.fnproject.fn.api.flow.Flow; import com.fnproject.fn.api.flow.Flows; +import com.fnproject.fn.runtime.flow.FlowFeature; import java.io.Serializable; +@FnFeature(FlowFeature.class) public class FnFlowsFunction implements Serializable { public static void usingFlows() { diff --git a/flow-testing/pom.xml b/flow-testing/pom.xml new file mode 100644 index 00000000..03d2d9a3 --- /dev/null +++ b/flow-testing/pom.xml @@ -0,0 +1,78 @@ + + + + fdk + com.fnproject.fn + 1.0.0-SNAPSHOT + + 4.0.0 + + flow-testing + + + com.fnproject.fn + testing-junit4 + + + com.fnproject.fn + flow-api + + + com.fnproject.fn + flow-runtime + + + com.fnproject.fn + runtime + + + junit + junit + compile + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flow-testing/src/main/java/com/fnproject/fn/testing/flow/FlowTesting.java b/flow-testing/src/main/java/com/fnproject/fn/testing/flow/FlowTesting.java new file mode 100644 index 00000000..c8dfa8d2 --- /dev/null +++ b/flow-testing/src/main/java/com/fnproject/fn/testing/flow/FlowTesting.java @@ -0,0 +1,237 @@ +package com.fnproject.fn.testing.flow; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.flow.*; +import com.fnproject.fn.runtime.flow.*; +import com.fnproject.fn.testing.*; + +import java.io.PrintStream; +import java.lang.reflect.InvocationTargetException; +import java.util.*; +import java.util.concurrent.CompletableFuture; + +/** + * FlowTesting allows you to test Fn Flow functions by emulating the Fn Flow completer in a testing environment. + * + *

+ * * Created on 07/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class FlowTesting implements FnTestingRuleFeature { + private Map functionStubs = new HashMap<>(); + private static InMemCompleter completer = null; + private final FnTestingRule rule; + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private FlowTesting(FnTestingRule rule) { + + this.rule = rule; + rule.addSharedClass(InMemCompleter.CompleterInvokeClient.class); + rule.addSharedClass(BlobStoreClient.class); + rule.addSharedClass(BlobResponse.class); + rule.addSharedClass(CompleterClientFactory.class); + rule.addSharedClass(CompleterClient.class); + rule.addSharedClass(CompletionId.class); + rule.addSharedClass(FlowId.class); + rule.addSharedClass(Flow.FlowState.class); + rule.addSharedClass(CodeLocation.class); + rule.addSharedClass(HttpMethod.class); + rule.addSharedClass(com.fnproject.fn.api.flow.HttpRequest.class); + rule.addSharedClass(com.fnproject.fn.api.flow.HttpResponse.class); + rule.addSharedClass(FlowCompletionException.class); + rule.addSharedClass(FunctionInvocationException.class); + rule.addSharedClass(PlatformException.class); + rule.addFeature(this); + } + + /** + * Create a Flow + * + * @param rule + * @return + */ + public static FlowTesting create(FnTestingRule rule) { + Objects.requireNonNull(rule, "rule"); + return new FlowTesting(rule); + } + + @Override + public void prepareTest(ClassLoader functionClassLoader, PrintStream stderr, String cls, String method) { + InMemCompleter.CompleterInvokeClient client = new TestRuleCompleterInvokeClient(functionClassLoader, stderr, cls, method); + InMemCompleter.FnInvokeClient fnInvokeClient = new TestRuleFnInvokeClient(); + + // The following must be a static: otherwise the factory (the lambda) will not be serializable. + completer = new InMemCompleter(client, fnInvokeClient); + + } + + @Override + public void prepareFunctionClassLoader(FnTestingClassLoader cl) { + setCompleterClient(cl, completer); + } + + @Override + public void afterTestComplete() { + completer.awaitTermination(); + } + + + private class TestRuleCompleterInvokeClient implements InMemCompleter.CompleterInvokeClient { + private final ClassLoader functionClassLoader; + private final PrintStream oldSystemErr; + private final String cls; + private final String method; + private final Set pool = new HashSet<>(); + + + private TestRuleCompleterInvokeClient(ClassLoader functionClassLoader, PrintStream oldSystemErr, String cls, String method) { + this.functionClassLoader = functionClassLoader; + this.oldSystemErr = oldSystemErr; + this.cls = cls; + this.method = method; + } + + + @Override + public APIModel.CompletionResult invokeStage(String fnId, FlowId flowId, CompletionId stageId, APIModel.Blob closure, List input) { + // Construct a new ClassLoader hierarchy with a copy of the statics embedded in the runtime. + // Initialise it appropriately. + FnTestingClassLoader fcl = new FnTestingClassLoader(functionClassLoader, rule.getSharedPrefixes()); + + + setCompleterClient(fcl, completer); + + + APIModel.InvokeStageRequest request = new APIModel.InvokeStageRequest(); + request.stageId = stageId.getId(); + request.flowId = flowId.getId(); + request.closure = closure; + request.args = input; + + String inputBody = null; + try { + inputBody = objectMapper.writeValueAsString(request); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Invalid request"); + } + + // oldSystemErr.println("Body\n" + new String(inputBody)); + + InputEvent inputEvent = new FnHttpEventBuilder() + .withBody(inputBody) + .withHeader("Content-Type", "application/json") + .withHeader(FlowContinuationInvoker.FLOW_ID_HEADER, flowId.getId()).buildEvent(); + + Map mutableEnv = new HashMap<>(); + PrintStream functionErr = new PrintStream(oldSystemErr); + + // Do we want to capture IO from continuations on the main log stream? + // System.setOut(functionErr); + // System.setErr(functionErr); + + mutableEnv.putAll(rule.getConfig()); + mutableEnv.putAll(rule.getEventEnv()); + mutableEnv.put("FN_FORMAT", "http-stream"); + List output = new ArrayList<>(); + + + fcl.run( + mutableEnv, + new FnTestingRule.TestCodec(Collections.singletonList(inputEvent), output), + functionErr, + cls + "::" + method); + + FnResult out = output.get(0); + + APIModel.CompletionResult r; + try { + + APIModel.InvokeStageResponse response = objectMapper.readValue(out.getBodyAsBytes(), APIModel.InvokeStageResponse.class); + r = response.result; + + } catch (Exception e) { + oldSystemErr.println("Err\n" + e); + e.printStackTrace(oldSystemErr); + r = APIModel.CompletionResult.failure(APIModel.ErrorDatum.newError(APIModel.ErrorType.UnknownError, "Error reading fn Response:" + e.getMessage())); + } + + if (!r.successful) { + throw new ResultException(r.result); + } + return r; + + } + } + + private void setCompleterClient(FnTestingClassLoader cl, CompleterClientFactory completerClientFactory) { + try { + Class completerGlobals = cl.loadClass(FlowRuntimeGlobals.class.getName()); + completerGlobals.getMethod("setCompleterClientFactory", CompleterClientFactory.class).invoke(completerGlobals, completerClientFactory); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | ClassNotFoundException | IllegalArgumentException e) { + throw new RuntimeException("Something broke in the reflective classloader", e); + } + } + + private interface FnFunctionStub { + com.fnproject.fn.api.flow.HttpResponse stubFunction(HttpMethod method, Headers headers, byte[] body); + } + + + public FnFunctionStubBuilder givenFn(String id) { + return new FnFunctionStubBuilder() { + @Override + public FlowTesting withResult(byte[] result) { + return withAction((body) -> result); + } + + @Override + public FlowTesting withFunctionError() { + return withAction((body) -> { + throw new FunctionError("simulated by testing platform"); + }); + } + + @Override + public FlowTesting withPlatformError() { + return withAction((body) -> { + throw new PlatformError("simulated by testing platform"); + }); + } + + @Override + public FlowTesting withAction(ExternalFunctionAction f) { + functionStubs.put(id, (HttpMethod method, Headers headers, byte[] body) -> { + try { + return new DefaultHttpResponse(200, Headers.emptyHeaders(), f.apply(body)); + } catch (FunctionError functionError) { + return new DefaultHttpResponse(500, Headers.emptyHeaders(), functionError.getMessage().getBytes()); + } catch (PlatformError platformError) { + throw new RuntimeException("Platform Error"); + } + }); + return FlowTesting.this; + } + }; + } + + private class TestRuleFnInvokeClient implements InMemCompleter.FnInvokeClient { + @Override + public CompletableFuture invokeFunction(String fnId, HttpMethod method, Headers headers, byte[] data) { + FnFunctionStub stub = functionStubs.computeIfAbsent(fnId, (k) -> { + throw new IllegalStateException("Function was invoked that had no definition: " + k + " defined functions are " + String.join(",",functionStubs.keySet())); + }); + + try { + return CompletableFuture.completedFuture(stub.stubFunction(method, headers, data)); + } catch (Exception e) { + CompletableFuture respFuture = new CompletableFuture<>(); + respFuture.completeExceptionally(e); + return respFuture; + } + } + } +} diff --git a/testing/src/main/java/com/fnproject/fn/testing/FnFunctionStubBuilder.java b/flow-testing/src/main/java/com/fnproject/fn/testing/flow/FnFunctionStubBuilder.java similarity index 75% rename from testing/src/main/java/com/fnproject/fn/testing/FnFunctionStubBuilder.java rename to flow-testing/src/main/java/com/fnproject/fn/testing/flow/FnFunctionStubBuilder.java index 9ed01e45..c694be1f 100644 --- a/testing/src/main/java/com/fnproject/fn/testing/FnFunctionStubBuilder.java +++ b/flow-testing/src/main/java/com/fnproject/fn/testing/flow/FnFunctionStubBuilder.java @@ -1,32 +1,35 @@ -package com.fnproject.fn.testing; +package com.fnproject.fn.testing.flow; + +import com.fnproject.fn.testing.FunctionError; +import com.fnproject.fn.testing.PlatformError; /** * A builder for constructing stub external functions */ -public interface FnFunctionStubBuilder { +public interface FnFunctionStubBuilder { /** * Consume the builder and stub the function to return the provided byte array * * @param result A byte array returned by the function - * @return The original testing rule (usually {@link FnTestingRule}. The builder is consumed. + * @return The original testing rule (usually {@link FlowTesting}. The builder is consumed. */ - FnTestingRule withResult(byte[] result); + T withResult(byte[] result); /** * Consume the builder and stub the function to throw an error when it is invoked: this simulates a failure of the * called function, e.g. if the external function threw an exception. * - * @return The original testing rule (usually {@link FnTestingRule}. The builder is consumed. + * @return The original testing rule (usually {@link FlowTesting}. The builder is consumed. */ - FnTestingRule withFunctionError(); + T withFunctionError(); /** * Consume the builder and stub the function to throw a platform error, this simulates a failure of the Fn Flow * completions platform, and not any error of the user code. * - * @return The original testing rule (usually {@link FnTestingRule}. The builder is consumed. + * @return The original testing rule (usually {@link FlowTesting}. The builder is consumed. */ - FnTestingRule withPlatformError(); + T withPlatformError(); /** * Consume the builder and stub the function to perform some action; the action is an implementation of the @@ -37,9 +40,9 @@ public interface FnFunctionStubBuilder { * external state is accessed, a synchronization mechanism should be used. * * @param f an action to apply when this function is invoked - * @return The original testing rule (usually {@link FnTestingRule}. The builder is consumed. + * @return The original testing rule (usually {@link FlowTesting}. The builder is consumed. */ - FnTestingRule withAction(ExternalFunctionAction f); + T withAction(ExternalFunctionAction f); /** * Represents the calling interface of an external function. It takes a byte[] as input, diff --git a/testing/src/main/java/com/fnproject/fn/testing/InMemCompleter.java b/flow-testing/src/main/java/com/fnproject/fn/testing/flow/InMemCompleter.java similarity index 94% rename from testing/src/main/java/com/fnproject/fn/testing/InMemCompleter.java rename to flow-testing/src/main/java/com/fnproject/fn/testing/flow/InMemCompleter.java index 010b4fa1..c2c93649 100644 --- a/testing/src/main/java/com/fnproject/fn/testing/InMemCompleter.java +++ b/flow-testing/src/main/java/com/fnproject/fn/testing/flow/InMemCompleter.java @@ -1,4 +1,4 @@ -package com.fnproject.fn.testing; +package com.fnproject.fn.testing.flow; import com.fnproject.fn.api.Headers; import com.fnproject.fn.api.flow.*; @@ -570,37 +570,44 @@ private Stage addDelayStage(long delay) { private Stage addInvokeFunction(String functionId, HttpMethod method, Headers headers, byte[] data) { return addStage(new Stage(CompletableFuture.completedFuture(Collections.emptyList()), - (n, in) -> in.thenComposeAsync((ignored) -> { + (n, in) -> { + return in.thenComposeAsync((ignored) -> { - CompletionStage respFuture = fnInvokeClient.invokeFunction(functionId, method, headers, data); - return respFuture.thenApply((res) -> { - APIModel.HTTPResp apiResp = new APIModel.HTTPResp(); - apiResp.headers = res.getHeaders().getAll().entrySet() - .stream().map(e -> APIModel.HTTPHeader.create(e.getKey(), e.getValue())).collect(Collectors.toList()); + CompletionStage respFuture = fnInvokeClient.invokeFunction(functionId, method, headers, data); + return respFuture.thenApply((res) -> { + APIModel.HTTPResp apiResp = new APIModel.HTTPResp(); + List callHeaders = new ArrayList<>(); - BlobResponse blobResponse = writeBlob(flowId.getId(), res.getBodyAsBytes(), res.getHeaders().get("Content-type").orElse("application/octet-stream")); + for (Map.Entry> e : res.getHeaders().asMap().entrySet()) { + for (String v : e.getValue()) { + callHeaders.add(APIModel.HTTPHeader.create(e.getKey(), v)); + } + } + apiResp.headers = callHeaders; + BlobResponse blobResponse = writeBlob(flowId.getId(), res.getBodyAsBytes(), res.getHeaders().get("Content-type").orElse("application/octet-stream")); - apiResp.body = APIModel.Blob.fromBlobResponse(blobResponse); - apiResp.statusCode = res.getStatusCode(); + apiResp.body = APIModel.Blob.fromBlobResponse(blobResponse); + apiResp.statusCode = res.getStatusCode(); - APIModel.HTTPRespDatum datum = APIModel.HTTPRespDatum.create(apiResp); + APIModel.HTTPRespDatum datum = APIModel.HTTPRespDatum.create(apiResp); - if (apiResp.statusCode >= 200 && apiResp.statusCode < 400) { - return APIModel.CompletionResult.success(datum); - } else { - throw new ResultException(datum); - } + if (apiResp.statusCode >= 200 && apiResp.statusCode < 400) { + return APIModel.CompletionResult.success(datum); + } else { + throw new ResultException(datum); + } - }).exceptionally(e->{ - if (e.getCause() instanceof ResultException){ - throw (ResultException)e.getCause(); - }else{ - throw new ResultException(APIModel.ErrorDatum.newError(APIModel.ErrorType.FunctionInvokeFailed,e.getMessage())); - } - }); + }).exceptionally(e -> { + if (e.getCause() instanceof ResultException) { + throw (ResultException) e.getCause(); + } else { + throw new ResultException(APIModel.ErrorDatum.newError(APIModel.ErrorType.FunctionInvokeFailed, e.getMessage())); + } + }); - }, faasExecutor) + }, faasExecutor); + } )); } diff --git a/testing/src/main/java/com/fnproject/fn/testing/ResultException.java b/flow-testing/src/main/java/com/fnproject/fn/testing/flow/ResultException.java similarity index 89% rename from testing/src/main/java/com/fnproject/fn/testing/ResultException.java rename to flow-testing/src/main/java/com/fnproject/fn/testing/flow/ResultException.java index e2082b3c..be910af7 100644 --- a/testing/src/main/java/com/fnproject/fn/testing/ResultException.java +++ b/flow-testing/src/main/java/com/fnproject/fn/testing/flow/ResultException.java @@ -1,4 +1,4 @@ -package com.fnproject.fn.testing; +package com.fnproject.fn.testing.flow; import com.fnproject.fn.runtime.flow.APIModel; diff --git a/testing/src/test/java/com/fnproject/fn/testing/FnTestingRuleFlowsTest.java b/flow-testing/src/test/java/com/fnproject/fn/testing/flow/FnTestingRuleFlowsTest.java similarity index 74% rename from testing/src/test/java/com/fnproject/fn/testing/FnTestingRuleFlowsTest.java rename to flow-testing/src/test/java/com/fnproject/fn/testing/flow/FnTestingRuleFlowsTest.java index 00849159..f2e6a03b 100644 --- a/testing/src/test/java/com/fnproject/fn/testing/FnTestingRuleFlowsTest.java +++ b/flow-testing/src/test/java/com/fnproject/fn/testing/flow/FnTestingRuleFlowsTest.java @@ -1,9 +1,14 @@ -package com.fnproject.fn.testing; +package com.fnproject.fn.testing.flow; -import com.fnproject.fn.api.Headers; -import com.fnproject.fn.api.RuntimeContext; +import com.fnproject.fn.api.*; import com.fnproject.fn.api.flow.*; -import org.junit.*; +import com.fnproject.fn.runtime.flow.FlowFeature; +import com.fnproject.fn.testing.FnTestingRule; +import org.assertj.core.api.Assertions; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; import java.io.Serializable; import java.util.Arrays; @@ -11,14 +16,15 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; -import static org.assertj.core.api.Assertions.assertThat; public class FnTestingRuleFlowsTest { - private static final int HTTP_OK = 200; @Rule public FnTestingRule fn = FnTestingRule.createDefault(); + private FlowTesting flow = FlowTesting.create(fn); + + @FnFeature(FlowFeature.class) public static class Loop { public static int COUNT = 5; @@ -75,11 +81,10 @@ public void setup() { @Test public void completedValue() { fn.givenEvent().enqueue(); - fn.thenRun(TestFn.class, "completedValue"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(200); - assertThat(result).isEqualTo(Result.CompletedValue); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.CompletedValue); } @Test @@ -88,8 +93,8 @@ public void supply() { fn.thenRun(TestFn.class, "supply"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.Supply); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.Supply); } @Test @@ -98,8 +103,8 @@ public void allOf() { fn.thenRun(TestFn.class, "allOf"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.AllOf); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.AllOf); } @@ -109,8 +114,8 @@ public void anyOf() { fn.thenRun(TestFn.class, "anyOf"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.AnyOf); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.AnyOf); } @Test() @@ -121,28 +126,28 @@ public void nestedThenCompose() { fn.thenRun(Loop.class, "repeat"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(fn.getOnlyResult().getBodyAsString()) - .isEqualTo(String.join("", Collections.nCopies(Loop.COUNT, "hello world"))); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(fn.getOnlyResult().getBodyAsString()) + .isEqualTo(String.join("", Collections.nCopies(Loop.COUNT, "hello world"))); } @Test public void invokeFunctionWithResult() { fn.givenEvent().enqueue(); - fn.givenFn("user/echo") + flow.givenFn("user/echo") .withResult(Result.InvokeFunctionFixed.name().getBytes()); fn.thenRun(TestFn.class, "invokeFunctionEcho"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.InvokeFunctionFixed); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.InvokeFunctionFixed); } @Test public void invokeJsonFunction() { fn.givenEvent().enqueue(); - fn.givenFn("user/json") + flow.givenFn("user/json") .withAction((ign) -> { if (new String(ign).equals("{\"foo\":\"bar\"}")) { return "{\"foo\":\"baz\"}".getBytes(); @@ -153,20 +158,20 @@ public void invokeJsonFunction() { fn.thenRun(TestFn.class, "invokeJsonFunction"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(count).isEqualTo(1); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(count).isEqualTo(1); } @Test public void invokeFunctionWithFunctionError() { fn.givenEvent().enqueue(); - fn.givenFn("user/error") + flow.givenFn("user/error") .withFunctionError(); fn.thenRun(TestFn.class, "invokeFunctionError"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.Exceptionally); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.Exceptionally); isInstanceOfAny(exception, FunctionInvocationException.class); } @@ -176,35 +181,35 @@ public void invokeFunctionWithFailedFuture() { fn.thenRun(TestFn.class, "failedFuture"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.Exceptionally); - assertThat(exception).isInstanceOf(RuntimeException.class); - assertThat(exception).hasMessage("failedFuture"); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.Exceptionally); + Assertions.assertThat(exception).isInstanceOf(RuntimeException.class); + Assertions.assertThat(exception).hasMessage("failedFuture"); } @Test public void invokeFunctionWithPlatformError() { fn.givenEvent().enqueue(); - fn.givenFn("user/error") + flow.givenFn("user/error") .withPlatformError(); fn.thenRun(TestFn.class, "invokeFunctionError"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.Exceptionally); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.Exceptionally); isInstanceOfAny(exception, PlatformException.class); } @Test public void invokeFunctionWithAction() { fn.givenEvent().enqueue(); - fn.givenFn("user/echo") + flow.givenFn("user/echo") .withAction((p) -> p); fn.thenRun(TestFn.class, "invokeFunctionEcho"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.InvokeFunctionEcho); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.InvokeFunctionEcho); } @Test @@ -213,8 +218,8 @@ public void completingExceptionally() { fn.thenRun(TestFn.class, "completeExceptionally"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.Exceptionally); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.Exceptionally); } @Test @@ -223,8 +228,8 @@ public void completingExceptionallyWhenErrorIsThrownEarlyInGraph() { fn.thenRun(TestFn.class, "completeExceptionallyEarly"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.Exceptionally); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.Exceptionally); } @Test @@ -233,9 +238,9 @@ public void cancelledFutureCompletesExceptionally() { fn.thenRun(TestFn.class, "cancelFuture"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.Exceptionally); - assertThat(exception).isInstanceOf(CancellationException.class); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.Exceptionally); + Assertions.assertThat(exception).isInstanceOf(CancellationException.class); } @Test @@ -244,10 +249,10 @@ public void completeFutureExceptionallyWithCustomException() { fn.thenRun(TestFn.class, "completeFutureExceptionally"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.Exceptionally); - assertThat(exception).isInstanceOf(RuntimeException.class); - assertThat(exception.getMessage()).isEqualTo("Custom exception"); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.Exceptionally); + Assertions.assertThat(exception).isInstanceOf(RuntimeException.class); + Assertions.assertThat(exception.getMessage()).isEqualTo("Custom exception"); } @Test @@ -256,8 +261,8 @@ public void completedFutureCompletesNormally() { fn.thenRun(TestFn.class, "completeFuture"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.CompletedValue); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.CompletedValue); } @Test @@ -266,8 +271,8 @@ public void uncompletedFutureCanBeCompleted() { fn.thenRun(TestFn.class, "createFlowFuture"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.CompletedValue); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.CompletedValue); } @Test @@ -277,8 +282,8 @@ public void shouldLogMessagesToStdErrToPlatformStdErr() { fn.thenRun(TestFn.class, "logToStdErrInContinuation"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(fn.getStdErrAsString()).contains("TestFn logging: 1"); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(fn.getStdErrAsString()).contains("TestFn logging: 1"); } @Test @@ -288,8 +293,8 @@ public void shouldLogMessagesToStdOutToPlatformStdErr() { fn.thenRun(TestFn.class, "logToStdOutInContinuation"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(fn.getStdErrAsString()).contains("TestFn logging: 1"); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(fn.getStdErrAsString()).contains("TestFn logging: 1"); } @@ -299,9 +304,9 @@ public void shouldHandleMultipleEventsForFunctionWithoutInput() { fn.thenRun(TestFn.class, "anyOf"); - assertThat(fn.getResults().get(0).getStatus()).isEqualTo(HTTP_OK); - assertThat(fn.getResults().get(1).getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.AnyOf); + Assertions.assertThat(fn.getResults().get(0).getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(fn.getResults().get(1).getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.AnyOf); } @Test @@ -310,8 +315,8 @@ public void exceptionallyComposeHandle() { fn.thenRun(TestFn.class, "exceptionallyComposeHandle"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(count).isEqualTo(2); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(count).isEqualTo(2); } @@ -321,8 +326,8 @@ public void exceptionallyComposePassThru() { fn.givenEvent().enqueue(); fn.thenRun(TestFn.class, "exceptionallyComposePassThru"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(count).isEqualTo(1); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(count).isEqualTo(1); } @@ -331,8 +336,8 @@ public void exceptionallyComposeThrowsError() { fn.givenEvent().enqueue(); fn.thenRun(TestFn.class, "exceptionallyComposeThrowsError"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(count).isEqualTo(1); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(count).isEqualTo(1); } @@ -346,8 +351,8 @@ public void shouldHandleMultipleEventsForFunctionWithInput() { fn.thenRun(Loop.class, "repeat"); for (int i = 0; i < bodies.length; i++) { - assertThat(fn.getResults().get(i).getBodyAsString()) - .isEqualTo(String.join("", Collections.nCopies(Loop.COUNT, bodies[i]))); + Assertions.assertThat(fn.getResults().get(i).getBodyAsString()) + .isEqualTo(String.join("", Collections.nCopies(Loop.COUNT, bodies[i]))); } } @@ -356,15 +361,15 @@ public void shouldRunShutdownHooksInTest() { fn.givenEvent().enqueue(); fn.thenRun(TestFn.class, "terminationHooks"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(200); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); - assertThat(result).isEqualTo(Result.TerminationHookRun); + Assertions.assertThat(result).isEqualTo(Result.TerminationHookRun); } // Due to the alien nature of the stored exception, we supply a helper to assert isInstanceOfAny void isInstanceOfAny(Object o, Class... cs) { - assertThat(o).isNotNull(); + Assertions.assertThat(o).isNotNull(); ClassLoader loader = o.getClass().getClassLoader(); for (Class c : cs) { try { @@ -377,6 +382,7 @@ void isInstanceOfAny(Object o, Class... cs) { Assert.fail("Object " + o + "is not an instance of any of " + Arrays.toString(cs)); } + @FnFeature(FlowFeature.class) public static class TestFn { static Integer TO_ADD = null; @@ -384,6 +390,7 @@ public TestFn(RuntimeContext ctx) { TO_ADD = Integer.parseInt(ctx.getConfigurationByKey("ADD").orElse("-1")); } + public void completedValue() { Flows.currentFlow() .completedValue(Result.CompletedValue).thenAccept((r) -> result = r); @@ -670,9 +677,9 @@ static void reset() { // These members are external to the class under test so as to be visible from the unit tests. // They must be public, since the TestFn class will be instantiated under a separate ClassLoader; // therefore we need broader access than might be anticipated. - public static Result result = null; - public static Throwable exception = null; - public static Integer staticConfig = null; - public static Integer count = 0; + public static volatile Result result = null; + public static volatile Throwable exception = null; + public static volatile Integer staticConfig = null; + public static volatile Integer count = 0; } diff --git a/flow-testing/src/test/java/com/fnproject/fn/testing/flow/IntegrationTest.java b/flow-testing/src/test/java/com/fnproject/fn/testing/flow/IntegrationTest.java new file mode 100644 index 00000000..61e86171 --- /dev/null +++ b/flow-testing/src/test/java/com/fnproject/fn/testing/flow/IntegrationTest.java @@ -0,0 +1,42 @@ +package com.fnproject.fn.testing.flow; + +import com.fnproject.fn.testing.FnTestingRule; +import com.fnproject.fn.testing.FunctionError; +import com.fnproject.fn.testing.flowtestfns.ExerciseEverything; +import org.assertj.core.api.Assertions; +import org.junit.Rule; +import org.junit.Test; + +public class IntegrationTest { + + @Rule + public FnTestingRule fn = FnTestingRule.createDefault(); + + public FlowTesting flow = FlowTesting.create(fn); + + @Test + public void runIntegrationTests() { + + flow.givenFn("testFunctionNonExistant") + .withFunctionError() + + .givenFn("testFunction") + .withAction((body) -> { + if (new String(body).equals("PASS")) { + return "okay".getBytes(); + } else { + throw new FunctionError("failed as demanded"); + } + }); + + fn + .givenEvent() + .withBody("") // or "1,5,6,32" to select a set of tests individually + .enqueue() + + .thenRun(ExerciseEverything.class, "handleRequest"); + + Assertions.assertThat(fn.getResults().get(0).getBodyAsString()) + .endsWith("Everything worked\n"); + } +} diff --git a/testing/src/test/java/com/fnproject/fn/testing/MultipleEventsTest.java b/flow-testing/src/test/java/com/fnproject/fn/testing/flow/MultipleEventsTest.java similarity index 90% rename from testing/src/test/java/com/fnproject/fn/testing/MultipleEventsTest.java rename to flow-testing/src/test/java/com/fnproject/fn/testing/flow/MultipleEventsTest.java index 4622b44b..a6b23eec 100644 --- a/testing/src/test/java/com/fnproject/fn/testing/MultipleEventsTest.java +++ b/flow-testing/src/test/java/com/fnproject/fn/testing/flow/MultipleEventsTest.java @@ -1,22 +1,27 @@ -package com.fnproject.fn.testing; +package com.fnproject.fn.testing.flow; +import com.fnproject.fn.api.FnFeature; import com.fnproject.fn.api.flow.Flow; import com.fnproject.fn.api.flow.Flows; +import com.fnproject.fn.runtime.flow.FlowFeature; +import com.fnproject.fn.testing.FnTestingRule; +import org.assertj.core.api.Assertions; import org.junit.Rule; import org.junit.Test; import java.util.concurrent.Semaphore; -import static org.assertj.core.api.Assertions.assertThat; - public class MultipleEventsTest { @Rule public FnTestingRule fn = FnTestingRule.createDefault(); + public FlowTesting flow = FlowTesting.create(fn); + public static Semaphore oneGo = null; public static Semaphore twoGo = null; public static boolean success = false; + @FnFeature(FlowFeature.class) public static class TestFn { public void handleRequest(String s) { switch (s) { @@ -105,7 +110,7 @@ public void OverlappingFlowInvocationsShouldWork() { fn.thenRun(TestFn.class, "handleRequest"); - assertThat(success).isTrue(); + Assertions.assertThat(success).isTrue(); } } diff --git a/testing/src/test/java/com/fnproject/fn/testing/WhenCompleteTest.java b/flow-testing/src/test/java/com/fnproject/fn/testing/flow/WhenCompleteTest.java similarity index 70% rename from testing/src/test/java/com/fnproject/fn/testing/WhenCompleteTest.java rename to flow-testing/src/test/java/com/fnproject/fn/testing/flow/WhenCompleteTest.java index 7e44c78c..d219061f 100644 --- a/testing/src/test/java/com/fnproject/fn/testing/WhenCompleteTest.java +++ b/flow-testing/src/test/java/com/fnproject/fn/testing/flow/WhenCompleteTest.java @@ -1,19 +1,24 @@ -package com.fnproject.fn.testing; +package com.fnproject.fn.testing.flow; +import com.fnproject.fn.api.FnFeature; import com.fnproject.fn.api.flow.Flows; +import com.fnproject.fn.runtime.flow.FlowFeature; +import com.fnproject.fn.testing.FnTestingRule; +import org.assertj.core.api.Assertions; import org.junit.Rule; import org.junit.Test; import java.util.concurrent.atomic.AtomicInteger; -import static org.assertj.core.api.Assertions.assertThat; - public class WhenCompleteTest { @Rule public FnTestingRule fn = FnTestingRule.createDefault(); + private FlowTesting flow = FlowTesting.create(fn); public static AtomicInteger cas = new AtomicInteger(0); + + @FnFeature(FlowFeature.class) public static class TestFn { public void handleRequest() { Flows.currentFlow().completedValue(1) @@ -32,7 +37,7 @@ public void OverlappingFlowInvocationsShouldWork() { fn.thenRun(TestFn.class, "handleRequest"); - assertThat(cas.get()).isEqualTo(2); + Assertions.assertThat(cas.get()).isEqualTo(2); } } diff --git a/testing/src/test/java/com/fnproject/fn/testing/ExerciseEverything.java b/flow-testing/src/test/java/com/fnproject/fn/testing/flowtestfns/ExerciseEverything.java similarity index 95% rename from testing/src/test/java/com/fnproject/fn/testing/ExerciseEverything.java rename to flow-testing/src/test/java/com/fnproject/fn/testing/flowtestfns/ExerciseEverything.java index 1d4ce58e..8807dacc 100644 --- a/testing/src/test/java/com/fnproject/fn/testing/ExerciseEverything.java +++ b/flow-testing/src/test/java/com/fnproject/fn/testing/flowtestfns/ExerciseEverything.java @@ -1,9 +1,8 @@ -package com.fnproject.fn.testing; +package com.fnproject.fn.testing.flowtestfns; -import com.fnproject.fn.api.Headers; -import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.*; import com.fnproject.fn.api.flow.*; -import com.fnproject.fn.runtime.flow.HttpClient; +import com.fnproject.fn.runtime.flow.FlowFeature; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.TeeOutputStream; @@ -19,8 +18,10 @@ import java.util.*; import java.util.stream.Collectors; +@FnFeature(FlowFeature.class) public class ExerciseEverything { + private boolean okay = true; private ByteArrayOutputStream bos = new ByteArrayOutputStream(); private PrintStream out = new PrintStream(new TeeOutputStream(System.err, bos)); @@ -131,24 +132,22 @@ public FlowFuture catchBubbledException(Flow fl) { @Test(11) @Test.Catch({FlowCompletionException.class, FunctionInvocationException.class}) public FlowFuture nonexistentExternalEvaluation(Flow fl) { - return fl.invokeFunction("nonexistent/nonexistent", HttpMethod.POST, Headers.emptyHeaders(), new byte[0]); + return fl.invokeFunction("testFunctionNonExistant", HttpMethod.POST, Headers.emptyHeaders(), new byte[0]); } @Test(12) @Test.Expect("okay") - public FlowFuture checkPassingExternalInvocation(Flow fl) { - return fl.invokeFunction(inputEvent.getAppName() + inputEvent.getRoute(), HttpMethod.POST, Headers.emptyHeaders(), "PASS".getBytes()) - .thenApply((resp) -> { - return resp.getStatusCode() != 200 ? "failure" : new String(resp.getBodyAsBytes()); - }); + public FlowFuture checkPassingInvocation(Flow fl) { + return fl.invokeFunction("testFunction", HttpMethod.POST, Headers.emptyHeaders(), "PASS".getBytes()) + .thenApply((resp) -> resp.getStatusCode() != 200 ? "failure" : new String(resp.getBodyAsBytes())); } // There is currently no way for a hot function to signal failure in the Fn platform. // This test will only work in default mode. @Test(13) @Test.Catch({FlowCompletionException.class, FunctionInvocationException.class}) - public FlowFuture checkFailingExternalInvocation(Flow fl) { - return fl.invokeFunction(inputEvent.getAppName() + inputEvent.getRoute(), HttpMethod.POST, Headers.emptyHeaders(), "FAIL".getBytes()); + public FlowFuture checkFailingInvocation(Flow fl) { + return fl.invokeFunction("testFunction", HttpMethod.POST, Headers.emptyHeaders(), "FAIL".getBytes()); } // This original version captures the RT, which captures the factory, which is not serializable @@ -367,6 +366,11 @@ public FlowFuture exceptionallyComposePropagateError(Flow fl) throws IOE private int id; + private final RuntimeContext runtimeContext; + + public ExerciseEverything(RuntimeContext rtc){ + this.runtimeContext = rtc; + } void fail() { if (!failures.contains(id)) { failures.add(id); diff --git a/fn-spring-cloud-function/pom.xml b/fn-spring-cloud-function/pom.xml index 97130c81..9acec1ba 100644 --- a/fn-spring-cloud-function/pom.xml +++ b/fn-spring-cloud-function/pom.xml @@ -12,8 +12,7 @@ fn-spring-cloud-function - UTF-8 - 1.0.0.M1 + 1.0.0.RELEASE @@ -21,7 +20,6 @@ com.fnproject.fn api - ${project.version} @@ -33,47 +31,52 @@ io.projectreactor reactor-core - 3.0.7.RELEASE + 3.2.0.M3 net.jodah typetools - 0.5.0 com.fnproject.fn - testing - ${project.version} + testing-core + test + + + com.fnproject.fn + testing-junit4 test - org.mockito mockito-core - ${mockito.version} test org.assertj assertj-core - ${assertj-core.version} test junit junit - ${junit.version} + test + + + + commons-logging + commons-logging test com.github.stefanbirkner system-rules - 1.16.0 + 1.18.0 test diff --git a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionFeature.java b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionFeature.java new file mode 100644 index 00000000..87d51d4d --- /dev/null +++ b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionFeature.java @@ -0,0 +1,21 @@ +package com.fnproject.springframework.function; + +import com.fnproject.fn.api.FunctionInvoker; +import com.fnproject.fn.api.RuntimeContext; +import com.fnproject.fn.api.RuntimeFeature; + +/** + * + * The SpringCloudFunctionFeature enables a function to be run with a spring cloud function configuration + * + * Created on 10/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class SpringCloudFunctionFeature implements RuntimeFeature { + + @Override + public void initialize(RuntimeContext ctx) { + ctx.addInvoker(new SpringCloudFunctionInvoker(ctx.getMethod().getTargetClass()),FunctionInvoker.Phase.Call); + } +} diff --git a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionInvoker.java b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionInvoker.java index bca11b33..3519bc1e 100644 --- a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionInvoker.java +++ b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionInvoker.java @@ -1,11 +1,6 @@ package com.fnproject.springframework.function; -import com.fnproject.fn.api.FunctionInvoker; -import com.fnproject.fn.api.InputEvent; -import com.fnproject.fn.api.InvocationContext; -import com.fnproject.fn.api.MethodWrapper; -import com.fnproject.fn.api.OutputEvent; -import com.fnproject.fn.api.RuntimeContext; +import com.fnproject.fn.api.*; import com.fnproject.fn.api.exception.FunctionInputHandlingException; import com.fnproject.fn.api.exception.FunctionOutputHandlingException; import com.fnproject.springframework.function.functions.SpringCloudMethod; @@ -145,7 +140,7 @@ private Flux convertToFlux(Object[] params) { } @Override - public void close() throws IOException { + public void close() { applicationContext.close(); } } diff --git a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionLoader.java b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionLoader.java index 5090a595..e6a2209c 100644 --- a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionLoader.java +++ b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionLoader.java @@ -6,8 +6,8 @@ import com.fnproject.springframework.function.functions.SpringCloudMethod; import com.fnproject.springframework.function.functions.SpringCloudSupplier; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cloud.function.context.FunctionInspector; -import org.springframework.cloud.function.core.FunctionCatalog; +import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.cloud.function.context.catalog.FunctionInspector; import reactor.core.publisher.Flux; import java.util.function.Consumer; @@ -67,24 +67,24 @@ void loadFunction() { private void loadSpringCloudFunctionFromEnvVars() { String functionName = System.getenv(ENV_VAR_FUNCTION_NAME); if (functionName != null) { - function = this.catalog.lookupFunction(functionName); + function = this.catalog.lookup(Function.class, functionName); } String consumerName = System.getenv(ENV_VAR_CONSUMER_NAME); if (consumerName != null) { - consumer = this.catalog.lookupConsumer(consumerName); + consumer = this.catalog.lookup(Consumer.class, consumerName); } String supplierName = System.getenv(ENV_VAR_SUPPLIER_NAME); if (supplierName != null) { - supplier = this.catalog.lookupSupplier(supplierName); + supplier = this.catalog.lookup(Supplier.class, supplierName); } } private void loadSpringCloudFunctionFromDefaults() { - function = this.catalog.lookupFunction(DEFAULT_FUNCTION_BEAN); - consumer = this.catalog.lookupConsumer(DEFAULT_CONSUMER_BEAN); - supplier = this.catalog.lookupSupplier(DEFAULT_SUPPLIER_BEAN); + function = this.catalog.lookup(Function.class, DEFAULT_FUNCTION_BEAN); + consumer = this.catalog.lookup(Consumer.class, DEFAULT_CONSUMER_BEAN); + supplier = this.catalog.lookup(Supplier.class, DEFAULT_SUPPLIER_BEAN); } diff --git a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudConsumer.java b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudConsumer.java index bc702a16..720f58ae 100644 --- a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudConsumer.java +++ b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudConsumer.java @@ -2,7 +2,7 @@ import com.fnproject.fn.api.TypeWrapper; import com.fnproject.springframework.function.SimpleTypeWrapper; -import org.springframework.cloud.function.context.FunctionInspector; +import org.springframework.cloud.function.context.catalog.FunctionInspector; import reactor.core.publisher.Flux; import java.util.function.Consumer; diff --git a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudFunction.java b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudFunction.java index e7cc1bd5..dfbf40ea 100644 --- a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudFunction.java +++ b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudFunction.java @@ -1,9 +1,8 @@ package com.fnproject.springframework.function.functions; -import org.springframework.cloud.function.context.FunctionInspector; +import org.springframework.cloud.function.context.catalog.FunctionInspector; import reactor.core.publisher.Flux; -import java.util.function.Consumer; import java.util.function.Function; /** diff --git a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudMethod.java b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudMethod.java index 77f8bb08..e2daffcc 100644 --- a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudMethod.java +++ b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudMethod.java @@ -3,7 +3,7 @@ import com.fnproject.fn.api.MethodWrapper; import com.fnproject.fn.api.TypeWrapper; import com.fnproject.springframework.function.SimpleTypeWrapper; -import org.springframework.cloud.function.context.FunctionInspector; +import org.springframework.cloud.function.context.catalog.FunctionInspector; import reactor.core.publisher.Flux; import java.lang.reflect.Method; diff --git a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudSupplier.java b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudSupplier.java index 182d708b..52554b44 100644 --- a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudSupplier.java +++ b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudSupplier.java @@ -2,10 +2,9 @@ import com.fnproject.fn.api.TypeWrapper; import com.fnproject.springframework.function.SimpleTypeWrapper; -import org.springframework.cloud.function.context.FunctionInspector; +import org.springframework.cloud.function.context.catalog.FunctionInspector; import reactor.core.publisher.Flux; -import java.util.function.Consumer; import java.util.function.Supplier; /** diff --git a/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SimpleFunctionInspector.java b/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SimpleFunctionInspector.java index 22e301e9..9e52b6f1 100644 --- a/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SimpleFunctionInspector.java +++ b/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SimpleFunctionInspector.java @@ -1,13 +1,19 @@ package com.fnproject.springframework.function; import net.jodah.typetools.TypeResolver; -import org.springframework.cloud.function.context.FunctionInspector; +import org.springframework.cloud.function.context.FunctionRegistration; +import org.springframework.cloud.function.context.catalog.FunctionInspector; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; public class SimpleFunctionInspector implements FunctionInspector { + @Override + public FunctionRegistration getRegistration(Object function) { + return null; + } + @Override public boolean isMessage(Object function) { throw new IllegalStateException("Not implemented"); @@ -53,11 +59,6 @@ public Class getOutputWrapper(Object function) { throw new IllegalStateException("Not implemented"); } - @Override - public Object convert(Object function, String value) { - throw new IllegalStateException("Not implemented"); - } - @Override public String getName(Object function) { return function.toString(); diff --git a/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SpringCloudFunctionInvokerIntegrationTest.java b/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SpringCloudFunctionInvokerIntegrationTest.java index 131e77ef..eb859b2e 100644 --- a/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SpringCloudFunctionInvokerIntegrationTest.java +++ b/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SpringCloudFunctionInvokerIntegrationTest.java @@ -3,6 +3,7 @@ import com.fnproject.fn.testing.FnTestingRule; import com.fnproject.springframework.function.testfns.EmptyFunctionConfig; import com.fnproject.springframework.function.testfns.FunctionConfig; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.EnvironmentVariables; @@ -21,7 +22,7 @@ public class SpringCloudFunctionInvokerIntegrationTest { public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); @Test - public void shouldInvokeFunction() throws IOException { + public void shouldInvokeFunction() { fnRule.givenEvent().withBody("HELLO").enqueue(); fnRule.thenRun(FunctionConfig.class, "handleRequest"); @@ -30,18 +31,23 @@ public void shouldInvokeFunction() throws IOException { } @Test - public void shouldInvokeConsumer() throws IOException { + @Ignore("Consumer behaviour seems broken in this release of Spring Cloud Function") + // NB the problem is that FluxConsumer is not a subclass of j.u.f.Consumer, but _is_ + // a subclass of j.u.f.Function. + // Effectively a Consumer is treated as a Function which means when we lookup + // by env var name "consumer", we don't find a j.u.f.Consumer, so we fall back to the default + // behaviour which is to invoke the bean named "function" + public void shouldInvokeConsumer() { environmentVariables.set(SpringCloudFunctionLoader.ENV_VAR_CONSUMER_NAME, "consumer"); - String consumerInput = "consumer input"; - fnRule.givenEvent().withBody(consumerInput).enqueue(); + fnRule.givenEvent().withBody("consumer input").enqueue(); fnRule.thenRun(FunctionConfig.class, "handleRequest"); - assertThat(fnRule.getStdErrAsString()).contains(consumerInput); + assertThat(fnRule.getStdErrAsString()).contains("consumer input"); } @Test - public void shouldInvokeSupplier() throws IOException { + public void shouldInvokeSupplier() { environmentVariables.set(SpringCloudFunctionLoader.ENV_VAR_SUPPLIER_NAME, "supplier"); fnRule.givenEvent().enqueue(); @@ -59,7 +65,7 @@ public void shouldThrowFunctionLoadExceptionIfNoValidFunction() { int exitCode = fnRule.getLastExitCode(); - assertThat(exitCode).isEqualTo(2); + assertThat(exitCode).isEqualTo(1); assertThat(fnRule.getResults()).isEmpty(); // fails at init so no results. assertThat(fnRule.getStdErrAsString()).contains("No Spring Cloud Function found"); } @@ -72,7 +78,7 @@ public void noNPEifFunctionReturnsNull() { int exitCode = fnRule.getLastExitCode(); - assertThat(exitCode).isEqualTo(2); + assertThat(exitCode).isEqualTo(1); assertThat(fnRule.getResults()).isEmpty(); // fails at init so no results. assertThat(fnRule.getStdErrAsString()).contains("No Spring Cloud Function found"); } diff --git a/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SpringCloudFunctionLoaderTest.java b/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SpringCloudFunctionLoaderTest.java index 9ce1049c..6e2bbf02 100644 --- a/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SpringCloudFunctionLoaderTest.java +++ b/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SpringCloudFunctionLoaderTest.java @@ -8,7 +8,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.cloud.function.core.FunctionCatalog; +import org.springframework.cloud.function.context.catalog.InMemoryFunctionCatalog; import java.util.function.Consumer; import java.util.function.Function; @@ -24,8 +24,9 @@ public class SpringCloudFunctionLoaderTest { public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); private SpringCloudFunctionLoader loader; + @Mock - private FunctionCatalog catalog; + private InMemoryFunctionCatalog catalog; @Before public void setUp() { @@ -34,9 +35,7 @@ public void setUp() { } private void setUpCatalogToReturnNullForLookupByDefault() { - when(catalog.lookupFunction(any())).thenReturn(null); - when(catalog.lookupConsumer(any())).thenReturn(null); - when(catalog.lookupSupplier(any())).thenReturn(null); + when(catalog.lookup(any(), any())).thenReturn(null); } @Test @@ -49,7 +48,8 @@ public void shouldLoadFunctionBeanCalledFunction() { @Test public void shouldLoadConsumerBeanCalledConsumerIfFunctionNotAvailable() { - Consumer consumer = (x) -> {}; + Consumer consumer = (x) -> { + }; stubCatalogToReturnConsumer(consumer); assertThat(getDiscoveredFunction().getTargetClass()).isEqualTo(consumer.getClass()); @@ -80,7 +80,8 @@ public void shouldLoadUserSpecifiedSupplierInEnvVarOverDefaultFunction() { @Test public void shouldLoadUserSpecifiedConsumerInEnvVarOverDefaultFunction() { String beanName = "myConsumer"; - Consumer consumer = (x) -> {}; + Consumer consumer = (x) -> { + }; Function function = (x) -> x; setConsumerEnvVar(beanName); @@ -104,15 +105,15 @@ public void shouldLoadUserSpecifiedFunctionInEnvVarOverDefaultFunction() { } private void stubCatalogToReturnFunction(String beanName, Function function) { - when(catalog.lookupFunction(beanName)).thenReturn(function); + when(catalog.lookup(Function.class, beanName)).thenReturn(function); } private void stubCatalogToReturnConsumer(String beanName, Consumer consumer) { - when(catalog.lookupConsumer(beanName)).thenReturn(consumer); + when(catalog.lookup(Consumer.class, beanName)).thenReturn(consumer); } private void stubCatalogToReturnSupplier(String beanName, Supplier supplier) { - when(catalog.lookupSupplier(beanName)).thenReturn(supplier); + when(catalog.lookup(Supplier.class, beanName)).thenReturn(supplier); } private void stubCatalogToReturnSupplier(Supplier supplier) { diff --git a/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/testfns/EmptyFunctionConfig.java b/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/testfns/EmptyFunctionConfig.java index a6444bc2..0f650d22 100644 --- a/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/testfns/EmptyFunctionConfig.java +++ b/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/testfns/EmptyFunctionConfig.java @@ -1,24 +1,21 @@ package com.fnproject.springframework.function.testfns; import com.fnproject.fn.api.FnConfiguration; +import com.fnproject.fn.api.FnFeature; +import com.fnproject.fn.api.FunctionInvoker; import com.fnproject.fn.api.RuntimeContext; +import com.fnproject.springframework.function.SpringCloudFunctionFeature; import com.fnproject.springframework.function.SpringCloudFunctionInvoker; -import org.springframework.cloud.function.context.ContextFunctionCatalogAutoConfiguration; -import org.springframework.context.annotation.Bean; +import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - @Configuration @Import(ContextFunctionCatalogAutoConfiguration.class) +@FnFeature(SpringCloudFunctionFeature.class) public class EmptyFunctionConfig { - @FnConfiguration - public static void configure(RuntimeContext ctx) { - ctx.setInvoker(new SpringCloudFunctionInvoker(EmptyFunctionConfig.class)); + + public void handleRequest() { } - public void handleRequest() { } } diff --git a/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/testfns/FunctionConfig.java b/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/testfns/FunctionConfig.java index c5d4d21a..fb8dbeb7 100644 --- a/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/testfns/FunctionConfig.java +++ b/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/testfns/FunctionConfig.java @@ -1,9 +1,11 @@ package com.fnproject.springframework.function.testfns; import com.fnproject.fn.api.FnConfiguration; +import com.fnproject.fn.api.FnFeature; import com.fnproject.fn.api.RuntimeContext; +import com.fnproject.springframework.function.SpringCloudFunctionFeature; import com.fnproject.springframework.function.SpringCloudFunctionInvoker; -import org.springframework.cloud.function.context.ContextFunctionCatalogAutoConfiguration; +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; @@ -14,12 +16,11 @@ @Configuration @Import(ContextFunctionCatalogAutoConfiguration.class) +@FnFeature(SpringCloudFunctionFeature.class) public class FunctionConfig { - @FnConfiguration - public static void configure(RuntimeContext ctx) { - ctx.setInvoker(new SpringCloudFunctionInvoker(FunctionConfig.class)); + + public void handleRequest() { } - public void handleRequest() { } @Bean public Supplier supplier() { @@ -28,6 +29,7 @@ public Supplier supplier() { @Bean public Consumer consumer() { + System.out.println("LOADED"); return System.out::println; } @@ -35,13 +37,16 @@ public Consumer consumer() { public Function function() { return String::toLowerCase; } + @Bean public Function upperCaseFunction() { return String::toUpperCase; } @Bean - public String notAFunction() { return "NotAFunction"; } + public String notAFunction() { + return "NotAFunction"; + } // Empty entrypoint that isn't used but necessary for the EntryPoint. Our invoker ignores this and loads our own // function to invoke diff --git a/images/build-native/Dockerfile b/images/build-native/Dockerfile new file mode 100644 index 00000000..e50f84dc --- /dev/null +++ b/images/build-native/Dockerfile @@ -0,0 +1,45 @@ +FROM openjdk:8-jdk-slim as build +LABEL maintainer="tomas.zezula@oracle.com" + +RUN set -x \ + && apt-get -y update \ + && apt-get -y install gcc g++ git make python zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* + +ENV JVMCI_VERSION 19.2-b02 + +WORKDIR /build + +RUN set -x \ + && git clone https://github.com/graalvm/mx.git \ + && git clone https://github.com/graalvm/graal-jvmci-8.git \ + && git -C graal-jvmci-8 checkout jvmci-${JVMCI_VERSION} \ + && mx/mx --primary-suite graal-jvmci-8 --vm=server build -DFULL_DEBUG_SYMBOLS=0 \ + && mx/mx --primary-suite graal-jvmci-8 --vm=server -v vm -version \ + && mx/mx --primary-suite graal-jvmci-8 --vm=server -v unittest \ + && cp -r $(/build/mx/mx --primary-suite graal-jvmci-8 jdkhome) /build/jvmcijdk8 + +RUN git clone https://github.com/oracle/graal.git \ + && git -C graal checkout vm-19.2.0 +WORKDIR /build/graal/vm +RUN export JAVA_HOME=/build/jvmcijdk8 \ + && /build/mx/mx --dy /substratevm --force-bash-launchers=true --disable-polyglot --disable-libpolyglot build + +WORKDIR /build/graal/vm/latest_graalvm +RUN LONG_NAME=$(ls) \ + && SHORT_NAME=graalvm \ + && mv $LONG_NAME $SHORT_NAME + +FROM debian:stretch-slim as final +LABEL maintainer="tomas.zezula@oracle.com" + +RUN set -x \ + && apt-get -y update \ + && apt-get -y install gcc zlib1g-dev + +COPY --from=build /build/graal/vm/latest_graalvm/graalvm /usr/local/graalvm +COPY src/main/c/libfnunixsocket.so /function/runtime/lib/ + + +ENV GRAALVM_HOME=/usr/local/graalvm +WORKDIR /function diff --git a/images/build-native/README.md b/images/build-native/README.md new file mode 100644 index 00000000..11d3eb0a --- /dev/null +++ b/images/build-native/README.md @@ -0,0 +1,5 @@ +# Native Build image + +This rebuilds the substrate build image for native java functions - this build does not run by default on all builds + +To update the build image, make a change to `native.version` (the target version for the release image) on a branch and merge into master. \ No newline at end of file diff --git a/images/build-native/docker-build.sh b/images/build-native/docker-build.sh new file mode 100755 index 00000000..9e5625bc --- /dev/null +++ b/images/build-native/docker-build.sh @@ -0,0 +1,21 @@ +#!/bin/sh +if [ -z "$1" ] +then + echo "Needs runtime folder as an argument" + exit 1 +fi +native_version=$(cat native.version) +set -e + +native_image="fnproject/fn-java-native:${native_version}" +if docker pull ${native_image} ; then + echo ${native_image} already exists, skipping native build + exit 0 +fi +workdir=${1} +dockerfiledir=$(pwd) +( + cd ${workdir} + docker build -f ${dockerfiledir}/Dockerfile -t "fnproject/fn-java-native:${native_version}" . +) +echo "fnproject/fn-java-native:${native_version}" > native_build.image diff --git a/images/build-native/native.version b/images/build-native/native.version new file mode 100644 index 00000000..8f0916f7 --- /dev/null +++ b/images/build-native/native.version @@ -0,0 +1 @@ +0.5.0 diff --git a/build-image/Dockerfile b/images/build/Dockerfile similarity index 100% rename from build-image/Dockerfile rename to images/build/Dockerfile diff --git a/build-image/Dockerfile-jdk9 b/images/build/Dockerfile-jdk11 similarity index 86% rename from build-image/Dockerfile-jdk9 rename to images/build/Dockerfile-jdk11 index c6396862..e8b96ddc 100644 --- a/build-image/Dockerfile-jdk9 +++ b/images/build/Dockerfile-jdk11 @@ -1,8 +1,8 @@ -FROM maven:3-jdk-9-slim +FROM maven:3-jdk-11-slim + ARG FN_REPO_URL ADD pom.xml /tmp/cache-deps/pom.xml ADD cache-deps.sh /tmp/cache-deps/cache-deps.sh ADD src /tmp/cache-deps/src - RUN /tmp/cache-deps/cache-deps.sh diff --git a/build-image/cache-deps.sh b/images/build/cache-deps.sh similarity index 100% rename from build-image/cache-deps.sh rename to images/build/cache-deps.sh diff --git a/images/build/docker-build.sh b/images/build/docker-build.sh new file mode 100755 index 00000000..c8685a89 --- /dev/null +++ b/images/build/docker-build.sh @@ -0,0 +1,21 @@ +#!/bin/bash -ex +if [ -z ${REPOSITORY_LOCATION} ] ; then + echo no REPOSITORY_LOCATION set + exit 1; +fi + +docker rm -f fn_mvn_repo || true +docker run -d \ + -v "${REPOSITORY_LOCATION}":/repo:ro \ + -w /repo \ + --name fn_mvn_repo \ + python:2.7 \ + python -mSimpleHTTPServer 18080 + + +DOCKER_LOCALHOST=$(docker inspect --type container -f '{{.NetworkSettings.IPAddress}}' fn_mvn_repo) +REPO_ENV="--build-arg FN_REPO_URL=http://${DOCKER_LOCALHOST}:18080" + +docker build $REPO_ENV $* + +docker rm -f fn_mvn_repo \ No newline at end of file diff --git a/build-image/pom.xml b/images/build/pom.xml similarity index 72% rename from build-image/pom.xml rename to images/build/pom.xml index 474548d4..0a5ab9ad 100644 --- a/build-image/pom.xml +++ b/images/build/pom.xml @@ -7,7 +7,9 @@ UTF-8 - 1.0.0-SNAPSHOT + UTF-8 + + 1.0.0-SNAPSHOT http://172.17.0.1:18080 @@ -21,12 +23,18 @@ com.fnproject.fn api - ${fnproject.version} + ${fdk.version} + + + com.fnproject.fn + testing-core + ${fdk.version} + test com.fnproject.fn - testing - ${fnproject.version} + testing-junit4 + ${fdk.version} test @@ -41,12 +49,20 @@ org.apache.maven.plugins maven-compiler-plugin - 3.3 + 3.8.0 1.8 1.8 + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.1 + + false + + org.apache.maven.plugins maven-deploy-plugin diff --git a/build-image/src/main/java/com/example/fn/HelloFunction.java b/images/build/src/main/java/com/example/fn/HelloFunction.java similarity index 100% rename from build-image/src/main/java/com/example/fn/HelloFunction.java rename to images/build/src/main/java/com/example/fn/HelloFunction.java diff --git a/images/build/src/test/java/com/example/fn/HelloFunctionTest.java b/images/build/src/test/java/com/example/fn/HelloFunctionTest.java new file mode 100644 index 00000000..ee9f2a1f --- /dev/null +++ b/images/build/src/test/java/com/example/fn/HelloFunctionTest.java @@ -0,0 +1,24 @@ +package com.example.fn; + +import com.fnproject.fn.testing.FnResult; +import com.fnproject.fn.testing.FnTestingRule; +import org.junit.Rule; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class HelloFunctionTest { + + @Rule + public final FnTestingRule testing = FnTestingRule.createDefault(); + + @Test + public void shouldReturnGreeting() { + testing.givenEvent().enqueue(); + testing.thenRun(HelloFunction.class, "handleRequest"); + + FnResult result = testing.getOnlyResult(); + assertEquals("Hello, world!", result.getBodyAsString()); + } + +} \ No newline at end of file diff --git a/images/init-native/Dockerfile b/images/init-native/Dockerfile new file mode 100644 index 00000000..ec251021 --- /dev/null +++ b/images/init-native/Dockerfile @@ -0,0 +1,35 @@ +FROM fnproject/fn-java-fdk-build:latest as build +LABEL maintainer="tomas.zezula@oracle.com" +WORKDIR /function +ENV MAVEN_OPTS=-Dmaven.repo.local=/usr/share/maven/ref/repository +ADD pom.xml pom.xml +RUN ["mvn", "package", "dependency:copy-dependencies", "-DincludeScope=runtime", "-DskipTests=true", "-Dmdep.prependGroupId=true", "-DoutputDirectory=target"] +ADD src src +RUN ["mvn", "package"] + +FROM fnproject/fn-java-native:latest as build-native-image +LABEL maintainer="tomas.zezula@oracle.com" +WORKDIR /function +COPY --from=build /function/target/*.jar target/ +COPY --from=build /function/src/main/conf/reflection.json reflection.json +COPY --from=build /function/src/main/conf/jni.json jni.json +RUN /usr/local/graalvm/bin/native-image \ + --static \ + --no-fallback \ + --initialize-at-build-time= \ + --initialize-at-run-time=com.fnproject.fn.runtime.ntv.UnixSocketNative \ + -H:Name=func \ + -H:+ReportUnsupportedElementsAtRuntime \ + -H:ReflectionConfigurationFiles=reflection.json \ + -H:JNIConfigurationFiles=jni.json \ + -classpath "target/*"\ + com.fnproject.fn.runtime.EntryPoint + + +FROM busybox:glibc +LABEL maintainer="tomas.zezula@oracle.com" +WORKDIR /function +COPY --from=build-native-image /function/func func +COPY --from=build-native-image /function/runtime/lib/* . +ENTRYPOINT ["./func", "-XX:MaximumHeapSizePercent=80"] +CMD [ "com.example.fn.HelloFunction::handleRequest" ] diff --git a/images/init-native/Dockerfile-init-image b/images/init-native/Dockerfile-init-image new file mode 100644 index 00000000..0850e955 --- /dev/null +++ b/images/init-native/Dockerfile-init-image @@ -0,0 +1,25 @@ +FROM alpine:latest +LABEL maintainer="tomas.zezula@oracle.com" + +WORKDIR /build +COPY src src +COPY pom.xml . +COPY func.init.yaml . +COPY Dockerfile . + +RUN echo $'#!/bin/sh\n\ +if [ -n ${FN_FUNCTION_NAME} ]\n\ + then\n\ + JAVA_NAME=$(echo ${FN_FUNCTION_NAME:0:1} | tr "[:lower:]" "[:upper:]")${FN_FUNCTION_NAME:1}\n\ + sed -i -e "s|hello|${FN_FUNCTION_NAME}|" pom.xml\n\ + sed -i -e "s|com.example.fn.HelloFunction|com.example.fn.${JAVA_NAME}|" Dockerfile\n\ + sed -i -e "s|com.example.fn.HelloFunction|com.example.fn.${JAVA_NAME}|" src/main/conf/reflection.json\n\ + sed -i -e "s|HelloFunction|${JAVA_NAME}|" src/main/java/com/example/fn/HelloFunction.java\n\ + mv src/main/java/com/example/fn/HelloFunction.java "src/main/java/com/example/fn/${JAVA_NAME}.java"\n\ + sed -i -e "s|HelloFunction|${JAVA_NAME}|" src/test/java/com/example/fn/HelloFunctionTest.java\n\ + mv src/test/java/com/example/fn/HelloFunctionTest.java "src/test/java/com/example/fn/${JAVA_NAME}Test.java"\n\ +fi\n\ +tar c src pom.xml func.init.yaml Dockerfile\n' > build_init_image.sh \ + && chmod 755 build_init_image.sh + +CMD ["./build_init_image.sh"] diff --git a/images/init-native/README.md b/images/init-native/README.md new file mode 100644 index 00000000..080f648f --- /dev/null +++ b/images/init-native/README.md @@ -0,0 +1,2 @@ +# Native init image + diff --git a/images/init-native/docker-build.sh b/images/init-native/docker-build.sh new file mode 100755 index 00000000..4aea7a2b --- /dev/null +++ b/images/init-native/docker-build.sh @@ -0,0 +1,6 @@ +#!/bin/sh +if [ -z "${FN_FDK_VERSION}" ]; then + FN_FDK_VERSION=$(cat ../../release.version) +fi +sed -i.bak -e "s|.*|${FN_FDK_VERSION}|" pom.xml && rm pom.xml.bak +docker build -t fnproject/fn-java-native-init:${FN_FDK_VERSION} -f Dockerfile-init-image . diff --git a/integration-tests/main/test-2/input b/images/init-native/func.init.yaml similarity index 100% rename from integration-tests/main/test-2/input rename to images/init-native/func.init.yaml diff --git a/images/init-native/pom.xml b/images/init-native/pom.xml new file mode 100644 index 00000000..4d8e2703 --- /dev/null +++ b/images/init-native/pom.xml @@ -0,0 +1,88 @@ + + + 4.0.0 + + UTF-8 + 1.0.0-SNAPSHOT + + com.example.fn + hello + 1.0.0 + + + + fn-release-repo + https://dl.bintray.com/fnproject/fnproject + + true + + + false + + + + + + + com.fnproject.fn + api + ${fdk.version} + + + com.fnproject.fn + testing-core + ${fdk.version} + test + + + com.fnproject.fn + testing-junit4 + ${fdk.version} + test + + + junit + junit + 4.12 + test + + + com.fnproject.fn + runtime + ${fdk.version} + runtime + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.1 + + false + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + true + + + + + diff --git a/images/init-native/src/main/conf/jni.json b/images/init-native/src/main/conf/jni.json new file mode 100644 index 00000000..05def46f --- /dev/null +++ b/images/init-native/src/main/conf/jni.json @@ -0,0 +1,22 @@ +[ + { + "name" : "com.fnproject.fn.runtime.ntv.UnixSocketNative", + "methods" : [ + { "name" : "socket" }, + { "name" : "bind" }, + { "name" : "connect" }, + { "name" : "listen" }, + { "name" : "accept" }, + { "name" : "recv" }, + { "name" : "send" }, + { "name" : "close" }, + { "name" : "setSendTimeout" }, + { "name" : "getSendTimeout" }, + { "name" : "setRecvTimeout" }, + { "name" : "getRecvTimeout" }, + { "name" : "setSendBufSize" }, + { "name" : "setRecvBufSize" }, + { "name" : "shutdown"} + ] + } +] diff --git a/images/init-native/src/main/conf/reflection.json b/images/init-native/src/main/conf/reflection.json new file mode 100644 index 00000000..e1e25e08 --- /dev/null +++ b/images/init-native/src/main/conf/reflection.json @@ -0,0 +1,9 @@ +[ + { + "name" : "com.example.fn.HelloFunction", + "methods" : [ + { "name" : "handleRequest" }, + { "name" : "" } + ] + } +] diff --git a/images/init-native/src/main/java/com/example/fn/HelloFunction.java b/images/init-native/src/main/java/com/example/fn/HelloFunction.java new file mode 100644 index 00000000..8c581e76 --- /dev/null +++ b/images/init-native/src/main/java/com/example/fn/HelloFunction.java @@ -0,0 +1,11 @@ +package com.example.fn; + +public class HelloFunction { + + public String handleRequest(String input) { + String name = (input == null || input.isEmpty()) ? "world" : input; + + return "Hello, " + name + "!"; + } + +} \ No newline at end of file diff --git a/build-image/src/test/java/com/example/fn/HelloFunctionTest.java b/images/init-native/src/test/java/com/example/fn/HelloFunctionTest.java similarity index 100% rename from build-image/src/test/java/com/example/fn/HelloFunctionTest.java rename to images/init-native/src/test/java/com/example/fn/HelloFunctionTest.java diff --git a/runtime/Dockerfile b/images/runtime/Dockerfile similarity index 65% rename from runtime/Dockerfile rename to images/runtime/Dockerfile index cb69e29e..bdad842f 100644 --- a/runtime/Dockerfile +++ b/images/runtime/Dockerfile @@ -1,7 +1,10 @@ -FROM openjdk:8-slim +FROM openjdk:8-jre-slim COPY target/runtime-*.jar target/dependency/*.jar /function/runtime/ +COPY src/main/c/libfnunixsocket.so /function/runtime/lib/ -RUN ["/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java", "-Xshare:dump"] +RUN ["java", "-Xshare:dump"] + +RUN addgroup --system --gid 1000 fn && adduser --uid 1000 --gid 1000 fn # UseCGroupMemoryLimitForHeap looks up /sys/fs/cgroup/memory/memory.limit_in_bytes inside the container to determine # what the heap should be set to. This is an experimental feature at the moment, thus we need to unlock to use it. @@ -15,4 +18,4 @@ RUN ["/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java", "-Xshare:dump"] # # The max memory value obtained with these args seem to be okay for most memory limits. The exception is when the # memory limit is set to 128MiB, in which case maxMemory returns roughly half. -ENTRYPOINT [ "/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseCGroupMemoryLimitForHeap", "-XX:MaxRAMFraction=2", "-XX:+UseSerialGC", "-Xshare:on", "-cp", "/function/app/*:/function/runtime/*", "com.fnproject.fn.runtime.EntryPoint" ] +ENTRYPOINT [ "java", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseCGroupMemoryLimitForHeap", "-XX:-UsePerfData", "-XX:MaxRAMFraction=2", "-XX:+UseSerialGC", "-Xshare:on", "-Djava.library.path=/function/runtime/lib", "-cp", "/function/app/*:/function/runtime/*", "com.fnproject.fn.runtime.EntryPoint" ] diff --git a/images/runtime/Dockerfile-jre11 b/images/runtime/Dockerfile-jre11 new file mode 100644 index 00000000..2f704297 --- /dev/null +++ b/images/runtime/Dockerfile-jre11 @@ -0,0 +1,20 @@ +FROM openjdk:11-jre-slim +COPY target/runtime-*.jar target/dependency/*.jar /function/runtime/ +COPY src/main/c/libfnunixsocket.so /function/runtime/lib/ + +RUN ["/usr/local/openjdk-11/bin/java", "-Xshare:dump"] + +RUN addgroup --system --gid 1000 fn && adduser --uid 1000 --gid 1000 fn + +# The UseExeperimentalVMOptions, UseCGroupMemoryLimitForHeap and MaxRAMFraction options that were used in the JDK 9 builds are +# no longer supported in JDK 11 - so these have been removed. We now rely on the built-in ContainerSupport option that Linux JDKs +# use to configure themselves when detecting they are running in a container. +# +# SerialGC is used here as it's likely that we'll be running many JVMs on the same host machine and it's also likely +# that the number of JVMs will outnumber the number of available processors. +# +ENTRYPOINT [ "/usr/local/openjdk-11/bin/java", "-XX:-UsePerfData", "-XX:+UseSerialGC", "-Xshare:on", \ + "-Djava.awt.headless=true" , \ + "-Djava.library.path=/function/runtime/lib", \ + "-cp", "/function/app/*:/function/runtime/*", \ + "com.fnproject.fn.runtime.EntryPoint" ] diff --git a/images/runtime/README.md b/images/runtime/README.md new file mode 100644 index 00000000..dfd530a0 --- /dev/null +++ b/images/runtime/README.md @@ -0,0 +1,3 @@ +# Fn Java runtime base images + +These images are used as a base image for functions - they include a JDK and the latest version of the runtime \ No newline at end of file diff --git a/infra/update/functions/Jenkinsfile b/infra/update/functions/Jenkinsfile index b5691ecf..8162ef57 100644 --- a/infra/update/functions/Jenkinsfile +++ b/infra/update/functions/Jenkinsfile @@ -1,7 +1,7 @@ pipeline { agent any parameters { - string(name: 'IMAGE', defaultValue: 'registry.oracledx.com/skeppare/functions-service:latest', description: 'Which image to use (full repository:tag, e.g. fnproject/functions:latest)') + string(name: 'IMAGE', defaultValue: 'registry.oracledx.com/skeppare/functions-service:latest', description: 'Which image to use (full repository:tag, e.g. fnproject/fnserver:latest)') string(name: 'SOURCE_REPO', defaultValue: 'git@github.com:fnproject/fn.git', description: 'Which git repo to use to build the CLI tool') string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: 'Which git repo branch to use to build the CLI tool') } diff --git a/integration-tests/.gitignore b/integration-tests/.gitignore deleted file mode 100644 index 9b3667f0..00000000 --- a/integration-tests/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -*/*/actual -*/*/build-output -*/*/success -*/*/failure -*/*/output diff --git a/integration-tests/README.md b/integration-tests/README.md index 0d13256d..90e881e7 100644 --- a/integration-tests/README.md +++ b/integration-tests/README.md @@ -11,60 +11,19 @@ They should _not_ be used for: * extensive feature testing (use unit tests) * Performance/load testing -## Creating a new test - -Put main integration tests under main/test- - -the content of a test dir is a a typically a new function (containing func.yaml, pom.xml etc. ) - -create the following files: -* `input` : the input to pass to the deployed function -* `expected` : the verbatim expected result of the function -* `expected.sh` : a shell script that should succeed when the test passed - this is used in place of `expected` -* `config` : A newline seperated list of config variables to set on the function -* `pre-test.sh` a script that is run before the function is called (e.g. to call fn init to check bootstrapping) - - # Running locally -To run locally you will need to deploy the fn artifacts to a local repository: - -(in top-level dir) +Build the runtime: ```bash -export REPOSITORY_LOCATION=/tmp/staging-repository -# on OSX: -export DOCKER_LOCALHOST=docker.for.mac.localhost - -mvn deploy -DaltDeploymentRepository=localStagingDir::default::file://"$REPOSITORY_LOCATION" +./build.sh ``` -You may also want to/need build local copies of the build images: -```bash -cd build-image -./docker-build.sh -t fnproject/fn-java-fdk-build . -``` - -and runtime images: -``` -cd runtime -docker build -t fnproject/fn-java-fdk . -docker build -f Dockerfile-jdk9 -t fnproject/fn-java-fdk:jdk9-latest . -``` - -Finally you can run the integration tests: +Run the integration tests: ```bash -./integration-tests/run-local.sh +./integration-tests/run_tests_ci.sh ``` -Note that these will update the pom files in the tests - don't check these in! - - -# Running against a remote environment -For running against a remote integration environment, configure - ~/.fn-token - ~/.fn-api-url - ~/.fn-flow-base-url -and run the `run-remote.sh` script. +This will start/stop fnserver and flow server \ No newline at end of file diff --git a/integration-tests/fnserver.env b/integration-tests/fnserver.env new file mode 100644 index 00000000..fd6e3b46 --- /dev/null +++ b/integration-tests/fnserver.env @@ -0,0 +1,3 @@ +FN_MAX_REQUEST_SIZE=6291456 +FN_MAX_RESPONSE_SIZE=6291456 +FN_MAX_HDR_RESPONSE_SIZE=16384 \ No newline at end of file diff --git a/integration-tests/main/test-4/README.md b/integration-tests/funcs/flowAllFeatures/README.md similarity index 100% rename from integration-tests/main/test-4/README.md rename to integration-tests/funcs/flowAllFeatures/README.md diff --git a/integration-tests/main/test-4/func.yaml b/integration-tests/funcs/flowAllFeatures/func.yaml similarity index 74% rename from integration-tests/main/test-4/func.yaml rename to integration-tests/funcs/flowAllFeatures/func.yaml index d7e5fee1..9a41741c 100644 --- a/integration-tests/main/test-4/func.yaml +++ b/integration-tests/funcs/flowAllFeatures/func.yaml @@ -1,9 +1,10 @@ -version: 0.0.13 +schema_version: 20180708 +name: flowallfeatures +version: 0.0.15 runtime: java cmd: com.fnproject.fn.integration.ExerciseEverything::handleRequest +format: http-stream +timeout: 120 build: - mvn package dependency:copy-dependencies -DincludeScope=runtime -DskipTests=true -Dmdep.prependGroupId=true -DoutputDirectory=target -format: http -timeout: 120 -name: test-4 diff --git a/integration-tests/funcs/flowAllFeatures/pom.xml b/integration-tests/funcs/flowAllFeatures/pom.xml new file mode 100644 index 00000000..55361cbb --- /dev/null +++ b/integration-tests/funcs/flowAllFeatures/pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + + UTF-8 + 1.0.0-SNAPSHOT + + com.fnproject.fn + integration-test-4 + 1.0.0 + + jar + + + + com.fnproject.fn + api + ${fdk.version} + + + + com.fnproject.fn + runtime + ${fdk.version} + + + com.fnproject.fn + flow-runtime + ${fdk.version} + + + com.fnproject.fn + testing-core + ${fdk.version} + + + com.fnproject.fn + testing-junit4 + ${fdk.version} + + + junit + junit + 4.12 + test + + + commons-io + commons-io + 2.5 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + true + + + + + + + + fn-maven-releases + https://dl.bintray.com/fnproject/fnproject + + + diff --git a/integration-tests/main/test-4/src/main/java/com/fnproject/fn/integration/ExerciseEverything.java b/integration-tests/funcs/flowAllFeatures/src/main/java/com/fnproject/fn/integration/ExerciseEverything.java similarity index 94% rename from integration-tests/main/test-4/src/main/java/com/fnproject/fn/integration/ExerciseEverything.java rename to integration-tests/funcs/flowAllFeatures/src/main/java/com/fnproject/fn/integration/ExerciseEverything.java index b11c1a20..55d12da3 100644 --- a/integration-tests/main/test-4/src/main/java/com/fnproject/fn/integration/ExerciseEverything.java +++ b/integration-tests/funcs/flowAllFeatures/src/main/java/com/fnproject/fn/integration/ExerciseEverything.java @@ -1,8 +1,11 @@ package com.fnproject.fn.integration; +import com.fnproject.fn.api.FnFeature; import com.fnproject.fn.api.Headers; import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.api.flow.*; +import com.fnproject.fn.runtime.flow.FlowFeature; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.TeeOutputStream; @@ -16,14 +19,20 @@ import java.util.stream.Collectors; @SuppressWarnings("unused") +@FnFeature(FlowFeature.class) public class ExerciseEverything { - private boolean okay = true; - private ByteArrayOutputStream bos = new ByteArrayOutputStream(); - private PrintStream out = new PrintStream(new TeeOutputStream(System.err, bos)); - private String testSelector = null; - private InputEvent inputEvent; - private List failures = new ArrayList<>(); + private boolean okay = true; + private ByteArrayOutputStream bos = new ByteArrayOutputStream(); + private PrintStream out = new PrintStream(new TeeOutputStream(System.err, bos)); + private String testSelector = null; + private InputEvent inputEvent; + private List failures = new ArrayList<>(); + private final RuntimeContext runtimeContext; + + public ExerciseEverything(RuntimeContext runtimeContext) { + this.runtimeContext = runtimeContext; + } @Test(1) @Test.Expect("completed value") @@ -134,7 +143,7 @@ public FlowFuture nonexistentExternalEvaluation(Flow fl) { @Test(12) @Test.Expect("okay") public FlowFuture checkPassingExternalInvocation(Flow fl) { - return fl.invokeFunction(inputEvent.getAppName() + inputEvent.getRoute(), HttpMethod.POST, Headers.emptyHeaders(), "PASS".getBytes()) + return fl.invokeFunction(runtimeContext.getFunctionID(), HttpMethod.POST, Headers.emptyHeaders(), "PASS".getBytes()) .thenApply((resp) -> resp.getStatusCode() != 200 ? "failure" : new String(resp.getBodyAsBytes())); } @@ -143,7 +152,7 @@ public FlowFuture checkPassingExternalInvocation(Flow fl) { @Test(13) @Test.Catch({FlowCompletionException.class, FunctionInvocationException.class}) public FlowFuture checkFailingExternalInvocation(Flow fl) { - return fl.invokeFunction(inputEvent.getAppName() + inputEvent.getRoute(), HttpMethod.POST, Headers.emptyHeaders(), "FAIL".getBytes()); + return fl.invokeFunction(runtimeContext.getFunctionID(), HttpMethod.POST, Headers.emptyHeaders(), "FAIL".getBytes()); } @Test(14) diff --git a/integration-tests/main/test-4/src/main/java/com/fnproject/fn/integration/Test.java b/integration-tests/funcs/flowAllFeatures/src/main/java/com/fnproject/fn/integration/Test.java similarity index 100% rename from integration-tests/main/test-4/src/main/java/com/fnproject/fn/integration/Test.java rename to integration-tests/funcs/flowAllFeatures/src/main/java/com/fnproject/fn/integration/Test.java diff --git a/integration-tests/main/test-1/func.yaml b/integration-tests/funcs/flowBasic/func.yaml similarity index 57% rename from integration-tests/main/test-1/func.yaml rename to integration-tests/funcs/flowBasic/func.yaml index 81f45b5c..6022a127 100644 --- a/integration-tests/main/test-1/func.yaml +++ b/integration-tests/funcs/flowBasic/func.yaml @@ -1,6 +1,7 @@ -name: test-1 -version: 0.0.5 +schema_version: 20180708 +name: flowbasic +version: 0.0.7 runtime: java cmd: com.fnproject.fn.integration.test_1.CompleterFunction::handleRequest -format: http +format: http-stream timeout: 120 diff --git a/integration-tests/main/test-1-jdk8/pom.xml b/integration-tests/funcs/flowBasic/pom.xml similarity index 80% rename from integration-tests/main/test-1-jdk8/pom.xml rename to integration-tests/funcs/flowBasic/pom.xml index 37f78c46..6cac356e 100644 --- a/integration-tests/main/test-1-jdk8/pom.xml +++ b/integration-tests/funcs/flowBasic/pom.xml @@ -5,7 +5,7 @@ 4.0.0 UTF-8 - 1.0.0-SNAPSHOT + 1.0.0-SNAPSHOT com.fnproject.fn integration-test-1 @@ -17,20 +17,14 @@ com.fnproject.fn api - ${fnproject.version} + ${fdk.version} com.fnproject.fn - testing - ${fnproject.version} - test - - - junit - junit - 4.12 - test + flow-runtime + ${fdk.version} + diff --git a/integration-tests/main/test-1-jdk8/src/main/java/com/fnproject/fn/integration/test_1/CompleterFunction.java b/integration-tests/funcs/flowBasic/src/main/java/com/fnproject/fn/integration/test_1/CompleterFunction.java similarity index 76% rename from integration-tests/main/test-1-jdk8/src/main/java/com/fnproject/fn/integration/test_1/CompleterFunction.java rename to integration-tests/funcs/flowBasic/src/main/java/com/fnproject/fn/integration/test_1/CompleterFunction.java index edd04e86..0740d8ba 100644 --- a/integration-tests/main/test-1-jdk8/src/main/java/com/fnproject/fn/integration/test_1/CompleterFunction.java +++ b/integration-tests/funcs/flowBasic/src/main/java/com/fnproject/fn/integration/test_1/CompleterFunction.java @@ -1,8 +1,11 @@ package com.fnproject.fn.integration.test_1; +import com.fnproject.fn.api.FnFeature; import com.fnproject.fn.api.flow.Flow; +import com.fnproject.fn.runtime.flow.FlowFeature; import com.fnproject.fn.api.flow.Flows; +@FnFeature(FlowFeature.class) public class CompleterFunction { public Integer handleRequest(String input) { diff --git a/integration-tests/main/test-1-jdk8/func.yaml b/integration-tests/funcs/flowBasicJDK8/func.yaml similarity index 56% rename from integration-tests/main/test-1-jdk8/func.yaml rename to integration-tests/funcs/flowBasicJDK8/func.yaml index 43b2d3a1..143565c7 100644 --- a/integration-tests/main/test-1-jdk8/func.yaml +++ b/integration-tests/funcs/flowBasicJDK8/func.yaml @@ -1,6 +1,7 @@ -version: 0.0.1 +schema_version: 20180708 +name: flowbasicj8 +version: 0.0.3 runtime: java8 cmd: com.fnproject.fn.integration.test_1.CompleterFunction::handleRequest -format: http +format: http-stream timeout: 120 -name: test-1-jdk8 \ No newline at end of file diff --git a/integration-tests/main/test-1/pom.xml b/integration-tests/funcs/flowBasicJDK8/pom.xml similarity index 80% rename from integration-tests/main/test-1/pom.xml rename to integration-tests/funcs/flowBasicJDK8/pom.xml index 37f78c46..e674247f 100644 --- a/integration-tests/main/test-1/pom.xml +++ b/integration-tests/funcs/flowBasicJDK8/pom.xml @@ -5,7 +5,7 @@ 4.0.0 UTF-8 - 1.0.0-SNAPSHOT + 1.0.0-SNAPSHOT com.fnproject.fn integration-test-1 @@ -17,19 +17,12 @@ com.fnproject.fn api - ${fnproject.version} + ${fdk.version} com.fnproject.fn - testing - ${fnproject.version} - test - - - junit - junit - 4.12 - test + flow-runtime + ${fdk.version} diff --git a/integration-tests/main/test-1/src/main/java/com/fnproject/fn/integration/test_1/CompleterFunction.java b/integration-tests/funcs/flowBasicJDK8/src/main/java/com/fnproject/fn/integration/test_1/CompleterFunction.java similarity index 76% rename from integration-tests/main/test-1/src/main/java/com/fnproject/fn/integration/test_1/CompleterFunction.java rename to integration-tests/funcs/flowBasicJDK8/src/main/java/com/fnproject/fn/integration/test_1/CompleterFunction.java index edd04e86..0740d8ba 100644 --- a/integration-tests/main/test-1/src/main/java/com/fnproject/fn/integration/test_1/CompleterFunction.java +++ b/integration-tests/funcs/flowBasicJDK8/src/main/java/com/fnproject/fn/integration/test_1/CompleterFunction.java @@ -1,8 +1,11 @@ package com.fnproject.fn.integration.test_1; +import com.fnproject.fn.api.FnFeature; import com.fnproject.fn.api.flow.Flow; +import com.fnproject.fn.runtime.flow.FlowFeature; import com.fnproject.fn.api.flow.Flows; +@FnFeature(FlowFeature.class) public class CompleterFunction { public Integer handleRequest(String input) { diff --git a/integration-tests/funcs/flowExitHooks/func.yaml b/integration-tests/funcs/flowExitHooks/func.yaml new file mode 100644 index 00000000..52034a1e --- /dev/null +++ b/integration-tests/funcs/flowExitHooks/func.yaml @@ -0,0 +1,7 @@ +schema_version: 20180708 +name: flowexithooks +version: 0.0.3 +runtime: java11 +cmd: com.fnproject.fn.integration.test_5.CompleterFunction::handleRequest +format: http-stream +timeout: 120 diff --git a/integration-tests/main/test-5/pom.xml b/integration-tests/funcs/flowExitHooks/pom.xml similarity index 75% rename from integration-tests/main/test-5/pom.xml rename to integration-tests/funcs/flowExitHooks/pom.xml index 400510fc..2b9bc0f7 100644 --- a/integration-tests/main/test-5/pom.xml +++ b/integration-tests/funcs/flowExitHooks/pom.xml @@ -5,7 +5,7 @@ 4.0.0 UTF-8 - 1.0.0-SNAPSHOT + 1.0.0-SNAPSHOT com.fnproject.fn integration-test-5 @@ -17,12 +17,24 @@ com.fnproject.fn api - ${fnproject.version} + ${fdk.version} com.fnproject.fn - testing - ${fnproject.version} + flow-runtime + ${fdk.version} + compile + + + com.fnproject.fn + testing-core + ${fdk.version} + test + + + com.fnproject.fn + testing-junit4 + ${fdk.version} test diff --git a/integration-tests/funcs/flowExitHooks/src/main/java/com/fnproject/fn/integration/test_5/CompleterFunction.java b/integration-tests/funcs/flowExitHooks/src/main/java/com/fnproject/fn/integration/test_5/CompleterFunction.java new file mode 100644 index 00000000..57c0c0ce --- /dev/null +++ b/integration-tests/funcs/flowExitHooks/src/main/java/com/fnproject/fn/integration/test_5/CompleterFunction.java @@ -0,0 +1,45 @@ +package com.fnproject.fn.integration.test_5; + +import com.fnproject.fn.api.FnFeature; +import com.fnproject.fn.api.RuntimeContext; +import com.fnproject.fn.api.flow.Flow; +import com.fnproject.fn.api.flow.Flows; +import com.fnproject.fn.runtime.flow.FlowFeature; + +import java.io.Serializable; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; + +@FnFeature(FlowFeature.class) +public class CompleterFunction implements Serializable { + + private final URL callbackURL; + + public CompleterFunction(RuntimeContext rtc) throws Exception { + callbackURL = URI.create(rtc.getConfigurationByKey("TERMINATION_HOOK_URL").orElseThrow(() -> new RuntimeException("No config set "))).toURL(); + } + + public Integer handleRequest(String input) { + Flow fl = Flows.currentFlow(); + fl.addTerminationHook((ignored) -> { + try { + HttpURLConnection con = (HttpURLConnection) callbackURL.openConnection(); + + System.err.println("Ran the hook."); + + con.setRequestMethod("GET"); + if (con.getResponseCode() != 200) { + throw new RuntimeException("Got bad code from callback"); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + + }); + return fl.supply(() -> { + return 42; + }).get(); + } + +} diff --git a/integration-tests/funcs/flowTimeouts/func.yaml b/integration-tests/funcs/flowTimeouts/func.yaml new file mode 100644 index 00000000..6d83f8ec --- /dev/null +++ b/integration-tests/funcs/flowTimeouts/func.yaml @@ -0,0 +1,7 @@ +schema_version: 20180708 +name: flowtimeouts +version: 0.0.3 +runtime: java11 +cmd: com.fnproject.fn.integration.test_6.CompleterFunction::handleRequest +format: http-stream +timeout: 120 diff --git a/integration-tests/main/test-4/pom.xml b/integration-tests/funcs/flowTimeouts/pom.xml similarity index 79% rename from integration-tests/main/test-4/pom.xml rename to integration-tests/funcs/flowTimeouts/pom.xml index 380c0d33..011e9853 100644 --- a/integration-tests/main/test-4/pom.xml +++ b/integration-tests/funcs/flowTimeouts/pom.xml @@ -5,10 +5,10 @@ 4.0.0 UTF-8 - 1.0.0-SNAPSHOT + 1.0.0-SNAPSHOT com.fnproject.fn - integration-test-4 + integration-test-6 1.0.0 jar @@ -17,18 +17,23 @@ com.fnproject.fn api - ${fnproject.version} + ${fdk.version} - com.fnproject.fn - runtime - ${fnproject.version} + flow-runtime + ${fdk.version} com.fnproject.fn - testing - ${fnproject.version} + testing-core + ${fdk.version} + test + + + com.fnproject.fn + testing-junit4 + ${fdk.version} test @@ -37,11 +42,6 @@ 4.12 test - - commons-io - commons-io - 2.5 - diff --git a/integration-tests/main/test-6/src/main/java/com/fnproject/fn/integration/test_6/CompleterFunction.java b/integration-tests/funcs/flowTimeouts/src/main/java/com/fnproject/fn/integration/test_6/CompleterFunction.java similarity index 67% rename from integration-tests/main/test-6/src/main/java/com/fnproject/fn/integration/test_6/CompleterFunction.java rename to integration-tests/funcs/flowTimeouts/src/main/java/com/fnproject/fn/integration/test_6/CompleterFunction.java index 5041c8dc..bb603493 100644 --- a/integration-tests/main/test-6/src/main/java/com/fnproject/fn/integration/test_6/CompleterFunction.java +++ b/integration-tests/funcs/flowTimeouts/src/main/java/com/fnproject/fn/integration/test_6/CompleterFunction.java @@ -1,23 +1,26 @@ package com.fnproject.fn.integration.test_6; +import com.fnproject.fn.api.FnFeature; import com.fnproject.fn.api.flow.Flow; import com.fnproject.fn.api.flow.Flows; +import com.fnproject.fn.runtime.flow.FlowFeature; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; + +@FnFeature(FlowFeature.class) public class CompleterFunction { - public Integer handleRequest(String input) { + public String handleRequest(String input) { Flow fl = Flows.currentFlow(); try { return fl.supply(() -> { Thread.sleep(10000); - return 42; + return "nope"; }).get(1000, TimeUnit.MILLISECONDS); } catch (TimeoutException t) { - System.err.println("Caught timeout"); - return 20; + return "timeout"; } } diff --git a/integration-tests/funcs/helloFunc/func-proto.yaml b/integration-tests/funcs/helloFunc/func-proto.yaml new file mode 100644 index 00000000..e43cf9f4 --- /dev/null +++ b/integration-tests/funcs/helloFunc/func-proto.yaml @@ -0,0 +1,6 @@ +schema_version: 20180708 +name: hellofunc +version: 0.0.1 +runtime: java8 +cmd: com.fnproject.fn.integration.hello.HelloFunction::handleRequest +format: http-stream \ No newline at end of file diff --git a/integration-tests/funcs/helloFunc/pom.xml b/integration-tests/funcs/helloFunc/pom.xml new file mode 100644 index 00000000..f53da8dd --- /dev/null +++ b/integration-tests/funcs/helloFunc/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + UTF-8 + 1.0.0-SNAPSHOT + + com.example.fn + hello-func + 1.0.0 + + + + fn-release-repo + https://dl.bintray.com/fnproject/fnproject + + + + + + com.fnproject.fn + api + ${fdk.version} + + + com.fnproject.fn + testing-core + ${fdk.version} + test + + + com.fnproject.fn + testing-junit4 + ${fdk.version} + test + + + junit + junit + 4.12 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + 8 + 8 + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.1 + + false + + + + + diff --git a/integration-tests/funcs/helloFunc/src-proto/main/java/com/fnproject/fn/integration/hello/HelloFunction.java b/integration-tests/funcs/helloFunc/src-proto/main/java/com/fnproject/fn/integration/hello/HelloFunction.java new file mode 100644 index 00000000..7efa98ab --- /dev/null +++ b/integration-tests/funcs/helloFunc/src-proto/main/java/com/fnproject/fn/integration/hello/HelloFunction.java @@ -0,0 +1,8 @@ +package com.fnproject.fn.integration.hello; + +public class HelloFunction { + public String handleRequest(String input) { + String name = (input == null || input.isEmpty()) ? "world" : input; + return "Hello, " + name + "!"; + } +} \ No newline at end of file diff --git a/integration-tests/funcs/httpgwfunc/func.yaml b/integration-tests/funcs/httpgwfunc/func.yaml new file mode 100644 index 00000000..8380eb34 --- /dev/null +++ b/integration-tests/funcs/httpgwfunc/func.yaml @@ -0,0 +1,10 @@ +schema_version: 20180708 +name: httpgwfunc +version: 0.0.1 +runtime: java +cmd: com.example.fn.TriggerFunction::handleRequest +format: http-stream +triggers: +- name: trig + type: http + source: /httpgwfunc-trigger diff --git a/integration-tests/funcs/httpgwfunc/pom.xml b/integration-tests/funcs/httpgwfunc/pom.xml new file mode 100644 index 00000000..28d3df65 --- /dev/null +++ b/integration-tests/funcs/httpgwfunc/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + UTF-8 + 1.0.0-SNAPSHOT + + com.example.fn + hello + 1.0.0 + + + + fn-release-repo + https://dl.bintray.com/fnproject/fnproject + + + + + + com.fnproject.fn + api + ${fdk.version} + + + com.fnproject.fn + testing-core + ${fdk.version} + test + + + com.fnproject.fn + testing-junit4 + ${fdk.version} + test + + + junit + junit + 4.12 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + 8 + 8 + + + + + diff --git a/integration-tests/funcs/httpgwfunc/src/main/java/com/example/fn/TriggerFunction.java b/integration-tests/funcs/httpgwfunc/src/main/java/com/example/fn/TriggerFunction.java new file mode 100644 index 00000000..4d45661f --- /dev/null +++ b/integration-tests/funcs/httpgwfunc/src/main/java/com/example/fn/TriggerFunction.java @@ -0,0 +1,27 @@ +package com.example.fn; + +import com.fnproject.fn.api.InvocationContext; +import com.fnproject.fn.api.httpgateway.HTTPGatewayContext; + +public class TriggerFunction { + + public String handleRequest(String input, HTTPGatewayContext hctx, InvocationContext ctx) { + String name = (input == null || input.isEmpty()) ? "world" : input; + + + ctx.setResponseHeader("MyRawHeader", "bar"); + ctx.addResponseHeader("MyRawHeader", "bob"); + + + hctx.setResponseHeader("Content-Type", "text/plain"); + hctx.addResponseHeader("MyHTTPHeader", "foo"); + + hctx.addResponseHeader("GotMethod", hctx.getMethod()); + hctx.addResponseHeader("GotURL", hctx.getRequestURL()); + hctx.addResponseHeader("GotHeader", hctx.getHeaders().get("Foo").orElse("nope")); + + hctx.setStatusCode(202); + return "Hello, " + name + "!"; + } + +} \ No newline at end of file diff --git a/integration-tests/funcs/httpgwfunc/src/test/java/com/example/fn/TriggerFunctionTest.java b/integration-tests/funcs/httpgwfunc/src/test/java/com/example/fn/TriggerFunctionTest.java new file mode 100644 index 00000000..2c46f1e8 --- /dev/null +++ b/integration-tests/funcs/httpgwfunc/src/test/java/com/example/fn/TriggerFunctionTest.java @@ -0,0 +1,40 @@ +package com.example.fn; + +import com.fnproject.fn.testing.FnResult; +import com.fnproject.fn.testing.FnTestingRule; +import org.junit.Rule; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class TriggerFunctionTest { + + @Rule + public final FnTestingRule testing = FnTestingRule.createDefault(); + + @Test + public void shouldReturnGreeting() { + testing.givenEvent().enqueue(); + testing.thenRun(TriggerFunction.class, "handleRequest"); + + FnResult result = testing.getOnlyResult(); + assertEquals("Hello, world!", result.getBodyAsString()); + } + + + @Test + public void shouldWorkWithHTTP() { + testing.givenEvent() + .withHeader("Fn-Http-Method", "POST") + .withHeader("Fn-Http-H-Foo", "bar") + .withHeader("Fn-Http-Request-Url", "http://mysite.com/?q1=2&q3=4") + .enqueue(); + testing.thenRun(TriggerFunction.class, "handleRequest"); + + FnResult result = testing.getOnlyResult(); + assertEquals("Hello, world!", result.getBodyAsString()); + assertEquals("202", result.getHeaders().get("Fn-Http-Status").orElse("")); + } + + +} \ No newline at end of file diff --git a/integration-tests/funcs/simpleFunc/func.yaml b/integration-tests/funcs/simpleFunc/func.yaml new file mode 100644 index 00000000..c0c9c79b --- /dev/null +++ b/integration-tests/funcs/simpleFunc/func.yaml @@ -0,0 +1,6 @@ +schema_version: 20180708 +name: simplefunc +version: 0.0.3 +runtime: java11 +cmd: com.fnproject.fn.integration.test2.PlainFunction::handleRequest +format: http-stream diff --git a/integration-tests/main/test-2/pom.xml b/integration-tests/funcs/simpleFunc/pom.xml similarity index 83% rename from integration-tests/main/test-2/pom.xml rename to integration-tests/funcs/simpleFunc/pom.xml index da270e30..60e2ab1c 100644 --- a/integration-tests/main/test-2/pom.xml +++ b/integration-tests/funcs/simpleFunc/pom.xml @@ -5,7 +5,7 @@ 4.0.0 UTF-8 - 1.0.0-SNAPSHOT + 1.0.0-SNAPSHOT com.fnproject.fn integration-test-2 @@ -15,13 +15,17 @@ com.fnproject.fn api - ${fnproject.version} + ${fdk.version} com.fnproject.fn - testing - ${fnproject.version} - test + testing-core + ${fdk.version} + + + com.fnproject.fn + testing-junit4 + ${fdk.version} junit diff --git a/integration-tests/main/test-2/src/main/java/com/fnproject/fn/integration/test2/PlainFunction.java b/integration-tests/funcs/simpleFunc/src/main/java/com/fnproject/fn/integration/test2/PlainFunction.java similarity index 86% rename from integration-tests/main/test-2/src/main/java/com/fnproject/fn/integration/test2/PlainFunction.java rename to integration-tests/funcs/simpleFunc/src/main/java/com/fnproject/fn/integration/test2/PlainFunction.java index 7ff61897..60aeff69 100644 --- a/integration-tests/main/test-2/src/main/java/com/fnproject/fn/integration/test2/PlainFunction.java +++ b/integration-tests/funcs/simpleFunc/src/main/java/com/fnproject/fn/integration/test2/PlainFunction.java @@ -1,6 +1,7 @@ package com.fnproject.fn.integration.test2; -import com.fnproject.fn.api.*; +import com.fnproject.fn.api.FnConfiguration; +import com.fnproject.fn.api.RuntimeContext; public class PlainFunction { diff --git a/integration-tests/main/test-2/src/test/java/com/fnproject/fn/integration/test2/PlainFunctionTest.java b/integration-tests/funcs/simpleFunc/src/test/java/com/fnproject/fn/integration/test2/PlainFunctionTest.java similarity index 100% rename from integration-tests/main/test-2/src/test/java/com/fnproject/fn/integration/test2/PlainFunctionTest.java rename to integration-tests/funcs/simpleFunc/src/test/java/com/fnproject/fn/integration/test2/PlainFunctionTest.java diff --git a/integration-tests/lib.sh b/integration-tests/lib.sh deleted file mode 100644 index 83c04b49..00000000 --- a/integration-tests/lib.sh +++ /dev/null @@ -1,48 +0,0 @@ -# Bash clean-up junk -- -typeset -a _deferrals -_on_exit() { - set +x - local idx - for (( idx=${#_deferrals[@]}-1 ; idx>=0 ; idx-- )) ; do - set -x - ${_deferrals[idx]} - set +x - done -} -defer() { - _deferrals+=("$*") -} -trap _on_exit EXIT TERM INT -# -- Bash clean-up junk - -wait_for_http() { - ( - set +ex - local i - for i in {1..15}; do - curl --connect-timeout 5 --max-time 2 "$1" && exit - sleep 1 - done - exit 1 - ) -} - -# prefix each line, whilst evaluating -- -prefix_lines() { - local prefix="$1" - local file="$2" - eval awk \''{print "'"$prefix"' " $0}'\' '<<-EOF'$'\n'"$(< "$file")"$'\n'EOF -} -# -- prefix each line, whilst evaluating - - -line() { - echo -------------------------------------------------------------------------------- -} - -error() { - echo "$1" 1>&2 - exit "${2:-1}" -} - -export SCRIPT_DIR="$( cd "$(dirname "$0")" && command pwd )" diff --git a/integration-tests/main/test-1-jdk8/config b/integration-tests/main/test-1-jdk8/config deleted file mode 100644 index a7c1a5a8..00000000 --- a/integration-tests/main/test-1-jdk8/config +++ /dev/null @@ -1 +0,0 @@ -COMPLETER_BASE_URL=${COMPLETER_BASE_URL} diff --git a/integration-tests/main/test-1-jdk8/expected b/integration-tests/main/test-1-jdk8/expected deleted file mode 100644 index 62f94575..00000000 --- a/integration-tests/main/test-1-jdk8/expected +++ /dev/null @@ -1 +0,0 @@ -6 \ No newline at end of file diff --git a/integration-tests/main/test-1-jdk8/input b/integration-tests/main/test-1-jdk8/input deleted file mode 100644 index 00750edc..00000000 --- a/integration-tests/main/test-1-jdk8/input +++ /dev/null @@ -1 +0,0 @@ -3 diff --git a/integration-tests/main/test-1/config b/integration-tests/main/test-1/config deleted file mode 100644 index a7c1a5a8..00000000 --- a/integration-tests/main/test-1/config +++ /dev/null @@ -1 +0,0 @@ -COMPLETER_BASE_URL=${COMPLETER_BASE_URL} diff --git a/integration-tests/main/test-1/expected b/integration-tests/main/test-1/expected deleted file mode 100644 index 62f94575..00000000 --- a/integration-tests/main/test-1/expected +++ /dev/null @@ -1 +0,0 @@ -6 \ No newline at end of file diff --git a/integration-tests/main/test-1/input b/integration-tests/main/test-1/input deleted file mode 100644 index 00750edc..00000000 --- a/integration-tests/main/test-1/input +++ /dev/null @@ -1 +0,0 @@ -3 diff --git a/integration-tests/main/test-2/config b/integration-tests/main/test-2/config deleted file mode 100644 index 5646b494..00000000 --- a/integration-tests/main/test-2/config +++ /dev/null @@ -1 +0,0 @@ -GREETING=Salutations diff --git a/integration-tests/main/test-2/expected b/integration-tests/main/test-2/expected deleted file mode 100644 index 4082c5fd..00000000 --- a/integration-tests/main/test-2/expected +++ /dev/null @@ -1 +0,0 @@ -Salutations, world! \ No newline at end of file diff --git a/integration-tests/main/test-2/func.yaml b/integration-tests/main/test-2/func.yaml deleted file mode 100644 index aec91b13..00000000 --- a/integration-tests/main/test-2/func.yaml +++ /dev/null @@ -1,5 +0,0 @@ -name: test-2 -version: 0.0.1 -runtime: java9 -cmd: com.fnproject.fn.integration.test2.PlainFunction::handleRequest -format: http diff --git a/integration-tests/main/test-3/.gitignore b/integration-tests/main/test-3/.gitignore deleted file mode 100644 index a9c2e6bb..00000000 --- a/integration-tests/main/test-3/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/func.yaml -/pom.xml -/src/ diff --git a/integration-tests/main/test-3/expected b/integration-tests/main/test-3/expected deleted file mode 100644 index fda590c7..00000000 --- a/integration-tests/main/test-3/expected +++ /dev/null @@ -1 +0,0 @@ -Hello, function! \ No newline at end of file diff --git a/integration-tests/main/test-3/input b/integration-tests/main/test-3/input deleted file mode 100644 index e2dbde09..00000000 --- a/integration-tests/main/test-3/input +++ /dev/null @@ -1 +0,0 @@ -function diff --git a/integration-tests/main/test-3/pre-test.sh b/integration-tests/main/test-3/pre-test.sh deleted file mode 100755 index 40f613f4..00000000 --- a/integration-tests/main/test-3/pre-test.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -set -ex - -rm -rf Dockerfile func.yaml pom.xml src -FN_JAVA_FDK_VERSION=$(cat ../../../release.version) fn init --runtime=java9 --name app/test diff --git a/integration-tests/main/test-4/Dockerfile.custom b/integration-tests/main/test-4/Dockerfile.custom deleted file mode 100644 index e9077432..00000000 --- a/integration-tests/main/test-4/Dockerfile.custom +++ /dev/null @@ -1,4 +0,0 @@ -FROM fnproject/fn-java-fdk:jdk9-latest -WORKDIR /function -COPY target/*.jar /function/app/ -CMD ["com.fnproject.fn.integration.ExerciseEverything::handleRequest"] diff --git a/integration-tests/main/test-4/config b/integration-tests/main/test-4/config deleted file mode 100644 index a7c1a5a8..00000000 --- a/integration-tests/main/test-4/config +++ /dev/null @@ -1 +0,0 @@ -COMPLETER_BASE_URL=${COMPLETER_BASE_URL} diff --git a/integration-tests/main/test-4/expected.sh b/integration-tests/main/test-4/expected.sh deleted file mode 100755 index 1c081e44..00000000 --- a/integration-tests/main/test-4/expected.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -tail actual | grep 'Everything worked' diff --git a/integration-tests/main/test-4/input b/integration-tests/main/test-4/input deleted file mode 100644 index 8b137891..00000000 --- a/integration-tests/main/test-4/input +++ /dev/null @@ -1 +0,0 @@ - diff --git a/integration-tests/main/test-4/pre-test.sh b/integration-tests/main/test-4/pre-test.sh deleted file mode 100755 index f6e539dc..00000000 --- a/integration-tests/main/test-4/pre-test.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -set -ex - -cp Dockerfile.custom Dockerfile diff --git a/integration-tests/main/test-5/config b/integration-tests/main/test-5/config deleted file mode 100644 index a7c1a5a8..00000000 --- a/integration-tests/main/test-5/config +++ /dev/null @@ -1 +0,0 @@ -COMPLETER_BASE_URL=${COMPLETER_BASE_URL} diff --git a/integration-tests/main/test-5/expected.sh b/integration-tests/main/test-5/expected.sh deleted file mode 100755 index fd53afcd..00000000 --- a/integration-tests/main/test-5/expected.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -# Horrible bash checker... - -FOUND_FILENAME=`pwd`/success -rm -f "$FOUND_FILENAME" -ATTEMPT=0 -while [ ! -f "$FOUND_FILENAME" ] ; -do - sleep 1 - calls_found=`fn calls list "test-5" | grep "Status: success" | wc -l` - echo "$calls_found successful function calls found" - - # TODO: Remove this check when `fn logs` becomes reliable - if [[ -n `echo $calls_found | grep "3"` ]]; then - touch "$FOUND_FILENAME" - fi - - # TODO: Use this check instead when `fn logs` becomes reliable - # fn calls list "test-5" | while read k v - # do - # if [[ "$k" = "ID:" ]]; then id="$v"; fi - # if [[ -z "$k" ]]; then - # LOG=`fn logs get "test-5" "$id"` - # echo $LOG - # if [[ $LOG == *"Ran the hook."* ]]; then - # touch "$FOUND_FILENAME" - # fi - # fi - # done - - ATTEMPT=$((ATTEMPT + 1)) - if [ $ATTEMPT -ge 120 ]; - then - # echo "Did not find termination hook output" - echo "Termination hook was not called or failed" - exit 1 - fi -done diff --git a/integration-tests/main/test-5/func.yaml b/integration-tests/main/test-5/func.yaml deleted file mode 100644 index 1fa56725..00000000 --- a/integration-tests/main/test-5/func.yaml +++ /dev/null @@ -1,6 +0,0 @@ -version: 0.0.1 -runtime: java9 -cmd: com.fnproject.fn.integration.test_5.CompleterFunction::handleRequest -format: http -timeout: 120 -name: test-5 \ No newline at end of file diff --git a/integration-tests/main/test-5/input b/integration-tests/main/test-5/input deleted file mode 100644 index e69de29b..00000000 diff --git a/integration-tests/main/test-5/src/main/java/com/fnproject/fn/integration/test_5/CompleterFunction.java b/integration-tests/main/test-5/src/main/java/com/fnproject/fn/integration/test_5/CompleterFunction.java deleted file mode 100644 index e4d2e3c7..00000000 --- a/integration-tests/main/test-5/src/main/java/com/fnproject/fn/integration/test_5/CompleterFunction.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.fnproject.fn.integration.test_5; - -import com.fnproject.fn.api.flow.Flow; -import com.fnproject.fn.api.flow.Flows; - -public class CompleterFunction { - - public Integer handleRequest(String input) { - Flow fl = Flows.currentFlow(); - fl.addTerminationHook( (ignored) -> { System.err.println("Ran the hook."); }); - return fl.supply(() -> { Thread.sleep(1000); return 42; }).get(); - } - -} diff --git a/integration-tests/main/test-6/config b/integration-tests/main/test-6/config deleted file mode 100644 index a7c1a5a8..00000000 --- a/integration-tests/main/test-6/config +++ /dev/null @@ -1 +0,0 @@ -COMPLETER_BASE_URL=${COMPLETER_BASE_URL} diff --git a/integration-tests/main/test-6/expected.sh b/integration-tests/main/test-6/expected.sh deleted file mode 100755 index a7bc7275..00000000 --- a/integration-tests/main/test-6/expected.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash - -# Horrible bash checker... - -FOUND_FILENAME=`pwd`/success -rm -f "$FOUND_FILENAME" -ATTEMPT=0 -while [ ! -f "$FOUND_FILENAME" ] ; -do - sleep 1 - calls_found=`fn calls list "test-6" | grep "Status: success" | wc -l` - echo "$calls_found successful function calls found" - - fn calls list "test-6" | while read k v - do - if [[ "$k" = "ID:" ]]; then id="$v"; fi - if [[ -z "$k" ]]; then - LOG=`fn logs get "test-6" "$id"` - echo $LOG - if [[ $LOG == *"Caught timeout"* ]]; then - touch "$FOUND_FILENAME" - fi - fi - done - - ATTEMPT=$((ATTEMPT + 1)) - if [ $ATTEMPT -ge 120 ]; - then - echo "Did not find expected output" - exit 1 - fi -done diff --git a/integration-tests/main/test-6/func.yaml b/integration-tests/main/test-6/func.yaml deleted file mode 100644 index 0178652a..00000000 --- a/integration-tests/main/test-6/func.yaml +++ /dev/null @@ -1,6 +0,0 @@ -version: 0.0.1 -runtime: java9 -cmd: com.fnproject.fn.integration.test_6.CompleterFunction::handleRequest -format: http -timeout: 120 -name: test-6 \ No newline at end of file diff --git a/integration-tests/main/test-6/input b/integration-tests/main/test-6/input deleted file mode 100644 index e69de29b..00000000 diff --git a/integration-tests/main/test-6/pom.xml b/integration-tests/main/test-6/pom.xml deleted file mode 100644 index bacc9706..00000000 --- a/integration-tests/main/test-6/pom.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - 4.0.0 - - UTF-8 - 1.0.0-SNAPSHOT - - com.fnproject.fn - integration-test-6 - 1.0.0 - - jar - - - - com.fnproject.fn - api - ${fnproject.version} - - - com.fnproject.fn - testing - ${fnproject.version} - test - - - junit - junit - 4.12 - test - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.3 - - 1.8 - 1.8 - - - - org.apache.maven.plugins - maven-deploy-plugin - 2.8.2 - - true - - - - - - - - fn-maven-releases - https://dl.bintray.com/fnproject/fnproject - - - diff --git a/integration-tests/main/test-7/delete.sh b/integration-tests/main/test-7/delete.sh deleted file mode 100644 index 75a939ed..00000000 --- a/integration-tests/main/test-7/delete.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -ex - -fn routes delete "$TESTNAME" /test-7 diff --git a/integration-tests/main/test-7/deploy.sh b/integration-tests/main/test-7/deploy.sh deleted file mode 100755 index 515e2071..00000000 --- a/integration-tests/main/test-7/deploy.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -ex - -fn deploy --app "$TESTNAME" --local diff --git a/integration-tests/main/test-7/expected b/integration-tests/main/test-7/expected deleted file mode 100644 index 5dd01c17..00000000 --- a/integration-tests/main/test-7/expected +++ /dev/null @@ -1 +0,0 @@ -Hello, world! \ No newline at end of file diff --git a/integration-tests/main/test-7/pre-test.sh b/integration-tests/main/test-7/pre-test.sh deleted file mode 100755 index f4699600..00000000 --- a/integration-tests/main/test-7/pre-test.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -ex - -set -ex - -rm -rf Dockerfile func.yaml pom.xml src -FN_JAVA_FDK_VERSION=$(cat ../../../release.version) fn init --runtime=java --name app/hello diff --git a/integration-tests/main/test-7/run-test.sh b/integration-tests/main/test-7/run-test.sh deleted file mode 100755 index 0af4edc3..00000000 --- a/integration-tests/main/test-7/run-test.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -ex - -fn call "$TESTNAME" /test-7 > actual diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 30588095..ec2ccf8b 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -3,21 +3,64 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - - com.fnproject.fn - fdk - 1.0.0-SNAPSHOT - - 4.0.0 - com.fnproject.flow + com.fnproject integration-tests + 1.0.0-SNAPSHOT + + 3.6.2 + 2.5 + 2.9.10 + 4.12 + 2.22.1 + + jar - pom + + + commons-io + commons-io + ${commons-io.version} + + + junit + junit + ${junit.version} + + + org.assertj + assertj-core + ${assertj-core.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + 1.8 + 1.8 + + org.apache.maven.plugins maven-deploy-plugin @@ -26,7 +69,14 @@ true + + org.apache.maven.plugins + maven-surefire-plugin + ${surefire.version} + + false + + - diff --git a/integration-tests/post-configure-hook.sh b/integration-tests/post-configure-hook.sh deleted file mode 100755 index a81dbd98..00000000 --- a/integration-tests/post-configure-hook.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -# This hook runs in the test directory immediately prior to any "fn build" invocation. - -# Turn these lines in func.yaml: - -# name: jbloggs/fn-flows-function -# version: 0.0.1 - -# into these: - -# name: docker-registry:5000/jbloggs/fn-flows-function -# version: 4837492387439724389 <- whatever suffix is. - -set -ex - -docker push $(awk '/^name:/ { print $2 }' func.yaml):$SUFFIX -mv .func.yaml-old func.yaml diff --git a/integration-tests/pre-build-hook.sh b/integration-tests/pre-build-hook.sh deleted file mode 100755 index 1eb1d178..00000000 --- a/integration-tests/pre-build-hook.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash - -# This hook runs in the test directory immediately prior to any "fn build" invocation. - -# Turn these lines in func.yaml: - -# name: jbloggs/fn-flow-function -# version: 0.0.1 - -# into these: - -# name: docker-registry:5000/jbloggs/fn-flow-function -# version: 4837492387439724389 <- whatever suffix is. - -set -e - -while read key rest -do - case "$key" in - name:) - rest="docker-registry:5000/$rest" - ;; - version:) - rest="$SUFFIX" - ;; - esac - echo "$key $rest" -done < func.yaml > .func.yaml-new - -mv func.yaml .func.yaml-old -mv .func.yaml-new func.yaml diff --git a/integration-tests/run-all-tests.sh b/integration-tests/run-all-tests.sh deleted file mode 100755 index 68a54511..00000000 --- a/integration-tests/run-all-tests.sh +++ /dev/null @@ -1,100 +0,0 @@ -#!/bin/bash - -# Run all smoke-tests in parallel, recording their output. -# Report the results on any failures. - -source "$SCRIPT_DIR/lib.sh" - -set -ex - -# ---------------------------------------------------------------------- -# The following variables need to be set -# ---------------------------------------------------------------------- - -# This is an awful bashism -if [[ -z "${FN_API_URL+x}" ]]; then echo "Please set FN_API_URL"; exit 1; fi -if [[ -z "${COMPLETER_BASE_URL+x}" ]]; then echo "Please set COMPLETER_BASE_URL"; exit 1; fi - - -# ---------------------------------------------------------------------- -# Run each smoke-test in parallel -# ---------------------------------------------------------------------- - -printenv -fn apps list - -set +x - -SMOKE_HARNESS="$SCRIPT_DIR/smoke-test.sh" -export LIBFUNS="$SCRIPT_DIR/lib.sh" -export FN_TOKEN -export no_proxy=$no_proxy,127.0.0.1,10.167.103.241 - -cd "$SCRIPT_DIR" - -if [[ $# = 0 ]]; then - tests=main/test-* - show= - background='> "$d/output" 2>&1 &' -else - tests=$(find "$@" -type d -name test-\* -prune) - show='set -x' - background= -fi - -echo "Running tests: $tests" - -eval "$show" -for d in $tests -do - - rm -f "$d"/actual "$d"/output - eval "( - # Run the integration test - - cd \"$d\" && \"$SMOKE_HARNESS\" - ) $background" - -done -wait -set +x - - -# ---------------------------------------------------------------------- -# Report on results sequentially -# ---------------------------------------------------------------------- - -okay=1 -report() { - echo "Test $(basename "$1") expected -" - cat "$1/expected" - echo "Test $(basename "$1") actual -" - cat "$1/actual" - echo "Test $(basename "$1") output -" - cat "$1/output" - line -} - -for d in $tests -do - set +e - - if [[ -f "$d/failure" ]]; then - okay=0 - line - echo "Test $(basename "$d") failed:" - report "$d" - elif [[ -f "$d/success" ]]; then - line - echo "Test $(basename "$d") succeeded" - line - else - okay=0 - line - echo "**************** Test $(basename "$d") unknown status" - report "$d" - fi -done - -[[ $okay = 1 ]] || exit 1 -echo Success! diff --git a/integration-tests/run-local.sh b/integration-tests/run-local.sh deleted file mode 100755 index ec73332e..00000000 --- a/integration-tests/run-local.sh +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env bash - -# Set up a local test environment in order to run integration tests, -# then execute them. - -source "$(dirname "$0")/lib.sh" - -set -ex - -# ---------------------------------------------------------------------- -# The following variables may be set to parameterise the operation of this script -# ---------------------------------------------------------------------- - -: ${FUNCTIONS_DOCKER_IMAGE:=fnproject/fnserver} -: ${SUFFIX:=$(git rev-parse HEAD)} -: ${COMPLETER_DOCKER_IMAGE:=fnproject/flow} - -# ---------------------------------------------------------------------- -# Stand up a local staging maven directory, if needed -# ---------------------------------------------------------------------- - -if [[ -n "$REPOSITORY_LOCATION" ]]; then - REPO_CONTAINER_ID=$( - docker run -d \ - -v "$REPOSITORY_LOCATION":/repo:ro \ - -w /repo \ - --name repo-$SUFFIX \ - python:2.7 \ - python -mSimpleHTTPServer 18080 - ) - defer docker rm -f $REPO_CONTAINER_ID - REPO_INTERNAL_IP=$( - docker inspect \ - --type container \ - -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \ - $REPO_CONTAINER_ID - ) - export MAVEN_REPOSITORY_LOCATION="http://$REPO_INTERNAL_IP:18080" - export no_proxy="$no_proxy,$REPO_INTERNAL_IP" -fi - -# ---------------------------------------------------------------------- -# Stand up the functions platform -# ---------------------------------------------------------------------- - -docker pull $FUNCTIONS_DOCKER_IMAGE - -FUNCTIONS_CONTAINER_ID=$( - docker run -d \ - -p 8080:8080 \ - -v /var/run/docker.sock:/var/run/docker.sock \ - --name functions-$SUFFIX \ - -e FN_LOG_LEVEL=debug \ - $FUNCTIONS_DOCKER_IMAGE - ) -defer docker rm -f $FUNCTIONS_CONTAINER_ID -defer docker logs functions-$SUFFIX -defer echo ---- FUNCTIONS OUTPUT FOR TEST ----------------------------------------------------------- - -FUNCTIONS_HOST=$( - docker inspect \ - --type container \ - -f '{{range index .NetworkSettings.Ports "8080/tcp"}}{{.HostIp}}{{end}}' \ - $FUNCTIONS_CONTAINER_ID - ) - -FUNCTIONS_PORT=$( - docker inspect \ - --type container \ - -f '{{range index .NetworkSettings.Ports "8080/tcp"}}{{.HostPort}}{{end}}' \ - $FUNCTIONS_CONTAINER_ID - ) - -FUNCTIONS_INTERNAL_IP=$( - docker inspect \ - --type container \ - -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \ - $FUNCTIONS_CONTAINER_ID - ) - -export FN_API_URL="http://$FUNCTIONS_HOST:$FUNCTIONS_PORT" -export no_proxy="$no_proxy,$FUNCTIONS_HOST" - - -# ---------------------------------------------------------------------- -# Stand up the completer -# ---------------------------------------------------------------------- - -COMPLETER_CONTAINER_ID=$( - docker run -d \ - -p 8081 \ - --env API_URL=http://${FUNCTIONS_INTERNAL_IP}:8080 \ - --env no_proxy=$no_proxy,${FUNCTIONS_INTERNAL_IP} \ - --name flow-server-$SUFFIX \ - $COMPLETER_DOCKER_IMAGE - ) -defer docker rm -f $COMPLETER_CONTAINER_ID -defer docker logs $COMPLETER_CONTAINER_ID -defer echo ---- COMPLETER OUTPUT FOR TEST ----------------------------------------------------------- - -COMPLETER_HOST=$( - docker inspect \ - --type container \ - -f '{{range index .NetworkSettings.Ports "8081/tcp"}}{{.HostIp}}{{end}}' \ - $COMPLETER_CONTAINER_ID - ) - -COMPLETER_PORT=$( - docker inspect \ - --type container \ - -f '{{range index .NetworkSettings.Ports "8081/tcp"}}{{.HostPort}}{{end}}' \ - $COMPLETER_CONTAINER_ID - ) - -COMPLETER_INTERNAL_IP=$( - docker inspect \ - --type container \ - -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \ - $COMPLETER_CONTAINER_ID - ) - -export COMPLETER_BASE_URL=http://$COMPLETER_INTERNAL_IP:8081 -export no_proxy="$no_proxy,$COMPLETER_HOST" - - -# ---------------------------------------------------------------------- -# Wait for the containers to become ready -# ---------------------------------------------------------------------- - -export HTTP_PROXY="$http_proxy" -export HTTPS_PROXY="$https_proxy" -export NO_PROXY="$no_proxy" - -wait_for_http "$FN_API_URL" -wait_for_http "http://$COMPLETER_HOST:$COMPLETER_PORT/ping" - -set +x - -"$SCRIPT_DIR/run-all-tests.sh" "$@" diff --git a/integration-tests/run-remote.sh b/integration-tests/run-remote.sh deleted file mode 100755 index 69a1ac4f..00000000 --- a/integration-tests/run-remote.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash - -# Set up a local test environment in order to run integration tests, -# then execute them. - -source "$(dirname "$0")/lib.sh" - -set -ex - -# ---------------------------------------------------------------------- -# The following variables may be set to parameterise the operation of this script -# ---------------------------------------------------------------------- - -export SUFFIX=$(git rev-parse HEAD) -export FN_TOKEN=${FN_TOKEN:-$(cat ~/.fn-token)} - - -# ---------------------------------------------------------------------- -# The following variables should be set in the integration environment -# ---------------------------------------------------------------------- - -export FN_API_URL=$(cat ~/.fn-api-url) -export COMPLETER_BASE_URL=$(cat ~/.fn-flow-base-url) - - -# We need to push our images into the test environment, so let's ensure that our tunnel is set up -systemctl --user restart ssh-tunnels - -# Ensure we have the hooks we want in place -export PRE_BUILD_HOOK="$SCRIPT_DIR/pre-build-hook.sh" -export POST_CONFIGURE_HOOK="$SCRIPT_DIR/post-configure-hook.sh" - -export HTTP_PROXY="$http_proxy" -export HTTPS_PROXY="$https_proxy" -export NO_PROXY="$no_proxy" - -set +x - -"$SCRIPT_DIR/run-all-tests.sh" "$@" diff --git a/integration-tests/run_tests_ci.sh b/integration-tests/run_tests_ci.sh new file mode 100755 index 00000000..21145e79 --- /dev/null +++ b/integration-tests/run_tests_ci.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +set -e +set -x + + +cd $(dirname $0) + +if [ -z ${REPOSITORY_LOCATION} ]; then + echo no REPOSITORY_LOCATION is specified in env using /tmp/staging_repo as default + + REPOSITORY_LOCATION=/tmp/staging_repo +fi + + +docker rm -f fn_mvn_repo || true +docker run -d \ + -v "${REPOSITORY_LOCATION}":/repo:ro \ + -w /repo -p18080:18080 \ + --name fn_mvn_repo \ + python:2.7 \ + python -mSimpleHTTPServer 18080 + + +until $(curl --output /dev/null --silent --fail http://localhost:18080); do + printf '.' + sleep 1 +done + + +# Start Fn +fn stop || true +fn start --log-level=debug -d --env-file fnserver.env + +until $(curl --output /dev/null --silent --fail http://localhost:8080/); do + printf '.' + sleep 1 +done + + +export FN_LOG_FILE=/tmp/fn.log +docker logs -f fnserver >& ${FN_LOG_FILE} & + +FNSERVER_IP=$(docker inspect --type container -f '{{.NetworkSettings.IPAddress}}' fnserver) + + + +docker rm -f flowserver || true +docker run --rm -d \ + -p 8081:8081 \ + -e API_URL="http://${FNSERVER_IP}:8080/invoke" \ + -e no_proxy=${FNSERVER_IP} \ + --name flowserver \ + fnproject/flow:latest + +until $(curl --output /dev/null --silent --fail http://localhost:8081/ping); do + printf '.' + sleep 1 +done +export FLOW_LOG_FILE=/tmp/flow.log + +docker logs -f flowserver >& ${FLOW_LOG_FILE} & +set +e + + +if [ $(uname -s) == "Darwin" ] ; then + DOCKER_LOCALHOST=docker.for.mac.host.internal +else + DOCKER_LOCALHOST=$(ifconfig eth0| grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1') +fi + +export DOCKER_LOCALHOST + +REPO_IP=$(docker inspect --type container -f '{{.NetworkSettings.IPAddress}}' fn_mvn_repo) +MAVEN_REPOSITORY="http://${REPO_IP}:18080" +export MAVEN_REPOSITORY +COMPLETER_IP=$(docker inspect --type container -f '{{.NetworkSettings.IPAddress}}' flowserver) +COMPLETER_BASE_URL="http://${COMPLETER_IP}:8081" +export COMPLETER_BASE_URL + +export no_proxy="${no_proxy},${DOCKER_LOCALHOST},${COMPLETER_IP},${REPO_IP}" + + + + +echo "Running tests" +mvn test +result=$? + + + +docker rm -f flowserver +docker rm -f fnserver +docker rm -rf fn_mvn_repo + +exit $result diff --git a/integration-tests/smoke-test.sh b/integration-tests/smoke-test.sh deleted file mode 100755 index 234b5185..00000000 --- a/integration-tests/smoke-test.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/bin/bash - -. "$LIBFUNS" - -# Run an individual smoketest - -# Environmental requirements: -# - cwd is the directory of the smoketest to run -# - LIBFUNS points to the shell helper library -# - up-to-date "fn" command on the PATH -# - FN_TOKEN is set to something that the functions platform will approve -# - FN_API_URL points to the functions platform endpoint -# - any http_proxy, etc. settings are correct to permit access to that endpoint and any maven repos required by fn build -# - COMPLETER_BASE_URL is set to a value that should be configured on the target function -# - MAVEN_REPOSITORY_LOCATION, if set, corresponds to the URL that should be replaced in the test pom files. -# - the runtime docker image is up-to-date - -rm -f success failure Dockerfile -export TESTNAME="$(basename $(pwd))" -set -ex - -if [ -f pre-test.sh ]; then - ./pre-test.sh -fi - -# Replace the maven repo with a staging location, if required -if [ -n "$MAVEN_REPOSITORY_LOCATION" ]; then - sed -i.bak \ - -e "s|https://dl.bintray.com/fnproject/fnproject|$MAVEN_REPOSITORY_LOCATION|g" \ - pom.xml - rm pom.xml.bak -fi - -# Build the integration test - -[[ -n "$PRE_BUILD_HOOK" ]] && $PRE_BUILD_HOOK - -fn -v build --no-cache >build-output 2>&1 || { - echo "Test function build failed:" - cat build-output - exit 1 -} - -if [ -f config ]; then - fn apps create "$TESTNAME" $(echo $(prefix_lines --config config)) -else - fn apps create "$TESTNAME" -fi - -if [[ -x deploy.sh ]] -then - ./deploy.sh -else - fn deploy --app "$TESTNAME" --local -fi - -[[ -n "$POST_CONFIGURE_HOOK" ]] && $POST_CONFIGURE_HOOK - -fn apps inspect "$TESTNAME" -[[ -x route-create.sh ]] || fn routes inspect "$TESTNAME" "$TESTNAME" - -if [[ -x run-test.sh ]] -then - ./run-test.sh -else - curl -v "$FN_API_URL/r/$TESTNAME/$TESTNAME" -d @input > actual -fi - -if [[ -x expected.sh ]] -then - ./expected.sh && touch success || touch failure -else - diff --ignore-all-space -u expected actual && touch success || touch failure -fi - -set +x -fn calls list "$TESTNAME" | while read k v -do - echo "$k $v" - if [[ "$k" = "ID:" ]]; then id="$v"; fi - if [[ -z "$k" ]]; then - echo '[[[' - fn logs get "$TESTNAME" "$id" - echo ']]]' - echo - fi -done - -set -x - -if [[ -x delete.sh ]] -then - ./delete.sh -fi -fn apps delete "$TESTNAME" diff --git a/integration-tests/src/main/java/com/fnproject/fn/integrationtest/IntegrationTestRule.java b/integration-tests/src/main/java/com/fnproject/fn/integrationtest/IntegrationTestRule.java new file mode 100644 index 00000000..ed2463f8 --- /dev/null +++ b/integration-tests/src/main/java/com/fnproject/fn/integrationtest/IntegrationTestRule.java @@ -0,0 +1,351 @@ +package com.fnproject.fn.integrationtest; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.input.Tailer; +import org.apache.commons.io.input.TailerListenerAdapter; +import org.assertj.core.api.Assertions; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +/** + * Wrapper around fn cli to invoke function integration tests. + * Created on 14/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class IntegrationTestRule implements TestRule { + + + private static final String repoPlaceholder = "https://dl.bintray.com/fnproject/fnproject"; + private static final String versionPlaceholder = "1.0.0-SNAPSHOT"; + private static final String snapshotPlaceholderRegex = ".*"; + private int appCount = 0; + private String testName; + + + private final List cleanupDirs = new ArrayList<>(); + private final List cleanupApps = new ArrayList<>(); + + public String getFlowURL() { + String url = System.getenv("COMPLETER_BASE_URL"); + if (url == null) { + return "http://" + getDockerLocalhost() + ":8081"; + } + return url; + } + + /** + * Returns a hostname tha resolves to the test host from within a docker container + */ + public String getDockerLocalhost() { + String dockerLocalhost = System.getenv("DOCKER_LOCALHOST"); + if (dockerLocalhost == null) { + String osName = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH); + + if (osName.contains("darwin") || osName.contains("mac")) { + return "docker.for.mac.host.internal"; + } else + throw new RuntimeException("Unable to determine docker localhost address - set 'DOCKER_LOCALHOST' env variable to the docker host network address "); + } + return dockerLocalhost; + } + + private String getLocalFnRepo() { + String envRepo = System.getenv("MAVEN_REPOSITORY"); + if (envRepo == null) { + envRepo = "http://" + getDockerLocalhost() + ":18080"; + } + return envRepo; + } + + + private String getProjectVersion() { + String version = System.getenv("FN_JAVA_FDK_VERSION"); + + if (version == null) { + version = "1.0.0-SNAPSHOT"; + } + return version; + } + + private String getFnLogFile() { + return System.getenv("FN_LOG_FILE"); + } + + private String getFlowLogFile() { + return System.getenv("FLOW_LOG_FILE"); + } + + private String getFnCmd() { + String cmd = System.getenv("FN_CMD"); + if (cmd == null) { + return "fn"; + } + return cmd; + } + + + public static class CmdResult { + private final String cmd; + private final boolean success; + private final String stdout; + private final String stderr; + + private CmdResult(String cmd, boolean success, String stdout, String stderr) { + this.success = success; + this.stdout = stdout; + this.stderr = stderr; + this.cmd = cmd; + } + + boolean isSuccess() { + return success; + } + + public String getStdout() { + return stdout; + } + + public String getStderr() { + return stderr; + } + + public String toString() { + return "CmdResult: cmd=" + cmd + ", success=" + success + ", stdout=" + stdout + ", stderr=" + stderr; + } + } + + public class TestContext { + + private File baseDir; + private final String testName; + + public TestContext(File baseDir, String testName) { + this.baseDir = baseDir; + this.testName = testName; + } + + /** + * Copies the contents of a given directory (relative to test root) into the temporary test directory + * + * @param location local directory + * @return + * @throws IOException + */ + public TestContext withDirFrom(String location) throws IOException { + FileUtils.copyDirectory(new File(location), baseDir); + return this; + } + + public CmdResult runFnWithInput(String input, String... args) throws Exception { + CmdResult res = runFnWithInputAllowError(input, args); + + if (res.isSuccess() == false) { + System.err.println("FN FAIL!"); + System.err.println(res.toString()); + } + + Assertions.assertThat(res.isSuccess()).withFailMessage("Expected command '" + res.cmd + "' to return 0." + "FN FAIL: " + res).isTrue(); + return res; + } + + /** + * Runs the configured Fn command with input returning a process result + * + * @param input the input string ot pass as fn stdin + * @param args args to fn (excluding the fn command itself) + * @return a command result to get the result of a command + * @throws Exception + */ + public CmdResult runFnWithInputAllowError(String input, String... args) throws Exception { + List cmd = new ArrayList<>(); + cmd.add(getFnCmd()); + cmd.addAll(Arrays.asList(args)); + + System.err.println("Running '" + String.join(" ", cmd) + "'"); + ProcessBuilder pb = new ProcessBuilder(cmd); + pb.directory(baseDir); + + if (System.getenv("FN_JAVA_FDK_VERSION") == null) { + // this means that FN init will pick up the local version not the latest. + pb.environment().put("FN_JAVA_FDK_VERSION", "1.0.0-SNAPSHOT"); + } + + // Sort of a hack for local mac running with a proxy + String noProxy = Optional.ofNullable(System.getenv("no_proxy")).map((f) -> f + ",").orElse("") + getDockerLocalhost(); + System.err.printf("setting no_proxy '%s'\n",noProxy); + pb.environment().put("no_proxy", noProxy); + + Process p = pb.start(); + + p.getOutputStream().write(input.getBytes()); + p.getOutputStream().close(); + + CompletableFuture stderr = new CompletableFuture<>(); + + new Thread(() -> { + + try { + BufferedReader bri = new BufferedReader + (new InputStreamReader(p.getErrorStream())); + + StringBuilder output = new StringBuilder(); + String line; + while ((line = bri.readLine()) != null) { + System.err.println("FN ERR: " + line); + output.append(line); + } + stderr.complete(output.toString()); + } catch (IOException e) { + stderr.completeExceptionally(e); + } + }).start(); + + BufferedReader bri = new BufferedReader + (new InputStreamReader(p.getInputStream())); + + StringBuilder output = new StringBuilder(); + String line; + while ((line = bri.readLine()) != null) { + System.err.println("FN OUT: " + line); + output.append(line); + } + p.waitFor(600, TimeUnit.SECONDS); + System.err.println("Command '" + String.join(" ", cmd) + "' with code " + p.exitValue()); + + return new CmdResult(String.join(" ", cmd), p.exitValue() == 0, output.toString(), stderr.get()); + } + + /** + * Runs the configure `fn` command with given arguments fails if the command returns without succes + * + * @param args + * @return a command result capturinng the output of fn + * @throws Exception + */ + public CmdResult runFn(String... args) throws Exception { + return runFnWithInput("", args); + } + + + /** + * Rewrites the POM to reflect the correct target repo + */ + public TestContext rewritePOM() throws Exception { + File pomFile = new File(baseDir, "pom.xml"); + + String pomFileContent = FileUtils.readFileToString(pomFile, StandardCharsets.UTF_8); + String newPomContent = pomFileContent.replace(repoPlaceholder, "" + getLocalFnRepo() + ""); + Assertions.assertThat(newPomContent).withFailMessage("No placeholder found in POM").isNotEqualTo(pomFileContent); + + String versionPomContent = newPomContent.replace(versionPlaceholder, "" + getProjectVersion() + ""); + + versionPomContent = versionPomContent.replaceFirst(snapshotPlaceholderRegex, "true"); + + System.err.println(versionPomContent); + FileUtils.writeStringToFile(pomFile, versionPomContent, StandardCharsets.UTF_8); + return this; + } + + + /** + * Gets the app name you should use for tests. + * + * @return + */ + public String appName() { + return this.testName; + } + + public TestContext mkdir(String name) { + new File(baseDir, name).mkdir(); + return this; + } + + public TestContext cd(String dir) { + baseDir = new File(baseDir, dir); + return this; + } + } + + + /** + * creates a new test contesxt + * + * @return + * @throws IOException + */ + public TestContext newTest() throws IOException { + Path tmpDir = Files.createTempDirectory("fnitest"); + cleanupDirs.add(tmpDir.toFile()); + String appName = testName + appCount++; + cleanupApps.add(appName); + return new TestContext(tmpDir.toFile(), appName); + } + + + @Override + public Statement apply(Statement statement, Description description) { + + return new Statement() { + @Override + public void evaluate() throws Throwable { + testName = description.getMethodName(); + StringBuilder fnOutput = new StringBuilder(); + StringBuilder flowOutput = new StringBuilder(); + + Tailer fnTailer = null; + if (getFnLogFile() != null) { + fnTailer = Tailer.create(new File(getFnLogFile()), new TailerListenerAdapter() { + @Override + public void handle(final String line) { + System.err.println("FNSRV:" + line); + fnOutput.append(line); + } + }, 10, true); + } + + Tailer flowTailer = null; + + if (getFlowLogFile() != null) { + flowTailer = Tailer.create(new File(getFlowLogFile()), new TailerListenerAdapter() { + @Override + public void handle(final String line) { + System.err.println("FLOW:" + line); + fnOutput.append(line); + } + }, 10, true); + } + + try { + statement.evaluate(); + } finally { + + for (File cleanup : cleanupDirs) { + FileUtils.deleteDirectory(cleanup); + } + + if (fnTailer != null) { + fnTailer.stop(); + } + + if (flowTailer != null) { + flowTailer.stop(); + } + } + } + }; + } +} diff --git a/integration-tests/src/test/java/com/fnproject/fn/integrationtest/FlowTest.java b/integration-tests/src/test/java/com/fnproject/fn/integrationtest/FlowTest.java new file mode 100644 index 00000000..45dd0bc9 --- /dev/null +++ b/integration-tests/src/test/java/com/fnproject/fn/integrationtest/FlowTest.java @@ -0,0 +1,100 @@ +package com.fnproject.fn.integrationtest; + +import com.fnproject.fn.integrationtest.IntegrationTestRule.CmdResult; +import com.sun.net.httpserver.HttpServer; +import org.junit.Rule; +import org.junit.Test; + +import java.net.InetSocketAddress; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Created on 14/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class FlowTest { + + @Rule + public final IntegrationTestRule testRule = new IntegrationTestRule(); + + + @Test + public void shouldInvokeBasicFlow() throws Exception { + IntegrationTestRule.TestContext tc = testRule.newTest(); + tc.withDirFrom("funcs/flowBasic").rewritePOM(); + tc.runFn("--verbose", "deploy", "--create-app", "--app", tc.appName(), "--local"); + tc.runFn("config", "app", tc.appName(), "COMPLETER_BASE_URL", testRule.getFlowURL()); + CmdResult r = tc.runFnWithInput("1", "invoke", tc.appName(), "flowbasic"); + assertThat(r.getStdout()).isEqualTo("4"); + } + + + @Test + public void shouldInvokeBasicFlowJDK8() throws Exception { + IntegrationTestRule.TestContext tc = testRule.newTest(); + tc.withDirFrom("funcs/flowBasicJDK8").rewritePOM(); + tc.runFn("--verbose", "deploy", "--create-app", "--app", tc.appName(), "--local"); + tc.runFn("config", "app", tc.appName(), "COMPLETER_BASE_URL", testRule.getFlowURL()); + CmdResult r = tc.runFnWithInput("1", "invoke", tc.appName(), "flowbasicj8"); + assertThat(r.getStdout()).isEqualTo("4"); + } + + + @Test + public void shouldExerciseAllFlow() throws Exception { + IntegrationTestRule.TestContext tc = testRule.newTest(); + tc.withDirFrom("funcs/flowAllFeatures").rewritePOM(); + tc.runFn("--verbose", "deploy", "--create-app", "--app", tc.appName(), "--local"); + tc.runFn("config", "app", tc.appName(), "COMPLETER_BASE_URL", testRule.getFlowURL()); + CmdResult r = tc.runFnWithInput("1", "invoke", tc.appName(), "flowallfeatures"); + assertThat(r.getStdout()).contains("Everything worked"); + } + + + @Test + public void shouldCallExitHooks() throws Exception { + CompletableFuture done = new CompletableFuture<>(); + + HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0); + server.createContext("/exited", httpExchange -> { + done.complete(true); + String resp = "ok"; + httpExchange.sendResponseHeaders(200, resp.length()); + httpExchange.getResponseBody().write(resp.getBytes()); + httpExchange.getResponseBody().close(); + }); + server.setExecutor(null); // creates a default executor + server.start(); + try { + IntegrationTestRule.TestContext tc = testRule.newTest(); + tc.withDirFrom("funcs/flowExitHooks").rewritePOM(); + tc.runFn("--verbose", "build", "--no-cache"); + tc.runFn("--verbose", "deploy", "--create-app", "--app", tc.appName(), "--local"); + tc.runFn("config", "app", tc.appName(), "COMPLETER_BASE_URL", testRule.getFlowURL()); + tc.runFn("config", "app", tc.appName(), "TERMINATION_HOOK_URL", "http://" + testRule.getDockerLocalhost() + ":" + 8000 + "/exited"); + CmdResult r = tc.runFnWithInput("1", "invoke", tc.appName(), "flowexithooks"); + assertThat(r.getStdout()).contains("42"); + + assertThat(done.get(10, TimeUnit.SECONDS)).withFailMessage("Expected callback within 10 seconds").isTrue(); + + } finally { + server.stop(0); + } + } + + + @Test + public void shouldHandleTimeouts() throws Exception { + IntegrationTestRule.TestContext tc = testRule.newTest(); + tc.withDirFrom("funcs/flowTimeouts").rewritePOM(); + tc.runFn("--verbose", "deploy", "--create-app", "--app", tc.appName(), "--local"); + tc.runFn("config", "app", tc.appName(), "COMPLETER_BASE_URL", testRule.getFlowURL()); + CmdResult r = tc.runFn("invoke", tc.appName(), "flowtimeouts"); + assertThat(r.getStdout()).contains("timeout"); + } + +} diff --git a/integration-tests/src/test/java/com/fnproject/fn/integrationtest/FunctionsTest.java b/integration-tests/src/test/java/com/fnproject/fn/integrationtest/FunctionsTest.java new file mode 100644 index 00000000..ab73d8b8 --- /dev/null +++ b/integration-tests/src/test/java/com/fnproject/fn/integrationtest/FunctionsTest.java @@ -0,0 +1,97 @@ +package com.fnproject.fn.integrationtest; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fnproject.fn.integrationtest.IntegrationTestRule.CmdResult; +import org.junit.Rule; +import org.junit.Test; + +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; +import java.util.logging.Level; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Created on 14/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class FunctionsTest { + + @Rule + public final IntegrationTestRule testRule = new IntegrationTestRule(); + + + @Test + public void shouldCallExistingFn() throws Exception { + IntegrationTestRule.TestContext tc = testRule.newTest(); + tc.withDirFrom("funcs/simpleFunc").rewritePOM(); + + tc.runFn("--verbose", "deploy", "--create-app", "--app", tc.appName(), "--local"); + tc.runFn("config", "app", tc.appName(), "GREETING", "Salutations"); + + CmdResult r1 = tc.runFnWithInput("", "invoke", tc.appName(), "simplefunc"); + assertThat(r1.getStdout()).isEqualTo("Salutations, world!"); + + CmdResult r2 = tc.runFnWithInput("tests", "invoke", tc.appName(), "simplefunc"); + assertThat(r2.getStdout()).isEqualTo("Salutations, tests!"); + + } + + @Test() + public void checkBoilerPlate() throws Exception { + for (String runtime : new String[]{"java8", "java11"}) { + IntegrationTestRule.TestContext tc = testRule.newTest(); + String fnName = "bp" + runtime; + tc.runFn("init", "--runtime", runtime, "--name", fnName); + tc.rewritePOM(); + tc.runFn("--verbose", "deploy", "--create-app", "--app", tc.appName(), "--local"); + CmdResult rs = tc.runFnWithInput("wibble", "invoke", tc.appName(), fnName); + assertThat(rs.getStdout()).contains("Hello, wibble!"); + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class InspectResponse { + + public Map annotations = new HashMap<>(); + } + + @Test() + public void shouldHandleTrigger() throws Exception { + IntegrationTestRule.TestContext tc = testRule.newTest(); + tc.withDirFrom("funcs/httpgwfunc").rewritePOM(); + tc.runFn("--verbose", "deploy", "--create-app", "--app", tc.appName(), "--local"); + + // Get me the trigger URL + CmdResult output = tc.runFn("inspect", "trigger", tc.appName(), "httpgwfunc", "trig"); + + ObjectMapper om = new ObjectMapper(); + InspectResponse resp = om.readValue(output.getStdout(), InspectResponse.class); + String dest = resp.annotations.get("fnproject.io/trigger/httpEndpoint"); + assertThat(dest).withFailMessage("Missing trigger endpoint annotation").isNotNull(); + + String url = dest + "?q1=a&q2=b"; + URL invokeURL = URI.create(url).toURL(); + + System.out.println("calling " + url); + HttpURLConnection con = (HttpURLConnection) invokeURL.openConnection(); + + con.setRequestMethod("POST"); + con.addRequestProperty("Foo","bar"); + + + assertThat(con.getResponseCode()).isEqualTo(202); + assertThat(con.getHeaderField("GotMethod")).isEqualTo("POST"); + assertThat(con.getHeaderField("GotURL")).isEqualTo(url); + assertThat(con.getHeaderField("GotHeader")).isEqualTo("bar"); + assertThat(con.getHeaderField("MyHTTPHeader")).isEqualTo("foo"); + + } + +} diff --git a/pom.xml b/pom.xml index 837a23bc..187182ee 100644 --- a/pom.xml +++ b/pom.xml @@ -1,42 +1,176 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 fdk com.fnproject.fn pom 1.0.0-SNAPSHOT + + api runtime - fn-spring-cloud-function + testing-core + testing-junit4 testing + flow-api + flow-runtime + flow-testing + fn-spring-cloud-function examples - integration-tests UTF-8 - 0.7.9 - 1.7.25 - 2.5 - 2.8.7 - 2.8.47 - 3.6.2 - 4.4.6 + UTF-8 - 1.16.0 + 3.10.0 + 2.6 + 4.4.10 + 2.9.10 + 0.8.1 + 9.4.12.v20180830 4.12 + 2.21.0 + 1.4.0 + 1.7.25 + 2.22.1 + 1.16.0 + + + + + + com.fnproject.fn + api + ${project.version} + + + + com.fnproject.fn + flow-api + ${project.version} + + + com.fnproject.fn + flow-runtime + ${project.version} + + + com.fnproject.fn + runtime + ${project.version} + + + com.fnproject.fn + testing + ${project.version} + + + com.fnproject.fn + testing-core + ${project.version} + + + com.fnproject.fn + testing-junit4 + ${project.version} + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + org.apache.httpcomponents + httpcore + ${httpcore.version} + + + org.apache.httpcomponents + httpmime + 4.5.6 + + + commons-io + commons-io + ${commons-io.version} + + + commons-logging + commons-logging + 1.2 + + + net.jodah + typetools + 0.5.0 + + + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.assertj + assertj-core + ${assertj-core.version} + test + + + junit + junit + ${junit.version} + test + + + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.0.1 + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + + maven-dependency-plugin + 3.1.1 + + + org.netbeans.tools + sigtest-maven-plugin + 1.0 + + + org.pitest + pitest-maven + ${pitest.version} + + + org.apache.maven.plugins maven-compiler-plugin - 3.6.1 + 3.8.0 javac-with-errorprone true @@ -47,19 +181,19 @@ org.codehaus.plexus plexus-compiler-javac-errorprone - 2.8 + 2.8.4 com.google.errorprone error_prone_core - 2.0.21 + 2.3.1 org.apache.maven.plugins maven-source-plugin - 3.0.0 + 3.0.1 attach-sources @@ -103,13 +237,39 @@ + + + org.pitest + pitest-maven + ${pitest.version} + + com.spotify dockerfile-maven-extension - 1.3.1 + 1.4.3 + + + + + _qm-qs + + false + + + + + org.codehaus.mojo + versions-maven-plugin + 2.5 + + + + + diff --git a/release.version b/release.version index 4bf1778f..882cfd44 100644 --- a/release.version +++ b/release.version @@ -1 +1 @@ -1.0.60 +1.0.103 diff --git a/runtime/Dockerfile-jdk9 b/runtime/Dockerfile-jdk9 deleted file mode 100644 index 886bebc6..00000000 --- a/runtime/Dockerfile-jdk9 +++ /dev/null @@ -1,18 +0,0 @@ -FROM openjdk:9-slim -COPY target/runtime-*.jar target/dependency/*.jar /function/runtime/ - -RUN ["/usr/bin/java", "-Xshare:dump"] - -# UseCGroupMemoryLimitForHeap looks up /sys/fs/cgroup/memory/memory.limit_in_bytes inside the container to determine -# what the heap should be set to. This is an experimental feature at the moment, thus we need to unlock to use it. -# -# MaxRAMFraction is used modify the heap size and it is used as a denominator where the numerator is phys_mem. -# It seems that this value is a uint in the JVM code, thus can only specify 1 => 100%, 2 => 50%, 3 => 33.3%, 4 => 25% -# and so on. -# -# SerialGC is used here as it's likely that we'll be running many JVMs on the same host machine and it's also likely -# that the number of JVMs will outnumber the number of available processors. -# -# The max memory value obtained with these args seem to be okay for most memory limits. The exception is when the -# memory limit is set to 128MiB, in which case maxMemory returns roughly half. -ENTRYPOINT [ "/usr/bin/java", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseCGroupMemoryLimitForHeap", "-XX:MaxRAMFraction=2", "-XX:+UseSerialGC", "-Xshare:on", "-cp", "/function/app/*:/function/runtime/*", "com.fnproject.fn.runtime.EntryPoint" ] diff --git a/runtime/pom.xml b/runtime/pom.xml index 89800faa..dca46c91 100644 --- a/runtime/pom.xml +++ b/runtime/pom.xml @@ -11,77 +11,73 @@ runtime - - UTF-8 - - - com.fnproject.fn api - ${project.version} - com.fasterxml.jackson.core jackson-databind - ${jackson.version} - org.apache.httpcomponents httpcore - ${httpcore.version} + commons-io commons-io - ${commons-io.version} net.jodah typetools - 0.5.0 org.mockito mockito-core - ${mockito.version} + test + + + junit + junit test org.assertj assertj-core - ${assertj-core.version} test - - junit - junit - 4.12 + org.apache.httpcomponents + httpmime + test + + + org.eclipse.jetty + jetty-client + ${jetty.version} test - org.apache.httpcomponents - httpmime - 4.5.3 + org.eclipse.jetty + jetty-unixsocket + ${jetty.version} test + maven-dependency-plugin - 3.0.1 copy-dependencies diff --git a/runtime/src/main/c/.gitignore b/runtime/src/main/c/.gitignore new file mode 100644 index 00000000..417eb980 --- /dev/null +++ b/runtime/src/main/c/.gitignore @@ -0,0 +1,4 @@ +build/ +libfnunixsocket.so +libfnunixsocket.dylib +cmake-build-debug diff --git a/runtime/src/main/c/CMakeLists.txt b/runtime/src/main/c/CMakeLists.txt new file mode 100644 index 00000000..8d2746a7 --- /dev/null +++ b/runtime/src/main/c/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 2.8) +project(fnunixsocket) +set(CMAKE_BUILD_TYPE Release) +find_package(JNI REQUIRED) +include_directories(${JNI_INCLUDE_DIRS}) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall") + + +add_library(fnunixsocket SHARED unix_socket.c) diff --git a/runtime/src/main/c/Dockerfile-buildimage b/runtime/src/main/c/Dockerfile-buildimage new file mode 100644 index 00000000..7cd37f95 --- /dev/null +++ b/runtime/src/main/c/Dockerfile-buildimage @@ -0,0 +1,8 @@ +FROM oraclelinux:7.5 + +RUN yum install -y gcc cmake java-1.8.0-openjdk-devel.x86_64 make +RUN yum install -y gcc-c++ + +RUN mkdir /build +WORKDIR /build + diff --git a/runtime/src/main/c/README.md b/runtime/src/main/c/README.md new file mode 100644 index 00000000..7e4efe65 --- /dev/null +++ b/runtime/src/main/c/README.md @@ -0,0 +1,19 @@ +# Native components for Fn unix socket protocol + +This is a very simple JNI binding to expose unix sockets to the Fn runtime + +## Building + +you can rebuild a linux version (for the FDK itself) of the JNI library using `./rebuild_so.sh` this runs `buildit.sh` in a suitable docker container + +For testing on a mac you can also compile locally by running `buildit.sh`, you will need at least: + +* XCode compiler toolchain +* cmake +* make +* a JDK installed (for cmake JNI) + + +Current issues: +* This is using old-style JNI array passing which is slow - it should be using native buffers +* Doesn't support non-blocking operations, specifically reads and writes which will block indefinitely \ No newline at end of file diff --git a/runtime/src/main/c/buildit.sh b/runtime/src/main/c/buildit.sh new file mode 100755 index 00000000..09958c0a --- /dev/null +++ b/runtime/src/main/c/buildit.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -e + +src_dir=$(pwd) +build_dir=${src_dir}/build/$(uname -s| tr '[:upper:]' '[:lower:]') + +mkdir -p ${build_dir} +( + cd ${build_dir} + cmake ${src_dir} + + make +) +mv ${build_dir}/libfnunixsocket.* ${src_dir} \ No newline at end of file diff --git a/runtime/src/main/c/rebuild_so.sh b/runtime/src/main/c/rebuild_so.sh new file mode 100755 index 00000000..05c6edfd --- /dev/null +++ b/runtime/src/main/c/rebuild_so.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + + +mydir=$(cd "$(dirname "$0")"; pwd) +cd ${mydir} + +set -e +docker build -t fdk_c_build -f Dockerfile-buildimage . + +docker run -v $(pwd):/build fdk_c_build ./buildit.sh diff --git a/runtime/src/main/c/unix_socket.c b/runtime/src/main/c/unix_socket.c new file mode 100644 index 00000000..f773d715 --- /dev/null +++ b/runtime/src/main/c/unix_socket.c @@ -0,0 +1,560 @@ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef US_DEBUG +#define debuglog(...) fprintf (stderr, __VA_ARGS__) +#else +#define debuglog(...) + +#endif + +/** + * Throws com.fnproject.fn.runtime.ntv.UnixSocetException, adding strerr(errno) as the second arg if that is set + * @param jenv java env + * @param message message to send + */ +void throwIOException(JNIEnv *jenv, const char *message) { + jclass exc = (*jenv)->FindClass(jenv, + "com/fnproject/fn/runtime/ntv/UnixSocketException"); + if (exc == NULL) { // JVM exception + return; + } + jmethodID constr = (*jenv)->GetMethodID(jenv, exc, "", + "(Ljava/lang/String;Ljava/lang/String;)V"); + + if (constr == NULL) { // JVM exception + return; + } + jstring str = (*jenv)->NewStringUTF(jenv, message); + if (str == NULL) { // JVM exception + return; + } + jstring estr = (*jenv)->NewStringUTF(jenv, strerror(errno)); + if (estr == NULL) { // JVM exception + return; + } + jthrowable t = (jthrowable) (*jenv)->NewObject(jenv, exc, constr, str, estr); + if (t == NULL) { // JVM exception + return; + } + (*jenv)->Throw(jenv, t); +} + + +/** + * Throws a single-arg string exception + * @param jenv + * @param clazz class path (e.g. "java/lang/NullPointerException" + * @param message message to pass into constructor + */ +void throwSingleArgStringException(JNIEnv *jenv, const char *clazz, const char *message) { + jclass exc = (*jenv)->FindClass(jenv, clazz); + if (exc == NULL) { + return; + } + jmethodID constr = (*jenv)->GetMethodID(jenv, exc, "", + "(Ljava/lang/String;)V"); + if (constr == NULL) { // JVM exception + return; + } + jstring str = (*jenv)->NewStringUTF(jenv, message); + if (str == NULL) { // JVM OOM + return; + } + jthrowable t = (jthrowable) (*jenv)->NewObject(jenv, exc, constr, str); + if (t == NULL) { // JVM error + return; + } + (*jenv)->Throw(jenv, t); +} + +void throwIllegalArgumentException(JNIEnv *jenv, const char *message) { + throwSingleArgStringException(jenv, "java/lang/IllegalArgumentException", message); +} + + +void throwSocketTimeoutException(JNIEnv *jenv, const char *message) { + throwSingleArgStringException(jenv, "java/net/SocketTimeoutException", message); +} + + +void throwNPE(JNIEnv *jenv, const char *message) { + throwSingleArgStringException(jenv, "java/lang/NullPointerException", message); +} + + + +// public static native int createSocket(); +JNIEXPORT jint JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_socket(JNIEnv *jenv, jclass jClass) { + errno = 0; + int rv = socket(PF_UNIX, SOCK_STREAM, 0); + + if (rv == -1) { + throwIOException(jenv, "Could not create socket"); + return -1; + } + debuglog("got result from socket %d\n",rv); + + return rv; +} + + + +// public static native int bind(int socket, String path) +JNIEXPORT void JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_bind(JNIEnv *jenv, jclass jClass, jint jsocket, jstring jpath) { + errno = 0; + + struct sockaddr_un addr; + bzero(&addr, sizeof(struct sockaddr_un)); + + addr.sun_family = AF_UNIX; + + const char *nativePath = (*jenv)->GetStringUTFChars(jenv, jpath, 0); + if (nativePath == NULL) { // JVM OOM + return; + } + + if (strlen(nativePath) >= sizeof(addr.sun_path)) { + (*jenv)->ReleaseStringUTFChars(jenv, jpath, nativePath); + throwIllegalArgumentException(jenv, "Path too long"); + return; + } + + strncpy(addr.sun_path, nativePath, sizeof(addr.sun_path)); + (*jenv)->ReleaseStringUTFChars(jenv, jpath, nativePath); + + int rv = bind(jsocket, (struct sockaddr *) &addr, sizeof(addr)); + debuglog("got result from bind %d,%s\n",rv,strerror(errno)); + if (rv < 0) { + throwIOException(jenv, "Error in bind"); + return; + } +} + +// public static native void connect(int socket, String path) throws UnixSocketException; +JNIEXPORT void JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_connect(JNIEnv *jenv, jclass jClass, jint jsocket, jstring jpath) { + errno = 0; + + + struct sockaddr_un addr; + bzero(&addr, sizeof(struct sockaddr_un)); + addr.sun_family = AF_UNIX; + + const char *nativePath = (*jenv)->GetStringUTFChars(jenv, jpath, 0); + if (nativePath == NULL) {// JVM OOM + return; + } + + if (strlen(nativePath) >= sizeof(addr.sun_path)) { + (*jenv)->ReleaseStringUTFChars(jenv, jpath, nativePath); + throwIllegalArgumentException(jenv, "Path too long"); + return; + } + + strncpy(addr.sun_path, nativePath, sizeof(addr.sun_path)); + (*jenv)->ReleaseStringUTFChars(jenv, jpath, nativePath); + int result = connect(jsocket, (struct sockaddr *) &addr, sizeof(addr)); + debuglog("%d: got result from connect %d %s\n",jsocket,result,strerror(errno)); + if (result < 0) { + if (errno == ETIMEDOUT) { + throwSocketTimeoutException(jenv, "Socket connect timed out"); + return; + } + throwIOException(jenv, "Error in connect"); + return; + } +} + +// public static native void listen(int socket, int backlog); +JNIEXPORT void JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_listen(JNIEnv *jenv, jclass jClass, jint jsocket, jint jbacklog) { + errno = 0; + + int rv = listen(jsocket, jbacklog); + debuglog("got result from listen %d,%s\n",rv,strerror(errno)); + + if (rv < 0) { + throwIOException(jenv, "Error in listen"); + return; + } +} + + + +// public static native int accept(int socket, long timeoutMs) throws UnixSocketException; +// returns 0 in case that the accept timed out +JNIEXPORT jint JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_accept(JNIEnv *jenv, jclass jClass, jint jsocket, jlong timeoutMs) { + errno = 0; + + if (timeoutMs < 0) { + throwIllegalArgumentException(jenv, "Invalid timeout"); + return -1; + } + + struct timeval startTime; + + if (gettimeofday(&startTime, NULL) < 0) { + throwIOException(jenv, "Failed to get time"); + return -1; + } + + struct timeval timeoutAbs; + timeoutAbs.tv_sec = timeoutMs / 1000; + timeoutAbs.tv_usec = (int) (timeoutMs % 1000) * 1000; + + + int rv; + + struct timeval *toPtr = NULL; + struct timeval actualTo; + + + do { + errno = 0; + + if (timeoutMs > 0) { + struct timeval nowTime, used; + if (gettimeofday(&nowTime, NULL) < 0) { + throwIOException(jenv, "Failed to get time"); + return -1; + } + + timersub(&nowTime, &startTime, &used); + timersub(&timeoutAbs, &used, &actualTo); + if (actualTo.tv_sec < 0 || (actualTo.tv_sec == 0 && actualTo.tv_usec ==0) ) { + // hit end of poll in loop + return 0; + + } + toPtr = &actualTo; + } + + fd_set set; + FD_ZERO(&set); /* clear the set */ + FD_SET(jsocket, &set); /* add our file descriptor to the set */ + + rv = select(jsocket + 1, &set, NULL, NULL, toPtr); + debuglog("XXX %d Got result from select %d : %s\n",jsocket,rv,strerror(errno)); + + if(!FD_ISSET(jsocket,&set)){ + continue; + } + } while (rv == -1 && errno == EINTR); + + if (rv < 0) { + throwIOException(jenv, "Error in select"); + return -1; + } else if (rv == 0) { + return 0; // timeout + } + + + int result; + do { + struct sockaddr_un addr; + bzero(&addr, sizeof(struct sockaddr_un)); + socklen_t rlen = sizeof(struct sockaddr_un); + result = accept(jsocket, (struct sockaddr *) &addr, &rlen); + debuglog("XXX %d Got result from accept %d : %s\n",jsocket, result,strerror(errno)); + + } while (result == -1 && errno == EINTR); + + if (result < 0) { + throwIOException(jenv, "Error in accept"); + } + return result; +} + + + +// public static native int recv(int socket, byte[] buffer, jint offset, jint length) throws UnixSocketException; +JNIEXPORT jint JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_recv(JNIEnv *jenv, jclass jClass, jint jsocket, jbyteArray jbuf, + jint offset, jint length) { + errno = 0; + + if (offset < 0 || length <= 0) { + throwIllegalArgumentException(jenv, "Invalid offset, length"); + return -1; + } + + if (jbuf == NULL) { + throwNPE(jenv, "buffer is null"); + return -1; + } + + jint bufLen = (*jenv)->GetArrayLength(jenv, jbuf); + + if (offset >= bufLen) { + throwIllegalArgumentException(jenv, "Invalid offset, beyond end of buffer"); + return -1; + } + if (length > (bufLen - offset)) { + length = bufLen - offset; + } + + + jbyte *buf = (*jenv)->GetByteArrayElements(jenv, jbuf, NULL); + if (buf == NULL) { + return -1; + } + + + ssize_t rcount; + + do { + rcount = read(jsocket, &(buf[offset]), (size_t) length); + debuglog("XXX %d Got result from read %ld : %s\n",jsocket,rcount,strerror(errno)); + + } while (rcount == -1 && errno == EINTR); + + (*jenv)->ReleaseByteArrayElements(jenv, jbuf, buf, 0); + + + if (rcount == 0) { + // EOF in c is -1 in java + return -1; + } else if (rcount < 0) { + if (errno == EAGAIN) { + throwSocketTimeoutException(jenv, "Timeout reading from socket"); + return -1; + } + throwIOException(jenv, "Error reading from socket"); + return -1; + } + return (jint) rcount; + +} + + +// public static native int send(int socket, byte[] buffer) throws UnixSocketException; +JNIEXPORT jint JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_send(JNIEnv *jenv, jclass jClass, jint jsocket, jbyteArray jbuf, + jint offset, jint length) { + errno = 0; + + if (offset < 0 || length <= 0) { + throwIllegalArgumentException(jenv, "Invalid offset, length or timeout"); + return -1; + } + + if (jbuf == NULL) { + throwNPE(jenv, "buffer is null"); + return -1; + } + + jint bufLen = (*jenv)->GetArrayLength(jenv, jbuf); + + + if ((offset >= bufLen) || (length > (bufLen - offset))) { + throwIllegalArgumentException(jenv, "Invalid offset or length, beyond end of buffer"); + return -1; + } + + + jbyte *buf = (*jenv)->GetByteArrayElements(jenv, jbuf, NULL); + if (buf == NULL) { // JVM OOM + return -1; + } + ssize_t wcount; + do { + wcount = write(jsocket, &(buf[offset]), (size_t) length); + debuglog("XXX %d Got result from write %ld : %s\n",jsocket,wcount,strerror(errno)); + } while (wcount == -1 && errno == EINTR); + + + (*jenv)->ReleaseByteArrayElements(jenv, jbuf, buf, 0); + + if (wcount < 0) { + if (errno == EAGAIN) { + throwSocketTimeoutException(jenv, "Timeout writing to socket"); + return -1; + } + throwIOException(jenv, "Error reading from socket"); + return -1; + } + + return (jint) wcount; + +} + + + + +// public static native close(int socket); + +JNIEXPORT void JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_close(JNIEnv *jenv, jclass jClass, jint jsocket) { + errno = 0; + + int rv = close(jsocket); + debuglog("XXX %d got result from close %d,%s\n",jsocket,rv,strerror(errno)); + if (rv < 0) { + throwIOException(jenv, "Error in closing socket"); + return; + } +} + + +JNIEXPORT void JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_setSendBufSize(JNIEnv *jenv, jclass jClass, jint socket, + jint bufsize) { + errno = 0; + if (bufsize <= 0) { + throwIllegalArgumentException(jenv, "Invalid buffer size"); + return; + } + + int rv = setsockopt(socket, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(jint)); + if (rv < 0) { + throwIOException(jenv, "Error setting socket options"); + return; + } +} + +JNIEXPORT void JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_setRecvBufSize(JNIEnv *jenv, jclass jClass, jint socket, + jint bufsize) { + errno = 0; + if (bufsize <= 0) { + throwIllegalArgumentException(jenv, "invalid buffer size"); + return; + } + + int rv = setsockopt(socket, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(jint)); + if (rv < 0) { + throwIOException(jenv, "Error setting socket options"); + return; + } +} + +JNIEXPORT void JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_setSendTimeout(JNIEnv *jenv, jclass jClass, jint socket, + jint timeout) { + errno = 0; + if (timeout < 0) { + throwIllegalArgumentException(jenv, "invalid timeout"); + return; + } + + struct timeval tv; + tv.tv_sec = timeout / 1000; + tv.tv_usec = (timeout % 1000) * 1000; + + int rv = setsockopt(socket, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(struct timeval)); + if (rv < 0) { + throwIOException(jenv, "Error setting socket options"); + return; + } +} + + +JNIEXPORT jint JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_getSendTimeout(JNIEnv *jenv, jclass jClass, jint socket) { + errno = 0; + + struct timeval tv; + bzero(&tv, sizeof(struct timeval)); + socklen_t len = sizeof(struct timeval); + + int rv = getsockopt(socket, SOL_SOCKET, SO_SNDTIMEO, &tv, &len); + debuglog("XXX %d getsockopt _getSendTimeout rv %dz\n",socket,rv); + + if (rv < 0) { + throwIOException(jenv, "Error setting socket options"); + return -1; + } + time_t msecs = tv.tv_sec * 1000 + tv.tv_usec / 1000; + if (msecs > INT_MAX) { + return (jint) INT_MAX; + } + return (jint) msecs; +} + + +JNIEXPORT void JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_setRecvTimeout(JNIEnv *jenv, jclass jClass, jint socket, + jint timeout) { + errno = 0; + if (timeout < 0) { + throwIllegalArgumentException(jenv, "Invalid timeout"); + return; + } + + struct timeval tv; + tv.tv_sec = timeout / 1000; + tv.tv_usec = (timeout % 1000) * 1000; + + int rv = setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(struct timeval)); + debuglog("XXX %d setsockopt setRecvTimeout rv %dz\n",socket,rv); + if (rv < 0) { + throwIOException(jenv, "Error setting socket options"); + return; + } +} + + +JNIEXPORT jint JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_getRecvTimeout(JNIEnv *jenv, jclass jClass, jint socket) { + errno = 0; + + struct timeval tv; + bzero(&tv, sizeof(struct timeval)); + socklen_t len = sizeof(struct timeval); + + int rv = getsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, &tv, &len); + debuglog("XXX %d getsockopt rv %dz\n",socket,rv); + + if (rv < 0) { + throwIOException(jenv, "Error setting socket options"); + return -1; + } + + debuglog("XXX %d getsockopt _getSendTimeout rv %dz\n",socket,rv); + + time_t msecs = tv.tv_sec * 1000 + tv.tv_usec / 1000; + if (msecs > INT_MAX) { + return (jint) INT_MAX; + } + return (jint) msecs; +} + + +// public static native void shutdown(int socket, boolean input, boolean output) ; +JNIEXPORT void JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_shutdown(JNIEnv *jenv, jclass jClass, jint jsocket, jboolean input, + jboolean output) { + errno = 0; + int how; + + if (input && output) { + how = SHUT_RDWR; + } else if (input) { + how = SHUT_RD; + } else if (output) { + how = SHUT_WR; + } else { + return; + } + + + int rv = shutdown(jsocket, how); + debuglog("XXX %d got result from shutdown %d %d,%s\n",jsocket,how,rv,strerror(errno)); + if (rv < 0) { + throwIOException(jenv, "Failed to shut down socket "); + return; + } +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/DefaultEventCodec.java b/runtime/src/main/java/com/fnproject/fn/runtime/DefaultEventCodec.java deleted file mode 100644 index 090c0c9c..00000000 --- a/runtime/src/main/java/com/fnproject/fn/runtime/DefaultEventCodec.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.fnproject.fn.runtime; - -import com.fnproject.fn.api.Headers; -import com.fnproject.fn.api.InputEvent; -import com.fnproject.fn.api.OutputEvent; -import com.fnproject.fn.api.exception.FunctionInputHandlingException; -import com.fnproject.fn.api.exception.FunctionOutputHandlingException; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -/** - * DefaultEventCodec handles plain docker invocations on functions - *

- * This parses inputs from environment variables and reads and writes raw body and responses to the specified input and output streams - */ -class DefaultEventCodec implements EventCodec { - - private final Map env; - private final InputStream in; - private final OutputStream out; - - public DefaultEventCodec(Map env, InputStream in, OutputStream out) { - this.env = env; - this.in = in; - this.out = out; - } - - - private String getRequiredEnv(String name) { - String val = env.get(name); - if (val == null) { - throw new FunctionInputHandlingException("Required environment variable " + name + " is not set - are you running a function outside of fn run?"); - } - return val; - } - - @Override - public Optional readEvent() { - String method = getRequiredEnv("FN_METHOD"); - String appName = getRequiredEnv("FN_APP_NAME"); - String route = getRequiredEnv("FN_PATH"); - String requestUrl = getRequiredEnv("FN_REQUEST_URL"); - - Map headers = new HashMap<>(); - for (Map.Entry entry : env.entrySet()) { - String lowerCaseKey = entry.getKey().toLowerCase(); - if (lowerCaseKey.startsWith("fn_header_")) { - headers.put(entry.getKey().substring("fn_header_".length()), entry.getValue()); - } - } - - return Optional.of(new ReadOnceInputEvent(appName, route, requestUrl, method, in, Headers.fromMap(headers), QueryParametersParser.getParams(requestUrl))); - } - - @Override - public boolean shouldContinue() { - return false; - } - - @Override - public void writeEvent(OutputEvent evt) { - try { - evt.writeToOutput(out); - }catch(IOException e){ - throw new FunctionOutputHandlingException("error writing event",e); - } - } -} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/DefaultMethodWrapper.java b/runtime/src/main/java/com/fnproject/fn/runtime/DefaultMethodWrapper.java index 8d81dd47..98a61a2c 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/DefaultMethodWrapper.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/DefaultMethodWrapper.java @@ -1,7 +1,7 @@ package com.fnproject.fn.runtime; -import com.fnproject.fn.api.TypeWrapper; import com.fnproject.fn.api.MethodWrapper; +import com.fnproject.fn.api.TypeWrapper; import java.lang.reflect.Method; import java.util.Arrays; @@ -19,12 +19,11 @@ public class DefaultMethodWrapper implements MethodWrapper { this.srcMethod = srcMethod; } - public DefaultMethodWrapper(Class srcClass, String srcMethod) { - this.srcClass = srcClass; - this.srcMethod = Arrays.stream(srcClass.getMethods()) - .filter((m) -> m.getName().equals(srcMethod)) - .findFirst() - .orElseThrow(() -> new RuntimeException(new NoSuchMethodException(srcClass.getCanonicalName() + "::" + srcMethod))); + DefaultMethodWrapper(Class srcClass, String srcMethod) { + this(srcClass, Arrays.stream(srcClass.getMethods()) + .filter((m) -> m.getName().equals(srcMethod)) + .findFirst() + .orElseThrow(() -> new RuntimeException(new NoSuchMethodException(srcClass.getCanonicalName() + "::" + srcMethod)))); } @@ -40,12 +39,12 @@ public Method getTargetMethod() { @Override public TypeWrapper getParamType(int index) { - return new ParameterWrapper(this, index); + return MethodTypeWrapper.fromParameter(this, index); } @Override public TypeWrapper getReturnType() { - return new ReturnTypeWrapper(this); + return MethodTypeWrapper.fromReturnType(this); } @Override diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/EntryPoint.java b/runtime/src/main/java/com/fnproject/fn/runtime/EntryPoint.java index 6c5856a7..76ca4b3b 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/EntryPoint.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/EntryPoint.java @@ -1,18 +1,18 @@ package com.fnproject.fn.runtime; -import com.fnproject.fn.api.InputEvent; -import com.fnproject.fn.api.OutputEvent; +import com.fnproject.fn.api.*; import com.fnproject.fn.api.exception.FunctionInputHandlingException; import com.fnproject.fn.api.exception.FunctionLoadException; import com.fnproject.fn.api.exception.FunctionOutputHandlingException; -import com.fnproject.fn.runtime.exception.*; +import com.fnproject.fn.runtime.exception.FunctionInitializationException; +import com.fnproject.fn.runtime.exception.InternalFunctionInvocationException; +import com.fnproject.fn.runtime.exception.InvalidEntryPointException; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.io.PrintStream; import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; /** * Main entry point @@ -25,22 +25,30 @@ public static void main(String... args) throws Exception { // Override stdout while the function is running, so that the function result can be serialized to stdout // without interference from the user printing stuff to what they believe is stdout. System.setOut(System.err); - int exitCode = new EntryPoint().run( - System.getenv(), - System.in, - originalSystemOut, - System.err, - args); + + String format = System.getenv("FN_FORMAT"); + EventCodec codec; + if (format.equals(HTTPStreamCodec.HTTP_STREAM_FORMAT)) { + codec = new HTTPStreamCodec(System.getenv()); + } else { + throw new FunctionInputHandlingException("Unsupported function format:" + format); + } + + int exitCode = new EntryPoint().run(System.getenv(), codec, System.err, args); System.setOut(originalSystemOut); System.exit(exitCode); } /** - * Entrypoint runner - this executes the whole lifecycle of the fn Java FDK runtime - including multiple invocations in the function for hot functions + * Entry point runner - this executes the whole lifecycle of the fn Java FDK runtime - including multiple invocations in the function for hot functions * + * @param env the map of environment variables to run the function with (typically System.getenv but may be customised for testing) + * @param codec the codec to run the function with + * @param loggingOutput the stream to send function error/logging to - this will be wrapped into System.err within the funciton + * @param args any further args passed to the entry point - specifically the class/method name * @return the desired process exit status */ - public int run(Map env, InputStream functionInput, OutputStream functionOutput, PrintStream loggingOutput, String... args) { + public int run(Map env, EventCodec codec, PrintStream loggingOutput, String... args) { if (args.length != 1) { throw new InvalidEntryPointException("Expected one argument, of the form com.company.project.MyFunctionClass::myFunctionMethod"); } @@ -53,77 +61,93 @@ public int run(Map env, InputStream functionInput, OutputStream String cls = classMethod[0]; String mth = classMethod[1]; - int lastStatus = 0; + // TODO deprecate with default contract + final AtomicInteger lastStatus = new AtomicInteger(); try { final Map configFromEnvVars = Collections.unmodifiableMap(excludeInternalConfigAndHeaders(env)); FunctionLoader functionLoader = new FunctionLoader(); - FunctionRuntimeContext runtimeContext = new FunctionRuntimeContext(functionLoader.loadClass(cls, mth), configFromEnvVars); + + MethodWrapper method = functionLoader.loadClass(cls, mth); + FunctionRuntimeContext runtimeContext = new FunctionRuntimeContext(method, configFromEnvVars); + FnFeature f = method.getTargetClass().getAnnotation(FnFeature.class); + if (f != null) { + enableFeature(runtimeContext, f); + } + FnFeatures fs = method.getTargetClass().getAnnotation(FnFeatures.class); + if (fs != null) { + for (FnFeature fnFeature : fs.value()) { + enableFeature(runtimeContext,fnFeature); + } + } FunctionConfigurer functionConfigurer = new FunctionConfigurer(); functionConfigurer.configure(runtimeContext); - String format = env.get("FN_FORMAT"); - EventCodec codec; - - if (format != null && format.equalsIgnoreCase("http")) { - codec = new HttpEventCodec(env, functionInput, functionOutput); - } else if (format == null || format.equalsIgnoreCase("default")) { - codec = new DefaultEventCodec(env, functionInput, functionOutput); - } else { - throw new FunctionInputHandlingException("Unsupported function format:" + format); - } - do { + codec.runCodec((evt) -> { try { - Optional evtOpt = codec.readEvent(); - if (!evtOpt.isPresent()) { - break; - } - - FunctionInvocationContext fic = runtimeContext.newInvocationContext(); - try (InputEvent evt = evtOpt.get()) { + FunctionInvocationContext fic = runtimeContext.newInvocationContext(evt); + try (InputEvent myEvt = evt) { OutputEvent output = runtimeContext.tryInvoke(evt, fic); if (output == null) { throw new FunctionInputHandlingException("No invoker found for input event"); } - codec.writeEvent(output); if (output.isSuccess()) { - lastStatus = 0; + lastStatus.set(0); fic.fireOnSuccessfulInvocation(); } else { - lastStatus = 1; + lastStatus.set(1); fic.fireOnFailedInvocation(); } - } catch (IOException e) { + + return output.withHeaders(output.getHeaders().setHeaders(fic.getAdditionalResponseHeaders())); + + + } catch (IOException err) { fic.fireOnFailedInvocation(); - throw new FunctionInputHandlingException("Error closing function input", e); + throw new FunctionInputHandlingException("Error closing function input", err); } catch (Exception e) { // Make sure we commit any pending Flows, then rethrow fic.fireOnFailedInvocation(); throw e; } - } catch (InternalFunctionInvocationException fie) { loggingOutput.println("An error occurred in function: " + filterStackTraceToOnlyIncludeUsersCode(fie)); - codec.writeEvent(fie.toOutput()); - // Here: completer-invoked continuations are *always* reported as successful to the Fn platform; // the completer interprets the embedded HTTP-framed response. - lastStatus = fie.toOutput().isSuccess() ? 0 : 1; + lastStatus.set(fie.toOutput().isSuccess() ? 0 : 1); + return fie.toOutput(); } - } while (codec.shouldContinue()); + + }); } catch (FunctionLoadException | FunctionInputHandlingException | FunctionOutputHandlingException e) { // catch all block; loggingOutput.println(filterStackTraceToOnlyIncludeUsersCode(e)); return 2; - } catch (Exception ee){ + } catch (Exception ee) { loggingOutput.println("An unexpected error occurred:"); ee.printStackTrace(loggingOutput); return 1; } - return lastStatus; + return lastStatus.get(); + } + + private void enableFeature(FunctionRuntimeContext runtimeContext, FnFeature f) { + RuntimeFeature rf; + try { + Class featureClass = f.value(); + rf = featureClass.newInstance(); + } catch (Exception e) { + throw new FunctionInitializationException("Could not load feature class " + f.value().toString(), e); + } + + try { + rf.initialize(runtimeContext); + } catch (Exception e) { + throw new FunctionInitializationException("Exception while calling initialization on runtime feature " + f.value(), e); + } } @@ -178,7 +202,7 @@ private void addExceptionToStringBuilder(StringBuilder sb, Throwable t) { */ private Map excludeInternalConfigAndHeaders(Map env) { Set nonConfigEnvKeys = new HashSet<>(Arrays.asList("fn_app_name", "fn_path", "fn_method", "fn_request_url", - "fn_format", "content-length", "fn_call_id")); + "fn_format", "content-length", "fn_call_id")); Map config = new HashMap<>(); for (Map.Entry entry : env.entrySet()) { String lowerCaseKey = entry.getKey().toLowerCase(); diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/EventCodec.java b/runtime/src/main/java/com/fnproject/fn/runtime/EventCodec.java index 56408b7d..9a7a9885 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/EventCodec.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/EventCodec.java @@ -3,33 +3,33 @@ import com.fnproject.fn.api.InputEvent; import com.fnproject.fn.api.OutputEvent; -import java.io.IOException; -import java.util.Optional; - /** * Event Codec - deals with different calling conventions between fn and the function docker container */ public interface EventCodec { - /** - * Read a event from the input - * - * @return an empty input stream if the end of the stream is reached or an event if otherwise - */ - Optional readEvent(); /** - * Should the codec be used again - * - * @return true if {@link #readEvent()} can read another message + * Handler handles function content based on codec events + *

+ * A handler should generally deal with all exceptions (except errors) and convert them into appropriate OutputEvents */ - boolean shouldContinue(); + interface Handler { + /** + * Handle a function input event and generate a response + * + * @param event the event to handle + * @return an output event indicating the result of calling a function or an error + */ + OutputEvent handle(InputEvent event); + } /** - * Write an event to the output + * Run Codec should continuously run the function event loop until either the FDK should exit normally (returning normally) or an error occurred. + *

+ * Codec should invoke the handler for each received event * - * @param evt event to write - * @throws IOException if an error occurs while writing + * @param h the handler to run */ - void writeEvent(OutputEvent evt); + void runCodec(Handler h); } diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/FunctionConfigurer.java b/runtime/src/main/java/com/fnproject/fn/runtime/FunctionConfigurer.java index e61934ab..f9a1b939 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/FunctionConfigurer.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/FunctionConfigurer.java @@ -7,7 +7,9 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.*; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Optional; /** * Loads function entry points based on their class name and method name creating a {@link FunctionRuntimeContext} @@ -19,7 +21,6 @@ public class FunctionConfigurer { * create a function runtime context for a given class and method name * * @param runtimeContext The runtime context encapsulating the function to be run - * @return a new runtime context */ public void configure(FunctionRuntimeContext runtimeContext) { validateConfigurationMethods(runtimeContext.getMethodWrapper()); diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/FunctionInvocationCallback.java b/runtime/src/main/java/com/fnproject/fn/runtime/FunctionInvocationCallback.java index 4d51637a..9c1c5772 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/FunctionInvocationCallback.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/FunctionInvocationCallback.java @@ -1,7 +1,6 @@ package com.fnproject.fn.runtime; public interface FunctionInvocationCallback { - void fireOnSuccessfulInvocation(); void fireOnFailedInvocation(); diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/FunctionInvocationContext.java b/runtime/src/main/java/com/fnproject/fn/runtime/FunctionInvocationContext.java index c828bf83..9103726d 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/FunctionInvocationContext.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/FunctionInvocationContext.java @@ -1,9 +1,12 @@ package com.fnproject.fn.runtime; +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InputEvent; import com.fnproject.fn.api.InvocationContext; import com.fnproject.fn.api.InvocationListener; -import java.util.List; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; /** @@ -12,10 +15,14 @@ */ public class FunctionInvocationContext implements InvocationContext, FunctionInvocationCallback { private final FunctionRuntimeContext runtimeContext; - private List invocationListeners = new CopyOnWriteArrayList<>(); + private final List invocationListeners = new CopyOnWriteArrayList<>(); - public FunctionInvocationContext(FunctionRuntimeContext ctx) { + private final InputEvent event; + private final Map> additionalResponseHeaders = new ConcurrentHashMap<>(); + + FunctionInvocationContext(FunctionRuntimeContext ctx, InputEvent event) { this.runtimeContext = ctx; + this.event = event; } @Override @@ -28,12 +35,52 @@ public void addListener(InvocationListener listener) { invocationListeners.add(listener); } + @Override + public Headers getRequestHeaders() { + return event.getHeaders(); + } + + @Override + public void addResponseHeader(String key, String value) { + Objects.requireNonNull(key, "key"); + Objects.requireNonNull(value, "value"); + + additionalResponseHeaders.merge(key, Collections.singletonList(value), (a, b) -> { + List l = new ArrayList<>(a); + l.addAll(b); + return l; + }); + } + + /** + * returns the internal map of added response headers + * + * @return mutable map of internal response headers + */ + Map> getAdditionalResponseHeaders() { + return additionalResponseHeaders; + } + + @Override + public void setResponseHeader(String key, String value, String... vs) { + Objects.requireNonNull(key, "key"); + Objects.requireNonNull(vs, "vs"); + Arrays.stream(vs).forEach(v->Objects.requireNonNull(v,"null value in list ")); + + String cKey = Headers.canonicalKey(key); + if (value == null) { + additionalResponseHeaders.remove(cKey); + return; + } + additionalResponseHeaders.put(cKey, Collections.singletonList(value)); + } + @Override public void fireOnSuccessfulInvocation() { for (InvocationListener listener : invocationListeners) { try { listener.onSuccess(); - } catch (Exception e) { + } catch (Exception ignored) { } } } @@ -43,7 +90,7 @@ public void fireOnFailedInvocation() { for (InvocationListener listener : invocationListeners) { try { listener.onFailure(); - } catch (Exception e) { + } catch (Exception ignored) { } } } diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/FunctionLoader.java b/runtime/src/main/java/com/fnproject/fn/runtime/FunctionLoader.java index 092645a5..30b10101 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/FunctionLoader.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/FunctionLoader.java @@ -20,6 +20,8 @@ public class FunctionLoader { */ public MethodWrapper loadClass(String className, String fnName) { Class targetClass = loadClass(className); + + return new DefaultMethodWrapper(targetClass, getTargetMethod(targetClass, fnName)); } @@ -66,7 +68,7 @@ private Class loadClass(String className) { * Override the classloader used for fn class resolution * Primarily for testing, otherwise the system/default classloader is used. * - * @param loader + * @param loader the context class loader to use for this function */ public static void setContextClassLoader(ClassLoader loader) { contextClassLoader = loader; diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/FunctionRuntimeContext.java b/runtime/src/main/java/com/fnproject/fn/runtime/FunctionRuntimeContext.java index 1425b7f0..24c53e43 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/FunctionRuntimeContext.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/FunctionRuntimeContext.java @@ -1,50 +1,30 @@ package com.fnproject.fn.runtime; -import com.fnproject.fn.api.FunctionInvoker; -import com.fnproject.fn.api.InputBinding; -import com.fnproject.fn.api.InputCoercion; -import com.fnproject.fn.api.InputEvent; -import com.fnproject.fn.api.InvocationContext; -import com.fnproject.fn.api.MethodWrapper; -import com.fnproject.fn.api.OutputBinding; -import com.fnproject.fn.api.OutputCoercion; -import com.fnproject.fn.api.OutputEvent; -import com.fnproject.fn.api.RuntimeContext; -import com.fnproject.fn.runtime.coercion.ByteArrayCoercion; -import com.fnproject.fn.runtime.coercion.InputEventCoercion; -import com.fnproject.fn.runtime.coercion.OutputEventCoercion; -import com.fnproject.fn.runtime.coercion.StringCoercion; -import com.fnproject.fn.runtime.coercion.VoidCoercion; -import com.fnproject.fn.runtime.coercion.jackson.JacksonCoercion; -import com.fnproject.fn.runtime.exception.FunctionClassInstantiationException; +import com.fnproject.fn.api.*; import com.fnproject.fn.api.exception.FunctionConfigurationException; import com.fnproject.fn.api.exception.FunctionInputHandlingException; -import com.fnproject.fn.runtime.flow.FlowContinuationInvoker; +import com.fnproject.fn.runtime.coercion.*; +import com.fnproject.fn.runtime.coercion.jackson.JacksonCoercion; +import com.fnproject.fn.runtime.exception.FunctionClassInstantiationException; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; +import java.util.*; public class FunctionRuntimeContext implements RuntimeContext { private final Map config; private final MethodWrapper method; - private Map attributes = new HashMap<>(); - private List configuredInvokers = new ArrayList<>(); + private final Map attributes = new HashMap<>(); + private final List preCallHandlers = new ArrayList<>(); + private final List configuredInvokers = new ArrayList<>(); private Object instance; - private final List builtinInputCoercions = Arrays.asList(new StringCoercion(), new ByteArrayCoercion(), new InputEventCoercion(), JacksonCoercion.instance()); + private final List builtinInputCoercions = Arrays.asList(new ContextCoercion(), new StringCoercion(), new ByteArrayCoercion(), new InputEventCoercion(), JacksonCoercion.instance()); private final List userInputCoercions = new LinkedList<>(); private final List builtinOutputCoercions = Arrays.asList(new StringCoercion(), new ByteArrayCoercion(), new VoidCoercion(), new OutputEventCoercion(), JacksonCoercion.instance()); private final List userOutputCoercions = new LinkedList<>(); @@ -52,7 +32,17 @@ public class FunctionRuntimeContext implements RuntimeContext { public FunctionRuntimeContext(MethodWrapper method, Map config) { this.method = method; this.config = Objects.requireNonNull(config); - configuredInvokers.addAll(Arrays.asList(new FlowContinuationInvoker(), new MethodFunctionInvoker())); + configuredInvokers.add(new MethodFunctionInvoker()); + } + + @Override + public String getAppID() { + return config.getOrDefault("FN_APP_ID", ""); + } + + @Override + public String getFunctionID() { + return config.getOrDefault("FN_FN_ID", ""); } @Override @@ -69,7 +59,7 @@ public Optional getInvokeInstance() { if (RuntimeContext.class.isAssignableFrom(ctor.getParameterTypes()[0])) { instance = ctor.newInstance(FunctionRuntimeContext.this); } else { - if ( getMethod().getTargetClass().getEnclosingClass() != null && ! Modifier.isStatic(getMethod().getTargetClass().getModifiers()) ) { + if (getMethod().getTargetClass().getEnclosingClass() != null && !Modifier.isStatic(getMethod().getTargetClass().getModifiers())) { throw new FunctionClassInstantiationException("The function " + getMethod().getTargetClass() + " cannot be instantiated as it is a non-static inner class"); } else { throw new FunctionClassInstantiationException("The function " + getMethod().getTargetClass() + " cannot be instantiated as its constructor takes an unrecognized argument of type " + constructors[0].getParameterTypes()[0] + ". Function classes should have a single public constructor that takes either no arguments or a RuntimeContext argument"); @@ -131,11 +121,11 @@ public void addInputCoercion(InputCoercion ic) { public List getInputCoercions(MethodWrapper targetMethod, int param) { Annotation parameterAnnotations[] = targetMethod.getTargetMethod().getParameterAnnotations()[param]; Optional coercionAnnotation = Arrays.stream(parameterAnnotations) - .filter((ann) -> ann.annotationType().equals(InputBinding.class)) - .findFirst(); + .filter((ann) -> ann.annotationType().equals(InputBinding.class)) + .findFirst(); if (coercionAnnotation.isPresent()) { try { - List coercionList = new ArrayList(); + List coercionList = new ArrayList<>(); InputBinding inputBindingAnnotation = (InputBinding) coercionAnnotation.get(); coercionList.add(inputBindingAnnotation.coercion().getDeclaredConstructor().newInstance()); return coercionList; @@ -154,9 +144,19 @@ public void addOutputCoercion(OutputCoercion oc) { userOutputCoercions.add(Objects.requireNonNull(oc)); } + @Override - public void setInvoker(FunctionInvoker invoker) { - configuredInvokers.add(1, invoker); + public void addInvoker(FunctionInvoker invoker, FunctionInvoker.Phase phase) { + switch (phase) { + case PreCall: + preCallHandlers.add(0, invoker); + break; + case Call: + configuredInvokers.add(0, invoker); + break; + default: + throw new IllegalArgumentException("Unsupported phase " + phase); + } } @Override @@ -164,20 +164,25 @@ public MethodWrapper getMethod() { return method; } - public FunctionInvocationContext newInvocationContext() { - return new FunctionInvocationContext(this); + public FunctionInvocationContext newInvocationContext(InputEvent inputEvent) { + return new FunctionInvocationContext(this, inputEvent); } public OutputEvent tryInvoke(InputEvent evt, InvocationContext entryPoint) { - OutputEvent output = null; + for (FunctionInvoker invoker : preCallHandlers) { + Optional result = invoker.tryInvoke(entryPoint, evt); + if (result.isPresent()) { + return result.get(); + } + } + for (FunctionInvoker invoker : configuredInvokers) { Optional result = invoker.tryInvoke(entryPoint, evt); if (result.isPresent()) { - output = result.get(); - break; + return result.get(); } } - return output; + return null; } @Override @@ -185,7 +190,7 @@ public List getOutputCoercions(Method method) { OutputBinding coercionAnnotation = method.getAnnotation(OutputBinding.class); if (coercionAnnotation != null) { try { - List coercionList = new ArrayList(); + List coercionList = new ArrayList<>(); coercionList.add(coercionAnnotation.coercion().getDeclaredConstructor().newInstance()); return coercionList; @@ -193,7 +198,7 @@ public List getOutputCoercions(Method method) { throw new FunctionConfigurationException("Unable to instantiate output coercion class for method " + getMethod()); } } - List outputList = new ArrayList(); + List outputList = new ArrayList<>(); outputList.addAll(userOutputCoercions); outputList.addAll(builtinOutputCoercions); return outputList; diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/HTTPStreamCodec.java b/runtime/src/main/java/com/fnproject/fn/runtime/HTTPStreamCodec.java new file mode 100644 index 00000000..a28d9ccd --- /dev/null +++ b/runtime/src/main/java/com/fnproject/fn/runtime/HTTPStreamCodec.java @@ -0,0 +1,352 @@ +package com.fnproject.fn.runtime; + + +import com.fasterxml.jackson.core.io.CharTypes; +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.OutputEvent; +import com.fnproject.fn.api.exception.FunctionInputHandlingException; +import com.fnproject.fn.api.exception.FunctionOutputHandlingException; +import com.fnproject.fn.runtime.exception.FunctionIOException; +import com.fnproject.fn.runtime.exception.FunctionInitializationException; +import com.fnproject.fn.runtime.ntv.UnixServerSocket; +import com.fnproject.fn.runtime.ntv.UnixSocket; +import org.apache.http.*; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.DefaultBHttpServerConnection; +import org.apache.http.impl.io.EmptyInputStream; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicStatusLine; +import org.apache.http.protocol.BasicHttpContext; +import org.apache.http.protocol.HttpService; +import org.apache.http.protocol.ImmutableHttpProcessor; +import org.apache.http.protocol.UriHttpRequestHandlerMapper; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.attribute.PosixFilePermissions; +import java.time.Instant; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Fn HTTP Stream over Unix domain sockets codec + *

+ *

+ * This creates a new unix socket on the address specified by env["FN_LISTENER"] - and accepts requests. + *

+ * This currently only handles exactly one concurrent connection + *

+ * Created on 24/08/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public final class HTTPStreamCodec implements EventCodec, Closeable { + + public static final String HTTP_STREAM_FORMAT = "http-stream"; + private static final String FN_LISTENER = "FN_LISTENER"; + private static final Set stripInputHeaders; + private static final Set stripOutputHeaders; + private final Map env; + private final AtomicBoolean stopping = new AtomicBoolean(false); + private final File socketFile; + private final CompletableFuture stopped = new CompletableFuture<>(); + private final UnixServerSocket socket; + private final File tempFile; + + + static { + Set hin = new HashSet<>(); + hin.add("Host"); + hin.add("Accept-Encoding"); + hin.add("Transfer-Encoding"); + hin.add("User-Agent"); + hin.add("Connection"); + hin.add("TE"); + + stripInputHeaders = Collections.unmodifiableSet(hin); + + Set hout = new HashSet<>(); + hout.add("Content-Length"); + hout.add("Transfer-Encoding"); + hout.add("Connection"); + stripOutputHeaders = Collections.unmodifiableSet(hout); + } + + + + private String randomString() { + int leftLimit = 97; + int rightLimit = 122; + int targetStringLength = 10; + Random random = new Random(); + StringBuilder buffer = new StringBuilder(targetStringLength); + for (int i = 0; i < targetStringLength; i++) { + int randomLimitedInt = leftLimit + (int) + (random.nextFloat() * (rightLimit - leftLimit + 1)); + buffer.append((char) randomLimitedInt); + } + return buffer.toString(); + } + + /** + * Construct a new HTTPStreamCodec based on the environment + * + * @param env an env map + */ + HTTPStreamCodec(Map env) { + this.env = Objects.requireNonNull(env, "env"); + String listenerAddress = getRequiredEnv(FN_LISTENER); + + if (!listenerAddress.startsWith("unix:/")) { + throw new FunctionInitializationException("Invalid listener address - it should start with unix:/ :'" + listenerAddress + "'"); + } + String listenerFile = listenerAddress.substring("unix:".length()); + + socketFile = new File(listenerFile); + + + UnixServerSocket serverSocket = null; + File listenerDir = socketFile.getParentFile(); + tempFile = new File(listenerDir, randomString() + ".sock"); + try { + + serverSocket = UnixServerSocket.listen(tempFile.getAbsolutePath(), 1); + // Adjust socket permissions and move file + Files.setPosixFilePermissions(tempFile.toPath(), PosixFilePermissions.fromString("rw-rw-rw-")); + Files.createSymbolicLink(socketFile.toPath(), tempFile.toPath().getFileName()); + + this.socket = serverSocket; + } catch (IOException e) { + if (serverSocket != null) { + try { + serverSocket.close(); + } catch (IOException ignored) { + } + + } + throw new FunctionInitializationException("Unable to bind to unix socket in " + socketFile, e); + } + + + } + + + private String jsonError(String message, String detail) { + if (message == null) { + message = ""; + } + + StringBuilder sb = new StringBuilder(); + sb.append("{ \"message\":\""); + CharTypes.appendQuoted(sb, message); + sb.append("\""); + + if (detail != null) { + sb.append(", \"detail\":\""); + CharTypes.appendQuoted(sb, detail); + sb.append("\""); + } + + sb.append("}"); + return sb.toString(); + } + + @Override + public void runCodec(Handler h) { + + UriHttpRequestHandlerMapper mapper = new UriHttpRequestHandlerMapper(); + mapper.register("/call", ((request, response, context) -> { + InputEvent evt; + try { + evt = readEvent(request); + } catch (FunctionInputHandlingException e) { + response.setStatusCode(500); + response.setEntity(new StringEntity(jsonError("Invalid input for function", e.getMessage()), ContentType.APPLICATION_JSON)); + return; + } + + OutputEvent outEvt; + + try { + outEvt = h.handle(evt); + } catch (Exception e) { + response.setStatusCode(500); + response.setEntity(new StringEntity(jsonError("Unhandled internal error in FDK",e.getMessage()), ContentType.APPLICATION_JSON)); + return; + } + + try { + writeEvent(outEvt, response); + } catch (Exception e) { + // TODO strange edge cases might appear with headers where the response is half written here + response.setStatusCode(500); + response.setEntity(new StringEntity(jsonError("Unhandled internal error while writing FDK response",e.getMessage()), ContentType.APPLICATION_JSON)); + } + } + )); + + ImmutableHttpProcessor requestProcess = new ImmutableHttpProcessor(new HttpRequestInterceptor[0], new HttpResponseInterceptor[0]); + HttpService svc = new HttpService(requestProcess, mapper); + + try { + + while (!stopping.get()) { + try (UnixSocket sock = socket.accept(100)) { + if (sock == null) { + // timeout during accept, try again + continue; + } + // TODO tweak these properly + sock.setSendBufferSize(65535); + sock.setReceiveBufferSize(65535); + + + if (stopping.get()) { + // ignore IO errors on stop + return; + } + try { + DefaultBHttpServerConnection con = new DefaultBHttpServerConnection(65535); + con.bind(sock); + while (!sock.isClosed()) { + try { + svc.handleRequest(con, new BasicHttpContext()); + } catch (HttpException e) { + sock.close(); + throw e; + } + } + } catch (HttpException | IOException e) { + System.err.println("FDK Got Exception while handling HTTP request" + e.getMessage()); + e.printStackTrace(); + // we continue here and leave the container hot + } + } catch (IOException e) { + if (stopping.get()) { + // ignore IO errors on stop + return; + } + throw new FunctionIOException("failed to accept connection from platform, terminating", e); + } + + } + } finally { + stopped.complete(true); + } + + + } + + + private String getRequiredEnv(String name) { + String val = env.get(name); + if (val == null) { + throw new FunctionInputHandlingException("Required environment variable " + name + " is not set - are you running a function outside of fn run?"); + } + return val; + } + + private static String getRequiredHeader(HttpRequest request, String headerName) { + Header header = request.getFirstHeader(headerName); + if (header == null) { + throw new FunctionInputHandlingException("Required FDK header variable " + headerName + " is not set, check you are using the latest fn and FDK versions"); + } + return header.getValue(); + } + + private InputEvent readEvent(HttpRequest request) { + + InputStream bodyStream; + if (request instanceof HttpEntityEnclosingRequest) { + HttpEntityEnclosingRequest entityEnclosingRequest = (HttpEntityEnclosingRequest) request; + try { + bodyStream = entityEnclosingRequest.getEntity().getContent(); + } catch (IOException exception) { + throw new FunctionInputHandlingException("error handling input", exception); + } + } else { + bodyStream = EmptyInputStream.INSTANCE; + } + + + String deadline = getRequiredHeader(request, "Fn-Deadline"); + String callID = getRequiredHeader(request, "Fn-Call-Id"); + + if (callID == null) { + callID = ""; + } + Instant deadlineDate = Instant.now().plus(1, ChronoUnit.HOURS); + if (deadline != null) { + try { + deadlineDate = Instant.parse(deadline); + } catch (DateTimeParseException e) { + throw new FunctionInputHandlingException("Invalid deadline date format", e); + } + } + Headers headersIn = Headers.emptyHeaders(); + + + for (Header h : request.getAllHeaders()) { + if (stripInputHeaders.contains(Headers.canonicalKey(h.getName()))) { + continue; + } + headersIn = headersIn.addHeader(h.getName(), h.getValue()); + } + + return new ReadOnceInputEvent(bodyStream, headersIn, callID, deadlineDate); + + } + + private void writeEvent(OutputEvent evt, HttpResponse response) { + + evt.getHeaders().asMap() + .entrySet() + .stream() + .filter(e -> !stripOutputHeaders.contains(e.getKey())) + .flatMap(e -> e.getValue().stream().map((v) -> new BasicHeader(e.getKey(), v))) + .forEachOrdered(response::addHeader); + + ContentType contentType = evt.getContentType().map(c -> { + try { + return ContentType.parse(c); + } catch (ParseException e) { + return ContentType.DEFAULT_BINARY; + } + }).orElse(ContentType.DEFAULT_BINARY); + + response.setHeader("Content-Type", contentType.toString()); + response.setStatusLine(new BasicStatusLine(HttpVersion.HTTP_1_1, evt.getStatus().getCode(), evt.getStatus().name())); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + // TODO remove output buffering here - possibly change OutputEvent contract to support providing an InputStream? + try { + evt.writeToOutput(bos); + } catch (IOException e) { + throw new FunctionOutputHandlingException("Error writing output", e); + } + byte[] data = bos.toByteArray(); + response.setEntity(new ByteArrayEntity(data, contentType)); + + } + + + @Override + public void close() throws IOException { + if (stopping.compareAndSet(false, true)) { + socket.close(); + + try { + stopped.get(); + } catch (Exception ignored) { + } + socketFile.delete(); + tempFile.delete(); + } + + } +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/HttpEventCodec.java b/runtime/src/main/java/com/fnproject/fn/runtime/HttpEventCodec.java deleted file mode 100644 index 8ec9e398..00000000 --- a/runtime/src/main/java/com/fnproject/fn/runtime/HttpEventCodec.java +++ /dev/null @@ -1,156 +0,0 @@ -package com.fnproject.fn.runtime; - - -import com.fnproject.fn.api.Headers; -import com.fnproject.fn.api.InputEvent; -import com.fnproject.fn.api.OutputEvent; -import com.fnproject.fn.api.exception.FunctionInputHandlingException; -import com.fnproject.fn.api.exception.FunctionOutputHandlingException; -import org.apache.http.Header; -import org.apache.http.HttpException; -import org.apache.http.HttpRequest; -import org.apache.http.ProtocolVersion; -import org.apache.http.config.MessageConstraints; -import org.apache.http.impl.io.*; -import org.apache.http.io.HttpMessageParser; -import org.apache.http.io.SessionInputBuffer; -import org.apache.http.io.SessionOutputBuffer; -import org.apache.http.message.BasicHttpResponse; -import org.apache.http.message.BasicStatusLine; - -import java.io.*; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Function; - - -/** - * Reads input via an InputStream as an HTTP request. - *

- * This does not consume the whole event from the buffer, The caller is responsible for ensuring that either {@link InputEvent#consumeBody(Function)} or {@link InputEvent#close()} is called before reading a new event - */ -public class HttpEventCodec implements EventCodec { - - private static final String CONTENT_TYPE_HEADER = "Content-Type"; - private final SessionInputBuffer sib; - private final SessionOutputBuffer sob; - private final HttpMessageParser parser; - - private final Map env; - - HttpEventCodec(Map env, InputStream input, OutputStream output) { - - this.env = env; - - SessionInputBufferImpl sib = new SessionInputBufferImpl(new HttpTransportMetricsImpl(), 65535); - sib.bind(Objects.requireNonNull(input)); - this.sib = sib; - - SessionOutputBufferImpl sob = new SessionOutputBufferImpl(new HttpTransportMetricsImpl(), 65535); - sob.bind(output); - this.sob = sob; - - parser = new DefaultHttpRequestParserFactory(null, null).create(sib, MessageConstraints.custom().setMaxHeaderCount(65535).setMaxLineLength(65535).build()); - } - - private static String requiredHeader(HttpRequest req, String id) { - return Optional.ofNullable(req.getFirstHeader(id)).map(Header::getValue).orElseThrow(() -> new FunctionInputHandlingException("Incoming HTTP frame is missing required header: " + id)); - } - - private String getRequiredEnv(String name) { - String val = env.get(name); - if (val == null) { - throw new FunctionInputHandlingException("Required environment variable " + name + " is not set - are you running a function outside of fn run?"); - } - return val; - } - - @Override - public final Optional readEvent() { - - HttpRequest req; - try { - req = parser.parse(); - } catch (org.apache.http.ConnectionClosedException e) { - // End of stream - signal normal termination - return Optional.empty(); - } catch (IOException | HttpException e) { - throw new FunctionInputHandlingException("Failed to read HTTP content from input", e); - } - - InputStream bodyStream; - if (req.getHeaders("content-length").length > 0) { - long contentLength = Long.parseLong(requiredHeader(req, "content-length")); - bodyStream = new ContentLengthInputStream(sib, contentLength); - } else if (req.getHeaders("transfer-encoding").length > 0 && - req.getFirstHeader("transfer-encoding").getValue().equalsIgnoreCase("chunked")) { - bodyStream = new ChunkedInputStream(sib); - } else { - bodyStream = new ByteArrayInputStream(new byte[]{}); - } - String appName = getRequiredEnv("FN_APP_NAME"); - String route = getRequiredEnv("FN_PATH"); - String method = requiredHeader(req, "fn_method"); - String requestUrl = requiredHeader(req, "fn_request_url"); - - Map headers = new HashMap<>(); - for (Header h : req.getAllHeaders()) { - headers.put(h.getName(), h.getValue()); - } - - return Optional.of(new ReadOnceInputEvent(appName, route, requestUrl, method, - bodyStream, Headers.fromMap(headers), - QueryParametersParser.getParams(requestUrl))); - - } - - @Override - public boolean shouldContinue() { - return true; - } - - @Override - public void writeEvent(OutputEvent evt) { - try { - // TODO: We buffer the whole output here just to get the content-length - // TODO: functions should support chunked - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - - evt.writeToOutput(bos); - - byte[] data = bos.toByteArray(); - - BasicHttpResponse response; - - if (evt.isSuccess()) { - response = new BasicHttpResponse(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), evt.getStatusCode(), "INVOKED")); - } else { - response = new BasicHttpResponse(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), evt.getStatusCode(), "INVOKE FAILED")); - } - - evt.getHeaders().getAll().forEach(response::setHeader); - evt.getContentType().ifPresent((ct) -> response.setHeader(CONTENT_TYPE_HEADER, ct)); - response.setHeader("Content-length", String.valueOf(data.length)); - - - DefaultHttpResponseWriter writer = new DefaultHttpResponseWriter(sob); - try { - writer.write(response); - } catch (HttpException e) { - throw new FunctionOutputHandlingException("Failed to write response", e); - } - ContentLengthOutputStream clos = new ContentLengthOutputStream(sob, data.length); - clos.write(data); - clos.flush(); - clos.close(); - sob.flush(); - - } catch (IOException e) { - throw new FunctionOutputHandlingException("Failed to write output to stream", e); - } - } - -} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/MethodFunctionInvoker.java b/runtime/src/main/java/com/fnproject/fn/runtime/MethodFunctionInvoker.java index 30f95986..04192a37 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/MethodFunctionInvoker.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/MethodFunctionInvoker.java @@ -3,8 +3,8 @@ import com.fnproject.fn.api.*; import com.fnproject.fn.api.exception.FunctionInputHandlingException; -import com.fnproject.fn.runtime.exception.InternalFunctionInvocationException; import com.fnproject.fn.api.exception.FunctionOutputHandlingException; +import com.fnproject.fn.runtime.exception.InternalFunctionInvocationException; import java.lang.reflect.InvocationTargetException; import java.util.Optional; @@ -17,6 +17,26 @@ */ public class MethodFunctionInvoker implements FunctionInvoker { + /* + * If enabled, print the logging framing content + */ + public void logFramer(FunctionRuntimeContext rctx, InputEvent evt) { + String framer = rctx.getConfigurationByKey("FN_LOGFRAME_NAME").orElse(""); + + if (framer != "") { + String valueSrc = rctx.getConfigurationByKey("FN_LOGFRAME_HDR").orElse(""); + + if (valueSrc != "") { + String id = evt.getHeaders().get(valueSrc).orElse(""); + if (id != "") { + System.out.println("\n" + framer + "=" + id + "\n"); + System.err.println("\n" + framer + "=" + id + "\n"); + } + } + } + } + + /** * Invoke the function wrapped by this loader * @@ -33,6 +53,8 @@ public Optional tryInvoke(InvocationContext ctx, InputEvent evt) th Object rawResult; + logFramer(runtimeContext, evt); + try { rawResult = method.getTargetMethod().invoke(ctx.getRuntimeContext().getInvokeInstance().orElse(null), userFunctionParams); } catch (IllegalAccessException | InvocationTargetException e) { diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/MethodTypeWrapper.java b/runtime/src/main/java/com/fnproject/fn/runtime/MethodTypeWrapper.java index e7743a72..3972784f 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/MethodTypeWrapper.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/MethodTypeWrapper.java @@ -1,18 +1,16 @@ package com.fnproject.fn.runtime; -import com.fnproject.fn.api.TypeWrapper; import com.fnproject.fn.api.MethodWrapper; +import com.fnproject.fn.api.TypeWrapper; import net.jodah.typetools.TypeResolver; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -public abstract class MethodTypeWrapper implements TypeWrapper { - protected final MethodWrapper src; - protected Class parameterClass; +public final class MethodTypeWrapper implements TypeWrapper { + private final Class parameterClass; - public MethodTypeWrapper(MethodWrapper src, Class parameterClass) { - this.src = src; + private MethodTypeWrapper(Class parameterClass) { this.parameterClass = parameterClass; } @@ -21,7 +19,7 @@ public Class getParameterClass() { return parameterClass; } - protected static Class resolveType(Type type, MethodWrapper src) { + static Class resolveType(Type type, MethodWrapper src) { if (type instanceof Class) { return PrimitiveTypeResolver.resolve((Class) type); } else if (type instanceof ParameterizedType) { @@ -36,4 +34,13 @@ protected static Class resolveType(Type type, MethodWrapper src) { } } + public static TypeWrapper fromParameter(MethodWrapper method, int paramIndex) { + return new MethodTypeWrapper(resolveType(method.getTargetMethod().getGenericParameterTypes()[paramIndex], method)); + } + + public static TypeWrapper fromReturnType(MethodWrapper method) { + return new MethodTypeWrapper(resolveType(method.getTargetMethod().getGenericReturnType(), method)); + + } + } diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/ParameterWrapper.java b/runtime/src/main/java/com/fnproject/fn/runtime/ParameterWrapper.java deleted file mode 100644 index efe099a4..00000000 --- a/runtime/src/main/java/com/fnproject/fn/runtime/ParameterWrapper.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.fnproject.fn.runtime; - -import com.fnproject.fn.api.MethodWrapper; - -/** - * A {@link com.fnproject.fn.api.TypeWrapper} for capturing type information about a method's parameter. - */ -class ParameterWrapper extends MethodTypeWrapper { - - /** - * Constructor - * - * @param method the method - * @param paramIndex the index of the parameter which we store type information about - */ - public ParameterWrapper(MethodWrapper method, int paramIndex) { - super(method, resolveType(method.getTargetMethod().getGenericParameterTypes()[paramIndex], method)); - } - - @Override - public Class getParameterClass() { - return parameterClass; - } -} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/PrimitiveTypeResolver.java b/runtime/src/main/java/com/fnproject/fn/runtime/PrimitiveTypeResolver.java index 6c3e66a2..5f2f8e3f 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/PrimitiveTypeResolver.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/PrimitiveTypeResolver.java @@ -4,7 +4,7 @@ import java.util.Map; public class PrimitiveTypeResolver { - private static Map, Class> boxedTypes = new HashMap<>(); + private static final Map, Class> boxedTypes = new HashMap<>(); static { boxedTypes.put(void.class, Void.class); boxedTypes.put(boolean.class, Boolean.class); diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/ReadOnceInputEvent.java b/runtime/src/main/java/com/fnproject/fn/runtime/ReadOnceInputEvent.java index c120b6bd..7bfb4cde 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/ReadOnceInputEvent.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/ReadOnceInputEvent.java @@ -2,12 +2,12 @@ import com.fnproject.fn.api.Headers; import com.fnproject.fn.api.InputEvent; -import com.fnproject.fn.api.QueryParameters; import com.fnproject.fn.api.exception.FunctionInputHandlingException; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; +import java.time.Instant; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; @@ -18,26 +18,18 @@ * This in */ public class ReadOnceInputEvent implements InputEvent { - - private final String appName; - private final String route; - private final String requestUrl; - private final String method; - private final BufferedInputStream body; - private AtomicBoolean consumed = new AtomicBoolean(false); - private QueryParameters queryParameters; + private final AtomicBoolean consumed = new AtomicBoolean(false); private final Headers headers; + private final Instant deadline; + private final String callID; - public ReadOnceInputEvent(String appName, String route, String requestUrl, String method, InputStream body, Headers headers, QueryParameters parameters) { - this.appName = Objects.requireNonNull(appName); - this.route = Objects.requireNonNull(route); - this.requestUrl = Objects.requireNonNull(requestUrl); - this.method = Objects.requireNonNull(method).toUpperCase(); - this.body = new BufferedInputStream(Objects.requireNonNull(body)); - this.headers = Objects.requireNonNull(headers); - this.queryParameters = Objects.requireNonNull(parameters); + public ReadOnceInputEvent(InputStream body, Headers headers, String callID, Instant deadline) { + this.body = new BufferedInputStream(Objects.requireNonNull(body, "body")); + this.headers = Objects.requireNonNull(headers, "headers"); + this.callID = Objects.requireNonNull(callID, "callID"); + this.deadline = Objects.requireNonNull(deadline, "deadline"); body.mark(Integer.MAX_VALUE); } @@ -63,57 +55,22 @@ public T consumeBody(Function dest) { } - /** - * @return The fn application name associated with this call - */ - @Override - public String getAppName() { - return appName; - } - /** - * @return The route associated with this call (starting with a slash) - */ @Override - public String getRoute() { - return route; + public String getCallID() { + return callID; } - /** - * @return The full request URL into the app - */ @Override - public String getRequestUrl() { - return requestUrl; + public Instant getDeadline() { + return deadline; } - /** - * @return The HTTP method (capitalised) of this request - */ - @Override - public String getMethod() { - return method; - } - - /** - * The HTTP headers on the request - * - * @return an immutable map of headers - */ @Override public Headers getHeaders() { return headers; } - /** - * The query parameters of the function invocation - * - * @return an immutable map of query parameters parsed from the request URL - */ - @Override - public QueryParameters getQueryParameters() { - return queryParameters; - } @Override public void close() throws IOException { diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/ReturnTypeWrapper.java b/runtime/src/main/java/com/fnproject/fn/runtime/ReturnTypeWrapper.java deleted file mode 100644 index 120fffd1..00000000 --- a/runtime/src/main/java/com/fnproject/fn/runtime/ReturnTypeWrapper.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.fnproject.fn.runtime; - -import com.fnproject.fn.api.MethodWrapper; - -/** - * A {@link com.fnproject.fn.api.TypeWrapper} for capturing type information about a method's parameter. - */ -class ReturnTypeWrapper extends MethodTypeWrapper { - - /** - * Constructor - * - * @param method the method which we store return-type information about - */ - ReturnTypeWrapper(MethodWrapper method) { - super(method, resolveType(method.getTargetMethod().getGenericReturnType(), method)); - } -} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/coercion/ByteArrayCoercion.java b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/ByteArrayCoercion.java index 2f8034b0..401791d2 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/coercion/ByteArrayCoercion.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/ByteArrayCoercion.java @@ -13,7 +13,7 @@ public class ByteArrayCoercion implements InputCoercion, OutputCoercion { public Optional wrapFunctionResult(InvocationContext ctx, MethodWrapper method, Object value) { if (method.getReturnType().getParameterClass().equals(byte[].class)) { - return Optional.of(OutputEvent.fromBytes(((byte[]) value), OutputEvent.SUCCESS, "application/octet-stream")); + return Optional.of(OutputEvent.fromBytes(((byte[]) value), OutputEvent.Status.Success, "application/octet-stream")); } else { return Optional.empty(); } diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/coercion/ContextCoercion.java b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/ContextCoercion.java new file mode 100644 index 00000000..9bea4611 --- /dev/null +++ b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/ContextCoercion.java @@ -0,0 +1,28 @@ +package com.fnproject.fn.runtime.coercion; + +import com.fnproject.fn.api.*; +import com.fnproject.fn.api.httpgateway.HTTPGatewayContext; +import com.fnproject.fn.runtime.httpgateway.FunctionHTTPGatewayContext; + +import java.util.Optional; + +/** + * Handles coercion to build in context objects ({@link RuntimeContext}, {@link InvocationContext} , {@link HTTPGatewayContext}) + */ +public class ContextCoercion implements InputCoercion { + + @Override + public Optional tryCoerceParam(InvocationContext currentContext, int arg, InputEvent input, MethodWrapper method) { + Class paramClass = method.getParamType(arg).getParameterClass(); + + if (paramClass.equals(RuntimeContext.class)) { + return Optional.of(currentContext.getRuntimeContext()); + } else if (paramClass.equals(InvocationContext.class)) { + return Optional.of(currentContext); + } else if (paramClass.equals(HTTPGatewayContext.class)) { + return Optional.of(new FunctionHTTPGatewayContext(currentContext)); + } else { + return Optional.empty(); + } + } +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/coercion/StringCoercion.java b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/StringCoercion.java index 0fb6c46f..b51b9d14 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/coercion/StringCoercion.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/StringCoercion.java @@ -11,7 +11,7 @@ public class StringCoercion implements InputCoercion, OutputCoercion { @Override public Optional wrapFunctionResult(InvocationContext ctx, MethodWrapper method, Object value) { if (method.getReturnType().getParameterClass().equals(String.class)) { - return Optional.of(OutputEvent.fromBytes(((String) value).getBytes(), OutputEvent.SUCCESS, "text/plain")); + return Optional.of(OutputEvent.fromBytes(((String) value).getBytes(), OutputEvent.Status.Success, "text/plain")); } else { return Optional.empty(); } diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/coercion/VoidCoercion.java b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/VoidCoercion.java index 5d0442b6..3586e8dc 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/coercion/VoidCoercion.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/VoidCoercion.java @@ -11,7 +11,7 @@ public class VoidCoercion implements OutputCoercion { @Override public Optional wrapFunctionResult(InvocationContext ctx, MethodWrapper method, Object value) { if (method.getReturnType().getParameterClass().equals(Void.class)) { - return Optional.of(OutputEvent.emptyResult(OutputEvent.SUCCESS)); + return Optional.of(OutputEvent.emptyResult(OutputEvent.Status.Success)); } else { return Optional.empty(); } diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/coercion/jackson/JacksonCoercion.java b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/jackson/JacksonCoercion.java index 93636d38..50a1f8d3 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/coercion/jackson/JacksonCoercion.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/jackson/JacksonCoercion.java @@ -15,10 +15,14 @@ * This supports marshalling and unmarshalling of event parameters and responses to */ public class JacksonCoercion implements InputCoercion, OutputCoercion { - private static String OM_KEY = JacksonCoercion.class.getCanonicalName() + ".om"; + private static final String OM_KEY = JacksonCoercion.class.getCanonicalName() + ".om"; - private static JacksonCoercion instance = new JacksonCoercion(); + private static final JacksonCoercion instance = new JacksonCoercion(); + /** + * Return the global instance of this coercion + * @return a singleton instance of the JSON coercion for the VM + */ public static JacksonCoercion instance() { return instance; } @@ -64,7 +68,7 @@ private static RuntimeException coercionFailed(Type paramType) { public Optional wrapFunctionResult(InvocationContext ctx, MethodWrapper method, Object value) { try { - return Optional.of(OutputEvent.fromBytes(objectMapper(ctx).writeValueAsBytes(value), OutputEvent.SUCCESS, + return Optional.of(OutputEvent.fromBytes(objectMapper(ctx).writeValueAsBytes(value), OutputEvent.Status.Success, "application/json")); } catch (JsonProcessingException e) { diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/exception/FunctionIOException.java b/runtime/src/main/java/com/fnproject/fn/runtime/exception/FunctionIOException.java new file mode 100644 index 00000000..7b99da89 --- /dev/null +++ b/runtime/src/main/java/com/fnproject/fn/runtime/exception/FunctionIOException.java @@ -0,0 +1,20 @@ +package com.fnproject.fn.runtime.exception; + +/** + * The FDK experienced a terminal issue communicating with the platform + */ +public final class FunctionIOException extends RuntimeException { + + + /** + * create a function invocation exception + * + * @param message private message for this exception - + * @param target the underlying user exception that triggered this failure + */ + public FunctionIOException(String message, Throwable target) { + super(message, target); + } + + +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/exception/FunctionInitializationException.java b/runtime/src/main/java/com/fnproject/fn/runtime/exception/FunctionInitializationException.java new file mode 100644 index 00000000..7bbc9323 --- /dev/null +++ b/runtime/src/main/java/com/fnproject/fn/runtime/exception/FunctionInitializationException.java @@ -0,0 +1,23 @@ +package com.fnproject.fn.runtime.exception; + +/** + * The FDK was not able to start up + */ +public final class FunctionInitializationException extends RuntimeException { + + + /** + * create a function invocation exception + * + * @param message private message for this exception - + * @param target the underlying user exception that triggered this failure + */ + public FunctionInitializationException(String message, Throwable target) { + super(message, target); + } + + + public FunctionInitializationException(String message) { + super(message); + } +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/exception/InternalFunctionInvocationException.java b/runtime/src/main/java/com/fnproject/fn/runtime/exception/InternalFunctionInvocationException.java index f12338d2..40ca5d80 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/exception/InternalFunctionInvocationException.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/exception/InternalFunctionInvocationException.java @@ -19,7 +19,7 @@ public final class InternalFunctionInvocationException extends RuntimeException public InternalFunctionInvocationException(String message, Throwable target) { super(message); this.cause = target; - this.event = OutputEvent.fromBytes(new byte[0], OutputEvent.FAILURE, null); + this.event = OutputEvent.fromBytes(new byte[0], OutputEvent.Status.FunctionError, null); } @@ -43,7 +43,7 @@ public Throwable getCause() { /** * map this exception to an output event - * @return + * @return the output event associated with this exception */ public OutputEvent toOutput() { return event; diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/exception/InvalidEntryPointException.java b/runtime/src/main/java/com/fnproject/fn/runtime/exception/InvalidEntryPointException.java index 8cc01cbc..2e7e03c6 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/exception/InvalidEntryPointException.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/exception/InvalidEntryPointException.java @@ -3,7 +3,7 @@ import com.fnproject.fn.api.exception.FunctionLoadException; /** - * The function entrypoint was malformed. + * The function entry point spec was malformed. */ public class InvalidEntryPointException extends FunctionLoadException { public InvalidEntryPointException(String msg) { diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/exception/PlatformCommunicationException.java b/runtime/src/main/java/com/fnproject/fn/runtime/exception/PlatformCommunicationException.java index 7a3249e7..09db0863 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/exception/PlatformCommunicationException.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/exception/PlatformCommunicationException.java @@ -1,5 +1,8 @@ package com.fnproject.fn.runtime.exception; +/** + * An error occurred in the + */ public class PlatformCommunicationException extends RuntimeException { public PlatformCommunicationException(String message) { super(message); diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/httpgateway/FunctionHTTPGatewayContext.java b/runtime/src/main/java/com/fnproject/fn/runtime/httpgateway/FunctionHTTPGatewayContext.java new file mode 100644 index 00000000..a0f1c8be --- /dev/null +++ b/runtime/src/main/java/com/fnproject/fn/runtime/httpgateway/FunctionHTTPGatewayContext.java @@ -0,0 +1,110 @@ +package com.fnproject.fn.runtime.httpgateway; + +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InvocationContext; +import com.fnproject.fn.api.OutputEvent; +import com.fnproject.fn.api.QueryParameters; +import com.fnproject.fn.api.httpgateway.HTTPGatewayContext; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Created on 19/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class FunctionHTTPGatewayContext implements HTTPGatewayContext { + + private final InvocationContext invocationContext; + private final Headers httpRequestHeaders; + private final String method; + private final String requestUrl; + private final QueryParameters queryParameters; + + public FunctionHTTPGatewayContext(InvocationContext invocationContext) { + this.invocationContext = Objects.requireNonNull(invocationContext, "invocationContext"); + + Map> myHeaders = new HashMap<>(); + + String requestUri = ""; + String method = ""; + for (Map.Entry> e : invocationContext.getRequestHeaders().asMap().entrySet()) { + String key = e.getKey(); + if (key.startsWith("Fn-Http-H-")) { + String httpKey = key.substring("Fn-Http-H-".length()); + if (httpKey.length() > 0) { + myHeaders.put(httpKey, e.getValue()); + } + } + + if (key.equals("Fn-Http-Request-Url")) { + requestUri = e.getValue().get(0); + } + if (key.equals("Fn-Http-Method")) { + method = e.getValue().get(0); + } + + } + this.queryParameters = QueryParametersParser.getParams(requestUri); + this.requestUrl = requestUri; + this.method = method; + this.httpRequestHeaders = Headers.emptyHeaders().setHeaders(myHeaders); + + } + + @Override + public InvocationContext getInvocationContext() { + return invocationContext; + } + + @Override + public Headers getHeaders() { + return httpRequestHeaders; + } + + @Override + public String getRequestURL() { + return requestUrl; + } + + @Override + public String getMethod() { + return method; + } + + @Override + public QueryParameters getQueryParameters() { + return queryParameters; + } + + @Override + public void addResponseHeader(String key, String value) { + invocationContext.addResponseHeader("Fn-Http-H-" + key, value); + + } + + @Override + public void setResponseHeader(String key, String value, String... vs) { + + if (Headers.canonicalKey(key).equals(OutputEvent.CONTENT_TYPE_HEADER)) { + invocationContext.setResponseContentType(value); + invocationContext.setResponseHeader("Fn-Http-H-" + key, value); + } else { + invocationContext.setResponseHeader("Fn-Http-H-" + key, value, vs); + + } + + + } + + @Override + public void setStatusCode(int code) { + if (code < 100 || code >= 600) { + throw new IllegalArgumentException("Invalid HTTP status code: " + code); + } + invocationContext.setResponseHeader("Fn-Http-Status", "" + code); + } +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/QueryParametersImpl.java b/runtime/src/main/java/com/fnproject/fn/runtime/httpgateway/QueryParametersImpl.java similarity index 76% rename from runtime/src/main/java/com/fnproject/fn/runtime/QueryParametersImpl.java rename to runtime/src/main/java/com/fnproject/fn/runtime/httpgateway/QueryParametersImpl.java index bf7309fc..885be4c3 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/QueryParametersImpl.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/httpgateway/QueryParametersImpl.java @@ -1,10 +1,11 @@ -package com.fnproject.fn.runtime; +package com.fnproject.fn.runtime.httpgateway; import com.fnproject.fn.api.QueryParameters; +import java.io.Serializable; import java.util.*; -public class QueryParametersImpl implements QueryParameters { +public class QueryParametersImpl implements QueryParameters, Serializable { private final Map> params; public QueryParametersImpl() { @@ -18,8 +19,8 @@ public QueryParametersImpl(Map> params) { public Optional get(String key) { Objects.requireNonNull(key); return Optional.of(getValues(key)) - .filter((values) -> values.size() > 0) - .flatMap((values) -> Optional.ofNullable(values.get(0))); + .filter((values) -> values.size() > 0) + .flatMap((values) -> Optional.ofNullable(values.get(0))); } public List getValues(String key) { diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/QueryParametersParser.java b/runtime/src/main/java/com/fnproject/fn/runtime/httpgateway/QueryParametersParser.java similarity index 97% rename from runtime/src/main/java/com/fnproject/fn/runtime/QueryParametersParser.java rename to runtime/src/main/java/com/fnproject/fn/runtime/httpgateway/QueryParametersParser.java index 3c1d8f9b..51242f06 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/QueryParametersParser.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/httpgateway/QueryParametersParser.java @@ -1,12 +1,12 @@ -package com.fnproject.fn.runtime; +package com.fnproject.fn.runtime.httpgateway; import com.fnproject.fn.api.QueryParameters; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; +import java.util.AbstractMap.SimpleImmutableEntry; import java.util.*; import java.util.Map.Entry; -import java.util.AbstractMap.SimpleImmutableEntry; import java.util.stream.Collectors; import static java.util.stream.Collectors.mapping; diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixServerSocket.java b/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixServerSocket.java new file mode 100644 index 00000000..a4fb310b --- /dev/null +++ b/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixServerSocket.java @@ -0,0 +1,62 @@ +package com.fnproject.fn.runtime.ntv; + +import java.io.Closeable; +import java.io.IOException; +import java.net.SocketException; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Created on 12/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class UnixServerSocket implements Closeable { + private final int fd; + private final AtomicBoolean closed = new AtomicBoolean(); + + private UnixServerSocket(int fd) { + this.fd = fd; + } + + + public static UnixServerSocket listen(String fileName, int backlog) throws IOException { + int fd = UnixSocketNative.socket(); + + try { + UnixSocketNative.bind(fd, fileName); + } catch (UnixSocketException e) { + UnixSocketNative.close(fd); + throw e; + } + + + try { + UnixSocketNative.listen(fd, backlog); + } catch (UnixSocketException e) { + UnixSocketNative.close(fd); + throw e; + } + return new UnixServerSocket(fd); + + } + + @Override + public void close() throws IOException { + if (closed.compareAndSet(false,true)) { + UnixSocketNative.close(fd); + } + } + + public UnixSocket accept(long timeoutMillis) throws IOException { + if (closed.get()) { + throw new SocketException("accept on closed socket"); + } + int newFd = UnixSocketNative.accept(fd, timeoutMillis); + if (newFd == 0) { + return null; + } + return new UnixSocket(newFd); + } + + +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixSocket.java b/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixSocket.java new file mode 100644 index 00000000..26ca8457 --- /dev/null +++ b/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixSocket.java @@ -0,0 +1,320 @@ +package com.fnproject.fn.runtime.ntv; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.*; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * This approximates a Java.net.socket for many operations but not by any means all + * Created on 12/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public final class UnixSocket extends Socket { + // Fall back to WTF for most unsupported operations + private static final SocketImpl fakeSocketImpl = new SocketImpl() { + @Override + protected void create(boolean stream) throws IOException { + throw new UnsupportedOperationException(); + + } + + @Override + protected void connect(String host, int port) throws IOException { + throw new UnsupportedOperationException(); + + } + + @Override + protected void connect(InetAddress address, int port) throws IOException { + throw new UnsupportedOperationException(); + + } + + @Override + protected void connect(SocketAddress address, int timeout) throws IOException { + throw new UnsupportedOperationException(); + + } + + @Override + protected void bind(InetAddress host, int port) throws IOException { + throw new UnsupportedOperationException(); + + } + + @Override + protected void listen(int backlog) throws IOException { + throw new UnsupportedOperationException(); + + } + + @Override + protected void accept(SocketImpl s) throws IOException { + throw new UnsupportedOperationException(); + + } + + @Override + protected InputStream getInputStream() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + protected OutputStream getOutputStream() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + protected int available() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + protected void close() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + protected void sendUrgentData(int data) throws IOException { + throw new UnsupportedOperationException(); + + } + + @Override + public void setOption(int optID, Object value) throws SocketException { + throw new UnsupportedOperationException(); + + } + + @Override + public Object getOption(int optID) throws SocketException { + throw new UnsupportedOperationException(); + } + }; + + + private final int fd; + private final AtomicBoolean closed = new AtomicBoolean(); + + private final AtomicBoolean inputClosed = new AtomicBoolean(); + private final AtomicBoolean outputClosed = new AtomicBoolean(); + + private final InputStream in; + private final OutputStream out; + + + UnixSocket(int fd) throws SocketException { + super(fakeSocketImpl); + this.fd = fd; + in = new UsInput(); + out = new UsOutput(); + + } + + private class UsInput extends InputStream { + + @Override + public int read() throws IOException { + + byte[] buf = new byte[1]; + int rv = read(buf, 0, 1); + if (rv == -1) { + return -1; + } + return (int) buf[0]; + } + + + public int read(byte b[]) throws IOException { + return this.read(b, 0, b.length); + } + + @Override + public int read(byte b[], int off, int len) throws IOException { + if (inputClosed.get()) { + throw new UnixSocketException("Read on closed stream"); + } + + return UnixSocketNative.recv(fd, b, off, len); + } + + @Override + public void close() throws IOException { + shutdownInput(); + } + } + + + private class UsOutput extends OutputStream { + + @Override + public void write(int b) throws IOException { + write(new byte[]{(byte) b}, 0, 1); + } + + public void write(byte b[], int off, int len) throws IOException { + if (outputClosed.get()) { + throw new UnixSocketException("Write to closed stream"); + } + Objects.requireNonNull(b); + while (len > 0) { + int sent = UnixSocketNative.send(fd, b, off, len); + + if (sent == 0) { + throw new UnixSocketException("No data written to buffer"); + } + off = off + sent; + len = len - sent; + } + } + + @Override + public void close() throws IOException { + shutdownOutput(); + } + } + + + public static UnixSocket connect(String destination) throws IOException { + int fd = UnixSocketNative.socket(); + UnixSocketNative.connect(fd, destination); + return new UnixSocket(fd); + } + + @Override + public InputStream getInputStream() { + return in; + } + + @Override + public OutputStream getOutputStream() { + return out; + } + + + @Override + public synchronized void setReceiveBufferSize(int size) throws SocketException { + UnixSocketNative.setRecvBufSize(fd, size); + } + + @Override + public synchronized void setSendBufferSize(int size) throws SocketException { + UnixSocketNative.setSendBufSize(fd, size); + } + + + @Override + public void setSoTimeout(int timeout) throws SocketException { + UnixSocketNative.setRecvTimeout(fd, timeout); + + } + + @Override + public int getSoTimeout() throws SocketException { + return UnixSocketNative.getRecvTimeout(fd); + } + + @Override + public void connect(SocketAddress endpoint) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void connect(SocketAddress endpoint, int timeout) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void bind(SocketAddress bindpoint) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public InetAddress getInetAddress() { + return null; + } + + @Override + public InetAddress getLocalAddress() { + throw new UnsupportedOperationException(); + } + + @Override + public int getPort() { + return 0; + } + + @Override + public int getLocalPort() { + return -1; + } + + @Override + public SocketAddress getRemoteSocketAddress() { + return null; + } + + @Override + public SocketAddress getLocalSocketAddress() { + return null; + } + + + @Override + public boolean isConnected() { + return true; + } + + @Override + public boolean isBound() { + return true; + } + + + @Override + public boolean isClosed() { + return closed.get(); + } + + + @Override + public boolean isInputShutdown() { + return inputClosed.get(); + } + + @Override + public boolean isOutputShutdown() { + return outputClosed.get(); + } + + @Override + public void shutdownInput() throws IOException { + if (inputClosed.compareAndSet(false, true)) { + UnixSocketNative.shutdown(fd, true, false); + } else { + throw new SocketException("Input already shut down"); + } + } + + @Override + public void shutdownOutput() throws IOException { + if (outputClosed.compareAndSet(false, true)) { + UnixSocketNative.shutdown(fd, false, true); + } else { + throw new SocketException("Output already shut down"); + } + } + + @Override + public void close() throws IOException { + if (closed.compareAndSet(false, true)) { + UnixSocketNative.close(fd); + } + } + + +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixSocketException.java b/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixSocketException.java new file mode 100644 index 00000000..32803bae --- /dev/null +++ b/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixSocketException.java @@ -0,0 +1,18 @@ +package com.fnproject.fn.runtime.ntv; + +import java.net.SocketException; + +/** + * Created on 12/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class UnixSocketException extends SocketException { + public UnixSocketException(String message, String detail) { + super(message + ":" + detail); + } + + public UnixSocketException(String message) { + super(message); + } +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixSocketNative.java b/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixSocketNative.java new file mode 100644 index 00000000..0dd06fda --- /dev/null +++ b/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixSocketNative.java @@ -0,0 +1,60 @@ +package com.fnproject.fn.runtime.ntv; + + +import java.io.IOException; + +/** + * Created on 12/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +class UnixSocketNative { + + public UnixSocketNative() {} + + static { + String lib = System.mapLibraryName("fnunixsocket"); + + + String libLocation = System.getProperty("com.fnproject.java.native.libdir"); + if (libLocation != null) { + if (!libLocation.endsWith("/")) { + libLocation = libLocation + "/"; + } + lib = libLocation + lib; + System.load(lib); + }else{ + System.loadLibrary("fnunixsocket"); + } + } + + public static native int socket() throws IOException; + + public static native void bind(int socket, String path) throws UnixSocketException; + + public static native void connect(int socket, String path) throws IOException; + + public static native void listen(int socket, int backlog) throws UnixSocketException; + + public static native int accept(int socket, long timeoutMs) throws IOException; + + public static native int recv(int socket, byte[] buffer, int offset, int length) throws IOException; + + public static native int send(int socket, byte[] buffer, int offset, int length) throws IOException; + + public static native void close(int socket) throws UnixSocketException; + + public static native void setSendTimeout(int socket, int timeout) throws UnixSocketException; + + public static native int getSendTimeout(int socket) throws IOException; + + public static native void setRecvTimeout(int socket, int timeout) throws UnixSocketException; + + public static native int getRecvTimeout(int socket) throws UnixSocketException; + + public static native void setSendBufSize(int socket, int bufSize) throws UnixSocketException; + + public static native void setRecvBufSize(int socket, int bufSize) throws UnixSocketException; + + public static native void shutdown(int socket, boolean input, boolean output) throws UnixSocketException; +} diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/ConfigurationMethodsTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/ConfigurationMethodsTest.java index cdde6e98..6b9816d1 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/ConfigurationMethodsTest.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/ConfigurationMethodsTest.java @@ -16,51 +16,51 @@ public class ConfigurationMethodsTest { @Test public void staticTargetWithNoConfigurationIsOK() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(TestFnWithConfigurationMethods.StaticTargetNoConfiguration.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("StaticTargetNoConfiguration\nHello World"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("StaticTargetNoConfiguration\nHello World"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void instanceTargetWithNoConfigurationIsOK() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(TestFnWithConfigurationMethods.InstanceTargetNoConfiguration.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("InstanceTargetNoConfiguration\nHello World"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("InstanceTargetNoConfiguration\nHello World"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void staticTargetWithStaticConfigurationIsOK() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(TestFnWithConfigurationMethods.StaticTargetStaticConfiguration.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("StaticTargetStaticConfiguration\nHello World"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("StaticTargetStaticConfiguration\nHello World"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void instanceTargetWithStaticConfigurationIsOK() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(TestFnWithConfigurationMethods.InstanceTargetStaticConfiguration.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("InstanceTargetStaticConfiguration\nHello World"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("InstanceTargetStaticConfiguration\nHello World"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void staticTargetWithInstanceConfigurationIsAnError() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); String expectedMessage = "Configuration method " + "'config'" + @@ -68,78 +68,78 @@ public void staticTargetWithInstanceConfigurationIsAnError() throws Exception { fn.thenRun(TestFnWithConfigurationMethods.StaticTargetInstanceConfiguration.class, "echo"); - assertThat(fn.getStdOutAsString()).isEmpty(); + assertThat(fn.getOutputs()).isEmpty(); assertThat(fn.getStdErrAsString()).startsWith(expectedMessage); assertThat(fn.exitStatus()).isEqualTo(2); } @Test public void instanceTargetWithInstanceConfigurationIsOK() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(TestFnWithConfigurationMethods.InstanceTargetInstanceConfiguration.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("InstanceTargetInstanceConfiguration\nHello World"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("InstanceTargetInstanceConfiguration\nHello World"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void staticTargetWithStaticConfigurationWithoutRuntimeContextParameterIsOK() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(TestFnWithConfigurationMethods.StaticTargetStaticConfigurationNoRuntime.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("StaticTargetStaticConfigurationNoRuntime\nHello World"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("StaticTargetStaticConfigurationNoRuntime\nHello World"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void instanceTargetWithStaticConfigurationWithoutRuntimeContextParameterIsOK() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(TestFnWithConfigurationMethods.InstanceTargetStaticConfigurationNoRuntime.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("InstanceTargetStaticConfigurationNoRuntime\nHello World"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("InstanceTargetStaticConfigurationNoRuntime\nHello World"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void instanceTargetWithInstanceConfigurationWithoutRuntimeContextParameterIsOK() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(TestFnWithConfigurationMethods.InstanceTargetInstanceConfigurationNoRuntime.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("InstanceTargetInstanceConfigurationNoRuntime\nHello World"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("InstanceTargetInstanceConfigurationNoRuntime\nHello World"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldReturnDefaultParameterIfNotProvided() { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFnWithConfigurationMethods.WithGetConfigurationByKey.class, "getParam"); - assertThat(fn.getStdOutAsString()).isEqualTo("default"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("default"); } @Test public void shouldReturnSetConfigParameterWhenProvided() { String value = "value"; fn.setConfig("PARAM", value); - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFnWithConfigurationMethods.WithGetConfigurationByKey.class, "getParam"); - assertThat(fn.getStdOutAsString()).isEqualTo(value); + assertThat(fn.getOnlyOutputAsString()).isEqualTo(value); } @Test public void nonVoidConfigurationMethodIsAnError() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(TestFnWithConfigurationMethods.ConfigurationMethodIsNonVoid.class, "echo"); @@ -147,7 +147,7 @@ public void nonVoidConfigurationMethodIsAnError() throws Exception { "'config'" + " does not have a void return type"; - assertThat(fn.getStdOutAsString()).isEmpty(); + assertThat(fn.getOutputs()).isEmpty(); assertThat(fn.getStdErrAsString()).startsWith(expectedMessage); assertThat(fn.exitStatus()).isEqualTo(2); } @@ -156,13 +156,13 @@ public void nonVoidConfigurationMethodIsAnError() throws Exception { @Test public void shouldBeAbleToAccessConfigInConfigurationMethodWhenDefault() { fn.setConfig("FOO", "BAR"); - fn.givenDefaultEvent() + fn.givenEvent() .withBody("FOO") .enqueue(); fn.thenRun(TestFnWithConfigurationMethods.ConfigurationMethodWithAccessToConfig.class, "configByKey"); - assertThat(fn.getStdOutAsString()).isEqualTo("ConfigurationMethodWithAccessToConfig\nBAR"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("ConfigurationMethodWithAccessToConfig\nBAR"); } @Test @@ -174,7 +174,7 @@ public void shouldBeAbleToAccessConfigInConfigurationMethodWhenHttp() { fn.thenRun(TestFnWithConfigurationMethods.ConfigurationMethodWithAccessToConfig.class, "configByKey"); - assertThat(fn.getStdOutAsString()).contains("ConfigurationMethodWithAccessToConfig\nBAR"); + assertThat(fn.getOnlyOutputAsString()).contains("ConfigurationMethodWithAccessToConfig\nBAR"); } @Test @@ -186,19 +186,19 @@ public void shouldOnlyExtractConfigFromEnvironmentNotHeaderWhenHttp() { fn.thenRun(TestFnWithConfigurationMethods.ConfigurationMethodWithAccessToConfig.class, "configByKey"); - assertThat(fn.getStdOutAsString()).doesNotContain("BAR"); + assertThat(fn.getOnlyOutputAsString()).doesNotContain("BAR"); } @Test public void shouldNotBeAbleToAccessHeadersInConfigurationWhenDefault() { - fn.givenDefaultEvent() + fn.givenEvent() .withHeader("FOO", "BAR") .withBody("HEADER_FOO") .enqueue(); fn.thenRun(TestFnWithConfigurationMethods.ConfigurationMethodWithAccessToConfig.class, "configByKey"); - assertThat(fn.getStdOutAsString()).doesNotContain("ConfigurationMethodWithAccessToConfig\nBAR"); + assertThat(fn.getOnlyOutputAsString()).doesNotContain("ConfigurationMethodWithAccessToConfig\nBAR"); } @Test @@ -210,18 +210,18 @@ public void shouldNotBeAbleToAccessHeadersInConfigurationWhenHttp() { fn.thenRun(TestFnWithConfigurationMethods.ConfigurationMethodWithAccessToConfig.class, "configByKey"); - assertThat(fn.getStdOutAsString()).doesNotContain("ConfigurationMethodWithAccessToConfig\nBAR"); + assertThat(fn.getOnlyOutputAsString()).doesNotContain("ConfigurationMethodWithAccessToConfig\nBAR"); } @Test public void shouldCallInheritedConfigMethodsInRightOrder() { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); TestFnWithConfigurationMethods.SubConfigClass.order = ""; fn.thenRun(TestFnWithConfigurationMethods.SubConfigClass.class, "invoke"); - assertThat(fn.getStdOutAsString()).isEqualTo("OK"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("OK"); assertThat(TestFnWithConfigurationMethods.SubConfigClass.order) .matches("\\.baseStatic1\\.subStatic1\\.baseFn\\d\\.baseFn\\d\\.subFn\\d\\.subFn\\d\\.subFn\\d\\.subFn\\d"); } diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/DataBindingTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/DataBindingTest.java index 559af89d..16095e8a 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/DataBindingTest.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/DataBindingTest.java @@ -16,132 +16,132 @@ public class DataBindingTest { @Test public void shouldUseInputCoercionSpecifiedOnFunctionRuntimeContext() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(CustomDataBindingFnWithConfig.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("dlroW olleH"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("dlroW olleH"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldUseInputCoercionSpecifiedWithAnnotation() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(CustomDataBindingFnWithAnnotation.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("dlroW olleH"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("dlroW olleH"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldUseOutputCoercionSpecifiedOnFunctionRuntimeContext() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(CustomOutputDataBindingFnWithConfig.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("dlroW olleH"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("dlroW olleH"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldUseOutputCoercionSpecifiedWithAnnotation() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(CustomOutputDataBindingFnWithAnnotation.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("dlroW olleH"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("dlroW olleH"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldUseFirstInputCoercionSpecifiedOnFunctionRuntimeContext() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(CustomDataBindingFnWithMultipleCoercions.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("HELLO WORLD"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("HELLO WORLD"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldUseFirstOutputCoercionSpecifiedOnFunctionRuntimeContext() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(CustomOutputDataBindingFnWithMultipleCoercions.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("HELLO WORLD"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("HELLO WORLD"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldUseBuiltInInputCoercionSpecifiedOnFunctionRuntimeContext() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); - fn.thenRun(CustomDataBindingFnWithNoUserCoersions.class, "echo"); + fn.thenRun(CustomDataBindingFnWithNoUserCoercions.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("Hello World"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("Hello World"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldUseBuiltInOutputCoercionSpecifiedOnFunctionRuntimeContext() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(CustomOutputDataBindingFnWithNoUserCoercions.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("Hello World"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("Hello World"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldUseSecondInputCoercionSpecifiedOnFunctionRuntimeContext() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(CustomDataBindingFnWithDudCoercion.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("dlroW olleH"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("dlroW olleH"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldUseSecondOutputCoercionSpecifiedOnFunctionRuntimeContext() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(CustomOutputDataBindingFnWithDudCoercion.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("dlroW olleH"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("dlroW olleH"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldApplyCoercionsForInputAndOutputSpecifiedOnFunctionRuntimeContext() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(CustomDataBindingFnInputOutput.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("DLROW OLLEH"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("DLROW OLLEH"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldPrioritiseAnnotationOverConfig() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(CustomDataBindingFnWithAnnotationAndConfig.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("HELLO WORLD"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("HELLO WORLD"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/DefaultEventCodecTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/DefaultEventCodecTest.java deleted file mode 100644 index d7a501a7..00000000 --- a/runtime/src/test/java/com/fnproject/fn/runtime/DefaultEventCodecTest.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.fnproject.fn.runtime; - -import com.fnproject.fn.api.exception.FunctionInputHandlingException; -import com.fnproject.fn.api.InputEvent; -import com.fnproject.fn.api.OutputEvent; -import org.apache.commons.io.input.NullInputStream; -import org.apache.commons.io.output.NullOutputStream; -import org.junit.Test; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; - -import static com.fnproject.fn.runtime.HeaderBuilder.headerEntry; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.fail; - -public class DefaultEventCodecTest { - - private final Map emptyConfig = new HashMap<>(); - private InputStream asStream(String s) { - return new ByteArrayInputStream(s.getBytes()); - } - - @Test - public void shouldExtractBasicEvent() { - Map env = new HashMap<>(); - env.put("FN_FORMAT", "default"); - env.put("FN_METHOD", "GET"); - env.put("FN_APP_NAME", "testapp"); - env.put("FN_PATH", "/route"); - env.put("FN_REQUEST_URL", "http://test.com/fn/tryInvoke"); - - env.put("FN_HEADER_CONTENT_TYPE", "text/plain"); - env.put("FN_HEADER_ACCEPT", "text/html, text/plain;q=0.9"); - env.put("FN_HEADER_ACCEPT_ENCODING", "gzip"); - env.put("FN_HEADER_USER_AGENT", "userAgent"); - - Map config = new HashMap<>(); - config.put("configparam", "configval"); - config.put("CONFIGPARAM", "CONFIGVAL"); - - DefaultEventCodec codec = new DefaultEventCodec(env, asStream("input"), new NullOutputStream()); - InputEvent evt = codec.readEvent().get(); - assertThat(evt.getMethod()).isEqualTo("GET"); - assertThat(evt.getAppName()).isEqualTo("testapp"); - assertThat(evt.getRoute()).isEqualTo("/route"); - assertThat(evt.getRequestUrl()).isEqualTo("http://test.com/fn/tryInvoke"); - - - assertThat(evt.getHeaders().getAll().size()).isEqualTo(4); - assertThat(evt.getHeaders().getAll()).contains( - headerEntry("CONTENT_TYPE", "text/plain"), - headerEntry("ACCEPT_ENCODING", "gzip"), - headerEntry("ACCEPT", "text/html, text/plain;q=0.9"), - headerEntry("USER_AGENT", "userAgent")); - - evt.consumeBody((body) -> assertThat(body).hasSameContentAs(asStream("input"))); - - assertThat(codec.shouldContinue()).isFalse(); - } - - - - @Test - public void shouldRejectMissingEnv() { - Map requiredEnv = new HashMap<>(); - - requiredEnv.put("FN_PATH", "/route"); - requiredEnv.put("FN_METHOD", "GET"); - requiredEnv.put("FN_APP_NAME", "app_name"); - requiredEnv.put("FN_REQUEST_URL", "http://test.com/fn/tryInvoke"); - - for (String key : requiredEnv.keySet()) { - Map newEnv = new HashMap<>(requiredEnv); - newEnv.remove(key); - - DefaultEventCodec codec = new DefaultEventCodec(newEnv, asStream("input"), new NullOutputStream()); - - try{ - codec.readEvent(); - fail("Should have rejected missing env "+ key); - }catch(FunctionInputHandlingException e){ - assertThat(e).hasMessageContaining("Required environment variable " + key+ " is not set - are you running a function outside of fn run?"); - } - } - - } - - @Test - public void shouldWriteOutputDirectlyToOutputStream() throws IOException{ - - OutputEvent evt = OutputEvent.fromBytes("hello".getBytes(),OutputEvent.SUCCESS,"text/plain"); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - - DefaultEventCodec codec = new DefaultEventCodec(new HashMap<>(), new NullInputStream(0),bos); - codec.writeEvent(evt); - assertThat(new String(bos.toByteArray())).isEqualTo("hello"); - - } -} diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/EndToEndInvokeTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/EndToEndInvokeTest.java index 1a019b47..a028c368 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/EndToEndInvokeTest.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/EndToEndInvokeTest.java @@ -1,5 +1,6 @@ package com.fnproject.fn.runtime; +import com.fnproject.fn.api.InputEvent; import com.fnproject.fn.runtime.testfns.BadTestFnDuplicateMethods; import com.fnproject.fn.runtime.testfns.TestFn; import org.junit.BeforeClass; @@ -27,13 +28,13 @@ public static void setup() { @Test public void shouldResolveTestCallWithEnvVarParams() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); TestFn.setOutput("Hello World Out"); fn.thenRun(TestFn.class, "fnStringInOut"); assertThat(TestFn.getInput()).isEqualTo("Hello World"); - assertThat(fn.getStdOutAsString()).isEqualTo("Hello World Out"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("Hello World Out"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @@ -50,85 +51,81 @@ public void shouldResolveTestCallFromHotCall() throws Exception { @Test public void shouldSerializeGenericCollections() throws Exception { - fn.givenDefaultEvent().withBody("four").enqueue(); + fn.givenEvent().withBody("four").enqueue(); fn.thenRun(TestFn.class, "fnGenericCollections"); - assertThat(fn.getStdOutAsString()).isEqualTo("[\"one\",\"two\",\"three\",\"four\"]"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("[\"one\",\"two\",\"three\",\"four\"]"); } @Test public void shouldSerializeAnimalCollections() throws Exception { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFn.class, "fnGenericAnimal"); - assertThat(fn.getStdOutAsString()).isEqualTo("[{\"name\":\"Spot\",\"age\":6},{\"name\":\"Jason\",\"age\":16}]"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("[{\"name\":\"Spot\",\"age\":6},{\"name\":\"Jason\",\"age\":16}]"); } @Test public void shouldDeserializeGenericCollections() throws Exception { - fn.givenDefaultEvent() - .withHeader("Content-type", "application/json") - .withBody("[\"one\",\"two\",\"three\",\"four\"]") - .enqueue(); + fn.givenEvent() + .withHeader("Content-type", "application/json") + .withBody("[\"one\",\"two\",\"three\",\"four\"]") + .enqueue(); fn.thenRun(TestFn.class, "fnGenericCollectionsInput"); - assertThat(fn.getStdOutAsString()).isEqualTo("ONE"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("ONE"); } @Test public void shouldDeserializeCustomObjects() throws Exception { - fn.givenDefaultEvent() - .withHeader("Content-type", "application/json") - .withBody("[{\"name\":\"Spot\",\"age\":6},{\"name\":\"Jason\",\"age\":16}]") - .enqueue(); + fn.givenEvent() + .withHeader("Content-type", "application/json") + .withBody("[{\"name\":\"Spot\",\"age\":6},{\"name\":\"Jason\",\"age\":16}]") + .enqueue(); fn.thenRun(TestFn.class, "fnCustomObjectsCollectionsInput"); - assertThat(fn.getStdOutAsString()).isEqualTo("Spot"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("Spot"); } @Test public void shouldDeserializeComplexCustomObjects() throws Exception { - fn.givenDefaultEvent() - .withHeader("Content-type", "application/json") - .withBody("{\"number1\":[{\"name\":\"Spot\",\"age\":6}]," + - "\"number2\":[{\"name\":\"Spot\",\"age\":16}]}") - .enqueue(); + fn.givenEvent() + .withHeader("Content-type", "application/json") + .withBody("{\"number1\":[{\"name\":\"Spot\",\"age\":6}]," + + "\"number2\":[{\"name\":\"Spot\",\"age\":16}]}") + .enqueue(); fn.thenRun(TestFn.class, "fnCustomObjectsNestedCollectionsInput"); - assertThat(fn.getStdOutAsString()).isEqualTo("Spot"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("Spot"); } @Test public void shouldHandledStreamedHotInputEvent() throws Exception { fn.givenEvent() - .withBody("message1") - .withMethod("POST") - .enqueue(); + .withBody("message1") + .enqueue(); fn.givenEvent() - .withBody("message2") - .withMethod("GET") - .enqueue(); + .withBody("message2") + .enqueue(); - fn.thenRun(TestFn.class,"fnEcho"); + fn.thenRun(TestFn.class, "fnEcho"); - List responses = fn.getParsedHttpResponses(); + List responses = fn.getOutputs(); assertThat(responses).size().isEqualTo(2); - FnTestHarness.ParsedHttpResponse r1 = responses.get(0); - assertThat(r1.getBodyAsString()).isEqualTo("message1"); - - FnTestHarness.ParsedHttpResponse r2 = responses.get(1); - assertThat(r2.getBodyAsString()).isEqualTo("message2"); - + FnTestHarness.TestOutput r1 = responses.get(0); + assertThat(new String(r1.getBody())).isEqualTo("message1"); + FnTestHarness.TestOutput r2 = responses.get(1); + assertThat(new String(r2.getBody())).isEqualTo("message2"); } @@ -136,12 +133,10 @@ public void shouldHandledStreamedHotInputEvent() throws Exception { @Test public void shouldPrintErrorOnUnknownMethod() throws Exception { - - fn.thenRun(TestFn.class, "unknownMethod"); - assertThat(fn.getStdOutAsString()).isEqualTo(""); + assertThat(fn.exitStatus()).isEqualTo(2); + assertThat(fn.getOutputs()).isEmpty(); assertThat(fn.getStdErrAsString()).startsWith("Method 'unknownMethod' was not found in class 'com.fnproject.fn.runtime.testfns.TestFn'"); - } @@ -150,8 +145,8 @@ public void shouldPrintErrorOnUnknownClass() throws Exception { fn.thenRun("com.fnproject.unknown.Class", "unknownMethod"); - - assertThat(fn.getStdOutAsString()).isEqualTo(""); + assertThat(fn.exitStatus()).isEqualTo(2); + assertThat(fn.getOutputs()).hasSize(0); assertThat(fn.getStdErrAsString()).startsWith("Class 'com.fnproject.unknown.Class' not found in function jar"); } @@ -159,32 +154,32 @@ public void shouldPrintErrorOnUnknownClass() throws Exception { @Test public void shouldDirectStdOutToStdErrForFunctions() throws Exception { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFn.class, "fnWritesToStdout"); - assertThat(fn.getStdOutAsString()).isEqualTo(""); + assertThat(fn.getOnlyOutputAsString()).isEqualTo(""); assertThat(fn.getStdErrAsString()).isEqualTo("STDOUT"); } @Test public void shouldTerminateDefaultContainerOnExceptionWithError() throws Exception { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFn.class, "fnThrowsException"); assertThat(fn.getStdErrAsString()).startsWith("An error occurred in function:"); - assertThat(fn.getStdOutAsString()).isEmpty(); + assertThat(fn.getOnlyOutputAsString()).isEmpty(); assertThat(fn.exitStatus()).isEqualTo(1); } @Test public void shouldReadJsonObject() throws Exception { - fn.givenDefaultEvent() - .withHeader("Content-type", "application/json") - .withBody("{\"foo\":\"bar\"}") - .enqueue(); + fn.givenEvent() + .withHeader("Content-type", "application/json") + .withBody("{\"foo\":\"bar\"}") + .enqueue(); fn.thenRun(TestFn.class, "fnReadsJsonObj"); @@ -194,21 +189,21 @@ public void shouldReadJsonObject() throws Exception { @Test public void shouldWriteJsonData() throws Exception { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); TestFn.JsonData data = new TestFn.JsonData(); data.foo = "bar"; TestFn.setOutput(data); fn.thenRun(TestFn.class, "fnWritesJsonObj"); - assertThat(fn.getStdOutAsString()).isEqualTo("{\"foo\":\"bar\"}"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("{\"foo\":\"bar\"}"); } @Test public void shouldReadBytesOnDefaultCodec() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(TestFn.class, "fnReadsBytes"); @@ -216,15 +211,28 @@ public void shouldReadBytesOnDefaultCodec() throws Exception { } + @Test + public void shouldPrintLogFrame() throws Exception { + fn.setConfig("FN_LOGFRAME_NAME", "containerID"); + fn.setConfig("FN_LOGFRAME_HDR", "fnID"); + fn.givenEvent().withHeader("fnID", "fnIDVal").withBody( "Hello world!").enqueue(); + + fn.thenRun(TestFn.class, "fnEcho"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("Hello world!"); + // stdout gets redirected to stderr - hence printing out twice + assertThat(fn.getStdErrAsString()).isEqualTo("\ncontainerID=fnIDVal\n\n\ncontainerID=fnIDVal\n\n"); + + } + @Test public void shouldWriteBytesOnDefaultCodec() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); TestFn.setOutput("OK".getBytes()); fn.thenRun(TestFn.class, "fnWritesBytes"); - assertThat(fn.getStdOutAsString()).isEqualTo("OK"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("OK"); } @@ -232,18 +240,18 @@ public void shouldWriteBytesOnDefaultCodec() throws Exception { @Test public void shouldRejectDuplicateMethodsInFunctionClass() throws Exception { - fn.thenRun(BadTestFnDuplicateMethods.class,"fn"); - - assertThat(fn.getStdOutAsString()).isEmpty(); + fn.thenRun(BadTestFnDuplicateMethods.class, "fn"); + assertThat(fn.getOutputs()).isEmpty(); + assertThat(fn.exitStatus()).isEqualTo(2); assertThat(fn.getStdErrAsString()).startsWith("Multiple methods match"); } @Test public void shouldReadRawJson() throws Exception { - fn.givenDefaultEvent() - .withHeader("Content-type", "application/json") - .withBody("[\"foo\",\"bar\"]") - .enqueue(); + fn.givenEvent() + .withHeader("Content-type", "application/json") + .withBody("[\"foo\",\"bar\"]") + .enqueue(); fn.thenRun(TestFn.class, "fnReadsRawJson"); @@ -253,24 +261,35 @@ public void shouldReadRawJson() throws Exception { } - @Test - public void shouldReadMultipleMessageWhenInputIsNotParsed() throws Exception { - fn.givenHttpEvent().withBody("Hello World 1").enqueue(); - fn.givenHttpEvent().withBody("Hello World 2").enqueue(); + public void shouldReadInputHeaders() throws Exception{ + fn.givenEvent() + .withHeader("myHeader", "Foo") + .withHeader("a-n-header", "b0o","b10") + .enqueue(); + fn.thenRun(TestFn.class, "readRawEvent"); + InputEvent iev = (InputEvent)TestFn.getInput(); + assertThat(iev).isNotNull(); + assertThat(iev.getHeaders().getAllValues("Myheader")).contains("Foo"); + assertThat(iev.getHeaders().getAllValues("A-N-Header")).contains("b0o","b10"); - fn.thenRun(TestFn.class, "readSecondInput"); - List results = fn.getParsedHttpResponses(); - assertThat(results).hasSize(2); - assertThat(results.get(0).getStatus()).isEqualTo(200); - assertThat(results.get(0).getBodyAsString()).isEqualTo("first;"); - assertThat(results.get(1).getStatus()).isEqualTo(200); - assertThat(results.get(1).getBodyAsString()).isEqualTo("Hello World 2"); + } + @Test + public void shouldExposeOutputHeaders() throws Exception{ + fn.givenEvent() + .enqueue(); + fn.thenRun(TestFn.class, "setsOutputHeaders"); - } + FnTestHarness.TestOutput to = fn.getOutputs().get(0); + + System.err.println("got response" + to ); + assertThat(to.getContentType()).contains("foo-ct"); + assertThat(to.getHeaders().get("Header-1")).contains("v1"); + assertThat(to.getHeaders().getAllValues("A")).contains("b1","b2"); + } } diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/ErrorMessagesTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/ErrorMessagesTest.java index f4f96c07..49c472e8 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/ErrorMessagesTest.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/ErrorMessagesTest.java @@ -18,7 +18,7 @@ private void assertIsErrorWithoutStacktrace(String errorMessage) { assertThat(fn.getStdErrAsString().split(System.getProperty("line.separator")).length).isEqualTo(1); } - private void assertIsEntrypointErrorWithStacktrace(String errorMessage) { + private void assertIsEntryPointErrorWithStacktrace(String errorMessage) { assertThat(fn.exitStatus()).isEqualTo(2); assertThat(fn.getStdErrAsString()).contains(errorMessage); assertThat(fn.getStdErrAsString().split(System.getProperty("line.separator")).length).isGreaterThan(1); @@ -54,32 +54,32 @@ public void userSpecifiesMethodWhichDoesNotExist(){ @Test public void userFunctionInputCoercionError(){ - fn.givenDefaultEvent().withBody("This is not a...").enqueue(); + fn.givenEvent().withBody("This is not a...").enqueue(); fn.thenRun(ErrorMessages.OtherMethodsClass.class, "takesAnInteger"); - assertIsEntrypointErrorWithStacktrace("An exception was thrown during Input Coercion: Failed to coerce event to user function parameter type class java.lang.Integer"); + assertIsEntryPointErrorWithStacktrace("An exception was thrown during Input Coercion: Failed to coerce event to user function parameter type class java.lang.Integer"); } @Test public void objectConstructionThrowsARuntimeException(){ - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(StacktraceFilteringTestFunctions.ExceptionInConstructor.class, "invoke"); - assertIsEntrypointErrorWithStacktrace("Whoops"); + assertIsEntryPointErrorWithStacktrace("Whoops"); } @Test public void objectConstructionThrowsADeepException(){ - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(StacktraceFilteringTestFunctions.DeepExceptionInConstructor.class, "invoke"); - assertIsEntrypointErrorWithStacktrace("Inside a method called by the constructor"); + assertIsEntryPointErrorWithStacktrace("Inside a method called by the constructor"); assertThat(fn.getStdErrAsString()).contains("at not.in.com.fnproject.fn.StacktraceFilteringTestFunctions$DeepExceptionInConstructor.naughtyMethod"); assertThat(fn.getStdErrAsString()).contains("at not.in.com.fnproject.fn.StacktraceFilteringTestFunctions$DeepExceptionInConstructor."); } @Test public void objectConstructionThrowsANestedException(){ - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(StacktraceFilteringTestFunctions.NestedExceptionInConstructor.class, "invoke"); - assertIsEntrypointErrorWithStacktrace("Caused by: java.lang.RuntimeException: Oh no!"); + assertIsEntryPointErrorWithStacktrace("Caused by: java.lang.RuntimeException: Oh no!"); assertThat(fn.getStdErrAsString()).contains("at not.in.com.fnproject.fn.StacktraceFilteringTestFunctions$NestedExceptionInConstructor.naughtyMethod"); assertThat(fn.getStdErrAsString()).contains("Caused by: java.lang.ArithmeticException: / by zero"); assertThat(fn.getStdErrAsString()).contains("at not.in.com.fnproject.fn.StacktraceFilteringTestFunctions$NestedExceptionInConstructor.naughtyMethod"); @@ -88,25 +88,25 @@ public void objectConstructionThrowsANestedException(){ @Test public void fnConfigurationThrowsARuntimeException(){ - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(StacktraceFilteringTestFunctions.ExceptionInConfiguration.class, "invoke"); - assertIsEntrypointErrorWithStacktrace("Caused by: java.lang.RuntimeException: Config fail"); + assertIsEntryPointErrorWithStacktrace("Caused by: java.lang.RuntimeException: Config fail"); } @Test public void fnConfigurationThrowsADeepException(){ - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(StacktraceFilteringTestFunctions.DeepExceptionInConfiguration.class, "invoke"); - assertIsEntrypointErrorWithStacktrace("Caused by: java.lang.RuntimeException: Deep config fail"); + assertIsEntryPointErrorWithStacktrace("Caused by: java.lang.RuntimeException: Deep config fail"); assertThat(fn.getStdErrAsString()).contains("at not.in.com.fnproject.fn.StacktraceFilteringTestFunctions$DeepExceptionInConfiguration.throwDeep"); assertThat(fn.getStdErrAsString()).contains("at not.in.com.fnproject.fn.StacktraceFilteringTestFunctions$DeepExceptionInConfiguration.config"); } @Test public void fnConfigurationThrowsANestedException(){ - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(StacktraceFilteringTestFunctions.NestedExceptionInConfiguration.class, "invoke"); - assertIsEntrypointErrorWithStacktrace("Error invoking configuration method: config"); + assertIsEntryPointErrorWithStacktrace("Error invoking configuration method: config"); assertThat(fn.getStdErrAsString()).contains("Caused by: java.lang.RuntimeException: nested at 3"); assertThat(fn.getStdErrAsString()).contains("Caused by: java.lang.RuntimeException: nested at 2"); assertThat(fn.getStdErrAsString()).contains("Caused by: java.lang.RuntimeException: nested at 1"); @@ -116,7 +116,7 @@ public void fnConfigurationThrowsANestedException(){ @Test public void functionThrowsNestedException(){ - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(StacktraceFilteringTestFunctions.CauseStackTraceInResult.class, "invoke"); assertIsFunctionErrorWithStacktrace("An error occurred in function: Throw two"); assertThat(fn.getStdErrAsString()).contains("Caused by: java.lang.RuntimeException: Throw two"); diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/FnTestHarness.java b/runtime/src/test/java/com/fnproject/fn/runtime/FnTestHarness.java index 5434029e..6c5561b6 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/FnTestHarness.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/FnTestHarness.java @@ -1,30 +1,26 @@ package com.fnproject.fn.runtime; -import org.apache.commons.io.IOUtils; +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.OutputEvent; import org.apache.commons.io.output.TeeOutputStream; -import org.apache.http.HttpResponse; -import org.apache.http.NoHttpResponseException; -import org.apache.http.impl.io.ContentLengthInputStream; -import org.apache.http.impl.io.DefaultHttpResponseParser; -import org.apache.http.impl.io.HttpTransportMetricsImpl; -import org.apache.http.impl.io.SessionInputBufferImpl; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; import java.io.*; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.*; /** - * Function testing harness - this provides the call-side of iron functions' process contract for both HTTP and default type functions + * Function internal testing harness - this provides access the call-side of the functions contract excluding the codec which is mocked */ public class FnTestHarness implements TestRule { - private Map vars = new HashMap<>(); - private boolean hasEvents = false; - private InputStream pendingInput = new ByteArrayInputStream(new byte[0]); - private ByteArrayOutputStream stdOut = new ByteArrayOutputStream(); - private ByteArrayOutputStream stdErr = new ByteArrayOutputStream(); + private final List pendingInput = Collections.synchronizedList(new ArrayList<>()); + private final List output = Collections.synchronizedList(new ArrayList<>()); + private final ByteArrayOutputStream stdErr = new ByteArrayOutputStream(); private int exitStatus = -1; private final Map config = new HashMap<>(); @@ -46,44 +42,55 @@ public void setConfig(String key, String value) { /** * Gets a function config variable by key, or null if absent * - * @param key the configuration key + * @param key the configuration key */ public String getConfig(String key) { return config.get(key.toUpperCase().replaceAll("[- ]", "_")); } + public String getOnlyOutputAsString() { + if (output.size() != 1) { + throw new IllegalStateException("expecting exactly one result, got " + output.size()); + } + return new String(output.get(0).getBody()); + } + /** * Builds a mocked input event into the function runtime */ - public abstract class EventBuilder { - protected String method = "GET"; - protected String appName = "appName"; - protected String route = "/route"; - protected String requestUrl = "http://example.com/r/appName/route"; - protected InputStream body = new ByteArrayInputStream(new byte[0]); - protected int contentLength = 0; - protected String contentType = null; - - protected Map headers = new HashMap<>(); + public final class EventBuilder { + InputStream body = new ByteArrayInputStream(new byte[0]); + String contentType = null; + String callID = "callID"; + Instant deadline = Instant.now().plus(1, ChronoUnit.HOURS); + Headers headers = Headers.emptyHeaders(); /** * Add a header to the input * Duplicate headers will be overwritten * * @param key header key - * @param value header value + * @param v1 header value + * @param vs other header values */ - public EventBuilder withHeader(String key, String value) { + public EventBuilder withHeader(String key, String v1, String... vs) { Objects.requireNonNull(key, "key"); - Objects.requireNonNull(value, "value"); - headers.put(key, value); + Objects.requireNonNull(v1, "value"); + Objects.requireNonNull(vs, "vs"); + Arrays.stream(vs).forEach(v -> Objects.requireNonNull(v, "null value in varags list ")); + headers = headers.addHeader(key, v1); + for (String v : vs) { + headers = headers.addHeader(key, v); + } + return this; } /** * Add a series of headers to the input * This may override duplicate headers - * @param headers Map of headers to add + * + * @param headers Map of headers to add */ public EventBuilder withHeaders(Map headers) { headers.forEach(this::withHeader); @@ -94,21 +101,11 @@ public EventBuilder withHeaders(Map headers) { * Set the body of the request by providing an InputStream * * @param body the bytes of the body - * @param contentLength how long the body is supposed to be */ - public EventBuilder withBody(InputStream body, int contentLength) { + public EventBuilder withBody(InputStream body) { Objects.requireNonNull(body, "body"); - if (contentLength < 0) { - throw new IllegalArgumentException("Invalid contentLength"); - } - // This is for safety. Because we concatenate events, an input stream shorter than content length will cause - // the implementation to continue reading through to the next http request. We need to avoid a sort of - // buffer overrun. - // FIXME: Make InputStream handling simpler. - SessionInputBufferImpl sib = new SessionInputBufferImpl(new HttpTransportMetricsImpl(), 65535); - sib.bind(body); - this.body = new ContentLengthInputStream(sib, contentLength); - this.contentLength = contentLength; + + this.body = body; return this; } @@ -119,7 +116,7 @@ public EventBuilder withBody(InputStream body, int contentLength) { */ public EventBuilder withBody(byte[] body) { Objects.requireNonNull(body, "body"); - return withBody(new ByteArrayInputStream(body), body.length); + return withBody(new ByteArrayInputStream(body)); } /** @@ -132,116 +129,79 @@ public EventBuilder withBody(String body) { return withBody(stringAsBytes); } - /** - * Set the body of the request from a stream - * @param contentStream the content of the body - */ - public EventBuilder withBody(InputStream contentStream) throws IOException { - return withBody(IOUtils.toByteArray(contentStream)); - } /** - * Set the fn route associated with the call + * Prepare an event for the configured codec - this sets appropriate environment variable in the Env mock and StdIn mocks. + *

* - * @param route the route + * @throws IllegalStateException If the the codec only supports one event and an event has already been enqueued. */ - public EventBuilder withRoute(String route) { - Objects.requireNonNull(route, "route"); - this.route = route; - return this; - } + public void enqueue() { + InputEvent event = new ReadOnceInputEvent(body, headers, callID, deadline); + pendingInput.add(event); - /** - * Set the HTTP method of the incoming request - * - * @param method an HTTP method - * @return - */ - public EventBuilder withMethod(String method) { - Objects.requireNonNull(method, "method"); - this.method = method.toUpperCase(); - return this; } - /** - * Set the app name the incoming event - * - * @param appName the app name - * @return - */ - public EventBuilder withAppName(String appName) { - Objects.requireNonNull(appName, "appName"); - this.appName = appName; - return this; + Map commonEnv() { + Map env = new HashMap<>(config); + env.put("FN_APP_ID", "appID"); + env.put("FN_FN_ID", "fnID"); + + return env; } + } - /** - * Set the request URL of the incoming event - * - * @param requestUrl the request URL - * @return - */ - public EventBuilder withRequestUrl(String requestUrl) { - Objects.requireNonNull(requestUrl, "requestUrl"); - this.requestUrl = requestUrl; - return this; + static class TestOutput implements OutputEvent { + private final OutputEvent from; + byte[] body; + + TestOutput(OutputEvent from) throws IOException { + this.from = from; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + from.writeToOutput(bos); + body = bos.toByteArray(); } - /** - * Prepare an event for the configured codec - this sets appropriate environment variable in the Env mock and StdIn mocks. - *

- * - * @throws IllegalStateException If the the codec only supports one event and an event has already been enqueued. - */ - public abstract void enqueue(); + @Override + public Status getStatus() { + return from.getStatus(); + } - Map commonEnv() { - Map env = new HashMap<>(); - env.putAll(config); - headers.forEach((k, v) -> { - env.put("FN_HEADER_" + k.toUpperCase().replaceAll("-", "_"), v); - }); - env.put("FN_METHOD", method); - env.put("FN_APP_NAME", appName); - env.put("FN_PATH", route); - env.put("FN_REQUEST_URL", requestUrl); - return env; + @Override + public Optional getContentType() { + return from.getContentType(); } - } - private final class HttpEventBuilder extends EventBuilder { @Override - public void enqueue() { - StringBuilder inputString = new StringBuilder(); - // Only set env for first event. - if (!hasEvents) { - commonEnv().forEach(vars::put); - vars.put("FN_FORMAT", "http"); - } - inputString.append(method); - inputString.append(" / HTTP/1.1\r\n"); - inputString.append("Fn_App_name: ").append(appName).append("\r\n"); - inputString.append("Fn_Method: ").append(method).append("\r\n"); - inputString.append("Fn_Path: ").append(route).append("\r\n"); - inputString.append("Fn_Request_url: ").append(requestUrl).append("\r\n"); - if (contentType != null) { - inputString.append("Content-Type: ").append(contentType).append("\r\n"); - } + public Headers getHeaders() { + return from.getHeaders(); + } - inputString.append("Content-length: ").append(Integer.toString(contentLength)).append("\r\n"); - headers.forEach((k, v) -> { - inputString.append(k).append(": ").append(v).append("\r\n"); - }); + @Override + public void writeToOutput(OutputStream out) throws IOException { + out.write(body); + } - // added to the http request as headers to mimic the behaviour of `functions` but should NOT be used as config - config.forEach((k, v) -> { - inputString.append(k).append(": ").append(v).append("\r\n"); - }); - inputString.append("\r\n"); + public byte[] getBody() { + return body; + } - pendingInput = new SequenceInputStream(pendingInput, new ByteArrayInputStream(inputString.toString().getBytes())); - pendingInput = new SequenceInputStream(pendingInput, body); - hasEvents = true; + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("TestOutput{"); + sb.append("body="); + if (body == null) sb.append("null"); + else { + sb.append('['); + for (int i = 0; i < body.length; ++i) + sb.append(i == 0 ? "" : ", ").append(body[i]); + sb.append(']'); + } + sb.append(", status=").append(getStatus()); + sb.append(", contentType=").append(getContentType()); + sb.append(", headers=").append(getHeaders()); + sb.append('}'); + return sb.toString(); } } @@ -255,6 +215,27 @@ public void thenRun(Class cls, String method) { thenRun(cls.getName(), method); } + static class TestCodec implements EventCodec { + private final List input; + private final List output; + + TestCodec(List input, List output) { + this.input = input; + this.output = output; + } + + @Override + public void runCodec(Handler h) { + for (InputEvent in : input) { + try { + output.add(new TestOutput(h.handle(in))); + } catch (IOException e) { + throw new RuntimeException("Unexpected exception in test", e); + } + } + } + } + /** * Runs the function runtime with the specified class and method * @@ -266,17 +247,21 @@ public void thenRun(String cls, String method) { PrintStream oldSystemOut = System.out; PrintStream oldSystemErr = System.err; try { - PrintStream functionOut = new PrintStream(stdOut); PrintStream functionErr = new PrintStream(new TeeOutputStream(stdErr, oldSystemErr)); System.setOut(functionErr); System.setErr(functionErr); + + Map fnConfig = new HashMap<>(config); + fnConfig.put("FN_APP_ID", "appID"); + fnConfig.put("FN_FORMAT", "http-stream"); + fnConfig.put("FN_FN_ID", "fnID"); + + exitStatus = new EntryPoint().run( - vars, - pendingInput, - functionOut, - functionErr, - cls + "::" + method); - stdOut.flush(); + fnConfig, + new TestCodec(pendingInput, output), + functionErr, + cls + "::" + method); stdErr.flush(); } catch (Exception e) { throw new RuntimeException(e); @@ -305,7 +290,7 @@ public int exitStatus() { * @return a new event builder. */ public EventBuilder givenEvent() { - return new HttpEventBuilder(); + return new EventBuilder(); } @@ -334,117 +319,10 @@ public String getStdErrAsString() { * * @return the bytes returned by the function runtime; */ - public byte[] getStdOut() { - return stdOut.toByteArray(); + public List getOutputs() { + return output; } - /** - * Get the output produced by the runtime as a string. - *

- * For Hot functions this will include the HTTP envelope with (possibly) multiple messages - * - * @return a string representation of the function output - */ - public String getStdOutAsString() { - return new String(stdOut.toByteArray()); - } - - - /** - * A simple abstraction for a parsed HTTP response returned by a hot function - */ - public interface ParsedHttpResponse { - /** - * Return the body of the function result as a byte array - * - * @return the function response body - */ - byte[] getBodyAsBytes(); - - /** - * return the body of the function response as a string - * - * @return a function response body - */ - String getBodyAsString(); - - /** - * A map of he headers returned by the function - *

- * These are squashed so duplicated headers will be ignored (takes the first header) - * - * @return a map of headers - */ - Map getHeaders(); - - /** - * @return the HTTP status code returned by the function - */ - int getStatus(); - } - - /** - * Parses any pending HTTP responses on the functions stdout stream - * - * @return a list of Parsed HTTP responses from the function runtime output; - */ - public List getParsedHttpResponses() { - return getParsedHttpResponses(stdOut.toByteArray()); - } - - public static List getParsedHttpResponses(byte[] streamAsBytes) { - - SessionInputBufferImpl sib = new SessionInputBufferImpl(new HttpTransportMetricsImpl(), 65535); - ByteArrayInputStream parseStream = new ByteArrayInputStream(streamAsBytes); - sib.bind(parseStream); - - DefaultHttpResponseParser parser = new DefaultHttpResponseParser(sib); - List responses = new ArrayList<>(); - - while (true) { - try { - HttpResponse response = parser.parse(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - ContentLengthInputStream cis = new ContentLengthInputStream(sib, Long.parseLong(response.getFirstHeader("Content-length").getValue())); - - IOUtils.copy(cis, bos); - cis.close(); - byte[] body = bos.toByteArray(); - ParsedHttpResponse r = new ParsedHttpResponse() { - @Override - public byte[] getBodyAsBytes() { - return body; - } - - @Override - public String getBodyAsString() { - return new String(body); - } - - @Override - public Map getHeaders() { - Map headers = new HashMap<>(); - Arrays.stream(response.getAllHeaders()).forEach((h) -> { - headers.put(h.getName(), h.getValue()); - }); - return headers; - } - - @Override - public int getStatus() { - return response.getStatusLine().getStatusCode(); - } - }; - responses.add(r); - } catch (NoHttpResponseException e) { - break; - } catch (Exception e) { - throw new RuntimeException("Invalid HTTP response", e); - } - } - return responses; - - } @Override public Statement apply(Statement base, Description description) { @@ -452,34 +330,5 @@ public Statement apply(Statement base, Description description) { } - private final class DefaultEventBuilder extends EventBuilder { - boolean sent = false; - - @Override - public void enqueue() { - if (sent) { - throw new IllegalStateException("Cannot enqueue multiple default events "); - } - pendingInput = body; - sent = true; - commonEnv().forEach(vars::put); - } - } - - /** - * mock a default event (Input and stdOut encoded as stdin/stdout) - * - * @return a new event builder. - */ - public EventBuilder givenDefaultEvent() { - return new DefaultEventBuilder(); - } - - /** - * mock an http event - */ - public EventBuilder givenHttpEvent() { - return new HttpEventBuilder(); - } } diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/FunctionConstructionTests.java b/runtime/src/test/java/com/fnproject/fn/runtime/FunctionConstructionTest.java similarity index 83% rename from runtime/src/test/java/com/fnproject/fn/runtime/FunctionConstructionTests.java rename to runtime/src/test/java/com/fnproject/fn/runtime/FunctionConstructionTest.java index 4d78c9e1..4d439175 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/FunctionConstructionTests.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/FunctionConstructionTest.java @@ -9,38 +9,38 @@ /** * End-to-end tests for function configuration methods */ -public class FunctionConstructionTests { +public class FunctionConstructionTest { @Rule public final FnTestHarness fn = new FnTestHarness(); @Test public void shouldConstructWithDefaultConstructor() { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFnConstructors.DefaultEmptyConstructor.class, "invoke"); assertThat(fn.exitStatus()).isEqualTo(0); - assertThat(fn.getStdOutAsString()).isEqualTo("OK"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("OK"); } @Test public void shouldConstructWithExplicitConstructor() { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFnConstructors.ExplicitEmptyConstructor.class, "invoke"); assertThat(fn.exitStatus()).isEqualTo(0); - assertThat(fn.getStdOutAsString()).isEqualTo("OK"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("OK"); } @Test public void shouldInjectConfigIntoConstructor() { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFnConstructors.ConfigurationOnConstructor.class, "invoke"); assertThat(fn.exitStatus()).isEqualTo(0); - assertThat(fn.getStdOutAsString()).isEqualTo("OK"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("OK"); } @Test public void shouldFailWithInaccessibleConstructor() { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFnConstructors.BadConstructorNotAccessible.class, "invoke"); assertThat(fn.exitStatus()).isEqualTo(2); assertThat(fn.getStdErrAsString()).contains("cannot be instantiated as it has no public constructors"); @@ -48,7 +48,7 @@ public void shouldFailWithInaccessibleConstructor() { @Test public void shouldFailFunctionWithTooManyConstructorArgs() { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFnConstructors.BadConstructorTooManyArgs.class, "invoke"); assertThat(fn.exitStatus()).isEqualTo(2); assertThat(fn.getStdErrAsString()).contains("cannot be instantiated as its constructor takes more than one argument"); @@ -56,7 +56,7 @@ public void shouldFailFunctionWithTooManyConstructorArgs() { @Test public void shouldFailFunctionWithAmbiguousConstructors() { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFnConstructors.BadConstructorAmbiguousConstructors.class, "invoke"); assertThat(fn.exitStatus()).isEqualTo(2); assertThat(fn.getStdErrAsString()).contains("cannot be instantiated as it has multiple public constructors"); @@ -64,7 +64,7 @@ public void shouldFailFunctionWithAmbiguousConstructors() { @Test public void shouldFailFunctionWithErrorInConstructor() { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFnConstructors.BadConstructorThrowsException.class, "invoke"); assertThat(fn.exitStatus()).isEqualTo(2); assertThat(fn.getStdErrAsString()).contains("An error occurred in the function constructor while instantiating class"); @@ -72,7 +72,7 @@ public void shouldFailFunctionWithErrorInConstructor() { @Test public void shouldFailFunctionWithBadSingleConstructConstructorArg() { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFnConstructors.BadConstructorUnrecognisedArg.class, "invoke"); assertThat(fn.exitStatus()).isEqualTo(2); assertThat(fn.getStdErrAsString()).contains("cannot be instantiated as its constructor takes an unrecognized argument of type int"); @@ -81,7 +81,7 @@ public void shouldFailFunctionWithBadSingleConstructConstructorArg() { @Test public void shouldFailNonStaticInnerClassWithANiceMessage(){ - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFnConstructors.NonStaticInnerClass.class, "invoke"); assertThat(fn.exitStatus()).isEqualTo(2); assertThat(fn.getStdErrAsString()).contains("cannot be instantiated as it is a non-static inner class"); diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/HTTPStreamCodecTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/HTTPStreamCodecTest.java new file mode 100644 index 00000000..2b3c07ed --- /dev/null +++ b/runtime/src/test/java/com/fnproject/fn/runtime/HTTPStreamCodecTest.java @@ -0,0 +1,330 @@ +package com.fnproject.fn.runtime; + + +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.OutputEvent; +import org.apache.commons.io.IOUtils; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.client.util.BytesContentProvider; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.unixsocket.client.HttpClientTransportOverUnixSockets; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.Timeout; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.attribute.PosixFilePermissions; +import java.security.MessageDigest; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * This uses the Jetty client largely as witness of "good HTTP behaviour" + * Created on 24/08/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class HTTPStreamCodecTest { + + + @Rule + public final Timeout to = Timeout.builder().withTimeout(60, TimeUnit.SECONDS).withLookingForStuckThread(true).build(); + + private static final Map defaultEnv; + private final List cleanups = new ArrayList<>(); + + private static File generateSocketFile() { + File f ; + try { + f = File.createTempFile("socket", ".sock"); + f.delete(); + f.deleteOnExit(); + } catch (IOException e) { + throw new RuntimeException("Error creating socket file",e); + } + + return f; + } + + static { + System.setProperty("com.fnproject.java.native.libdir", new File("src/main/c/").getAbsolutePath()); + System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.StdErrLog"); + System.setProperty("org.eclipse.jetty.LEVEL", "WARN"); + + + Map env = new HashMap<>(); + env.put("FN_APP_NAME", "myapp"); + env.put("FN_PATH", "mypath"); + + defaultEnv = Collections.unmodifiableMap(env); + } + + private HttpClient createClient(File unixSocket) throws Exception { + HttpClient client = new HttpClient(new HttpClientTransportOverUnixSockets(unixSocket.getAbsolutePath()), null); + client.start(); + cleanups.add(() -> { + try { + client.stop(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + return client; + } + + private Request defaultRequest(HttpClient httpClient) { + return httpClient.newRequest("http://localhost/call") + .method("POST") + .header("Fn-Call-Id", "callID") + .header("Fn-Deadline", "2002-10-02T10:00:00.992Z") + .header("Custom-header", "v1") + .header("Custom-header", "v2") + .header("Content-Type", "text/plain") + .content(new StringContentProvider("hello ")); + + } + + + @After + public void cleanup() throws Exception { + cleanups.forEach(Runnable::run); + } + + + File startCodec(Map env, EventCodec.Handler h) { + Map newEnv = new HashMap<>(env); + File socket = generateSocketFile(); + newEnv.put("FN_LISTENER", "unix:" + socket.getAbsolutePath()); + + HTTPStreamCodec codec = new HTTPStreamCodec(newEnv); + + Thread t = new Thread(() -> codec.runCodec(h)); + t.start(); + cleanups.add(() -> { + try { + codec.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + return socket; + } + + @Test + public void shouldAcceptDataOnHttp() throws Exception { + CompletableFuture lastEvent = new CompletableFuture<>(); + + File socketFile = startCodec(defaultEnv, (in) -> { + lastEvent.complete(in); + return OutputEvent.fromBytes("hello".getBytes(), OutputEvent.Status.Success, "text/plain", Headers.emptyHeaders().addHeader("x-test", "bar")); + }); + + HttpClient client = createClient(socketFile); + ContentResponse resp = client.newRequest("http://localhost/call") + .method("POST") + .header("Fn-Call-Id", "callID") + .header("Fn-Deadline", "2002-10-02T10:00:00.992Z") + .header("Custom-header", "v1") + .header("Custom-header", "v2") + .header("Content-Type", "text/plain") + .content(new StringContentProvider("hello ")).send(); + + assertThat(resp.getStatus()).isEqualTo(200); + assertThat(resp.getContent()).isEqualTo("hello".getBytes()); + assertThat(resp.getHeaders().get("x-test")).isEqualTo("bar"); + + InputEvent evt = lastEvent.get(1, TimeUnit.MILLISECONDS); + assertThat(evt.getCallID()).isEqualTo("callID"); + assertThat(evt.getDeadline().toEpochMilli()).isEqualTo(1033552800992L); + assertThat(evt.getHeaders()).isEqualTo(Headers.emptyHeaders().addHeader("Fn-Call-Id", "callID").addHeader("Fn-Deadline", "2002-10-02T10:00:00.992Z").addHeader("Custom-header", "v1", "v2").addHeader("Content-Type", "text/plain").addHeader("Content-Length", "6")); + + } + + @Test + public void shouldRejectFnMissingHeaders() throws Exception { + + Map headers = new HashMap<>(); + headers.put("Fn-Call-Id", "callID"); + headers.put("Fn-Deadline", "2002-10-02T10:00:00.992Z"); + + + File socket = startCodec(defaultEnv, (in) -> OutputEvent.emptyResult(OutputEvent.Status.Success)); + + HttpClient client = createClient(socket); + + Request positive = client.newRequest("http://localhost/call") + .method("POST"); + headers.forEach(positive::header); + assertThat(positive.send().getStatus()).withFailMessage("Expecting req with mandatory headers to pass").isEqualTo(200); + + for (String h : headers.keySet()) { + Request r = client.newRequest("http://localhost/call") + .method("POST"); + headers.forEach((k, v) -> { + if (!k.equals(h)) { + r.header(k, v); + } + }); + + + ContentResponse resp = r.send(); + + assertThat(resp.getStatus()).withFailMessage("Expected failure error code for missing header " + h).isEqualTo(500); + + } + } + + @Test + public void shouldHandleMultipleRequests() throws Exception { + AtomicReference lastInput = new AtomicReference<>(); + AtomicInteger count = new AtomicInteger(0); + + File socket = startCodec(defaultEnv, (in) -> { + byte[] body = in.consumeBody((is) -> { + try { + return IOUtils.toByteArray(is); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + lastInput.set(body); + return OutputEvent.fromBytes(body, OutputEvent.Status.Success, "application/octet-stream", Headers.emptyHeaders()); + }); + + HttpClient httpClient = createClient(socket); + + for (int i = 0; i < 200; i++) { + byte[] body = randomBytes(i * 1997); + ContentResponse resp = httpClient.newRequest("http://localhost/call") + .method("POST") + .header("Fn-Call-Id", "callID") + .header("Fn-Deadline", "2002-10-02T10:00:00.992Z") + .header("Custom-header", "v1") + .header("Custom-header", "v2") + .header("Content-Type", "text/plain") + .content(new BytesContentProvider(body)).send(); + + assertThat(resp.getStatus()).isEqualTo(200); + assertThat(resp.getContent()).isEqualTo(body); + assertThat(lastInput).isNotNull(); + assertThat(lastInput.get()).isEqualTo(body); + } + + } + + @Test + public void shouldHandleLargeBodies() throws Exception { + // Round trips 10 meg of data through the codec and validates it got the right stuff back + byte[] randomString = randomBytes(1024 * 1024 * 10); + byte[] inDigest = MessageDigest.getInstance("SHA-256").digest(randomString); + + + File socket = startCodec(defaultEnv, (in) -> { + byte[] content = in.consumeBody((is) -> { + try { + return IOUtils.toByteArray(is); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + return OutputEvent.fromBytes(content, OutputEvent.Status.Success, "application/octet-binary", Headers.emptyHeaders()); + }); + + HttpClient client = createClient(socket); + + CompletableFuture cdl = new CompletableFuture<>(); + MessageDigest readDigest = MessageDigest.getInstance("SHA-256"); + defaultRequest(client) + .content(new BytesContentProvider(randomString)) + .onResponseContent((response, byteBuffer) -> readDigest.update(byteBuffer)) + .send(cdl::complete); + Result r = cdl.get(); + assertThat(r.getResponse().getStatus()).isEqualTo(200); + assertThat(readDigest.digest()).isEqualTo(inDigest); + } + + private byte[] randomBytes(int sz) { + Random sr = new Random(); + byte[] part = new byte[997]; + sr.nextBytes(part); + + // Make random ascii for convenience in debugging + for(int i = 0 ; i < part.length; i++){ + part[i] = (byte)((part[i]%26) + 65); + } + + byte[] randomString = new byte[sz]; + int left = sz; + for (int i = 0; i < randomString.length; i += part.length) { + int copy = Math.min(left, part.length); + + System.arraycopy(part, 0, randomString, i, copy); + left -= part.length; + } + return randomString; + } + + @Test + public void shouldConvertStatusResponses() throws Exception { + + for (OutputEvent.Status s : OutputEvent.Status.values()) { + CompletableFuture lastEvent = new CompletableFuture<>(); + + File socket = startCodec(defaultEnv, (in) -> { + lastEvent.complete(in); + return OutputEvent.fromBytes("hello".getBytes(), s, "text/plain", Headers.emptyHeaders()); + }); + + HttpClient client = createClient(socket); + + ContentResponse resp = defaultRequest(client).send(); + + assertThat(resp.getStatus()).isEqualTo(s.getCode()); + } + } + + @Test + public void shouldStripHopToHopHeadersFromFunctionInput() throws Exception { + + for (String header[] : new String[][]{ + {"Transfer-encoding", "chunked"}, + {"Connection", "close"}, + }) { + CompletableFuture lastEvent = new CompletableFuture<>(); + + File socket = startCodec(defaultEnv, (in) -> { + lastEvent.complete(in); + return OutputEvent.fromBytes("hello".getBytes(), OutputEvent.Status.Success, "text/plain", Headers.emptyHeaders().addHeader(header[0], header[1])); + }); + HttpClient client = createClient(socket); + ContentResponse resp = defaultRequest(client).send(); + + assertThat(resp.getHeaders().get(header[0])).isNull(); + + } + } + + @Test + public void socketShouldHaveCorrectPermissions() throws Exception { + File listener = startCodec(defaultEnv, (in) -> OutputEvent.fromBytes("hello".getBytes(), OutputEvent.Status.Success, "text/plain", Headers.emptyHeaders())); + assertThat(Files.getPosixFilePermissions(listener.toPath())).isEqualTo(PosixFilePermissions.fromString("rw-rw-rw-")); + + cleanup(); + } + + +} diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/HeaderBuilder.java b/runtime/src/test/java/com/fnproject/fn/runtime/HeaderBuilder.java index bc5c4895..69c3fc7c 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/HeaderBuilder.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/HeaderBuilder.java @@ -1,10 +1,14 @@ package com.fnproject.fn.runtime; +import com.fnproject.fn.api.Headers; + import java.util.AbstractMap; +import java.util.Arrays; +import java.util.List; import java.util.Map; class HeaderBuilder { - static Map.Entry headerEntry(String key, String value) { - return new AbstractMap.SimpleEntry<>(key, value); + static Map.Entry> headerEntry(String key, String... values) { + return new AbstractMap.SimpleEntry<>(Headers.canonicalKey(key), Arrays.asList(values)); } } diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/HttpEventCodecTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/HttpEventCodecTest.java deleted file mode 100644 index 0f06c49b..00000000 --- a/runtime/src/test/java/com/fnproject/fn/runtime/HttpEventCodecTest.java +++ /dev/null @@ -1,287 +0,0 @@ -package com.fnproject.fn.runtime; - -import com.fnproject.fn.api.Headers; -import com.fnproject.fn.api.InputEvent; -import com.fnproject.fn.api.OutputEvent; -import com.fnproject.fn.api.exception.FunctionInputHandlingException; -import org.apache.commons.io.input.NullInputStream; -import org.apache.commons.io.output.NullOutputStream; -import org.junit.Test; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Collectors; - -import static com.fnproject.fn.runtime.HeaderBuilder.headerEntry; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; -import static org.junit.Assert.fail; - -public class HttpEventCodecTest { - private final OutputStream nullOut = new NullOutputStream(); - private final InputStream nullIn = new NullInputStream(0); - - private final String postReq = "GET /test HTTP/1.1\n" + - "Accept-Encoding: gzip\n" + - "User-Agent: useragent\n" + - "Accept: text/html, text/plain;q=0.9\n" + - "Fn_Request_url: http//localhost:8080/r/testapp/test\n" + - "Fn_Path: /test\n" + - "Fn_Method: POST\n" + - "Content-Length: 11\n" + - "Fn_App_name: testapp\n" + - "Fn_Call_id: task-id\n" + - "Myconfig: fooconfig\n" + - "Content-Type: text/plain\n\n" + - "Hello World"; - - - private final String getReq = "GET /test HTTP/1.1\n" + - "Accept-Encoding: gzip\n" + - "User-Agent: useragent\n" + - "Fn_Request_url: http//localhost:8080/r/testapp/test\n" + - "Fn_Method: GET\n" + - "Content-Length: 0\n" + - "Fn_Call_Id: task-id2\n" + - "Myconfig: fooconfig\n\n"; - - private final Map emptyConfig = new HashMap<>(); - private final Map env() { - HashMap env = new HashMap<>(); - env.put("FN_APP_NAME", "testapp"); - env.put("FN_PATH", "/test"); - return env; - } - - @Test - public void testParsingSimpleHttpRequestWithFnHeadersAndBody() { - ByteArrayInputStream bis = new ByteArrayInputStream(postReq.getBytes()); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - HttpEventCodec httpEventCodec = new HttpEventCodec(env(), bis, bos); - - InputEvent event = httpEventCodec.readEvent().get(); - isExpectedPostEvent(event); - } - - @Test - public void shouldReadMultipleRequestsOnSameStream() { - byte req1[] = postReq.getBytes(); - byte req2[] = getReq.getBytes(); - byte req3[] = postReq.getBytes(); - - byte input[] = new byte[req1.length + req2.length + req3.length]; - - System.arraycopy(req1, 0, input, 0, req1.length); - System.arraycopy(req2, 0, input, req1.length, req2.length); - System.arraycopy(req3, 0, input, req1.length + req2.length, req3.length); - - ByteArrayInputStream bis = new ByteArrayInputStream(input); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - HttpEventCodec httpEventCodec = new HttpEventCodec(env(), bis, bos); - - InputEvent postEvent = httpEventCodec.readEvent().get(); - isExpectedPostEvent(postEvent); - - InputEvent getEvent = httpEventCodec.readEvent().get(); - isExpectedGetEvent(getEvent); - - InputEvent postEvent2 = httpEventCodec.readEvent().get(); - isExpectedPostEvent(postEvent2); - - - } - - @Test - public void shouldRejectInvalidHttpRequest() { - try { - HttpEventCodec httpEventCodec = new HttpEventCodec(env(), asStream("NOT_HTTP " + getReq), nullOut); - - httpEventCodec.readEvent(); - fail(); - } catch (FunctionInputHandlingException e) { - assertThat(e).hasMessageContaining("Failed to read HTTP content from input"); - } - } - - - @Test - public void shouldRejectMissingHttpHeaders() { - - Map requiredHeaders = new HashMap<>(); - requiredHeaders.put("fn_request_url", "request_url"); - requiredHeaders.put("fn_method", "GET"); - - for (String key : requiredHeaders.keySet()) { - Map newMap = new HashMap<>(requiredHeaders); - newMap.remove(key); - String req = "GET / HTTP/1.1\n" + newMap.entrySet().stream().map(e -> e.getKey() + ": " + e.getValue()).collect(Collectors.joining("\n")) + "\n\n"; - - try { - HttpEventCodec httpEventCodec = new HttpEventCodec(env(), asStream(req), nullOut); - httpEventCodec.readEvent(); - fail("Should fail with header missing:" + key); - } catch (FunctionInputHandlingException e) { - assertThat(e).hasMessageMatching("Incoming HTTP frame is missing required header: " + key); - } - } - } - - @Test - public void shouldRejectMissingEnv() { - Map requiredEnv = new HashMap<>(); - - requiredEnv.put("FN_PATH", "/route"); - requiredEnv.put("FN_APP_NAME", "app_name"); - - for (String key : requiredEnv.keySet()) { - Map newMap = new HashMap<>(requiredEnv); - newMap.remove(key); - - try { - ByteArrayInputStream bis = new ByteArrayInputStream(postReq.getBytes()); - HttpEventCodec httpEventCodec = new HttpEventCodec(newMap, bis, nullOut); - httpEventCodec.readEvent(); - fail("Should fail with header missing:" + key); - } catch (FunctionInputHandlingException e) { - assertThat(e).hasMessageMatching("Required environment variable " + key + " is not set - are you running a function outside of fn run\\?"); - } - } - } - - @Test - public void shouldSerializeSimpleSuccessfulEvent() throws Exception{ - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - - HttpEventCodec httpEventCodec = new HttpEventCodec(env(), nullIn, bos); - OutputEvent outEvent = OutputEvent.fromBytes("Hello".getBytes(),OutputEvent.SUCCESS,"text/plain"); - - httpEventCodec.writeEvent(outEvent); - String httpResponse = new String(bos.toByteArray()); - - assertThat(statusLine(httpResponse)).isEqualTo("HTTP/1.1 200 INVOKED"); - assertThat(headers(httpResponse)).containsOnly(entry("content-type", "text/plain"), entry("content-length", "5")); - assertThat(body(httpResponse)).isEqualTo("Hello"); - - } - - private static String statusLine(String httpResponse) { - return httpResponse.split("\\\r\\\n", 2)[0]; - } - - private static Map headers(String httpResponse) { - Map hs = new HashMap<>(); - boolean firstLine = true; - for (String line: httpResponse.split("\\\r\\\n")) { - if (line.equals("")) { break; } - if (firstLine) { - firstLine = false; - continue; - } - String[] parts = line.split(": *", 2); - hs.put(parts[0].toLowerCase(), parts[1]); - } - return hs; - } - - private static String body(String httpResponse) { - return httpResponse.split("\\\r\\\n\\\r\\\n", 2)[1]; - } - - @Test - public void shouldSerializeSuccessfulEventWithHeaders() throws Exception{ - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - - HttpEventCodec httpEventCodec = new HttpEventCodec(env(), nullIn, bos); - Map hs = new HashMap<>(); - hs.put("foo", "bar"); - hs.put("Content-Type", "application/octet-stream"); // ignored - hs.put("Content-length", "99"); // ignored - OutputEvent outEvent = OutputEvent.fromBytes("Hello".getBytes(),OutputEvent.SUCCESS,"text/plain", Headers.fromMap(hs)); - - httpEventCodec.writeEvent(outEvent); - String httpResponse = new String(bos.toByteArray()); - - assertThat(statusLine(httpResponse)).isEqualTo("HTTP/1.1 200 INVOKED"); - assertThat(headers(httpResponse)).containsOnly(entry("foo", "bar"), - entry("content-type", "text/plain"), - entry("content-length", "5")); - assertThat(body(httpResponse)).isEqualTo("Hello"); - } - - - @Test - public void shouldSerializeSimpleFailedEvent() throws Exception{ - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - - HttpEventCodec httpEventCodec = new HttpEventCodec(env(), nullIn, bos); - OutputEvent outEvent = OutputEvent.fromBytes("Hello".getBytes(), OutputEvent.FAILURE,"text/plain"); - - httpEventCodec.writeEvent(outEvent); - String httpResponse = new String(bos.toByteArray()); - - assertThat(statusLine(httpResponse)).isEqualTo("HTTP/1.1 500 INVOKE FAILED"); - assertThat(headers(httpResponse)).containsOnly(entry("content-type", "text/plain"), - entry("content-length", "5")); - assertThat(body(httpResponse)).isEqualTo("Hello"); - } - - - @Test - public void shouldSerializeFailedEventWithHeaders() throws Exception{ - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - - HttpEventCodec httpEventCodec = new HttpEventCodec(env(), nullIn,bos); - Map hs = new HashMap<>(); - hs.put("foo", "bar"); - hs.put("Content-Type", "application/octet-stream"); // ignored - hs.put("Content-length", "99"); // ignored - OutputEvent outEvent = OutputEvent.fromBytes("Hello".getBytes(), OutputEvent.FAILURE,"text/plain", Headers.fromMap(hs)); - - httpEventCodec.writeEvent(outEvent); - String httpResponse = new String(bos.toByteArray()); - - assertThat(statusLine(httpResponse)).isEqualTo("HTTP/1.1 500 INVOKE FAILED"); - assertThat(headers(httpResponse)).containsOnly(entry("foo", "bar"), - entry("content-type", "text/plain"), - entry("content-length", "5")); - assertThat(body(httpResponse)).isEqualTo("Hello"); - } - - - private InputStream asStream(String sin) { - return new ByteArrayInputStream(sin.getBytes()); - } - - private void isExpectedGetEvent(InputEvent getEvent) { - assertThat(getEvent.getAppName()).isEqualTo("testapp"); - assertThat(getEvent.getMethod()).isEqualTo("GET"); - assertThat(getEvent.getRoute()).isEqualTo("/test"); - - assertThat(getEvent.getHeaders().getAll()) - .contains(headerEntry("Accept-Encoding", "gzip"), - headerEntry("User-Agent", "useragent")); - - - getEvent.consumeBody((is) -> assertThat(is).hasSameContentAs(asStream(""))); - } - - private void isExpectedPostEvent(InputEvent postEvent) { - assertThat(postEvent.getAppName()).isEqualTo("testapp"); - assertThat(postEvent.getMethod()).isEqualTo("POST"); - assertThat(postEvent.getRoute()).isEqualTo("/test"); - assertThat(postEvent.getHeaders().getAll().size()).isEqualTo(11); - assertThat(postEvent.getHeaders().getAll()) - .contains(headerEntry("Accept", "text/html, text/plain;q=0.9"), - headerEntry("Accept-Encoding", "gzip"), - headerEntry("User-Agent", "useragent"), - headerEntry("Content-Type", "text/plain")); - - postEvent.consumeBody((is) -> assertThat(is).hasSameContentAs(asStream("Hello World"))); - } - - -} diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/JacksonCoercionTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/JacksonCoercionTest.java index 28124dbb..35b3cba4 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/JacksonCoercionTest.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/JacksonCoercionTest.java @@ -9,7 +9,11 @@ import org.junit.Test; import java.io.ByteArrayInputStream; -import java.util.*; +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; public class JacksonCoercionTest { @@ -24,13 +28,13 @@ public void listOfCustomObjects() throws NoSuchMethodException { MethodWrapper method = new DefaultMethodWrapper(JacksonCoercionTest.class, "testMethod"); FunctionRuntimeContext frc = new FunctionRuntimeContext(method, new HashMap<>()); - FunctionInvocationContext invocationContext = new FunctionInvocationContext(frc); + FunctionInvocationContext invocationContext = new FunctionInvocationContext(frc,new ReadOnceInputEvent(new ByteArrayInputStream(new byte[0]),Headers.emptyHeaders(),"callID",Instant.now())); Map headers = new HashMap<>(); headers.put("content-type", "application/json"); ByteArrayInputStream body = new ByteArrayInputStream("[{\"name\":\"Spot\",\"age\":6},{\"name\":\"Jason\",\"age\":16}]".getBytes()); - InputEvent inputEvent = new ReadOnceInputEvent("", "", "", "testMethod", body, Headers.fromMap(headers), new QueryParametersImpl()); + InputEvent inputEvent = new ReadOnceInputEvent( body, Headers.fromMap(headers),"call",Instant.now()); Optional object = jc.tryCoerceParam(invocationContext, 0, inputEvent, method); @@ -46,13 +50,13 @@ public void failureToParseIsUserFriendlyError() throws NoSuchMethodException { MethodWrapper method = new DefaultMethodWrapper(JacksonCoercionTest.class, "testMethod"); FunctionRuntimeContext frc = new FunctionRuntimeContext(method, new HashMap<>()); - FunctionInvocationContext invocationContext = new FunctionInvocationContext(frc); + FunctionInvocationContext invocationContext = new FunctionInvocationContext(frc,new ReadOnceInputEvent(new ByteArrayInputStream(new byte[0]),Headers.emptyHeaders(),"callID",Instant.now())); Map headers = new HashMap<>(); headers.put("content-type", "application/json"); ByteArrayInputStream body = new ByteArrayInputStream("INVALID JSON".getBytes()); - InputEvent inputEvent = new ReadOnceInputEvent("", "", "", "testMethod", body, Headers.fromMap(headers), new QueryParametersImpl()); + InputEvent inputEvent = new ReadOnceInputEvent( body, Headers.fromMap(headers), "call",Instant.now()); boolean causedExpectedError; try { diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/MethodWrapperTests.java b/runtime/src/test/java/com/fnproject/fn/runtime/MethodWrapperTests.java index 2d4cca57..7f8341ad 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/MethodWrapperTests.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/MethodWrapperTests.java @@ -1,6 +1,7 @@ package com.fnproject.fn.runtime; import com.fnproject.fn.api.MethodWrapper; +import org.assertj.core.api.AbstractIntegerAssert; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -8,7 +9,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; -import java.util.function.Function; import static org.assertj.core.api.Assertions.assertThat; @@ -35,8 +35,9 @@ public void testMethodParameterHasExpectedType() throws NoSuchMethodException { if (parameterIndex >= 0) { assertThat(method.getParamType(parameterIndex).getParameterClass()).isEqualTo(expectedType); } else { - assertThat(parameterIndex).isEqualTo(-1) - .withFailMessage("You can only use non negative parameter indices or -1 to represent return value in this test suite"); + AbstractIntegerAssert withFailMessage = assertThat(parameterIndex) + .isEqualTo(-1) + .withFailMessage("You can only use non negative parameter indices or -1 to represent return value in this test suite"); assertThat(method.getReturnType().getParameterClass()).isEqualTo(expectedType); } } @@ -68,20 +69,20 @@ public static Collection data() throws Exception { } static class ConcreteTypeExamples { - public void voidReturnType() { }; - public void singleParameter(String s) { }; - public void singlePrimitiveParameter(boolean i) { }; - public void singlePrimitiveParameter(byte i) { }; - public void singlePrimitiveParameter(char i) { }; - public void singlePrimitiveParameter(short i) { }; - public void singlePrimitiveParameter(int i) { }; - public void singlePrimitiveParameter(long i) { }; - public void singlePrimitiveParameter(float i) { }; - public void singlePrimitiveParameter(double i) { }; - public void multipleParameters(String s, double i) { }; - public String noArgs() { return ""; }; - public int noArgsWithPrimitiveReturnType() { return 1; }; - public void singleGenericParameter(List s) { }; + public void voidReturnType() { } + public void singleParameter(String s) { } + public void singlePrimitiveParameter(boolean i) { } + public void singlePrimitiveParameter(byte i) { } + public void singlePrimitiveParameter(char i) { } + public void singlePrimitiveParameter(short i) { } + public void singlePrimitiveParameter(int i) { } + public void singlePrimitiveParameter(long i) { } + public void singlePrimitiveParameter(float i) { } + public void singlePrimitiveParameter(double i) { } + public void multipleParameters(String s, double i) { } + public String noArgs() { return ""; } + public int noArgsWithPrimitiveReturnType() { return 1; } + public void singleGenericParameter(List s) { } } static class ParentClassWithGenericType { diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/QueryParametersParserTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/QueryParametersParserTest.java index 2714b449..72d39463 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/QueryParametersParserTest.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/QueryParametersParserTest.java @@ -1,9 +1,13 @@ package com.fnproject.fn.runtime; import com.fnproject.fn.api.QueryParameters; +import com.fnproject.fn.runtime.httpgateway.QueryParametersParser; import org.junit.Test; -import java.util.*; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; @@ -17,7 +21,7 @@ public void noUrlParametersProducesEmptyMap() { } @Test - public void gettingNonExistantParameterProducesOptionalEmpty() { + public void gettingNonExistentParameterProducesOptionalEmpty() { QueryParameters params = QueryParametersParser.getParams("www.example.com"); assertThat(params.getValues("var")).isEmpty(); diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/flow/FlowsTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/flow/FlowsTest.java deleted file mode 100644 index 03e07a1c..00000000 --- a/runtime/src/test/java/com/fnproject/fn/runtime/flow/FlowsTest.java +++ /dev/null @@ -1,280 +0,0 @@ -package com.fnproject.fn.runtime.flow; - -import com.fnproject.fn.runtime.FnTestHarness; -import com.fnproject.fn.runtime.testfns.FnFlowsFunction; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.concurrent.atomic.AtomicBoolean; - -import static org.mockito.Mockito.*; - -public class FlowsTest { - - @Rule - public FnTestHarness fnTestHarness = new FnTestHarness(); - - private final String FUNCTION_ID = "app/testfn"; - private final FlowId FLOW_ID = new FlowId("test-flow-id"); - - // static to avoid issues with serialized AtomicRefs - static AtomicBoolean tag = new AtomicBoolean(false); - - @Mock - CompleterClient mockCompleterClient; - - TestBlobStore testBlobStore; - - @Before - public void setup() { - tag.set(false); - MockitoAnnotations.initMocks(this); - FlowRuntimeGlobals.resetCompleterClientFactory(); - - FlowRuntimeGlobals.setCompleterClientFactory(new CompleterClientFactory() { - @Override - public CompleterClient getCompleterClient() { - return mockCompleterClient; - } - - @Override - public BlobStoreClient getBlobStoreClient() { - return testBlobStore; - } - }); - } - - private FnTestHarness.EventBuilder eventToTestFN() { - return fnTestHarness.givenDefaultEvent().withAppName("app").withRoute("/testfn"); - } - - private FnTestHarness.EventBuilder httpEventToTestFN() { - return fnTestHarness.givenHttpEvent() - .withAppName("app") - .withRoute("/testfn"); - } - - @Test - public void completerNotCalledIfFlowRuntimeUnused() throws Exception { - - eventToTestFN().enqueue(); - fnTestHarness.thenRun(FnFlowsFunction.class, "notUsingFlows"); - - verify(mockCompleterClient, never()).createFlow(any()); - } - - @Test - public void completerCalledWhenFlowRuntimeIsAccessed() { - - when(mockCompleterClient.createFlow(FUNCTION_ID)).thenReturn(FLOW_ID); - - eventToTestFN().enqueue(); - fnTestHarness.thenRun(FnFlowsFunction.class, "usingFlows"); - - verify(mockCompleterClient, times(1)).createFlow(FUNCTION_ID); - } - - @Test - public void onlyOneThreadIsCreatedWhenRuntimeIsAccessedMultipleTimes() { - - when(mockCompleterClient.createFlow(FUNCTION_ID)).thenReturn(FLOW_ID); - - eventToTestFN().enqueue(); - fnTestHarness.thenRun(FnFlowsFunction.class, "accessRuntimeMultipleTimes"); - - verify(mockCompleterClient, times(1)).createFlow(FUNCTION_ID); - } - -// @Test -// public void invokeWithinAsyncFunction() throws InterruptedException, IOException, ClassNotFoundException { -// -// AtomicReference continuationResult = new AtomicReference<>(); -// CompletionId completionId = new CompletionId("continuation-completion-id"); -// -// when(mockCompleterClient.createFlow(FUNCTION_ID)).thenReturn(FLOW_ID); -// -// when(mockCompleterClient.supply(eq(FLOW_ID), -// isA(Flows.SerCallable.class),isA(CodeLocation.class))) -// .thenAnswer(invokeContinuation(completionId, continuationResult, "supplyAndGetResult")); -// when(mockCompleterClient.waitForCompletion(eq(FLOW_ID), eq(completionId), eq(getClass().getClassLoader()))) -// .thenAnswer(invocationOnMock -> continuationResult.get()); -// -// httpEventToTestFN().enqueue(); -// fnTestHarness.thenRun(FnFlowsFunction.class, "supplyAndGetResult"); -// -// FnTestHarness.ParsedHttpResponse response = getSingleItem(fnTestHarness.getParsedHttpResponses()); -// assertThat(response.getBodyAsString()).isEqualTo(continuationResult.toString()); -// ArgumentCaptor locCaptor = ArgumentCaptor.forClass(CodeLocation.class); -// verify(mockCompleterClient, times(1)) -// .supply(eq(FLOW_ID), isA(Flows.SerCallable.class), locCaptor.capture()); -// -// CodeLocation gotLocation = locCaptor.getValue(); -// assertThat(gotLocation.getLocation()) -// .matches(Pattern.compile("com\\.fnproject\\.fn\\.runtime\\.testfns\\.FnFlowsFunction\\.supplyAndGetResult\\(.*\\.java\\:\\d+\\)")); -// verify(mockCompleterClient, times(1)) -// .waitForCompletion(eq(FLOW_ID), eq(completionId), eq(getClass().getClassLoader())); -// } -// -// /** -// * Mock the behaviour of a call to the Completer service through supply -// *

-// * When called by Mockito in response to a matching method call, -// * starts a function using the test harness, puts the result into a shared -// * AtomicReference, and returns the supplied Completion Id. -// * -// * @param completionId CompletionId to return from the invocation -// * @param result The result from invoking the continuation -// * @param methodName -// * @return a Mockito Answer instance providing the mock behaviour -// */ -// private Answer invokeContinuation(CompletionId completionId, AtomicReference result, String methodName) { -// return fn -> { -// if (fn.getArguments().length == 3) { -// -// Flows.SerCallable closure = fn.getArgument(1); -// -// -// FnTestHarness fnTestHarness = new FnTestHarness(); -// ` -// fnTestHarness.thenRun(FnFlowsFunction.class, methodName); -// -// FnTestHarness.ParsedHttpResponse response = getInnerResponse(fnTestHarness); -// try { -// assertThat(normalisedHeaders(response)) -// .containsEntry(DATUM_TYPE_HEADER.toLowerCase(), DATUM_TYPE_BLOB) -// .containsEntry(CONTENT_TYPE_HEADER.toLowerCase(), CONTENT_TYPE_JAVA_OBJECT); -// result.set(SerUtils.deserializeObject(response.getBodyAsBytes())); -// } catch (Exception e) { -// result.set(e); -// } -// -// return completionId; -// } else { -// throw new RuntimeException("Too few arguments given to supply"); -// } -// }; -// } -// -// -// -// -// @Test -// public void capturedCallableIsInvoked() throws Exception { -// -// Callable r = (Flows.SerCallable) () -> "Foo Bar"; -// -// TestSerUtils.HttpMultipartSerialization ser = new TestSerUtils.HttpMultipartSerialization() -// .addJavaEntity(r); -// -// httpEventToTestFN() -// .withHeader(FLOW_ID_HEADER, FLOW_ID.getId()) -// .withHeaders(ser.getHeaders()) -// .withBody(ser.getContentStream()) -// .enqueue(); -// -// fnTestHarness.thenRun(FnFlowsFunction.class, "supply"); -// -// assertThat(getResultObjectFromSingleResponse(fnTestHarness)).isEqualTo("Foo Bar"); -// } -// -// @Test -// public void capturedRunnableIsInvoked() throws Exception { -// Runnable r = (Flows.SerRunnable) () -> { -// tag.set(true); -// }; -// -// TestSerUtils.HttpMultipartSerialization ser = new TestSerUtils.HttpMultipartSerialization() -// .addJavaEntity(r); -// -// httpEventToTestFN() -// .withHeader(FLOW_ID_HEADER, FLOW_ID.getId()) -// .withHeaders(ser.getHeaders()) -// .withBody(ser.getContentStream()) -// .enqueue(); -// -// fnTestHarness.thenRun(FnFlowsFunction.class, "supply"); -// assertThat(tag.get()).isTrue(); -// } -// -// -// @Test -// public void capturedFunctionWithArgsIsInvoked() throws Exception { -// Function func = (Flows.SerFunction) (in) ->"Foo" + in; -// -// TestSerUtils.HttpMultipartSerialization ser = new TestSerUtils.HttpMultipartSerialization() -// .addJavaEntity(func) -// .addJavaEntity("BAR"); -// -// httpEventToTestFN() -// .withHeader(FLOW_ID_HEADER, FLOW_ID.getId()) -// .withHeaders(ser.getHeaders()) -// .withBody(ser.getContentStream()) -// .enqueue(); -// -// fnTestHarness.thenRun(FnFlowsFunction.class, "supply"); -// -// assertThat(getResultObjectFromSingleResponse(fnTestHarness)).isEqualTo("FooBAR"); -// } -// -// @Test -// public void catastrophicFailureStillResultsInGraphCommitted() throws Exception { -// when(mockCompleterClient.createFlow(FUNCTION_ID)).thenReturn(FLOW_ID); -// -// httpEventToTestFN().enqueue(); -// fnTestHarness.thenRun(FnFlowsFunction.class, "createFlowAndThenFail"); -// -// verify(mockCompleterClient, times(1)).commit(FLOW_ID); -// } -// -// private Object getResultObjectFromSingleResponse(FnTestHarness fnTestHarness) throws IOException, ClassNotFoundException { -// FnTestHarness.ParsedHttpResponse innerResponse = getInnerResponse(fnTestHarness); -// assertThat(normalisedHeaders(innerResponse)) -// .containsEntry(DATUM_TYPE_HEADER.toLowerCase(), DATUM_TYPE_BLOB) -// .containsEntry(CONTENT_TYPE_HEADER.toLowerCase(), CONTENT_TYPE_JAVA_OBJECT); -// return SerUtils.deserializeObject(innerResponse.getBodyAsBytes()); -// } -// -// private FnTestHarness.ParsedHttpResponse getInnerResponse(FnTestHarness fnTestHarness) { -// FnTestHarness.ParsedHttpResponse response = getSingleItem(fnTestHarness.getParsedHttpResponses()); -// return getSingleItem(FnTestHarness.getParsedHttpResponses(response.getBodyAsBytes())); -// } -// -// private T getSingleItem(List items) { -// assertThat(items.size()).isEqualTo(1); -// return items.get(0); -// } -// -// private Map normalisedHeaders(FnTestHarness.ParsedHttpResponse response) { -// return response.getHeaders().entrySet().stream() -// .collect(Collectors.toMap((kv) -> kv.getKey().toLowerCase(), Map.Entry::getValue)); -// } -// -// @Test -// public void capturedRunnableCanGetCurrentFlowRuntime() throws Exception { -// Callable r = (Flows.SerCallable) () -> { -// return Flows.currentFlow().getClass().getName(); -// }; -// -// TestSerUtils.HttpMultipartSerialization ser = new TestSerUtils.HttpMultipartSerialization() -// .addJavaEntity(r); -// -// httpEventToTestFN() -// .withHeader(FLOW_ID_HEADER, FLOW_ID.getId()) -// .withHeaders(ser.getHeaders()) -// .withBody(ser.getContentStream()) -// .enqueue(); -// -// fnTestHarness.thenRun(FnFlowsFunction.class, "supply"); -// -// assertThat(getResultObjectFromSingleResponse(fnTestHarness)).isEqualTo(RemoteFlow.class.getName()); -// } - - //NotSerializedResult - //Throws Exception in closure - //throws unserialized exception in closure - //null result from closure - //null value to closure. -} diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/httpgateway/FunctionHTTPGatewayContextTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/httpgateway/FunctionHTTPGatewayContextTest.java new file mode 100644 index 00000000..93ac7015 --- /dev/null +++ b/runtime/src/test/java/com/fnproject/fn/runtime/httpgateway/FunctionHTTPGatewayContextTest.java @@ -0,0 +1,98 @@ +package com.fnproject.fn.runtime.httpgateway; + +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InvocationContext; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Created on 20/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class FunctionHTTPGatewayContextTest { + + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + + @Mock + InvocationContext ctx; + + @Test + public void shouldCreateGatewayContextFromInputs() { + Headers h = Headers.emptyHeaders() + .setHeader("H1", "h1val") + .setHeader("Fn-Http-Method", "PATCH") + .setHeader("Fn-Http-Request-Url", "http://blah.com?a=b&c=d&c=e") + .setHeader("Fn-Http-H-", "ignored") + .setHeader("Fn-Http-H-A", "b") + .setHeader("Fn-Http-H-mv", "c", "d"); + + Mockito.when(ctx.getRequestHeaders()).thenReturn(h); + + FunctionHTTPGatewayContext hctx = new FunctionHTTPGatewayContext(ctx); + + assertThat(hctx.getHeaders()) + .isEqualTo(Headers.emptyHeaders() + .addHeader("A", "b") + .addHeader("mv", "c", "d")); + + + assertThat(hctx.getRequestURL()) + .isEqualTo("http://blah.com?a=b&c=d&c=e"); + + assertThat(hctx.getQueryParameters().get("a")).contains("b"); + assertThat(hctx.getQueryParameters().getValues("c")).contains("d", "e"); + + assertThat(hctx.getMethod()).isEqualTo("PATCH"); + + + } + + + @Test + public void shouldCreateGatewayContextFromEmptyHeaders() { + + Mockito.when(ctx.getRequestHeaders()).thenReturn(Headers.emptyHeaders()); + + FunctionHTTPGatewayContext hctx = new FunctionHTTPGatewayContext(ctx); + + assertThat(hctx.getMethod()).isEqualTo(""); + assertThat(hctx.getRequestURL()).isEqualTo(""); + assertThat(hctx.getHeaders()).isEqualTo(Headers.emptyHeaders()); + assertThat(hctx.getQueryParameters().getAll()).isEmpty(); + + } + + + @Test + public void shouldPassThroughResponseAttributes() { + + Mockito.when(ctx.getRequestHeaders()).thenReturn(Headers.emptyHeaders()); + + FunctionHTTPGatewayContext hctx = new FunctionHTTPGatewayContext(ctx); + hctx.setResponseHeader("My-Header", "foo", "bar"); + Mockito.verify(ctx).setResponseHeader("Fn-Http-H-My-Header", "foo", "bar"); + + hctx.setResponseHeader("Content-Type", "my/ct", "ignored"); + Mockito.verify(ctx).setResponseContentType("my/ct"); + Mockito.verify(ctx).setResponseHeader("Fn-Http-H-Content-Type", "my/ct"); + + hctx.addResponseHeader("new-H", "v1"); + Mockito.verify(ctx).addResponseHeader("Fn-Http-H-new-H", "v1"); + + hctx.setStatusCode(101); + + + Mockito.verify(ctx).setResponseHeader("Fn-Http-Status", "101"); + + } + +} diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/ntv/UnixSocketNativeTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/ntv/UnixSocketNativeTest.java new file mode 100644 index 00000000..436da667 --- /dev/null +++ b/runtime/src/test/java/com/fnproject/fn/runtime/ntv/UnixSocketNativeTest.java @@ -0,0 +1,466 @@ +package com.fnproject.fn.runtime.ntv; + +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * Created on 12/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class UnixSocketNativeTest { + + @BeforeClass + public static void init() { + System.setProperty("com.fnproject.java.native.libdir", new File("src/main/c/").getAbsolutePath()); + } + + File createSocketFile() throws IOException { + File f = File.createTempFile("socket", "sock"); + f.delete(); + f.deleteOnExit(); + return f; + } + + @Test + public void shouldHandleBind() throws Exception { + + + try { // invalid socket + UnixSocketNative.bind(-1, createSocketFile().getAbsolutePath()); + fail("should have thrown an invalid argument"); + } catch (UnixSocketException ignored) { + } + + int socket = UnixSocketNative.socket(); + try { // invalid file location + UnixSocketNative.bind(socket, "/tmp/foodir/socket"); + fail("should have thrown an invalid argument"); + } catch (UnixSocketException ignored) { + } finally { + UnixSocketNative.close(socket); + } + + + socket = UnixSocketNative.socket(); + File socketFile = createSocketFile(); + try { // valid bind + UnixSocketNative.bind(socket, socketFile.getAbsolutePath()); + } finally { + UnixSocketNative.close(socket); + } + } + + public CompletableFuture runServerLoop(Callable loop) { + CompletableFuture result = new CompletableFuture<>(); + Thread t = new Thread(() -> { + try { + result.complete(loop.call()); + } catch (Exception e) { + result.completeExceptionally(e); + } + }); + t.start(); + return result; + } + + @Test + public void shouldHandleConnectAccept() throws Exception { + + // invalid socket + { + try { + UnixSocketNative.connect(-1, "/tmp/nonexistant_path.sock"); + fail("should have failed"); + } catch (UnixSocketException ignored) { + } + } + + + // unknown path + { + int socket = UnixSocketNative.socket(); + try { + UnixSocketNative.connect(socket, "/tmp/nonexistant_path.sock"); + fail("should have failed"); + } catch (UnixSocketException ignored) { + } finally { + UnixSocketNative.close(socket); + } + } + // accept rejects sresult = runServerLoop(() -> { + int ss = UnixSocketNative.socket(); + try { + UnixSocketNative.bind(ss, serverSocket.getAbsolutePath()); + UnixSocketNative.listen(ss, 1); + ready.countDown(); + int cs = UnixSocketNative.accept(ss, 0); + return cs > 0; + } finally { + UnixSocketNative.close(ss); + } + + }); + ready.await(); + int cs; + cs = UnixSocketNative.socket(); + UnixSocketNative.connect(cs, serverSocket.getAbsolutePath()); + + assertThat(sresult.get()).isTrue(); + } + + } + + @Test + public void shouldHonorWrites() throws Exception { + + CountDownLatch ready = new CountDownLatch(1); + File serverSocket = createSocketFile(); + + CompletableFuture result = runServerLoop(() -> { + int ss = UnixSocketNative.socket(); + try { + UnixSocketNative.bind(ss, serverSocket.getAbsolutePath()); + UnixSocketNative.listen(ss, 1); + ready.countDown(); + int cs = UnixSocketNative.accept(ss, 0); + byte[] buf = new byte[100]; + int read = UnixSocketNative.recv(cs, buf, 0, buf.length); + byte[] newBuf = new byte[read]; + System.arraycopy(buf, 0, newBuf, 0, read); + + return newBuf; + } finally { + UnixSocketNative.close(ss); + } + }); + + + {// zero byte write is a noop + ready.await(); + int cs = UnixSocketNative.socket(); + UnixSocketNative.connect(cs, serverSocket.getAbsolutePath()); + + // must NPE on buff + try { + UnixSocketNative.send(cs, null, 0, 10); + fail("should have NPEd"); + } catch (NullPointerException ignored) { + + } + + + // invalid offset + try { + UnixSocketNative.send(cs, "hello".getBytes(), 100, 10); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + + } + + // invalid offset + try { + UnixSocketNative.send(cs, "hello".getBytes(), -1, 10); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + + } + + // invalid offset + try { + UnixSocketNative.send(cs, "hello".getBytes(), 0, 10); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + + } + + try { + // Must nop on write + UnixSocketNative.send(cs, new byte[0], 0, 0); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + + } + + // validate a real write to be sure + UnixSocketNative.send(cs, "hello".getBytes(), 0, 5); + byte[] got = result.get(); + + assertThat(got).isEqualTo("hello".getBytes()); + } + } + + + @Test + public void shouldHonorReads() throws Exception { + CountDownLatch ready = new CountDownLatch(1); + File serverSocket = createSocketFile(); + CompletableFuture result = runServerLoop(() -> { + int ss = UnixSocketNative.socket(); + try { + UnixSocketNative.bind(ss, serverSocket.getAbsolutePath()); + UnixSocketNative.listen(ss, 1); + ready.countDown(); + int cs = UnixSocketNative.accept(ss, 0); + int read = UnixSocketNative.send(cs, "hello".getBytes(), 0, 5); + UnixSocketNative.close(cs); + return true; + } finally { + UnixSocketNative.close(ss); + } + }); + + + {// zero byte write is a noop + ready.await(); + int cs = UnixSocketNative.socket(); + UnixSocketNative.connect(cs, serverSocket.getAbsolutePath()); + + // must NPE on buff + try { + UnixSocketNative.recv(cs, null, 0, 10); + fail("should have NPEd"); + } catch (NullPointerException ignored) { + + } + + // invalid offset + try { + UnixSocketNative.recv(cs, new byte[5], -1, 1); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + + } + // invalid length + try { + UnixSocketNative.recv(cs, new byte[5], 0, -1); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + + } + + // invalid length + try { + UnixSocketNative.recv(cs, new byte[5], 0, 0); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + + } + + // invalid offset beyond buffer + try { + UnixSocketNative.recv(cs, new byte[5], 100, 10); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + + } + + // invalid offset + try { + UnixSocketNative.recv(cs, "hello".getBytes(), -1, 10); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + + } + + // validate a real write to be sure + byte[] buf = new byte[5]; + + int count = UnixSocketNative.recv(cs, buf, 0, 5); + assertThat(count).isEqualTo(5); + + assertThat(buf).isEqualTo("hello".getBytes()); + } + } + + @Test + public void shouldSetSocketOpts() throws Exception { + + int sock = UnixSocketNative.socket(); + try { + try { + UnixSocketNative.setSendBufSize(-1, 1); + fail("should have failed"); + } catch (UnixSocketException ignored) { + } + + try { + UnixSocketNative.setSendBufSize(sock, -1); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + } + + UnixSocketNative.setSendBufSize(sock, 65535); + + + try { + UnixSocketNative.setRecvBufSize(-1, 1); + fail("should have failed"); + } catch (UnixSocketException ignored) { + } + + try { + UnixSocketNative.setRecvBufSize(sock, -1); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + } + + UnixSocketNative.setRecvBufSize(sock, 65535); + + + try { + UnixSocketNative.setSendTimeout(-1, 1); + fail("should have failed"); + } catch (UnixSocketException ignored) { + } + + try { + UnixSocketNative.setSendTimeout(sock, -1); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + } + + UnixSocketNative.setSendTimeout(sock, 2000); + assertThat(UnixSocketNative.getSendTimeout(sock)).isEqualTo(2000); + + + try { + UnixSocketNative.setRecvTimeout(-1, 1); + fail("should have failed"); + } catch (UnixSocketException ignored) { + } + + try { + UnixSocketNative.setRecvTimeout(sock, -1); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + } + + UnixSocketNative.setRecvTimeout(sock, 3000); + assertThat(UnixSocketNative.getRecvTimeout(sock)).isEqualTo(3000); + + + } finally { + UnixSocketNative.close(sock); + } + + } + + + @Test + public void shouldHandleReadTimeouts() throws Exception { + + CountDownLatch ready = new CountDownLatch(1); + File serverSocket = createSocketFile(); + + CompletableFuture result = runServerLoop(() -> { + int ss = UnixSocketNative.socket(); + try { + UnixSocketNative.bind(ss, serverSocket.getAbsolutePath()); + UnixSocketNative.listen(ss, 1); + ready.countDown(); + int cs = UnixSocketNative.accept(ss, 0); + Thread.sleep(100); + int read = UnixSocketNative.send(cs, "hello".getBytes(), 0, 5); + UnixSocketNative.close(cs); + return true; + } finally { + UnixSocketNative.close(ss); + } + }); + + int clientFd = UnixSocketNative.socket(); + UnixSocketNative.setRecvTimeout(clientFd,50); + + ready.await(); + UnixSocketNative.connect(clientFd,serverSocket.getAbsolutePath()); + byte[] buf = new byte[100]; + try { + UnixSocketNative.recv(clientFd, buf, 0, buf.length); + fail("should have timed out"); + }catch (SocketTimeoutException ignored){ + } + + } + + + @Test + public void shouldHandleConnectTimeouts() throws Exception { + + CountDownLatch ready = new CountDownLatch(1); + File serverSocket = createSocketFile(); + + CompletableFuture result = runServerLoop(() -> { + int ss = UnixSocketNative.socket(); + try { + UnixSocketNative.bind(ss, serverSocket.getAbsolutePath()); + UnixSocketNative.listen(ss, 1); + ready.countDown(); + Thread.sleep(1000); + + return true; + } finally { + UnixSocketNative.close(ss); + } + }); + + int clientFd = UnixSocketNative.socket(); + UnixSocketNative.setSendTimeout(clientFd,50); + ready.await(); + try { + UnixSocketNative.connect(clientFd,serverSocket.getAbsolutePath()); + + }catch (SocketTimeoutException ignored){ + } + + } + +} diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/ntv/UnixSocketTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/ntv/UnixSocketTest.java new file mode 100644 index 00000000..050b9ba9 --- /dev/null +++ b/runtime/src/test/java/com/fnproject/fn/runtime/ntv/UnixSocketTest.java @@ -0,0 +1,93 @@ +package com.fnproject.fn.runtime.ntv; + +import org.assertj.core.api.Assertions; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.DataInputStream; +import java.io.File; +import java.io.IOException; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; + +/** + * Created on 12/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class UnixSocketTest { + + @BeforeClass + public static void setup() { + System.setProperty("com.fnproject.java.native.libdir", new File("src/main/c/").getAbsolutePath()); + } + + File createSocketFile() throws IOException { + File f = File.createTempFile("socket", "sock"); + f.delete(); + f.deleteOnExit(); + return f; + } + + public byte[] roundTripViaEcho(byte[] data) throws Exception { + + File f = createSocketFile(); + try (UnixServerSocket ss = UnixServerSocket.listen(f.getPath(), 1)) { + + CompletableFuture result = new CompletableFuture<>(); + CountDownLatch cdl = new CountDownLatch(1); + Thread client = new Thread(() -> { + try { + cdl.await(); + try (UnixSocket us = UnixSocket.connect(f.getPath())) { + us.setReceiveBufferSize(65535); + us.setSendBufferSize(65535); + byte[] buf = new byte[data.length]; + us.getOutputStream().write(data); + DataInputStream dis = new DataInputStream(us.getInputStream()); + dis.readFully(buf); + result.complete(buf); + } + } catch (Exception e) { + result.completeExceptionally(e); + } + }); + client.start(); + + cdl.countDown(); + UnixSocket in = ss.accept(1000); + byte[] sbuf = new byte[data.length]; + in.setReceiveBufferSize(65535); + in.setSendBufferSize(65535); + new DataInputStream(in.getInputStream()).readFully(sbuf); + in.getOutputStream().write(sbuf); + in.close(); + return result.get(); + } + } + + @Test + public void shouldHandleEmptyData() throws Exception { + byte[] data = "hello".getBytes(); + Assertions.assertThat(roundTripViaEcho(data)).isEqualTo(data); + + } + + + @Test + public void shouldHandleBigData() throws Exception { + Random r = new Random(); + byte[] dataPart = new byte[2048]; + + r.nextBytes(dataPart); + + byte[] data = new byte[1024 * 1024 * 10]; + for (int i = 0 ; i < data.length ;i += dataPart.length){ + System.arraycopy(dataPart,0,data,i,dataPart.length); + } + + Assertions.assertThat(roundTripViaEcho(data)).isEqualTo(data); + + } +} diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnInputOutput.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnInputOutput.java index b4df6eb0..a5813281 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnInputOutput.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnInputOutput.java @@ -1,9 +1,9 @@ package com.fnproject.fn.runtime.testfns; +import com.fnproject.fn.api.FnConfiguration; import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.runtime.testfns.coercions.StringReversalCoercion; import com.fnproject.fn.runtime.testfns.coercions.StringUpperCaseCoercion; -import com.fnproject.fn.api.FnConfiguration; public class CustomDataBindingFnInputOutput { diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithAnnotation.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithAnnotation.java index 990642a3..a6116aee 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithAnnotation.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithAnnotation.java @@ -1,8 +1,6 @@ package com.fnproject.fn.runtime.testfns; import com.fnproject.fn.api.InputBinding; - - import com.fnproject.fn.runtime.testfns.coercions.StringReversalCoercion; public class CustomDataBindingFnWithAnnotation { diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithAnnotationAndConfig.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithAnnotationAndConfig.java index a85d9c91..59691b8d 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithAnnotationAndConfig.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithAnnotationAndConfig.java @@ -1,9 +1,8 @@ package com.fnproject.fn.runtime.testfns; -import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.api.FnConfiguration; import com.fnproject.fn.api.InputBinding; - +import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.runtime.testfns.coercions.StringReversalCoercion; import com.fnproject.fn.runtime.testfns.coercions.StringUpperCaseCoercion; diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithConfig.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithConfig.java index 7c5b1e75..5117d0ed 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithConfig.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithConfig.java @@ -1,7 +1,6 @@ package com.fnproject.fn.runtime.testfns; import com.fnproject.fn.api.FnConfiguration; - import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.runtime.testfns.coercions.StringReversalCoercion; diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithDudCoercion.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithDudCoercion.java index 1ccf8281..9e13224c 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithDudCoercion.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithDudCoercion.java @@ -2,7 +2,6 @@ import com.fnproject.fn.api.FnConfiguration; import com.fnproject.fn.api.RuntimeContext; - import com.fnproject.fn.runtime.testfns.coercions.DudCoercion; import com.fnproject.fn.runtime.testfns.coercions.StringReversalCoercion; diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithMultipleCoercions.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithMultipleCoercions.java index 9b881a88..cb7d35db 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithMultipleCoercions.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithMultipleCoercions.java @@ -1,8 +1,7 @@ package com.fnproject.fn.runtime.testfns; -import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.api.FnConfiguration; - +import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.runtime.testfns.coercions.StringReversalCoercion; import com.fnproject.fn.runtime.testfns.coercions.StringUpperCaseCoercion; diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithNoUserCoersions.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithNoUserCoercions.java similarity index 83% rename from runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithNoUserCoersions.java rename to runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithNoUserCoercions.java index 9dad4a3d..6902cdf0 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithNoUserCoersions.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithNoUserCoercions.java @@ -1,9 +1,9 @@ package com.fnproject.fn.runtime.testfns; -import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.api.FnConfiguration; +import com.fnproject.fn.api.RuntimeContext; -public class CustomDataBindingFnWithNoUserCoersions { +public class CustomDataBindingFnWithNoUserCoercions { @FnConfiguration public static void inputConfig(RuntimeContext ctx){ diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithAnnotation.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithAnnotation.java index 089b461d..738ff335 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithAnnotation.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithAnnotation.java @@ -1,7 +1,7 @@ package com.fnproject.fn.runtime.testfns; -import com.fnproject.fn.runtime.testfns.coercions.StringReversalCoercion; import com.fnproject.fn.api.OutputBinding; +import com.fnproject.fn.runtime.testfns.coercions.StringReversalCoercion; public class CustomOutputDataBindingFnWithAnnotation { diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithConfig.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithConfig.java index ce8dbc75..63af1473 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithConfig.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithConfig.java @@ -1,8 +1,7 @@ package com.fnproject.fn.runtime.testfns; -import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.api.FnConfiguration; - +import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.runtime.testfns.coercions.StringReversalCoercion; diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithDudCoercion.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithDudCoercion.java index b4963004..072e23c9 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithDudCoercion.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithDudCoercion.java @@ -1,8 +1,7 @@ package com.fnproject.fn.runtime.testfns; -import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.api.FnConfiguration; - +import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.runtime.testfns.coercions.DudCoercion; import com.fnproject.fn.runtime.testfns.coercions.StringReversalCoercion; diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithMultipleCoercions.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithMultipleCoercions.java index 9f194366..8b2f36d5 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithMultipleCoercions.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithMultipleCoercions.java @@ -1,7 +1,7 @@ package com.fnproject.fn.runtime.testfns; -import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.api.FnConfiguration; +import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.runtime.testfns.coercions.StringReversalCoercion; import com.fnproject.fn.runtime.testfns.coercions.StringUpperCaseCoercion; diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithNoUserCoercions.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithNoUserCoercions.java index c62934d1..39b1ec80 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithNoUserCoercions.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithNoUserCoercions.java @@ -1,7 +1,7 @@ package com.fnproject.fn.runtime.testfns; -import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.api.FnConfiguration; +import com.fnproject.fn.api.RuntimeContext; public class CustomOutputDataBindingFnWithNoUserCoercions { diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/TestFn.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/TestFn.java index 345262a5..306bce2e 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/TestFn.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/TestFn.java @@ -1,6 +1,7 @@ package com.fnproject.fn.runtime.testfns; import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.InvocationContext; import org.apache.commons.io.IOUtils; import java.io.IOException; @@ -105,6 +106,21 @@ public static String readSecondInput(InputEvent evt) { } + + public static void readRawEvent(InputEvent evt) { + input = evt; + + } + + + public static void setsOutputHeaders(InvocationContext ic) { + ic.addResponseHeader("Header-1","v1"); + ic.setResponseContentType("foo-ct"); + ic.addResponseHeader("a","b1"); + ic.addResponseHeader("a","b2"); + + + } public static List fnGenericAnimal() { Animal dog = new Animal("Spot", 6); Animal cat = new Animal("Jason", 16); @@ -115,7 +131,7 @@ public static List fnGenericAnimal() { * Reset the internal (static) state * Should be called between runs; */ - public static final void reset() { + public static void reset() { input = NOTHING; output = NOTHING; count = 0; diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/TestFnWithConfigurationMethods.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/TestFnWithConfigurationMethods.java index 3c69ac64..a6e28862 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/TestFnWithConfigurationMethods.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/TestFnWithConfigurationMethods.java @@ -1,7 +1,7 @@ package com.fnproject.fn.runtime.testfns; -import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.api.FnConfiguration; +import com.fnproject.fn.api.RuntimeContext; import java.util.Map; diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/coercions/StringReversalCoercion.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/coercions/StringReversalCoercion.java index a08d6bb7..9994b3d2 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/coercions/StringReversalCoercion.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/coercions/StringReversalCoercion.java @@ -26,7 +26,7 @@ public Optional wrapFunctionResult(InvocationContext ctx, MethodWra if (ctx.getRuntimeContext().getMethod().getTargetMethod().getReturnType().equals(String.class)) { try { String reversedOutput = new StringBuffer((String) value).reverse().toString(); - return Optional.of(OutputEvent.fromBytes(reversedOutput.getBytes(), OutputEvent.SUCCESS, "text/plain")); + return Optional.of(OutputEvent.fromBytes(reversedOutput.getBytes(), OutputEvent.Status.Success, "text/plain")); } catch (ClassCastException e) { return Optional.empty(); } diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/coercions/StringUpperCaseCoercion.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/coercions/StringUpperCaseCoercion.java index b2a5959e..3ac217fd 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/coercions/StringUpperCaseCoercion.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/coercions/StringUpperCaseCoercion.java @@ -14,7 +14,7 @@ public Optional tryCoerceParam(InvocationContext currentContext, int arg return Optional.of( input.consumeBody(is -> { try { - return new StringBuffer(IOUtils.toString(is, StandardCharsets.UTF_8)).toString().toUpperCase(); + return IOUtils.toString(is, StandardCharsets.UTF_8).toUpperCase(); } catch (IOException e) { return null; // Tests will fail if we end up here } @@ -26,8 +26,8 @@ public Optional tryCoerceParam(InvocationContext currentContext, int arg public Optional wrapFunctionResult(InvocationContext ctx, MethodWrapper method, Object value) { if (ctx.getRuntimeContext().getMethod().getTargetMethod().getReturnType().equals(String.class)) { try { - String capitilisedOutput = new StringBuffer((String) value).toString().toUpperCase(); - return Optional.of(OutputEvent.fromBytes(capitilisedOutput.getBytes(), OutputEvent.SUCCESS, "text/plain")); + String capitalizedOutput = ((String) value).toUpperCase(); + return Optional.of(OutputEvent.fromBytes(capitalizedOutput.getBytes(), OutputEvent.Status.Success, "text/plain")); } catch (ClassCastException e) { return Optional.empty(); } diff --git a/testing-core/pom.xml b/testing-core/pom.xml new file mode 100644 index 00000000..62f22357 --- /dev/null +++ b/testing-core/pom.xml @@ -0,0 +1,22 @@ + + + + fdk + com.fnproject.fn + 1.0.0-SNAPSHOT + + 4.0.0 + + testing-core + + + + com.fnproject.fn + runtime + + + + + \ No newline at end of file diff --git a/testing/src/main/java/com/fnproject/fn/testing/FnEventBuilder.java b/testing-core/src/main/java/com/fnproject/fn/testing/FnEventBuilder.java similarity index 54% rename from testing/src/main/java/com/fnproject/fn/testing/FnEventBuilder.java rename to testing-core/src/main/java/com/fnproject/fn/testing/FnEventBuilder.java index 1dce2476..21804284 100644 --- a/testing/src/main/java/com/fnproject/fn/testing/FnEventBuilder.java +++ b/testing-core/src/main/java/com/fnproject/fn/testing/FnEventBuilder.java @@ -1,11 +1,12 @@ package com.fnproject.fn.testing; +import java.io.IOException; import java.io.InputStream; /** * Builder for function input events */ -public interface FnEventBuilder { +public interface FnEventBuilder { /** * Add a header to the input with a variable number of values; duplicate headers will be overwritten @@ -14,7 +15,7 @@ public interface FnEventBuilder { * @param value header value(s) * @return an event builder */ - FnEventBuilder withHeader(String key, String value); + FnEventBuilder withHeader(String key, String value); /** * Set the body of the request by providing an InputStream @@ -22,10 +23,9 @@ public interface FnEventBuilder { * Note - setting the body to an input stream means that only one event can be enqueued using this builder. * * @param body the bytes of the body - * @param contentLength how long the body is supposed to be * @return an event builder */ - FnEventBuilder withBody(InputStream body, int contentLength); + FnEventBuilder withBody(InputStream body) throws IOException; /** * Set the body of the request as a byte array @@ -33,7 +33,7 @@ public interface FnEventBuilder { * @param body the bytes of the body * @return an event builder */ - FnEventBuilder withBody(byte[] body); + FnEventBuilder withBody(byte[] body); /** * Set the body of the request as a String @@ -41,48 +41,7 @@ public interface FnEventBuilder { * @param body the String of the body * @return an event builder */ - FnEventBuilder withBody(String body); - - /** - * Set the app name associated with the call - * - * @param appName the app name - * @return an event builder - */ - FnEventBuilder withAppName(String appName); - - /** - * Set the fn route associated with the call - * - * @param route the route - * @return an event builder - */ - FnEventBuilder withRoute(String route); - - /** - * Set the HTTP method of the incoming request - * - * @param method an HTTP method - * @return an event builder - */ - FnEventBuilder withMethod(String method); - - /** - * Set the request URL of the incoming event - * - * @param requestUrl the request URL - * @return an event builder - */ - FnEventBuilder withRequestUrl(String requestUrl); - - /** - * Add a query parameter to the request URL - * - * @param key - non URL encoded key - * @param value - non URL encoded value - * @return an event builder - */ - FnEventBuilder withQueryParameter(String key, String value); + FnEventBuilder withBody(String body); /** * Consume the builder and enqueue this event to be passed into the function when it is run @@ -90,7 +49,8 @@ public interface FnEventBuilder { * @return The original testing rule. The builder is consumed. * @throws IllegalStateException if this event has already been enqueued and the event input can only be read once. */ - FnTestingRule enqueue(); +// FnTestingRule enqueue(); + T enqueue(); /** * Consume the builder and enqueue multiple copies of this event. @@ -102,5 +62,6 @@ public interface FnEventBuilder { * @return The original testing rule. The builder is consumed. * @throws IllegalStateException if the body cannot be read multiple times. */ - FnTestingRule enqueue(int n); +// FnTestingRule enqueue(int n); + T enqueue(int n); } diff --git a/testing-core/src/main/java/com/fnproject/fn/testing/FnHttpEventBuilder.java b/testing-core/src/main/java/com/fnproject/fn/testing/FnHttpEventBuilder.java new file mode 100644 index 00000000..0813ce5b --- /dev/null +++ b/testing-core/src/main/java/com/fnproject/fn/testing/FnHttpEventBuilder.java @@ -0,0 +1,63 @@ +package com.fnproject.fn.testing; + +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.runtime.ReadOnceInputEvent; +import org.apache.commons.io.IOUtils; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Map; +import java.util.Objects; + +public class FnHttpEventBuilder { + private byte[] bodyBytes = new byte[0]; + private Headers headers = Headers.emptyHeaders(); + private Instant deadline = Instant.now().plus(1, ChronoUnit.HOURS); + + public FnHttpEventBuilder withHeader(String key, String value) { + Objects.requireNonNull(key, "key"); + Objects.requireNonNull(value, "value"); + headers = headers.addHeader(key, value); + return this; + } + + public FnHttpEventBuilder withBody(InputStream body) throws IOException { + Objects.requireNonNull(body, "body"); + this.bodyBytes = IOUtils.toByteArray(body); + + return this; + } + + public FnHttpEventBuilder withBody(byte[] body) { + Objects.requireNonNull(body, "body"); + this.bodyBytes = body; + + return this; + } + + public FnHttpEventBuilder withBody(String body) { + byte stringAsBytes[] = Objects.requireNonNull(body, "body").getBytes(); + return withBody(stringAsBytes); + } + + + public FnHttpEventBuilder withHeaders(Map headers) { + Headers h = this.headers; + for (Map.Entry he : headers.entrySet()) { + h = h.setHeader(he.getKey(), he.getValue()); + } + this.headers = h; + return this; + } + + + public InputEvent buildEvent() { + return new ReadOnceInputEvent(new ByteArrayInputStream(bodyBytes), headers, "callId", deadline); + } + + +} diff --git a/testing-core/src/main/java/com/fnproject/fn/testing/FnResult.java b/testing-core/src/main/java/com/fnproject/fn/testing/FnResult.java new file mode 100644 index 00000000..6c660013 --- /dev/null +++ b/testing-core/src/main/java/com/fnproject/fn/testing/FnResult.java @@ -0,0 +1,32 @@ +package com.fnproject.fn.testing; + +import com.fnproject.fn.api.OutputEvent; + +/** + * A simple abstraction over {@link OutputEvent} that buffers the response body + */ +public interface FnResult extends OutputEvent { + /** + * Returns the body of the function result as a byte array + * + * @return the function response body + */ + byte[] getBodyAsBytes(); + + /** + * Returns the body of the function response as a string + * + * @return a function response body + */ + String getBodyAsString(); + + + /** + * Determine if the status code corresponds to a successful invocation + * + * @return true if the status code indicates success + */ + default boolean isSuccess() { + return getStatus() == Status.Success; + } +} diff --git a/testing/src/main/java/com/fnproject/fn/testing/FnTestingClassLoader.java b/testing-core/src/main/java/com/fnproject/fn/testing/FnTestingClassLoader.java similarity index 65% rename from testing/src/main/java/com/fnproject/fn/testing/FnTestingClassLoader.java rename to testing-core/src/main/java/com/fnproject/fn/testing/FnTestingClassLoader.java index ba3dc085..ff45abec 100644 --- a/testing/src/main/java/com/fnproject/fn/testing/FnTestingClassLoader.java +++ b/testing-core/src/main/java/com/fnproject/fn/testing/FnTestingClassLoader.java @@ -1,14 +1,11 @@ package com.fnproject.fn.testing; import com.fnproject.fn.runtime.EntryPoint; -import com.fnproject.fn.runtime.flow.CompleterClient; -import com.fnproject.fn.runtime.flow.CompleterClientFactory; -import com.fnproject.fn.runtime.flow.FlowRuntimeGlobals; +import com.fnproject.fn.runtime.EventCodec; import org.apache.commons.io.IOUtils; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; import java.io.PrintStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -19,11 +16,11 @@ /** * Testing classloader that loads all classes afresh when needed, otherwise delegates shared classes to the parent classloader */ -class FnTestingClassLoader extends ClassLoader { +public class FnTestingClassLoader extends ClassLoader { private final List sharedPrefixes; private final Map> loaded = new HashMap<>(); - FnTestingClassLoader(ClassLoader parent, List sharedPrefixes) { + public FnTestingClassLoader(ClassLoader parent, List sharedPrefixes) { super(parent); this.sharedPrefixes = sharedPrefixes; } @@ -68,16 +65,8 @@ public synchronized Class loadClass(String className) throws ClassNotFoundExc return cls; } - void setCompleterClient(CompleterClientFactory completerClientFactory) { - try { - Class completerGlobals = loadClass(FlowRuntimeGlobals.class.getName()); - callMethodInFnClassloader(completerGlobals, "setCompleterClientFactory", CompleterClientFactory.class).invoke(completerGlobals, completerClientFactory); - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | ClassNotFoundException | IllegalArgumentException e) { - throw new RuntimeException("Something broke in the reflective classloader", e); - } - } - public int run(Map mutableEnv, InputStream is, PrintStream functionOut, PrintStream functionErr, String... s) { + public int run(Map mutableEnv, EventCodec codec, PrintStream functionErr, String... s) { ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader(); try { @@ -87,16 +76,16 @@ public int run(Map mutableEnv, InputStream is, PrintStream funct Class entryPoint_class = loadClass(EntryPoint.class.getName()); Object entryPoint = entryPoint_class.newInstance(); - return (int) callMethodInFnClassloader(entryPoint, "run", Map.class, InputStream.class, OutputStream.class, PrintStream.class, String[].class) - .invoke(entryPoint, mutableEnv, is, functionOut, functionErr, s); + return (int) getMethodInClassLoader(entryPoint, "run", Map.class, EventCodec.class, PrintStream.class, String[].class) + .invoke(entryPoint, mutableEnv, codec, functionErr, s); } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException | IllegalArgumentException e) { throw new RuntimeException("Something broke in the reflective classloader", e); - }finally { + } finally { Thread.currentThread().setContextClassLoader(currentClassLoader); } } - private Method callMethodInFnClassloader(Object target, String method, Class... types) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + private Method getMethodInClassLoader(Object target, String method, Class... types) throws NoSuchMethodException { Class targetClass; if (target instanceof Class) { targetClass = (Class) target; diff --git a/testing/src/main/java/com/fnproject/fn/testing/FunctionError.java b/testing-core/src/main/java/com/fnproject/fn/testing/FunctionError.java similarity index 100% rename from testing/src/main/java/com/fnproject/fn/testing/FunctionError.java rename to testing-core/src/main/java/com/fnproject/fn/testing/FunctionError.java diff --git a/testing/src/main/java/com/fnproject/fn/testing/HeaderWriter.java b/testing-core/src/main/java/com/fnproject/fn/testing/HeaderWriter.java similarity index 100% rename from testing/src/main/java/com/fnproject/fn/testing/HeaderWriter.java rename to testing-core/src/main/java/com/fnproject/fn/testing/HeaderWriter.java diff --git a/testing/src/main/java/com/fnproject/fn/testing/PlatformError.java b/testing-core/src/main/java/com/fnproject/fn/testing/PlatformError.java similarity index 100% rename from testing/src/main/java/com/fnproject/fn/testing/PlatformError.java rename to testing-core/src/main/java/com/fnproject/fn/testing/PlatformError.java diff --git a/testing-junit4/pom.xml b/testing-junit4/pom.xml new file mode 100644 index 00000000..1e8f9c37 --- /dev/null +++ b/testing-junit4/pom.xml @@ -0,0 +1,36 @@ + + + + fdk + com.fnproject.fn + 1.0.0-SNAPSHOT + + 4.0.0 + + testing-junit4 + + + + com.fnproject.fn + testing-core + + + com.fnproject.fn + runtime + + + + junit + junit + compile + + + org.assertj + assertj-core + test + + + + \ No newline at end of file diff --git a/testing-junit4/src/main/java/com/fnproject/fn/testing/FnEventBuilderJUnit4.java b/testing-junit4/src/main/java/com/fnproject/fn/testing/FnEventBuilderJUnit4.java new file mode 100644 index 00000000..80a2312d --- /dev/null +++ b/testing-junit4/src/main/java/com/fnproject/fn/testing/FnEventBuilderJUnit4.java @@ -0,0 +1,5 @@ +package com.fnproject.fn.testing; + +public interface FnEventBuilderJUnit4 extends FnEventBuilder { + +} diff --git a/testing-junit4/src/main/java/com/fnproject/fn/testing/FnTestingRule.java b/testing-junit4/src/main/java/com/fnproject/fn/testing/FnTestingRule.java new file mode 100644 index 00000000..367cf5be --- /dev/null +++ b/testing-junit4/src/main/java/com/fnproject/fn/testing/FnTestingRule.java @@ -0,0 +1,444 @@ +package com.fnproject.fn.testing; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fnproject.fn.api.*; +import com.fnproject.fn.runtime.EventCodec; +import org.apache.commons.io.output.TeeOutputStream; +import org.junit.Rule; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.io.*; +import java.util.*; + + +/** + * Testing {@link Rule} for fn Java FDK functions. + *

+ * This interface facilitates: + *

    + *
  • The creation of an in-memory environment replicating the functionality of the {@code fn} service
  • + *
  • The creation of input events passed to a user function using {@link #givenEvent()}
  • + *
  • The verification of function behaviour by accessing output represented by {@link FnResult} instances.
  • + *
+ *

Example Usage:

+ *
{@code
+ * public class MyFunctionTest {
+ *     {@literal @}Rule
+ *     public final FnTestingRule testing = FnTestingRule.createDefault();
+ *
+ *     {@literal @}Test
+ *     public void myTest() {
+ *         // Create an event to invoke MyFunction and put it into the event queue
+ *         fn.givenEvent()
+ *            .addHeader("FOO", "BAR")
+ *            .withBody("Body")
+ *            .enqueue();
+ *
+ *         // Run MyFunction#handleRequest using the built event queue from above
+ *         fn.thenRun(MyFunction.class, "handleRequest");
+ *
+ *         // Get the function result and check it for correctness
+ *         FnResult result = fn.getOnlyResult();
+ *         assertThat(result.getStatus()).isEqualTo(200);
+ *         assertThat(result.getBodyAsString()).isEqualTo("expected return value of my function");
+ *     }
+ * }}
+ */ +public final class FnTestingRule implements TestRule { + private final Map config = new HashMap<>(); + private Map eventEnv = new HashMap<>(); + private boolean hasEvents = false; + private List pendingInput = Collections.synchronizedList(new ArrayList<>()); + private List output = Collections.synchronizedList(new ArrayList<>()); + private ByteArrayOutputStream stdErr = new ByteArrayOutputStream(); + + private static final ObjectMapper objectMapper = new ObjectMapper(); + private final List sharedPrefixes = new ArrayList<>(); + private int lastExitCode; + private final List features = new ArrayList<>(); + + { + // Internal shared classes required to bridge completer into tests + addSharedClassPrefix("java."); + addSharedClassPrefix("javax."); + addSharedClassPrefix("sun."); + addSharedClassPrefix("jdk."); + + + addSharedClass(Headers.class); + addSharedClass(InputEvent.class); + addSharedClass(OutputEvent.class); + addSharedClass(OutputEvent.Status.class); + addSharedClass(TestOutput.class); + addSharedClass(TestCodec.class); + addSharedClass(EventCodec.class); + addSharedClass(EventCodec.Handler.class); + + + + } + + /** + * TestOutput represents an output of a function it wraps OutputEvent and provides buffered access to the function output + */ + public static final class TestOutput implements FnResult { + private final OutputEvent from; + byte[] body; + + private TestOutput(OutputEvent from) throws IOException { + this.from = Objects.requireNonNull(from, "from"); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + from.writeToOutput(bos); + body = bos.toByteArray(); + } + + /** + * construct a test output from an output event - this consums the body of the output event + * + * @param from an output event to consume + * @return a new TestEvent that wraps the passed even t + * @throws IOException + */ + public static TestOutput fromOutputEvent(OutputEvent from) throws IOException { + return new TestOutput(from); + } + + @Override + public Status getStatus() { + return from.getStatus(); + } + + @Override + public Optional getContentType() { + return from.getContentType(); + } + + @Override + public Headers getHeaders() { + return from.getHeaders(); + } + + @Override + public void writeToOutput(OutputStream out) throws IOException { + out.write(body); + } + + + @Override + public byte[] getBodyAsBytes() { + return body; + } + + /** + * Returns the buffered body of the event as a string + * + * @return the body of the event as a string + */ + @Override + public String getBodyAsString() { + return new String(body); + } + + } + + private FnTestingRule() { + } + + + public void addFeature(FnTestingRuleFeature f) { + this.features.add(f); + } + + /** + * Create an instance of the testing {@link Rule}, with Flows support + * + * @return a new test rule + */ + public static FnTestingRule createDefault() { + return new FnTestingRule(); + } + + + /** + * Add a config variable to the function for the test + *

+ * Config names will be translated to upper case with hyphens and spaces translated to _. Clashing config keys will + * be overwritten. + * + * @param key the configuration key + * @param value the configuration value + * @return the current test rule + */ + public FnTestingRule setConfig(String key, String value) { + config.put(key.toUpperCase().replaceAll("[- ]", "_"), value); + return this; + } + + /** + * Add a class or package name to be forked during the test. + * The test will be run under the aegis of a classloader that duplicates the class hierarchy named. + * + * @param prefix A class name or package prefix, such as "com.example.foo." + */ + public FnTestingRule addSharedClassPrefix(String prefix) { + sharedPrefixes.add(prefix); + return this; + } + + /** + * Add a class to be forked during the test. + * The test will be run under the aegis of a classloader that duplicates the class hierarchy named. + * + * @param cls A class + */ + public FnTestingRule addSharedClass(Class cls) { + sharedPrefixes.add("=" + cls.getName()); + return this; + } + + @Override + public Statement apply(Statement base, Description description) { + return base; + } + + /** + * Create an HTTP event builder for the function + * + * @return a new event builder + */ + public FnEventBuilderJUnit4 givenEvent() { + return new DefaultFnEventBuilder(); + } + + /** + * Runs the function runtime with the specified class and method (and waits for Flow stages to finish + * if the test spawns any flows) + * + * @param cls class to thenRun + * @param method the method name + */ + public void thenRun(Class cls, String method) { + thenRun(cls.getName(), method); + } + + + public static class TestCodec implements EventCodec { + private final List input; + private final List output; + + public TestCodec(List input, List output) { + this.input = input; + this.output = output; + } + + @Override + public void runCodec(Handler h) { + for (InputEvent in : input) { + try { + output.add(new TestOutput(h.handle(in))); + } catch (IOException e) { + throw new RuntimeException("Unexpected exception in test", e); + } + } + } + } + + /** + * Runs the function runtime with the specified class and method (and waits for Flow stages to finish + * if the test spawns any Flow) + * + * @param cls class to thenRun + * @param method the method name + */ + public void thenRun(String cls, String method) { + final ClassLoader functionClassLoader; + Class c = null; + try { + // Trick to work around Maven class loader separation + // if passed class is a valid class then set the classloader to the same as the class's loader + c = Class.forName(cls); + } catch (Exception ignored) { + // TODO don't fall through here + } + if (c != null) { + functionClassLoader = c.getClassLoader(); + } else { + functionClassLoader = getClass().getClassLoader(); + } + + PrintStream oldSystemOut = System.out; + PrintStream oldSystemErr = System.err; + + for (FnTestingRuleFeature f : features) { + f.prepareTest(functionClassLoader, oldSystemErr, cls, method); + } + + Map mutableEnv = new HashMap<>(); + + try { + PrintStream functionErr = new PrintStream(new TeeOutputStream(stdErr, oldSystemErr)); + System.setOut(functionErr); + System.setErr(functionErr); + + mutableEnv.putAll(config); + mutableEnv.putAll(eventEnv); + mutableEnv.put("FN_FORMAT", "http-stream"); + mutableEnv.put("FN_FN_ID","myFnID"); + mutableEnv.put("FN_APP_ID","myAppID"); + + FnTestingClassLoader forked = new FnTestingClassLoader(functionClassLoader, sharedPrefixes); + if (forked.isShared(cls)) { + oldSystemErr.println("WARNING: The function class under test is shared with the test ClassLoader."); + oldSystemErr.println(" This may result in unexpected behaviour around function initialization and configuration."); + } + for (FnTestingRuleFeature f : features) { + f.prepareFunctionClassLoader(forked); + } + + TestCodec codec = new TestCodec(pendingInput, output); + + lastExitCode = forked.run( + mutableEnv, + codec, + functionErr, + cls + "::" + method); + + stdErr.flush(); + + for (FnTestingRuleFeature f : features) { + f.afterTestComplete(); + } + } catch (Exception e) { + throw new RuntimeException("internal error raised by entry point or flushing the test streams", e); + } finally { + System.out.flush(); + System.err.flush(); + System.setOut(oldSystemOut); + System.setErr(oldSystemErr); + + } + } + + /** + * Get the exit code from the most recent invocation + * 0 = success + * 1 = failed + * 2 = not run due to initialization error + */ + public int getLastExitCode() { + return lastExitCode; + } + + /** + * Get the StdErr stream returned by the function as a byte array + * + * @return the StdErr stream as bytes from the runtime + */ + public byte[] getStdErr() { + return stdErr.toByteArray(); + } + + /** + * Gets the StdErr stream returned by the function as a String + * + * @return the StdErr stream as a string from the function + */ + public String getStdErrAsString() { + return new String(stdErr.toByteArray()); + } + + /** + * Parses any pending HTTP responses on the functions output stream and returns the corresponding FnResult instances + * + * @return a list of Parsed HTTP responses (as {@link FnResult}s) from the function runtime output + */ + public List getResults() { + return output; + } + + /** + * Convenience method to get the one and only parsed http response expected on the output of the function + * + * @return a single parsed HTTP response from the function runtime output + * @throws IllegalStateException if zero or more than one responses were produced + */ + public FnResult getOnlyResult() { + List results = getResults(); + if (results.size() == 1) { + return results.get(0); + } + throw new IllegalStateException("One and only one response expected, but " + results.size() + " responses were generated."); + } + + + public List getSharedPrefixes() { + return Collections.unmodifiableList(sharedPrefixes); + } + + public Map getConfig() { + return Collections.unmodifiableMap(config); + } + + public Map getEventEnv() { + return Collections.unmodifiableMap(eventEnv); + } + + + /** + * Builds a mocked input event into the function runtime + */ + private class DefaultFnEventBuilder implements FnEventBuilderJUnit4 { + + FnHttpEventBuilder builder = new FnHttpEventBuilder(); + + + @Override + public FnEventBuilder withHeader(String key, String value) { + builder.withHeader(key, value); + return this; + } + + @Override + public FnEventBuilder withBody(InputStream body) throws IOException { + builder.withBody(body); + return this; + } + + @Override + public FnEventBuilder withBody(byte[] body) { + builder.withBody(body); + return this; + } + + @Override + public FnEventBuilder withBody(String body) { + builder.withBody(body); + return this; + } + + + @Override + public FnTestingRule enqueue() { + + pendingInput.add(builder.buildEvent()); + return FnTestingRule.this; + } + + + @Override + public FnTestingRule enqueue(int n) { + if (n <= 0) { + throw new IllegalArgumentException("Invalid count"); + } + for (int i = 0; i < n; i++) { + enqueue(); + } + return FnTestingRule.this; + } + + + } + +} diff --git a/testing-junit4/src/main/java/com/fnproject/fn/testing/FnTestingRuleFeature.java b/testing-junit4/src/main/java/com/fnproject/fn/testing/FnTestingRuleFeature.java new file mode 100644 index 00000000..42b25454 --- /dev/null +++ b/testing-junit4/src/main/java/com/fnproject/fn/testing/FnTestingRuleFeature.java @@ -0,0 +1,26 @@ +package com.fnproject.fn.testing; + +import java.io.PrintStream; + +/** + * Created on 07/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public interface FnTestingRuleFeature { + + /** + * Prepares a test + * @param functionClassLoader + * @param stderr + * @param cls + * @param method + */ + void prepareTest(ClassLoader functionClassLoader, PrintStream stderr, String cls, String method); + + + void prepareFunctionClassLoader(FnTestingClassLoader cl); + + + void afterTestComplete(); +} diff --git a/testing-junit4/src/test/java/com/fnproject/fn/testing/FnTestingRuleTest.java b/testing-junit4/src/test/java/com/fnproject/fn/testing/FnTestingRuleTest.java new file mode 100644 index 00000000..03df8764 --- /dev/null +++ b/testing-junit4/src/test/java/com/fnproject/fn/testing/FnTestingRuleTest.java @@ -0,0 +1,340 @@ +package com.fnproject.fn.testing; + +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.OutputEvent; +import com.fnproject.fn.api.RuntimeContext; +import org.apache.commons.io.IOUtils; +import org.assertj.core.api.Assertions; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.*; + +public class FnTestingRuleTest { + + public static Map configuration; + public static InputEvent inEvent; + public static List capturedInputs = new ArrayList<>(); + public static List capturedBodies = new ArrayList<>(); + + @Rule + public FnTestingRule fn = FnTestingRule.createDefault(); + private final String exampleBaseUrl = "http://www.example.com"; + + @Before + public void reset() { + fn.addSharedClass(FnTestingRuleTest.class); + fn.addSharedClass(InputEvent.class); + + + FnTestingRuleTest.configuration = null; + FnTestingRuleTest.inEvent = null; + FnTestingRuleTest.capturedInputs = new ArrayList<>(); + FnTestingRuleTest.capturedBodies = new ArrayList<>(); + } + + + public static class TestFn { + private RuntimeContext ctx; + + public TestFn(RuntimeContext ctx) { + this.ctx = ctx; + } + + public void copyConfiguration() { + configuration = new HashMap<>(ctx.getConfiguration()); + } + + public void copyInputEvent(InputEvent inEvent) { + FnTestingRuleTest.inEvent = inEvent; + } + + public void err() { + throw new RuntimeException("ERR"); + } + + public void captureInput(InputEvent in) { + capturedInputs.add(in); + capturedBodies.add(in.consumeBody(TestFn::consumeToBytes)); + } + + private static byte[] consumeToBytes(InputStream is) { + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + IOUtils.copy(is, bos); + return bos.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + public OutputEvent echoInput(InputEvent in) { + byte[] result = in.consumeBody(TestFn::consumeToBytes); + return OutputEvent.fromBytes(result, OutputEvent.Status.Success, "application/octet-stream"); + } + + } + + + @Test + public void shouldSetEnvironmentInsideFnScope() { + fn.givenEvent().enqueue(); + fn.setConfig("CONFIG_FOO", "BAR"); + + fn.thenRun(FnTestingRuleTest.TestFn.class, "copyConfiguration"); + + Assertions.assertThat(configuration).containsEntry("CONFIG_FOO", "BAR"); + } + + + @Test + public void shouldCleanEnvironmentOfSpecialVarsInsideFnScope() { + fn.givenEvent().enqueue(); + fn.setConfig("CONFIG_FOO", "BAR"); + + fn.thenRun(FnTestingRuleTest.TestFn.class, "copyConfiguration"); + + Assertions.assertThat(configuration).doesNotContainKeys("APP_NAME", "ROUTE", "METHOD", "REQUEST_URL"); + } + + + @Test + public void shouldHandleErrors() { + fn.givenEvent().enqueue(); + + fn.thenRun(FnTestingRuleTest.TestFn.class, "err"); + + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.FunctionError); + Assertions.assertThat(fn.getStdErrAsString()).contains("An error occurred in function: ERR"); + } + + + @Test + public void configShouldNotOverrideIntrinsicHeaders() { + fn.givenEvent().enqueue(); + fn.setConfig("Fn-Call-Id", "BAR"); + + fn.thenRun(FnTestingRuleTest.TestFn.class, "copyInputEvent"); + + Assertions.assertThat(inEvent.getCallID()).isEqualTo("callId"); + } + + + @Test + public void configShouldBeCaptitalisedAndReplacedWithUnderscores() {// Basic test + // Test uppercasing and mangling of keys + fn.givenEvent().enqueue(); + + fn.setConfig("some-key-with-dashes", "some-value"); + + fn.thenRun(FnTestingRuleTest.TestFn.class, "copyConfiguration"); + + Assertions.assertThat(configuration).containsEntry("SOME_KEY_WITH_DASHES", "some-value"); + + } + + + @Test + public void shouldSendEventDataToSDKInputEvent() { + + fn.setConfig("SOME_CONFIG", "SOME_VALUE"); + fn.givenEvent() + .withHeader("FOO", "BAR, BAZ") + .withHeader("FEH", "") + .withBody("Body") // body as string + .enqueue(); + + fn.thenRun(TestFn.class, "captureInput"); + + FnResult result = fn.getOnlyResult(); + Assertions.assertThat(result.getBodyAsString()).isEmpty(); + Assertions.assertThat(result.getStatus()).isEqualTo(OutputEvent.Status.Success); + + InputEvent event = capturedInputs.get(0); + Assertions.assertThat(event.getHeaders().asMap()) + .contains(headerEntry("FOO", "BAR, BAZ")) + .contains(headerEntry("FEH", "")); + Assertions.assertThat(capturedBodies.get(0)).isEqualTo("Body".getBytes()); + } + + + @Test + public void shouldEnqueueMultipleDistinctEvents() { + fn.setConfig("SOME_CONFIG", "SOME_VALUE"); + fn.givenEvent() + .withHeader("FOO", "BAR") + .withBody("Body") // body as string + .enqueue(); + + + fn.givenEvent() + .withHeader("FOO2", "BAR2") + .withBody("Body2") // body as string + .enqueue(); + + fn.thenRun(TestFn.class, "captureInput"); + + FnResult result = fn.getResults().get(0); + Assertions.assertThat(result.getBodyAsString()).isEmpty(); + Assertions.assertThat(result.getStatus()).isEqualTo(OutputEvent.Status.Success); + + InputEvent event = capturedInputs.get(0); + Assertions.assertThat(event.getHeaders().asMap()).contains(headerEntry("FOO", "BAR")); + Assertions.assertThat(capturedBodies.get(0)).isEqualTo("Body".getBytes()); + + + FnResult result2 = fn.getResults().get(1); + Assertions.assertThat(result2.getBodyAsString()).isEmpty(); + Assertions.assertThat(result2.getStatus()).isEqualTo(OutputEvent.Status.Success); + + InputEvent event2 = capturedInputs.get(1); + Assertions.assertThat(event2.getHeaders().asMap()).contains(headerEntry("FOO2", "BAR2")); + Assertions.assertThat(capturedBodies.get(1)).isEqualTo("Body2".getBytes()); + } + + + @Test + public void shouldEnqueueMultipleIdenticalEvents() { + fn.givenEvent() + .withHeader("FOO", "BAR") + .withHeader("Content-Type", "application/octet-stream") + .withBody("Body") // body as string + .enqueue(10); + + fn.thenRun(TestFn.class, "echoInput"); + + List results = fn.getResults(); + Assertions.assertThat(results).hasSize(10); + + + results.forEach((r) -> { + Assertions.assertThat(r.getStatus()).isEqualTo(OutputEvent.Status.Success); + + }); + } + + + @Test + public void shouldEnqueuIndependentEventsWithInputStreams() throws IOException { + fn.givenEvent() + .withBody(new ByteArrayInputStream("Body".getBytes())) // body as string + .enqueue(); + + fn.givenEvent() + .withBody(new ByteArrayInputStream("Body1".getBytes())) // body as string + .enqueue(); + + fn.thenRun(TestFn.class, "echoInput"); + + List results = fn.getResults(); + Assertions.assertThat(results).hasSize(2); + + Assertions.assertThat(results.get(0).getBodyAsString()).isEqualTo("Body"); + Assertions.assertThat(results.get(1).getBodyAsString()).isEqualTo("Body1"); + } + + @Test + public void shouldHandleBodyAsInputStream() throws IOException { + fn.givenEvent().withBody(new ByteArrayInputStream("FOO BAR".getBytes())).enqueue(); + + fn.thenRun(TestFn.class, "captureInput"); + + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(capturedBodies.get(0)).isEqualTo("FOO BAR".getBytes()); + } + + // TODO move this to HTTP gateway +// @Test +// public void shouldLeaveQueryParamtersOffIfNotSpecified() { +// String baseUrl = "www.example.com"; +// fn.givenEvent() +// .withRequestUrl(baseUrl) +// .enqueue(); +// fn.thenRun(TestFn.class, "copyInputEvent"); +// +// Assertions.assertThat(inEvent.getRequestUrl()).isEqualTo(baseUrl); +// } +// +// @Test +// public void shouldPrependQuestionMarkForFirstQueryParam() { +// String baseUrl = "www.example.com"; +// fn.givenEvent() +// .withRequestUrl(baseUrl) +// .withQueryParameter("var", "val") +// .enqueue(); +// fn.thenRun(TestFn.class, "copyInputEvent"); +// Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(200); +// Assertions.assertThat(inEvent.getRequestUrl()).isEqualTo(baseUrl + "?var=val"); +// } +// +// @Test +// public void shouldHandleMultipleQueryParameters() { +// String baseUrl = "www.example.com"; +// fn.givenEvent() +// .withRequestUrl(baseUrl) +// .withQueryParameter("var1", "val1") +// .withQueryParameter("var2", "val2") +// .enqueue(); +// fn.thenRun(TestFn.class, "copyInputEvent"); +// +// Assertions.assertThat(inEvent.getRequestUrl()).isEqualTo(baseUrl + "?var1=val1&var2=val2"); +// } +// +// @Test +// public void shouldHandleMultipleQueryParametersWithSameKey() { +// String baseUrl = "www.example.com"; +// fn.givenEvent() +// .withRequestUrl(baseUrl) +// .withQueryParameter("var", "val1") +// .withQueryParameter("var", "val2") +// .enqueue(); +// fn.thenRun(TestFn.class, "copyInputEvent"); +// +// Assertions.assertThat(inEvent.getRequestUrl()).isEqualTo(baseUrl + "?var=val1&var=val2"); +// } +// +// @Test +// public void shouldUrlEncodeQueryParameterKey() { +// fn.givenEvent() +// .withRequestUrl(exampleBaseUrl) +// .withQueryParameter("&", "val") +// .enqueue(); +// fn.thenRun(TestFn.class, "copyInputEvent"); +// +// Assertions.assertThat(inEvent.getRequestUrl()).isEqualTo(exampleBaseUrl + "?%26=val"); +// } +// +// @Test +// public void shouldHandleQueryParametersWithSpaces() { +// fn.givenEvent() +// .withRequestUrl(exampleBaseUrl) +// .withQueryParameter("my var", "this val") +// .enqueue(); +// fn.thenRun(TestFn.class, "copyInputEvent"); +// +// Assertions.assertThat(inEvent.getRequestUrl()).isEqualTo(exampleBaseUrl + "?my+var=this+val"); +// } +// +// @Test +// public void shouldUrlEncodeQueryParameterValue() { +// String baseUrl = "www.example.com"; +// fn.givenEvent() +// .withRequestUrl(baseUrl) +// .withQueryParameter("var", "&") +// .enqueue(); +// fn.thenRun(TestFn.class, "copyInputEvent"); +// +// Assertions.assertThat(inEvent.getRequestUrl()).isEqualTo(baseUrl + "?var=%26"); +// } + + private static Map.Entry> headerEntry(String key, String... values) { + return new AbstractMap.SimpleEntry<>(Headers.canonicalKey(key), Arrays.asList(values)); + } +} diff --git a/testing/pom.xml b/testing/pom.xml index 567b31ff..41d1c188 100644 --- a/testing/pom.xml +++ b/testing/pom.xml @@ -11,27 +11,14 @@ testing - - UTF-8 - 1.2.2 - - com.fnproject.fn runtime - ${project.version} - - - junit - junit - ${junit.version} - org.assertj - assertj-core - ${assertj-core.version} - test + com.fnproject.fn + testing-core @@ -40,7 +27,6 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.0.0-M1 attach-javadocs @@ -50,11 +36,6 @@ - - org.pitest - pitest-maven - ${pitest.version} - diff --git a/testing/src/main/java/com/fnproject/fn/testing/FnHttpEventBuilder.java b/testing/src/main/java/com/fnproject/fn/testing/FnHttpEventBuilder.java deleted file mode 100644 index 9fe4a5b0..00000000 --- a/testing/src/main/java/com/fnproject/fn/testing/FnHttpEventBuilder.java +++ /dev/null @@ -1,178 +0,0 @@ -package com.fnproject.fn.testing; - -import org.apache.http.impl.io.ContentLengthInputStream; -import org.apache.http.impl.io.HttpTransportMetricsImpl; -import org.apache.http.impl.io.SessionInputBufferImpl; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.io.SequenceInputStream; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.*; -import java.util.stream.Collectors; - -class FnHttpEventBuilder { - private Map> queryParams = new TreeMap<>(); - private boolean streamRead = false; - private String method; - private String appName; - private String route; - private String requestUrl; - private byte[] bodyBytes = new byte[0]; - private InputStream bodyStream; - private int contentLength = 0; - private Map headers = new HashMap<>(); - - - public FnHttpEventBuilder withHeader(String key, String value) { - Objects.requireNonNull(key, "key"); - Objects.requireNonNull(value, "value"); - headers.put(key, value); - return this; - } - - public FnHttpEventBuilder withBody(InputStream body, int contentLength) { - Objects.requireNonNull(body, "body"); - if (contentLength < 0) { - throw new IllegalArgumentException("Invalid contentLength"); - } - // This is for safety. Because we concatenate events, an input stream shorter than content length will cause - // the implementation to continue reading through to the next http request. We need to avoid a sort of - // buffer overrun. - // FIXME: Make InputStream handling simpler. - SessionInputBufferImpl sib = new SessionInputBufferImpl(new HttpTransportMetricsImpl(), 65535); - sib.bind(body); - this.bodyStream = new ContentLengthInputStream(sib, contentLength); - this.contentLength = contentLength; - return this; - } - - public FnHttpEventBuilder withBody(byte[] body) { - Objects.requireNonNull(body, "body"); - this.bodyBytes = body; - this.contentLength = body.length; - this.bodyStream = null; - return this; - } - - public FnHttpEventBuilder withBody(String body) { - byte stringAsBytes[] = Objects.requireNonNull(body, "body").getBytes(); - return withBody(stringAsBytes); - } - - public FnHttpEventBuilder withRoute(String route) { - Objects.requireNonNull(route, "route"); - this.route = route; - return this; - } - - public FnHttpEventBuilder withMethod(String method) { - Objects.requireNonNull(method, "method"); - this.method = method.toUpperCase(); - return this; - } - - public FnHttpEventBuilder withAppName(String appName) { - Objects.requireNonNull(appName, "appName"); - this.appName = appName; - return this; - } - - public FnHttpEventBuilder withRequestUrl(String requestUrl) { - Objects.requireNonNull(requestUrl, "requestUrl"); - this.requestUrl = requestUrl; - return this; - } - - private String buildQueryParams() { - return queryParams.entrySet().stream() - .flatMap((e) -> e.getValue().stream() - .map((v) -> urlEncode(e.getKey()) + "=" + urlEncode(v))) - .collect(Collectors.joining("&")); - } - - - public FnHttpEventBuilder withQueryParameter(String key, String value) { - if (!this.queryParams.containsKey(key)) { - this.queryParams.put(key, new ArrayList<>()); - } - this.queryParams.get(key).add(value); - return this; - } - - public FnHttpEventBuilder withHeaders(Map headers) { - this.headers.putAll(headers); - return this; - } - - private String urlEncode(String str) { - try { - return URLEncoder.encode(str, "utf-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException("Your jvm doesn't support UTF-8, cannot continue."); - } - } - - private InputStream bodyAsStream() { - if (bodyStream != null) { - if (streamRead) { - throw new IllegalStateException("events with an overridden input stream can only be enqueued once"); - } - streamRead = true; - return bodyStream; - } else { - return new ByteArrayInputStream(bodyBytes); - } - } - - private void verify() { - Objects.requireNonNull(method, "method not set"); - Objects.requireNonNull(appName, "appName not set"); - Objects.requireNonNull(route, "route not set"); - Objects.requireNonNull(requestUrl, "requestUrl not set"); - - } - - public Map currentEventEnv() { - verify(); - Map env = new HashMap<>(); - headers.forEach((k, v) -> env.put("FN_HEADER_" + k.toUpperCase().replaceAll("-", "_"), v)); - env.put("FN_METHOD", method); - env.put("FN_APP_NAME", appName); - env.put("FN_PATH", route); - env.put("FN_REQUEST_URL", requestUrl); - return env; - } - - public InputStream currentEventInputStream() { - verify(); - - String queryParamsFullString = buildQueryParams(); - StringBuilder inputString = new StringBuilder(); - - inputString.append(method); - inputString.append(" / HTTP/1.1\r\n"); - inputString.append("Fn_App_name: ").append(appName).append("\r\n"); - inputString.append("Fn_Method: ").append(method).append("\r\n"); - inputString.append("Fn_Path: ").append(route).append("\r\n"); - inputString.append("Fn_Request_url: ").append(requestUrl); - if (!queryParamsFullString.isEmpty()) { - inputString.append("?").append(queryParamsFullString); - } - inputString.append("\r\n"); - - - inputString.append("Content-length: ").append(Integer.toString(contentLength)).append("\r\n"); - headers.forEach((k, v) -> inputString.append(k).append(": ").append(String.join(", ", v)).append("\r\n")); - - - inputString.append("\r\n"); - - return new SequenceInputStream( - new ByteArrayInputStream(inputString.toString().getBytes()), - bodyAsStream()); - } - - -} diff --git a/testing/src/main/java/com/fnproject/fn/testing/FnResult.java b/testing/src/main/java/com/fnproject/fn/testing/FnResult.java deleted file mode 100644 index cbfadb6e..00000000 --- a/testing/src/main/java/com/fnproject/fn/testing/FnResult.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.fnproject.fn.testing; - -import com.fnproject.fn.api.Headers; - -/** - * A simple abstraction for a parsed HTTP response returned by a function - */ -public interface FnResult { - /** - * Returns the body of the function result as a byte array - * - * @return the function response body - */ - byte[] getBodyAsBytes(); - - /** - * Returns the body of the function response as a string - * - * @return a function response body - */ - String getBodyAsString(); - - /** - * A map of the headers returned by the function - *

- * These are squashed so duplicated headers will be ignored (takes the first header). - * - * @return a map of headers - */ - Headers getHeaders(); - - /** - * Returns the HTTP status code of the function response - * - * @return the HTTP status code returned by the function - */ - int getStatus(); - - /** - * Determine if the status code corresponds to a successful invocation - * - * @return true if the status code indicates success - */ - default boolean isSuccess() { - return 100 <= getStatus() && getStatus() < 400; - } -} diff --git a/testing/src/main/java/com/fnproject/fn/testing/FnTestingRule.java b/testing/src/main/java/com/fnproject/fn/testing/FnTestingRule.java deleted file mode 100644 index 77daf715..00000000 --- a/testing/src/main/java/com/fnproject/fn/testing/FnTestingRule.java +++ /dev/null @@ -1,640 +0,0 @@ -package com.fnproject.fn.testing; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fnproject.fn.api.Headers; -import com.fnproject.fn.api.InputEvent; -import com.fnproject.fn.api.OutputEvent; -import com.fnproject.fn.api.QueryParameters; -import com.fnproject.fn.api.flow.Flow; -import com.fnproject.fn.api.flow.FlowCompletionException; -import com.fnproject.fn.api.flow.FunctionInvocationException; -import com.fnproject.fn.api.flow.HttpMethod; -import com.fnproject.fn.api.flow.PlatformException; -import com.fnproject.fn.runtime.flow.APIModel; -import com.fnproject.fn.runtime.flow.BlobResponse; -import com.fnproject.fn.runtime.flow.BlobStoreClient; -import com.fnproject.fn.runtime.flow.CodeLocation; -import com.fnproject.fn.runtime.flow.CompleterClient; -import com.fnproject.fn.runtime.flow.CompleterClientFactory; -import com.fnproject.fn.runtime.flow.CompletionId; -import com.fnproject.fn.runtime.flow.DefaultHttpResponse; -import com.fnproject.fn.runtime.flow.FlowContinuationInvoker; -import com.fnproject.fn.runtime.flow.FlowId; -import org.apache.commons.io.IOUtils; -import org.apache.commons.io.output.TeeOutputStream; -import org.apache.http.HttpResponse; -import org.apache.http.NoHttpResponseException; -import org.apache.http.impl.io.ContentLengthInputStream; -import org.apache.http.impl.io.DefaultHttpResponseParser; -import org.apache.http.impl.io.HttpTransportMetricsImpl; -import org.apache.http.impl.io.IdentityInputStream; -import org.apache.http.impl.io.SessionInputBufferImpl; -import org.junit.rules.TestRule; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.io.PrintStream; -import java.io.SequenceInputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; - -import static com.fnproject.fn.runtime.flow.RemoteFlowApiClient.CONTENT_TYPE_HEADER; - -/** - * Testing {@link org.junit.Rule} for fn Java FDK functions. - *

- * This interface facilitates: - *

    - *
  • The creation of an in-memory environment replicating the functionality of the {@code fn} service
  • - *
  • The creation of input events passed to a user function using {@link #givenEvent()}
  • - *
  • The verification of function behaviour by accessing output represented by {@link FnResult} instances.
  • - *
- *

Example Usage:

- *
{@code
- * public class MyFunctionTest {
- *     {@literal @}Rule
- *     public final FnTestingRule testing = FnTestingRule.createDefault();
- *
- *     {@literal @}Test
- *     public void myTest() {
- *         // Create an event to invoke MyFunction and put it into the event queue
- *         fn.givenEvent()
- *            .withAppName("alpha")
- *            .withRoute("/bravo")
- *            .withRequestUrl("http://charlie/alpha/bravo")
- *            .withMethod("POST")
- *            .withHeader("FOO", "BAR")
- *            .withBody("Body")
- *            .enqueue();
- *
- *         // Run MyFunction#handleRequest using the built event queue from above
- *         fn.thenRun(MyFunction.class, "handleRequest");
- *
- *         // Get the function result and check it for correctness
- *         FnResult result = fn.getOnlyResult();
- *         assertThat(result.getStatus()).isEqualTo(200);
- *         assertThat(result.getBodyAsString()).isEqualTo("expected return value of my function");
- *     }
- * }}
- */ -public final class FnTestingRule implements TestRule { - private final Map config = new HashMap<>(); - private Map eventEnv = new HashMap<>(); - private boolean hasEvents = false; - private InputStream pendingInput = new ByteArrayInputStream(new byte[0]); - private ByteArrayOutputStream stdOut = new ByteArrayOutputStream(); - private ByteArrayOutputStream stdErr = new ByteArrayOutputStream(); - private Map functionStubs = new HashMap<>(); - public static InMemCompleter completer = null; - - private static final ObjectMapper objectMapper = new ObjectMapper(); - - private final List sharedPrefixes = new ArrayList<>(); - private int lastExitCode; - - { - // Internal shared classes required to bridge completer into tests - addSharedClassPrefix("java."); - addSharedClassPrefix("javax."); - addSharedClassPrefix("sun."); - addSharedClassPrefix("jdk."); - - addSharedClass(CompleterClient.class); - addSharedClass(BlobStoreClient.class); - addSharedClass(BlobResponse.class); - - addSharedClass(CompleterClientFactory.class); - addSharedClass(CompletionId.class); - addSharedClass(FlowId.class); - addSharedClass(Flow.FlowState.class); - addSharedClass(CodeLocation.class); - addSharedClass(Headers.class); - addSharedClass(HttpMethod.class); - addSharedClass(com.fnproject.fn.api.flow.HttpRequest.class); - addSharedClass(com.fnproject.fn.api.flow.HttpResponse.class); - addSharedClass(QueryParameters.class); - addSharedClass(InputEvent.class); - addSharedClass(OutputEvent.class); - addSharedClass(FlowCompletionException.class); - addSharedClass(FunctionInvocationException.class); - addSharedClass(PlatformException.class); - - } - - private FnTestingRule() { - } - - /** - * Create an instance of the testing {@link org.junit.Rule}, with Flows support - * - * @return a new test rule - */ - public static FnTestingRule createDefault() { - return new FnTestingRule(); - } - - - /** - * Add a config variable to the function for the test - *

- * Config names will be translated to upper case with hyphens and spaces translated to _. Clashing config keys will - * be overwritten. - * - * @param key the configuration key - * @param value the configuration value - * @return the current test rule - */ - public FnTestingRule setConfig(String key, String value) { - config.put(key.toUpperCase().replaceAll("[- ]", "_"), value); - return this; - } - - /** - * Add a class or package name to be forked during the test. - * The test will be run under the aegis of a classloader that duplicates the class hierarchy named. - * - * @param prefix A class name or package prefix, such as "com.example.foo." - */ - public FnTestingRule addSharedClassPrefix(String prefix) { - sharedPrefixes.add(prefix); - return this; - } - - /** - * Add a class to be forked during the test. - * The test will be run under the aegis of a classloader that duplicates the class hierarchy named. - * - * @param cls A class - */ - public FnTestingRule addSharedClass(Class cls) { - sharedPrefixes.add("=" + cls.getName()); - return this; - } - - @Override - public Statement apply(Statement base, Description description) { - return base; - } - - /** - * Create an HTTP event builder for the function - * - * @return a new event builder - */ - public FnEventBuilder givenEvent() { - return new DefaultFnEventBuilder(); - } - - /** - * Runs the function runtime with the specified class and method (and waits for Flow stages to finish - * if the test spawns any flows) - * - * @param cls class to thenRun - * @param method the method name - */ - public void thenRun(Class cls, String method) { - thenRun(cls.getName(), method); - } - - - /** - * Runs the function runtime with the specified class and method (and waits for Flow stages to finish - * if the test spawns any Flow) - * - * @param cls class to thenRun - * @param method the method name - */ - public void thenRun(String cls, String method) { - final ClassLoader functionClassLoader; - Class c = null; - try { - // Trick to work around Maven class loader separation - // if passed class is a valid class then set the classloader to the same as the class's loader - c = Class.forName(cls); - } catch (Exception ignored) { - // TODO don't fall through here - } - if (c != null) { - functionClassLoader = c.getClassLoader(); - } else { - functionClassLoader = getClass().getClassLoader(); - } - - PrintStream oldSystemOut = System.out; - PrintStream oldSystemErr = System.err; - - InMemCompleter.CompleterInvokeClient client = new TestRuleCompleterInvokeClient(functionClassLoader, oldSystemErr, cls, method); - - InMemCompleter.FnInvokeClient fnInvokeClient = new TestRuleFnInvokeClient(); - - // FlowContinuationInvoker.setTestingMode(true); - // The following must be a static: otherwise the factory (the lambda) will not be serializable. - completer = new InMemCompleter(client, fnInvokeClient); - - //TestSupport.installCompleterClientFactory(completer, oldSystemErr); - - - Map mutableEnv = new HashMap<>(); - - try { - PrintStream functionOut = new PrintStream(stdOut); - PrintStream functionErr = new PrintStream(new TeeOutputStream(stdErr, oldSystemErr)); - System.setOut(functionErr); - System.setErr(functionErr); - - mutableEnv.putAll(config); - mutableEnv.putAll(eventEnv); - mutableEnv.put("FN_FORMAT", "http"); - - FnTestingClassLoader forked = new FnTestingClassLoader(functionClassLoader, sharedPrefixes); - if (forked.isShared(cls)) { - oldSystemErr.println("WARNING: The function class under test is shared with the test ClassLoader."); - oldSystemErr.println(" This may result in unexpected behaviour around function initialization and configuration."); - } - forked.setCompleterClient(completer); - lastExitCode = forked.run( - mutableEnv, - pendingInput, - functionOut, - functionErr, - cls + "::" + method); - - stdOut.flush(); - stdErr.flush(); - - completer.awaitTermination(); - } catch (Exception e) { - throw new RuntimeException("internal error raised by entry point or flushing the test streams", e); - } finally { - System.out.flush(); - System.err.flush(); - System.setOut(oldSystemOut); - System.setErr(oldSystemErr); - - } - } - - /** - * Get the exit code from the most recent invocation - * 0 = success - * 1 = failed - * 2 = not run due to initialization error - */ - public int getLastExitCode() { - return lastExitCode; - } - - /** - * Get the StdErr stream returned by the function as a byte array - * - * @return the StdErr stream as bytes from the runtime - */ - public byte[] getStdErr() { - return stdErr.toByteArray(); - } - - /** - * Gets the StdErr stream returned by the function as a String - * - * @return the StdErr stream as a string from the function - */ - public String getStdErrAsString() { - return new String(stdErr.toByteArray()); - } - - /** - * Parses any pending HTTP responses on the functions output stream and returns the corresponding FnResult instances - * - * @return a list of Parsed HTTP responses (as {@link FnResult}s) from the function runtime output - */ - public List getResults() { - return parseHttpStreamForResults(stdOut.toByteArray()); - } - - /** - * Convenience method to get the one and only parsed http response expected on the output of the function - * - * @return a single parsed HTTP response from the function runtime output - * @throws IllegalStateException if zero or more than one responses were produced - */ - public FnResult getOnlyResult() { - List results = getResults(); - if (results.size() == 1) { - return results.get(0); - } - throw new IllegalStateException("One and only one response expected, but " + results.size() + " responses were generated."); - } - - - private List parseHttpStreamForResults(byte[] httpStream) { - SessionInputBufferImpl sib = new SessionInputBufferImpl(new HttpTransportMetricsImpl(), 65535); - ByteArrayInputStream parseStream = new ByteArrayInputStream(httpStream); - sib.bind(parseStream); - - DefaultHttpResponseParser parser = new DefaultHttpResponseParser(sib); - List responses = new ArrayList<>(); - - while (true) { - try { - HttpResponse response = parser.parse(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - ContentLengthInputStream cis = new ContentLengthInputStream(sib, Long.parseLong(response.getFirstHeader("Content-length").getValue())); - - IOUtils.copy(cis, bos); - cis.close(); - byte[] body = bos.toByteArray(); - FnResult r = new FnResult() { - @Override - public byte[] getBodyAsBytes() { - return body; - } - - @Override - public String getBodyAsString() { - return new String(body); - } - - @Override - public Headers getHeaders() { - Map headers = new HashMap<>(); - Arrays.stream(response.getAllHeaders()).forEach((h) -> - headers.put(h.getName(), h.getValue())); - return Headers.fromMap(headers); - } - - @Override - public int getStatus() { - return response.getStatusLine().getStatusCode(); - } - }; - responses.add(r); - } catch (NoHttpResponseException e) { - break; - } catch (Exception e) { - throw new RuntimeException("Invalid HTTP response", e); - } - } - return responses; - } - - - public FnFunctionStubBuilder givenFn(String id) { - return new FnFunctionStubBuilder() { - @Override - public FnTestingRule withResult(byte[] result) { - return withAction((body) -> result); - } - - @Override - public FnTestingRule withFunctionError() { - return withAction((body) -> { - throw new FunctionError("simulated by testing platform"); - }); - } - - @Override - public FnTestingRule withPlatformError() { - return withAction((body) -> { - throw new PlatformError("simulated by testing platform"); - }); - } - - @Override - public FnTestingRule withAction(ExternalFunctionAction f) { - functionStubs.put(id, (HttpMethod method, Headers headers, byte[] body) -> { - try { - return new DefaultHttpResponse(200, Headers.emptyHeaders(), f.apply(body)); - } catch (FunctionError functionError) { - return new DefaultHttpResponse(500, Headers.emptyHeaders(), functionError.getMessage().getBytes()); - } catch (PlatformError platformError) { - throw new RuntimeException("Platform Error"); - } - }); - return FnTestingRule.this; - } - }; - } - - private interface FnFunctionStub { - com.fnproject.fn.api.flow.HttpResponse stubFunction(HttpMethod method, Headers headers, byte[] body); - } - - /** - * Builds a mocked input event into the function runtime - */ - private class DefaultFnEventBuilder implements FnEventBuilder { - - FnHttpEventBuilder builder = new FnHttpEventBuilder().withMethod("GET") - .withAppName("appName") - .withRoute("/route") - .withRequestUrl("http://example.com/r/appName/route"); - - - @Override - public FnEventBuilder withHeader(String key, String value) { - builder.withHeader(key, value); - return this; - } - - @Override - public FnEventBuilder withBody(InputStream body, int contentLength) { - builder.withBody(body, contentLength); - return this; - } - - @Override - public FnEventBuilder withBody(byte[] body) { - builder.withBody(body); - return this; - } - - @Override - public FnEventBuilder withBody(String body) { - builder.withBody(body); - return this; - } - - @Override - public FnEventBuilder withAppName(String appName) { - builder.withAppName(appName); - return this; - } - - @Override - public FnEventBuilder withRoute(String route) { - builder.withRoute(route); - return this; - } - - @Override - public FnEventBuilder withMethod(String method) { - builder.withMethod(method); - return this; - } - - @Override - public FnEventBuilder withRequestUrl(String requestUrl) { - builder.withRequestUrl(requestUrl); - return this; - - } - - @Override - public FnEventBuilder withQueryParameter(String key, String value) { - builder.withQueryParameter(key, value); - return this; - } - - @Override - public FnTestingRule enqueue() { - - // Only set env for first event. - if (!hasEvents) { - eventEnv.putAll(builder.currentEventEnv()); - } - hasEvents = true; - - pendingInput = new SequenceInputStream(pendingInput, builder.currentEventInputStream()); - return FnTestingRule.this; - } - - - @Override - public FnTestingRule enqueue(int n) { - if (n <= 0) { - throw new IllegalArgumentException("Invalid count"); - } - for (int i = 0; i < n; i++) { - enqueue(); - } - return FnTestingRule.this; - } - - - } - - private class TestRuleCompleterInvokeClient implements InMemCompleter.CompleterInvokeClient { - private final ClassLoader functionClassLoader; - private final PrintStream oldSystemErr; - private final String cls; - private final String method; - private final Set pool = new HashSet<>(); - - - public TestRuleCompleterInvokeClient(ClassLoader functionClassLoader, PrintStream oldSystemErr, String cls, String method) { - this.functionClassLoader = functionClassLoader; - this.oldSystemErr = oldSystemErr; - this.cls = cls; - this.method = method; - } - - - @Override - public APIModel.CompletionResult invokeStage(String fnId, FlowId flowId, CompletionId stageId, APIModel.Blob closure, List input) { - // Construct a new ClassLoader hierarchy with a copy of the statics embedded in the runtime. - // Initialise it appropriately. - FnTestingClassLoader fcl = new FnTestingClassLoader(functionClassLoader, sharedPrefixes); - fcl.setCompleterClient(completer); - - - APIModel.InvokeStageRequest request = new APIModel.InvokeStageRequest(); - request.stageId = stageId.getId(); - request.flowId = flowId.getId(); - request.closure = closure; - request.args = input; - - String inputBody = null; - try { - inputBody = objectMapper.writeValueAsString(request); - } catch (JsonProcessingException e) { - throw new IllegalStateException("Invalid request"); - } - - // oldSystemErr.println("Body\n" + new String(inputBody)); - - InputStream is = new FnHttpEventBuilder() - .withBody(inputBody) - .withAppName("appName") - .withRoute("/route").withRequestUrl("http://some/url") - .withMethod("POST") - .withHeader(CONTENT_TYPE_HEADER, "application/json") - .withHeader(FlowContinuationInvoker.FLOW_ID_HEADER, flowId.getId()).currentEventInputStream(); - - ByteArrayOutputStream output = new ByteArrayOutputStream(); - Map mutableEnv = new HashMap<>(); - PrintStream functionOut = new PrintStream(output); - PrintStream functionErr = new PrintStream(oldSystemErr); - - // Do we want to capture IO from continuations on the main log stream? - // System.setOut(functionErr); - // System.setErr(functionErr); - - mutableEnv.putAll(config); - mutableEnv.putAll(eventEnv); - mutableEnv.put("FN_FORMAT", "http"); - - - fcl.run( - mutableEnv, - is, - functionOut, - functionErr, - cls + "::" + method); - - - SessionInputBufferImpl sib = new SessionInputBufferImpl(new HttpTransportMetricsImpl(), 65535); - ByteArrayInputStream parseStream = new ByteArrayInputStream(output.toByteArray()); - sib.bind(parseStream); - DefaultHttpResponseParser parser = new DefaultHttpResponseParser(sib); - APIModel.CompletionResult r; - try { - // Read wrapping result, and throw it away - parser.parse(); - IdentityInputStream iis = new IdentityInputStream(sib); - byte[] responseBody = IOUtils.toByteArray(iis); - - APIModel.InvokeStageResponse response = objectMapper.readValue(responseBody, APIModel.InvokeStageResponse.class); - r = response.result; - - } catch (Exception e) { - oldSystemErr.println("Err\n" + e); - e.printStackTrace(oldSystemErr); - r = APIModel.CompletionResult.failure(APIModel.ErrorDatum.newError(APIModel.ErrorType.UnknownError, "Error reading fn Response:" + e.getMessage())); - } - - if (!r.successful) { - throw new ResultException(r.result); - } - return r; - - } - } - - private class TestRuleFnInvokeClient implements InMemCompleter.FnInvokeClient { - @Override - public CompletableFuture invokeFunction(String fnId, HttpMethod method, Headers headers, byte[] data) { - FnFunctionStub stub = functionStubs - .computeIfAbsent(fnId, (k) -> { - throw new IllegalStateException("Function was invoked that had no definition: " + k); - }); - - try { - return CompletableFuture.completedFuture(stub.stubFunction(method, headers, data)); - } catch (Exception e) { - CompletableFuture respFuture = new CompletableFuture<>(); - respFuture.completeExceptionally(e); - return respFuture; - } - } - } -} diff --git a/testing/src/test/java/com/fnproject/fn/testing/FnTestingRuleTest.java b/testing/src/test/java/com/fnproject/fn/testing/FnTestingRuleTest.java deleted file mode 100644 index d46b8689..00000000 --- a/testing/src/test/java/com/fnproject/fn/testing/FnTestingRuleTest.java +++ /dev/null @@ -1,401 +0,0 @@ -package com.fnproject.fn.testing; - -import com.fnproject.fn.api.InputEvent; -import com.fnproject.fn.api.OutputEvent; -import com.fnproject.fn.api.RuntimeContext; -import org.apache.commons.io.IOUtils; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.*; - -import static org.assertj.core.api.Assertions.assertThat; - -public class FnTestingRuleTest { - - public static Map configuration; - public static InputEvent inEvent; - public static List capturedInputs = new ArrayList<>(); - public static List capturedBodies = new ArrayList<>(); - - @Rule - public FnTestingRule fn = FnTestingRule.createDefault(); - private final String exampleBaseUrl = "http://www.example.com"; - - @Before - public void reset() { - fn.addSharedClass(FnTestingRuleTest.class); - fn.addSharedClass(InputEvent.class); - - - FnTestingRuleTest.configuration = null; - FnTestingRuleTest.inEvent = null; - FnTestingRuleTest.capturedInputs = new ArrayList<>(); - FnTestingRuleTest.capturedBodies = new ArrayList<>(); - } - - - public static class TestFn { - private RuntimeContext ctx; - - public TestFn(RuntimeContext ctx) { - this.ctx = ctx; - } - - public void copyConfiguration() { - configuration = new HashMap<>(ctx.getConfiguration()); - } - - public void copyInputEvent(InputEvent inEvent) { - FnTestingRuleTest.inEvent = inEvent; - } - - public void err() { - throw new RuntimeException("ERR"); - } - - public void captureInput(InputEvent in) { - capturedInputs.add(in); - capturedBodies.add(in.consumeBody(TestFn::consumeToBytes)); - } - - private static byte[] consumeToBytes(InputStream is) { - try { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - IOUtils.copy(is, bos); - return bos.toByteArray(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - - public OutputEvent echoInput(InputEvent in) { - byte[] result = in.consumeBody(TestFn::consumeToBytes); - return OutputEvent.fromBytes(result, OutputEvent.SUCCESS, "application/octet-stream"); - } - - } - - - @Test - public void shouldSetEnvironmentInsideFnScope() { - fn.givenEvent().enqueue(); - fn.setConfig("CONFIG_FOO", "BAR"); - - fn.thenRun(FnTestingRuleTest.TestFn.class, "copyConfiguration"); - - assertThat(configuration).containsEntry("CONFIG_FOO", "BAR"); - } - - - @Test - public void shouldCleanEnvironmentOfSpecialVarsInsideFnScope() { - fn.givenEvent().enqueue(); - fn.setConfig("CONFIG_FOO", "BAR"); - - fn.thenRun(FnTestingRuleTest.TestFn.class, "copyConfiguration"); - - assertThat(configuration).doesNotContainKeys("APP_NAME", "ROUTE", "METHOD", "REQUEST_URL"); - } - - - @Test - public void shouldHandleErrors() { - fn.givenEvent().enqueue(); - - fn.thenRun(FnTestingRuleTest.TestFn.class, "err"); - - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(500); - assertThat(fn.getStdErrAsString()).contains("An error occurred in function: ERR"); - } - - - @Test - public void configShouldNotOverrideIntrinsicHeaders() { - fn.givenEvent().enqueue(); - fn.setConfig("APP_NAME", "BAR"); - - fn.thenRun(FnTestingRuleTest.TestFn.class, "copyInputEvent"); - - assertThat(inEvent.getAppName()).isEqualTo("appName"); - } - - - @Test - public void configShouldBeCaptitalisedAndReplacedWithUnderscores() {// Basic test - // Test uppercasing and mangling of keys - fn.givenEvent().enqueue(); - - fn.setConfig("some-key-with-dashes", "some-value"); - - fn.thenRun(FnTestingRuleTest.TestFn.class, "copyConfiguration"); - - assertThat(configuration).containsEntry("SOME_KEY_WITH_DASHES", "some-value"); - - } - - @Test - public void shouldSetArgsInFirstEvent() { - fn.givenEvent().withAppName("TEST_APP") - .withHeader("H1", "H2") - .withMethod("PUT") - .withRequestUrl("http://example.com/mytest") - .withRoute("/myroute") - .enqueue(); - - fn.thenRun(FnTestingRuleTest.TestFn.class, "copyInputEvent"); - - assertThat(inEvent.getAppName()).isEqualTo("TEST_APP"); - assertThat(inEvent.getRoute()).isEqualTo("/myroute"); - assertThat(inEvent.getMethod()).isEqualTo("PUT"); - assertThat(inEvent.getRequestUrl()).isEqualTo("http://example.com/mytest"); - } - - - @Test - public void shouldSendEventDataToSDKInputEvent() { - final String APP_NAME = "alpha"; - final String ROUTE = "/bravo"; - final String REQUEST_URL = "http://charlie/alpha/bravo"; - final String METHOD = "POST"; - - fn.setConfig("SOME_CONFIG", "SOME_VALUE"); - fn.givenEvent() - .withAppName(APP_NAME) - .withRoute(ROUTE) - .withRequestUrl(REQUEST_URL) - .withMethod(METHOD) - .withHeader("FOO", "BAR, BAZ") - .withHeader("FEH", "") - .withBody("Body") // body as string - .enqueue(); - - fn.thenRun(TestFn.class, "captureInput"); - - FnResult result = fn.getOnlyResult(); - assertThat(result.getBodyAsString()).isEmpty(); - assertThat(result.getHeaders().getAll()).contains(headerEntry("Content-length", "0")); - assertThat(result.getStatus()).isEqualTo(200); - - InputEvent event = capturedInputs.get(0); - assertThat(event.getAppName()).isEqualTo(APP_NAME); - assertThat(event.getHeaders().getAll()) - .contains(headerEntry("FOO", "BAR, BAZ")) - .contains(headerEntry("FEH", "")); - assertThat(event.getMethod()).isEqualTo(METHOD); - assertThat(capturedBodies.get(0)).isEqualTo("Body".getBytes()); - } - - - @Test - public void shouldEnqueueMultipleDistinctEvents() { - fn.setConfig("SOME_CONFIG", "SOME_VALUE"); - fn.givenEvent() - .withAppName("alpha") - .withRoute("/bravo") - .withRequestUrl("http://charlie/alpha/bravo") - .withMethod("POST") - .withHeader("FOO", "BAR") - .withBody("Body") // body as string - .enqueue(); - - - fn.givenEvent() - .withAppName("alpha") - .withRoute("/bravo2") - .withRequestUrl("http://charlie/alpha/bravo2") - .withMethod("PUT") - .withHeader("FOO2", "BAR2") - .withBody("Body2") // body as string - .enqueue(); - - fn.thenRun(TestFn.class, "captureInput"); - - FnResult result = fn.getResults().get(0); - assertThat(result.getBodyAsString()).isEmpty(); - assertThat(result.getHeaders().getAll()).contains(headerEntry("Content-length", "0")); - assertThat(result.getStatus()).isEqualTo(200); - - InputEvent event = capturedInputs.get(0); - assertThat(event.getAppName()).isEqualTo("alpha"); - assertThat(event.getHeaders().getAll()).contains(headerEntry("FOO", "BAR")); - assertThat(event.getMethod()).isEqualTo("POST"); - assertThat(capturedBodies.get(0)).isEqualTo("Body".getBytes()); - - - FnResult result2 = fn.getResults().get(1); - assertThat(result2.getBodyAsString()).isEmpty(); - assertThat(result2.getHeaders().getAll()).contains(headerEntry("Content-length", "0")); - assertThat(result2.getStatus()).isEqualTo(200); - - InputEvent event2 = capturedInputs.get(1); - assertThat(event2.getAppName()).isEqualTo("alpha"); - assertThat(event2.getHeaders().getAll()).contains(headerEntry("FOO2", "BAR2")); - assertThat(event2.getMethod()).isEqualTo("PUT"); - assertThat(capturedBodies.get(1)).isEqualTo("Body2".getBytes()); - } - - - @Test - public void shouldEnqueueMultipleIdenticalEvents() { - fn.givenEvent() - .withAppName("alpha") - .withRoute("/bravo") - .withRequestUrl("http://charlie/alpha/bravo") - .withMethod("POST") - .withHeader("FOO", "BAR") - .withBody("Body") // body as string - .enqueue(10); - - fn.thenRun(TestFn.class, "echoInput"); - - List results = fn.getResults(); - assertThat(results).hasSize(10); - - - results.forEach((r) -> { - assertThat(r.getStatus()).isEqualTo(200); - assertThat(r.getHeaders().getAll()) - .contains(headerEntry("Content-Type", "application/octet-stream")) - .contains(headerEntry("Content-length", String.valueOf("Body".getBytes().length))); - - }); - } - - - @Test(expected = IllegalStateException.class) - public void shouldNotAllowSecondEnqueueOnInputStreamInput() { - fn.givenEvent() - .withAppName("alpha") - .withRoute("/bravo") - .withRequestUrl("http://charlie/alpha/bravo") - .withMethod("POST") - .withHeader("FOO", "BAR") - .withBody(new ByteArrayInputStream("Body".getBytes()), 4) // body as string - .enqueue(2); - - - } - - @Test - public void shouldEnqueuIndependentEventsWithInputStreams() { - fn.givenEvent() - .withBody(new ByteArrayInputStream("Body".getBytes()), 4) // body as string - .enqueue(); - - fn.givenEvent() - .withBody(new ByteArrayInputStream("Body1".getBytes()), 5) // body as string - .enqueue(); - - fn.thenRun(TestFn.class, "echoInput"); - - List results = fn.getResults(); - assertThat(results).hasSize(2); - - assertThat(results.get(0).getBodyAsString()).isEqualTo("Body"); - assertThat(results.get(1).getBodyAsString()).isEqualTo("Body1"); - } - - @Test - public void shouldHandleBodyAsInputStream() { - fn.givenEvent().withBody(new ByteArrayInputStream("FOO BAR".getBytes()), 3).enqueue(); - - fn.thenRun(TestFn.class, "captureInput"); - - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(200); - assertThat(capturedBodies.get(0)).isEqualTo("FOO".getBytes()); - } - - @Test - public void shouldLeaveQueryParamtersOffIfNotSpecified() { - String baseUrl = "www.example.com"; - fn.givenEvent() - .withRequestUrl(baseUrl) - .enqueue(); - fn.thenRun(TestFn.class, "copyInputEvent"); - - assertThat(inEvent.getRequestUrl()).isEqualTo(baseUrl); - } - - @Test - public void shouldPrependQuestionMarkForFirstQueryParam() { - String baseUrl = "www.example.com"; - fn.givenEvent() - .withRequestUrl(baseUrl) - .withQueryParameter("var", "val") - .enqueue(); - fn.thenRun(TestFn.class, "copyInputEvent"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(200); - assertThat(inEvent.getRequestUrl()).isEqualTo(baseUrl + "?var=val"); - } - - @Test - public void shouldHandleMultipleQueryParameters() { - String baseUrl = "www.example.com"; - fn.givenEvent() - .withRequestUrl(baseUrl) - .withQueryParameter("var1", "val1") - .withQueryParameter("var2", "val2") - .enqueue(); - fn.thenRun(TestFn.class, "copyInputEvent"); - - assertThat(inEvent.getRequestUrl()).isEqualTo(baseUrl + "?var1=val1&var2=val2"); - } - - @Test - public void shouldHandleMultipleQueryParametersWithSameKey() { - String baseUrl = "www.example.com"; - fn.givenEvent() - .withRequestUrl(baseUrl) - .withQueryParameter("var", "val1") - .withQueryParameter("var", "val2") - .enqueue(); - fn.thenRun(TestFn.class, "copyInputEvent"); - - assertThat(inEvent.getRequestUrl()).isEqualTo(baseUrl + "?var=val1&var=val2"); - } - - @Test - public void shouldUrlEncodeQueryParameterKey() { - fn.givenEvent() - .withRequestUrl(exampleBaseUrl) - .withQueryParameter("&", "val") - .enqueue(); - fn.thenRun(TestFn.class, "copyInputEvent"); - - assertThat(inEvent.getRequestUrl()).isEqualTo(exampleBaseUrl + "?%26=val"); - } - - @Test - public void shouldHandleQueryParametersWithSpaces() { - fn.givenEvent() - .withRequestUrl(exampleBaseUrl) - .withQueryParameter("my var", "this val") - .enqueue(); - fn.thenRun(TestFn.class, "copyInputEvent"); - - assertThat(inEvent.getRequestUrl()).isEqualTo(exampleBaseUrl + "?my+var=this+val"); - } - - @Test - public void shouldUrlEncodeQueryParameterValue() { - String baseUrl = "www.example.com"; - fn.givenEvent() - .withRequestUrl(baseUrl) - .withQueryParameter("var", "&") - .enqueue(); - fn.thenRun(TestFn.class, "copyInputEvent"); - - assertThat(inEvent.getRequestUrl()).isEqualTo(baseUrl + "?var=%26"); - } - - private static Map.Entry headerEntry(String key, String value) { - return new AbstractMap.SimpleEntry<>(key, value); - } -} diff --git a/testing/src/test/java/com/fnproject/fn/testing/IntegrationTest.java b/testing/src/test/java/com/fnproject/fn/testing/IntegrationTest.java deleted file mode 100644 index 4de9171d..00000000 --- a/testing/src/test/java/com/fnproject/fn/testing/IntegrationTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.fnproject.fn.testing; - -import org.junit.Rule; -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -public class IntegrationTest { - - @Rule - public FnTestingRule fn = FnTestingRule.createDefault(); - - @Test - public void runIntegrationTests() { - - fn.givenFn("nonexistent/nonexistent") - .withFunctionError() - - .givenFn("appName/route") - .withAction((body) -> { - if (new String(body).equals("PASS")) { - return "okay".getBytes(); - } else { - throw new FunctionError("failed as demanded"); - } - }) - .givenEvent() - .withBody("") // or "1,5,6,32" to select a set of tests individually - .enqueue() - - .thenRun(ExerciseEverything.class, "handleRequest"); - - assertThat(fn.getResults().get(0).getBodyAsString()) - .endsWith("Everything worked\n"); - } -}