From 35761d841bc28438bb35b89ded54b2fe9379856c Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 22 Oct 2025 09:28:27 -0700 Subject: [PATCH 1/4] chore: Migrate java-test-helpers to java-core. --- .github/workflows/release-please.yml | 34 + .github/workflows/test-helpers.yml | 32 + .release-please-manifest.json | 1 + lib/shared/test-helpers/.gitignore | 22 + lib/shared/test-helpers/CHANGELOG.md | 43 ++ lib/shared/test-helpers/CONTRIBUTING.md | 39 + lib/shared/test-helpers/LICENSE | 13 + lib/shared/test-helpers/NOTICE | 24 + lib/shared/test-helpers/README.md | 28 + lib/shared/test-helpers/build.gradle.kts | 115 +++ .../test-helpers/buildSrc/build.gradle.kts | 7 + .../build/classes/kotlin/main/Libs.class | Bin 0 -> 2622 bytes .../main/META-INF/buildSrc.kotlin_module | Bin 0 -> 24 bytes .../classes/kotlin/main/PluginVersions.class | Bin 0 -> 782 bytes .../classes/kotlin/main/ProjectValues.class | Bin 0 -> 1354 bytes .../kotlin/main/TestCoverageOverrides.class | Bin 0 -> 5112 bytes .../build/classes/kotlin/main/Versions.class | Bin 0 -> 1116 bytes .../main/helpers/Idea$configure$1.class | Bin 0 -> 1469 bytes .../classes/kotlin/main/helpers/Idea.class | Bin 0 -> 1320 bytes .../helpers/Jacoco$configureTasks$1$1.class | Bin 0 -> 2043 bytes .../helpers/Jacoco$configureTasks$1.class | Bin 0 -> 1647 bytes .../Jacoco$configureTasks$2$1$1$1$1.class | Bin 0 -> 2111 bytes .../Jacoco$configureTasks$2$1$1$1.class | Bin 0 -> 2054 bytes .../helpers/Jacoco$configureTasks$2$1$1.class | Bin 0 -> 1934 bytes .../Jacoco$configureTasks$2$1$2$1.class | Bin 0 -> 1884 bytes .../helpers/Jacoco$configureTasks$2$1$2.class | Bin 0 -> 3429 bytes .../helpers/Jacoco$configureTasks$2$1.class | Bin 0 -> 2050 bytes .../helpers/Jacoco$configureTasks$2.class | Bin 0 -> 1709 bytes .../classes/kotlin/main/helpers/Jacoco.class | Bin 0 -> 1869 bytes .../helpers/Javadoc$configureTask$1.class | Bin 0 -> 2288 bytes .../classes/kotlin/main/helpers/Javadoc.class | Bin 0 -> 1717 bytes .../main/helpers/Pom$standardPom$1$1.class | Bin 0 -> 1657 bytes .../main/helpers/Pom$standardPom$1.class | Bin 0 -> 1540 bytes .../main/helpers/Pom$standardPom$2$1.class | Bin 0 -> 1634 bytes .../main/helpers/Pom$standardPom$2.class | Bin 0 -> 1550 bytes .../main/helpers/Pom$standardPom$3.class | Bin 0 -> 1750 bytes .../classes/kotlin/main/helpers/Pom.class | Bin 0 -> 1865 bytes .../main/helpers/Test$configureTask$1.class | Bin 0 -> 1826 bytes .../main/helpers/Test$configureTask$2$1.class | Bin 0 -> 1947 bytes .../main/helpers/Test$configureTask$2.class | Bin 0 -> 1972 bytes .../classes/kotlin/main/helpers/Test.class | Bin 0 -> 1997 bytes .../build/kotlin/buildSrcjar-classes.txt | 1 + .../caches-jvm/inputs/source-to-output.tab | Bin 0 -> 4096 bytes .../inputs/source-to-output.tab.keystream | Bin 0 -> 4096 bytes .../inputs/source-to-output.tab.keystream.len | Bin 0 -> 8 bytes .../inputs/source-to-output.tab.len | Bin 0 -> 8 bytes .../inputs/source-to-output.tab.values.at | Bin 0 -> 2715 bytes .../caches-jvm/inputs/source-to-output.tab_i | Bin 0 -> 32768 bytes .../inputs/source-to-output.tab_i.len | Bin 0 -> 8 bytes .../jvm/kotlin/class-attributes.tab | Bin 0 -> 4096 bytes .../jvm/kotlin/class-attributes.tab.keystream | Bin 0 -> 4096 bytes .../kotlin/class-attributes.tab.keystream.len | Bin 0 -> 8 bytes .../jvm/kotlin/class-attributes.tab.len | Bin 0 -> 8 bytes .../jvm/kotlin/class-attributes.tab.values.at | Bin 0 -> 79 bytes .../jvm/kotlin/class-attributes.tab_i | Bin 0 -> 32768 bytes .../jvm/kotlin/class-attributes.tab_i.len | Bin 0 -> 8 bytes .../jvm/kotlin/class-fq-name-to-source.tab | Bin 0 -> 4096 bytes .../class-fq-name-to-source.tab.keystream | Bin 0 -> 4096 bytes .../class-fq-name-to-source.tab.keystream.len | Bin 0 -> 8 bytes .../kotlin/class-fq-name-to-source.tab.len | Bin 0 -> 8 bytes .../class-fq-name-to-source.tab.values.at | Bin 0 -> 503 bytes .../jvm/kotlin/class-fq-name-to-source.tab_i | Bin 0 -> 32768 bytes .../kotlin/class-fq-name-to-source.tab_i.len | Bin 0 -> 8 bytes .../caches-jvm/jvm/kotlin/constants.tab | Bin 0 -> 4096 bytes .../jvm/kotlin/constants.tab.keystream | Bin 0 -> 4096 bytes .../jvm/kotlin/constants.tab.keystream.len | Bin 0 -> 8 bytes .../caches-jvm/jvm/kotlin/constants.tab.len | Bin 0 -> 8 bytes .../jvm/kotlin/constants.tab.values.at | Bin 0 -> 833 bytes .../caches-jvm/jvm/kotlin/constants.tab_i | Bin 0 -> 32768 bytes .../caches-jvm/jvm/kotlin/constants.tab_i.len | Bin 0 -> 8 bytes .../jvm/kotlin/internal-name-to-source.tab | Bin 0 -> 4096 bytes .../internal-name-to-source.tab.keystream | Bin 0 -> 4096 bytes .../internal-name-to-source.tab.keystream.len | Bin 0 -> 8 bytes .../kotlin/internal-name-to-source.tab.len | Bin 0 -> 8 bytes .../internal-name-to-source.tab.values.at | Bin 0 -> 1279 bytes .../jvm/kotlin/internal-name-to-source.tab_i | Bin 0 -> 32768 bytes .../kotlin/internal-name-to-source.tab_i.len | Bin 0 -> 8 bytes .../cacheable/caches-jvm/jvm/kotlin/proto.tab | Bin 0 -> 4096 bytes .../caches-jvm/jvm/kotlin/proto.tab.keystream | Bin 0 -> 4096 bytes .../jvm/kotlin/proto.tab.keystream.len | Bin 0 -> 8 bytes .../caches-jvm/jvm/kotlin/proto.tab.len | Bin 0 -> 8 bytes .../caches-jvm/jvm/kotlin/proto.tab.values.at | Bin 0 -> 2930 bytes .../caches-jvm/jvm/kotlin/proto.tab_i | Bin 0 -> 32768 bytes .../caches-jvm/jvm/kotlin/proto.tab_i.len | Bin 0 -> 8 bytes .../jvm/kotlin/source-to-classes.tab | Bin 0 -> 4096 bytes .../kotlin/source-to-classes.tab.keystream | Bin 0 -> 4096 bytes .../source-to-classes.tab.keystream.len | Bin 0 -> 8 bytes .../jvm/kotlin/source-to-classes.tab.len | Bin 0 -> 8 bytes .../kotlin/source-to-classes.tab.values.at | Bin 0 -> 920 bytes .../jvm/kotlin/source-to-classes.tab_i | Bin 0 -> 32768 bytes .../jvm/kotlin/source-to-classes.tab_i.len | Bin 0 -> 8 bytes .../cacheable/caches-jvm/lookups/counters.tab | 2 + .../caches-jvm/lookups/file-to-id.tab | Bin 0 -> 4096 bytes .../lookups/file-to-id.tab.keystream | Bin 0 -> 4096 bytes .../lookups/file-to-id.tab.keystream.len | Bin 0 -> 8 bytes .../caches-jvm/lookups/file-to-id.tab.len | Bin 0 -> 8 bytes .../lookups/file-to-id.tab.values.at | Bin 0 -> 97 bytes .../caches-jvm/lookups/file-to-id.tab_i | Bin 0 -> 32768 bytes .../caches-jvm/lookups/file-to-id.tab_i.len | Bin 0 -> 8 bytes .../caches-jvm/lookups/id-to-file.tab | Bin 0 -> 4096 bytes .../lookups/id-to-file.tab.keystream | Bin 0 -> 4096 bytes .../lookups/id-to-file.tab.keystream.len | Bin 0 -> 8 bytes .../caches-jvm/lookups/id-to-file.tab.len | Bin 0 -> 8 bytes .../lookups/id-to-file.tab.values.at | Bin 0 -> 407 bytes .../caches-jvm/lookups/id-to-file.tab_i | Bin 0 -> 32768 bytes .../caches-jvm/lookups/id-to-file.tab_i.len | Bin 0 -> 8 bytes .../cacheable/caches-jvm/lookups/lookups.tab | Bin 0 -> 8192 bytes .../caches-jvm/lookups/lookups.tab.keystream | Bin 0 -> 8192 bytes .../lookups/lookups.tab.keystream.len | Bin 0 -> 8 bytes .../caches-jvm/lookups/lookups.tab.len | Bin 0 -> 8 bytes .../caches-jvm/lookups/lookups.tab.values.at | Bin 0 -> 3517 bytes .../caches-jvm/lookups/lookups.tab_i | Bin 0 -> 32768 bytes .../caches-jvm/lookups/lookups.tab_i.len | Bin 0 -> 8 bytes .../compileKotlin/cacheable/last-build.bin | Bin 0 -> 18 bytes .../local-state/build-history.bin | Bin 0 -> 31 bytes .../buildSrc/build/libs/buildSrc.jar | Bin 0 -> 33947 bytes .../plugin-under-test-metadata.properties | 1 + .../plugin-development/validation-report.txt | 0 .../buildSrc/build/tmp/jar/MANIFEST.MF | 2 + .../buildSrc/src/main/kotlin/Dependencies.kt | 44 ++ .../buildSrc/src/main/kotlin/Idea.kt | 16 + .../buildSrc/src/main/kotlin/Jacoco.kt | 52 ++ .../buildSrc/src/main/kotlin/Javadoc.kt | 21 + .../buildSrc/src/main/kotlin/Pom.kt | 31 + .../buildSrc/src/main/kotlin/ProjectValues.kt | 15 + .../buildSrc/src/main/kotlin/Test.kt | 34 + .../src/main/kotlin/TestCoverageOverrides.kt | 30 + lib/shared/test-helpers/checkstyle.xml | 14 + lib/shared/test-helpers/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59536 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + lib/shared/test-helpers/gradlew | 234 ++++++ lib/shared/test-helpers/gradlew.bat | 89 +++ lib/shared/test-helpers/settings.gradle.kts | 1 + .../launchdarkly/testhelpers/Assertions.java | 51 ++ .../testhelpers/ConcurrentHelpers.java | 185 +++++ .../testhelpers/InternalHelpers.java | 14 + .../testhelpers/JsonAssertions.java | 482 ++++++++++++ .../testhelpers/JsonTestValue.java | 77 ++ .../com/launchdarkly/testhelpers/TempDir.java | 115 +++ .../launchdarkly/testhelpers/TempFile.java | 94 +++ .../testhelpers/TypeBehavior.java | 170 +++++ .../testhelpers/httptest/Handler.java | 17 + .../testhelpers/httptest/HandlerSwitcher.java | 40 + .../testhelpers/httptest/Handlers.java | 294 ++++++++ .../testhelpers/httptest/HttpServer.java | 208 ++++++ .../testhelpers/httptest/RequestContext.java | 68 ++ .../testhelpers/httptest/RequestInfo.java | 104 +++ .../testhelpers/httptest/RequestRecorder.java | 108 +++ .../httptest/SequentialHandler.java | 22 + .../httptest/ServerTLSConfiguration.java | 123 +++ .../testhelpers/httptest/SimpleRouter.java | 152 ++++ .../httptest/SpecialHttpConfigurations.java | 389 ++++++++++ .../httptest/impl/HttpServerImpl.java | 22 + .../impl/NanoHttpdServerDelegate.java | 296 ++++++++ .../testhelpers/httptest/nanohttpd/CHANGES.md | 19 + .../testhelpers/httptest/nanohttpd/LICENSE.md | 17 + .../protocols/http/ClientHandler.java | 95 +++ .../nanohttpd/protocols/http/HTTPSession.java | 698 ++++++++++++++++++ .../protocols/http/IHTTPSession.java | 93 +++ .../nanohttpd/protocols/http/NanoHTTPD.java | 641 ++++++++++++++++ .../protocols/http/ServerRunnable.java | 90 +++ .../protocols/http/content/ContentType.java | 112 +++ .../protocols/http/content/Cookie.java | 78 ++ .../protocols/http/content/CookieHandler.java | 127 ++++ .../protocols/http/request/Method.java | 122 +++ .../http/response/ChunkedOutputStream.java | 76 ++ .../protocols/http/response/IStatus.java | 41 + .../protocols/http/response/Response.java | 448 +++++++++++ .../protocols/http/response/Status.java | 111 +++ .../sockets/DefaultServerSocketFactory.java | 51 ++ .../sockets/SecureServerSocketFactory.java | 73 ++ .../http/tempfiles/DefaultTempFile.java | 79 ++ .../tempfiles/DefaultTempFileManager.java | 85 +++ .../DefaultTempFileManagerFactory.java | 47 ++ .../protocols/http/tempfiles/ITempFile.java | 53 ++ .../http/tempfiles/ITempFileManager.java | 49 ++ .../http/threading/DefaultAsyncRunner.java | 90 +++ .../http/threading/IAsyncRunner.java | 48 ++ .../httptest/nanohttpd/util/IFactory.java | 46 ++ .../nanohttpd/util/IFactoryThrowing.java | 49 ++ .../httptest/nanohttpd/util/IHandler.java | 49 ++ .../httptest/nanohttpd/util/ServerRunner.java | 75 ++ .../testhelpers/httptest/package-info.java | 107 +++ .../testhelpers/tcptest/TcpHandler.java | 19 + .../testhelpers/tcptest/TcpHandlers.java | 137 ++++ .../testhelpers/tcptest/TcpServer.java | 118 +++ .../testhelpers/AssertionsTest.java | 59 ++ .../testhelpers/ConcurrentHelpersTest.java | 97 +++ .../testhelpers/JsonAssertionsTest.java | 202 +++++ .../testhelpers/JsonTestValueTest.java | 69 ++ .../launchdarkly/testhelpers/TempDirTest.java | 48 ++ .../testhelpers/TempFileTest.java | 55 ++ .../testhelpers/TypeBehaviorTest.java | 192 +++++ .../httptest/HandlerSwitcherTest.java | 27 + .../testhelpers/httptest/HandlersTest.java | 178 +++++ .../testhelpers/httptest/HttpServerTest.java | 78 ++ .../httptest/RequestRecorderTest.java | 152 ++++ .../httptest/SequentialHandlerTest.java | 28 + .../httptest/SimpleRouterTest.java | 118 +++ .../SpecialHttpConfigurationsTest.java | 171 +++++ .../testhelpers/httptest/StreamingTest.java | 116 +++ .../testhelpers/httptest/TestUtil.java | 24 + .../testhelpers/tcptest/TcpHandlersTest.java | 94 +++ .../testhelpers/tcptest/TcpServerTest.java | 42 ++ .../testhelpers/tcptest/TestUtil.java | 47 ++ release-please-config.json | 8 + 207 files changed, 9732 insertions(+) create mode 100644 .github/workflows/test-helpers.yml create mode 100644 lib/shared/test-helpers/.gitignore create mode 100644 lib/shared/test-helpers/CHANGELOG.md create mode 100644 lib/shared/test-helpers/CONTRIBUTING.md create mode 100644 lib/shared/test-helpers/LICENSE create mode 100644 lib/shared/test-helpers/NOTICE create mode 100644 lib/shared/test-helpers/README.md create mode 100644 lib/shared/test-helpers/build.gradle.kts create mode 100644 lib/shared/test-helpers/buildSrc/build.gradle.kts create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/Libs.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/META-INF/buildSrc.kotlin_module create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/PluginVersions.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/ProjectValues.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/TestCoverageOverrides.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/Versions.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Idea$configure$1.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Idea.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$1$1.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$1.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$1$1$1.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$1$1.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$1.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$2$1.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$2.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Javadoc$configureTask$1.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Javadoc.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$1$1.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$1.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$2$1.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$2.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$3.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Test$configureTask$1.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Test$configureTask$2$1.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Test$configureTask$2.class create mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Test.class create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/buildSrcjar-classes.txt create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values.at create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/counters.tab create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i.len create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/last-build.bin create mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/local-state/build-history.bin create mode 100644 lib/shared/test-helpers/buildSrc/build/libs/buildSrc.jar create mode 100644 lib/shared/test-helpers/buildSrc/build/pluginUnderTestMetadata/plugin-under-test-metadata.properties create mode 100644 lib/shared/test-helpers/buildSrc/build/reports/plugin-development/validation-report.txt create mode 100644 lib/shared/test-helpers/buildSrc/build/tmp/jar/MANIFEST.MF create mode 100644 lib/shared/test-helpers/buildSrc/src/main/kotlin/Dependencies.kt create mode 100644 lib/shared/test-helpers/buildSrc/src/main/kotlin/Idea.kt create mode 100644 lib/shared/test-helpers/buildSrc/src/main/kotlin/Jacoco.kt create mode 100644 lib/shared/test-helpers/buildSrc/src/main/kotlin/Javadoc.kt create mode 100644 lib/shared/test-helpers/buildSrc/src/main/kotlin/Pom.kt create mode 100644 lib/shared/test-helpers/buildSrc/src/main/kotlin/ProjectValues.kt create mode 100644 lib/shared/test-helpers/buildSrc/src/main/kotlin/Test.kt create mode 100644 lib/shared/test-helpers/buildSrc/src/main/kotlin/TestCoverageOverrides.kt create mode 100644 lib/shared/test-helpers/checkstyle.xml create mode 100644 lib/shared/test-helpers/gradle.properties create mode 100644 lib/shared/test-helpers/gradle/wrapper/gradle-wrapper.jar create mode 100644 lib/shared/test-helpers/gradle/wrapper/gradle-wrapper.properties create mode 100755 lib/shared/test-helpers/gradlew create mode 100644 lib/shared/test-helpers/gradlew.bat create mode 100644 lib/shared/test-helpers/settings.gradle.kts create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/Assertions.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/ConcurrentHelpers.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/InternalHelpers.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/JsonAssertions.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/JsonTestValue.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/TempDir.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/TempFile.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/TypeBehavior.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/Handler.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/HandlerSwitcher.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/Handlers.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/HttpServer.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/RequestContext.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/RequestInfo.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/RequestRecorder.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/SequentialHandler.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/ServerTLSConfiguration.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/SimpleRouter.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/SpecialHttpConfigurations.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/impl/HttpServerImpl.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/impl/NanoHttpdServerDelegate.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/CHANGES.md create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/LICENSE.md create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/ClientHandler.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/HTTPSession.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/IHTTPSession.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/NanoHTTPD.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/ServerRunnable.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/content/ContentType.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/content/Cookie.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/content/CookieHandler.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/request/Method.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/response/ChunkedOutputStream.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/response/IStatus.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/response/Response.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/response/Status.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/sockets/DefaultServerSocketFactory.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/sockets/SecureServerSocketFactory.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/tempfiles/DefaultTempFile.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/tempfiles/DefaultTempFileManager.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/tempfiles/DefaultTempFileManagerFactory.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/tempfiles/ITempFile.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/tempfiles/ITempFileManager.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/threading/DefaultAsyncRunner.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/threading/IAsyncRunner.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/util/IFactory.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/util/IFactoryThrowing.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/util/IHandler.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/util/ServerRunner.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/package-info.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/tcptest/TcpHandler.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/tcptest/TcpHandlers.java create mode 100644 lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/tcptest/TcpServer.java create mode 100644 lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/AssertionsTest.java create mode 100644 lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/ConcurrentHelpersTest.java create mode 100644 lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/JsonAssertionsTest.java create mode 100644 lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/JsonTestValueTest.java create mode 100644 lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/TempDirTest.java create mode 100644 lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/TempFileTest.java create mode 100644 lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/TypeBehaviorTest.java create mode 100644 lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/HandlerSwitcherTest.java create mode 100644 lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/HandlersTest.java create mode 100644 lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/HttpServerTest.java create mode 100644 lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/RequestRecorderTest.java create mode 100644 lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/SequentialHandlerTest.java create mode 100644 lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/SimpleRouterTest.java create mode 100644 lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/SpecialHttpConfigurationsTest.java create mode 100644 lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/StreamingTest.java create mode 100644 lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/TestUtil.java create mode 100644 lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/tcptest/TcpHandlersTest.java create mode 100644 lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/tcptest/TcpServerTest.java create mode 100644 lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/tcptest/TestUtil.java diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index f0140ec..36edea0 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -12,6 +12,7 @@ jobs: outputs: package-sdk-common-released: ${{ steps.release.outputs['lib/shared/common--release_created'] }} package-sdk-internal-released: ${{ steps.release.outputs['lib/shared/internal--release_created'] }} + package-test-helpers-released: ${{ steps.release.outputs['lib/shared/test-helpers--release_created'] }} package-server-sdk-released: ${{ steps.release.outputs['lib/sdk/server--release_created'] }} package-server-sdk-otel-released: ${{ steps.release.outputs['lib/java-server-sdk-otel--release_created'] }} package-server-sdk-redis-store-released: ${{ steps.release.outputs['lib/java-server-sdk-redis-store--release_created'] }} @@ -188,3 +189,36 @@ jobs: sonatype_password: ${{ env.SONATYPE_PASSWORD }} aws_role: ${{ vars.AWS_ROLE_ARN }} token: ${{ secrets.GITHUB_TOKEN }} + + release-test-helpers: + runs-on: ubuntu-latest + needs: release-please + permissions: + id-token: write + contents: write + pull-requests: write + if: ${{ needs.release-please.outputs.package-test-helpers-released == 'true'}} + steps: + - uses: actions/checkout@v4 + + - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.1.0 + name: Get secrets + with: + aws_assume_role: ${{ vars.AWS_ROLE_ARN }} + ssm_parameter_pairs: '/production/common/releasing/sonatype/central/username = SONATYPE_USER_NAME, + /production/common/releasing/sonatype/central/password = SONATYPE_PASSWORD, + /production/common/releasing/java/keyId = SIGNING_KEY_ID' + s3_path_pairs: 'launchdarkly-releaser/java/code-signing-keyring.gpg = code-signing-keyring.gpg' + + - uses: ./.github/actions/full-release + with: + workspace_path: lib/shared/test-helpers + dry_run: false + prerelease: false + code_signing_keyring: ${{ github.workspace }}/code-signing-keyring.gpg + signing_key_id: ${{ env.SIGNING_KEY_ID }} + signing_key_passphrase: '' + sonatype_username: ${{ env.SONATYPE_USER_NAME }} + sonatype_password: ${{ env.SONATYPE_PASSWORD }} + aws_role: ${{ vars.AWS_ROLE_ARN }} + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test-helpers.yml b/.github/workflows/test-helpers.yml new file mode 100644 index 0000000..35c6469 --- /dev/null +++ b/.github/workflows/test-helpers.yml @@ -0,0 +1,32 @@ +name: test-helpers + +on: + push: + branches: [main, 'feat/**'] + paths-ignore: + - '**.md' #Do not need to run CI for markdown changes. + pull_request: + branches: [main, 'feat/**'] + paths-ignore: + - '**.md' + +jobs: + build-test-helpers: + runs-on: ${{ matrix.os }} + strategy: + matrix: + java-version: [8, 11, 17, 19] + os: [ubuntu-latest] + include: + - java-version: 11 + os: windows-latest + - java-version: 17 + os: windows-latest + steps: + - uses: actions/checkout@v3 + + - name: Shared CI Steps + uses: ./.github/actions/ci + with: + workspace_path: 'lib/shared/test-helpers' + java_version: ${{ matrix.java-version }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c94dacd..7b37aac 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -3,5 +3,6 @@ "lib/java-server-sdk-redis-store": "3.0.1", "lib/shared/common": "2.1.2", "lib/shared/internal": "1.5.1", + "lib/shared/test-helpers": "2.0.2", "lib/sdk/server": "7.10.2" } diff --git a/lib/shared/test-helpers/.gitignore b/lib/shared/test-helpers/.gitignore new file mode 100644 index 0000000..9cbae19 --- /dev/null +++ b/lib/shared/test-helpers/.gitignore @@ -0,0 +1,22 @@ +# Eclipse project files +.classpath +.project +.settings + +# Intellij project files +*.iml +*.ipr +*.iws +.idea/ + +#Gradle +.gradletasknamecache +.gradle/ +build/ +bin/ +out/ +classes/ + +# Test code that gets temporarily copied by our Android CI build +src/androidTest/java/com/launchdarkly/sdk/**/*.java +!src/androidTest/java/com/launchdarkly/sdk/BaseTest.java diff --git a/lib/shared/test-helpers/CHANGELOG.md b/lib/shared/test-helpers/CHANGELOG.md new file mode 100644 index 0000000..a1c69ee --- /dev/null +++ b/lib/shared/test-helpers/CHANGELOG.md @@ -0,0 +1,43 @@ +# Change log + +All notable changes to the project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). + +## [2.0.2] - 2023-06-27 +### Changed: +- Bumping Guava version to incorporate CVE fixes. + +## [2.0.1] - 2022-12-18 +(This release replaces the broken 2.0.0 release, which was accidentally duplicated from 1.3.0.) + +This release improves compatibility of the library with Android code by removing usage of Java 8 APIs that are not supported in Android. It also revises the embedded HTTP mechanism to use a fork of `nanohttpd` rather than the heavier-weight Jetty. + +### Changed: +- All methods that took a `java.time.Duration` now take `long, TimeUnit` instead. +- The `HttpServer` class is now based on a fork of the lightweight `nanohttpd` (https://github.com/launchdarkly-labs/nanohttpd) library. This should work correctly in any server-side Java environment; it has not been validated in Android, but the previous Jetty implementation did not work in Android anyway. + +## [2.0.0] - 2022-11-17 +This release improves compatibility of the library with Android code by removing usage of Java 8 APIs that are not supported in Android. It also revises the embedded HTTP mechanism to use a fork of `nanohttpd` rather than the heavier-weight Jetty. + +### Changed: +- All methods that took a `java.time.Duration` now take `long, TimeUnit` instead. +- The `HttpServer` class is now based on a fork of the lightweight `nanohttpd` (https://github.com/launchdarkly-labs/nanohttpd) library. This should work correctly in any server-side Java environment; it has not been validated in Android, but the previous Jetty implementation did not work in Android anyway. + +## [1.3.0] - 2022-08-29 +### Added: +- `com.launchdarkly.testhelpers.tcptest`: this package is analogous to `httptest` but much simpler, providing a basic TCP listener that can be configured with behaviors like "close connections without sending a response" or "forward the connection to another port". +- `com.launchdarkly.testhelpers.httptest.SpecialHttpConfigurations`: test helpers to validate several standard kinds of HTTP client configurations. + +## [1.2.0] - 2022-07-08 +### Added: +- `TypeBehavior.singletonValueFactory` is a new method that can be used with `TypeBehavior.checkEqualsAndHashCode` to allow testing of types that have interned/singleton values. + +## [1.1.1] - 2022-06-17 +### Fixed: +- Fixed Hamcrest dependency to use `hamcrest-library` rather than `hamcrest-all`, because JUnit (which is commonly used in any unit test code that would also use Hamcrest) has a transitive dependency on `hamcrest-library` and using both would result in duplication on the classpath. + +## [1.1.0] - 2021-07-21 +### Added: +- `Assertions`, `ConcurrentHelpers`, `JsonAssertions`, `TempDir`, `TempFile`, `TypeBehavior`. + +## [1.0.0] - 2021-06-25 +Initial release. diff --git a/lib/shared/test-helpers/CONTRIBUTING.md b/lib/shared/test-helpers/CONTRIBUTING.md new file mode 100644 index 0000000..7c6925e --- /dev/null +++ b/lib/shared/test-helpers/CONTRIBUTING.md @@ -0,0 +1,39 @@ +# Contributing to this project + +## Submitting bug reports and feature requests + +The LaunchDarkly SDK team monitors the [issue tracker](https://github.com/launchdarkly/java-test-helpers/issues) in the GitHub repository. Bug reports and feature requests specific to this project should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days. + +## Submitting pull requests + +We encourage pull requests and other contributions from the community. Before submitting pull requests, ensure that all temporary or unintended code is removed. Don't worry about adding reviewers to the pull request; the LaunchDarkly SDK team will add themselves. The SDK team will acknowledge all pull requests within two business days. + +## Build instructions + +### Prerequisites + +The project builds with [Gradle](https://gradle.org/) and should be built against Java 8. + +### Building + +To build the project without running any tests: +``` +./gradlew jar +``` + +If you wish to clean your working directory between builds, you can clean it by running: +``` +./gradlew clean +``` + +If you wish to use your generated SDK artifact by another Maven/Gradle project such as [java-server-sdk](https://github.com/launchdarkly/java-server-sdk), you will likely want to publish the artifact to your local Maven repository so that your other project can access it. +``` +./gradlew publishToMavenLocal +``` + +### Testing + +To build the project and run all unit tests: +``` +./gradlew test +``` diff --git a/lib/shared/test-helpers/LICENSE b/lib/shared/test-helpers/LICENSE new file mode 100644 index 0000000..c27e062 --- /dev/null +++ b/lib/shared/test-helpers/LICENSE @@ -0,0 +1,13 @@ +Copyright 2021 Catamorphic, Co. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/lib/shared/test-helpers/NOTICE b/lib/shared/test-helpers/NOTICE new file mode 100644 index 0000000..cd26131 --- /dev/null +++ b/lib/shared/test-helpers/NOTICE @@ -0,0 +1,24 @@ +java-test-helpers +Copyright 2021 Catamorphic, Co. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +================================================================================ + +This project bundles the following dependencies under the BSD 3-Clause License. +See bundled license files for details. + +- NanoHttpd (core module) + https://github.com/NanoHttpd/nanohttpd + Copyright (c) 2012-2013 by Paul S. Hawke, 2001, 2005-2013 by Jarno Elonen, 2010 by Konstantinos Togias + Files: com.launchdarkly.testhelpers.httptest.nanohttpd.* diff --git a/lib/shared/test-helpers/README.md b/lib/shared/test-helpers/README.md new file mode 100644 index 0000000..f66a84a --- /dev/null +++ b/lib/shared/test-helpers/README.md @@ -0,0 +1,28 @@ +# LaunchDarkly Java Test Helpers + +[![Quality control](https://github.com/launchdarkly/java-test-helpers/actions/workflows/ci.yml/badge.svg)](https://github.com/launchdarkly/java-test-helpers/actions/workflows/ci.yml) +[![Javadocs](http://javadoc.io/badge/com.launchdarkly/java-test-helpers.svg)](http://javadoc.io/doc/com.launchdarkly/java-test-helpers) + +## Overview + +This project centralizes some test support code that is used by LaunchDarkly's Java and Android SDKs and related components, which may be useful in other Java projects. + +See [API documentation](http://javadoc.io/doc/com.launchdarkly/java-test-helpers) for full details. + +## Contributing + +We encourage pull requests and other contributions from the community. See [Contributing](CONTRIBUTING.md). + +## About LaunchDarkly + +* LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can: + * Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. + * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). + * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. + * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline. +* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/docs) for a complete list. +* Explore LaunchDarkly + * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information + * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides + * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation + * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates diff --git a/lib/shared/test-helpers/build.gradle.kts b/lib/shared/test-helpers/build.gradle.kts new file mode 100644 index 0000000..27ac7f0 --- /dev/null +++ b/lib/shared/test-helpers/build.gradle.kts @@ -0,0 +1,115 @@ +import java.time.Duration +import org.gradle.external.javadoc.CoreJavadocOptions + +buildscript { + repositories { + mavenCentral() + mavenLocal() + } +} + +plugins { // see Dependencies.kt in buildSrc + Libs.javaBuiltInGradlePlugins.forEach { id(it) } + Libs.javaExtGradlePlugins.forEach { (n, v) -> id(n) version v } +} + +repositories { + mavenLocal() + // Before LaunchDarkly release artifacts get synced to Maven Central they are here along with snapshots: + maven { url = uri("https://oss.sonatype.org/content/groups/public/") } + mavenCentral() +} + +configurations.all { + // check for updates every build for dependencies with: 'changing: true' + resolutionStrategy.cacheChangingModulesFor(0, "seconds") +} + +base { + group = ProjectValues.groupId + archivesBaseName = ProjectValues.artifactId + version = version +} + +java { + withJavadocJar() + withSourcesJar() + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { // see Dependencies.kt in buildSrc + Libs.implementation.forEach { implementation(it) } + Libs.javaTestImplementation.forEach { testImplementation(it) } +} + +checkstyle { + toolVersion = "9.3" + configFile = file("${project.rootDir}/checkstyle.xml") +} + +tasks.checkstyleMain { + // Exclude embedded nanohttpd code from checkstyle + exclude("com/launchdarkly/testhelpers/httptest/nanohttpd/**") +} + +tasks.jar { + manifest { + attributes(mapOf("Implementation-Version" to project.version)) + } + // Include NOTICE file in binary distribution + from(".") { + include("NOTICE") + into("META-INF") + } + // Include nanohttpd license in binary distribution per BSD 3-Clause requirements + from("src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd") { + include("LICENSE.md") + into("META-INF/licenses/nanohttpd") + } +} + +tasks.javadoc { + // Force the Javadoc build to fail if there are any Javadoc warnings. + (options as CoreJavadocOptions).addStringOption("Xwerror") + + // Exclude embedded nanohttpd code from Javadoc generation + exclude("com/launchdarkly/testhelpers/httptest/nanohttpd/**") +} + +helpers.Test.configureTask(tasks.compileTestJava, tasks.test, configurations["testRuntimeClasspath"]) + +helpers.Jacoco.configureTasks( + tasks.jacocoTestReport, + tasks.jacocoTestCoverageVerification +) + +helpers.Idea.configure(idea) + +publishing { + publications { + create("mavenJava") { + from(components["java"]) + + helpers.Pom.standardPom(pom) // see Pom.kt in buildSrc + } + } + repositories { + mavenLocal() + } +} + +nexusPublishing { + clientTimeout.set(Duration.ofMinutes(2)) // we've seen extremely long delays in creating repositories + repositories { + sonatype{ + nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/")) + snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/")) + } + } +} + +signing { + setRequired({ findProperty("skipSigning") != "true" }) + sign(publishing.publications["mavenJava"]) +} diff --git a/lib/shared/test-helpers/buildSrc/build.gradle.kts b/lib/shared/test-helpers/buildSrc/build.gradle.kts new file mode 100644 index 0000000..876c922 --- /dev/null +++ b/lib/shared/test-helpers/buildSrc/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + `kotlin-dsl` +} + +repositories { + mavenCentral() +} diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/Libs.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/Libs.class new file mode 100644 index 0000000000000000000000000000000000000000..adb64fd331e1bde49f4a915fb452dbc64cd588a1 GIT binary patch literal 2622 zcma)7OLNm!6#lOKQiPK@N+5{?O@Kg?w3g!}6zZh35FRxcLs6RIrqIf=Dl3sC*OD?Y z-DK4t(p7hTuz+c&oebUeuk=53damVwUzuTZ@44skorlgj`scsj{{pav21B%HRlEck zLuyys*Rrf;h!f*)qI7htm|qoq@6>QX!noFvZQU2bS#@oZC&UY8wyi zz@*d|X4(LQq)AMK5T_U>IK<7TevkQRN4od428DX5n=>HQ!9|~J2h#6jtgC*Zm&?7p zxAc?56sCER-=XSmSdQgiWr!>+ZYME^vl5~>#~>C;>ejW==1n3ubi1uJR@`%adSvez zzE;(Ijgl4W`w?nT#*ZjJs1b!qiw6d_N3Uv*;pfAHIZ2ojBq4QpAPFKTh>;p0I!5A6n6b*w0tMOrI-F{1K6{Et=ovvwTij6+xeQ zuKoRV*S2YE(30@7n{Vj5v`0RnuCQrW-2avWSlH?4)Ns?R_lyY41Le@pV?HK5<|kXOoz6W zhzAVAb#32pGL4`@k=H~#BrL0H5Y`rDv#aT@?uyu^ge+GvEx#626tlGiGLG>y@RYV? zrqS97A|5fssAfFcFBwMLRoe<^y?VSzUr~GbF2m|Ve|`Eni$`PgK(m_bNj$;V5+36l zhUo#{gQmOXqe3y%wFdtV?<{m7bbbN83zDe9kf4K9M{gJn!>JmMZW*4k=Q9kcR4L65 zng*@9hk@f;b)(?yTb@<1jcafAI@N5G+La)RmSdEHdc|mNX{5s-7hPSmx3#9l-)*L( zx1EMNKmxmT$N7ZOo#wmxSQ#zhV`a3>Udz&5 znA3b#3BS`%9Ag-Qgb4P?t3py5azuZ_@Go==u<7J60pqCCnGEF^AtbaTKhcFaMu-FO z#(VM7Z;f|y5Z+`j-t1fB&ERx@zO%h}i^t-f>B?8&zX_@euV0pKUaQ#B3!MLzh|U7a z9e}ZR0YzMRZTv5_1vmcw4+!NKSiQUS9G4P*;&M46f2fwD^151%$seiZxcssD5;vb? zI2KB8b7>*uji=fscRBMOH@VMEK2ysHSyjuT{JC0A#pN$T^+~xb@4STe66SNv@{n2U zn?J!M(K3DSar&MUIE9m#p>vM-^Tb=C4$Ts0jcR#?=&P8;4U!fpof6JL#d$m;&Bv5r z1s7qGybfN|CHQm;a9ZW5!KK?IuNP?=-rYf@fQ14U3#9kn(d&Kw%8&rc4wy%dmv=Dc jVbnv;!&wip7D4oo2g$>Phm#(rTFe;_r#+-RuD|j>s8&3} literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/META-INF/buildSrc.kotlin_module b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/META-INF/buildSrc.kotlin_module new file mode 100644 index 0000000000000000000000000000000000000000..3c12ac66a6a98c50ad2cfef9dd42f905b947be56 GIT binary patch literal 24 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 H!v6mO{8vM8 literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/Versions.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/Versions.class new file mode 100644 index 0000000000000000000000000000000000000000..407dbb5f78e488ff9f2a21a1bae94233e5a6246f GIT binary patch literal 1116 zcmZWnTTc@~6#iy<+b%7Xi^xUsf+(QuwxtM0VuGkiW336TjfSUfJ1N_4cgfC_=RWa6 z^u<@>1BoW4!8d=D@hldp?aQ2VzVDke=Q6*3fBOL-hbIhLMflS8Tp0snNHwh!%XBQa zVZN<4MNP#JVhA^+=Q2zcuGo_DZMX51A(Sa+>W-{-%@RKg%c`WD%W0554$fig1!e!%B zm6k7-%6rBAm*no&oc_(pLd#PWm~Vu#>Xx!d$=fF(8XXIcaB$Fgc=ZAU18Y%K&oF%I zb!T)w$910Sb#<-<&N$hq_Vbj_^}1P}X3t|9KmQozDSj}X3N7$-`2G25IY9JYm!QZf z6di?PqmVWV#fPBt&rv-nHOr_MXw{DG)JuL%2ghOpx3Cl^&t#Z*A&!Mx7jDfK(rD3y zll!z#Wx1+iIUT{Uc+hc`-4=QG#Flo|5qnpAr(y4VbrMY!Y*!RJ?W*w07HNXNj8jdi^WXK@#>~5{_-cIL&W|>^oxbQ0lvf2<5Sy u$GtpOaQ`xT5Jam)VC@i0VjT|;F($Dj@knApB68vA5-EvUiD`+X#Ks?K-^}m; literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Idea$configure$1.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Idea$configure$1.class new file mode 100644 index 0000000000000000000000000000000000000000..5c3934e8aaeadeada096d375f8f21c6abbb8d33b GIT binary patch literal 1469 zcmb7ETW=dh6#iy?&Du@uK-(lhOIw#ZNt<;_xz#4ENmFX%n5u~)L7v9DQ)iO(Otd>| z;4Qy}e}Ir$1QLpPwL3+{`Kqc0Pdi}uy7#4V-dw(ClI{m zYxPtPl1S9n3t$ZQTDy0a=6Y=?DTea+r7;#I<|b8U5cu9 zCxhG;h6mhmD#00!GQ?kIm#kefr0b_TFt8(%f^~-4=}Fl=J#n3i<2nlt9V@3NOHPBW zI1T5K`^S%Yr)WKc+YF@^S6clr)JaUGQIzuZ2nzU^XmIv87#eRtC=sSt^(j8He7au7 z4ZJ@?rxtPDKDxCdLzCYm-ZIQ}r4qel*cZ_*@7wv#ceT&MJswFbPn5MMi8691I%*_i zN%}MLwEyKJy*--Qfey}$bhYJX_&>hEkV1a-g`OUjTzUrpQBS z3t61x+#6?{N`wY=lcSl@F9VCVJ$o;eHa(#k(%d8I3LzZYXI55EQF?~ipGaW1Pivlb zGN5W-95N|FeXaT_1*BOBvRjE`bmjZyp?h*Drg literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Idea.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Idea.class new file mode 100644 index 0000000000000000000000000000000000000000..90fdfeebe2fbc3c06a0a632a344a2307efb7da3d GIT binary patch literal 1320 zcmb7CTTc@~6#iy<*>)|^qF@096_A^{fOm@*Bx|f zXotNJfoMp2$S^iuIo?OPG*Dzn6RS;^ z$nQTw{sC;H2ABKGDyole6i3ie!5CU7D7KJF|@PAyaEA*I5c| z6CA^O979>st}z&T#jk8c3}{z~)m%A$u!2P00`YXV|IRY<{PBiajnIj`bjFRAbC&;bSStMx)NKNiw*&m2pC&MqKxq}Cd!z^)NkvOwl32JuB?KERPgjFG9iXS%!IfQA{8q}h;t!ke*yk_ BEQA06 literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$1$1.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$1$1.class new file mode 100644 index 0000000000000000000000000000000000000000..3c1e8aecb2890ace65a2e2df476772f5b9c7fdf4 GIT binary patch literal 2043 zcmb7F+fo}x5IwU9R!DLHCC0{1Y>1Uu0^19Y<2Z<5V;O=REC`YnV{#j<25DinOLj&o zc;hGX1Nnngs$y`eQdA!EQK|H-WKaQ#iw8~bOm|P8KGWU$`=8f;0=Nf(ZQ&Xua6O7^EOlf*)lef6#a<67BS2l&Mva_d;mi(Y*)dF61 zg~hiWYtB}V?`0VVPkNh(FkC#-+(aLPDRzY2RDz7sr{iSG4EMX7m5|CIWSe0cmeS}u zIjxFqKTtB~dx|@r2u#EnvgvN5=kU&E$_Ye~h{3=GhW=C_Y~i#-z;GkHa#N^EK!K#i zJA@ zfv7U1PUGAT{FYM{fwdC&RIqB7;N3tfjt}t>!_aYcJ@$Q9aL>d|s-c#KwY014}fH3e84E1SMW^=_I-2+uSYlz|s58VQRJMDW@UwUdxdV zogYnux#Iyt1D4pU`_&^QJ$xeX(Y(vKTuMRPfqBn%eMxQJHYHF+r{-}*%ZZ96Uv#7( z8KK=vY|xiM)YL-xJ?bPMQM9MT{#Wbh$zv&xXL%H`{ARbJcVFvebsbEih|)SD63->d I5;=+Jzm@9{)Bpeg literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$1.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$1.class new file mode 100644 index 0000000000000000000000000000000000000000..0c14ff43579a7621e8001f96cb5af00a550af1cc GIT binary patch literal 1647 zcmb7FZEqVz5PtUjeldyT(l#m366#VXDY>M)*QTv8DRnTWG*J-Br}cSp);{k=w`&W& z<+tz;0I5YFA&ZavDAd`rNgWa?>IdKM%+BmQGtbQWpMU@O3&02Xis8ng2#!Rgy&dj{ zepvED^@HpVBeBPIUzfHrUKH^@$RoDBb-4nj4+&`;ohEdn+M!XXUj~_{| z<{KHR3Pa)l-WenrE}v_jL7E{WeiZ)D2r??4jkCJXaA(HZ2yG-G>%~cU#$uml^i&*$ zk;%Yi*ecKByU?eyx}QfDc^3{|W5|^v;R`tu5yQ>+qMk7Ah)bnCu2g8a%~gA?(6oj@ zP|0F};d(zbfmB{^H1MP{B2ql?8p=fEC4HSikrMO|h2NjLJ>d}_2r^{2eNN&trP#HX zbYqaOp-h#k`ov#+wAh@I`L!^-!wn}tXD~|8C^7p<+E0RX?N|o})+ed6#ZVd_=iTwK z>l7TCvTQ*4Gl5B7N^tvpqho(`3f1JO_;sU_)8TDfv`#Wq^PR&uu(%( zf|j{LjRrkOni@$fWZ}@7TU#Gv?iuEPA%Wp@`le|GTrApm0g2drXKG$tFE0L$tLwkw ztuZpsu=1Vtb7DVR&NSx9?-J^yy%Gl~;+;4=P(%s0=&-({pn*LQjJU~+8BaM$W IZfRs+0Mmt|1ONa4 literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$1$1$1.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$1$1$1.class new file mode 100644 index 0000000000000000000000000000000000000000..9e555c82f380f7cb6515083d0ab96997e3a2adb1 GIT binary patch literal 2111 zcmbVNT~iZR7=F$Y*s!D!qEb)`rAAFc%YwF645A3Ap&?knw3L3GY!1l@n_Xsi!{8k^ z{UiMcov{{r(P4VoAJyr5Hmj!SjA@7Ayyrdpet$gq>+j$H0PrcA4A<+z-WQ&4u5!z@ z+_dF7yRz2u#1{AW{Pd0VZ2M0KW4JfJb*I?i2i&x|Q#02q4PgcOg*Tsyu2(Z_9MaHNs@fK;>Y1 zEnB?IuPB=`&|xs#%5(cZNK)ShsUHw6h& zv))pIj-qTTBegKd^SEH(97Y&o#X@N%j!_1~7r~O-Qa;aAvqSMBE)8G+ml+1u3Y(iN z%S-D|OIvY_GsH>1%qvN%;h>Al**|yvlI3D-|8r8HUnL zez+!mUsQ|I5kAAYj?Afkq9!~to<*)78B8$@{#zlAPZ%zB7Ix)u-xI!1EsC--+eM>f zhF_0qWLn1!ssJ@Clx672WD5yeq0bE5#4U#Np+=Jjb@PF&EelIFxvk?iP4I!L+4?TS zSY|cb4*HB7#eD~P19O;X7>VRFs{9~*d=*1V* zhhnrXYa-xP9&n0IYaa9vdNFmx)X_t4mGBJ=Y>&9=EJOPAC}Er)8Cp^^v{8+&$ESGumV0&!79WxYDdrUkB#7<2uipZp){L$;!fi1BbG7?QqvO2x=mb_=Yf{i}BX+ zEyLtH1$BJSF!2sC9X}E%wSjzkk6IU7a;t=#LuzA6t!71dTfAb^ORDHv+%9uZDn5El zZxA-frYJZE(wFr93V9Y^gBd|M^`h=pUwx$EN)#MNcuO{?^(1y^#j$MHCoF1-k(a1o!|ovQ>lp)d_*+&9M_)X zz1Y)IDNPu%tnm*Fy6tGS!E13}ON!god{%7C~P}!g%Jej1}U@ z6&~Udg|bU`orV)hy42{um#TC`lxDp^&ng?GtFke2Q@`R`?l%;bKN3Zq&Kyzo6&NLW zYt!!{`a?u{L?zJ}N$sFPH;t0s3uy&5(fj0YjjWY+&{M#A0Z$6}5*shMO~q}g4@)7?B`NANg literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$1$1.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$1$1.class new file mode 100644 index 0000000000000000000000000000000000000000..7c272acd7062c31f4cbaf04cc57e25eb4020c87a GIT binary patch literal 2054 zcmb7E>rNX-6#iy0tXbS(oF*k9H-b})9ay001x!gOA;pP-6sXgb-p1>R8CcJv-7!l4 z@hp9VN=+MDsY3nPhpKvJae@<&0OMyv299>vL6iSE^gW)5Y22Pvs!2yY-OQeBtsP z>CSmt2CB%Aep}l{g5ko6;x@V%Z1G%pO)W?$cihiNg<+~qS&K+Zifk)R#nmPQH*EUi zRdy8__}s*@N{==ihS^+uuxB$+9I2#`M9P7M_ZYgfq40#<6(Pe=Y;{ZMYRILET&`50 zW6zPh9O&hy?-!HkWw^E-XkRLKYq#M_rA4T?@0OGfNlSW>jSQvkZ3=I@rS_PIydg-C zVfciSv=wR17^%lWzK;tI&f_9O*V4lJ;>!Ib`Wc)^=mlRigwk)-w@vjj271tgD znn7SFp2_{UB%?^w%2LH`IzcjpB4!L|K4<92 znYNALu7gh8W9TclP_!g8uW`*O0IRXvL92_IBVmpXdaK57U|`$C)g}l~2YV^!V9&AA zmSgo>bWI=ZUAD$q-WsoL39R9ZGfN$tu=();U$F?Y0sXcJRSH-p;ij|8YxNhuM|`Cmu)>$fcIg%AvBsX?AboTl#wO&?-K`r^JEa_!*|i{|4P{ zLKxC?`HudbRH8kiBr#3TCWQggHJI$p7hYg6fBOYy$@`pfO7i%g&OFi4nD37r0sVN~ z;@4ZW(xN{QP0-g>{75%r`lpya0VjHooPI&p<~Pt$!u=8!N_c=TUvZ0uE1Bcr23Ukg RC~qJUVJX6LgsBM0{{h6H7B>I@ literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$1.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$1.class new file mode 100644 index 0000000000000000000000000000000000000000..69721e93b72d14eadce5b5795ce6611dce8c066e GIT binary patch literal 1934 zcmbVMZByGu5PnWJmJtSwlhA}VA%JPX&?wOKEhe;tlH$~m6sS8LK6DQGVh&`9(Vdv| z8^5LhpfhdLmzj>IAN!*^-IJAInq){S&FtzP?d?9dx4VD;^ZTCwZeyEaYG3$AB2w-H z?uA}h_QK$?Y{ik-;p$M87t0If1sjZEcWLKN{eT~H*XKdYeYkfZJYD_zCD`(dW?dxK z(sv)nfv05{xcB607^t`{qAEl2zn*Pm8LlSitG3z5G32=Ci6hN$W9C(1d-={b&cxc* zkqlZk9EOoo2DLzomWXT=7-o9icFn6jfl|}zY@>iYh8$QJX6RS46>v>iWEg7mCmT{J z(X7irD9U&}j9Tu2(0dV=fpU2egqoX_D0ef|o3Za#^B7^cb{J}32JXRe+m%Eq3b^l5 zK%%Uqr)*qexa92%?=ao=h)29F$ne~3qz5sq-@;`FqqvesiQ!^d?@RUFu?+o$(N^pm z%4-SbR>YgWaJ5jHlI9*H1>6~=S1-yf+{gk0=4AZYscBb&I z(+w}lK}8BFQ$Bk8Ur6=h?ZVYTPi|7B|921C{yiD74^_w1*^sxc1Ww{0>jY>N|29Km! zr(k(24zz5GT5v3tB>Z`UuAKHf%_B2)C&9`Y2a4&WlCUi{_4sD>S$I39Zp;8X0Ws+!f35qEdN6qfPnF7gVWDXb`rB(ju9 H0*C$uxjg#3 literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$2$1.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$2$1.class new file mode 100644 index 0000000000000000000000000000000000000000..56923e748c5688d992bd1632146f788f186c4682 GIT binary patch literal 1884 zcmbVNZBr9h6n^d|upvnWQbka!MPkj1vVhfEgIEl)hK68COiSs@TsGHm!@e-Pn+*NN zm;RCdgHC5E+L;d1kNr`dp1X-OgUm?#Veh%;+;i@E&fDF;{(k)jfRFJ5!;Kwb?Fr8} zR=Me#ZrXI67qZpyM4kJ)etIE2uRa}&;n8CKL8Z;R+_1RQGS;`-!VC&aZ;L9f*D_ij zZ(73Od(v1o1L-;ihOv{@I-(30PF2@2%%F>XVRiyRN}0F)WE%|g1IB^y14+o*VJJqR zA~3v;CEi3=k+uwU#2M~o20@<9u#jye5koQ#4ev3Gq&;B@*%cncOn6&c1lt~$j&E?s zaRaWR_Km6=R6CYch+&lB>aH7D(lOdy+mKEmJcnCG*$F(dlBTaCMWLEI!rbkfJ>wp? z1t~JzJS7DKNvWwzS|P~uxDdztm|%!j%GFW~lMHcR1Vy)_e4fbWfZ-)vP9T9R42iXJ ztyWqou0OBVW4Ou?Bl`xoIwFY=ds@>B=fi^9JlHWF%hrl8rOhoJGgP^*(p`VSFqK)& z_F$gSDj$l-kOf#(-KlkyaLIOLY>%o&V8A48x1kk--u}B%{#W z!zXbJ;XdWKT&>lYtHly+7ftGuP;0!>Z<;j`@Fov9!O-k(ghrUDC90MP$+klo7}zdx z&3T6O(LpkPbP(6Zw752@(RS?U;F31Sa@w5sH2&w$!&*#PHWw(eMeexHkG9+K2^!rp zbB3JgC^Ibmw_$2X*9vE2gGRGIR9~o}x}U@Y+!>)=m0|jw-gP`D}71w<9S^Ecf{#o zqtfiS&ErFAsbtx4gjckG%i#e%wbZ#ax8s@OiBv$Q`aT;y zj{}Y{N4XiI($IU1DU~c${;_3xl}1SNoVaPy(1?!Y@<&MghI79V!_c60m?#j(xO&78 z3Dw(((hkVwQ{#uYm`_c;!Ur1iscWxrooM9Ym7flg*-YgQar+hKHK_Zv@V*q;4kL$i z$YTPxki~6$MW+)XfE*S=cyu@YY3`Emujx#u=EN9nHt8Fpv>Xss6F_1Gf1Id7Ih1>e zPn9bW*uwGg#qeq%@jJB5mstB1>pi6*d_$_&36TmOn8a$Ie%YtBKHVZ3rPB(&rJY96 ze;3j^z9;$_d0(UyKiop3jHNOjl}WbzhI_2IqFPqAz~F$a` zdW7^y+9v6pvq#$WaDHt&6NhG|!}PcIk81jPD+vkPQ`?jzT&d{R3 zvn|Ad=?4}yTL}$G1ycW%SGCQCZHty`>r$7i1gg*krUqM;eKZCWLyK{AAg)0{mq2II zGY!*PH$D0^XDl_W9Z3#mnbA za6~}!&0y9xE2b0N<=+<4$MHlAF?>qk_(SALGOgk%fsWilp*X!TGfO4&4@EW+M<0%L z;wYXLXtS+~MV<0{g42>oLyr`+yyQj!NgS7^e454ydaH=(*jZ7dV$Fw863*RF^2INOZF_IIB7XnFva`9 zoJ_)Jk=2j^E9^j%$Bb*+EKp{2VmOb_3G_B~XxyvFv6z>2f5F_K0<`D(IOdR#;S%O)h7*DhGxztuU_@W zZixI;GhF^0y?%>cS+lFYz+}D5LlI4^iI!4tFrR46ySg^Zur!phA`p}HzG2u^<~n`Z z$~MH6nPj6PYs3*yWCb4Fo5wYeGtCB)R#H!%sOW1a{>RivW&{KSuUUavNm}dt>*ZD&hUx)n`1qHx>2v-P-gN(?Kfl#ALw-)~nsBxQ+Vh)wE_ z>ugrss?WATF2^3&mR0;r;NqSZ{AlyhpK+alm6quQm7GOD0V`=g^KBTW721asRAi*sjm{T0^Ch1*^;q<+F*I zQ*f)EVP3GLVh3u6^X5{uJi8H?oa-*jD^XWvv8G6ie4L_h+881}x{#30l4;OA&8J2u zA;0437%>#Cox>wLh<%8Cf8jyk*Zj6~1vK=?mkA;gUA)Y7AE6(N^!9v&!y~v&T`0{g@I?_5cUdjUnBfYr0^ckx9RHNlJ*w;wMzOme2p%4c=F)$YtYdF zVIAaj301<{F-djS@pVMQ-3B%pssVC%fwvJH$1=XbTN#`W@XauEa`$b#$hf>GRb~OB zo15S9*TziR;!3@?2$rGk;VL88Hhc%)k&Xbxe|Pu#aL5{neGKLL9lY^3yj4SL$M1>y zH1#?f5*?`{9>LG+=XdJs`&^@h{t|!SPNCL+4A*z@C+14DFh6}o+pRclE_|PA!-c-a z?t(Vbm+os~JfMW-zYnCy^xb4}`YdAYKEqF5%>_4ahB$M9Hk{Ye{pr!M)7i9^&W!bE zvSVko^jM}pGkQ8ZHl_{FHZ=+>OB?=w)S{R(| z6^AFQNZ;ie%c|bY`<~*`6M+efVQKOt?2D$#Wh-g)B5lFIC5B`s5RQ;d5ipF$`FDic z3aDymbIG2iURl5RMR8v24K7%m;V zP-PQYqSv+`EOMvLFxKhYe`SbUHjVdFn8FAD3+kK+o8flx{6o#fJ)BKq26GGvO<0EV zDLS4|QTF0Ar8;-Z|q+Y|=IKWrGVXf`djc>_y zj7~Bh2Mtr3^xtqNU>K#2y-90=tY`R+-VDP3eY}?O1Fes!`Xg%Z!X~;3cw9iffX}gb cXg$%^7y7ZZ2^OM&r<+KG_%cK>L@q?{KW_*gI{*Lx literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2.class new file mode 100644 index 0000000000000000000000000000000000000000..ba5386ac14925cc3b0ceab1bc5f915a66207a2d2 GIT binary patch literal 1709 zcmb7FTW=dh6#iyod+m)$979{DKuf4godmi`xW}fgaZ~DGOw~k2C=$|mJ$1%j&sMu* z3*Pcu_y>U0B9M^9BR>lD%-W<5iDdO*XXczam+#zW|M~ZizW{uMuNkf%3;(GIBX^&B zffp3LKs}M&VJHrH)Q^fgHW;D;qs@_ zt$JDps?3o4zqXA8!=($wZKN1%@uTpDT98obyr1xB+Ctp`c2$XkXDndA*fI8d(PxUSr480aIGI`Un;jZ8n{wv5i0Jxb)`enl3rvZPr-Y~!s}1f9`TS51PLHG&3$MrOl#OWW40D3O_>!T`M!qh4i@naLqZ=*^0+n+GBx9J z(a64Rb%x4ZIFs?XsF}!Msv?!tcNi8*rZ3*ZEeA=ID3=wFDjZ*}G^XYLMrhvQnv1+vYd`7&pQ_fI7SEtDcnMdGJ7>s0xvOSw4Q_2fOO!T`b zo2e+7#<-E86XD2A(s#SBsZh0`Lu0+%pb|ERgSH3{dD|yWz7crbZ}CtXdde(59x5#d zqOL|VlEgn9g6hk;qW6WedK`34FR2r8qr)D3NecUfcGHo>Ht<$xHz<-D&#`V?fWT~=USEk#1B-vc z`u;h-_zia_LP<0dVTHWvcL0m>RDy_%*aG#3-sjRHlgcp9$tR<$q9wOw zq;{1jVig7yz+~=3NjOt9y&S<#0e#h-Bz;A2Nj8n8~N6c`04aAU29E)bc=56BWLGAv+FYm_;*I#skSRKe}&{bMBe7W{|(Wr;(VN)_(si{EL zI|mIJZ3)6K6l$R-{B02_BM+4JrfLNurcWKi?6BX{wL23aPmS9Po2QpFFu7_F$Y|Xc zT1yh0&9D=B^06{urv_fP&*h30sx58$jww?r(KV7XL>)5&n7{~0X*?uppRi^!<`xU1 zzhGjiP@r@1B_?~A{uQ@4mR@4+g#U?v9P4zBlTnVo0+SSA7WeQK>2CEM;e85eesi!; zq@aegiQ{-gJVzFfiH`9>KK-nokjmy{K_o%ZkQ2b9=n^$vMi!2-^g0Do52zv&I@bpb zLRRIZY1kSSjn#hu DTa()Z literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Javadoc$configureTask$1.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Javadoc$configureTask$1.class new file mode 100644 index 0000000000000000000000000000000000000000..f3e3553e5071f235ce40de112f63a860f7b1872d GIT binary patch literal 2288 zcmah~TUQ%Z6#mYTGGPcMpsj7SZ76BW#b!W@mj-AtQX32x8>ArKP9}#iFqz4kIYIiy zvp>Us;95$nuBBN%=vw|Lm-|c-2uZLHbLL$3+57wU?fmoaufGGhj~#}ayTYjmKQNd0 z0WW%1((>FLyHxeXIuG`f(>fT#!`bx*xjhmv9qyLQ)xw^zWO}}HE9dzov*h!lBTQbg z%>_%^o||Tfx8K$gVYt*)Tt_d1E)IoNm4bxEI_->aFeKY9OTrtN$_~+J($QfsjE}YB z>MENa--scKm;ns~41GyoSi(LKKEr6JU{A<`&uup_x$An8D{g_g;>ne&DTEY)4qxR#FL3a%RH$2Ed~8YabQ%?mj! z9$F%_q2n!vyh|quD|A7kAZQlIg!B@w=iUpC$@#X3x6n>t#K3jjVo0^bzN!8s^quN;Bq^gdzZ1Ui`BB_q805ubU31+H4B1xi zH;w+UJI_#fY&)W^iX1wIaRaw8L3y9F93BJ}E_X9wVKHl}8djL{50F+wW(lC&wF%%z?iHPjy3Sxy z$)2_}a$LK;?wx>Px&>E9hIK6@9Un1Fb`Ux*x~hfsjH#BBz@mW*c+4=AYqXDLA$gHY zP8_uIK@T-+riO?bdT6f@?ty{r)2cX4-LQ5PGipbM7S{~zibm6T?dX~|#U`{V?TPW{ z54~Dc-7Hcn^v`lC*5_rf8c?;7mccex>iC>t;LJAj-Zg=nb z>|=O_O=@6`DoX833T?(KQpX?6*{)crmJ7mP=LLs$2J(_y`^&u2SP$nsi#r?Kx7B%L zGx@aYO1mtw?tvZHw13iD7f#zXl_WWL*DIbJQZB`^t}Fboz631?7hTKo0_wJPQF;sV zcFEqalGr-NyJ}v;%rE#*!!hQRi5`4KqYiCTM`kp%H%z*FagiX!se27! zmY$cdN$7IjBIfasR=^w{VS%uHLvx+dkHmp*>AOH~JEA{j^T)KU=&8Gko-r}`Gt7w> z_(T~eQE;_OQtA}+{1j-6U;|sQXr>XsV)*|8g8q+4@(JC_ZlfoQr7S+pB8TPXYDKM9 Y)v&e=7T_~H-9{upKEQf_g#gk20E}&ag8%>k literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Javadoc.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Javadoc.class new file mode 100644 index 0000000000000000000000000000000000000000..c510c83abd724eb8d7977487e8ac6bbc2c1d949b GIT binary patch literal 1717 zcmb7EU2hsk6g@LwSk}14)NU|ooy5kDW2ae6(@$Kt4oxInN~7XhO8qn}8yI`pwRQ%c z{nQ`Qr@mHwaN9@~O5ggUs@?(YG@wK&($4*uJLldzXD)yL^T(e6?&A@|e8=<8yhu3* zqAyyZn*?LXoRH!8B4|6`HcvcPClO$QwRQ+Wa<-Pxvcy@ez z)YrW03#HD4?u<%gSQ{oi@pLm1GEj~Pf>00ZR!%+C^`7sS849n9q7c#ay|M+1VdXT` zz6_j`e%FzK_M$-ePBqXGflF5Yp?4O@E;E3aI(K4zF*aHfHx5Pm7k&f=c1tevBrLnI&!(8YkWy`oO?w zn0RZ4p*c8TwRw))JTo}Axn&e?8`b#e8>@qJ>#rYqZg5`WMfM6U*~r zyp+z&I%CCJcqcf!`yCOwLDH3+<@Vy4+6({6;l$n_pclXiKG zdNfrlcBr|yVGGNqSCYGAB}m$aIV@m?tT;X=>j#pC&)jx4{Ts5|&#?I9?gerK!S-xOtQmU@CG*8kCu>3*3H@-jokIU{a??0prvb?pz&SCnys)_YC~W1wQ$D>sNec zK-jQ{OOO~U}UNYYgN6|Ed)JS6!&`M2nfwvQ33;%*i1S8)%{rByW64sD=x45sh_ d?jK`TVO8Nng>{9Qp<*elDSV`mQYa{V{4Xeztt8i#z$ZI8>I{j75+99`ZyFKYNf&+Y}h>j|3=UAy9F*Oz66 zg<0!3q6}Bws2)d*AugT^XQTxwt(=du+GbdvF&>UOo~!!yfRBZ=cg;yTG+akWCE`di zlvZZ(U1(EYZD)``CIt(Z7?SxwIKmx^fZ=ZF=uqfRz+I_qE~T%z$yM1+UpGgdS59D_ z;r5ZQJy+U?k0jB;4$P^R?j6H_+z�(&p;sonZTIfLt1N+XRM48^|I!!6rBK0Yq; zAty{lQ($jWKV>)S)mpPvyT4i7DC*}rfp-{kvn`m75XWtXh!Q%39Fl3=!7{a$Fg7X4 z4DL>%=Nazo_eHJ}hRsb|ww`O#Oj1d66f%ML8KS!HDu!E)3+9z(?NZ+8<)_FgvvY;G zXi~^QY}=Kt-XY{GW(3x8KZQ9I84~qoYroR0*66zJP+vo>Y-8HwU7>lGYfdh#!8k(e z#mo^kM}%aZPzDBeL|nH-T{=C3Dv)!sxKE7 zv%lb4;T3LDC2qaKyB3IfkC=NQW87Gwo6BSx!#r*40cx5JgnnGeDn|ZvEe>3 W$JPOuLK)i!h$?)lu%l2=Nc;_p0G9&* literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$1.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$1.class new file mode 100644 index 0000000000000000000000000000000000000000..6bbed8aa90581ed765be3ad02dca47202aa03f5e GIT binary patch literal 1540 zcmb7EZEq7t5PtUja`xFx9EX=A6v~S!jzc*oEwo@4a6>3A#zi=agz{;9UfG+R_oCag z)qcxw=|7;AN@=Ca>c{@5s=l4GWe1WG7iv!{BiBP-G zgJDTWT=jU^Gv`$sjNw6j^ImI*kGbn})px(|?g%fc-9M>n1!3RqhrH(tm+wip;YBh~ zHHN}*Ya2<1v!}vsq#11SgYZU?AW-?FpXDuv>M`TJQP-Dx;12m%DEFCJDT9{ugwmq3 zCp;St!|n3%7~gAGTi(hegS-O^A2DQ0q40zpi;&@3{Me3&x*?ZJyIiRt;wES9wu7iW z^8H!{vkaGZgUFZ4-5C#EsUi_7?z>GDg=8f?ZKFtudIRC@PR*Y4kPig`8E%|Hz!6G1 z=8=9JQ_=;7OtanDY_!)m7%sNn0j}k-gbP_L;$td`f50Zg(g_-; zr8Zn$!Y4W8aG4<)4Wy=M-tK{VW2R(KrRm!QU;FS9F{JBK$>=^qqHJRL6gM2CP^L`l z9x)$Z&9$awcqSs=;}IwC)^MDlF<@p%nk7NHZma_X+a;;D%21l@=bg#EV-+mNTCnI? znCvfERaUX8))VKipVC&wSoYQ!a&@i(_2V!YX^Mwl$sHls##f{`GGh#t6J(y=lW~pj zp=KJfZ0d3?ODA%XrbfTD8xh~@L61gewk4HlkA__lZt|{Anqn*PxWC0iY2>N0^kSqU zITTGbmRgeju<08A_c6^bu{8*KhnG~Ld{ZeAuKQeTLE`L&^87$k8y-MuNIJ5wcr*&h zx6=tmp(h?o6UgG!=hlJ8BaTp|WKzTyeR8PXC_7V;LW4#xOPU5rR|sLzo~^7*koy%g zuSj6{hSoIgAn2T#R3u{f(X;W*Rdn(r=x#=VA}@ zxElKhjZ+EN>G4N&XH!F!xL=&Gi iajOXz)&HbbBdwX`^KCFqpZeR|NNU{C_)_D#M&@6o19w&c literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$2$1.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$2$1.class new file mode 100644 index 0000000000000000000000000000000000000000..6e121a664db5c4c008225b0a62b50bd3fd2a45ac GIT binary patch literal 1634 zcmb7EU2hvj6g{(c{7vHo(ljk?6KbdvgEvVFh1#SsNlHx}H{d7|ig+0Bj_iqdXQQ39 zMc(pT_y-7yQjk!@BR>jpXXA#-DMdVZX71g4?>YC&fByCB?*Q)OYla(r;T?-m+k1gu z(h*lK4_)(KFRi7)7#`L4AGVJ8h}#}lJ^NYrNH|e#PB|JwG4%=cRynwPx zBgb&BJjw7sylTq_1!Pdj!NOY%*-|JRAx9!)xD$su5>YqgQfZqj6-3F6|ODM_aC$$2uVIhw^t`m(GA1bH6&BK9r zve?;vy0|a6pTWBfi<9M^43x$#hJ+ST0gK4yaT`kv8RD|zbLkau=NzR(DB2z9O)rL` zQ6s3T6A;P=49Tc3HN(x;8%C1GX}VT^?}?Vfmv3Z4om^(i{oWbz>bE*nlK=jrwqQ zTF9N9=B%QXv#we6ES{ZSw^rGTwQ4=d{qaM}${0^~ogrW6Do`hWFw{hiqAdJtDbjev z@ZkTBbmv$&j&<|N9uH;sgww}Jg&7Mae7BNUZ82H z&rNCNInI!28wCQCL z-)Hgt7<+VY652Xd@!la4P26wd<0d}AgO}2$M*7Sg4-dgKYN#I~sqwkShDJpr^EV9G Bk>>ya literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$2.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$2.class new file mode 100644 index 0000000000000000000000000000000000000000..7e07cf22ea5835f313f6c406318133ca49c8a71a GIT binary patch literal 1550 zcmbVMZEqVz5PtUja`xE}$LWic(w0(}Iu7k6X`xV?wkA!fsq0pC6ba?i`fg=!a^8z> z&lY^kZ{Z&R66Hlg79aUhh}nx9q9jF#54XEB`^?VFGc)(+U%∋4YpqEDlAmFCy)} z3`bQRbLI2MH|KR5jNxH(>wagKPq-U!HE>__cZC->9-LNn!f46MQl)+;$5kW|~ z_4b8l!(q5rI~nDFuxu=E7m-8JfrWP%^3_OqLQX`)a6Nf$SH%5@OQl_|R2XxUw|2W> z+#Lr&BZoPLD|=xaNagNMMy^z`h!hXpwu&RNlAg9vrewXL@b+eAFL}gAf`AM+&miF# zEj{zdAPMpu&O2DZ1%^v+EoybmMuj2Q?)J7?-HlC#%bkA#H;P!or92k#9;NQTVU%I% z6qU1r8@8_CY5@hj&ybFXQd2x{_hCAuYEadq*U8}S|0xthwkeg2A26h9CXNqr(?JF` z%D3qe|H;*SXI6~oBIZ7iIYnTNCMg;SW|p*BQl#rAIxw(3lKeG>>hz%KOb;BZWI0yF zqGM@#uw<>Wy0vOOb^iD+Yvqikzs^u-auup?N8wmgJoHN87|k|5CB?C6W2m1Z^z7bD zDt#Xf(~dP$o$Glzk&7&~{O!G%INu0;8lJh1RH8c`^+mMB`vGanozUaKHjku{XUgiU zv5MtLwADmvN&2JKYyH#BR3l<*82U$-RHI^BDG_Z1Tx&t%+@|t^P*XD=LaASRa-euT zj>xyu3&)Wso=6kO;>_pvp~qv6uu92fh%Ng0P}5O%rXr;l4Pc%$Et0Mg!lFH2Uzwut z6W;xv1conY&C(8n&YO8fDuF+n!53D_^FQO_$`80ah5ZYzSuloHVraO{ z6n=yC^=o|oBOV@tGI&DJGF>kwMwJ<`f-gx<<1;kzlui~U)=u_E_=@&NM8=YkZ<{PveG0?r$p1d*1*6 literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$3.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$3.class new file mode 100644 index 0000000000000000000000000000000000000000..a8a18685fdf99f50e59f573f1855b37380c5a1bd GIT binary patch literal 1750 zcma)6TTdHD6#iy0_?mKQLjoje(zuO*;w2&J1t={4Q3j#yu zPp6R*xcXLj8Uq4p^^@|tMiErHc+cdfKyE1~h*GVHMFjG3u z?Na>0E42=dv@ynREi|=m5G-UcC~#{h zG=Z<(?Owi3UR!xPN5|G>Bs+>gfqQQW_)H4w)=4WN`2nux z@IFQbuC$a{3AJWjNJgMon!FHi6!!e4irn=mWUbBKLIKw?mPa1r0&}s~nQ!?f`COM$ zyW1#xVTUE}YOl?P*a`MrTgDkLj8vJ33~mVAK3|6O*Q9Yvpf6UYfZNFC@geRAjFRtD z)l-3`a!R~_yN8U50*{Uv#c|uRdUDEX{{J|AA}~PjucDxU(xJCWfyuTpyYc)xM3%v{ zK+3fJSYV=h>0q#_vqg(4lBdAbg>+xmjyyQ9=xg6BF)bzAYxnUWhkncnWGc1##&T_C zmExku21-!F)uV1&Q${wWk#ym7dVQQ;VV9I$`eb*|bjY*AO@@Ili<0%bio(6{ejw6+{ z>N9*}=V`8hM<{10eakd(KFd3X*610vX4O>foyOf-vSZ9YR^ry5G{$ zbR*uKtB2jlQ_p-$WbEk9&BGhdT!dK$)6Z=2rNo$x&Q@e-nJ-9|mNja3h;X=Pr=|~( z{|)c`LPem)b$~kv9kve+`VzQ4f|sX9hJVMk={J~QCT_mLM-HfbOy%COGDYRnM6o`d z#ODZ7^gGUnVS*EP12edZ7yLV#kSJ5P6v5Rcd4YAxY4!|;D16VapQ+jBZX1(k8^SPm z%d~&$H6GflEa}fqeqT#m1NlGU{P-G`U-8``s2?v0&GD>F0Qlh4xu&p+=h&d+Fy>du yUd1N&Cq#bEbbhggz6!pqV7`I{EFNoLS#8NKkGCLV!k4#@it%-fl^C-zGXDW&EVy3) literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom.class new file mode 100644 index 0000000000000000000000000000000000000000..287ab9edba3b3c76947eb061a979fab50d5ea2df GIT binary patch literal 1865 zcma)6OH&(15dLNbS|J3MA$E8qM2R8h5m+A1%fe1D4_jEs7%+)L9$xKEq=nTkwYyT4 zZ=CWoa>yyyI0u`gDn&WB{HRpESqV(Qp|XeR?%D2dy1(w3zyAL14*)mty+CJGy8AK+ zjdj1KLkMJd&4y{XrdKf@ly{{S=}6Emj7-lq1DkFF6stf7D()?kZW3E@GdY&Jdk>gV@`BACvy7?4zfuTJ=avjgu zZPX0Mi)7%Lu2Jx!fKiSW>PQP*v8vMAYlW?wfmxGu6u3TGJez&~)Xt^?sW7PwGU!U9 z6WszM|Em%mSpmHwqY}>&7#OYd80=yynH(X4w`b;%6& z+^4x!b~mSz$(fw5{7?T0rFqvAx>dU?w7Yn%##X}p_rbu z{6MDgzCiz}IT}7SEWxZ(&pAeX>nO#u=t;@q--1$9T>w0Ecy&V3zn)(#}v_A z$C6$sLxF*FquBI|yF85<#sBOD>CNi6aZygQaZVp~;6r>YkhWz*x~jin2A?+3Z(e{t zr(^yi^tl-Qb_Z_ZGu3RXmcc?3_KrYXRCQQl*J;A?obq#{_xj7nw$3}A6Wu2As2Z2M zxR*v73M6pe;vI{BT`smR+?tF`+l)+}pw$`)J`h4JZEE2Qpq1l22#7se_Kd*Yql3P* z)~BVl?9oA5ODRQ5E0udk2dO`Q(E5%JX0>T?M^9-le@gQAYIim28~WqwLn^@L8;rx8z@-SyEH=?USGENv7a=GFWuY zFrLEnre6;%x#Fm!`dg^m=kmi0i)mBL^4798%5P{TRpp9h&T2cY1iq#9BYic>jftL) z7q~QWh$}4P%|pDcfkuwTd&kBIjq6S0877Zl9Ab`M&+tjC=VSfzSYOoe3sE(E$8`ug z&r741Z()v);v~8_*1gQE9}oHW34N|Mv%nH})wjWz6=rNwXX~1&mua-~g{08boN-&# zU{JELXdi!$)mO)=g29rXaLqIwTJR~{KM5Wol=AEP9opmPczl2SSL`Sd5$Apkl7Pj% z0kSqm3H0%*Hs!6Bv?(=w4M|yKbdfBt?I2M=selIstmEOawV|v{{=n7_M2JV&-a#tF Ta)>WN+zZhjD|(2P5MTZS{ZGF} literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Test$configureTask$1.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Test$configureTask$1.class new file mode 100644 index 0000000000000000000000000000000000000000..24f602468ba30595b1a92295a5ba409e04e42bc3 GIT binary patch literal 1826 zcma)6OH&(15dLN*5G;!k#BVz!V8s?v@M6b#Sp=KNb}S+=2`Z{moJOlbTC|!~c1Eh? z#&5|#NL4}}hmA(mV}+d_DuKozIaw|gV zj2Fs7zut*Ng{x-fP6~|S!FuI>`G|M98*o{7pH+{9rwbdSqH-A3-Fn1pfpB@-cQ-xl zhqAzs9=1*)$#7w;dJ2;aDe;5wI$DrYb~Mh)K0|iMSQB35dSR>WQ}iuDTkPLcuo+gf z!`RN$FRbi4n1W-&!YspdCK8_TyCPz^5j!~&x*Bm`Dwj(cYHo5=?oOz8IzdpF!W_e5 zGt_}E-J@>H^`#b(h>T&XtKjbi+ORG;C5d8GYA?rG)rRXS0Oyx`z6u1r(1R~ekT(8U*?7;a`) zhP@n`>OLV-41<8$z`U=6>v-433>F!dhn{7OehTk1B-=qpF>H;%9Ai-UpD31bV+Kpe zP#)^FJgB5^hZUFfCT^KTvII~!d;+*we$#kRYtLnj;;Xq~$PEWOyVV#b*L~^h4Tk&( z#n@hMXCaG$cW?YAgc-YZ6x@(@gK! zv3tk1m9}i_vPIW)@A#^<#&Xu0_0<0Rr%7wdSk~?`%&c=6${$-{M-gQ5;+(Ed3J)1( zU)vxI-;dvD0Z) zMO5L{fF!efntRP{-X8D^<1d!{xpJA`9@fK z`ICCbDM=}!c%B5YbC0DLgo>uOuS&aR&#z0aY1=Z`dtoQ?gc&V{D}$K({fMUmVT~qr zf+|m&Ih{76YaTYeNgHdLq)&)jA`Oeq>0G{tnG>A*nHYwzh)&W8IG8sJ4vAR3PNz{{ z&M(aWj!U^;aP1#l%71r)_sqSAr9be2g%hk8KM6dIDO-vio+H5|xtqlT#XXM_{k+(B zhaB?6kz;&_RSLXC_Y^fA(scQnz6q+yF`d-ME{P3xV{5?Jx%{uVmirBNO;A!r*+@yt z`~~H`O*Dy3Ji-p$EW+_DJ|AP3&WEJ=5n0_kK%#_?OZcROPx09c?sLQ4H>7X?rm&7L P4vV6c41-Iudx_u1XEn}47E@i%}^@Eyb1Z4q=tq@0>i zI`4(@vES@QqQ+G#eMFp`giC;Xm>7%ud^?g+gVQ7q+fDMQUo z6_isAb+sD=r8GvV?yXPs;Yx`{8k1C9)$%(XBTUo5W9|orKf{n!n#%@{8tYx1 z##x3FFBGd$6h>*BBYV*kQY(gu!sd~ngGXU1^FA(QkwuB6+|56SYamlzYFkC-&`&XZMqGLL+7DjEZQ%{ixEIjpvPN-VDLu(KD2epX2bLkjLbr zFMYkl5G$D0UdN3L5?EkJm#c1Vxw^7S?4n06xo4U9Rx+|tyGm9RleX|zI% zq}pw7iKxc60@B!((Br`dk9;E!l==Hzsr|Mn%bu@%(jN}-@_&1Qwj)(*J8T>tQnPbq zNtB!hJt+El)xFnq?3ybK;Y;hmi-|*JYBrw$In-Pub_)J$zuH7~jK;g@?so;OyaJ3nqZo4`3rs zK_)Rn*yb^f1)RnMI-NKOL4Q{UF9pM=xJF<$h)>Z2!Wc0R>6f57?GiPwl+-4laiRux zV)iLYCKL$$&BNnUeb+$tPgwP*SpOAY9~dR@4Vl_>Y|;avstjnIXq==f9ua3zAvXK; yKE5S-o1!gIQEqJ_R>tizmdp4ID=(zajkIb$YnxySU*OIr;tFMjFBPsSq@M#=p!~i7 literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Test$configureTask$2.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Test$configureTask$2.class new file mode 100644 index 0000000000000000000000000000000000000000..65bc91ae51836b9103ed7d539b4c58dd0680f23b GIT binary patch literal 1972 zcma)7U0WMP6n+W)oVPQ0zs{%sKm>Io~J$J^TA9fDf_5aIqoW0}+IFRfH<- z_}-qZw*yh-VKaTp1Y@|nR=rc&=N)dl+^gGHjW!V$6~0){J*_P$WN0heBAbI*f-eWSDRI%9WnI z-)Y&>QzG!VYZpBgkd|~p6Vnum(-2OxueQYl-Vy|4xPD5ACxWi%N9vK2@8Fz;w{f2A zomI8;s)-qfM6q0{=F5c*hK17E_*@cmcy|o5xX3WBvsm)$bu!B^HLG?9MII=*#~l^gJxDu8GKeKi;U6W}Id^x(uZF^!8PicCF(KYEwwa$^VkRGxO{7)iH?TO@=*=U67&#rP54`h^&VuZfP*2_!M4S1n=@yiKQu-^fl+ z{fP_NUva4m^9YNX-*EL`5$cca~}~$X7Dj?lN(jKo79G+0DMEg5z5dZoz&Hzl2}Kgb+v0&Ci^?CX8ypM zHcU`#6P>1Svk^ZNp@12DOMDz(p^We7W{{IlqVo~Hr}G}Y`x$Ak?;uvh-6HN4k;mu9 cTtRbRXtJ>b7UDj>+(A6VgAm0Kw?ibJ15%s~DgXcg literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Test.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Test.class new file mode 100644 index 0000000000000000000000000000000000000000..25f3e67e0528905ffe692807581320e99c31c545 GIT binary patch literal 1997 zcmbVMTTdHD6#iyx*!5zAaT)^T($YA^7?L$Ky_(VzH)%~=H!27z;%U6zn1S^!-C5Je z^3)&Fr@mHwpe<5`=v#kO)id6Oa0v+2lIL9ZoNvx`{`vRMzW_W&gCV==dOL2Y>@`M4NH;4EOu_Y`IY@6w+6=@ckg_mqgjk zAZqqJugXxo%8E=x%X6zHOon^gLF7r_-rDWj(vRHG7oJ`BqmaO*qYMl)+;lcwXFEZE zAwto0$&g{X)HpvI)l-^P4bq`bWs%3Ig%K1?WEk$89(>2UsU+>v^6M?62G@eN%P`WAzT50|TW+|f_l=>@2pr+9i%@DgQD#`AE~0t^2+k*7R%Pl4z|4z0h%=(G$lok)W<$h!yjc zSz31*6FO0%Pe>XhI;6XfQ4EljMvbH&iE3MWwvhWBH|7d&(K&m7v6m0`F?onPf8bq? zxdRln;=!@vS90O_lFnfQ)K9@(+R;at!c)?fV@SM5m+PMm6D7J{a~a}zpR+JSzR!r~ zFz^-88>uA5I8rJADZi$FE=33+U%;Y2*Yi)NIsu!gMjj~dWA1fWI$Myx4ksHMB;lA8 z=CShwg3@-m1MrvoSbARm4PR&w*3a!2B!xQZ3S>M+6d0$SkLd~#j%7S2x%eBr;~hCNxeklkC3z`SvHTN?YGy zJk!~P_n;$6HZa7e#^u~J8*@H*Chv8-|Hq#9M=Y35AR~(d$jN30YI4|sflSDq99JOb iBlB|qi%W_O$bbyUfDFih49I{C$bbyUfDFjMkAVxAp(p?V literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream new file mode 100644 index 0000000000000000000000000000000000000000..1904d59660aee8a5c7c0f330d8556b1613e7c77e GIT binary patch literal 4096 zcmeIuJr2S!42EG269fy*#syl2W@%JatjJ0%RQls84Lv`h2M8;}m&MC#D|(c&ZDVd_ z_o(Vt=`}b#Q9tWJ;+Te&sTkrZ9~pAuBz4DpS9_*Zo%5BUjl8%NUh`qcYb<5sKu>Pc z2)*a^_b*?OML}f z+JTsuI4x(>E}HYr|DW5`CS9N$WPJ!sNM7#}e8&+-4zF3Y5JNaTrAQlBSP2gsYTgdt%wxe7~y@X{ne-oJyWj ztdpGM?9I`ny%jZ)6gANlotDgXzwO@~JNKQukR!<&c?~B^!IKa^j>uKnbi0%ZCUPD9 zYQYv0(Ni!>B=}SWVHvQn&4e2OPI4Px~Dq5;gM?m8q$S-izuWdf!&a zcz^seWp6cdQ8tYZ2b#}Vr%LEX4cU!3NMADGpm6u{=Rx_7$z+kn9v@AD)iC(u8}iSUE}Q6 pNkm1djqgn_OlCp!JGg0jW-pi`x7G`}G(Fst;u%=H`9nlj@dNbVz7qfd literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i new file mode 100644 index 0000000000000000000000000000000000000000..61b38d12620aaf992869c3fe7577043145b130a8 GIT binary patch literal 32768 zcmeI(p$fuK7>41&%7S;Gcqd+hV7J)BtW^*!Mw>Q`Mx)hcU*9D-F>3L61!HA{ooD$! ze7sLzk|k-O1PBm_P~aGv-}!SJVS3cbn^6BL(o56`;t3ESK!5-N z0t5&UAkd(|HEgX%_hEyRrU(!qK!5-N0t5&UAV7cs0Ro*8*oB(IESq#HN=&7|K3tTi zUS4_VfB*pk1PBlyK!5-N0t5&Us4ejSp@%AZmegsQ=E(N7M-72@oJafB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs U0RjXF5FkK+009C72t+RM16CduG5`Po literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len new file mode 100644 index 0000000000000000000000000000000000000000..131e265740f37d77b7c4a3676d2a7704ca3e4a29 GIT binary patch literal 8 McmZQz0D%Su009U9fdBvi literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab new file mode 100644 index 0000000000000000000000000000000000000000..4298b5ede431bf34b2500b1091d3b174f5c4576a GIT binary patch literal 4096 zcmeIuu?d4f7=~djO%Vi}G)~|Y>BS8kAp_XCN5&8Wq96!@Ac!03`=bX(3Mcq}I3LeD zEZ%S4Jp<-p4>$4ovO&$-8T;bj3EA6nV`s^=@xQzCJ6HZ6abdw1&Y0qa8IJhI0SoN0 k#1=pJ!wMS&jS8rM3aEezsDKKnfC{L93aEezsK8%=13pVG;{X5v literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream new file mode 100644 index 0000000000000000000000000000000000000000..d06342dd592229787f3a02b4d8be118137d66725 GIT binary patch literal 4096 zcmeIuF%H5o4259^2BbonxCWPC0wIwBb$64e5)|qxZhL;p5I6zb^J4vjJ$#!6*`oGV zyEJvNFS)+P)^;xI6Mk$YO|ZiqHlB1wmJYXNOmjFH#6dEh^3|E=kQR@klJr@J%cTOUx-v4KB$qN=#2> IWMII90L29o4gdfE literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i new file mode 100644 index 0000000000000000000000000000000000000000..cb94abc1b2fb5ace1a63f7e023236a5c281ab42c GIT binary patch literal 32768 zcmeI)v1-Cl6oBD?n_#zYE|oq+!J(5w2cZtS^#y!|EP@2G1l;W4;OJC33NDU5gH!3! zt4wo B7c>9> literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len new file mode 100644 index 0000000000000000000000000000000000000000..131e265740f37d77b7c4a3676d2a7704ca3e4a29 GIT binary patch literal 8 McmZQz0D%Su009U9fdBvi literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab new file mode 100644 index 0000000000000000000000000000000000000000..5fdd38e279d55f773479cc8e9988ddf6fd1f8fd9 GIT binary patch literal 4096 zcmbR3vzw0r2)IB53?{WW literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream new file mode 100644 index 0000000000000000000000000000000000000000..d06342dd592229787f3a02b4d8be118137d66725 GIT binary patch literal 4096 zcmeIuF%H5o4259^2BbonxCWPC0wIwBb$64e5)|qxZhL;p5I6zb^J4vjJ$#!6*`oGV zyEJvNFS)+P)^;xI6Mk$YO|ZiqHlB1wmJYXNW2oiPB zpdc-SE9sy{ZIrP%+8oUL2GaxjFWxO_cwxehVck^aC2gNs(S+P+!3R(y@5uHpGMsRf zI0}U(>FmEfXNkX0t?7fpt{av@XLj-*v}kNfGcIc@7h1`6$&N+-!$Kms2Ql0GT?v8& aIeB35mYhM7a5Q3}pVl-uw%KtR=iCpZC#R4wo B7c>9> literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len new file mode 100644 index 0000000000000000000000000000000000000000..131e265740f37d77b7c4a3676d2a7704ca3e4a29 GIT binary patch literal 8 McmZQz0D%Su009U9fdBvi literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab new file mode 100644 index 0000000000000000000000000000000000000000..70006f07920ea792a9b109a988059c9e8d0309a6 GIT binary patch literal 4096 zcmbR3vzw0r2$(?x3mS5Ee0oOZMnhmU1V%$(Gz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONU^E1X4*>vG Cgb~01 literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream new file mode 100644 index 0000000000000000000000000000000000000000..8544032d7d66de29559ffadb98cb5258d9cdb5d9 GIT binary patch literal 4096 zcmeIuu?heX6hKjN(2rI$#Q$N!srgt~XT>#`OK_6L09B6viIL?n>N0*m)ycd!kM*V5sLu71jGkpY(9nK>y1+fPbgxpD`wf^68mZzt)j+wNFR$x> z!sAdZty5t1s5FqAs@SRryif7!qQ)!x5V#w(?wMqwapcv4bWeL7Fn!`Q0j*vra_aMY z%M@ecQPhfU(E7LT8EfGT8Kt0hsvpXH-4@do%-Du(Lcf%$Gvbm3bN!*F5wZ&LJED}8 zXXGZKrDKINBcdE#3(-OPjEKkl@?-8jedhMAE*E;8E%ZwClML^b%%zie(hc4SSGe~} zU(sf&!Y}h>=@K1YE(*88!toV1nJ;6_Q!s~2pZ}EH(7<}Zuc7!OciQtg-x{#xr^4AY xgr7M%S+wUj4IX2Ioe!+IhI)Y!1IV5rZ7-Gl25vAV_fzl8)^a*oYDhb7_8%Mijw=8F literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i new file mode 100644 index 0000000000000000000000000000000000000000..29fdc6af0590d746252d564f1485229b45208d25 GIT binary patch literal 32768 zcmeIup$&jA5CzaC^*E+s3g*EeYnreRn;a+)n2@oJafB*pk1PBlyK!5-N0t5&UAV7cs I0Rle@H1^^L$p8QV literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i.len new file mode 100644 index 0000000000000000000000000000000000000000..131e265740f37d77b7c4a3676d2a7704ca3e4a29 GIT binary patch literal 8 McmZQz0D%Su009U9fdBvi literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab new file mode 100644 index 0000000000000000000000000000000000000000..81144297e847e784ede281592ed8706964010384 GIT binary patch literal 4096 zcmeIuF{nXN7{Kwvpa@x%g;`k)79~Y*l*|T;fk8$Y4U|kKi;~PH_4LYMdp48d4F;3Z zV0?I=|LM79vRb~c?){y6&+T^lj|;1yVWRftOz3}6cNx=RDU5yd{0&>iUe4IP^yA_A zy|e$yE&uoO!2~1NMEk!iCNO|G-2I6ZagH_YVhhVS!X$p72Y+yX6Zypj#yC_TKd^!g zOyLlHxWvO%{eafT&aV;nh#juy}YT0jeE V0WF{fw15`S0$M-|Xo0sCxC34rcp3lz literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream new file mode 100644 index 0000000000000000000000000000000000000000..07f496f4ca81e2709010792add3ccd486b713810 GIT binary patch literal 4096 zcmeH_&rZWI48}QdKmx`IX^4plVYK7q>=8I^LZXLh_mG$_P?{-j_x#Hc-MUHr^8n7J z`RdR9te%Kb$KN(-`ywQCc%G`DONfwiSL)h#hc$!&?$YyPjHs_W=+{9 z&X~?E>0w=f@%fia%RqpGs<6<;8$UjkN9R)W4+B1T)B!stsE!E=@TEN?IsjjLcmOB8 zT)h%ikHomy-7#^itv59qfBFR{u4z{kc4U}KkR?23CfcFhjY tX8)*0Rq$S%@7`8aPc5A>|3Z!s5CTF#2nYcoAOwVf5D)@FKnVO3*a2S322KC~ literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len new file mode 100644 index 0000000000000000000000000000000000000000..0a6ca48908c79b8dd95d5f4b24232dc0cd2c619b GIT binary patch literal 8 McmZQz00E|Z008O$;Q#;t literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len new file mode 100644 index 0000000000000000000000000000000000000000..fa432244558c364281f5897ea3a81444683a68a6 GIT binary patch literal 8 McmZQz00G7f0043TY5)KL literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at new file mode 100644 index 0000000000000000000000000000000000000000..d960e5cc8a07b913240d7d7fe80b1a02e051e9ef GIT binary patch literal 1279 zcmeH`Jr2S!425%)4h70Uv%vxQQCkU76~v0H#DYK@qa^hFq&+|=Ll?y4<@1Z5oyS>* zAkjn>3NkXw)R#}JIWngc6c%Gqt;n9(SE2eA_m(`)zu1}XuUd7xR!5dJTKgiX- zZ19O%q818m`fy|OoTcG;N<()PuJ_Olv|`I}!wIchQ;VJ3gD*zO-pQ0r!i~8^@DB{- z5;hC#6(C5ESNxNK|2FED<}CSiqZT9yf21$a35&z3B{g302e6M>=)yK6Gb3?tIbNUv literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i new file mode 100644 index 0000000000000000000000000000000000000000..914ae9cf3d9daa9dff68a2a8b498191ebf1c1946 GIT binary patch literal 32768 zcmeI*KS%;$9LDjBmWGO!prOE`!NI^K2wED02r3YS3JO9Z3ThD|f|kGnAqb+WjQ$Wt zVYCERv;;RaG**+e)FjbVB+@ekA}->2a-Lt8pLhB`@B91QaEGU8bQtvp4+IdfB(ST| zE78$+OCbzK009ILK%htgPn}`xz1%MORrgf~RAZ_|4(DwMAbGe5s zaV*NZfR69R<3z@K5DAqKn9}je-b^l5VlL)D009ILKmY-E3V3xrah5*+ac6h#?JB0$ zqUpI#_jX8U2q1s}0tmQAAfi^9`(In#(-EB@P$+P%UCVdj&B9mMg8%{u*ehTvzGrM^ z+um77M*sl?5Gc1mn?9F5x@uZ4cP@DdAbDl9Ur>uu{Ab>#W0x1ps-8mWhR?VoEp2SE55I`Vb;8~mLfc5*pTgtiU>dp^m z3jqWWKmY**5U3%6gnIRd;@KK%nN|@%z-EDa<;Qx*SD#c5RBfJ)cmxnY0D*sj27M36 z7Y;oDf&c;tAb|NlDz>7PIU|MvsZ9zZ%4NdNl&|9>u!KJ)GW|4JaO z2Bh17^oB1G_X3%tU^E0qLtr!nMnhmU1V%$(Gz3ONU^E0qLtr!nMnhmU1V%#u7yTek{PI-SMrWzbEoi{e;iJZjH+=qmM!moIT^=x tU0d_`z59Gs`zPn%<8@?L0xd8QfB*y_009U<00Izz00bZa0SNq4;0+kMH;@1T literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len new file mode 100644 index 0000000000000000000000000000000000000000..5024611e4c342043a1ef83a11517021f6f2694f1 GIT binary patch literal 8 LcmZQz0E4Lj0G$Aq literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len new file mode 100644 index 0000000000000000000000000000000000000000..a5413564614472ae2e55bb44e79510947726def0 GIT binary patch literal 8 LcmZQz0D~I<0N?=G literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at new file mode 100644 index 0000000000000000000000000000000000000000..4a5f370c66c1dda44992dd015679c52c6c3e8979 GIT binary patch literal 2930 zcmcIm&2HpG5biiL9*^fIP85+L5C~xf*%jbXPDq20NOoZpIqWEjLJoWB*d0$Nw%h7% zkMaa7#3OJ*D-OBw7F>7)j@;NIFF>_D_IUnwBeX47Ijl)s5e^4%*YY zOZzoLd#P)dcG9+%H??l%;>8)rCIY!SCF@giZAvz#scU>ahcG-JLRqfJA}Smn@JMA zImJ7@3bs13oPLn_G@1#l8IjvW^pnegiUFt=x4zQVt>~BSscS(_@vXx80C29(31S0q zHr6U@TDRJ*ywnX7ZB`83s-0|D#_T~I4z?_Fvz=Lt>>9aU(`tcTd)98ns@>@p#JGxd z$Dx;FwA(a{0}=vhu!J(CiLClW9!_qr?LKs_p&WK1X<$1*vmWZGjeS5~9!B4B>;)tP zPN4U|!O^0_b!GPCR!*8Av@tT8Ia-~#8+hGA?sAXusb0KzobR@+m7KyGj;ikg8xl3yp25N? z;80AER!HQ4dE$(n-q1L$wgtpEp4gqo=*^Mg4}jw_6@qvUjmQB@qHM5ijlIlMnM2g~ zV@|Hn@9~s1Lz{Rvkdb$IK%E`rKjMxBax~ z?KmBm!iHn@U^`SgsiRc86!0Hv#3J;3XF7;q8J%HqBOn-)L%(pTMD%evFC0@_DkWQ;K{pN|JKjTIl$9hf5sG48 zr`L@1vpZEEa+s4A5R_g~HyyC)HDBG+Lr!sc6%`IjXuirO*~NcD^Mko)8h~b{v(&BU zNUoNU9Ab~T1(dA`C;`can#!PYe<(jG12M*Xrl4@qPntN*Ew z;_dWL)*->}n>%D-kAF6|vJ-u>XTV!W)wyw}HzC?qX#%L>T4Xg-m*#v1ub3w@CJg^H=3dY$XM=dwWG1~)!Z)Ic40)MxS%JgT+QPv#x;2!0~;FVE$^ A8~^|S literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i new file mode 100644 index 0000000000000000000000000000000000000000..cd3e6395ac4067781e593fa94c6f57273d6a8578 GIT binary patch literal 32768 zcmeI(A!|ZW7zW@A#-VL(Wn$bv*ss`6SP>CcFN_t-Kn53#iwhIPAi~OEFf78_v>Gio z>xyCDKM=X#!RvGK`Of>jPe0j3R?8d$1WF0K#`IHhw<-0AVFCmQ5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0*M6PBDdIfuM@?KB|v}x0RjXL5a`F<#_j6zfDu;-oSeWkHh%oh zUrugu3lktffB*pk1PCM%m__RQNBucTxKIKF2oNAZfB*pk1PBlyKp?5WDAMmc!>goA z2q!>*009C7&O%@iEho*^S#(vB009C72oNAZfB*pk1PBlyK%lw;pHb~5Z_lgSP%{Dq z2oNAZ;MfAa2>e?2i(}`?tG2)*>g|@lf7NcO9RUIa2oNAZfB*pk1PGKB_=yL04`cs| R%0ikaK!5-N0tEI7`~xN69i#vN literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i.len new file mode 100644 index 0000000000000000000000000000000000000000..131e265740f37d77b7c4a3676d2a7704ca3e4a29 GIT binary patch literal 8 McmZQz0D%Su009U9fdBvi literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab new file mode 100644 index 0000000000000000000000000000000000000000..33f7e47fa1492d4d978a7a3b135843a311402878 GIT binary patch literal 4096 zcmbR3vzw0r2sl6l3 z=@1~D38ZU)G{?XH|Ca*krw%?hODftK|Bfw&7OGzvyTU^E0qLtr!n gMnhmU1V%$(Gz3ONU^E0qLtr!nMnhmU1n`6a0DG||=l}o! literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream new file mode 100644 index 0000000000000000000000000000000000000000..6154bd007f9f8125cb8185c37a6e751085335156 GIT binary patch literal 4096 zcmeIuI}XAy5JgcIV+0Lx#|9FLq)8M-w8*kY$mBD~Hd#Ld3vehYo||c|p5${uXGGn&&Dr$pA2>zA(#6qAge^P``tz{}fB*y_009U<00Izz00jO@ F;0?O0bV2|C literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len new file mode 100644 index 0000000000000000000000000000000000000000..a3d0573b8dcc2547169d53137dafd9a763c5596e GIT binary patch literal 8 McmZQz00G7@003YBSO5S3 literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len new file mode 100644 index 0000000000000000000000000000000000000000..fa606b628b9821e4e1a5e524f5973b5db3fb44ba GIT binary patch literal 8 LcmZQz0D}Vn0LTEr literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at new file mode 100644 index 0000000000000000000000000000000000000000..4530e1cb9572225f18322dfe135fd791d9942401 GIT binary patch literal 920 zcma)5T}#6-6wQNzpw=fr@vCBnFXBkWXCDSKMX~7EtHk8m)pfZeN%w#JWBwM?L1~xK zwNJ^(x#xZyZu(>hT!IeYL7zi zC#fpo)=TzyCd9uH45|p9OT%f>< zRaj|9+)NE+mn}SHEg7Pg3=w5ln`^iT%6c7QD7)?y)v03Cp`z=|Gx&iVR4pvHL{YP6 ziB}z7OYlo}%LFmbj%#;nELW0i>E0IN;19ovjLtVG*PZW?!=l0f7f^hsH+Se%g5TXg#x+R1 literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i new file mode 100644 index 0000000000000000000000000000000000000000..377c853678786946746d36cd6e564d180987804c GIT binary patch literal 32768 zcmeI!p$>vj0ESUE3c)wf`6fPso84k_X02{+7Nbp@Mx)Vcv(tS9aYij(?GyrR+(W%w z{(UErWJ#Jd@SMWta6IcZFl~|m0RjXf6xfI6SN>Q>m>zZVG1R~B>F>A0Lx2DQ0t5&U zAV7cs0RlA$tir}}aP8MHWr6?!0t5&UAV7cs0RjXF5FpS>fo-VTO|nrd<3yDTY{E%# z=;Wm@I3PfP009C72oNAZfB*pk1S$)h!_CEW-mUxr7o!xogzC?uXGGn&&Dr$pA2>zA(#6qAge^P``tz{}fB*y_009U<00Izz00jO@ F;0?O0bV2|C literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len new file mode 100644 index 0000000000000000000000000000000000000000..a3d0573b8dcc2547169d53137dafd9a763c5596e GIT binary patch literal 8 McmZQz00G7@003YBSO5S3 literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len new file mode 100644 index 0000000000000000000000000000000000000000..fa606b628b9821e4e1a5e524f5973b5db3fb44ba GIT binary patch literal 8 LcmZQz0D}Vn0LTEr literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at new file mode 100644 index 0000000000000000000000000000000000000000..6a3671f42dc73a4e10c7b13921587fe18801003a GIT binary patch literal 97 zcmdOA@JLNeNi9+cN=?o$N>OmjFH#6dEh^3|E=kQR@klJr@J%cTOUx-v4KB$qN=#2> aVE_Rz$p|Kyz$7!6WC4?`V3G|?vI77Ok`h1w literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i new file mode 100644 index 0000000000000000000000000000000000000000..377c853678786946746d36cd6e564d180987804c GIT binary patch literal 32768 zcmeI!p$>vj0ESUE3c)wf`6fPso84k_X02{+7Nbp@Mx)Vcv(tS9aYij(?GyrR+(W%w z{(UErWJ#Jd@SMWta6IcZFl~|m0RjXf6xfI6SN>Q>m>zZVG1R~B>F>A0Lx2DQ0t5&U zAV7cs0RlA$tir}}aP8MHWr6?!0t5&UAV7cs0RjXF5FpS>fo-VTO|nrd<3yDTY{E%# z=;Wm@I3PfP009C72oNAZfB*pk1S$)h!_CEW-mUxr7o!xogzC?G2FT9+WKb)~&nut_x@M-U6 z1%|Q&yw1cdQz0c?{mlHj3A^9T&in4p1QR&G6!tKK9n9fzT3f&ky8mVf6d8~K8IS=P UkO3Kx0U3}18IS=Pkbxfq7d}!YR{#J2 literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream new file mode 100644 index 0000000000000000000000000000000000000000..ce37c138ab839b18480398897dbf540eb72b4401 GIT binary patch literal 4096 ucmeIu0Sy2k2mmkyg#L}~;r literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len new file mode 100644 index 0000000000000000000000000000000000000000..d9e6aa615b956553ed1ed6dae0706469d4755209 GIT binary patch literal 8 LcmZQz00RyH01yBP literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len new file mode 100644 index 0000000000000000000000000000000000000000..fa606b628b9821e4e1a5e524f5973b5db3fb44ba GIT binary patch literal 8 LcmZQz0D}Vn0LTEr literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at new file mode 100644 index 0000000000000000000000000000000000000000..37eccfc3f005c3bb9f096a81606619b6ab91679d GIT binary patch literal 407 zcma*jK@Ng26a>&7g#{=sq&qzTXn@2(khnEJ?T=QVpR|?pTQ~r?n$623Go0lJ5>3#c zAR~h-nV?2vm363H4)%S6@d5o8?~V+-Fk#2AZmRN{6O=ezx*qlw4l>0eLNln*aa+ literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i new file mode 100644 index 0000000000000000000000000000000000000000..37f0862ae17450ce536dc425e42f483200cefd4d GIT binary patch literal 32768 zcmeIuD-M7_5JbV@yH`gdkw_$x<89-(0!el9k`be-T21xYmU~Eh8q<;9hcnITN=v%a znx52>5gO?>*=HKIi*+d_Ldr`}_Ihd0y}L>%5;! zc_%Vpv>bNm`p1Z2mq4H&X7~gR{MVdA)BpXKus?qL{_sE`?LWSE`#+7f-hZT-tJ#e! zc|5Dwhf8=m7jYnqc>(7#nX`B$3z)&Fyor;U!->3`<2jvKe3Y4-%~5=oBU#E3e1*eU z#UXr`gILQ1e#$s*WM6*E817(C?q(F*7|Hg+3TGt4cr4ov27@td<*D4o1n%H@Y~(O* z;pN=OQLN_;tmAmr@=n%pDy#Vrt60PeKFu;NU@8B~5-#ODzRlTO$wGe20sZe(S;q#}@<-ONh1L9xRqUX~E7+N3 z?7>q0i6xBXJf6kbOk^Q1W&u+af+e4fKt#vy!- zgILW3zRx(;u@^sQ6t^&vht_I4?&86=U~nH>7}ic+Jc^Bs;ufC3jqJ->p22bs;$mLN z5{}>;Ud5TrWIk_Z9w#z`_b{C^n94shnRA%L=Qx0i7|U1Lhs)TL?=gz2*^!?yz)jqL zAQ=3PEo|aW?qMSjaubhe9|){x7p~!PT*cm8!PEE_2e6Xoa}kHLm{)KvN3)POvVapf zm3MIxr*Q%wW+rDbjn6QJ3pt!GGm(`X$afgeRgC2)?86Q0&TkmO?d-r`*tS0y+|O1X z-XRcZVgws_3^%hEH}Dj$V?5XJT&`jgt9co#n9d4b&oXASly|U%d7Q%sS;Rs<_j$166W)DPUCXs@&o2@ zEwlIqM{_IF_ybee%p@LQBHM+l2XP<=XD?XA##hpZ{PUH#3+2WDa*Si@!3H z0gZnYkK{;p;|LzlMD}38;8b?wLyTY%JMd`+xPbdx^?$Z-DR=U1Zs$ri z@MCV~dT!v?T*qx(!=Jf|d%1##9c8~{M^^G^mNS}*c`_HWKZ|(|=W-}#@lwuY8uNJ_ zr*Ryo@ODn(6i(m+9LJd)!zVeK#Z2RiOkp{P^9>H=3J&In9Kdz#&o9}J4eZSy8O;`U z=Wp!74iWYjc4j;F;K4omKld`0%{+@uOynkB%sQsBme;U`V_D5xS;bsd@IIEYfTet# zC7j23e4eve#zMZv0#->colmvliheTBRG*Acn<@d!Tl}zKU+A5JNX>9a}gW(DmQZ(H}E~K z<7%$qXI#ZiT*2@77MobfJuK%zF6I$VJqy`|#XOF4*_*R?8fS6<^Laj}aX6>&3Qpo^ zPT-9k#|a$6yEvNDn8t^h!dV>7XE>A#IhZeV04v#_hfZbvxQe~`38T4z-T4i>a63El z7q;Vm9^9?}cUF&#U^9T&#rfR97ud)p+``wn zk;_@n4_L>wtmPN1;Z|1j2Uf9}6+FN)w(DyBvlC0$o%47iXR{v*c_s@unA3R?^O(Y1 zUd?+fal{q3Q8dQ|uM5-^xTbLxiy1BV1oP%-WZ-a4 zS-I31HRZTXE>Na{$9-T-v>8Ki14qnM(hxldDChfnes28*^W*gg&*Qv5_kLdQ_ve1^ zmoFGs&+fZi&OZ}UI=Cgc9-FxYxaU@7g3OGH10a87^AO-NpS}e8mPI9kMiD0`*d

+ * The {@code assert} methods here provide simple assertions for strings that are assumed + * to contain JSON. + *

+ * The other methods are factories for type-safe Hamcrest matchers. These are much more + * flexible, as you can use standard Hamcrest combinators like {@code allOf} or {@code not}. + * These use {@link JsonTestValue} as their type parameter, to prevent confusion between + * test code that operates on JSON strings and test code that operates on other kinds of + * strings. {@link JsonTestValue} is easily convertible from strings or other types; + * see {@link JsonTestValue#jsonOf(String)} and {@link JsonTestValue#jsonFromValue(Object)}. + *

+ * Examples: + *


+ *     // check for the exact JSON properties {"a": 1, "b": 2} in any order
+ *     assertThat(jsonOf(myString), jsonEquals("{\"a\":1, \"b\": 2}");
+ *     
+ *     // check that a JSON object's property "p" is equal to a specific boolean value
+ *     assertThat(jsonOf(myString), jsonProperty("p", someBooleanValue));
+ *     
+ *     // check that a JSON object's property "p" is either null or omitted
+ *     assertThat(jsonOf(myString),
+ *         jsonProperty("p", anyOf(jsonNull(), jsonUndefined())));
+ *     
+ *     // check that a JSON object's property "p" is an array containing a specific value
+ *     assertThat(jsonOf(myString),
+ *         jsonProperty("p", isJsonArray(hasItem(jsonEqualsValue(someValue)))));
+ * 
+ *

+ * When comparing unequal JSON objects or arrays, these methods will do their best to + * show you a localized difference such as a specific property, rather than only showing + * the entire actual and expected values. + * + * @since 1.1.0 + */ +public abstract class JsonAssertions { + /** + * Parses two strings as JSON and compares them for deep equality. If they are unequal, + * it tries to describe the difference as specifically as possible by recursing into + * object properties or array elements. + * + * @param expected the expected JSON string + * @param actual the actual JSON string + * @throws AssertionError if the values are not deeply equal, or are not valid JSON + */ + public static void assertJsonEquals(String expected, String actual) { + assertThat(jsonOf(actual), jsonEquals(jsonOf(expected))); + } + + /** + * Equivalent to {@link #assertJsonEquals(String, String)}, but as a typed matcher. + * + * @param expected the expected JSON value + * @return a matcher + */ + public static Matcher jsonEquals(final JsonTestValue expected) { + checkNotNull(expected, "expected"); + return new TypeSafeDiagnosingMatcher() { + @Override + public void describeTo(Description description) { + description.appendText("JSON is equal to: " + expected); + } + + @Override + protected boolean matchesSafely(JsonTestValue actual, Description mismatchDescription) { + if (!actual.isDefined()) { + if (!expected.isDefined()) { + return true; + } + mismatchDescription.appendValue(actual); + return false; + } + if (!expected.isDefined()) { + mismatchDescription.appendValue(expected); + return false; + } + if (actual.parsed.equals(expected.parsed)) { + return true; + } + String diff = describeJsonDifference(expected.parsed, actual.parsed, "", false); + if (diff == null) { + diff = "expected: " + expected + "\nactual: " + actual.raw; + } else { + diff = diff + "\nfull JSON was: " + actual.raw; + } + mismatchDescription.appendText(diff); + return false; + } + }; + } + + /** + * Equivalent to {@code jsonEquals(JsonTestValue.jsonOf(expected))}. + * + * @param expected the expected JSON as a string + * @return a matcher + */ + public static Matcher jsonEquals(String expected) { + return jsonEquals(jsonOf(expected)); + } + + /** + * Equivalent to {@code jsonEquals(JsonTestValue.jsonFromValue(expected))}. + * + * @param expected a value that will be serialized to JSON and matched + * @return a matcher + */ + public static Matcher jsonEqualsValue(Object expected) { + return jsonEquals(jsonFromValue(expected)); + } + + /** + * Same as {@link #assertJsonEquals(String, String)} except that it allows any JSON + * objects in the actual data to contain extra properties that are not in the expected + * data. + * + * @param expected the expected JSON string + * @param actual the actual JSON string + * @throws AssertionError if the expected values are not a subset of the actual + * values, or if the strings are not valid JSON + */ + public static void assertJsonIncludes(String expected, String actual) { + assertThat(jsonOf(actual), jsonIncludes(expected)); + } + + /** + * Equivalent to {@link #assertJsonIncludes(String, String)}, but as a Hamcrest matcher. + * + * @param expected the expected JSON object properties + * @return a string matcher + */ + public static Matcher jsonIncludes(JsonTestValue expected) { + checkNotNull(expected, "expected"); + return new TypeSafeDiagnosingMatcher() { + @Override + public void describeTo(Description description) { + description.appendText("includes these JSON properties: " + expected); + } + + @Override + protected boolean matchesSafely(JsonTestValue actual, Description mismatchDescription) { + if (!actual.isDefined()) { + if (!expected.isDefined()) { + return true; + } + mismatchDescription.appendValue(actual); + return false; + } + if (!expected.isDefined()) { + mismatchDescription.appendValue(expected); + return false; + } + if (isJsonSubset(expected.parsed, actual.parsed)) { + return true; + } + String diff = describeJsonDifference(expected.parsed, actual.parsed, "", true); + if (diff == null) { + diff = "expected: " + expected + "\nactual: " + actual.raw; + } else { + diff = diff + "\nfull JSON was: " + actual.raw; + } + mismatchDescription.appendText(diff); + return false; + } + }; + } + + /** + * Equivalent to {@code jsonIncludes(JsonTestValue.jsonOf(expected))}. + * + * @param expected the expected JSON as a string + * @return a matcher + */ + public static Matcher jsonIncludes(String expected) { + return jsonIncludes(jsonOf(expected)); + } + + /** + * A matcher that verifies that the input value is a JSON null. This is equivalent to + * {@code jsonEquals(JsonTestValue.jsonOf("null"))}. + * + * @return a matcher + */ + public static Matcher jsonNull() { + return jsonEquals(jsonOf("null")); + } + + /** + * A matcher that verifies that the input value is completely undefined (as opposed to + * being a JSON null). + * + * @return a matcher + */ + public static Matcher jsonUndefined() { + return new TypeSafeDiagnosingMatcher() { + @Override + public void describeTo(Description description) { + description.appendText("is undefined"); + } + + @Override + protected boolean matchesSafely(JsonTestValue item, Description mismatchDescription) { + if (item == null || !item.isDefined()) { + return true; + } + mismatchDescription.appendText("had value: " + item.raw); + return false; + } + }; + } + + /** + * A matcher that verifies that the input value is an object that has a property with + * the specified name, and that the property value matches the specified matcher. + * + * @param name the property name + * @param matcher a matcher for the property value + * @return a matcher + */ + + public static Matcher jsonProperty(final String name, final Matcher matcher) { + checkNotNull(name, "name"); + checkNotNull(matcher, "matcher"); + return new TypeSafeDiagnosingMatcher() { + @Override + public void describeTo(Description description) { + description.appendText(String.format("property \"%s\": ", name)); + matcher.describeTo(description); + } + + @Override + protected boolean matchesSafely(JsonTestValue actual, Description mismatchDescription) { + if (!actual.isDefined()) { + mismatchDescription.appendValue(actual); + return false; + } + if (actual.parsed instanceof JsonObject) { + JsonTestValue propValue = JsonTestValue.ofParsed(((JsonObject)actual.parsed).get(name)); + if (!matcher.matches(propValue)) { + matcher.describeMismatch(propValue, mismatchDescription); + return false; + } + return true; + } + mismatchDescription.appendText("not a JSON object: ").appendText(actual.raw); + return false; + } + }; + } + + /** + * A shortcut for using {@link #jsonProperty} with {@link #jsonEquals(JsonTestValue)}. + * + * @param name a property name + * @param value the desired value + * @return a matcher + */ + public static Matcher jsonProperty(String name, JsonTestValue value) { + return jsonProperty(name, jsonEquals(value)); + } + + /** + * A shortcut for using {@link #jsonProperty(String, JsonTestValue)} with + * {@link JsonTestValue#jsonFromValue(Object)}. + * + * @param name a property name + * @param value a value that will be converted to JSON + * @return a matcher + */ + public static Matcher jsonProperty(String name, boolean value) { + return jsonProperty(name, jsonFromValue(value)); + } + + /** + * A shortcut for using {@link #jsonProperty(String, JsonTestValue)} with + * {@link JsonTestValue#jsonFromValue(Object)}. + * + * @param name a property name + * @param value a value that will be converted to JSON + * @return a matcher + */ + public static Matcher jsonProperty(String name, int value) { + return jsonProperty(name, jsonFromValue(value)); + } + + /** + * A shortcut for using {@link #jsonProperty(String, JsonTestValue)} with + * {@link JsonTestValue#jsonFromValue(Object)}. + * + * @param name a property name + * @param value a value that will be converted to JSON + * @return a matcher + */ + public static Matcher jsonProperty(String name, double value) { + return jsonProperty(name, jsonFromValue(value)); + } + + /** + * A shortcut for using {@link #jsonProperty(String, JsonTestValue)} with + * {@link JsonTestValue#jsonFromValue(Object)}. + * + * @param name a property name + * @param value a value that will be converted to JSON + * @return a matcher + */ + public static Matcher jsonProperty(String name, String value) { + return jsonProperty(name, jsonFromValue(value)); + } + + /** + * A matcher that verifies that the input value is an array whose elements match the + * specified matchers. + * + * @param elementsMatcher a matcher for the contents of the array + * @return a matcher + */ + public static Matcher isJsonArray(final Matcher> elementsMatcher) { + checkNotNull(elementsMatcher, "elementsMatcher"); + return new TypeSafeDiagnosingMatcher() { + @Override + public void describeTo(Description description) { + description.appendText("JSON array: "); + elementsMatcher.describeTo(description); + } + + @Override + protected boolean matchesSafely(JsonTestValue actual, Description mismatchDescription) { + if (!actual.isDefined()) { + mismatchDescription.appendValue(actual); + return false; + } + if (actual.parsed instanceof JsonArray) { + List values = new ArrayList<>(); + for (JsonElement element: (JsonArray)actual.parsed) { + values.add(JsonTestValue.ofParsed(element)); + } + if (!elementsMatcher.matches(values)) { + elementsMatcher.describeMismatch(values, mismatchDescription); + return false; + } + return true; + } + mismatchDescription.appendText("not a JSON array: ").appendText(actual.raw); + return false; + } + }; + } + + private static boolean isJsonSubset(JsonElement expected, JsonElement actual) { + if (expected instanceof JsonObject && actual instanceof JsonObject) { + JsonObject eo = (JsonObject)expected, ao = (JsonObject)actual; + for (Map.Entry e: eo.entrySet()) { + if (!ao.has(e.getKey()) || !isJsonSubset(e.getValue(), ao.get(e.getKey()))) { + return false; + } + } + return true; + } + if (expected instanceof JsonArray && actual instanceof JsonArray) { + JsonArray ea = (JsonArray)expected, aa = (JsonArray)actual; + if (ea.size() != aa.size()) { + return false; + } + for (int i = 0; i < ea.size(); i++) { + if (!isJsonSubset(ea.get(i), aa.get(i))) { + return false; + } + } + return true; + } + return actual.equals(expected); + } + + private static String describeJsonDifference( + JsonElement expected, + JsonElement actual, + String prefix, + boolean allowExtraProps + ) { + if (actual instanceof JsonObject && expected instanceof JsonObject) { + return describeJsonObjectDifference((JsonObject)expected, (JsonObject)actual, prefix, allowExtraProps); + } + if (actual instanceof JsonArray && expected instanceof JsonArray) { + return describeJsonArrayDifference((JsonArray)expected, (JsonArray)actual, prefix, allowExtraProps); + } + return null; + } + + private static String describeJsonObjectDifference( + JsonObject expected, + JsonObject actual, + String prefix, + boolean allowExtraProps + ) { + List diffs = new ArrayList<>(); + Set allKeys = new HashSet<>(); + for (Map.Entry e: expected.entrySet()) { + allKeys.add(e.getKey()); + } + for (Map.Entry e: actual.entrySet()) { + allKeys.add(e.getKey()); + } + for (String key: allKeys) { + String prefixedKey = prefix + (prefix == "" ? "" : ".") + key; + String expectedDesc = null, actualDesc = null, detailDiff = null; + if (expected.has(key)) { + if (actual.has(key)) { + JsonElement actualValue = actual.get(key), expectedValue = expected.get(key); + if (!actualValue.equals(expectedValue)) { + expectedDesc = expectedValue.toString(); + actualDesc = actualValue.toString(); + detailDiff = describeJsonDifference(expectedValue, actualValue, prefixedKey, allowExtraProps); + } + } else { + expectedDesc = expected.get(key).toString(); + actualDesc = ""; + } + } else if (!allowExtraProps) { + actualDesc = actual.get(key).toString(); + expectedDesc = ""; + } + if (expectedDesc != null || actualDesc != null) { + if (detailDiff != null) { + diffs.add(detailDiff); + } else { + diffs.add(String.format("at \"%s\": expected = %s, actual = %s", prefixedKey, + expectedDesc, actualDesc)); + } + } + } + return Joiner.on("\n").join(diffs); + } + + private static String describeJsonArrayDifference( + JsonArray expected, + JsonArray actual, + String prefix, + boolean allowExtraProps + ) { + if (expected.size() != actual.size()) { + return null; // can't provide a detailed diff, just show the whole values + } + List diffs = new ArrayList<>(); + for (int i = 0; i < expected.size(); i++) { + String prefixedIndex = String.format("%s[%d]", prefix, i); + JsonElement actualValue = actual.get(i), expectedValue = expected.get(i); + if (!actualValue.equals(expectedValue)) { + String detailDiff = describeJsonDifference(expectedValue, actualValue, prefixedIndex, allowExtraProps); + if (detailDiff != null) { + diffs.add(detailDiff); + } else { + diffs.add(String.format("at \"%s\": expected = %s, actual = %s", prefixedIndex, + expectedValue.toString(), actualValue.toString())); + } + } + } + return Joiner.on("\n").join(diffs); + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/JsonTestValue.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/JsonTestValue.java new file mode 100644 index 0000000..ce81f68 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/JsonTestValue.java @@ -0,0 +1,77 @@ +package com.launchdarkly.testhelpers; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; + +/** + * A simple wrapper for a string that can be parsed as JSON for tests. + *

+ * This class provides strong typing so that it is clear when test matchers apply to JSON + * values versus strings, and hides the implementation details of parsing and serialization + * which are not relevant to the test logic. + * + * @see JsonAssertions + * @since 1.1.0 + */ +public final class JsonTestValue { + private static final Gson gson = new Gson(); + + final String raw; + final JsonElement parsed; + + private JsonTestValue(String raw, JsonElement parsed) { + this.raw = raw; + this.parsed = parsed; + } + + /** + * Creates a {@code JsonTestValue} from a string that should contain JSON. + *

+ * This method fails immediately for any string that is not well-formed JSON. However, if + * it is a null reference, it returns an "undefined" instance that will return {@code false} + * from {@link #isDefined()}. + * + * @param raw the input string + * @return a {@code JsonTestValue} + * @throws AssertionError for malformed JSON + */ + public static JsonTestValue jsonOf(String raw) { + if (raw == null) { + return new JsonTestValue(null, null); + } + try { + return new JsonTestValue(raw, gson.fromJson(raw, JsonElement.class)); + } catch (Exception e) { + throw new AssertionError("not valid JSON (" + e + "): " + raw); + } + } + + static JsonTestValue ofParsed(JsonElement json) { + return new JsonTestValue(json == null ? null : gson.toJson(json), json); + } + + /** + * Creates a {@code JsonTestValue} by serializing an arbitrary value to JSON. For + * instance, {@code jsonFromValue(true)} is equivalent to {@code jsonOf("true")}. + * + * @param value an arbitrary value + * @return a {@code JsonTestValue} + */ + public static JsonTestValue jsonFromValue(Object value) { + return ofParsed(gson.toJsonTree(value)); + } + + @Override + public String toString() { + return raw == null ? "" : raw; + } + + /** + * Returns true if there is a value (that is, the original string was not a null reference). + * + * @return true if defined + */ + public boolean isDefined() { + return raw != null; + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/TempDir.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/TempDir.java new file mode 100644 index 0000000..499e3ca --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/TempDir.java @@ -0,0 +1,115 @@ +package com.launchdarkly.testhelpers; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; + +/** + * Provides a temporary directory for use by a test. The {@link #close} method deletes + * the directory, so a try-with-resources block will ensure that it is cleaned up. + *

+ * All methods that could cause an IOException will throw it as a RuntimeException + * instead, so tests do not need to catch IOException. + * + *


+ *     try (TempDir dir = TempDir.create()) {
+ *         TempFile f = dir.tempFile(".txt");
+ *         f.setContents("test data");
+ *     }
+ * 
+ * + * All IOExceptions are rethrown as RuntimeExceptions so that the test code does + * not need to catch or declare them. + * + * @see TempDir + * @since 1.1.0 + */ +public final class TempDir implements AutoCloseable { + private final Path path; + + private TempDir(Path path) { + this.path = path; + } + + /** + * Creates a temporary directory. + * + * @return a directory object + */ + public static TempDir create() { + try { + return new TempDir(Files.createTempDirectory("java-sdk-tests")); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns the directory path. + * + * @return a path + */ + public Path getPath() { + return path; + } + + /** + * Calls {@link #delete()} if the directory still exists. + */ + @Override + public void close() { + if (Files.exists(path)) { + delete(); + } + } + + /** + * Deletes the directory and all its contents. + */ + public void delete() { + try { + Files.walkFileTree(path, + new SimpleFileVisitor() { + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Creates a temporary file within the directory. + * + * @return a file object + */ + public TempFile tempFile() { + return tempFile(""); + } + + /** + * Creates a temporary file within the directory. + * + * @param suffix optional filename suffix, may be empty + * @return a file object + */ + public TempFile tempFile(String suffix) { + try { + return new TempFile(Files.createTempFile(path, "java-sdk-tests", suffix)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/TempFile.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/TempFile.java new file mode 100644 index 0000000..182c476 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/TempFile.java @@ -0,0 +1,94 @@ +package com.launchdarkly.testhelpers; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Provides a temporary file for use by a test. The {@link #close} method deletes + * the directory, so a try-with-resources block will ensure that it is cleaned up. + * + *

+ *     try (TempFile f = TempFile.create(".txt") {
+ *         f.setContents("test data");
+ *     }
+ * 
+ * + * All IOExceptions are rethrown as RuntimeExceptions so that the test code does + * not need to catch or declare them. + * + * @see TempDir + * @since 1.1.0 + */ +public final class TempFile implements AutoCloseable { + private final Path path; + + TempFile(Path path) { + this.path = path; + } + + /** + * Creates a temporary file in the default directory for temporary files. + * + * @return a file object + * @see TempDir#tempFile(String) + */ + public static TempFile create() { + return create(""); + } + + /** + * Creates a temporary file in the default directory for temporary files. + * + * @param suffix optional filename suffix, may be empty + * @return a file object + * @see TempDir#tempFile(String) + */ + public static TempFile create(String suffix) { + try { + return new TempFile(Files.createTempFile("", suffix)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void close() { + if (Files.exists(path)) { + delete(); + } + } + + /** + * Returns the file path. + * + * @return the file path + */ + public Path getPath() { + return path; + } + + /** + * Deletes the file. + */ + public void delete() { + try { + Files.delete(path); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Replaces the file's contents with the specified data (in UTF-8 encoding). + * + * @param content the new content + */ + public void setContents(String content) { + try { + Files.write(path, content.getBytes("UTF-8")); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/TypeBehavior.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/TypeBehavior.java new file mode 100644 index 0000000..ba0c7a0 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/TypeBehavior.java @@ -0,0 +1,170 @@ +package com.launchdarkly.testhelpers; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Test assertions that may be helpful in testing generic type behavior. + * + * @since 1.1.0 + */ +public abstract class TypeBehavior { + /** + * A supplier interface for use {@link #checkEqualsAndHashCode(List)}. + * + * @param the value type + */ + public interface ValueFactory { + /** + * Returns a new instance of the value type. + * + * @return an instance + */ + T get(); + } + + private static class SingletonValueFactory implements ValueFactory { + private final T value; + + SingletonValueFactory(T value) { + this.value = value; + } + + public T get() { + return value; + } + } + + /** + * Creates a simple {@link ValueFactory} that returns the specified instances in order + * each time it is called. After all instances are used, it starts over at the first. + * This is for use with {@link #checkEqualsAndHashCode(List)}. + * + * @param the value type + * @param values the instances + * @return a value factory + */ + @SuppressWarnings("unchecked") + public static ValueFactory valueFactoryFromInstances(T...values) { + AtomicInteger counter = new AtomicInteger(0); + return () -> { + int i = counter.getAndIncrement(); + if (counter.get() >= values.length) { + counter.set(0); + } + return values[i]; + }; + } + + /** + * Creates a simple {@link ValueFactory} that returns the sameinstance each time it + * is called. After all instances are used, it starts over at the first. This is for use + * with {@link #checkEqualsAndHashCode(List)}, and you should use it instead of + * a lambda like {@code () -> value} whenever the type enforces singleton usage, because + * otherwise {@link #checkEqualsAndHashCode(List)} will expect the return values to be + * equal only value and not by reference. + * + * @param the value type + * @param value the instance + * @return a value factory + */ + public static ValueFactory singletonValueFactory(T value) { + return new SingletonValueFactory<>(value); + } + + /** + * Implements a standard test suite for custom implementations of {@code equals()} and + * {@code hashCode()}. + *

+ * The {@code valueFactories} parameter is a list of value factories. Each factory must + * produce only instances that are equal to each other, and not equal to the instances + * produced by any of the other factories. The test suite verifies the following: + *

    + *
  • For any instance {@code a} created by any of the factories, {@code a.equals(a)} + * is true, {@code a.equals(null)} is false, and {@code a.equals(x)} where {@code x} is + * an instance of a different class is false.
  • + *
  • For any two instances {@code a} and {@code b} created by the same factory, + * {@code a.equals(b)}, {@code b.equals(a)}, and {@code a.hashCode() == b.hashCode()} + * are all true.
  • + *
  • For any two instances {@code a} and {@code b} created by different factories, + * {@code a.equals(b)} and {@code b.equals(a)} are false (there is no requirement that + * the hash codes are different).
  • + *
+ *

+ * If the type uses a singleton/interning pattern so that there can only be one + * instance with a particular value, use {@link #singletonValueFactory(Object)} to + * indicate that that is deliberate; otherwise {@link #checkEqualsAndHashCode(List)} + * will assume that it is a test logic error if it sees the same instance twice. + * + * @param the value type + * @param valueFactories list of factories for distinct values + * @throws AssertionError if a test condition fails + */ + public static void checkEqualsAndHashCode(List> valueFactories) { + for (int i = 0; i < valueFactories.size(); i++) { + for (int j = 0; j < valueFactories.size(); j++) { + T value1 = valueFactories.get(i).get(); + T value2 = valueFactories.get(j).get(); + if (i == j) { + // Here, value1 and value2 are from the same value factory, so we expect them to be equal, + // as follows: + // 1. An instance must be equal to itself. + if (!value1.equals(value1)) { + throw new AssertionError("value was not equal to itself: " + value1); + } + + // In normal usage of checkEqualsAndHashCode, we're testing for value equality (and + // consistent hashing by value) between different instances of T that have the same + // properties, so value1 and value2 should *not* be the exact same object. However, + // some types use a singleton or interning pattern where it's not possible to have + // multiple instances with the same properties; if so, the test logic should tell us + // this by explicitly using singletonValueFactory, and then we will skip that check + // as well as other tests that are for multiple instances (2 & 3 below). + if (!(valueFactories.get(i) instanceof SingletonValueFactory)) { + if (value1 == value2) { + throw new AssertionError("value factory for checkEqualsAndHashCode returned the same" + + " instance twice in a row; if this is intentionally a singleton, you must use" + + " TypeBehavior.singletonValueFactory"); + } + + // 2. Commutative equality: value1.equals(value2) and value2.equals(value1) must + // both be true. + if (!value1.equals(value2)) { + throw new AssertionError("(" + value1 + ").equals(" + value2 + ") was false"); + } + if (!value2.equals(value1)) { + throw new AssertionError("(" + value1 + ").equals(" + value2 + ") was true, but (" + + value2 + ").equals(" + value1 + ") was false"); + } + + // 3. The hashCodes for two logically equal instances must be equal. + if (value1.hashCode() != value2.hashCode()) { + throw new AssertionError("(" + value1 + ").hashCode() was " + value1.hashCode() + " but (" + + value2 + ").hashCode() was " + value2.hashCode()); + } + } + + // 4. An instance of anything is always unequal to null. + if (value1.equals(null)) { + throw new AssertionError("value was equal to null: " + value1); + } + // 5. An instance of T is always unequal to an instance of a class that isn't T. + if (value1.equals(new Object())) { + throw new AssertionError("value was equal to Object: " + value1); + } + } else { + // Here, value1 and value2 are not from the same factory, so we expect them to be + // unequal (regardless of which one we call equals on). Note that we do *not* have a + // similar test for the hashCodes being unequal, because that's not a requirement in + // Java-- collisions are allowed. + if (value1.equals(value2)) { + throw new AssertionError("(" + value1 + ").equals(" + value2 + ") was true"); + } + if (value2.equals(value1)) { + throw new AssertionError("(" + value2 + ").equals(" + value1 + ") was true"); + } + } + } + } + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/Handler.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/Handler.java new file mode 100644 index 0000000..299e3e0 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/Handler.java @@ -0,0 +1,17 @@ +package com.launchdarkly.testhelpers.httptest; + +/** + * An object or lambda that handles HTTP requests for a {@link HttpServer}. + *

+ * Use the factory methods in {@link Handlers} to create standard implementations. + */ +@FunctionalInterface +public interface Handler { + /** + * Processes the request. + * + * @param context a {@link RequestContext} that provides both the request information + * and the ability to modify the response + */ + public void apply(RequestContext context); +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/HandlerSwitcher.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/HandlerSwitcher.java new file mode 100644 index 0000000..4b5df30 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/HandlerSwitcher.java @@ -0,0 +1,40 @@ +package com.launchdarkly.testhelpers.httptest; + +/** + * A delegator that forwards requests to another handler, which can be changed at any time. + */ +public final class HandlerSwitcher implements Handler { + private volatile Handler target; + + /** + * Creates an instance with an initial target. + * + * @param target the handler to delegate to initially + */ + public HandlerSwitcher(Handler target) { + this.target = target; + } + + @Override + public void apply(RequestContext context) { + target.apply(context); + } + + /** + * Returns the current handler that will receive requests. + * + * @return the current target + */ + public Handler getTarget() { + return target; + } + + /** + * Changes the handler that will receive requests. + * + * @param target the new target + */ + public void setTarget(Handler target) { + this.target = target; + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/Handlers.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/Handlers.java new file mode 100644 index 0000000..bfa6a5f --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/Handlers.java @@ -0,0 +1,294 @@ +package com.launchdarkly.testhelpers.httptest; + +import java.nio.charset.Charset; +import java.util.concurrent.Semaphore; + +/** + * Factory methods for standard {@link Handler} implementations. + */ +public abstract class Handlers { + /** + * Creates a {@link Handler} that calls all of the specified handlers in order. + *

+ * You can use this to chain together operations like {@link #status(int)} and + * {@link #header(String, String)}. + * + * @param handlers a series of handlers + * @return a {@link Handler} + */ + public static Handler all(Handler... handlers) { + return ctx -> { + for (Handler h: handlers) { + h.apply(ctx); + } + }; + } + + /** + * Creates a {@link Handler} that sets the HTTP response status. + * + * @param status the status code + * @return a {@link Handler} + */ + public static Handler status(int status) { + return ctx -> ctx.setStatus(status); + } + + /** + * Creates a {@link Handler} that sets a response header. + * + * @param name the header name + * @param value the header value + * @return a {@link Handler} + */ + public static Handler header(String name, String value) { + return ctx -> ctx.setHeader(name, value); + } + + /** + * Creates a {@link Handler} that adds a response header, without overwriting previous values. + * + * @param name the header name + * @param value the header value + * @return a {@link Handler} + */ + public static Handler addHeader(String name, String value) { + return ctx -> ctx.addHeader(name, value); + } + + /** + * Creates a {@link Handler} that sends the specified response body. + * + * @param contentType response content type + * @param body response body (null is equivalent to an empty array) + * @return a {@link Handler} + */ + public static Handler body(String contentType, byte[] body) { + return ctx -> { + ctx.setHeader("Content-Type", contentType); + ctx.setHeader("Content-Length", String.valueOf(body == null ? 0 : body.length)); + if (body != null) { + ctx.write(body); + } + }; + } + + /** + * Creates a {@link Handler} that sends the specified response body. + *

+ * The response is encoded with UTF-8 by default, but "charset" is not added to the Content-Type. + * + * @param contentType response content type + * @param body response body (may be null) + * @return a {@link Handler} + */ + public static Handler bodyString(String contentType, String body) { + return bodyString(contentType, body, null); + } + + /** + * Creates a {@link Handler} that sends the specified response body. + *

+ * If specified, the encoding's name is added to the Content-Type as the "charset". + * + * @param contentType response content type + * @param body response body (may be null) + * @param encoding character encoding; if null, UTF-8 will be used + * @return a {@link Handler} + */ + public static Handler bodyString(String contentType, String body, Charset encoding) { + return body( + encoding == null ? contentType : + (contentType.contains("charset=") ? contentType : contentType + ";charset=" + encoding.name().toLowerCase()), + body == null ? null : + body.getBytes(encoding == null ? Charset.forName("UTF-8") : encoding) + ); + } + + /** + * Creates a {@link Handler} that sends a response body with JSON content type. + * + * @param json the JSON data + * @return a {@link Handler} + */ + public static Handler bodyJson(String json) { + return bodyJson(json, null); + } + + /** + * Creates a {@link Handler} that sends a response body with JSON content type. + * + * @param json the JSON data + * @param encoding character encoding; if null, UTF-8 will be used + * @return a {@link Handler} + */ + public static Handler bodyJson(String json, Charset encoding) { + return bodyString("application/json", json, encoding); + } + + /** + * Creates a {@link Handler} that starts writing a chunked response. + * + *


+   * Handler handler = Handlers.all(
+   *     Handlers.startChunks("text/my-stream-data"),
+   *     Handlers.writeChunkString("data1"),
+   *     Handlers.writeChunkString("data2")
+   * );
+   * 
+ * + * @param contentType the content type + * @param encoding character encoding to include in the Content-Type header, if any + * @return a {@link Handler} + */ + public static Handler startChunks(String contentType, Charset encoding) { + return ctx -> { + ctx.setHeader("Content-Type", encoding == null ? contentType : + (contentType + ";charset=" + encoding.name().toLowerCase())); + ctx.setChunked(); + ctx.write(null); + }; + } + + /** + * Creates a {@link Handler} that writes response data in a chunked response. + * + * @param data the chunk data + * @return a {@link Handler} + */ + public static Handler writeChunk(byte[] data) { + return ctx -> ctx.write(data); + } + + /** + * Creates a {@link Handler} that writes response data in a chunked response. + *

+ * This always uses the default character encoding to conver the string to bytes. To + * use a different encoding, do the conversion yourself and call {@link #writeChunk(byte[])}. + * + * @param data the chunk data + * @return a {@link Handler} + */ + public static Handler writeChunkString(String data) { + return writeChunk(data.getBytes()); + } + + /** + * Creates a {@link Handler} that sleeps for the specified amount of time. + * + * @param delayMillis how long to delay, in milliseconds + * @return a {@link Handler} + */ + public static Handler delay(long delayMillis) { + return ctx -> { + try { + Thread.sleep(delayMillis); + } catch (InterruptedException e) {} + }; + } + + /** + * Creates a {@link Handler} that waits until the specified semaphore is available. + * This can be used to synchronize test logic so that the HTTP response does not + * proceed until signaled to by the test. + * + * @param semaphore the semaphore to wait on + * @return a {@link Handler} + */ + public static Handler waitFor(Semaphore semaphore) { + return ctx -> { + try { + semaphore.acquire(); + } catch (InterruptedException e) { + return; + } + }; + } + + /** + * Creates a {@link Handler} that sleeps indefinitely, holding the connection open, + * until the server is closed. + * + * @return a {@link Handler} + */ + public static Handler hang() { + return ctx -> { + while (true) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + return; + } + } + }; + } + + /** + * Creates a stateful {@link Handler} that delegates to each of the specified handlers in sequence + * as each request is received. + *

+ * Any requests that happen after the last handler in the list has been used will receive a + * 500 error. + * + * @param handlers a series of handlers + * @return a {@link Handler} + */ + public static Handler sequential(Handler...handlers) { + return new SequentialHandler(handlers); + } + + /** + * Shortcut handlers for simulating a Server-Sent Events stream. + */ + public static abstract class SSE { + /** + * Starts a chunked stream with the standard content type "text/event-stream", + * and the charset UTF-8. + * + * @return a {@link Handler} + */ + public static Handler start() { + return startChunks("text/event-stream", Charset.forName("UTF-8")); + } + + /** + * Writes an SSE comment line. + * + * @param text the content that should appear after the colon + * @return a {@link Handler} + */ + public static Handler comment(String text) { + return writeChunkString(":" + text + "\n"); + } + + /** + * Writes an SSE event terminated by two newlines. + * + * @param content the full event + * @return a {@link Handler} + */ + public static Handler event(String content) { + return writeChunkString(content + "\n\n"); + } + + /** + * Writes an SSE event created from individual fields. + * + * @param message the "event" field + * @param data the "data" field + * @return a {@link Handler} + */ + public static Handler event(String message, String data) { + return event("event: " + message + "\ndata: " + data); + } + + /** + * Waits indefinitely without closing the stream. Equivalent to {@link Handlers#hang()}. + * + * @return a {@link Handler} + */ + public static Handler leaveOpen() { + return hang(); + } + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/HttpServer.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/HttpServer.java new file mode 100644 index 0000000..2dbd8b6 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/HttpServer.java @@ -0,0 +1,208 @@ +package com.launchdarkly.testhelpers.httptest; + +import com.launchdarkly.testhelpers.httptest.impl.HttpServerImpl; +import com.launchdarkly.testhelpers.tcptest.TcpServer; + +import java.io.Closeable; +import java.io.IOException; +import java.net.URI; +import java.net.URL; + +/** + * A simplified wrapper for an embedded test HTTP server. + *

+ * See {@link com.launchdarkly.testhelpers.httptest} for more details and examples. + *

+ * The server can be configured with any implementation of {@link Handler} to specify how + * HTTP requests should be handled. However, this is limited to behavior that is valid in + * HTTP. If you want to simulate a server that does not return a valid HTTP response, use + * {@link TcpServer} instead. + */ +public final class HttpServer implements Closeable { + private final Delegate delegate; + private final int port; + private final URI uri; + private final RequestRecorder recorder; + + /** + * An abstraction for the part of the server implementation that could vary by platform. + */ + public interface Delegate extends Closeable { + /** + * Starts the server and returns the port it's listening on. + * + * @return the port + * @throws IOException if starting the server fails + */ + int start() throws IOException; + + /** + * Factory pattern for the server abstraction. + * + * @since 2.0.0 + * @see com.launchdarkly.testhelpers.httptest.impl.HttpServerImpl + */ + public interface Factory { + /** + * Creates the platform-specific server implementation, but does not start it. + * + * @param port the port it will listen on, or 0 to select any available port + * @param handler the request handler + * @param tlsConfig TLS configuration if using TLS, or null + * @return the delegate implementation + */ + Delegate createServerDelegate(int port, Handler handler, ServerTLSConfiguration tlsConfig); + } + } + + private HttpServer(Delegate delegate, int port, URI uri, RequestRecorder recorder) { + this.delegate = delegate; + this.port = port; + this.uri = uri; + this.recorder = recorder; + } + + /** + * Starts a new test server on a specific port. + * + * @param port the port to listen on + * @param handler An object or lambda that will handle all requests to this server. Use + * the factory methods in {@link Handlers} for standard handlers. If you will need + * to change the behavior of the handler during the lifetime of the server, use + * {@link HandlerSwitcher}. + * @return the started server instance + */ + public static HttpServer start(int port, Handler handler) { + return startInternal(port, handler, null); + } + + /** + * Starts a new test server on any available port. + * + * @param handler An object or lambda that will handle all requests to this server. Use + * the factory methods in {@link Handlers} for standard handlers. If you will need + * to change the behavior of the handler during the lifetime of the server, use + * {@link HandlerSwitcher}. + * @return the started server instance + */ + public static HttpServer start(Handler handler) { + return start(0, handler); + } + + /** + * Starts a new HTTPS test server on a specific port. + * + * @param tlsConfig certificate and key data; to use a self-signed certificate, call + * {@link ServerTLSConfiguration#makeSelfSignedCertificate()} + * @param port the port to listen on + * @param handler An object or lambda that will handle all requests to this server. Use + * the factory methods in {@link Handlers} for standard handlers. If you will need + * to change the behavior of the handler during the lifetime of the server, use + * {@link HandlerSwitcher}. + * @return the started server instance + */ + public static HttpServer startSecure(ServerTLSConfiguration tlsConfig, int port, Handler handler) { + return startInternal(port, handler, tlsConfig); + } + + /** + * Starts a new HTTPS test server on any available port. + * + * @param certData certificate and key data; to use a self-signed certificate, call + * {@link ServerTLSConfiguration#makeSelfSignedCertificate()} + * @param handler An object or lambda that will handle all requests to this server. Use + * the factory methods in {@link Handlers} for standard handlers. If you will need + * to change the behavior of the handler during the lifetime of the server, use + * {@link HandlerSwitcher}. + * @return the started server instance + */ + public static HttpServer startSecure(ServerTLSConfiguration certData, Handler handler) { + return startSecure(certData, 0, handler); + } + + private static HttpServer startInternal(int port, Handler handler, ServerTLSConfiguration tlsConfig) { + RequestRecorder recorder = new RequestRecorder(); + Handler rootHandler = ctx -> { + recorder.apply(ctx); + try { + handler.apply(ctx); + } catch (Exception e) { + ctx.setStatus(500); + ctx.write(e.toString().getBytes()); + } + }; + + Delegate delegate = HttpServerImpl.factory().createServerDelegate(port, rootHandler, tlsConfig); + + int realPort; + try { + realPort = delegate.start(); + } catch (IOException e) { + try { + delegate.close(); + } catch (Exception ignore) {} + throw new RuntimeException(e); + } + + return new HttpServer( + delegate, + realPort, + URI.create(String.format("%s://localhost:%d/", + tlsConfig == null ? "http" : "https", realPort)), + recorder + ); + } + + /** + * Returns the server's port. + * + * @return the port + */ + public int getPort() { + return port; + } + + /** + * Returns the server's base URI. + * + * @return the base URI + */ + public URI getUri() { + return uri; + } + + /** + * Returns the server's base URI. + * + * @return the base URI as a URL + */ + public URL getUrl() { + try { + return uri.toURL(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Returns the {@link RequestRecorder} that receives all requests to this server, + * unless you disable it with {@link RequestRecorder#setEnabled(boolean)}. + * + * @return the recorder + */ + public RequestRecorder getRecorder() { + return recorder; + } + + /** + * Shuts down the server. + */ + @Override + public void close() { + try { + delegate.close(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/RequestContext.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/RequestContext.java new file mode 100644 index 0000000..b4106da --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/RequestContext.java @@ -0,0 +1,68 @@ +package com.launchdarkly.testhelpers.httptest; + +/** + * An abstraction used by {@link Handler} implementations to hide the details of + * the underlying HTTP server framework. + */ +public interface RequestContext { + /** + * Returns the {@link RequestInfo}. + * + * @return a {@link RequestInfo} + */ + RequestInfo getRequest(); + + /** + * Sets the response status. + * + * @param status the status code + */ + void setStatus(int status); + + /** + * Sets a response header. + * + * @param name the header name + * @param value the header value + */ + void setHeader(String name, String value); + + /** + * Adds a response header, without overwriting any previous values. + * + * @param name the header name + * @param value the header value + */ + void addHeader(String name, String value); + + /** + * Turns on chunked encoding. + *

+ * It's only valid to call this when {@link #write(byte[])} has not yet been called. After + * {@link #write(byte[])} is called, the behavior of {@link #setChunked()} is undefined. + */ + void setChunked(); + + /** + * Writes data to the output stream. + * + * @param data the data to write; null or zero-length data means to only flush the stream + */ + void write(byte[] data); + + /** + * Returns a path parameter, if any path parameters were captured. + *

+ * By default, this will always return null. It is non-null only if you used + * {@link SimpleRouter} and matched a regex pattern that was added with + * {@link SimpleRouter#addRegex(java.util.regex.Pattern, Handler)}, and the pattern + * contained capture groups. For instance, if the pattern was {@code /a/([^/]*)/c/(.*)} + * and the request path was {@code /a/b/c/d/e}, {@code getPathParam(0)} would return + * {@code "b"} and {@code getPathParam(1)} would return {@code "d/e"}. + * + * @param i a zero-based positional index + * @return the path parameter string; null if there were no path parameters, or if the index + * is out of range + */ + String getPathParam(int i); +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/RequestInfo.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/RequestInfo.java new file mode 100644 index 0000000..befbe61 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/RequestInfo.java @@ -0,0 +1,104 @@ +package com.launchdarkly.testhelpers.httptest; + +import com.google.common.collect.ImmutableMap; + +import java.net.URI; + +/** + * Properties of a request received by {@link HttpServer}. + *

+ * We capture all of the request properties, including the request body, before passing the request + * to the configured handler, because tests often need to record and inspect the request. + */ +public final class RequestInfo { + private final String method; + private final URI uri; + private final String path; + private final String query; + private final ImmutableMap headers; + private final String body; + + /** + * Constructs an instance, specifying all properties. + * + * @param method the HTTP method + * @param uri the URI + * @param path the request path + * @param query the query string + * @param headers the headers + * @param body the body, or null + */ + public RequestInfo(String method, URI uri, String path, String query, + ImmutableMap headers, String body) { + this.method = method.toUpperCase(); + this.uri = uri; + this.path = path; + this.query = query; + this.headers = headers == null ? ImmutableMap.of() : headers; + this.body = body; + } + + /** + * Returns the HTTP method. + * + * @return the HTTP method + */ + public String getMethod() { + return method; + } + + /** + * Returns the full request URI. + * + * @return the request URI + */ + public URI getUri() { + return uri; + } + + /** + * Returns the request path. + * + * @return the path + */ + public String getPath() { + return path; + } + + /** + * Returns the request query string. + * + * @return the query string (including the leading "?"), or null if there is none + */ + public String getQuery() { + return query; + } + + /** + * Returns a request header by name. + * + * @param name a case-insensitive header name + * @return the header value, or null if not found + */ + public String getHeader(String name) { + return headers.get(name.toLowerCase()); + } + + /** + * Returns all request header names. + * + * @return the header names + */ + public Iterable getHeaderNames() { + return headers.keySet(); + } + + /** + * Returns the request body as a string. + * + * @return the request body, or null if there is none + */ + public String getBody() { + return body; + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/RequestRecorder.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/RequestRecorder.java new file mode 100644 index 0000000..412fc11 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/RequestRecorder.java @@ -0,0 +1,108 @@ +package com.launchdarkly.testhelpers.httptest; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * An object that records all requests. + *

+ * Normally you won't need to use this class directly, because {@link HttpServer} has a + * built-in instance that captures all requests. You can use it if you need to capture + * only a subset of requests. + */ +public final class RequestRecorder implements Handler { + /** + * The default timeout for {@link #requireRequest()}: 5 seconds. + */ + public static final int DEFAULT_TIMEOUT_MILLIS = 5000; + + private final BlockingQueue requests = new LinkedBlockingQueue<>(); + private final AtomicBoolean enabled = new AtomicBoolean(true); + + @Override + public void apply(RequestContext context) { + if (enabled.get()) { + requests.add(context.getRequest()); + } + } + + /** + * The number of requests currently in the queue. + * + * @return the number of stored requests that have not been consumed + */ + public int count() { + return requests.size(); + } + + /** + * Returns true if the recorder is capturing requests. This is true by default. + * + * @return true if enabled + */ + public boolean isEnabled() { + return enabled.get(); + } + + /** + * Sets whether the recorder should capture requests. This is true by default. + * + * @param enabled true to enable the recorder, false to disable + */ + public void setEnabled(boolean enabled) { + this.enabled.set(enabled); + } + + /** + * Consumes and returns the first request in the queue, blocking until one is available, + * using {@link #DEFAULT_TIMEOUT_MILLIS}. + * + * @return the request information + * @throws IllegalStateException if the timeout expires + */ + public RequestInfo requireRequest() { + return requireRequest(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + } + + /** + * Consumes and returns the first request in the queue, blocking until one is available. + * + * @param timeout the maximum length of time to wait + * @param timeoutUnit the time unit for the timeout + * @return the request information + * @throws RuntimeException if the timeout expires + */ + public RequestInfo requireRequest(long timeout, TimeUnit timeoutUnit) { + try { + RequestInfo ret = requests.poll(timeout, timeoutUnit == null ? TimeUnit.MILLISECONDS : timeoutUnit); + if (ret == null) { + throw new IllegalStateException(new TimeoutException()); + } + return ret; + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + /** + * Asserts that there are no requests in the queue and none are received within + * the specified timeout. + * + * @param timeout the maximum length of time to wait + * @param timeoutUnit the time unit for the timeout + * @throws IllegalStateException if a request was received + */ + public void requireNoRequests(long timeout, TimeUnit timeoutUnit) { + try { + RequestInfo ret = requests.poll(timeout, timeoutUnit == null ? TimeUnit.MILLISECONDS : timeoutUnit); + if (ret != null) { + throw new IllegalStateException("received an unexpected request"); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/SequentialHandler.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/SequentialHandler.java new file mode 100644 index 0000000..ea4e073 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/SequentialHandler.java @@ -0,0 +1,22 @@ +package com.launchdarkly.testhelpers.httptest; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; + +final class SequentialHandler implements Handler { + private final Handler[] handlers; + private final AtomicInteger index = new AtomicInteger(0); + + SequentialHandler(Handler[] handlers) { + this.handlers = Arrays.copyOf(handlers, handlers.length); + } + + @Override + public void apply(RequestContext context) { + int i = index.getAndIncrement(); + if (i >= handlers.length) { + throw new RuntimeException("server received unexpected request"); + } + handlers[i].apply(context); + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/ServerTLSConfiguration.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/ServerTLSConfiguration.java new file mode 100644 index 0000000..11d205a --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/ServerTLSConfiguration.java @@ -0,0 +1,123 @@ +package com.launchdarkly.testhelpers.httptest; + +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import okhttp3.tls.HandshakeCertificates; +import okhttp3.tls.HeldCertificate; + +/** + * Holds all certificate/key data required to configure an {@link HttpServer} for HTTPS, + * and to configure a client to make requests to that server. + *

+ * This implementation uses OkHttp's {@code okhttp-tls} package. + */ +public final class ServerTLSConfiguration { + private final X509Certificate certificate; + private final PrivateKey privateKey; + private final PublicKey publicKey; + private final SSLSocketFactory socketFactory; + private final X509TrustManager trustManager; + + private ServerTLSConfiguration(X509Certificate certificate, PrivateKey privateKey, PublicKey publicKey, + SSLSocketFactory socketFactory, X509TrustManager trustManager) { + this.certificate = certificate; + this.privateKey = privateKey; + this.publicKey = publicKey; + this.socketFactory = socketFactory; + this.trustManager = trustManager; + } + + /** + * Creates an instance with a self-signed certificate. + *

+ * HTTP clients will normally reject this certificate. To configure a client to accept it, + * use the objects provided by {@link #getSocketFactory()} and {@link #getTrustManager()}. + *

+ * The certificate's hostname is "localhost". It expires in 24 hours. + * + * @return a {@link ServerTLSConfiguration} + */ + public static ServerTLSConfiguration makeSelfSignedCertificate() { + String hostname; + try { + hostname = InetAddress.getByName("localhost").getCanonicalHostName(); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + + HeldCertificate certInfo = new HeldCertificate.Builder() + .rsa2048() + .serialNumber(BigInteger.ONE) + .certificateAuthority(1) + .commonName(hostname) + .addSubjectAlternativeName("localhost") + .build(); + + HandshakeCertificates hc = new HandshakeCertificates.Builder() + .heldCertificate(certInfo) + .addTrustedCertificate(certInfo.certificate()) + .build(); + + return new ServerTLSConfiguration( + certInfo.certificate(), + certInfo.keyPair().getPrivate(), + certInfo.keyPair().getPublic(), + hc.sslSocketFactory(), + hc.trustManager() + ); + } + + /** + * Returns the server certificate. + * + * @return the server certificate + */ + public X509Certificate getCertificate() { + return certificate; + } + + /** + * Returns the private key. + * + * @return the private key + */ + public PrivateKey getPrivateKey() { + return privateKey; + } + + /** + * Returns the public key. + * + * @return the public key + */ + public PublicKey getPublicKey() { + return publicKey; + } + + /** + * Returns an {@link SSLSocketFactory} for use by the client. + * + * @return an {@link SSLSocketFactory} + */ + public SSLSocketFactory getSocketFactory() { + return socketFactory; + } + + /** + * Returns a {@link TrustManager} for use by the client. + * + * @return a {@link TrustManager} + */ + public X509TrustManager getTrustManager() { + return trustManager; + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/SimpleRouter.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/SimpleRouter.java new file mode 100644 index 0000000..7d09d21 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/SimpleRouter.java @@ -0,0 +1,152 @@ +package com.launchdarkly.testhelpers.httptest; + +import com.google.common.collect.ImmutableList; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A delegator that provides simple request path/method matching. + *

+ * The request is sent to the handler for the first matching path. If there is no matching path, it + * returns a 404. If there is a matching path but only for a different HTTP method, it returns a 405. + */ +public final class SimpleRouter implements Handler { + private final List routes = new ArrayList<>(); + + private static class Route { + final String method; + final Pattern pattern; + final Handler handler; + + Route(String method, Pattern pattern, Handler handler) { + this.method = method; + this.pattern = pattern; + this.handler = handler; + } + } + + @Override + public void apply(RequestContext context) { + boolean matchedPath = false; + for (Route r: routes) { + Matcher m = r.pattern.matcher(context.getRequest().getPath()); + if (m.matches()) { + matchedPath = true; + if (r.method != null && !r.method.equalsIgnoreCase(context.getRequest().getMethod())) { + continue; + } + if (m.groupCount() > 0) { + ImmutableList.Builder params = ImmutableList.builder(); + for (int i = 1; i <= m.groupCount(); i++) { + params.add(m.group(i)); + } + context = new RequestContextWithPathParams(context, params.build()); + } + r.handler.apply(context); + return; + } + } + context.setStatus(matchedPath ? 405 : 404); + } + + /** + * Adds an exact-match path. + * + * @param path the desired path + * @param handler the handler to call for a matching request + * @return the same instance + */ + public SimpleRouter add(String path, Handler handler) { + return add(null, path, handler); + } + + /** + * Adds an exact-match path, specifying the HTTP method. + * + * @param method the desired method + * @param path the desired path + * @param handler the handler to call for a matching request + * @return the same instance + */ + public SimpleRouter add(String method, String path, Handler handler) { + return addRegex(method, Pattern.compile(Pattern.quote(path)), handler); + } + + /** + * Adds a regex path pattern. + *

+ * The regex must match the entire path. If it contains any capture groups, the matched groups + * will be available from {@link RequestContext#getPathParam(int)}. + * + * @param regex the regex to match + * @param handler the handler to call for a matching request + * @return the same instance + */ + public SimpleRouter addRegex(Pattern regex, Handler handler) { + return addRegex(null, regex, handler); + } + + /** + * Adds a regex path pattern, speifying the HTTP method. + *

+ * The regex must match the entire path. If it contains any capture groups, the matched groups + * will be available from {@link RequestContext#getPathParam(int)}. + * + * @param method the desired method + * @param regex the regex to match + * @param handler the handler to call for a matching request + * @return the same instance + */ + public SimpleRouter addRegex(String method, Pattern regex, Handler handler) { + routes.add(new Route(method, regex, handler)); + return this; + } + + private static final class RequestContextWithPathParams implements RequestContext { + private final RequestContext wrapped; + private final ImmutableList pathParams; + + RequestContextWithPathParams(RequestContext wrapped, ImmutableList pathParams) { + this.wrapped = wrapped; + this.pathParams = pathParams; + } + + @Override + public RequestInfo getRequest() { + return wrapped.getRequest(); + } + + @Override + public void setStatus(int status) { + wrapped.setStatus(status); + } + + @Override + public void setHeader(String name, String value) { + wrapped.setHeader(name, value); + } + + @Override + public void addHeader(String name, String value) { + wrapped.addHeader(name, value); + } + + @Override + public void setChunked() { + wrapped.setChunked(); + } + + @Override + public void write(byte[] data) { + wrapped.write(data); + } + + @Override + public String getPathParam(int i) { + return i < 0 || i >= pathParams.size() ? null : pathParams.get(i); + } + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/SpecialHttpConfigurations.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/SpecialHttpConfigurations.java new file mode 100644 index 0000000..c41eff4 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/SpecialHttpConfigurations.java @@ -0,0 +1,389 @@ +package com.launchdarkly.testhelpers.httptest; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.URI; + +import javax.net.SocketFactory; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; + +/** + * Testing tools for validating that HTTP client logic is correctly applying configuration parameters. + * The test methods set up a server with a defined behavior, then delegate to a provided test action + * that is expected to succeed or fail depending on the test conditions. + * + * @since 1.3.0 + */ +public class SpecialHttpConfigurations { + /** + * See {@link TestAction}. + */ + public static class Params { + private final ServerTLSConfiguration tlsConfig; + private final SocketFactory socketFactory; + private final String proxyHost; + private final int proxyPort; + private final String proxyBasicAuthUser; + private final String proxyBasicAuthPassword; + + Params(ServerTLSConfiguration tlsConfig, SocketFactory socketFactory, String proxyHost, int proxyPort, + String proxyBasicAuthUser, String proxyBasicAuthPassword) { + this.tlsConfig = tlsConfig; + this.socketFactory = socketFactory; + this.proxyHost = proxyHost; + this.proxyPort = proxyPort; + this.proxyBasicAuthUser = proxyBasicAuthUser; + this.proxyBasicAuthPassword = proxyBasicAuthPassword; + } + + /** + * If the {@link ServerTLSConfiguration#getSocketFactory()} and {@link ServerTLSConfiguration#getTrustManager()} + * properties are non-null, the {@link TestAction} should configure its HTTP client to use these + * values for TLS configuration. + * @return the TLS configuration + */ + public ServerTLSConfiguration getTlsConfig() { + return tlsConfig; + } + + /** + * If this property is non-null, the {@link TestAction} should configure its HTTP client to use it. + * @return a custom socket factory + */ + public SocketFactory getSocketFactory() { + return socketFactory; + } + + /** + * If this property is non-null, the {@link TestAction} should configure its HTTP client to use a + * web proxy with this hostname plus the other proxy properties. + * @return the proxy hostname + */ + public String getProxyHost() { + return proxyHost; + } + + /** + * See {@link #getProxyHost()}. + * @return the proxy port + */ + public int getProxyPort() { + return proxyPort; + } + + /** + * See {@link #getProxyHost()}. + * @return the username for proxy basicauth or null + */ + public String getProxyBasicAuthUser() { + return proxyBasicAuthUser; + } + + /** + * See {@link #getProxyHost()}. + * @return the password for proxy basicauth or null + */ + public String getProxyBasicAuthPassword() { + return proxyBasicAuthPassword; + } + } + + /** + * See {@link TestAction}. + */ + @SuppressWarnings("serial") + public static class UnexpectedResponseException extends Exception { + @SuppressWarnings("javadoc") + public UnexpectedResponseException(String message) { + super(message); + } + } + + /** + * Implemented by the caller to perform some action against an HTTP server. + */ + public interface TestAction { + /** + * Implement this method to perform whatever kind of HTTP client action you are testing. + * The test framework has already set up a server with some predefined configuration, and has + * provided the base URI of that server in {@code targetUri}. The properties in {@code params} + * indicate how you should customize your HTTP client. The goal is to verify that 1. these + * features (such as TLS and proxy configuration) work correctly in the client and 2. your + * configuration logic is accurately transferring these parameters to the client. + * + * @param targetUri the URI to query + * @param params client configuration options + * @return true if successful; false if your client implementation does not support some of + * those parameters and you are deliberately skipping this test + * @throws IOException if the connection failed + * @throws UnexpectedResponseException if you were able to connect and send the request, but + * the content of the response was not consistent with the {@code handler} you passed to + * the test method + */ + boolean doTest(URI targetUri, Params params) throws IOException, UnexpectedResponseException; + } + + /** + * Runs all of the other {@code test} methods with the same client logic. + * + * @param handler determines what the server should return for all responses + * @param testAction a {@link TestAction} implementation + */ + public static void testAll( + Handler handler, + TestAction testAction + ) { + testHttpClientDoesNotAllowSelfSignedCertByDefault(handler, testAction); + testHttpClientCanBeConfiguredToAllowSelfSignedCert(handler, testAction); + testHttpClientCanUseCustomSocketFactory(handler, testAction); + testHttpClientCanUseProxy(handler, testAction); + testHttpClientCanUseProxyWithBasicAuth(handler, testAction); + } + + /** + * Runs a test to verify that the HTTP client logic in the {@link TestAction} will fail if it + * connects to an HTTPS endpoint that has a self-signed certificate, when it has not been + * specifically configured to accept that certificate. + * + * @param handler determines what the server should return for all responses + * @param testAction a {@link TestAction} implementation + * @return true if successful; false if the {@link TestAction} returned false to indicate + * that the client does not support this kind of test + */ + public static boolean testHttpClientDoesNotAllowSelfSignedCertByDefault(Handler handler, + TestAction testAction) { + Params params = new Params(null, null, null, 0, null, null); + // deliberately don't include a TLS configuration in the Params, so the client doesn't know about the cert + ServerTLSConfiguration tlsConfig = ServerTLSConfiguration.makeSelfSignedCertificate(); + try (HttpServer secureServer = HttpServer.startSecure(tlsConfig, handler)) { + boolean didTest; + try { + didTest = testAction.doTest(secureServer.getUri(), params); + // test was expected to throw an exception, so we should only get here if the test really didn't do anything + assertThat("test action appears to have succeeded, but it should have failed", !didTest); + return false; + } catch (UnexpectedResponseException e) { + assertThat("test action was able to get a response, even if its content was invalid; should have gotten an IOException", false); + } catch (Exception e) { // any other kind of failure counts as an expected result + } + assertThat("expected the server not to receive a request due to TLS negotiation failure for a self-signed certificate", + secureServer.getRecorder().count(), equalTo(0)); + return true; + } + } + + /** + * Runs a test to verify that the HTTP client logic in the {@link TestAction} can be + * configured to accept a specific HTTPS certificate (which in this case is self-signed). + * + * @param handler determines what the server should return for all responses + * @param testAction a {@link TestAction} implementation + * @return true if successful; false if the {@link TestAction} returned false to indicate + * that the client does not support this kind of test + */ + public static boolean testHttpClientCanBeConfiguredToAllowSelfSignedCert(Handler handler, + TestAction testAction) { + String desc = "when the client was configured to accept a self-signed certificate"; + try { + ServerTLSConfiguration tlsConfig = ServerTLSConfiguration.makeSelfSignedCertificate(); + Params params = new Params(tlsConfig, null, null, 0, null, null); + try (HttpServer secureServer = HttpServer.startSecure(tlsConfig, handler)) { + boolean didTest = testAction.doTest(secureServer.getUri(), params); + if (didTest) { + assertThat("expected the server to receive a request " + desc, + secureServer.getRecorder().count(), equalTo(1)); + } + return didTest; + } + } catch (Exception e) { + throw unexpectedRequestFailure(e, desc); + } + } + + /** + * Runs a test to verify that the HTTP client logic in the {@link TestAction} can be + * configured to use a custom {@link SocketFactory}. The test method will provide a + * deliberately incorrect URI, plus a custom socket factory that rewrites requests to go + * to the correct URI, to verify that the socket factory is really being used. + * + * @param handler determines what the server should return for all responses + * @param testAction a {@link TestAction} implementation + * @return true if successful; false if the {@link TestAction} returned false to indicate + * that the client does not support this kind of test + */ + public static boolean testHttpClientCanUseCustomSocketFactory(Handler handler, + TestAction testAction) { + String desc = "when the client was configured with a custom socket factory"; + try { + try (HttpServer server = HttpServer.start(handler)) { + Params params = new Params(null, + makeSocketFactoryThatChangesHostAndPort(server.getUri().getHost(), server.getPort()), + null, 0, null, null); + URI uriWithWrongPort = URI.create("http://localhost:1"); + boolean didTest = testAction.doTest(uriWithWrongPort, params); + if (didTest) { + assertThat("expected the server to receive a request " + desc, + server.getRecorder().count(), equalTo(1)); + } + return didTest; + } + } catch (Exception e) { + throw unexpectedRequestFailure(e, desc); + } + } + + /** + * Runs a test to verify that the HTTP client logic in the {@link TestAction} can be + * configured to use a web proxy. The test method will provide a deliberately incorrect + * URI, plus the host/port of a fake proxy that accepts requests and returns the + * configured response, to verify that the proxy settings are really being used. + * + * @param handler determines what the server should return for all responses + * @param testAction a {@link TestAction} implementation + * @return true if successful; false if the {@link TestAction} returned false to indicate + * that the client does not support this kind of test + */ + public static boolean testHttpClientCanUseProxy(Handler handler, + TestAction testAction) { + String desc = "when the client was configured with a proxy"; + try { + try (HttpServer server = HttpServer.start(handler)) { + Params params = new Params(null, null, server.getUri().getHost(), server.getPort(), null, null); + URI fakeBaseUri = URI.create("http://not-a-real-host"); + boolean didTest = testAction.doTest(fakeBaseUri, params); + if (didTest) { + assertThat("expected the server to receive a request " + desc, + server.getRecorder().count(), equalTo(1)); + } + return didTest; + } + } catch (Exception e) { + throw unexpectedRequestFailure(e, desc); + } + } + + /** + * Runs a test to verify that the HTTP client logic in the {@link TestAction} can be + * configured to use a web proxy with basic authentication. The test method will provide + * a deliberately incorrect URI, plus the host/port of a fake proxy that accepts requests + * and returns the configured response, to verify that the proxy settings are really being + * used; then it will verify that the fake proxy received the expected authorization header. + * + * @param handler determines what the server should return for all responses + * @param testAction a {@link TestAction} implementation + * @return true if successful; false if the {@link TestAction} returned false to indicate + * that the client does not support this kind of test + */ + public static boolean testHttpClientCanUseProxyWithBasicAuth(Handler handler, + TestAction testAction) { + String desc = "when the client was configured with a proxy with basicauth"; + Handler proxyHandler = ctx -> { + if (ctx.getRequest().getHeader("Proxy-Authorization") == null) { + ctx.setStatus(407); + ctx.setHeader("Proxy-Authenticate", "Basic realm=x"); + } else { + handler.apply(ctx); + } + }; + try { + try (HttpServer server = HttpServer.start(proxyHandler)) { + Params params = new Params(null, null, server.getUri().getHost(), server.getPort(), "user", "pass"); + + URI fakeBaseUri = URI.create("http://not-a-real-host"); + boolean didTest = testAction.doTest(fakeBaseUri, params); + if (didTest) { + assertThat("expected the server to receive two requests " + desc, + server.getRecorder().count(), equalTo(2)); + RequestInfo req1 = server.getRecorder().requireRequest(); + assertThat("expected the first request not to have a Proxy-Authorization header", + req1.getHeader("Proxy-Authorization"), nullValue()); + RequestInfo req2 = server.getRecorder().requireRequest(); + assertThat("expected the second request (response to challenge) to have a valid Proxy-Authorization header", + req2.getHeader("Proxy-Authorization"), equalTo("Basic dXNlcjpwYXNz")); + } + return didTest; + } + } catch (Exception e) { + throw unexpectedRequestFailure(e, desc); + } + } + + private static AssertionError unexpectedRequestFailure(Exception e, String desc) { + return new AssertionError("request failed " + desc + ": " + e); + } + + /** + * Creates a {@link SocketFactory} implementation that rewrites all requests to go to the + * specified host and port, instead of the ones given in the URI. This is a simple way to + * verify that a given piece of client logic is really using the configured socket factory. + * + * @param host the hostname to send requests to + * @param port the port to send requests to + * @return a socket factory + */ + public static SocketFactorySingleHost makeSocketFactoryThatChangesHostAndPort(String host, int port) { + return new SocketFactorySingleHost(host, port); + } + + private static final class SocketSingleHost extends Socket { + private final String host; + private final int port; + + SocketSingleHost(String host, int port) { + this.host = host; + this.port = port; + } + + @Override public void connect(SocketAddress endpoint) throws IOException { + super.connect(new InetSocketAddress(this.host, this.port), 0); + } + + @Override public void connect(SocketAddress endpoint, int timeout) throws IOException { + super.connect(new InetSocketAddress(this.host, this.port), timeout); + } + } + + static final class SocketFactorySingleHost extends SocketFactory { + private final String host; + private final int port; + + public SocketFactorySingleHost(String host, int port) { + this.host = host; + this.port = port; + } + + @Override public Socket createSocket() throws IOException { + return new SocketSingleHost(this.host, this.port); + } + + @Override public Socket createSocket(String host, int port) throws IOException { + Socket socket = createSocket(); + socket.connect(new InetSocketAddress(this.host, this.port)); + return socket; + } + + @Override public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { + Socket socket = createSocket(); + socket.connect(new InetSocketAddress(this.host, this.port)); + return socket; + } + + @Override public Socket createSocket(InetAddress host, int port) throws IOException { + Socket socket = createSocket(); + socket.connect(new InetSocketAddress(this.host, this.port)); + return socket; + } + + @Override public Socket createSocket(InetAddress host, int port, InetAddress localAddress, int localPort) throws IOException { + Socket socket = createSocket(); + socket.connect(new InetSocketAddress(this.host, this.port)); + return socket; + } + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/impl/HttpServerImpl.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/impl/HttpServerImpl.java new file mode 100644 index 0000000..41e4bc1 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/impl/HttpServerImpl.java @@ -0,0 +1,22 @@ +package com.launchdarkly.testhelpers.httptest.impl; + +import com.launchdarkly.testhelpers.httptest.HttpServer; + +/** + * This class just contains the reference to the specific HTTP server implementation we will use, + * so that HttpServer can contain only portable code. + * + * @since 2.0.0 + */ +public abstract class HttpServerImpl { + private static final HttpServer.Delegate.Factory FACTORY = + (port, handler, tlsConfig) -> new NanoHttpdServerDelegate(port, handler, tlsConfig); + + /** + * Returns the implementation factory. + * @return the factory + */ + public static HttpServer.Delegate.Factory factory() { + return FACTORY; + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/impl/NanoHttpdServerDelegate.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/impl/NanoHttpdServerDelegate.java new file mode 100644 index 0000000..e6bc0bf --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/impl/NanoHttpdServerDelegate.java @@ -0,0 +1,296 @@ +package com.launchdarkly.testhelpers.httptest.impl; + +import com.google.common.collect.ImmutableMap; +import com.launchdarkly.testhelpers.httptest.Handler; +import com.launchdarkly.testhelpers.httptest.HttpServer; +import com.launchdarkly.testhelpers.httptest.RequestContext; +import com.launchdarkly.testhelpers.httptest.RequestInfo; +import com.launchdarkly.testhelpers.httptest.ServerTLSConfiguration; + +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.IHTTPSession; +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.NanoHTTPD; +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.response.IStatus; +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.response.Response; +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.response.Status; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.net.URI; +import java.nio.charset.Charset; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import javax.net.ssl.KeyManagerFactory; + +class NanoHttpdServerDelegate implements HttpServer.Delegate { + private final ServerImpl server; + + public NanoHttpdServerDelegate(int port, Handler handler, ServerTLSConfiguration tlsConfig) { + server = new ServerImpl(port, handler, tlsConfig); // NanoHTTPD will pick a port for us if this is zero + } + + @Override + public void close() throws IOException { + server.closeAllConnections(); + server.stop(); + } + + @Override + public int start() throws IOException { + server.start(); + return server.getListeningPort(); + } + + private static final class ServerImpl extends NanoHTTPD { + private final Handler handler; + + ServerImpl(int port, Handler handler, ServerTLSConfiguration tlsConfig) { + super(port); + this.handler = handler; + + if (tlsConfig != null) { + try { + char[] fakePassword = "secret".toCharArray(); + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null); + keyStore.setCertificateEntry("localhost", tlsConfig.getCertificate()); + keyStore.setEntry("localhost", + new KeyStore.PrivateKeyEntry(tlsConfig.getPrivateKey(), new Certificate[] { tlsConfig.getCertificate() }), + new KeyStore.PasswordProtection(fakePassword)); + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, fakePassword); + makeSecure(NanoHTTPD.makeSSLSocketFactory(keyStore, keyManagerFactory), null); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + @Override + public Response serve(IHTTPSession session) { + // We need to call the handler on a separate thread so that we can support chunked streaming. + // NanoHTTPD doesn't have an imperative "start writing the response" method; instead, we need + // to return the response to it, and *then* if there is additional streaming content, the + // handler will continue writing it. + CompletableFuture responseReceiver = new CompletableFuture<>(); + RequestContextImpl ctx = new RequestContextImpl(makeRequestInfo(session), responseReceiver); + + new Thread(() -> { + try { + handler.apply(ctx); + ctx.commit(); + } catch (Exception e) { + responseReceiver.completeExceptionally(e); + } + }).start(); + + try { + Response response = responseReceiver.get(); + return response; + } catch (ExecutionException e) { + throw new RuntimeException(e.getCause()); + } catch (InterruptedException e) { + throw new RuntimeException(e.getCause()); + } + } + + private RequestInfo makeRequestInfo(IHTTPSession session) { + String path = session.getUri(); // NanoHTTPD calls this the URI but it's really the path + String query = session.getQueryParameterString(); + String queryWithPrefix = query == null || query.isEmpty() ? "" : ("?" + query); + URI requestUri = URI.create(getBaseUri() + path + queryWithPrefix); + ImmutableMap.Builder headers = ImmutableMap.builder(); + String body = ""; + int contentLength = 0; + + for (Map.Entry h: session.getHeaders().entrySet()) { + headers.put(h.getKey().toLowerCase(), h.getValue()); + if (h.getKey().equalsIgnoreCase("content-length")) { + contentLength = Integer.parseInt(h.getValue()); + } + } + if (contentLength > 0) { + try { + InputStream bodyStream = session.getInputStream(); + byte[] data = new byte[contentLength]; + int n = bodyStream.read(data); + body = new String(data, 0, n, Charset.forName("UTF-8")); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + return new RequestInfo(session.getMethod().toString(), requestUri, path, + queryWithPrefix.isEmpty() ? null : queryWithPrefix, + headers.build(), body); + } + + private String getBaseUri() { + return "http://" + (this.getHostname() == null ? "localhost" : this.getHostname()) + + ":" + this.getListeningPort(); + } + } + + private static final class RequestContextImpl implements RequestContext { + private final RequestInfo requestInfo; + private final CompletableFuture responseReceiver; + + int status = 200; + String contentType = null; + Map> headers = new HashMap<>(); + + boolean chunked = false; + volatile Response response = null; + PipedOutputStream chunkedPipe = null; + + RequestContextImpl(RequestInfo requestInfo, CompletableFuture responseReceiver) { + this.requestInfo = requestInfo; + this.responseReceiver = responseReceiver; + } + + void commit() { + if (chunked) { + try { + chunkedPipe.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + if (response == null) { + // a status was set but nothing was written; call write() to force us to create a response + write(null); + } + responseReceiver.complete(response); + } + } + + @Override + public RequestInfo getRequest() { + return requestInfo; + } + + @Override + public void setStatus(int status) { + this.status = status; + } + + @Override + public void setHeader(String name, String value) { + headers.remove(name); + addHeader(name, value); + } + + @Override + public void addHeader(String name, String value) { + String lowerName = name.toLowerCase(); + List values = headers.get(lowerName); + if (values == null) { + values = new ArrayList<>(); + headers.put(lowerName, values); + } + values.add(value); + } + + @Override + public void setChunked() { + if (!chunked) { + if (response != null) { + throw new RuntimeException("setChunked was called after writing a non-chunked response"); + } + chunked = true; + chunkedPipe = new PipedOutputStream(); + InputStream pipeReader; + try { + pipeReader = new PipedInputStream(chunkedPipe); + } catch (IOException e) { + throw new RuntimeException(e); + } + response = Response.newChunkedResponse(statusWithCode(status), + contentType, pipeReader); + setHeaders(response); + response.setUseGzip(false); + + // We need to tell the ServerImpl code to return this response immediately to the server, + // while the handler (which will write the actual stream data) continues executing. That's + // what it provided this CompletableFuture for. + responseReceiver.complete(response); + } + } + + @Override + public void write(byte[] data) { + if (chunked) { + try { + if (data != null) { + chunkedPipe.write(data); + } + chunkedPipe.flush(); + Thread.sleep(200); + } catch (Exception e) { + throw new RuntimeException(e); + } + return; + } + + if (response != null) { + throw new RuntimeException("write was called twice for a non-chunked response"); + } + if (data == null) { + data = new byte[0]; + } + if (data.length != 0 && contentType == null) { + contentType = "text/plain"; + } + response = Response.newFixedLengthResponse(statusWithCode(status), + contentType, new ByteArrayInputStream(data), data.length); + setHeaders(response); + } + + @Override + public String getPathParam(int i) { + return null; + } + + private void setHeaders(Response r) { + for (Map.Entry> h: headers.entrySet()) { + String name = h.getKey(); + String value = String.join(",", h.getValue()); + if (name.equals("content-type")) { + r.setMimeType(value); + } else { + r.addHeader(name, value); + // The name addHeader in NanoHTTPD is misleading: it replaces any previous value, so we need + // to pre-concatenate with String.join() if we want multiple values to work. + // https://github.com/NanoHttpd/nanohttpd/issues/629 + } + } + } + + private IStatus statusWithCode(int statusCode) { + IStatus builtin = Status.lookup(statusCode); + if (builtin != null) { + return builtin; + } + return new IStatus() { + @Override + public int getRequestStatus() { + return statusCode; + } + + @Override + public String getDescription() { + return statusCode + " UNKNOWN"; + } + }; + } + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/CHANGES.md b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/CHANGES.md new file mode 100644 index 0000000..e542b12 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/CHANGES.md @@ -0,0 +1,19 @@ +# Changes Made to nanohttpd Source Code + +This directory contains source code from the nanohttpd project (https://github.com/launchdarkly-labs/nanohttpd) that has been modified for embedding into this project. + +The project was originally forked from: https://github.com/NanoHttpd/nanohttpd +The initial fork was in order to allow for any HTTP verb to be used instead of an enumerated list supported by nanohttpd. + +This package has now been vendored as it was only used by this test-helpers project and only a subset of the entire project was required. + +## Modifications + +### Package Namespace Change +- Changed all package declarations from `org.nanohttpd.*` to `com.launchdarkly.testhelpers.httptest.nanohttpd.*` +- Updated all import statements to reference the new package namespace + +## Original Source +- Repository: https://github.com/NanoHttpd/nanohttpd +- License: BSD 3-Clause (see LICENSE.md in this directory) +- All original copyright notices and license headers have been preserved in source files diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/LICENSE.md b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/LICENSE.md new file mode 100644 index 0000000..6a11673 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/LICENSE.md @@ -0,0 +1,17 @@ +================================================================================ +NOTE: This license applies only to the source code in the +com.launchdarkly.testhelpers.httptest.nanohttpd package and its subpackages. +================================================================================ + +Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, 2010 by Konstantinos Togias +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +* Neither the name of the NanoHttpd organization nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/ClientHandler.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/ClientHandler.java new file mode 100644 index 0000000..c11da19 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/ClientHandler.java @@ -0,0 +1,95 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.util.logging.Level; + +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.tempfiles.ITempFileManager; + +/** + * The runnable that will be used for every new client connection. + */ +public class ClientHandler implements Runnable { + + private final NanoHTTPD httpd; + + private final InputStream inputStream; + + private final Socket acceptSocket; + + public ClientHandler(NanoHTTPD httpd, InputStream inputStream, Socket acceptSocket) { + this.httpd = httpd; + this.inputStream = inputStream; + this.acceptSocket = acceptSocket; + } + + public void close() { + NanoHTTPD.safeClose(this.inputStream); + NanoHTTPD.safeClose(this.acceptSocket); + } + + @Override + public void run() { + OutputStream outputStream = null; + try { + outputStream = this.acceptSocket.getOutputStream(); + ITempFileManager tempFileManager = httpd.getTempFileManagerFactory().create(); + HTTPSession session = new HTTPSession(httpd, tempFileManager, this.inputStream, outputStream, this.acceptSocket.getInetAddress()); + while (!this.acceptSocket.isClosed()) { + session.execute(); + } + } catch (Exception e) { + // When the socket is closed by the client, + // we throw our own SocketException + // to break the "keep alive" loop above. If + // the exception was anything other + // than the expected SocketException OR a + // SocketTimeoutException, print the + // stacktrace + if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage())) && !(e instanceof SocketTimeoutException)) { + NanoHTTPD.LOG.log(Level.SEVERE, "Communication with the client broken, or an bug in the handler code", e); + } + } finally { + NanoHTTPD.safeClose(outputStream); + NanoHTTPD.safeClose(this.inputStream); + NanoHTTPD.safeClose(this.acceptSocket); + httpd.asyncRunner.closed(this); + } + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/HTTPSession.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/HTTPSession.java new file mode 100644 index 0000000..d440bb1 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/HTTPSession.java @@ -0,0 +1,698 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.net.InetAddress; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.util.regex.Matcher; + +import javax.net.ssl.SSLException; + +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.NanoHTTPD.ResponseException; +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.content.ContentType; +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.content.CookieHandler; +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.request.Method; +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.response.Response; +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.response.Status; +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.tempfiles.ITempFile; +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.tempfiles.ITempFileManager; + +public class HTTPSession implements IHTTPSession { + + public static final String POST_DATA = "postData"; + + private static final int REQUEST_BUFFER_LEN = 512; + + private static final int MEMORY_STORE_LIMIT = 1024; + + public static final int BUFSIZE = 8192; + + public static final int MAX_HEADER_SIZE = 1024; + + private final NanoHTTPD httpd; + + private final ITempFileManager tempFileManager; + + private final OutputStream outputStream; + + private final BufferedInputStream inputStream; + + private int splitbyte; + + private int rlen; + + private String uri; + + private Method method; + + private Map> parms; + + private Map headers; + + private CookieHandler cookies; + + private String queryParameterString; + + private String remoteIp; + + private String protocolVersion; + + public HTTPSession(NanoHTTPD httpd, ITempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) { + this.httpd = httpd; + this.tempFileManager = tempFileManager; + this.inputStream = new BufferedInputStream(inputStream, HTTPSession.BUFSIZE); + this.outputStream = outputStream; + } + + public HTTPSession(NanoHTTPD httpd, ITempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) { + this.httpd = httpd; + this.tempFileManager = tempFileManager; + this.inputStream = new BufferedInputStream(inputStream, HTTPSession.BUFSIZE); + this.outputStream = outputStream; + this.remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString(); + this.headers = new HashMap(); + } + + /** + * Decodes the sent headers and loads the data into Key/value pairs + */ + private void decodeHeader(BufferedReader in, Map pre, Map> parms, Map headers) throws ResponseException { + try { + // Read the request line + String inLine = in.readLine(); + if (inLine == null) { + return; + } + + StringTokenizer st = new StringTokenizer(inLine); + if (!st.hasMoreTokens()) { + throw new ResponseException(Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html"); + } + + pre.put("method", st.nextToken()); + + if (!st.hasMoreTokens()) { + throw new ResponseException(Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html"); + } + + String uri = st.nextToken(); + + // Decode parameters from the URI + int qmi = uri.indexOf('?'); + if (qmi >= 0) { + decodeParms(uri.substring(qmi + 1), parms); + uri = NanoHTTPD.decodePercent(uri.substring(0, qmi)); + } else { + uri = NanoHTTPD.decodePercent(uri); + } + + // If there's another token, its protocol version, + // followed by HTTP headers. + // NOTE: this now forces header names lower case since they are + // case insensitive and vary by client. + if (st.hasMoreTokens()) { + protocolVersion = st.nextToken(); + } else { + protocolVersion = "HTTP/1.1"; + NanoHTTPD.LOG.log(Level.FINE, "no protocol version specified, strange. Assuming HTTP/1.1."); + } + String line = in.readLine(); + while (line != null && !line.trim().isEmpty()) { + int p = line.indexOf(':'); + if (p >= 0) { + headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim()); + } + line = in.readLine(); + } + + pre.put("uri", uri); + } catch (IOException ioe) { + throw new ResponseException(Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); + } + } + + /** + * Decodes the Multipart Body data and put it into Key/Value pairs. + */ + private void decodeMultipartFormData(ContentType contentType, ByteBuffer fbuf, Map> parms, Map files) throws ResponseException { + int pcount = 0; + try { + int[] boundaryIdxs = getBoundaryPositions(fbuf, contentType.getBoundary().getBytes()); + if (boundaryIdxs.length < 2) { + throw new ResponseException(Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but contains less than two boundary strings."); + } + + byte[] partHeaderBuff = new byte[MAX_HEADER_SIZE]; + for (int boundaryIdx = 0; boundaryIdx < boundaryIdxs.length - 1; boundaryIdx++) { + fbuf.position(boundaryIdxs[boundaryIdx]); + int len = (fbuf.remaining() < MAX_HEADER_SIZE) ? fbuf.remaining() : MAX_HEADER_SIZE; + fbuf.get(partHeaderBuff, 0, len); + BufferedReader in = + new BufferedReader(new InputStreamReader(new ByteArrayInputStream(partHeaderBuff, 0, len), Charset.forName(contentType.getEncoding())), len); + + int headerLines = 0; + // First line is boundary string + String mpline = in.readLine(); + headerLines++; + if (mpline == null || !mpline.contains(contentType.getBoundary())) { + throw new ResponseException(Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but chunk does not start with boundary."); + } + + String partName = null, fileName = null, partContentType = null; + // Parse the reset of the header lines + mpline = in.readLine(); + headerLines++; + while (mpline != null && mpline.trim().length() > 0) { + Matcher matcher = NanoHTTPD.CONTENT_DISPOSITION_PATTERN.matcher(mpline); + if (matcher.matches()) { + String attributeString = matcher.group(2); + matcher = NanoHTTPD.CONTENT_DISPOSITION_ATTRIBUTE_PATTERN.matcher(attributeString); + while (matcher.find()) { + String key = matcher.group(1); + if ("name".equalsIgnoreCase(key)) { + partName = matcher.group(2); + } else if ("filename".equalsIgnoreCase(key)) { + fileName = matcher.group(2); + // add these two line to support multiple + // files uploaded using the same field Id + if (!fileName.isEmpty()) { + if (pcount > 0) + partName = partName + String.valueOf(pcount++); + else + pcount++; + } + } + } + } + matcher = NanoHTTPD.CONTENT_TYPE_PATTERN.matcher(mpline); + if (matcher.matches()) { + partContentType = matcher.group(2).trim(); + } + mpline = in.readLine(); + headerLines++; + } + int partHeaderLength = 0; + while (headerLines-- > 0) { + partHeaderLength = scipOverNewLine(partHeaderBuff, partHeaderLength); + } + // Read the part data + if (partHeaderLength >= len - 4) { + throw new ResponseException(Status.INTERNAL_ERROR, "Multipart header size exceeds MAX_HEADER_SIZE."); + } + int partDataStart = boundaryIdxs[boundaryIdx] + partHeaderLength; + int partDataEnd = boundaryIdxs[boundaryIdx + 1] - 4; + + fbuf.position(partDataStart); + + List values = parms.get(partName); + if (values == null) { + values = new ArrayList(); + parms.put(partName, values); + } + + if (partContentType == null) { + // Read the part into a string + byte[] data_bytes = new byte[partDataEnd - partDataStart]; + fbuf.get(data_bytes); + + values.add(new String(data_bytes, contentType.getEncoding())); + } else { + // Read it into a file + String path = saveTmpFile(fbuf, partDataStart, partDataEnd - partDataStart, fileName); + if (!files.containsKey(partName)) { + files.put(partName, path); + } else { + int count = 2; + while (files.containsKey(partName + count)) { + count++; + } + files.put(partName + count, path); + } + values.add(fileName); + } + } + } catch (ResponseException re) { + throw re; + } catch (Exception e) { + throw new ResponseException(Status.INTERNAL_ERROR, e.toString()); + } + } + + private int scipOverNewLine(byte[] partHeaderBuff, int index) { + while (partHeaderBuff[index] != '\n') { + index++; + } + return ++index; + } + + /** + * Decodes parameters in percent-encoded URI-format ( e.g. + * "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given Map. + */ + private void decodeParms(String parms, Map> p) { + if (parms == null) { + this.queryParameterString = ""; + return; + } + + this.queryParameterString = parms; + StringTokenizer st = new StringTokenizer(parms, "&"); + while (st.hasMoreTokens()) { + String e = st.nextToken(); + int sep = e.indexOf('='); + String key = null; + String value = null; + + if (sep >= 0) { + key = NanoHTTPD.decodePercent(e.substring(0, sep)).trim(); + value = NanoHTTPD.decodePercent(e.substring(sep + 1)); + } else { + key = NanoHTTPD.decodePercent(e).trim(); + value = ""; + } + + List values = p.get(key); + if (values == null) { + values = new ArrayList(); + p.put(key, values); + } + + values.add(value); + } + } + + @Override + public void execute() throws IOException { + Response r = null; + try { + // Read the first 8192 bytes. + // The full header should fit in here. + // Apache's default header limit is 8KB. + // Do NOT assume that a single read will get the entire header + // at once! + byte[] buf = new byte[HTTPSession.BUFSIZE]; + this.splitbyte = 0; + this.rlen = 0; + + int read = -1; + this.inputStream.mark(HTTPSession.BUFSIZE); + try { + read = this.inputStream.read(buf, 0, HTTPSession.BUFSIZE); + } catch (SSLException e) { + throw e; + } catch (IOException e) { + NanoHTTPD.safeClose(this.inputStream); + NanoHTTPD.safeClose(this.outputStream); + throw new SocketException("NanoHttpd Shutdown"); + } + if (read == -1) { + // socket was been closed + NanoHTTPD.safeClose(this.inputStream); + NanoHTTPD.safeClose(this.outputStream); + throw new SocketException("NanoHttpd Shutdown"); + } + while (read > 0) { + this.rlen += read; + this.splitbyte = findHeaderEnd(buf, this.rlen); + if (this.splitbyte > 0) { + break; + } + read = this.inputStream.read(buf, this.rlen, HTTPSession.BUFSIZE - this.rlen); + } + + if (this.splitbyte < this.rlen) { + this.inputStream.reset(); + this.inputStream.skip(this.splitbyte); + } + + this.parms = new HashMap>(); + if (null == this.headers) { + this.headers = new HashMap(); + } else { + this.headers.clear(); + } + + // Create a BufferedReader for parsing the header. + BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, this.rlen))); + + // Decode the header into parms and header java properties + Map pre = new HashMap(); + decodeHeader(hin, pre, this.parms, this.headers); + + if (null != this.remoteIp) { + this.headers.put("remote-addr", this.remoteIp); + this.headers.put("http-client-ip", this.remoteIp); + } + + this.method = Method.lookup(pre.get("method")); + if (this.method == null) { + throw new ResponseException(Status.BAD_REQUEST, "BAD REQUEST: Syntax error. HTTP verb " + pre.get("method") + " not allowed"); + } + + this.uri = pre.get("uri"); + + this.cookies = new CookieHandler(this.headers); + + String connection = this.headers.get("connection"); + boolean keepAlive = "HTTP/1.1".equals(protocolVersion) && (connection == null || !connection.matches("(?i).*close.*")); + + // Ok, now do the serve() + + // TODO: long body_size = getBodySize(); + // TODO: long pos_before_serve = this.inputStream.totalRead() + // (requires implementation for totalRead()) + r = httpd.handle(this); + // TODO: this.inputStream.skip(body_size - + // (this.inputStream.totalRead() - pos_before_serve)) + + if (r == null) { + throw new ResponseException(Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response."); + } else { + String acceptEncoding = this.headers.get("accept-encoding"); + this.cookies.unloadQueue(r); + r.setRequestMethod(this.method); + if (acceptEncoding == null || !acceptEncoding.contains("gzip")) { + r.setUseGzip(false); + } + r.setKeepAlive(keepAlive); + r.send(this.outputStream); + } + if (!keepAlive || r.isCloseConnection()) { + throw new SocketException("NanoHttpd Shutdown"); + } + } catch (SocketException e) { + // throw it out to close socket object (finalAccept) + throw e; + } catch (SocketTimeoutException ste) { + // treat socket timeouts the same way we treat socket exceptions + // i.e. close the stream & finalAccept object by throwing the + // exception up the call stack. + throw ste; + } catch (SSLException ssle) { + Response resp = Response.newFixedLengthResponse(Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SSL PROTOCOL FAILURE: " + ssle.getMessage()); + resp.send(this.outputStream); + NanoHTTPD.safeClose(this.outputStream); + } catch (IOException ioe) { + Response resp = Response.newFixedLengthResponse(Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); + resp.send(this.outputStream); + NanoHTTPD.safeClose(this.outputStream); + } catch (ResponseException re) { + Response resp = Response.newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); + resp.send(this.outputStream); + NanoHTTPD.safeClose(this.outputStream); + } finally { + NanoHTTPD.safeClose(r); + this.tempFileManager.clear(); + } + } + + /** + * Find byte index separating header from body. It must be the last byte of + * the first two sequential new lines. + */ + private int findHeaderEnd(final byte[] buf, int rlen) { + int splitbyte = 0; + while (splitbyte + 1 < rlen) { + + // RFC2616 + if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && splitbyte + 3 < rlen && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') { + return splitbyte + 4; + } + + // tolerance + if (buf[splitbyte] == '\n' && buf[splitbyte + 1] == '\n') { + return splitbyte + 2; + } + splitbyte++; + } + return 0; + } + + /** + * Find the byte positions where multipart boundaries start. This reads a + * large block at a time and uses a temporary buffer to optimize (memory + * mapped) file access. + */ + private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) { + int[] res = new int[0]; + if (b.remaining() < boundary.length) { + return res; + } + + int search_window_pos = 0; + byte[] search_window = new byte[4 * 1024 + boundary.length]; + + int first_fill = (b.remaining() < search_window.length) ? b.remaining() : search_window.length; + b.get(search_window, 0, first_fill); + int new_bytes = first_fill - boundary.length; + + do { + // Search the search_window + for (int j = 0; j < new_bytes; j++) { + for (int i = 0; i < boundary.length; i++) { + if (search_window[j + i] != boundary[i]) + break; + if (i == boundary.length - 1) { + // Match found, add it to results + int[] new_res = new int[res.length + 1]; + System.arraycopy(res, 0, new_res, 0, res.length); + new_res[res.length] = search_window_pos + j; + res = new_res; + } + } + } + search_window_pos += new_bytes; + + // Copy the end of the buffer to the start + System.arraycopy(search_window, search_window.length - boundary.length, search_window, 0, boundary.length); + + // Refill search_window + new_bytes = search_window.length - boundary.length; + new_bytes = (b.remaining() < new_bytes) ? b.remaining() : new_bytes; + b.get(search_window, boundary.length, new_bytes); + } while (new_bytes > 0); + return res; + } + + @Override + public CookieHandler getCookies() { + return this.cookies; + } + + @Override + public final Map getHeaders() { + return this.headers; + } + + @Override + public final InputStream getInputStream() { + return this.inputStream; + } + + @Override + public final Method getMethod() { + return this.method; + } + + /** + * @deprecated use {@link #getParameters()} instead. + */ + @Override + @Deprecated + public final Map getParms() { + Map result = new HashMap(); + for (String key : this.parms.keySet()) { + result.put(key, this.parms.get(key).get(0)); + } + + return result; + } + + @Override + public final Map> getParameters() { + return this.parms; + } + + @Override + public String getQueryParameterString() { + return this.queryParameterString; + } + + private RandomAccessFile getTmpBucket() { + try { + ITempFile tempFile = this.tempFileManager.createTempFile(null); + return new RandomAccessFile(tempFile.getName(), "rw"); + } catch (Exception e) { + throw new Error(e); // we won't recover, so throw an error + } + } + + @Override + public final String getUri() { + return this.uri; + } + + /** + * Deduce body length in bytes. Either from "content-length" header or read + * bytes. + */ + public long getBodySize() { + if (this.headers.containsKey("content-length")) { + return Long.parseLong(this.headers.get("content-length")); + } else if (this.splitbyte < this.rlen) { + return this.rlen - this.splitbyte; + } + return 0; + } + + @Override + public void parseBody(Map files) throws IOException, ResponseException { + RandomAccessFile randomAccessFile = null; + try { + long size = getBodySize(); + ByteArrayOutputStream baos = null; + DataOutput requestDataOutput = null; + + // Store the request in memory or a file, depending on size + if (size < MEMORY_STORE_LIMIT) { + baos = new ByteArrayOutputStream(); + requestDataOutput = new DataOutputStream(baos); + } else { + randomAccessFile = getTmpBucket(); + requestDataOutput = randomAccessFile; + } + + // Read all the body and write it to request_data_output + byte[] buf = new byte[REQUEST_BUFFER_LEN]; + while (this.rlen >= 0 && size > 0) { + this.rlen = this.inputStream.read(buf, 0, (int) Math.min(size, REQUEST_BUFFER_LEN)); + size -= this.rlen; + if (this.rlen > 0) { + requestDataOutput.write(buf, 0, this.rlen); + } + } + + ByteBuffer fbuf = null; + if (baos != null) { + fbuf = ByteBuffer.wrap(baos.toByteArray(), 0, baos.size()); + } else { + fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length()); + randomAccessFile.seek(0); + } + + // If the method is POST, there may be parameters + // in data section, too, read it: + if (Method.POST.equals(this.method)) { + ContentType contentType = new ContentType(this.headers.get("content-type")); + if (contentType.isMultipart()) { + String boundary = contentType.getBoundary(); + if (boundary == null) { + throw new ResponseException(Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html"); + } + decodeMultipartFormData(contentType, fbuf, this.parms, files); + } else { + byte[] postBytes = new byte[fbuf.remaining()]; + fbuf.get(postBytes); + String postLine = new String(postBytes, contentType.getEncoding()).trim(); + // Handle application/x-www-form-urlencoded + if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType.getContentType())) { + decodeParms(postLine, this.parms); + } else if (postLine.length() != 0) { + // Special case for raw POST data => create a + // special files entry "postData" with raw content + // data + files.put(POST_DATA, postLine); + } + } + } else if (Method.PUT.equals(this.method)) { + files.put("content", saveTmpFile(fbuf, 0, fbuf.limit(), null)); + } + } finally { + NanoHTTPD.safeClose(randomAccessFile); + } + } + + /** + * Retrieves the content of a sent file and saves it to a temporary file. + * The full path to the saved file is returned. + */ + private String saveTmpFile(ByteBuffer b, int offset, int len, String filename_hint) { + String path = ""; + if (len > 0) { + FileOutputStream fileOutputStream = null; + try { + ITempFile tempFile = this.tempFileManager.createTempFile(filename_hint); + ByteBuffer src = b.duplicate(); + fileOutputStream = new FileOutputStream(tempFile.getName()); + FileChannel dest = fileOutputStream.getChannel(); + src.position(offset).limit(offset + len); + dest.write(src.slice()); + path = tempFile.getName(); + } catch (Exception e) { // Catch exception if any + throw new Error(e); // we won't recover, so throw an error + } finally { + NanoHTTPD.safeClose(fileOutputStream); + } + } + return path; + } + + @Override + public String getRemoteIpAddress() { + return this.remoteIp; + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/IHTTPSession.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/IHTTPSession.java new file mode 100644 index 0000000..d81c86f --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/IHTTPSession.java @@ -0,0 +1,93 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; + +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.NanoHTTPD.ResponseException; +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.content.CookieHandler; +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.request.Method; + +/** + * Handles one session, i.e. parses the HTTP request and returns the response. + */ +public interface IHTTPSession { + + void execute() throws IOException; + + CookieHandler getCookies(); + + Map getHeaders(); + + InputStream getInputStream(); + + Method getMethod(); + + /** + * This method will only return the first value for a given parameter. You + * will want to use getParameters if you expect multiple values for a given + * key. + * + * @deprecated use {@link #getParameters()} instead. + */ + @Deprecated + Map getParms(); + + Map> getParameters(); + + String getQueryParameterString(); + + /** + * @return the path part of the URL. + */ + String getUri(); + + /** + * Adds the files in the request body to the files map. + * + * @param files + * map to modify + */ + void parseBody(Map files) throws IOException, ResponseException; + + /** + * Get the remote ip address of the requester. + * + * @return the IP address. + */ + String getRemoteIpAddress(); +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/NanoHTTPD.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/NanoHTTPD.java new file mode 100644 index 0000000..0a12a49 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/NanoHTTPD.java @@ -0,0 +1,641 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URL; +import java.net.URLDecoder; +import java.security.KeyStore; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.TrustManagerFactory; + +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.response.Response; +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.response.Status; +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.sockets.DefaultServerSocketFactory; +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.sockets.SecureServerSocketFactory; +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.tempfiles.DefaultTempFileManagerFactory; +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.tempfiles.ITempFileManager; +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.threading.DefaultAsyncRunner; +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.threading.IAsyncRunner; +import com.launchdarkly.testhelpers.httptest.nanohttpd.util.IFactory; +import com.launchdarkly.testhelpers.httptest.nanohttpd.util.IFactoryThrowing; +import com.launchdarkly.testhelpers.httptest.nanohttpd.util.IHandler; + +/** + * A simple, tiny, nicely embeddable HTTP server in Java + *

+ *

+ * NanoHTTPD + *

+ * Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, + * 2010 by Konstantinos Togias + *

+ *

+ *

+ * Features + limitations: + *

    + *

    + *

  • Only one Java file
  • + *
  • Java 5 compatible
  • + *
  • Released as open source, Modified BSD licence
  • + *
  • No fixed config files, logging, authorization etc. (Implement yourself if + * you need them.)
  • + *
  • Supports parameter parsing of GET and POST methods (+ rudimentary PUT + * support in 1.25)
  • + *
  • Supports both dynamic content and file serving
  • + *
  • Supports file upload (since version 1.2, 2010)
  • + *
  • Supports partial content (streaming)
  • + *
  • Supports ETags
  • + *
  • Never caches anything
  • + *
  • Doesn't limit bandwidth, request time or simultaneous connections
  • + *
  • Default code serves files and shows all HTTP parameters and headers
  • + *
  • File server supports directory listing, index.html and index.htm
  • + *
  • File server supports partial content (streaming)
  • + *
  • File server supports ETags
  • + *
  • File server does the 301 redirection trick for directories without '/'
  • + *
  • File server supports simple skipping for files (continue download)
  • + *
  • File server serves also very long files without memory overhead
  • + *
  • Contains a built-in list of most common MIME types
  • + *
  • All header names are converted to lower case so they don't vary between + * browsers/clients
  • + *

    + *

+ *

+ *

+ * How to use: + *

    + *

    + *

  • Subclass and implement serve() and embed to your own program
  • + *

    + *

+ *

+ * See the separate "LICENSE.md" file for the distribution license (Modified BSD + * licence) + */ +public abstract class NanoHTTPD { + + public static final String CONTENT_DISPOSITION_REGEX = "([ |\t]*Content-Disposition[ |\t]*:)(.*)"; + + public static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern.compile(CONTENT_DISPOSITION_REGEX, Pattern.CASE_INSENSITIVE); + + public static final String CONTENT_TYPE_REGEX = "([ |\t]*content-type[ |\t]*:)(.*)"; + + public static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile(CONTENT_TYPE_REGEX, Pattern.CASE_INSENSITIVE); + + public static final String CONTENT_DISPOSITION_ATTRIBUTE_REGEX = "[ |\t]*([a-zA-Z]*)[ |\t]*=[ |\t]*['|\"]([^\"^']*)['|\"]"; + + public static final Pattern CONTENT_DISPOSITION_ATTRIBUTE_PATTERN = Pattern.compile(CONTENT_DISPOSITION_ATTRIBUTE_REGEX); + + public static final class ResponseException extends Exception { + + private static final long serialVersionUID = 6569838532917408380L; + + private final Status status; + + public ResponseException(Status status, String message) { + super(message); + this.status = status; + } + + public ResponseException(Status status, String message, Exception e) { + super(message, e); + this.status = status; + } + + public Status getStatus() { + return this.status; + } + } + + /** + * Maximum time to wait on Socket.getInputStream().read() (in milliseconds) + * This is required as the Keep-Alive HTTP connections would otherwise block + * the socket reading thread forever (or as long the browser is open). + */ + public static final int SOCKET_READ_TIMEOUT = 5000; + + /** + * Common MIME type for dynamic content: plain text + */ + public static final String MIME_PLAINTEXT = "text/plain"; + + /** + * Common MIME type for dynamic content: html + */ + public static final String MIME_HTML = "text/html"; + + /** + * Pseudo-Parameter to use to store the actual query string in the + * parameters map for later re-processing. + */ + private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING"; + + /** + * logger to log to. + */ + public static final Logger LOG = Logger.getLogger(NanoHTTPD.class.getName()); + + /** + * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE + */ + protected static Map MIME_TYPES; + + public static Map mimeTypes() { + if (MIME_TYPES == null) { + MIME_TYPES = new HashMap(); + loadMimeTypes(MIME_TYPES, "META-INF/nanohttpd/default-mimetypes.properties"); + loadMimeTypes(MIME_TYPES, "META-INF/nanohttpd/mimetypes.properties"); + if (MIME_TYPES.isEmpty()) { + LOG.log(Level.WARNING, "no mime types found in the classpath! please provide mimetypes.properties"); + } + } + return MIME_TYPES; + } + + @SuppressWarnings({ + "unchecked", + "rawtypes" + }) + private static void loadMimeTypes(Map result, String resourceName) { + try { + Enumeration resources = NanoHTTPD.class.getClassLoader().getResources(resourceName); + while (resources.hasMoreElements()) { + URL url = (URL) resources.nextElement(); + Properties properties = new Properties(); + InputStream stream = null; + try { + stream = url.openStream(); + properties.load(stream); + } catch (IOException e) { + LOG.log(Level.SEVERE, "could not load mimetypes from " + url, e); + } finally { + safeClose(stream); + } + result.putAll((Map) properties); + } + } catch (IOException e) { + LOG.log(Level.INFO, "no mime types available at " + resourceName); + } + }; + + /** + * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and an + * array of loaded KeyManagers. These objects must properly + * loaded/initialized by the caller. + */ + public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManager[] keyManagers) throws IOException { + SSLServerSocketFactory res = null; + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(loadedKeyStore); + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(keyManagers, trustManagerFactory.getTrustManagers(), null); + res = ctx.getServerSocketFactory(); + } catch (Exception e) { + throw new IOException(e.getMessage()); + } + return res; + } + + /** + * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and a + * loaded KeyManagerFactory. These objects must properly loaded/initialized + * by the caller. + */ + public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManagerFactory loadedKeyFactory) throws IOException { + try { + return makeSSLSocketFactory(loadedKeyStore, loadedKeyFactory.getKeyManagers()); + } catch (Exception e) { + throw new IOException(e.getMessage()); + } + } + + /** + * Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your + * certificate and passphrase + */ + public static SSLServerSocketFactory makeSSLSocketFactory(String keyAndTrustStoreClasspathPath, char[] passphrase) throws IOException { + try { + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + InputStream keystoreStream = NanoHTTPD.class.getResourceAsStream(keyAndTrustStoreClasspathPath); + + if (keystoreStream == null) { + throw new IOException("Unable to load keystore from classpath: " + keyAndTrustStoreClasspathPath); + } + + keystore.load(keystoreStream, passphrase); + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keystore, passphrase); + return makeSSLSocketFactory(keystore, keyManagerFactory); + } catch (Exception e) { + throw new IOException(e.getMessage()); + } + } + + /** + * Get MIME type from file name extension, if possible + * + * @param uri + * the string representing a file + * @return the connected mime/type + */ + public static String getMimeTypeForFile(String uri) { + int dot = uri.lastIndexOf('.'); + String mime = null; + if (dot >= 0) { + mime = mimeTypes().get(uri.substring(dot + 1).toLowerCase()); + } + return mime == null ? "application/octet-stream" : mime; + } + + public static final void safeClose(Object closeable) { + try { + if (closeable != null) { + if (closeable instanceof Closeable) { + ((Closeable) closeable).close(); + } else if (closeable instanceof Socket) { + ((Socket) closeable).close(); + } else if (closeable instanceof ServerSocket) { + ((ServerSocket) closeable).close(); + } else { + throw new IllegalArgumentException("Unknown object to close"); + } + } + } catch (IOException e) { + NanoHTTPD.LOG.log(Level.SEVERE, "Could not close", e); + } + } + + public final String hostname; + + public final int myPort; + + private volatile ServerSocket myServerSocket; + + public ServerSocket getMyServerSocket() { + return myServerSocket; + } + + private IFactoryThrowing serverSocketFactory = new DefaultServerSocketFactory(); + + private Thread myThread; + + private IHandler httpHandler; + + protected List> interceptors = new ArrayList>(4); + + /** + * Pluggable strategy for asynchronously executing requests. + */ + protected IAsyncRunner asyncRunner; + + /** + * Pluggable strategy for creating and cleaning up temporary files. + */ + private IFactory tempFileManagerFactory; + + /** + * Constructs an HTTP server on given port. + */ + public NanoHTTPD(int port) { + this(null, port); + } + + // ------------------------------------------------------------------------------- + // // + // + // Threading Strategy. + // + // ------------------------------------------------------------------------------- + // // + + /** + * Constructs an HTTP server on given hostname and port. + */ + public NanoHTTPD(String hostname, int port) { + this.hostname = hostname; + this.myPort = port; + setTempFileManagerFactory(new DefaultTempFileManagerFactory()); + setAsyncRunner(new DefaultAsyncRunner()); + + // creates a default handler that redirects to deprecated serve(); + this.httpHandler = new IHandler() { + + @Override + public Response handle(IHTTPSession input) { + return NanoHTTPD.this.serve(input); + } + }; + } + + public void setHTTPHandler(IHandler handler) { + this.httpHandler = handler; + } + + public void addHTTPInterceptor(IHandler interceptor) { + interceptors.add(interceptor); + } + + /** + * Forcibly closes all connections that are open. + */ + public synchronized void closeAllConnections() { + stop(); + } + + /** + * create a instance of the client handler, subclasses can return a subclass + * of the ClientHandler. + * + * @param finalAccept + * the socket the cleint is connected to + * @param inputStream + * the input stream + * @return the client handler + */ + protected ClientHandler createClientHandler(final Socket finalAccept, final InputStream inputStream) { + return new ClientHandler(this, inputStream, finalAccept); + } + + /** + * Instantiate the server runnable, can be overwritten by subclasses to + * provide a subclass of the ServerRunnable. + * + * @param timeout + * the socet timeout to use. + * @return the server runnable. + */ + protected ServerRunnable createServerRunnable(final int timeout) { + return new ServerRunnable(this, timeout); + } + + /** + * Decode parameters from a URL, handing the case where a single parameter + * name might have been supplied several times, by return lists of values. + * In general these lists will contain a single element. + * + * @param parms + * original NanoHTTPD parameters values, as passed to the + * serve() method. + * @return a map of String (parameter name) to + * List<String> (a list of the values supplied). + */ + protected static Map> decodeParameters(Map parms) { + return decodeParameters(parms.get(NanoHTTPD.QUERY_STRING_PARAMETER)); + } + + // ------------------------------------------------------------------------------- + // // + + /** + * Decode parameters from a URL, handing the case where a single parameter + * name might have been supplied several times, by return lists of values. + * In general these lists will contain a single element. + * + * @param queryString + * a query string pulled from the URL. + * @return a map of String (parameter name) to + * List<String> (a list of the values supplied). + */ + protected static Map> decodeParameters(String queryString) { + Map> parms = new HashMap>(); + if (queryString != null) { + StringTokenizer st = new StringTokenizer(queryString, "&"); + while (st.hasMoreTokens()) { + String e = st.nextToken(); + int sep = e.indexOf('='); + String propertyName = sep >= 0 ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim(); + if (!parms.containsKey(propertyName)) { + parms.put(propertyName, new ArrayList()); + } + String propertyValue = sep >= 0 ? decodePercent(e.substring(sep + 1)) : null; + if (propertyValue != null) { + parms.get(propertyName).add(propertyValue); + } + } + } + return parms; + } + + /** + * Decode percent encoded String values. + * + * @param str + * the percent encoded String + * @return expanded form of the input, for example "foo%20bar" becomes + * "foo bar" + */ + public static String decodePercent(String str) { + String decoded = null; + try { + decoded = URLDecoder.decode(str, "UTF8"); + } catch (UnsupportedEncodingException ignored) { + NanoHTTPD.LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored); + } + return decoded; + } + + public final int getListeningPort() { + return this.myServerSocket == null ? -1 : this.myServerSocket.getLocalPort(); + } + + public final boolean isAlive() { + return wasStarted() && !this.myServerSocket.isClosed() && this.myThread.isAlive(); + } + + public IFactoryThrowing getServerSocketFactory() { + return serverSocketFactory; + } + + public void setServerSocketFactory(IFactoryThrowing serverSocketFactory) { + this.serverSocketFactory = serverSocketFactory; + } + + public String getHostname() { + return hostname; + } + + public IFactory getTempFileManagerFactory() { + return tempFileManagerFactory; + } + + /** + * Call before start() to serve over HTTPS instead of HTTP + */ + public void makeSecure(SSLServerSocketFactory sslServerSocketFactory, String[] sslProtocols) { + this.serverSocketFactory = new SecureServerSocketFactory(sslServerSocketFactory, sslProtocols); + } + + /** + * This is the "master" method that delegates requests to handlers and makes + * sure there is a response to every request. You are not supposed to call + * or override this method in any circumstances. But no one will stop you if + * you do. I'm a Javadoc, not Code Police. + * + * @param session + * the incoming session + * @return a response to the incoming session + */ + public Response handle(IHTTPSession session) { + for (IHandler interceptor : interceptors) { + Response response = interceptor.handle(session); + if (response != null) + return response; + } + return httpHandler.handle(session); + } + + /** + * Override this to customize the server. + *

+ *

+ * (By default, this returns a 404 "Not Found" plain text error response.) + * + * @param session + * The HTTP session + * @return HTTP response, see class Response for details + */ + @Deprecated + protected Response serve(IHTTPSession session) { + return Response.newFixedLengthResponse(Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found"); + } + + /** + * Pluggable strategy for asynchronously executing requests. + * + * @param asyncRunner + * new strategy for handling threads. + */ + public void setAsyncRunner(IAsyncRunner asyncRunner) { + this.asyncRunner = asyncRunner; + } + + /** + * Pluggable strategy for creating and cleaning up temporary files. + * + * @param tempFileManagerFactory + * new strategy for handling temp files. + */ + public void setTempFileManagerFactory(IFactory tempFileManagerFactory) { + this.tempFileManagerFactory = tempFileManagerFactory; + } + + /** + * Start the server. + * + * @throws IOException + * if the socket is in use. + */ + public void start() throws IOException { + start(NanoHTTPD.SOCKET_READ_TIMEOUT); + } + + /** + * Starts the server (in setDaemon(true) mode). + */ + public void start(final int timeout) throws IOException { + start(timeout, true); + } + + /** + * Start the server. + * + * @param timeout + * timeout to use for socket connections. + * @param daemon + * start the thread daemon or not. + * @throws IOException + * if the socket is in use. + */ + public void start(final int timeout, boolean daemon) throws IOException { + this.myServerSocket = this.getServerSocketFactory().create(); + this.myServerSocket.setReuseAddress(true); + + ServerRunnable serverRunnable = createServerRunnable(timeout); + this.myThread = new Thread(serverRunnable); + this.myThread.setDaemon(daemon); + this.myThread.setName("NanoHttpd Main Listener"); + this.myThread.start(); + while (!serverRunnable.hasBinded() && serverRunnable.getBindException() == null) { + try { + Thread.sleep(10L); + } catch (Throwable e) { + // on android this may not be allowed, that's why we + // catch throwable the wait should be very short because we are + // just waiting for the bind of the socket + } + } + if (serverRunnable.getBindException() != null) { + throw serverRunnable.getBindException(); + } + } + + /** + * Stop the server. + */ + public void stop() { + try { + safeClose(this.myServerSocket); + this.asyncRunner.closeAll(); + if (this.myThread != null) { + this.myThread.join(); + } + } catch (Exception e) { + NanoHTTPD.LOG.log(Level.SEVERE, "Could not stop all connections", e); + } + } + + public final boolean wasStarted() { + return this.myServerSocket != null && this.myThread != null; + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/ServerRunnable.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/ServerRunnable.java new file mode 100644 index 0000000..3ef23de --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/ServerRunnable.java @@ -0,0 +1,90 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.logging.Level; + +/** + * The runnable that will be used for the main listening thread. + */ +public class ServerRunnable implements Runnable { + + private NanoHTTPD httpd; + + private final int timeout; + + private IOException bindException; + + private boolean hasBinded = false; + + public ServerRunnable(NanoHTTPD httpd, int timeout) { + this.httpd = httpd; + this.timeout = timeout; + } + + @Override + public void run() { + try { + httpd.getMyServerSocket().bind(httpd.hostname != null ? new InetSocketAddress(httpd.hostname, httpd.myPort) : new InetSocketAddress(httpd.myPort)); + hasBinded = true; + } catch (IOException e) { + this.bindException = e; + return; + } + do { + try { + final Socket finalAccept = httpd.getMyServerSocket().accept(); + if (this.timeout > 0) { + finalAccept.setSoTimeout(this.timeout); + } + final InputStream inputStream = finalAccept.getInputStream(); + httpd.asyncRunner.exec(httpd.createClientHandler(finalAccept, inputStream)); + } catch (IOException e) { + NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e); + } + } while (!httpd.getMyServerSocket().isClosed()); + } + + public IOException getBindException() { + return bindException; + } + + public boolean hasBinded() { + return hasBinded; + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/content/ContentType.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/content/ContentType.java new file mode 100644 index 0000000..dbe26d4 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/content/ContentType.java @@ -0,0 +1,112 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.content; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ContentType { + + private static final String ASCII_ENCODING = "US-ASCII"; + + private static final String MULTIPART_FORM_DATA_HEADER = "multipart/form-data"; + + private static final String CONTENT_REGEX = "[ |\t]*([^/^ ^;^,]+/[^ ^;^,]+)"; + + private static final Pattern MIME_PATTERN = Pattern.compile(CONTENT_REGEX, Pattern.CASE_INSENSITIVE); + + private static final String CHARSET_REGEX = "[ |\t]*(charset)[ |\t]*=[ |\t]*['|\"]?([^\"^'^;^,]*)['|\"]?"; + + private static final Pattern CHARSET_PATTERN = Pattern.compile(CHARSET_REGEX, Pattern.CASE_INSENSITIVE); + + private static final String BOUNDARY_REGEX = "[ |\t]*(boundary)[ |\t]*=[ |\t]*['|\"]?([^\"^'^;^,]*)['|\"]?"; + + private static final Pattern BOUNDARY_PATTERN = Pattern.compile(BOUNDARY_REGEX, Pattern.CASE_INSENSITIVE); + + private final String contentTypeHeader; + + private final String contentType; + + private final String encoding; + + private final String boundary; + + public ContentType(String contentTypeHeader) { + this.contentTypeHeader = contentTypeHeader; + if (contentTypeHeader != null) { + contentType = getDetailFromContentHeader(contentTypeHeader, MIME_PATTERN, "", 1); + encoding = getDetailFromContentHeader(contentTypeHeader, CHARSET_PATTERN, null, 2); + } else { + contentType = ""; + encoding = "UTF-8"; + } + if (MULTIPART_FORM_DATA_HEADER.equalsIgnoreCase(contentType)) { + boundary = getDetailFromContentHeader(contentTypeHeader, BOUNDARY_PATTERN, null, 2); + } else { + boundary = null; + } + } + + private String getDetailFromContentHeader(String contentTypeHeader, Pattern pattern, String defaultValue, int group) { + Matcher matcher = pattern.matcher(contentTypeHeader); + return matcher.find() ? matcher.group(group) : defaultValue; + } + + public String getContentTypeHeader() { + return contentTypeHeader; + } + + public String getContentType() { + return contentType; + } + + public String getEncoding() { + return encoding == null ? ASCII_ENCODING : encoding; + } + + public String getBoundary() { + return boundary; + } + + public boolean isMultipart() { + return MULTIPART_FORM_DATA_HEADER.equalsIgnoreCase(contentType); + } + + public ContentType tryUTF8() { + if (encoding == null) { + return new ContentType(this.contentTypeHeader + "; charset=UTF-8"); + } + return this; + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/content/Cookie.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/content/Cookie.java new file mode 100644 index 0000000..b537faf --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/content/Cookie.java @@ -0,0 +1,78 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.content; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Locale; +import java.util.TimeZone; + +/** + * A simple cookie representation. This is old code and is flawed in many ways. + * + * @author LordFokas + */ +public class Cookie { + + public static String getHTTPTime(int days) { + Calendar calendar = Calendar.getInstance(); + SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + calendar.add(Calendar.DAY_OF_MONTH, days); + return dateFormat.format(calendar.getTime()); + } + + private final String n, v, e; + + public Cookie(String name, String value) { + this(name, value, 30); + } + + public Cookie(String name, String value, int numDays) { + this.n = name; + this.v = value; + this.e = getHTTPTime(numDays); + } + + public Cookie(String name, String value, String expires) { + this.n = name; + this.v = value; + this.e = expires; + } + + public String getHTTPHeader() { + String fmt = "%s=%s; expires=%s"; + return String.format(fmt, this.n, this.v, this.e); + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/content/CookieHandler.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/content/CookieHandler.java new file mode 100644 index 0000000..26822fa --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/content/CookieHandler.java @@ -0,0 +1,127 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.content; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.response.Response; + +/** + * Provides rudimentary support for cookies. Doesn't support 'path', 'secure' + * nor 'httpOnly'. Feel free to improve it and/or add unsupported features. This + * is old code and it's flawed in many ways. + * + * @author LordFokas + */ +public class CookieHandler implements Iterable { + + private final HashMap cookies = new HashMap(); + + private final ArrayList queue = new ArrayList(); + + public CookieHandler(Map httpHeaders) { + String raw = httpHeaders.get("cookie"); + if (raw != null) { + String[] tokens = raw.split(";"); + for (String token : tokens) { + String[] data = token.trim().split("="); + if (data.length == 2) { + this.cookies.put(data[0], data[1]); + } + } + } + } + + /** + * Set a cookie with an expiration date from a month ago, effectively + * deleting it on the client side. + * + * @param name + * The cookie name. + */ + public void delete(String name) { + set(name, "-delete-", -30); + } + + @Override + public Iterator iterator() { + return this.cookies.keySet().iterator(); + } + + /** + * Read a cookie from the HTTP Headers. + * + * @param name + * The cookie's name. + * @return The cookie's value if it exists, null otherwise. + */ + public String read(String name) { + return this.cookies.get(name); + } + + public void set(Cookie cookie) { + this.queue.add(cookie); + } + + /** + * Sets a cookie. + * + * @param name + * The cookie's name. + * @param value + * The cookie's value. + * @param expires + * How many days until the cookie expires. + */ + public void set(String name, String value, int expires) { + this.queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires))); + } + + /** + * Internally used by the webserver to add all queued cookies into the + * Response's HTTP Headers. + * + * @param response + * The Response object to which headers the queued cookies will + * be added. + */ + public void unloadQueue(Response response) { + for (Cookie cookie : this.queue) { + response.addCookieHeader(cookie.getHTTPHeader()); + } + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/request/Method.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/request/Method.java new file mode 100644 index 0000000..3eb9439 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/request/Method.java @@ -0,0 +1,122 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.request; + +import java.util.HashMap; +import java.util.Map; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +/** + * Represents an HTTP request method/verb. This class includes predefined instances for commonly + * used verbs, but it is not an enum, because HTTP allows other verbs to be used as long as the + * client and server both agree they are valid. + */ +public final class Method { + private String name; + + private Method(String name) { + this.name = name; + } + + private static Map BUILTINS = new HashMap<>(); + private static Method makeBuiltin(String name) { + Method m = new Method(name); + BUILTINS.put(name, m); + return m; + } + + public static Method + GET = makeBuiltin("GET"), + PUT = makeBuiltin("PUT"), + POST = makeBuiltin("POST"), + DELETE = makeBuiltin("DELETE"), + HEAD = makeBuiltin("HEAD"), + OPTIONS = makeBuiltin("OPTIONS"), + TRACE = makeBuiltin("TRACE"), + CONNECT = makeBuiltin("CONNECT"), + PATCH = makeBuiltin("PATCH"), + PROPFIND = makeBuiltin("PROPFIND"), + PROPPATCH = makeBuiltin("PROPPATCH"), + MKCOL = makeBuiltin("MKCOL"), + MOVE = makeBuiltin("MOVE"), + LOCK = makeBuiltin("LOCK"), + UNLOCK = makeBuiltin("UNLOCK"), + NOTIFY = makeBuiltin("NOTIFY"), + SUBSCRIBE = makeBuiltin("SUBSCRIBE"); + + /** + * Returns a Method instance for the given name, as long as it is syntactically valid. + * @param name the method name as a string + * @return a Method, or null if not valid + */ + public static Method lookup(String name) { + if (name == null) { + return null; + } + Method m = BUILTINS.get(name); + if (m != null) { + return m; + } + return isValid(name) ? new Method(name) : null; + } + + private static boolean isValid(String name) { + // allowable character set is the same as for any "token" in HTTP: no control chars or separators + for (int i = 0; i < name.length(); i++) { + char ch = name.charAt(i); + if (ch <= ' ' || "()<>@,;:\\\"/[]?={}".contains(String.valueOf(ch))) { + return false; + } + } + return true; + } + + public String name() { // for backward compatibility with code that treated this as an enum + return name; + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object other) { + return other instanceof Method && ((Method)other).name.equals(name); + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/response/ChunkedOutputStream.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/response/ChunkedOutputStream.java new file mode 100644 index 0000000..004b17d --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/response/ChunkedOutputStream.java @@ -0,0 +1,76 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.response; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Output stream that will automatically send every write to the wrapped + * OutputStream according to chunked transfer: + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1 + */ +public class ChunkedOutputStream extends FilterOutputStream { + + public ChunkedOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(int b) throws IOException { + byte[] data = { + (byte) b + }; + write(data, 0, 1); + } + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (len == 0) + return; + out.write(String.format("%x\r\n", len).getBytes()); + out.write(b, off, len); + out.write("\r\n".getBytes()); + } + + public void finish() throws IOException { + out.write("0\r\n\r\n".getBytes()); + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/response/IStatus.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/response/IStatus.java new file mode 100644 index 0000000..c7657cb --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/response/IStatus.java @@ -0,0 +1,41 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.response; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +public interface IStatus { + + String getDescription(); + + int getRequestStatus(); +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/response/Response.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/response/Response.java new file mode 100644 index 0000000..b42392a --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/response/Response.java @@ -0,0 +1,448 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.response; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TimeZone; +import java.util.logging.Level; +import java.util.zip.GZIPOutputStream; + +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.NanoHTTPD; +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.content.ContentType; +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.request.Method; + +/** + * HTTP response. Return one of these from serve(). + */ +public class Response implements Closeable { + + /** + * HTTP status code after processing, e.g. "200 OK", Status.OK + */ + private IStatus status; + + /** + * MIME type of content, e.g. "text/html" + */ + private String mimeType; + + /** + * Data of the response, may be null. + */ + private InputStream data; + + private long contentLength; + + /** + * Headers for the HTTP response. Use addHeader() to add lines. the + * lowercase map is automatically kept up to date. + */ + @SuppressWarnings("serial") + private final Map header = new HashMap() { + + public String put(String key, String value) { + lowerCaseHeader.put(key == null ? key : key.toLowerCase(), value); + return super.put(key, value); + }; + }; + + /** + * copy of the header map with all the keys lowercase for faster searching. + */ + private final Map lowerCaseHeader = new HashMap(); + + /** + * The request method that spawned this response. + */ + private Method requestMethod; + + /** + * Use chunkedTransfer + */ + private boolean chunkedTransfer; + + private boolean keepAlive; + + private List cookieHeaders; + + private GzipUsage gzipUsage = GzipUsage.DEFAULT; + + private static enum GzipUsage { + DEFAULT, + ALWAYS, + NEVER; + } + + /** + * Creates a fixed length response if totalBytes>=0, otherwise chunked. + */ + @SuppressWarnings({ + "rawtypes", + "unchecked" + }) + protected Response(IStatus status, String mimeType, InputStream data, long totalBytes) { + this.status = status; + this.mimeType = mimeType; + if (data == null) { + this.data = new ByteArrayInputStream(new byte[0]); + this.contentLength = 0L; + } else { + this.data = data; + this.contentLength = totalBytes; + } + this.chunkedTransfer = this.contentLength < 0; + this.keepAlive = true; + this.cookieHeaders = new ArrayList(10); + } + + @Override + public void close() throws IOException { + if (this.data != null) { + this.data.close(); + } + } + + /** + * Adds a cookie header to the list. Should not be called manually, this is + * an internal utility. + */ + public void addCookieHeader(String cookie) { + cookieHeaders.add(cookie); + } + + /** + * Should not be called manually. This is an internally utility for JUnit + * test purposes. + * + * @return All unloaded cookie headers. + */ + public List getCookieHeaders() { + return cookieHeaders; + } + + /** + * Adds given line to the header. + */ + public void addHeader(String name, String value) { + this.header.put(name, value); + } + + /** + * Indicate to close the connection after the Response has been sent. + * + * @param close + * {@code true} to hint connection closing, {@code false} to let + * connection be closed by client. + */ + public void closeConnection(boolean close) { + if (close) + this.header.put("connection", "close"); + else + this.header.remove("connection"); + } + + /** + * @return {@code true} if connection is to be closed after this Response + * has been sent. + */ + public boolean isCloseConnection() { + return "close".equals(getHeader("connection")); + } + + public InputStream getData() { + return this.data; + } + + public String getHeader(String name) { + return this.lowerCaseHeader.get(name.toLowerCase()); + } + + public String getMimeType() { + return this.mimeType; + } + + public Method getRequestMethod() { + return this.requestMethod; + } + + public IStatus getStatus() { + return this.status; + } + + public void setKeepAlive(boolean useKeepAlive) { + this.keepAlive = useKeepAlive; + } + + /** + * Sends given response to the socket. + */ + public void send(OutputStream outputStream) { + SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); + gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT")); + + try { + if (this.status == null) { + throw new Error("sendResponse(): Status can't be null."); + } + PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, new ContentType(this.mimeType).getEncoding())), false); + pw.append("HTTP/1.1 ").append(this.status.getDescription()).append(" \r\n"); + if (this.mimeType != null) { + printHeader(pw, "Content-Type", this.mimeType); + } + if (getHeader("date") == null) { + printHeader(pw, "Date", gmtFrmt.format(new Date())); + } + for (Entry entry : this.header.entrySet()) { + printHeader(pw, entry.getKey(), entry.getValue()); + } + for (String cookieHeader : this.cookieHeaders) { + printHeader(pw, "Set-Cookie", cookieHeader); + } + if (getHeader("connection") == null) { + printHeader(pw, "Connection", (this.keepAlive ? "keep-alive" : "close")); + } + if (getHeader("content-length") != null) { + setUseGzip(false); + } + if (useGzipWhenAccepted()) { + printHeader(pw, "Content-Encoding", "gzip"); + setChunkedTransfer(true); + } + long pending = this.data != null ? this.contentLength : 0; + if (this.requestMethod != Method.HEAD && this.chunkedTransfer) { + printHeader(pw, "Transfer-Encoding", "chunked"); + } else if (!useGzipWhenAccepted()) { + pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, pending); + } + pw.append("\r\n"); + pw.flush(); + sendBodyWithCorrectTransferAndEncoding(outputStream, pending); + outputStream.flush(); + NanoHTTPD.safeClose(this.data); + } catch (IOException ioe) { + NanoHTTPD.LOG.log(Level.SEVERE, "Could not send response to the client", ioe); + } + } + + @SuppressWarnings("static-method") + protected void printHeader(PrintWriter pw, String key, String value) { + pw.append(key).append(": ").append(value).append("\r\n"); + } + + protected long sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, long defaultSize) { + String contentLengthString = getHeader("content-length"); + long size = defaultSize; + if (contentLengthString != null) { + try { + size = Long.parseLong(contentLengthString); + } catch (NumberFormatException ex) { + NanoHTTPD.LOG.severe("content-length was no number " + contentLengthString); + } + }else{ + pw.print("Content-Length: " + size + "\r\n"); + } + return size; + } + + private void sendBodyWithCorrectTransferAndEncoding(OutputStream outputStream, long pending) throws IOException { + if (this.requestMethod != Method.HEAD && this.chunkedTransfer) { + ChunkedOutputStream chunkedOutputStream = new ChunkedOutputStream(outputStream); + sendBodyWithCorrectEncoding(chunkedOutputStream, -1); + try { + chunkedOutputStream.finish(); + } catch (Exception e) { + if(this.data != null) { + this.data.close(); + } + } + } else { + sendBodyWithCorrectEncoding(outputStream, pending); + } + } + + private void sendBodyWithCorrectEncoding(OutputStream outputStream, long pending) throws IOException { + if (useGzipWhenAccepted()) { + GZIPOutputStream gzipOutputStream = null; + try { + gzipOutputStream = new GZIPOutputStream(outputStream); + } catch (Exception e) { + if(this.data != null) { + this.data.close(); + } + } + if (gzipOutputStream != null) { + sendBody(gzipOutputStream, -1); + gzipOutputStream.finish(); + } + } else { + sendBody(outputStream, pending); + } + } + + /** + * Sends the body to the specified OutputStream. The pending parameter + * limits the maximum amounts of bytes sent unless it is -1, in which case + * everything is sent. + * + * @param outputStream + * the OutputStream to send data to + * @param pending + * -1 to send everything, otherwise sets a max limit to the + * number of bytes sent + * @throws IOException + * if something goes wrong while sending the data. + */ + private void sendBody(OutputStream outputStream, long pending) throws IOException { + long BUFFER_SIZE = 16 * 1024; + byte[] buff = new byte[(int) BUFFER_SIZE]; + boolean sendEverything = pending == -1; + while (pending > 0 || sendEverything) { + long bytesToRead = sendEverything ? BUFFER_SIZE : Math.min(pending, BUFFER_SIZE); + int read = this.data.read(buff, 0, (int) bytesToRead); + if (read <= 0) { + break; + } + try { + outputStream.write(buff, 0, read); + } catch (Exception e) { + if(this.data != null) { + this.data.close(); + } + } + if (!sendEverything) { + pending -= read; + } + } + } + + public void setChunkedTransfer(boolean chunkedTransfer) { + this.chunkedTransfer = chunkedTransfer; + } + + public void setData(InputStream data) { + this.data = data; + } + + public void setMimeType(String mimeType) { + this.mimeType = mimeType; + } + + public void setRequestMethod(Method requestMethod) { + this.requestMethod = requestMethod; + } + + public void setStatus(IStatus status) { + this.status = status; + } + + /** + * Create a response with unknown length (using HTTP 1.1 chunking). + */ + public static Response newChunkedResponse(IStatus status, String mimeType, InputStream data) { + return new Response(status, mimeType, data, -1); + } + + public static Response newFixedLengthResponse(IStatus status, String mimeType, byte[] data) { + return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(data), data.length); + } + + /** + * Create a response with known length. + */ + public static Response newFixedLengthResponse(IStatus status, String mimeType, InputStream data, long totalBytes) { + return new Response(status, mimeType, data, totalBytes); + } + + /** + * Create a text response with known length. + */ + public static Response newFixedLengthResponse(IStatus status, String mimeType, String txt) { + ContentType contentType = new ContentType(mimeType); + if (txt == null) { + return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(new byte[0]), 0); + } else { + byte[] bytes; + try { + CharsetEncoder newEncoder = Charset.forName(contentType.getEncoding()).newEncoder(); + if (!newEncoder.canEncode(txt)) { + contentType = contentType.tryUTF8(); + } + bytes = txt.getBytes(contentType.getEncoding()); + } catch (UnsupportedEncodingException e) { + NanoHTTPD.LOG.log(Level.SEVERE, "encoding problem, responding nothing", e); + bytes = new byte[0]; + } + return newFixedLengthResponse(status, contentType.getContentTypeHeader(), new ByteArrayInputStream(bytes), bytes.length); + } + } + + /** + * Create a text response with known length. + */ + public static Response newFixedLengthResponse(String msg) { + return newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_HTML, msg); + } + + public Response setUseGzip(boolean useGzip) { + gzipUsage = useGzip ? GzipUsage.ALWAYS : GzipUsage.NEVER; + return this; + } + + // If a Gzip usage has been enforced, use it. + // Else decide whether or not to use Gzip. + public boolean useGzipWhenAccepted() { + if (gzipUsage == GzipUsage.DEFAULT) + return getMimeType() != null && (getMimeType().toLowerCase().contains("text/") || getMimeType().toLowerCase().contains("/json")); + else + return gzipUsage == GzipUsage.ALWAYS; + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/response/Status.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/response/Status.java new file mode 100644 index 0000000..9a6ff33 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/response/Status.java @@ -0,0 +1,111 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.response; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +/** + * Some HTTP response status codes + */ +public enum Status implements IStatus { + SWITCH_PROTOCOL(101, "Switching Protocols"), + + OK(200, "OK"), + CREATED(201, "Created"), + ACCEPTED(202, "Accepted"), + NO_CONTENT(204, "No Content"), + PARTIAL_CONTENT(206, "Partial Content"), + MULTI_STATUS(207, "Multi-Status"), + + REDIRECT(301, "Moved Permanently"), + /** + * Many user agents mishandle 302 in ways that violate the RFC1945 spec + * (i.e., redirect a POST to a GET). 303 and 307 were added in RFC2616 to + * address this. You should prefer 303 and 307 unless the calling user agent + * does not support 303 and 307 functionality + */ + @Deprecated + FOUND(302, "Found"), + REDIRECT_SEE_OTHER(303, "See Other"), + NOT_MODIFIED(304, "Not Modified"), + TEMPORARY_REDIRECT(307, "Temporary Redirect"), + + BAD_REQUEST(400, "Bad Request"), + UNAUTHORIZED(401, "Unauthorized"), + FORBIDDEN(403, "Forbidden"), + NOT_FOUND(404, "Not Found"), + METHOD_NOT_ALLOWED(405, "Method Not Allowed"), + NOT_ACCEPTABLE(406, "Not Acceptable"), + REQUEST_TIMEOUT(408, "Request Timeout"), + CONFLICT(409, "Conflict"), + GONE(410, "Gone"), + LENGTH_REQUIRED(411, "Length Required"), + PRECONDITION_FAILED(412, "Precondition Failed"), + PAYLOAD_TOO_LARGE(413, "Payload Too Large"), + UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"), + RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"), + EXPECTATION_FAILED(417, "Expectation Failed"), + TOO_MANY_REQUESTS(429, "Too Many Requests"), + + INTERNAL_ERROR(500, "Internal Server Error"), + NOT_IMPLEMENTED(501, "Not Implemented"), + SERVICE_UNAVAILABLE(503, "Service Unavailable"), + UNSUPPORTED_HTTP_VERSION(505, "HTTP Version Not Supported"); + + private final int requestStatus; + + private final String description; + + Status(int requestStatus, String description) { + this.requestStatus = requestStatus; + this.description = description; + } + + public static Status lookup(int requestStatus) { + for (Status status : Status.values()) { + if (status.getRequestStatus() == requestStatus) { + return status; + } + } + return null; + } + + @Override + public String getDescription() { + return "" + this.requestStatus + " " + this.description; + } + + @Override + public int getRequestStatus() { + return this.requestStatus; + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/sockets/DefaultServerSocketFactory.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/sockets/DefaultServerSocketFactory.java new file mode 100644 index 0000000..2bfebbb --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/sockets/DefaultServerSocketFactory.java @@ -0,0 +1,51 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.sockets; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.io.IOException; +import java.net.ServerSocket; + +import com.launchdarkly.testhelpers.httptest.nanohttpd.util.IFactoryThrowing; + +/** + * Creates a normal ServerSocket for TCP connections + */ +public class DefaultServerSocketFactory implements IFactoryThrowing { + + @Override + public ServerSocket create() throws IOException { + return new ServerSocket(); + } + +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/sockets/SecureServerSocketFactory.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/sockets/SecureServerSocketFactory.java new file mode 100644 index 0000000..e57df5c --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/sockets/SecureServerSocketFactory.java @@ -0,0 +1,73 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.sockets; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.io.IOException; +import java.net.ServerSocket; + +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLServerSocketFactory; + +import com.launchdarkly.testhelpers.httptest.nanohttpd.util.IFactoryThrowing; + +/** + * Creates a new SSLServerSocket + */ +public class SecureServerSocketFactory implements IFactoryThrowing { + + private SSLServerSocketFactory sslServerSocketFactory; + + private String[] sslProtocols; + + public SecureServerSocketFactory(SSLServerSocketFactory sslServerSocketFactory, String[] sslProtocols) { + this.sslServerSocketFactory = sslServerSocketFactory; + this.sslProtocols = sslProtocols; + } + + @Override + public ServerSocket create() throws IOException { + SSLServerSocket ss = null; + ss = (SSLServerSocket) this.sslServerSocketFactory.createServerSocket(); + if (this.sslProtocols != null) { + ss.setEnabledProtocols(this.sslProtocols); + } else { + ss.setEnabledProtocols(ss.getSupportedProtocols()); + } + ss.setUseClientMode(false); + ss.setWantClientAuth(false); + ss.setNeedClientAuth(false); + return ss; + } + +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/tempfiles/DefaultTempFile.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/tempfiles/DefaultTempFile.java new file mode 100644 index 0000000..9c38799 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/tempfiles/DefaultTempFile.java @@ -0,0 +1,79 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.tempfiles; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.NanoHTTPD; + +/** + * Default strategy for creating and cleaning up temporary files. + *

+ *

+ * By default, files are created by File.createTempFile() in the + * directory specified. + *

+ */ +public class DefaultTempFile implements ITempFile { + + private final File file; + + private final OutputStream fstream; + + public DefaultTempFile(File tempdir) throws IOException { + this.file = File.createTempFile("NanoHTTPD-", "", tempdir); + this.fstream = new FileOutputStream(this.file); + } + + @Override + public void delete() throws Exception { + NanoHTTPD.safeClose(this.fstream); + if (!this.file.delete()) { + throw new Exception("could not delete temporary file: " + this.file.getAbsolutePath()); + } + } + + @Override + public String getName() { + return this.file.getAbsolutePath(); + } + + @Override + public OutputStream open() throws Exception { + return this.fstream; + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/tempfiles/DefaultTempFileManager.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/tempfiles/DefaultTempFileManager.java new file mode 100644 index 0000000..f1a4a3e --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/tempfiles/DefaultTempFileManager.java @@ -0,0 +1,85 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.tempfiles; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; + +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.NanoHTTPD; + +/** + * Default strategy for creating and cleaning up temporary files. + *

+ *

+ * This class stores its files in the standard location (that is, wherever + * java.io.tmpdir points to). Files are added to an internal list, + * and deleted when no longer needed (that is, when clear() is + * invoked at the end of processing a request). + *

+ */ +public class DefaultTempFileManager implements ITempFileManager { + + private final File tmpdir; + + private final List tempFiles; + + public DefaultTempFileManager() { + this.tmpdir = new File(System.getProperty("java.io.tmpdir")); + if (!tmpdir.exists()) { + tmpdir.mkdirs(); + } + this.tempFiles = new ArrayList(); + } + + @Override + public void clear() { + for (ITempFile file : this.tempFiles) { + try { + file.delete(); + } catch (Exception ignored) { + NanoHTTPD.LOG.log(Level.WARNING, "could not delete file ", ignored); + } + } + this.tempFiles.clear(); + } + + @Override + public ITempFile createTempFile(String filename_hint) throws Exception { + DefaultTempFile tempFile = new DefaultTempFile(this.tmpdir); + this.tempFiles.add(tempFile); + return tempFile; + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/tempfiles/DefaultTempFileManagerFactory.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/tempfiles/DefaultTempFileManagerFactory.java new file mode 100644 index 0000000..76112fa --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/tempfiles/DefaultTempFileManagerFactory.java @@ -0,0 +1,47 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.tempfiles; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import com.launchdarkly.testhelpers.httptest.nanohttpd.util.IFactory; + +/** + * Default strategy for creating and cleaning up temporary files. + */ +public class DefaultTempFileManagerFactory implements IFactory { + + @Override + public ITempFileManager create() { + return new DefaultTempFileManager(); + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/tempfiles/ITempFile.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/tempfiles/ITempFile.java new file mode 100644 index 0000000..2da5c7d --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/tempfiles/ITempFile.java @@ -0,0 +1,53 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.tempfiles; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.io.OutputStream; + +/** + * A temp file. + *

+ *

+ * Temp files are responsible for managing the actual temporary storage and + * cleaning themselves up when no longer needed. + *

+ */ +public interface ITempFile { + + public void delete() throws Exception; + + public String getName(); + + public OutputStream open() throws Exception; +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/tempfiles/ITempFileManager.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/tempfiles/ITempFileManager.java new file mode 100644 index 0000000..563cfa1 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/tempfiles/ITempFileManager.java @@ -0,0 +1,49 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.tempfiles; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +/** + * Temp file manager. + *

+ *

+ * Temp file managers are created 1-to-1 with incoming requests, to create and + * cleanup temporary files created as a result of handling the request. + *

+ */ +public interface ITempFileManager { + + void clear(); + + public ITempFile createTempFile(String filename_hint) throws Exception; +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/threading/DefaultAsyncRunner.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/threading/DefaultAsyncRunner.java new file mode 100644 index 0000000..3ffea99 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/threading/DefaultAsyncRunner.java @@ -0,0 +1,90 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.threading; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.ClientHandler; + +/** + * Default threading strategy for NanoHTTPD. + *

+ *

+ * By default, the server spawns a new Thread for every incoming request. These + * are set to daemon status, and named according to the request number. + * The name is useful when profiling the application. + *

+ */ +public class DefaultAsyncRunner implements IAsyncRunner { + + protected long requestCount; + + private final List running = Collections.synchronizedList(new ArrayList()); + + /** + * @return a list with currently running clients. + */ + public List getRunning() { + return running; + } + + @Override + public void closeAll() { + // copy of the list for concurrency + for (ClientHandler clientHandler : new ArrayList(this.running)) { + clientHandler.close(); + } + } + + @Override + public void closed(ClientHandler clientHandler) { + this.running.remove(clientHandler); + } + + @Override + public void exec(ClientHandler clientHandler) { + ++this.requestCount; + this.running.add(clientHandler); + createThread(clientHandler).start(); + } + + protected Thread createThread(ClientHandler clientHandler) { + Thread t = new Thread(clientHandler); + t.setDaemon(true); + t.setName("NanoHttpd Request Processor (#" + this.requestCount + ")"); + return t; + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/threading/IAsyncRunner.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/threading/IAsyncRunner.java new file mode 100644 index 0000000..7a6350b --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/protocols/http/threading/IAsyncRunner.java @@ -0,0 +1,48 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.threading; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.ClientHandler; + +/** + * Pluggable strategy for asynchronously executing requests. + */ +public interface IAsyncRunner { + + void closeAll(); + + void closed(ClientHandler clientHandler); + + void exec(ClientHandler code); +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/util/IFactory.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/util/IFactory.java new file mode 100644 index 0000000..5c11969 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/util/IFactory.java @@ -0,0 +1,46 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.util; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +/** + * Represents a simple factory + * + * @author LordFokas + * @param + * The Type of object to create + */ +public interface IFactory { + + T create(); +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/util/IFactoryThrowing.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/util/IFactoryThrowing.java new file mode 100644 index 0000000..4796667 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/util/IFactoryThrowing.java @@ -0,0 +1,49 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.util; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +/** + * Represents a factory that can throw an exception instead of actually creating + * an object + * + * @author LordFokas + * @param + * The Type of object to create + * @param + * The base Type of exceptions that can be thrown + */ +public interface IFactoryThrowing { + + T create() throws E; +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/util/IHandler.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/util/IHandler.java new file mode 100644 index 0000000..ce7648f --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/util/IHandler.java @@ -0,0 +1,49 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.util; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2016 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +/** + * Defines a generic handler that returns an object of type O when given an + * object of type I. + * + * @author LordFokas + * @param + * The input type. + * @param + * The output type. + */ +public interface IHandler { + + public O handle(I input); +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/util/ServerRunner.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/util/ServerRunner.java new file mode 100644 index 0000000..82c74ba --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/nanohttpd/util/ServerRunner.java @@ -0,0 +1,75 @@ +package com.launchdarkly.testhelpers.httptest.nanohttpd.util; + +/* + * #%L + * NanoHttpd-Webserver + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.launchdarkly.testhelpers.httptest.nanohttpd.protocols.http.NanoHTTPD; + +public class ServerRunner { + + /** + * logger to log to. + */ + private static final Logger LOG = Logger.getLogger(ServerRunner.class.getName()); + + public static void executeInstance(NanoHTTPD server) { + try { + server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false); + } catch (IOException ioe) { + System.err.println("Couldn't start server:\n" + ioe); + System.exit(-1); + } + + System.out.println("Server started, Hit Enter to stop.\n"); + + try { + System.in.read(); + } catch (Throwable ignored) { + } + + server.stop(); + System.out.println("Server stopped.\n"); + } + + public static void run(Class serverClass) { + try { + executeInstance(serverClass.newInstance()); + } catch (Exception e) { + ServerRunner.LOG.log(Level.SEVERE, "Could not create server", e); + } + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/package-info.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/package-info.java new file mode 100644 index 0000000..8a9af8d --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/httptest/package-info.java @@ -0,0 +1,107 @@ +/** + * A simple portable HTTP test server with response mocking. + *

+ * This package provides a simple abstraction for setting up embedded HTTP test servers + * that return programmed responses, and verifying that the expected requests have been + * made in tests. + *

+ * Although Java has a standard servlet API for representing HTTP requests and responses, + * it does not have a built-in HTTP server implementation. Some other Java libraries + * provide a mockable server, but these have generally not been suitable for LaunchDarkly's + * testing needs due to limitations in their design (for instance, they generally do not + * support streaming responses). + *

+ * This package uses a fork of nanohttpd as its underlying implementation. Only the core + * server implementation is used, without a webapp framework, so it is fairly lightweight + * and has dependencies other than Java 7 or above. However, this implementation has only + * been validated for server-side Java; it may not work correctly in Android. + *

+ * It is possible to build a simple web service with this package, but it should not be used + * for production services. + *

+ * An {@link com.launchdarkly.testhelpers.httptest.HttpServer} is an HTTP server that + * starts listening on an arbitrarily chosen port as soon as you create it. You should + * normally do this inside a try-with-resources block to ensure that the server is shut + * down when you're done with it. The server's {@link com.launchdarkly.testhelpers.httptest.HttpServer#getUri()} + * method gives you the address for making your test requests. + *

+ * You configure the server with a single {@link com.launchdarkly.testhelpers.httptest.Handler} + * that receives all requests. The library provides a variety of handler implementations and + * combinators, or you can define your own. + *

+ * Examples + *

+ *

+ * 1. Invariant response with error status + *


+ *     HttpServer server = HttpServer.start(Handlers.status(500));
+ * 
+ *

+ * 2. Invariant response with status, headers, and body + *


+ *     HttpServer server = HttpServer.start(
+ *         Handlers.all(
+ *             Handlers.status(202),
+ *             Handlers.header("Etag", "123"),
+ *             Handlers.bodyString("text/plain", "thanks")
+ *         )
+ *     );
+ * 
+ *

+ * 3. Verifying requests made to the server + *


+ *     try (HttpServer server = HttpServer.start(Handlers.status(200))) {
+ *         doSomethingThatMakesARequest(server.getUri());
+ *         doSomethingElseThatMakesARequest(server.getUri());
+ *         
+ *         RequestInfo request1 = server.getRecorder().requireRequest();
+ *         assertEquals("/path1", request1.getPath());
+ *         
+ *         RequestInfo request2 = server.getRecorder().requireRequest();
+ *         assertEquals("/path2", request2.getPath());
+ *     }
+ * 
+ *

+ * 4. Response with custom logic depending on the request + *


+ *     HttpServer server = HttpServer.start(
+ *         ctx -> {
+ *             if (ctx.getRequest().getHeader("Header-Name").equals("good-value")) {
+ *                 Handlers.status(200).apply(ctx);
+ *             } else {
+ *                 Handlers.status(400).apply(ctx);
+ *             }
+ *         }
+ *     );
+ * 
+ *

+ * 5. Simple routing to simulate two endpoints + *


+ *     SimpleRouter router = new SimpleRouter();
+ *     router.add("/path1", Handlers.status(200));
+ *     router.add("/path2", Handlers.status(500));
+ *     HttpServer server = HttpServer.start(router);
+ * 
+ *

+ * 6. Programmed sequence of responses + *


+ *     HttpServer server = HttpServer.start(
+ *         Handlers.sequential(
+ *             Handlers.status(200), // first request gets a 200
+ *             Handlers.status(500)  // next request gets a 500
+ *         )
+ *     );
+ * 
+ *

+ * 7. Changing server behavior during a test + *


+ *     HandlerSwitcher switcher = new HandlerSwitcher(Handlers.status(200));
+ *     try (HttpServer server = HttpServer.start(switcher) {
+ *         // Initially the server returns 200 for all requests
+ *         
+ *         switcher.setTarget(Handlers.status(500));
+ *         // Now the server returns 500 for all requests
+ *     }
+ * 
+ */ +package com.launchdarkly.testhelpers.httptest; diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/tcptest/TcpHandler.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/tcptest/TcpHandler.java new file mode 100644 index 0000000..a16c72d --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/tcptest/TcpHandler.java @@ -0,0 +1,19 @@ +package com.launchdarkly.testhelpers.tcptest; + +import java.io.IOException; +import java.net.Socket; + +/** + * Use with {@link TcpServer} to define behavior for a TCP endpoint in a test. + * + * @since 1.3.0 + */ +public interface TcpHandler { + /** + * Processes the request. + * + * @param socket the incoming socket + * @throws IOException for any I/O error + */ + void apply(Socket socket) throws IOException; +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/tcptest/TcpHandlers.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/tcptest/TcpHandlers.java new file mode 100644 index 0000000..347ada8 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/tcptest/TcpHandlers.java @@ -0,0 +1,137 @@ +package com.launchdarkly.testhelpers.tcptest; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Factory methods for standard {@link TcpHandler} implementations. + * + * @since 1.3.0 + */ +public abstract class TcpHandlers { + private TcpHandlers() {} + + /** + * Creates an implementation of {@link TcpHandler} that writes some data to the socket. + * + * @param data the data buffer + * @param offset the starting offset + * @param length the number of bytes to write + * @return a handler + */ + public static TcpHandler writeData(final byte[] data, final int offset, final int length) { + return new TcpHandler() { + @Override + public void apply(Socket socket) throws IOException { + socket.getOutputStream().write(data, offset, length); + } + }; + } + + /** + * Creates an implementation of {@link TcpHandler} that writes a UTF-8 string to the socket. + * + * @param s the string + * @return a handler + */ + public static TcpHandler writeString(String s) { + byte[] data = s.getBytes(Charset.forName("UTF-8")); + return writeData(data, 0, data.length); + } + + /** + * Creates an implementation of {@link TcpHandler} that, for each incoming request, opens + * a socket connection to the specified port and then forwards all traffic from the incoming + * request to that socket, and vice versa. + * + * @param forwardToPort the port to forward to + * @return a handler + */ + public static TcpHandler forwardToPort(final int forwardToPort) { + return new TcpHandler() { + @Override + public void apply(Socket incomingSocket) throws IOException { + InputStream incomingSocketRead = incomingSocket.getInputStream(); + OutputStream incomingSocketWrite = incomingSocket.getOutputStream(); + try (Socket forwardedSocket = new Socket(incomingSocket.getInetAddress().getHostAddress(), forwardToPort)) { + InputStream forwardedSocketRead = forwardedSocket.getInputStream(); + OutputStream forwardedSocketWrite = forwardedSocket.getOutputStream(); + final CountDownLatch closeSignal = new CountDownLatch(1); + new Thread(newForwarder(incomingSocketRead, forwardedSocketWrite, closeSignal)).start(); + new Thread(newForwarder(forwardedSocketRead, incomingSocketWrite, closeSignal)).start(); + try { + closeSignal.await(); + } catch (InterruptedException e) {} + } + } + }; + } + + private static Runnable newForwarder(InputStream fromStream, OutputStream toStream, CountDownLatch closeSignal) { + return new Runnable() { + @Override + public void run() { + byte[] buffer = new byte[1000]; + while (true) { + try { + int n = fromStream.read(buffer); + if (n < 0) { + break; + } + toStream.write(buffer, 0, n); + toStream.flush(); + } catch (IOException e) { + break; + } + } + closeSignal.countDown(); + } + }; + } + + /** + * Returns an implementation of {@link TcpHandler} that immediately exits, so that + * {@link TcpServer} will close the socket with no response. + *

+ * A typical use case would be to simulate an I/O error when testing client code that is + * making HTTP requests; if the (simulated) HTTP server closes the socket without writing + * a response, clients will treat this as a broken connection error. + * + * @return a handler + */ + public static TcpHandler noResponse() { + return new TcpHandler() { + @Override + public void apply(Socket socket) {} + }; + } + + /** + * Creates a stateful {@link TcpHandler} that delegates to each of the specified handlers in sequence + * as each request is received. + * + * @param handlers a sequence of handlers + * @return a handler + */ + public static TcpHandler sequential(TcpHandler... handlers) { + final AtomicInteger index = new AtomicInteger(0); + final TcpHandler[] h = Arrays.copyOf(handlers, handlers.length); + + return new TcpHandler() { + @Override + public void apply(Socket socket) throws IOException { + int i = index.getAndIncrement(); + if (i >= h.length) { + throw new RuntimeException("received more requests than the number of configured handlers"); + } + h[i].apply(socket); + } + }; + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/tcptest/TcpServer.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/tcptest/TcpServer.java new file mode 100644 index 0000000..c62c7df --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/tcptest/TcpServer.java @@ -0,0 +1,118 @@ +package com.launchdarkly.testhelpers.tcptest; + +import com.launchdarkly.testhelpers.httptest.HttpServer; + +import java.io.Closeable; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URI; +import java.util.Date; + +/** + * A simple mechanism for creating a TCP listener and configuring its behavior. + *

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 G2EG6c=_AMh diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i deleted file mode 100644 index c0655e1ed7c74dfa48344f619f879beab2337d08..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 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 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i.len deleted file mode 100644 index 131e265740f37d77b7c4a3676d2a7704ca3e4a29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8 McmZQz0D%Su009U9fdBvi diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/last-build.bin b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/last-build.bin deleted file mode 100644 index b0f4e6f822c997e6c846fb7e7a88250800f0832a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18 YcmZ4UmVvdLhk=1{7Ec!6KL#KG05MbqKL7v# diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/local-state/build-history.bin b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/local-state/build-history.bin deleted file mode 100644 index 20a873d14184de3f5b5744830f16ed55056b6e21..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31 ccmZ4UmVvcgk^ur385kI6@nrG+gYp>|09gYBO#lD@ diff --git a/lib/shared/test-helpers/buildSrc/build/libs/buildSrc.jar b/lib/shared/test-helpers/buildSrc/build/libs/buildSrc.jar deleted file mode 100644 index 968408cadc1d20b8cd357fb0a834dd45f66b86ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33947 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 diff --git a/lib/shared/test-helpers/buildSrc/build/pluginUnderTestMetadata/plugin-under-test-metadata.properties b/lib/shared/test-helpers/buildSrc/build/pluginUnderTestMetadata/plugin-under-test-metadata.properties deleted file mode 100644 index 47058b9..0000000 --- a/lib/shared/test-helpers/buildSrc/build/pluginUnderTestMetadata/plugin-under-test-metadata.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-classpath=/home/rlamb/tmpfs/code/launchdarkly/java-core/lib/shared/test-helpers/buildSrc/build/classes/java/main\:/home/rlamb/tmpfs/code/launchdarkly/java-core/lib/shared/test-helpers/buildSrc/build/classes/groovy/main\:/home/rlamb/tmpfs/code/launchdarkly/java-core/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main\:/home/rlamb/tmpfs/code/launchdarkly/java-core/lib/shared/test-helpers/buildSrc/build/resources/main diff --git a/lib/shared/test-helpers/buildSrc/build/reports/plugin-development/validation-report.txt b/lib/shared/test-helpers/buildSrc/build/reports/plugin-development/validation-report.txt deleted file mode 100644 index e69de29..0000000 diff --git a/lib/shared/test-helpers/buildSrc/build/tmp/jar/MANIFEST.MF b/lib/shared/test-helpers/buildSrc/build/tmp/jar/MANIFEST.MF deleted file mode 100644 index 58630c0..0000000 --- a/lib/shared/test-helpers/buildSrc/build/tmp/jar/MANIFEST.MF +++ /dev/null @@ -1,2 +0,0 @@ -Manifest-Version: 1.0 -

+ * This is analogous to {@link HttpServer}, but much simpler since it has no knowledge of + * any particular protocol that might be used over TCP. See {@link TcpHandlers} for examples + * of configurable behavior. + * + * @since 1.3.0 + */ +public class TcpServer implements Closeable { + private final ServerSocket listener; + private final int listenerPort; + + /** + * Starts a new TCP test server on a specific port. + * + * @param port the port to listen on + * @param handler a {@link TcpHandler} implementation + * @return a server + */ + public static TcpServer start(int port, TcpHandler handler) { + return new TcpServer(port, handler); + } + + /** + * Starts a new TCP test server on any available port. + * + * @param handler a {@link TcpHandler} implementation + * @return a server + */ + public static TcpServer start(TcpHandler handler) { + return new TcpServer(0, handler); + } + + TcpServer(int port, final TcpHandler handler) { + try { + listener = new ServerSocket(port); + } catch (IOException e) { + throw new RuntimeException("unable to create TCP listener", e); + } + listenerPort = port == 0 ? listener.getLocalPort() : port; + + new Thread(new Runnable() { + @Override + public void run() { + while (true) { + final Socket socket; + try { + socket = listener.accept(); + } catch (IOException e) { + // almost certainly means we closed the socket + return; + } + new Thread(new Runnable() { + @Override + public void run() { + try { + handler.apply(socket); + } catch (Exception e) { + logError("handler threw exception: " + e); + } + try { + socket.close(); + } catch (IOException e) { + logError("failed to close socket: " + e); + } + } + }).run(); + } + } + }).start(); + } + + @Override + public void close() { + try { + listener.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns the port we are listening on. + * + * @return the port + */ + public int getPort() { + return listenerPort; + } + + /** + * Convenience method for constructing an HTTP URI with the listener port. This does not + * mean the listener necessarily can accept HTTP requests, but it may be useful if for + * instance you have configured it with {@link TcpHandlers#forwardToPort(int)} to forward + * requests to an {@link HttpServer}. + * + * @return an HTTP URI using localhost and the value of {@link #getPort()} + */ + public URI getHttpUri() { + return URI.create("http://localhost:" + listenerPort); + } + + private void logError(String message) { + System.err.println("TcpServer [" + new Date() + "]: " + message); + } +} diff --git a/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/AssertionsTest.java b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/AssertionsTest.java new file mode 100644 index 0000000..bface7e --- /dev/null +++ b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/AssertionsTest.java @@ -0,0 +1,59 @@ +package com.launchdarkly.testhelpers; + +import org.junit.Test; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; + +@SuppressWarnings("javadoc") +public class AssertionsTest { + @Test + public void assertPolledFunctionReturnsValueSuccessOnFirstTry() { + String value = Assertions.assertPolledFunctionReturnsValue( + 1, TimeUnit.SECONDS, + 10, TimeUnit.MILLISECONDS, + () -> "yes" + ); + assertThat(value, equalTo("yes")); + } + + @Test + public void assertPolledFunctionReturnsValueSuccessOnLaterTry() { + AtomicInteger i = new AtomicInteger(0); + String value = Assertions.assertPolledFunctionReturnsValue( + 200, TimeUnit.MILLISECONDS, + 10, TimeUnit.MILLISECONDS, + () -> { + return i.incrementAndGet() >= 5 ? "yes" : null; + }); + assertThat(value, equalTo("yes")); + } + + @Test + public void assertPolledFunctionReturnsValueFailure() { + AtomicInteger i = new AtomicInteger(0); + requireAssertionError(() -> { + Assertions.assertPolledFunctionReturnsValue( + 200, TimeUnit.MILLISECONDS, + 10, TimeUnit.MILLISECONDS, + () -> { + i.incrementAndGet(); + return null; + }); + }); + assertThat(i.get(), greaterThan(1)); + } + + public static String requireAssertionError(Runnable action) { + try { + action.run(); + throw new AssertionError("expected AssertionError, did not get one"); + } catch (AssertionError e) { + return e.getMessage(); + } + } +} diff --git a/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/ConcurrentHelpersTest.java b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/ConcurrentHelpersTest.java new file mode 100644 index 0000000..245eb9e --- /dev/null +++ b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/ConcurrentHelpersTest.java @@ -0,0 +1,97 @@ +package com.launchdarkly.testhelpers; + +import org.junit.Test; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import static com.launchdarkly.testhelpers.AssertionsTest.requireAssertionError; +import static com.launchdarkly.testhelpers.ConcurrentHelpers.assertFutureIsCompleted; +import static com.launchdarkly.testhelpers.ConcurrentHelpers.assertNoMoreValues; +import static com.launchdarkly.testhelpers.ConcurrentHelpers.awaitValue; +import static com.launchdarkly.testhelpers.ConcurrentHelpers.isCompletedWithin; +import static com.launchdarkly.testhelpers.ConcurrentHelpers.trySleep; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +@SuppressWarnings("javadoc") +public class ConcurrentHelpersTest { + @Test + public void awaitValueSuccess() { + BlockingQueue q = new LinkedBlockingQueue<>(); + new Thread(() -> { + q.add("a"); + }).start(); + String value = awaitValue(q, 1, TimeUnit.SECONDS); + assertThat(value, equalTo("a")); + } + + @Test + public void awaitValueFailure() { + BlockingQueue q = new LinkedBlockingQueue<>(); + String message = requireAssertionError(() -> { + awaitValue(q, 100, TimeUnit.MILLISECONDS); + }); + assertThat(message, equalTo("did not receive a value within 100 milliseconds")); + } + + @Test + public void assertNoMoreValuesSuccess() { + BlockingQueue q = new LinkedBlockingQueue<>(); + assertNoMoreValues(q, 50, TimeUnit.MILLISECONDS); + } + + @Test + public void assertNoMoreValuesFailure() { + BlockingQueue q = new LinkedBlockingQueue<>(); + new Thread(() -> { + trySleep(10, TimeUnit.MILLISECONDS); + q.add("a"); + }).start(); + String message = requireAssertionError(() -> { + assertNoMoreValues(q, 100, TimeUnit.MILLISECONDS); + }); + assertThat(message, equalTo("expected no more values, but received: a")); + } + + @Test + public void assertFutureIsCompletedSuccess() { + CompletableFuture f = new CompletableFuture(); + new Thread(() -> { + f.complete("a"); + }).start(); + String value = assertFutureIsCompleted(f, 1, TimeUnit.SECONDS); + assertThat(value, equalTo("a")); + } + + @Test + public void assertFutureIsCompletedFailure() { + CompletableFuture f = new CompletableFuture(); + String message = requireAssertionError(() -> { + assertFutureIsCompleted(f, 50, TimeUnit.MILLISECONDS); + }); + assertThat(message, equalTo("Future was not completed within 50 milliseconds")); + } + + @Test + public void futureIsCompletedMatcherSuccess() { + CompletableFuture f = new CompletableFuture(); + new Thread(() -> { + f.complete("a"); + }).start(); + assertThat(f, isCompletedWithin(1, TimeUnit.SECONDS)); + } + + @Test + public void futureIsCompletedMatcherFailure() { + CompletableFuture f = new CompletableFuture(); + String message = requireAssertionError(() -> { + assertThat(f, isCompletedWithin(50, TimeUnit.MILLISECONDS)); + }); + assertThat(message, containsString("Expected: Future is completed within 50 milliseconds")); + assertThat(message, containsString("but: timed out")); + } +} diff --git a/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/JsonAssertionsTest.java b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/JsonAssertionsTest.java new file mode 100644 index 0000000..29f51e9 --- /dev/null +++ b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/JsonAssertionsTest.java @@ -0,0 +1,202 @@ +package com.launchdarkly.testhelpers; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; +import org.hamcrest.TypeSafeDiagnosingMatcher; +import org.junit.Test; + +import static com.launchdarkly.testhelpers.JsonAssertions.assertJsonEquals; +import static com.launchdarkly.testhelpers.JsonAssertions.assertJsonIncludes; +import static com.launchdarkly.testhelpers.JsonAssertions.isJsonArray; +import static com.launchdarkly.testhelpers.JsonAssertions.jsonEquals; +import static com.launchdarkly.testhelpers.JsonAssertions.jsonEqualsValue; +import static com.launchdarkly.testhelpers.JsonAssertions.jsonIncludes; +import static com.launchdarkly.testhelpers.JsonAssertions.jsonNull; +import static com.launchdarkly.testhelpers.JsonAssertions.jsonProperty; +import static com.launchdarkly.testhelpers.JsonAssertions.jsonUndefined; +import static com.launchdarkly.testhelpers.JsonTestValue.jsonOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.emptyIterable; + +@SuppressWarnings("javadoc") +public class JsonAssertionsTest { + @Test + public void assertJsonEqualsSuccess() { + jsonEqualsShouldSucceed("null", "null"); + jsonEqualsShouldSucceed("true", "true"); + jsonEqualsShouldSucceed("1", "1"); + jsonEqualsShouldSucceed("\"x\"", "\"x\""); + jsonEqualsShouldSucceed("{\"a\":1,\"b\":{\"c\":2}}", "{\"b\":{\"c\":2},\"a\":1}"); + jsonEqualsShouldSucceed("[1,2,[3,4]]","[1,2,[3,4]]"); + + assertThat(jsonOf("true"), jsonEqualsValue(true)); + } + + private static void jsonEqualsShouldSucceed(String expected, String actual) { + assertJsonEquals(expected, actual); + assertThat(jsonOf(actual), jsonEquals(jsonOf(expected))); + assertThat(jsonOf(actual), jsonEquals(expected)); + } + + @Test + public void assertJsonEqualsFailureWithNoDetailedDiff() { + jsonEqualsShouldFail("null", null, "no value"); + jsonEqualsShouldFail("null", "{", "not valid JSON"); + jsonEqualsShouldFail("null", "true", "expected: null\nactual: true"); + jsonEqualsShouldFail("false", "true", "expected: false\nactual: true"); + jsonEqualsShouldFail("{\"a\":1}", "3", "expected: {\"a\":1}\nactual: 3"); + jsonEqualsShouldFail("[1,2]", "3", "expected: [1,2]\nactual: 3"); + jsonEqualsShouldFail("[1,2]", "[1,2,3]", "expected: [1,2]\nactual: [1,2,3]"); + } + + @Test + public void assertJsonEqualsFailureWithDetailedDiff() { + jsonEqualsShouldFail("{\"a\":1,\"b\":2}", "{\"a\":1,\"b\":3}", + "at \"b\": expected = 2, actual = 3"); + + jsonEqualsShouldFail("{\"a\":1,\"b\":2}", "{\"a\":1}", + "at \"b\": expected = 2, actual = "); + + jsonEqualsShouldFail("{\"a\":1}", "{\"a\":1,\"b\":2}", + "at \"b\": expected = , actual = 2"); + + jsonEqualsShouldFail("{\"a\":1,\"b\":{\"c\":2}}", "{\"a\":1,\"b\":{\"c\":3}}", + "at \"b.c\": expected = 2, actual = 3"); + + jsonEqualsShouldFail("{\"a\":1,\"b\":[2,3]}", "{\"a\":1,\"b\":[3,3]}", + "at \"b[0]\": expected = 2, actual = 3"); + + jsonEqualsShouldFail("[100,200,300]", "[100,201,300]", + "at \"[1]\": expected = 200, actual = 201"); + + jsonEqualsShouldFail("[100,[200,210],300]", "[100,[201,210],300]", + "at \"[1][0]\": expected = 200, actual = 201"); + + jsonEqualsShouldFail("[100,{\"a\":1},300]", "[100,{\"a\":2},300]", + "at \"[1].a\": expected = 1, actual = 2"); + } + + private static void jsonEqualsShouldFail(String expected, String actual, String expectedMessage) { + assertThat(() -> assertJsonEquals(expected, actual), + shouldFailWithMessage(Matchers.containsString(expectedMessage))); + assertThat(() -> assertThat(jsonOf(actual), jsonEquals(jsonOf(expected))), + shouldFailWithMessage(Matchers.containsString(expectedMessage))); + } + + @Test + public void assertJsonIncludesSuccess() { + jsonIncludesShouldSucceed("{\"a\":1,\"b\":2}", "{\"b\":2,\"a\":1}"); + jsonIncludesShouldSucceed("{\"a\":1,\"b\":2}", "{\"b\":2,\"a\":1,\"c\":3}"); + jsonIncludesShouldSucceed("{\"a\":1,\"b\":{\"c\":2}}", "{\"b\":{\"c\":2,\"d\":3},\"a\":1}"); + } + + private void jsonIncludesShouldSucceed(String expected, String actual) { + assertJsonIncludes(expected, actual); + assertThat(jsonOf(actual), jsonIncludes(jsonOf(expected))); + assertThat(jsonOf(actual), jsonIncludes(expected)); + } + + @Test + public void assertJsonIncludesFailure() { + jsonIncludesShouldFail("null", null, "no value"); + jsonIncludesShouldFail("null", "{", "not valid JSON"); + + jsonIncludesShouldFail("{\"a\":1}", "{\"a\":0,\"b\":2,\"c\":3}", + "at \"a\": expected = 1, actual = 0"); + + jsonIncludesShouldFail("{\"a\":1}", "{\"b\":2,\"c\":3}", + "at \"a\": expected = 1, actual = "); + + jsonIncludesShouldFail("{\"b\":2,\"a\":1,\"c\":3}", "{\"a\":1,\"b\":2}", + "at \"c\": expected = 3, actual = "); + + jsonIncludesShouldFail("{\"b\":{\"c\":2,\"d\":3},\"a\":1}", "{\"a\":1,\"b\":{\"c\":2}}", + "at \"b.d\": expected = 3, actual = "); + } + + private static void jsonIncludesShouldFail(String expected, String actual, String expectedMessage) { + assertThat(() -> assertJsonIncludes(expected, actual), + shouldFailWithMessage(Matchers.containsString(expectedMessage))); + assertThat(() -> assertThat(jsonOf(actual), jsonIncludes(expected)), + shouldFailWithMessage(Matchers.containsString(expectedMessage))); + } + + @Test + public void jsonPropertySuccess() { + assertThat(jsonOf("{\"a\":true}"), jsonProperty("a", jsonEquals("true"))); + assertThat(jsonOf("{\"a\":true}"), jsonProperty("a", true)); + + assertThat(jsonOf("{\"a\":1}"), jsonProperty("a", 1)); + assertThat(jsonOf("{\"a\":2.5}"), jsonProperty("a", 2.5)); + assertThat(jsonOf("{\"a\":\"x\"}"), jsonProperty("a", "x")); + + assertThat(jsonOf("{\"a\":{\"b\": 1}}"), jsonProperty("a", jsonProperty("b", 1))); + + assertThat(jsonOf("{\"a\":true}"), jsonProperty("b", jsonUndefined())); + assertThat(jsonOf("{\"a\":null}"), jsonProperty("a", jsonNull())); + } + + @Test + public void jsonPropertyFailure() { + assertThat(() -> assertThat(jsonOf(null), jsonProperty("a", true)), + shouldFailWithMessage(containsString("no value"))); + + assertThat(() -> assertThat(jsonOf("[]"), jsonProperty("a", true)), + shouldFailWithMessage(containsString("not a JSON object"))); + + assertThat(() -> assertThat(jsonOf("{\"a\":1}"), jsonProperty("b", 1)), + shouldFailWithMessage(Matchers.allOf(containsString("Expected: property \"b\""), containsString("no value")))); + + assertThat(() -> assertThat(jsonOf("{\"a\":1}"), jsonProperty("a", 2)), + shouldFailWithMessage(Matchers.allOf(containsString("Expected: property \"a\""), containsString("actual: 1")))); +} + + @SuppressWarnings("unchecked") + @Test + public void isJsonArraySuccess() { + assertThat(jsonOf("[]"), isJsonArray(emptyIterable())); + assertThat(jsonOf("[true]"), isJsonArray(contains(jsonEqualsValue(true)))); + assertThat(jsonOf("[true, false]"), isJsonArray(contains(jsonEqualsValue(true), jsonEqualsValue(false)))); + } + + @Test + public void isJsonArrayFailure() { + assertThat(() -> assertThat(jsonOf(null), isJsonArray(emptyIterable())), + shouldFailWithMessage(containsString("no value"))); + + assertThat(() -> assertThat(jsonOf("{}"), isJsonArray(emptyIterable())), + shouldFailWithMessage(containsString("not a JSON array"))); + + assertThat(() -> assertThat(jsonOf("[true]"), isJsonArray(contains(jsonEqualsValue(false)))), + shouldFailWithMessage(containsString("item 0: expected: false\nactual: true"))); + } + + private static Matcher shouldFailWithMessage(Matcher matcher) { + return new TypeSafeDiagnosingMatcher() { + @Override + public void describeTo(Description description) { + description.appendText("should fail with message:"); + matcher.describeTo(description); + } + + @Override + protected boolean matchesSafely(Runnable item, Description mismatchDescription) { + try { + item.run(); + mismatchDescription.appendText("did not throw exception"); + return false; + } catch (AssertionError e) { + String message = e.getMessage().trim(); + if (!matcher.matches(message)) { + matcher.describeMismatch(message, mismatchDescription); + return false; + } + return true; + } + } + }; + } +} diff --git a/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/JsonTestValueTest.java b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/JsonTestValueTest.java new file mode 100644 index 0000000..4b29b1e --- /dev/null +++ b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/JsonTestValueTest.java @@ -0,0 +1,69 @@ +package com.launchdarkly.testhelpers; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; + +import org.junit.Test; + +import static com.launchdarkly.testhelpers.AssertionsTest.requireAssertionError; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +@SuppressWarnings("javadoc") +public class JsonTestValueTest { + @Test + public void parseUndefined() { + JsonTestValue v = JsonTestValue.jsonOf(null); + assertThat(v, notNullValue()); + assertThat(v.isDefined(), equalTo(false)); + assertThat(v.raw, nullValue()); + assertThat(v.parsed, nullValue()); + } + + @Test + public void parseMalformed() { + assertThat(requireAssertionError(() -> JsonTestValue.jsonOf("{no")), + allOf(containsString("not valid JSON"), containsString("{no"))); + } + + @Test + public void parseSuccess() { + JsonTestValue v = JsonTestValue.jsonOf("123"); + assertThat(v, notNullValue()); + assertThat(v.isDefined(), equalTo(true)); + assertThat(v.raw, equalTo("123")); + assertThat(v.parsed, equalTo(new JsonPrimitive(123))); + } + + @Test + public void fromParsedUndefined() { + JsonTestValue v = JsonTestValue.ofParsed(null); + assertThat(v, notNullValue()); + assertThat(v.isDefined(), equalTo(false)); + assertThat(v.raw, nullValue()); + assertThat(v.parsed, nullValue()); + } + + @Test + public void fromParsedValue() { + JsonElement parsed = new JsonPrimitive(123); + JsonTestValue v = JsonTestValue.ofParsed(parsed); + assertThat(v, notNullValue()); + assertThat(v.isDefined(), equalTo(true)); + assertThat(v.raw, equalTo("123")); + assertThat(v.parsed, equalTo(parsed)); + } + + @Test + public void fromValue() { + JsonTestValue v = JsonTestValue.jsonFromValue(true); + assertThat(v, notNullValue()); + assertThat(v.isDefined(), equalTo(true)); + assertThat(v.raw, equalTo("true")); + assertThat(v.parsed, equalTo(new JsonPrimitive(true))); + } +} diff --git a/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/TempDirTest.java b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/TempDirTest.java new file mode 100644 index 0000000..50c8890 --- /dev/null +++ b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/TempDirTest.java @@ -0,0 +1,48 @@ +package com.launchdarkly.testhelpers; + +import org.junit.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; + +@SuppressWarnings("javadoc") +public class TempDirTest { + @Test + public void tempDir() { + Path path = null; + try (TempDir dir = TempDir.create()) { + path = dir.getPath(); + assertThat(Files.isDirectory(path), is(true)); + + TempFile f1 = dir.tempFile(); + assertThat(Files.isRegularFile(f1.getPath()), is(true)); + assertThat(f1.getPath().toString(), startsWith(path.toString())); + + TempFile f2 = dir.tempFile(".x"); + assertThat(Files.isRegularFile(f2.getPath()), is(true)); + assertThat(f2.getPath().toString(), startsWith(path.toString())); + assertThat(f2.getPath().toString(), endsWith(".x")); + } + assertThat(Files.exists(path), is(false)); + } + + @Test + public void canDeleteTempDirBeforeClosing() { + Path path = null; + try (TempDir dir = TempDir.create()) { + path = dir.getPath(); + assertThat(Files.isDirectory(path), is(true)); + + dir.tempFile(""); + + dir.delete(); + assertThat(Files.exists(path), is(false)); + } + assertThat(Files.exists(path), is(false)); + } +} diff --git a/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/TempFileTest.java b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/TempFileTest.java new file mode 100644 index 0000000..3ce7e37 --- /dev/null +++ b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/TempFileTest.java @@ -0,0 +1,55 @@ +package com.launchdarkly.testhelpers; + +import org.junit.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +@SuppressWarnings("javadoc") +public class TempFileTest { + @Test + public void tempFile() throws Exception { + Path path = null; + try (TempFile file = TempFile.create()) { + path = file.getPath(); + assertThat(Files.isRegularFile(path), is(true)); + + assertThat(new String(Files.readAllBytes(path)), equalTo("")); + + file.setContents("xyz"); + + assertThat(new String(Files.readAllBytes(path)), equalTo("xyz")); + } + assertThat(Files.exists(path), is(false)); + } + + @Test + public void tempFileWithSuffix() throws Exception { + Path path = null; + try (TempFile file = TempFile.create(".x")) { + path = file.getPath(); + assertThat(Files.isRegularFile(path), is(true)); + assertThat(path.toString(), endsWith(".x")); + } + assertThat(Files.exists(path), is(false)); + } + + @Test + public void canDeleteTempFileBeforeClosing() throws Exception { + Path path = null; + try (TempFile file = TempFile.create()) { + path = file.getPath(); + assertThat(Files.isRegularFile(path), is(true)); + + file.delete(); + + assertThat(Files.exists(path), is(false)); + } + assertThat(Files.exists(path), is(false)); + } +} diff --git a/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/TypeBehaviorTest.java b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/TypeBehaviorTest.java new file mode 100644 index 0000000..7ff31d6 --- /dev/null +++ b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/TypeBehaviorTest.java @@ -0,0 +1,192 @@ +package com.launchdarkly.testhelpers; + +import org.junit.Test; + +import java.util.Arrays; + +import static com.launchdarkly.testhelpers.TypeBehavior.checkEqualsAndHashCode; +import static com.launchdarkly.testhelpers.TypeBehavior.valueFactoryFromInstances; + +@SuppressWarnings("javadoc") +public class TypeBehaviorTest { + @Test + public void checkEqualsAndHashCodeSuccess() { + checkEqualsAndHashCode( + Arrays.asList( + valueFactoryFromInstances( + new TypeWithValueAndHashCode("a", 1), + new TypeWithValueAndHashCode("a", 1)), + valueFactoryFromInstances( + new TypeWithValueAndHashCode("b", 2), + new TypeWithValueAndHashCode("b", 2)), + valueFactoryFromInstances( + new TypeWithValueAndHashCode("c", 2), + new TypeWithValueAndHashCode("c", 2)) // hash codes deliberately the same as b - that is allowed + )); + } + + @Test(expected=AssertionError.class) + public void checkEqualsAndHashCodeFailureForIncorrectEquality() { + checkEqualsAndHashCode( + Arrays.asList( + () -> new TypeThatEqualsEveryObjectAndAlwaysHasSameHashCode(), + () -> new TypeThatEqualsEveryObjectAndAlwaysHasSameHashCode() + )); + } + + @Test(expected=AssertionError.class) + public void checkEqualsAndHashCodeFailureForEqualingNull() { + checkEqualsAndHashCode( + Arrays.asList( + () -> new TypeThatEqualsEveryObjectOrNullAndAlwaysHasSameHashCode() + )); + } + + @Test(expected=AssertionError.class) + public void checkEqualsAndHashCodeFailureForIncorrectInequality() { + checkEqualsAndHashCode( + Arrays.asList( + () -> new TypeThatEqualsOnlyItself() + )); + } + + @Test(expected=AssertionError.class) + public void checkEqualsAndHashCodeFailureForObjectNotEqualingItself() { + checkEqualsAndHashCode( + Arrays.asList( + () -> new TypeThatEqualsNothing() + )); + } + + @Test(expected=AssertionError.class) + public void checkEqualsAndHashCodeFailureForNonTransitiveEquality() { + checkEqualsAndHashCode( + Arrays.asList( + valueFactoryFromInstances( + new TypeThatEqualsSameOrHigherValue(1), + new TypeThatEqualsSameOrHigherValue(2) + ) + )); + } + + @Test(expected=AssertionError.class) + public void checkEqualsAndHashCodeFailureForNonTransitiveInequality() { + checkEqualsAndHashCode( + Arrays.asList( + valueFactoryFromInstances( + new TypeThatEqualsSameOrHigherValue(1), + new TypeThatEqualsSameOrHigherValue(1)), + valueFactoryFromInstances( + new TypeThatEqualsSameOrHigherValue(2), + new TypeThatEqualsSameOrHigherValue(2)) + )); + } + + @Test(expected=AssertionError.class) + public void checkEqualsAndHashCodeFailureForInconsistentHashCode() { + checkEqualsAndHashCode( + Arrays.asList( + valueFactoryFromInstances( + new TypeWithValueAndHashCode("a", 1), + new TypeWithValueAndHashCode("a", 2)) + )); + } + + @Test(expected=AssertionError.class) + public void checkEqualsAndHashCodeFailureForSameInstanceSeenTwice() { + TypeThatEqualsOnlyItself instance1 = new TypeThatEqualsOnlyItself(); + TypeThatEqualsOnlyItself instance2 = new TypeThatEqualsOnlyItself(); + checkEqualsAndHashCode( + Arrays.asList( + () -> instance1, + () -> instance2 + )); + } + + @Test + public void checkEqualsAndHashCodeAllowsSingletonPattern() { + TypeThatEqualsOnlyItself instance1 = new TypeThatEqualsOnlyItself(); + TypeThatEqualsOnlyItself instance2 = new TypeThatEqualsOnlyItself(); + checkEqualsAndHashCode( + Arrays.asList( + TypeBehavior.singletonValueFactory(instance1), + TypeBehavior.singletonValueFactory(instance2) + )); + } + + private static class TypeWithValueAndHashCode { + private final String value; + private final int hashCode; + + public TypeWithValueAndHashCode(String value, int hashCode) { + this.value = value; + this.hashCode = hashCode; + } + + public boolean equals(Object o) { + return o instanceof TypeWithValueAndHashCode && + ((TypeWithValueAndHashCode)o).value.equals(this.value); + } + + public int hashCode() { + return this.hashCode; + } + + public String toString() { + return value + "/" + hashCode; + } + } + + private static class TypeThatEqualsEveryObjectAndAlwaysHasSameHashCode { + public boolean equals(Object o) { + return o != null; + } + + public int hashCode() { + return 1; + } + } + + private static class TypeThatEqualsEveryObjectOrNullAndAlwaysHasSameHashCode { + public boolean equals(Object o) { + return true; + } + + public int hashCode() { + return 1; + } + } + + private static class TypeThatEqualsOnlyItself { + public boolean equals(Object o) { + return this == o; + } + + public int hashCode() { + return 1; + } + } + + private static class TypeThatEqualsNothing { + public boolean equals(Object o) { + return false; + } + + public int hashCode() { + return 1; + } + } + + private static class TypeThatEqualsSameOrHigherValue { + private final int index; + + TypeThatEqualsSameOrHigherValue(int index) { + this.index = index; + } + + public boolean equals(Object o) { + return o instanceof TypeThatEqualsSameOrHigherValue && + ((TypeThatEqualsSameOrHigherValue)o).index >= this.index; + } + } +} diff --git a/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/HandlerSwitcherTest.java b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/HandlerSwitcherTest.java new file mode 100644 index 0000000..1e2c6ca --- /dev/null +++ b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/HandlerSwitcherTest.java @@ -0,0 +1,27 @@ +package com.launchdarkly.testhelpers.httptest; + +import org.junit.Test; + +import static com.launchdarkly.testhelpers.httptest.TestUtil.simpleGet; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +import okhttp3.Response; + +@SuppressWarnings("javadoc") +public class HandlerSwitcherTest { + @Test + public void switchHandlers() throws Exception { + HandlerSwitcher switchable = new HandlerSwitcher(Handlers.status(200)); + + try (HttpServer server = HttpServer.start(switchable)) { + Response resp1 = simpleGet(server.getUri()); + assertThat(resp1.code(), equalTo(200)); + + switchable.setTarget(Handlers.status(400)); + + Response resp2 = simpleGet(server.getUri()); + assertThat(resp2.code(), equalTo(400)); + } + } +} diff --git a/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/HandlersTest.java b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/HandlersTest.java new file mode 100644 index 0000000..b51a63e --- /dev/null +++ b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/HandlersTest.java @@ -0,0 +1,178 @@ +package com.launchdarkly.testhelpers.httptest; + +import com.google.common.collect.ImmutableList; + +import org.junit.Test; + +import java.nio.charset.Charset; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.launchdarkly.testhelpers.httptest.TestUtil.client; +import static com.launchdarkly.testhelpers.httptest.TestUtil.simpleGet; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; + +import okhttp3.Request; +import okhttp3.Response; + +@SuppressWarnings("javadoc") +public class HandlersTest { + @Test + public void status() throws Exception { + try (HttpServer server = HttpServer.start(Handlers.status(419))) { + try (Response resp = simpleGet(server.getUri())) { + assertThat(resp.code(), equalTo(419)); + assertThat(resp.header("Content-Type"), nullValue()); + assertThat(resp.body().string(), equalTo("")); + } + } + } + + @Test + public void header() throws Exception { + try (HttpServer server = HttpServer.start(Handlers.header("header-name", "value"))) { + try (Response resp = simpleGet(server.getUri())) { + assertThat(resp.code(), equalTo(200)); + assertThat(resp.header("Header-Name"), equalTo("value")); + } + } + } + + @Test + public void replaceHeader() throws Exception { + try (HttpServer server = HttpServer.start(Handlers.all( + Handlers.header("header-name", "old-value"), + Handlers.header("header-name", "new-value") + ))) { + try (Response resp = simpleGet(server.getUri())) { + assertThat(resp.code(), equalTo(200)); + assertThat(resp.headers("Header-Name"), equalTo(ImmutableList.of("new-value"))); + } + } + } + + @Test + public void addHeader() throws Exception { + try (HttpServer server = HttpServer.start(Handlers.all( + Handlers.addHeader("header-name", "old-value"), + Handlers.addHeader("header-name", "new-value") + ))) { + try (Response resp = simpleGet(server.getUri())) { + assertThat(resp.code(), equalTo(200)); + // Not all HTTP server implementations are able to write multiple header lines. The HTTP spec + // says that sending a comma-delimited list is exactly equivalent. + assertThat(resp.headers("Header-Name"), + anyOf( + equalTo(ImmutableList.of("old-value", "new-value")), + equalTo(ImmutableList.of("old-value,new-value")) + )); + } + } + } + + @Test + public void body() throws Exception { + byte[] data = new byte[] { 1, 2, 3 }; + try (HttpServer server = HttpServer.start(Handlers.body("application/weird", data))) { + try (Response resp = simpleGet(server.getUri())) { + assertThat(resp.code(), equalTo(200)); + assertThat(resp.body().bytes(), equalTo(data)); + } + } + } + + @Test + public void bodyStringWithNoCharset() throws Exception { + String body = "hello"; + try (HttpServer server = HttpServer.start(Handlers.bodyString("text/weird", body))) { + try (Response resp = simpleGet(server.getUri())) { + assertThat(resp.code(), equalTo(200)); + assertThat(resp.header("content-type"), equalTo("text/weird")); + assertThat(resp.body().string(), equalTo(body)); + } + } + } + + @Test + public void bodyStringWithCharset() throws Exception { + String body = "hello"; + try (HttpServer server = HttpServer.start(Handlers.bodyString("text/weird", body, Charset.forName("UTF-8")))) { + try (Response resp = simpleGet(server.getUri())) { + assertThat(resp.code(), equalTo(200)); + assertThat(resp.header("content-type"), equalTo("text/weird;charset=utf-8")); + assertThat(resp.body().string(), equalTo(body)); + } + } + } + + @Test + public void bodyJsonWithoutCharset() throws Exception { + String body = "true"; + try (HttpServer server = HttpServer.start(Handlers.bodyJson(body))) { + try (Response resp = client.newCall(new Request.Builder().url(server.getUrl()).build()).execute()) { + assertThat(resp.code(), equalTo(200)); + assertThat(resp.header("content-type"), equalTo("application/json")); + assertThat(resp.body().string(), equalTo(body)); + } + } + } + + @Test + public void bodyJsonWithCharset() throws Exception { + String body = "true"; + try (HttpServer server = HttpServer.start(Handlers.bodyJson(body, Charset.forName("UTF-8")))) { + try (Response resp = simpleGet(server.getUri())) { + assertThat(resp.code(), equalTo(200)); + assertThat(resp.header("content-type"), equalTo("application/json;charset=utf-8")); + assertThat(resp.body().string(), equalTo(body)); + } + } + } + + @Test + public void chainStatusAndHeadersAndBody() throws Exception { + Handler handler = Handlers.all( + Handlers.status(201), + Handlers.header("name1", "value1"), + Handlers.header("name2", "value2"), + Handlers.bodyString("text/plain", "hello") + ); + try (HttpServer server = HttpServer.start(handler)) { + try (Response resp = simpleGet(server.getUri())) { + assertThat(resp.code(), equalTo(201)); + assertThat(resp.header("name1"), equalTo("value1")); + assertThat(resp.header("name2"), equalTo("value2")); + assertThat(resp.header("content-type"), equalTo("text/plain")); + assertThat(resp.body().string(), equalTo("hello")); + } + } + } + + @Test + public void waitFor() throws Exception { + Semaphore signal = new Semaphore(0); + Handler handler = Handlers.all( + Handlers.waitFor(signal), + Handlers.status(200) + ); + try (HttpServer server = HttpServer.start(handler)) { + AtomicBoolean signaled = new AtomicBoolean(false); + new Thread(() -> { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + signaled.set(true); + signal.release(); + }).start(); + try (Response resp = simpleGet(server.getUri())) { + assertThat(signaled.get(), equalTo(true)); + assertThat(resp.code(), equalTo(200)); + } + } + } +} diff --git a/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/HttpServerTest.java b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/HttpServerTest.java new file mode 100644 index 0000000..beabe6f --- /dev/null +++ b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/HttpServerTest.java @@ -0,0 +1,78 @@ +package com.launchdarkly.testhelpers.httptest; + +import org.junit.Test; + +import static com.launchdarkly.testhelpers.httptest.TestUtil.simpleGet; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.startsWith; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +@SuppressWarnings("javadoc") +public class HttpServerTest { + @Test + public void serverWithSimpleStatusHandler() throws Exception { + try (HttpServer server = HttpServer.start(Handlers.status(419))) { + try (Response resp = simpleGet(server.getUri())) { + assertThat(resp.code(), equalTo(419)); + } + } + } + + @Test + public void serverOnSpecificPort() throws Exception { + try (HttpServer server = HttpServer.start(12345, Handlers.status(419))) { + assertThat(server.getPort(), equalTo(12345)); + assertThat(server.getUri().toString(), equalTo("http://localhost:12345/")); + try (Response resp = simpleGet(server.getUri())) { + assertThat(resp.code(), equalTo(419)); + } + } + } + + @Test + public void multipleServers() throws Exception { + try (HttpServer server1 = HttpServer.start(Handlers.status(200))) { + try (HttpServer server2 = HttpServer.start(Handlers.status(419))) { + try (Response resp1 = simpleGet(server1.getUri())) { + assertThat(resp1.code(), equalTo(200)); + } + try (Response resp2 = simpleGet(server2.getUri())) { + assertThat(resp2.code(), equalTo(419)); + } + } + } + } + + @Test + public void serverReturns500StatusForExceptionFromHandler() throws Exception { + Handler handler = ctx -> { + throw new RuntimeException("unfortunate"); + }; + try (HttpServer server = HttpServer.start(handler)) { + try (Response resp = simpleGet(server.getUri())) { + assertThat(resp.code(), equalTo(500)); + assertThat(resp.body().string(), equalTo("java.lang.RuntimeException: unfortunate")); + } + } + } + + @Test + public void secureServerWithSelfSignedCert() throws Exception { + ServerTLSConfiguration certData = ServerTLSConfiguration.makeSelfSignedCertificate(); + OkHttpClient client = new OkHttpClient.Builder() + .sslSocketFactory(certData.getSocketFactory(), certData.getTrustManager()) + .build(); + + try (HttpServer server = HttpServer.startSecure(certData, Handlers.status(419))) { + assertThat(server.getUri().toString(), startsWith("https:")); + + try (Response resp = client.newCall(new Request.Builder().url(server.getUrl()).build()).execute()) { + assertThat(resp.code(), equalTo(419)); + } + } + } +} diff --git a/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/RequestRecorderTest.java b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/RequestRecorderTest.java new file mode 100644 index 0000000..1207eea --- /dev/null +++ b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/RequestRecorderTest.java @@ -0,0 +1,152 @@ +package com.launchdarkly.testhelpers.httptest; + +import org.junit.Test; + +import java.net.URI; + +import static com.launchdarkly.testhelpers.httptest.TestUtil.client; +import static com.launchdarkly.testhelpers.httptest.TestUtil.simpleGet; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.startsWith; + +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +@SuppressWarnings("javadoc") +public class RequestRecorderTest { + // Note that these tests are really testing two things: the RequestRecorder API, and the + // ability of the underlying server implementation to correctly get the request properties. + + @Test + public void getMethodAndUri() throws Exception { + try (HttpServer server = HttpServer.start(Handlers.status(200))) { + URI requestedUri = server.getUri().resolve("/request/path"); + Response resp = client.newCall( + new Request.Builder().url(requestedUri.toURL()).build() + ).execute(); + + assertThat(resp.code(), equalTo(200)); + + RequestInfo received = server.getRecorder().requireRequest(); + assertThat(received.getMethod(), equalTo("GET")); + assertThat(received.getUri(), equalTo(requestedUri)); + assertThat(received.getPath(), equalTo("/request/path")); + assertThat(received.getQuery(), nullValue()); + } + } + + @Test + public void queryString() throws Exception { + try (HttpServer server = HttpServer.start(Handlers.status(200))) { + URI requestedUri = server.getUri().resolve("/request/path?a=b"); + Response resp = client.newCall( + new Request.Builder().url(requestedUri.toURL()).build() + ).execute(); + + assertThat(resp.code(), equalTo(200)); + + RequestInfo received = server.getRecorder().requireRequest(); + assertThat(received.getMethod(), equalTo("GET")); + assertThat(received.getUri(), equalTo(requestedUri)); + assertThat(received.getPath(), equalTo("/request/path")); + assertThat(received.getQuery(), equalTo("?a=b")); + } + } + + @Test + public void requestHeaders() throws Exception { + try (HttpServer server = HttpServer.start(Handlers.status(200))) { + Response resp = client.newCall( + new Request.Builder().url(server.getUri().toURL()) + .header("name1", "value1") + .header("name2", "value2") + .build() + ).execute(); + + assertThat(resp.code(), equalTo(200)); + + RequestInfo received = server.getRecorder().requireRequest(); + assertThat(received.getHeader("name1"), equalTo("value1")); + assertThat(received.getHeader("name2"), equalTo("value2")); + } + } + + @Test + public void emptyRequestBodyByDefault() throws Exception { + try (HttpServer server = HttpServer.start(Handlers.status(200))) { + Response resp = client.newCall( + new Request.Builder().url(server.getUri().toURL()).build() + ).execute(); + + assertThat(resp.code(), equalTo(200)); + + RequestInfo received = server.getRecorder().requireRequest(); + assertThat(received.getBody(), equalTo("")); + } + } + + @Test + public void patchRequestWithBody() throws Exception { + doRequestWithBody("PATCH"); + } + + @Test + public void postRequestWithBody() throws Exception { + doRequestWithBody("POST"); + } + + @Test + public void putRequestWithBody() throws Exception { + doRequestWithBody("PUT"); + } + + @Test + public void reportRequestWithBody() throws Exception { + doRequestWithBody("REPORT"); + } + + private void doRequestWithBody(String method) throws Exception { + try (HttpServer server = HttpServer.start(Handlers.status(200))) { + Response resp = client.newCall( + new Request.Builder().url(server.getUri().toURL()) + .method(method, RequestBody.create("{}", MediaType.parse("application/json"))) + .build() + ).execute(); + + assertThat(resp.code(), equalTo(200)); + + RequestInfo received = server.getRecorder().requireRequest(); + assertThat(received.getMethod(), equalTo(method)); + assertThat(received.getHeader("Content-Type"), startsWith("application/json")); + assertThat(received.getBody(), equalTo("{}")); + } + } + + @Test + public void canDisableRecorder() throws Exception { + try (HttpServer server = HttpServer.start(Handlers.status(200))) { + simpleGet(server.getUri().resolve("/path1")); + + server.getRecorder().setEnabled(false); + + simpleGet(server.getUri().resolve("/path2")); + simpleGet(server.getUri().resolve("/path3")); + + server.getRecorder().setEnabled(true); + + simpleGet(server.getUri().resolve("/path4")); + + RequestInfo received1 = server.getRecorder().requireRequest(); + assertThat(received1.getPath(), equalTo("/path1")); + + RequestInfo received2 = server.getRecorder().requireRequest(); + assertThat(received2.getPath(), equalTo("/path4")); + + assertThat(server.getRecorder().count(), equalTo(0)); + } + } +} diff --git a/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/SequentialHandlerTest.java b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/SequentialHandlerTest.java new file mode 100644 index 0000000..626c0a6 --- /dev/null +++ b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/SequentialHandlerTest.java @@ -0,0 +1,28 @@ +package com.launchdarkly.testhelpers.httptest; + +import org.junit.Test; + +import static com.launchdarkly.testhelpers.httptest.TestUtil.simpleGet; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +import okhttp3.Response; + +@SuppressWarnings("javadoc") +public class SequentialHandlerTest { + @Test + public void handlersAreCalledInSequence() throws Exception { + Handler handler = Handlers.sequential(Handlers.status(200), Handlers.status(201)); + + try (HttpServer server = HttpServer.start(handler)) { + Response resp1 = simpleGet(server.getUri()); + assertThat(resp1.code(), equalTo(200)); + + Response resp2 = simpleGet(server.getUri()); + assertThat(resp2.code(), equalTo(201)); + + Response resp3 = simpleGet(server.getUri()); + assertThat(resp3.code(), equalTo(500)); + } + } +} diff --git a/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/SimpleRouterTest.java b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/SimpleRouterTest.java new file mode 100644 index 0000000..54a41ff --- /dev/null +++ b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/SimpleRouterTest.java @@ -0,0 +1,118 @@ +package com.launchdarkly.testhelpers.httptest; + +import org.junit.Test; + +import java.util.regex.Pattern; + +import static com.launchdarkly.testhelpers.httptest.TestUtil.client; +import static com.launchdarkly.testhelpers.httptest.TestUtil.simpleGet; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +@SuppressWarnings("javadoc") +public class SimpleRouterTest { + @Test + public void noPathsMatchByDefault() throws Exception { + SimpleRouter router = new SimpleRouter(); + + try (HttpServer server = HttpServer.start(router)) { + Response resp = simpleGet(server.getUri()); + assertThat(resp.code(), equalTo(404)); + } + } + + @Test + public void simplePathMatch() throws Exception { + SimpleRouter router = new SimpleRouter(); + router.add("/path1", Handlers.status(201)); + router.add("/path2", Handlers.status(419)); + + try (HttpServer server = HttpServer.start(router)) { + Response resp1 = simpleGet(server.getUri().resolve("/path1")); + assertThat(resp1.code(), equalTo(201)); + + Response resp2 = simpleGet(server.getUri().resolve("/path2")); + assertThat(resp2.code(), equalTo(419)); + + Response resp3 = simpleGet(server.getUri().resolve("/path3")); + assertThat(resp3.code(), equalTo(404)); + } + } + + @Test + public void simplePathMatchWithMethod() throws Exception { + SimpleRouter router = new SimpleRouter(); + router.add("GET", "/path1", Handlers.status(201)); + router.add("DELETE", "/path1", Handlers.status(204)); + + try (HttpServer server = HttpServer.start(router)) { + Response resp1 = simpleGet(server.getUri().resolve("/path1")); + assertThat(resp1.code(), equalTo(201)); + + Response resp2 = client.newCall(new Request.Builder().url(server.getUri().resolve("/path1").toURL()) + .method("DELETE", null).build()).execute(); + assertThat(resp2.code(), equalTo(204)); + } + } + + @Test + public void pathRegexMatch() throws Exception { + SimpleRouter router = new SimpleRouter(); + router.addRegex(Pattern.compile("/path[12]"), Handlers.status(201)); + router.addRegex(Pattern.compile("/path[34]"), Handlers.status(419)); + + try (HttpServer server = HttpServer.start(router)) { + Response resp1 = simpleGet(server.getUri().resolve("/path1")); + assertThat(resp1.code(), equalTo(201)); + + Response resp2 = simpleGet(server.getUri().resolve("/path3")); + assertThat(resp2.code(), equalTo(419)); + + Response resp3 = simpleGet(server.getUri().resolve("/path5")); + assertThat(resp3.code(), equalTo(404)); + } + } + + @Test + public void pathRegexMatchWithMethod() throws Exception { + SimpleRouter router = new SimpleRouter(); + router.addRegex("GET", Pattern.compile("/path[12]"), Handlers.status(201)); + router.addRegex("DELETE", Pattern.compile("/path[12]"), Handlers.status(419)); + + try (HttpServer server = HttpServer.start(router)) { + Response resp1 = simpleGet(server.getUri().resolve("/path1")); + assertThat(resp1.code(), equalTo(201)); + + Response resp2 = client.newCall(new Request.Builder().url(server.getUri().resolve("/path1").toURL()) + .method("DELETE", null).build()).execute(); + assertThat(resp2.code(), equalTo(419)); + + Response resp3 = client.newCall(new Request.Builder().url(server.getUri().resolve("/path1").toURL()) + .method("POST", RequestBody.create(new byte[0])).build()).execute(); + assertThat(resp3.code(), equalTo(405)); + + Response resp4 = client.newCall(new Request.Builder().url(server.getUri().resolve("/path3").toURL()) + .method("DELETE", null).build()).execute(); + assertThat(resp4.code(), equalTo(404)); + } + } + + @Test + public void pathRegexMatchWithPathParam() throws Exception { + SimpleRouter router = new SimpleRouter(); + router.addRegex(Pattern.compile("/path1/([^/]*)/do/(.*)"), ctx -> { + String message = "I did " + ctx.getPathParam(1) + " in " + ctx.getPathParam(0); + Handlers.bodyString("text/plain", message).apply(ctx); + }); + + try (HttpServer server = HttpServer.start(router)) { + Response resp1 = simpleGet(server.getUri().resolve("/path1/Chicago/do/something/or/other")); + assertThat(resp1.code(), equalTo(200)); + assertThat(resp1.body().string(), equalTo("I did something/or/other in Chicago")); + } + } +} diff --git a/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/SpecialHttpConfigurationsTest.java b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/SpecialHttpConfigurationsTest.java new file mode 100644 index 0000000..3079dc8 --- /dev/null +++ b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/SpecialHttpConfigurationsTest.java @@ -0,0 +1,171 @@ +package com.launchdarkly.testhelpers.httptest; + +import com.launchdarkly.testhelpers.httptest.SpecialHttpConfigurations.Params; +import com.launchdarkly.testhelpers.httptest.SpecialHttpConfigurations.UnexpectedResponseException; + +import org.junit.Test; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URI; + +import static org.junit.Assert.assertFalse; + +import okhttp3.Authenticator; +import okhttp3.Credentials; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.Route; + +@SuppressWarnings("javadoc") +public class SpecialHttpConfigurationsTest { + private static final int EXPECTED_STATUS = 418; + + // An implementation of TestAction that just converts the specified parameters to OkHttp settings + // and makes a request with the OkHttp client. + public static class MyTestClientAction implements SpecialHttpConfigurations.TestAction { + @Override + public boolean doTest(URI targetUri, Params params) throws IOException, SpecialHttpConfigurations.UnexpectedResponseException { + OkHttpClient.Builder cb = new OkHttpClient.Builder(); + if (params.getTlsConfig() != null && params.getTlsConfig().getSocketFactory() != null) { + cb.sslSocketFactory(params.getTlsConfig().getSocketFactory(), params.getTlsConfig().getTrustManager()); + } + if (params.getSocketFactory() != null) { + cb.socketFactory(params.getSocketFactory()); + } + if (params.getProxyHost() != null) { + cb.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(params.getProxyHost(), params.getProxyPort()))); + if (params.getProxyBasicAuthUser() != null) { + cb.proxyAuthenticator(new Authenticator() { + public Request authenticate(Route route, Response response) throws IOException { + return response.request().newBuilder() + .header("Proxy-Authorization", + Credentials.basic(params.getProxyBasicAuthUser(), params.getProxyBasicAuthPassword())) + .build(); + } + }); + } + } + + OkHttpClient client = cb.build(); + + Request req = new Request.Builder().url(targetUri.toString()).build(); + + Response resp = client.newCall(req).execute(); + if (resp.code() != EXPECTED_STATUS) { // we know we will be configuring the server with testActionHandler() + throw new SpecialHttpConfigurations.UnexpectedResponseException("got unexpected response status " + resp.code()); + } + + return true; + } + } + + private static Handler testActionHandler() { + return Handlers.status(EXPECTED_STATUS); + } + + @Test + public void testAllCorrect() { + SpecialHttpConfigurations.testAll(testActionHandler(), new MyTestClientAction()); + } + + @Test + public void testSelfSignedCertFails() { + SpecialHttpConfigurations.TestAction testActionThatIgnoresTlsConfigParam = new SpecialHttpConfigurations.TestAction() { + @Override + public boolean doTest(URI targetUri, Params params) throws IOException, UnexpectedResponseException { + params = new Params(null, + params.getSocketFactory(), params.getProxyHost(), params.getProxyPort(), + params.getProxyBasicAuthUser(), params.getProxyBasicAuthPassword()); + return new MyTestClientAction().doTest(targetUri, params); + } + }; + boolean passed = false; + try { + SpecialHttpConfigurations.testHttpClientCanBeConfiguredToAllowSelfSignedCert(testActionHandler(), + testActionThatIgnoresTlsConfigParam); + passed = true; + } catch (AssertionError e) {} + assertFalse("expected test to fail", passed); + } + + @Test + public void testSocketFactoryFails() { + SpecialHttpConfigurations.TestAction testActionThatIgnoresSocketFactoryParam = new SpecialHttpConfigurations.TestAction() { + @Override + public boolean doTest(URI targetUri, Params params) throws IOException, UnexpectedResponseException { + params = new Params(params.getTlsConfig(), + null, + params.getProxyHost(), params.getProxyPort(), params.getProxyBasicAuthUser(), params.getProxyBasicAuthPassword()); + return new MyTestClientAction().doTest(targetUri, params); + } + }; + boolean passed = false; + try { + SpecialHttpConfigurations.testHttpClientCanUseCustomSocketFactory(testActionHandler(), + testActionThatIgnoresSocketFactoryParam); + passed = true; + } catch (AssertionError e) {} + assertFalse("expected test to fail", passed); + } + + @Test + public void testProxyFails() { + SpecialHttpConfigurations.TestAction testActionThatIgnoresProxyParams = new SpecialHttpConfigurations.TestAction() { + @Override + public boolean doTest(URI targetUri, Params params) throws IOException, UnexpectedResponseException { + params = new Params(params.getTlsConfig(), params.getSocketFactory(), + null, 0, + params.getProxyBasicAuthUser(), params.getProxyBasicAuthPassword()); + return new MyTestClientAction().doTest(targetUri, params); + } + }; + boolean passed = false; + try { + SpecialHttpConfigurations.testHttpClientCanUseProxy(testActionHandler(), + testActionThatIgnoresProxyParams); + passed = true; + } catch (AssertionError e) {} + assertFalse("expected test to fail", passed); + } + + @Test + public void testProxyAuthFailsWithNoAuthProvided() { + SpecialHttpConfigurations.TestAction testActionThatIgnoresProxyAuthParams = new SpecialHttpConfigurations.TestAction() { + @Override + public boolean doTest(URI targetUri, Params params) throws IOException, UnexpectedResponseException { + params = new Params(params.getTlsConfig(), params.getSocketFactory(), params.getProxyHost(), params.getProxyPort(), + null, null); + return new MyTestClientAction().doTest(targetUri, params); + } + }; + boolean passed = false; + try { + SpecialHttpConfigurations.testHttpClientCanUseProxyWithBasicAuth(testActionHandler(), + testActionThatIgnoresProxyAuthParams); + passed = true; + } catch (AssertionError e) {} + assertFalse("expected test to fail", passed); + } + + @Test + public void testProxyAuthFailsWithWrongAuthProvided() { + SpecialHttpConfigurations.TestAction testActionThatChangesProxyAuthParams = new SpecialHttpConfigurations.TestAction() { + @Override + public boolean doTest(URI targetUri, Params params) throws IOException, UnexpectedResponseException { + params = new Params(params.getTlsConfig(), params.getSocketFactory(), params.getProxyHost(), params.getProxyPort(), + params.getProxyBasicAuthUser(), "x" + params.getProxyBasicAuthPassword()); + return new MyTestClientAction().doTest(targetUri, params); + } + }; + boolean passed = false; + try { + SpecialHttpConfigurations.testHttpClientCanUseProxyWithBasicAuth(testActionHandler(), + testActionThatChangesProxyAuthParams); + passed = true; + } catch (AssertionError e) {} + assertFalse("expected test to fail", passed); + } +} diff --git a/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/StreamingTest.java b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/StreamingTest.java new file mode 100644 index 0000000..91d6234 --- /dev/null +++ b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/StreamingTest.java @@ -0,0 +1,116 @@ +package com.launchdarkly.testhelpers.httptest; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +import org.junit.Test; + +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.List; +import java.util.concurrent.Semaphore; + +import static com.launchdarkly.testhelpers.httptest.TestUtil.simpleGet; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +import okhttp3.Response; + +@SuppressWarnings("javadoc") +public class StreamingTest { + @Test + public void basicChunkedResponseWithNoCharsetInHeader() throws Exception { + List chunks = ImmutableList.of("first.", "second.", "third"); + doStreamingTest( + Handlers.startChunks("text/plain", null), + Iterables.toArray(Iterables.transform(chunks, Handlers::writeChunkString), Handler.class), + Handlers.hang(), + "text/plain", + chunks + ); + } + + @Test + public void basicChunkedResponseWithCharsetInHeader() throws Exception { + List chunks = ImmutableList.of("first.", "second.", "third"); + doStreamingTest( + Handlers.startChunks("text/plain", Charset.forName("UTF-8")), + Iterables.toArray(Iterables.transform(chunks, Handlers::writeChunkString), Handler.class), + Handlers.hang(), + "text/plain;charset=utf-8", + chunks + ); + } + + @Test + public void sseStream() throws Exception { + doStreamingTest( + Handlers.SSE.start(), + new Handler[] { + Handlers.SSE.event("e1", "d1"), + Handlers.SSE.comment("comment"), + Handlers.SSE.event("e2", "d2"), + Handlers.SSE.event("data: all done"), + }, + Handlers.SSE.leaveOpen(), + "text/event-stream;charset=utf-8", + ImmutableList.of( + "event: e1\ndata: d1\n\n", + ":comment\n", + "event: e2\ndata: d2\n\n", + "data: all done\n\n" + ) + ); + } + + private void doStreamingTest( + Handler startAction, + Handler[] chunkActions, + Handler endAction, + String expectedContentType, + List expectedChunks + ) throws Exception { + Semaphore[] didWriteChunk = new Semaphore[expectedChunks.size()]; + Semaphore[] didReadChunk = new Semaphore[expectedChunks.size()]; + for (int i = 0; i < expectedChunks.size(); i++) { + didWriteChunk[i] = new Semaphore(0); + didReadChunk[i] = new Semaphore(0); + } + + Handler handler = Handlers.all( + startAction, + ctx -> { + for (int i = 0; i < expectedChunks.size(); i++) { + chunkActions[i].apply(ctx); + didWriteChunk[i].release(); + try { + didReadChunk[i].acquire(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }, + endAction + ); + + try (HttpServer server = HttpServer.start(handler)) { + try (Response resp = simpleGet(server.getUri())) { + assertThat(resp.code(), equalTo(200)); + assertThat(resp.header("Content-Type"), equalTo(expectedContentType)); + + InputStream stream = resp.body().byteStream(); + + for (int i = 0; i < expectedChunks.size(); i++) { + didWriteChunk[i].acquire(); + + byte[] buf = new byte[100]; + int n = stream.read(buf); + String s = new String(buf, 0, n); + assertThat(s, equalTo(expectedChunks.get(i))); + + didReadChunk[i].release(); + } + } + } + } +} diff --git a/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/TestUtil.java b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/TestUtil.java new file mode 100644 index 0000000..0b1074a --- /dev/null +++ b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/httptest/TestUtil.java @@ -0,0 +1,24 @@ +package com.launchdarkly.testhelpers.httptest; + +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +@SuppressWarnings("javadoc") +public class TestUtil { + public static final OkHttpClient client = new OkHttpClient.Builder() + .readTimeout(5, TimeUnit.MINUTES) + .retryOnConnectionFailure(false) + .build(); + + public static Response simpleGet(URI uri) { + try { + return client.newCall(new Request.Builder().url(uri.toURL()).build()).execute(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/tcptest/TcpHandlersTest.java b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/tcptest/TcpHandlersTest.java new file mode 100644 index 0000000..10a3125 --- /dev/null +++ b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/tcptest/TcpHandlersTest.java @@ -0,0 +1,94 @@ +package com.launchdarkly.testhelpers.tcptest; + +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.Socket; + +import static com.launchdarkly.testhelpers.tcptest.TestUtil.readStreamFully; +import static com.launchdarkly.testhelpers.tcptest.TestUtil.toUtf8Bytes; +import static com.launchdarkly.testhelpers.tcptest.TestUtil.toUtf8String; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +@SuppressWarnings("javadoc") +public class TcpHandlersTest { + @Test + public void writeData() throws IOException { + byte[] expected = new byte[] { 100, 101, 102 }; + TcpHandler handler = TcpHandlers.writeData(expected, 0, expected.length); + + try (TcpServer server = TcpServer.start(handler)) { + try (Socket s = new Socket("localhost", server.getPort())) { + byte[] actual = readStreamFully(s.getInputStream()); + assertArrayEquals(expected, actual); + } + } + } + + @Test + public void writeString() throws IOException { + String message = "hello"; + TcpHandler handler = TcpHandlers.writeString(message); + + try (TcpServer server = TcpServer.start(handler)) { + try (Socket s = new Socket("localhost", server.getPort())) { + assertEquals(message, toUtf8String(readStreamFully(s.getInputStream()))); + } + } + } + + @Test + public void forwardToPort() throws IOException { + String question = "question?"; + String answer = "answer!"; + ByteArrayOutputStream receivedData = new ByteArrayOutputStream(); + TcpHandler handler = new TcpHandler() { + @Override + public void apply(Socket socket) throws IOException { + byte[] data = TestUtil.readStream(socket.getInputStream(), toUtf8Bytes(question).length); + receivedData.write(data); + TcpHandlers.writeString(answer).apply(socket); + } + }; + + try (TcpServer underlyingServer = TcpServer.start(handler)) { + try (TcpServer forwardingServer = TcpServer.start(TcpHandlers.forwardToPort(underlyingServer.getPort()))) { + try (Socket s = new Socket("localhost", forwardingServer.getPort())) { + TcpHandlers.writeString(question).apply(s); + assertEquals(answer, toUtf8String(readStreamFully(s.getInputStream()))); + } + } + } + } + + @Test + public void noResponse() throws IOException { + TcpHandler handler = TcpHandlers.noResponse(); + + try (TcpServer server = TcpServer.start(handler)) { + try (Socket s = new Socket("localhost", server.getPort())) { + byte[] data = readStreamFully(s.getInputStream()); + assertEquals(0, data.length); + } + } + } + + @Test + public void sequential() throws IOException { + String message = "hello"; + TcpHandler handler = TcpHandlers.sequential( + TcpHandlers.noResponse(), + TcpHandlers.writeString(message)); + + try (TcpServer server = TcpServer.start(handler)) { + try (Socket s1 = new Socket("localhost", server.getPort())) { + assertEquals("", toUtf8String(readStreamFully(s1.getInputStream()))); + } + try (Socket s2 = new Socket("localhost", server.getPort())) { + assertEquals(message, toUtf8String(readStreamFully(s2.getInputStream()))); + } + } + } +} diff --git a/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/tcptest/TcpServerTest.java b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/tcptest/TcpServerTest.java new file mode 100644 index 0000000..5778f66 --- /dev/null +++ b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/tcptest/TcpServerTest.java @@ -0,0 +1,42 @@ +package com.launchdarkly.testhelpers.tcptest; + +import org.junit.Test; + +import java.io.IOException; +import java.net.Socket; + +import static com.launchdarkly.testhelpers.tcptest.TestUtil.doesPortHaveListener; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.fail; + +@SuppressWarnings("javadoc") +public class TcpServerTest { + @Test + public void listensOnAnyAvailablePort() throws IOException { + int port; + try (TcpServer server = TcpServer.start(TcpHandlers.noResponse())) { + assertNotEquals(0, server.getPort()); + port = server.getPort(); + try (Socket s = new Socket("localhost", server.getPort())) {} // just verify that we can connect + } + assertFalse("expected listener to be closed, but it wasn't", doesPortHaveListener(port)); + } + + @Test + public void listensOnSpecificPort() throws IOException { + int specificPort = 10000; + while (doesPortHaveListener(specificPort)) { + if (specificPort == 65535) { + fail("test could not find an available port"); + } + specificPort++; + } + try (TcpServer server = TcpServer.start(specificPort, TcpHandlers.noResponse())) { + assertEquals(specificPort, server.getPort()); + try (Socket s = new Socket("localhost", specificPort)) {} // just verify that we can connect + } + assertFalse("expected listener to be closed, but it wasn't", doesPortHaveListener(specificPort)); + } +} diff --git a/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/tcptest/TestUtil.java b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/tcptest/TestUtil.java new file mode 100644 index 0000000..5b69184 --- /dev/null +++ b/lib/shared/test-helpers/src/test/java/com/launchdarkly/testhelpers/tcptest/TestUtil.java @@ -0,0 +1,47 @@ +package com.launchdarkly.testhelpers.tcptest; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; +import java.nio.charset.Charset; + +@SuppressWarnings("javadoc") +public class TestUtil { + public static boolean doesPortHaveListener(int port) { + try { + try (Socket s = new Socket("localhost", port)) {} + return true; + } catch (IOException e) { + return false; + } + } + + public static byte[] readStreamFully(InputStream input) throws IOException { + return readStream(input, -1); + } + + public static byte[] readStream(InputStream input, int maxLength) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + byte[] buffer = new byte[1000]; + while (true) { + int n = input.read(buffer); + if (n < 0) { + break; + } + bytes.write(buffer, 0, n); + if (maxLength > 0 && bytes.size() >= maxLength) { + break; + } + } + return bytes.toByteArray(); + } + + public static String toUtf8String(byte[] data) { + return new String(data, Charset.forName("UTF-8")); + } + + public static byte[] toUtf8Bytes(String s) { + return s.getBytes(Charset.forName("UTF-8")); + } +} diff --git a/release-please-config.json b/release-please-config.json index 1b7cb83..6b9cf39 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -35,6 +35,14 @@ "gradle.properties" ] }, + "lib/shared/test-helpers": { + "package-name": "test-helpers", + "bump-minor-pre-major": true, + "include-v-in-tag": false, + "extra-files": [ + "gradle.properties" + ] + }, "lib/sdk/server": { "package-name": "launchdarkly-java-server-sdk", "bump-minor-pre-major": true, From 7d9164635160f38fe10b3b9ecc6c46ee1c85beb6 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 22 Oct 2025 10:08:36 -0700 Subject: [PATCH 2/4] Don't fail matrix fast. --- .github/workflows/test-helpers.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-helpers.yml b/.github/workflows/test-helpers.yml index 35c6469..ee4919a 100644 --- a/.github/workflows/test-helpers.yml +++ b/.github/workflows/test-helpers.yml @@ -14,6 +14,7 @@ jobs: build-test-helpers: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: java-version: [8, 11, 17, 19] os: [ubuntu-latest] From 26371f9e3def1057213fc7eb86fc872ff7b8c934 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 22 Oct 2025 10:15:17 -0700 Subject: [PATCH 3/4] Add test-helpers to manual workflow. --- .github/workflows/manual-publish-docs.yml | 1 + .github/workflows/manual-publish.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/manual-publish-docs.yml b/.github/workflows/manual-publish-docs.yml index c011c70..50fc31d 100644 --- a/.github/workflows/manual-publish-docs.yml +++ b/.github/workflows/manual-publish-docs.yml @@ -8,6 +8,7 @@ on: options: - lib/shared/common - lib/shared/internal + - lib/shared/test-helpers - lib/sdk/server - lib/java-server-sdk-otel - lib/java-server-sdk-redis-store diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index 02a8650..7849df3 100644 --- a/.github/workflows/manual-publish.yml +++ b/.github/workflows/manual-publish.yml @@ -9,6 +9,7 @@ on: options: - lib/shared/common - lib/shared/internal + - lib/shared/test-helpers - lib/sdk/server - lib/java-server-sdk-otel - lib/java-server-sdk-redis-store From 9bcb02cc2ee52851cc6db84dbd33e35536f414df Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 22 Oct 2025 10:35:11 -0700 Subject: [PATCH 4/4] Update contributing. Remove build artifacts. --- lib/shared/test-helpers/.gitignore | 2 ++ lib/shared/test-helpers/CONTRIBUTING.md | 4 ++-- .../build/classes/kotlin/main/Libs.class | Bin 2622 -> 0 bytes .../kotlin/main/META-INF/buildSrc.kotlin_module | Bin 24 -> 0 bytes .../classes/kotlin/main/PluginVersions.class | Bin 782 -> 0 bytes .../classes/kotlin/main/ProjectValues.class | Bin 1354 -> 0 bytes .../kotlin/main/TestCoverageOverrides.class | Bin 5112 -> 0 bytes .../build/classes/kotlin/main/Versions.class | Bin 1116 -> 0 bytes .../kotlin/main/helpers/Idea$configure$1.class | Bin 1469 -> 0 bytes .../classes/kotlin/main/helpers/Idea.class | Bin 1320 -> 0 bytes .../helpers/Jacoco$configureTasks$1$1.class | Bin 2043 -> 0 bytes .../main/helpers/Jacoco$configureTasks$1.class | Bin 1647 -> 0 bytes .../Jacoco$configureTasks$2$1$1$1$1.class | Bin 2111 -> 0 bytes .../helpers/Jacoco$configureTasks$2$1$1$1.class | Bin 2054 -> 0 bytes .../helpers/Jacoco$configureTasks$2$1$1.class | Bin 1934 -> 0 bytes .../helpers/Jacoco$configureTasks$2$1$2$1.class | Bin 1884 -> 0 bytes .../helpers/Jacoco$configureTasks$2$1$2.class | Bin 3429 -> 0 bytes .../helpers/Jacoco$configureTasks$2$1.class | Bin 2050 -> 0 bytes .../main/helpers/Jacoco$configureTasks$2.class | Bin 1709 -> 0 bytes .../classes/kotlin/main/helpers/Jacoco.class | Bin 1869 -> 0 bytes .../main/helpers/Javadoc$configureTask$1.class | Bin 2288 -> 0 bytes .../classes/kotlin/main/helpers/Javadoc.class | Bin 1717 -> 0 bytes .../main/helpers/Pom$standardPom$1$1.class | Bin 1657 -> 0 bytes .../kotlin/main/helpers/Pom$standardPom$1.class | Bin 1540 -> 0 bytes .../main/helpers/Pom$standardPom$2$1.class | Bin 1634 -> 0 bytes .../kotlin/main/helpers/Pom$standardPom$2.class | Bin 1550 -> 0 bytes .../kotlin/main/helpers/Pom$standardPom$3.class | Bin 1750 -> 0 bytes .../build/classes/kotlin/main/helpers/Pom.class | Bin 1865 -> 0 bytes .../main/helpers/Test$configureTask$1.class | Bin 1826 -> 0 bytes .../main/helpers/Test$configureTask$2$1.class | Bin 1947 -> 0 bytes .../main/helpers/Test$configureTask$2.class | Bin 1972 -> 0 bytes .../classes/kotlin/main/helpers/Test.class | Bin 1997 -> 0 bytes .../build/kotlin/buildSrcjar-classes.txt | 1 - .../caches-jvm/inputs/source-to-output.tab | Bin 4096 -> 0 bytes .../inputs/source-to-output.tab.keystream | Bin 4096 -> 0 bytes .../inputs/source-to-output.tab.keystream.len | Bin 8 -> 0 bytes .../caches-jvm/inputs/source-to-output.tab.len | Bin 8 -> 0 bytes .../inputs/source-to-output.tab.values.at | Bin 2715 -> 0 bytes .../caches-jvm/inputs/source-to-output.tab_i | Bin 32768 -> 0 bytes .../inputs/source-to-output.tab_i.len | Bin 8 -> 0 bytes .../caches-jvm/jvm/kotlin/class-attributes.tab | Bin 4096 -> 0 bytes .../jvm/kotlin/class-attributes.tab.keystream | Bin 4096 -> 0 bytes .../kotlin/class-attributes.tab.keystream.len | Bin 8 -> 0 bytes .../jvm/kotlin/class-attributes.tab.len | Bin 8 -> 0 bytes .../jvm/kotlin/class-attributes.tab.values.at | Bin 79 -> 0 bytes .../jvm/kotlin/class-attributes.tab_i | Bin 32768 -> 0 bytes .../jvm/kotlin/class-attributes.tab_i.len | Bin 8 -> 0 bytes .../jvm/kotlin/class-fq-name-to-source.tab | Bin 4096 -> 0 bytes .../class-fq-name-to-source.tab.keystream | Bin 4096 -> 0 bytes .../class-fq-name-to-source.tab.keystream.len | Bin 8 -> 0 bytes .../jvm/kotlin/class-fq-name-to-source.tab.len | Bin 8 -> 0 bytes .../class-fq-name-to-source.tab.values.at | Bin 503 -> 0 bytes .../jvm/kotlin/class-fq-name-to-source.tab_i | Bin 32768 -> 0 bytes .../kotlin/class-fq-name-to-source.tab_i.len | Bin 8 -> 0 bytes .../caches-jvm/jvm/kotlin/constants.tab | Bin 4096 -> 0 bytes .../jvm/kotlin/constants.tab.keystream | Bin 4096 -> 0 bytes .../jvm/kotlin/constants.tab.keystream.len | Bin 8 -> 0 bytes .../caches-jvm/jvm/kotlin/constants.tab.len | Bin 8 -> 0 bytes .../jvm/kotlin/constants.tab.values.at | Bin 833 -> 0 bytes .../caches-jvm/jvm/kotlin/constants.tab_i | Bin 32768 -> 0 bytes .../caches-jvm/jvm/kotlin/constants.tab_i.len | Bin 8 -> 0 bytes .../jvm/kotlin/internal-name-to-source.tab | Bin 4096 -> 0 bytes .../internal-name-to-source.tab.keystream | Bin 4096 -> 0 bytes .../internal-name-to-source.tab.keystream.len | Bin 8 -> 0 bytes .../jvm/kotlin/internal-name-to-source.tab.len | Bin 8 -> 0 bytes .../internal-name-to-source.tab.values.at | Bin 1279 -> 0 bytes .../jvm/kotlin/internal-name-to-source.tab_i | Bin 32768 -> 0 bytes .../kotlin/internal-name-to-source.tab_i.len | Bin 8 -> 0 bytes .../cacheable/caches-jvm/jvm/kotlin/proto.tab | Bin 4096 -> 0 bytes .../caches-jvm/jvm/kotlin/proto.tab.keystream | Bin 4096 -> 0 bytes .../jvm/kotlin/proto.tab.keystream.len | Bin 8 -> 0 bytes .../caches-jvm/jvm/kotlin/proto.tab.len | Bin 8 -> 0 bytes .../caches-jvm/jvm/kotlin/proto.tab.values.at | Bin 2930 -> 0 bytes .../cacheable/caches-jvm/jvm/kotlin/proto.tab_i | Bin 32768 -> 0 bytes .../caches-jvm/jvm/kotlin/proto.tab_i.len | Bin 8 -> 0 bytes .../caches-jvm/jvm/kotlin/source-to-classes.tab | Bin 4096 -> 0 bytes .../jvm/kotlin/source-to-classes.tab.keystream | Bin 4096 -> 0 bytes .../kotlin/source-to-classes.tab.keystream.len | Bin 8 -> 0 bytes .../jvm/kotlin/source-to-classes.tab.len | Bin 8 -> 0 bytes .../jvm/kotlin/source-to-classes.tab.values.at | Bin 920 -> 0 bytes .../jvm/kotlin/source-to-classes.tab_i | Bin 32768 -> 0 bytes .../jvm/kotlin/source-to-classes.tab_i.len | Bin 8 -> 0 bytes .../cacheable/caches-jvm/lookups/counters.tab | 2 -- .../cacheable/caches-jvm/lookups/file-to-id.tab | Bin 4096 -> 0 bytes .../caches-jvm/lookups/file-to-id.tab.keystream | Bin 4096 -> 0 bytes .../lookups/file-to-id.tab.keystream.len | Bin 8 -> 0 bytes .../caches-jvm/lookups/file-to-id.tab.len | Bin 8 -> 0 bytes .../caches-jvm/lookups/file-to-id.tab.values.at | Bin 97 -> 0 bytes .../caches-jvm/lookups/file-to-id.tab_i | Bin 32768 -> 0 bytes .../caches-jvm/lookups/file-to-id.tab_i.len | Bin 8 -> 0 bytes .../cacheable/caches-jvm/lookups/id-to-file.tab | Bin 4096 -> 0 bytes .../caches-jvm/lookups/id-to-file.tab.keystream | Bin 4096 -> 0 bytes .../lookups/id-to-file.tab.keystream.len | Bin 8 -> 0 bytes .../caches-jvm/lookups/id-to-file.tab.len | Bin 8 -> 0 bytes .../caches-jvm/lookups/id-to-file.tab.values.at | Bin 407 -> 0 bytes .../caches-jvm/lookups/id-to-file.tab_i | Bin 32768 -> 0 bytes .../caches-jvm/lookups/id-to-file.tab_i.len | Bin 8 -> 0 bytes .../cacheable/caches-jvm/lookups/lookups.tab | Bin 8192 -> 0 bytes .../caches-jvm/lookups/lookups.tab.keystream | Bin 8192 -> 0 bytes .../lookups/lookups.tab.keystream.len | Bin 8 -> 0 bytes .../caches-jvm/lookups/lookups.tab.len | Bin 8 -> 0 bytes .../caches-jvm/lookups/lookups.tab.values.at | Bin 3517 -> 0 bytes .../cacheable/caches-jvm/lookups/lookups.tab_i | Bin 32768 -> 0 bytes .../caches-jvm/lookups/lookups.tab_i.len | Bin 8 -> 0 bytes .../compileKotlin/cacheable/last-build.bin | Bin 18 -> 0 bytes .../compileKotlin/local-state/build-history.bin | Bin 31 -> 0 bytes .../buildSrc/build/libs/buildSrc.jar | Bin 33947 -> 0 bytes .../plugin-under-test-metadata.properties | 1 - .../plugin-development/validation-report.txt | 0 .../buildSrc/build/tmp/jar/MANIFEST.MF | 2 -- 110 files changed, 4 insertions(+), 8 deletions(-) delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/Libs.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/META-INF/buildSrc.kotlin_module delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/PluginVersions.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/ProjectValues.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/TestCoverageOverrides.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/Versions.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Idea$configure$1.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Idea.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$1$1.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$1.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$1$1$1.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$1$1.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$1.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$2$1.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$2.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Javadoc$configureTask$1.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Javadoc.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$1$1.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$1.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$2$1.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$2.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$3.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Test$configureTask$1.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Test$configureTask$2$1.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Test$configureTask$2.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Test.class delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/buildSrcjar-classes.txt delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values.at delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/counters.tab delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i.len delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/last-build.bin delete mode 100644 lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/local-state/build-history.bin delete mode 100644 lib/shared/test-helpers/buildSrc/build/libs/buildSrc.jar delete mode 100644 lib/shared/test-helpers/buildSrc/build/pluginUnderTestMetadata/plugin-under-test-metadata.properties delete mode 100644 lib/shared/test-helpers/buildSrc/build/reports/plugin-development/validation-report.txt delete mode 100644 lib/shared/test-helpers/buildSrc/build/tmp/jar/MANIFEST.MF diff --git a/lib/shared/test-helpers/.gitignore b/lib/shared/test-helpers/.gitignore index 9cbae19..454a0b3 100644 --- a/lib/shared/test-helpers/.gitignore +++ b/lib/shared/test-helpers/.gitignore @@ -16,6 +16,8 @@ build/ bin/ out/ classes/ +buildSrc/build +buildSrc/.gradle # Test code that gets temporarily copied by our Android CI build src/androidTest/java/com/launchdarkly/sdk/**/*.java diff --git a/lib/shared/test-helpers/CONTRIBUTING.md b/lib/shared/test-helpers/CONTRIBUTING.md index 7c6925e..b07973e 100644 --- a/lib/shared/test-helpers/CONTRIBUTING.md +++ b/lib/shared/test-helpers/CONTRIBUTING.md @@ -2,7 +2,7 @@ ## Submitting bug reports and feature requests -The LaunchDarkly SDK team monitors the [issue tracker](https://github.com/launchdarkly/java-test-helpers/issues) in the GitHub repository. Bug reports and feature requests specific to this project should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days. +The LaunchDarkly SDK team monitors the [issue tracker](https://github.com/launchdarkly/java-core/issues) in the GitHub repository. Bug reports and feature requests specific to this project should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days. ## Submitting pull requests @@ -26,7 +26,7 @@ If you wish to clean your working directory between builds, you can clean it by ./gradlew clean ``` -If you wish to use your generated SDK artifact by another Maven/Gradle project such as [java-server-sdk](https://github.com/launchdarkly/java-server-sdk), you will likely want to publish the artifact to your local Maven repository so that your other project can access it. +If you wish to use your generated artifact by another Maven/Gradle project, you will likely want to publish the artifact to your local Maven repository so that your other project can access it. ``` ./gradlew publishToMavenLocal ``` diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/Libs.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/Libs.class deleted file mode 100644 index adb64fd331e1bde49f4a915fb452dbc64cd588a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2622 zcma)7OLNm!6#lOKQiPK@N+5{?O@Kg?w3g!}6zZh35FRxcLs6RIrqIf=Dl3sC*OD?Y z-DK4t(p7hTuz+c&oebUeuk=53damVwUzuTZ@44skorlgj`scsj{{pav21B%HRlEck zLuyys*Rrf;h!f*)qI7htm|qoq@6>QX!noFvZQU2bS#@oZC&UY8wyi zz@*d|X4(LQq)AMK5T_U>IK<7TevkQRN4od428DX5n=>HQ!9|~J2h#6jtgC*Zm&?7p zxAc?56sCER-=XSmSdQgiWr!>+ZYME^vl5~>#~>C;>ejW==1n3ubi1uJR@`%adSvez zzE;(Ijgl4W`w?nT#*ZjJs1b!qiw6d_N3Uv*;pfAHIZ2ojBq4QpAPFKTh>;p0I!5A6n6b*w0tMOrI-F{1K6{Et=ovvwTij6+xeQ zuKoRV*S2YE(30@7n{Vj5v`0RnuCQrW-2avWSlH?4)Ns?R_lyY41Le@pV?HK5<|kXOoz6W zhzAVAb#32pGL4`@k=H~#BrL0H5Y`rDv#aT@?uyu^ge+GvEx#626tlGiGLG>y@RYV? zrqS97A|5fssAfFcFBwMLRoe<^y?VSzUr~GbF2m|Ve|`Eni$`PgK(m_bNj$;V5+36l zhUo#{gQmOXqe3y%wFdtV?<{m7bbbN83zDe9kf4K9M{gJn!>JmMZW*4k=Q9kcR4L65 zng*@9hk@f;b)(?yTb@<1jcafAI@N5G+La)RmSdEHdc|mNX{5s-7hPSmx3#9l-)*L( zx1EMNKmxmT$N7ZOo#wmxSQ#zhV`a3>Udz&5 znA3b#3BS`%9Ag-Qgb4P?t3py5azuZ_@Go==u<7J60pqCCnGEF^AtbaTKhcFaMu-FO z#(VM7Z;f|y5Z+`j-t1fB&ERx@zO%h}i^t-f>B?8&zX_@euV0pKUaQ#B3!MLzh|U7a z9e}ZR0YzMRZTv5_1vmcw4+!NKSiQUS9G4P*;&M46f2fwD^151%$seiZxcssD5;vb? zI2KB8b7>*uji=fscRBMOH@VMEK2ysHSyjuT{JC0A#pN$T^+~xb@4STe66SNv@{n2U zn?J!M(K3DSar&MUIE9m#p>vM-^Tb=C4$Ts0jcR#?=&P8;4U!fpof6JL#d$m;&Bv5r z1s7qGybfN|CHQm;a9ZW5!KK?IuNP?=-rYf@fQ14U3#9kn(d&Kw%8&rc4wy%dmv=Dc jVbnv;!&wip7D4oo2g$>Phm#(rTFe;_r#+-RuD|j>s8&3} diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/META-INF/buildSrc.kotlin_module b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/META-INF/buildSrc.kotlin_module deleted file mode 100644 index 3c12ac66a6a98c50ad2cfef9dd42f905b947be56..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24 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 H!v6mO{8vM8 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/Versions.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/Versions.class deleted file mode 100644 index 407dbb5f78e488ff9f2a21a1bae94233e5a6246f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1116 zcmZWnTTc@~6#iy<+b%7Xi^xUsf+(QuwxtM0VuGkiW336TjfSUfJ1N_4cgfC_=RWa6 z^u<@>1BoW4!8d=D@hldp?aQ2VzVDke=Q6*3fBOL-hbIhLMflS8Tp0snNHwh!%XBQa zVZN<4MNP#JVhA^+=Q2zcuGo_DZMX51A(Sa+>W-{-%@RKg%c`WD%W0554$fig1!e!%B zm6k7-%6rBAm*no&oc_(pLd#PWm~Vu#>Xx!d$=fF(8XXIcaB$Fgc=ZAU18Y%K&oF%I zb!T)w$910Sb#<-<&N$hq_Vbj_^}1P}X3t|9KmQozDSj}X3N7$-`2G25IY9JYm!QZf z6di?PqmVWV#fPBt&rv-nHOr_MXw{DG)JuL%2ghOpx3Cl^&t#Z*A&!Mx7jDfK(rD3y zll!z#Wx1+iIUT{Uc+hc`-4=QG#Flo|5qnpAr(y4VbrMY!Y*!RJ?W*w07HNXNj8jdi^WXK@#>~5{_-cIL&W|>^oxbQ0lvf2<5Sy u$GtpOaQ`xT5Jam)VC@i0VjT|;F($Dj@knApB68vA5-EvUiD`+X#Ks?K-^}m; diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Idea$configure$1.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Idea$configure$1.class deleted file mode 100644 index 5c3934e8aaeadeada096d375f8f21c6abbb8d33b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1469 zcmb7ETW=dh6#iy?&Du@uK-(lhOIw#ZNt<;_xz#4ENmFX%n5u~)L7v9DQ)iO(Otd>| z;4Qy}e}Ir$1QLpPwL3+{`Kqc0Pdi}uy7#4V-dw(ClI{m zYxPtPl1S9n3t$ZQTDy0a=6Y=?DTea+r7;#I<|b8U5cu9 zCxhG;h6mhmD#00!GQ?kIm#kefr0b_TFt8(%f^~-4=}Fl=J#n3i<2nlt9V@3NOHPBW zI1T5K`^S%Yr)WKc+YF@^S6clr)JaUGQIzuZ2nzU^XmIv87#eRtC=sSt^(j8He7au7 z4ZJ@?rxtPDKDxCdLzCYm-ZIQ}r4qel*cZ_*@7wv#ceT&MJswFbPn5MMi8691I%*_i zN%}MLwEyKJy*--Qfey}$bhYJX_&>hEkV1a-g`OUjTzUrpQBS z3t61x+#6?{N`wY=lcSl@F9VCVJ$o;eHa(#k(%d8I3LzZYXI55EQF?~ipGaW1Pivlb zGN5W-95N|FeXaT_1*BOBvRjE`bmjZyp?h*Drg diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Idea.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Idea.class deleted file mode 100644 index 90fdfeebe2fbc3c06a0a632a344a2307efb7da3d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1320 zcmb7CTTc@~6#iy<*>)|^qF@096_A^{fOm@*Bx|f zXotNJfoMp2$S^iuIo?OPG*Dzn6RS;^ z$nQTw{sC;H2ABKGDyole6i3ie!5CU7D7KJF|@PAyaEA*I5c| z6CA^O979>st}z&T#jk8c3}{z~)m%A$u!2P00`YXV|IRY<{PBiajnIj`bjFRAbC&;bSStMx)NKNiw*&m2pC&MqKxq}Cd!z^)NkvOwl32JuB?KERPgjFG9iXS%!IfQA{8q}h;t!ke*yk_ BEQA06 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$1$1.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$1$1.class deleted file mode 100644 index 3c1e8aecb2890ace65a2e2df476772f5b9c7fdf4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2043 zcmb7F+fo}x5IwU9R!DLHCC0{1Y>1Uu0^19Y<2Z<5V;O=REC`YnV{#j<25DinOLj&o zc;hGX1Nnngs$y`eQdA!EQK|H-WKaQ#iw8~bOm|P8KGWU$`=8f;0=Nf(ZQ&Xua6O7^EOlf*)lef6#a<67BS2l&Mva_d;mi(Y*)dF61 zg~hiWYtB}V?`0VVPkNh(FkC#-+(aLPDRzY2RDz7sr{iSG4EMX7m5|CIWSe0cmeS}u zIjxFqKTtB~dx|@r2u#EnvgvN5=kU&E$_Ye~h{3=GhW=C_Y~i#-z;GkHa#N^EK!K#i zJA@ zfv7U1PUGAT{FYM{fwdC&RIqB7;N3tfjt}t>!_aYcJ@$Q9aL>d|s-c#KwY014}fH3e84E1SMW^=_I-2+uSYlz|s58VQRJMDW@UwUdxdV zogYnux#Iyt1D4pU`_&^QJ$xeX(Y(vKTuMRPfqBn%eMxQJHYHF+r{-}*%ZZ96Uv#7( z8KK=vY|xiM)YL-xJ?bPMQM9MT{#Wbh$zv&xXL%H`{ARbJcVFvebsbEih|)SD63->d I5;=+Jzm@9{)Bpeg diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$1.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$1.class deleted file mode 100644 index 0c14ff43579a7621e8001f96cb5af00a550af1cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1647 zcmb7FZEqVz5PtUjeldyT(l#m366#VXDY>M)*QTv8DRnTWG*J-Br}cSp);{k=w`&W& z<+tz;0I5YFA&ZavDAd`rNgWa?>IdKM%+BmQGtbQWpMU@O3&02Xis8ng2#!Rgy&dj{ zepvED^@HpVBeBPIUzfHrUKH^@$RoDBb-4nj4+&`;ohEdn+M!XXUj~_{| z<{KHR3Pa)l-WenrE}v_jL7E{WeiZ)D2r??4jkCJXaA(HZ2yG-G>%~cU#$uml^i&*$ zk;%Yi*ecKByU?eyx}QfDc^3{|W5|^v;R`tu5yQ>+qMk7Ah)bnCu2g8a%~gA?(6oj@ zP|0F};d(zbfmB{^H1MP{B2ql?8p=fEC4HSikrMO|h2NjLJ>d}_2r^{2eNN&trP#HX zbYqaOp-h#k`ov#+wAh@I`L!^-!wn}tXD~|8C^7p<+E0RX?N|o})+ed6#ZVd_=iTwK z>l7TCvTQ*4Gl5B7N^tvpqho(`3f1JO_;sU_)8TDfv`#Wq^PR&uu(%( zf|j{LjRrkOni@$fWZ}@7TU#Gv?iuEPA%Wp@`le|GTrApm0g2drXKG$tFE0L$tLwkw ztuZpsu=1Vtb7DVR&NSx9?-J^yy%Gl~;+;4=P(%s0=&-({pn*LQjJU~+8BaM$W IZfRs+0Mmt|1ONa4 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$1$1$1.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$1$1$1.class deleted file mode 100644 index 9e555c82f380f7cb6515083d0ab96997e3a2adb1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2111 zcmbVNT~iZR7=F$Y*s!D!qEb)`rAAFc%YwF645A3Ap&?knw3L3GY!1l@n_Xsi!{8k^ z{UiMcov{{r(P4VoAJyr5Hmj!SjA@7Ayyrdpet$gq>+j$H0PrcA4A<+z-WQ&4u5!z@ z+_dF7yRz2u#1{AW{Pd0VZ2M0KW4JfJb*I?i2i&x|Q#02q4PgcOg*Tsyu2(Z_9MaHNs@fK;>Y1 zEnB?IuPB=`&|xs#%5(cZNK)ShsUHw6h& zv))pIj-qTTBegKd^SEH(97Y&o#X@N%j!_1~7r~O-Qa;aAvqSMBE)8G+ml+1u3Y(iN z%S-D|OIvY_GsH>1%qvN%;h>Al**|yvlI3D-|8r8HUnL zez+!mUsQ|I5kAAYj?Afkq9!~to<*)78B8$@{#zlAPZ%zB7Ix)u-xI!1EsC--+eM>f zhF_0qWLn1!ssJ@Clx672WD5yeq0bE5#4U#Np+=Jjb@PF&EelIFxvk?iP4I!L+4?TS zSY|cb4*HB7#eD~P19O;X7>VRFs{9~*d=*1V* zhhnrXYa-xP9&n0IYaa9vdNFmx)X_t4mGBJ=Y>&9=EJOPAC}Er)8Cp^^v{8+&$ESGumV0&!79WxYDdrUkB#7<2uipZp){L$;!fi1BbG7?QqvO2x=mb_=Yf{i}BX+ zEyLtH1$BJSF!2sC9X}E%wSjzkk6IU7a;t=#LuzA6t!71dTfAb^ORDHv+%9uZDn5El zZxA-frYJZE(wFr93V9Y^gBd|M^`h=pUwx$EN)#MNcuO{?^(1y^#j$MHCoF1-k(a1o!|ovQ>lp)d_*+&9M_)X zz1Y)IDNPu%tnm*Fy6tGS!E13}ON!god{%7C~P}!g%Jej1}U@ z6&~Udg|bU`orV)hy42{um#TC`lxDp^&ng?GtFke2Q@`R`?l%;bKN3Zq&Kyzo6&NLW zYt!!{`a?u{L?zJ}N$sFPH;t0s3uy&5(fj0YjjWY+&{M#A0Z$6}5*shMO~q}g4@)7?B`NANg diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$1$1.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$1$1.class deleted file mode 100644 index 7c272acd7062c31f4cbaf04cc57e25eb4020c87a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2054 zcmb7E>rNX-6#iy0tXbS(oF*k9H-b})9ay001x!gOA;pP-6sXgb-p1>R8CcJv-7!l4 z@hp9VN=+MDsY3nPhpKvJae@<&0OMyv299>vL6iSE^gW)5Y22Pvs!2yY-OQeBtsP z>CSmt2CB%Aep}l{g5ko6;x@V%Z1G%pO)W?$cihiNg<+~qS&K+Zifk)R#nmPQH*EUi zRdy8__}s*@N{==ihS^+uuxB$+9I2#`M9P7M_ZYgfq40#<6(Pe=Y;{ZMYRILET&`50 zW6zPh9O&hy?-!HkWw^E-XkRLKYq#M_rA4T?@0OGfNlSW>jSQvkZ3=I@rS_PIydg-C zVfciSv=wR17^%lWzK;tI&f_9O*V4lJ;>!Ib`Wc)^=mlRigwk)-w@vjj271tgD znn7SFp2_{UB%?^w%2LH`IzcjpB4!L|K4<92 znYNALu7gh8W9TclP_!g8uW`*O0IRXvL92_IBVmpXdaK57U|`$C)g}l~2YV^!V9&AA zmSgo>bWI=ZUAD$q-WsoL39R9ZGfN$tu=();U$F?Y0sXcJRSH-p;ij|8YxNhuM|`Cmu)>$fcIg%AvBsX?AboTl#wO&?-K`r^JEa_!*|i{|4P{ zLKxC?`HudbRH8kiBr#3TCWQggHJI$p7hYg6fBOYy$@`pfO7i%g&OFi4nD37r0sVN~ z;@4ZW(xN{QP0-g>{75%r`lpya0VjHooPI&p<~Pt$!u=8!N_c=TUvZ0uE1Bcr23Ukg RC~qJUVJX6LgsBM0{{h6H7B>I@ diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$1.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$1.class deleted file mode 100644 index 69721e93b72d14eadce5b5795ce6611dce8c066e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1934 zcmbVMZByGu5PnWJmJtSwlhA}VA%JPX&?wOKEhe;tlH$~m6sS8LK6DQGVh&`9(Vdv| z8^5LhpfhdLmzj>IAN!*^-IJAInq){S&FtzP?d?9dx4VD;^ZTCwZeyEaYG3$AB2w-H z?uA}h_QK$?Y{ik-;p$M87t0If1sjZEcWLKN{eT~H*XKdYeYkfZJYD_zCD`(dW?dxK z(sv)nfv05{xcB607^t`{qAEl2zn*Pm8LlSitG3z5G32=Ci6hN$W9C(1d-={b&cxc* zkqlZk9EOoo2DLzomWXT=7-o9icFn6jfl|}zY@>iYh8$QJX6RS46>v>iWEg7mCmT{J z(X7irD9U&}j9Tu2(0dV=fpU2egqoX_D0ef|o3Za#^B7^cb{J}32JXRe+m%Eq3b^l5 zK%%Uqr)*qexa92%?=ao=h)29F$ne~3qz5sq-@;`FqqvesiQ!^d?@RUFu?+o$(N^pm z%4-SbR>YgWaJ5jHlI9*H1>6~=S1-yf+{gk0=4AZYscBb&I z(+w}lK}8BFQ$Bk8Ur6=h?ZVYTPi|7B|921C{yiD74^_w1*^sxc1Ww{0>jY>N|29Km! zr(k(24zz5GT5v3tB>Z`UuAKHf%_B2)C&9`Y2a4&WlCUi{_4sD>S$I39Zp;8X0Ws+!f35qEdN6qfPnF7gVWDXb`rB(ju9 H0*C$uxjg#3 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$2$1.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2$1$2$1.class deleted file mode 100644 index 56923e748c5688d992bd1632146f788f186c4682..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1884 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 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Jacoco$configureTasks$2.class deleted file mode 100644 index ba5386ac14925cc3b0ceab1bc5f915a66207a2d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1709 zcmb7FTW=dh6#iyod+m)$979{DKuf4godmi`xW}fgaZ~DGOw~k2C=$|mJ$1%j&sMu* z3*Pcu_y>U0B9M^9BR>lD%-W<5iDdO*XXczam+#zW|M~ZizW{uMuNkf%3;(GIBX^&B zffp3LKs}M&VJHrH)Q^fgHW;D;qs@_ zt$JDps?3o4zqXA8!=($wZKN1%@uTpDT98obyr1xB+Ctp`c2$XkXDndA*fI8d(PxUSr480aIGI`Un;jZ8n{wv5i0Jxb)`enl3rvZPr-Y~!s}1f9`TS51PLHG&3$MrOl#OWW40D3O_>!T`M!qh4i@naLqZ=*^0+n+GBx9J z(a64Rb%x4ZIFs?XsF}!Msv?!tcNi8*rZ3*ZEeA=ID3=wFDjZ*}G^XYLMrhvQnv1+vYd`7&pQ_fI7SEtDcnMdGJ7>s0xvOSw4Q_2fOO!T`b zo2e+7#<-E86XD2A(s#SBsZh0`Lu0+%pb|ERgSH3{dD|yWz7crbZ}CtXdde(59x5#d zqOL|VlEgn9g6hk;qW6WedK`34FR2r8qr)D3NecUfcGHo>Ht<$xHz<-D&#`V?fWT~=USEk#1B-vc z`u;h-_zia_LP<0dVTHWvcL0m>RDy_%*aG#3-sjRHlgcp9$tR<$q9wOw zq;{1jVig7yz+~=3NjOt9y&S<#0e#h-Bz;A2Nj8n8~N6c`04aAU29E)bc=56BWLGAv+FYm_;*I#skSRKe}&{bMBe7W{|(Wr;(VN)_(si{EL zI|mIJZ3)6K6l$R-{B02_BM+4JrfLNurcWKi?6BX{wL23aPmS9Po2QpFFu7_F$Y|Xc zT1yh0&9D=B^06{urv_fP&*h30sx58$jww?r(KV7XL>)5&n7{~0X*?uppRi^!<`xU1 zzhGjiP@r@1B_?~A{uQ@4mR@4+g#U?v9P4zBlTnVo0+SSA7WeQK>2CEM;e85eesi!; zq@aegiQ{-gJVzFfiH`9>KK-nokjmy{K_o%ZkQ2b9=n^$vMi!2-^g0Do52zv&I@bpb zLRRIZY1kSSjn#hu DTa()Z diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Javadoc$configureTask$1.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Javadoc$configureTask$1.class deleted file mode 100644 index f3e3553e5071f235ce40de112f63a860f7b1872d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2288 zcmah~TUQ%Z6#mYTGGPcMpsj7SZ76BW#b!W@mj-AtQX32x8>ArKP9}#iFqz4kIYIiy zvp>Us;95$nuBBN%=vw|Lm-|c-2uZLHbLL$3+57wU?fmoaufGGhj~#}ayTYjmKQNd0 z0WW%1((>FLyHxeXIuG`f(>fT#!`bx*xjhmv9qyLQ)xw^zWO}}HE9dzov*h!lBTQbg z%>_%^o||Tfx8K$gVYt*)Tt_d1E)IoNm4bxEI_->aFeKY9OTrtN$_~+J($QfsjE}YB z>MENa--scKm;ns~41GyoSi(LKKEr6JU{A<`&uup_x$An8D{g_g;>ne&DTEY)4qxR#FL3a%RH$2Ed~8YabQ%?mj! z9$F%_q2n!vyh|quD|A7kAZQlIg!B@w=iUpC$@#X3x6n>t#K3jjVo0^bzN!8s^quN;Bq^gdzZ1Ui`BB_q805ubU31+H4B1xi zH;w+UJI_#fY&)W^iX1wIaRaw8L3y9F93BJ}E_X9wVKHl}8djL{50F+wW(lC&wF%%z?iHPjy3Sxy z$)2_}a$LK;?wx>Px&>E9hIK6@9Un1Fb`Ux*x~hfsjH#BBz@mW*c+4=AYqXDLA$gHY zP8_uIK@T-+riO?bdT6f@?ty{r)2cX4-LQ5PGipbM7S{~zibm6T?dX~|#U`{V?TPW{ z54~Dc-7Hcn^v`lC*5_rf8c?;7mccex>iC>t;LJAj-Zg=nb z>|=O_O=@6`DoX833T?(KQpX?6*{)crmJ7mP=LLs$2J(_y`^&u2SP$nsi#r?Kx7B%L zGx@aYO1mtw?tvZHw13iD7f#zXl_WWL*DIbJQZB`^t}Fboz631?7hTKo0_wJPQF;sV zcFEqalGr-NyJ}v;%rE#*!!hQRi5`4KqYiCTM`kp%H%z*FagiX!se27! zmY$cdN$7IjBIfasR=^w{VS%uHLvx+dkHmp*>AOH~JEA{j^T)KU=&8Gko-r}`Gt7w> z_(T~eQE;_OQtA}+{1j-6U;|sQXr>XsV)*|8g8q+4@(JC_ZlfoQr7S+pB8TPXYDKM9 Y)v&e=7T_~H-9{upKEQf_g#gk20E}&ag8%>k diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Javadoc.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Javadoc.class deleted file mode 100644 index c510c83abd724eb8d7977487e8ac6bbc2c1d949b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1717 zcmb7EU2hsk6g@LwSk}14)NU|ooy5kDW2ae6(@$Kt4oxInN~7XhO8qn}8yI`pwRQ%c z{nQ`Qr@mHwaN9@~O5ggUs@?(YG@wK&($4*uJLldzXD)yL^T(e6?&A@|e8=<8yhu3* zqAyyZn*?LXoRH!8B4|6`HcvcPClO$QwRQ+Wa<-Pxvcy@ez z)YrW03#HD4?u<%gSQ{oi@pLm1GEj~Pf>00ZR!%+C^`7sS849n9q7c#ay|M+1VdXT` zz6_j`e%FzK_M$-ePBqXGflF5Yp?4O@E;E3aI(K4zF*aHfHx5Pm7k&f=c1tevBrLnI&!(8YkWy`oO?w zn0RZ4p*c8TwRw))JTo}Axn&e?8`b#e8>@qJ>#rYqZg5`WMfM6U*~r zyp+z&I%CCJcqcf!`yCOwLDH3+<@Vy4+6({6;l$n_pclXiKG zdNfrlcBr|yVGGNqSCYGAB}m$aIV@m?tT;X=>j#pC&)jx4{Ts5|&#?I9?gerK!S-xOtQmU@CG*8kCu>3*3H@-jokIU{a??0prvb?pz&SCnys)_YC~W1wQ$D>sNec zK-jQ{OOO~U}UNYYgN6|Ed)JS6!&`M2nfwvQ33;%*i1S8)%{rByW64sD=x45sh_ d?jK`TVO8Nng>{9Qp<*elDSV`mQYa{V{4Xeztt8i#z$ZI8>I{j75+99`ZyFKYNf&+Y}h>j|3=UAy9F*Oz66 zg<0!3q6}Bws2)d*AugT^XQTxwt(=du+GbdvF&>UOo~!!yfRBZ=cg;yTG+akWCE`di zlvZZ(U1(EYZD)``CIt(Z7?SxwIKmx^fZ=ZF=uqfRz+I_qE~T%z$yM1+UpGgdS59D_ z;r5ZQJy+U?k0jB;4$P^R?j6H_+z�(&p;sonZTIfLt1N+XRM48^|I!!6rBK0Yq; zAty{lQ($jWKV>)S)mpPvyT4i7DC*}rfp-{kvn`m75XWtXh!Q%39Fl3=!7{a$Fg7X4 z4DL>%=Nazo_eHJ}hRsb|ww`O#Oj1d66f%ML8KS!HDu!E)3+9z(?NZ+8<)_FgvvY;G zXi~^QY}=Kt-XY{GW(3x8KZQ9I84~qoYroR0*66zJP+vo>Y-8HwU7>lGYfdh#!8k(e z#mo^kM}%aZPzDBeL|nH-T{=C3Dv)!sxKE7 zv%lb4;T3LDC2qaKyB3IfkC=NQW87Gwo6BSx!#r*40cx5JgnnGeDn|ZvEe>3 W$JPOuLK)i!h$?)lu%l2=Nc;_p0G9&* diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$1.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$1.class deleted file mode 100644 index 6bbed8aa90581ed765be3ad02dca47202aa03f5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1540 zcmb7EZEq7t5PtUja`xFx9EX=A6v~S!jzc*oEwo@4a6>3A#zi=agz{;9UfG+R_oCag z)qcxw=|7;AN@=Ca>c{@5s=l4GWe1WG7iv!{BiBP-G zgJDTWT=jU^Gv`$sjNw6j^ImI*kGbn})px(|?g%fc-9M>n1!3RqhrH(tm+wip;YBh~ zHHN}*Ya2<1v!}vsq#11SgYZU?AW-?FpXDuv>M`TJQP-Dx;12m%DEFCJDT9{ugwmq3 zCp;St!|n3%7~gAGTi(hegS-O^A2DQ0q40zpi;&@3{Me3&x*?ZJyIiRt;wES9wu7iW z^8H!{vkaGZgUFZ4-5C#EsUi_7?z>GDg=8f?ZKFtudIRC@PR*Y4kPig`8E%|Hz!6G1 z=8=9JQ_=;7OtanDY_!)m7%sNn0j}k-gbP_L;$td`f50Zg(g_-; zr8Zn$!Y4W8aG4<)4Wy=M-tK{VW2R(KrRm!QU;FS9F{JBK$>=^qqHJRL6gM2CP^L`l z9x)$Z&9$awcqSs=;}IwC)^MDlF<@p%nk7NHZma_X+a;;D%21l@=bg#EV-+mNTCnI? znCvfERaUX8))VKipVC&wSoYQ!a&@i(_2V!YX^Mwl$sHls##f{`GGh#t6J(y=lW~pj zp=KJfZ0d3?ODA%XrbfTD8xh~@L61gewk4HlkA__lZt|{Anqn*PxWC0iY2>N0^kSqU zITTGbmRgeju<08A_c6^bu{8*KhnG~Ld{ZeAuKQeTLE`L&^87$k8y-MuNIJ5wcr*&h zx6=tmp(h?o6UgG!=hlJ8BaTp|WKzTyeR8PXC_7V;LW4#xOPU5rR|sLzo~^7*koy%g zuSj6{hSoIgAn2T#R3u{f(X;W*Rdn(r=x#=VA}@ zxElKhjZ+EN>G4N&XH!F!xL=&Gi iajOXz)&HbbBdwX`^KCFqpZeR|NNU{C_)_D#M&@6o19w&c diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$2$1.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$2$1.class deleted file mode 100644 index 6e121a664db5c4c008225b0a62b50bd3fd2a45ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1634 zcmb7EU2hvj6g{(c{7vHo(ljk?6KbdvgEvVFh1#SsNlHx}H{d7|ig+0Bj_iqdXQQ39 zMc(pT_y-7yQjk!@BR>jpXXA#-DMdVZX71g4?>YC&fByCB?*Q)OYla(r;T?-m+k1gu z(h*lK4_)(KFRi7)7#`L4AGVJ8h}#}lJ^NYrNH|e#PB|JwG4%=cRynwPx zBgb&BJjw7sylTq_1!Pdj!NOY%*-|JRAx9!)xD$su5>YqgQfZqj6-3F6|ODM_aC$$2uVIhw^t`m(GA1bH6&BK9r zve?;vy0|a6pTWBfi<9M^43x$#hJ+ST0gK4yaT`kv8RD|zbLkau=NzR(DB2z9O)rL` zQ6s3T6A;P=49Tc3HN(x;8%C1GX}VT^?}?Vfmv3Z4om^(i{oWbz>bE*nlK=jrwqQ zTF9N9=B%QXv#we6ES{ZSw^rGTwQ4=d{qaM}${0^~ogrW6Do`hWFw{hiqAdJtDbjev z@ZkTBbmv$&j&<|N9uH;sgww}Jg&7Mae7BNUZ82H z&rNCNInI!28wCQCL z-)Hgt7<+VY652Xd@!la4P26wd<0d}AgO}2$M*7Sg4-dgKYN#I~sqwkShDJpr^EV9G Bk>>ya diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$2.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$2.class deleted file mode 100644 index 7e07cf22ea5835f313f6c406318133ca49c8a71a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1550 zcmbVMZEqVz5PtUja`xE}$LWic(w0(}Iu7k6X`xV?wkA!fsq0pC6ba?i`fg=!a^8z> z&lY^kZ{Z&R66Hlg79aUhh}nx9q9jF#54XEB`^?VFGc)(+U%∋4YpqEDlAmFCy)} z3`bQRbLI2MH|KR5jNxH(>wagKPq-U!HE>__cZC->9-LNn!f46MQl)+;$5kW|~ z_4b8l!(q5rI~nDFuxu=E7m-8JfrWP%^3_OqLQX`)a6Nf$SH%5@OQl_|R2XxUw|2W> z+#Lr&BZoPLD|=xaNagNMMy^z`h!hXpwu&RNlAg9vrewXL@b+eAFL}gAf`AM+&miF# zEj{zdAPMpu&O2DZ1%^v+EoybmMuj2Q?)J7?-HlC#%bkA#H;P!or92k#9;NQTVU%I% z6qU1r8@8_CY5@hj&ybFXQd2x{_hCAuYEadq*U8}S|0xthwkeg2A26h9CXNqr(?JF` z%D3qe|H;*SXI6~oBIZ7iIYnTNCMg;SW|p*BQl#rAIxw(3lKeG>>hz%KOb;BZWI0yF zqGM@#uw<>Wy0vOOb^iD+Yvqikzs^u-auup?N8wmgJoHN87|k|5CB?C6W2m1Z^z7bD zDt#Xf(~dP$o$Glzk&7&~{O!G%INu0;8lJh1RH8c`^+mMB`vGanozUaKHjku{XUgiU zv5MtLwADmvN&2JKYyH#BR3l<*82U$-RHI^BDG_Z1Tx&t%+@|t^P*XD=LaASRa-euT zj>xyu3&)Wso=6kO;>_pvp~qv6uu92fh%Ng0P}5O%rXr;l4Pc%$Et0Mg!lFH2Uzwut z6W;xv1conY&C(8n&YO8fDuF+n!53D_^FQO_$`80ah5ZYzSuloHVraO{ z6n=yC^=o|oBOV@tGI&DJGF>kwMwJ<`f-gx<<1;kzlui~U)=u_E_=@&NM8=YkZ<{PveG0?r$p1d*1*6 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$3.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom$standardPom$3.class deleted file mode 100644 index a8a18685fdf99f50e59f573f1855b37380c5a1bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1750 zcma)6TTdHD6#iy0_?mKQLjoje(zuO*;w2&J1t={4Q3j#yu zPp6R*xcXLj8Uq4p^^@|tMiErHc+cdfKyE1~h*GVHMFjG3u z?Na>0E42=dv@ynREi|=m5G-UcC~#{h zG=Z<(?Owi3UR!xPN5|G>Bs+>gfqQQW_)H4w)=4WN`2nux z@IFQbuC$a{3AJWjNJgMon!FHi6!!e4irn=mWUbBKLIKw?mPa1r0&}s~nQ!?f`COM$ zyW1#xVTUE}YOl?P*a`MrTgDkLj8vJ33~mVAK3|6O*Q9Yvpf6UYfZNFC@geRAjFRtD z)l-3`a!R~_yN8U50*{Uv#c|uRdUDEX{{J|AA}~PjucDxU(xJCWfyuTpyYc)xM3%v{ zK+3fJSYV=h>0q#_vqg(4lBdAbg>+xmjyyQ9=xg6BF)bzAYxnUWhkncnWGc1##&T_C zmExku21-!F)uV1&Q${wWk#ym7dVQQ;VV9I$`eb*|bjY*AO@@Ili<0%bio(6{ejw6+{ z>N9*}=V`8hM<{10eakd(KFd3X*610vX4O>foyOf-vSZ9YR^ry5G{$ zbR*uKtB2jlQ_p-$WbEk9&BGhdT!dK$)6Z=2rNo$x&Q@e-nJ-9|mNja3h;X=Pr=|~( z{|)c`LPem)b$~kv9kve+`VzQ4f|sX9hJVMk={J~QCT_mLM-HfbOy%COGDYRnM6o`d z#ODZ7^gGUnVS*EP12edZ7yLV#kSJ5P6v5Rcd4YAxY4!|;D16VapQ+jBZX1(k8^SPm z%d~&$H6GflEa}fqeqT#m1NlGU{P-G`U-8``s2?v0&GD>F0Qlh4xu&p+=h&d+Fy>du yUd1N&Cq#bEbbhggz6!pqV7`I{EFNoLS#8NKkGCLV!k4#@it%-fl^C-zGXDW&EVy3) diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Pom.class deleted file mode 100644 index 287ab9edba3b3c76947eb061a979fab50d5ea2df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1865 zcma)6OH&(15dLNbS|J3MA$E8qM2R8h5m+A1%fe1D4_jEs7%+)L9$xKEq=nTkwYyT4 zZ=CWoa>yyyI0u`gDn&WB{HRpESqV(Qp|XeR?%D2dy1(w3zyAL14*)mty+CJGy8AK+ zjdj1KLkMJd&4y{XrdKf@ly{{S=}6Emj7-lq1DkFF6stf7D()?kZW3E@GdY&Jdk>gV@`BACvy7?4zfuTJ=avjgu zZPX0Mi)7%Lu2Jx!fKiSW>PQP*v8vMAYlW?wfmxGu6u3TGJez&~)Xt^?sW7PwGU!U9 z6WszM|Em%mSpmHwqY}>&7#OYd80=yynH(X4w`b;%6& z+^4x!b~mSz$(fw5{7?T0rFqvAx>dU?w7Yn%##X}p_rbu z{6MDgzCiz}IT}7SEWxZ(&pAeX>nO#u=t;@q--1$9T>w0Ecy&V3zn)(#}v_A z$C6$sLxF*FquBI|yF85<#sBOD>CNi6aZygQaZVp~;6r>YkhWz*x~jin2A?+3Z(e{t zr(^yi^tl-Qb_Z_ZGu3RXmcc?3_KrYXRCQQl*J;A?obq#{_xj7nw$3}A6Wu2As2Z2M zxR*v73M6pe;vI{BT`smR+?tF`+l)+}pw$`)J`h4JZEE2Qpq1l22#7se_Kd*Yql3P* z)~BVl?9oA5ODRQ5E0udk2dO`Q(E5%JX0>T?M^9-le@gQAYIim28~WqwLn^@L8;rx8z@-SyEH=?USGENv7a=GFWuY zFrLEnre6;%x#Fm!`dg^m=kmi0i)mBL^4798%5P{TRpp9h&T2cY1iq#9BYic>jftL) z7q~QWh$}4P%|pDcfkuwTd&kBIjq6S0877Zl9Ab`M&+tjC=VSfzSYOoe3sE(E$8`ug z&r741Z()v);v~8_*1gQE9}oHW34N|Mv%nH})wjWz6=rNwXX~1&mua-~g{08boN-&# zU{JELXdi!$)mO)=g29rXaLqIwTJR~{KM5Wol=AEP9opmPczl2SSL`Sd5$Apkl7Pj% z0kSqm3H0%*Hs!6Bv?(=w4M|yKbdfBt?I2M=selIstmEOawV|v{{=n7_M2JV&-a#tF Ta)>WN+zZhjD|(2P5MTZS{ZGF} diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Test$configureTask$1.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Test$configureTask$1.class deleted file mode 100644 index 24f602468ba30595b1a92295a5ba409e04e42bc3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1826 zcma)6OH&(15dLN*5G;!k#BVz!V8s?v@M6b#Sp=KNb}S+=2`Z{moJOlbTC|!~c1Eh? z#&5|#NL4}}hmA(mV}+d_DuKozIaw|gV zj2Fs7zut*Ng{x-fP6~|S!FuI>`G|M98*o{7pH+{9rwbdSqH-A3-Fn1pfpB@-cQ-xl zhqAzs9=1*)$#7w;dJ2;aDe;5wI$DrYb~Mh)K0|iMSQB35dSR>WQ}iuDTkPLcuo+gf z!`RN$FRbi4n1W-&!YspdCK8_TyCPz^5j!~&x*Bm`Dwj(cYHo5=?oOz8IzdpF!W_e5 zGt_}E-J@>H^`#b(h>T&XtKjbi+ORG;C5d8GYA?rG)rRXS0Oyx`z6u1r(1R~ekT(8U*?7;a`) zhP@n`>OLV-41<8$z`U=6>v-433>F!dhn{7OehTk1B-=qpF>H;%9Ai-UpD31bV+Kpe zP#)^FJgB5^hZUFfCT^KTvII~!d;+*we$#kRYtLnj;;Xq~$PEWOyVV#b*L~^h4Tk&( z#n@hMXCaG$cW?YAgc-YZ6x@(@gK! zv3tk1m9}i_vPIW)@A#^<#&Xu0_0<0Rr%7wdSk~?`%&c=6${$-{M-gQ5;+(Ed3J)1( zU)vxI-;dvD0Z) zMO5L{fF!efntRP{-X8D^<1d!{xpJA`9@fK z`ICCbDM=}!c%B5YbC0DLgo>uOuS&aR&#z0aY1=Z`dtoQ?gc&V{D}$K({fMUmVT~qr zf+|m&Ih{76YaTYeNgHdLq)&)jA`Oeq>0G{tnG>A*nHYwzh)&W8IG8sJ4vAR3PNz{{ z&M(aWj!U^;aP1#l%71r)_sqSAr9be2g%hk8KM6dIDO-vio+H5|xtqlT#XXM_{k+(B zhaB?6kz;&_RSLXC_Y^fA(scQnz6q+yF`d-ME{P3xV{5?Jx%{uVmirBNO;A!r*+@yt z`~~H`O*Dy3Ji-p$EW+_DJ|AP3&WEJ=5n0_kK%#_?OZcROPx09c?sLQ4H>7X?rm&7L P4vV6c41-Iudx_u1XEn}47E@i%}^@Eyb1Z4q=tq@0>i zI`4(@vES@QqQ+G#eMFp`giC;Xm>7%ud^?g+gVQ7q+fDMQUo z6_isAb+sD=r8GvV?yXPs;Yx`{8k1C9)$%(XBTUo5W9|orKf{n!n#%@{8tYx1 z##x3FFBGd$6h>*BBYV*kQY(gu!sd~ngGXU1^FA(QkwuB6+|56SYamlzYFkC-&`&XZMqGLL+7DjEZQ%{ixEIjpvPN-VDLu(KD2epX2bLkjLbr zFMYkl5G$D0UdN3L5?EkJm#c1Vxw^7S?4n06xo4U9Rx+|tyGm9RleX|zI% zq}pw7iKxc60@B!((Br`dk9;E!l==Hzsr|Mn%bu@%(jN}-@_&1Qwj)(*J8T>tQnPbq zNtB!hJt+El)xFnq?3ybK;Y;hmi-|*JYBrw$In-Pub_)J$zuH7~jK;g@?so;OyaJ3nqZo4`3rs zK_)Rn*yb^f1)RnMI-NKOL4Q{UF9pM=xJF<$h)>Z2!Wc0R>6f57?GiPwl+-4laiRux zV)iLYCKL$$&BNnUeb+$tPgwP*SpOAY9~dR@4Vl_>Y|;avstjnIXq==f9ua3zAvXK; yKE5S-o1!gIQEqJ_R>tizmdp4ID=(zajkIb$YnxySU*OIr;tFMjFBPsSq@M#=p!~i7 diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Test$configureTask$2.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Test$configureTask$2.class deleted file mode 100644 index 65bc91ae51836b9103ed7d539b4c58dd0680f23b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1972 zcma)7U0WMP6n+W)oVPQ0zs{%sKm>Io~J$J^TA9fDf_5aIqoW0}+IFRfH<- z_}-qZw*yh-VKaTp1Y@|nR=rc&=N)dl+^gGHjW!V$6~0){J*_P$WN0heBAbI*f-eWSDRI%9WnI z-)Y&>QzG!VYZpBgkd|~p6Vnum(-2OxueQYl-Vy|4xPD5ACxWi%N9vK2@8Fz;w{f2A zomI8;s)-qfM6q0{=F5c*hK17E_*@cmcy|o5xX3WBvsm)$bu!B^HLG?9MII=*#~l^gJxDu8GKeKi;U6W}Id^x(uZF^!8PicCF(KYEwwa$^VkRGxO{7)iH?TO@=*=U67&#rP54`h^&VuZfP*2_!M4S1n=@yiKQu-^fl+ z{fP_NUva4m^9YNX-*EL`5$cca~}~$X7Dj?lN(jKo79G+0DMEg5z5dZoz&Hzl2}Kgb+v0&Ci^?CX8ypM zHcU`#6P>1Svk^ZNp@12DOMDz(p^We7W{{IlqVo~Hr}G}Y`x$Ak?;uvh-6HN4k;mu9 cTtRbRXtJ>b7UDj>+(A6VgAm0Kw?ibJ15%s~DgXcg diff --git a/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Test.class b/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main/helpers/Test.class deleted file mode 100644 index 25f3e67e0528905ffe692807581320e99c31c545..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1997 zcmbVMTTdHD6#iyx*!5zAaT)^T($YA^7?L$Ky_(VzH)%~=H!27z;%U6zn1S^!-C5Je z^3)&Fr@mHwpe<5`=v#kO)id6Oa0v+2lIL9ZoNvx`{`vRMzW_W&gCV==dOL2Y>@`M4NH;4EOu_Y`IY@6w+6=@ckg_mqgjk zAZqqJugXxo%8E=x%X6zHOon^gLF7r_-rDWj(vRHG7oJ`BqmaO*qYMl)+;lcwXFEZE zAwto0$&g{X)HpvI)l-^P4bq`bWs%3Ig%K1?WEk$89(>2UsU+>v^6M?62G@eN%P`WAzT50|TW+|f_l=>@2pr+9i%@DgQD#`AE~0t^2+k*7R%Pl4z|4z0h%=(G$lok)W<$h!yjc zSz31*6FO0%Pe>XhI;6XfQ4EljMvbH&iE3MWwvhWBH|7d&(K&m7v6m0`F?onPf8bq? zxdRln;=!@vS90O_lFnfQ)K9@(+R;at!c)?fV@SM5m+PMm6D7J{a~a}zpR+JSzR!r~ zFz^-88>uA5I8rJADZi$FE=33+U%;Y2*Yi)NIsu!gMjj~dWA1fWI$Myx4ksHMB;lA8 z=CShwg3@-m1MrvoSbARm4PR&w*3a!2B!xQZ3S>M+6d0$SkLd~#j%7S2x%eBr;~hCNxeklkC3z`SvHTN?YGy zJk!~P_n;$6HZa7e#^u~J8*@H*Chv8-|Hq#9M=Y35AR~(d$jN30YI4|sflSDq99JOb iBlB|qi%W_O$bbyUfDFih49I{C$bbyUfDFjMkAVxAp(p?V diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream deleted file mode 100644 index 1904d59660aee8a5c7c0f330d8556b1613e7c77e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 zcmeIuJr2S!42EG269fy*#syl2W@%JatjJ0%RQls84Lv`h2M8;}m&MC#D|(c&ZDVd_ z_o(Vt=`}b#Q9tWJ;+Te&sTkrZ9~pAuBz4DpS9_*Zo%5BUjl8%NUh`qcYb<5sKu>Pc z2)*a^_b*?OML}f z+JTsuI4x(>E}HYr|DW5`CS9N$WPJ!sNM7#}e8&+-4zF3Y5JNaTrAQlBSP2gsYTgdt%wxe7~y@X{ne-oJyWj ztdpGM?9I`ny%jZ)6gANlotDgXzwO@~JNKQukR!<&c?~B^!IKa^j>uKnbi0%ZCUPD9 zYQYv0(Ni!>B=}SWVHvQn&4e2OPI4Px~Dq5;gM?m8q$S-izuWdf!&a zcz^seWp6cdQ8tYZ2b#}Vr%LEX4cU!3NMADGpm6u{=Rx_7$z+kn9v@AD)iC(u8}iSUE}Q6 pNkm1djqgn_OlCp!JGg0jW-pi`x7G`}G(Fst;u%=H`9nlj@dNbVz7qfd diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i deleted file mode 100644 index 61b38d12620aaf992869c3fe7577043145b130a8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI(p$fuK7>41&%7S;Gcqd+hV7J)BtW^*!Mw>Q`Mx)hcU*9D-F>3L61!HA{ooD$! ze7sLzk|k-O1PBm_P~aGv-}!SJVS3cbn^6BL(o56`;t3ESK!5-N z0t5&UAkd(|HEgX%_hEyRrU(!qK!5-N0t5&UAV7cs0Ro*8*oB(IESq#HN=&7|K3tTi zUS4_VfB*pk1PBlyK!5-N0t5&Us4ejSp@%AZmegsQ=E(N7M-72@oJafB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs U0RjXF5FkK+009C72t+RM16CduG5`Po diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len deleted file mode 100644 index 131e265740f37d77b7c4a3676d2a7704ca3e4a29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8 McmZQz0D%Su009U9fdBvi diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab deleted file mode 100644 index 4298b5ede431bf34b2500b1091d3b174f5c4576a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 zcmeIuu?d4f7=~djO%Vi}G)~|Y>BS8kAp_XCN5&8Wq96!@Ac!03`=bX(3Mcq}I3LeD zEZ%S4Jp<-p4>$4ovO&$-8T;bj3EA6nV`s^=@xQzCJ6HZ6abdw1&Y0qa8IJhI0SoN0 k#1=pJ!wMS&jS8rM3aEezsDKKnfC{L93aEezsK8%=13pVG;{X5v diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream deleted file mode 100644 index d06342dd592229787f3a02b4d8be118137d66725..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 zcmeIuF%H5o4259^2BbonxCWPC0wIwBb$64e5)|qxZhL;p5I6zb^J4vjJ$#!6*`oGV zyEJvNFS)+P)^;xI6Mk$YO|ZiqHlB1wmJYXNOmjFH#6dEh^3|E=kQR@klJr@J%cTOUx-v4KB$qN=#2> IWMII90L29o4gdfE diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i deleted file mode 100644 index cb94abc1b2fb5ace1a63f7e023236a5c281ab42c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI)v1-Cl6oBD?n_#zYE|oq+!J(5w2cZtS^#y!|EP@2G1l;W4;OJC33NDU5gH!3! zt4wo B7c>9> diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len deleted file mode 100644 index 131e265740f37d77b7c4a3676d2a7704ca3e4a29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8 McmZQz0D%Su009U9fdBvi diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab deleted file mode 100644 index 5fdd38e279d55f773479cc8e9988ddf6fd1f8fd9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 zcmbR3vzw0r2)IB53?{WW diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream deleted file mode 100644 index d06342dd592229787f3a02b4d8be118137d66725..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 zcmeIuF%H5o4259^2BbonxCWPC0wIwBb$64e5)|qxZhL;p5I6zb^J4vjJ$#!6*`oGV zyEJvNFS)+P)^;xI6Mk$YO|ZiqHlB1wmJYXNW2oiPB zpdc-SE9sy{ZIrP%+8oUL2GaxjFWxO_cwxehVck^aC2gNs(S+P+!3R(y@5uHpGMsRf zI0}U(>FmEfXNkX0t?7fpt{av@XLj-*v}kNfGcIc@7h1`6$&N+-!$Kms2Ql0GT?v8& aIeB35mYhM7a5Q3}pVl-uw%KtR=iCpZC#R4wo B7c>9> diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len deleted file mode 100644 index 131e265740f37d77b7c4a3676d2a7704ca3e4a29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8 McmZQz0D%Su009U9fdBvi diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab deleted file mode 100644 index 70006f07920ea792a9b109a988059c9e8d0309a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 zcmbR3vzw0r2$(?x3mS5Ee0oOZMnhmU1V%$(Gz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONU^E1X4*>vG Cgb~01 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream deleted file mode 100644 index 8544032d7d66de29559ffadb98cb5258d9cdb5d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 zcmeIuu?heX6hKjN(2rI$#Q$N!srgt~XT>#`OK_6L09B6viIL?n>N0*m)ycd!kM*V5sLu71jGkpY(9nK>y1+fPbgxpD`wf^68mZzt)j+wNFR$x> z!sAdZty5t1s5FqAs@SRryif7!qQ)!x5V#w(?wMqwapcv4bWeL7Fn!`Q0j*vra_aMY z%M@ecQPhfU(E7LT8EfGT8Kt0hsvpXH-4@do%-Du(Lcf%$Gvbm3bN!*F5wZ&LJED}8 zXXGZKrDKINBcdE#3(-OPjEKkl@?-8jedhMAE*E;8E%ZwClML^b%%zie(hc4SSGe~} zU(sf&!Y}h>=@K1YE(*88!toV1nJ;6_Q!s~2pZ}EH(7<}Zuc7!OciQtg-x{#xr^4AY xgr7M%S+wUj4IX2Ioe!+IhI)Y!1IV5rZ7-Gl25vAV_fzl8)^a*oYDhb7_8%Mijw=8F diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i deleted file mode 100644 index 29fdc6af0590d746252d564f1485229b45208d25..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeIup$&jA5CzaC^*E+s3g*EeYnreRn;a+)n2@oJafB*pk1PBlyK!5-N0t5&UAV7cs I0Rle@H1^^L$p8QV diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i.len deleted file mode 100644 index 131e265740f37d77b7c4a3676d2a7704ca3e4a29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8 McmZQz0D%Su009U9fdBvi diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab deleted file mode 100644 index 81144297e847e784ede281592ed8706964010384..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 zcmeIuF{nXN7{Kwvpa@x%g;`k)79~Y*l*|T;fk8$Y4U|kKi;~PH_4LYMdp48d4F;3Z zV0?I=|LM79vRb~c?){y6&+T^lj|;1yVWRftOz3}6cNx=RDU5yd{0&>iUe4IP^yA_A zy|e$yE&uoO!2~1NMEk!iCNO|G-2I6ZagH_YVhhVS!X$p72Y+yX6Zypj#yC_TKd^!g zOyLlHxWvO%{eafT&aV;nh#juy}YT0jeE V0WF{fw15`S0$M-|Xo0sCxC34rcp3lz diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream deleted file mode 100644 index 07f496f4ca81e2709010792add3ccd486b713810..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 zcmeH_&rZWI48}QdKmx`IX^4plVYK7q>=8I^LZXLh_mG$_P?{-j_x#Hc-MUHr^8n7J z`RdR9te%Kb$KN(-`ywQCc%G`DONfwiSL)h#hc$!&?$YyPjHs_W=+{9 z&X~?E>0w=f@%fia%RqpGs<6<;8$UjkN9R)W4+B1T)B!stsE!E=@TEN?IsjjLcmOB8 zT)h%ikHomy-7#^itv59qfBFR{u4z{kc4U}KkR?23CfcFhjY tX8)*0Rq$S%@7`8aPc5A>|3Z!s5CTF#2nYcoAOwVf5D)@FKnVO3*a2S322KC~ diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len deleted file mode 100644 index 0a6ca48908c79b8dd95d5f4b24232dc0cd2c619b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8 McmZQz00E|Z008O$;Q#;t diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len deleted file mode 100644 index fa432244558c364281f5897ea3a81444683a68a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8 McmZQz00G7f0043TY5)KL diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at deleted file mode 100644 index d960e5cc8a07b913240d7d7fe80b1a02e051e9ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1279 zcmeH`Jr2S!425%)4h70Uv%vxQQCkU76~v0H#DYK@qa^hFq&+|=Ll?y4<@1Z5oyS>* zAkjn>3NkXw)R#}JIWngc6c%Gqt;n9(SE2eA_m(`)zu1}XuUd7xR!5dJTKgiX- zZ19O%q818m`fy|OoTcG;N<()PuJ_Olv|`I}!wIchQ;VJ3gD*zO-pQ0r!i~8^@DB{- z5;hC#6(C5ESNxNK|2FED<}CSiqZT9yf21$a35&z3B{g302e6M>=)yK6Gb3?tIbNUv diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i deleted file mode 100644 index 914ae9cf3d9daa9dff68a2a8b498191ebf1c1946..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI*KS%;$9LDjBmWGO!prOE`!NI^K2wED02r3YS3JO9Z3ThD|f|kGnAqb+WjQ$Wt zVYCERv;;RaG**+e)FjbVB+@ekA}->2a-Lt8pLhB`@B91QaEGU8bQtvp4+IdfB(ST| zE78$+OCbzK009ILK%htgPn}`xz1%MORrgf~RAZ_|4(DwMAbGe5s zaV*NZfR69R<3z@K5DAqKn9}je-b^l5VlL)D009ILKmY-E3V3xrah5*+ac6h#?JB0$ zqUpI#_jX8U2q1s}0tmQAAfi^9`(In#(-EB@P$+P%UCVdj&B9mMg8%{u*ehTvzGrM^ z+um77M*sl?5Gc1mn?9F5x@uZ4cP@DdAbDl9Ur>uu{Ab>#W0x1ps-8mWhR?VoEp2SE55I`Vb;8~mLfc5*pTgtiU>dp^m z3jqWWKmY**5U3%6gnIRd;@KK%nN|@%z-EDa<;Qx*SD#c5RBfJ)cmxnY0D*sj27M36 z7Y;oDf&c;tAb|NlDz>7PIU|MvsZ9zZ%4NdNl&|9>u!KJ)GW|4JaO z2Bh17^oB1G_X3%tU^E0qLtr!nMnhmU1V%$(Gz3ONU^E0qLtr!nMnhmU1V%#u7yTek{PI-SMrWzbEoi{e;iJZjH+=qmM!moIT^=x tU0d_`z59Gs`zPn%<8@?L0xd8QfB*y_009U<00Izz00bZa0SNq4;0+kMH;@1T diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len deleted file mode 100644 index 5024611e4c342043a1ef83a11517021f6f2694f1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8 LcmZQz0E4Lj0G$Aq diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len deleted file mode 100644 index a5413564614472ae2e55bb44e79510947726def0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8 LcmZQz0D~I<0N?=G diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at deleted file mode 100644 index 4a5f370c66c1dda44992dd015679c52c6c3e8979..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2930 zcmcIm&2HpG5biiL9*^fIP85+L5C~xf*%jbXPDq20NOoZpIqWEjLJoWB*d0$Nw%h7% zkMaa7#3OJ*D-OBw7F>7)j@;NIFF>_D_IUnwBeX47Ijl)s5e^4%*YY zOZzoLd#P)dcG9+%H??l%;>8)rCIY!SCF@giZAvz#scU>ahcG-JLRqfJA}Smn@JMA zImJ7@3bs13oPLn_G@1#l8IjvW^pnegiUFt=x4zQVt>~BSscS(_@vXx80C29(31S0q zHr6U@TDRJ*ywnX7ZB`83s-0|D#_T~I4z?_Fvz=Lt>>9aU(`tcTd)98ns@>@p#JGxd z$Dx;FwA(a{0}=vhu!J(CiLClW9!_qr?LKs_p&WK1X<$1*vmWZGjeS5~9!B4B>;)tP zPN4U|!O^0_b!GPCR!*8Av@tT8Ia-~#8+hGA?sAXusb0KzobR@+m7KyGj;ikg8xl3yp25N? z;80AER!HQ4dE$(n-q1L$wgtpEp4gqo=*^Mg4}jw_6@qvUjmQB@qHM5ijlIlMnM2g~ zV@|Hn@9~s1Lz{Rvkdb$IK%E`rKjMxBax~ z?KmBm!iHn@U^`SgsiRc86!0Hv#3J;3XF7;q8J%HqBOn-)L%(pTMD%evFC0@_DkWQ;K{pN|JKjTIl$9hf5sG48 zr`L@1vpZEEa+s4A5R_g~HyyC)HDBG+Lr!sc6%`IjXuirO*~NcD^Mko)8h~b{v(&BU zNUoNU9Ab~T1(dA`C;`can#!PYe<(jG12M*Xrl4@qPntN*Ew z;_dWL)*->}n>%D-kAF6|vJ-u>XTV!W)wyw}HzC?qX#%L>T4Xg-m*#v1ub3w@CJg^H=3dY$XM=dwWG1~)!Z)Ic40)MxS%JgT+QPv#x;2!0~;FVE$^ A8~^|S diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i deleted file mode 100644 index cd3e6395ac4067781e593fa94c6f57273d6a8578..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI(A!|ZW7zW@A#-VL(Wn$bv*ss`6SP>CcFN_t-Kn53#iwhIPAi~OEFf78_v>Gio z>xyCDKM=X#!RvGK`Of>jPe0j3R?8d$1WF0K#`IHhw<-0AVFCmQ5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0*M6PBDdIfuM@?KB|v}x0RjXL5a`F<#_j6zfDu;-oSeWkHh%oh zUrugu3lktffB*pk1PCM%m__RQNBucTxKIKF2oNAZfB*pk1PBlyKp?5WDAMmc!>goA z2q!>*009C7&O%@iEho*^S#(vB009C72oNAZfB*pk1PBlyK%lw;pHb~5Z_lgSP%{Dq z2oNAZ;MfAa2>e?2i(}`?tG2)*>g|@lf7NcO9RUIa2oNAZfB*pk1PGKB_=yL04`cs| R%0ikaK!5-N0tEI7`~xN69i#vN diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i.len deleted file mode 100644 index 131e265740f37d77b7c4a3676d2a7704ca3e4a29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8 McmZQz0D%Su009U9fdBvi diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab deleted file mode 100644 index 33f7e47fa1492d4d978a7a3b135843a311402878..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 zcmbR3vzw0r2sl6l3 z=@1~D38ZU)G{?XH|Ca*krw%?hODftK|Bfw&7OGzvyTU^E0qLtr!n gMnhmU1V%$(Gz3ONU^E0qLtr!nMnhmU1n`6a0DG||=l}o! diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream deleted file mode 100644 index 6154bd007f9f8125cb8185c37a6e751085335156..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 zcmeIuI}XAy5JgcIV+0Lx#|9FLq)8M-w8*kY$mBD~Hd#Ld3vehYo||c|p5${uXGGn&&Dr$pA2>zA(#6qAge^P``tz{}fB*y_009U<00Izz00jO@ F;0?O0bV2|C diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len deleted file mode 100644 index a3d0573b8dcc2547169d53137dafd9a763c5596e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8 McmZQz00G7@003YBSO5S3 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len deleted file mode 100644 index fa606b628b9821e4e1a5e524f5973b5db3fb44ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8 LcmZQz0D}Vn0LTEr diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at deleted file mode 100644 index 4530e1cb9572225f18322dfe135fd791d9942401..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 920 zcma)5T}#6-6wQNzpw=fr@vCBnFXBkWXCDSKMX~7EtHk8m)pfZeN%w#JWBwM?L1~xK zwNJ^(x#xZyZu(>hT!IeYL7zi zC#fpo)=TzyCd9uH45|p9OT%f>< zRaj|9+)NE+mn}SHEg7Pg3=w5ln`^iT%6c7QD7)?y)v03Cp`z=|Gx&iVR4pvHL{YP6 ziB}z7OYlo}%LFmbj%#;nELW0i>E0IN;19ovjLtVG*PZW?!=l0f7f^hsH+Se%g5TXg#x+R1 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i deleted file mode 100644 index 377c853678786946746d36cd6e564d180987804c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI!p$>vj0ESUE3c)wf`6fPso84k_X02{+7Nbp@Mx)Vcv(tS9aYij(?GyrR+(W%w z{(UErWJ#Jd@SMWta6IcZFl~|m0RjXf6xfI6SN>Q>m>zZVG1R~B>F>A0Lx2DQ0t5&U zAV7cs0RlA$tir}}aP8MHWr6?!0t5&UAV7cs0RjXF5FpS>fo-VTO|nrd<3yDTY{E%# z=;Wm@I3PfP009C72oNAZfB*pk1S$)h!_CEW-mUxr7o!xogzC?uXGGn&&Dr$pA2>zA(#6qAge^P``tz{}fB*y_009U<00Izz00jO@ F;0?O0bV2|C diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len deleted file mode 100644 index a3d0573b8dcc2547169d53137dafd9a763c5596e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8 McmZQz00G7@003YBSO5S3 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len deleted file mode 100644 index fa606b628b9821e4e1a5e524f5973b5db3fb44ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8 LcmZQz0D}Vn0LTEr diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at deleted file mode 100644 index 6a3671f42dc73a4e10c7b13921587fe18801003a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 97 zcmdOA@JLNeNi9+cN=?o$N>OmjFH#6dEh^3|E=kQR@klJr@J%cTOUx-v4KB$qN=#2> aVE_Rz$p|Kyz$7!6WC4?`V3G|?vI77Ok`h1w diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i deleted file mode 100644 index 377c853678786946746d36cd6e564d180987804c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI!p$>vj0ESUE3c)wf`6fPso84k_X02{+7Nbp@Mx)Vcv(tS9aYij(?GyrR+(W%w z{(UErWJ#Jd@SMWta6IcZFl~|m0RjXf6xfI6SN>Q>m>zZVG1R~B>F>A0Lx2DQ0t5&U zAV7cs0RlA$tir}}aP8MHWr6?!0t5&UAV7cs0RjXF5FpS>fo-VTO|nrd<3yDTY{E%# z=;Wm@I3PfP009C72oNAZfB*pk1S$)h!_CEW-mUxr7o!xogzC?G2FT9+WKb)~&nut_x@M-U6 z1%|Q&yw1cdQz0c?{mlHj3A^9T&in4p1QR&G6!tKK9n9fzT3f&ky8mVf6d8~K8IS=P UkO3Kx0U3}18IS=Pkbxfq7d}!YR{#J2 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream deleted file mode 100644 index ce37c138ab839b18480398897dbf540eb72b4401..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 ucmeIu0Sy2k2mmkyg#L}~;r diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len deleted file mode 100644 index d9e6aa615b956553ed1ed6dae0706469d4755209..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8 LcmZQz00RyH01yBP diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len deleted file mode 100644 index fa606b628b9821e4e1a5e524f5973b5db3fb44ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8 LcmZQz0D}Vn0LTEr diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at deleted file mode 100644 index 37eccfc3f005c3bb9f096a81606619b6ab91679d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 407 zcma*jK@Ng26a>&7g#{=sq&qzTXn@2(khnEJ?T=QVpR|?pTQ~r?n$623Go0lJ5>3#c zAR~h-nV?2vm363H4)%S6@d5o8?~V+-Fk#2AZmRN{6O=ezx*qlw4l>0eLNln*aa+ diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i deleted file mode 100644 index 37f0862ae17450ce536dc425e42f483200cefd4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 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 G2EG6c=_AMh literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i new file mode 100644 index 0000000000000000000000000000000000000000..c0655e1ed7c74dfa48344f619f879beab2337d08 GIT binary patch literal 32768 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 literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i.len b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i.len new file mode 100644 index 0000000000000000000000000000000000000000..131e265740f37d77b7c4a3676d2a7704ca3e4a29 GIT binary patch literal 8 McmZQz0D%Su009U9fdBvi literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/last-build.bin b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/cacheable/last-build.bin new file mode 100644 index 0000000000000000000000000000000000000000..b0f4e6f822c997e6c846fb7e7a88250800f0832a GIT binary patch literal 18 YcmZ4UmVvdLhk=1{7Ec!6KL#KG05MbqKL7v# literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/local-state/build-history.bin b/lib/shared/test-helpers/buildSrc/build/kotlin/compileKotlin/local-state/build-history.bin new file mode 100644 index 0000000000000000000000000000000000000000..20a873d14184de3f5b5744830f16ed55056b6e21 GIT binary patch literal 31 ccmZ4UmVvcgk^ur385kI6@nrG+gYp>|09gYBO#lD@ literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/libs/buildSrc.jar b/lib/shared/test-helpers/buildSrc/build/libs/buildSrc.jar new file mode 100644 index 0000000000000000000000000000000000000000..968408cadc1d20b8cd357fb0a834dd45f66b86ef GIT binary patch literal 33947 zcma%iWmH_-x@AIe3JC5H+}%C6yE_C54elPSaCZ;x?(XhdxVsZH5TH5lzVrH?+dW2i zjj?L)A8YLWXU%VZbIxxo%6x)^|L_48_QRBby3U99AH-jO??1TrB`2mTL@y;T&iLtr z;y*Ul`a)dby>GyIU+Dk7QBFu+N?c4ul|fGYMs9peR)(Ho22qBdW^#P0PMK+*b!&fT zAL8F0Hu?Tm!o6MNg!k9#LVoyw^!_k)po61@t&Jmt@pmIfN1(Ef0-6x|tEtB-1ARjH zu-LUf*4JfyD*xD&FP5{KR>D?!+ZL4Kwfjp`OF z-&qd5lDQoZ&i*{ToWuO!%VIHC+m;%0HUUHsFR&;~)LZjR+_q`U%&P&2L3S#wNPV)` z()6@b4rx=^!raCZA`wOnM;;SSGR0|T+EKFUQM)uMPz7`38i`)36rTCVDu~eDv}Ps| zXQpFlxD?nsHqV+R@nQ5M@tiJ^0%9eqs!Q1olQYSys_E?dMKw(mbQ6*`!NN z6Zpf(@A7@l4(D3^d_~??rwfpnHTk3wz}jNtaHN^=HwJk&#j8^jenL;>9WOI{IC)I^|@1AO}Bi_-I z5PwHYsWQ{>m$EncJ^?#ClQbIbD5~F?X;(B9?*2`Ci*jw29_l|}^7UQ3s~GfkK5F8l zS4Jg@C8Z(EbsOL6kcOa+qU=mpi!n=chNQhmyT_oE$awm{2 z5J+(+5`Gy)%}GqfDP}41xCx+>;$b!0ZzMOtn37xvF7|)vk}CL`-0|Xj8spe}iAR_{Rdo1xE37fbWsfk4371CzOR7TA}&<2SooH41b;9pXLSC7w_j+8tTIbwDPvh>Vn-|a`TK`|do}-IIM)>i9*O1eOMDZYLRzDF zI%_GF=sX)d9`T!JAENEUopNAvD zsC1U!q{UP6np;h|9%bMqgjsq?)s(feYE*uhDYm@SL?-*LSCR+Y40|qe5iMt*cDq4T zg(c&pYcNBuGy9oJuO6*2dB5>iB)2qIPG|@zK~+bex>9P!#V*5$@A##jK0)Easo2M9 z>od~@LfnTuW}8*{bb!iYBjxtG+n z7rcKzzV$0(CG~>p^%VWe2hUq079oEx8Iartb}^6u?W3KnjPTumrPltOmT4D=!9-Bi zOH14O`GeR5cAcajZnKbIL<6_5;{#{T{!{%-pi8mB7-fB*_eyM_OSwY2qUq7N^ZLgHC8Gh6xRuE)*&h{_#Kf!gNT7Y2%%q9^b+C7YV5X9i2pM zU4RZoWiMKHx&nDCYjVrC%l#-n5 zO7IH4wa8tpv~3)KJZ%TVLR3&hQ7|ypMNt%m(D%b@6XBBlHm|y_-T#Chq6l8f%~HrU z1`idugZ2|zOm!CC0!2JfGIivEDyMC;Q3Zzw4>Ctm zsf%83S`(>ts{CdTWi1)U#ZfkKSxlLs{zi(HtL+CF?Zj@Lwg&WGpAoM(dq0W!Piak4 zH*u>ysLs!{?D?-0Ih$=a+pKDH6dcy0)WtZG)noP`MA_UDY22U_l^)<1FX;E+OmVNY zo1O~}N9ys7UE>v7(q`uiFK2We}giSZX!4dwJ}^! zB8~d2EhvmbrwcTKv++c%V=(-jUaf`gUI+R1k-vx+hTP5bSsuzBp^^G3M@QJui-ez@ zkC{dM*VjQRsfGefx!`!8v^-Nai30VhS#CySO;iJc>w)Rp@fJSG7)|?IN03uY|L5xC|v-QLpu%qJR~0I*WkSn~y`2s)A(5VeIb4Ij=KvAv zBBEsPY`3luXRQTo-ZcBVkQeksuaxfDXo?K82>l)lg4TjITaqzSfQJz4uQcEg1a`u- z>aQ^=wI&mQ6c>++JUhINshnl0^}#oQ_uNT^Uu|7@4el~Cv8U`5@EKxBrU~4i7LgCB z^?go0BeuH4wY^IIE42LwSL!8XeLbs@-V))B=`&+G;$SzIp@f(tIqP!9XDe>+k}y)# z-%R4@0AtnOBk`M}q?;8x^$FfIjIpw$ik6}*&Ixys@QLu7<*0%ZnHtaUt}^O%Zj+>_ z9x8dH6xM>5{qRZ5Nnje9 zHxj2>pro4hqRLS~CYy;7UNVz6kb`Dbl67xuh-q(9Ugv#2N}ix+!7sQL24IjJ*@tB7%)TO_`R z2JLgFI>8+3u_7Fk{tg-tu7v`dX^cwkl!eelp1$}V^3yc`w71ZbU7BIe*KkLMwx@ks za*5B#+wC#CO17mAN=~x)GK0yZQ5D4l!D2lqEq{Pe(P6o#8aM9r$%)l%O^tS7f#!mj zd7qC47@rMDAvfU*Wj{y51rYXdmbM1(2;eA8>qTj0KwXpJ4qVvAj;z}>LSC!dzquZT zc=e#z?i7`wI(yydc`2-|KalsY$6%GjF4F0--v+POqU5b8O)%8C<&lC$T~{!bUO$<7 zWKt1n6^T{9g-CEd?u430FETeAv;cO79z*@j>}yU&LG&_um=G*JqqI8U6w`$4YVihE z%buUSjjAV9L)$)_!@i(wgtbEqK1zr-ZvwA%Qyj+G8p|Ztb)_k7@eRi7GK%&<0~zxY z4!uFz@-?2wBgutOc7c)LTo2XA3sYzo^s{wmo$8~z=PIgfD+}FTLlh9Fk*U+d3=*S4 zYFqdM(4~<}T!yD`+?=m8d$%h+1jv$)vx17&#^tKDy)qXkC1;ka7Pho0E{3c%QSrgD z%JrjD5<-J1W~OSO7H55ZSERh%Z3f;CpnkOj>qOPciS%t%%l)7YJ%?p)JpAdbvX-B+ z`WHGYodAO-OO;an<||#lU-Cvk_v|*HjGoB1a6n8&e7n526y8+koOLD=uKwi%Ia{Uk zZMhApmzCJ=#FrE*x`}HPR3wZCpsDI^!syywd;fQNUaz0n5Ctu990=;-yt(00p7t!I z6Bq|Wlf2rKg**ak z*ZWu3>^z?ZYQ%&8Y)}1`iKpet^na+#MAKmA?{I{wL#_Pch-0SC9DnN`jO$XhRUW8@ zE!E{J8TF{q-7X1D;)lUR%0NO|96Vwf@kkO`=0;XRCbE%JSmu^s_6UFZZ0*ILTfSxQ z_e)NGW3VaSI#w~if{IX)gqKF9C6Cc{%K!*%5YX9sJ(ZVw~FZjf5LDu>$A0hV}cBLR9swOpY!O9SPn-o`=8Rft56iY;KY;>y?_s?8;fUy82LKti$yshV{_Z0;yYRWV=`Fr+mtq^~jOv;#2Q$ zU16KT#Fkh-!Qg4p?>JqQ3t_(TZ!ne6C9d#QyNKGpa8T_dYN^P7+*?ngBW{Y8kX}OD zq3?MV6dMye(dL250xNHPeW&cEVuXx_L3~oJ*#moO7l7$fr1riA@s*|MvU%dU9kj=~ z?s#bCP}h>aQ|07qLNC5Ktv_O0Qccy%y8ASn9)J2K+8(YI4rW=8cl#(4PZzBBQ&umi zcxTsvCn2?Uyc^!@h|XBez@IvNbHY2VU1+0>+y!Xsbr^aJ&MsClIM zSMr^aW)?SzT(d8!*ovsn_i!XR{V=GWo~Uim@u5QGK0{lbS)wd88llLDXT-_30>GiA zr;>G;y*ECdb8N}m{-FDv6uY6^6F+bNIxqovo@J%m0OyPlhrDT`n>Xrwx7i!NNX>9lFyvUpddH3vrv00`_vXOwcJEHYKfH^-RTo%KtwT_$zKpRR{>pq#eR%d&W6yLkIcr+OudhsG0oR%XZk6 z$aBv+I(V(Fh9`J4|agtP@4gZc_qGzdHejWW8ni8*Y`Q@rusLo}LE4dy6XDQAp2YU(wm ziz0_r*>mcHn)i#?4$hSjcXLeGV(1C&dq|14yA*IpcRzA|W%1&0K44m!qKug`B-e^6 zg|Jaqom-wfcueOVk2mF`p3hX%_R4MCNhyDaB7A6aqe9Gvl~It89?LN=t%)EDMM|o? z_J}?{S8jq9y8J4Y>!m`CQu)jm2E);n!zUPGj##AVK}N8wp&Km>zw6|$bQB6R+pjDA z%|?!t5I&FwgH>idR{^O4PtA-%*?f9#EG}PY4N6$k$&8j3ZBPH(g*#uqr&HlEv=G*~ zj2I0_P~b?5P6H*V58I+}=wh(JnP(lX9gn z=&N4{;?tD!`ZKgpS<+sUA;(}bO_kl9`}ZuN{jc6UE{J=tKkm*jI$xk%3x8_y{j8Hb zDM&Gp{Sp2+dHZ4WXH>BLAT`_WGec+5GXu9i@#$1ktmRHPMJ4-o?h{dyWiz{@R|_KM zOwsEThbhTg$dz;87z)c9#6P8>b}GbJ**g^7-=RqSZ;<^zp!n}##1s1qjQwB^m!HY? z_0GjPlLZ@O^}65U7n>Q_xoo?aj0-5K|Q zQ=7%SGcr5Kc$xe?X?<{+-1P>xh16->yn=P1_~_ShjtK8}P&)Qf$z6r9xMK)-py9=v z@D?~|98;U}SzE@6VzA9pW9RQOd+^$D4w?v_t>n@6C>@XCM~pNM+wQg6wjXEeA3)oV zcn9P)-P8_4-zfF&cJc1$Pg~?iO)Af^@&pkT9$_ewDD*zTtf{Hoi#h z7J|fZ%@vLfDC$1w;!(G>`#{<~Eq&`#+y3$5u=5z)IE${}RXuHDhalmSvc)#-#R_79 zcWs>f=OKLZquU_I&#Bgq*~oz$WKH_B-ZAL-)ytZy^1GR{A<!#S1#;Pv>{D zmQsQl9bm9{yvmrWWFDqhy1 zV!d$URL*4+ILe+!VGB3Wrt9cnggD6Dsy|MrVj3D-iFVT6jU;(esgxWQ%~GeRj-gJ>GH{{K^?4Zn~bTn+b=2lWQ+cp+CA0R7-y$>$nIgiu!A36 zp6yzV^|}C{UXzkP_o)HJ2UNDX^=Lm%-v7Evs)`??Q%DVzl>x zv|#@$+q@=poGRs<-pgh0^zPT7Q_uDI?EF!-1XQ1i{f`rb*9(ce9v>&3A*GvY=sm+u zV-?s+8aaOvTr1*E{tj_;?R|tt**8`qviKl(E$dRqy-WklAe#V_(SPpE*@+nMXfLvD}0?ZpXI?K|KI_8A@Q740xxBdgCS1Pw`=DcV!d)1zw2E4 zYJyw?eZgkea|iS*_djc)_*NS}cD z@B#JTkp7o<^DmywRFzS|llc5hJ|ljXG@YSDx;%7XNtPXPV$lpYIRg=;JJ3{kPv(KZ1*y z^P6;S!H5MN7t)E4#>3xl_-#DbY?n9L8})+gnL#L&R|a9~luqE5HKWHIMTFj@b$ItV zN9#`7?_wVQIXCXuXQ737NpHb$$cb(v1mx$^j9)ZJuEy%{07DufEw?Ucl;)T4v*pUx zi3|q%ox(m?-PD#zKcSBcJD?70Nd`Si%)vIaa_ojf_|Y;gV+-ow*lnA@j`LFS;UGrq zi)X*E>3lt8)s9_vAm|TJ-J};!wF)kN`(FMh-}dUNr?t-GD5CepvYJ8A9tD1CKUJTz z*_gA7$I+Dg@G1ii%Q(8FnOUBfa9yF0wyFs(PJ|A&$f9W2_Q43sSaw?8A7NB54ysHq zmSw8Z#Tuf)+9=zG*kmsx1@$hgK5Z0-^bU02t?!0CY7@3{Ks$EfByeUR+`lvUA=n;o zE*adA(yxc$o~fg24L8E1&1<25v3a(?+GDp5V(N!qFGQ#BXO?hOtEj4|nx~Bp)pKaq z4RlHXHV;0|?>4I4QNprx}&Uk^B$nvaeT)`?SVNynJn=c7xR z4yJ`2YFa!#fk_WxV^FyBFh)d`R&!|9n zo338uD>e(8Xv)(J746H6ty|L5`6W1IaGMpbD~O!B;OD`Z3Mixp4mYa$HI>3e(Xm4s z-1QB2Ds>s-mSn8mejP9^cJ zin-B}(w_+yo1*LivFH*(`Pm_sI-U%?zj!Mr%rmb`lg9IOw7#RbZ>5Za7G+$QYIKK_L!%0@U?$Rr|{H`lj z1e+15`cL^IUZ)2}kN_O2NTPXC1ZA#=L;a0jD=w674{L6^iM2|~EX6l9I@KD@fnc|# zWP#b)q5d7T+Jr~+Z0m2L-^6zuLv#BT`%_S%LTg2(ze)ARr6fM?82l6Ti+XEOEAODM ze+NCue}JCM*w)6>!pzwLsA}YBMaJ^4A`q#jqk*S}{yIsNaX$e0l`8oeAR+@NrWx+Id~@E> z<;~6e_UHK!;RDgP@a??=V`ENlh+*t%SVv8gMFqKjc|i@zAY|Q?L%#AE-m2d z?C+Ow67&_f;iEp&)p#GP+IkPHEvk5=pEgiM?#bpw`Cz%4sutlI04h}eb6`TD42t5GI~hH|Y7 zSU3(HFA)o?O%dfrIB3uWbsaBK#(w*eP`j~rV1eL30+`vh9#{$e{R4-G7zdz?+MVaHggJ_46~Aq5_XN*6WI z`{Qo9NU`XutyxswxOfrKJ|nqux_`Oj$2`b#;Um|{-E#&~N!>bFmUrTc-riH)wE4X3 zOP~U!vnc(GKSJqAEbZGhC;k&`Nf?sh~z#(mqWKkx3QT?M>J_C1uY&Crd zm$AD|_hx=-mAp}qBy4oebVM;K^)xxLglyEA`}$v+`+jv1BtK82CzEbA=nX(szh0iH zD7MW$1zID}^^17b1et_L5K9wZ1Y$oS|FbfXc%ma4zgGs^_r{<6-ztN&k+H3@?LTXS z&$$U_+NVInBaV;aC;dvsGL8zRWN2}ci-z- z8@CLyf;g!p=^@~>{_N!_UX~}bk&6*0@$o)bO?Eu2Ex(*zZoYoF`q=rcxt+4t8^!99 z7&-R~4NUBlzDsb;(m`w07#=qhsIB3Ab%Kz{o^h}Al;83})Uebwsw_=u?XH#iGO%~| zdA1BjT)cEt@u|rQNd;hLaGE8OL`>=Pxn~;Ip%i~eYs0A7ooor#nk>~*3MH$ir3ix` z0B5ECKu5S_z40yhzJ%p?$XX|54>ny#;OX;hZ?y|gE>10bNq$RTOvw^Re+Ut4 zu2`|iQYanZkkNpW+DlecQQ2ej^n`t>|YDb5D+!XkJx95 z1veI4_lTKgR&bq8etMY7u9e>qcd=m}pKlJEj}9;|hD(r7^=|mY?Iql*vD&>wL~5*D z#4Z?>7kj4Y`xYEt@yb!^azLAOUk0_JL>L73sJ;1_J+fFtF3SqlqGJ0e{f_kpl2qMv z1O(y5m8LegT2eM3G4|7TS?qkAYA~t3ue;R|qLAKtI8U&+GKRO9$_#AFqJMvj97bR) z1r23o!hF&hjScGPdWAVQ%uZ*03YPMCCfp9nmvR;Uxp_5@nwhUEen~CLnG7C&Y|e2> z!+kksMnLC?RI?QCv>5m3MLPbESU2|_KV)}anVbzY^-|t24mErSd5!n7jE;5i4$15r zGquK**KpV*1vo>y+u+*Q6YV1>U;Xd7)9@0^>!jW+Xr8-0rbP>nCsuoE3lk4DebNFC zYaGl4vhfke{~#T<9X_66tz~k_?fUrdDK6hPrSLH9$gDzmh2avC;h1evPED0?OpjJe z&v3fNUSDTLvl9_6zQuBsJB=}1Uiw=+m3lRYQ%6R#2`n}j#`=EzCraObWXh$yqg3vl zzyC^b|8~m$h0=d9cER6mJ6Sz4BxYD9Y{sJ>vEdheVqOIlO3fMqqo8I$4$Xi?J#K%u zW%7#F@i?_0S&{BJ{%{_R77chm^D zH!1JH*p~1y@nc4J5mj*WgI(Qtm>@W&#AGo!me@|RbTw{axUDoTp5n-DK5sCdfzPMB zPe@Kdn9>!D%*(PIY4-RSzlsNd!3t?`nk9?nQA*g7PrL7KHRxk)!5WaddxffI#I41= zGof2?5E-92DkyCzKR@I|r11_NRL><0{G~Sd>wU{3Tq!hPdWy>zD-X^5j zV8p7s8X5OBXhXhgw}dIyo(iIhkb>Er+CXQRRrk87po`beljhq@tjRDF>nhi3-3i=8 z8r*E5(+&0jITk6^(z_j(ZSy*RjHE!X?H1fqSyfrVShYyVvi3b=S8-w&e#6-g=TfQG zEX#M9Q%wiE=D`_?!-rp_%9#~+j7aIw$G4gRT?BGh^Fb|14{(HN0c zvaBX#MSrhnH=aH@&ecCt#_iq3+XmOSV7AVrM4*N2DP3mzCE0)x_pTbV!90Y4U6o3s zEweBJ47JRJju*@5kBCA`ikvW+W*JN(b!c<)eQmda7fSKxptVHBu8i{jsS(w#_Lur3 zvQ{f4&ij2*;O9XYCC-gujBr&Q4(sb?>ck@!Dem0L)Q=HHA^CKKG4(Qyri6uu;eB03 z-*Ytc9bciOgHs*}IJq$O4WB}M)^objuWGP-~@j>a=D0(Re*m zbHnE5!TF2wF&gIa*T`DcY! zQBdku47%$Fl$*^=(n)VV(!apOt`uUG)IKc{sojmQAd=;fmwp>Le8kUQ^*a1@XwdbF zw1vrmJ=#XX^*Q#Q&`$8SFN~;ks1`%wTl}fFUTVd-s;Mp$==}G@Y9znUaL13bqz5}Q zg-6qiG8HF;s?yf9rQvQ*eA>-nMR(VI0k z%AI83Q+kZ$^emWxfkpS$HK=RT(X*d)1JlHf0=FZ0sdTfq03T$}9gf-;6tLTAhjC*ftehOuKl9c0$D(>DMK>HFntp*AO zWApO5fs6ZYl%?uohMEUl0YHztP=`mqTg>!K?NR{{rxmRCV z*ifl?XvG2a{hAQ8FOlG{ftUrimRLBG*(|XD+6B&8jy9H(Qi4SL@q_8$2MaCSj%8g8 z5NFGF!lVUJ;ZZ4*%xqG=WK;Z6clW)Nv8^!5-ss5TT}MUw zJTteJW{I)2mzzNWg4A2(rTiT1+Ol@cCRfq=5!bVn1l*jd0D3|x+J}V1J58KrOlN1R z2C{`4C_TOn_f`qW9h)VuSIUxK&SKMyP#G7{p(10W$ii~xr)kPcctcneO(=u0j2BYr zHx7)4>YTfwqRmoeUz zrx$VDk&0VQ*_~Q^n+a~{XOJ`N_hBGcPhz}#d?EBT4^F8cb4yUo>`jI<=;67B-v9k0 z*Sk4Vm4ImB+MXbL>t1j+Bk_Q~r4F&M=7&O4?82k0u~J^jw6d@i_ao;&D?z^=B~!wC zCHVILu-xBkz~A|owJo05XP^6w$IL;vd<*Rrii#Y0KqnbJx@G(znT6F!^T+r6t3UW| zeYEq@hOt#oXHUlm;q9kzE;@wmUGADhHPh~puDnEEVT)yla=+5w$AF` zv1VX`^Pt*MMK0U4UdX;Pya^Rami+$cUL12F@q%zJnP)(vKW zUlKOFVE&2nFv}W!=F}Cveqe$G?r%D}_MsSR@m`b-MNvT0u41sTE6Ifv(2-hXLhj?J zO;9WQ)r%#r3V`z|=bqn!2NbA5`0c(%n1c={*5ci; zJ)~#@P_q{n{+(hZ{J=M0*YR7BR_c^Zjm<^uVIc)ueZ#pLFp@^e-ytuREc;2Yr$|dS zS6GTu`@F-$$}xQ@Z8y52FnyQ%0(L^Lk~^r{-^jl*1c${)(%&gl69#&bazw9yJ0oka zu82~r%n(>Xi&+yi7hkpruhXbrPPr`TE3Iv@dtAx9XQ>PN#RUr!_LX|{k8dMMP%z+O z)45<(q8F#idA;13JQ{-z7_yq16$31&viO{yBZJ4baGP(A=pPkw?>0m^A;YaMyvbwKyzg9LmP~+UKgD|%9)QoQ#h4~N+xeDGlYyme>a>U zUwQ6K4h{%0tUb5;%)Jw9#&kH8@=V$f{!vUAP9c?SxMEhyx=TG@vIDRnjIWo(#Oz4j z|3&T4wKaR=_Dvy#;utDbKD|wXgyYTVd#7er>MMrhesZ&TX7ej*6Ww3#y6d-|z?vPV zhF@#r}M`FskO6+YF)*^oS4>2va19pRXpPh$( zPrYgX|H%Dsa2Ke0s-S70zpcv|8cC7&TPk@G*93}c{=DzWqY|T=`G85aI18RQz-9P; z!P3SV@e(~XYr7?DJuvB%^16(?aNLqkh0~3)IAVpLQxX&?+^C$yGWzCKI&y=(|*Dgsl;=y)Z!323OfwzQM;@v}SXs>1h{ z>L9ugyvN+J7v)93pM}`75^C0DIFp&hHhGynmgv|qv}giB^8Q;}`B7FuDD-_mK)^!|V6 z+(V$)HuR%{)=LHFpXuA9Ed;>v={ zB+2MMX7+tO9|TN}a_7TGZ%#2#m@_5_7T8k{nM_!wRPyA5>;rzKC(PHYDo-)(ITola z5A=v{@n8>9iKH`H#o=m?c5LGSI?#5c2k{mJqM~Klo3P_NRFr1|ipD1albR-x8laLl zKUdHi9F;}vXFD;q!0W!V$;HPxF&wd6q!qj1oT7Qr zncV62UiKo$@)a9@bu3niS$w#-L}MK6B@X*UW{CM7{ZZ~E$MX7p%??cOLgae}?L0c$ zNNt2kgRXiVg27rAdCjx~8bv{{zlW#_CF7?e^YTcbKl}ql?-R?a+`soAHq{8ihKQ%bpTbF)snAC;SS2U>iyLr#ro zQ24hg*5RV%l`#6SdTwrTB@VRFyVh2%)CJaJX3wXfdLU6`VK99JoEPYz8!mQ zF(ANdvKb|i(L>I|IJepnWh>>=xs8#%r4m5>^aMad_YLT@7RH{vy(V~BEf(`cj2tnA z^ER=HS;}POlkj~#a2!{>T{f%9>;06#5ff33ZA9HG8W{DTvY$j9 zAqf7xo=Clm1(biQC-15Jf4lU5YksH|Ac`hvDfSDoG=Mp}`V<7<*YUC7+tUm8 z535JNoFZ%*W+6wC_&a9&@RnLPu2Byia7!sFUS@;z6UeltZE1hTV^xWfPhA;@4 z?1x0L;6=A`&z&0?FnN(}L?kclMyTb>no!xkq&zv75XpV0K&|rgJCENVXI>NF8BWm_ zXKlEh@hicRo{=puO<;O`R2su47`LiBOR_O%^pT&v_weOm&Yy6ZpqyJLi)ZwaI)RV6 z2>BT)J$IMkV4gm$sJK>sA9Ih9D2y?2Hm8HVVfi#$5$?a~K-#nuTA&dhNTq{>?~#oa zR>>$m;UN~dn&E{F9p*RDK|t6#dO;VJHOP}i{`Nr*jlH3pdn3O1)hCXXq`Kr=%6^IGNr$~G;)vAgj{Vu4A^>aQ;YhTRGvMoLE6I3K7E6ZRfuFC zI_h@w+1Ba{sl=csVE`D8i}Oq%YMB=i0{3F}EL?de|uHq+Tf7@_`qc zGo=tf1 zk8gu)X*lJ>(s<6?J7QcvZC?+~mi-pDR5PObGshNy1FBYXXUrsIXIB zkJA{%g@=M8b>gENX!iX=O3axbllU^U5q6*S_=ehFMl3P?jUNISBZ29Tg`{shoG84F zAmswk*s1n5;vr6^`PHnv(k~wio-JgD_m{>*03<`NKq|YxTu;Z3hA3WlTT!9KCEw-& zl8W7Y4ITsf&jsEG?XVFX&dKxfW6zepba0J;?H31dGK@6^3Kj|?0bzh;aVxW?#Zuc< zWqIe4z4sEdXZO*TWjwk7X-AnT0s@8EOT%YCv^-&>_OMof8WJavtXO^ z%~os~X)qE0oGN+^{m)akV%|xsOadcYNl?X1HEFNj2ApK={Ja423SYu~2CNLS_~>ws zdGUXZ;xP)H4|L={1Zl2=*G0%fe<6yoDkHn0FDvOTsr&pSwLF_oh+B!}ACTkwa1liq z)Y(i*e-H#&$1lbQ5$IfMmZVu=7Au|wU!O@ke?&bTn=f7jthGBz*U?Kiyr4eF?w@EE zvr6my5ia)Z@C74%Q)WDQF$vSR=XXQ))gokSk*1INOUBaFY^r$d9}cCWo0bxojnM7$ z%D^)8cQ=WZjgxBHH2u}AO_-Oq@+w%vVCH*+9< z>y`Zg{^}5)c!8BZ*}!O0FVt#b|WB!fmkV=pV(;qNnl4hIIC&*ij3{uwRpu4UMeDmMB zamuEV2?~jx^LY4O>$K^{LkK{g7yOV{))4Q9tjaRrTDU4M&aP! ziTR`>MLl-B8XmS{zmkPfsXyTg7#@v3^`h2Ndx5NC8(Lw<1d?qG9l&?Om9wzp7>v3W zOZTd%{D6QqeRCObIVXWiYtEIPO}ZH8t(1(mUlgFgav`t*uZ(|Nz*V$3BO*h|A1%Wf zn2!tPj*<+}{YoUFQ_@@;BPtkx5@I)L)f`*WRO&8#%~I)@;T4Z`lxi( zj}UZmK*_yd1L%>{SkN}x9DM_lecMJ8UQ}f_y>UoQ&TBbHHZ+k71R%dPp$`6Ma9|fpUyL9TALM7^ z?8vVTYGbTxPehKw-purRILgC8YutDu6D(?p{s#SVelA{OTnRfCTA0LDS26Lx2@48y zPiE)(_ja(|q5spT)dCHmKe4A*3`-O;fW87pyGtB=d)))H&cLX*x1M>t0VC0uu+auu zf!SR7A5wSOA;MD1$J;j@mJ+fECZ2Gacge2_&I29EDtA3W+ph2@kFiEa1mAdk3Jd0+ zxEf`q8z$T_P8JB+KhPo;(fvWxZw!s6cE@buY++pBH8lmBb4p5}uNMNyYBs)SE&C?p zRM_?A9;j`2k*Sv+wf1J$am(THdt&O2-9yl5$t3t|C#xj~qto17wSuqcuO(@+KfO-` zBn_cOG0F4hW>)_+$a2!AM}%_rNRnQa(hb+|Bg;WKr;8xWJ0)+;mY`S6sSBVo{Hal4 zMMiRMW*%@7H!>r8Ni2Xr0l?oTMa)ahA@vfjzk=Y@-4N@a69Bs2KP5 zAT*K>V5t&2U^OZI6mT^7`NsguG)(JT>eN)VnG*k#!vs z9mpY6*_i_u%*MZK5<}*WT7;j`Z2_vYC9rfJ5Dy{-U)*zOuQxb@95Y>i;EB=gd?!SB z43vH_cnC7O)})>DOBbEoor8dXj4%>M2Fm_!=LapvlnSXKx5xKHBIBToG*TzdyJEAj z#}8rXIx~giF09`A-&%IYBkh(HuE-Fp$@xy(#Ub6KoZ1|*? zw?$ZQ)#Ky)2XL1mrpJ&%hZCM+6n=tI+muP8H(lG~vr}B0K`*=vnoJ<|Z{}&K1rPtL zv9AD%YumO>f`mZh?g{Sh?(XjH?k<7g!8JGpx5hmX++9L&cXxvR&VBbB&f%SV|Jy}% z7gfwrtJ%HQo@=f#=1>%Yuq4V{Vr}mxYF#aEkQzBgww)Bn5*p(0u2#Cv;2$f_`!$>z zL9cqL+Hl1{Kbq}h=S^Y{i}Dz7dH*k_{xyHK8()po3{0q&xF0GP6L%Y{*_wQ;J>=oW zx8PoRV;(n;8O4lG^##GsbE5r z#wS3<=PozPV~Fv**egCd6=wqM}47{DXOr-8J593NLH3JNabc)2$6^11l1Mp)ytsMT59npmLlDJ};Lx>*&$L4h%sxEZRi{aT+9-ND5UiPYyt@EM+8P;Q7_e(E zQEe&iv|IZxemfMRCh3cBn&fL(V+OK?Vh^i?yP!{sYT?tbS&MSfRd9c`s0Pel@smvR zh+#g63}CSA%r5gZSPfN>8}_KlJe*3F$Bd;g7khYQG-up7PQrjWNT>kQ<2i7 zM3-a1m4~*_{-F&ld<7_SroFJX*R{wb)Xtoa1U=}XMjKH@f=x!9>SJ#ydKu^m&NC8tZug6n>nMI{BQmMv|0aoRdBX%P{oUq+tk3uCdT}uU zUT&7%{p?sAvv(b4tQW6gnIA0o5MZJRF2Rp*p76q?z;?IkSl~~m%hisdE>|=;ddcEW zL-6t$oNaPElbWv+Y0%~2PqpKP0n4JU5WHjFNjQtdmY+34Q0>b@1Q0U~cSD-Wi)b$@ zzd@j7g}l`ad3(daN6yE?xAvl1u$7eI^9r=4Xo1R%|F>g{KhGr7mF5*dS^@5ABr< z#IT5Vmjk^GH6#|88>C)69I6d9BSg5^1O$g7eP%SmD}h6x(ndtmz1U-0LNP{cF-k(O zml6ec3G)Yf<+}5VNk;C1JEv=><4oU&Th5R3HyNvQbrmjw+xwIYPSUKlO(Tx8M;>3> zA8>hjlQNZc--NHO3aqz|uW|!57tHr9HF5PJ&p(%!0A?)rt)uFxvd^`PXp{Y%(%pj& ze6!zmfetUAW%Z&ze7Fjjg$4J6Ep0lni82ayXZPM zaak*Jb6h>@Xwv!I;OZ}mk==}lrFs2u@+@p%97!2G2e@rM^TlZK=7EzkIdA6TLb{`E zE4bn30PJ%#N{HxtqR)ioT$*Ke_H?JY^xa;gPl~N033}9Ol&dBVT8>&o5YaFa^|$ty z0sB@uc`W=_t#-P%+Up+0>XHJ%YzS0)(qrl8CpKsp*;YiSqw95L|ri!{y?bRe(oEkj(GgF;!WNKfKd2e9?tl};93(H6Mw^sgD$3qQr_3vzt@QClA{v!aP+ z?hs_@P}0v`G!nz`T?U!ZTHe`XA}N<6qc_i2B+bX_f@$7n|0Y%`xp*C_+{%6#-3bTC zmcMzc^o1whzj&~&6AU_mqyET+Taha<%#mx&>Izd9K|jcXkxy3e$uPXcplExPM*elM z*m<}Na=@4vTta?i^dp3+P5|zBu5&a|bVtZ-=m)e8&JaOnX>$rzhmYrqji-WMS^5;N zn+U4nJ$PETr>_Ddw02*y!qb=V|C$u=Wv{{&dS_||E@}@ZywH`Diw_G%rcVQqJ+xVe zv-cIg17{|!s+rBQ{Wwrnj==O<{)VvFb6RKDuVEQH zR@+RCPF~3ENBUBsv(I8b2pfweMCu`1-!X=+&daf!;P%Ugn)!X>{wY)%)=hz@B3^&7 zcOjNOV4=PQelFj>F5TtrwY44^#mN)v`(m#UgR{wz3fe0WfEJSfkIdlToQ8-$4=|w6 z^ua6XeFDHsF+M<`agGcU{1mD*MD!ESDwMFtDuXm1vrw7EEQ-bcPW;rS{LURmp%}-Z zFG>Pb5wfOwKdx`uK(FEM*=pLm5?<=qo>VlfJzN-Z8M%|MIHOob|Cv+KanJ5s7hF)$`Y< zLVl;)7a4+a)Fl%cP$Z;9acG|>&`E7}XdP_Qj0+wpFU2s#EC6CH;X0>eD2WJ#z10y< z4k^tokNc9}b>DP?&0UR)cbZSf(zgcjE;HspgR~Z7Ls`r56wRct6_a;GqrRaCeH~K4 z`xbdCCMI}+b&m4N1BZ|{m+mvRVf=w~kdpM5HAz{3YqjKq4xDvu#)LfJ6WCNa6yA+O`!h9Y%liJ zNWzmPw^Aj5sHdaUUW12yfX*MSVowIiu0{k!>wp zV{p$q=#&{Ka4E=WW$7=aqhMMR*JTKr-x6IXi^Qp>Pe(%fqAJ2pN2?fon`e8uC+uMt z0cv8Du!3b&LCSlBM(Pxmi#94kHNBLt#YtRXudy}1O|KUWg4j#mc;bWC0m-&=1AX~q zMUhjnLgg;b>zB8a(7&mQm(#LHc@h$#WEq0|2OG$nZ?p3cxs~(kja7f3u%1peH0yJo z1*VG!VYhs_D?l6t@1U^Wqf^e%*>ai#05;YD6ka%? z-Nnhq7SkW#u@mdsV{R(D74Y%cQ`H)M2+;Ih^>~~|{${vKv*p|A2`5uw^gwh%#WU-5 zZ~`4exMA~V66Kmgf+&^UIT+{6Wh%r2e-OnjZifgKZ$ijRc$sc`M!-tGwLJu6wGw10_Q+UA1UqnfVnfHdRCxC-I*vS!JUOy}2+4Q4uXx&B0o(Nf%ZS-k zmIAwJ*nI^$)!1QJs$~nc3tph4U__uyZFY;>=1z;{uy)vbb#G!#U!E8$)rD~bR`=(( z^;t>a)f;sK4D0|B1XDI|A}&Aupa3_L1t1#u0R9zY^@yD%pSoh#z>oMqQ9fQXmSOX% z;)BUSzo~^d0;%`i6<_cHS2vJ-zL4TZ3Ke!?`b6l^s&3qf3|e8OHZU!iNJ_I>1ALBg ziB%`GN`#6a#VKh`pK;)JN=5W!N8H9w#w$x7V zat~6Zg`m{O2oTqL|83`85o)K9&wAmA(FqStUW`cOdkHgB%A2KK&FnsK+qk34`?zG} zneXxK##;3G=K$8IvX-+URNP6w>GxKSDc{tCZuW8{0<+!jF(6CjsPNZv5K-(BK1iCw zEif+dw10%fVpUG-{3d>h1yNGZRXiIwkgA&S!-~r^i~XV41Q48f2JR@csB@BAfR1V4 zjyxs3AfXQ>9AwnEH~erh_ME^3;;d09Liak6hu6pGzY(%)KUM?E5-<$C*K8x#L!H!V@rBkY~XzW6B%x zrD>ju=14bTt7lI&on%+nx2?1L29PifJOWc-8W@hAwZhsFv~OWO;zcNxGA-&`$UCr> zFT`WZz-d6dg&oXR+9B=(iNji9^((H;m>s>)B%KzR3l4?aI{%(_;!p--aagsh;Rz(p zCIW3#kcu+jDlb~v;N6)s>^7d_O?+nhknEDi#M6iz{3HM}7C9#Bi z01=!$`aAt`b_UI#g14&QF1aieB3e70-e88OCc#%iJrX7}o+y0ae8e6>iS;!o#2b`h zzjV7QRm2}?-Jc4sK4!lZy82rE8J-Gdt@%skNy7=_fwq@bJ6i?8TlKE~ej*dHI(Mm@ zkZL!fvrupJ3s>Vc=Ba`P{`OMj^sLt%7>H53tU}In^iU}iK0NCt9YfOxF!33pd*TZ` z4Lr!nkQ2Ub#oycJXm335nF4IThLrN?AtgMjvoaF`!${>f=&gjd1-&GTxL7x-3vD{m@X^+C|FW%aFg33f>2 zzS^3Y#)sX6JX`#?k76!qSsc~`{&GC1E`_BUIMM+V*TwZ$m?G#<4G*Iv5B684%1W+} z=qFYOJV47Omp}zkf`bTZOqH*C-lSG<1w00jGPZ+bnyXTLe&RIQBxGNZP%Sxi{6FY#4}6-sqO&O6q3Q0l&pdG&!XK(i3%NC;?z1ijYc7wLQ<>7_=JHg1T-S zR+N#&OX)E#O)PEe)jjzR>JEEG>&HTx?`o05BB3DM&)UiOv3oCKrrwqz+lq3~(P42c z%^H|x;?bslYm?64y3gcrl;;I2cl!A7`2Rog%^I&Yp-gpxj%^PxeiP;G7S#9_^wdGKBhw0*S^glqCo znAnT#08F3{=Y;^f-QJQIg?M%Aox@|1Ec16k?Q~nQQFFMr)}%it@7khmZ|dx>h2PiW zP%=`_UJ7Br3Yc>o8DZ*x^^H4T-;Zp>D$O=+v-3;aRttWeJqz|bqo#*ZSS8-yQ&y75t3DH_z7dKtt|AM!p({G8t1ysD&0#O5_P#BmpER~hZ?oukiYhCsN{sNyZy=V-?Ui3ML+Pheg z783bl@DP3HFygqB!F|1P?CbM{+-BVX$Z=k^{wy~a)qu>#Klkn)w*Ch|IC+pdIYUA= z8PpM^YIC=P#MXX(-Dg;**a}*7cqjt})xW}IMnLB>V7Ok}b41=|Wt8rs1&$ffl+Bl7 zL5TGexl6#Ll0RQ^oPCc9<`E8~iU38@RH{5?-NdY`g$Ck0f1wf03973%VoC*u2}59g zb$%Z?QFN8w3XY&G2TG)!CV^1{R^tLnF%pu}+`=_jw=@3)4P^1pi*L!`9+)(?5Ce|h zp4HP8+Pa}*+ZpNC%3>-<P~9xQsTNCT_Jc-dT_wM_;XC9~%j|IcY6f|FzvxM>B@e z4)EP&%_{vuUwF%-Uq>O2wpS>cELB5>*$h`)2SQMs*cySLRlzFp2~TIjjY{Xw&j!{b^4D9$WFbjS+!O05U+6k1p!E80HLWcll`ZVXn{Pv9MaI# zFIJAJab+krwT-s{?<1{u+i;F$oO~x9JN0Qa=_55hT>A zMlp)&cFrO4i4WB~EKj8K@t(YWxZ_VQ$7g=0TxX^asPuSA1^}C^9!A3<)bnhnY=lfvSP(L83y3E;e zAu*|HBe?O9NU4p4EB&ca6)Xsns!|ocSX2Tj?~KPgI;}Sk#py8p8kaUY-WOX!JYgYa z6g6Q;FTvkrN9Z_KzX92?7WY)`y_5pCb{F0Qt)3cs;di-Z9Mna@So|Fp38+t38KHaBh~N%x$w5-yCw$bt6IIwk`kQhLXigc zLnOIk)Drr{i5?;TzSDwXCC^V3qy(NVR_4V1Wa>H^y>6X`7_2A%UUkP5O9oj}z! zWITRWL7{_p)ie-(BJ+pgW%RkSxQSgetS%ZhI8DfYBXletDKp7x6Ig;M^tf2J7#=_1n=IOJTx^CImr zcf!&0zeT3*|eVf{{X+((K%0WlwYow+!!uMb1$pO0!{`-K& zq{dkwk3W;~)U9jBxfX3$D!^Wcrk<((Js_|;y!C=Z_NSZP>{sR-7~JkJk=T85O9j%Y zcRmq=hhB1O0PFrfZgXpU#aj`cQf|CGckFr(GE7GReVF5;9CGJyT#SlFUC765wGHig zP)S4|2brP=T|g3f=t(mmg0! zDI{m3DP&i86E-5OO`4(dGY7V*;oSL6Q0SAk7@}8KG`FjhGR!`Pb>3xKBn_9|4sFn( zA-|Z;RxC1N+1yT+$?()zQ4pxQ{RXrk(Uc7!Pr;_%(YG{~TfUQ?fn-i0Nx4g^wND)H zsSUo(CA(j z7#thAg*#j5W%n-tbH}z886e1YYH%vm$`b?X2P9dw8{NA0gIlo>$G8DyPRmlA%OpfXHm6Ww`L!gG5_!o_Vw2s#-4DAYWkFEct~gG{ONOA&hp_lfJBS|t(-g04gg z#@oh_&O~DDG^j8^dZ0153pMJDy=GHsMt6rr(h-nMCMvo{+9TIBm8E4OKlt2oE}4yq z5jX#Pt*_GLsAlLONkr;UTkCbp@$uyMr}JyyS8Tz}Z(2T2=)`1XHX3{Z?&eF)G+Ha~ zXHa23R%H(4RTZT{ez4OoO{1m3ED`BpNwe76>xgCgG=BP4UU;U&q&P#q(zr*hk^$L7 zuW_W1Z~LJXO9LG4co3K^%@=1mQTjGkYWMBo#ppY(7D={fAHA4wD3G!5;-4_fZ3fGJ zZc)v!#Gma8S&TvSOm@SUeY`>r`H)TM59fCkPbzI*CK1IBTT9cg*Um`QJBksx4w;>m zne6;&w#hoG$4+h~)R?t`nzXt`V^B@zJ$G*fBLwLQnQ?M!kWG*cjf7((xy$fLcnTms zrqx@a3Pw>uqBuJ)*iNmNkd0uBg;BZH>(T(w3+|)~saRJkPTD~Gy;%sq+t9x2l-@56 zew?(ZG>8=3AaGW}-N3~mOa={K!ivccFU_pio{N0ULX-J359;Lcr1-;=>Ufw0Hf*k) zLg1M_q-Oq5?w|suvU@_XsWMSqIvShCLK507>sX`@0GdlDp*r9_TQeaAZqVji=^f(xaArL7w)5JRQ-^D41#0y(j(arXyU{8(dE#rZiNu)3WWK z_v22!AN=&gyV$F?)w}5jEB);!CLxq_3u0zsz(^0qA|u6T>Wm%ZdD>GdhL+M}z1n!c z?kq!a<;2S#*nc8|jf=1SX(zH$wHtX5L(O!mL>DM&uhmrm-h`iPNoMy29oAO`2%*n4 zN7hj89QOG$*6~P}*1XEw0TJ#TL`i(?vQb zo!KifF+yU~JMkU22U`6aM(;K_+<}_K)HcjigW^?{rbe9##$JXMnau5-Fh=ndjAk-a zSwOOyoM!NqXNsb&O;X>oGL3J_l?@{$rOP5Zh9vqm<=&C}vI0+26zk^-C#6av1l99a zlR4)rM#tpVOjYODh?EpW(f40TPVDS6!>fmH6;uYq(u*-E3m#|Gw!y(+%B#IC$So6m z!O1*0h5M@pWuzcJCoX@i!+=|2<=iL^AjCTwr3Mn-kp{Lj<>|h!EaOHomq)n%*=dz4 zziCER#uD1LBkD%d;R9dbq%BIRlNDu{znHcHG^j@ZsV>qD1*j6WD3Na+5%AWSB9&MW z2mxbkk}B-*j+xVv-AUc|$YuO08Y%2{xS-|BtQaY@v7!ubu4?}TM*Jg+kCCX7X#Pg#1sDnz93M|3Qo{@_}E zu9l%7t7Ioig-fXEksp>Y6O<)A+dn8Ef*OZ>%sUo^XP6GyJF8I^gz|xoyqe7Fb}H1% z1QvJ9=|A1G*mokxmr0Nk$k@USCs-Qx-r=P=NX=g+e}<$?r&3pTA=5V9j$ddp9gs|{ zaxZ+X6%TW{H>L6Z7qA(p+-NePF^I7FU>)E%Ie>I3iXKa$UJT1Y`lZdqz(;xqoSlGc z{w*XcqA&K_C1}od*893sa3`lCU6K$Ams6xsu&!42pNp4pk#H`gtS4^ZpL~UnNors( zgzY}Hjm@($#zl$t2A@$M6Sz8>fyusG==@32b6z$cQ)e^?$wOY@H~7Y#TD)J~Z^lk# z5eQ@TH7$x*Gp^FhlLrxZJnj&?A$yGV9JgPn$`CBGAiMeafpWGf)O`cF-;IHPG27S? z`3#rJwk?8B7`fZ_m-L4-Jd@}#gmHu!JDC&RPPQmYBILJ`31Ob#xZ&KxYKf_~9$;VQdA4qLzT9jbz|{Zf}|=S0!`%qiFYS(8IDSh$Ri&*+ni z%S6-KiiKHXouqUF0qjEOXWPIvdN0FNFD50}u4UnkE$;h!+Z*9eMXeC0%UTj&;;6~`*b&av8Sfi6{qTcY2*$1GV-m~_OKUZRw<`a# zVy3DM|IH1LkKO0*Gbe)hyl1E1qJF-VC%szh<`)E&_F6!8D|mmY7l<1f>p!ROf0PLl zl(hdxUf^P7ddT7;F;B!NsafwF7watX%R)*cE`O?zUKv@~pDV)kt!eC~R1Kr$3ibCu zU%3ypP$2noUzEUtdMbjMdtc{Lvgh`~D0{W<{jWjWS4TOWboHcbf-zsRkl*RR*WRkY zO$be?hUo#|HRs`*?!3A_eJSBTQTEjhaB)6#R#YYZq!)cchW45_o}aTup+q@o&o^6m z1Cyo|QAfxxo2(UG-vYu9t9-k$y3_7D{#-u|E|0c=&CM{QXd3co7ySl@ENd#| z3SKNo1@#y*8S;4PKFOf`5OoW>1?1KCu0u@O8#%S4jVo*{UgKE@en|Bg+tsg8Fb!RF zmI2n4Y?OoK>&9_{_&ee6e`4>Brep*0D6kTwD zUWFB9U!!#-I2MYudKiL)bm7ye>5${)DZc3yZB@PWvK`5>zPvGOx2zH_-{rpaeX3kZ zNHcYc%3fHLD8UcnV2+Rfsw5MY6%rK!N$RL)mfpj-{O5k>(d+77QPJ~a`D4#$n~%*iCe}UPW*hQS`EE$gauT*88z1>GC)93E3ggY z;B|m9SkCQ8F^uHY@CwwVWw=JdBJEA89^?C^>Zbd>V^8+R3Xsxa7|YY%^O4=E;pmhe zZ$(_i8cx?}A1hf5iXIVVZQxN?NJjZ?T3?rG5XL79?wtVrIpt9V8|I* ziI>w)C4z+GcPG!^`RRXS<}&`F6`Xef)!4bKW6LZntU}f$k&p;Ns0_=HNwug}g~;F# z`@%wT6yq~RO32L*7u9D*!~&P^sqgv2C0D3jPGF(@NIujzH$=-c|c)Z=EC>2aI)gMh~KG4M5j>N1F&llI|*50Z>7%o|ji9`&ClWNDn=mTf(O zd$T25cakX0N|FARz--uiqRtPAuWx)KS(#5nmrPO?^mrz)fgFA>O(OsAlB=sSJ-` zto;|fB|A*1sMm9`WA0z6PP*Yw0#us>X5Ns44A_Fa^j8I1_37+532In7KOSk|$5(%8 zw}J%m>`KXcbq||3Zmm*{%9m-_{bc#?CU)fo58U6@Wmj&1XOkfkgzf|CA8DoDuwoiW z>`>W!NN5jk7^ai<$_(*40c^#)p}7Wyp8?>bFv&rE%GVWTN}jrRk%R2d9vLpG$N(+W zbW|5LOR7ozRH4E_4M&Y(`mLuU?PRGVbcI}}Mr1mzEg`zy2|%5kDwHASrOpSYo{Em> zQdUjPX%p-8I@Dh5Y!d7on>mCO*M_Z)j`tHOz+NEUr#$rqtD6Ypu8?pDxcE5|~IiuQ3@=X~5i9-g*o3_e*X`oxS7w(3uP^ zH;{x=RH&S}hRrBTsm>)tF5GNe1h}ZKDJYt&%@T~`8Mn(byLzaDs&|_w98@9%)zpRs zXLbk;ABA--X3(A4?GhyO<7SI8A_=+KmCAI+*nf%VTcu-Bm%3|_^Y3*vIep_BXM)Xk z%Au~OY&;4^Az6gJ%_hufBH211+q$A#O9t_Ojd}_-E))!Y7@67-spg>*2`9o%*dTX& zWmeYW64+Axc1_WgOVV$z!9ON`zVaTeeG^JYBQ50~b_Q)(FN4B<9hRqxZ=g4Kfmy4^EwClnkRU8bbp%Y}h{L%$pPM%>cg*g#=*!o-^U!@g!TOFq-X9j!p6 zk6bmbVX2VbHx%%ZDQNx4Um8ifuU3{5K2Uj;F#aRltp+R~N@tN@Yt=FkYcW9JO!x04 zv;%2tqhBDuGcwQv_^-Wue?Nf#eX#b2XP;)%p+;7g-fM^r0#<#Pwb~E@c}41Js1Nj9 z()EUzO^d|Mni=kA!8aR}DIw!sO#gB_QlU^doV?kH;9~|&os1z$UnYL5yKgYeZtp7guaj^SAA%O!_leN4QsXg zh0{}6k;CrDF$N*q(EHewfzzgdhhI;^CUHAGo8V1$fgYCnp~n)A-3o$xJH}rc zU-@xN0O%QhSP^`G0{kkdSfzTvpc@Oe1dQux-U4tbm3-JZO+KAxPGaV{_i3OrkElQ5-up zkeQswj zsugl?i}yi4BR@2V>Z)jUr$tmlDgtPhKW#}!?SGfMNv4uhZ<)_|3Y*TDIj-0{UEbis zCjtwe774`+?*CD-^h=ga5m8Zuv=|9xeIpUsR?3_Pj>Xnxr7IH3jyv)FCu zac=Zo`~+DuWP>ibQweVJ3;VbYxpkt@khR~hDLWGwdwfBmk|;!c>+GyMjH}8M^uJG( zg7d*bJkTWJ0Xb}feqKqL8~oc35Y*?^aZ&~0k!TrZm8c4e$UX}kib>p(ofXTXW6%>9D%3FWt?T)`IX1Vs;pn8rJh>m@-)qiTly& z#8oh0gET5sylR_tlY|;2IDAEZrtDjL#tfXA_T1GC*5J;v)i~b^*(2xqFFz}@76#Qx ztFTW(4RG^p&Nlj#Z{G!gJ($D@ctCMg#aoTlc){&LOQyV!WW0mzJa%Lg=U>1o92D4V z>&GGad0LWf{ML9ozku0lJp(r-9xJ#ALob@;b0u7tJ)9w6?RGtD`(`J5=*Y|(`ck0Y zh_CpQIDQn^?sd%mTKW2>LHPmogAu)-c;WaZu(vkBE)l|TyvN7V zpYOqG&n6x-`e3a1-2+_OkWO?Q!}n+n zu~;dD>+3V|57fqD!~g}@n7v%0Em4l*pDTTS{3=-a)r6l$A|;ZOc=L$Su8?{a5cV!; zu!;*lcg-eKna~aV)Z8uZfNRF|YFo`cszaG{mY$b$s@EsWKjz3YQ&XUUZs=OQ_<+nv z6s=&YrK5lr-`!DX3Ponm);n~6EHKR2QMY!)^eL-FYi}s9?MjkzfY?~R4eNtKBZYKX zo~IH-p6X|RYxxHcS&7dBRj-=bc5fb?X6lM8g*LzWRJ2NaZycBvr_V_jiA^yML#r`|P~%5WoFL6P;3~?Tema3Ibc3v#O6U7h*Z~vGN$o|{3h{U9 zKm!ea#!^ZXC4GoqH=J7uud1;~Xqh%JIP$e{;X7hwx1l)^u|;BnT}lr;zd?QXy39=e z@a|XFOoN0?c&nC(@Tahsz}MIO2{^wB6+tS$;iJ{`S(<<)ooHdOAShfT4v%3oY?)3w zr@@ni^pBm<5>n>;JD5{@N5L|Bwc%COmcLXMS%5ef&TCfb8I+c0iH1s@DvKv9OjX$# zI`+=POH*8*4-K;mo3abAS*hxh!~IzA^eV-=V{=WS1u|+~=hSP~W7S9`{WI+*ag~x99x6$~;13~UdciKxmhz?c4`Hji1mK)?5RqSn4eN4xp+E$1 zhg=wX(eJO_L_JT>Y$IH)5SH9%mx}6pQaeI@zA3i2Ioft{&Ia~YkAjzB>d^N{4)gZT z7I1$&`_QHYAX{13?|h2J&cAzfJ*nMRYvq0dpfmWc7*K!kE&LICR;(e}k6%<<5e)tsYNj zQNg%qSl~U>x_qB{(=|>@{kjv3B9lyZQj=jxieOeTN@AdXV%|3h3s=e~Al9H{d{1Y+*el4qoaK;r>ADtCCrL4u&stD<%tmxVcRf+egJy{x1$YYIrt+mu1%A@MenH|m@7HZDx)`Tigus%l#| zr#U<)3C-F(-r&<@kD>Xd-O^lGR*hfE$0;qFJ(*G6)C?Lg(m_uzIKX2cP`j$vvt7Ng zov_-rodF~9Xt(0cyIS4Z4ykK63WLY{^ko~hRkPS=>qg`1@R#_!afs_9d8VTR_E5iv ze(_fw%H7))GBkO9cmJ==fKBwXmMQL{W{H$(!9hCZoS4$FI>N6?3`58HbH#+}Gra?a zbg`P~k~s-eYj`mOuzALe+|b4oXkGRNA~gp;u8(ou9L!01 zfp$tUf*JQ6Q0{JgJhI5r(dA9-WUXoI7vt?v)JV_^68+mMBXrIj9zJX&V8mPmn)lN0 zl}L{DZ*5NrDi`OcR8L$@8_q2@8IRHb6G4f;-jE`(Qy{J-D-v?qNT z_R{nE_aFbha{YdH)ciB-FAms$MffM9>wkrVUTXYYK(BxQM!$@GX)^pA%L1Ag|6n=% z65yo`>ob5dXea;Y@c-GA^(EL#3)W|_d=S`wF=TxS^g9qVxBqp4PUQc6A-sCvZ$bEr z>h~{q@SiNa{)nsb+x%(6-R@1ODhS;+Zs`ol}emwln1A>lzJ?LR)^ zU%N!VM0nYc_8DOoL;?Fd!rwop<8Pw(AI065urHfiKEtkq6lQA}kz^-`Ji3|uGm4EzsbhW{|}Udkb!@nYouE8d^th?gUL$&-FY98h{j{2OcfCB#eq z<}-w|+Fv35E1UBr=u4W`Gw8b3UqSy1Y3n8AOH#!%WQhJ>A^&Ndzi0EmDHbnbUmiz2 z!{(Vi!~XN()Jv?Fwe@E#BCBVtms926mG+llFUxe#U?g_WV9!Onm+>#F7tissj(@FX lyo`I9uRq5nxczhXE++{A${K!K0DgFt4KjA$@c8}P{{vKV_-p_G literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/buildSrc/build/pluginUnderTestMetadata/plugin-under-test-metadata.properties b/lib/shared/test-helpers/buildSrc/build/pluginUnderTestMetadata/plugin-under-test-metadata.properties new file mode 100644 index 0000000..47058b9 --- /dev/null +++ b/lib/shared/test-helpers/buildSrc/build/pluginUnderTestMetadata/plugin-under-test-metadata.properties @@ -0,0 +1 @@ +implementation-classpath=/home/rlamb/tmpfs/code/launchdarkly/java-core/lib/shared/test-helpers/buildSrc/build/classes/java/main\:/home/rlamb/tmpfs/code/launchdarkly/java-core/lib/shared/test-helpers/buildSrc/build/classes/groovy/main\:/home/rlamb/tmpfs/code/launchdarkly/java-core/lib/shared/test-helpers/buildSrc/build/classes/kotlin/main\:/home/rlamb/tmpfs/code/launchdarkly/java-core/lib/shared/test-helpers/buildSrc/build/resources/main diff --git a/lib/shared/test-helpers/buildSrc/build/reports/plugin-development/validation-report.txt b/lib/shared/test-helpers/buildSrc/build/reports/plugin-development/validation-report.txt new file mode 100644 index 0000000..e69de29 diff --git a/lib/shared/test-helpers/buildSrc/build/tmp/jar/MANIFEST.MF b/lib/shared/test-helpers/buildSrc/build/tmp/jar/MANIFEST.MF new file mode 100644 index 0000000..58630c0 --- /dev/null +++ b/lib/shared/test-helpers/buildSrc/build/tmp/jar/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 + diff --git a/lib/shared/test-helpers/buildSrc/src/main/kotlin/Dependencies.kt b/lib/shared/test-helpers/buildSrc/src/main/kotlin/Dependencies.kt new file mode 100644 index 0000000..d94e2e3 --- /dev/null +++ b/lib/shared/test-helpers/buildSrc/src/main/kotlin/Dependencies.kt @@ -0,0 +1,44 @@ + +// Centralize dependencies here instead of writing them out in the top-level +// build script(s). + +object Versions { + const val gson = "2.7" + const val guava = "32.0.1-jre" + const val okhttpTls = "4.8.1" + const val hamcrest = "1.3" + const val okhttp = "4.5.0" + const val junit = "4.12" +} + +object PluginVersions { + const val nexusPublish = "1.3.0" +} + +object Libs { + val implementation = listOf( + "com.google.code.gson:gson:${Versions.gson}", + "com.google.guava:guava:${Versions.guava}", + "com.squareup.okhttp3:okhttp-tls:${Versions.okhttpTls}", + "org.hamcrest:hamcrest-library:${Versions.hamcrest}" + ) + + val javaTestImplementation = listOf( + "com.squareup.okhttp3:okhttp:${Versions.okhttp}", + "junit:junit:${Versions.junit}" + ) + + val javaBuiltInGradlePlugins = listOf( + "java", + "java-library", + "checkstyle", + "signing", + "maven-publish", + "idea", + "jacoco" + ) + + val javaExtGradlePlugins = mapOf( + "io.github.gradle-nexus.publish-plugin" to PluginVersions.nexusPublish + ) +} diff --git a/lib/shared/test-helpers/buildSrc/src/main/kotlin/Idea.kt b/lib/shared/test-helpers/buildSrc/src/main/kotlin/Idea.kt new file mode 100644 index 0000000..c2f85d5 --- /dev/null +++ b/lib/shared/test-helpers/buildSrc/src/main/kotlin/Idea.kt @@ -0,0 +1,16 @@ +package helpers + +import org.gradle.api.tasks.TaskProvider +import org.gradle.plugins.ide.idea.model.IdeaModel + +// Idea.configure provides reusable configuration logic for the Idea +// behavior we normally use. + +object Idea { + fun configure(ideaModel: IdeaModel) { + ideaModel.module { + isDownloadJavadoc = true + isDownloadSources = true + } + } +} diff --git a/lib/shared/test-helpers/buildSrc/src/main/kotlin/Jacoco.kt b/lib/shared/test-helpers/buildSrc/src/main/kotlin/Jacoco.kt new file mode 100644 index 0000000..aa8267a --- /dev/null +++ b/lib/shared/test-helpers/buildSrc/src/main/kotlin/Jacoco.kt @@ -0,0 +1,52 @@ +package helpers + +import org.gradle.api.tasks.TaskProvider +import org.gradle.testing.jacoco.tasks.JacocoReport +import org.gradle.testing.jacoco.tasks.JacocoCoverageVerification + +// Jacoco.configureTasks provides reusable configuration logic for using the Jacoco +// test coverage plugin in a Java project. See also: TestCoverageOverrides.kt + +object Jacoco { + fun configureTasks(reportTask: TaskProvider, + verificationTask: TaskProvider) { + reportTask.configure { + reports { + xml.required.set(false) + csv.required.set(false) + html.required.set(true) + } + } + + verificationTask.configure { + // See notes in CONTRIBUTING.md on code coverage. Unfortunately we can't configure line-by-line code + // coverage overrides within the source code itself, because Jacoco operates on bytecode. + violationRules { + TestCoverageOverrides.methodsWithMissedLineCount.forEach { signature, maxMissedLines -> + rule { + element = "METHOD" + includes = listOf(signature) + limit { + counter = "LINE" + value = "MISSEDCOUNT" + maximum = maxMissedLines.toBigDecimal() + } + } + } + + // General rule that we should expect 100% test coverage; exclude any methods that + // have overrides in TestCoverageOverrides. + rule { + element = "METHOD" + limit { + counter = "LINE" + value = "MISSEDCOUNT" + maximum = 0.toBigDecimal() + } + excludes = TestCoverageOverrides.methodsWithMissedLineCount.map { it.key } + + TestCoverageOverrides.methodsToSkip + } + } + } + } +} diff --git a/lib/shared/test-helpers/buildSrc/src/main/kotlin/Javadoc.kt b/lib/shared/test-helpers/buildSrc/src/main/kotlin/Javadoc.kt new file mode 100644 index 0000000..2549ebc --- /dev/null +++ b/lib/shared/test-helpers/buildSrc/src/main/kotlin/Javadoc.kt @@ -0,0 +1,21 @@ +package helpers + +import org.gradle.api.artifacts.Configuration +import org.gradle.api.tasks.TaskProvider +import org.gradle.api.tasks.javadoc.Javadoc +import org.gradle.external.javadoc.CoreJavadocOptions + +object Javadoc { + fun configureTask(javadocTask: TaskProvider, classpathConfig: Configuration?) { + javadocTask.configure { + // Force the Javadoc build to fail if there are any Javadoc warnings. See: https://discuss.gradle.org/t/javadoc-fail-on-warning/18141/3 + // See JDK-8200363 (https://bugs.openjdk.java.net/browse/JDK-8200363) + // for information about the -Xwerror option. + (options as CoreJavadocOptions).addStringOption("Xwerror") + + if (classpathConfig != null) { + classpath += classpathConfig + } + } + } +} diff --git a/lib/shared/test-helpers/buildSrc/src/main/kotlin/Pom.kt b/lib/shared/test-helpers/buildSrc/src/main/kotlin/Pom.kt new file mode 100644 index 0000000..ac9906e --- /dev/null +++ b/lib/shared/test-helpers/buildSrc/src/main/kotlin/Pom.kt @@ -0,0 +1,31 @@ +package helpers + +import org.gradle.api.publish.maven.MavenPom + +// Pom.standardPom provides reusable logic for setting the pom.xml properties +// of LaunchDarkly packages. It gets its values from ProjectValues.kt. + +object Pom { + fun standardPom(pom: MavenPom) { + pom.name.set(ProjectValues.artifactId) + pom.description.set(ProjectValues.description) + pom.url.set("https://github.com/${ProjectValues.githubRepo}") + pom.licenses { + license { + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + name.set("The Apache License, Version 2.0") + } + } + pom.developers { + developer { + name.set(ProjectValues.pomDeveloperName) + email.set(ProjectValues.pomDeveloperEmail) + } + } + pom.scm { + connection.set("scm:git:git://github.com/${ProjectValues.githubRepo}.git") + developerConnection.set("scm:git:ssh:git@github.com:${ProjectValues.githubRepo}.git") + url.set("https://github.com/${ProjectValues.githubRepo}") + } + } +} diff --git a/lib/shared/test-helpers/buildSrc/src/main/kotlin/ProjectValues.kt b/lib/shared/test-helpers/buildSrc/src/main/kotlin/ProjectValues.kt new file mode 100644 index 0000000..a6fae71 --- /dev/null +++ b/lib/shared/test-helpers/buildSrc/src/main/kotlin/ProjectValues.kt @@ -0,0 +1,15 @@ + +// This file defines basic properties of the project that are used in the +// build script and the helper code. + +object ProjectValues { + const val groupId = "com.launchdarkly" + const val artifactId = "test-helpers" + const val description = "LaunchDarkly Java test helpers" + const val githubRepo = "launchdarkly/java-core" + + const val sdkBasePackage = "com.launchdarkly.testhelpers" + + const val pomDeveloperName = "LaunchDarkly SDK Team" + const val pomDeveloperEmail = "sdks@launchdarkly.com" +} diff --git a/lib/shared/test-helpers/buildSrc/src/main/kotlin/Test.kt b/lib/shared/test-helpers/buildSrc/src/main/kotlin/Test.kt new file mode 100644 index 0000000..c213db5 --- /dev/null +++ b/lib/shared/test-helpers/buildSrc/src/main/kotlin/Test.kt @@ -0,0 +1,34 @@ +package helpers + +import org.gradle.api.artifacts.Configuration +import org.gradle.api.tasks.TaskProvider +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.api.tasks.testing.Test +import org.gradle.api.tasks.testing.logging.TestExceptionFormat + +// Test.configureTask provides reusable configuration logic for the Java test +// behavior we normally use. + +object Test { + fun configureTask(compileTestTask: TaskProvider, testTask: TaskProvider, + classpathConfig: Configuration?) { + + compileTestTask.configure { + if (classpathConfig != null) { + classpath += classpathConfig + } + } + + testTask.configure { + testLogging { + events("passed", "skipped", "failed", "standardOut", "standardError") + showStandardStreams = true + exceptionFormat = TestExceptionFormat.FULL + } + + if (classpathConfig != null) { + classpath += classpathConfig + } + } + } +} diff --git a/lib/shared/test-helpers/buildSrc/src/main/kotlin/TestCoverageOverrides.kt b/lib/shared/test-helpers/buildSrc/src/main/kotlin/TestCoverageOverrides.kt new file mode 100644 index 0000000..697eef6 --- /dev/null +++ b/lib/shared/test-helpers/buildSrc/src/main/kotlin/TestCoverageOverrides.kt @@ -0,0 +1,30 @@ + +// See notes in CONTRIBUTING.md on code coverage. Unfortunately we can't configure +// line-by-line code coverage overrides within the source code itself, because Jacoco +// operates on bytecode. + +// These values are used by helpers/Jacoco.kt. + +object TestCoverageOverrides { + val prefixForAllMethodSignatures = ProjectValues.sdkBasePackage + "." + + // Each entry in methodsWithMissedLineCount is an override to tell the Jacoco plugin + // that we're aware of a gap in our test coverage and are OK with it. In each entry, + // the key is the method signature and the value is the number of lines that we + // expect Jacoco to report as missed. + val methodsWithMissedLineCount = mapOf( + "EvaluationReason.error(com.launchdarkly.sdk.EvaluationReason.ErrorKind)" to 1, + "EvaluationReasonTypeAdapter.parse(com.google.gson.stream.JsonReader)" to 1, + "LDContext.urlEncodeKey(java.lang.String)" to 2, + "LDValue.equals(java.lang.Object)" to 1, + "LDValueTypeAdapter.read(com.google.gson.stream.JsonReader)" to 1, + "json.LDGson.LDTypeAdapter.write(com.google.gson.stream.JsonWriter, java.lang.Object)" to 1, + "json.LDJackson.GsonReaderToJacksonParserAdapter.peekInternal()" to 3 + ).mapKeys { prefixForAllMethodSignatures + it.key } + + // Each entry in methodsToSkip is an override to tell the Jacoco plugin to ignore + // code coverage in the method with the specified signature. + val methodsToSkip = listOf( + "json.JsonSerialization.getDeserializableClasses()" + ).map { prefixForAllMethodSignatures + it } +} diff --git a/lib/shared/test-helpers/checkstyle.xml b/lib/shared/test-helpers/checkstyle.xml new file mode 100644 index 0000000..0101956 --- /dev/null +++ b/lib/shared/test-helpers/checkstyle.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/shared/test-helpers/gradle.properties b/lib/shared/test-helpers/gradle.properties new file mode 100644 index 0000000..b4db4c7 --- /dev/null +++ b/lib/shared/test-helpers/gradle.properties @@ -0,0 +1,3 @@ +# x-release-please-start-version +version=2.0.2 +# x-release-please-end diff --git a/lib/shared/test-helpers/gradle/wrapper/gradle-wrapper.jar b/lib/shared/test-helpers/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7454180f2ae8848c63b8b4dea2cb829da983f2fa GIT binary patch literal 59536 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 QuEJmfE|#`Dqx|px0L@K;Y5)KL literal 0 HcmV?d00001 diff --git a/lib/shared/test-helpers/gradle/wrapper/gradle-wrapper.properties b/lib/shared/test-helpers/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..070cb70 --- /dev/null +++ b/lib/shared/test-helpers/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/lib/shared/test-helpers/gradlew b/lib/shared/test-helpers/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/lib/shared/test-helpers/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/lib/shared/test-helpers/gradlew.bat b/lib/shared/test-helpers/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/lib/shared/test-helpers/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lib/shared/test-helpers/settings.gradle.kts b/lib/shared/test-helpers/settings.gradle.kts new file mode 100644 index 0000000..ac84ee4 --- /dev/null +++ b/lib/shared/test-helpers/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "test-helpers" diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/Assertions.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/Assertions.java new file mode 100644 index 0000000..f7c31fd --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/Assertions.java @@ -0,0 +1,51 @@ +package com.launchdarkly.testhelpers; + +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import static com.launchdarkly.testhelpers.InternalHelpers.timeUnit; + +/** + * General test assertions that may be helpful in unit tests. + * + * For more specific categories of assertions, see {@link ConcurrentHelpers}, + * {@link JsonAssertions}, and {@link TypeBehavior}. + * + * @since 1.1.0 + */ +public abstract class Assertions { + /** + * Repeatedly calls a function until it returns a non-null value or until a timeout elapses, + * whichever comes first. + * + * @param the return type + * @param timeout maximum time to wait + * @param timeoutUnit time unit for timeout (null defaults to milliseconds) + * @param interval how often to call the function + * @param intervalUnit time unit for interval (null defaults to milliseconds) + * @param fn the function to call + * @return the function's return value + * @throws AssertionError if the function did not return a non-null value before the timeout + */ + public static T assertPolledFunctionReturnsValue( + long timeout, + TimeUnit timeoutUnit, + long interval, + TimeUnit intervalUnit, + Supplier fn + ) { + long deadline = System.currentTimeMillis() + timeUnit(timeoutUnit).toMillis(timeout); + while (System.currentTimeMillis() < deadline) { + T result = fn.get(); + if (result != null) { + return result; + } + try { + Thread.sleep(timeUnit(intervalUnit).toMillis(interval)); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + throw new AssertionError("timed out after " + timeout); + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/ConcurrentHelpers.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/ConcurrentHelpers.java new file mode 100644 index 0000000..c5ee3b1 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/ConcurrentHelpers.java @@ -0,0 +1,185 @@ +package com.launchdarkly.testhelpers; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeDiagnosingMatcher; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static com.launchdarkly.testhelpers.InternalHelpers.timeDesc; +import static com.launchdarkly.testhelpers.InternalHelpers.timeUnit; + +/** + * Helper methods and test assertions related to concurrent data structures. + * + * @since 1.1.0 + */ +public abstract class ConcurrentHelpers { + /** + * Asserts that a future is completed within the specified timeout. + * + * @param the future's value type + * @param future the future + * @param timeout the maximum time to wait + * @param timeoutUnit the time unit for the timeout (null defaults to milliseconds) + * @return the completed value + * @throws AssertionError if the timeout expires + */ + public static T assertFutureIsCompleted(Future future, long timeout, TimeUnit timeoutUnit) { + try { + return future.get(timeout, timeUnit(timeoutUnit)); + } catch (TimeoutException e) { + throw new AssertionError("Future was not completed within " + timeDesc(timeout, timeoutUnit)); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + /** + * Equivalent to {@link #assertFutureIsCompleted(Future, long, TimeUnit)}, but as a Hamcrest matcher. + * + * @param the future's value type + * @param timeout the maximum time to wait + * @param timeoutUnit the time unit for the timeout (null defaults to milliseconds) + * @return a matcher + */ + public static Matcher> isCompletedWithin(long timeout, TimeUnit timeoutUnit) { + return new TypeSafeDiagnosingMatcher>() { + @Override + public void describeTo(Description description) { + description.appendText("Future is completed within " + timeDesc(timeout, timeoutUnit)); + } + + @Override + protected boolean matchesSafely(Future item, Description mismatchDescription) { + try { + item.get(timeout, timeUnit(timeoutUnit)); + return true; + } catch (TimeoutException e) { + mismatchDescription.appendText("timed out"); + return false; + } catch (ExecutionException e) { + throw new RuntimeException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }; + } + + /** + * Asserts that a future is completed within the specified timeout. + * + * @param the future's value type + * @param future the future + * @param timeout the maximum time to wait + * @param timeoutUnit the time unit for the timeout (null defaults to milliseconds) + * @throws AssertionError if the future is completed + */ + public static void assertFutureIsNotCompleted(Future future, long timeout, TimeUnit timeoutUnit) { + try { + T value = future.get(timeout, timeUnit(timeoutUnit)); + throw new AssertionError("Future was unexpectedly completed with value: " + value); + } catch (TimeoutException e) { + return; + } catch (ExecutionException e) { + throw new RuntimeException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + /** + * Equivalent to {@link #assertFutureIsNotCompleted(Future, long, TimeUnit)}, but as a Hamcrest matcher. + * + * @param the future's value type + * @param timeout the maximum time to wait + * @param timeoutUnit the time unit for the timeout (null defaults to milliseconds) + * @return a matcher + */ + public static Matcher> isNotCompletedWithin(long timeout, TimeUnit timeoutUnit) { + return new TypeSafeDiagnosingMatcher>() { + @Override + public void describeTo(Description description) { + description.appendText("Future is not completed within " + timeDesc(timeout, timeoutUnit)); + } + + @Override + protected boolean matchesSafely(Future item, Description mismatchDescription) { + try { + T value = item.get(timeout, timeUnit(timeoutUnit)); + mismatchDescription.appendText("unexpectedly completed with value: " + value); + return false; + } catch (TimeoutException e) { + return true; + } catch (ExecutionException e) { + throw new RuntimeException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }; + } + + /** + * Waits for a value to be available from a {@code BlockingQueue} and consumes the value. + * + * @param the value type + * @param values the queue + * @param timeout the maximum time to wait + * @param timeoutUnit the time unit for the timeout (null defaults to milliseconds) + * @return the value obtained from the queue + * @throws AssertionError if the timeout expires + */ + public static T awaitValue(BlockingQueue values, long timeout, TimeUnit timeoutUnit) { + try { + T value = values.poll(timeout, timeUnit(timeoutUnit)); + if (value == null) { + throw new AssertionError("did not receive a value within " + timeDesc(timeout, timeoutUnit)); + } + return value; + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + /** + * Asserts that no values are available fro a queue within the specified timeout. + * + * @param the value type + * @param values the queue + * @param timeout the maximum time to wait + * @param timeoutUnit the time unit for the timeout (null defaults to milliseconds) + * @throws AssertionError if a value was available from the queue + */ + public static void assertNoMoreValues(BlockingQueue values, long timeout, TimeUnit timeoutUnit) { + try { + T value = values.poll(timeout, timeUnit(timeoutUnit)); + if (value != null) { + throw new AssertionError("expected no more values, but received: " + value); + } + } catch (InterruptedException e) {} + } + + /** + * Shortcut for calling {@code Thread.sleep()} when an {@code InterruptedException} is not + * expected, so you do not have to catch it. + * + * @param delay the length of time to wait + * @param delayUnit the time unit for the delay (null defaults to milliseconds) + * @throws RuntimeException if an {@code InterruptedException} unexpectedly happened + */ + public static void trySleep(long delay, TimeUnit delayUnit) { + try { + Thread.sleep(timeUnit(delayUnit).toMillis(delay)); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/InternalHelpers.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/InternalHelpers.java new file mode 100644 index 0000000..8ed3ca7 --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/InternalHelpers.java @@ -0,0 +1,14 @@ +package com.launchdarkly.testhelpers; + +import java.util.concurrent.TimeUnit; + +abstract class InternalHelpers { + static TimeUnit timeUnit(TimeUnit unit) { + return unit == null ? TimeUnit.MILLISECONDS : unit; + } + + static String timeDesc(long value, TimeUnit unit) { + String unitName = timeUnit(unit).name().toLowerCase(); + return String.format("%d %s", value, value == 1 ? unitName.substring(0, unitName.length() - 1) : unitName); + } +} diff --git a/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/JsonAssertions.java b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/JsonAssertions.java new file mode 100644 index 0000000..06b803b --- /dev/null +++ b/lib/shared/test-helpers/src/main/java/com/launchdarkly/testhelpers/JsonAssertions.java @@ -0,0 +1,482 @@ +package com.launchdarkly.testhelpers; + +import com.google.common.base.Joiner; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeDiagnosingMatcher; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.launchdarkly.testhelpers.JsonTestValue.jsonFromValue; +import static com.launchdarkly.testhelpers.JsonTestValue.jsonOf; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Test assertions and matchers related to JSON. + *