diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index df4cb359..b099541a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ on: push jobs: lint: - runs-on: ubuntu-latest + runs-on: macos-latest steps: - name: "Checkout" uses: actions/checkout@v4 @@ -14,23 +14,10 @@ jobs: distribution: 'adopt' java-version: '17' - name: "lint" - run: ./gradlew lint - - ktlint: - runs-on: ubuntu-latest - steps: - - name: "Checkout" - uses: actions/checkout@v4 - - name: "Set up Java" - uses: actions/setup-java@v4 - with: - distribution: 'adopt' - java-version: '17' - - name: "ktlint" - run: ./gradlew ktlintCheck + run: ./gradlew lint ktlintCheck test: - runs-on: ubuntu-latest + runs-on: macos-latest steps: - name: "Checkout" uses: actions/checkout@v4 @@ -43,7 +30,7 @@ jobs: run: ./gradlew test check_coverage: - runs-on: ubuntu-latest + runs-on: macos-latest steps: - name: "Checkout" uses: actions/checkout@v4 @@ -53,23 +40,10 @@ jobs: distribution: 'adopt' java-version: '17' - name: "Check if coverage is satisfied" - run: ./gradlew jacocoTestCoverageVerification - - mutation_test: - runs-on: ubuntu-latest - steps: - - name: "Checkout" - uses: actions/checkout@v4 - - name: "Set up Java" - uses: actions/setup-java@v4 - with: - distribution: 'adopt' - java-version: '17' - - name: "Mutation Testing" - run: ./gradlew pitest + run: ./gradlew koverVerify api_validation: - runs-on: ubuntu-latest + runs-on: macos-latest steps: - name: "Checkout" uses: actions/checkout@v4 @@ -82,8 +56,8 @@ jobs: run: ./gradlew apiCheck assemble: - needs: [lint, ktlint, test, check_coverage, mutation_test] - runs-on: ubuntu-latest + needs: [lint, test, check_coverage] + runs-on: macos-latest steps: - name: "Checkout" uses: actions/checkout@v4 @@ -97,7 +71,7 @@ jobs: upload_coverage: needs: [assemble] - runs-on: ubuntu-latest + runs-on: macos-latest steps: - name: "Checkout" uses: actions/checkout@v4 @@ -107,10 +81,10 @@ jobs: distribution: 'adopt' java-version: '17' - name: "Create coverage reports" - run: ./gradlew check jacocoTestReport + run: ./gradlew koverXmlReport - name: "Upload coverage to codecov" - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5.3.1 with: token: ${{ secrets.CODECOV_TOKEN }} - file: ./control-core/build/reports/jacoco/test/jacocoTestReport.xml - fail_ci_if_error: true \ No newline at end of file + files: ./control-core/build/reports/kover/report.xml + fail_ci_if_error: true diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index c5a4f8ce..019e20cf 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -3,46 +3,41 @@ name: "deploy release" on: push: tags-ignore: - - '*-SNAPSHOT' + - "*-SNAPSHOT" jobs: all_checks: - runs-on: ubuntu-latest + runs-on: macos-latest steps: - name: "Checkout" uses: actions/checkout@v4 - name: "Set up Java" uses: actions/setup-java@v4 with: - distribution: 'adopt' - java-version: '17' + distribution: "adopt" + java-version: "17" - name: "Checks all the things" - run: ./gradlew lint ktlintCheck test jacocoTestCoverageVerification pitest apiCheck assemble + run: ./gradlew lint ktlintCheck test koverVerify apiCheck assemble publish: needs: [ all_checks ] - runs-on: ubuntu-latest + runs-on: macos-latest steps: - name: "Checkout" uses: actions/checkout@v4 - name: "Set up Java" uses: actions/setup-java@v4 with: - distribution: 'adopt' - java-version: '17' + distribution: "adopt" + java-version: "17" - name: "Get tag and save into env" uses: olegtarasov/get-tag@v2.1 id: tagName - name: "Upload release" - run: ./gradlew publishAllPublicationsToMavenCentralRepository --no-daemon --no-parallel + run: ./gradlew publishAndReleaseToMavenCentral --no-daemon --no-parallel --no-configuration-cache env: libraryVersionTag: ${{ steps.tagName.outputs.tag }} ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_PRIVATE_KEY }} ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} - - name: "Publish release" - run: ./gradlew closeAndReleaseRepository --no-daemon --no-parallel - env: - ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} - ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} diff --git a/.github/workflows/deploy-snapshot.yml b/.github/workflows/deploy-snapshot.yml index 946114eb..76a3359f 100644 --- a/.github/workflows/deploy-snapshot.yml +++ b/.github/workflows/deploy-snapshot.yml @@ -3,39 +3,41 @@ name: "deploy snapshot" on: push: tags: - - '*-SNAPSHOT' + - "*-SNAPSHOT" jobs: all_checks: - runs-on: ubuntu-latest + runs-on: macos-latest steps: - name: "Checkout" uses: actions/checkout@v4 - name: "Set up Java" uses: actions/setup-java@v4 with: - distribution: 'adopt' - java-version: '17' + distribution: "adopt" + java-version: "17" - name: "Checks all the things" - run: ./gradlew lint ktlintCheck test jacocoTestCoverageVerification pitest apiCheck assemble + run: ./gradlew lint ktlintCheck test koverVerify apiCheck assemble publish: needs: [ all_checks ] - runs-on: ubuntu-latest + runs-on: macos-latest steps: - name: "Checkout" uses: actions/checkout@v4 - name: "Set up Java" uses: actions/setup-java@v4 with: - distribution: 'adopt' - java-version: '17' + distribution: "adopt" + java-version: "17" - name: "Get tag and save into env" uses: olegtarasov/get-tag@v2.1 id: tagName - - name: "Upload release" - run: ./gradlew publishAllPublicationsToMavenCentralRepository --no-daemon --no-parallel + - name: "Upload snapshot" + run: ./gradlew publishAllPublicationsToMavenCentralRepository --no-configuration-cache env: libraryVersionTag: ${{ steps.tagName.outputs.tag }} ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_PRIVATE_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} diff --git a/.gitignore b/.gitignore index 227a0538..806feb11 100644 --- a/.gitignore +++ b/.gitignore @@ -200,4 +200,4 @@ fabric.properties # End of https://www.toptal.com/developers/gitignore/api/android,androidstudio,macos *.salive -.java-version \ No newline at end of file +.kotlin/ \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 56ca56c6..59bf2c4e 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,6 +4,7 @@ - + diff --git a/.java-version b/.java-version new file mode 100644 index 00000000..03b6389f --- /dev/null +++ b/.java-version @@ -0,0 +1 @@ +17.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ee9d9fc..30856857 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # changelog +## `[2.0.0]` - 2025-03-16 + +- Update Kotlin to `2.1.10` +- Update kotlinx.coroutines to `1.10.1` +- Publish `control-core` as kotlin multiplatform library +- Create custom `@TestOnlyStub` annotation and use it instead of `@TestOnly` to support KMP + implementation + ## `[1.3.0]` - 2024-11-16 - Update Kotlin to `2.0.21` @@ -48,17 +56,20 @@ ## `[0.11.0]` - 2020-05-30 -- `CoroutineScope.createController` and `CoroutineScope.createSynchronousController` now accept a custom `ControllerStart` parameter instead of `CoroutineStart`. +- `CoroutineScope.createController` and `CoroutineScope.createSynchronousController` now accept a + custom `ControllerStart` parameter instead of `CoroutineStart`. - Add `ManagedController`. - `Controller.stub` is now marked as `@TestOnly`. - binary compatibility is now checked on each `[build]` & `[publish]`. ## `[0.10.0]` - 2020-05-11 -- `ControllerStub` is removed from `Controller` interface. -- `ControllerStub` is now accessible via the `Controller.stub()` extension function. once a `Controller` is stubbed via this extension function, it cannot be un-stubbed. +- `ControllerStub` is removed from `Controller` interface. +- `ControllerStub` is now accessible via the `Controller.stub()` extension function. once a + `Controller` is stubbed via this extension function, it cannot be un-stubbed. ## `[0.9.0]` - 2020-05-10 -- `ControllerImplementation` now uses `MutableStateFlow` instead of `ConflatedBroadCastChannel` internally. +- `ControllerImplementation` now uses `MutableStateFlow` instead of `ConflatedBroadCastChannel` + internally. - `Controller.state` emissions are now distinct by default (via `StateFlow`). diff --git a/README.md b/README.md index 58bf540b..a8e381c1 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,14 @@ repositories { mavenCentral() } -dependencies { - implementation("at.florianschuster.control:control-core:$version") +kotlin { + sourceSets { + commonMain { + dependencies { + implementation("at.florianschuster.control:control-core:$version") + } + } + } } ``` @@ -30,7 +36,10 @@ see [changelog](https://github.com/floschu/control/blob/develop/CHANGELOG.md) f

flow

-A [Controller](control-core/src/main/kotlin/at/florianschuster/control/Controller.kt) is an ui-independent class that controls the state of a view. The role of a `Controller` is to separate business-logic from view-logic. A `Controller` has no dependency to the view, so it can easily be unit tested. +A [Controller](control-core/src/main/kotlin/at/florianschuster/control/Controller.kt) is an +ui-independent class that controls the state of a view. The role of a `Controller` is to separate +business-logic from view-logic. A `Controller` has no dependency to the view, so it can easily be +unit tested. ## info & documentation @@ -44,8 +53,9 @@ A [Controller](control-core/src/main/kotlin/at/florianschuster/control/Controlle ## examples -* [kotlin-counter](examples/kotlin-counter): most basic kotlin example. uses `Controller`. -* [android-counter](examples/android-counter): android counter example built with [jetpack compose](https://developer.android.com/jetpack/compose). +* [kotlin-counter](examples/kotlin-counter): most basic kotlin example. uses `Controller`. +* [android-counter](examples/android-counter): android counter example built + with [jetpack compose](https://developer.android.com/jetpack/compose). ## author diff --git a/build.gradle.kts b/build.gradle.kts index 114bf33a..3fc7a158 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,35 +1,22 @@ -buildscript { - repositories { - google() - mavenCentral() - maven(url = "https://plugins.gradle.org/m2/") - } - - dependencies { - classpath(libs.kotlin.gradle.plugin) - classpath(libs.pitest.gradle.plugin) - classpath(libs.binary.compat.validator) - classpath(libs.maven.publish.plugin) - classpath(libs.dokka.gradle.plugin) - - // examples - classpath(libs.android.gradle.plugin) - classpath(libs.kotlin.serialization) - } -} +import kotlinx.validation.ExperimentalBCVApi plugins { - jacoco + alias(libs.plugins.binary.compatibility.validator) alias(libs.plugins.ktlint) + alias(libs.plugins.kover) `maven-publish` signing + alias(libs.plugins.kotlin.multiplatform) apply false + alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.vanniktech.maven.publish) apply false } // ---- api-validation --- // -apply(plugin = "binary-compatibility-validator") - -configure { +apiValidation { + @OptIn(ExperimentalBCVApi::class) + klib { enabled = true } ignoredProjects.addAll( listOf( "kotlin-counter", @@ -39,26 +26,3 @@ configure { } // ---- end api-validation --- // - -// ---- jacoco --- // - -subprojects { - configurations.all { - resolutionStrategy { - eachDependency { - if (requested.group == "org.jacoco") { - useVersion("0.8.7") - } - } - } - } -} - -// ---- end jacoco --- // - -allprojects { - repositories { - google() - mavenCentral() - } -} diff --git a/control-core/api/control-core.api b/control-core/api/control-core.api index 6bcd44d0..0f7e176b 100644 --- a/control-core/api/control-core.api +++ b/control-core/api/control-core.api @@ -49,10 +49,16 @@ public final class at/florianschuster/control/ControllerLog$Custom : at/florians public final class at/florianschuster/control/ControllerLog$None : at/florianschuster/control/ControllerLog { public static final field INSTANCE Lat/florianschuster/control/ControllerLog$None; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class at/florianschuster/control/ControllerLog$Println : at/florianschuster/control/ControllerLog { public static final field INSTANCE Lat/florianschuster/control/ControllerLog$Println; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public abstract class at/florianschuster/control/ControllerStart { @@ -60,10 +66,16 @@ public abstract class at/florianschuster/control/ControllerStart { public final class at/florianschuster/control/ControllerStart$Immediately : at/florianschuster/control/ControllerStart { public static final field INSTANCE Lat/florianschuster/control/ControllerStart$Immediately; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class at/florianschuster/control/ControllerStart$Lazy : at/florianschuster/control/ControllerStart { public static final field INSTANCE Lat/florianschuster/control/ControllerStart$Lazy; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public abstract interface class at/florianschuster/control/ControllerStub : at/florianschuster/control/Controller { @@ -120,6 +132,9 @@ public final class at/florianschuster/control/TakeUntilKt { public static synthetic fun takeUntil$default (Lkotlinx/coroutines/flow/Flow;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; } +public abstract interface annotation class at/florianschuster/control/TestOnlyStub : java/lang/annotation/Annotation { +} + public abstract interface class at/florianschuster/control/TransformerContext { } diff --git a/control-core/api/control-core.klib.api b/control-core/api/control-core.klib.api new file mode 100644 index 00000000..a50780f9 --- /dev/null +++ b/control-core/api/control-core.klib.api @@ -0,0 +1,121 @@ +// Klib ABI Dump +// Targets: [iosArm64, iosSimulatorArm64, iosX64, linuxArm64, linuxX64, macosArm64, macosX64, watchosArm64, watchosSimulatorArm64, watchosX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +open annotation class at.florianschuster.control/TestOnlyStub : kotlin/Annotation { // at.florianschuster.control/TestOnlyStub|null[0] + constructor () // at.florianschuster.control/TestOnlyStub.|(){}[0] +} + +abstract fun interface <#A: kotlin/Any?> at.florianschuster.control/EffectEmitter { // at.florianschuster.control/EffectEmitter|null[0] + abstract fun emitEffect(#A) // at.florianschuster.control/EffectEmitter.emitEffect|emitEffect(1:0){}[0] +} + +abstract interface <#A: kotlin/Any?, #B: kotlin/Any?, #C: kotlin/Any?> at.florianschuster.control/EffectController : at.florianschuster.control/Controller<#A, #B> { // at.florianschuster.control/EffectController|null[0] + abstract val effects // at.florianschuster.control/EffectController.effects|{}effects[0] + abstract fun (): kotlinx.coroutines.flow/Flow<#C> // at.florianschuster.control/EffectController.effects.|(){}[0] +} + +abstract interface <#A: kotlin/Any?, #B: kotlin/Any?, #C: kotlin/Any?> at.florianschuster.control/EffectControllerStub : at.florianschuster.control/ControllerStub<#A, #B> { // at.florianschuster.control/EffectControllerStub|null[0] + abstract fun emitEffect(#C) // at.florianschuster.control/EffectControllerStub.emitEffect|emitEffect(1:2){}[0] +} + +abstract interface <#A: kotlin/Any?, #B: kotlin/Any?, #C: kotlin/Any?> at.florianschuster.control/EffectMutatorContext : at.florianschuster.control/EffectEmitter<#C>, at.florianschuster.control/MutatorContext<#A, #B> // at.florianschuster.control/EffectMutatorContext|null[0] + +abstract interface <#A: kotlin/Any?, #B: kotlin/Any?> at.florianschuster.control/Controller { // at.florianschuster.control/Controller|null[0] + abstract val state // at.florianschuster.control/Controller.state|{}state[0] + abstract fun (): kotlinx.coroutines.flow/StateFlow<#B> // at.florianschuster.control/Controller.state.|(){}[0] + + abstract fun dispatch(#A) // at.florianschuster.control/Controller.dispatch|dispatch(1:0){}[0] +} + +abstract interface <#A: kotlin/Any?, #B: kotlin/Any?> at.florianschuster.control/ControllerStub : at.florianschuster.control/Controller<#A, #B> { // at.florianschuster.control/ControllerStub|null[0] + abstract val dispatchedActions // at.florianschuster.control/ControllerStub.dispatchedActions|{}dispatchedActions[0] + abstract fun (): kotlin.collections/List<#A> // at.florianschuster.control/ControllerStub.dispatchedActions.|(){}[0] + + abstract fun emitState(#B) // at.florianschuster.control/ControllerStub.emitState|emitState(1:1){}[0] +} + +abstract interface <#A: kotlin/Any?, #B: kotlin/Any?> at.florianschuster.control/MutatorContext { // at.florianschuster.control/MutatorContext|null[0] + abstract val actions // at.florianschuster.control/MutatorContext.actions|{}actions[0] + abstract fun (): kotlinx.coroutines.flow/Flow<#A> // at.florianschuster.control/MutatorContext.actions.|(){}[0] + abstract val currentState // at.florianschuster.control/MutatorContext.currentState|{}currentState[0] + abstract fun (): #B // at.florianschuster.control/MutatorContext.currentState.|(){}[0] +} + +abstract interface <#A: kotlin/Any?> at.florianschuster.control/EffectReducerContext : at.florianschuster.control/EffectEmitter<#A>, at.florianschuster.control/ReducerContext // at.florianschuster.control/EffectReducerContext|null[0] + +abstract interface <#A: kotlin/Any?> at.florianschuster.control/EffectTransformerContext : at.florianschuster.control/EffectEmitter<#A>, at.florianschuster.control/TransformerContext // at.florianschuster.control/EffectTransformerContext|null[0] + +abstract interface at.florianschuster.control/LoggerContext { // at.florianschuster.control/LoggerContext|null[0] + abstract val event // at.florianschuster.control/LoggerContext.event|{}event[0] + abstract fun (): at.florianschuster.control/ControllerEvent // at.florianschuster.control/LoggerContext.event.|(){}[0] +} + +abstract interface at.florianschuster.control/ReducerContext // at.florianschuster.control/ReducerContext|null[0] + +abstract interface at.florianschuster.control/TransformerContext // at.florianschuster.control/TransformerContext|null[0] + +sealed class at.florianschuster.control/ControllerEvent { // at.florianschuster.control/ControllerEvent|null[0] + open fun toString(): kotlin/String // at.florianschuster.control/ControllerEvent.toString|toString(){}[0] + + final class Action : at.florianschuster.control/ControllerEvent // at.florianschuster.control/ControllerEvent.Action|null[0] + + final class Completed : at.florianschuster.control/ControllerEvent // at.florianschuster.control/ControllerEvent.Completed|null[0] + + final class Created : at.florianschuster.control/ControllerEvent // at.florianschuster.control/ControllerEvent.Created|null[0] + + final class Effect : at.florianschuster.control/ControllerEvent // at.florianschuster.control/ControllerEvent.Effect|null[0] + + final class Error : at.florianschuster.control/ControllerEvent // at.florianschuster.control/ControllerEvent.Error|null[0] + + final class Mutation : at.florianschuster.control/ControllerEvent // at.florianschuster.control/ControllerEvent.Mutation|null[0] + + final class Started : at.florianschuster.control/ControllerEvent // at.florianschuster.control/ControllerEvent.Started|null[0] + + final class State : at.florianschuster.control/ControllerEvent // at.florianschuster.control/ControllerEvent.State|null[0] + + final class Stub : at.florianschuster.control/ControllerEvent // at.florianschuster.control/ControllerEvent.Stub|null[0] +} + +sealed class at.florianschuster.control/ControllerLog { // at.florianschuster.control/ControllerLog|null[0] + final class Custom : at.florianschuster.control/ControllerLog { // at.florianschuster.control/ControllerLog.Custom|null[0] + constructor (kotlin/Function2) // at.florianschuster.control/ControllerLog.Custom.|(kotlin.Function2){}[0] + } + + final object None : at.florianschuster.control/ControllerLog { // at.florianschuster.control/ControllerLog.None|null[0] + final fun equals(kotlin/Any?): kotlin/Boolean // at.florianschuster.control/ControllerLog.None.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // at.florianschuster.control/ControllerLog.None.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // at.florianschuster.control/ControllerLog.None.toString|toString(){}[0] + } + + final object Println : at.florianschuster.control/ControllerLog { // at.florianschuster.control/ControllerLog.Println|null[0] + final fun equals(kotlin/Any?): kotlin/Boolean // at.florianschuster.control/ControllerLog.Println.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // at.florianschuster.control/ControllerLog.Println.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // at.florianschuster.control/ControllerLog.Println.toString|toString(){}[0] + } +} + +sealed class at.florianschuster.control/ControllerStart { // at.florianschuster.control/ControllerStart|null[0] + final object Immediately : at.florianschuster.control/ControllerStart { // at.florianschuster.control/ControllerStart.Immediately|null[0] + final fun equals(kotlin/Any?): kotlin/Boolean // at.florianschuster.control/ControllerStart.Immediately.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // at.florianschuster.control/ControllerStart.Immediately.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // at.florianschuster.control/ControllerStart.Immediately.toString|toString(){}[0] + } + + final object Lazy : at.florianschuster.control/ControllerStart { // at.florianschuster.control/ControllerStart.Lazy|null[0] + final fun equals(kotlin/Any?): kotlin/Boolean // at.florianschuster.control/ControllerStart.Lazy.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // at.florianschuster.control/ControllerStart.Lazy.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // at.florianschuster.control/ControllerStart.Lazy.toString|toString(){}[0] + } +} + +final fun <#A: kotlin/Any?, #B: kotlin/Any?, #C: kotlin/Any?, #D: kotlin/Any?> (kotlinx.coroutines/CoroutineScope).at.florianschuster.control/createEffectController(#C, kotlin/Function2, #A, kotlinx.coroutines.flow/Flow<#B>> = ..., kotlin/Function3, #B, #C, #C> = ..., kotlin/Function2, kotlinx.coroutines.flow/Flow<#A>, kotlinx.coroutines.flow/Flow<#A>> = ..., kotlin/Function2, kotlinx.coroutines.flow/Flow<#B>, kotlinx.coroutines.flow/Flow<#B>> = ..., kotlin/Function2, kotlinx.coroutines.flow/Flow<#C>, kotlinx.coroutines.flow/Flow<#C>> = ..., kotlin/String = ..., at.florianschuster.control/ControllerLog = ..., at.florianschuster.control/ControllerStart = ..., kotlinx.coroutines/CoroutineDispatcher = ...): at.florianschuster.control/EffectController<#A, #C, #D> // at.florianschuster.control/createEffectController|createEffectController@kotlinx.coroutines.CoroutineScope(0:2;kotlin.Function2,0:0,kotlinx.coroutines.flow.Flow<0:1>>;kotlin.Function3,0:1,0:2,0:2>;kotlin.Function2,kotlinx.coroutines.flow.Flow<0:0>,kotlinx.coroutines.flow.Flow<0:0>>;kotlin.Function2,kotlinx.coroutines.flow.Flow<0:1>,kotlinx.coroutines.flow.Flow<0:1>>;kotlin.Function2,kotlinx.coroutines.flow.Flow<0:2>,kotlinx.coroutines.flow.Flow<0:2>>;kotlin.String;at.florianschuster.control.ControllerLog;at.florianschuster.control.ControllerStart;kotlinx.coroutines.CoroutineDispatcher){0§;1§;2§;3§}[0] +final fun <#A: kotlin/Any?, #B: kotlin/Any?, #C: kotlin/Any?> (at.florianschuster.control/EffectController<#A, #B, #C>).at.florianschuster.control/toStub(): at.florianschuster.control/EffectControllerStub<#A, #B, #C> // at.florianschuster.control/toStub|toStub@at.florianschuster.control.EffectController<0:0,0:1,0:2>(){0§;1§;2§}[0] +final fun <#A: kotlin/Any?, #B: kotlin/Any?, #C: kotlin/Any?> (kotlinx.coroutines/CoroutineScope).at.florianschuster.control/createController(#C, kotlin/Function2, #A, kotlinx.coroutines.flow/Flow<#B>> = ..., kotlin/Function3 = ..., kotlin/Function2, kotlinx.coroutines.flow/Flow<#A>> = ..., kotlin/Function2, kotlinx.coroutines.flow/Flow<#B>> = ..., kotlin/Function2, kotlinx.coroutines.flow/Flow<#C>> = ..., kotlin/String = ..., at.florianschuster.control/ControllerLog = ..., at.florianschuster.control/ControllerStart = ..., kotlinx.coroutines/CoroutineDispatcher = ...): at.florianschuster.control/Controller<#A, #C> // at.florianschuster.control/createController|createController@kotlinx.coroutines.CoroutineScope(0:2;kotlin.Function2,0:0,kotlinx.coroutines.flow.Flow<0:1>>;kotlin.Function3;kotlin.Function2,kotlinx.coroutines.flow.Flow<0:0>>;kotlin.Function2,kotlinx.coroutines.flow.Flow<0:1>>;kotlin.Function2,kotlinx.coroutines.flow.Flow<0:2>>;kotlin.String;at.florianschuster.control.ControllerLog;at.florianschuster.control.ControllerStart;kotlinx.coroutines.CoroutineDispatcher){0§;1§;2§}[0] +final fun <#A: kotlin/Any?, #B: kotlin/Any?> (at.florianschuster.control/Controller<#A, #B>).at.florianschuster.control/toStub(): at.florianschuster.control/ControllerStub<#A, #B> // at.florianschuster.control/toStub|toStub@at.florianschuster.control.Controller<0:0,0:1>(){0§;1§}[0] +final fun <#A: kotlin/Any?, #B: kotlin/Any?> (kotlinx.coroutines.flow/Flow<#A>).at.florianschuster.control/takeUntil(kotlinx.coroutines.flow/Flow<#B>): kotlinx.coroutines.flow/Flow<#A> // at.florianschuster.control/takeUntil|takeUntil@kotlinx.coroutines.flow.Flow<0:0>(kotlinx.coroutines.flow.Flow<0:1>){0§;1§}[0] +final fun <#A: kotlin/Any?> (kotlinx.coroutines.flow/Flow<#A>).at.florianschuster.control/takeUntil(kotlin/Boolean = ..., kotlin.coroutines/SuspendFunction1<#A, kotlin/Boolean>): kotlinx.coroutines.flow/Flow<#A> // at.florianschuster.control/takeUntil|takeUntil@kotlinx.coroutines.flow.Flow<0:0>(kotlin.Boolean;kotlin.coroutines.SuspendFunction1<0:0,kotlin.Boolean>){0§}[0] diff --git a/control-core/build.gradle.kts b/control-core/build.gradle.kts index e3edf126..25b02646 100644 --- a/control-core/build.gradle.kts +++ b/control-core/build.gradle.kts @@ -1,82 +1,114 @@ +import com.vanniktech.maven.publish.SonatypeHost +import kotlinx.kover.gradle.plugin.dsl.AggregationType +import kotlinx.kover.gradle.plugin.dsl.CoverageUnit + plugins { - id("kotlin") - id("jacoco") - id("info.solidsoft.pitest") - id("com.vanniktech.maven.publish") + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.vanniktech.maven.publish) + alias(libs.plugins.kover) } -dependencies { - api(libs.kotlinx.coroutines.core) +kotlin { + jvm() - testImplementation(kotlin("test")) - testImplementation(libs.mockk) - testImplementation(libs.kotlinx.coroutines.test) -} + iosX64() + iosArm64() + iosSimulatorArm64() -// ---- jacoco --- // + macosX64() + macosArm64() -tasks.jacocoTestCoverageVerification { - violationRules { - rule { limit { minimum = "0.95".toBigDecimal() } } - } - classDirectories.setFrom( - sourceSets.main.get().output.asFileTree.matching { - // jacoco cannot handle inline functions properly - exclude( - "at/florianschuster/control/DefaultTagKt*", - "at/florianschuster/control/TakeUntilKt*", - ) - // builders - exclude( - "at/florianschuster/control/ControllerKt*", - "at/florianschuster/control/EffectControllerKt*", - ) - } - ) -} + watchosX64() + watchosArm64() + watchosSimulatorArm64() -tasks.jacocoTestReport { - reports { - xml.required.set(true) - html.required.set(true) - csv.required.set(false) + linuxX64() + linuxArm64() + + sourceSets { + commonMain { + dependencies { + api(libs.kotlinx.coroutines.core) + } + } + commonTest { + dependencies { + implementation(kotlin("test")) + implementation(libs.kotlinx.coroutines.test) + } + } } } -// ---- end jacoco --- // - -// ---- pitest --- // - -pitest { - targetClasses.add("at.florianschuster.control.*") - mutationThreshold.set(100) - excludedClasses.addAll( - // inline function - "at.florianschuster.control.DefaultTagKt**", - "at.florianschuster.control.TakeUntilKt**", +// ---- code coverage --- // - // builder - "at.florianschuster.control.Controller**", - "at.florianschuster.control.EffectController**", - - // inlined invokeSuspend - "at.florianschuster.control.ControllerImplementation\$stateJob\$1", - "at.florianschuster.control.ControllerImplementation\$stateJob\$1\$2" - ) - threads.set(4) - jvmArgs.add("-ea") - avoidCallsTo.addAll( - "kotlin.jvm.internal", - "kotlin.ResultKt", - "kotlinx.coroutines" - ) - verbose.set(true) +kover { + reports { + filters { excludes { classes("*DefaultControllerTag*") } } + verify { + rule("line coverage") { + bound { + aggregationForGroup = AggregationType.COVERED_PERCENTAGE + coverageUnits = CoverageUnit.LINE + minValue = 100 + } + } + rule("branch coverage") { + bound { + aggregationForGroup = AggregationType.COVERED_PERCENTAGE + coverageUnits = CoverageUnit.BRANCH + minValue = 100 + } + } + rule("instruction coverage") { + bound { + aggregationForGroup = AggregationType.COVERED_PERCENTAGE + coverageUnits = CoverageUnit.INSTRUCTION + minValue = 100 + } + } + } + } } -// ---- end pitest --- // +// ---- end code coverage --- // // ---- publishing --- // +group = "at.florianschuster.control" version = System.getenv("libraryVersionTag") +mavenPublishing { + // Snapshots will be immediately available at: + // https://s01.oss.sonatype.org/content/repositories/snapshots/at/florianschuster/control/ + publishToMavenCentral(SonatypeHost.S01) + signAllPublications() + coordinates(group.toString(), "control-core", version.toString()) + pom { + name = "control-core" + description = "coroutines flow based uni-directional architecture" + inceptionYear = "2019" + url = "https://github.com/floschu/control" + licenses { + license { + name = "The Apache Software License, Version 2.0" + url = "http://www.apache.org/licenses/LICENSE-2.0.txt" + distribution = "repo" + } + } + developers { + developer { + id = "floschu" + name = "Florian Schuster" + url = "https://github.com/floschu" + } + } + scm { + url = "https://github.com/floschu/control" + connection = "scm:git@github.com:floschu/control.git" + developerConnection = "scm:git@github.com:floschu/control.git" + } + } +} + // ---- end publishing --- // diff --git a/control-core/src/main/kotlin/at/florianschuster/control/Controller.kt b/control-core/src/commonMain/kotlin/at/florianschuster/control/Controller.kt similarity index 99% rename from control-core/src/main/kotlin/at/florianschuster/control/Controller.kt rename to control-core/src/commonMain/kotlin/at/florianschuster/control/Controller.kt index 4e6ceec6..fa0a4374 100644 --- a/control-core/src/main/kotlin/at/florianschuster/control/Controller.kt +++ b/control-core/src/commonMain/kotlin/at/florianschuster/control/Controller.kt @@ -114,7 +114,7 @@ fun CoroutineScope.createController( /** * Used for [ControllerLog] and as [CoroutineName] for the internal state machine. */ - tag: String = defaultTag(), + tag: String = defaultControllerTag(), /** * Log configuration for [ControllerEvent]s. See [ControllerLog]. */ diff --git a/control-core/src/main/kotlin/at/florianschuster/control/EffectController.kt b/control-core/src/commonMain/kotlin/at/florianschuster/control/EffectController.kt similarity index 99% rename from control-core/src/main/kotlin/at/florianschuster/control/EffectController.kt rename to control-core/src/commonMain/kotlin/at/florianschuster/control/EffectController.kt index 75be24a3..98c889ec 100644 --- a/control-core/src/main/kotlin/at/florianschuster/control/EffectController.kt +++ b/control-core/src/commonMain/kotlin/at/florianschuster/control/EffectController.kt @@ -60,7 +60,7 @@ fun CoroutineScope.createEffectController( /** * Used for [ControllerLog] and as [CoroutineName] for the internal state machine. */ - tag: String = defaultTag(), + tag: String = defaultControllerTag(), /** * Log configuration for [ControllerEvent]s. See [ControllerLog]. */ diff --git a/control-core/src/commonMain/kotlin/at/florianschuster/control/defaultControllerTag.kt b/control-core/src/commonMain/kotlin/at/florianschuster/control/defaultControllerTag.kt new file mode 100644 index 00000000..77a8053a --- /dev/null +++ b/control-core/src/commonMain/kotlin/at/florianschuster/control/defaultControllerTag.kt @@ -0,0 +1,3 @@ +package at.florianschuster.control + +internal expect fun defaultControllerTag(): String diff --git a/control-core/src/main/kotlin/at/florianschuster/control/defaultDispatcher.kt b/control-core/src/commonMain/kotlin/at/florianschuster/control/defaultDispatcher.kt similarity index 100% rename from control-core/src/main/kotlin/at/florianschuster/control/defaultDispatcher.kt rename to control-core/src/commonMain/kotlin/at/florianschuster/control/defaultDispatcher.kt diff --git a/control-core/src/main/kotlin/at/florianschuster/control/errors.kt b/control-core/src/commonMain/kotlin/at/florianschuster/control/errors.kt similarity index 100% rename from control-core/src/main/kotlin/at/florianschuster/control/errors.kt rename to control-core/src/commonMain/kotlin/at/florianschuster/control/errors.kt diff --git a/control-core/src/main/kotlin/at/florianschuster/control/event.kt b/control-core/src/commonMain/kotlin/at/florianschuster/control/event.kt similarity index 100% rename from control-core/src/main/kotlin/at/florianschuster/control/event.kt rename to control-core/src/commonMain/kotlin/at/florianschuster/control/event.kt diff --git a/control-core/src/main/kotlin/at/florianschuster/control/implementation.kt b/control-core/src/commonMain/kotlin/at/florianschuster/control/implementation.kt similarity index 100% rename from control-core/src/main/kotlin/at/florianschuster/control/implementation.kt rename to control-core/src/commonMain/kotlin/at/florianschuster/control/implementation.kt diff --git a/control-core/src/main/kotlin/at/florianschuster/control/log.kt b/control-core/src/commonMain/kotlin/at/florianschuster/control/log.kt similarity index 93% rename from control-core/src/main/kotlin/at/florianschuster/control/log.kt rename to control-core/src/commonMain/kotlin/at/florianschuster/control/log.kt index 0ecc2ec7..f5b3913a 100644 --- a/control-core/src/main/kotlin/at/florianschuster/control/log.kt +++ b/control-core/src/commonMain/kotlin/at/florianschuster/control/log.kt @@ -22,14 +22,14 @@ sealed class ControllerLog { /** * No logging. */ - object None : ControllerLog() { + data object None : ControllerLog() { override val logger: Logger? get() = null } /** * Uses [println] to log. */ - object Println : ControllerLog() { + data object Println : ControllerLog() { override val logger: Logger = { message -> println(message) } } diff --git a/control-core/src/main/kotlin/at/florianschuster/control/start.kt b/control-core/src/commonMain/kotlin/at/florianschuster/control/start.kt similarity index 84% rename from control-core/src/main/kotlin/at/florianschuster/control/start.kt rename to control-core/src/commonMain/kotlin/at/florianschuster/control/start.kt index 54cf6fa7..f3742414 100644 --- a/control-core/src/main/kotlin/at/florianschuster/control/start.kt +++ b/control-core/src/commonMain/kotlin/at/florianschuster/control/start.kt @@ -14,21 +14,21 @@ sealed class ControllerStart { * The state machine is started once [Controller.state] or [Controller.dispatch] * are accessed. */ - object Lazy : ControllerStart() { + data object Lazy : ControllerStart() { override val logName: String = "Lazy" } /** * The state machine is iImmediately started once the [Controller] is built. */ - object Immediately : ControllerStart() { + data object Immediately : ControllerStart() { override val logName: String = "Immediately" } /** * The state machine is started once [ControllerImplementation.start] is called. */ - internal object Manual : ControllerStart() { + internal data object Manual : ControllerStart() { override val logName: String = "Manual" } } diff --git a/control-core/src/main/kotlin/at/florianschuster/control/stub.kt b/control-core/src/commonMain/kotlin/at/florianschuster/control/stub.kt similarity index 87% rename from control-core/src/main/kotlin/at/florianschuster/control/stub.kt rename to control-core/src/commonMain/kotlin/at/florianschuster/control/stub.kt index a3c37763..61aacc16 100644 --- a/control-core/src/main/kotlin/at/florianschuster/control/stub.kt +++ b/control-core/src/commonMain/kotlin/at/florianschuster/control/stub.kt @@ -1,6 +1,11 @@ package at.florianschuster.control -import org.jetbrains.annotations.TestOnly +/** + * Marker to remind users, that stubs should only be used in tests. + * Use `@OptIn(TestOnlyStub::class)` on test classes using the stub api. + */ +@RequiresOptIn("The stub API is only for testing purposes and should not be used in production code") +annotation class TestOnlyStub /** * A stub of a [Controller] for view testing. @@ -26,7 +31,7 @@ interface ControllerStub : Controller { * * Custom implementations of [Controller] cannot be stubbed. */ -@TestOnly +@TestOnlyStub fun Controller.toStub(): ControllerStub { require(this is ControllerImplementation) { "Cannot stub a custom implementation of a Controller." @@ -56,7 +61,7 @@ interface EffectControllerStub : ControllerStub EffectController.toStub(): EffectControllerStub { require(this is ControllerImplementation) { "Cannot stub a custom implementation of a EffectController." diff --git a/control-core/src/main/kotlin/at/florianschuster/control/takeUntil.kt b/control-core/src/commonMain/kotlin/at/florianschuster/control/takeUntil.kt similarity index 95% rename from control-core/src/main/kotlin/at/florianschuster/control/takeUntil.kt rename to control-core/src/commonMain/kotlin/at/florianschuster/control/takeUntil.kt index 6bd94140..623f33d8 100644 --- a/control-core/src/main/kotlin/at/florianschuster/control/takeUntil.kt +++ b/control-core/src/commonMain/kotlin/at/florianschuster/control/takeUntil.kt @@ -67,4 +67,4 @@ fun Flow.takeUntil(other: Flow): Flow = flow { } } -private class TakeUntilException : CancellationException() +private class TakeUntilException : CancellationException("takeUntil was cancelled") diff --git a/control-core/src/commonTest/kotlin/at/florianschuster/control/CreateControllerTest.kt b/control-core/src/commonTest/kotlin/at/florianschuster/control/CreateControllerTest.kt new file mode 100644 index 00000000..770ab747 --- /dev/null +++ b/control-core/src/commonTest/kotlin/at/florianschuster/control/CreateControllerTest.kt @@ -0,0 +1,108 @@ +package at.florianschuster.control + +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.single +import kotlinx.coroutines.flow.singleOrNull +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +@Suppress("UNCHECKED_CAST") +internal class CreateControllerTest { + + @Test + fun `controller builder`() = runTest { + val expectedInitialState = 42 + val sut = createController( + initialState = expectedInitialState + ) as ControllerImplementation + + assertEquals(this, sut.scope) + assertEquals(expectedInitialState, sut.initialState) + + val mutatorContext = object : EffectMutatorContext { + override val currentState: Int + get() = notImplemented() + override val actions: Flow + get() = notImplemented() + + override fun emitEffect(effect: Nothing) { + notImplemented() + } + } + assertEquals(null, sut.mutator(mutatorContext, 3).singleOrNull()) + + val reducerContext = object : EffectReducerContext { + override fun emitEffect(effect: Nothing) { + notImplemented() + } + } + assertEquals(1, sut.reducer(reducerContext, 0, 1)) + + val transformerContext = object : EffectTransformerContext { + override fun emitEffect(effect: Nothing) { + notImplemented() + } + } + assertEquals(1, sut.actionsTransformer(transformerContext, flowOf(1)).single()) + assertEquals(2, sut.mutationsTransformer(transformerContext, flowOf(2)).single()) + assertEquals(3, sut.statesTransformer(transformerContext, flowOf(3)).single()) + + assertEquals(defaultControllerTag(), sut.tag) + assertEquals(ControllerLog.None, sut.controllerLog) + + assertEquals(ControllerStart.Lazy, sut.controllerStart) + assertEquals(defaultScopeDispatcher(), sut.dispatcher) + + coroutineContext.cancelChildren() + } + + @Test + fun `effect controller builder`() = runTest { + val expectedInitialState = 42 + val sut = createEffectController( + initialState = expectedInitialState + ) as ControllerImplementation + + assertEquals(this, sut.scope) + assertEquals(expectedInitialState, sut.initialState) + + val mutatorContext = object : EffectMutatorContext { + override val currentState: Int + get() = notImplemented() + override val actions: Flow + get() = notImplemented() + + override fun emitEffect(effect: Int) { + notImplemented() + } + } + assertEquals(null, sut.mutator(mutatorContext, 3).singleOrNull()) + + val reducerContext = object : EffectReducerContext { + override fun emitEffect(effect: Int) { + notImplemented() + } + } + assertEquals(1, sut.reducer(reducerContext, 0, 1)) + + val transformerContext = object : EffectTransformerContext { + override fun emitEffect(effect: Int) { + notImplemented() + } + } + assertEquals(1, sut.actionsTransformer(transformerContext, flowOf(1)).single()) + assertEquals(2, sut.mutationsTransformer(transformerContext, flowOf(2)).single()) + assertEquals(3, sut.statesTransformer(transformerContext, flowOf(3)).single()) + + assertEquals(defaultControllerTag(), sut.tag) + assertEquals(ControllerLog.None, sut.controllerLog) + + assertEquals(ControllerStart.Lazy, sut.controllerStart) + assertEquals(defaultScopeDispatcher(), sut.dispatcher) + + coroutineContext.cancelChildren() + } +} diff --git a/control-core/src/test/kotlin/at/florianschuster/control/DefaultScopeDispatcherTest.kt b/control-core/src/commonTest/kotlin/at/florianschuster/control/DefaultScopeDispatcherTest.kt similarity index 92% rename from control-core/src/test/kotlin/at/florianschuster/control/DefaultScopeDispatcherTest.kt rename to control-core/src/commonTest/kotlin/at/florianschuster/control/DefaultScopeDispatcherTest.kt index bd8aa611..a9856426 100644 --- a/control-core/src/test/kotlin/at/florianschuster/control/DefaultScopeDispatcherTest.kt +++ b/control-core/src/commonTest/kotlin/at/florianschuster/control/DefaultScopeDispatcherTest.kt @@ -4,7 +4,7 @@ import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.StandardTestDispatcher -import org.junit.Test +import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -12,7 +12,7 @@ internal class DefaultScopeDispatcherTest { @Test fun `regular dispatcher`() { - val expectedDispatcher = Dispatchers.IO + val expectedDispatcher = Dispatchers.Default assertEquals( expectedDispatcher, CoroutineScope(expectedDispatcher).defaultScopeDispatcher() diff --git a/control-core/src/test/kotlin/at/florianschuster/control/EventTest.kt b/control-core/src/commonTest/kotlin/at/florianschuster/control/EventTest.kt similarity index 85% rename from control-core/src/test/kotlin/at/florianschuster/control/EventTest.kt rename to control-core/src/commonTest/kotlin/at/florianschuster/control/EventTest.kt index eec4782a..fb663fe7 100644 --- a/control-core/src/test/kotlin/at/florianschuster/control/EventTest.kt +++ b/control-core/src/commonTest/kotlin/at/florianschuster/control/EventTest.kt @@ -1,15 +1,14 @@ package at.florianschuster.control import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher -import org.junit.Test +import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -@OptIn(ExperimentalCoroutinesApi::class) +@OptIn(TestOnlyStub::class) internal class EventTest { @Test @@ -17,8 +16,8 @@ internal class EventTest { val tag = "some_tag" val event = ControllerEvent.Completed(tag) - assertTrue(event.toString().contains("control")) - assertTrue(event.toString().contains(tag)) + assertTrue("control" in event.toString()) + assertTrue(tag in event.toString()) } @Test @@ -30,7 +29,7 @@ internal class EventTest { ) assertTrue(events.last() is ControllerEvent.Created) - assertTrue(events.last().toString().contains(ControllerStart.Manual.logName)) + assertTrue(ControllerStart.Manual.logName in events.last().toString()) sut.start() events.takeLast(2).let { lastEvents -> @@ -45,7 +44,7 @@ internal class EventTest { assertTrue(lastEvents[2] is ControllerEvent.State) } - sut.dispatch(effectValue) + sut.dispatch(EFFECT_VALUE) events.takeLast(4).let { lastEvents -> assertTrue(lastEvents[0] is ControllerEvent.Action) assertTrue(lastEvents[1] is ControllerEvent.Effect) @@ -92,7 +91,7 @@ internal class EventTest { val events = mutableListOf() val sut = TestScope(UnconfinedTestDispatcher()).eventsController(events) - sut.dispatch(mutatorErrorValue) + sut.dispatch(MUTATOR_ERROR_VALUE) events.takeLast(2).let { lastEvents -> assertTrue(lastEvents[0] is ControllerEvent.Error) assertTrue(lastEvents[1] is ControllerEvent.Completed) @@ -104,7 +103,7 @@ internal class EventTest { val events = mutableListOf() val sut = TestScope(UnconfinedTestDispatcher()).eventsController(events) - sut.dispatch(reducerErrorValue) + sut.dispatch(REDUCER_ERROR_VALUE) events.takeLast(2).let { lastEvents -> assertTrue(lastEvents[0] is ControllerEvent.Error) assertTrue(lastEvents[1] is ControllerEvent.Completed) @@ -116,8 +115,8 @@ internal class EventTest { val events = mutableListOf() val sut = TestScope(UnconfinedTestDispatcher()).eventsController(events) - repeat(ControllerImplementation.CAPACITY) { sut.dispatch(effectValue) } - sut.dispatch(effectValue) + repeat(ControllerImplementation.CAPACITY) { sut.dispatch(EFFECT_VALUE) } + sut.dispatch(EFFECT_VALUE) events.takeLast(2).let { lastEvents -> assertTrue(lastEvents[0] is ControllerEvent.Error) @@ -135,13 +134,13 @@ internal class EventTest { initialState = 0, mutator = { action -> flow { - if (action == effectValue) emitEffect(effectValue) - check(action != mutatorErrorValue) + if (action == EFFECT_VALUE) emitEffect(EFFECT_VALUE) + check(action != MUTATOR_ERROR_VALUE) emit(action) } }, reducer = { mutation, previousState -> - check(mutation != reducerErrorValue) + check(mutation != REDUCER_ERROR_VALUE) previousState }, actionsTransformer = { it }, @@ -152,8 +151,8 @@ internal class EventTest { ) companion object { - private const val mutatorErrorValue = 42 - private const val reducerErrorValue = 69 - private const val effectValue = 420 + private const val MUTATOR_ERROR_VALUE = 42 + private const val REDUCER_ERROR_VALUE = 69 + private const val EFFECT_VALUE = 420 } } \ No newline at end of file diff --git a/control-core/src/test/kotlin/at/florianschuster/control/ImplementationTest.kt b/control-core/src/commonTest/kotlin/at/florianschuster/control/ImplementationTest.kt similarity index 96% rename from control-core/src/test/kotlin/at/florianschuster/control/ImplementationTest.kt rename to control-core/src/commonTest/kotlin/at/florianschuster/control/ImplementationTest.kt index e1f515f5..76aec321 100644 --- a/control-core/src/test/kotlin/at/florianschuster/control/ImplementationTest.kt +++ b/control-core/src/commonTest/kotlin/at/florianschuster/control/ImplementationTest.kt @@ -1,7 +1,6 @@ package at.florianschuster.control import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -21,11 +20,12 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest -import org.junit.Test +import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds -@OptIn(ExperimentalCoroutinesApi::class) internal class ImplementationTest { @Test @@ -155,27 +155,27 @@ internal class ImplementationTest { val sut = scope.createStopWatchController() sut.dispatch(StopWatchAction.Start) - scope.advanceTimeBy(MinimumStopWatchDelay * 2 + 1) + scope.advanceTimeBy(MINIMUM_STOP_WATCH_DELAY * 2 + 1.milliseconds) sut.dispatch(StopWatchAction.Stop) assertEquals(2, sut.state.value) sut.dispatch(StopWatchAction.Start) - scope.advanceTimeBy(MinimumStopWatchDelay * 3 + 1) + scope.advanceTimeBy(MINIMUM_STOP_WATCH_DELAY * 3 + 1.milliseconds) sut.dispatch(StopWatchAction.Stop) assertEquals(5, sut.state.value) sut.dispatch(StopWatchAction.Start) - scope.advanceTimeBy(MinimumStopWatchDelay * 4 + 1) + scope.advanceTimeBy(MINIMUM_STOP_WATCH_DELAY * 4 + 1.milliseconds) sut.dispatch(StopWatchAction.Stop) assertEquals(9, sut.state.value) sut.dispatch(StopWatchAction.Start) - scope.advanceTimeBy(MinimumStopWatchDelay / 2) + scope.advanceTimeBy(MINIMUM_STOP_WATCH_DELAY / 2) sut.dispatch(StopWatchAction.Stop) assertEquals(9, sut.state.value) sut.dispatch(StopWatchAction.Start) - scope.advanceTimeBy(MinimumStopWatchDelay + 1) + scope.advanceTimeBy(MINIMUM_STOP_WATCH_DELAY + 1.milliseconds) sut.dispatch(StopWatchAction.Stop) assertEquals(10, sut.state.value) @@ -260,7 +260,7 @@ internal class ImplementationTest { } @Test - fun `effects are received from mutator, reducer and transformer`() { + fun `effects are received from mutator - reducer and transformer`() { val scope = TestScope(UnconfinedTestDispatcher()) val sut = scope.createEffectTestController() val states = sut.state.testIn(scope) @@ -475,7 +475,7 @@ internal class ImplementationTest { is StopWatchAction.Start -> { flow { while (isActive) { - delay(MinimumStopWatchDelay) + delay(MINIMUM_STOP_WATCH_DELAY) emit(1) } }.takeUntil(actions.filterIsInstance()) @@ -551,7 +551,7 @@ internal class ImplementationTest { ) companion object { - private const val MinimumStopWatchDelay = 1000L + private val MINIMUM_STOP_WATCH_DELAY = 1.seconds } } diff --git a/control-core/src/commonTest/kotlin/at/florianschuster/control/LogTest.kt b/control-core/src/commonTest/kotlin/at/florianschuster/control/LogTest.kt new file mode 100644 index 00000000..1827d080 --- /dev/null +++ b/control-core/src/commonTest/kotlin/at/florianschuster/control/LogTest.kt @@ -0,0 +1,56 @@ +package at.florianschuster.control + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +internal class LogTest { + + @Test + fun `none logger - methods are not called`() { + assertNull(ControllerLog.None.logger) + } + + @Test + fun `custom logger - methods are called`() { + val logs = mutableListOf() + val sut = ControllerLog.Custom { message -> logs.add(message) } + assertNotNull(sut.logger) + + sut.log { CreatedEvent } + assertEquals(CreatedEvent.toString(), logs.last()) + sut.log { CompletedEvent } + assertEquals(CompletedEvent.toString(), logs.last()) + } + + @Test + fun `LoggerContext factory function`() { + val sut = createLoggerContext(CreatedEvent) + assertEquals(CreatedEvent, sut.event) + } + + @Test + fun `log event is only created if logger exists`() { + var logged = false + ControllerLog.None.log { + logged = true + ControllerEvent.Action("", "") + } + assertFalse(logged) + + ControllerLog.Println.log { + logged = true + ControllerEvent.Action("", "") + } + assertTrue(logged) + } + + companion object { + private const val TAG = "TestTag" + private val CreatedEvent: ControllerEvent = ControllerEvent.Created(TAG, "lazy") + private val CompletedEvent: ControllerEvent = ControllerEvent.Completed(TAG) + } +} \ No newline at end of file diff --git a/control-core/src/commonTest/kotlin/at/florianschuster/control/NotImplemented.kt b/control-core/src/commonTest/kotlin/at/florianschuster/control/NotImplemented.kt new file mode 100644 index 00000000..c75c6ea6 --- /dev/null +++ b/control-core/src/commonTest/kotlin/at/florianschuster/control/NotImplemented.kt @@ -0,0 +1,3 @@ +package at.florianschuster.control + +internal fun notImplemented(): Nothing = throw NotImplementedError("not implemented") diff --git a/control-core/src/test/kotlin/at/florianschuster/control/StartTest.kt b/control-core/src/commonTest/kotlin/at/florianschuster/control/StartTest.kt similarity index 96% rename from control-core/src/test/kotlin/at/florianschuster/control/StartTest.kt rename to control-core/src/commonTest/kotlin/at/florianschuster/control/StartTest.kt index 2f7ffcd4..f3bfa996 100644 --- a/control-core/src/test/kotlin/at/florianschuster/control/StartTest.kt +++ b/control-core/src/commonTest/kotlin/at/florianschuster/control/StartTest.kt @@ -5,7 +5,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope -import org.junit.Test +import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -114,7 +114,7 @@ internal class StartTest { } @Test - fun `manual start mode, start when already started`() { + fun `manual start mode - start when already started`() { val sut = TestScope().createSimpleCounterController( controllerStart = ControllerStart.Manual ) @@ -127,7 +127,7 @@ internal class StartTest { } @Test - fun `manual start mode, cancel implementation`() { + fun `manual start mode - cancel implementation`() { val sut = TestScope().createSimpleCounterController( controllerStart = ControllerStart.Manual ) diff --git a/control-core/src/test/kotlin/at/florianschuster/control/StubTest.kt b/control-core/src/commonTest/kotlin/at/florianschuster/control/StubTest.kt similarity index 94% rename from control-core/src/test/kotlin/at/florianschuster/control/StubTest.kt rename to control-core/src/commonTest/kotlin/at/florianschuster/control/StubTest.kt index 1a0b0c7f..1e75a1c1 100644 --- a/control-core/src/test/kotlin/at/florianschuster/control/StubTest.kt +++ b/control-core/src/commonTest/kotlin/at/florianschuster/control/StubTest.kt @@ -1,7 +1,6 @@ package at.florianschuster.control import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -10,14 +9,13 @@ import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher -import org.junit.Test -import java.lang.IllegalArgumentException +import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertTrue -@OptIn(ExperimentalCoroutinesApi::class) +@OptIn(TestOnlyStub::class) internal class StubTest { @Test @@ -42,7 +40,7 @@ internal class StubTest { } @Test - fun `Controller stub is enabled only after conversion()`() { + fun `Controller stub is enabled only after conversion`() { val scope = TestScope() val sut = scope.createStringController() assertFalse(sut.stubEnabled) @@ -54,7 +52,7 @@ internal class StubTest { } @Test - fun `EffectController stub is enabled only after conversion()`() { + fun `EffectController stub is enabled only after conversion`() { val scope = TestScope() val sut = scope.createStringController() diff --git a/control-core/src/test/kotlin/at/florianschuster/control/TakeUntilTest.kt b/control-core/src/commonTest/kotlin/at/florianschuster/control/TakeUntilTest.kt similarity index 98% rename from control-core/src/test/kotlin/at/florianschuster/control/TakeUntilTest.kt rename to control-core/src/commonTest/kotlin/at/florianschuster/control/TakeUntilTest.kt index 3c2fba1e..4799dd15 100644 --- a/control-core/src/test/kotlin/at/florianschuster/control/TakeUntilTest.kt +++ b/control-core/src/commonTest/kotlin/at/florianschuster/control/TakeUntilTest.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.runTest -import org.junit.Test +import kotlin.test.Test import kotlin.test.assertEquals internal class TakeUntilTest { diff --git a/control-core/src/main/kotlin/at/florianschuster/control/defaultTag.kt b/control-core/src/jvmMain/kotlin/at/florianschuster/control/defaultControllerTag.jvm.kt similarity index 81% rename from control-core/src/main/kotlin/at/florianschuster/control/defaultTag.kt rename to control-core/src/jvmMain/kotlin/at/florianschuster/control/defaultControllerTag.jvm.kt index 3e9f24f0..e1c5ea3b 100644 --- a/control-core/src/main/kotlin/at/florianschuster/control/defaultTag.kt +++ b/control-core/src/jvmMain/kotlin/at/florianschuster/control/defaultControllerTag.jvm.kt @@ -1,8 +1,8 @@ package at.florianschuster.control @Suppress("NOTHING_TO_INLINE") -internal inline fun defaultTag(): String { +internal actual inline fun defaultControllerTag(): String { val stackTrace = Throwable().stackTrace check(stackTrace.size >= 2) { "Stacktrace didn't have enough elements." } return stackTrace[1].className.split("$").first().split(".").last() -} +} \ No newline at end of file diff --git a/control-core/src/jvmTest/kotlin/at/florianschuster/control/DefaultControllerTagTestJvm.kt b/control-core/src/jvmTest/kotlin/at/florianschuster/control/DefaultControllerTagTestJvm.kt new file mode 100644 index 00000000..9727f097 --- /dev/null +++ b/control-core/src/jvmTest/kotlin/at/florianschuster/control/DefaultControllerTagTestJvm.kt @@ -0,0 +1,37 @@ +package at.florianschuster.control + +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class DefaultControllerTagTestJvm { + + @Test + fun `defaultControllerTag in object`() { + assertEquals(EXPECTED_TAG, TestObject.tag) + } + + @Test + fun `defaultControllerTag in class`() { + assertEquals(EXPECTED_TAG, TestClass().tag) + } + + @Test + fun `defaultControllerTag in anonymous object`() { + val sut = object { + val tag = defaultControllerTag() + } + assertEquals(EXPECTED_TAG, sut.tag) + } + + companion object { + private const val EXPECTED_TAG = "DefaultControllerTagTestJvm" + } +} + +private object TestObject { + val tag = defaultControllerTag() +} + +private class TestClass { + val tag = defaultControllerTag() +} \ No newline at end of file diff --git a/control-core/src/jvmTest/kotlin/at/florianschuster/control/LogTestJvm.kt b/control-core/src/jvmTest/kotlin/at/florianschuster/control/LogTestJvm.kt new file mode 100644 index 00000000..8faa312d --- /dev/null +++ b/control-core/src/jvmTest/kotlin/at/florianschuster/control/LogTestJvm.kt @@ -0,0 +1,33 @@ +package at.florianschuster.control + +import java.io.ByteArrayOutputStream +import java.io.PrintStream +import kotlin.test.Test +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +internal class LogTestJvm { + + @Test + fun `println logger - methods are called`() { + val originalOut = System.out + val outContent = ByteArrayOutputStream() + System.setOut(PrintStream(outContent)) + + val sut = ControllerLog.Println + assertNotNull(sut.logger) + + sut.log { CreatedEvent } + assertTrue(CreatedEvent.toString() in outContent.toString() ) + sut.log { CompletedEvent } + assertTrue(CompletedEvent.toString() in outContent.toString()) + + System.setOut(originalOut) + } + + companion object { + private const val TAG = "TestTag" + private val CreatedEvent: ControllerEvent = ControllerEvent.Created(TAG, "lazy") + private val CompletedEvent: ControllerEvent = ControllerEvent.Completed(TAG) + } +} \ No newline at end of file diff --git a/control-core/src/nativeMain/kotlin/at/florianschuster/control/defaultControllerTag.native.kt b/control-core/src/nativeMain/kotlin/at/florianschuster/control/defaultControllerTag.native.kt new file mode 100644 index 00000000..d4efa133 --- /dev/null +++ b/control-core/src/nativeMain/kotlin/at/florianschuster/control/defaultControllerTag.native.kt @@ -0,0 +1,3 @@ +package at.florianschuster.control + +internal actual fun defaultControllerTag(): String = "Controller" \ No newline at end of file diff --git a/control-core/src/nativeTest/kotlin/at/florianschuster/control/DefaultControllerTagTestNative.kt b/control-core/src/nativeTest/kotlin/at/florianschuster/control/DefaultControllerTagTestNative.kt new file mode 100644 index 00000000..b2094fe3 --- /dev/null +++ b/control-core/src/nativeTest/kotlin/at/florianschuster/control/DefaultControllerTagTestNative.kt @@ -0,0 +1,12 @@ +package at.florianschuster.control + +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class DefaultControllerTagTestNative { + + @Test + fun `defaultControllerTag in object`() { + assertEquals("Controller", defaultControllerTag()) + } +} \ No newline at end of file diff --git a/control-core/src/test/kotlin/at/florianschuster/control/CreateControllerTest.kt b/control-core/src/test/kotlin/at/florianschuster/control/CreateControllerTest.kt deleted file mode 100644 index 3077aac5..00000000 --- a/control-core/src/test/kotlin/at/florianschuster/control/CreateControllerTest.kt +++ /dev/null @@ -1,66 +0,0 @@ -package at.florianschuster.control - -import io.mockk.mockk -import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.single -import kotlinx.coroutines.flow.singleOrNull -import kotlinx.coroutines.test.runTest -import org.junit.Test -import kotlin.test.assertEquals - -@Suppress("UNCHECKED_CAST") -internal class CreateControllerTest { - - @Test - fun `controller builder`() = runTest { - val expectedInitialState = 42 - val sut = createController( - initialState = expectedInitialState - ) as ControllerImplementation - - assertEquals(this, sut.scope) - assertEquals(expectedInitialState, sut.initialState) - - assertEquals(null, sut.mutator(mockk(), 3).singleOrNull()) - assertEquals(1, sut.reducer(mockk(), 0, 1)) - - assertEquals(1, sut.actionsTransformer(mockk(), flowOf(1)).single()) - assertEquals(2, sut.mutationsTransformer(mockk(), flowOf(2)).single()) - assertEquals(3, sut.statesTransformer(mockk(), flowOf(3)).single()) - - assertEquals(defaultTag(), sut.tag) - assertEquals(ControllerLog.None, sut.controllerLog) - - assertEquals(ControllerStart.Lazy, sut.controllerStart) - assertEquals(defaultScopeDispatcher(), sut.dispatcher) - - coroutineContext.cancelChildren() - } - - @Test - fun `effect controller builder`() = runTest { - val expectedInitialState = 42 - val sut = createEffectController( - initialState = expectedInitialState - ) as ControllerImplementation - - assertEquals(this, sut.scope) - assertEquals(expectedInitialState, sut.initialState) - - assertEquals(null, sut.mutator(mockk(), 3).singleOrNull()) - assertEquals(1, sut.reducer(mockk(), 0, 1)) - - assertEquals(1, sut.actionsTransformer(mockk(), flowOf(1)).single()) - assertEquals(2, sut.mutationsTransformer(mockk(), flowOf(2)).single()) - assertEquals(3, sut.statesTransformer(mockk(), flowOf(3)).single()) - - assertEquals(defaultTag(), sut.tag) - assertEquals(ControllerLog.None, sut.controllerLog) - - assertEquals(ControllerStart.Lazy, sut.controllerStart) - assertEquals(defaultScopeDispatcher(), sut.dispatcher) - - coroutineContext.cancelChildren() - } -} diff --git a/control-core/src/test/kotlin/at/florianschuster/control/DefaultTagTest.kt b/control-core/src/test/kotlin/at/florianschuster/control/DefaultTagTest.kt deleted file mode 100644 index e04d50f5..00000000 --- a/control-core/src/test/kotlin/at/florianschuster/control/DefaultTagTest.kt +++ /dev/null @@ -1,37 +0,0 @@ -package at.florianschuster.control - -import org.junit.Test -import kotlin.test.assertEquals - -internal class DefaultTagTest { - - @Test - fun `defaultTag in object`() { - assertEquals(expectedTag, TestObject.tag) - } - - @Test - fun `defaultTag in class`() { - assertEquals(expectedTag, TestClass().tag) - } - - @Test - fun `defaultTag in anonymous object`() { - val sut = object { - val tag = defaultTag() - } - assertEquals(expectedTag, sut.tag) - } - - companion object { - private const val expectedTag = "DefaultTagTest" - } -} - -private object TestObject { - val tag = defaultTag() -} - -private class TestClass { - val tag = defaultTag() -} \ No newline at end of file diff --git a/control-core/src/test/kotlin/at/florianschuster/control/LogTest.kt b/control-core/src/test/kotlin/at/florianschuster/control/LogTest.kt deleted file mode 100644 index 9a740201..00000000 --- a/control-core/src/test/kotlin/at/florianschuster/control/LogTest.kt +++ /dev/null @@ -1,83 +0,0 @@ -package at.florianschuster.control - -import io.mockk.Runs -import io.mockk.every -import io.mockk.just -import io.mockk.mockk -import io.mockk.slot -import io.mockk.spyk -import io.mockk.verify -import org.junit.Test -import java.io.PrintStream -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue - -internal class LogTest { - - @Test - fun `none logger, methods are not called`() { - assertNull(ControllerLog.None.logger) - } - - @Test - fun `println logger, methods are called`() { - val out = mockk(relaxed = true) - System.setOut(out) - val capturedLogMessage = slot() - every { out.println(capture(capturedLogMessage)) } just Runs - - val sut = ControllerLog.Println - assertNotNull(sut.logger) - - sut.log { CreatedEvent } - assertEquals(CreatedEvent.toString(), capturedLogMessage.captured) - sut.log { CompletedEvent } - assertEquals(CompletedEvent.toString(), capturedLogMessage.captured) - verify(exactly = 2) { out.println(any()) } - } - - @Test - fun `custom logger, methods are called`() { - val sut = spyk(ControllerLog.Custom { }) - assertNotNull(sut.logger) - - val capturedLogMessage = slot() - every { sut.logger.invoke(any(), capture(capturedLogMessage)) } just Runs - - sut.log { CreatedEvent } - assertEquals(CreatedEvent.toString(), capturedLogMessage.captured) - sut.log { CompletedEvent } - assertEquals(CompletedEvent.toString(), capturedLogMessage.captured) - } - - @Test - fun `LoggerContext factory function`() { - val sut = createLoggerContext(CreatedEvent) - assertEquals(CreatedEvent, sut.event) - } - - @Test - fun `log event is only created if logger exists`() { - var logged = false - ControllerLog.None.log { - logged = true - ControllerEvent.Action("", "") - } - assertFalse(logged) - - ControllerLog.Println.log { - logged = true - ControllerEvent.Action("", "") - } - assertTrue(logged) - } - - companion object { - private const val tag = "TestTag" - private val CreatedEvent: ControllerEvent = ControllerEvent.Created(tag, "lazy") - private val CompletedEvent: ControllerEvent = ControllerEvent.Completed(tag) - } -} \ No newline at end of file diff --git a/examples/android-counter/build.gradle.kts b/examples/android-counter/build.gradle.kts index 03affba7..131bdc51 100644 --- a/examples/android-counter/build.gradle.kts +++ b/examples/android-counter/build.gradle.kts @@ -1,16 +1,16 @@ plugins { - id("com.android.application") id("kotlin-android") + alias(libs.plugins.android.application) alias(libs.plugins.compose.compiler) } android { namespace = "at.florianschuster.control.counter" - compileSdk = 34 + compileSdk = libs.versions.android.compileSdk.get().toInt() defaultConfig { applicationId = "at.florianschuster.control.counter" - minSdk = 23 - targetSdk = 34 + minSdk = libs.versions.android.minSdk.get().toInt() + targetSdk = libs.versions.android.compileSdk.get().toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { diff --git a/examples/android-counter/src/androidTest/kotlin/at/florianschuster/control/counter/CounterScreenTest.kt b/examples/android-counter/src/androidTest/kotlin/at/florianschuster/control/counter/CounterScreenTest.kt index b7107684..d33a7f5f 100644 --- a/examples/android-counter/src/androidTest/kotlin/at/florianschuster/control/counter/CounterScreenTest.kt +++ b/examples/android-counter/src/androidTest/kotlin/at/florianschuster/control/counter/CounterScreenTest.kt @@ -6,13 +6,14 @@ import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.performClick import at.florianschuster.control.ControllerStub +import at.florianschuster.control.TestOnlyStub import at.florianschuster.control.toStub import kotlinx.coroutines.cancel import kotlinx.coroutines.test.TestScope import org.junit.After import org.junit.Before import org.junit.Rule -import org.junit.Test +import kotlin.test.Test import kotlin.test.assertEquals internal class CounterScreenTest { @@ -23,6 +24,7 @@ internal class CounterScreenTest { private lateinit var scope: TestScope private lateinit var stub: ControllerStub + @OptIn(TestOnlyStub::class) @Before fun setup() { scope = TestScope() diff --git a/examples/android-counter/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/examples/android-counter/src/main/res/mipmap-anydpi/ic_launcher.xml similarity index 79% rename from examples/android-counter/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to examples/android-counter/src/main/res/mipmap-anydpi/ic_launcher.xml index 036d09bc..65291b96 100644 --- a/examples/android-counter/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/examples/android-counter/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/examples/android-counter/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/android-counter/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 6fdc69ec..00000000 Binary files a/examples/android-counter/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/examples/android-counter/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/android-counter/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 097f1836..00000000 Binary files a/examples/android-counter/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/examples/android-counter/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/android-counter/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 588e2193..00000000 Binary files a/examples/android-counter/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/examples/android-counter/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/android-counter/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 8eca1a97..00000000 Binary files a/examples/android-counter/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/examples/android-counter/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/android-counter/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 005761b8..00000000 Binary files a/examples/android-counter/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/examples/kotlin-counter/src/main/kotlin/at/florianschuster/control/counter/CounterController.kt b/examples/kotlin-counter/src/main/kotlin/at/florianschuster/control/counter/CounterController.kt index 697cf57d..d90f3138 100644 --- a/examples/kotlin-counter/src/main/kotlin/at/florianschuster/control/counter/CounterController.kt +++ b/examples/kotlin-counter/src/main/kotlin/at/florianschuster/control/counter/CounterController.kt @@ -6,6 +6,7 @@ import at.florianschuster.control.createController import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flow +import kotlin.time.Duration.Companion.milliseconds typealias CounterController = Controller @@ -49,14 +50,14 @@ fun CoroutineScope.createCounterController( when (action) { is CounterAction.Increment -> flow { emit(CounterMutation.SetLoading(true)) - delay(500) + delay(500.milliseconds) emit(CounterMutation.IncreaseValue) emit(CounterMutation.SetLoading(false)) } is CounterAction.Decrement -> flow { emit(CounterMutation.SetLoading(true)) - delay(500) + delay(500.milliseconds) emit(CounterMutation.DecreaseValue) emit(CounterMutation.SetLoading(false)) } diff --git a/examples/kotlin-counter/src/test/kotlin/at/florianschuster/control/counter/CounterControllerTest.kt b/examples/kotlin-counter/src/test/kotlin/at/florianschuster/control/counter/CounterControllerTest.kt index b6e52cce..e47aaf4e 100644 --- a/examples/kotlin-counter/src/test/kotlin/at/florianschuster/control/counter/CounterControllerTest.kt +++ b/examples/kotlin-counter/src/test/kotlin/at/florianschuster/control/counter/CounterControllerTest.kt @@ -1,16 +1,14 @@ package at.florianschuster.control.counter -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest -import org.junit.Test +import kotlin.test.Test import kotlin.test.assertEquals -@OptIn(ExperimentalCoroutinesApi::class) internal class CounterControllerTest { @Test diff --git a/gradle.properties b/gradle.properties index 827fdffb..0d60745e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,26 +1,3 @@ org.gradle.jvmargs=-Xmx1536m -# AndroidX android.useAndroidX=true -# Automatically convert third-party libraries to use AndroidX -android.enableJetifier=true -# Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official - -# maven -GROUP=at.florianschuster.control -POM_ARTIFACT_ID=control-core -POM_NAME=control-core -POM_DESCRIPTION=coroutines flow based uni-directional architecture -POM_INCEPTION_YEAR=2019 -POM_URL=https://github.com/floschu/control -POM_SCM_URL=https://github.com/floschu/control -POM_SCM_CONNECTION=scm:git@github.com:floschu/control.git -POM_SCM_DEV_CONNECTION=scm:git@github.com:floschu/control.git -POM_LICENCE_NAME=The Apache Software License, Version 2.0 -POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt -POM_LICENCE_DIST=repo -POM_DEVELOPER_ID=floschu -POM_DEVELOPER_NAME=Florian Schuster -POM_DEVELOPER_URL=https://github.com/floschu -SONATYPE_HOST=S01 -RELEASE_SIGNING_ENABLED=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 69cf5b29..df64bd2e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,45 +1,46 @@ [versions] # lib -kotlin = "2.0.21" -coroutines = "1.9.0" +kotlin = "2.1.10" +coroutines = "1.10.1" +binary-compatibility-validator = "0.17.0" # examples -activity-compose = "1.9.3" -material3 = "1.3.1" -compose-bom = "2024.11.00" +agp = "8.9.0" +android-minSdk = "28" +android-compileSdk = "35" +androidx-activity-compose = "1.10.1" +androidx-compose-bom = "2025.03.00" +androidx-material3 = "1.3.1" espresso = "3.6.1" junit-ktx = "1.2.1" -mockk = "1.13.13" -pitest = "1.15.0" -binary-compat-validator = "0.14.0" -maven-publish-plugin = "0.25.3" -dokka = "1.9.20" -android-gradle-plugin = "8.6.1" -ktlint = "12.1.1" -ui-test-junit4 = "1.7.5" +maven-publish-plugin = "0.30.0" +kover = "0.9.1" +ktlint = "12.2.0" +androidx-ui-test-junit4 = "1.7.8" [libraries] -activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } -androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } -androidx-compose-material = { module = "androidx.compose.material3:material3", version.ref = "material3" } +# lib +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } + +# examples +activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" } +androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" } +androidx-compose-material = { module = "androidx.compose.material3:material3", version.ref = "androidx-material3" } androidx-compose-material-icons-core = { module = "androidx.compose.material:material-icons-core" } androidx-compose-ui = { module = "androidx.compose.ui:ui" } androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } -androidx-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "ui-test-junit4" } +androidx-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidx-ui-test-junit4" } androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" } junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "junit-ktx" } -kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } -kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } -kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } -mockk = { module = "io.mockk:mockk", version.ref = "mockk" } -pitest-gradle-plugin = { module = "info.solidsoft.gradle.pitest:gradle-pitest-plugin", version.ref = "pitest" } -binary-compat-validator = { module = "org.jetbrains.kotlinx:binary-compatibility-validator", version.ref = "binary-compat-validator" } -maven-publish-plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "maven-publish-plugin" } -dokka-gradle-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } -android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "android-gradle-plugin" } -kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } [plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +android-library = { id = "com.android.library", version.ref = "agp" } +binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } -compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } \ No newline at end of file +vanniktech-maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish-plugin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4b7f81d5..58c3237e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip diff --git a/settings.gradle.kts b/settings.gradle.kts index 7d421412..998c5c7c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,22 @@ -include(":control-core") +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + @Suppress("UnstableApiUsage") + repositories { + google() + mavenCentral() + maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots") + } +} +rootProject.name = "control" + +include(":control-core") include(":examples:kotlin-counter") include(":examples:android-counter")