diff --git a/.github/actions/gradle-task-with-commit/action.yml b/.github/actions/gradle-task-with-commit/action.yml index 1e01d520f2..bf41a202d3 100644 --- a/.github/actions/gradle-task-with-commit/action.yml +++ b/.github/actions/gradle-task-with-commit/action.yml @@ -29,12 +29,14 @@ inputs : runs: using: 'composite' steps: - - name: Check if PERSONAL_ACCESS_TOKEN is set + - name: Check if access token is set id: can-push shell: bash run: | if [[ "${{ inputs.personal-access-token }}" == '' ]]; then echo "can_push=false" >> $GITHUB_OUTPUT + elif [[ "${{ env.GITHUB_REF_PROTECTED }}" == 'true' ]]; then + echo "can_push=false" >> $GITHUB_OUTPUT elif [[ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]]; then echo "can_push=false" >> $GITHUB_OUTPUT else diff --git a/.github/actions/gradle-task/action.yml b/.github/actions/gradle-task/action.yml index 50b9874beb..9171e02394 100644 --- a/.github/actions/gradle-task/action.yml +++ b/.github/actions/gradle-task/action.yml @@ -41,6 +41,18 @@ runs : cache-read-only : false gradle-home-cache-cleanup : true + # Calculate all the hashes for keys just one time. + # These should only be referenced before the actual task action, since that action + # may generate changes and we want the final cache key to reflect its current state. + - name : Calculate hashes + id : hashes + shell: bash + run : | + echo "lib_versions=${{ hashFiles('**/libs.versions.toml') }}" >> $GITHUB_OUTPUT + echo "gradle_props=${{ hashFiles('**/gradle.properties') }}" >> $GITHUB_OUTPUT + echo "gradle_kts=${{ hashFiles('**/*.gradle.kts') }}" >> $GITHUB_OUTPUT + echo "src_kt=${{ hashFiles('**/src/**/*.kt') }}" >> $GITHUB_OUTPUT + # Attempt to restore from the write-cache-key, or fall back to a partial match for the write key. # Skipped if the write-cache-key wasn't set. # This step's "cache_hit" output will only be true if an exact match was found. @@ -51,9 +63,15 @@ runs : with : path : | ~/.gradle/caches/build-cache-1 - ./**/build/** - key : ${{runner.os}}-${{inputs.write-cache-key}}-${{hashFiles('**/*.gradle.kt*')}}-${{hashFiles('**/libs.versions.toml')}}-${{hashFiles('**/gradle.properties')}} - restore-keys : ${{runner.os}}-${{inputs.write-cache-key}} + ~/.konan + ./**/build/**/!(*.dex) + ./**/.gradle + key : ${{runner.os}}-${{inputs.write-cache-key}}-${{steps.hashes.outputs.lib_versions}}-${{steps.hashes.outputs.gradle_props}}-${{steps.hashes.outputs.gradle_kts}}-${{steps.hashes.outputs.src_kt}} + restore-keys : | + ${{runner.os}}-${{inputs.write-cache-key}}-${{steps.hashes.outputs.lib_versions}}-${{steps.hashes.outputs.gradle_props}}-${{steps.hashes.outputs.gradle_kts}} + ${{runner.os}}-${{inputs.write-cache-key}}-${{steps.hashes.outputs.lib_versions}}-${{steps.hashes.outputs.gradle_props}} + ${{runner.os}}-${{inputs.write-cache-key}}-${{steps.hashes.outputs.lib_versions}} + ${{runner.os}}-${{inputs.write-cache-key}} # Attempt to restore from the restore-cache-key, or fall back to a partial match for the restore key. # Skipped if the restore-cache-key wasn't set, or if the write-cache-key restore had an exact match. @@ -63,9 +81,15 @@ runs : with : path : | ~/.gradle/caches/build-cache-1 - ./**/build/** - key : ${{runner.os}}-${{inputs.restore-cache-key}}-${{hashFiles('**/*.gradle.kt*')}}-${{hashFiles('**/libs.versions.toml')}}-${{hashFiles('**/gradle.properties')}} - restore-keys : ${{runner.os}}-${{inputs.restore-cache-key}} + ~/.konan + ./**/build/**/!(*.dex) + ./**/.gradle + key : ${{runner.os}}-${{inputs.restore-cache-key}}-${{steps.hashes.outputs.lib_versions}}-${{steps.hashes.outputs.gradle_props}}-${{steps.hashes.outputs.gradle_kts}}-${{steps.hashes.outputs.src_kt}} + restore-keys : | + ${{runner.os}}-${{inputs.restore-cache-key}}-${{steps.hashes.outputs.lib_versions}}-${{steps.hashes.outputs.gradle_props}}-${{steps.hashes.outputs.gradle_kts}} + ${{runner.os}}-${{inputs.restore-cache-key}}-${{steps.hashes.outputs.lib_versions}}-${{steps.hashes.outputs.gradle_props}} + ${{runner.os}}-${{inputs.restore-cache-key}}-${{steps.hashes.outputs.lib_versions}} + ${{runner.os}}-${{inputs.restore-cache-key}} - uses : gradle/wrapper-validation-action@v1 @@ -92,8 +116,10 @@ runs : with : path : | ~/.gradle/caches/build-cache-1 - ./**/build/** - key : ${{runner.os}}-${{inputs.write-cache-key}}-${{hashFiles('**/*.gradle.kt*')}}-${{hashFiles('**/libs.versions.toml')}}-${{hashFiles('**/gradle.properties')}} + ~/.konan + ./**/build/**/!(*.dex) + ./**/.gradle + key : ${{runner.os}}-${{inputs.write-cache-key}}-${{hashFiles('**/libs.versions.toml')}}-${{hashFiles('**/gradle.properties')}}-${{hashFiles('**/*.gradle.kts')}}-${{hashFiles('**/src/**/*.kt')}} - name : Upload heap dump if : failure() diff --git a/.github/actions/gradle-tasks-with-emulator/action.yml b/.github/actions/gradle-tasks-with-emulator/action.yml new file mode 100644 index 0000000000..ef628fc4ef --- /dev/null +++ b/.github/actions/gradle-tasks-with-emulator/action.yml @@ -0,0 +1,88 @@ +name : Run Android Instrumentation Tests with Artifact and AVD Caching +description: This action sets up Gradle, runs a preparatory task, runs Android tests on an emulator, and uploads test results. + +inputs: + prepare-task: + description: 'Gradle task for preparing necessary artifacts. Supports multi-line input.' + required: true + test-task: + description: 'Gradle task for running instrumentation tests. Supports multi-line input.' + required: true + api-level : + description : 'The Android SDK api level, like `29`' + required : true + build-root-directory : + description : 'Path to the root directory of the build' + required : false + java-version : + description : 'The Java version to set up.' + default : '11' + distribution : + description : 'The JDK distribution to use.' + default : 'zulu' + restore-cache-key : + description : 'The unique identifier for the associated cache. Any other consumers or producers for this cache must use the same name.' + default : 'null' + write-cache-key : + description : 'The unique identifier for the associated cache. Any other consumers or producers for this cache must use the same name.' + default : 'null' + +runs : + using : 'composite' + steps : + + # Create or fetch the artifacts used for these tests. + - name : Run ${{ inputs.prepare-task }} + uses : ./.github/actions/gradle-task + with : + build-root-directory : ${{ inputs.build-root-directory }} + distribution : ${{ inputs.distribution }} + java-version : ${{ inputs.java-version }} + restore-cache-key : ${{ inputs.restore-cache-key }} + task : ${{ inputs.prepare-task }} + write-cache-key : ${{ inputs.write-cache-key }} + + # Get the AVD if it's already cached. + - name: AVD cache + uses: actions/cache@v3 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-${{ matrix.api-level }} + + # If the AVD cache didn't exist, create an AVD and cache it. + - name: create AVD and generate snapshot for caching + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ inputs.api-level }} + arch : x86_64 + disable-animations: false + emulator-boot-timeout: 12000 + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + force-avd-creation: false + profile : Galaxy Nexus + ram-size: 4096M + script: echo "Generated AVD snapshot." + + # Run the actual emulator tests. + # At this point every task should be up-to-date and the AVD should be ready to go. + - name: run tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ inputs.api-level }} + arch : x86_64 + disable-animations: true + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + force-avd-creation: false + profile : Galaxy Nexus + script : ./gradlew ${{ inputs.test-task }} + + - name : Upload results + if : ${{ always() }} + uses : actions/upload-artifact@v3 + with : + name : instrumentation-test-results + path : ./**/build/reports/androidTests/connected/** diff --git a/.github/workflows/kotlin.yml b/.github/workflows/kotlin.yml index 72a305a4ec..9c6c19df3d 100644 --- a/.github/workflows/kotlin.yml +++ b/.github/workflows/kotlin.yml @@ -14,22 +14,22 @@ concurrency : jobs : - build-all: - name: Build all - runs-on: macos-latest - steps: - - uses: actions/checkout@v3 - - - name: main build - uses: ./.github/actions/gradle-task - with: - task: compileKotlin compileDebugKotlin - write-cache-key: main-build-artifacts + build-all : + name : Build all + runs-on : macos-latest + steps : + - uses : actions/checkout@v3 + + - name : main build + uses : ./.github/actions/gradle-task + with : + task : compileKotlin assembleDebug + write-cache-key : main-build-artifacts dokka : name : Assemble & Dokka runs-on : ubuntu-latest - needs: build-all + needs : build-all steps : - uses : actions/checkout@v3 @@ -39,6 +39,20 @@ jobs : task : siteDokka write-cache-key : main-build-artifacts + shards-and-version : + name : Shard Matrix Yaml + runs-on : ubuntu-latest + steps : + - uses : actions/checkout@v3 + + - name : check published artifacts + uses : ./.github/actions/gradle-task-with-commit + with : + check-task : connectedCheckShardMatrixYamlCheck checkVersionIsSnapshot + fix-task : connectedCheckShardMatrixYamlUpdate checkVersionIsSnapshot + personal-access-token: ${{ secrets.PR_UPDATE_TOKEN }} + write-cache-key : build-logic + artifacts-check : name : ArtifactsCheck # the `artifactsCheck` task has to run on macOS in order to see the iOS KMP artifacts @@ -51,6 +65,7 @@ jobs : with : check-task : artifactsCheck fix-task : artifactsDump + personal-access-token: ${{ secrets.PR_UPDATE_TOKEN }} write-cache-key : build-logic dependency-guard : @@ -66,6 +81,7 @@ jobs : with : check-task : dependencyGuard --refresh-dependencies fix-task : dependencyGuardBaseline --refresh-dependencies + personal-access-token: ${{ secrets.PR_UPDATE_TOKEN }} write-cache-key : build-logic ktlint : @@ -81,6 +97,7 @@ jobs : with : check-task : ktLintCheck fix-task : ktLintFormat + personal-access-token: ${{ secrets.PR_UPDATE_TOKEN }} write-cache-key : build-logic api-check : @@ -96,12 +113,13 @@ jobs : with : check-task : apiCheck fix-task : apiDump + personal-access-token: ${{ secrets.PR_UPDATE_TOKEN }} write-cache-key : build-logic android-lint : name : Android Lint runs-on : ubuntu-latest - needs: build-all + needs : build-all timeout-minutes : 20 steps : - uses : actions/checkout@v3 @@ -114,7 +132,7 @@ jobs : check : name : Check runs-on : ubuntu-latest - needs: build-all + needs : build-all timeout-minutes : 20 steps : - uses : actions/checkout@v3 @@ -122,9 +140,8 @@ jobs : uses : ./.github/actions/gradle-task with : task : | - checkVersionIsSnapshot allTests - test + test --continue restore-cache-key : build-logic write-cache-key : main-build-artifacts @@ -262,87 +279,42 @@ jobs : steps : - uses : actions/checkout@v3 - ## Build before running tests, using cache. - - name : Build instrumented tests - uses : ./.github/actions/gradle-task - with : - task : :benchmarks:performance-poetry:complex-poetry:assembleDebugAndroidTest - restore-cache-key : main-build-artifacts - - ## Actual task - - name : Render Pass Counting Test - uses : reactivecircus/android-emulator-runner@v2 + - name : Instrumented tests + uses : ./.github/actions/gradle-tasks-with-emulator with : - # @ychescale9 suspects Galaxy Nexus is the fastest one - profile : Galaxy Nexus api-level : ${{ matrix.api-level }} - arch : x86_64 - script : ./gradlew :benchmarks:performance-poetry:complex-poetry:connectedCheck --continue - - - name : Upload results - if : ${{ always() }} - uses : actions/upload-artifact@v3 - with : - name : renderpass-counting-results-${{ matrix.api-level }} - path : ./**/build/reports/androidTests/connected/** - - build-instrumentation-tests : - name : Build Instrumentation tests - runs-on : macos-latest - needs: build-all - timeout-minutes : 45 - steps : - - uses : actions/checkout@v3 - - - name : Build instrumented tests - uses : ./.github/actions/gradle-task - with : - task : assembleDebugAndroidTest - restore-cache-key : main-build-artifacts - write-cache-key : androidTest-build-artifacts + prepare-task : :benchmarks:performance-poetry:complex-poetry:prepareDebugAndroidTestArtifacts + test-task : :benchmarks:performance-poetry:complex-poetry:connectedCheck --continue + restore-cache-key : androidTest-build-artifacts instrumentation-tests : name : Instrumentation tests - needs: build-instrumentation-tests runs-on : macos-latest timeout-minutes : 45 strategy : # Allow tests to continue on other devices if they fail on one device. fail-fast : false matrix : + # Unclear that older versions actually honor command to disable animation. + # Newer versions are reputed to be too slow: https://github.com/ReactiveCircus/android-emulator-runner/issues/222 api-level : - 29 - # Unclear that older versions actually honor command to disable animation. - # Newer versions are reputed to be too slow: https://github.com/ReactiveCircus/android-emulator-runner/issues/222 + ### + shardNum : [ 1, 2, 3 ] + ### steps : - uses : actions/checkout@v3 - # This really just pulls the cache from the dependency job - - name : Build instrumented tests - uses : ./.github/actions/gradle-task + - name : Instrumented tests + uses : ./.github/actions/gradle-tasks-with-emulator with : - task : assembleDebugAndroidTest - restore-cache-key : androidTest-build-artifacts - - ## Actual task - - name : Instrumentation Tests - uses : reactivecircus/android-emulator-runner@v2 - with : - # @ychescale9 suspects Galaxy Nexus is the fastest one - profile : Galaxy Nexus api-level : ${{ matrix.api-level }} - arch : x86_64 - # Skip the benchmarks as this is running on emulators - script : ./gradlew connectedCheck -x :benchmarks:dungeon-benchmark:connectedCheck -x :benchmarks:performance-poetry:complex-benchmark:connectedCheck -x :benchmarks:performance-poetry:complex-poetry:connectedCheck - - - name : Upload results - if : ${{ always() }} - uses : actions/upload-artifact@v3 - with : - name : instrumentation-test-results-${{ matrix.api-level }} - path : ./**/build/reports/androidTests/connected/** + prepare-task : prepareConnectedCheckShard${{matrix.shardNum}} + test-task : connectedCheckShard${{matrix.shardNum}} -x :benchmarks:dungeon-benchmark:connectedCheck -x :benchmarks:performance-poetry:complex-benchmark:connectedCheck -x :benchmarks:performance-poetry:complex-poetry:connectedCheck + write-cache-key : androidTest-build-artifacts-${{matrix.shardNum}} + restore-cache-key : main-build-artifacts - conflate-renderings-instrumentation-tests : + runtime-instrumentation-tests : name : Conflate Stale Renderings Instrumentation tests runs-on : macos-latest timeout-minutes : 45 @@ -352,128 +324,21 @@ jobs : matrix : api-level : - 29 - # Unclear that older versions actually honor command to disable animation. - # Newer versions are reputed to be too slow: https://github.com/ReactiveCircus/android-emulator-runner/issues/222 + ### + shardNum : [ 1, 2, 3 ] + ### + runtime : [ conflate, baseline-stateChange, conflate-stateChange ] steps : - uses : actions/checkout@v3 - ## Build before running tests, using cache. - - name : Build instrumented tests - uses : ./.github/actions/gradle-task + - name : Instrumented tests + uses : ./.github/actions/gradle-tasks-with-emulator with : - # Unfortunately I don't think we can key this cache based on our project property so - # we clean and rebuild. - task : clean assembleDebugAndroidTest -Pworkflow.runtime=conflate - - ## Actual task - - name : Instrumentation Tests - uses : reactivecircus/android-emulator-runner@v2 - with : - # @ychescale9 suspects Galaxy Nexus is the fastest one - profile : Galaxy Nexus api-level : ${{ matrix.api-level }} - arch : x86_64 - # Skip the benchmarks as this is running on emulators - script : ./gradlew connectedCheck -x :benchmarks:dungeon-benchmark:connectedCheck -x :benchmarks:performance-poetry:complex-benchmark:connectedCheck -x :benchmarks:performance-poetry:complex-poetry:connectedCheck -Pworkflow.runtime=conflate - - - name : Upload results - if : ${{ always() }} - uses : actions/upload-artifact@v3 - with : - name : instrumentation-test-results-${{ matrix.api-level }} - path : ./**/build/reports/androidTests/connected/** - - stateChange-runtime-instrumentation-tests : - name : Render on State Change Only Instrumentation tests - runs-on : macos-latest - timeout-minutes : 45 - strategy : - # Allow tests to continue on other devices if they fail on one device. - fail-fast : false - matrix : - api-level : - - 29 - # Unclear that older versions actually honor command to disable animation. - # Newer versions are reputed to be too slow: https://github.com/ReactiveCircus/android-emulator-runner/issues/222 - steps : - - uses : actions/checkout@v3 - - name : set up JDK 11 - uses : actions/setup-java@v3 - with : - distribution : 'zulu' - java-version : 11 - - ## Build before running tests, using cache. - - name : Build instrumented tests - uses : ./.github/actions/gradle-task - with : - # Unfortunately I don't think we can key this cache based on our project property so - # we clean and rebuild. - task : clean assembleDebugAndroidTest -Pworkflow.runtime=baseline-stateChange - - ## Actual task - - name : Instrumentation Tests - uses : reactivecircus/android-emulator-runner@v2 - with : - # @ychescale9 suspects Galaxy Nexus is the fastest one - profile : Galaxy Nexus - api-level : ${{ matrix.api-level }} - arch : x86_64 - # Skip the benchmarks as this is running on emulators - script : ./gradlew connectedCheck -x :benchmarks:dungeon-benchmark:connectedCheck -x :benchmarks:performance-poetry:complex-benchmark:connectedCheck -x :benchmarks:performance-poetry:complex-poetry:connectedCheck -Pworkflow.runtime=baseline-stateChange - - - name : Upload results - if : ${{ always() }} - uses : actions/upload-artifact@v3 - with : - name : stateChange-instrumentation-test-results-${{ matrix.api-level }} - path : ./**/build/reports/androidTests/connected/** - - conflate-stateChange-runtime-instrumentation-tests : - name : Render on State Change Only and Conflate Stale Renderings Instrumentation tests - runs-on : macos-latest - timeout-minutes : 45 - strategy : - # Allow tests to continue on other devices if they fail on one device. - fail-fast : false - matrix : - api-level : - - 29 - # Unclear that older versions actually honor command to disable animation. - # Newer versions are reputed to be too slow: https://github.com/ReactiveCircus/android-emulator-runner/issues/222 - steps : - - uses : actions/checkout@v3 - - name : set up JDK 11 - uses : actions/setup-java@v3 - with : - distribution : 'zulu' - java-version : 11 - - ## Build before running tests, using cache. - - name : Build instrumented tests - uses : ./.github/actions/gradle-task - with : - # Unfortunately I don't think we can key this cache based on our project property so - # we clean and rebuild. - task : clean assembleDebugAndroidTest -Pworkflow.runtime=conflate-stateChange - - ## Actual task - - name : Instrumentation Tests - uses : reactivecircus/android-emulator-runner@v2 - with : - # @ychescale9 suspects Galaxy Nexus is the fastest one - profile : Galaxy Nexus - api-level : ${{ matrix.api-level }} - arch : x86_64 - # Skip the benchmarks as this is running on emulators - script : ./gradlew connectedCheck -x :benchmarks:dungeon-benchmark:connectedCheck -x :benchmarks:performance-poetry:complex-benchmark:connectedCheck -x :benchmarks:performance-poetry:complex-poetry:connectedCheck -Pworkflow.runtime=conflate-stateChange - - - name : Upload results - if : ${{ always() }} - uses : actions/upload-artifact@v3 - with : - name : conflate-stateChange-instrumentation-test-results-${{ matrix.api-level }} - path : ./**/build/reports/androidTests/connected/** + prepare-task : prepareConnectedCheckShard${{matrix.shardNum}} -Pworkflow.runtime=${{matrix.runtime}} + test-task : connectedCheckShard${{matrix.shardNum}} -Pworkflow.runtime=${{matrix.runtime}} -x :benchmarks:dungeon-benchmark:connectedCheck -x :benchmarks:performance-poetry:complex-benchmark:connectedCheck -x :benchmarks:performance-poetry:complex-poetry:connectedCheck + write-cache-key : androidTest-build-artifacts-${{matrix.shardNum}}-${{matrix.runtime}} + restore-cache-key : main-build-artifacts all-green : if : always() @@ -483,8 +348,6 @@ jobs : - api-check - artifacts-check - check - - conflate-renderings-instrumentation-tests - - conflate-stateChange-runtime-instrumentation-tests - dependency-guard - dokka - instrumentation-tests @@ -495,7 +358,8 @@ jobs : - jvm-stateChange-runtime-test - ktlint - performance-tests - - stateChange-runtime-instrumentation-tests + - runtime-instrumentation-tests + - shards-and-version - tutorials steps : diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 21caf00735..b17cf94ea6 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(libs.squareup.moshi) implementation(libs.squareup.moshi.adapters) implementation(libs.vanniktech.publish) + implementation(libs.java.diff.utils) ksp(libs.squareup.moshi.codegen) } diff --git a/build-logic/src/main/java/com/squareup/workflow1/buildsrc/diff.kt b/build-logic/src/main/java/com/squareup/workflow1/buildsrc/diff.kt new file mode 100644 index 0000000000..4be2b35912 --- /dev/null +++ b/build-logic/src/main/java/com/squareup/workflow1/buildsrc/diff.kt @@ -0,0 +1,56 @@ +package com.squareup.workflow1.buildsrc + +import com.github.difflib.text.DiffRow.Tag +import com.github.difflib.text.DiffRowGenerator +import com.squareup.workflow1.buildsrc.Color.Companion.colorized +import com.squareup.workflow1.buildsrc.Color.LIGHT_GREEN +import com.squareup.workflow1.buildsrc.Color.LIGHT_YELLOW + +fun diffString(oldStr: String, newStr: String): String { + + return buildString { + + val rows = DiffRowGenerator.create() + .showInlineDiffs(true) + .inlineDiffByWord(true) + .oldTag { _: Boolean? -> "" } + .newTag { _: Boolean? -> "" } + .build() + .generateDiffRows(oldStr.lines(), newStr.lines()) + + val linePadding = rows.size.toString().length + 1 + + rows.forEachIndexed { line, diffRow -> + if (diffRow.tag != Tag.EQUAL) { + append("line ${line.inc().toString().padEnd(linePadding)} ") + } + + if (diffRow.tag == Tag.CHANGE || diffRow.tag == Tag.DELETE) { + appendLine("-- ${diffRow.oldLine}".colorized(LIGHT_YELLOW)) + } + if (diffRow.tag == Tag.CHANGE) { + append(" " + " ".repeat(linePadding)) + } + if (diffRow.tag == Tag.CHANGE || diffRow.tag == Tag.INSERT) { + appendLine("++ ${diffRow.newLine}".colorized(LIGHT_GREEN)) + } + } + } +} + +@Suppress("MagicNumber") +internal enum class Color(val code: Int) { + LIGHT_GREEN(92), + LIGHT_YELLOW(93); + + companion object { + + private val supported = "win" !in System.getProperty("os.name").lowercase() + + fun String.colorized(color: Color) = if (supported) { + "\u001B[${color.code}m$this\u001B[0m" + } else { + this + } + } +} diff --git a/build-logic/src/main/java/com/squareup/workflow1/buildsrc/shardConnectedChecks.kt b/build-logic/src/main/java/com/squareup/workflow1/buildsrc/shardConnectedChecks.kt new file mode 100644 index 0000000000..cdf1e59762 --- /dev/null +++ b/build-logic/src/main/java/com/squareup/workflow1/buildsrc/shardConnectedChecks.kt @@ -0,0 +1,233 @@ +package com.squareup.workflow1.buildsrc + +import com.squareup.workflow1.buildsrc.sharding.ShardMatrixYamlTask.Companion.registerYamlShardsTasks +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.kotlin.dsl.provideDelegate +import kotlin.LazyThreadSafetyMode.NONE + +private const val SHARD_COUNT = 3 + +/** + * Create "shard" tasks which collectively depend upon all Android `connectedCheck` tasks in the + * entire project. + * + * Each shard depends upon the `connectedCheck` tasks of some subset of Android projects. + * Projects are assigned to a shard by counting the number of `@Test` annotations within their + * `androidTest` directory, then associating those projects to a shard in a round-robin fashion. + * + * These shards are invoked in CI using a GitHub Actions matrix. If the number of shards changes, + * the `connectedCheckShardMatrixYamlUpdate` task can automatically update the workflow file so + * that they're all invoked. + * + * The shard tasks are invoked as: + * ```shell + * # roughly 1/3 of the tests + * ./gradlew connectedCheckShard1 + * # the second third + * ./gradlew connectedCheckShard2 + * # the last third + * ./gradlew connectedCheckShard3 + * ``` + * + * @param target the root project which gets the shard tasks + */ +fun shardConnectedCheckTasks(target: Project) { + if (target != target.rootProject) { + throw GradleException("Only add connectedCheck shard tasks from the root project.") + } + + target.registerYamlShardsTasks( + shardCount = SHARD_COUNT, + startTagName = "### ", + endTagName = "### ", + taskNamePart = "connectedCheck", + yamlFile = target.rootProject.file(".github/workflows/kotlin.yml") + ) + + // Calculate the cost of each project's tests + val projectsWithTestCount = lazy(NONE) { + target.subprojects + // Only Android projects can have these tasks. + // Use the KGP Android plugin instead of AGP since KGP has only one ID to look for. + .filter { it.plugins.hasPlugin("org.jetbrains.kotlin.android") } + .map { it to it.androidTestCost() } + } + + // Assign each project to a shard. + // The values are lazy so that the work only happens at task configuration time, but they're + // outside the task configuration block so that it only happens once. + val shardAssignments = projectsWithTestCount.shards() + + val connectedTestName = "connectedCheck" + + shardAssignments.forEach { shard -> + + val projects by shard.projectsLazy + + val paths by lazy { + projects.joinToString(prefix = "[ ", postfix = " ]") { it.path } + } + + target.tasks.register("connectedCheckShard${shard.number}") { + + group = "Verification" + + validateSharding( + projectsWithTestCount = projectsWithTestCount.value, + shardAssignments = shardAssignments + ) + + description = "Runs $connectedTestName in projects: $paths" + + val assignedTests = projects.map { project -> + project.tasks.matching { it.name == connectedTestName } + } + + dependsOn(assignedTests) + } + + target.tasks.register("prepareConnectedCheckShard${shard.number}") { + + validateSharding( + projectsWithTestCount = projectsWithTestCount.value, + shardAssignments = shardAssignments + ) + + description = "Builds all artifacts for running connected tests in projects: $paths" + + val regex = Regex("""prepare[A-Z]\w*AndroidTestArtifacts""") + + val prepareTasks = projects.map { project -> + project.tasks.matching { it.name.matches(regex) } + } + + dependsOn(prepareTasks) + } + } +} + +/** + * Assigns each project to a shard, distributing them by the number of tests they have. + * The combined test costs of all shards should be approximately equal. + * + * There's a lot of `Lazy` here so that defer parsing all the tests until task configuration. + * If the tasks aren't actually being invoked, no parsing happens. + * + * @receiver Every project with its associated test cost. + * @return A list of shards, where each shard encapsulates a subset of projects. + */ +private fun Lazy>>.shards(): List { + + val shards by lazy { + List>>(SHARD_COUNT) { mutableListOf() } + .also { shards -> + + fun next(): MutableList> { + return shards.minBy { it.sumOf { (_, count) -> count } } + } + + // Sort the projects by descending test cost, then fall back to the project paths + // The path sort is just so that the shard composition is stable. If the shard composition + // isn't stable, the shard tasks may not be up-to-date and build caching in CI is broken. + val sorted = value.sortedWith(compareBy({ it.second }, { it.first })) + .reversed() + + for (pair in sorted) { + next().add(pair) + } + } + } + + return List(SHARD_COUNT) { index -> + Shard( + number = index + 1, + testCountLazy = lazy { shards[index].sumOf { (_, count) -> count } }, + projectsLazy = lazy { shards[index].map { (project, _) -> project } } + ) + } +} + +private data class Shard( + val number: Int, + val testCountLazy: Lazy, + val projectsLazy: Lazy> +) { + val testCount by testCountLazy + val projects by projectsLazy + override fun toString(): String { + return "Shard(number=$number, testCount=$testCount, projects=${projects.joinToString("\n") { it.path }})" + } +} + +private fun validateSharding( + projectsWithTestCount: List>, + shardAssignments: List, +) { + + val allShardsText by lazy(NONE) { shardAssignments.joinToString("\n") } + + if (shardAssignments.size != SHARD_COUNT) { + throw GradleException( + "Unexpected shard configuration. There should be $SHARD_COUNT shards, " + + "but `shardAssignments` is:\n$allShardsText" + ) + } + + val allShardedProjects = shardAssignments.flatMap { it.projects } + + val duplicates = allShardedProjects.groupingBy { it } + .eachCount() + .filter { it.value > 1 } + .keys + + if (duplicates.isNotEmpty()) { + throw GradleException( + "There are duplicated projects in shards.\n" + + "Duplicated projects: ${duplicates.map { it.path }}\n" + + "All shards:\n$allShardsText" + ) + } + + val missingInShards = projectsWithTestCount + .map { it.first } + .minus(allShardedProjects.toSet()) + + if (missingInShards.isNotEmpty()) { + throw GradleException( + "There are projects missing from all shards.\n" + + "Missing projects: $missingInShards\n" + + "All shards:\n$allShardsText" + ) + } +} + +/** + * matches: + * ``` + * @org.junit.Test + * @Test + * ``` + */ +private val testAnnotationRegex = """@(?:org\.junit\.)?Test\s+""".toRegex() + +/** + * Counts all the `androidTest` functions annotated with `@Test` within this project. + * + * Each test function has a cost of 1. A project with 20 tests has a cost of 20. + */ +private fun Project.androidTestCost(): Int { + + val androidTestSrc = file("src/androidTest/java") + + if (!androidTestSrc.exists()) return 0 + + return androidTestSrc + .walkTopDown() + .filter { it.isFile && it.extension == "kt" } + .sumOf { file -> + val fileText = file.readText() + + testAnnotationRegex.findAll(fileText).count() + } +} diff --git a/build-logic/src/main/java/com/squareup/workflow1/buildsrc/sharding/ShardMatrixYamlTask.kt b/build-logic/src/main/java/com/squareup/workflow1/buildsrc/sharding/ShardMatrixYamlTask.kt new file mode 100644 index 0000000000..0437afbc4a --- /dev/null +++ b/build-logic/src/main/java/com/squareup/workflow1/buildsrc/sharding/ShardMatrixYamlTask.kt @@ -0,0 +1,227 @@ +package com.squareup.workflow1.buildsrc.sharding + +import com.android.build.gradle.internal.tasks.factory.dependsOn +import com.squareup.workflow1.buildsrc.diffString +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity.RELATIVE +import org.gradle.api.tasks.TaskAction +import org.gradle.internal.logging.text.StyledTextOutput +import org.gradle.internal.logging.text.StyledTextOutputFactory +import org.gradle.language.base.plugins.LifecycleBasePlugin +import java.io.File +import javax.inject.Inject +import kotlin.LazyThreadSafetyMode.NONE + +/** + * This task manages test shard matrix configuration in a GitHub Actions workflow file, + * ensuring that it matches the value of [numShards]. + * + * @property yamlFile The CI configuration file this task works on. + * @property startTagProperty The start tag to identify the matrix section in the CI configuration file. + * @property endTagProperty The end tag to identify the matrix section in the CI configuration file. + * @property numShards The number of shards to use for tests. + * @property autoCorrect If `true`, the task will automatically correct any incorrect test shard + * matrix configurations. + * @property updateTaskName The name of the task that updates the test shard matrix. + */ +abstract class ShardMatrixYamlTask @Inject constructor( + objectFactory: ObjectFactory +) : DefaultTask() { + + /** kotlin.yml */ + @get:InputFile + @get:PathSensitive(RELATIVE) + val yamlFile = objectFactory.fileProperty() + + /** + * Used to identify the start of the matrix. + * Everything after this tag and before the end tag will be overwritten. + * + * ex: `### ` + */ + @get:Input abstract val startTagProperty: Property + private val startTag: String + get() = startTagProperty.get() + + /** + * Used to identify the end of the matrix. + * ex: `### ` + * */ + @get:Input abstract val endTagProperty: Property + private val endTag: String + get() = endTagProperty.get() + + /** for `3`, the matrix value would be `[ 1, 2, 3]` */ + @get:Input + abstract val numShards: Property + + /** + * If true the file will be updated. If false, the task will fail if the matrix is out of date. + */ + @get:Input + abstract val autoCorrect: Property + + @get:Input + abstract val updateTaskName: Property + + private val matrixSectionRegex by lazy(NONE) { + + val startTagEscaped = Regex.escape(startTag) + val endTagEscaped = Regex.escape(endTag) + + Regex("""( *)(.*$startTagEscaped.*\n)[\s\S]+?(.*$endTagEscaped)""") + } + + @TaskAction + fun execute() { + val ciFile = requireCiFile() + + val ciText = ciFile.readText() + + val newText = replaceYamlSections(ciText) + + if (ciText != newText) { + + if (autoCorrect.get()) { + + ciFile.writeText(newText) + + val message = "Updated the test shard matrix in the CI file.\n" + + "\tfile://${yamlFile.get()}" + + services + .get(StyledTextOutputFactory::class.java) + .create("workflow-yaml-matrix") + .withStyle(StyledTextOutput.Style.Description) + .println(message) + + println() + println(diffString(ciText, newText)) + println() + } else { + val message = "The test shard matrix in the CI file is out of date.\n" + + "\tfile://${yamlFile.get()}\n\n" + + "Run ./gradlew ${updateTaskName.get()} to automatically update." + + throw GradleException(message) + } + } + } + + private fun replaceYamlSections(ciText: String): String { + + if (!ciText.contains(matrixSectionRegex)) { + val message = + "Couldn't find any `$startTag`/`$endTag` sections in the CI file:" + + "\tfile://${yamlFile.get()}\n\n" + + "\tSurround the matrix section with the comments '$startTag' and `$endTag':\n\n" + + "\t strategy:\n" + + "\t ### $startTag\n" + + "\t matrix:\n" + + "\t [ ... ]\n" + + "\t ### $endTag\n" + + throw GradleException(message) + } + + return ciText.replace(matrixSectionRegex) { match -> + + val (indent, startTag, closingLine) = match.destructured + + val newContent = createYaml(indent, numShards.get()) + + "$indent$startTag$newContent$closingLine" + } + } + + private fun requireCiFile(): File { + val ciFile = yamlFile.get().asFile + + require(ciFile.exists()) { + "Could not resolve file: file://$ciFile" + } + + return ciFile + } + + private fun createYaml( + indent: String, + numShards: Int + ): String { + + val shardList = buildString { + append("[ ") + repeat(numShards) { + val i = it + 1 + append("$i") + if (i < numShards) append(", ") + } + append(" ]") + } + + return "${indent}shardNum : $shardList\n" + } + + companion object { + /** + * Registers tasks to check and update the test shard matrix configuration in `kotlin.yml`. + * + * @param shardCount The number of test shards. + * @param startTagName The start tag to identify the matrix section in `kotlin.yml`. + * @param endTagName The end tag to identify the matrix section in `kotlin.yml`. + * @param taskNamePart The part of the sharded task name which will be prepended to + * the matrix update task names. + * @param yamlFile presumably `kotlin.yml`. + */ + fun Project.registerYamlShardsTasks( + shardCount: Int, + startTagName: String, + endTagName: String, + taskNamePart: String, + yamlFile: File + ) { + + require(yamlFile.exists()) { + "Could not resolve '$yamlFile'." + } + + val updateName = "${taskNamePart}ShardMatrixYamlUpdate" + val updateTask = tasks.register( + updateName, + ShardMatrixYamlTask::class.java + ) { + val task = this + task.yamlFile.set(yamlFile) + numShards.set(shardCount) + startTagProperty.set(startTagName) + endTagProperty.set(endTagName) + autoCorrect.set(true) + updateTaskName.set(updateName) + } + + val checkTask = tasks.register( + "${taskNamePart}ShardMatrixYamlCheck", + ShardMatrixYamlTask::class.java + ) { + val task = this + task.yamlFile.set(yamlFile) + numShards.set(shardCount) + startTagProperty.set(startTagName) + endTagProperty.set(endTagName) + autoCorrect.set(false) + updateTaskName.set(updateName) + mustRunAfter(updateTask) + } + + // Automatically run this check task when running the `check` lifecycle task + tasks.named(LifecycleBasePlugin.CHECK_TASK_NAME).dependsOn(checkTask) + } + } +} diff --git a/build-logic/src/main/java/kotlin-android.gradle.kts b/build-logic/src/main/java/kotlin-android.gradle.kts index a4f228adde..d2afd125a3 100644 --- a/build-logic/src/main/java/kotlin-android.gradle.kts +++ b/build-logic/src/main/java/kotlin-android.gradle.kts @@ -1,4 +1,6 @@ +import com.android.build.api.variant.AndroidComponentsExtension import com.squareup.workflow1.buildsrc.kotlinCommonSettings +import org.gradle.configurationcache.extensions.capitalized plugins { kotlin("android") @@ -10,3 +12,28 @@ extensions.getByType(JavaPluginExtension::class).apply { } project.kotlinCommonSettings(bomConfigurationName = "implementation") + +// For every variant which extends the `debug` type, create a new task which generates all the +// artifacts used in the associated `connected____AndroidTest` task. +extensions.configure>("androidComponents") { + onVariants(selector().withBuildType("debug")) { variant -> + + val nameCaps = variant.name.capitalized() + val testTask = "connected${nameCaps}AndroidTest" + + tasks.register("prepare${nameCaps}AndroidTestArtifacts") { + description = "Creates all artifacts used in `$testTask` without trying to execute tests." + + dependsOn(tasks.getByName(testTask).taskDependencies) + } + } +} + + +/* + +macOS-main-build-artifacts-09555650eb7d6a7cc5d80ddfef5d6ea7bcf31c6cacdf093c62360bd678cb852f-3bd2c177794706d31840536092bfe0e2022bde51249e328646cb585e83e3c119-11a8ec8535b59590a882b41cbf4c2a235a2995758775ba8f9d82847ed5f3f87c-470e8ec00b1701c50f78eb15d625d72753e92d468b096562a31ffab8e1d3f1dd +macOS-main-build-artifacts-09555650eb7d6a7cc5d80ddfef5d6ea7bcf31c6cacdf093c62360bd678cb852f-3bd2c177794706d31840536092bfe0e2022bde51249e328646cb585e83e3c119-ea20820e0c811509dddd86ac8a9f53b6d1bd0785caed7898f1ef2d3390cfae19-470e8ec00b1701c50f78eb15d625d72753e92d468b096562a31ffab8e1d3f1dd + + + */ diff --git a/build.gradle.kts b/build.gradle.kts index b2b5487f40..dbca9edf6d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,4 @@ +import com.squareup.workflow1.buildsrc.shardConnectedCheckTasks import org.jetbrains.dokka.gradle.AbstractDokkaLeafTask import java.net.URL @@ -28,6 +29,8 @@ plugins { alias(libs.plugins.ktlint) } +shardConnectedCheckTasks(project) + subprojects { afterEvaluate { diff --git a/dependencies/classpath.txt b/dependencies/classpath.txt index 5db68a654c..44ff8b93ef 100644 --- a/dependencies/classpath.txt +++ b/dependencies/classpath.txt @@ -1,43 +1,43 @@ -androidx.databinding:databinding-common:7.4.1 -androidx.databinding:databinding-compiler-common:7.4.1 -com.android.databinding:baseLibrary:7.4.1 -com.android.tools.analytics-library:crash:30.4.1 -com.android.tools.analytics-library:protos:30.4.1 -com.android.tools.analytics-library:shared:30.4.1 -com.android.tools.analytics-library:tracker:30.4.1 +androidx.databinding:databinding-common:7.4.2 +androidx.databinding:databinding-compiler-common:7.4.2 +com.android.databinding:baseLibrary:7.4.2 +com.android.tools.analytics-library:crash:30.4.2 +com.android.tools.analytics-library:protos:30.4.2 +com.android.tools.analytics-library:shared:30.4.2 +com.android.tools.analytics-library:tracker:30.4.2 com.android.tools.build.jetifier:jetifier-core:1.0.0-beta10 com.android.tools.build.jetifier:jetifier-processor:1.0.0-beta10 -com.android.tools.build:aapt2-proto:7.4.1-8841542 -com.android.tools.build:aaptcompiler:7.4.1 -com.android.tools.build:apksig:7.4.1 -com.android.tools.build:apkzlib:7.4.1 -com.android.tools.build:builder-model:7.4.1 -com.android.tools.build:builder-test-api:7.4.1 -com.android.tools.build:builder:7.4.1 +com.android.tools.build:aapt2-proto:7.4.2-8841542 +com.android.tools.build:aaptcompiler:7.4.2 +com.android.tools.build:apksig:7.4.2 +com.android.tools.build:apkzlib:7.4.2 +com.android.tools.build:builder-model:7.4.2 +com.android.tools.build:builder-test-api:7.4.2 +com.android.tools.build:builder:7.4.2 com.android.tools.build:bundletool:1.11.4 -com.android.tools.build:gradle-api:7.4.1 -com.android.tools.build:gradle-settings-api:7.4.1 -com.android.tools.build:gradle:7.4.1 -com.android.tools.build:manifest-merger:30.4.1 +com.android.tools.build:gradle-api:7.4.2 +com.android.tools.build:gradle-settings-api:7.4.2 +com.android.tools.build:gradle:7.4.2 +com.android.tools.build:manifest-merger:30.4.2 com.android.tools.build:transform-api:2.0.0-deprecated-use-gradle-api -com.android.tools.ddms:ddmlib:30.4.1 -com.android.tools.layoutlib:layoutlib-api:30.4.1 -com.android.tools.lint:lint-model:30.4.1 -com.android.tools.lint:lint-typedef-remover:30.4.1 -com.android.tools.utp:android-device-provider-ddmlib-proto:30.4.1 -com.android.tools.utp:android-device-provider-gradle-proto:30.4.1 -com.android.tools.utp:android-test-plugin-host-additional-test-output-proto:30.4.1 -com.android.tools.utp:android-test-plugin-host-coverage-proto:30.4.1 -com.android.tools.utp:android-test-plugin-host-retention-proto:30.4.1 -com.android.tools.utp:android-test-plugin-result-listener-gradle-proto:30.4.1 -com.android.tools:annotations:30.4.1 -com.android.tools:common:30.4.1 -com.android.tools:dvlib:30.4.1 -com.android.tools:repository:30.4.1 -com.android.tools:sdk-common:30.4.1 -com.android.tools:sdklib:30.4.1 -com.android:signflinger:7.4.1 -com.android:zipflinger:7.4.1 +com.android.tools.ddms:ddmlib:30.4.2 +com.android.tools.layoutlib:layoutlib-api:30.4.2 +com.android.tools.lint:lint-model:30.4.2 +com.android.tools.lint:lint-typedef-remover:30.4.2 +com.android.tools.utp:android-device-provider-ddmlib-proto:30.4.2 +com.android.tools.utp:android-device-provider-gradle-proto:30.4.2 +com.android.tools.utp:android-test-plugin-host-additional-test-output-proto:30.4.2 +com.android.tools.utp:android-test-plugin-host-coverage-proto:30.4.2 +com.android.tools.utp:android-test-plugin-host-retention-proto:30.4.2 +com.android.tools.utp:android-test-plugin-result-listener-gradle-proto:30.4.2 +com.android.tools:annotations:30.4.2 +com.android.tools:common:30.4.2 +com.android.tools:dvlib:30.4.2 +com.android.tools:repository:30.4.2 +com.android.tools:sdk-common:30.4.2 +com.android.tools:sdklib:30.4.2 +com.android:signflinger:7.4.2 +com.android:zipflinger:7.4.2 com.dropbox.dependency-guard:dependency-guard:0.4.3 com.fasterxml.jackson.core:jackson-annotations:2.12.7 com.fasterxml.jackson.core:jackson-core:2.12.7 @@ -69,8 +69,8 @@ com.googlecode.java-diff-utils:diffutils:1.3.0 com.googlecode.juniversalchardet:juniversalchardet:1.0.3 com.rickbusarow.ktlint:com.rickbusarow.ktlint.gradle.plugin:0.1.7 com.rickbusarow.ktlint:ktlint-gradle-plugin:0.1.7 -com.squareup.moshi:moshi-adapters:1.13.0 -com.squareup.moshi:moshi:1.13.0 +com.squareup.moshi:moshi-adapters:1.15.0 +com.squareup.moshi:moshi:1.15.0 com.squareup.okhttp3:okhttp:4.10.0 com.squareup.okio:okio-jvm:3.0.0 com.squareup.okio:okio:3.0.0 @@ -81,11 +81,12 @@ com.squareup:javawriter:2.5.0 com.sun.activation:javax.activation:1.2.0 com.sun.istack:istack-commons-runtime:3.0.8 com.sun.xml.fastinfoset:FastInfoset:1.2.16 -com.vanniktech:gradle-maven-publish-plugin:0.22.0 -com.vanniktech:nexus:0.22.0 +com.vanniktech:gradle-maven-publish-plugin:0.25.2 +com.vanniktech:nexus:0.25.2 commons-codec:commons-codec:1.11 commons-io:commons-io:2.4 commons-logging:commons-logging:1.2 +io.github.java-diff-utils:java-diff-utils:4.12 io.grpc:grpc-api:1.39.0 io.grpc:grpc-context:1.39.0 io.grpc:grpc-core:1.39.0 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ce51ccea4f..97d58a2511 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -androidTools = "7.4.1" +androidTools = "7.4.2" compileSdk = "33" minSdk = "21" @@ -23,13 +23,13 @@ androidx-paging = "3.0.1" androidx-profileinstaller = "1.2.0-alpha02" androidx-recyclerview = "1.2.1" androidx-room = "2.4.0-alpha04" -androidx-savedstate = "1.1.0" +androidx-savedstate = "1.2.1" androidx-startup = "1.1.0" androidx-test = "1.5.0" androidx-test-espresso = "3.5.1" androidx-test-junit-ext = "1.1.5" androidx-test-runner = "1.5.2" -androidx-test-truth-ext = "1.4.0" +androidx-test-truth-ext = "1.5.0" androidx-tracing = "1.1.0" androidx-transition = "1.4.1" androidx-viewbinding = "4.2.1" @@ -46,44 +46,45 @@ google-material = "1.4.0" groovy = "3.0.9" jUnit = "4.13.2" +java-diff-utils = "4.12" javaParser = "3.24.0" kotest = "5.1.0" kotlin = "1.8.10" kotlinx-binary-compatibility = "0.13.2" kotlinx-coroutines = "1.7.1" -kotlinx-serialization-json = "1.3.2" -kotlinx-atomicfu = "0.17.2" +kotlinx-serialization-json = "1.5.1" +kotlinx-atomicfu = "0.21.0" ktlint = "0.49.1" ktlint-gradle = "0.1.7" material = "1.3.0" mavenPublish = "0.13.0" -mockito-core = "3.3.3" +mockito-core = "3.12.4" mockito-kotlin = "3.2.0" -mockk = "1.11.0" -robolectric = "4.9.2" +mockk = "1.13.5" +robolectric = "4.10.3" rxjava2-android = "2.1.1" rxjava2-core = "2.2.21" squareup-curtains = "1.2.4" squareup-cycler = "0.1.9" -squareup-leakcanary = "2.10" -squareup-moshi = "1.13.0" +squareup-leakcanary = "2.12" +squareup-moshi = "1.15.0" squareup-okhttp = "4.9.1" -squareup-okio = "3.0.0" +squareup-okio = "3.3.0" squareup-radiography = "2.4.1" squareup-retrofit = "2.9.0" squareup-seismic = "1.0.3" squareup-workflow = "1.0.0" -timber = "4.7.1" -truth = "1.1.3" -turbine = "0.13.0" -vanniktech-publish = "0.22.0" +timber = "5.0.1" +truth = "1.1.5" +turbine = "1.0.0" +vanniktech-publish = "0.25.2" [plugins] @@ -182,7 +183,9 @@ google-ksp = { module = "com.google.devtools.ksp:symbol-processing-gradle-plugin hamcrest = "org.hamcrest:hamcrest-core:2.2" -jetbrains-annotations = "org.jetbrains:annotations:19.0.0" +java-diff-utils = { module = "io.github.java-diff-utils:java-diff-utils", version.ref = "java-diff-utils" } + +jetbrains-annotations = "org.jetbrains:annotations:24.0.1" junit = { module = "junit:junit", version.ref = "jUnit" } diff --git a/samples/tutorial/build.gradle b/samples/tutorial/build.gradle index d0b671dd6c..763cd2306e 100644 --- a/samples/tutorial/build.gradle +++ b/samples/tutorial/build.gradle @@ -8,7 +8,7 @@ buildscript { deps = [ activityktx: 'androidx.activity:activity-ktx:1.3.0', - agp: "com.android.tools.build:gradle:7.4.1", + agp: "com.android.tools.build:gradle:7.4.2", appcompat: 'androidx.appcompat:appcompat:1.3.1', constraintlayout: 'androidx.constraintlayout:constraintlayout:2.0.1', kotlin: [ diff --git a/samples/tutorial/tutorial-base/build.gradle b/samples/tutorial/tutorial-base/build.gradle index a58e90c29c..aef3354b83 100644 --- a/samples/tutorial/tutorial-base/build.gradle +++ b/samples/tutorial/tutorial-base/build.gradle @@ -32,4 +32,15 @@ dependencies { implementation deps.material implementation deps.workflow.core_android implementation project(':tutorial-views') + + implementation deps.activityktx + implementation deps.viewmodelktx + implementation deps.viewmodelsavedstate + + + implementation deps.workflow.container_android + implementation deps.workflow.core_android + + testImplementation deps.kotlin.test + testImplementation deps.workflow.testing } diff --git a/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/RootWorkflow.kt b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/RootWorkflow.kt new file mode 100644 index 0000000000..c9b7feb01f --- /dev/null +++ b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/RootWorkflow.kt @@ -0,0 +1,66 @@ +package workflow.tutorial + +import com.squareup.workflow1.Snapshot +import com.squareup.workflow1.StatefulWorkflow +import com.squareup.workflow1.action +import com.squareup.workflow1.renderChild +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.container.BackStackScreen +import com.squareup.workflow1.ui.container.toBackStackScreen +import workflow.tutorial.RootWorkflow.State +import workflow.tutorial.RootWorkflow.State.Todo +import workflow.tutorial.RootWorkflow.State.Welcome +import workflow.tutorial.TodoListWorkflow.ListProps +import workflow.tutorial.TodoWorkflow.TodoProps + +@OptIn(WorkflowUiExperimentalApi::class) +object RootWorkflow : StatefulWorkflow>() { + + sealed class State { + object Welcome : State() + data class Todo(val username: String) : State() + } + override fun initialState( + props: Unit, + snapshot: Snapshot? + ): State = Welcome + + override fun render( + renderProps: Unit, + renderState: State, + context: RenderContext + ): BackStackScreen { + + val backStackScreens = mutableListOf() + + val welcomeScreen = context.renderChild(child = WelcomeWorkflow, key = "") { output -> + login(output.username) + } + + backStackScreens += welcomeScreen + + + when(renderState) { + is Welcome -> {} + is Todo -> { + val todoScreens = context.renderChild(child = TodoWorkflow, TodoProps(username = renderState.username)) { + logout() + } + backStackScreens.addAll(todoScreens) + } + } + + return backStackScreens.toBackStackScreen() + } + + override fun snapshotState(state: State): Snapshot? = null + + private fun login(username: String) = action { + state = Todo(username) + } + + private fun logout() = action { + state = Welcome + } +} diff --git a/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoEditScreen.kt b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoEditScreen.kt new file mode 100644 index 0000000000..e221ee1071 --- /dev/null +++ b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoEditScreen.kt @@ -0,0 +1,15 @@ +package workflow.tutorial + +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +@OptIn(WorkflowUiExperimentalApi::class) +data class TodoEditScreen( + val title: String, + val note: String, + val onTitleChanged: (String) -> Unit, + val onNoteChanged: (String) -> Unit, + + val discardChanges: () -> Unit, + val saveChanges: () -> Unit +): Screen diff --git a/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoEditScreenViewRunner.kt b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoEditScreenViewRunner.kt new file mode 100644 index 0000000000..c024bae759 --- /dev/null +++ b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoEditScreenViewRunner.kt @@ -0,0 +1,37 @@ +package workflow.tutorial + +import com.squareup.workflow1.ui.LayoutRunner +import com.squareup.workflow1.ui.LayoutRunner.Companion.bind +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewFactory.Companion +import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.ViewFactory +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.backPressedHandler +import com.squareup.workflow1.ui.setTextChangedListener +import com.squareup.workflow1.ui.updateText +import workflow.tutorial.views.databinding.TodoEditViewBinding + +@OptIn(WorkflowUiExperimentalApi::class) +class TodoEditScreenViewRunner( + private val binding: TodoEditViewBinding +) : ScreenViewRunner { + + override fun showRendering( + rendering: TodoEditScreen, + viewEnvironment: ViewEnvironment + ) { + binding.root.backPressedHandler = rendering.discardChanges + binding.save.setOnClickListener { rendering.saveChanges() } + binding.todoTitle.updateText(rendering.title) + binding.todoTitle.setTextChangedListener { rendering.onTitleChanged(it.toString()) } + binding.todoNote.updateText(rendering.note) + binding.todoNote.setTextChangedListener { rendering.onNoteChanged(it.toString()) } + } + + companion object : ScreenViewFactory by ScreenViewFactory.Companion.fromViewBinding( + bindingInflater = TodoEditViewBinding::inflate, + constructor = ::TodoEditScreenViewRunner + ) +} diff --git a/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoEditWorkflow.kt b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoEditWorkflow.kt new file mode 100644 index 0000000000..1aee563e38 --- /dev/null +++ b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoEditWorkflow.kt @@ -0,0 +1,70 @@ +package workflow.tutorial + +import android.icu.text.CaseMap.Title +import com.squareup.workflow1.Snapshot +import com.squareup.workflow1.StatefulWorkflow +import com.squareup.workflow1.action +import workflow.tutorial.TodoEditWorkflow.EditProps +import workflow.tutorial.TodoEditWorkflow.Output +import workflow.tutorial.TodoEditWorkflow.Output.Discard +import workflow.tutorial.TodoEditWorkflow.Output.Save +import workflow.tutorial.TodoEditWorkflow.State + +object TodoEditWorkflow : StatefulWorkflow() { + + data class EditProps( + val initialTodo: TodoModel + ) + + data class State( + val todo:TodoModel + ) + + sealed class Output { + object Discard: Output() + data class Save(val todo: TodoModel): Output() + } + override fun initialState( + props: EditProps, + snapshot: Snapshot? + ): State = State(props.initialTodo) + + override fun onPropsChanged( + old: EditProps, + new: EditProps, + state: State + ): State { + if (old.initialTodo != new.initialTodo) { + return state.copy(todo = new.initialTodo) + } + return state + } + + override fun render( + renderProps: EditProps, + renderState: State, + context: RenderContext + ): TodoEditScreen { + return TodoEditScreen( + title = renderState.todo.title, + note = renderState.todo.note, + onTitleChanged = { context.actionSink.send(onTitleChanged(it)) }, + onNoteChanged = { context.actionSink.send(onNoteChanged(it)) }, + saveChanges = { context.actionSink.send(onSave()) }, + discardChanges = { context.actionSink.send(onDiscard()) } + ) + } + + override fun snapshotState(state: State): Snapshot? = null + + internal fun onTitleChanged(title: String) = action { state = state.withTitle(title) } + + internal fun onNoteChanged(note: String) = action { state = state.withNote(note) } + + internal fun onDiscard() = action { setOutput(Discard) } + + internal fun onSave() = action { setOutput(Save(state.todo)) } + + private fun State.withTitle(title: String) = copy(todo = todo.copy(title = title)) + private fun State.withNote(note: String) = copy(todo = todo.copy(note = note)) +} diff --git a/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoListScreen.kt b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoListScreen.kt new file mode 100644 index 0000000000..99b422ecee --- /dev/null +++ b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoListScreen.kt @@ -0,0 +1,12 @@ +package workflow.tutorial + +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +@OptIn(WorkflowUiExperimentalApi::class) +data class TodoListScreen( + val username: String, + val todoTitles: List, + val onTodoSelected: (Int) -> Unit, + val onBack: () -> Unit +): Screen diff --git a/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoListScreenViewRunner.kt b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoListScreenViewRunner.kt new file mode 100644 index 0000000000..2b532a74cb --- /dev/null +++ b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoListScreenViewRunner.kt @@ -0,0 +1,40 @@ +package workflow.tutorial + +import androidx.recyclerview.widget.LinearLayoutManager +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.backPressedHandler +import workflow.tutorial.views.TodoListAdapter +import workflow.tutorial.views.databinding.TodoListViewBinding + +@OptIn(WorkflowUiExperimentalApi::class) +class TodoListScreenViewRunner( + private val binding: TodoListViewBinding +) : ScreenViewRunner { + + val adapter = TodoListAdapter() + init { + binding.todoList.layoutManager = LinearLayoutManager(binding.root.context) + binding.todoList.adapter = adapter + } + override fun showRendering( + rendering: TodoListScreen, + viewEnvironment: ViewEnvironment + ) { + binding.root.backPressedHandler = rendering.onBack + + with(binding.todoListWelcome) { + text = resources.getString(R.string.todo_list_welcome, rendering.username) + } + + adapter.todoList = rendering.todoTitles + adapter.onTodoSelected = rendering.onTodoSelected + adapter.notifyDataSetChanged() + } + + companion object : ScreenViewFactory by ScreenViewFactory.fromViewBinding( + TodoListViewBinding::inflate, ::TodoListScreenViewRunner + ) +} diff --git a/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoListWorkflow.kt b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoListWorkflow.kt new file mode 100644 index 0000000000..a5fbd3fa83 --- /dev/null +++ b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoListWorkflow.kt @@ -0,0 +1,45 @@ +package workflow.tutorial + +import com.squareup.workflow1.Snapshot +import com.squareup.workflow1.StatefulWorkflow +import com.squareup.workflow1.StatelessWorkflow +import com.squareup.workflow1.action +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import workflow.tutorial.TodoListWorkflow.ListProps +import workflow.tutorial.TodoListWorkflow.Output + +@OptIn(WorkflowUiExperimentalApi::class) +object TodoListWorkflow : StatelessWorkflow() { + + data class ListProps( + val username: String, + val todos: List + ) + + sealed class Output { + object Back : Output() + data class SelectTodo(val index: Int) : Output() + } + + + private fun onBack() = action { + setOutput(Output.Back) + } + + private fun selectTodo(index: Int) = action { + setOutput(Output.SelectTodo(index)) + } + + override fun render( + renderProps: ListProps, + context: RenderContext + ): TodoListScreen { + val titles = renderProps.todos.map { it.title } + return TodoListScreen( + username = renderProps.username, + todoTitles = titles, + onTodoSelected = { context.actionSink.send(selectTodo(it)) }, + onBack = { context.actionSink.send(onBack()) } + ) + } +} diff --git a/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoModel.kt b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoModel.kt new file mode 100644 index 0000000000..d9eb22fef2 --- /dev/null +++ b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoModel.kt @@ -0,0 +1,6 @@ +package workflow.tutorial + +data class TodoModel( + val title: String, + val note: String +) diff --git a/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoWorkflow.kt b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoWorkflow.kt new file mode 100644 index 0000000000..85a03167c4 --- /dev/null +++ b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TodoWorkflow.kt @@ -0,0 +1,114 @@ +package workflow.tutorial + +import com.squareup.workflow1.Snapshot +import com.squareup.workflow1.StatefulWorkflow +import com.squareup.workflow1.action +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import workflow.tutorial.TodoEditWorkflow.EditProps +import workflow.tutorial.TodoEditWorkflow.Output.Discard +import workflow.tutorial.TodoEditWorkflow.Output.Save +import workflow.tutorial.TodoListWorkflow.ListProps +import workflow.tutorial.TodoListWorkflow.Output +import workflow.tutorial.TodoListWorkflow.Output.SelectTodo +import workflow.tutorial.TodoWorkflow.Back +import workflow.tutorial.TodoWorkflow.State +import workflow.tutorial.TodoWorkflow.State.Step +import workflow.tutorial.TodoWorkflow.TodoProps + +@OptIn(WorkflowUiExperimentalApi::class) +object TodoWorkflow : StatefulWorkflow>() { + + data class TodoProps(val username: String) + object Back + data class State( + val todos: List, + val step: Step + ) { + sealed class Step { + object List: Step() + + data class Edit(val index: Int) : Step() + } + } + + override fun initialState( + props: TodoProps, + snapshot: Snapshot? + ): State = State( + todos = listOf( + TodoModel( + title = "Take the cat for a walk", + note = "Cats really need their outside sunshine time. Don't forget to walk " + + "Charlie. Hamilton is less excited about the prospect.", + ) + ), + step = Step.List + ) + + override fun render( + renderProps: TodoProps, + renderState: State, + context: RenderContext + ): List { + val todoListScreen = context.renderChild( + TodoListWorkflow, + props = ListProps( + username = renderProps.username, + todos = renderState.todos + ) + ) { output -> + when (output) { + Output.Back -> onBack() + is SelectTodo -> editTodo(output.index) + } + } + + return when (val step = renderState.step) { + // On the "list" step, return just the list screen. + Step.List -> listOf(todoListScreen) + is Step.Edit -> { + // On the "edit" step, return both the list and edit screens. + val todoEditScreen = context.renderChild( + TodoEditWorkflow, + EditProps(renderState.todos[step.index]) + ) { output -> + when (output) { + // Send the discardChanges action when the discard output is received. + Discard -> discardChanges() + // Send the saveChanges action when the save output is received. + is Save -> saveChanges(output.todo, step.index) + } + } + return listOf(todoListScreen, todoEditScreen) + } + } + } + + private fun discardChanges() = action { + // When a discard action is received, return to the list. + state = state.copy(step = Step.List) + } + + private fun saveChanges( + todo: TodoModel, + index: Int + ) = action { + // When changes are saved, update the state of that todo item and return to the list. + state = state.copy( + todos = state.todos.toMutableList().also { it[index] = todo }, + step = Step.List + ) + } + + private fun onBack() = action { + // When an onBack action is received, emit a Back output. + setOutput(Back) + } + + private fun editTodo(index: Int) = action { + // When a todo item is selected, edit it. + state = state.copy(step = Step.Edit(index)) + } + override fun snapshotState(state: State): Snapshot? = null +} diff --git a/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TutorialActivity.kt b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TutorialActivity.kt index 1a5ea328cf..d926c65633 100644 --- a/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TutorialActivity.kt +++ b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/TutorialActivity.kt @@ -1,12 +1,57 @@ +@file:OptIn(WorkflowUiExperimentalApi::class) package workflow.tutorial import android.os.Bundle +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.viewModelScope +import com.squareup.workflow1.ui.ViewRegistry +import com.squareup.workflow1.ui.WorkflowLayout +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.asScreen +import com.squareup.workflow1.ui.container.BackStackContainer +import com.squareup.workflow1.ui.container.withRegistry +import com.squareup.workflow1.ui.renderWorkflowIn +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map class TutorialActivity : AppCompatActivity() { + + private val viewRegistry = ViewRegistry( + // No need to add BackStackContainer. Its now by default built in ViewRegistry + // com.squareup.workflow1.ui.backstack.BackStackContainer, + WelcomeScreenViewRunner, + TodoListScreenViewRunner, + TodoEditScreenViewRunner + ) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.welcome_view) + + val viewModel: TutorialViewModel by viewModels() + + setContentView( + WorkflowLayout(this).apply { + take( + this@TutorialActivity.lifecycle, + viewModel.renderings.map { asScreen(it).withRegistry(viewRegistry) } + ) + } + ) + } +} + +class TutorialViewModel(savedState: SavedStateHandle): ViewModel() { + val renderings : StateFlow by lazy { + renderWorkflowIn( + workflow = RootWorkflow, + scope = viewModelScope, + savedStateHandle = savedState + ) } } diff --git a/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/WelcomeScreen.kt b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/WelcomeScreen.kt new file mode 100644 index 0000000000..7aa22bb645 --- /dev/null +++ b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/WelcomeScreen.kt @@ -0,0 +1,11 @@ +package workflow.tutorial + +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +@OptIn(WorkflowUiExperimentalApi::class) +data class WelcomeScreen( + val username: String, + val onUsernameChanged: (String) -> Unit, + val onLoginTapped: () -> Unit +): Screen diff --git a/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/WelcomeScreenViewRunner.kt b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/WelcomeScreenViewRunner.kt new file mode 100644 index 0000000000..10d8c55c93 --- /dev/null +++ b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/WelcomeScreenViewRunner.kt @@ -0,0 +1,31 @@ +package workflow.tutorial + +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.setTextChangedListener +import com.squareup.workflow1.ui.updateText +import workflow.tutorial.views.databinding.WelcomeViewBinding + +@OptIn(WorkflowUiExperimentalApi::class) +class WelcomeScreenViewRunner( + private val binding: WelcomeViewBinding +) : ScreenViewRunner { + + override fun showRendering( + rendering: WelcomeScreen, + viewEnvironment: ViewEnvironment + ) { + binding.username.updateText(rendering.username) + binding.username.setTextChangedListener { + rendering.onUsernameChanged(it.toString()) + } + binding.login.setOnClickListener { rendering.onLoginTapped() } + } + + companion object : ScreenViewFactory by ScreenViewFactory.Companion.fromViewBinding( + bindingInflater = WelcomeViewBinding::inflate, + constructor = { welcomeViewBinding -> WelcomeScreenViewRunner(welcomeViewBinding) } + ) +} diff --git a/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/WelcomeWorkflow.kt b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/WelcomeWorkflow.kt new file mode 100644 index 0000000000..7f6e9848bd --- /dev/null +++ b/samples/tutorial/tutorial-base/src/main/java/workflow/tutorial/WelcomeWorkflow.kt @@ -0,0 +1,41 @@ +package workflow.tutorial + +import com.squareup.workflow1.Snapshot +import com.squareup.workflow1.StatefulWorkflow +import com.squareup.workflow1.action +import workflow.tutorial.WelcomeWorkflow.LoggedIn +import workflow.tutorial.WelcomeWorkflow.State + +object WelcomeWorkflow : StatefulWorkflow() { + + data class LoggedIn(val username: String) + data class State( + val username: String + ) + + override fun initialState( + props: Unit, + snapshot: Snapshot? + ): State = State(username = "") + + override fun render( + renderProps: Unit, + renderState: State, + context: RenderContext + ): WelcomeScreen = WelcomeScreen( + username = renderState.username, + onUsernameChanged = { context.actionSink.send(onUsernameChanged(it)) }, + onLoginTapped = { context.actionSink.send(onLogin()) } + ) + override fun snapshotState(state: State): Snapshot? = null + + internal fun onUsernameChanged(username: String) = action { + state = state.copy(username = username + "a") + } + + internal fun onLogin() =action { + if (state.username.isNotEmpty()) { + setOutput(LoggedIn(username = state.username)) + } + } +} diff --git a/samples/tutorial/tutorial-base/src/test/java/workflow/tutorial/RootWorkflowTest.kt b/samples/tutorial/tutorial-base/src/test/java/workflow/tutorial/RootWorkflowTest.kt new file mode 100644 index 0000000000..ae32e95b4f --- /dev/null +++ b/samples/tutorial/tutorial-base/src/test/java/workflow/tutorial/RootWorkflowTest.kt @@ -0,0 +1,63 @@ +package workflow.tutorial + +import com.squareup.workflow1.WorkflowOutput +import com.squareup.workflow1.testing.expectWorkflow +import com.squareup.workflow1.testing.testRender +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import org.junit.Test +import workflow.tutorial.RootWorkflow.State.Todo +import workflow.tutorial.RootWorkflow.State.Welcome +import workflow.tutorial.WelcomeWorkflow.LoggedIn +import kotlin.test.assertEquals +import kotlin.test.assertNull + +@OptIn(WorkflowUiExperimentalApi::class) +class RootWorkflowTest { + @Test + fun `welcome rendering`() { + RootWorkflow + .testRender(initialState = Welcome, props = Unit) + .expectWorkflow( + workflowType = WelcomeWorkflow::class, + rendering = WelcomeScreen( + username = "Ada", + onUsernameChanged = {}, + onLoginTapped = {} + ) + ) + .render {rendering -> + val backstack = rendering.frames + assertEquals(1, backstack.size) + + val welcomeScreen = backstack[0] as WelcomeScreen + assertEquals("Ada", welcomeScreen.username) + } + .verifyActionResult { _, output -> + assertNull(output) + } + } + + @Test + fun `login event`() { + RootWorkflow + .testRender(initialState = Welcome, props = Unit) + .expectWorkflow( + workflowType = WelcomeWorkflow::class, + rendering = WelcomeScreen( + username = "Ada", + onUsernameChanged = {}, + onLoginTapped = {} + ), + output = WorkflowOutput(LoggedIn("Ada")) + ) + .render { rendering -> + val backStack = rendering.frames + val welcomeScreen = backStack[0] as WelcomeScreen + assertEquals(1, backStack.size) + assertEquals("Ada", welcomeScreen.username) + } + .verifyActionResult { newState, _ -> + assertEquals(Todo(username = "Ada"), newState) + } + } +} diff --git a/samples/tutorial/tutorial-base/src/test/java/workflow/tutorial/TodoEditWorkflowTest.kt b/samples/tutorial/tutorial-base/src/test/java/workflow/tutorial/TodoEditWorkflowTest.kt new file mode 100644 index 0000000000..21ac6301a3 --- /dev/null +++ b/samples/tutorial/tutorial-base/src/test/java/workflow/tutorial/TodoEditWorkflowTest.kt @@ -0,0 +1,66 @@ +package workflow.tutorial + +import com.squareup.workflow1.applyTo +import org.junit.Test +import workflow.tutorial.TodoEditWorkflow.EditProps +import workflow.tutorial.TodoEditWorkflow.Output.Save +import workflow.tutorial.TodoEditWorkflow.State +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class TodoEditWorkflowTest { + + private val startState = State(todo = TodoModel(title = "Title", note = "Note")) + + @Test + fun `title is updated`() { + val props = EditProps(initialTodo = TodoModel(title = "", note = "")) + + val (newState, actionApplied) = TodoEditWorkflow.onTitleChanged("Updated Title") + .applyTo(props, startState) + + assertNull(actionApplied.output) + assertEquals(TodoModel(title = "Updated Title", note = "Note"), newState.todo) + } + + @Test + fun `note is updated`() { + val props = EditProps(initialTodo = TodoModel(title = "", note = "")) + + val (newState, actionApplied) = TodoEditWorkflow.onNoteChanged("Updated Note") + .applyTo(props, startState) + + assertNull(actionApplied.output) + assertEquals(TodoModel(title = "Title", note = "Updated Note"), newState.todo) + } + + @Test + fun `save emits model`() { + val props = EditProps(initialTodo = TodoModel(title = "", note = "")) + + val (_, actionApplied) = TodoEditWorkflow.onSave().applyTo(props, startState) + + assertEquals(Save(TodoModel(title = "Title", note = "Note")), actionApplied.output?.value) + } + + @Test + fun `changed props updated local state`() { + val initialProps = EditProps(TodoModel(title = "Title", note = "Note")) + var state = TodoEditWorkflow.initialState(initialProps, null) + + assertEquals("Title", state.todo.title) + assertEquals("Note", state.todo.note) + + state = State(TodoModel(title = "Updated Title", note = "Note")) + + state = TodoEditWorkflow.onPropsChanged(initialProps, initialProps, state) + assertEquals("Updated Title", state.todo.title) + assertEquals("Note", state.todo.note) + + val updatedProps = EditProps(initialTodo = TodoModel(title = "New Title", note = "New Note")) + state = TodoEditWorkflow.onPropsChanged(initialProps, updatedProps, state) + assertEquals("New Title", state.todo.title) + assertEquals("New Note", state.todo.note) + + } +} diff --git a/samples/tutorial/tutorial-base/src/test/java/workflow/tutorial/TodoWorkflowTest.kt b/samples/tutorial/tutorial-base/src/test/java/workflow/tutorial/TodoWorkflowTest.kt new file mode 100644 index 0000000000..7021356b3c --- /dev/null +++ b/samples/tutorial/tutorial-base/src/test/java/workflow/tutorial/TodoWorkflowTest.kt @@ -0,0 +1,155 @@ +package workflow.tutorial + +import com.squareup.workflow1.WorkflowOutput +import com.squareup.workflow1.testing.expectWorkflow +import com.squareup.workflow1.testing.launchForTestingFromStartWith +import com.squareup.workflow1.testing.testRender +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import org.junit.Test +import workflow.tutorial.RootWorkflow.State.Todo +import workflow.tutorial.TodoEditWorkflow.Output.Save +import workflow.tutorial.TodoListWorkflow.Output.SelectTodo +import workflow.tutorial.TodoWorkflow.State +import workflow.tutorial.TodoWorkflow.State.Step +import workflow.tutorial.TodoWorkflow.TodoProps +import workflow.tutorial.TodoWorkflow.initialState +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@OptIn(WorkflowUiExperimentalApi::class) +class TodoWorkflowTest { + + @Test + fun `selecting todo`() { + val todos = listOf(TodoModel("Title", note = "Note")) + + TodoWorkflow + .testRender( + props = TodoProps(username = "Ada"), + initialState = State(todos = todos, step = Step.List) + ) + .expectWorkflow( + workflowType = TodoListWorkflow::class, + rendering = TodoListScreen( + username = "", + todoTitles = listOf("Title"), + onTodoSelected = {}, + onBack = {} + ), + output = WorkflowOutput(SelectTodo(index = 0)) + ) + .render { rendering -> + assertEquals(1, rendering.size) + } + .verifyActionResult { newState, _ -> + assertEquals(State(todos = listOf(TodoModel(title = "Title", note = "Note")), step = Step.Edit(0)), newState) + } + } + + @Test + fun `saving todo`() { + val todos = listOf(TodoModel("Title", note = "Note")) + + TodoWorkflow + .testRender( + props = TodoProps(username = "Ada"), + initialState = State(todos = todos, step = Step.Edit(0)) + ) + .expectWorkflow( + workflowType = TodoListWorkflow::class, + rendering = TodoListScreen( + username = "", + todoTitles = listOf("Title"), + onTodoSelected = {}, + onBack = {} + ), + ) + .expectWorkflow( + workflowType = TodoEditWorkflow::class, + rendering = TodoEditScreen( + title = "Title", + note = "Note", + onTitleChanged = {}, + onNoteChanged = {}, + discardChanges = {}, + saveChanges = {} + ), + output = WorkflowOutput( + Save(TodoModel(title = "Updated Title", note = "Updated Note")) + ) + ) + .render { rendering -> + assertEquals(2, rendering.size) + } + .verifyActionResult { newState, _ -> + assertEquals( + State( + todos = listOf(TodoModel(title = "Updated Title", note = "Updated Note")), + step = Step.List + ), + newState + ) + } + } + + + /////// Integration testing /////// + + @Test + fun `app flow`() { + RootWorkflow.launchForTestingFromStartWith { + awaitNextRendering().let { rendering -> + assertEquals(1, rendering.frames.size) + val welcomeScreen = rendering.frames[0] as WelcomeScreen + + welcomeScreen.onUsernameChanged("Ada") + } + + awaitNextRendering().let { rendering -> + assertEquals(1, rendering.frames.size) + + val welcomeScreen = rendering.frames[0] as WelcomeScreen + + welcomeScreen.onLoginTapped() + } + + awaitNextRendering().let {rendering -> + assertEquals(2, rendering.frames.size) + assertTrue(rendering.frames[0] is WelcomeScreen) + val todoScreen = rendering.frames[1] as TodoListScreen + + assertEquals(1, todoScreen.todoTitles.size) + + todoScreen.onTodoSelected(0) + } + + awaitNextRendering().let { rendering -> + assertEquals(3, rendering.frames.size) + assertTrue(rendering.frames[0] is WelcomeScreen) + assertTrue(rendering.frames[1] is TodoListScreen) + + val editScreen = rendering.frames[2] as TodoEditScreen + editScreen.onTitleChanged("New title") + } + + awaitNextRendering().let { rendering -> + assertEquals(3, rendering.frames.size) + assertTrue(rendering.frames[0] is WelcomeScreen) + assertTrue(rendering.frames[1] is TodoListScreen) + + val editScreen = rendering.frames[2] as TodoEditScreen + editScreen.saveChanges() + } + + awaitNextRendering().let { rendering -> + assertEquals(2, rendering.frames.size) + assertTrue(rendering.frames[0] is WelcomeScreen) + + val todoScreen = rendering.frames[1] as TodoListScreen + assertEquals(1, todoScreen.todoTitles.size) + assertEquals("New title", todoScreen.todoTitles[0]) + } + } + } + +} diff --git a/samples/tutorial/tutorial-base/src/test/java/workflow/tutorial/WelcomeWorkflowTest.kt b/samples/tutorial/tutorial-base/src/test/java/workflow/tutorial/WelcomeWorkflowTest.kt new file mode 100644 index 0000000000..2fe309768a --- /dev/null +++ b/samples/tutorial/tutorial-base/src/test/java/workflow/tutorial/WelcomeWorkflowTest.kt @@ -0,0 +1,78 @@ +package workflow.tutorial + +import com.squareup.workflow1.applyTo +import com.squareup.workflow1.testing.testRender +import org.junit.Test +import workflow.tutorial.WelcomeWorkflow.LoggedIn +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class WelcomeWorkflowTest { + + @Test + fun `username updates`() { + val startState = WelcomeWorkflow.State("") + val action = WelcomeWorkflow.onUsernameChanged("myName") + + val (state, output) = action.applyTo(state = startState, props = Unit) + assertNull(output.output) + + assertEquals("myNamea", state.username) + } + + @Test + fun `login works`() { + val startState = WelcomeWorkflow.State("myName") + val action = WelcomeWorkflow.onLogin() + + val (_, actionApplied) = action.applyTo(state = startState, props = Unit) + + assertEquals(LoggedIn("myName"), actionApplied.output?.value) + } + + @Test + fun `login does nothing when name is empty`() { + val startState = WelcomeWorkflow.State("") + val action = WelcomeWorkflow.onLogin() + val (state, actionApplied) = action.applyTo(state = startState, props = Unit) + + assertNull(actionApplied.output) + + assertEquals("", state.username) + } + + @Test + fun `rendering initial`() { + WelcomeWorkflow.testRender(props = Unit) + .render { screen -> + assertEquals("", screen.username) + + screen.onLoginTapped() + } + .verifyActionResult { _, output -> + assertNull(output) + } + } + + @Test + fun `rendering name change`() { + WelcomeWorkflow.testRender(props = Unit) + .render { screen -> + screen.onUsernameChanged("myName") + } + .verifyActionResult { state, _ -> + assertEquals("myNamea", (state as WelcomeWorkflow.State).username) + } + } + + @Test + fun `rendering login`() { + WelcomeWorkflow.testRender( initialState = WelcomeWorkflow.State("myName"),props = Unit) + .render { screen -> + screen.onLoginTapped() + } + .verifyActionResult { _, output -> + assertEquals(LoggedIn("myName"), output?.value) + } + } +} diff --git a/trace-encoder/dependencies/runtimeClasspath.txt b/trace-encoder/dependencies/runtimeClasspath.txt index 8e1e76cbc2..aaeae6d225 100644 --- a/trace-encoder/dependencies/runtimeClasspath.txt +++ b/trace-encoder/dependencies/runtimeClasspath.txt @@ -1,12 +1,12 @@ -com.squareup.moshi:moshi-adapters:1.13.0 -com.squareup.moshi:moshi:1.13.0 -com.squareup.okio:okio-jvm:3.0.0 -com.squareup.okio:okio:3.0.0 +com.squareup.moshi:moshi-adapters:1.15.0 +com.squareup.moshi:moshi:1.15.0 +com.squareup.okio:okio-jvm:3.3.0 +com.squareup.okio:okio:3.3.0 org.jetbrains.kotlin:kotlin-bom:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib:1.8.20 +org.jetbrains.kotlin:kotlin-stdlib-common:1.8.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.21 +org.jetbrains.kotlin:kotlin-stdlib:1.8.21 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.1 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.1 org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 diff --git a/workflow-config/config-android/dependencies/releaseRuntimeClasspath.txt b/workflow-config/config-android/dependencies/releaseRuntimeClasspath.txt index f244b70938..b1b31b28eb 100644 --- a/workflow-config/config-android/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-config/config-android/dependencies/releaseRuntimeClasspath.txt @@ -1,5 +1,5 @@ -com.squareup.okio:okio-jvm:3.0.0 -com.squareup.okio:okio:3.0.0 +com.squareup.okio:okio-jvm:3.3.0 +com.squareup.okio:okio:3.3.0 org.jetbrains.kotlin:kotlin-bom:1.8.10 org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20 diff --git a/workflow-config/config-jvm/dependencies/runtimeClasspath.txt b/workflow-config/config-jvm/dependencies/runtimeClasspath.txt index f244b70938..b1b31b28eb 100644 --- a/workflow-config/config-jvm/dependencies/runtimeClasspath.txt +++ b/workflow-config/config-jvm/dependencies/runtimeClasspath.txt @@ -1,5 +1,5 @@ -com.squareup.okio:okio-jvm:3.0.0 -com.squareup.okio:okio:3.0.0 +com.squareup.okio:okio-jvm:3.3.0 +com.squareup.okio:okio:3.3.0 org.jetbrains.kotlin:kotlin-bom:1.8.10 org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20 diff --git a/workflow-core/dependencies/jsRuntimeClasspath.txt b/workflow-core/dependencies/jsRuntimeClasspath.txt index 28f247bb61..f94d8469cf 100644 --- a/workflow-core/dependencies/jsRuntimeClasspath.txt +++ b/workflow-core/dependencies/jsRuntimeClasspath.txt @@ -1,5 +1,5 @@ -com.squareup.okio:okio-js:3.0.0 -com.squareup.okio:okio:3.0.0 +com.squareup.okio:okio-js:3.3.0 +com.squareup.okio:okio:3.3.0 org.jetbrains.kotlin:kotlin-bom:1.8.10 org.jetbrains.kotlin:kotlin-stdlib-js:1.8.20 org.jetbrains.kotlin:kotlin-stdlib:1.8.10 diff --git a/workflow-core/dependencies/jvmRuntimeClasspath.txt b/workflow-core/dependencies/jvmRuntimeClasspath.txt index ae34ead52c..66d9bc7ac4 100644 --- a/workflow-core/dependencies/jvmRuntimeClasspath.txt +++ b/workflow-core/dependencies/jvmRuntimeClasspath.txt @@ -1,5 +1,5 @@ -com.squareup.okio:okio-jvm:3.0.0 -com.squareup.okio:okio:3.0.0 +com.squareup.okio:okio-jvm:3.3.0 +com.squareup.okio:okio:3.3.0 org.jetbrains.kotlin:kotlin-bom:1.8.10 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10 diff --git a/workflow-core/dependencies/runtimeClasspath.txt b/workflow-core/dependencies/runtimeClasspath.txt index 3d4e96caf5..c0a590ef48 100644 --- a/workflow-core/dependencies/runtimeClasspath.txt +++ b/workflow-core/dependencies/runtimeClasspath.txt @@ -1,5 +1,5 @@ -com.squareup.okio:okio-jvm:3.0.0 -com.squareup.okio:okio:3.0.0 +com.squareup.okio:okio-jvm:3.3.0 +com.squareup.okio:okio:3.3.0 org.jetbrains.kotlin:kotlin-bom:1.8.10 org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10 diff --git a/workflow-runtime/dependencies/jsRuntimeClasspath.txt b/workflow-runtime/dependencies/jsRuntimeClasspath.txt index 28f247bb61..f94d8469cf 100644 --- a/workflow-runtime/dependencies/jsRuntimeClasspath.txt +++ b/workflow-runtime/dependencies/jsRuntimeClasspath.txt @@ -1,5 +1,5 @@ -com.squareup.okio:okio-js:3.0.0 -com.squareup.okio:okio:3.0.0 +com.squareup.okio:okio-js:3.3.0 +com.squareup.okio:okio:3.3.0 org.jetbrains.kotlin:kotlin-bom:1.8.10 org.jetbrains.kotlin:kotlin-stdlib-js:1.8.20 org.jetbrains.kotlin:kotlin-stdlib:1.8.10 diff --git a/workflow-runtime/dependencies/jvmRuntimeClasspath.txt b/workflow-runtime/dependencies/jvmRuntimeClasspath.txt index 353cfc3b58..42f31697d0 100644 --- a/workflow-runtime/dependencies/jvmRuntimeClasspath.txt +++ b/workflow-runtime/dependencies/jvmRuntimeClasspath.txt @@ -1,5 +1,5 @@ -com.squareup.okio:okio-jvm:3.0.0 -com.squareup.okio:okio:3.0.0 +com.squareup.okio:okio-jvm:3.3.0 +com.squareup.okio:okio:3.3.0 org.jetbrains.kotlin:kotlin-bom:1.8.10 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20 diff --git a/workflow-rx2/dependencies/runtimeClasspath.txt b/workflow-rx2/dependencies/runtimeClasspath.txt index f2b5510c57..c84b70adf5 100644 --- a/workflow-rx2/dependencies/runtimeClasspath.txt +++ b/workflow-rx2/dependencies/runtimeClasspath.txt @@ -1,5 +1,5 @@ -com.squareup.okio:okio-jvm:3.0.0 -com.squareup.okio:okio:3.0.0 +com.squareup.okio:okio-jvm:3.3.0 +com.squareup.okio:okio:3.3.0 io.reactivex.rxjava2:rxjava:2.2.21 org.jetbrains.kotlin:kotlin-bom:1.8.10 org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 diff --git a/workflow-testing/dependencies/runtimeClasspath.txt b/workflow-testing/dependencies/runtimeClasspath.txt index e28f00ccab..1598c01128 100644 --- a/workflow-testing/dependencies/runtimeClasspath.txt +++ b/workflow-testing/dependencies/runtimeClasspath.txt @@ -1,10 +1,10 @@ -app.cash.turbine:turbine-jvm:0.13.0 -app.cash.turbine:turbine:0.13.0 -com.squareup.okio:okio-jvm:3.0.0 -com.squareup.okio:okio:3.0.0 +app.cash.turbine:turbine-jvm:1.0.0 +app.cash.turbine:turbine:1.0.0 +com.squareup.okio:okio-jvm:3.3.0 +com.squareup.okio:okio:3.3.0 org.jetbrains.kotlin:kotlin-bom:1.8.10 org.jetbrains.kotlin:kotlin-reflect:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.8.21 +org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10 org.jetbrains.kotlin:kotlin-stdlib:1.8.10 diff --git a/workflow-tracing/dependencies/runtimeClasspath.txt b/workflow-tracing/dependencies/runtimeClasspath.txt index 8e1e76cbc2..aaeae6d225 100644 --- a/workflow-tracing/dependencies/runtimeClasspath.txt +++ b/workflow-tracing/dependencies/runtimeClasspath.txt @@ -1,12 +1,12 @@ -com.squareup.moshi:moshi-adapters:1.13.0 -com.squareup.moshi:moshi:1.13.0 -com.squareup.okio:okio-jvm:3.0.0 -com.squareup.okio:okio:3.0.0 +com.squareup.moshi:moshi-adapters:1.15.0 +com.squareup.moshi:moshi:1.15.0 +com.squareup.okio:okio-jvm:3.3.0 +com.squareup.okio:okio:3.3.0 org.jetbrains.kotlin:kotlin-bom:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib:1.8.20 +org.jetbrains.kotlin:kotlin-stdlib-common:1.8.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.21 +org.jetbrains.kotlin:kotlin-stdlib:1.8.21 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.1 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.1 org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 diff --git a/workflow-ui/compose-tooling/dependencies/releaseRuntimeClasspath.txt b/workflow-ui/compose-tooling/dependencies/releaseRuntimeClasspath.txt index b3fbbd67d9..628e5d9746 100644 --- a/workflow-ui/compose-tooling/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-ui/compose-tooling/dependencies/releaseRuntimeClasspath.txt @@ -34,15 +34,15 @@ androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1 androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1 androidx.lifecycle:lifecycle-viewmodel:2.5.1 androidx.profileinstaller:profileinstaller:1.2.0 -androidx.savedstate:savedstate-ktx:1.2.0 -androidx.savedstate:savedstate:1.2.0 +androidx.savedstate:savedstate-ktx:1.2.1 +androidx.savedstate:savedstate:1.2.1 androidx.startup:startup-runtime:1.1.1 androidx.tracing:tracing:1.0.0 androidx.transition:transition:1.4.1 androidx.versionedparcelable:versionedparcelable:1.1.1 com.google.guava:listenablefuture:1.0 -com.squareup.okio:okio-jvm:3.0.0 -com.squareup.okio:okio:3.0.0 +com.squareup.okio:okio-jvm:3.3.0 +com.squareup.okio:okio:3.3.0 org.jetbrains.kotlin:kotlin-bom:1.8.10 org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20 diff --git a/workflow-ui/compose/dependencies/releaseRuntimeClasspath.txt b/workflow-ui/compose/dependencies/releaseRuntimeClasspath.txt index af7f4e383d..0cc2b7342d 100644 --- a/workflow-ui/compose/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-ui/compose/dependencies/releaseRuntimeClasspath.txt @@ -31,15 +31,15 @@ androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1 androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1 androidx.lifecycle:lifecycle-viewmodel:2.5.1 androidx.profileinstaller:profileinstaller:1.2.0 -androidx.savedstate:savedstate-ktx:1.2.0 -androidx.savedstate:savedstate:1.2.0 +androidx.savedstate:savedstate-ktx:1.2.1 +androidx.savedstate:savedstate:1.2.1 androidx.startup:startup-runtime:1.1.1 androidx.tracing:tracing:1.0.0 androidx.transition:transition:1.4.1 androidx.versionedparcelable:versionedparcelable:1.1.1 com.google.guava:listenablefuture:1.0 -com.squareup.okio:okio-jvm:3.0.0 -com.squareup.okio:okio:3.0.0 +com.squareup.okio:okio-jvm:3.3.0 +com.squareup.okio:okio:3.3.0 org.jetbrains.kotlin:kotlin-bom:1.8.10 org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20 diff --git a/workflow-ui/container-android/dependencies/releaseRuntimeClasspath.txt b/workflow-ui/container-android/dependencies/releaseRuntimeClasspath.txt index 7fece34677..53130e672f 100644 --- a/workflow-ui/container-android/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-ui/container-android/dependencies/releaseRuntimeClasspath.txt @@ -26,7 +26,7 @@ androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1 androidx.lifecycle:lifecycle-viewmodel:2.5.1 androidx.loader:loader:1.0.0 androidx.resourceinspection:resourceinspection-annotation:1.0.1 -androidx.savedstate:savedstate:1.2.0 +androidx.savedstate:savedstate:1.2.1 androidx.startup:startup-runtime:1.1.1 androidx.tracing:tracing:1.0.0 androidx.transition:transition:1.4.1 @@ -35,8 +35,8 @@ androidx.vectordrawable:vectordrawable:1.1.0 androidx.versionedparcelable:versionedparcelable:1.1.1 androidx.viewpager:viewpager:1.0.0 com.google.guava:listenablefuture:1.0 -com.squareup.okio:okio-jvm:3.0.0 -com.squareup.okio:okio:3.0.0 +com.squareup.okio:okio-jvm:3.3.0 +com.squareup.okio:okio:3.3.0 org.jetbrains.kotlin:kotlin-bom:1.8.10 org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10 diff --git a/workflow-ui/container-common/dependencies/runtimeClasspath.txt b/workflow-ui/container-common/dependencies/runtimeClasspath.txt index 3d4e96caf5..c0a590ef48 100644 --- a/workflow-ui/container-common/dependencies/runtimeClasspath.txt +++ b/workflow-ui/container-common/dependencies/runtimeClasspath.txt @@ -1,5 +1,5 @@ -com.squareup.okio:okio-jvm:3.0.0 -com.squareup.okio:okio:3.0.0 +com.squareup.okio:okio-jvm:3.3.0 +com.squareup.okio:okio:3.3.0 org.jetbrains.kotlin:kotlin-bom:1.8.10 org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10 diff --git a/workflow-ui/core-android/dependencies/releaseRuntimeClasspath.txt b/workflow-ui/core-android/dependencies/releaseRuntimeClasspath.txt index 7d7d9bc105..4612d1ccac 100644 --- a/workflow-ui/core-android/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-ui/core-android/dependencies/releaseRuntimeClasspath.txt @@ -13,13 +13,13 @@ androidx.lifecycle:lifecycle-runtime-ktx:2.5.1 androidx.lifecycle:lifecycle-runtime:2.5.1 androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1 androidx.lifecycle:lifecycle-viewmodel:2.5.1 -androidx.savedstate:savedstate:1.2.0 +androidx.savedstate:savedstate:1.2.1 androidx.tracing:tracing:1.0.0 androidx.transition:transition:1.4.1 androidx.versionedparcelable:versionedparcelable:1.1.1 com.google.guava:listenablefuture:1.0 -com.squareup.okio:okio-jvm:3.0.0 -com.squareup.okio:okio:3.0.0 +com.squareup.okio:okio-jvm:3.3.0 +com.squareup.okio:okio:3.3.0 org.jetbrains.kotlin:kotlin-bom:1.8.10 org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10 diff --git a/workflow-ui/core-common/dependencies/runtimeClasspath.txt b/workflow-ui/core-common/dependencies/runtimeClasspath.txt index 3d4e96caf5..c0a590ef48 100644 --- a/workflow-ui/core-common/dependencies/runtimeClasspath.txt +++ b/workflow-ui/core-common/dependencies/runtimeClasspath.txt @@ -1,5 +1,5 @@ -com.squareup.okio:okio-jvm:3.0.0 -com.squareup.okio:okio:3.0.0 +com.squareup.okio:okio-jvm:3.3.0 +com.squareup.okio:okio:3.3.0 org.jetbrains.kotlin:kotlin-bom:1.8.10 org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10 diff --git a/workflow-ui/radiography/dependencies/releaseRuntimeClasspath.txt b/workflow-ui/radiography/dependencies/releaseRuntimeClasspath.txt index 1db03758e3..b3bd6821b9 100644 --- a/workflow-ui/radiography/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-ui/radiography/dependencies/releaseRuntimeClasspath.txt @@ -13,14 +13,14 @@ androidx.lifecycle:lifecycle-runtime-ktx:2.5.1 androidx.lifecycle:lifecycle-runtime:2.5.1 androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1 androidx.lifecycle:lifecycle-viewmodel:2.5.1 -androidx.savedstate:savedstate:1.2.0 +androidx.savedstate:savedstate:1.2.1 androidx.tracing:tracing:1.0.0 androidx.transition:transition:1.4.1 androidx.versionedparcelable:versionedparcelable:1.1.1 com.google.guava:listenablefuture:1.0 com.squareup.curtains:curtains:1.2.2 -com.squareup.okio:okio-jvm:3.0.0 -com.squareup.okio:okio:3.0.0 +com.squareup.okio:okio-jvm:3.3.0 +com.squareup.okio:okio:3.3.0 com.squareup.radiography:radiography:2.4.1 org.jetbrains.kotlin:kotlin-bom:1.8.10 org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20