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/.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' 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/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 f9012bb..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 @@ -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 @@ -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( @@ -61,7 +65,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 +80,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,11 +95,12 @@ internal class ToastRelayTest { @Test fun `GIVEN toast relay configured to toast all WHEN event received no significance THEN event is not toasted`() = - runBlockingTest { - val config = TestConfig().copy( - toastSignificantEvents = true, - minimumSignificance = Significance.ERROR, - ) + runTest { + val config = + TestConfig().copy( + toastSignificantEvents = true, + minimumSignificance = Significance.ERROR, + ) val relay = config.getRelay() relay.process( @@ -109,11 +114,12 @@ 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 { - val config = TestConfig().copy( - toastSignificantEvents = true, - minimumSignificance = Significance.ERROR, - ) + runTest { + val config = + TestConfig().copy( + toastSignificantEvents = true, + minimumSignificance = Significance.ERROR, + ) val relay = config.getRelay() relay.process( @@ -127,11 +133,12 @@ internal class ToastRelayTest { @Test fun `GIVEN toast relay configured to toast all WHEN event received with minimum significance THEN event is toasted`() = - runBlockingTest { - val config = TestConfig().copy( - toastSignificantEvents = true, - minimumSignificance = Significance.ERROR, - ) + runTest { + val config = + TestConfig().copy( + toastSignificantEvents = true, + minimumSignificance = Significance.ERROR, + ) val relay = config.getRelay() relay.process( @@ -145,7 +152,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 +170,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() @@ -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/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/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/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/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/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/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/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/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/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..6283b3f 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.4" +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 = "11" +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 f6b961f..1b33c55 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ 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/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 fdf09f2..8a7a9fe 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 @@ -45,7 +46,6 @@ import javax.inject.Inject @AndroidEntryPoint class MainActivity : AppCompatActivity() { - private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) } @@ -54,6 +54,7 @@ class MainActivity : AppCompatActivity() { lateinit var telemeter: ModuleOneTelemeter override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) setContentView(binding.root) @@ -84,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, + ), + ) } } @@ -118,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) @@ -132,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/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) } 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 c1d8c90..29b9bed 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,12 +36,18 @@ 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 { - data class StringFacet(val value: String) : Facet - data class IntFacet(val value: Int) : Facet + runTest { + 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,19 +59,25 @@ 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) } @Test fun `GIVEN typed relay WHEN event processed with relevant facet type THEN event is processed`() = - runBlockingTest { - data class StringFacet(val value: String) : Facet - data class IntFacet(val value: Int) : Facet + runTest { + data class StringFacet( + val value: String, + ) : Facet + + data class IntFacet( + val value: Int, + ) : Facet class BasicRelay : TypedRelay { var processedCount = 0 @@ -78,19 +90,25 @@ 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) } @Test fun `GIVEN typed relay WHEN event processed without relevant facet type THEN event is not processed`() = - runBlockingTest { - data class StringFacet(val value: String) : Facet - data class IntFacet(val value: Int) : Facet + runTest { + data class StringFacet( + val value: String, + ) : Facet + + data class IntFacet( + val value: Int, + ) : Facet class BasicRelay : TypedRelay { var processedCount = 0 @@ -103,25 +121,27 @@ 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) - 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..d503afa 100644 --- a/telemetry/src/test/java/com/kroger/telemetry/TelemeterTest.kt +++ b/telemetry/src/test/java/com/kroger/telemetry/TelemeterTest.kt @@ -32,123 +32,130 @@ 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`() { - 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 = scope), - ) - - val numEvents = 10_000 - for (i in 1..numEvents) { - val event = FakeEvent(description = "event num $i") - telemeter.record(event) + 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`() { - 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), - ) + 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 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`() { - 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 = scope), - ) + 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) - 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`() = - runBlockingTest { + 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 = scope), - 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() 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,397 +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`() { - 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 = scope), - ) - - val numEvents = 10_000 - for (i in 1..numEvents) telemeter.record(FakeEvent()) - - assertTrue(completedShortRelayJobs > completedLongRelayJobs) - scope.advanceUntilIdle() - assertEquals(numEvents, completedShortRelayJobs) - } + 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 + } - @Test - fun `GIVEN telemeter with default shared flow config WHEN event recorded by relay with long running process function THEN all events processed`() { - 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), + ) + + val numEvents = 10_000 + repeat(numEvents) { + telemeter.record(FakeEvent()) + testScheduler.runCurrent() + } + assertTrue(completedShortRelayJobs > completedLongRelayJobs) + assertEquals(numEvents, completedShortRelayJobs) } - val telemeter = Telemeter.build( - relays = listOf(longRelay, shortRelay), - facets = listOf(), - flowConfig = Telemeter.defaultSharedFlowConfig.copy(scope = scope), - ) + @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 + } - val numEvents = 10_000 - for (i in 1..numEvents) telemeter.record(FakeEvent()) + val telemeter = + Telemeter.build( + relays = listOf(longRelay, shortRelay), + facets = listOf(), + flowConfig = Telemeter.defaultSharedFlowConfig.copy(scope = backgroundScope), + ) - assertTrue(completedShortRelayJobs > completedLongRelayJobs) - scope.advanceUntilIdle() - assertEquals(numEvents, completedShortRelayJobs) - assertEquals(numEvents, completedLongRelayJobs) - } + val numEvents = 10_000 + 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`() { - val childFacet = object : Facet {} - val parent = Telemeter.build( - relays = listOf(), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = scope), - ) - - var childProcessed = false - val childRelay = FakeRelay { - println(it) - childProcessed = true + 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) } - val child = parent.child( - listOf(childRelay), - listOf(childFacet), - ) - - child.record(FakeEvent()) - 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`() { - var numProcessed = 0 - - class RepeatedRelay : Relay { - override suspend fun process(event: Event) { - numProcessed += 1 - println(event) + 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) + } } - } - val parent = Telemeter.build( - relays = listOf(RepeatedRelay()), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = scope), - ) - 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()) - 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`() { - 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 = scope), - ) + 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)) - - 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`() { - 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 = scope), + 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() - }, - ) - - 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`() { - 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 - } + testScheduler.runCurrent() + assertEquals(firstPrefix, recorded[0].facets[0]) + assertEquals(secondPrefix, recorded[0].facets[1]) + assertEquals(thirdPrefix, recorded[0].facets[2]) } - val telemeter = Telemeter.build( - relays = listOf(relay), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = scope), - ) - val computedFacet = object : Facet.Computed { - override val compute: () -> Boolean = { - true - } - } - - telemeter.record(FakeEvent(facets = listOf(computedFacet))) - - assertTrue(processed) - } - @Test - fun `GIVEN lazy facet WHEN recorded THEN lazy value will be result of computation `() { - 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 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 = scope), - ) - val lazyFacet = object : Facet.Lazy() { - override val compute: () -> Int = { 1 } + 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(lazyFacet))) - - assertEquals(1, computedCount) - } - @Test - fun `GIVEN telemeter configured to track thread data WHEN event recorded THEN additional facet is included`() { - 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 = scope, - ), - ) - - telemeter.record(FakeEvent()) - - 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`() { - val currentThreadName = Thread.currentThread().name - val recordedFacets = mutableListOf() - var processedThread = "" - val relay = FakeRelay { - scope.launch { - withContext(Dispatchers.IO) { - recordedFacets.addAll(it.facets) - processedThread = Thread.currentThread().name + 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( - shouldPropagateThreadData = true, - scope = scope, - ), - ) - - telemeter.record(FakeEvent()) + val telemeter = + Telemeter.build( + relays = listOf(relay), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), + ) + val lazyFacet = + object : Facet.Lazy() { + override val compute: () -> Int = { 1 } + } - while (processedThread.isEmpty()) Unit - val threadData = recordedFacets[0] as ThreadData - assertEquals(currentThreadName, threadData.threadName) - assertTrue(processedThread != "" && processedThread != threadData.threadName) - } + telemeter.record(FakeEvent(facets = listOf(lazyFacet))) + testScheduler.runCurrent() + assertEquals(1, computedCount) + } @Test - fun `GIVEN telemeter tree with more than one node configured to record thread data WHEN event recorded THEN only one thread data recorded`() { - val recordedFacets = mutableListOf() - val relay = FakeRelay { recordedFacets.addAll(it.facets) } - - val child = Telemeter.build( - relays = listOf(relay), - flowConfig = Telemeter.defaultTelemetryFlowConfig.copy( - shouldPropagateThreadData = true, - scope = scope, - ), - ).child(listOf()) + 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()) + } - child.record(FakeEvent()) + @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 + } + } + } - val threadDataFacets = recordedFacets.filterIsInstance() - assertEquals(1, threadDataFacets.size) - } + 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) + } @Test - fun `GIVEN telemeter with relay WHEN relay throws THEN telemeter catches and records error without looping`() { - 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 = scope), - ) - - telemeter.record(FakeEvent()) - - 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`() { - 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 + } - class TestUnresolvedFacet : UnresolvedFacet { - val badNumber = 2 + 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) } - 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 = scope), - ) + 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`() { - 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 = scope), - ) + 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 7563d22..163d4aa 100644 --- a/telemetry/src/test/java/com/kroger/telemetry/relay/PrintRelayTest.kt +++ b/telemetry/src/test/java/com/kroger/telemetry/relay/PrintRelayTest.kt @@ -29,280 +29,315 @@ 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() - } - - @Test - fun `GIVEN detailed mode is enabled WHEN event logged with multiple prefixes THEN all are included in tag`() { - 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) - - 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`() { - 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) - - 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`() { - 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) - - 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`() { - 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) - - 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`() { - val event = FakeEvent( - facets = listOf( - Significance.VERBOSE, - Significance.INTERNAL_ERROR, - Significance.WARNING, - ), - ) - - telemeter.record(event) - - 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`() { - val event = FakeEvent() - - telemeter.record(event) - - 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`() { - 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) + @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 expectedMessage = - """ + 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) - - relay.configuration.detailedMode = false - } + """.trimIndent() + assertEquals(expectedMessage, messages[0].value) - @Test - fun `if not configured for detailed mode, only description is include in message value`() { - val description = "hello there" - val firstMsg = "general" - val secondMsg = "kenobi" - val facet1 = object : Facet { - override fun toString(): String = firstMsg + relay.configuration.detailedMode = false } - val facet2 = object : Facet { - override fun toString(): String = secondMsg - } - - val event = FakeEvent( - description = description, - facets = listOf(facet1, facet2), - ) - telemeter.record(event) - - assertEquals(description, messages[0].value) - } @Test - fun `GIVEN event with significance lower than minimum WHEN processed THEN nothing will be printed`() { - relay.configuration.minimumSignificance = Significance.INTERNAL_ERROR - val event = FakeEvent(facets = listOf(Significance.ERROR)) - - telemeter.record(event) + 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 + } - assertEquals(0, messages.size) - } + 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 equal to minimum WHEN processed THEN message will be printed`() { - relay.configuration.minimumSignificance = Significance.ERROR - val event = FakeEvent(facets = listOf(Significance.ERROR)) - - telemeter.record(event) - - assertEquals(1, 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 higher than minimum WHEN processed THEN message will be printed`() { - relay.configuration.minimumSignificance = Significance.VERBOSE - val event = FakeEvent(facets = listOf(Significance.ERROR)) - - telemeter.record(event) - - 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 config delegated to default WHEN property is accessed THEN backing prop is accessed `() { - var backingProp = true - - class Config : PrintRelay.Configuration by PrintRelay.Configuration.Default() { - override var detailedMode: Boolean - get() = backingProp - set(value) { - backingProp = value - } + 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) } - val config = Config() - assertTrue(config.detailedMode) - } - @Test - fun `GIVEN config delegated to default WHEN property is written THEN backing prop is written `() { - 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() - config.detailedMode = false - assertFalse(backingProp) - } + @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 + } + } + + 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 467bd3a..fc390d6 100644 --- a/telemetry/src/test/java/com/kroger/telemetry/relay/TelemeterLogExtensionsTest.kt +++ b/telemetry/src/test/java/com/kroger/telemetry/relay/TelemeterLogExtensionsTest.kt @@ -28,103 +28,133 @@ 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 fakeRelay = + FakeRelay { + captured.add(it) + } private val message = "hello there" @AfterEach fun teardown() { captured.clear() - scope.cleanupTestCoroutines() } - @Test - fun `log records significance passed to it`() { - telemeter.log(message = message, significance = Significance.ERROR) - assertTrue(captured[0].facets[0] == Significance.ERROR) - } + private fun TestScope.createTelemeter() = + Telemeter.build( + relays = listOf(fakeRelay), + flowConfig = Telemeter.defaultTelemetryFlowConfig.copy(scope = backgroundScope), + ) @Test - fun `tag is ignored if null`() { - telemeter.log(message = message, significance = Significance.ERROR) - assertTrue(captured[0].description == message) - } + 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 prepended if present`() { - val tag = Telemeter.TAG - telemeter.log(tag = tag, message = message, significance = Significance.ERROR) - - val expected = "$tag - $message" - assertEquals(expected, captured[0].description) - } + 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 `v records verbose significant event`() { - telemeter.v(message = message) - assertTrue(captured[0].facets[0] == Significance.VERBOSE) - } + 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 `d records debug significant event`() { - telemeter.d(message = message) - assertTrue(captured[0].facets[0] == Significance.DEBUG) - } + 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 `i records informational significant event`() { - telemeter.i(message = message) - assertTrue(captured[0].facets[0] == Significance.INFORMATIONAL) - } + 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 `w records warn significant event`() { - telemeter.w(message = message) - assertTrue(captured[0].facets[0] == Significance.WARNING) - } + 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 `e records exceptional significant event`() { - telemeter.e(message = message) - assertTrue(captured[0].facets[0] == Significance.ERROR) - } + 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 `wtf records internal_error significant event`() { - telemeter.wtf(message = message) - assertTrue(captured[0].facets[0] == Significance.INTERNAL_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 `throwable is not used if not specified`() { - telemeter.wtf(message = message) - assertEquals(message, captured[0].description) - } + 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 used if specified`() { - val exceptionMessage = "oh no" - telemeter.wtf(message = message, throwable = IllegalStateException(exceptionMessage)) + fun `throwable is not used if not specified`() = + runTest { + val telemeter = createTelemeter() + telemeter.wtf(message = message) + testScheduler.runCurrent() + assertEquals(message, captured[0].description) + } - val expectedMessage = "$message - $exceptionMessage" - assertEquals(expectedMessage, 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) + } } 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) }