From 1fbaf190043a2a3a08a8da1a27cee77d7e663336 Mon Sep 17 00:00:00 2001 From: Tim Romel Date: Mon, 19 May 2025 15:27:16 -0400 Subject: [PATCH 1/6] feat: convention plugins and kotlin 2.x update --- android/build.gradle.kts | 25 +- android/src/main/AndroidManifest.xml | 2 - build.gradle.kts | 64 ++-- buildSrc/build.gradle.kts | 27 -- buildSrc/src/main/java/Plugins.kt | 35 -- buildSrc/src/main/java/SdkVersions.kt | 32 -- .../java/android-library-module.gradle.kts | 58 ---- .../main/java/java-library-module.gradle.kts | 10 - .../src/main/java/release-module.gradle.kts | 9 - context-aware/build.gradle.kts | 21 +- context-aware/src/main/AndroidManifest.xml | 2 - firebase/build.gradle.kts | 38 ++- firebase/src/main/AndroidManifest.xml | 2 - gradle.properties | 7 + gradle/libs.versions.toml | 101 ++++-- gradle/wrapper/gradle-wrapper.jar | Bin 54329 -> 43764 bytes gradle/wrapper/gradle-wrapper.properties | 5 +- gradlew | 305 +++++++++++------- gradlew.bat | 80 +++-- sample/build.gradle.kts | 49 +-- .../kroger/telemetry/sample/MainActivity.kt | 2 + settings.gradle.kts | 28 +- telemetry/build.gradle.kts | 19 +- 23 files changed, 429 insertions(+), 492 deletions(-) delete mode 100644 android/src/main/AndroidManifest.xml delete mode 100644 buildSrc/build.gradle.kts delete mode 100644 buildSrc/src/main/java/Plugins.kt delete mode 100644 buildSrc/src/main/java/SdkVersions.kt delete mode 100644 buildSrc/src/main/java/android-library-module.gradle.kts delete mode 100644 buildSrc/src/main/java/java-library-module.gradle.kts delete mode 100644 buildSrc/src/main/java/release-module.gradle.kts delete mode 100644 context-aware/src/main/AndroidManifest.xml delete mode 100644 firebase/src/main/AndroidManifest.xml diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 52253d5..222077c 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -1,15 +1,26 @@ +import com.kroger.gradle.config.junit5 + plugins { - id(Plugins.androidLibrary.id) - id(Plugins.release.id) + alias(libs.plugins.conventions.publishedAndroidLibrary) +} + +android { + namespace = "com.kroger.telemetry.android" +} + +kover { + currentProject { + createVariant("default") { + add("debug") + } + } } dependencies { api(project(":telemetry")) - implementation(libs.coroutinesAndroid) - implementation(libs.stdLib) + implementation(libs.kotlinx.coroutinesAndroid) - testImplementation(libs.coroutinesTest) - testImplementation(libs.jupiterApi) - testRuntimeOnly(libs.jupiterEngine) + junit5() + testImplementation(libs.kotlinx.coroutinesTest) } diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml deleted file mode 100644 index 915c254..0000000 --- a/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/build.gradle.kts b/build.gradle.kts index 5814f36..301e053 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,48 +1,32 @@ -import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask - -// Top-level build file where you can add configuration options common to all sub-projects/modules. -buildscript { - repositories { - mavenCentral() - google() - } -} - -allprojects { - group = "com.kroger.telemetry" - version = "0.0.1" - - repositories { - mavenCentral() - google() - } -} - plugins { - id("com.github.ben-manes.versions") version "0.36.0" - id("org.jlleitschuh.gradle.ktlint") version "11.1.0" + alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.junit5) apply false + alias(libs.plugins.compose) apply false + alias(libs.plugins.conventions.androidApplication) apply false + alias(libs.plugins.conventions.publishedAndroidLibrary) apply false + alias(libs.plugins.conventions.publishedKotlinLibrary) apply false + alias(libs.plugins.conventions.root) + alias(libs.plugins.dependencyAnalysis) apply false + alias(libs.plugins.dokka) + alias(libs.plugins.gradleVersions) apply false + alias(libs.plugins.dagger.hilt) apply false + alias(libs.plugins.kotlinter) apply false + alias(libs.plugins.kotlinx.kover) apply true + alias(libs.plugins.ksp) apply false + alias(libs.plugins.mavenPublish) apply false } -subprojects { - apply(plugin = "org.jlleitschuh.gradle.ktlint") - - configure { - version.set("0.46.1") - android.set(true) - debug.set(true) - reporters { - reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE) - } - filter { - exclude("**/generated/**", "**/src/test/**") +kover { + currentProject { + createVariant("default") { + // no sources and tests in root module } } } -tasks.named("dependencyUpdates", DependencyUpdatesTask::class.java).configure { - // optional parameters - checkForGradleUpdate = true - outputFormatter = "json" - outputDir = "build/dependencyUpdates" - reportfileName = "report" +dependencies { + kover(project(":android")) + kover(project(":context-aware")) + kover(project(":firebase")) + kover(project(":telemetry")) } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts deleted file mode 100644 index e6b174b..0000000 --- a/buildSrc/build.gradle.kts +++ /dev/null @@ -1,27 +0,0 @@ -plugins { - `kotlin-dsl` -} - -buildscript { - repositories { - mavenCentral() - google() - } -} - -allprojects { - repositories { - mavenCentral() - google() - } -} - -dependencies { - implementation("com.android.tools.build:gradle:7.3.1") - implementation("com.squareup:javapoet:1.13.0") - implementation("de.mannodermaus.gradle.plugins:android-junit5:1.8.0.0") - implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31") - implementation("org.jetbrains.dokka:dokka-gradle-plugin:1.5.31") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3") - implementation("com.vanniktech:gradle-maven-publish-plugin:0.22.0") -} diff --git a/buildSrc/src/main/java/Plugins.kt b/buildSrc/src/main/java/Plugins.kt deleted file mode 100644 index ed5c9be..0000000 --- a/buildSrc/src/main/java/Plugins.kt +++ /dev/null @@ -1,35 +0,0 @@ -/** - * MIT License - * - * Copyright (c) 2021 The Kroger Co. All rights reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -object Plugins { - /** - * @param id used to reference the plugin inside of a `plugins` block - * @param coordinates used to add the plugin to the classpath - */ - data class Plugin(val id: String, val coordinates: String) - - val androidLibrary = Plugin("android-library-module", "N/A") - val javaLibrary = Plugin("java-library-module", "N/A") - val release = Plugin("release-module", "N/A") -} diff --git a/buildSrc/src/main/java/SdkVersions.kt b/buildSrc/src/main/java/SdkVersions.kt deleted file mode 100644 index 9dc34bb..0000000 --- a/buildSrc/src/main/java/SdkVersions.kt +++ /dev/null @@ -1,32 +0,0 @@ -/** - * MIT License - * - * Copyright (c) 2021 The Kroger Co. All rights reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -/** - * Project-specific Version numbers - */ -object SdkVersions { - const val targetSdkVersion = 32 - const val compileSdkVersion = 32 - const val minSdkVersion = 24 -} diff --git a/buildSrc/src/main/java/android-library-module.gradle.kts b/buildSrc/src/main/java/android-library-module.gradle.kts deleted file mode 100644 index 81989cb..0000000 --- a/buildSrc/src/main/java/android-library-module.gradle.kts +++ /dev/null @@ -1,58 +0,0 @@ -plugins { - id("com.android.library") - kotlin("android") - id("jacoco") - id("de.mannodermaus.android-junit5") - id("org.jetbrains.dokka") -} - -android { - compileSdk = SdkVersions.compileSdkVersion - - defaultConfig { - minSdk = (SdkVersions.minSdkVersion) - targetSdk = (SdkVersions.targetSdkVersion) - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - - kotlinOptions { - jvmTarget = "11" - } - - packagingOptions { - exclude("META-INF/AL2.0") - exclude("META-INF/LGPL2.1") - } -} - -jacoco { - toolVersion = "0.8.7" -} - -tasks { - withType { - kotlinOptions.jvmTarget = "11" - kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" - kotlinOptions.freeCompilerArgs += "-Xexplicit-api=strict" - } - - withType { - reports { - csv.isEnabled = false - html.isEnabled = false - } - } -} - -tasks.dokkaHtml { - dokkaSourceSets { - configureEach { - offlineMode.set(true) - } - } -} diff --git a/buildSrc/src/main/java/java-library-module.gradle.kts b/buildSrc/src/main/java/java-library-module.gradle.kts deleted file mode 100644 index 3584544..0000000 --- a/buildSrc/src/main/java/java-library-module.gradle.kts +++ /dev/null @@ -1,10 +0,0 @@ -plugins { - id("org.jetbrains.kotlin.jvm") - `java-library` -} - -tasks { - test { - useJUnitPlatform() - } -} diff --git a/buildSrc/src/main/java/release-module.gradle.kts b/buildSrc/src/main/java/release-module.gradle.kts deleted file mode 100644 index 27852ad..0000000 --- a/buildSrc/src/main/java/release-module.gradle.kts +++ /dev/null @@ -1,9 +0,0 @@ -plugins { - id("com.vanniktech.maven.publish") -} - -val libraryVersion = System.getenv("BUILD_VERSION") ?: "0.0.1" - -mavenPublishing { - version = libraryVersion -} diff --git a/context-aware/build.gradle.kts b/context-aware/build.gradle.kts index a3db912..1ab02f6 100644 --- a/context-aware/build.gradle.kts +++ b/context-aware/build.gradle.kts @@ -1,14 +1,25 @@ +import com.kroger.gradle.config.junit5 + plugins { - id(Plugins.androidLibrary.id) - id(Plugins.release.id) + alias(libs.plugins.conventions.publishedAndroidLibrary) +} + +android { + namespace = "com.kroger.telemetry.contextaware" +} + +kover { + currentProject { + createVariant("default") { + add("debug") + } + } } dependencies { implementation(project(":telemetry")) - implementation(libs.injectJavax) + junit5() testImplementation(libs.mockk) - testImplementation(libs.jupiterApi) - testRuntimeOnly(libs.jupiterEngine) } diff --git a/context-aware/src/main/AndroidManifest.xml b/context-aware/src/main/AndroidManifest.xml deleted file mode 100644 index 3bc14e7..0000000 --- a/context-aware/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/firebase/build.gradle.kts b/firebase/build.gradle.kts index c532450..1fdf7ad 100644 --- a/firebase/build.gradle.kts +++ b/firebase/build.gradle.kts @@ -1,31 +1,37 @@ +import com.kroger.gradle.config.junit5 + plugins { - id(Plugins.androidLibrary.id) - id(Plugins.release.id) + alias(libs.plugins.conventions.publishedAndroidLibrary) } android { - defaultConfig { - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + namespace = "com.kroger.telemetry.firebase" +} + +kover { + currentProject { + createVariant("default") { + add("debug") + } } } dependencies { implementation(project(":telemetry")) - implementation(libs.androidCoreKtx) - implementation(libs.coroutines) - implementation(libs.firebaseAnalytics) + implementation(libs.androidx.coreKtx) + implementation(libs.kotlinx.coroutinesCore) + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.analytics) implementation(libs.injectJavax) - implementation(libs.stdLib) + junit5() testImplementation(libs.mockk) - testImplementation(libs.coroutinesTest) - testImplementation(libs.jupiterApi) - testRuntimeOnly(libs.jupiterEngine) + testImplementation(libs.kotlinx.coroutinesTest) - androidTestImplementation(libs.androidxTestCore) - androidTestImplementation(libs.androidxTestRules) - androidTestImplementation(libs.androidxTestRunner) - androidTestImplementation(libs.junit) - androidTestImplementation(libs.junitTestKtx) + androidTestImplementation(libs.junit4) + androidTestImplementation(libs.androidx.test.core) + androidTestImplementation(libs.androidx.test.ext.junitKtx) + androidTestImplementation(libs.androidx.test.rules) + androidTestImplementation(libs.androidx.test.runner) } diff --git a/firebase/src/main/AndroidManifest.xml b/firebase/src/main/AndroidManifest.xml deleted file mode 100644 index 360f88f..0000000 --- a/firebase/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 029522e..bcd73ca 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,6 +18,13 @@ android.useAndroidX=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official +## Kroger Gradle Plugin Properties +kgp.android.autoconfigure.compose=false +kgp.android.autoconfigure.hilt.application=false + +# Ignore v1 deprecation warning since v2 is still experimental +org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true + ## Publishing Properties SONATYPE_HOST=DEFAULT RELEASE_SIGNING_ENABLED=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8505da2..e94d25f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,50 +1,83 @@ [versions] -androidTest = "1.4.0" -coroutines = "1.5.0" -junit4 = "4.13.1" -junit5 = "5.7.1" -kotlin = "1.5.31" -mockk = "1.12.0" -serialize = "1.2.2" +androidGradlePlugin = "8.10.0" +androidJunit5Plugin = "1.12.0.0" +androidMaterial = "1.12.0" +androidxAppCompat = "1.7.0" +androidxConstraintLayout = "2.2.1" +androidxCore = "1.16.0" +androidxTestCore = "1.6.1" +androidxTestEspresso = "3.6.1" +androidxTestExtJunit = "1.2.1" +androidxTestRules = "1.6.1" +androidxTestRunner = "1.6.2" +conventionPlugin = "2.0.0-alpha.3" +dependencyAnalysis = "2.17.0" +firebaseBom = "32.8.0" +gradleMavenPublishPlugin = "0.31.0" +gradleVersions = "0.51.0" +javaxInject = "1" +kgpCompileSdk = "35" +kgpDagger = "2.56.1" +kgpAndroidDesugarJdkLibs = "2.1.5" +kgpAndroidxComposeBom = "2025.05.00" +kgpDokka = "2.0.0" +kgpJdk = "24" +kgpJvmTarget = "17" +kgpJunit4 = "4.13.2" +kgpJunitBom = "5.12.2" +kgpMinSdk = "24" +kgpTargetSdk = "35" +kotlin = "2.1.20" +kotlinter = "5.0.2" +kotlinxCoroutines = "1.10.2" +kover = "0.9.1" +ksp = "2.1.20-1.0.32" +mockk = "1.14.2" [libraries] # android -androidCoreKtx = { module = "androidx.core:core-ktx", version = "1.8.0" } -annotation = { module = "androidx.annotation:annotation", version = "1.0.0" } -appCompat = { module = "androidx.appcompat:appcompat", version = "1.4.1" } -contraintLayount = { module = "androidx.constraintlayout:constraintlayout", version = "2.1.3" } -googleMaterial = { module = "com.google.android.material:material", version = "1.5.0" } - -# dagger (sample app only) -hilt = { module = "com.google.dagger:hilt-android", version = "2.41" } -hiltAndroidCompiler = { module = "com.google.dagger:hilt-android-compiler", version = "2.41" } -hiltCompiler = { module = "androidx.hilt:hilt-compiler", version = "1.0.0" } +android-material = { module = "com.google.android.material:material", version.ref = "androidMaterial" } +androidx-constrainlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidxConstraintLayout" } +androidx-coreKtx = { module = "androidx.core:core-ktx", version.ref = "androidxCore" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidxAppCompat" } # firebase -firebaseAnalytics = { module = "com.google.firebase:firebase-analytics", version = "19.0.0" } +firebase-analytics = { module = "com.google.firebase:firebase-analytics" } +firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } # kotlin -coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } -coroutinesAndroid = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } -coroutinesTest = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } +kotlinx-coroutinesCore = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } +kotlinx-coroutinesAndroid = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } +kotlinx-coroutinesTest = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } kotlinTest = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } -serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialize" } -stdLib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk7", version.ref = "kotlin" } # other -injectJavax = { module = "javax.inject:javax.inject", version = "1" } +injectJavax = { module = "javax.inject:javax.inject", version.ref = "javaxInject" } ## unit tests -jupiterApi = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit5" } -jupiterEngine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit5" } -androidJunitTestExt = { module = "androidx.test.ext:junit", version = "1.1.3" } +android-test-espressoCore = { module = "androidx.test.espresso:espresso-core", version.ref = "androidxTestEspresso" } +junit4 = { module = "junit:junit", version.ref = "kgpJunit4" } mockk = { module = "io.mockk:mockk", version.ref = "mockk" } -textEspresso = { module = "androidx.test.espresso:espresso-core", version = "3.4.0" } -truth = { module = "com.google.truth:truth", version = "1.1" } # instrumented tests -androidxTestCore = { module = "androidx.test:core", version = "1.4.1-alpha07" } -androidxTestRules = { module = "androidx.test:rules", version.ref = "androidTest" } -androidxTestRunner = { module = "androidx.test:runner", version.ref = "androidTest" } -junit = { module = "junit:junit", version.ref = "junit4" } -junitTestKtx = { module = "androidx.test.ext:junit-ktx", version = "1.1.2" } +androidx-test-core = { module = "androidx.test:core", version.ref = "androidxTestCore" } +androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidxTestRules" } +androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidxTestRunner" } +androidx-test-ext-junitKtx = { module = "androidx.test.ext:junit-ktx", version.ref = "androidxTestExtJunit" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } +android-junit5 = { id = "de.mannodermaus.android-junit5", version.ref = "androidJunit5Plugin" } +compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +conventions-androidApplication = { id = "com.kroger.gradle.android-application-conventions", version.ref = "conventionPlugin" } +conventions-publishedAndroidLibrary = { id = "com.kroger.gradle.published-android-library-conventions", version.ref = "conventionPlugin" } +conventions-publishedKotlinLibrary = { id = "com.kroger.gradle.published-kotlin-library-conventions", version.ref = "conventionPlugin" } +conventions-root = { id = "com.kroger.gradle.root", version.ref = "conventionPlugin" } +dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "kgpDagger" } +dependencyAnalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" } +dokka = { id = "org.jetbrains.dokka", version.ref = "kgpDokka" } +gradleVersions = { id = "com.github.ben-manes.versions", version.ref = "gradleVersions" } +kotlinter = { id = "org.jmailen.kotlinter", version.ref = "kotlinter" } +kotlinx-kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "gradleMavenPublishPlugin" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f6b961fd5a86aa5fbfe90f707c3138408be7c718..1b33c55baabb587c669f562ae36f953de2481846 100644 GIT binary patch literal 43764 zcma&OWmKeVvL#I6?i3D%6z=Zs?ofE*?rw#G$eqJB ziT4y8-Y@s9rkH0Tz>ll(^xkcTl)CY?rS&9VNd66Yc)g^6)JcWaY(5$5gt z8gr3SBXUTN;~cBgz&})qX%#!Fxom2Yau_`&8)+6aSN7YY+pS410rRUU*>J}qL0TnJ zRxt*7QeUqTh8j)Q&iavh<}L+$Jqz))<`IfKussVk%%Ah-Ti?Eo0hQH!rK%K=#EAw0 zwq@@~XNUXRnv8$;zv<6rCRJ6fPD^hfrh;0K?n z=p!u^3xOgWZ%f3+?+>H)9+w^$Tn1e;?UpVMJb!!;f)`6f&4|8mr+g)^@x>_rvnL0< zvD0Hu_N>$(Li7|Jgu0mRh&MV+<}`~Wi*+avM01E)Jtg=)-vViQKax!GeDc!xv$^mL z{#OVBA$U{(Zr8~Xm|cP@odkHC*1R8z6hcLY#N@3E-A8XEvpt066+3t9L_6Zg6j@9Q zj$$%~yO-OS6PUVrM2s)(T4#6=JpI_@Uz+!6=GdyVU?`!F=d;8#ZB@(5g7$A0(`eqY z8_i@3w$0*es5mrSjhW*qzrl!_LQWs4?VfLmo1Sd@Ztt53+etwzAT^8ow_*7Jp`Y|l z*UgSEwvxq+FYO!O*aLf-PinZYne7Ib6ny3u>MjQz=((r3NTEeU4=-i0LBq3H-VJH< z^>1RE3_JwrclUn9vb7HcGUaFRA0QHcnE;6)hnkp%lY1UII#WPAv?-;c?YH}LWB8Nl z{sx-@Z;QxWh9fX8SxLZk8;kMFlGD3Jc^QZVL4nO)1I$zQwvwM&_!kW+LMf&lApv#< zur|EyC|U@5OQuph$TC_ZU`{!vJp`13e9alaR0Dbn5ikLFH7>eIz4QbV|C=%7)F=qo z_>M&5N)d)7G(A%c>}UCrW!Ql_6_A{?R7&CL`;!KOb3 z8Z=$YkV-IF;c7zs{3-WDEFJzuakFbd*4LWd<_kBE8~BFcv}js_2OowRNzWCtCQ6&k z{&~Me92$m*@e0ANcWKuz)?YjB*VoSTx??-3Cc0l2U!X^;Bv@m87eKHukAljrD54R+ zE;@_w4NPe1>3`i5Qy*3^E9x#VB6?}v=~qIprrrd5|DFkg;v5ixo0IsBmik8=Y;zv2 z%Bcf%NE$a44bk^`i4VwDLTbX=q@j9;JWT9JncQ!+Y%2&HHk@1~*L8-{ZpY?(-a9J-1~<1ltr9i~D9`P{XTIFWA6IG8c4;6bFw*lzU-{+?b&%OcIoCiw00n>A1ra zFPE$y@>ebbZlf(sN_iWBzQKDV zmmaLX#zK!@ZdvCANfwV}9@2O&w)!5gSgQzHdk2Q`jG6KD7S+1R5&F)j6QTD^=hq&7 zHUW+r^da^%V(h(wonR(j?BOiC!;y=%nJvz?*aW&5E87qq;2z`EI(f zBJNNSMFF9U{sR-af5{IY&AtoGcoG)Iq-S^v{7+t0>7N(KRoPj;+2N5;9o_nxIGjJ@ z7bYQK)bX)vEhy~VL%N6g^NE@D5VtV+Q8U2%{ji_=6+i^G%xeskEhH>Sqr194PJ$fB zu1y^){?9Vkg(FY2h)3ZHrw0Z<@;(gd_dtF#6y_;Iwi{yX$?asr?0N0_B*CifEi7<6 zq`?OdQjCYbhVcg+7MSgIM|pJRu~`g?g3x?Tl+V}#$It`iD1j+!x+!;wS0+2e>#g?Z z*EA^k7W{jO1r^K~cD#5pamp+o@8&yw6;%b|uiT?{Wa=4+9<}aXWUuL#ZwN1a;lQod zW{pxWCYGXdEq9qAmvAB904}?97=re$>!I%wxPV#|f#@A*Y=qa%zHlDv^yWbR03%V0 zprLP+b(#fBqxI%FiF*-n8HtH6$8f(P6!H3V^ysgd8de-N(@|K!A< z^qP}jp(RaM9kQ(^K(U8O84?D)aU(g?1S8iWwe)gqpHCaFlJxb*ilr{KTnu4_@5{K- z)n=CCeCrPHO0WHz)dDtkbZfUfVBd?53}K>C5*-wC4hpDN8cGk3lu-ypq+EYpb_2H; z%vP4@&+c2p;thaTs$dc^1CDGlPG@A;yGR5@$UEqk6p58qpw#7lc<+W(WR;(vr(D>W z#(K$vE#uBkT=*q&uaZwzz=P5mjiee6>!lV?c}QIX%ZdkO1dHg>Fa#xcGT6~}1*2m9 zkc7l3ItD6Ie~o_aFjI$Ri=C!8uF4!Ky7iG9QTrxVbsQroi|r)SAon#*B*{}TB-?=@ z8~jJs;_R2iDd!$+n$%X6FO&PYS{YhDAS+U2o4su9x~1+U3z7YN5o0qUK&|g^klZ6X zj_vrM5SUTnz5`*}Hyts9ADwLu#x_L=nv$Z0`HqN`Zo=V>OQI)fh01n~*a%01%cx%0 z4LTFVjmW+ipVQv5rYcn3;d2o4qunWUY!p+?s~X~(ost@WR@r@EuDOSs8*MT4fiP>! zkfo^!PWJJ1MHgKS2D_hc?Bs?isSDO61>ebl$U*9*QY(b=i&rp3@3GV@z>KzcZOxip z^dzA~44;R~cnhWz7s$$v?_8y-k!DZys}Q?4IkSyR!)C0j$(Gm|t#e3|QAOFaV2}36 z?dPNY;@I=FaCwylc_;~kXlZsk$_eLkNb~TIl8QQ`mmH&$*zwwR8zHU*sId)rxHu*K z;yZWa8UmCwju%aSNLwD5fBl^b0Ux1%q8YR*uG`53Mi<`5uA^Dc6Ync)J3N7;zQ*75)hf%a@{$H+%S?SGT)ks60)?6j$ zspl|4Ad6@%-r1t*$tT(en!gIXTUDcsj?28ZEzz)dH)SV3bZ+pjMaW0oc~rOPZP@g! zb9E+ndeVO_Ib9c_>{)`01^`ZS198 z)(t=+{Azi11$eu%aU7jbwuQrO`vLOixuh~%4z@mKr_Oc;F%Uq01fA)^W&y+g16e?rkLhTxV!EqC%2}sx_1u7IBq|}Be&7WI z4I<;1-9tJsI&pQIhj>FPkQV9{(m!wYYV@i5h?A0#BN2wqlEwNDIq06|^2oYVa7<~h zI_OLan0Do*4R5P=a3H9`s5*>xU}_PSztg`+2mv)|3nIy=5#Z$%+@tZnr> zLcTI!Mxa`PY7%{;KW~!=;*t)R_sl<^b>eNO@w#fEt(tPMg_jpJpW$q_DoUlkY|uo> z0-1{ouA#;t%spf*7VjkK&$QrvwUERKt^Sdo)5@?qAP)>}Y!h4(JQ!7{wIdkA+|)bv z&8hBwoX4v|+fie}iTslaBX^i*TjwO}f{V)8*!dMmRPi%XAWc8<_IqK1jUsApk)+~R zNFTCD-h>M5Y{qTQ&0#j@I@tmXGj%rzhTW5%Bkh&sSc=$Fv;M@1y!zvYG5P2(2|(&W zlcbR1{--rJ&s!rB{G-sX5^PaM@3EqWVz_y9cwLR9xMig&9gq(voeI)W&{d6j1jh&< zARXi&APWE1FQWh7eoZjuP z;vdgX>zep^{{2%hem;e*gDJhK1Hj12nBLIJoL<=0+8SVEBx7!4Ea+hBY;A1gBwvY<)tj~T=H`^?3>zeWWm|LAwo*S4Z%bDVUe z6r)CH1H!(>OH#MXFJ2V(U(qxD{4Px2`8qfFLG+=a;B^~Te_Z!r3RO%Oc#ZAHKQxV5 zRYXxZ9T2A%NVJIu5Pu7!Mj>t%YDO$T@M=RR(~mi%sv(YXVl`yMLD;+WZ{vG9(@P#e zMo}ZiK^7^h6TV%cG+;jhJ0s>h&VERs=tuZz^Tlu~%d{ZHtq6hX$V9h)Bw|jVCMudd zwZ5l7In8NT)qEPGF$VSKg&fb0%R2RnUnqa){)V(X(s0U zkCdVZe6wy{+_WhZh3qLp245Y2RR$@g-!9PjJ&4~0cFSHMUn=>dapv)hy}|y91ZWTV zCh=z*!S3_?`$&-eZ6xIXUq8RGl9oK0BJw*TdU6A`LJqX9eS3X@F)g$jLkBWFscPhR zpCv8#KeAc^y>>Y$k^=r|K(DTC}T$0#jQBOwB#@`P6~*IuW_8JxCG}J4va{ zsZzt}tt+cv7=l&CEuVtjD6G2~_Meh%p4RGuY?hSt?(sreO_F}8r7Kp$qQdvCdZnDQ zxzc*qchE*E2=WK)^oRNa>Ttj`fpvF-JZ5tu5>X1xw)J@1!IqWjq)ESBG?J|ez`-Tc zi5a}GZx|w-h%5lNDE_3ho0hEXMoaofo#Z;$8|2;EDF&*L+e$u}K=u?pb;dv$SXeQM zD-~7P0i_`Wk$#YP$=hw3UVU+=^@Kuy$>6?~gIXx636jh{PHly_a2xNYe1l60`|y!7 z(u%;ILuW0DDJ)2%y`Zc~hOALnj1~txJtcdD#o4BCT68+8gZe`=^te6H_egxY#nZH&P*)hgYaoJ^qtmpeea`35Fw)cy!w@c#v6E29co8&D9CTCl%^GV|X;SpneSXzV~LXyRn-@K0Df z{tK-nDWA!q38M1~`xUIt_(MO^R(yNY#9@es9RQbY@Ia*xHhD&=k^T+ zJi@j2I|WcgW=PuAc>hs`(&CvgjL2a9Rx zCbZyUpi8NWUOi@S%t+Su4|r&UoU|ze9SVe7p@f1GBkrjkkq)T}X%Qo1g!SQ{O{P?m z-OfGyyWta+UCXH+-+(D^%kw#A1-U;?9129at7MeCCzC{DNgO zeSqsV>W^NIfTO~4({c}KUiuoH8A*J!Cb0*sp*w-Bg@YfBIPZFH!M}C=S=S7PLLcIG zs7K77g~W)~^|+mx9onzMm0qh(f~OsDTzVmRtz=aZTllgR zGUn~_5hw_k&rll<4G=G+`^Xlnw;jNYDJz@bE?|r866F2hA9v0-8=JO3g}IHB#b`hy zA42a0>{0L7CcabSD+F7?pGbS1KMvT{@1_@k!_+Ki|5~EMGt7T%u=79F)8xEiL5!EJ zzuxQ`NBliCoJMJdwu|);zRCD<5Sf?Y>U$trQ-;xj6!s5&w=9E7)%pZ+1Nh&8nCCwM zv5>Ket%I?cxr3vVva`YeR?dGxbG@pi{H#8@kFEf0Jq6~K4>kt26*bxv=P&jyE#e$| zDJB_~imk^-z|o!2njF2hL*|7sHCnzluhJjwLQGDmC)Y9 zr9ZN`s)uCd^XDvn)VirMgW~qfn1~SaN^7vcX#K1G`==UGaDVVx$0BQnubhX|{e z^i0}>k-;BP#Szk{cFjO{2x~LjK{^Upqd&<+03_iMLp0$!6_$@TbX>8U-f*-w-ew1?`CtD_0y_Lo|PfKi52p?`5$Jzx0E8`M0 zNIb?#!K$mM4X%`Ry_yhG5k@*+n4||2!~*+&pYLh~{`~o(W|o64^NrjP?-1Lgu?iK^ zTX6u3?#$?R?N!{599vg>G8RGHw)Hx&=|g4599y}mXNpM{EPKKXB&+m?==R3GsIq?G zL5fH={=zawB(sMlDBJ+{dgb)Vx3pu>L=mDV0{r1Qs{0Pn%TpopH{m(By4;{FBvi{I z$}x!Iw~MJOL~&)p93SDIfP3x%ROjg}X{Sme#hiJ&Yk&a;iR}V|n%PriZBY8SX2*;6 z4hdb^&h;Xz%)BDACY5AUsV!($lib4>11UmcgXKWpzRL8r2Srl*9Y(1uBQsY&hO&uv znDNff0tpHlLISam?o(lOp#CmFdH<6HmA0{UwfU#Y{8M+7od8b8|B|7ZYR9f<#+V|ZSaCQvI$~es~g(Pv{2&m_rKSB2QQ zMvT}$?Ll>V+!9Xh5^iy3?UG;dF-zh~RL#++roOCsW^cZ&({6q|?Jt6`?S8=16Y{oH zp50I7r1AC1(#{b`Aq5cw>ypNggHKM9vBx!W$eYIzD!4KbLsZGr2o8>g<@inmS3*>J zx8oG((8f!ei|M@JZB`p7+n<Q}?>h249<`7xJ?u}_n;Gq(&km#1ULN87CeTO~FY zS_Ty}0TgQhV zOh3T7{{x&LSYGQfKR1PDIkP!WnfC1$l+fs@Di+d4O=eVKeF~2fq#1<8hEvpwuqcaH z4A8u~r^gnY3u6}zj*RHjk{AHhrrDqaj?|6GaVJbV%o-nATw}ASFr!f`Oz|u_QPkR# z0mDudY1dZRlk@TyQ?%Eti=$_WNFtLpSx9=S^be{wXINp%MU?a`F66LNU<c;0&ngifmP9i;bj6&hdGMW^Kf8e6ZDXbQD&$QAAMo;OQ)G zW(qlHh;}!ZP)JKEjm$VZjTs@hk&4{?@+NADuYrr!R^cJzU{kGc1yB?;7mIyAWwhbeA_l_lw-iDVi7wcFurf5 z#Uw)A@a9fOf{D}AWE%<`s1L_AwpZ?F!Vac$LYkp<#A!!`XKaDC{A%)~K#5z6>Hv@V zBEqF(D5?@6r3Pwj$^krpPDCjB+UOszqUS;b2n>&iAFcw<*im2(b3|5u6SK!n9Sg4I z0KLcwA6{Mq?p%t>aW0W!PQ>iUeYvNjdKYqII!CE7SsS&Rj)eIw-K4jtI?II+0IdGq z2WT|L3RL?;GtGgt1LWfI4Ka`9dbZXc$TMJ~8#Juv@K^1RJN@yzdLS8$AJ(>g!U9`# zx}qr7JWlU+&m)VG*Se;rGisutS%!6yybi%B`bv|9rjS(xOUIvbNz5qtvC$_JYY+c& za*3*2$RUH8p%pSq>48xR)4qsp!Q7BEiJ*`^>^6INRbC@>+2q9?x(h0bpc>GaNFi$K zPH$6!#(~{8@0QZk=)QnM#I=bDx5vTvjm$f4K}%*s+((H2>tUTf==$wqyoI`oxI7>C z&>5fe)Yg)SmT)eA(|j@JYR1M%KixxC-Eceknf-;N=jJTwKvk#@|J^&5H0c+%KxHUI z6dQbwwVx3p?X<_VRVb2fStH?HH zFR@Mp=qX%#L3XL)+$PXKV|o|#DpHAoqvj6uQKe@M-mnhCSou7Dj4YuO6^*V`m)1lf z;)@e%1!Qg$10w8uEmz{ENb$^%u}B;J7sDd zump}onoD#!l=agcBR)iG!3AF0-63%@`K9G(CzKrm$VJ{v7^O9Ps7Zej|3m= zVXlR&yW6=Y%mD30G@|tf=yC7-#L!16Q=dq&@beWgaIL40k0n% z)QHrp2Jck#evLMM1RGt3WvQ936ZC9vEje0nFMfvmOHVI+&okB_K|l-;|4vW;qk>n~ z+|kk8#`K?x`q>`(f6A${wfw9Cx(^)~tX7<#TpxR#zYG2P+FY~mG{tnEkv~d6oUQA+ z&hNTL=~Y@rF`v-RZlts$nb$3(OL1&@Y11hhL9+zUb6)SP!;CD)^GUtUpCHBE`j1te zAGud@miCVFLk$fjsrcpjsadP__yj9iEZUW{Ll7PPi<$R;m1o!&Xdl~R_v0;oDX2z^!&8}zNGA}iYG|k zmehMd1%?R)u6R#<)B)1oe9TgYH5-CqUT8N7K-A-dm3hbm_W21p%8)H{O)xUlBVb+iUR}-v5dFaCyfSd zC6Bd7=N4A@+Bna=!-l|*_(nWGDpoyU>nH=}IOrLfS+-d40&(Wo*dDB9nQiA2Tse$R z;uq{`X7LLzP)%Y9aHa4YQ%H?htkWd3Owv&UYbr5NUDAH^<l@Z0Cx%`N+B*i!!1u>D8%;Qt1$ zE5O0{-`9gdDxZ!`0m}ywH!;c{oBfL-(BH<&SQ~smbcobU!j49O^f4&IIYh~f+hK*M zZwTp%{ZSAhMFj1qFaOA+3)p^gnXH^=)`NTYgTu!CLpEV2NF=~-`(}7p^Eof=@VUbd z_9U|8qF7Rueg&$qpSSkN%%%DpbV?8E8ivu@ensI0toJ7Eas^jyFReQ1JeY9plb^{m z&eQO)qPLZQ6O;FTr*aJq=$cMN)QlQO@G&%z?BKUs1&I^`lq>=QLODwa`(mFGC`0H< zOlc*|N?B5&!U6BuJvkL?s1&nsi$*5cCv7^j_*l&$-sBmRS85UIrE--7eD8Gr3^+o? zqG-Yl4S&E;>H>k^a0GdUI(|n1`ws@)1%sq2XBdK`mqrNq_b4N{#VpouCXLzNvjoFv zo9wMQ6l0+FT+?%N(ka*;%m~(?338bu32v26!{r)|w8J`EL|t$}TA4q_FJRX5 zCPa{hc_I(7TGE#@rO-(!$1H3N-C0{R$J=yPCXCtGk{4>=*B56JdXU9cQVwB`6~cQZ zf^qK21x_d>X%dT!!)CJQ3mlHA@ z{Prkgfs6=Tz%63$6Zr8CO0Ak3A)Cv#@BVKr&aiKG7RYxY$Yx>Bj#3gJk*~Ps-jc1l z;4nltQwwT4@Z)}Pb!3xM?+EW0qEKA)sqzw~!C6wd^{03-9aGf3Jmt=}w-*!yXupLf z;)>-7uvWN4Unn8b4kfIza-X=x*e4n5pU`HtgpFFd))s$C@#d>aUl3helLom+RYb&g zI7A9GXLRZPl}iQS*d$Azxg-VgcUr*lpLnbPKUV{QI|bsG{8bLG<%CF( zMoS4pRDtLVYOWG^@ox^h8xL~afW_9DcE#^1eEC1SVSb1BfDi^@g?#f6e%v~Aw>@w- zIY0k+2lGWNV|aA*e#`U3=+oBDmGeInfcL)>*!w|*;mWiKNG6wP6AW4-4imN!W)!hE zA02~S1*@Q`fD*+qX@f3!2yJX&6FsEfPditB%TWo3=HA;T3o2IrjS@9SSxv%{{7&4_ zdS#r4OU41~GYMiib#z#O;zohNbhJknrPPZS6sN$%HB=jUnlCO_w5Gw5EeE@KV>soy z2EZ?Y|4RQDDjt5y!WBlZ(8M)|HP<0YyG|D%RqD+K#e7-##o3IZxS^wQ5{Kbzb6h(i z#(wZ|^ei>8`%ta*!2tJzwMv+IFHLF`zTU8E^Mu!R*45_=ccqI};Zbyxw@U%a#2}%f zF>q?SrUa_a4H9l+uW8JHh2Oob>NyUwG=QH~-^ZebU*R@67DcXdz2{HVB4#@edz?B< z5!rQH3O0>A&ylROO%G^fimV*LX7>!%re{_Sm6N>S{+GW1LCnGImHRoF@csnFzn@P0 zM=jld0z%oz;j=>c7mMwzq$B^2mae7NiG}%>(wtmsDXkWk{?BeMpTrIt3Mizq?vRsf zi_WjNp+61uV(%gEU-Vf0;>~vcDhe(dzWdaf#4mH3o^v{0EWhj?E?$5v02sV@xL0l4 zX0_IMFtQ44PfWBbPYN#}qxa%=J%dlR{O!KyZvk^g5s?sTNycWYPJ^FK(nl3k?z-5t z39#hKrdO7V(@!TU)LAPY&ngnZ1MzLEeEiZznn7e-jLCy8LO zu^7_#z*%I-BjS#Pg-;zKWWqX-+Ly$T!4`vTe5ZOV0j?TJVA*2?*=82^GVlZIuH%9s zXiV&(T(QGHHah=s&7e|6y?g+XxZGmK55`wGV>@1U)Th&=JTgJq>4mI&Av2C z)w+kRoj_dA!;SfTfkgMPO>7Dw6&1*Hi1q?54Yng`JO&q->^CX21^PrU^JU#CJ_qhV zSG>afB%>2fx<~g8p=P8Yzxqc}s@>>{g7}F!;lCXvF#RV)^fyYb_)iKVCz1xEq=fJ| z0a7DMCK*FuP=NM*5h;*D`R4y$6cpW-E&-i{v`x=Jbk_xSn@2T3q!3HoAOB`@5Vg6) z{PW|@9o!e;v1jZ2{=Uw6S6o{g82x6g=k!)cFSC*oemHaVjg?VpEmtUuD2_J^A~$4* z3O7HsbA6wxw{TP5Kk)(Vm?gKo+_}11vbo{Tp_5x79P~#F)ahQXT)tSH5;;14?s)On zel1J>1x>+7;g1Iz2FRpnYz;sD0wG9Q!vuzE9yKi3@4a9Nh1!GGN?hA)!mZEnnHh&i zf?#ZEN2sFbf~kV;>K3UNj1&vFhc^sxgj8FCL4v>EOYL?2uuT`0eDH}R zmtUJMxVrV5H{L53hu3#qaWLUa#5zY?f5ozIn|PkMWNP%n zWB5!B0LZB0kLw$k39=!akkE9Q>F4j+q434jB4VmslQ;$ zKiO#FZ`p|dKS716jpcvR{QJkSNfDVhr2%~eHrW;fU45>>snr*S8Vik-5eN5k*c2Mp zyxvX&_cFbB6lODXznHHT|rsURe2!swomtrqc~w5 zymTM8!w`1{04CBprR!_F{5LB+2_SOuZN{b*!J~1ZiPpP-M;);!ce!rOPDLtgR@Ie1 zPreuqm4!H)hYePcW1WZ0Fyaqe%l}F~Orr)~+;mkS&pOhP5Ebb`cnUt!X_QhP4_4p( z8YKQCDKGIy>?WIFm3-}Br2-N`T&FOi?t)$hjphB9wOhBXU#Hb+zm&We_-O)s(wc`2 z8?VsvU;J>Ju7n}uUb3s1yPx_F*|FlAi=Ge=-kN?1;`~6szP%$3B0|8Sqp%ebM)F8v zADFrbeT0cgE>M0DMV@_Ze*GHM>q}wWMzt|GYC%}r{OXRG3Ij&<+nx9;4jE${Fj_r* z`{z1AW_6Myd)i6e0E-h&m{{CvzH=Xg!&(bLYgRMO_YVd8JU7W+7MuGWNE=4@OvP9+ zxi^vqS@5%+#gf*Z@RVyU9N1sO-(rY$24LGsg1>w>s6ST^@)|D9>cT50maXLUD{Fzf zt~tp{OSTEKg3ZSQyQQ5r51){%=?xlZ54*t1;Ow)zLe3i?8tD8YyY^k%M)e`V*r+vL zPqUf&m)U+zxps+NprxMHF{QSxv}>lE{JZETNk1&F+R~bp{_T$dbXL2UGnB|hgh*p4h$clt#6;NO~>zuyY@C-MD@)JCc5XrYOt`wW7! z_ti2hhZBMJNbn0O-uTxl_b6Hm313^fG@e;RrhIUK9@# z+DHGv_Ow$%S8D%RB}`doJjJy*aOa5mGHVHz0e0>>O_%+^56?IkA5eN+L1BVCp4~m=1eeL zb;#G!#^5G%6Mw}r1KnaKsLvJB%HZL)!3OxT{k$Yo-XrJ?|7{s4!H+S2o?N|^Z z)+?IE9H7h~Vxn5hTis^3wHYuOU84+bWd)cUKuHapq=&}WV#OxHpLab`NpwHm8LmOo zjri+!k;7j_?FP##CpM+pOVx*0wExEex z@`#)K<-ZrGyArK;a%Km`^+We|eT+#MygHOT6lXBmz`8|lyZOwL1+b+?Z$0OhMEp3R z&J=iRERpv~TC=p2-BYLC*?4 zxvPs9V@g=JT0>zky5Poj=fW_M!c)Xxz1<=&_ZcL=LMZJqlnO1P^xwGGW*Z+yTBvbV z-IFe6;(k1@$1;tS>{%pXZ_7w+i?N4A2=TXnGf=YhePg8bH8M|Lk-->+w8Y+FjZ;L=wSGwxfA`gqSn)f(XNuSm>6Y z@|#e-)I(PQ^G@N`%|_DZSb4_pkaEF0!-nqY+t#pyA>{9^*I-zw4SYA1_z2Bs$XGUZbGA;VeMo%CezHK0lO={L%G)dI-+8w?r9iexdoB{?l zbJ}C?huIhWXBVs7oo{!$lOTlvCLZ_KN1N+XJGuG$rh<^eUQIqcI7^pmqhBSaOKNRq zrx~w^?9C?*&rNwP_SPYmo;J-#!G|{`$JZK7DxsM3N^8iR4vvn>E4MU&Oe1DKJvLc~ zCT>KLZ1;t@My zRj_2hI^61T&LIz)S!+AQIV23n1>ng+LUvzv;xu!4;wpqb#EZz;F)BLUzT;8UA1x*6vJ zicB!3Mj03s*kGV{g`fpC?V^s(=JG-k1EMHbkdP4P*1^8p_TqO|;!Zr%GuP$8KLxuf z=pv*H;kzd;P|2`JmBt~h6|GxdU~@weK5O=X&5~w$HpfO}@l-T7@vTCxVOwCkoPQv8 z@aV_)I5HQtfs7^X=C03zYmH4m0S!V@JINm6#(JmZRHBD?T!m^DdiZJrhKpBcur2u1 zf9e4%k$$vcFopK5!CC`;ww(CKL~}mlxK_Pv!cOsFgVkNIghA2Au@)t6;Y3*2gK=5d z?|@1a)-(sQ%uFOmJ7v2iG&l&m^u&^6DJM#XzCrF%r>{2XKyxLD2rgWBD;i(!e4InDQBDg==^z;AzT2z~OmV0!?Z z0S9pX$+E;w3WN;v&NYT=+G8hf=6w0E1$0AOr61}eOvE8W1jX%>&Mjo7&!ulawgzLH zbcb+IF(s^3aj12WSi#pzIpijJJzkP?JzRawnxmNDSUR#7!29vHULCE<3Aa#be}ie~d|!V+ z%l~s9Odo$G&fH!t!+`rUT0T9DulF!Yq&BfQWFZV1L9D($r4H(}Gnf6k3^wa7g5|Ws zj7%d`!3(0bb55yhC6@Q{?H|2os{_F%o=;-h{@Yyyn*V7?{s%Grvpe!H^kl6tF4Zf5 z{Jv1~yZ*iIWL_9C*8pBMQArfJJ0d9Df6Kl#wa}7Xa#Ef_5B7=X}DzbQXVPfCwTO@9+@;A^Ti6il_C>g?A-GFwA0#U;t4;wOm-4oS})h z5&on>NAu67O?YCQr%7XIzY%LS4bha9*e*4bU4{lGCUmO2UQ2U)QOqClLo61Kx~3dI zmV3*(P6F_Tr-oP%x!0kTnnT?Ep5j;_IQ^pTRp=e8dmJtI4YgWd0}+b2=ATkOhgpXe z;jmw+FBLE}UIs4!&HflFr4)vMFOJ19W4f2^W(=2)F%TAL)+=F>IE$=e=@j-*bFLSg z)wf|uFQu+!=N-UzSef62u0-C8Zc7 zo6@F)c+nZA{H|+~7i$DCU0pL{0Ye|fKLuV^w!0Y^tT$isu%i1Iw&N|tX3kwFKJN(M zXS`k9js66o$r)x?TWL}Kxl`wUDUpwFx(w4Yk%49;$sgVvT~n8AgfG~HUcDt1TRo^s zdla@6heJB@JV z!vK;BUMznhzGK6PVtj0)GB=zTv6)Q9Yt@l#fv7>wKovLobMV-+(8)NJmyF8R zcB|_K7=FJGGn^X@JdFaat0uhKjp3>k#^&xE_}6NYNG?kgTp>2Iu?ElUjt4~E-?`Du z?mDCS9wbuS%fU?5BU@Ijx>1HG*N?gIP+<~xE4u=>H`8o((cS5M6@_OK%jSjFHirQK zN9@~NXFx*jS{<|bgSpC|SAnA@I)+GB=2W|JJChLI_mx+-J(mSJ!b)uUom6nH0#2^(L@JBlV#t zLl?j54s`Y3vE^c_3^Hl0TGu*tw_n?@HyO@ZrENxA+^!)OvUX28gDSF*xFtQzM$A+O zCG=n#6~r|3zt=8%GuG} z<#VCZ%2?3Q(Ad#Y7GMJ~{U3>E{5e@z6+rgZLX{Cxk^p-7dip^d29;2N1_mm4QkASo z-L`GWWPCq$uCo;X_BmGIpJFBlhl<8~EG{vOD1o|X$aB9KPhWO_cKiU*$HWEgtf=fn zsO%9bp~D2c@?*K9jVN@_vhR03>M_8h!_~%aN!Cnr?s-!;U3SVfmhRwk11A^8Ns`@KeE}+ zN$H}a1U6E;*j5&~Og!xHdfK5M<~xka)x-0N)K_&e7AjMz`toDzasH+^1bZlC!n()crk9kg@$(Y{wdKvbuUd04N^8}t1iOgsKF zGa%%XWx@WoVaNC1!|&{5ZbkopFre-Lu(LCE5HWZBoE#W@er9W<>R=^oYxBvypN#x3 zq#LC8&q)GFP=5^-bpHj?LW=)-g+3_)Ylps!3^YQ{9~O9&K)xgy zMkCWaApU-MI~e^cV{Je75Qr7eF%&_H)BvfyKL=gIA>;OSq(y z052BFz3E(Prg~09>|_Z@!qj}@;8yxnw+#Ej0?Rk<y}4ghbD569B{9hSFr*^ygZ zr6j7P#gtZh6tMk6?4V$*Jgz+#&ug;yOr>=qdI#9U&^am2qoh4Jy}H2%a|#Fs{E(5r z%!ijh;VuGA6)W)cJZx+;9Bp1LMUzN~x_8lQ#D3+sL{be-Jyeo@@dv7XguJ&S5vrH` z>QxOMWn7N-T!D@1(@4>ZlL^y5>m#0!HKovs12GRav4z!>p(1~xok8+_{| z#Ae4{9#NLh#Vj2&JuIn5$d6t@__`o}umFo(n0QxUtd2GKCyE+erwXY?`cm*h&^9*8 zJ+8x6fRZI-e$CRygofIQN^dWysCxgkyr{(_oBwwSRxZora1(%(aC!5BTtj^+YuevI zx?)H#(xlALUp6QJ!=l9N__$cxBZ5p&7;qD3PsXRFVd<({Kh+mShFWJNpy`N@ab7?9 zv5=klvCJ4bx|-pvOO2-+G)6O?$&)ncA#Urze2rlBfp#htudhx-NeRnJ@u%^_bfw4o z4|{b8SkPV3b>Wera1W(+N@p9H>dc6{cnkh-sgr?e%(YkWvK+0YXVwk0=d`)}*47*B z5JGkEdVix!w7-<%r0JF~`ZMMPe;f0EQHuYHxya`puazyph*ZSb1mJAt^k4549BfS; zK7~T&lRb=W{s&t`DJ$B}s-eH1&&-wEOH1KWsKn0a(ZI+G!v&W4A*cl>qAvUv6pbUR z#(f#EKV8~hk&8oayBz4vaswc(?qw1vn`yC zZQDl2PCB-&Uu@g9ZQHhO+v(W0bNig{-k0;;`+wM@#@J)8r?qOYs#&vUna8ILxN7S{ zp1s41KnR8miQJtJtOr|+qk}wrLt+N*z#5o`TmD1)E&QD(Vh&pjZJ_J*0!8dy_ z>^=@v=J)C`x&gjqAYu`}t^S=DFCtc0MkBU2zf|69?xW`Ck~(6zLD)gSE{7n~6w8j_ zoH&~$ED2k5-yRa0!r8fMRy z;QjBYUaUnpd}mf%iVFPR%Dg9!d>g`01m~>2s))`W|5!kc+_&Y>wD@@C9%>-lE`WB0 zOIf%FVD^cj#2hCkFgi-fgzIfOi+ya)MZK@IZhHT5FVEaSbv-oDDs0W)pA0&^nM0TW zmgJmd7b1R7b0a`UwWJYZXp4AJPteYLH>@M|xZFKwm!t3D3&q~av?i)WvAKHE{RqpD{{%OhYkK?47}+}` zrR2(Iv9bhVa;cDzJ%6ntcSbx7v7J@Y4x&+eWSKZ*eR7_=CVIUSB$^lfYe@g+p|LD{ zPSpQmxx@b$%d!05|H}WzBT4_cq?@~dvy<7s&QWtieJ9)hd4)$SZz}#H2UTi$CkFWW|I)v_-NjuH!VypONC=1`A=rm_jfzQ8Fu~1r8i{q-+S_j$ z#u^t&Xnfi5tZtl@^!fUJhx@~Cg0*vXMK}D{>|$#T*+mj(J_@c{jXBF|rm4-8%Z2o! z2z0o(4%8KljCm^>6HDK!{jI7p+RAPcty_~GZ~R_+=+UzZ0qzOwD=;YeZt*?3%UGdr z`c|BPE;yUbnyARUl&XWSNJ<+uRt%!xPF&K;(l$^JcA_CMH6)FZt{>6ah$|(9$2fc~ z=CD00uHM{qv;{Zk9FR0~u|3|Eiqv9?z2#^GqylT5>6JNZwKqKBzzQpKU2_pmtD;CT zi%Ktau!Y2Tldfu&b0UgmF(SSBID)15*r08eoUe#bT_K-G4VecJL2Pa=6D1K6({zj6 za(2Z{r!FY5W^y{qZ}08+h9f>EKd&PN90f}Sc0ejf%kB4+f#T8Q1=Pj=~#pi$U zp#5rMR%W25>k?<$;$x72pkLibu1N|jX4cWjD3q^Pk3js!uK6h7!dlvw24crL|MZs_ zb%Y%?Fyp0bY0HkG^XyS76Ts*|Giw{31LR~+WU5NejqfPr73Rp!xQ1mLgq@mdWncLy z%8}|nzS4P&`^;zAR-&nm5f;D-%yNQPwq4N7&yULM8bkttkD)hVU>h>t47`{8?n2&4 zjEfL}UEagLUYwdx0sB2QXGeRmL?sZ%J!XM`$@ODc2!y|2#7hys=b$LrGbvvjx`Iqi z&RDDm3YBrlKhl`O@%%&rhLWZ*ABFz2nHu7k~3@e4)kO3%$=?GEFUcCF=6-1n!x^vmu+Ai*amgXH+Rknl6U>#9w;A} zn2xanZSDu`4%%x}+~FG{Wbi1jo@wqBc5(5Xl~d0KW(^Iu(U3>WB@-(&vn_PJt9{1`e9Iic@+{VPc`vP776L*viP{wYB2Iff8hB%E3|o zGMOu)tJX!`qJ}ZPzq7>=`*9TmETN7xwU;^AmFZ-ckZjV5B2T09pYliaqGFY|X#E-8 z20b>y?(r-Fn5*WZ-GsK}4WM>@TTqsxvSYWL6>18q8Q`~JO1{vLND2wg@58OaU!EvT z1|o+f1mVXz2EKAbL!Q=QWQKDZpV|jznuJ}@-)1&cdo z^&~b4Mx{*1gurlH;Vhk5g_cM&6LOHS2 zRkLfO#HabR1JD4Vc2t828dCUG#DL}f5QDSBg?o)IYYi@_xVwR2w_ntlpAW0NWk$F1 z$If?*lP&Ka1oWfl!)1c3fl`g*lMW3JOn#)R1+tfwrs`aiFUgz3;XIJ>{QFxLCkK30 zNS-)#DON3yb!7LBHQJ$)4y%TN82DC2-9tOIqzhZ27@WY^<6}vXCWcR5iN{LN8{0u9 zNXayqD=G|e?O^*ms*4P?G%o@J1tN9_76e}E#66mr89%W_&w4n66~R;X_vWD(oArwj z4CpY`)_mH2FvDuxgT+akffhX0b_slJJ*?Jn3O3~moqu2Fs1oL*>7m=oVek2bnprnW zixkaIFU%+3XhNA@@9hyhFwqsH2bM|`P?G>i<-gy>NflhrN{$9?LZ1ynSE_Mj0rADF zhOz4FnK}wpLmQuV zgO4_Oz9GBu_NN>cPLA=`SP^$gxAnj;WjJnBi%Q1zg`*^cG;Q)#3Gv@c^j6L{arv>- zAW%8WrSAVY1sj$=umcAf#ZgC8UGZGoamK}hR7j6}i8#np8ruUlvgQ$j+AQglFsQQq zOjyHf22pxh9+h#n$21&$h?2uq0>C9P?P=Juw0|;oE~c$H{#RGfa>| zj)Iv&uOnaf@foiBJ}_;zyPHcZt1U~nOcNB{)og8Btv+;f@PIT*xz$x!G?u0Di$lo7 zOugtQ$Wx|C($fyJTZE1JvR~i7LP{ zbdIwqYghQAJi9p}V&$=*2Azev$6K@pyblphgpv8^9bN!?V}{BkC!o#bl&AP!3DAjM zmWFsvn2fKWCfjcAQmE+=c3Y7j@#7|{;;0f~PIodmq*;W9Fiak|gil6$w3%b_Pr6K_ zJEG@&!J%DgBZJDCMn^7mk`JV0&l07Bt`1ymM|;a)MOWz*bh2#d{i?SDe9IcHs7 zjCrnyQ*Y5GzIt}>`bD91o#~5H?4_nckAgotN{2%!?wsSl|LVmJht$uhGa+HiH>;av z8c?mcMYM7;mvWr6noUR{)gE!=i7cZUY7e;HXa221KkRoc2UB>s$Y(k%NzTSEr>W(u z<(4mcc)4rB_&bPzX*1?*ra%VF}P1nwiP5cykJ&W{!OTlz&Td0pOkVp+wc z@k=-Hg=()hNg=Q!Ub%`BONH{ z_=ZFgetj@)NvppAK2>8r!KAgi>#%*7;O-o9MOOfQjV-n@BX6;Xw;I`%HBkk20v`qoVd0)}L6_49y1IhR z_OS}+eto}OPVRn*?UHC{eGyFU7JkPz!+gX4P>?h3QOwGS63fv4D1*no^6PveUeE5% zlehjv_3_^j^C({a2&RSoVlOn71D8WwMu9@Nb@=E_>1R*ve3`#TF(NA0?d9IR_tm=P zOP-x;gS*vtyE1Cm zG0L?2nRUFj#aLr-R1fX*$sXhad)~xdA*=hF3zPZhha<2O$Ps+F07w*3#MTe?)T8|A!P!v+a|ot{|^$q(TX`35O{WI0RbU zCj?hgOv=Z)xV?F`@HKI11IKtT^ocP78cqHU!YS@cHI@{fPD?YXL)?sD~9thOAv4JM|K8OlQhPXgnevF=F7GKD2#sZW*d za}ma31wLm81IZxX(W#A9mBvLZr|PoLnP>S4BhpK8{YV_}C|p<)4#yO{#ISbco92^3 zv&kCE(q9Wi;9%7>>PQ!zSkM%qqqLZW7O`VXvcj;WcJ`2~v?ZTYB@$Q&^CTfvy?1r^ z;Cdi+PTtmQwHX_7Kz?r#1>D zS5lWU(Mw_$B&`ZPmqxpIvK<~fbXq?x20k1~9az-Q!uR78mCgRj*eQ>zh3c$W}>^+w^dIr-u{@s30J=)1zF8?Wn|H`GS<=>Om|DjzC{}Jt?{!fSJe*@$H zg>wFnlT)k#T?LslW zu$^7Uy~$SQ21cE?3Ijl+bLfuH^U5P^$@~*UY#|_`uvAIe(+wD2eF}z_y!pvomuVO; zS^9fbdv)pcm-B@CW|Upm<7s|0+$@@<&*>$a{aW+oJ%f+VMO<#wa)7n|JL5egEgoBv zl$BY(NQjE0#*nv=!kMnp&{2Le#30b)Ql2e!VkPLK*+{jv77H7)xG7&=aPHL7LK9ER z5lfHxBI5O{-3S?GU4X6$yVk>lFn;ApnwZybdC-GAvaznGW-lScIls-P?Km2mF>%B2 zkcrXTk+__hj-3f48U%|jX9*|Ps41U_cd>2QW81Lz9}%`mTDIhE)jYI$q$ma7Y-`>% z8=u+Oftgcj%~TU}3nP8&h7k+}$D-CCgS~wtWvM|UU77r^pUw3YCV80Ou*+bH0!mf0 zxzUq4ed6y>oYFz7+l18PGGzhB^pqSt)si=9M>~0(Bx9*5r~W7sa#w+_1TSj3Jn9mW zMuG9BxN=}4645Cpa#SVKjFst;9UUY@O<|wpnZk$kE+to^4!?0@?Cwr3(>!NjYbu?x z1!U-?0_O?k!NdM^-rIQ8p)%?M+2xkhltt*|l=%z2WFJhme7*2xD~@zk#`dQR$6Lmd zb3LOD4fdt$Cq>?1<%&Y^wTWX=eHQ49Xl_lFUA(YQYHGHhd}@!VpYHHm=(1-O=yfK#kKe|2Xc*9}?BDFN zD7FJM-AjVi)T~OG)hpSWqH>vlb41V#^G2B_EvYlWhDB{Z;Q9-0)ja(O+By`31=biA zG&Fs#5!%_mHi|E4Nm$;vVQ!*>=_F;ZC=1DTPB#CICS5fL2T3XmzyHu?bI;m7D4@#; ztr~;dGYwb?m^VebuULtS4lkC_7>KCS)F@)0OdxZIFZp@FM_pHnJes8YOvwB|++#G( z&dm*OP^cz95Wi15vh`Q+yB>R{8zqEhz5of>Po$9LNE{xS<)lg2*roP*sQ}3r3t<}; zPbDl{lk{pox~2(XY5=qg0z!W-x^PJ`VVtz$git7?)!h>`91&&hESZy1KCJ2nS^yMH z!=Q$eTyRi68rKxdDsdt+%J_&lapa{ds^HV9Ngp^YDvtq&-Xp}60B_w@Ma>_1TTC;^ zpbe!#gH}#fFLkNo#|`jcn?5LeUYto%==XBk6Ik0kc4$6Z+L3x^4=M6OI1=z5u#M%0 z0E`kevJEpJjvvN>+g`?gtnbo$@p4VumliZV3Z%CfXXB&wPS^5C+7of2tyVkMwNWBiTE2 z8CdPu3i{*vR-I(NY5syRR}I1TJOV@DJy-Xmvxn^IInF>Tx2e)eE9jVSz69$6T`M9-&om!T+I znia!ZWJRB28o_srWlAxtz4VVft8)cYloIoVF=pL zugnk@vFLXQ_^7;%hn9x;Vq?lzg7%CQR^c#S)Oc-8d=q_!2ZVH764V z!wDKSgP}BrVV6SfCLZnYe-7f;igDs9t+K*rbMAKsp9L$Kh<6Z;e7;xxced zn=FGY<}CUz31a2G}$Q(`_r~75PzM4l_({Hg&b@d8&jC}B?2<+ed`f#qMEWi z`gm!STV9E4sLaQX+sp5Nu9*;9g12naf5?=P9p@H@f}dxYprH+3ju)uDFt^V{G0APn zS;16Dk{*fm6&BCg#2vo?7cbkkI4R`S9SSEJ=#KBk3rl69SxnCnS#{*$!^T9UUmO#&XXKjHKBqLdt^3yVvu8yn|{ zZ#%1CP)8t-PAz(+_g?xyq;C2<9<5Yy<~C74Iw(y>uUL$+$mp(DRcCWbCKiGCZw@?_ zdomfp+C5xt;j5L@VfhF*xvZdXwA5pcdsG>G<8II-|1dhAgzS&KArcb0BD4ZZ#WfiEY{hkCq5%z9@f|!EwTm;UEjKJsUo696V>h zy##eXYX}GUu%t{Gql8vVZKkNhQeQ4C%n|RmxL4ee5$cgwlU+?V7a?(jI#&3wid+Kz5+x^G!bb#$q>QpR#BZ}Xo5UW^ zD&I`;?(a}Oys7-`I^|AkN?{XLZNa{@27Dv^s4pGowuyhHuXc zuctKG2x0{WCvg_sGN^n9myJ}&FXyGmUQnW7fR$=bj$AHR88-q$D!*8MNB{YvTTEyS zn22f@WMdvg5~o_2wkjItJN@?mDZ9UUlat2zCh(zVE=dGi$rjXF7&}*sxac^%HFD`Y zTM5D3u5x**{bW!68DL1A!s&$2XG@ytB~dX-?BF9U@XZABO`a|LM1X3HWCllgl0+uL z04S*PX$%|^WAq%jkzp~%9HyYIF{Ym?k)j3nMwPZ=hlCg9!G+t>tf0o|J2%t1 ztC+`((dUplgm3`+0JN~}&FRRJ3?l*>Y&TfjS>!ShS`*MwO{WIbAZR#<%M|4c4^dY8 z{Rh;-!qhY=dz5JthbWoovLY~jNaw>%tS4gHVlt5epV8ekXm#==Po$)}mh^u*cE>q7*kvX&gq)(AHoItMYH6^s6f(deNw%}1=7O~bTHSj1rm2|Cq+3M z93djjdomWCTCYu!3Slx2bZVy#CWDozNedIHbqa|otsUl+ut?>a;}OqPfQA05Yim_2 zs@^BjPoFHOYNc6VbNaR5QZfSMh2S*`BGwcHMM(1@w{-4jVqE8Eu0Bi%d!E*^Rj?cR z7qgxkINXZR)K^=fh{pc0DCKtrydVbVILI>@Y0!Jm>x-xM!gu%dehm?cC6ok_msDVA*J#{75%4IZt}X|tIVPReZS#aCvuHkZxc zHVMtUhT(wp09+w9j9eRqz~LtuSNi2rQx_QgQ(}jBt7NqyT&ma61ldD(s9x%@q~PQl zp6N*?=N$BtvjQ_xIT{+vhb1>{pM0Arde0!X-y))A4znDrVx8yrP3B1(7bKPE5jR@5 zwpzwT4cu~_qUG#zYMZ_!2Tkl9zP>M%cy>9Y(@&VoB84#%>amTAH{(hL4cDYt!^{8L z645F>BWO6QaFJ-{C-i|-d%j7#&7)$X7pv#%9J6da#9FB5KyDhkA+~)G0^87!^}AP>XaCSScr;kL;Z%RSPD2CgoJ;gpYT5&6NUK$86$T?jRH=w8nI9Z534O?5fk{kd z`(-t$8W|#$3>xoMfXvV^-A(Q~$8SKDE^!T;J+rQXP71XZ(kCCbP%bAQ1|%$%Ov9_a zyC`QP3uPvFoBqr_+$HenHklqyIr>PU_Fk5$2C+0eYy^~7U&(!B&&P2%7#mBUhM!z> z_B$Ko?{Pf6?)gpYs~N*y%-3!1>o-4;@1Zz9VQHh)j5U1aL-Hyu@1d?X;jtDBNk*vMXPn@ z+u@wxHN*{uHR!*g*4Xo&w;5A+=Pf9w#PeZ^x@UD?iQ&${K2c}UQgLRik-rKM#Y5rdDphdcNTF~cCX&9ViRP}`>L)QA4zNXeG)KXFzSDa6 zd^St;inY6J_i=5mcGTx4_^Ys`M3l%Q==f>{8S1LEHn{y(kbxn5g1ezt4CELqy)~TV6{;VW>O9?5^ ztcoxHRa0jQY7>wwHWcxA-BCwzsP>63Kt&3fy*n#Cha687CQurXaRQnf5wc9o8v7Rw zNwGr2fac;Wr-Ldehn7tF^(-gPJwPt@VR1f;AmKgxN&YPL;j=0^xKM{!wuU|^mh3NE zy35quf}MeL!PU;|{OW_x$TBothLylT-J>_x6p}B_jW1L>k)ps6n%7Rh z96mPkJIM0QFNYUM2H}YF5bs%@Chs6#pEnloQhEl?J-)es!(SoJpEPoMTdgA14-#mC zghayD-DJWtUu`TD8?4mR)w5E`^EHbsz2EjH5aQLYRcF{l7_Q5?CEEvzDo(zjh|BKg z3aJl_n#j&eFHsUw4~lxqnr!6NL*se)6H=A+T1e3xUJGQrd}oSPwSy5+$tt{2t5J5@(lFxl43amsARG74iyNC}uuS zd2$=(r6RdamdGx^eatX@F2D8?U23tDpR+Os?0Gq2&^dF+$9wiWf?=mDWfjo4LfRwL zI#SRV9iSz>XCSgEj!cW&9H-njJopYiYuq|2w<5R2!nZ27DyvU4UDrHpoNQZiGPkp@ z1$h4H46Zn~eqdj$pWrv;*t!rTYTfZ1_bdkZmVVIRC21YeU$iS-*XMNK`#p8Z_DJx| zk3Jssf^XP7v0X?MWFO{rACltn$^~q(M9rMYoVxG$15N;nP)A98k^m3CJx8>6}NrUd@wp-E#$Q0uUDQT5GoiK_R{ z<{`g;8s>UFLpbga#DAf%qbfi`WN1J@6IA~R!YBT}qp%V-j!ybkR{uY0X|x)gmzE0J z&)=eHPjBxJvrZSOmt|)hC+kIMI;qgOnuL3mbNR0g^<%|>9x7>{}>a2qYSZAGPt4it?8 zNcLc!Gy0>$jaU?}ZWxK78hbhzE+etM`67*-*x4DN>1_&{@5t7_c*n(qz>&K{Y?10s zXsw2&nQev#SUSd|D8w7ZD2>E<%g^; zV{yE_O}gq?Q|zL|jdqB^zcx7vo(^})QW?QKacx$yR zhG|XH|8$vDZNIfuxr-sYFR{^csEI*IM#_gd;9*C+SysUFejP0{{z7@P?1+&_o6=7V|EJLQun^XEMS)w(=@eMi5&bbH*a0f;iC~2J74V2DZIlLUHD&>mlug5+v z6xBN~8-ovZylyH&gG#ptYsNlT?-tzOh%V#Y33zlsJ{AIju`CjIgf$@gr8}JugRq^c zAVQ3;&uGaVlVw}SUSWnTkH_6DISN&k2QLMBe9YU=sA+WiX@z)FoSYX`^k@B!j;ZeC zf&**P?HQG6Rk98hZ*ozn6iS-dG}V>jQhb3?4NJB*2F?6N7Nd;EOOo;xR7acylLaLy z9)^lykX39d@8@I~iEVar4jmjjLWhR0d=EB@%I;FZM$rykBNN~jf>#WbH4U{MqhhF6 zU??@fSO~4EbU4MaeQ_UXQcFyO*Rae|VAPLYMJEU`Q_Q_%s2*>$#S^)&7er+&`9L=1 z4q4ao07Z2Vsa%(nP!kJ590YmvrWg+YrgXYs_lv&B5EcoD`%uL79WyYA$0>>qi6ov7 z%`ia~J^_l{p39EY zv>>b}Qs8vxsu&WcXEt8B#FD%L%ZpcVtY!rqVTHe;$p9rbb5O{^rFMB>auLn-^;s+-&P1#h~mf~YLg$8M9 zZ4#87;e-Y6x6QO<{McUzhy(%*6| z)`D~A(TJ$>+0H+mct(jfgL4x%^oC^T#u(bL)`E2tBI#V1kSikAWmOOYrO~#-cc_8! zCe|@1&mN2{*ceeiBldHCdrURk4>V}79_*TVP3aCyV*5n@jiNbOm+~EQ_}1#->_tI@ zqXv+jj2#8xJtW508rzFrYcJxoek@iW6SR@1%a%Bux&;>25%`j3UI`0DaUr7l79`B1 zqqUARhW1^h6=)6?;@v>xrZNM;t}{yY3P@|L}ey@gG( z9r{}WoYN(9TW&dE2dEJIXkyHA4&pU6ki=rx&l2{DLGbVmg4%3Dlfvn!GB>EVaY_%3+Df{fBiqJV>~Xf8A0aqUjgpa} zoF8YXO&^_x*Ej}nw-$-F@(ddB>%RWoPUj?p8U{t0=n>gAI83y<9Ce@Q#3&(soJ{64 z37@Vij1}5fmzAuIUnXX`EYe;!H-yTVTmhAy;y8VZeB#vD{vw9~P#DiFiKQ|kWwGFZ z=jK;JX*A;Jr{#x?n8XUOLS;C%f|zj-7vXtlf_DtP7bpurBeX%Hjwr z4lI-2TdFpzkjgiv!8Vfv`=SP+s=^i3+N~1ELNWUbH|ytVu>EyPN_3(4TM^QE1swRo zoV7Y_g)a>28+hZG0e7g%@2^s>pzR4^fzR-El}ARTmtu!zjZLuX%>#OoU3}|rFjJg} zQ2TmaygxJ#sbHVyiA5KE+yH0LREWr%^C*yR|@gM$nK2P zo}M}PV0v))uJh&33N>#aU376@ZH79u(Yw`EQ2hM3SJs9f99+cO6_pNW$j$L-CtAfe zYfM)ccwD!P%LiBk!eCD?fHCGvgMQ%Q2oT_gmf?OY=A>&PaZQOq4eT=lwbaf}33LCH zFD|)lu{K7$8n9gX#w4~URjZxWm@wlH%oL#G|I~Fb-v^0L0TWu+`B+ZG!yII)w05DU z>GO?n(TN+B=>HdxVDSlIH76pta$_LhbBg;eZ`M7OGcqt||qi zogS72W1IN%=)5JCyOHWoFP7pOFK0L*OAh=i%&VW&4^LF@R;+K)t^S!96?}^+5QBIs zjJNTCh)?)4k^H^g1&jc>gysM`y^8Rm3qsvkr$9AeWwYpa$b22=yAd1t<*{ zaowSEFP+{y?Ob}8&cwfqoy4Pb9IA~VnM3u!trIK$&&0Op#Ql4j>(EW?UNUv#*iH1$ z^j>+W{afcd`{e&`-A{g}{JnIzYib)!T56IT@YEs{4|`sMpW3c8@UCoIJv`XsAw!XC z34|Il$LpW}CIHFC5e*)}00I5{%OL*WZRGzC0?_}-9{#ue?-ug^ zLE|uv-~6xnSs_2_&CN9{9vyc!Xgtn36_g^wI0C4s0s^;8+p?|mm;Odt3`2ZjwtK;l zfd6j)*Fr#53>C6Y8(N5?$H0ma;BCF3HCjUs7rpb2Kf*x3Xcj#O8mvs#&33i+McX zQpBxD8!O{5Y8D&0*QjD=Yhl9%M0)&_vk}bmN_Ud^BPN;H=U^bn&(csl-pkA+GyY0Z zKV7sU_4n;}uR78ouo8O%g*V;79KY?3d>k6%gpcmQsKk&@Vkw9yna_3asGt`0Hmj59 z%0yiF*`jXhByBI9QsD=+>big5{)BGe&+U2gAARGe3ID)xrid~QN_{I>k}@tzL!Md_ z&=7>TWciblF@EMC3t4-WX{?!m!G6$M$1S?NzF*2KHMP3Go4=#ZHkeIv{eEd;s-yD# z_jU^Ba06TZqvV|Yd;Z_sN%$X=!T+&?#p+OQIHS%!LO`Hx0q_Y0MyGYFNoM{W;&@0@ zLM^!X4KhdtsET5G<0+|q0oqVXMW~-7LW9Bg}=E$YtNh1#1D^6Mz(V9?2g~I1( zoz9Cz=8Hw98zVLwC2AQvp@pBeKyidn6Xu0-1SY1((^Hu*-!HxFUPs)yJ+i`^BC>PC zjwd0mygOVK#d2pRC9LxqGc6;Ui>f{YW9Bvb>33bp^NcnZoH~w9(lM5@JiIlfa-6|k ziy31UoMN%fvQfhi8^T+=yrP{QEyb-jK~>$A4SZT-N56NYEbpvO&yUme&pWKs3^94D zH{oXnUTb3T@H+RgzML*lejx`WAyw*?K7B-I(VJx($2!NXYm%3`=F~TbLv3H<{>D?A zJo-FDYdSA-(Y%;4KUP2SpHKAIcv9-ld(UEJE7=TKp|Gryn;72?0LHqAN^fk6%8PCW z{g_-t)G5uCIf0I`*F0ZNl)Z>))MaLMpXgqWgj-y;R+@A+AzDjsTqw2Mo9ULKA3c70 z!7SOkMtZb+MStH>9MnvNV0G;pwSW9HgP+`tg}e{ij0H6Zt5zJ7iw`hEnvye!XbA@!~#%vIkzowCOvq5I5@$3wtc*w2R$7!$*?}vg4;eDyJ_1=ixJuEp3pUS27W?qq(P^8$_lU!mRChT}ctvZz4p!X^ zOSp|JOAi~f?UkwH#9k{0smZ7-#=lK6X3OFEMl7%)WIcHb=#ZN$L=aD`#DZKOG4p4r zwlQ~XDZ`R-RbF&hZZhu3(67kggsM-F4Y_tI^PH8PMJRcs7NS9ogF+?bZB*fcpJ z=LTM4W=N9yepVvTj&Hu~0?*vR1HgtEvf8w%Q;U0^`2@e8{SwgX5d(cQ|1(!|i$km! zvY03MK}j`sff;*-%mN~ST>xU$6Bu?*Hm%l@0dk;j@%>}jsgDcQ)Hn*UfuThz9(ww_ zasV`rSrp_^bp-0sx>i35FzJwA!d6cZ5#5#nr@GcPEjNnFHIrtUYm1^Z$;{d&{hQV9 z6EfFHaIS}46p^5I-D_EcwwzUUuO}mqRh&T7r9sfw`)G^Q%oHxEs~+XoM?8e*{-&!7 z7$m$lg9t9KP9282eke608^Q2E%H-xm|oJ8=*SyEo} z@&;TQ3K)jgspgKHyGiKVMCz>xmC=H5Fy3!=TP)-R3|&1S-B)!6q50wfLHKM@7Bq6E z44CY%G;GY>tC`~yh!qv~YdXw! zSkquvYNs6k1r7>Eza?Vkkxo6XRS$W7EzL&A`o>=$HXgBp{L(i^$}t`NcnAxzbH8Ht z2!;`bhKIh`f1hIFcI5bHI=ueKdzmB9)!z$s-BT4ItyY|NaA_+o=jO%MU5as9 zc2)aLP>N%u>wlaXTK!p)r?+~)L+0eCGb5{8WIk7K52$nufnQ+m8YF+GQc&{^(zh-$ z#wyWV*Zh@d!b(WwXqvfhQX)^aoHTBkc;4ossV3&Ut*k>AI|m+{#kh4B!`3*<)EJVj zwrxK>99v^k4&Y&`Awm>|exo}NvewV%E+@vOc>5>%H#BK9uaE2$vje zWYM5fKuOTtn96B_2~~!xJPIcXF>E_;yO8AwpJ4)V`Hht#wbO3Ung~@c%%=FX4)q+9 z99#>VC2!4l`~0WHs9FI$Nz+abUq# zz`Of97})Su=^rGp2S$)7N3rQCj#0%2YO<R&p>$<#lgXcUj=4H_{oAYiT3 z44*xDn-$wEzRw7#@6aD)EGO$0{!C5Z^7#yl1o;k0PhN=aVUQu~eTQ^Xy{z8Ow6tk83 z4{5xe%(hx)%nD&|e*6sTWH`4W&U!Jae#U4TnICheJmsw{l|CH?UA{a6?2GNgpZLyzU2UlFu1ZVwlALmh_DOs03J^Cjh1im`E3?9&zvNmg(MuMw&0^Lu$(#CJ*q6DjlKsY-RMJ^8yIY|{SQZ*9~CH|u9L z`R78^r=EbbR*_>5?-)I+$6i}G)%mN(`!X72KaV(MNUP7Nv3MS9S|Pe!%N2AeOt5zG zVJ;jI4HZ$W->Ai_4X+`9c(~m=@ek*m`ZQbv3ryI-AD#AH=`x$~WeW~M{Js57(K7(v ze5`};LG|%C_tmd>bkufMWmAo&B+DT9ZV~h(4jg0>^aeAqL`PEUzJJtI8W1M!bQWpv zvN(d}E1@nlYa!L!!A*RN!(Q3F%J?5PvQ0udu?q-T)j3JKV~NL>KRb~w-lWc685uS6 z=S#aR&B8Sc8>cGJ!!--?kwsJTUUm`Jk?7`H z7PrO~xgBrSW2_tTlCq1LH8*!o?pj?qxy8}(=r_;G18POrFh#;buWR0qU24+XUaVZ0 z?(sXcr@-YqvkCmHr{U2oPogHL{r#3r49TeR<{SJX1pcUqyWPrkYz^X8#QW~?F)R5i z>p^!i<;qM8Nf{-fd6!_&V*e_9qP6q(s<--&1Ttj01j0w>bXY7y1W*%Auu&p|XSOH=)V7Bd4fUKh&T1)@cvqhuD-d=?w}O zjI%i(f|thk0Go*!d7D%0^ztBfE*V=(ZIN84f5HU}T9?ulmEYzT5usi=DeuI*d|;M~ zp_=Cx^!4k#=m_qSPBr5EK~E?3J{dWWPH&oCcNepYVqL?nh4D5ynfWip$m*YlZ8r^Z zuFEUL-nW!3qjRCLIWPT0x)FDL7>Yt7@8dA?R2kF@WE>ysMY+)lTsgNM#3VbXVGL}F z1O(>q>2a+_`6r5Xv$NZAnp=Kgnr3)cL(^=8ypEeOf3q8(HGe@7Tt59;yFl||w|mnO zHDxg2G3z8=(6wjj9kbcEY@Z0iOd7Gq5GiPS5% z*sF1J<#daxDV2Z8H>wxOF<;yKzMeTaSOp_|XkS9Sfn6Mpe9UBi1cSTieGG5$O;ZLIIJ60Y>SN4vC?=yE_CWlo(EEE$e4j?z&^FM%kNmRtlbEL^dPPgvs9sbK5fGw*r@ z+!EU@u$T8!nZh?Fdf_qk$VuHk^yVw`h`_#KoS*N%epIIOfQUy_&V}VWDGp3tplMbf z5Se1sJUC$7N0F1-9jdV2mmGK{-}fu|Nv;12jDy0<-kf^AmkDnu6j~TPWOgy1MT68|D z=4=50jVbUKdKaQgD`eWGr3I&^<6uhkjz$YwItY8%Yp9{z4-{6g{73<_b*@XJ4Nm3-3z z?BW3{aY_ccRjb@W1)i5nLg|7BnWS!B`_Uo9CWaE`Ij327QH?i)9A}4Ug4wmxVVa^b z-4+m%-wwOl7cKH7+=x&nrCrbEC)Q$fpg&V83#uEH;C=GNMz`ps@^RxK%T*8%OPnC` z{WO~J%nxYJ`x|N%?&i7?;{_8t^jM&=50HlaOQj8fS}_`moH$c;vI<|cruPFnpT8yU zS%rPOCUSd5Zdb(zwk`hqwTQn)*&n)uYsP*F_(~xEWq}C= zv30kFmZFwJZ@ELVX3?$dXQh|icO7UrL*_5G=I^xXjImz`ZPp>?g#tf(ej~KaIU0algsG!IS09;>?MvqGg#c{i+}qY|{P8W~O%#>|gFd z<1dr$-oxyRGN17yZo1OwLnzwYs0|;IS_nymNB0IlSzPQ%-r`?T=;_XQ^~&#}b|AB} zkNbN5uB?-sUB-T5QLlg%Uk3)uHB;>VIzGe9_J9 zaeISkQm!v(9d(0ML^b9fR^sfHFlH?7Mvddt37OuR{|O0{uv)(&-6<87W4 zyO>s!=cPgP3O&7xxU5DlIPw_o3O>6o6Qb?JWs3qw#p3sBc3g$?Dx zi(6D+DYgV;GrUis-CL%Qe{nvZnwaVXmbhH(|GFh|Q)k=1uvA$I@1DXI7bKlQ@8D6P zS?(*?><>)G49q0wr;NajpxP4W2G)kHl6^=Z>hrNEI4Mwd_$O6$1dXF;Q#hE(-eeW6 zz03GJF%Wl?HO=_ztv5*zRlcU~{+{k%#N59mgm~eK>P!QZ6E?#Cu^2)+K8m@ySvZ*5 z|HDT}BkF@3!l(0%75G=1u2hETXEj!^1Z$!)!lyGXlWD!_vqGE$Z)#cUVBqlORW>0^ zDjyVTxwKHKG|0}j-`;!R-p>}qQfBl(?($7pP<+Y8QE#M8SCDq~k<+>Q^Zf@cT_WdX3~BSe z+|KK|7OL5Hm5(NFP~j>Ct3*$wi0n0!xl=(C61`q&cec@mFlH(sy%+RH<=s)8aAPN`SfJdkAQjdv82G5iRdv8 zh{9wHUZaniSEpslXl^_ODh}mypC?b*9FzLjb~H@3DFSe;D(A-K3t3eOTB(m~I6C;(-lKAvit(70k`%@+O*Ztdz;}|_TS~B?Tpmi=QKC^m_ z2YpEaT3iiz*;T~ap1yiA)a`dKMwu`^UhIUeltNQ1Yjo=q@bI@&3zH?rVUg=IxLy-ni zyxDu%-Fr{H6owTjZU2O5>nDb=q&Jz_TjeSq%!2m40x&U6w~GQ({quPL73IsJS;f`$ zsuhioqCBj(gJ>2hoo)Gou7(WP*pX)f=Y=!=k!&1K?EYY%jJ~X&DnK{^saPQK<1BJ z_A`_{%ZozcB(3w$z^To^6d|XuT@=X~wtW!+{4ID@N{AB~J6AL5vuY>JwvWCNFKsKh zd}@>q@_WV#QZ&UJ0#?X(pXR!oyXOEG3rqzHbCzGLONDb042i$})fM@XF)uSP(DHUc z^&{|$*xe{cs?Gp8=B%RY3L7#$ve$?TWh>MZdxF1zH1v}1z+$Ov#G7?%D)bBCyDe*% zSeKSpETC2V1){II>@UwJi>4uBN+iAx+82E~gb|Cr&8E^i&)A!uv-g?jzH99wU}8+# z$nh>yvb;TwZmS@7LrvuCu_d0-WxFNI&C7%sWuTL%YU!l|I1{|->=dlOeHOCtUO#zkS3ESO8LHV4hTdQL5EdV zuWD33fFPH}HPrW^s$Qn1Xgp&AT6<-He{{4%eIu3rN=iK|9mURdKXfB&Q?qGok%!cs ze53UP{Z!TO-Y@q2;;k2avA3`lm4OoN4@S*k=UA)7H;qZ`d8`XaYFCv?Ba+uGW@r5v z&&{nf(24WSBOhc7!qF^@0cz;XcUynNaj6w2349;s!K{KVqs5yS{ z7VubS`2OzT^5#1~6Tt^RTvt9-J|D2F>y~>2;jeF>g`hx5l%B3H=aLExQihuYngzlnBTYOTHJQMzl>kwqN5JYs)Ej zblA@ntkUS~xi+}y6|(81helS}Q~&VB37qyV|S3Y=><^1wh%msQM?fz z<58MX(=|PSUKCF#)dbhR%D&xgCD?$aR0qen+wpp6 zst}vX18!Be96TD??j1HsHTUx(a&@F?=gT`Q$oJFFyrh^;zgz!(NlAHGn0cJy@us=w zNhC#l5G;H}+>49Nsh12=ZPO2r*2OBQe5kpb&1?*PIBFitK8}FUfb~S-#hKfF0o#&d z#3aPkB$9scYku&kA6{0xHnBV#&Wei5J>5T-XX-gUXEPo+9b7WL=*XESc(3BshL`aj zXp}QIp*40}oWJt*l043e8_5;H5PI5c)U&IEw5dF(4zjX0y_lk9 zAp@!mK>WUqHo)-jop=DoK>&no>kAD=^qIE7qis&_*4~ z6q^EF$D@R~3_xseCG>Ikb6Gfofb$g|75PPyyZN&tiRxqovo_k zO|HA|sgy#B<32gyU9x^&)H$1jvw@qp+1b(eGAb)O%O!&pyX@^nQd^9BQ4{(F8<}|A zhF&)xusQhtoXOOhic=8#Xtt5&slLia3c*a?dIeczyTbC#>FTfiLST57nc3@Y#v_Eg#VUv zT8cKH#f3=1PNj!Oroz_MAR*pow%Y0*6YCYmUy^7`^r|j23Q~^*TW#cU7CHf0eAD_0 zEWEVddxFgQ7=!nEBQ|ibaScslvhuUk^*%b#QUNrEB{3PG@uTxNwW}Bs4$nS9wc(~O zG7Iq>aMsYkcr!9#A;HNsJrwTDYkK8ikdj{M;N$sN6BqJ<8~z>T20{J8Z2rRUuH7~3 z=tgS`AgxbBOMg87UT4Lwge`*Y=01Dvk>)^{Iu+n6fuVX4%}>?3czOGR$0 zpp*wp>bsFFSV`V;r_m+TZns$ZprIi`OUMhe^cLE$2O+pP3nP!YB$ry}2THx2QJs3< za1;>d-AggCarrQ>&Z!d@;mW+!q6eXhb&`GbzUDSxpl8AJ#Cm#tuc)_xh(2NV=5XMs zrf_ozRYO$NkC=pKFX5OH8v1>0i9Z$ec`~Mf+_jQ68spn(CJwclDhEEkH2Qw;${J$clv__nUjn5jA0wCLEnu1j;v!0vB>Ri6m9`;R{JMS%^)4FC zU0Z44+u$I$w=Bj|iu4DT5h~sS`C*zbmX?@-crY}E+hy>}2~C0Nn(EKk@5^qO4@l@! z6O0lr%tzGC`D^)8xU3FnMZVm0kX1sBWhaQyzVoXFWwr%Ny?=2M{5s#5i7fTu3gEkG zc{(Pr$v=;`Y#&`y*J}#M9ux>0?xu!`$9cUKm#Bdd_&S#LPTS?ZPV6zN6>W6JTS~-LfjL{mB=b(KMk3 z2HjBSlJeyUVqDd=Mt!=hpYsvby2GL&3~zm;0{^nZJq+4vb?5HH4wufvr}IX42sHeK zm@x?HN$8TsTavXs)tLDFJtY9b)y~Tl@7z4^I8oUQq4JckH@~CVQ;FoK(+e0XAM>1O z(ei}h?)JQp>)d=6ng-BZF1Z5hsAKW@mXq+hU?r8I(*%`tnIIOXw7V6ZK(T9RFJJe@ zZS!aC+p)Gf2Ujc=a6hx4!A1Th%YH!Lb^xpI!Eu` zmJO{9rw){B1Ql18d%F%da+Tbu1()?o(zT7StYqK6_w`e+fjXq5L^y(0 z09QA6H4oFj59c2wR~{~>jUoDzDdKz}5#onYPJRwa`SUO)Pd4)?(ENBaFVLJr6Kvz= zhTtXqbx09C1z~~iZt;g^9_2nCZ{};-b4dQJbv8HsWHXPVg^@(*!@xycp#R?a|L!+` zY5w))JWV`Gls(=}shH0#r*;~>_+-P5Qc978+QUd>J%`fyn{*TsiG-dWMiJXNgwBaT zJ=wgYFt+1ACW)XwtNx)Q9tA2LPoB&DkL16P)ERWQlY4%Y`-5aM9mZ{eKPUgI!~J3Z zkMd5A_p&v?V-o-6TUa8BndiX?ooviev(DKw=*bBVOW|=zps9=Yl|-R5@yJe*BPzN}a0mUsLn{4LfjB_oxpv(mwq# zSY*%E{iB)sNvWfzg-B!R!|+x(Q|b@>{-~cFvdDHA{F2sFGA5QGiIWy#3?P2JIpPKg6ncI^)dvqe`_|N=8j?q^^Y^VFp)SH8qbSJ)2BQ2giqr}t zFG7D6)c?v~^Z#E_K}1nTQbJ9gQ9<%vVRAxVj)8FwL5_iTdUB>&m3fhE=kRWl;g`&m z!W5kh{WsV%fO*%je&j+Lv4xxK~zsEYQls$Q-p&dwID|A)!7uWtJF-=Tm1{V@#x*+kUI$=%KUuf2ka zjiZ{oiL1MXE2EjciJM!jrjFNwCh`~hL>iemrqwqnX?T*MX;U>>8yRcZb{Oy+VKZos zLiFKYPw=LcaaQt8tj=eoo3-@bG_342HQ%?jpgAE?KCLEHC+DmjxAfJ%Og^$dpC8Xw zAcp-)tfJm}BPNq_+6m4gBgBm3+CvmL>4|$2N$^Bz7W(}fz1?U-u;nE`+9`KCLuqg} zwNstNM!J4Uw|78&Y9~9>MLf56to!@qGkJw5Thx%zkzj%Ek9Nn1QA@8NBXbwyWC>9H z#EPwjMNYPigE>*Ofz)HfTF&%PFj$U6mCe-AFw$U%-L?~-+nSXHHKkdgC5KJRTF}`G zE_HNdrE}S0zf4j{r_f-V2imSqW?}3w-4=f@o@-q+cZgaAbZ((hn))@|eWWhcT2pLpTpL!;_5*vM=sRL8 zqU##{U#lJKuyqW^X$ETU5ETeEVzhU|1m1750#f}38_5N9)B_2|v@1hUu=Kt7-@dhA zq_`OMgW01n`%1dB*}C)qxC8q;?zPeF_r;>}%JYmlER_1CUbKa07+=TV45~symC*g8 zW-8(gag#cAOuM0B1xG8eTp5HGVLE}+gYTmK=`XVVV*U!>H`~j4+ROIQ+NkN$LY>h4 zqpwdeE_@AX@PL};e5vTn`Ro(EjHVf$;^oiA%@IBQq>R7_D>m2D4OwwEepkg}R_k*M zM-o;+P27087eb+%*+6vWFCo9UEGw>t&WI17Pe7QVuoAoGHdJ(TEQNlJOqnjZ8adCb zI`}op16D@v7UOEo%8E-~m?c8FL1utPYlg@m$q@q7%mQ4?OK1h%ODjTjFvqd!C z-PI?8qX8{a@6d&Lb_X+hKxCImb*3GFemm?W_du5_&EqRq!+H?5#xiX#w$eLti-?E$;Dhu`{R(o>LzM4CjO>ICf z&DMfES#FW7npnbcuqREgjPQM#gs6h>`av_oEWwOJZ2i2|D|0~pYd#WazE2Bbsa}X@ zu;(9fi~%!VcjK6)?_wMAW-YXJAR{QHxrD5g(ou9mR6LPSA4BRG1QSZT6A?kelP_g- zH(JQjLc!`H4N=oLw=f3{+WmPA*s8QEeEUf6Vg}@!xwnsnR0bl~^2GSa5vb!Yl&4!> zWb|KQUsC$lT=3A|7vM9+d;mq=@L%uWKwXiO9}a~gP4s_4Yohc!fKEgV7WbVo>2ITbE*i`a|V!^p@~^<={#?Gz57 zyPWeM2@p>D*FW#W5Q`1`#5NW62XduP1XNO(bhg&cX`-LYZa|m-**bu|>}S;3)eP8_ zpNTnTfm8 ze+7wDH3KJ95p)5tlwk`S7mbD`SqHnYD*6`;gpp8VdHDz%RR_~I_Ar>5)vE-Pgu7^Y z|9Px+>pi3!DV%E%4N;ii0U3VBd2ZJNUY1YC^-e+{DYq+l@cGtmu(H#Oh%ibUBOd?C z{y5jW3v=0eV0r@qMLgv1JjZC|cZ9l9Q)k1lLgm))UR@#FrJd>w^`+iy$c9F@ic-|q zVHe@S2UAnc5VY_U4253QJxm&Ip!XKP8WNcnx9^cQ;KH6PlW8%pSihSH2(@{2m_o+m zr((MvBja2ctg0d0&U5XTD;5?d?h%JcRJp{_1BQW1xu&BrA3(a4Fh9hon-ly$pyeHq zG&;6q?m%NJ36K1Sq_=fdP(4f{Hop;_G_(i?sPzvB zDM}>*(uOsY0I1j^{$yn3#U(;B*g4cy$-1DTOkh3P!LQ;lJlP%jY8}Nya=h8$XD~%Y zbV&HJ%eCD9nui-0cw!+n`V~p6VCRqh5fRX z8`GbdZ@73r7~myQLBW%db;+BI?c-a>Y)m-FW~M=1^|<21_Sh9RT3iGbO{o-hpN%d6 z7%++#WekoBOP^d0$$|5npPe>u3PLvX_gjH2x(?{&z{jJ2tAOWTznPxv-pAv<*V7r$ z6&glt>7CAClWz6FEi3bToz-soY^{ScrjwVPV51=>n->c(NJngMj6TyHty`bfkF1hc zkJS%A@cL~QV0-aK4>Id!9dh7>0IV;1J9(myDO+gv76L3NLMUm9XyPauvNu$S<)-|F zZS}(kK_WnB)Cl`U?jsdYfAV4nrgzIF@+%1U8$poW&h^c6>kCx3;||fS1_7JvQT~CV zQ8Js+!p)3oW>Df(-}uqC`Tcd%E7GdJ0p}kYj5j8NKMp(KUs9u7?jQ94C)}0rba($~ zqyBx$(1ae^HEDG`Zc@-rXk1cqc7v0wibOR4qpgRDt#>-*8N3P;uKV0CgJE2SP>#8h z=+;i_CGlv+B^+$5a}SicVaSeaNn29K`C&=}`=#Nj&WJP9Xhz4mVa<+yP6hkrq1vo= z1rX4qg8dc4pmEvq%NAkpMK>mf2g?tg_1k2%v}<3`$6~Wlq@ItJ*PhHPoEh1Yi>v57 z4k0JMO)*=S`tKvR5gb-(VTEo>5Y>DZJZzgR+j6{Y`kd|jCVrg!>2hVjz({kZR z`dLlKhoqT!aI8=S+fVp(5*Dn6RrbpyO~0+?fy;bm$0jmTN|t5i6rxqr4=O}dY+ROd zo9Et|x}!u*xi~>-y>!M^+f&jc;IAsGiM_^}+4|pHRn{LThFFpD{bZ|TA*wcGm}XV^ zr*C6~@^5X-*R%FrHIgo-hJTBcyQ|3QEj+cSqp#>&t`ZzB?cXM6S(lRQw$I2?m5=wd z78ki`R?%;o%VUhXH?Z#(uwAn9$m`npJ=cA+lHGk@T7qq_M6Zoy1Lm9E0UUysN)I_x zW__OAqvku^>`J&CB=ie@yNWsaFmem}#L3T(x?a`oZ+$;3O-icj2(5z72Hnj=9Z0w% z<2#q-R=>hig*(t0^v)eGq2DHC%GymE-_j1WwBVGoU=GORGjtaqr0BNigOCqyt;O(S zKG+DoBsZU~okF<7ahjS}bzwXxbAxFfQAk&O@>LsZMsZ`?N?|CDWM(vOm%B3CBPC3o z%2t@%H$fwur}SSnckUm0-k)mOtht`?nwsDz=2#v=RBPGg39i#%odKq{K^;bTD!6A9 zskz$}t)sU^=a#jLZP@I=bPo?f-L}wpMs{Tc!m7-bi!Ldqj3EA~V;4(dltJmTXqH0r z%HAWKGutEc9vOo3P6Q;JdC^YTnby->VZ6&X8f{obffZ??1(cm&L2h7q)*w**+sE6dG*;(H|_Q!WxU{g)CeoT z(KY&bv!Usc|m+Fqfmk;h&RNF|LWuNZ!+DdX*L=s-=_iH=@i` z?Z+Okq^cFO4}_n|G*!)Wl_i%qiMBaH8(WuXtgI7EO=M>=i_+;MDjf3aY~6S9w0K zUuDO7O5Ta6+k40~xh~)D{=L&?Y0?c$s9cw*Ufe18)zzk%#ZY>Tr^|e%8KPb0ht`b( zuP@8#Ox@nQIqz9}AbW0RzE`Cf>39bOWz5N3qzS}ocxI=o$W|(nD~@EhW13Rj5nAp; zu2obEJa=kGC*#3=MkdkWy_%RKcN=?g$7!AZ8vBYKr$ePY(8aIQ&yRPlQ=mudv#q$q z4%WzAx=B{i)UdLFx4os?rZp6poShD7Vc&mSD@RdBJ=_m^&OlkEE1DFU@csgKcBifJ zz4N7+XEJhYzzO=86 z#%eBQZ$Nsf2+X0XPHUNmg#(sNt^NW1Y0|M(${e<0kW6f2q5M!2YE|hSEQ*X-%qo(V zHaFwyGZ0on=I{=fhe<=zo{=Og-_(to3?cvL4m6PymtNsdDINsBh8m>a%!5o3s(en) z=1I z6O+YNertC|OFNqd6P=$gMyvmfa`w~p9*gKDESFqNBy(~Zw3TFDYh}$iudn)9HxPBi zdokK@o~nu?%imcURr5Y~?6oo_JBe}t|pU5qjai|#JDyG=i^V~7+a{dEnO<(y>ahND#_X_fcEBNiZ)uc&%1HVtx8Ts z*H_Btvx^IhkfOB#{szN*n6;y05A>3eARDXslaE>tnLa>+`V&cgho?ED+&vv5KJszf zG4@G;7i;4_bVvZ>!mli3j7~tPgybF5|J6=Lt`u$D%X0l}#iY9nOXH@(%FFJLtzb%p zzHfABnSs;v-9(&nzbZytLiqqDIWzn>JQDk#JULcE5CyPq_m#4QV!}3421haQ+LcfO*>r;rg6K|r#5Sh|y@h1ao%Cl)t*u`4 zMTP!deC?aL7uTxm5^nUv#q2vS-5QbBKP|drbDXS%erB>fYM84Kpk^au99-BQBZR z7CDynflrIAi&ahza+kUryju5LR_}-Z27g)jqOc(!Lx9y)e z{cYc&_r947s9pteaa4}dc|!$$N9+M38sUr7h(%@Ehq`4HJtTpA>B8CLNO__@%(F5d z`SmX5jbux6i#qc}xOhumzbAELh*Mfr2SW99=WNOZRZgoCU4A2|4i|ZVFQt6qEhH#B zK_9G;&h*LO6tB`5dXRSBF0hq0tk{2q__aCKXYkP#9n^)@cq}`&Lo)1KM{W+>5mSed zKp~=}$p7>~nK@va`vN{mYzWN1(tE=u2BZhga5(VtPKk(*TvE&zmn5vSbjo zZLVobTl%;t@6;4SsZ>5+U-XEGUZGG;+~|V(pE&qqrp_f~{_1h@5ZrNETqe{bt9ioZ z#Qn~gWCH!t#Ha^n&fT2?{`}D@s4?9kXj;E;lWV9Zw8_4yM0Qg-6YSsKgvQ*fF{#Pq z{=(nyV>#*`RloBVCs;Lp*R1PBIQOY=EK4CQa*BD0MsYcg=opP?8;xYQDSAJBeJpw5 zPBc_Ft9?;<0?pBhCmOtWU*pN*;CkjJ_}qVic`}V@$TwFi15!mF1*m2wVX+>5p%(+R zQ~JUW*zWkalde{90@2v+oVlkxOZFihE&ZJ){c?hX3L2@R7jk*xjYtHi=}qb+4B(XJ z$gYcNudR~4Kz_WRq8eS((>ALWCO)&R-MXE+YxDn9V#X{_H@j616<|P(8h(7z?q*r+ zmpqR#7+g$cT@e&(%_|ipI&A%9+47%30TLY(yuf&*knx1wNx|%*H^;YB%ftt%5>QM= z^i;*6_KTSRzQm%qz*>cK&EISvF^ovbS4|R%)zKhTH_2K>jP3mBGn5{95&G9^a#4|K zv+!>fIsR8z{^x4)FIr*cYT@Q4Z{y}};rLHL+atCgHbfX*;+k&37DIgENn&=k(*lKD zG;uL-KAdLn*JQ?@r6Q!0V$xXP=J2i~;_+i3|F;_En;oAMG|I-RX#FwnmU&G}w`7R{ z788CrR-g1DW4h_`&$Z`ctN~{A)Hv_-Bl!%+pfif8wN32rMD zJDs$eVWBYQx1&2sCdB0!vU5~uf)=vy*{}t{2VBpcz<+~h0wb7F3?V^44*&83Z2#F` z32!rd4>uc63rQP$3lTH3zb-47IGR}f)8kZ4JvX#toIpXH`L%NnPDE~$QI1)0)|HS4 zVcITo$$oWWwCN@E-5h>N?Hua!N9CYb6f8vTFd>h3q5Jg-lCI6y%vu{Z_Uf z$MU{{^o~;nD_@m2|E{J)q;|BK7rx%`m``+OqZAqAVj-Dy+pD4-S3xK?($>wn5bi90CFAQ+ACd;&m6DQB8_o zjAq^=eUYc1o{#+p+ zn;K<)Pn*4u742P!;H^E3^Qu%2dM{2slouc$AN_3V^M7H_KY3H)#n7qd5_p~Za7zAj|s9{l)RdbV9e||_67`#Tu*c<8!I=zb@ z(MSvQ9;Wrkq6d)!9afh+G`!f$Ip!F<4ADdc*OY-y7BZMsau%y?EN6*hW4mOF%Q~bw z2==Z3^~?q<1GTeS>xGN-?CHZ7a#M4kDL zQxQr~1ZMzCSKFK5+32C%+C1kE#(2L=15AR!er7GKbp?Xd1qkkGipx5Q~FI-6zt< z*PTpeVI)Ngnnyaz5noIIgNZtb4bQdKG{Bs~&tf)?nM$a;7>r36djllw%hQxeCXeW^ z(i6@TEIuxD<2ulwLTt|&gZP%Ei+l!(%p5Yij6U(H#HMkqM8U$@OKB|5@vUiuY^d6X zW}fP3;Kps6051OEO(|JzmVU6SX(8q>*yf*x5QoxDK={PH^F?!VCzES_Qs>()_y|jg6LJlJWp;L zKM*g5DK7>W_*uv}{0WUB0>MHZ#oJZmO!b3MjEc}VhsLD~;E-qNNd?x7Q6~v zR=0$u>Zc2Xr}>x_5$-s#l!oz6I>W?lw;m9Ae{Tf9eMX;TI-Wf_mZ6sVrMnY#F}cDd z%CV*}fDsXUF7Vbw>PuDaGhu631+3|{xp<@Kl|%WxU+vuLlcrklMC!Aq+7n~I3cmQ! z`e3cA!XUEGdEPSu``&lZEKD1IKO(-VGvcnSc153m(i!8ohi`)N2n>U_BemYJ`uY>8B*Epj!oXRLV}XK}>D*^DHQ7?NY*&LJ9VSo`Ogi9J zGa;clWI8vIQqkngv2>xKd91K>?0`Sw;E&TMg&6dcd20|FcTsnUT7Yn{oI5V4@Ow~m zz#k~8TM!A9L7T!|colrC0P2WKZW7PNj_X4MfESbt<-soq*0LzShZ}fyUx!(xIIDwx zRHt^_GAWe0-Vm~bDZ(}XG%E+`XhKpPlMBo*5q_z$BGxYef8O!ToS8aT8pmjbPq)nV z%x*PF5ZuSHRJqJ!`5<4xC*xb2vC?7u1iljB_*iUGl6+yPyjn?F?GOF2_KW&gOkJ?w z3e^qc-te;zez`H$rsUCE0<@7PKGW?7sT1SPYWId|FJ8H`uEdNu4YJjre`8F*D}6Wh z|FQ`xf7yiphHIAkU&OYCn}w^ilY@o4larl?^M7&8YI;hzBIsX|i3UrLsx{QDKwCX< zy;a>yjfJ6!sz`NcVi+a!Fqk^VE^{6G53L?@Tif|j!3QZ0fk9QeUq8CWI;OmO-Hs+F zuZ4sHLA3{}LR2Qlyo+{d@?;`tpp6YB^BMoJt?&MHFY!JQwoa0nTSD+#Ku^4b{5SZVFwU9<~APYbaLO zu~Z)nS#dxI-5lmS-Bnw!(u15by(80LlC@|ynj{TzW)XcspC*}z0~8VRZq>#Z49G`I zgl|C#H&=}n-ajxfo{=pxPV(L*7g}gHET9b*s=cGV7VFa<;Htgjk>KyW@S!|z`lR1( zGSYkEl&@-bZ*d2WQ~hw3NpP=YNHF^XC{TMG$Gn+{b6pZn+5=<()>C!N^jncl0w6BJ zdHdnmSEGK5BlMeZD!v4t5m7ct7{k~$1Ie3GLFoHjAH*b?++s<|=yTF+^I&jT#zuMx z)MLhU+;LFk8bse|_{j+d*a=&cm2}M?*arjBPnfPgLwv)86D$6L zLJ0wPul7IenMvVAK$z^q5<^!)7aI|<&GGEbOr=E;UmGOIa}yO~EIr5xWU_(ol$&fa zR5E(2vB?S3EvJglTXdU#@qfDbCYs#82Yo^aZN6`{Ex#M)easBTe_J8utXu(fY1j|R z9o(sQbj$bKU{IjyhosYahY{63>}$9_+hWxB3j}VQkJ@2$D@vpeRSldU?&7I;qd2MF zSYmJ>zA(@N_iK}m*AMPIJG#Y&1KR)6`LJ83qg~`Do3v^B0>fU&wUx(qefuTgzFED{sJ65!iw{F2}1fQ3= ziFIP{kezQxmlx-!yo+sC4PEtG#K=5VM9YIN0z9~c4XTX?*4e@m;hFM!zVo>A`#566 z>f&3g94lJ{r)QJ5m7Xe3SLau_lOpL;A($wsjHR`;xTXgIiZ#o&vt~ zGR6KdU$FFbLfZCC3AEu$b`tj!9XgOGLSV=QPIYW zjI!hSP#?8pn0@ezuenOzoka8!8~jXTbiJ6+ZuItsWW03uzASFyn*zV2kIgPFR$Yzm zE<$cZlF>R8?Nr2_i?KiripBc+TGgJvG@vRTY2o?(_Di}D30!k&CT`>+7ry2!!iC*X z<@=U0_C#16=PN7bB39w+zPwDOHX}h20Ap);dx}kjXX0-QkRk=cr};GYsjSvyLZa-t zzHONWddi*)RDUH@RTAsGB_#&O+QJaaL+H<<9LLSE+nB@eGF1fALwjVOl8X_sdOYme z0lk!X=S(@25=TZHR7LlPp}fY~yNeThMIjD}pd9+q=j<_inh0$>mIzWVY+Z9p<{D^#0Xk+b_@eNSiR8;KzSZ#7lUsk~NGMcB8C2c=m2l5paHPq`q{S(kdA7Z1a zyfk2Y;w?^t`?@yC5Pz9&pzo}Hc#}mLgDmhKV|PJ3lKOY(Km@Fi2AV~CuET*YfUi}u zfInZnqDX(<#vaS<^fszuR=l)AbqG{}9{rnyx?PbZz3Pyu!eSJK`uwkJU!ORQXy4x83r!PNgOyD33}}L=>xX_93l6njNTuqL8J{l%*3FVn3MG4&Fv*`lBXZ z?=;kn6HTT^#SrPX-N)4EZiIZI!0ByXTWy;;J-Tht{jq1mjh`DSy7yGjHxIaY%*sTx zuy9#9CqE#qi>1misx=KRWm=qx4rk|}vd+LMY3M`ow8)}m$3Ggv&)Ri*ON+}<^P%T5 z_7JPVPfdM=Pv-oH<tecoE}(0O7|YZc*d8`Uv_M*3Rzv7$yZnJE6N_W=AQ3_BgU_TjA_T?a)U1csCmJ&YqMp-lJe`y6>N zt++Bi;ZMOD%%1c&-Q;bKsYg!SmS^#J@8UFY|G3!rtyaTFb!5@e(@l?1t(87ln8rG? z--$1)YC~vWnXiW3GXm`FNSyzu!m$qT=Eldf$sMl#PEfGmzQs^oUd=GIQfj(X=}dw+ zT*oa0*oS%@cLgvB&PKIQ=Ok?>x#c#dC#sQifgMwtAG^l3D9nIg(Zqi;D%807TtUUCL3_;kjyte#cAg?S%e4S2W>9^A(uy8Ss0Tc++ZTjJw1 z&Em2g!3lo@LlDyri(P^I8BPpn$RE7n*q9Q-c^>rfOMM6Pd5671I=ZBjAvpj8oIi$! zl0exNl(>NIiQpX~FRS9UgK|0l#s@#)p4?^?XAz}Gjb1?4Qe4?j&cL$C8u}n)?A@YC zfmbSM`Hl5pQFwv$CQBF=_$Sq zxsV?BHI5bGZTk?B6B&KLdIN-40S426X3j_|ceLla*M3}3gx3(_7MVY1++4mzhH#7# zD>2gTHy*%i$~}mqc#gK83288SKp@y3wz1L_e8fF$Rb}ex+`(h)j}%~Ld^3DUZkgez zOUNy^%>>HHE|-y$V@B}-M|_{h!vXpk01xaD%{l{oQ|~+^>rR*rv9iQen5t?{BHg|% zR`;S|KtUb!X<22RTBA4AAUM6#M?=w5VY-hEV)b`!y1^mPNEoy2K)a>OyA?Q~Q*&(O zRzQI~y_W=IPi?-OJX*&&8dvY0zWM2%yXdFI!D-n@6FsG)pEYdJbuA`g4yy;qrgR?G z8Mj7gv1oiWq)+_$GqqQ$(ZM@#|0j7})=#$S&hZwdoijFI4aCFLVI3tMH5fLreZ;KD zqA`)0l~D2tuIBYOy+LGw&hJ5OyE+@cnZ0L5+;yo2pIMdt@4$r^5Y!x7nHs{@>|W(MzJjATyWGNwZ^4j+EPU0RpAl-oTM@u{lx*i0^yyWPfHt6QwPvYpk9xFMWfBFt!+Gu6TlAmr zeQ#PX71vzN*_-xh&__N`IXv6`>CgV#eA_%e@7wjgkj8jlKzO~Ic6g$cT`^W{R{606 zCDP~+NVZ6DMO$jhL~#+!g*$T!XW63#(ngDn#Qwy71yj^gazS{e;3jGRM0HedGD@pt z?(ln3pCUA(ekqAvvnKy0G@?-|-dh=eS%4Civ&c}s%wF@0K5Bltaq^2Os1n6Z3%?-Q zAlC4goQ&vK6TpgtzkHVt*1!tBYt-`|5HLV1V7*#45Vb+GACuU+QB&hZ=N_flPy0TY zR^HIrdskB#<$aU;HY(K{a3(OQa$0<9qH(oa)lg@Uf>M5g2W0U5 zk!JSlhrw8quBx9A>RJ6}=;W&wt@2E$7J=9SVHsdC?K(L(KACb#z)@C$xXD8^!7|uv zZh$6fkq)aoD}^79VqdJ!Nz-8$IrU(_-&^cHBI;4 z^$B+1aPe|LG)C55LjP;jab{dTf$0~xbXS9!!QdcmDYLbL^jvxu2y*qnx2%jbL%rB z{aP85qBJe#(&O~Prk%IJARcdEypZ)vah%ZZ%;Zk{eW(U)Bx7VlzgOi8)x z`rh4l`@l_Ada7z&yUK>ZF;i6YLGwI*Sg#Fk#Qr0Jg&VLax(nNN$u-XJ5=MsP3|(lEdIOJ7|(x3iY;ea)5#BW*mDV%^=8qOeYO&gIdJVuLLN3cFaN=xZtFB=b zH{l)PZl_j^u+qx@89}gAQW7ofb+k)QwX=aegihossZq*+@PlCpb$rpp>Cbk9UJO<~ zDjlXQ_Ig#W0zdD3&*ei(FwlN#3b%FSR%&M^ywF@Fr>d~do@-kIS$e%wkIVfJ|Ohh=zc zF&Rnic^|>@R%v?@jO}a9;nY3Qrg_!xC=ZWUcYiA5R+|2nsM*$+c$TOs6pm!}Z}dfM zGeBhMGWw3$6KZXav^>YNA=r6Es>p<6HRYcZY)z{>yasbC81A*G-le8~QoV;rtKnkx z;+os8BvEe?0A6W*a#dOudsv3aWs?d% z0oNngyVMjavLjtjiG`!007#?62ClTqqU$@kIY`=x^$2e>iqIy1>o|@Tw@)P)B8_1$r#6>DB_5 zmaOaoE~^9TolgDgooKFuEFB#klSF%9-~d2~_|kQ0Y{Ek=HH5yq9s zDq#1S551c`kSiWPZbweN^A4kWiP#Qg6er1}HcKv{fxb1*BULboD0fwfaNM_<55>qM zETZ8TJDO4V)=aPp_eQjX%||Ud<>wkIzvDlpNjqW>I}W!-j7M^TNe5JIFh#-}zAV!$ICOju8Kx)N z0vLtzDdy*rQN!7r>Xz7rLw8J-(GzQlYYVH$WK#F`i_i^qVlzTNAh>gBWKV@XC$T-` z3|kj#iCquDhiO7NKum07i|<-NuVsX}Q}mIP$jBJDMfUiaWR3c|F_kWBMw0_Sr|6h4 zk`_r5=0&rCR^*tOy$A8K;@|NqwncjZ>Y-75vlpxq%Cl3EgH`}^^~=u zoll6xxY@a>0f%Ddpi;=cY}fyG!K2N-dEyXXmUP5u){4VnyS^T4?pjN@Ot4zjL(Puw z_U#wMH2Z#8Pts{olG5Dy0tZj;N@;fHheu>YKYQU=4Bk|wcD9MbA`3O4bj$hNRHwzb zSLcG0SLV%zywdbuwl(^E_!@&)TdXge4O{MRWk2RKOt@!8E{$BU-AH(@4{gxs=YAz9LIob|Hzto0}9cWoz6Tp2x0&xi#$ zHh$dwO&UCR1Ob2w00-2eG7d4=cN(Y>0R#$q8?||q@iTi+7-w-xR%uMr&StFIthC<# zvK(aPduwuNB}oJUV8+Zl)%cnfsHI%4`;x6XW^UF^e4s3Z@S<&EV8?56Wya;HNs0E> z`$0dgRdiUz9RO9Au3RmYq>K#G=X%*_dUbSJHP`lSfBaN8t-~@F>)BL1RT*9I851A3 z<-+Gb#_QRX>~av#Ni<#zLswtu-c6{jGHR>wflhKLzC4P@b%8&~u)fosoNjk4r#GvC zlU#UU9&0Hv;d%g72Wq?Ym<&&vtA3AB##L}=ZjiTR4hh7J)e>ei} zt*u+>h%MwN`%3}b4wYpV=QwbY!jwfIj#{me)TDOG`?tI!%l=AwL2G@9I~}?_dA5g6 zCKgK(;6Q0&P&K21Tx~k=o6jwV{dI_G+Ba*Zts|Tl6q1zeC?iYJTb{hel*x>^wb|2RkHkU$!+S4OU4ZOKPZjV>9OVsqNnv5jK8TRAE$A&^yRwK zj-MJ3Pl?)KA~fq#*K~W0l4$0=8GRx^9+?w z!QT8*-)w|S^B0)ZeY5gZPI2G(QtQf?DjuK(s^$rMA!C%P22vynZY4SuOE=wX2f8$R z)A}mzJi4WJnZ`!bHG1=$lwaxm!GOnRbR15F$nRC-M*H<*VfF|pQw(;tbSfp({>9^5 zw_M1-SJ9eGF~m(0dvp*P8uaA0Yw+EkP-SWqu zqal$hK8SmM7#Mrs0@OD+%_J%H*bMyZiWAZdsIBj#lkZ!l2c&IpLu(5^T0Ge5PHzR} zn;TXs$+IQ_&;O~u=Jz+XE0wbOy`=6>m9JVG} zJ~Kp1e5m?K3x@@>!D)piw^eMIHjD4RebtR`|IlckplP1;r21wTi8v((KqNqn%2CB< zifaQc&T}*M&0i|LW^LgdjIaX|o~I$`owHolRqeH_CFrqCUCleN130&vH}dK|^kC>) z-r2P~mApHotL4dRX$25lIcRh_*kJaxi^%ZN5-GAAMOxfB!6flLPY-p&QzL9TE%ho( zRwftE3sy5<*^)qYzKkL|rE>n@hyr;xPqncY6QJ8125!MWr`UCWuC~A#G1AqF1@V$kv>@NBvN&2ygy*{QvxolkRRb%Ui zsmKROR%{*g*WjUUod@@cS^4eF^}yQ1>;WlGwOli z+Y$(8I`0(^d|w>{eaf!_BBM;NpCoeem2>J}82*!em=}}ymoXk>QEfJ>G(3LNA2-46 z5PGvjr)Xh9>aSe>vEzM*>xp{tJyZox1ZRl}QjcvX2TEgNc^(_-hir@Es>NySoa1g^ zFow_twnHdx(j?Q_3q51t3XI7YlJ4_q&(0#)&a+RUy{IcBq?)eaWo*=H2UUVIqtp&lW9JTJiP&u zw8+4vo~_IJXZIJb_U^&=GI1nSD%e;P!c{kZALNCm5c%%oF+I3DrA63_@4)(v4(t~JiddILp7jmoy+>cD~ivwoctFfEL zP*#2Rx?_&bCpX26MBgp^4G>@h`Hxc(lnqyj!*t>9sOBcXN(hTwEDpn^X{x!!gPX?1 z*uM$}cYRwHXuf+gYTB}gDTcw{TXSOUU$S?8BeP&sc!Lc{{pEv}x#ELX>6*ipI1#>8 zKes$bHjiJ1OygZge_ak^Hz#k;=od1wZ=o71ba7oClBMq>Uk6hVq|ePPt)@FM5bW$I z;d2Or@wBjbTyZj|;+iHp%Bo!Vy(X3YM-}lasMItEV_QrP-Kk_J4C>)L&I3Xxj=E?| zsAF(IfVQ4w+dRRnJ>)}o^3_012YYgFWE)5TT=l2657*L8_u1KC>Y-R{7w^S&A^X^U}h20jpS zQsdeaA#WIE*<8KG*oXc~$izYilTc#z{5xhpXmdT-YUnGh9v4c#lrHG6X82F2-t35} zB`jo$HjKe~E*W$=g|j&P>70_cI`GnOQ;Jp*JK#CT zuEGCn{8A@bC)~0%wsEv?O^hSZF*iqjO~_h|>xv>PO+?525Nw2472(yqS>(#R)D7O( zg)Zrj9n9$}=~b00=Wjf?E418qP-@8%MQ%PBiCTX=$B)e5cHFDu$LnOeJ~NC;xmOk# z>z&TbsK>Qzk)!88lNI8fOE2$Uxso^j*1fz>6Ot49y@=po)j4hbTIcVR`ePHpuJSfp zxaD^Dn3X}Na3@<_Pc>a;-|^Pon(>|ytG_+U^8j_JxP=_d>L$Hj?|0lz>_qQ#a|$+( z(x=Lipuc8p4^}1EQhI|TubffZvB~lu$zz9ao%T?%ZLyV5S9}cLeT?c} z>yCN9<04NRi~1oR)CiBakoNhY9BPnv)kw%*iv8vdr&&VgLGIs(-FbJ?d_gfbL2={- zBk4lkdPk~7+jIxd4{M(-W1AC_WcN&Oza@jZoj zaE*9Y;g83#m(OhA!w~LNfUJNUuRz*H-=$s*z+q+;snKPRm9EptejugC-@7-a-}Tz0 z@KHra#Y@OXK+KsaSN9WiGf?&jlZ!V7L||%KHP;SLksMFfjkeIMf<1e~t?!G3{n)H8 zQAlFY#QwfKuj;l@<$YDATAk;%PtD%B(0<|8>rXU< zJ66rkAVW_~Dj!7JGdGGi4NFuE?7ZafdMxIh65Sz7yQoA7fBZCE@WwysB=+`kT^LFX zz8#FlSA5)6FG9(qL3~A24mpzL@@2D#>0J7mMS1T*9UJ zvOq!!a(%IYY69+h45CE?(&v9H4FCr>gK0>mK~F}5RdOuH2{4|}k@5XpsX7+LZo^Qa4sH5`eUj>iffoBVm+ zz4Mtf`h?NW$*q1yr|}E&eNl)J``SZvTf6Qr*&S%tVv_OBpbjnA0&Vz#(;QmGiq-k! zgS0br4I&+^2mgA15*~Cd00cXLYOLA#Ep}_)eED>m+K@JTPr_|lSN}(OzFXQSBc6fM z@f-%2;1@BzhZa*LFV z-LrLmkmB%<<&jEURBEW>soaZ*rSIJNwaV%-RSaCZi4X)qYy^PxZ=oL?6N-5OGOMD2 z;q_JK?zkwQ@b3~ln&sDtT5SpW9a0q+5Gm|fpVY2|zqlNYBR}E5+ahgdj!CvK$Tlk0 z9g$5N;aar=CqMsudQV>yb4l@hN(9Jcc=1(|OHsqH6|g=K-WBd8GxZ`AkT?OO z-z_Ued-??Z*R4~L7jwJ%-`s~FK|qNAJ;EmIVDVpk{Lr7T4l{}vL)|GuUuswe9c5F| zv*5%u01hlv08?00Vpwyk*Q&&fY8k6MjOfpZfKa@F-^6d=Zv|0@&4_544RP5(s|4VPVP-f>%u(J@23BHqo2=zJ#v9g=F!cP((h zpt0|(s++ej?|$;2PE%+kc6JMmJjDW)3BXvBK!h!E`8Y&*7hS{c_Z?4SFP&Y<3evqf z9-ke+bSj$%Pk{CJlJbWwlBg^mEC^@%Ou?o>*|O)rl&`KIbHrjcpqsc$Zqt0^^F-gU2O=BusO+(Op}!jNzLMc zT;0YT%$@ClS%V+6lMTfhuzzxomoat=1H?1$5Ei7&M|gxo`~{UiV5w64Np6xV zVK^nL$)#^tjhCpTQMspXI({TW^U5h&Wi1Jl8g?P1YCV4=%ZYyjSo#5$SX&`r&1PyC zzc;uzCd)VTIih|8eNqFNeBMe#j_FS6rq81b>5?aXg+E#&$m++Gz9<+2)h=K(xtn}F ziV{rmu+Y>A)qvF}ms}4X^Isy!M&1%$E!rTO~5(p+8{U6#hWu>(Ll1}eD64Xa>~73A*538wry?v$vW z>^O#FRdbj(k0Nr&)U`Tl(4PI*%IV~;ZcI2z&rmq=(k^}zGOYZF3b2~Klpzd2eZJl> zB=MOLwI1{$RxQ7Y4e30&yOx?BvAvDkTBvWPpl4V8B7o>4SJn*+h1Ms&fHso%XLN5j z-zEwT%dTefp~)J_C8;Q6i$t!dnlh-!%haR1X_NuYUuP-)`IGWjwzAvp!9@h`kPZhf zwLwFk{m3arCdx8rD~K2`42mIN4}m%OQ|f)4kf%pL?Af5Ul<3M2fv>;nlhEPR8b)u} zIV*2-wyyD%%) zl$G@KrC#cUwoL?YdQyf9WH)@gWB{jd5w4evI& zOFF)p_D8>;3-N1z6mES!OPe>B^<;9xsh)){Cw$Vs-ez5nXS95NOr3s$IU;>VZSzKn zBvub8_J~I%(DozZW@{)Vp37-zevxMRZ8$8iRfwHmYvyjOxIOAF2FUngKj289!(uxY zaClWm!%x&teKmr^ABrvZ(ikx{{I-lEzw5&4t3P0eX%M~>$wG0ZjA4Mb&op+0$#SO_ z--R`>X!aqFu^F|a!{Up-iF(K+alKB{MNMs>e(i@Tpy+7Z-dK%IEjQFO(G+2mOb@BO zP>WHlS#fSQm0et)bG8^ZDScGnh-qRKIFz zfUdnk=m){ej0i(VBd@RLtRq3Ep=>&2zZ2%&vvf?Iex01hx1X!8U+?>ER;yJlR-2q4 z;Y@hzhEC=d+Le%=esE>OQ!Q|E%6yG3V_2*uh&_nguPcZ{q?DNq8h_2ahaP6=pP-+x zK!(ve(yfoYC+n(_+chiJ6N(ZaN+XSZ{|H{TR1J_s8x4jpis-Z-rlRvRK#U%SMJ(`C z?T2 zF(NNfO_&W%2roEC2j#v*(nRgl1X)V-USp-H|CwFNs?n@&vpRcj@W@xCJwR6@T!jt377?XjZ06=`d*MFyTdyvW!`mQm~t3luzYzvh^F zM|V}rO>IlBjZc}9Z zd$&!tthvr>5)m;5;96LWiAV0?t)7suqdh0cZis`^Pyg@?t>Ms~7{nCU;z`Xl+raSr zXpp=W1oHB*98s!Tpw=R5C)O{{Inl>9l7M*kq%#w9a$6N~v?BY2GKOVRkXYCgg*d

<5G2M1WZP5 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 F{|7XC?P&l2 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ee8007b..ca025c8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Fri Dec 04 09:19:29 PST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip diff --git a/gradlew b/gradlew index cccdd3d..23d15a9 100755 --- a/gradlew +++ b/gradlew @@ -1,78 +1,129 @@ -#!/usr/bin/env sh +#!/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. +# +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# 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/HEAD/platforms/jvm/plugins-application/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 -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +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 -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$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="" +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +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 - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +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 +CLASSPATH="\\\"\\\"" + # 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" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -81,92 +132,120 @@ 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. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + 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 fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -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" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + 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 - i=$((i+1)) + # 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 - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$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"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" fi +# 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/gradlew.bat b/gradlew.bat index e95643d..5eed7ee 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,4 +1,22 @@ -@if "%DEBUG%" == "" @echo off +@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 +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -9,25 +27,29 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused 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= +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 init +if %ERRORLEVEL% equ 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. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -35,48 +57,36 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +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. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 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 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index f97477f..c844d19 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -1,39 +1,19 @@ -plugins { - id("com.android.application") - id("kotlin-android") - kotlin("kapt") - id("dagger.hilt.android.plugin") -} +import com.kroger.gradle.config.hiltKsp -kapt { - correctErrorTypes = true +plugins { + alias(libs.plugins.conventions.androidApplication) + alias(libs.plugins.dagger.hilt) + alias(libs.plugins.ksp) } android { - compileSdk = (SdkVersions.compileSdkVersion) - buildToolsVersion = ("30.0.3") namespace = "com.kroger.telemetry.sample" - defaultConfig { - minSdk = (SdkVersions.minSdkVersion) - targetSdk = (SdkVersions.targetSdkVersion) - compileSdk = (SdkVersions.compileSdkVersion) - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - buildTypes { getByName("release") { isMinifyEnabled = false } } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - - kotlinOptions { - jvmTarget = "11" - } buildFeatures { viewBinding = true @@ -48,17 +28,14 @@ dependencies { implementation(project(":android")) implementation(project(":context-aware")) - implementation(libs.androidCoreKtx) - implementation(libs.appCompat) - implementation(libs.contraintLayount) - implementation(libs.coroutines) - implementation(libs.googleMaterial) - implementation(libs.stdLib) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.constrainlayout) + implementation(libs.androidx.coreKtx) + implementation(libs.kotlinx.coroutinesCore) + implementation(libs.android.material) - implementation(libs.hilt) - kapt(libs.hiltAndroidCompiler) - kapt(libs.hiltCompiler) + hiltKsp() - androidTestImplementation(libs.androidJunitTestExt) - androidTestImplementation(libs.textEspresso) + androidTestImplementation(libs.android.test.espressoCore) + androidTestImplementation(libs.androidx.test.ext.junitKtx) } diff --git a/sample/src/main/java/com/kroger/telemetry/sample/MainActivity.kt b/sample/src/main/java/com/kroger/telemetry/sample/MainActivity.kt index fdf09f2..6b756a9 100644 --- a/sample/src/main/java/com/kroger/telemetry/sample/MainActivity.kt +++ b/sample/src/main/java/com/kroger/telemetry/sample/MainActivity.kt @@ -26,6 +26,7 @@ package com.kroger.telemetry.sample import android.os.Bundle import android.util.Log +import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import com.kroger.telemetry.Event import com.kroger.telemetry.Relay @@ -54,6 +55,7 @@ class MainActivity : AppCompatActivity() { lateinit var telemeter: ModuleOneTelemeter override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) setContentView(binding.root) diff --git a/settings.gradle.kts b/settings.gradle.kts index 9c65401..e586986 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,36 +3,20 @@ include(":android") include(":context-aware") include(":firebase") include(":sample") -enableFeaturePreview("VERSION_CATALOGS") rootProject.name = "telemetry" pluginManagement { repositories { mavenCentral() google() - - // Public portal required for ben-manes:version gradlePluginPortal() } - plugins { - id("de.mannodermaus.android-junit5").version("1.8.0.0") - id("org.jetbrains.dokka").version("1.5.31") - id("com.android.application").version("7.3.0") - id("org.jetbrains.kotlin.plugin.serialization").version("1.5.31") - id("com.vanniktech.maven.publish").version("0.24.0") - } +} - resolutionStrategy { - eachPlugin { - when (requested.id.id) { - "dagger.hilt.android.plugin" -> useModule("com.google.dagger:hilt-android-gradle-plugin:2.40.5") - "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31" -> useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31") - } - } - dependencyResolutionManagement { - versionCatalogs { - (files("gradle/libs.versions.toml")) - } - } +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() } } diff --git a/telemetry/build.gradle.kts b/telemetry/build.gradle.kts index 6108010..b8ade36 100644 --- a/telemetry/build.gradle.kts +++ b/telemetry/build.gradle.kts @@ -1,12 +1,21 @@ +import com.kroger.gradle.config.junit5 + plugins { - id(Plugins.javaLibrary.id) - id(Plugins.release.id) + alias(libs.plugins.conventions.publishedKotlinLibrary) +} + +kover { + currentProject { + createVariant("default") { + add("jvm") + } + } } dependencies { - implementation(libs.coroutines) - implementation(libs.stdLib) + implementation(libs.kotlinx.coroutinesCore) - testImplementation(libs.coroutinesTest) + junit5() + testImplementation(libs.kotlinx.coroutinesTest) testImplementation(libs.kotlinTest) } From ed4dd92432d64c4c20831cc0008ed178f743a158 Mon Sep 17 00:00:00 2001 From: Tim Romel Date: Mon, 19 May 2025 15:28:19 -0400 Subject: [PATCH 2/6] test: fix tests after coroutine library update --- .../telemetry/android/relay/ToastRelayTest.kt | 16 +- .../firebase/FirebaseAnalyticsRelayTest.kt | 6 +- .../java/com/kroger/telemetry/RelayTest.kt | 10 +- .../com/kroger/telemetry/TelemeterTest.kt | 171 +++++++++--------- .../kroger/telemetry/relay/PrintRelayTest.kt | 81 +++++---- .../relay/TelemeterLogExtensionsTest.kt | 63 ++++--- 6 files changed, 181 insertions(+), 166 deletions(-) diff --git a/android/src/test/java/com/kroger/telemetry/android/relay/ToastRelayTest.kt b/android/src/test/java/com/kroger/telemetry/android/relay/ToastRelayTest.kt index f9012bb..cba041f 100644 --- a/android/src/test/java/com/kroger/telemetry/android/relay/ToastRelayTest.kt +++ b/android/src/test/java/com/kroger/telemetry/android/relay/ToastRelayTest.kt @@ -29,7 +29,7 @@ import com.kroger.telemetry.Event import com.kroger.telemetry.android.facet.ToastFacet import com.kroger.telemetry.facet.Facet import com.kroger.telemetry.facet.Significance -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue @@ -61,7 +61,7 @@ internal class ToastRelayTest { @Test fun `GIVEN toast relay disabled WHEN event received THEN nothing is toasted`() = - runBlockingTest { + runTest { val config = TestConfig().copy(enabled = false) val relay = config.getRelay() @@ -76,7 +76,7 @@ internal class ToastRelayTest { @Test fun `GIVEN toast relay WHEN event received with toast facet THEN facet is toasted`() = - runBlockingTest { + runTest { val config = TestConfig() val relay = config.getRelay() @@ -91,7 +91,7 @@ internal class ToastRelayTest { @Test fun `GIVEN toast relay configured to toast all WHEN event received no significance THEN event is not toasted`() = - runBlockingTest { + runTest { val config = TestConfig().copy( toastSignificantEvents = true, minimumSignificance = Significance.ERROR, @@ -109,7 +109,7 @@ internal class ToastRelayTest { @Test fun `GIVEN toast relay configured to toast all WHEN event received with lower than minimum significance THEN event is not toasted`() = - runBlockingTest { + runTest { val config = TestConfig().copy( toastSignificantEvents = true, minimumSignificance = Significance.ERROR, @@ -127,7 +127,7 @@ internal class ToastRelayTest { @Test fun `GIVEN toast relay configured to toast all WHEN event received with minimum significance THEN event is toasted`() = - runBlockingTest { + runTest { val config = TestConfig().copy( toastSignificantEvents = true, minimumSignificance = Significance.ERROR, @@ -145,7 +145,7 @@ internal class ToastRelayTest { @Test fun `GIVEN toast relay configured with bad length WHEN toasting THEN uses length short as default`() = - runBlockingTest { + runTest { val toaster = FakeToaster() var lengthUsed = 42 toaster.fakeFunToast = { _, length -> lengthUsed = length } @@ -163,7 +163,7 @@ internal class ToastRelayTest { @Test fun `GIVEN an enabled toast relay WHEN disabled THEN toasts will not be shown`() = - runBlockingTest { + runTest { val config = TestConfig().copy(enabled = true) val relay = config.getRelay() diff --git a/firebase/src/test/java/com/kroger/telemetry/firebase/FirebaseAnalyticsRelayTest.kt b/firebase/src/test/java/com/kroger/telemetry/firebase/FirebaseAnalyticsRelayTest.kt index 19dbea2..bb7923e 100644 --- a/firebase/src/test/java/com/kroger/telemetry/firebase/FirebaseAnalyticsRelayTest.kt +++ b/firebase/src/test/java/com/kroger/telemetry/firebase/FirebaseAnalyticsRelayTest.kt @@ -33,8 +33,7 @@ import io.mockk.just import io.mockk.mockk import io.mockk.runs import io.mockk.verify -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test internal class FirebaseAnalyticsRelayTest { @@ -47,10 +46,9 @@ internal class FirebaseAnalyticsRelayTest { override val eventName: String = fakeName } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun `GIVEN event with firebase facet WHEN recorded THEN name matches in logged event`() = - runBlockingTest { + runTest { every { mockFirebaseAnalytics.logEvent(any(), any()) } just runs firebaseAnalyticsRelay.process( diff --git a/telemetry/src/test/java/com/kroger/telemetry/RelayTest.kt b/telemetry/src/test/java/com/kroger/telemetry/RelayTest.kt index c1d8c90..48189aa 100644 --- a/telemetry/src/test/java/com/kroger/telemetry/RelayTest.kt +++ b/telemetry/src/test/java/com/kroger/telemetry/RelayTest.kt @@ -27,7 +27,7 @@ package com.kroger.telemetry import com.kroger.telemetry.facet.Facet import com.kroger.telemetry.util.FakeEvent import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test @@ -36,7 +36,7 @@ import org.junit.jupiter.api.Test internal class RelayTest { @Test fun `GIVEN relay with filter in processing WHEN event processed THEN facets easily extracted`() = - runBlockingTest { + runTest { data class StringFacet(val value: String) : Facet data class IntFacet(val value: Int) : Facet @@ -63,7 +63,7 @@ internal class RelayTest { @Test fun `GIVEN typed relay WHEN event processed with relevant facet type THEN event is processed`() = - runBlockingTest { + runTest { data class StringFacet(val value: String) : Facet data class IntFacet(val value: Int) : Facet @@ -88,7 +88,7 @@ internal class RelayTest { @Test fun `GIVEN typed relay WHEN event processed without relevant facet type THEN event is not processed`() = - runBlockingTest { + runTest { data class StringFacet(val value: String) : Facet data class IntFacet(val value: Int) : Facet @@ -121,7 +121,7 @@ internal class RelayTest { TypedRelay by Relay.buildTypedRelay({ facet -> passed = facet.passed }) val passingFacet = TestFacet(passed = true) - runBlockingTest { + runTest { TestTypedRelay().process(FakeEvent(facets = listOf(passingFacet))) } diff --git a/telemetry/src/test/java/com/kroger/telemetry/TelemeterTest.kt b/telemetry/src/test/java/com/kroger/telemetry/TelemeterTest.kt index 1e2a9e6..91874b9 100644 --- a/telemetry/src/test/java/com/kroger/telemetry/TelemeterTest.kt +++ b/telemetry/src/test/java/com/kroger/telemetry/TelemeterTest.kt @@ -32,29 +32,20 @@ import com.kroger.telemetry.facet.ThreadData import com.kroger.telemetry.facet.UnresolvedFacet import com.kroger.telemetry.util.FakeEvent import com.kroger.telemetry.util.FakeRelay -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlinx.coroutines.test.TestCoroutineScope -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withContext -import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test +import kotlin.time.Duration.Companion.milliseconds -@ExperimentalCoroutinesApi internal class TelemeterTest { - private val scope = TestCoroutineScope() - - @AfterEach - fun teardown() { - scope.cleanupTestCoroutines() - } - @Test - fun `GIVEN telemeter with relays WHEN event recorded THEN relays receive events`() { + fun `GIVEN telemeter with relays WHEN event recorded THEN relays receive events`() = runTest { var relayOneProcessCount = 0 val relayOne = FakeRelay { relayOneProcessCount += 1 } @@ -63,42 +54,43 @@ internal class TelemeterTest { val telemeter = Telemeter.build( relays = listOf(relayOne, relayTwo), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = scope), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), ) val numEvents = 10_000 - for (i in 1..numEvents) { - val event = FakeEvent(description = "event num $i") - telemeter.record(event) + repeat(numEvents) { + telemeter.record(FakeEvent(description = "event num $it")) + testScheduler.runCurrent() } assertTrue(relayOneProcessCount == numEvents && relayTwoProcessCount == numEvents) } @Test - fun `GIVEN faceted telemeter WHEN event recorded THEN facets are added to incoming events`() { - class TelemeterFacet : Facet + fun `GIVEN faceted telemeter WHEN event recorded THEN facets are added to incoming events`() = + runTest { + class TelemeterFacet : Facet - var facetIsPresent = false - val relay = FakeRelay { event -> - facetIsPresent = event.facets.any { facet -> facet is TelemeterFacet } - } - - val telemeter = Telemeter.build( - relays = listOf(relay), - facets = listOf(TelemeterFacet()), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = scope), - ) + var facetIsPresent = false + val relay = FakeRelay { event -> + facetIsPresent = event.facets.any { facet -> facet is TelemeterFacet } + } - val event = FakeEvent() - telemeter.record(event) + val telemeter = Telemeter.build( + relays = listOf(relay), + facets = listOf(TelemeterFacet()), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), + ) - assertTrue(facetIsPresent) - } + val event = FakeEvent() + telemeter.record(event) + testScheduler.runCurrent() + assertTrue(facetIsPresent) + } // Please don't actually do this. In a perfect world, no mutable data would enter the pipeline // and relays would avoid trying to mutate data @Test - fun `GIVEN mutable facet WHEN facet is mutated downstream THEN changes are propagated`() { + fun `GIVEN mutable facet WHEN facet is mutated downstream THEN changes are propagated`() = runTest { class MutableFacet(var mutableField: String) : Facet val mutatedString = "i'm a mutation" @@ -117,18 +109,19 @@ internal class TelemeterTest { val telemeter = Telemeter.build( relays = listOf(mutatingRelay, mutationCheckingRelay), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = scope), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), ) val event = FakeEvent("", listOf(MutableFacet("i'm mutating"))) telemeter.record(event) + testScheduler.runCurrent() assertTrue(facetIsMutated) } @Test fun `GIVEN parent telemeter WHEN child telemeter created THEN child can attach additional facets scoped to child`() = - runBlockingTest { + runTest { class ParentFacet : Facet class ChildFacet : Facet @@ -139,7 +132,7 @@ internal class TelemeterTest { val parentFacet = ParentFacet() val childFacet = ChildFacet() val parent = Telemeter.build( - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = scope), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), relays = listOf(fakeRelay), facets = listOf(parentFacet), ) @@ -148,7 +141,7 @@ internal class TelemeterTest { val event = FakeEvent() parent.record(event) child.record(event) - + testScheduler.runCurrent() assertEquals(1, recordedEvents[0].facets.size) assertEquals(parentFacet, recordedEvents[0].facets[0]) assertEquals(2, recordedEvents[1].facets.size) @@ -157,7 +150,7 @@ internal class TelemeterTest { } @Test - fun `GIVEN telemeter with default flow config WHEN event recorded by relay with long running process function THEN shorter relay processing not blocked`() { + fun `GIVEN telemeter with default flow config WHEN event recorded by relay with long running process function THEN shorter relay processing not blocked`() = runTest { var completedLongRelayJobs = 0 val longRelay = FakeRelay { delay(500) @@ -171,22 +164,24 @@ internal class TelemeterTest { val telemeter = Telemeter.build( relays = listOf(longRelay, shortRelay), facets = listOf(), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = scope), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), ) val numEvents = 10_000 - for (i in 1..numEvents) telemeter.record(FakeEvent()) - + repeat(numEvents) { + telemeter.record(FakeEvent()) + testScheduler.runCurrent() + } assertTrue(completedShortRelayJobs > completedLongRelayJobs) - scope.advanceUntilIdle() assertEquals(numEvents, completedShortRelayJobs) } @Test - fun `GIVEN telemeter with default shared flow config WHEN event recorded by relay with long running process function THEN all events processed`() { + fun `GIVEN telemeter with default shared flow config WHEN event recorded by relay with long running process function THEN all events processed`() = runTest(UnconfinedTestDispatcher()) { + val longRelayDelay = 500.milliseconds var completedLongRelayJobs = 0 val longRelay = FakeRelay { - delay(500) + delay(longRelayDelay) completedLongRelayJobs += 1 } var completedShortRelayJobs = 0 @@ -197,24 +192,25 @@ internal class TelemeterTest { val telemeter = Telemeter.build( relays = listOf(longRelay, shortRelay), facets = listOf(), - flowConfig = Telemeter.defaultSharedFlowConfig.copy(scope = scope), + flowConfig = Telemeter.defaultSharedFlowConfig.copy(scope = backgroundScope), ) val numEvents = 10_000 - for (i in 1..numEvents) telemeter.record(FakeEvent()) - - assertTrue(completedShortRelayJobs > completedLongRelayJobs) - scope.advanceUntilIdle() + repeat(numEvents) { + telemeter.record(FakeEvent()) + } + testScheduler.advanceTimeBy(longRelayDelay * (numEvents + 1)) + assertTrue(completedShortRelayJobs == completedLongRelayJobs) assertEquals(numEvents, completedShortRelayJobs) assertEquals(numEvents, completedLongRelayJobs) } @Test - fun `GIVEN child telemeter with additional relay WHEN event recorded THEN additional relay will receive events`() { + fun `GIVEN child telemeter with additional relay WHEN event recorded THEN additional relay will receive events`() = runTest { val childFacet = object : Facet {} val parent = Telemeter.build( relays = listOf(), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = scope), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), ) var childProcessed = false @@ -228,6 +224,7 @@ internal class TelemeterTest { ) child.record(FakeEvent()) + testScheduler.runCurrent() assertTrue(childProcessed) } @@ -236,7 +233,7 @@ internal class TelemeterTest { * relays. */ @Test - fun `GIVEN parent and child telemeter with identical relay added to both WHEN events recorded THEN processed twice`() { + fun `GIVEN parent and child telemeter with identical relay added to both WHEN events recorded THEN processed twice`() = runTest { var numProcessed = 0 class RepeatedRelay : Relay { @@ -248,16 +245,17 @@ internal class TelemeterTest { val parent = Telemeter.build( relays = listOf(RepeatedRelay()), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = scope), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), ) val child = parent.child(relays = listOf(RepeatedRelay())) child.record(FakeEvent()) + testScheduler.runCurrent() assertTrue(numProcessed == 2) } @Test - fun `GIVEN event to be recorded WHEN passed additional facets THEN facets are relayed`() { + fun `GIVEN event to be recorded WHEN passed additional facets THEN facets are relayed`() = runTest { val recorded = mutableListOf() val fakeRelay = FakeRelay { if (it.facets.any { facet -> facet is Prefix.App }) { @@ -267,7 +265,7 @@ internal class TelemeterTest { val telemeter = Telemeter.build( relays = listOf(fakeRelay), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = scope), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), ) val additionalFacet = Prefix.App("") @@ -277,12 +275,12 @@ internal class TelemeterTest { } telemeter.record(event, listOf(additionalFacet)) - + testScheduler.runCurrent() assertTrue(recorded[0].facets[0] is Prefix.App) } @Test - fun `GIVEN prefixes attached through several children WHEN event recorded THEN prefixes remain in order they were added`() { + fun `GIVEN prefixes attached through several children WHEN event recorded THEN prefixes remain in order they were added`() = runTest { val recorded = mutableListOf() val fakeRelay = FakeRelay { recorded.add(it) @@ -295,7 +293,7 @@ internal class TelemeterTest { .build( relays = listOf(fakeRelay), facets = listOf(firstPrefix), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = scope), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), ) .child(facets = listOf(secondPrefix)) .child(facets = listOf(thirdPrefix)) @@ -306,14 +304,14 @@ internal class TelemeterTest { override val facets: List = listOf() }, ) - + testScheduler.runCurrent() assertEquals(firstPrefix, recorded[0].facets[0]) assertEquals(secondPrefix, recorded[0].facets[1]) assertEquals(thirdPrefix, recorded[0].facets[2]) } @Test - fun `GIVEN computed facet WHEN recorded THEN computation can be run in relay`() { + fun `GIVEN computed facet WHEN recorded THEN computation can be run in relay`() = runTest { var processed = false val relay = object : TypedRelay> { override val type: Class> = Facet.Computed::class.java @@ -324,21 +322,20 @@ internal class TelemeterTest { val telemeter = Telemeter.build( relays = listOf(relay), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = scope), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), ) val computedFacet = object : Facet.Computed { override val compute: () -> Boolean = { true } } - telemeter.record(FakeEvent(facets = listOf(computedFacet))) - + testScheduler.runCurrent() assertTrue(processed) } @Test - fun `GIVEN lazy facet WHEN recorded THEN lazy value will be result of computation `() { + fun `GIVEN lazy facet WHEN recorded THEN lazy value will be result of computation`() = runTest { var computedCount: Int? = null val relay = object : TypedRelay> { override val type: Class> = Facet.Lazy::class.java @@ -349,19 +346,19 @@ internal class TelemeterTest { val telemeter = Telemeter.build( relays = listOf(relay), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = scope), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), ) val lazyFacet = object : Facet.Lazy() { override val compute: () -> Int = { 1 } } telemeter.record(FakeEvent(facets = listOf(lazyFacet))) - + testScheduler.runCurrent() assertEquals(1, computedCount) } @Test - fun `GIVEN telemeter configured to track thread data WHEN event recorded THEN additional facet is included`() { + fun `GIVEN telemeter configured to track thread data WHEN event recorded THEN additional facet is included`() = runTest { val currentThreadName = Thread.currentThread().name val recordedFacets = mutableListOf() val relay = FakeRelay { recordedFacets.addAll(it.facets) } @@ -370,25 +367,27 @@ internal class TelemeterTest { relays = listOf(relay), flowConfig = Telemeter.defaultTelemetryFlowConfig.copy( shouldPropagateThreadData = true, - scope = scope, + scope = backgroundScope, ), ) telemeter.record(FakeEvent()) - + testScheduler.runCurrent() val threadData = recordedFacets[0] as ThreadData assertEquals(currentThreadName, threadData.threadName) assertTrue(threadData.currentStackTrace.isNotEmpty()) } @Test - fun `GIVEN relay that processes on different thread WHEN event recorded with thread data THEN original thread is preserved`() { + fun `GIVEN relay that processes on different thread WHEN event recorded with thread data THEN original thread is preserved`() = runTest { val currentThreadName = Thread.currentThread().name + val dispatcher = StandardTestDispatcher(testScheduler) val recordedFacets = mutableListOf() var processedThread = "" + val relay = FakeRelay { - scope.launch { - withContext(Dispatchers.IO) { + launch { + withContext(dispatcher) { recordedFacets.addAll(it.facets) processedThread = Thread.currentThread().name } @@ -399,12 +398,12 @@ internal class TelemeterTest { relays = listOf(relay), flowConfig = Telemeter.defaultTelemetryFlowConfig.copy( shouldPropagateThreadData = true, - scope = scope, + scope = backgroundScope, ), ) telemeter.record(FakeEvent()) - + testScheduler.runCurrent() while (processedThread.isEmpty()) Unit val threadData = recordedFacets[0] as ThreadData assertEquals(currentThreadName, threadData.threadName) @@ -412,7 +411,7 @@ internal class TelemeterTest { } @Test - fun `GIVEN telemeter tree with more than one node configured to record thread data WHEN event recorded THEN only one thread data recorded`() { + fun `GIVEN telemeter tree with more than one node configured to record thread data WHEN event recorded THEN only one thread data recorded`() = runTest { val recordedFacets = mutableListOf() val relay = FakeRelay { recordedFacets.addAll(it.facets) } @@ -420,18 +419,18 @@ internal class TelemeterTest { relays = listOf(relay), flowConfig = Telemeter.defaultTelemetryFlowConfig.copy( shouldPropagateThreadData = true, - scope = scope, + scope = backgroundScope, ), ).child(listOf()) child.record(FakeEvent()) - + testScheduler.runCurrent() val threadDataFacets = recordedFacets.filterIsInstance() assertEquals(1, threadDataFacets.size) } @Test - fun `GIVEN telemeter with relay WHEN relay throws THEN telemeter catches and records error without looping`() { + fun `GIVEN telemeter with relay WHEN relay throws THEN telemeter catches and records error without looping`() = runTest { val recorded = mutableListOf() val goodRelay = FakeRelay { recorded.add(it) @@ -444,18 +443,18 @@ internal class TelemeterTest { val telemeter = Telemeter.build( relays = listOf(badRelay, goodRelay), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = scope), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), ) telemeter.record(FakeEvent()) - + testScheduler.runCurrent() val failureEvents = recorded.filter { it.facets.any { facet -> facet is Failure } } val failureFacet = failureEvents[0].facets[0] as Failure assertEquals(exception, failureFacet.throwable) } @Test - fun `Given telemeter with facetResolvers, When record is called with unresolved facets, Then they should be resolved`() { + fun `Given telemeter with facetResolvers, When record is called with unresolved facets, Then they should be resolved`() = runTest { val recorded = mutableListOf() val recordingRelay = FakeRelay { recorded.add(it) @@ -483,7 +482,7 @@ internal class TelemeterTest { val telemeter = Telemeter.build( relays = listOf(recordingRelay), facetResolvers = mapOf(testFacetResolver.getType() to testFacetResolver), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = scope), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), ) val numEvents = 10 @@ -501,7 +500,7 @@ internal class TelemeterTest { } @Test - fun `Given telemeter with a facetResolver that returns two facets, When record is called with an unresolved facet, Then return 2 resolved facets`() { + fun `Given telemeter with a facetResolver that returns two facets, When record is called with an unresolved facet, Then return 2 resolved facets`() = runTest { val recorded = mutableListOf() val recordingRelay = FakeRelay { recorded.add(it) @@ -532,7 +531,7 @@ internal class TelemeterTest { val telemeter = Telemeter.build( relays = listOf(recordingRelay), facetResolvers = mapOf(testFacetResolver.getType() to testFacetResolver), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = scope), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), ) val numEvents = 10 diff --git a/telemetry/src/test/java/com/kroger/telemetry/relay/PrintRelayTest.kt b/telemetry/src/test/java/com/kroger/telemetry/relay/PrintRelayTest.kt index 7563d22..1d2bcae 100644 --- a/telemetry/src/test/java/com/kroger/telemetry/relay/PrintRelayTest.kt +++ b/telemetry/src/test/java/com/kroger/telemetry/relay/PrintRelayTest.kt @@ -29,45 +29,36 @@ import com.kroger.telemetry.facet.Facet import com.kroger.telemetry.facet.Prefix import com.kroger.telemetry.facet.Significance import com.kroger.telemetry.util.FakeEvent -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestCoroutineScope -import org.junit.jupiter.api.AfterEach +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -@ExperimentalCoroutinesApi internal class PrintRelayTest { private val messages: MutableList = mutableListOf() private val capturePrinter: (PrintRelay.Message) -> Unit = { messages.add(it) } - private val coroutineScope = TestCoroutineScope() - private lateinit var relay: PrintRelay - private lateinit var telemeter: Telemeter @BeforeEach fun setup() { relay = PrintRelay(printer = capturePrinter) - telemeter = Telemeter.build( - relays = listOf(relay), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = coroutineScope), - ) - messages.clear() } - @AfterEach - fun teardown() { - coroutineScope.cleanupTestCoroutines() - } + private fun TestScope.createTelemeter() = Telemeter.build( + relays = listOf(relay), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), + ) @Test - fun `GIVEN detailed mode is enabled WHEN event logged with multiple prefixes THEN all are included in tag`() { + fun `GIVEN detailed mode is enabled WHEN event logged with multiple prefixes THEN all are included in tag`() = runTest { + val telemeter = createTelemeter() val appName = "app" val moduleName = "module" val className = "class" @@ -83,7 +74,7 @@ internal class PrintRelayTest { relay.configuration.detailedMode = true telemeter.record(event) - + testScheduler.runCurrent() val expectedTag = Telemeter.TAG + PrintRelay.separator + relay.configuration.defaultSignificance.toString() + PrintRelay.separator + appName + PrintRelay.separator + @@ -94,7 +85,8 @@ internal class PrintRelayTest { } @Test - fun `GIVEN detailed mode is enabled WHEN event logged with multiple prefixes in bad order THEN order is retained in tag`() { + fun `GIVEN detailed mode is enabled WHEN event logged with multiple prefixes in bad order THEN order is retained in tag`() = runTest { + val telemeter = createTelemeter() val appName = "app" val moduleName = "module" val screenName = "screen" @@ -110,7 +102,7 @@ internal class PrintRelayTest { relay.configuration.detailedMode = true telemeter.record(event) - + testScheduler.runCurrent() val expectedTag = Telemeter.TAG + PrintRelay.separator + relay.configuration.defaultSignificance.toString() + PrintRelay.separator + screenName + PrintRelay.separator + @@ -121,7 +113,8 @@ internal class PrintRelayTest { } @Test - fun `GIVEN detailed mode is disabled WHEN event logged with multiple prefixes THEN most recent prefix is used`() { + fun `GIVEN detailed mode is disabled WHEN event logged with multiple prefixes THEN most recent prefix is used`() = runTest { + val telemeter = createTelemeter() val appName = "app" val moduleName = "module" val screenName = "screen" @@ -136,7 +129,7 @@ internal class PrintRelayTest { ) telemeter.record(event) - + testScheduler.runCurrent() val expectedTag = Telemeter.TAG + PrintRelay.separator + relay.configuration.defaultSignificance.toString() + PrintRelay.separator + localName @@ -144,7 +137,8 @@ internal class PrintRelayTest { } @Test - fun `GIVEN detailed mode is disabled WHEN event logged with multiple prefixes in bad order THEN most recent prefix is used`() { + fun `GIVEN detailed mode is disabled WHEN event logged with multiple prefixes in bad order THEN most recent prefix is used`() = runTest { + val telemeter = createTelemeter() val appName = "app" val moduleName = "module" val screenName = "screen" @@ -159,7 +153,7 @@ internal class PrintRelayTest { ) telemeter.record(event) - + testScheduler.runCurrent() val expectedTag = Telemeter.TAG + PrintRelay.separator + relay.configuration.defaultSignificance.toString() + PrintRelay.separator + moduleName @@ -167,7 +161,8 @@ internal class PrintRelayTest { } @Test - fun `tags will include highest significance attached to event`() { + fun `tags will include highest significance attached to event`() = runTest { + val telemeter = createTelemeter() val event = FakeEvent( facets = listOf( Significance.VERBOSE, @@ -177,23 +172,25 @@ internal class PrintRelayTest { ) telemeter.record(event) - + testScheduler.runCurrent() val expectedTag = Telemeter.TAG + PrintRelay.separator + Significance.INTERNAL_ERROR.toString() assertEquals(expectedTag, messages[0].tag) } @Test - fun `relay significance will be attached if not specified on event`() { + fun `relay significance will be attached if not specified on event`() = runTest { + val telemeter = createTelemeter() val event = FakeEvent() telemeter.record(event) - + testScheduler.runCurrent() assertEquals(Significance.DEBUG, messages[0].significance) } @Test - fun `if configured for detailedMode, all facets will be included in message value`() { + fun `if configured for detailedMode, all facets will be included in message value`() = runTest { + val telemeter = createTelemeter() relay.configuration.detailedMode = true val description = "hello there" val firstMsg = "general" @@ -210,7 +207,7 @@ internal class PrintRelayTest { facets = listOf(facet1, facet2), ) telemeter.record(event) - + testScheduler.runCurrent() val expectedMessage = """ $description @@ -223,7 +220,8 @@ internal class PrintRelayTest { } @Test - fun `if not configured for detailed mode, only description is include in message value`() { + fun `if not configured for detailed mode, only description is include in message value`() = runTest { + val telemeter = createTelemeter() val description = "hello there" val firstMsg = "general" val secondMsg = "kenobi" @@ -239,42 +237,45 @@ internal class PrintRelayTest { facets = listOf(facet1, facet2), ) telemeter.record(event) - + testScheduler.runCurrent() assertEquals(description, messages[0].value) } @Test - fun `GIVEN event with significance lower than minimum WHEN processed THEN nothing will be printed`() { + fun `GIVEN event with significance lower than minimum WHEN processed THEN nothing will be printed`() = runTest { + val telemeter = createTelemeter() relay.configuration.minimumSignificance = Significance.INTERNAL_ERROR val event = FakeEvent(facets = listOf(Significance.ERROR)) telemeter.record(event) - + testScheduler.runCurrent() assertEquals(0, messages.size) } @Test - fun `GIVEN event with significance equal to minimum WHEN processed THEN message will be printed`() { + fun `GIVEN event with significance equal to minimum WHEN processed THEN message will be printed`() = runTest { + val telemeter = createTelemeter() relay.configuration.minimumSignificance = Significance.ERROR val event = FakeEvent(facets = listOf(Significance.ERROR)) telemeter.record(event) - + testScheduler.runCurrent() assertEquals(1, messages.size) } @Test - fun `GIVEN event with significance higher than minimum WHEN processed THEN message will be printed`() { + fun `GIVEN event with significance higher than minimum WHEN processed THEN message will be printed`() = runTest { + val telemeter = createTelemeter() relay.configuration.minimumSignificance = Significance.VERBOSE val event = FakeEvent(facets = listOf(Significance.ERROR)) telemeter.record(event) - + testScheduler.runCurrent() assertEquals(1, messages.size) } @Test - fun `GIVEN config delegated to default WHEN property is accessed THEN backing prop is accessed `() { + fun `GIVEN config delegated to default WHEN property is accessed THEN backing prop is accessed`() = runTest { var backingProp = true class Config : PrintRelay.Configuration by PrintRelay.Configuration.Default() { @@ -290,7 +291,7 @@ internal class PrintRelayTest { } @Test - fun `GIVEN config delegated to default WHEN property is written THEN backing prop is written `() { + fun `GIVEN config delegated to default WHEN property is written THEN backing prop is written`() = runTest { var backingProp = true class Config : PrintRelay.Configuration by PrintRelay.Configuration.Default() { diff --git a/telemetry/src/test/java/com/kroger/telemetry/relay/TelemeterLogExtensionsTest.kt b/telemetry/src/test/java/com/kroger/telemetry/relay/TelemeterLogExtensionsTest.kt index 467bd3a..b3dcaea 100644 --- a/telemetry/src/test/java/com/kroger/telemetry/relay/TelemeterLogExtensionsTest.kt +++ b/telemetry/src/test/java/com/kroger/telemetry/relay/TelemeterLogExtensionsTest.kt @@ -28,102 +28,119 @@ import com.kroger.telemetry.Event import com.kroger.telemetry.Telemeter import com.kroger.telemetry.facet.Significance import com.kroger.telemetry.util.FakeRelay -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test -@ExperimentalCoroutinesApi internal class TelemeterLogExtensionsTest { - private val scope = TestCoroutineScope() - private val captured = mutableListOf() private val fakeRelay = FakeRelay { captured.add(it) } - private val telemeter = Telemeter.build( - relays = listOf(fakeRelay), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = scope), - ) private val message = "hello there" @AfterEach fun teardown() { captured.clear() - scope.cleanupTestCoroutines() } + private fun TestScope.createTelemeter() = Telemeter.build( + relays = listOf(fakeRelay), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), + ) + @Test - fun `log records significance passed to it`() { + fun `log records significance passed to it`() = runTest { + val telemeter = createTelemeter() telemeter.log(message = message, significance = Significance.ERROR) + testScheduler.runCurrent() assertTrue(captured[0].facets[0] == Significance.ERROR) } @Test - fun `tag is ignored if null`() { + fun `tag is ignored if null`() = runTest { + val telemeter = createTelemeter() telemeter.log(message = message, significance = Significance.ERROR) + testScheduler.runCurrent() assertTrue(captured[0].description == message) } @Test - fun `tag is prepended if present`() { + fun `tag is prepended if present`() = runTest { + val telemeter = createTelemeter() val tag = Telemeter.TAG telemeter.log(tag = tag, message = message, significance = Significance.ERROR) - + testScheduler.runCurrent() val expected = "$tag - $message" assertEquals(expected, captured[0].description) } @Test - fun `v records verbose significant event`() { + fun `v records verbose significant event`() = runTest { + val telemeter = createTelemeter() telemeter.v(message = message) + testScheduler.runCurrent() assertTrue(captured[0].facets[0] == Significance.VERBOSE) } @Test - fun `d records debug significant event`() { + fun `d records debug significant event`() = runTest { + val telemeter = createTelemeter() telemeter.d(message = message) + testScheduler.runCurrent() assertTrue(captured[0].facets[0] == Significance.DEBUG) } @Test - fun `i records informational significant event`() { + fun `i records informational significant event`() = runTest { + val telemeter = createTelemeter() telemeter.i(message = message) + testScheduler.runCurrent() assertTrue(captured[0].facets[0] == Significance.INFORMATIONAL) } @Test - fun `w records warn significant event`() { + fun `w records warn significant event`() = runTest { + val telemeter = createTelemeter() telemeter.w(message = message) + testScheduler.runCurrent() assertTrue(captured[0].facets[0] == Significance.WARNING) } @Test - fun `e records exceptional significant event`() { + fun `e records exceptional significant event`() = runTest { + val telemeter = createTelemeter() telemeter.e(message = message) + testScheduler.runCurrent() assertTrue(captured[0].facets[0] == Significance.ERROR) } @Test - fun `wtf records internal_error significant event`() { + fun `wtf records internal_error significant event`() = runTest { + val telemeter = createTelemeter() telemeter.wtf(message = message) + testScheduler.runCurrent() assertTrue(captured[0].facets[0] == Significance.INTERNAL_ERROR) } @Test - fun `throwable is not used if not specified`() { + fun `throwable is not used if not specified`() = runTest { + val telemeter = createTelemeter() telemeter.wtf(message = message) + testScheduler.runCurrent() assertEquals(message, captured[0].description) } @Test - fun `throwable is used if specified`() { + fun `throwable is used if specified`() = runTest { + val telemeter = createTelemeter() val exceptionMessage = "oh no" telemeter.wtf(message = message, throwable = IllegalStateException(exceptionMessage)) - + testScheduler.runCurrent() val expectedMessage = "$message - $exceptionMessage" assertEquals(expectedMessage, captured[0].description) } From 9cbc9de508a8d06fe8d6c159aa4b394cb5b0cdca Mon Sep 17 00:00:00 2001 From: Tim Romel Date: Mon, 19 May 2025 16:11:03 -0400 Subject: [PATCH 3/6] style: ktlint updates --- .editorconfig | 7 +- .../telemetry/android/facet/ToastFacet.kt | 4 +- .../telemetry/android/relay/LogRelay.kt | 17 +- .../telemetry/android/relay/ToastRelay.kt | 43 +- .../telemetry/android/relay/ToastRelayTest.kt | 38 +- .../contextaware/ContextAwareFacetResolver.kt | 5 +- .../ContextAwareFacetResolverTest.kt | 8 +- .../firebase/FirebaseAnalyticsRelayTest.kt | 14 +- .../telemetry/firebase/CrashlyticsWrapper.kt | 31 +- .../firebase/FirebaseAnalyticsRelay.kt | 8 +- .../firebase/FirebaseCrashlyticsRelay.kt | 75 +- .../com/kroger/telemetry/firebase/Samples.kt | 42 +- .../firebase/FirebaseCrashlyticsRelayTest.kt | 43 +- .../kroger/telemetry/sample/AppTelemeter.kt | 13 +- .../com/kroger/telemetry/sample/BarRelay.kt | 5 +- .../kroger/telemetry/sample/MainActivity.kt | 36 +- .../telemetry/sample/SampleApplication.kt | 9 +- .../StringResourceFormattedToastFacet.kt | 4 +- .../main/java/com/kroger/telemetry/Event.kt | 4 +- .../main/java/com/kroger/telemetry/Relay.kt | 1 + .../main/java/com/kroger/telemetry/Samples.kt | 62 +- .../com/kroger/telemetry/StandardTelemeter.kt | 99 +- .../java/com/kroger/telemetry/Telemeter.kt | 47 +- .../telemetry/facet/DeveloperMetricsFacet.kt | 4 +- .../com/kroger/telemetry/facet/Failure.kt | 5 +- .../java/com/kroger/telemetry/facet/Prefix.kt | 20 +- .../kroger/telemetry/facet/Significance.kt | 2 +- .../com/kroger/telemetry/relay/PrintRelay.kt | 126 +-- .../java/com/kroger/telemetry/EventTest.kt | 8 +- .../java/com/kroger/telemetry/RelayTest.kt | 62 +- .../com/kroger/telemetry/TelemeterTest.kt | 844 ++++++++++-------- .../com/kroger/telemetry/facet/FacetTest.kt | 18 +- .../kroger/telemetry/relay/PrintRelayTest.kt | 476 +++++----- .../relay/TelemeterLogExtensionsTest.kt | 167 ++-- .../com/kroger/telemetry/util/FakeRelay.kt | 4 +- 35 files changed, 1346 insertions(+), 1005 deletions(-) diff --git a/.editorconfig b/.editorconfig index ab8d30e..5c23ee0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,11 +1,14 @@ root = true [*.{kt,kts}] -disabled_rules = filename -max_line_length = off insert_final_newline = true ij_kotlin_name_count_to_use_star_import = 999 ij_kotlin_name_count_to_use_star_import_for_members = 999 ij_java_class_count_to_use_import_on_demand = 999 ij_kotlin_allow_trailing_comma = true ij_kotlin_allow_trailing_comma_on_call_site = true + +ktlint_ignore_back_ticked_identifier = true + +# Disabled annotation formatting so @Inject constructor() doesn't go to new line +ktlint_standard_annotation = disabled diff --git a/android/src/main/java/com/kroger/telemetry/android/facet/ToastFacet.kt b/android/src/main/java/com/kroger/telemetry/android/facet/ToastFacet.kt index 47460e8..ee74f4f 100644 --- a/android/src/main/java/com/kroger/telemetry/android/facet/ToastFacet.kt +++ b/android/src/main/java/com/kroger/telemetry/android/facet/ToastFacet.kt @@ -26,4 +26,6 @@ package com.kroger.telemetry.android.facet import com.kroger.telemetry.facet.Facet -public data class ToastFacet(val message: String) : Facet +public data class ToastFacet( + val message: String, +) : Facet diff --git a/android/src/main/java/com/kroger/telemetry/android/relay/LogRelay.kt b/android/src/main/java/com/kroger/telemetry/android/relay/LogRelay.kt index 057e3ca..bd48016 100644 --- a/android/src/main/java/com/kroger/telemetry/android/relay/LogRelay.kt +++ b/android/src/main/java/com/kroger/telemetry/android/relay/LogRelay.kt @@ -42,11 +42,12 @@ private val logPrinter: (PrintRelay.Message) -> Unit = { message -> Log.println(message.significance.toLogPriority(), message.tag, message.value) } -private fun Significance.toLogPriority(): Int = when (this) { - Significance.VERBOSE -> Log.VERBOSE - Significance.DEBUG -> Log.DEBUG - Significance.INFORMATIONAL -> Log.INFO - Significance.WARNING -> Log.WARN - Significance.ERROR -> Log.ERROR - Significance.INTERNAL_ERROR -> Log.ERROR -} +private fun Significance.toLogPriority(): Int = + when (this) { + Significance.VERBOSE -> Log.VERBOSE + Significance.DEBUG -> Log.DEBUG + Significance.INFORMATIONAL -> Log.INFO + Significance.WARNING -> Log.WARN + Significance.ERROR -> Log.ERROR + Significance.INTERNAL_ERROR -> Log.ERROR + } diff --git a/android/src/main/java/com/kroger/telemetry/android/relay/ToastRelay.kt b/android/src/main/java/com/kroger/telemetry/android/relay/ToastRelay.kt index 471e2a4..117e033 100644 --- a/android/src/main/java/com/kroger/telemetry/android/relay/ToastRelay.kt +++ b/android/src/main/java/com/kroger/telemetry/android/relay/ToastRelay.kt @@ -44,7 +44,6 @@ public class ToastRelay internal constructor( private val toaster: Toaster, public val configuration: Configuration, ) : Relay { - /** * A set of configurable options for a ToastRelay. * @property toastLength Should be one of [Toast.LENGTH_SHORT] or [Toast.LENGTH_LONG]. Defaults to short. @@ -83,31 +82,42 @@ public class ToastRelay internal constructor( override suspend fun process(event: Event) { val toastFacets = event.facets.filterIsInstance(ToastFacet::class.java) - val shouldToastWithoutToastFacet = event.hasHighEnoughSignificance() && - configuration.toastSignificantEvents + val shouldToastWithoutToastFacet = + event.hasHighEnoughSignificance() && + configuration.toastSignificantEvents val shouldToast = toastFacets.isNotEmpty() || shouldToastWithoutToastFacet if (shouldToast && configuration.enabled) { val message = toastFacets.firstOrNull()?.message ?: event.description - val correctedLength = when (configuration.toastLength) { - Toast.LENGTH_SHORT -> configuration.toastLength - Toast.LENGTH_LONG -> configuration.toastLength - else -> Toast.LENGTH_SHORT - } + val correctedLength = + when (configuration.toastLength) { + Toast.LENGTH_SHORT -> configuration.toastLength + Toast.LENGTH_LONG -> configuration.toastLength + else -> Toast.LENGTH_SHORT + } toaster.toast(message, correctedLength) } } - private fun Event.hasHighEnoughSignificance(): Boolean = facets - .filterIsInstance(Significance::class.java) - .any { it >= configuration.minimumSignificance } + private fun Event.hasHighEnoughSignificance(): Boolean = + facets + .filterIsInstance(Significance::class.java) + .any { it >= configuration.minimumSignificance } } internal interface Toaster { - suspend fun toast(message: String, length: Int) + suspend fun toast( + message: String, + length: Int, + ) } -private class ToasterImpl(private val context: Context) : Toaster { - override suspend fun toast(message: String, length: Int) = withContext(Dispatchers.Main) { +private class ToasterImpl( + private val context: Context, +) : Toaster { + override suspend fun toast( + message: String, + length: Int, + ) = withContext(Dispatchers.Main) { Toast.makeText(context, message, length).show() } } @@ -119,8 +129,9 @@ private interface Toggles { private fun sampleToastConfig() { val propertyChangeConfig = ToastRelay.Configuration.Default(toastSignificantEvents = true) - class PropertyBehaviorChangeConfig(private val toggles: Toggles) : - ToastRelay.Configuration by ToastRelay.Configuration.Default() { + class PropertyBehaviorChangeConfig( + private val toggles: Toggles, + ) : ToastRelay.Configuration by ToastRelay.Configuration.Default() { override var enabled: Boolean get() = toggles["ToastRelay Toggle"] set(_) = Unit diff --git a/android/src/test/java/com/kroger/telemetry/android/relay/ToastRelayTest.kt b/android/src/test/java/com/kroger/telemetry/android/relay/ToastRelayTest.kt index cba041f..f3cd683 100644 --- a/android/src/test/java/com/kroger/telemetry/android/relay/ToastRelayTest.kt +++ b/android/src/test/java/com/kroger/telemetry/android/relay/ToastRelayTest.kt @@ -39,7 +39,11 @@ internal class ToastRelayTest { private class FakeToaster : Toaster { var didToast = false var fakeFunToast: (String, Int) -> Unit = { _, _ -> didToast = true } - override suspend fun toast(message: String, length: Int) = fakeFunToast(message, length) + + override suspend fun toast( + message: String, + length: Int, + ) = fakeFunToast(message, length) } private data class TestConfig( @@ -92,10 +96,11 @@ internal class ToastRelayTest { @Test fun `GIVEN toast relay configured to toast all WHEN event received no significance THEN event is not toasted`() = runTest { - val config = TestConfig().copy( - toastSignificantEvents = true, - minimumSignificance = Significance.ERROR, - ) + val config = + TestConfig().copy( + toastSignificantEvents = true, + minimumSignificance = Significance.ERROR, + ) val relay = config.getRelay() relay.process( @@ -110,10 +115,11 @@ internal class ToastRelayTest { @Test fun `GIVEN toast relay configured to toast all WHEN event received with lower than minimum significance THEN event is not toasted`() = runTest { - val config = TestConfig().copy( - toastSignificantEvents = true, - minimumSignificance = Significance.ERROR, - ) + val config = + TestConfig().copy( + toastSignificantEvents = true, + minimumSignificance = Significance.ERROR, + ) val relay = config.getRelay() relay.process( @@ -128,10 +134,11 @@ internal class ToastRelayTest { @Test fun `GIVEN toast relay configured to toast all WHEN event received with minimum significance THEN event is toasted`() = runTest { - val config = TestConfig().copy( - toastSignificantEvents = true, - minimumSignificance = Significance.ERROR, - ) + val config = + TestConfig().copy( + toastSignificantEvents = true, + minimumSignificance = Significance.ERROR, + ) val relay = config.getRelay() relay.process( @@ -182,8 +189,9 @@ internal class ToastRelayTest { fun `GIVEN config with mutable backing data WHEN backing data is changed THEN config reflects update`() { val mutableBackingInstance = mutableListOf(false) - class MutableConfig(private val mutableBackingProp: List) : - ToastRelay.Configuration by ToastRelay.Configuration.Default() { + class MutableConfig( + private val mutableBackingProp: List, + ) : ToastRelay.Configuration by ToastRelay.Configuration.Default() { override var enabled: Boolean get() = mutableBackingProp.first() set(_) = Unit diff --git a/context-aware/src/main/java/com/kroger/telemetry/contextaware/ContextAwareFacetResolver.kt b/context-aware/src/main/java/com/kroger/telemetry/contextaware/ContextAwareFacetResolver.kt index acda674..ff18b02 100644 --- a/context-aware/src/main/java/com/kroger/telemetry/contextaware/ContextAwareFacetResolver.kt +++ b/context-aware/src/main/java/com/kroger/telemetry/contextaware/ContextAwareFacetResolver.kt @@ -30,8 +30,9 @@ import com.kroger.telemetry.facet.FacetResolver import com.kroger.telemetry.facet.UnresolvedFacet import javax.inject.Inject -public class ContextAwareFacetResolver @Inject constructor(private val context: Context) : - FacetResolver { +public class ContextAwareFacetResolver @Inject constructor( + private val context: Context, +) : FacetResolver { override fun getType(): Class = ContextAwareFacet::class.java override fun resolve(unresolvedFacet: UnresolvedFacet): List = diff --git a/context-aware/src/test/java/com/kroger/telemetry/contextaware/ContextAwareFacetResolverTest.kt b/context-aware/src/test/java/com/kroger/telemetry/contextaware/ContextAwareFacetResolverTest.kt index 319faf2..5c6b5c1 100644 --- a/context-aware/src/test/java/com/kroger/telemetry/contextaware/ContextAwareFacetResolverTest.kt +++ b/context-aware/src/test/java/com/kroger/telemetry/contextaware/ContextAwareFacetResolverTest.kt @@ -32,7 +32,6 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test public class ContextAwareFacetResolverTest { - private val context: Context = mockk() private lateinit var contextAwareFacetResolver: ContextAwareFacetResolver @@ -53,11 +52,10 @@ public class ContextAwareFacetResolverTest { @Test public fun `Given an ContextAwareFacetResolver, When resolve is called on a ContextAwareFacet, Then return the resolved Facet`() { val testFacet = object : Facet {} - val testUnresolvedFacet = object : ContextAwareFacet { - override fun resolve(context: Context): Facet { - return testFacet + val testUnresolvedFacet = + object : ContextAwareFacet { + override fun resolve(context: Context): Facet = testFacet } - } val sut = contextAwareFacetResolver.resolve(testUnresolvedFacet) diff --git a/firebase/src/androidTest/java/com/kroger/telemetry/firebase/FirebaseAnalyticsRelayTest.kt b/firebase/src/androidTest/java/com/kroger/telemetry/firebase/FirebaseAnalyticsRelayTest.kt index 9f273d4..b3ddf0a 100644 --- a/firebase/src/androidTest/java/com/kroger/telemetry/firebase/FirebaseAnalyticsRelayTest.kt +++ b/firebase/src/androidTest/java/com/kroger/telemetry/firebase/FirebaseAnalyticsRelayTest.kt @@ -1,3 +1,5 @@ +@file:Suppress("ktlint:standard:max-line-length") + package com.kroger.telemetry.firebase import androidx.core.os.bundleOf @@ -9,7 +11,6 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) internal class FirebaseAnalyticsRelayTest { - @Test fun given_event_with_firebase_facet_with_data_WHEN_recorded_THEN_data_matches_in_logged_event() = runBlocking { @@ -19,11 +20,12 @@ internal class FirebaseAnalyticsRelayTest { val val1 = "val1" val val2 = 2 - val facetWithData = object : DeveloperMetricsFacet { - override val eventName: String = fakeName - override val compute: () -> Map = - { mapOf(key1 to val1, key2 to val2) } - } + val facetWithData = + object : DeveloperMetricsFacet { + override val eventName: String = fakeName + override val compute: () -> Map = + { mapOf(key1 to val1, key2 to val2) } + } val bundleToCompare = bundleOf(key1 to val1, key2 to val2) diff --git a/firebase/src/main/java/com/kroger/telemetry/firebase/CrashlyticsWrapper.kt b/firebase/src/main/java/com/kroger/telemetry/firebase/CrashlyticsWrapper.kt index f673dff..dcbc831 100644 --- a/firebase/src/main/java/com/kroger/telemetry/firebase/CrashlyticsWrapper.kt +++ b/firebase/src/main/java/com/kroger/telemetry/firebase/CrashlyticsWrapper.kt @@ -32,7 +32,6 @@ package com.kroger.telemetry.firebase * @sample crashlyticsWrapperImplementation */ public interface CrashlyticsWrapper { - /** * Records a custom key and value to be associated with subsequent fatal and non-fatal reports. Multiple calls to this method * with the same key will update the value for that key. The value of any key at the time of a fatal or non-fatal event will @@ -44,7 +43,10 @@ public interface CrashlyticsWrapper { * *@param value A value to be associated with the given key */ - public fun setCustomKey(key: String, value: String) + public fun setCustomKey( + key: String, + value: String, + ) /** * Records a custom key and value to be associated with subsequent fatal and non-fatal reports. Multiple calls to this method @@ -57,7 +59,10 @@ public interface CrashlyticsWrapper { * *@param value A value to be associated with the given key */ - public fun setCustomKey(key: String, value: Boolean) + public fun setCustomKey( + key: String, + value: Boolean, + ) /** * Records a custom key and value to be associated with subsequent fatal and non-fatal reports. Multiple calls to this method @@ -70,7 +75,10 @@ public interface CrashlyticsWrapper { * *@param value A value to be associated with the given key */ - public fun setCustomKey(key: String, value: Int) + public fun setCustomKey( + key: String, + value: Int, + ) /** * Records a custom key and value to be associated with subsequent fatal and non-fatal reports. Multiple calls to this method @@ -83,7 +91,10 @@ public interface CrashlyticsWrapper { * *@param value A value to be associated with the given key */ - public fun setCustomKey(key: String, value: Long) + public fun setCustomKey( + key: String, + value: Long, + ) /** * Records a custom key and value to be associated with subsequent fatal and non-fatal reports. Multiple calls to this method @@ -96,7 +107,10 @@ public interface CrashlyticsWrapper { * *@param value A value to be associated with the given key */ - public fun setCustomKey(key: String, value: Float) + public fun setCustomKey( + key: String, + value: Float, + ) /** * Records a custom key and value to be associated with subsequent fatal and non-fatal reports. Multiple calls to this method @@ -109,7 +123,10 @@ public interface CrashlyticsWrapper { * *@param value A value to be associated with the given key */ - public fun setCustomKey(key: String, value: Double) + public fun setCustomKey( + key: String, + value: Double, + ) /** * Records a non-fatal report to send to Crashlytics. diff --git a/firebase/src/main/java/com/kroger/telemetry/firebase/FirebaseAnalyticsRelay.kt b/firebase/src/main/java/com/kroger/telemetry/firebase/FirebaseAnalyticsRelay.kt index 776cba2..3540287 100644 --- a/firebase/src/main/java/com/kroger/telemetry/firebase/FirebaseAnalyticsRelay.kt +++ b/firebase/src/main/java/com/kroger/telemetry/firebase/FirebaseAnalyticsRelay.kt @@ -40,9 +40,9 @@ import javax.inject.Inject public class FirebaseAnalyticsRelay @Inject constructor( private val firebaseAnalytics: FirebaseAnalytics, ) : Relay by Relay.buildTypedRelay( - { facet -> - firebaseAnalytics.logEvent(facet.eventName, facet.compute()?.toBundle()) - }, -) + { facet -> + firebaseAnalytics.logEvent(facet.eventName, facet.compute()?.toBundle()) + }, + ) internal fun Map.toBundle(): Bundle = bundleOf(*this.toList().toTypedArray()) diff --git a/firebase/src/main/java/com/kroger/telemetry/firebase/FirebaseCrashlyticsRelay.kt b/firebase/src/main/java/com/kroger/telemetry/firebase/FirebaseCrashlyticsRelay.kt index d36b1a2..26ba3be 100644 --- a/firebase/src/main/java/com/kroger/telemetry/firebase/FirebaseCrashlyticsRelay.kt +++ b/firebase/src/main/java/com/kroger/telemetry/firebase/FirebaseCrashlyticsRelay.kt @@ -52,8 +52,9 @@ import javax.inject.Inject * * @sample crashlyticsWrapperImplementation */ -public class FirebaseCrashlyticsRelay @Inject constructor(private val crashlytics: CrashlyticsWrapper) : Relay { - +public class FirebaseCrashlyticsRelay @Inject constructor( + private val crashlytics: CrashlyticsWrapper, +) : Relay { override suspend fun process(event: Event) { event.facets.filterIsInstance().forEach { when (it) { @@ -102,8 +103,9 @@ public class FirebaseCrashlyticsRelay @Inject constructor(private val crashlytic * * NOTE: Crashlytics supports up to 64 key/value pairs in a single report. Each key/value pair must be no larger than 1 kB in size. */ -public sealed class CrashlyticsKey(public val key: String) : Facet { - +public sealed class CrashlyticsKey( + public val key: String, +) : Facet { public companion object { /** * Builds a [CrashlyticsKey] for the key/value pair. @@ -111,7 +113,10 @@ public sealed class CrashlyticsKey(public val key: String) : Facet { * @param key a unique key to identify the value * @param value a String value */ - public fun build(key: String, value: String): CrashlyticsKey = StringState(key, value) + public fun build( + key: String, + value: String, + ): CrashlyticsKey = StringState(key, value) /** * Builds a [CrashlyticsKey] for the key/value pair. @@ -119,7 +124,10 @@ public sealed class CrashlyticsKey(public val key: String) : Facet { * @param key a unique key to identify the value * @param value a Boolean value */ - public fun build(key: String, value: Boolean): CrashlyticsKey = BooleanState(key, value) + public fun build( + key: String, + value: Boolean, + ): CrashlyticsKey = BooleanState(key, value) /** * Builds a [CrashlyticsKey] for the key/value pair. @@ -127,7 +135,10 @@ public sealed class CrashlyticsKey(public val key: String) : Facet { * @param key a unique key to identify the value * @param value an Int value */ - public fun build(key: String, value: Int): CrashlyticsKey = IntState(key, value) + public fun build( + key: String, + value: Int, + ): CrashlyticsKey = IntState(key, value) /** * Builds a [CrashlyticsKey] for the key/value pair. @@ -135,7 +146,10 @@ public sealed class CrashlyticsKey(public val key: String) : Facet { * @param key a unique key to identify the value * @param value a Long value */ - public fun build(key: String, value: Long): CrashlyticsKey = LongState(key, value) + public fun build( + key: String, + value: Long, + ): CrashlyticsKey = LongState(key, value) /** * Builds a [CrashlyticsKey] for the key/value pair. @@ -143,7 +157,10 @@ public sealed class CrashlyticsKey(public val key: String) : Facet { * @param key a unique key to identify the value * @param value a Float value */ - public fun build(key: String, value: Float): CrashlyticsKey = FloatState(key, value) + public fun build( + key: String, + value: Float, + ): CrashlyticsKey = FloatState(key, value) /** * Builds a [CrashlyticsKey] for the key/value pair. @@ -151,7 +168,10 @@ public sealed class CrashlyticsKey(public val key: String) : Facet { * @param key a unique key to identify the value * @param value a Double value */ - public fun build(key: String, value: Double): CrashlyticsKey = DoubleState(key, value) + public fun build( + key: String, + value: Double, + ): CrashlyticsKey = DoubleState(key, value) } } @@ -159,9 +179,32 @@ public sealed class CrashlyticsKey(public val key: String) : Facet { * These classes keep the key/value pairs typesafe, while the CrashlyticsKey build methods keep the public API * simple, i.e. CrashlyticsKey.build(key, value). */ -private class StringState(key: String, val value: String) : CrashlyticsKey(key) -private class BooleanState(key: String, val value: Boolean) : CrashlyticsKey(key) -private class IntState(key: String, val value: Int) : CrashlyticsKey(key) -private class LongState(key: String, val value: Long) : CrashlyticsKey(key) -private class FloatState(key: String, val value: Float) : CrashlyticsKey(key) -private class DoubleState(key: String, val value: Double) : CrashlyticsKey(key) +private class StringState( + key: String, + val value: String, +) : CrashlyticsKey(key) + +private class BooleanState( + key: String, + val value: Boolean, +) : CrashlyticsKey(key) + +private class IntState( + key: String, + val value: Int, +) : CrashlyticsKey(key) + +private class LongState( + key: String, + val value: Long, +) : CrashlyticsKey(key) + +private class FloatState( + key: String, + val value: Float, +) : CrashlyticsKey(key) + +private class DoubleState( + key: String, + val value: Double, +) : CrashlyticsKey(key) diff --git a/firebase/src/main/java/com/kroger/telemetry/firebase/Samples.kt b/firebase/src/main/java/com/kroger/telemetry/firebase/Samples.kt index b53f933..c7e1b99 100644 --- a/firebase/src/main/java/com/kroger/telemetry/firebase/Samples.kt +++ b/firebase/src/main/java/com/kroger/telemetry/firebase/Samples.kt @@ -32,8 +32,13 @@ import javax.inject.Inject * This serves to make the `RealCrashlytics` sample below compile without crashlytics on the classpath */ private interface FirebaseCrashlytics { - fun setCustomKey(key: String, value: Any) + fun setCustomKey( + key: String, + value: Any, + ) + fun recordException(e: Throwable) + fun log(message: String) } @@ -42,29 +47,48 @@ internal object Samples { * A sample of how one might implement [CrashlyticsWrapper] to forward calls to the real thing */ internal fun crashlyticsWrapperImplementation() { - class RealCrashlytics @Inject constructor(private val crashlytics: FirebaseCrashlytics) : - CrashlyticsWrapper { - override fun setCustomKey(key: String, value: String) { + class RealCrashlytics @Inject constructor( + private val crashlytics: FirebaseCrashlytics, + ) : CrashlyticsWrapper { + override fun setCustomKey( + key: String, + value: String, + ) { crashlytics.setCustomKey(key, value) } - override fun setCustomKey(key: String, value: Boolean) { + override fun setCustomKey( + key: String, + value: Boolean, + ) { crashlytics.setCustomKey(key, value) } - override fun setCustomKey(key: String, value: Int) { + override fun setCustomKey( + key: String, + value: Int, + ) { crashlytics.setCustomKey(key, value) } - override fun setCustomKey(key: String, value: Long) { + override fun setCustomKey( + key: String, + value: Long, + ) { crashlytics.setCustomKey(key, value) } - override fun setCustomKey(key: String, value: Float) { + override fun setCustomKey( + key: String, + value: Float, + ) { crashlytics.setCustomKey(key, value) } - override fun setCustomKey(key: String, value: Double) { + override fun setCustomKey( + key: String, + value: Double, + ) { crashlytics.setCustomKey(key, value) } diff --git a/firebase/src/test/java/com/kroger/telemetry/firebase/FirebaseCrashlyticsRelayTest.kt b/firebase/src/test/java/com/kroger/telemetry/firebase/FirebaseCrashlyticsRelayTest.kt index 298cafd..d636955 100644 --- a/firebase/src/test/java/com/kroger/telemetry/firebase/FirebaseCrashlyticsRelayTest.kt +++ b/firebase/src/test/java/com/kroger/telemetry/firebase/FirebaseCrashlyticsRelayTest.kt @@ -35,7 +35,6 @@ import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Test internal class FirebaseCrashlyticsRelayTest { - private val mockCrashlytics = mockk(relaxed = true) private val relay = FirebaseCrashlyticsRelay(mockCrashlytics) @@ -68,14 +67,15 @@ internal class FirebaseCrashlyticsRelayTest { runBlocking { relay.process( TestEvent( - keys = arrayOf( - CrashlyticsKey.build("STRING", "hello-crashlytics"), - CrashlyticsKey.build("BOOLEAN", true), - CrashlyticsKey.build("INT", 42), - CrashlyticsKey.build("LONG", 42L), - CrashlyticsKey.build("FLOAT", 4.2f), - CrashlyticsKey.build("DOUBLE", 4.2), - ), + keys = + arrayOf( + CrashlyticsKey.build("STRING", "hello-crashlytics"), + CrashlyticsKey.build("BOOLEAN", true), + CrashlyticsKey.build("INT", 42), + CrashlyticsKey.build("LONG", 42L), + CrashlyticsKey.build("FLOAT", 4.2f), + CrashlyticsKey.build("DOUBLE", 4.2), + ), ), ) } @@ -116,12 +116,13 @@ internal class FirebaseCrashlyticsRelayTest { val e1 = RuntimeException("Oh no") val key = "some key" val value = "some value" - val event = TestEvent( - message = testMessage, - significance = Significance.ERROR, - throwable = e1, - keys = arrayOf(CrashlyticsKey.build(key, value)), - ) + val event = + TestEvent( + message = testMessage, + significance = Significance.ERROR, + throwable = e1, + keys = arrayOf(CrashlyticsKey.build(key, value)), + ) runBlocking { relay.process(event) } @@ -142,11 +143,11 @@ internal class FirebaseCrashlyticsRelayTest { get() = message ?: "" override val facets: List - get() = ( - listOf(significance) + - keys + - throwable?.let { Failure(throwable = it) } - ) - .filterNotNull() + get() = + ( + listOf(significance) + + keys + + throwable?.let { Failure(throwable = it) } + ).filterNotNull() } } diff --git a/sample/src/main/java/com/kroger/telemetry/sample/AppTelemeter.kt b/sample/src/main/java/com/kroger/telemetry/sample/AppTelemeter.kt index 9d4283b..0119e29 100644 --- a/sample/src/main/java/com/kroger/telemetry/sample/AppTelemeter.kt +++ b/sample/src/main/java/com/kroger/telemetry/sample/AppTelemeter.kt @@ -36,9 +36,10 @@ class AppTelemeter @Inject constructor( private val context: Context, private val contextAwareFacetResolver: ContextAwareFacetResolver, ) : Telemeter by Telemeter.build( - relays = listOf(LogRelay(), BarRelay(), FooRelay(), ToastRelay(context)), - facetResolvers = mapOf( - contextAwareFacetResolver.getType() to contextAwareFacetResolver, - ), - facets = listOf(Prefix.App("sample")), -) + relays = listOf(LogRelay(), BarRelay(), FooRelay(), ToastRelay(context)), + facetResolvers = + mapOf( + contextAwareFacetResolver.getType() to contextAwareFacetResolver, + ), + facets = listOf(Prefix.App("sample")), + ) diff --git a/sample/src/main/java/com/kroger/telemetry/sample/BarRelay.kt b/sample/src/main/java/com/kroger/telemetry/sample/BarRelay.kt index f7a012f..75ae602 100644 --- a/sample/src/main/java/com/kroger/telemetry/sample/BarRelay.kt +++ b/sample/src/main/java/com/kroger/telemetry/sample/BarRelay.kt @@ -30,6 +30,7 @@ import com.kroger.telemetry.facet.Facet class BarRelay : TypedRelay { override val type: Class = BarFacet::class.java + override suspend fun processFacet(facet: BarFacet) { val string = "completed bar facet ${facet.data}" @@ -37,4 +38,6 @@ class BarRelay : TypedRelay { } } -data class BarFacet(val data: String) : Facet +data class BarFacet( + val data: String, +) : Facet diff --git a/sample/src/main/java/com/kroger/telemetry/sample/MainActivity.kt b/sample/src/main/java/com/kroger/telemetry/sample/MainActivity.kt index 6b756a9..8a7a9fe 100644 --- a/sample/src/main/java/com/kroger/telemetry/sample/MainActivity.kt +++ b/sample/src/main/java/com/kroger/telemetry/sample/MainActivity.kt @@ -46,7 +46,6 @@ import javax.inject.Inject @AndroidEntryPoint class MainActivity : AppCompatActivity() { - private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) } @@ -86,24 +85,28 @@ class MainActivity : AppCompatActivity() { } sealed class ActivityEvent : Event { - protected val activityFacets = listOf( - BarFacet("some bar:data from the activity"), - ) + protected val activityFacets = + listOf( + BarFacet("some bar:data from the activity"), + ) object Created : ActivityEvent() { override val description = "Activity Created" override val facets = activityFacets } - class ButtonClicked(buttonTag: String) : ActivityEvent() { + class ButtonClicked( + buttonTag: String, + ) : ActivityEvent() { override val description: String = "$buttonTag clicked" override val facets: List = - activityFacets + listOf( - StringResourceFormattedToastFacet( - R.string.button_click_message, - buttonTag, - ), - ) + activityFacets + + listOf( + StringResourceFormattedToastFacet( + R.string.button_click_message, + buttonTag, + ), + ) } } @@ -120,11 +123,13 @@ class ActivityRelay : Relay { class ModuleOneTelemeter( telemeter: Telemeter, contextAwareFacetResolver: ContextAwareFacetResolver, -) : - Telemeter by telemeter.child( +) : Telemeter by telemeter.child( relays = listOf(ActivityRelay()), facets = listOf(MainActivity.prefix), - facetResolvers = mapOf(StringResourceFormattedToastFacet::class.java to contextAwareFacetResolver), + facetResolvers = + mapOf( + StringResourceFormattedToastFacet::class.java to contextAwareFacetResolver, + ), ) @InstallIn(ActivityComponent::class) @@ -134,6 +139,5 @@ object ModuleOneModule { fun provideModuleOneTelemeter( appTelemeter: AppTelemeter, contextAwareFacetResolver: ContextAwareFacetResolver, - ): ModuleOneTelemeter = - ModuleOneTelemeter(appTelemeter, contextAwareFacetResolver) + ): ModuleOneTelemeter = ModuleOneTelemeter(appTelemeter, contextAwareFacetResolver) } diff --git a/sample/src/main/java/com/kroger/telemetry/sample/SampleApplication.kt b/sample/src/main/java/com/kroger/telemetry/sample/SampleApplication.kt index 4bc5fce..31e49e4 100644 --- a/sample/src/main/java/com/kroger/telemetry/sample/SampleApplication.kt +++ b/sample/src/main/java/com/kroger/telemetry/sample/SampleApplication.kt @@ -48,7 +48,10 @@ class SampleApplication : Application() { } } -data class ApplicationStartupEvent(val message: String, val toast: Boolean = false) : Event { +data class ApplicationStartupEvent( + val message: String, + val toast: Boolean = false, +) : Event { override val description = message override val facets: List = if (!toast) listOf() else listOf(ToastFacet(message)) } @@ -57,5 +60,7 @@ data class ApplicationStartupEvent(val message: String, val toast: Boolean = fal @Module object TelemeterModule { @Provides - fun provideContext(@ApplicationContext context: Context): Context = context + fun provideContext( + @ApplicationContext context: Context, + ): Context = context } diff --git a/sample/src/main/java/com/kroger/telemetry/sample/StringResourceFormattedToastFacet.kt b/sample/src/main/java/com/kroger/telemetry/sample/StringResourceFormattedToastFacet.kt index 017aafd..170cc29 100644 --- a/sample/src/main/java/com/kroger/telemetry/sample/StringResourceFormattedToastFacet.kt +++ b/sample/src/main/java/com/kroger/telemetry/sample/StringResourceFormattedToastFacet.kt @@ -34,7 +34,5 @@ class StringResourceFormattedToastFacet( @StringRes private val resId: Int, private vararg val formatArgs: String, ) : ContextAwareFacet { - override fun resolve(context: Context): Facet { - return ToastFacet(context.getString(resId, *formatArgs)) - } + override fun resolve(context: Context): Facet = ToastFacet(context.getString(resId, *formatArgs)) } diff --git a/telemetry/src/main/java/com/kroger/telemetry/Event.kt b/telemetry/src/main/java/com/kroger/telemetry/Event.kt index df4faee..5fc69fd 100644 --- a/telemetry/src/main/java/com/kroger/telemetry/Event.kt +++ b/telemetry/src/main/java/com/kroger/telemetry/Event.kt @@ -36,6 +36,8 @@ import com.kroger.telemetry.facet.Facet * @property facets A list of data and metadata pertinent to the event. */ public interface Event { - public val description: String get() = "${this::class.java.simpleName}\n${facets.joinToString("\n")}" + public val description: String get() = "${this::class.java.simpleName}\n${facets.joinToString( + "\n", + )}" public val facets: List } diff --git a/telemetry/src/main/java/com/kroger/telemetry/Relay.kt b/telemetry/src/main/java/com/kroger/telemetry/Relay.kt index 5737ab8..31a601a 100644 --- a/telemetry/src/main/java/com/kroger/telemetry/Relay.kt +++ b/telemetry/src/main/java/com/kroger/telemetry/Relay.kt @@ -60,6 +60,7 @@ public interface Relay { */ public interface TypedRelay : Relay { public val type: Class + override suspend fun process(event: Event) { event.facets.filterIsInstance(type).forEach { facet -> processFacet(facet) diff --git a/telemetry/src/main/java/com/kroger/telemetry/Samples.kt b/telemetry/src/main/java/com/kroger/telemetry/Samples.kt index 84dc843..2470ee8 100644 --- a/telemetry/src/main/java/com/kroger/telemetry/Samples.kt +++ b/telemetry/src/main/java/com/kroger/telemetry/Samples.kt @@ -29,32 +29,45 @@ import com.kroger.telemetry.facet.Prefix import com.kroger.telemetry.facet.Significance import com.kroger.telemetry.relay.PrintRelay -private data class MyDataType(val data: String) +private data class MyDataType( + val data: String, +) internal fun topLevelExample() { - data class MyFacet(override val compute: () -> MyDataType) : Facet.Computed + data class MyFacet( + override val compute: () -> MyDataType, + ) : Facet.Computed - data class MyEvent(val thingThatHappened: String) : Event { + data class MyEvent( + val thingThatHappened: String, + ) : Event { override val description: String = thingThatHappened - override val facets: List = listOf( - Significance.INFORMATIONAL, - MyFacet { /* expensive logic that results in */ MyDataType(thingThatHappened) }, - /* other facets for other relays */ - ) + override val facets: List = + listOf( + Significance.INFORMATIONAL, + MyFacet { + // expensive logic that results in + MyDataType(thingThatHappened) + }, + // other facets for other relays + ) } class MyRelay : TypedRelay { override val type: Class = MyFacet::class.java + override suspend fun processFacet(facet: MyFacet) { - /* do something with */ facet.compute().data + // do something with + facet.compute().data } } - /* Make appTelemeter available to your application */ - val appTelemeter = Telemeter.build( - relays = listOf(MyRelay()), - facets = listOf(Prefix.App("My Application Name")), - ) + // Make appTelemeter available to your application + val appTelemeter = + Telemeter.build( + relays = listOf(MyRelay()), + facets = listOf(Prefix.App("My Application Name")), + ) fun onThingHappened() { appTelemeter.record(MyEvent("a thing happened")) @@ -67,12 +80,16 @@ internal fun childTelemeterSample(parentTelemeter: Telemeter) { } internal fun createTypedRelay() { - data class MyFacet(val myValue: String) : Facet - class MyTypedRelay : TypedRelay by Relay.buildTypedRelay( - { myFacet -> - myFacet.myValue - }, - ) + data class MyFacet( + val myValue: String, + ) : Facet + + class MyTypedRelay : + TypedRelay by Relay.buildTypedRelay( + { myFacet -> + myFacet.myValue + }, + ) } private interface Toggles { @@ -80,8 +97,9 @@ private interface Toggles { } internal fun samplePrintConfig() { - class PropertyBehaviorChangeConfig(private val toggles: Toggles) : - PrintRelay.Configuration by PrintRelay.Configuration.Default() { + class PropertyBehaviorChangeConfig( + private val toggles: Toggles, + ) : PrintRelay.Configuration by PrintRelay.Configuration.Default() { override var detailedMode: Boolean get() = toggles["PrintRelay Toggle"] set(_) = Unit diff --git a/telemetry/src/main/java/com/kroger/telemetry/StandardTelemeter.kt b/telemetry/src/main/java/com/kroger/telemetry/StandardTelemeter.kt index 1e9b910..857146d 100644 --- a/telemetry/src/main/java/com/kroger/telemetry/StandardTelemeter.kt +++ b/telemetry/src/main/java/com/kroger/telemetry/StandardTelemeter.kt @@ -43,43 +43,50 @@ internal class StandardTelemeter( private val parent: Telemeter?, internal val flowConfig: Telemeter.EventFlowConfig, ) : Telemeter { - private val coroutineScope = flowConfig.scope ?: CoroutineScope(Dispatchers.Default) - private val events = MutableSharedFlow( - replay = flowConfig.replay, - extraBufferCapacity = flowConfig.extraBufferCapacity, - onBufferOverflow = flowConfig.onBufferOverflow, - ) + private val events = + MutableSharedFlow( + replay = flowConfig.replay, + extraBufferCapacity = flowConfig.extraBufferCapacity, + onBufferOverflow = flowConfig.onBufferOverflow, + ) init { relays.forEach { relay -> - events.onEach { event -> - Result.runCatching { - relay.process(event) - }.onFailure { - val message = "An error was caught during Relay processing. It was $it" - val facet = Failure(message = message, throwable = it) - // Avoid loops - if (event.facets.contains(facet)) return@onEach - val failureEvent = object : Event { - override val description: String = message - override val facets: List = listOf(facet) - } - record(failureEvent) - } - }.launchIn(coroutineScope) + events + .onEach { event -> + Result + .runCatching { + relay.process(event) + }.onFailure { + val message = "An error was caught during Relay processing. It was $it" + val facet = Failure(message = message, throwable = it) + // Avoid loops + if (event.facets.contains(facet)) return@onEach + val failureEvent = + object : Event { + override val description: String = message + override val facets: List = listOf(facet) + } + record(failureEvent) + } + }.launchIn(coroutineScope) } } - override fun record(event: Event, withFacets: List?) { + override fun record( + event: Event, + withFacets: List?, + ) { // Note that this composition of facets into an anonymous object erases the original // type of the event. This means that any type checking in relays should depend on facets. val allFacets = (resolveFacets(event.facets) + (withFacets ?: listOf())).addMetaFacets() - val additionallyFacetedEvent = object : Event { - override val description = event.description - override val facets = allFacets - } + val additionallyFacetedEvent = + object : Event { + override val description = event.description + override val facets = allFacets + } coroutineScope.launch { events.emit(additionallyFacetedEvent) @@ -88,24 +95,26 @@ internal class StandardTelemeter( parent?.record(additionallyFacetedEvent) } - private fun resolveFacets(facets: List): List = if (facetResolvers.isNotEmpty()) { - val unresolvedFacets: List = facets.filterIsInstance() - val resolvedFacets: List = facets.filterNot { it is UnresolvedFacet } + private fun resolveFacets(facets: List): List = + if (facetResolvers.isNotEmpty()) { + val unresolvedFacets: List = facets.filterIsInstance() + val resolvedFacets: List = facets.filterNot { it is UnresolvedFacet } - val finalFacets: List = unresolvedFacets.flatMap { unresolvedFacet -> - try { - facetResolvers[unresolvedFacet::class.java]?.resolve(unresolvedFacet) - ?: listOf(unresolvedFacet) - } catch (e: Exception) { - // We must swallow this error to continue processing the rest of the facets, and continue on to the relays - listOf(unresolvedFacet) - } - } + resolvedFacets + val finalFacets: List = + unresolvedFacets.flatMap { unresolvedFacet -> + try { + facetResolvers[unresolvedFacet::class.java]?.resolve(unresolvedFacet) + ?: listOf(unresolvedFacet) + } catch (e: Exception) { + // We must swallow this error to continue processing the rest of the facets, and continue on to the relays + listOf(unresolvedFacet) + } + } + resolvedFacets - finalFacets - } else { - facets - } + finalFacets + } else { + facets + } private fun List.addMetaFacets(): List { // Only a leaf node Telemeter needs to propagate ThreadData to a Relay, so check if @@ -115,7 +124,9 @@ internal class StandardTelemeter( listOf( Thread.currentThread().let { ThreadData(it.name, it.stackTrace) }, ) - } else listOf() + } else { + listOf() + } /* telemeter facets start the list so that as we move back upwards through the telemeter chain events get automatic scoping, for example: grandChildFacets + ... @@ -123,7 +134,7 @@ internal class StandardTelemeter( parentFacets + (childFacets + (grandChildFacets + ...)) so they could be something like: listOf(Prefix.App + (Prefix.Module + (Prefix.Class))) - */ + */ return facets + threadFacet + this } } diff --git a/telemetry/src/main/java/com/kroger/telemetry/Telemeter.kt b/telemetry/src/main/java/com/kroger/telemetry/Telemeter.kt index 9ff9691..6118b3c 100644 --- a/telemetry/src/main/java/com/kroger/telemetry/Telemeter.kt +++ b/telemetry/src/main/java/com/kroger/telemetry/Telemeter.kt @@ -54,7 +54,10 @@ public interface Telemeter { * type of an Event will be overwritten when recorded, so type-checks on Events should be * avoided. Instead, type-checks should be used on [Facet]s. */ - public fun record(event: Event, withFacets: List? = null) + public fun record( + event: Event, + withFacets: List? = null, + ) /** * Used to configure the event flow that propagates events to relays. @@ -95,35 +98,36 @@ public interface Telemeter { facets: List = listOf(), facetResolvers: Map, FacetResolver> = mapOf(), flowConfig: EventFlowConfig = defaultTelemetryFlowConfig, - ): Telemeter { - return StandardTelemeter( + ): Telemeter = + StandardTelemeter( relays = relays, facetResolvers = facetResolvers, facets = facets, parent = null, flowConfig = flowConfig, ) - } /** * Default event flow configuration for Telemetry, to ensure event processing is not * blocked by slowest relay. */ - public val defaultTelemetryFlowConfig: EventFlowConfig = EventFlowConfig( - replay = 64, - extraBufferCapacity = 64, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) + public val defaultTelemetryFlowConfig: EventFlowConfig = + EventFlowConfig( + replay = 64, + extraBufferCapacity = 64, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) /** * Default event flow configuration used by the coroutine standard library, which * will suspend new events until the slowest relay has finished processed the last event. */ - public val defaultSharedFlowConfig: EventFlowConfig = EventFlowConfig( - replay = 0, - extraBufferCapacity = 0, - onBufferOverflow = BufferOverflow.SUSPEND, - ) + public val defaultSharedFlowConfig: EventFlowConfig = + EventFlowConfig( + replay = 0, + extraBufferCapacity = 0, + onBufferOverflow = BufferOverflow.SUSPEND, + ) } } @@ -139,10 +143,11 @@ public fun Telemeter.child( relays: List = listOf(), facets: List = listOf(), facetResolvers: Map, FacetResolver> = mapOf(), -): Telemeter = StandardTelemeter( - relays = relays, - facets = facets, - parent = this, - flowConfig = (this as? StandardTelemeter)?.flowConfig ?: Telemeter.defaultTelemetryFlowConfig, - facetResolvers = facetResolvers, -) +): Telemeter = + StandardTelemeter( + relays = relays, + facets = facets, + parent = this, + flowConfig = (this as? StandardTelemeter)?.flowConfig ?: Telemeter.defaultTelemetryFlowConfig, + facetResolvers = facetResolvers, + ) diff --git a/telemetry/src/main/java/com/kroger/telemetry/facet/DeveloperMetricsFacet.kt b/telemetry/src/main/java/com/kroger/telemetry/facet/DeveloperMetricsFacet.kt index 93303c8..2a6622c 100644 --- a/telemetry/src/main/java/com/kroger/telemetry/facet/DeveloperMetricsFacet.kt +++ b/telemetry/src/main/java/com/kroger/telemetry/facet/DeveloperMetricsFacet.kt @@ -40,7 +40,9 @@ public interface DeveloperMetricsFacet : Facet.Computed?> { } internal fun sampleDeveloperMetricsFacet() { - data class AThingyHappenedDeveloperMetricsFacet(val tag: String) : DeveloperMetricsFacet { + data class AThingyHappenedDeveloperMetricsFacet( + val tag: String, + ) : DeveloperMetricsFacet { override val eventName: String = "A thingy happened!" override val compute: () -> Map = { mapOf( diff --git a/telemetry/src/main/java/com/kroger/telemetry/facet/Failure.kt b/telemetry/src/main/java/com/kroger/telemetry/facet/Failure.kt index b999d92..24ba3bc 100644 --- a/telemetry/src/main/java/com/kroger/telemetry/facet/Failure.kt +++ b/telemetry/src/main/java/com/kroger/telemetry/facet/Failure.kt @@ -28,4 +28,7 @@ package com.kroger.telemetry.facet * A facet representing an application failure. This is not limited to crashes, but could be used * to represent network errors, for example. */ -public data class Failure(val message: String? = null, val throwable: Throwable? = null) : Facet +public data class Failure( + val message: String? = null, + val throwable: Throwable? = null, +) : Facet diff --git a/telemetry/src/main/java/com/kroger/telemetry/facet/Prefix.kt b/telemetry/src/main/java/com/kroger/telemetry/facet/Prefix.kt index 3774e08..2f0851a 100644 --- a/telemetry/src/main/java/com/kroger/telemetry/facet/Prefix.kt +++ b/telemetry/src/main/java/com/kroger/telemetry/facet/Prefix.kt @@ -37,13 +37,23 @@ import com.kroger.telemetry.child public sealed class Prefix : Facet { public abstract val value: String - public data class App(override val value: String) : Prefix() + public data class App( + override val value: String, + ) : Prefix() - public data class Module(override val value: String) : Prefix() + public data class Module( + override val value: String, + ) : Prefix() - public data class Screen(override val value: String) : Prefix() + public data class Screen( + override val value: String, + ) : Prefix() - public data class Class(override val value: String) : Prefix() + public data class Class( + override val value: String, + ) : Prefix() - public data class LocalScope(override val value: String) : Prefix() + public data class LocalScope( + override val value: String, + ) : Prefix() } diff --git a/telemetry/src/main/java/com/kroger/telemetry/facet/Significance.kt b/telemetry/src/main/java/com/kroger/telemetry/facet/Significance.kt index 0d89d52..375bf0f 100644 --- a/telemetry/src/main/java/com/kroger/telemetry/facet/Significance.kt +++ b/telemetry/src/main/java/com/kroger/telemetry/facet/Significance.kt @@ -35,5 +35,5 @@ public enum class Significance : Facet { INFORMATIONAL, WARNING, ERROR, - INTERNAL_ERROR + INTERNAL_ERROR, } diff --git a/telemetry/src/main/java/com/kroger/telemetry/relay/PrintRelay.kt b/telemetry/src/main/java/com/kroger/telemetry/relay/PrintRelay.kt index 0a4a3c4..dfb9717 100644 --- a/telemetry/src/main/java/com/kroger/telemetry/relay/PrintRelay.kt +++ b/telemetry/src/main/java/com/kroger/telemetry/relay/PrintRelay.kt @@ -46,7 +46,6 @@ public open class PrintRelay( println("${message.tag} --- ${message.value}") }, ) : Relay { - /** * @property defaultSignificance Significance level to attach to Events if the Event does not include one. * @property minimumSignificance If an Event is less significant, it will not be processed. @@ -75,15 +74,17 @@ public open class PrintRelay( val tag = event.generateTag() val significance = event.getSignificance() if (significance < configuration.minimumSignificance) return - val message = Message( - tag = tag, - significance = significance, - value = if (configuration.detailedMode.not()) { - event.toSimpleMessage() - } else { - event.toDetailedMessage() - }, - ) + val message = + Message( + tag = tag, + significance = significance, + value = + if (configuration.detailedMode.not()) { + event.toSimpleMessage() + } else { + event.toDetailedMessage() + }, + ) printer(message) } @@ -98,25 +99,27 @@ public open class PrintRelay( val prefixes = facets.filterIsInstance() val significance = getSignificance() return if (configuration.detailedMode) { - val allPrefixes = prefixes.joinToString(separator) { it.value } + val allPrefixes = prefixes.joinToString(SEPARATOR) { it.value } listOf( Telemeter.TAG, significance, allPrefixes, - ).joinToString(separator) + ).joinToString(SEPARATOR) } else { val mostLocalPrefix = prefixes.lastOrNull()?.value listOfNotNull( Telemeter.TAG, significance, mostLocalPrefix, - ).joinToString(separator) + ).joinToString(SEPARATOR) } } // Get the highest significance level attached to the event. - private fun Event.getSignificance(): Significance = facets - .filterIsInstance().maxOrNull() ?: configuration.defaultSignificance + private fun Event.getSignificance(): Significance = + facets + .filterIsInstance() + .maxOrNull() ?: configuration.defaultSignificance public data class Message( val tag: String, @@ -125,7 +128,7 @@ public open class PrintRelay( ) public companion object { - internal const val separator = " | " + internal const val SEPARATOR = " | " } } @@ -136,12 +139,13 @@ public fun Telemeter.log( tag: String? = null, message: String, significance: Significance = Significance.DEBUG, -): Unit = record( - object : Event { - override val description: String = (tag?.let { "$tag - " } ?: "") + message - override val facets: List = listOf(significance) - }, -) +): Unit = + record( + object : Event { + override val description: String = (tag?.let { "$tag - " } ?: "") + message + override val facets: List = listOf(significance) + }, + ) /** * Convenience method to log a message to Logcat with a custom [Significance] @@ -152,59 +156,77 @@ public fun Telemeter.logError( message: String, significance: Significance = Significance.ERROR, throwable: Throwable? = null, -): Unit = record( - object : Event { - val usedTag = tag?.let { - "$tag " - } ?: "" - val usedThrowableMessage = throwable?.let { - " - ${throwable.message}" - } ?: "" - val usedMessage = usedTag + message + usedThrowableMessage - - // This is a list so it can be easily combined below - val failureFacet = throwable?.let { - listOf(Failure(usedMessage, throwable)) - } ?: listOf() - override val description: String = usedMessage - override val facets: List = listOf(significance) + failureFacet - }, -) +): Unit = + record( + object : Event { + val usedTag = + tag?.let { + "$tag " + } ?: "" + val usedThrowableMessage = + throwable?.let { + " - ${throwable.message}" + } ?: "" + val usedMessage = usedTag + message + usedThrowableMessage + + // This is a list so it can be easily combined below + val failureFacet = + throwable?.let { + listOf(Failure(usedMessage, throwable)) + } ?: listOf() + override val description: String = usedMessage + override val facets: List = listOf(significance) + failureFacet + }, + ) /** * Convenience method to log a message to Logcat with [Significance.VERBOSE] */ -public fun Telemeter.v(tag: String? = null, message: String): Unit = - log(tag, message, Significance.VERBOSE) +public fun Telemeter.v( + tag: String? = null, + message: String, +): Unit = log(tag, message, Significance.VERBOSE) /** * Convenience method to log a message to Logcat with [Significance.DEBUG] */ -public fun Telemeter.d(tag: String? = null, message: String): Unit = - log(tag, message, Significance.DEBUG) +public fun Telemeter.d( + tag: String? = null, + message: String, +): Unit = log(tag, message, Significance.DEBUG) /** * Convenience method to log a message to Logcat with [Significance.INFORMATIONAL] */ -public fun Telemeter.i(tag: String? = null, message: String): Unit = - log(tag, message, Significance.INFORMATIONAL) +public fun Telemeter.i( + tag: String? = null, + message: String, +): Unit = log(tag, message, Significance.INFORMATIONAL) /** * Convenience method to log a message to Logcat with [Significance.WARNING] */ -public fun Telemeter.w(tag: String? = null, message: String): Unit = - log(tag, message, Significance.WARNING) +public fun Telemeter.w( + tag: String? = null, + message: String, +): Unit = log(tag, message, Significance.WARNING) /** * Convenience method to log a message to Logcat with [Significance.ERROR]. * Throwables will be converted to [Failure] facets and included in the event. */ -public fun Telemeter.e(tag: String? = null, message: String, throwable: Throwable? = null): Unit = - logError(tag, message, Significance.ERROR, throwable) +public fun Telemeter.e( + tag: String? = null, + message: String, + throwable: Throwable? = null, +): Unit = logError(tag, message, Significance.ERROR, throwable) /** * Convenience method to log a message to Logcat with [Significance.INTERNAL_ERROR] * Throwables will be converted to [Failure] facets and included in the event. */ -public fun Telemeter.wtf(tag: String? = null, message: String, throwable: Throwable? = null): Unit = - logError(tag, message, Significance.INTERNAL_ERROR, throwable) +public fun Telemeter.wtf( + tag: String? = null, + message: String, + throwable: Throwable? = null, +): Unit = logError(tag, message, Significance.INTERNAL_ERROR, throwable) diff --git a/telemetry/src/test/java/com/kroger/telemetry/EventTest.kt b/telemetry/src/test/java/com/kroger/telemetry/EventTest.kt index a265bf0..94eb74b 100644 --- a/telemetry/src/test/java/com/kroger/telemetry/EventTest.kt +++ b/telemetry/src/test/java/com/kroger/telemetry/EventTest.kt @@ -29,14 +29,16 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test private object ModuleFacetOne : Facet + private object ModuleFacetTwo : Facet + private object ModuleFacetThree : Facet -private const val featureOneEventDesc = "i'm a feature one event" +private const val FEATURE_ONE_EVENT_DESC = "i'm a feature one event" private sealed class ModuleEvent : Event { sealed class FeatureOneEvent : ModuleEvent() { - override val description: String = featureOneEventDesc + override val description: String = FEATURE_ONE_EVENT_DESC override val facets: List = listOf(ModuleFacetOne) object EventOne : FeatureOneEvent() { @@ -54,7 +56,7 @@ private sealed class ModuleEvent : Event { public class EventTest { @Test public fun `GIVEN event hierarchy WHEN description defined by super THEN unnecessary in subclasses`() { - assertEquals(featureOneEventDesc, ModuleEvent.FeatureOneEvent.EventOne.description) + assertEquals(FEATURE_ONE_EVENT_DESC, ModuleEvent.FeatureOneEvent.EventOne.description) } @Test diff --git a/telemetry/src/test/java/com/kroger/telemetry/RelayTest.kt b/telemetry/src/test/java/com/kroger/telemetry/RelayTest.kt index 48189aa..29b9bed 100644 --- a/telemetry/src/test/java/com/kroger/telemetry/RelayTest.kt +++ b/telemetry/src/test/java/com/kroger/telemetry/RelayTest.kt @@ -37,11 +37,17 @@ internal class RelayTest { @Test fun `GIVEN relay with filter in processing WHEN event processed THEN facets easily extracted`() = runTest { - data class StringFacet(val value: String) : Facet - data class IntFacet(val value: Int) : Facet + data class StringFacet( + val value: String, + ) : Facet + + data class IntFacet( + val value: Int, + ) : Facet class BasicRelay : Relay { var processedCount = 0 + override suspend fun process(event: Event) { event.facets.filterIsInstance().forEach(::process) } @@ -53,10 +59,11 @@ internal class RelayTest { } val relay = BasicRelay() - val event = FakeEvent( - description = "test post please ignore", - facets = listOf(StringFacet("test facet please confirm"), IntFacet(1)), - ) + val event = + FakeEvent( + description = "test post please ignore", + facets = listOf(StringFacet("test facet please confirm"), IntFacet(1)), + ) relay.process(event) assertEquals(1, relay.processedCount) } @@ -64,8 +71,13 @@ internal class RelayTest { @Test fun `GIVEN typed relay WHEN event processed with relevant facet type THEN event is processed`() = runTest { - data class StringFacet(val value: String) : Facet - data class IntFacet(val value: Int) : Facet + data class StringFacet( + val value: String, + ) : Facet + + data class IntFacet( + val value: Int, + ) : Facet class BasicRelay : TypedRelay { var processedCount = 0 @@ -78,10 +90,11 @@ internal class RelayTest { } val relay = BasicRelay() - val event = FakeEvent( - description = "test post please ignore", - facets = listOf(StringFacet("test facet please confirm"), IntFacet(1)), - ) + val event = + FakeEvent( + description = "test post please ignore", + facets = listOf(StringFacet("test facet please confirm"), IntFacet(1)), + ) relay.process(event) assertEquals(1, relay.processedCount) } @@ -89,8 +102,13 @@ internal class RelayTest { @Test fun `GIVEN typed relay WHEN event processed without relevant facet type THEN event is not processed`() = runTest { - data class StringFacet(val value: String) : Facet - data class IntFacet(val value: Int) : Facet + data class StringFacet( + val value: String, + ) : Facet + + data class IntFacet( + val value: Int, + ) : Facet class BasicRelay : TypedRelay { var processedCount = 0 @@ -103,22 +121,24 @@ internal class RelayTest { } val relay = BasicRelay() - val event = FakeEvent( - description = "test post please ignore", - facets = listOf(IntFacet(1)), - ) + val event = + FakeEvent( + description = "test post please ignore", + facets = listOf(IntFacet(1)), + ) relay.process(event) assertEquals(0, relay.processedCount) } @Test fun `GIVEN facet to use in typed relay WHEN defining class THEN helper function is useful as delegate`() { - data class TestFacet(val passed: Boolean) : Facet + data class TestFacet( + val passed: Boolean, + ) : Facet var passed = false - class TestTypedRelay : - TypedRelay by Relay.buildTypedRelay({ facet -> passed = facet.passed }) + class TestTypedRelay : TypedRelay by Relay.buildTypedRelay({ facet -> passed = facet.passed }) val passingFacet = TestFacet(passed = true) runTest { diff --git a/telemetry/src/test/java/com/kroger/telemetry/TelemeterTest.kt b/telemetry/src/test/java/com/kroger/telemetry/TelemeterTest.kt index 91874b9..d503afa 100644 --- a/telemetry/src/test/java/com/kroger/telemetry/TelemeterTest.kt +++ b/telemetry/src/test/java/com/kroger/telemetry/TelemeterTest.kt @@ -45,25 +45,27 @@ import kotlin.time.Duration.Companion.milliseconds internal class TelemeterTest { @Test - fun `GIVEN telemeter with relays WHEN event recorded THEN relays receive events`() = runTest { - var relayOneProcessCount = 0 - val relayOne = FakeRelay { relayOneProcessCount += 1 } - - var relayTwoProcessCount = 0 - val relayTwo = FakeRelay { relayTwoProcessCount += 1 } - - val telemeter = Telemeter.build( - relays = listOf(relayOne, relayTwo), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), - ) - - val numEvents = 10_000 - repeat(numEvents) { - telemeter.record(FakeEvent(description = "event num $it")) - testScheduler.runCurrent() + fun `GIVEN telemeter with relays WHEN event recorded THEN relays receive events`() = + runTest { + var relayOneProcessCount = 0 + val relayOne = FakeRelay { relayOneProcessCount += 1 } + + var relayTwoProcessCount = 0 + val relayTwo = FakeRelay { relayTwoProcessCount += 1 } + + val telemeter = + Telemeter.build( + relays = listOf(relayOne, relayTwo), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), + ) + + val numEvents = 10_000 + repeat(numEvents) { + telemeter.record(FakeEvent(description = "event num $it")) + testScheduler.runCurrent() + } + assertTrue(relayOneProcessCount == numEvents && relayTwoProcessCount == numEvents) } - assertTrue(relayOneProcessCount == numEvents && relayTwoProcessCount == numEvents) - } @Test fun `GIVEN faceted telemeter WHEN event recorded THEN facets are added to incoming events`() = @@ -71,15 +73,17 @@ internal class TelemeterTest { class TelemeterFacet : Facet var facetIsPresent = false - val relay = FakeRelay { event -> - facetIsPresent = event.facets.any { facet -> facet is TelemeterFacet } - } + val relay = + FakeRelay { event -> + facetIsPresent = event.facets.any { facet -> facet is TelemeterFacet } + } - val telemeter = Telemeter.build( - relays = listOf(relay), - facets = listOf(TelemeterFacet()), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), - ) + val telemeter = + Telemeter.build( + relays = listOf(relay), + facets = listOf(TelemeterFacet()), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), + ) val event = FakeEvent() telemeter.record(event) @@ -90,52 +94,62 @@ internal class TelemeterTest { // Please don't actually do this. In a perfect world, no mutable data would enter the pipeline // and relays would avoid trying to mutate data @Test - fun `GIVEN mutable facet WHEN facet is mutated downstream THEN changes are propagated`() = runTest { - class MutableFacet(var mutableField: String) : Facet - - val mutatedString = "i'm a mutation" - val mutatingRelay = FakeRelay { - it.facets.filterIsInstance().forEach { facet -> - facet.mutableField = mutatedString - } - } + fun `GIVEN mutable facet WHEN facet is mutated downstream THEN changes are propagated`() = + runTest { + class MutableFacet( + var mutableField: String, + ) : Facet + + val mutatedString = "i'm a mutation" + val mutatingRelay = + FakeRelay { + it.facets.filterIsInstance().forEach { facet -> + facet.mutableField = mutatedString + } + } - var facetIsMutated = false - val mutationCheckingRelay = FakeRelay { - facetIsMutated = it.facets.filterIsInstance().any { facet -> - facet.mutableField == mutatedString - } - } + var facetIsMutated = false + val mutationCheckingRelay = + FakeRelay { + facetIsMutated = + it.facets.filterIsInstance().any { facet -> + facet.mutableField == mutatedString + } + } - val telemeter = Telemeter.build( - relays = listOf(mutatingRelay, mutationCheckingRelay), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), - ) + val telemeter = + Telemeter.build( + relays = listOf(mutatingRelay, mutationCheckingRelay), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), + ) - val event = FakeEvent("", listOf(MutableFacet("i'm mutating"))) + val event = FakeEvent("", listOf(MutableFacet("i'm mutating"))) - telemeter.record(event) - testScheduler.runCurrent() - assertTrue(facetIsMutated) - } + telemeter.record(event) + testScheduler.runCurrent() + assertTrue(facetIsMutated) + } @Test fun `GIVEN parent telemeter WHEN child telemeter created THEN child can attach additional facets scoped to child`() = runTest { class ParentFacet : Facet + class ChildFacet : Facet val recordedEvents = mutableListOf() - val fakeRelay = FakeRelay { - recordedEvents.add(it) - } + val fakeRelay = + FakeRelay { + recordedEvents.add(it) + } val parentFacet = ParentFacet() val childFacet = ChildFacet() - val parent = Telemeter.build( - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), - relays = listOf(fakeRelay), - facets = listOf(parentFacet), - ) + val parent = + Telemeter.build( + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), + relays = listOf(fakeRelay), + facets = listOf(parentFacet), + ) val child = parent.child(facets = listOf(childFacet)) val event = FakeEvent() @@ -150,403 +164,461 @@ internal class TelemeterTest { } @Test - fun `GIVEN telemeter with default flow config WHEN event recorded by relay with long running process function THEN shorter relay processing not blocked`() = runTest { - var completedLongRelayJobs = 0 - val longRelay = FakeRelay { - delay(500) - completedLongRelayJobs += 1 - } - var completedShortRelayJobs = 0 - val shortRelay = FakeRelay { - completedShortRelayJobs += 1 - } - - val telemeter = Telemeter.build( - relays = listOf(longRelay, shortRelay), - facets = listOf(), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), - ) + fun `GIVEN telemeter with default flow config WHEN event recorded by relay with long running process function THEN shorter relay processing not blocked`() = + runTest { + var completedLongRelayJobs = 0 + val longRelay = + FakeRelay { + delay(500) + completedLongRelayJobs += 1 + } + var completedShortRelayJobs = 0 + val shortRelay = + FakeRelay { + completedShortRelayJobs += 1 + } - val numEvents = 10_000 - repeat(numEvents) { - telemeter.record(FakeEvent()) - testScheduler.runCurrent() + val telemeter = + Telemeter.build( + relays = listOf(longRelay, shortRelay), + facets = listOf(), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), + ) + + val numEvents = 10_000 + repeat(numEvents) { + telemeter.record(FakeEvent()) + testScheduler.runCurrent() + } + assertTrue(completedShortRelayJobs > completedLongRelayJobs) + assertEquals(numEvents, completedShortRelayJobs) } - assertTrue(completedShortRelayJobs > completedLongRelayJobs) - assertEquals(numEvents, completedShortRelayJobs) - } @Test - fun `GIVEN telemeter with default shared flow config WHEN event recorded by relay with long running process function THEN all events processed`() = runTest(UnconfinedTestDispatcher()) { - val longRelayDelay = 500.milliseconds - var completedLongRelayJobs = 0 - val longRelay = FakeRelay { - delay(longRelayDelay) - completedLongRelayJobs += 1 - } - var completedShortRelayJobs = 0 - val shortRelay = FakeRelay { - completedShortRelayJobs += 1 - } + fun `GIVEN telemeter with default shared flow config WHEN event recorded by relay with long running process function THEN all events processed`() = + runTest(UnconfinedTestDispatcher()) { + val longRelayDelay = 500.milliseconds + var completedLongRelayJobs = 0 + val longRelay = + FakeRelay { + delay(longRelayDelay) + completedLongRelayJobs += 1 + } + var completedShortRelayJobs = 0 + val shortRelay = + FakeRelay { + completedShortRelayJobs += 1 + } - val telemeter = Telemeter.build( - relays = listOf(longRelay, shortRelay), - facets = listOf(), - flowConfig = Telemeter.defaultSharedFlowConfig.copy(scope = backgroundScope), - ) + val telemeter = + Telemeter.build( + relays = listOf(longRelay, shortRelay), + facets = listOf(), + flowConfig = Telemeter.defaultSharedFlowConfig.copy(scope = backgroundScope), + ) - val numEvents = 10_000 - repeat(numEvents) { - telemeter.record(FakeEvent()) + val numEvents = 10_000 + repeat(numEvents) { + telemeter.record(FakeEvent()) + } + testScheduler.advanceTimeBy(longRelayDelay * (numEvents + 1)) + assertTrue(completedShortRelayJobs == completedLongRelayJobs) + assertEquals(numEvents, completedShortRelayJobs) + assertEquals(numEvents, completedLongRelayJobs) } - testScheduler.advanceTimeBy(longRelayDelay * (numEvents + 1)) - assertTrue(completedShortRelayJobs == completedLongRelayJobs) - assertEquals(numEvents, completedShortRelayJobs) - assertEquals(numEvents, completedLongRelayJobs) - } @Test - fun `GIVEN child telemeter with additional relay WHEN event recorded THEN additional relay will receive events`() = runTest { - val childFacet = object : Facet {} - val parent = Telemeter.build( - relays = listOf(), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), - ) - - var childProcessed = false - val childRelay = FakeRelay { - println(it) - childProcessed = true - } - val child = parent.child( - listOf(childRelay), - listOf(childFacet), - ) + fun `GIVEN child telemeter with additional relay WHEN event recorded THEN additional relay will receive events`() = + runTest { + val childFacet = object : Facet {} + val parent = + Telemeter.build( + relays = listOf(), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), + ) + + var childProcessed = false + val childRelay = + FakeRelay { + println(it) + childProcessed = true + } + val child = + parent.child( + listOf(childRelay), + listOf(childFacet), + ) - child.record(FakeEvent()) - testScheduler.runCurrent() - assertTrue(childProcessed) - } + child.record(FakeEvent()) + testScheduler.runCurrent() + assertTrue(childProcessed) + } /** * This might not be desired behavior, but ultimately users should take care not to add repeated * relays. */ @Test - fun `GIVEN parent and child telemeter with identical relay added to both WHEN events recorded THEN processed twice`() = runTest { - var numProcessed = 0 + fun `GIVEN parent and child telemeter with identical relay added to both WHEN events recorded THEN processed twice`() = + runTest { + var numProcessed = 0 - class RepeatedRelay : Relay { - override suspend fun process(event: Event) { - numProcessed += 1 - println(event) + class RepeatedRelay : Relay { + override suspend fun process(event: Event) { + numProcessed += 1 + println(event) + } } - } - val parent = Telemeter.build( - relays = listOf(RepeatedRelay()), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), - ) - val child = parent.child(relays = listOf(RepeatedRelay())) + val parent = + Telemeter.build( + relays = listOf(RepeatedRelay()), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), + ) + val child = parent.child(relays = listOf(RepeatedRelay())) - child.record(FakeEvent()) - testScheduler.runCurrent() - assertTrue(numProcessed == 2) - } + child.record(FakeEvent()) + testScheduler.runCurrent() + assertTrue(numProcessed == 2) + } @Test - fun `GIVEN event to be recorded WHEN passed additional facets THEN facets are relayed`() = runTest { - val recorded = mutableListOf() - val fakeRelay = FakeRelay { - if (it.facets.any { facet -> facet is Prefix.App }) { - recorded.add(it) - } - } + fun `GIVEN event to be recorded WHEN passed additional facets THEN facets are relayed`() = + runTest { + val recorded = mutableListOf() + val fakeRelay = + FakeRelay { + if (it.facets.any { facet -> facet is Prefix.App }) { + recorded.add(it) + } + } - val telemeter = Telemeter.build( - relays = listOf(fakeRelay), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), - ) + val telemeter = + Telemeter.build( + relays = listOf(fakeRelay), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), + ) + + val additionalFacet = Prefix.App("") + val event = + object : Event { + override val description: String = "test" + override val facets: List = listOf() + } - val additionalFacet = Prefix.App("") - val event = object : Event { - override val description: String = "test" - override val facets: List = listOf() + telemeter.record(event, listOf(additionalFacet)) + testScheduler.runCurrent() + assertTrue(recorded[0].facets[0] is Prefix.App) } - telemeter.record(event, listOf(additionalFacet)) - testScheduler.runCurrent() - assertTrue(recorded[0].facets[0] is Prefix.App) - } - @Test - fun `GIVEN prefixes attached through several children WHEN event recorded THEN prefixes remain in order they were added`() = runTest { - val recorded = mutableListOf() - val fakeRelay = FakeRelay { - recorded.add(it) - } + fun `GIVEN prefixes attached through several children WHEN event recorded THEN prefixes remain in order they were added`() = + runTest { + val recorded = mutableListOf() + val fakeRelay = + FakeRelay { + recorded.add(it) + } - val firstPrefix = Prefix.App("") - val secondPrefix = Prefix.LocalScope("") - val thirdPrefix = Prefix.Screen("") - val grandChild = Telemeter - .build( - relays = listOf(fakeRelay), - facets = listOf(firstPrefix), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), + val firstPrefix = Prefix.App("") + val secondPrefix = Prefix.LocalScope("") + val thirdPrefix = Prefix.Screen("") + val grandChild = + Telemeter + .build( + relays = listOf(fakeRelay), + facets = listOf(firstPrefix), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), + ).child(facets = listOf(secondPrefix)) + .child(facets = listOf(thirdPrefix)) + + grandChild.record( + object : Event { + override val description: String = "" + override val facets: List = listOf() + }, ) - .child(facets = listOf(secondPrefix)) - .child(facets = listOf(thirdPrefix)) - - grandChild.record( - object : Event { - override val description: String = "" - override val facets: List = listOf() - }, - ) - testScheduler.runCurrent() - assertEquals(firstPrefix, recorded[0].facets[0]) - assertEquals(secondPrefix, recorded[0].facets[1]) - assertEquals(thirdPrefix, recorded[0].facets[2]) - } + testScheduler.runCurrent() + assertEquals(firstPrefix, recorded[0].facets[0]) + assertEquals(secondPrefix, recorded[0].facets[1]) + assertEquals(thirdPrefix, recorded[0].facets[2]) + } @Test - fun `GIVEN computed facet WHEN recorded THEN computation can be run in relay`() = runTest { - var processed = false - val relay = object : TypedRelay> { - override val type: Class> = Facet.Computed::class.java - override suspend fun processFacet(facet: Facet.Computed<*>) { - processed = facet.compute() as Boolean - } - } + fun `GIVEN computed facet WHEN recorded THEN computation can be run in relay`() = + runTest { + var processed = false + val relay = + object : TypedRelay> { + override val type: Class> = Facet.Computed::class.java + + override suspend fun processFacet(facet: Facet.Computed<*>) { + processed = facet.compute() as Boolean + } + } - val telemeter = Telemeter.build( - relays = listOf(relay), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), - ) - val computedFacet = object : Facet.Computed { - override val compute: () -> Boolean = { - true - } + val telemeter = + Telemeter.build( + relays = listOf(relay), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), + ) + val computedFacet = + object : Facet.Computed { + override val compute: () -> Boolean = { + true + } + } + telemeter.record(FakeEvent(facets = listOf(computedFacet))) + testScheduler.runCurrent() + assertTrue(processed) } - telemeter.record(FakeEvent(facets = listOf(computedFacet))) - testScheduler.runCurrent() - assertTrue(processed) - } @Test - fun `GIVEN lazy facet WHEN recorded THEN lazy value will be result of computation`() = runTest { - var computedCount: Int? = null - val relay = object : TypedRelay> { - override val type: Class> = Facet.Lazy::class.java - override suspend fun processFacet(facet: Facet.Lazy<*>) { - computedCount = facet.value as Int - } - } + fun `GIVEN lazy facet WHEN recorded THEN lazy value will be result of computation`() = + runTest { + var computedCount: Int? = null + val relay = + object : TypedRelay> { + override val type: Class> = Facet.Lazy::class.java + + override suspend fun processFacet(facet: Facet.Lazy<*>) { + computedCount = facet.value as Int + } + } - val telemeter = Telemeter.build( - relays = listOf(relay), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), - ) - val lazyFacet = object : Facet.Lazy() { - override val compute: () -> Int = { 1 } - } + val telemeter = + Telemeter.build( + relays = listOf(relay), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), + ) + val lazyFacet = + object : Facet.Lazy() { + override val compute: () -> Int = { 1 } + } - telemeter.record(FakeEvent(facets = listOf(lazyFacet))) - testScheduler.runCurrent() - assertEquals(1, computedCount) - } + telemeter.record(FakeEvent(facets = listOf(lazyFacet))) + testScheduler.runCurrent() + assertEquals(1, computedCount) + } @Test - fun `GIVEN telemeter configured to track thread data WHEN event recorded THEN additional facet is included`() = runTest { - val currentThreadName = Thread.currentThread().name - val recordedFacets = mutableListOf() - val relay = FakeRelay { recordedFacets.addAll(it.facets) } - - val telemeter = Telemeter.build( - relays = listOf(relay), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy( - shouldPropagateThreadData = true, - scope = backgroundScope, - ), - ) - - telemeter.record(FakeEvent()) - testScheduler.runCurrent() - val threadData = recordedFacets[0] as ThreadData - assertEquals(currentThreadName, threadData.threadName) - assertTrue(threadData.currentStackTrace.isNotEmpty()) - } + fun `GIVEN telemeter configured to track thread data WHEN event recorded THEN additional facet is included`() = + runTest { + val currentThreadName = Thread.currentThread().name + val recordedFacets = mutableListOf() + val relay = FakeRelay { recordedFacets.addAll(it.facets) } + + val telemeter = + Telemeter.build( + relays = listOf(relay), + flowConfig = + Telemeter.defaultTelemetryFlowConfig.copy( + shouldPropagateThreadData = true, + scope = backgroundScope, + ), + ) + + telemeter.record(FakeEvent()) + testScheduler.runCurrent() + val threadData = recordedFacets[0] as ThreadData + assertEquals(currentThreadName, threadData.threadName) + assertTrue(threadData.currentStackTrace.isNotEmpty()) + } @Test - fun `GIVEN relay that processes on different thread WHEN event recorded with thread data THEN original thread is preserved`() = runTest { - val currentThreadName = Thread.currentThread().name - val dispatcher = StandardTestDispatcher(testScheduler) - val recordedFacets = mutableListOf() - var processedThread = "" - - val relay = FakeRelay { - launch { - withContext(dispatcher) { - recordedFacets.addAll(it.facets) - processedThread = Thread.currentThread().name + fun `GIVEN relay that processes on different thread WHEN event recorded with thread data THEN original thread is preserved`() = + runTest { + val currentThreadName = Thread.currentThread().name + val dispatcher = StandardTestDispatcher(testScheduler) + val recordedFacets = mutableListOf() + var processedThread = "" + + val relay = + FakeRelay { + launch { + withContext(dispatcher) { + recordedFacets.addAll(it.facets) + processedThread = Thread.currentThread().name + } + } } - } - } - val telemeter = Telemeter.build( - relays = listOf(relay), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy( - shouldPropagateThreadData = true, - scope = backgroundScope, - ), - ) - - telemeter.record(FakeEvent()) - testScheduler.runCurrent() - while (processedThread.isEmpty()) Unit - val threadData = recordedFacets[0] as ThreadData - assertEquals(currentThreadName, threadData.threadName) - assertTrue(processedThread != "" && processedThread != threadData.threadName) - } + val telemeter = + Telemeter.build( + relays = listOf(relay), + flowConfig = + Telemeter.defaultTelemetryFlowConfig.copy( + shouldPropagateThreadData = true, + scope = backgroundScope, + ), + ) - @Test - fun `GIVEN telemeter tree with more than one node configured to record thread data WHEN event recorded THEN only one thread data recorded`() = runTest { - val recordedFacets = mutableListOf() - val relay = FakeRelay { recordedFacets.addAll(it.facets) } - - val child = Telemeter.build( - relays = listOf(relay), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy( - shouldPropagateThreadData = true, - scope = backgroundScope, - ), - ).child(listOf()) - - child.record(FakeEvent()) - testScheduler.runCurrent() - val threadDataFacets = recordedFacets.filterIsInstance() - assertEquals(1, threadDataFacets.size) - } + telemeter.record(FakeEvent()) + testScheduler.runCurrent() + while (processedThread.isEmpty()) Unit + val threadData = recordedFacets[0] as ThreadData + assertEquals(currentThreadName, threadData.threadName) + assertTrue(processedThread != "" && processedThread != threadData.threadName) + } @Test - fun `GIVEN telemeter with relay WHEN relay throws THEN telemeter catches and records error without looping`() = runTest { - val recorded = mutableListOf() - val goodRelay = FakeRelay { - recorded.add(it) - } - val exception = NullPointerException() - val badRelay = FakeRelay { - // This would loop if the implementation didn't prevent it - throw exception + fun `GIVEN telemeter tree with more than one node configured to record thread data WHEN event recorded THEN only one thread data recorded`() = + runTest { + val recordedFacets = mutableListOf() + val relay = FakeRelay { recordedFacets.addAll(it.facets) } + + val child = + Telemeter + .build( + relays = listOf(relay), + flowConfig = + Telemeter.defaultTelemetryFlowConfig.copy( + shouldPropagateThreadData = true, + scope = backgroundScope, + ), + ).child(listOf()) + + child.record(FakeEvent()) + testScheduler.runCurrent() + val threadDataFacets = recordedFacets.filterIsInstance() + assertEquals(1, threadDataFacets.size) } - val telemeter = Telemeter.build( - relays = listOf(badRelay, goodRelay), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), - ) - - telemeter.record(FakeEvent()) - testScheduler.runCurrent() - val failureEvents = recorded.filter { it.facets.any { facet -> facet is Failure } } - val failureFacet = failureEvents[0].facets[0] as Failure - assertEquals(exception, failureFacet.throwable) - } - @Test - fun `Given telemeter with facetResolvers, When record is called with unresolved facets, Then they should be resolved`() = runTest { - val recorded = mutableListOf() - val recordingRelay = FakeRelay { - recorded.add(it) - } + fun `GIVEN telemeter with relay WHEN relay throws THEN telemeter catches and records error without looping`() = + runTest { + val recorded = mutableListOf() + val goodRelay = + FakeRelay { + recorded.add(it) + } + val exception = NullPointerException() + val badRelay = + FakeRelay { + // This would loop if the implementation didn't prevent it + throw exception + } + + val telemeter = + Telemeter.build( + relays = listOf(badRelay, goodRelay), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), + ) - class TestUnresolvedFacet : UnresolvedFacet { - val badNumber = 2 + telemeter.record(FakeEvent()) + testScheduler.runCurrent() + val failureEvents = recorded.filter { it.facets.any { facet -> facet is Failure } } + val failureFacet = failureEvents[0].facets[0] as Failure + assertEquals(exception, failureFacet.throwable) } - data class ResolvedFacet(val goodNumber: Int) : Facet + @Test + fun `Given telemeter with facetResolvers, When record is called with unresolved facets, Then they should be resolved`() = + runTest { + val recorded = mutableListOf() + val recordingRelay = + FakeRelay { + recorded.add(it) + } - class TestFacetResolver : FacetResolver { - override fun getType(): Class<*> = TestUnresolvedFacet::class.java + class TestUnresolvedFacet : UnresolvedFacet { + val badNumber = 2 + } - override fun resolve(unresolvedFacet: UnresolvedFacet): List = - if (unresolvedFacet is TestUnresolvedFacet) { - listOf(ResolvedFacet(unresolvedFacet.badNumber * 2)) - } else { - listOf(unresolvedFacet) - } - } + data class ResolvedFacet( + val goodNumber: Int, + ) : Facet - val testFacetResolver = TestFacetResolver() + class TestFacetResolver : FacetResolver { + override fun getType(): Class<*> = TestUnresolvedFacet::class.java - val telemeter = Telemeter.build( - relays = listOf(recordingRelay), - facetResolvers = mapOf(testFacetResolver.getType() to testFacetResolver), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), - ) + override fun resolve(unresolvedFacet: UnresolvedFacet): List = + if (unresolvedFacet is TestUnresolvedFacet) { + listOf(ResolvedFacet(unresolvedFacet.badNumber * 2)) + } else { + listOf(unresolvedFacet) + } + } - val numEvents = 10 - for (i in 1..numEvents) { - val event = FakeEvent(description = "event num $i", listOf(TestUnresolvedFacet())) - telemeter.record(event) - } + val testFacetResolver = TestFacetResolver() - recorded.forEach { - assert(it.facets.filterIsInstance().isEmpty()) + val telemeter = + Telemeter.build( + relays = listOf(recordingRelay), + facetResolvers = mapOf(testFacetResolver.getType() to testFacetResolver), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), + ) - assert(it.facets.first() is ResolvedFacet) - assert((it.facets.first() as ResolvedFacet).goodNumber == 4) - } - } + val numEvents = 10 + for (i in 1..numEvents) { + val event = FakeEvent(description = "event num $i", listOf(TestUnresolvedFacet())) + telemeter.record(event) + } - @Test - fun `Given telemeter with a facetResolver that returns two facets, When record is called with an unresolved facet, Then return 2 resolved facets`() = runTest { - val recorded = mutableListOf() - val recordingRelay = FakeRelay { - recorded.add(it) - } + recorded.forEach { + assert(it.facets.filterIsInstance().isEmpty()) - class TestUnresolvedFacet : UnresolvedFacet { - val badNumber = 2 + assert(it.facets.first() is ResolvedFacet) + assert((it.facets.first() as ResolvedFacet).goodNumber == 4) + } } - data class ResolvedFacet(val goodNumber: Int) : Facet + @Test + fun `Given telemeter with a facetResolver that returns two facets, When record is called with an unresolved facet, Then return 2 resolved facets`() = + runTest { + val recorded = mutableListOf() + val recordingRelay = + FakeRelay { + recorded.add(it) + } - class TestFacetResolver : FacetResolver { - override fun getType(): Class<*> = TestUnresolvedFacet::class.java + class TestUnresolvedFacet : UnresolvedFacet { + val badNumber = 2 + } - override fun resolve(unresolvedFacet: UnresolvedFacet): List = - if (unresolvedFacet is TestUnresolvedFacet) { - listOf( - ResolvedFacet(unresolvedFacet.badNumber * 2), - ResolvedFacet(unresolvedFacet.badNumber * unresolvedFacet.badNumber * unresolvedFacet.badNumber), - ) - } else { - listOf(unresolvedFacet) - } - } + data class ResolvedFacet( + val goodNumber: Int, + ) : Facet + + class TestFacetResolver : FacetResolver { + override fun getType(): Class<*> = TestUnresolvedFacet::class.java + + override fun resolve(unresolvedFacet: UnresolvedFacet): List = + if (unresolvedFacet is TestUnresolvedFacet) { + listOf( + ResolvedFacet(unresolvedFacet.badNumber * 2), + ResolvedFacet( + unresolvedFacet.badNumber * unresolvedFacet.badNumber * + unresolvedFacet.badNumber, + ), + ) + } else { + listOf(unresolvedFacet) + } + } - val testFacetResolver = TestFacetResolver() + val testFacetResolver = TestFacetResolver() - val telemeter = Telemeter.build( - relays = listOf(recordingRelay), - facetResolvers = mapOf(testFacetResolver.getType() to testFacetResolver), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), - ) + val telemeter = + Telemeter.build( + relays = listOf(recordingRelay), + facetResolvers = mapOf(testFacetResolver.getType() to testFacetResolver), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), + ) - val numEvents = 10 - for (i in 1..numEvents) { - val event = FakeEvent(description = "event num $i", listOf(TestUnresolvedFacet())) - telemeter.record(event) - } + val numEvents = 10 + for (i in 1..numEvents) { + val event = FakeEvent(description = "event num $i", listOf(TestUnresolvedFacet())) + telemeter.record(event) + } - recorded.forEach { - assert(it.facets.filterIsInstance().isEmpty()) + recorded.forEach { + assert(it.facets.filterIsInstance().isEmpty()) - assert(it.facets.first() is ResolvedFacet) - assert((it.facets.first() as ResolvedFacet).goodNumber == 4) - assert(it.facets[1] is ResolvedFacet) - assert((it.facets[1] as ResolvedFacet).goodNumber == 8) + assert(it.facets.first() is ResolvedFacet) + assert((it.facets.first() as ResolvedFacet).goodNumber == 4) + assert(it.facets[1] is ResolvedFacet) + assert((it.facets[1] as ResolvedFacet).goodNumber == 8) + } } - } } diff --git a/telemetry/src/test/java/com/kroger/telemetry/facet/FacetTest.kt b/telemetry/src/test/java/com/kroger/telemetry/facet/FacetTest.kt index 592db07..096313b 100644 --- a/telemetry/src/test/java/com/kroger/telemetry/facet/FacetTest.kt +++ b/telemetry/src/test/java/com/kroger/telemetry/facet/FacetTest.kt @@ -32,9 +32,10 @@ internal class FacetTest { @Test fun `GIVEN computed facet THEN computations will be run multiple times`() { var count = 0 - val facet = object : Facet.Computed { - override val compute: () -> Unit = { count += 1 } - } + val facet = + object : Facet.Computed { + override val compute: () -> Unit = { count += 1 } + } for (i in 0..2) facet.compute() assertTrue(count > 1) @@ -43,12 +44,13 @@ internal class FacetTest { @Test fun `GIVEN lazy facet THEN computations will be be run once`() { var count = 0 - val facet = object : Facet.Lazy() { - override val compute: () -> Int = { - count += 1 - count + val facet = + object : Facet.Lazy() { + override val compute: () -> Int = { + count += 1 + count + } } - } // read value twice to ensure compute is only run once assertEquals(1, facet.value) diff --git a/telemetry/src/test/java/com/kroger/telemetry/relay/PrintRelayTest.kt b/telemetry/src/test/java/com/kroger/telemetry/relay/PrintRelayTest.kt index 1d2bcae..163d4aa 100644 --- a/telemetry/src/test/java/com/kroger/telemetry/relay/PrintRelayTest.kt +++ b/telemetry/src/test/java/com/kroger/telemetry/relay/PrintRelayTest.kt @@ -51,259 +51,293 @@ internal class PrintRelayTest { messages.clear() } - private fun TestScope.createTelemeter() = Telemeter.build( - relays = listOf(relay), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), - ) - - @Test - fun `GIVEN detailed mode is enabled WHEN event logged with multiple prefixes THEN all are included in tag`() = runTest { - val telemeter = createTelemeter() - val appName = "app" - val moduleName = "module" - val className = "class" - val localName = "local" - val event = FakeEvent( - facets = listOf( - Prefix.App(appName), - Prefix.Module(moduleName), - Prefix.Class(className), - Prefix.LocalScope(localName), - ), + private fun TestScope.createTelemeter() = + Telemeter.build( + relays = listOf(relay), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), ) - relay.configuration.detailedMode = true - - telemeter.record(event) - testScheduler.runCurrent() - val expectedTag = Telemeter.TAG + PrintRelay.separator + - relay.configuration.defaultSignificance.toString() + PrintRelay.separator + - appName + PrintRelay.separator + - moduleName + PrintRelay.separator + - className + PrintRelay.separator + - localName - assertEquals(expectedTag, messages[0].tag) - } @Test - fun `GIVEN detailed mode is enabled WHEN event logged with multiple prefixes in bad order THEN order is retained in tag`() = runTest { - val telemeter = createTelemeter() - val appName = "app" - val moduleName = "module" - val screenName = "screen" - val localName = "local" - val event = FakeEvent( - facets = listOf( - Prefix.Screen(screenName), - Prefix.App(appName), - Prefix.LocalScope(localName), - Prefix.Module(moduleName), - ), - ) - relay.configuration.detailedMode = true - - telemeter.record(event) - testScheduler.runCurrent() - val expectedTag = Telemeter.TAG + PrintRelay.separator + - relay.configuration.defaultSignificance.toString() + PrintRelay.separator + - screenName + PrintRelay.separator + - appName + PrintRelay.separator + - localName + PrintRelay.separator + - moduleName - assertEquals(expectedTag, messages[0].tag) - } + fun `GIVEN detailed mode is enabled WHEN event logged with multiple prefixes THEN all are included in tag`() = + runTest { + val telemeter = createTelemeter() + val appName = "app" + val moduleName = "module" + val className = "class" + val localName = "local" + val event = + FakeEvent( + facets = + listOf( + Prefix.App(appName), + Prefix.Module(moduleName), + Prefix.Class(className), + Prefix.LocalScope(localName), + ), + ) + relay.configuration.detailedMode = true + + telemeter.record(event) + testScheduler.runCurrent() + val expectedTag = + Telemeter.TAG + PrintRelay.SEPARATOR + + relay.configuration.defaultSignificance.toString() + PrintRelay.SEPARATOR + + appName + PrintRelay.SEPARATOR + + moduleName + PrintRelay.SEPARATOR + + className + PrintRelay.SEPARATOR + + localName + assertEquals(expectedTag, messages[0].tag) + } @Test - fun `GIVEN detailed mode is disabled WHEN event logged with multiple prefixes THEN most recent prefix is used`() = runTest { - val telemeter = createTelemeter() - val appName = "app" - val moduleName = "module" - val screenName = "screen" - val localName = "local" - val event = FakeEvent( - facets = listOf( - Prefix.App(appName), - Prefix.Module(moduleName), - Prefix.Screen(screenName), - Prefix.LocalScope(localName), - ), - ) - - telemeter.record(event) - testScheduler.runCurrent() - val expectedTag = Telemeter.TAG + PrintRelay.separator + - relay.configuration.defaultSignificance.toString() + PrintRelay.separator + - localName - assertEquals(expectedTag, messages[0].tag) - } + fun `GIVEN detailed mode is enabled WHEN event logged with multiple prefixes in bad order THEN order is retained in tag`() = + runTest { + val telemeter = createTelemeter() + val appName = "app" + val moduleName = "module" + val screenName = "screen" + val localName = "local" + val event = + FakeEvent( + facets = + listOf( + Prefix.Screen(screenName), + Prefix.App(appName), + Prefix.LocalScope(localName), + Prefix.Module(moduleName), + ), + ) + relay.configuration.detailedMode = true + + telemeter.record(event) + testScheduler.runCurrent() + val expectedTag = + Telemeter.TAG + PrintRelay.SEPARATOR + + relay.configuration.defaultSignificance.toString() + PrintRelay.SEPARATOR + + screenName + PrintRelay.SEPARATOR + + appName + PrintRelay.SEPARATOR + + localName + PrintRelay.SEPARATOR + + moduleName + assertEquals(expectedTag, messages[0].tag) + } @Test - fun `GIVEN detailed mode is disabled WHEN event logged with multiple prefixes in bad order THEN most recent prefix is used`() = runTest { - val telemeter = createTelemeter() - val appName = "app" - val moduleName = "module" - val screenName = "screen" - val localName = "local" - val event = FakeEvent( - facets = listOf( - Prefix.Screen(screenName), - Prefix.App(appName), - Prefix.LocalScope(localName), - Prefix.Module(moduleName), - ), - ) - - telemeter.record(event) - testScheduler.runCurrent() - val expectedTag = Telemeter.TAG + PrintRelay.separator + - relay.configuration.defaultSignificance.toString() + PrintRelay.separator + - moduleName - assertEquals(expectedTag, messages[0].tag) - } + fun `GIVEN detailed mode is disabled WHEN event logged with multiple prefixes THEN most recent prefix is used`() = + runTest { + val telemeter = createTelemeter() + val appName = "app" + val moduleName = "module" + val screenName = "screen" + val localName = "local" + val event = + FakeEvent( + facets = + listOf( + Prefix.App(appName), + Prefix.Module(moduleName), + Prefix.Screen(screenName), + Prefix.LocalScope(localName), + ), + ) + + telemeter.record(event) + testScheduler.runCurrent() + val expectedTag = + Telemeter.TAG + PrintRelay.SEPARATOR + + relay.configuration.defaultSignificance.toString() + PrintRelay.SEPARATOR + + localName + assertEquals(expectedTag, messages[0].tag) + } @Test - fun `tags will include highest significance attached to event`() = runTest { - val telemeter = createTelemeter() - val event = FakeEvent( - facets = listOf( - Significance.VERBOSE, - Significance.INTERNAL_ERROR, - Significance.WARNING, - ), - ) - - telemeter.record(event) - testScheduler.runCurrent() - val expectedTag = - Telemeter.TAG + PrintRelay.separator + Significance.INTERNAL_ERROR.toString() - assertEquals(expectedTag, messages[0].tag) - } + fun `GIVEN detailed mode is disabled WHEN event logged with multiple prefixes in bad order THEN most recent prefix is used`() = + runTest { + val telemeter = createTelemeter() + val appName = "app" + val moduleName = "module" + val screenName = "screen" + val localName = "local" + val event = + FakeEvent( + facets = + listOf( + Prefix.Screen(screenName), + Prefix.App(appName), + Prefix.LocalScope(localName), + Prefix.Module(moduleName), + ), + ) + + telemeter.record(event) + testScheduler.runCurrent() + val expectedTag = + Telemeter.TAG + PrintRelay.SEPARATOR + + relay.configuration.defaultSignificance.toString() + PrintRelay.SEPARATOR + + moduleName + assertEquals(expectedTag, messages[0].tag) + } @Test - fun `relay significance will be attached if not specified on event`() = runTest { - val telemeter = createTelemeter() - val event = FakeEvent() - - telemeter.record(event) - testScheduler.runCurrent() - assertEquals(Significance.DEBUG, messages[0].significance) - } + fun `tags will include highest significance attached to event`() = + runTest { + val telemeter = createTelemeter() + val event = + FakeEvent( + facets = + listOf( + Significance.VERBOSE, + Significance.INTERNAL_ERROR, + Significance.WARNING, + ), + ) + + telemeter.record(event) + testScheduler.runCurrent() + val expectedTag = + Telemeter.TAG + PrintRelay.SEPARATOR + Significance.INTERNAL_ERROR.toString() + assertEquals(expectedTag, messages[0].tag) + } @Test - fun `if configured for detailedMode, all facets will be included in message value`() = runTest { - val telemeter = createTelemeter() - relay.configuration.detailedMode = true - val description = "hello there" - val firstMsg = "general" - val secondMsg = "kenobi" - val facet1 = object : Facet { - override fun toString(): String = firstMsg - } - val facet2 = object : Facet { - override fun toString(): String = secondMsg + fun `relay significance will be attached if not specified on event`() = + runTest { + val telemeter = createTelemeter() + val event = FakeEvent() + + telemeter.record(event) + testScheduler.runCurrent() + assertEquals(Significance.DEBUG, messages[0].significance) } - val event = FakeEvent( - description = description, - facets = listOf(facet1, facet2), - ) - telemeter.record(event) - testScheduler.runCurrent() - val expectedMessage = - """ + @Test + fun `if configured for detailedMode, all facets will be included in message value`() = + runTest { + val telemeter = createTelemeter() + relay.configuration.detailedMode = true + val description = "hello there" + val firstMsg = "general" + val secondMsg = "kenobi" + val facet1 = + object : Facet { + override fun toString(): String = firstMsg + } + val facet2 = + object : Facet { + override fun toString(): String = secondMsg + } + + val event = + FakeEvent( + description = description, + facets = listOf(facet1, facet2), + ) + telemeter.record(event) + testScheduler.runCurrent() + val expectedMessage = + """ $description $firstMsg $secondMsg - """.trimIndent() - assertEquals(expectedMessage, messages[0].value) + """.trimIndent() + assertEquals(expectedMessage, messages[0].value) - relay.configuration.detailedMode = false - } + relay.configuration.detailedMode = false + } @Test - fun `if not configured for detailed mode, only description is include in message value`() = runTest { - val telemeter = createTelemeter() - val description = "hello there" - val firstMsg = "general" - val secondMsg = "kenobi" - val facet1 = object : Facet { - override fun toString(): String = firstMsg - } - val facet2 = object : Facet { - override fun toString(): String = secondMsg - } + fun `if not configured for detailed mode, only description is include in message value`() = + runTest { + val telemeter = createTelemeter() + val description = "hello there" + val firstMsg = "general" + val secondMsg = "kenobi" + val facet1 = + object : Facet { + override fun toString(): String = firstMsg + } + val facet2 = + object : Facet { + override fun toString(): String = secondMsg + } - val event = FakeEvent( - description = description, - facets = listOf(facet1, facet2), - ) - telemeter.record(event) - testScheduler.runCurrent() - assertEquals(description, messages[0].value) - } + val event = + FakeEvent( + description = description, + facets = listOf(facet1, facet2), + ) + telemeter.record(event) + testScheduler.runCurrent() + assertEquals(description, messages[0].value) + } @Test - fun `GIVEN event with significance lower than minimum WHEN processed THEN nothing will be printed`() = runTest { - val telemeter = createTelemeter() - relay.configuration.minimumSignificance = Significance.INTERNAL_ERROR - val event = FakeEvent(facets = listOf(Significance.ERROR)) - - telemeter.record(event) - testScheduler.runCurrent() - assertEquals(0, messages.size) - } + fun `GIVEN event with significance lower than minimum WHEN processed THEN nothing will be printed`() = + runTest { + val telemeter = createTelemeter() + relay.configuration.minimumSignificance = Significance.INTERNAL_ERROR + val event = FakeEvent(facets = listOf(Significance.ERROR)) + + telemeter.record(event) + testScheduler.runCurrent() + assertEquals(0, messages.size) + } @Test - fun `GIVEN event with significance equal to minimum WHEN processed THEN message will be printed`() = runTest { - val telemeter = createTelemeter() - relay.configuration.minimumSignificance = Significance.ERROR - val event = FakeEvent(facets = listOf(Significance.ERROR)) - - telemeter.record(event) - testScheduler.runCurrent() - assertEquals(1, messages.size) - } + fun `GIVEN event with significance equal to minimum WHEN processed THEN message will be printed`() = + runTest { + val telemeter = createTelemeter() + relay.configuration.minimumSignificance = Significance.ERROR + val event = FakeEvent(facets = listOf(Significance.ERROR)) + + telemeter.record(event) + testScheduler.runCurrent() + assertEquals(1, messages.size) + } @Test - fun `GIVEN event with significance higher than minimum WHEN processed THEN message will be printed`() = runTest { - val telemeter = createTelemeter() - relay.configuration.minimumSignificance = Significance.VERBOSE - val event = FakeEvent(facets = listOf(Significance.ERROR)) - - telemeter.record(event) - testScheduler.runCurrent() - assertEquals(1, messages.size) - } + fun `GIVEN event with significance higher than minimum WHEN processed THEN message will be printed`() = + runTest { + val telemeter = createTelemeter() + relay.configuration.minimumSignificance = Significance.VERBOSE + val event = FakeEvent(facets = listOf(Significance.ERROR)) + + telemeter.record(event) + testScheduler.runCurrent() + assertEquals(1, messages.size) + } @Test - fun `GIVEN config delegated to default WHEN property is accessed THEN backing prop is accessed`() = runTest { - var backingProp = true - - class Config : PrintRelay.Configuration by PrintRelay.Configuration.Default() { - override var detailedMode: Boolean - get() = backingProp - set(value) { - backingProp = value - } + fun `GIVEN config delegated to default WHEN property is accessed THEN backing prop is accessed`() = + runTest { + var backingProp = true + + class Config : PrintRelay.Configuration by PrintRelay.Configuration.Default() { + override var detailedMode: Boolean + get() = backingProp + set(value) { + backingProp = value + } + } + + val config = Config() + assertTrue(config.detailedMode) } - val config = Config() - assertTrue(config.detailedMode) - } - @Test - fun `GIVEN config delegated to default WHEN property is written THEN backing prop is written`() = runTest { - var backingProp = true - - class Config : PrintRelay.Configuration by PrintRelay.Configuration.Default() { - override var detailedMode: Boolean - get() = backingProp - set(value) { - backingProp = value - } + fun `GIVEN config delegated to default WHEN property is written THEN backing prop is written`() = + runTest { + var backingProp = true + + class Config : PrintRelay.Configuration by PrintRelay.Configuration.Default() { + override var detailedMode: Boolean + get() = backingProp + set(value) { + backingProp = value + } + } + + val config = Config() + config.detailedMode = false + assertFalse(backingProp) } - - val config = Config() - config.detailedMode = false - assertFalse(backingProp) - } } diff --git a/telemetry/src/test/java/com/kroger/telemetry/relay/TelemeterLogExtensionsTest.kt b/telemetry/src/test/java/com/kroger/telemetry/relay/TelemeterLogExtensionsTest.kt index b3dcaea..fc390d6 100644 --- a/telemetry/src/test/java/com/kroger/telemetry/relay/TelemeterLogExtensionsTest.kt +++ b/telemetry/src/test/java/com/kroger/telemetry/relay/TelemeterLogExtensionsTest.kt @@ -37,9 +37,10 @@ import org.junit.jupiter.api.Test internal class TelemeterLogExtensionsTest { private val captured = mutableListOf() - private val fakeRelay = FakeRelay { - captured.add(it) - } + private val fakeRelay = + FakeRelay { + captured.add(it) + } private val message = "hello there" @@ -48,100 +49,112 @@ internal class TelemeterLogExtensionsTest { captured.clear() } - private fun TestScope.createTelemeter() = Telemeter.build( - relays = listOf(fakeRelay), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), - ) + private fun TestScope.createTelemeter() = + Telemeter.build( + relays = listOf(fakeRelay), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), + ) @Test - fun `log records significance passed to it`() = runTest { - val telemeter = createTelemeter() - telemeter.log(message = message, significance = Significance.ERROR) - testScheduler.runCurrent() - assertTrue(captured[0].facets[0] == Significance.ERROR) - } + fun `log records significance passed to it`() = + runTest { + val telemeter = createTelemeter() + telemeter.log(message = message, significance = Significance.ERROR) + testScheduler.runCurrent() + assertTrue(captured[0].facets[0] == Significance.ERROR) + } @Test - fun `tag is ignored if null`() = runTest { - val telemeter = createTelemeter() - telemeter.log(message = message, significance = Significance.ERROR) - testScheduler.runCurrent() - assertTrue(captured[0].description == message) - } + fun `tag is ignored if null`() = + runTest { + val telemeter = createTelemeter() + telemeter.log(message = message, significance = Significance.ERROR) + testScheduler.runCurrent() + assertTrue(captured[0].description == message) + } @Test - fun `tag is prepended if present`() = runTest { - val telemeter = createTelemeter() - val tag = Telemeter.TAG - telemeter.log(tag = tag, message = message, significance = Significance.ERROR) - testScheduler.runCurrent() - val expected = "$tag - $message" - assertEquals(expected, captured[0].description) - } + fun `tag is prepended if present`() = + runTest { + val telemeter = createTelemeter() + val tag = Telemeter.TAG + telemeter.log(tag = tag, message = message, significance = Significance.ERROR) + testScheduler.runCurrent() + val expected = "$tag - $message" + assertEquals(expected, captured[0].description) + } @Test - fun `v records verbose significant event`() = runTest { - val telemeter = createTelemeter() - telemeter.v(message = message) - testScheduler.runCurrent() - assertTrue(captured[0].facets[0] == Significance.VERBOSE) - } + fun `v records verbose significant event`() = + runTest { + val telemeter = createTelemeter() + telemeter.v(message = message) + testScheduler.runCurrent() + assertTrue(captured[0].facets[0] == Significance.VERBOSE) + } @Test - fun `d records debug significant event`() = runTest { - val telemeter = createTelemeter() - telemeter.d(message = message) - testScheduler.runCurrent() - assertTrue(captured[0].facets[0] == Significance.DEBUG) - } + fun `d records debug significant event`() = + runTest { + val telemeter = createTelemeter() + telemeter.d(message = message) + testScheduler.runCurrent() + assertTrue(captured[0].facets[0] == Significance.DEBUG) + } @Test - fun `i records informational significant event`() = runTest { - val telemeter = createTelemeter() - telemeter.i(message = message) - testScheduler.runCurrent() - assertTrue(captured[0].facets[0] == Significance.INFORMATIONAL) - } + fun `i records informational significant event`() = + runTest { + val telemeter = createTelemeter() + telemeter.i(message = message) + testScheduler.runCurrent() + assertTrue(captured[0].facets[0] == Significance.INFORMATIONAL) + } @Test - fun `w records warn significant event`() = runTest { - val telemeter = createTelemeter() - telemeter.w(message = message) - testScheduler.runCurrent() - assertTrue(captured[0].facets[0] == Significance.WARNING) - } + fun `w records warn significant event`() = + runTest { + val telemeter = createTelemeter() + telemeter.w(message = message) + testScheduler.runCurrent() + assertTrue(captured[0].facets[0] == Significance.WARNING) + } @Test - fun `e records exceptional significant event`() = runTest { - val telemeter = createTelemeter() - telemeter.e(message = message) - testScheduler.runCurrent() - assertTrue(captured[0].facets[0] == Significance.ERROR) - } + fun `e records exceptional significant event`() = + runTest { + val telemeter = createTelemeter() + telemeter.e(message = message) + testScheduler.runCurrent() + assertTrue(captured[0].facets[0] == Significance.ERROR) + } @Test - fun `wtf records internal_error significant event`() = runTest { - val telemeter = createTelemeter() - telemeter.wtf(message = message) - testScheduler.runCurrent() - assertTrue(captured[0].facets[0] == Significance.INTERNAL_ERROR) - } + fun `wtf records internal_error significant event`() = + runTest { + val telemeter = createTelemeter() + telemeter.wtf(message = message) + testScheduler.runCurrent() + assertTrue(captured[0].facets[0] == Significance.INTERNAL_ERROR) + } @Test - fun `throwable is not used if not specified`() = runTest { - val telemeter = createTelemeter() - telemeter.wtf(message = message) - testScheduler.runCurrent() - assertEquals(message, captured[0].description) - } + fun `throwable is not used if not specified`() = + runTest { + val telemeter = createTelemeter() + telemeter.wtf(message = message) + testScheduler.runCurrent() + assertEquals(message, captured[0].description) + } @Test - fun `throwable is used if specified`() = runTest { - val telemeter = createTelemeter() - val exceptionMessage = "oh no" - telemeter.wtf(message = message, throwable = IllegalStateException(exceptionMessage)) - testScheduler.runCurrent() - val expectedMessage = "$message - $exceptionMessage" - assertEquals(expectedMessage, captured[0].description) - } + fun `throwable is used if specified`() = + runTest { + val telemeter = createTelemeter() + val exceptionMessage = "oh no" + telemeter.wtf(message = message, throwable = IllegalStateException(exceptionMessage)) + testScheduler.runCurrent() + val expectedMessage = "$message - $exceptionMessage" + assertEquals(expectedMessage, captured[0].description) + } } diff --git a/telemetry/src/test/java/com/kroger/telemetry/util/FakeRelay.kt b/telemetry/src/test/java/com/kroger/telemetry/util/FakeRelay.kt index a4d27e1..9b11da6 100644 --- a/telemetry/src/test/java/com/kroger/telemetry/util/FakeRelay.kt +++ b/telemetry/src/test/java/com/kroger/telemetry/util/FakeRelay.kt @@ -27,7 +27,9 @@ package com.kroger.telemetry.util import com.kroger.telemetry.Event import com.kroger.telemetry.Relay -public class FakeRelay(public var onEvent: suspend (Event) -> Unit = {}) : Relay { +public class FakeRelay( + public var onEvent: suspend (Event) -> Unit = {}, +) : Relay { override suspend fun process(event: Event) { onEvent(event) } From bac8e696c056ef26d5af7a445a6ff1f03c7b4288 Mon Sep 17 00:00:00 2001 From: Tim Romel Date: Mon, 19 May 2025 16:15:54 -0400 Subject: [PATCH 4/6] ci: workflow updates --- .github/workflows/commit.yml | 156 ++--------------------------- .github/workflows/pull_request.yml | 6 +- 2 files changed, 15 insertions(+), 147 deletions(-) diff --git a/.github/workflows/commit.yml b/.github/workflows/commit.yml index c0df3ef..d09e5d5 100644 --- a/.github/workflows/commit.yml +++ b/.github/workflows/commit.yml @@ -1,155 +1,19 @@ name: Commit -on: push - -env: - api_level: 29 - commitlint_version: '17' - conventional_changelog_version: '5' - java_version: 11 - ktlint_version: '0.46.1' - node_version: 18 - semantic_release_version: '20' +on: [ push, workflow_dispatch ] jobs: - commit_lint: - name: Commit Lint - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3.3.0 - with: - fetch-depth: 0 - - name: Run Commit Lint - id: commitlint - uses: ./.github/actions/commit-lint - with: - node_version: ${{ env.node_version }} - commitlint_version: ${{ env.commitlint_version }} - - code_lint: - name: Code Lint - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3.3.0 - - name: Run ktlint - id: ktlint - uses: ./.github/actions/code-lint - with: - ktlint_version: ${{ env.ktlint_version }} - - validation: - name: "Validation" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: gradle/actions/wrapper-validation@v3 - - determine_version: - name: Version Determination - runs-on: ubuntu-latest - outputs: - releaseType: ${{ steps.determine_version.outputs.releaseType }} - releaseChannel: ${{ steps.determine_version.outputs.releaseChannel }} - buildVersion: ${{ steps.determine_version.outputs.buildVersion }} - steps: - - name: Checkout - uses: actions/checkout@v3.3.0 - - name: Determine Version - id: determine_version - uses: ./.github/actions/determine-version - with: - node_version: ${{ env.node_version }} - semantic_release_version: ${{ env.semantic_release_version }} - conventional_changelog_version: ${{ env.conventional_changelog_version }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - build: - name: Build - needs: [ commit_lint, code_lint, validation ] - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3.3.0 - - name: Build - id: build - uses: ./.github/actions/build - with: - java_version: ${{ env.java_version }} - env: - BUILD_VERSION: "${{ needs.determine_version.outputs.buildVersion }}" - - unit_tests: - name: Unit Tests - needs: [ commit_lint, code_lint, validation ] - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3.3.0 - - name: Unit Tests - id: unit_tests - uses: ./.github/actions/unit-tests - with: - java_version: ${{ env.java_version }} - - instrumentation_tests: - name: Instrumentation Tests - needs: [ commit_lint, code_lint ] - runs-on: macos-11 - steps: - - name: Checkout - uses: actions/checkout@v3.3.0 - - name: Run Tests - id: instrumentation_tests - uses: ./.github/actions/instrumentation-test - with: - java_version: ${{ env.java_version }} - api_level: ${{ env.api_level }} - - release: - name: Release - runs-on: ubuntu-latest - needs: [ determine_version, build, unit_tests, instrumentation_tests ] - if: needs.determine_version.outputs.releaseType != '' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - outputs: - releaseType: ${{ steps.release.outputs.releaseType }} - releaseChannel: ${{ steps.release.outputs.releaseChannel }} - buildVersion: ${{ steps.release.outputs.buildVersion }} - steps: - - name: Checkout - uses: actions/checkout@v3.3.0 - - name: Release - id: release - uses: ./.github/actions/release - with: - node_version: ${{ env.node_version }} - semantic_release_version: ${{ env.semantic_release_version }} - conventional_changelog_version: ${{ env.conventional_changelog_version }} - - publish: - name: Publication - runs-on: ubuntu-latest - needs: release - if: needs.release.outputs.releaseType != '' - env: - RELEASE_TYPE: ${{ needs.release.outputs.releaseType }} - RELEASE_CHANNEL: ${{ needs.release.outputs.releaseChannel }} - BUILD_VERSION: ${{ needs.release.outputs.buildVersion }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + semantic_library_workflow: + name: push + uses: krogerco/Shared-CI-Workflow-Android/.github/workflows/semantic_library_workflow.yml@v1.5.0 + with: + java_version: '24' + ktlint_version: '-1' + test_command: 'test' + secrets: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_mavenCentralUsername }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_mavenCentralPassword }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ORG_GRADLE_PROJECT_signingInMemoryKey }} ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.ORG_GRADLE_PROJECT_signingInMemoryKeyId }} ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.ORG_GRADLE_PROJECT_signingInMemoryKeyPassword }} - steps: - - name: Checkout - uses: actions/checkout@v3.3.0 - - name: Publish - id: publish - uses: ./.github/actions/publish - with: - java_version: ${{ env.java_version }} \ No newline at end of file + REPO_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 157f5d7..480aa77 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -10,6 +10,10 @@ jobs: name: Check PR Title runs-on: ubuntu-latest steps: - - uses: thehanimo/pr-title-checker@v1.3.4 + - uses: thehanimo/pr-title-checker@v1.4.0 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + github_configuration_owner: 'krogerco' + github_configuration_repo: 'Shared-CI-Workflow-Android' + github_configuration_path: '.github/config/pr-title-checker-config.json' + github_configuration_ref: 'v1.0.0' From c7ca1917643433e16076154ffb04cc7977c0aab9 Mon Sep 17 00:00:00 2001 From: Tim Romel Date: Tue, 20 May 2025 09:05:35 -0400 Subject: [PATCH 5/6] build: set jvm target to 11 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e94d25f..c170ff1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ kgpAndroidDesugarJdkLibs = "2.1.5" kgpAndroidxComposeBom = "2025.05.00" kgpDokka = "2.0.0" kgpJdk = "24" -kgpJvmTarget = "17" +kgpJvmTarget = "11" kgpJunit4 = "4.13.2" kgpJunitBom = "5.12.2" kgpMinSdk = "24" From 707553386cb6593b5b57bb296437ab4150b37883 Mon Sep 17 00:00:00 2001 From: Tim Romel Date: Tue, 20 May 2025 11:42:05 -0400 Subject: [PATCH 6/6] chore: bump convention plugins --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c170ff1..6283b3f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ androidxTestEspresso = "3.6.1" androidxTestExtJunit = "1.2.1" androidxTestRules = "1.6.1" androidxTestRunner = "1.6.2" -conventionPlugin = "2.0.0-alpha.3" +conventionPlugin = "2.0.0-alpha.4" dependencyAnalysis = "2.17.0" firebaseBom = "32.8.0" gradleMavenPublishPlugin = "0.31.0"