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

-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")