diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..94c348a9a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# https://editorconfig.org +root = true + +[*] +indent_size = 2 +indent_style = space +# Handled by Detekt, which supports @Suppress annotation +max_line_length = off + +[*.{kt,kts}] +# Different rules in Detekt +ktlint_standard_property-naming = disabled +# I don't like it +ktlint_standard_multiline-expression-wrapping = disabled + +# Don't allow any wildcard imports +ij_kotlin_packages_to_use_import_on_demand = unset + +# Prevent wildcard imports +ij_kotlin_name_count_to_use_star_import = 99 +ij_kotlin_name_count_to_use_star_import_for_members = 99 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9ca4c1c8d..1a4186d5a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,10 +2,8 @@ ## :pencil: Checklist - -- [ ] I updated the [changelog](https://github.com/MiSikora/laboratory/blob/trunk/library/docs/changelog.md). -- [ ] I updated the [documentation](https://github.com/MiSikora/laboratory/tree/trunk/library/docs). -- [ ] I updated the [sample](https://github.com/MiSikora/laboratory/tree/trunk/sample). - -## :crystal_ball: Next steps - + +- [ ] I updated the [changelog](https://github.com/MiSikora/laboratory/blob/trunk/docs/changelog.md). +- [ ] I updated the [documentation](https://github.com/MiSikora/laboratory/tree/trunk/docs). +- [ ] I updated the [samples](https://github.com/MiSikora/laboratory/tree/trunk/samples). +- \ No newline at end of file diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 49ef7bc81..40613384c 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -12,13 +12,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout latest code - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.4 - name: Configure JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4.2.1 with: distribution: zulu - java-version: 19 + java-version: 21 - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@v1.0.5 + uses: gradle/actions/wrapper-validation@v3.3.2 diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 05a718dd3..916a208a7 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -12,21 +12,21 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.4 - name: Configure JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4.2.1 with: distribution: zulu - java-version: 19 + java-version: 21 - name: Cache Gradle Dirs - uses: actions/cache@v3.0.11 + uses: actions/cache@v4.0.2 with: path: | ~/.gradle/caches/ ~/.gradle/wrapper/ - key: cache-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + key: cache-gradle-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }} restore-keys: cache-gradle- - name: Deploy Release @@ -34,19 +34,19 @@ jobs: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SONATYPE_NEXUS_SIGNING_KEY }} - run: ./gradlew -p library publishAllPublicationsToMavenCentral --no-configuration-cache --stacktrace + run: ./gradlew publish --no-configuration-cache --stacktrace - name: Stop Gradle run: ./gradlew --stop - name: Extract Release Notes id: release-notes - uses: ffurrer2/extract-release-notes@v1.16.0 + uses: ffurrer2/extract-release-notes@v2.2.0 with: - changelog_file: ./library/docs/changelog.md + changelog_file: ./docs/changelog.md - name: Create GitHub Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2.0.5 with: body: ${{ steps.release-notes.outputs.release_notes }} files: | diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml index 953fd775f..0a1e8bb45 100644 --- a/.github/workflows/publish-snapshot.yml +++ b/.github/workflows/publish-snapshot.yml @@ -12,41 +12,41 @@ jobs: runs-on: ubuntu-latest steps: - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.11.0 + uses: styfle/cancel-workflow-action@0.12.1 - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.4 - name: Configure JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4.2.1 with: distribution: zulu - java-version: 19 + java-version: 21 - name: Cache Gradle Dirs - uses: actions/cache@v3.0.11 + uses: actions/cache@v4.0.2 with: path: | ~/.gradle/caches/ ~/.gradle/wrapper/ - key: cache-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + key: cache-gradle-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }} restore-keys: cache-gradle- - name: Deploy Snapshot env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} - run: ./gradlew -p library publish --no-daemon --no-parallel --stacktrace + run: ./gradlew publish --stacktrace - name: Build HTML Docs - run: ./gradlew -p library dokkaHtml --stacktrace + run: ./gradlew dokkaHtmlMultiModule --stacktrace - name: Stop Gradle run: ./gradlew --stop - name: Publish Website - uses: mhausenblas/mkdocs-deploy-gh-pages@1.25 + uses: mhausenblas/mkdocs-deploy-gh-pages@1.26 env: CONFIG_FILE: ./library/mkdocs.yml GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REQUIREMENTS: ./library/docs/requirements.txt + REQUIREMENTS: ./docs/requirements.txt diff --git a/.github/workflows/quality-check.yml b/.github/workflows/quality-check.yml index 0b2f1d66c..177c62c8f 100644 --- a/.github/workflows/quality-check.yml +++ b/.github/workflows/quality-check.yml @@ -17,34 +17,37 @@ jobs: android-tests: if: ${{ github.repository == 'MiSikora/laboratory' }} name: Android Tests - runs-on: macos-latest + runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.4 - name: Configure JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4.2.1 with: distribution: zulu - java-version: 19 + java-version: 21 - name: Cache Gradle Dirs - uses: actions/cache@v3.0.11 + uses: actions/cache@v4.0.2 with: path: | ~/.gradle/caches/ ~/.gradle/wrapper/ - key: cache-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + key: cache-gradle-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }} restore-keys: cache-gradle- + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - name: Run Tests - uses: reactivecircus/android-emulator-runner@v2.27.0 + uses: reactivecircus/android-emulator-runner@v2.30.1 with: api-level: 29 - emulator-build: 6110076 - script: ./gradlew -p library connectedCheck --stacktrace - env: - API_LEVEL: 29 + script: ./gradlew connectedCheck --stacktrace - name: Stop Gradle run: ./gradlew --stop @@ -55,83 +58,91 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.4 - name: Configure JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4.2.1 with: distribution: zulu - java-version: 19 + java-version: 21 - name: Cache Gradle Dirs - uses: actions/cache@v3.0.11 + uses: actions/cache@v4.0.2 with: path: | ~/.gradle/caches/ ~/.gradle/wrapper/ - key: cache-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + key: cache-gradle-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }} restore-keys: cache-gradle- - name: Run Tests - run: ./gradlew -p library test --stacktrace + run: ./gradlew test --stacktrace - name: Stop Gradle run: ./gradlew --stop - build-sample: + detekt: if: ${{ github.repository == 'MiSikora/laboratory' }} - name: Build sample + name: Detekt runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.4 - name: Configure JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4.2.1 with: distribution: zulu - java-version: 19 + java-version: 21 - name: Cache Gradle Dirs - uses: actions/cache@v3.0.11 + uses: actions/cache@v4.0.2 with: path: | ~/.gradle/caches/ ~/.gradle/wrapper/ - key: cache-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + key: cache-gradle-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }} restore-keys: cache-gradle- - - name: Assemble project - run: ./gradlew :samples:ci-check:assemble --stacktrace + - name: Run Detekt + run: ./gradlew detekt --stacktrace + + - name: Run Samples Detekt + working-directory: ./samples + run: ./gradlew detekt - name: Stop Gradle run: ./gradlew --stop - detekt: + spotless: if: ${{ github.repository == 'MiSikora/laboratory' }} - name: Detekt + name: Spotless runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.4 - name: Configure JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4.2.1 with: distribution: zulu - java-version: 19 + java-version: 21 - name: Cache Gradle Dirs - uses: actions/cache@v3.0.11 + uses: actions/cache@v4.0.2 with: path: | ~/.gradle/caches/ ~/.gradle/wrapper/ - key: cache-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + key: cache-gradle-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }} restore-keys: cache-gradle- - - name: Run Detekt - run: ./gradlew -p library detekt --stacktrace + - name: Run Spotless + run: ./gradlew spotlessCheck --stacktrace + + - name: Run Samples Spotless + working-directory: ./samples + run: ./gradlew spotlessCheck - name: Stop Gradle run: ./gradlew --stop @@ -142,25 +153,25 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.4 - name: Configure JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4.2.1 with: distribution: zulu - java-version: 19 + java-version: 21 - name: Cache Gradle Dirs - uses: actions/cache@v3.0.11 + uses: actions/cache@v4.0.2 with: path: | ~/.gradle/caches/ ~/.gradle/wrapper/ - key: cache-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + key: cache-gradle-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }} restore-keys: cache-gradle- - name: Run Lint - run: ./gradlew -p library lint --stacktrace + run: ./gradlew lint --stacktrace - name: Stop Gradle run: ./gradlew --stop @@ -171,25 +182,25 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.4 - name: Configure JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4.2.1 with: distribution: zulu - java-version: 19 + java-version: 21 - name: Cache Gradle Dirs - uses: actions/cache@v3.0.11 + uses: actions/cache@v4.0.2 with: path: | ~/.gradle/caches/ ~/.gradle/wrapper/ - key: cache-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + key: cache-gradle-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }} restore-keys: cache-gradle- - name: Check ABI - run: ./gradlew -p library apiCheck --stacktrace + run: ./gradlew apiCheck --stacktrace - name: Stop Gradle run: ./gradlew --stop diff --git a/.gitignore b/.gitignore index 170a5c0de..39cc19f14 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,6 @@ # IDEA *.iml -/.idea/ -!/.idea/codeStyles/ -!/.idea/codeStyles/* +.idea/ # Gradle .gradle/ @@ -17,4 +15,4 @@ build/ .DS_Store # MkDocs -/library/docs/api +/docs/api diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index abbae65c7..000000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,429 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 79ee123c2..000000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/library/.mkdocs-theme/404.html b/.mkdocs-theme/404.html similarity index 94% rename from library/.mkdocs-theme/404.html rename to .mkdocs-theme/404.html index 24b708de5..376998554 100644 --- a/library/.mkdocs-theme/404.html +++ b/.mkdocs-theme/404.html @@ -4,4 +4,4 @@ {% block content %}

404 - Not found

Whoops! Looks like the documentation is missing. Please report a bug on GitHub.

-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/README.md b/README.md index 8598de039..3e457a922 100644 --- a/README.md +++ b/README.md @@ -21,17 +21,11 @@ dependencies { } ``` -Enable Java 8 support. +Enable [default methods generation](https://blog.jetbrains.com/kotlin/2020/07/kotlin-1-4-m3-generating-default-methods-in-interfaces/). ```groovy android { - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() freeCompilerArgs += [ "-Xjvm-default=all", ] diff --git a/library/gradle-plugin/src/test/projects/factory-android-smoke/settings.gradle b/build-support/build.gradle.kts similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-android-smoke/settings.gradle rename to build-support/build.gradle.kts diff --git a/build-support/settings.gradle.kts b/build-support/settings.gradle.kts new file mode 100644 index 000000000..35c2c5019 --- /dev/null +++ b/build-support/settings.gradle.kts @@ -0,0 +1,10 @@ +rootProject.name = "build-support" + +include(":laboratory:runtime") +project(":laboratory:runtime").projectDir = File("../laboratory/runtime") + +dependencyResolutionManagement { + versionCatalogs { + create("libs").from(files("../gradle/libs.versions.toml")) + } +} diff --git a/build.gradle.kts b/build.gradle.kts index c768a47ae..0b81db271 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,13 +1,159 @@ -buildscript { - repositories { - mavenCentral() - gradlePluginPortal() - google() +import com.android.build.api.variant.LibraryAndroidComponentsExtension +import com.android.build.gradle.LibraryExtension +import com.android.build.gradle.LibraryPlugin +import com.diffplug.gradle.spotless.SpotlessExtension +import com.diffplug.gradle.spotless.SpotlessExtensionPredeclare +import com.diffplug.gradle.spotless.SpotlessPlugin +import com.diffplug.spotless.LineEnding +import com.vanniktech.maven.publish.MavenPublishBaseExtension +import io.gitlab.arturbosch.detekt.Detekt +import io.gitlab.arturbosch.detekt.DetektPlugin +import io.gitlab.arturbosch.detekt.extensions.DetektExtension +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompilerOptions +import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask + +plugins { + alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.mavenPublish) apply false + alias(libs.plugins.kotlinx.binaryCompatibilityValidator) + alias(libs.plugins.dokka) + alias(libs.plugins.spotless) + alias(libs.plugins.detekt) + alias(libs.plugins.buildconfig) apply false + alias(libs.plugins.wire) apply false + alias(libs.plugins.ksp) apply false +} + +tasks.dokkaHtmlMultiModule { + moduleName.set("Laboratory") + moduleVersion.set(project.property("VERSION_NAME") as String) + outputDirectory.set(rootDir.resolve("docs/api")) +} + +val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get()) +val ktlintVersion = libs.versions.ktlint.get() +val mavenPublishId = libs.plugins.mavenPublish.get().pluginId + +allprojects { + val configureSpotless: SpotlessExtension.() -> Unit = { + lineEndings = LineEnding.UNIX + + kotlin { + target("src/**/*.kt") + trimTrailingWhitespace() + endWithNewline() + ktlint(ktlintVersion).setEditorConfigPath(rootProject.file(".editorconfig")) + } + + kotlinGradle { + target("*.kts") + trimTrailingWhitespace() + endWithNewline() + ktlint(ktlintVersion).setEditorConfigPath(rootProject.file(".editorconfig")) + } + + format("misc") { + target("*.md", "*.yml", "*.proto", "*.properties", "*.toml", "*.xml", "*.txt", "*.html", "*.css", ".gitignore", ".editorconfig") + trimTrailingWhitespace() + endWithNewline() + } + } + plugins.withType().configureEach { + configure { + configureSpotless() + + if (project.rootProject == project) { + predeclareDeps() + } + } + if (project.rootProject == project) { + configure { configureSpotless() } + } + } + + plugins.withType().configureEach { + configure { + toolVersion = libs.versions.detekt.get() + allRules = true + parallel = true + buildUponDefaultConfig = true + config.from(rootProject.file("detekt.yml")) + } + tasks.withType().configureEach { + jvmTarget = javaTarget.target + reports { + html.required.set(true) + xml.required.set(true) + txt.required.set(true) + } + } + } +} + +subprojects { + group = project.property("GROUP") as String + version = project.property("VERSION_NAME") as String + + plugins.withType().configureEach { + tasks.withType>().configureEach { + compilerOptions { + jvmTarget.set(javaTarget) + progressiveMode.set(true) + allWarningsAsErrors.set(true) + optIn.addAll("kotlin.RequiresOptIn") + freeCompilerArgs.addAll("-Xjvm-default=all") + } + } + + configure { explicitApi() } + } + + tasks.withType().configureEach { + sourceCompatibility = javaTarget.target + targetCompatibility = javaTarget.target + } + + tasks.withType().configureEach { testLogging.events("skipped", "failed", "passed") } + + plugins.withType().configureEach { + configure { + compileOptions { + sourceCompatibility = JavaVersion.toVersion(javaTarget.target) + targetCompatibility = JavaVersion.toVersion(javaTarget.target) + } + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig.minSdk = libs.versions.minSdk.get().toInt() + testOptions.targetSdk = libs.versions.targetSdk.get().toInt() + + lint { + lintConfig = rootProject.file("lint.xml") + warningsAsErrors = true + + htmlReport = true + xmlReport = true + textReport = true + + checkGeneratedSources = true + checkTestSources = false + checkReleaseBuilds = false // Execute explicitly on CI instead + } + } + + configure { + beforeVariants { builder -> builder.enable = builder.buildType == "release" } + } } - dependencies { - classpath(libs.android.gradlePlugin) - classpath(libs.kotlin.gradlePlugin) - classpath(libs.googleServices.gradlePlugin) + pluginManager.withPlugin(mavenPublishId) { + configure { + publishToMavenCentral() + signAllPublications() + } } } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts deleted file mode 100644 index bc0172f0f..000000000 --- a/buildSrc/build.gradle.kts +++ /dev/null @@ -1,3 +0,0 @@ -plugins { - `kotlin-dsl` -} diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts deleted file mode 100644 index 0f47ea913..000000000 --- a/buildSrc/settings.gradle.kts +++ /dev/null @@ -1,6 +0,0 @@ -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - mavenCentral() - } -} diff --git a/buildSrc/src/main/kotlin/JavaConfig.kt b/buildSrc/src/main/kotlin/JavaConfig.kt deleted file mode 100644 index 2d5fa00cd..000000000 --- a/buildSrc/src/main/kotlin/JavaConfig.kt +++ /dev/null @@ -1,6 +0,0 @@ -import org.gradle.api.JavaVersion - -object JavaConfig { - val code = JavaVersion.VERSION_17 - val name = code.toString() -} diff --git a/detekt.yml b/detekt.yml new file mode 100644 index 000000000..95304d070 --- /dev/null +++ b/detekt.yml @@ -0,0 +1,22 @@ +build: + maxIssues: 0 + +complexity: + TooManyFunctions: + active: false + +naming: + ObjectPropertyNaming: + active: true + constantPattern: '^[a-zA-Z]+(?:[A-Z][a-z]+)*$' + propertyPattern: '^[a-zA-Z]+(?:[A-Z][a-z]+)*$' + privatePropertyPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' + TopLevelPropertyNaming: + active: true + constantPattern: '^[a-zA-Z]+(?:[A-Z][a-z]+)*$' + propertyPattern: '^[a-zA-Z]+(?:[A-Z][a-z]+)*$' + privatePropertyPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' + +style: + ForbiddenComment: + active: false diff --git a/library/docs/changelog.md b/docs/changelog.md similarity index 95% rename from library/docs/changelog.md rename to docs/changelog.md index 007b217a5..ff45e7153 100644 --- a/library/docs/changelog.md +++ b/docs/changelog.md @@ -6,20 +6,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- Upgrade to Kotlin `1.9.24`. +- Upgrade to Coroutines `1.8.1`. +- Upgrade to Gradle `8.7`. +- Upgrade to AGP to `8.4.0`. +- Upgrade to Wire `4.9.9`. +- Upgrade to KotlinPoet `1.16.0`. +- Upgrade to DataStore `1.1.1`. +- Upgrade to ViewPager2 `1.3.2`. +- Upgrade to ViewModel-ktx `2.8.0`. +- Upgrade to Fragment-ktx `1.7.1`. +- Upgrade to RecyclerView `1.3.2`. +- Upgrade to Material `1.12.0`. +- Upgrade to Hyperion `0.9.38`. +- Downgrade target and source compatibilities versions to Java 11. +- Change compile and target SDK to 34. + ## [1.1.0] - 2023-06-13 ### Changed -- Upgrade Kotlin to `1.8.21`. -- Upgrade Wire to `4.7.0`. -- Upgrade KotlinPoet to `1.14.2`. -- Upgrade Gradle to `8.1.1`. -- Upgrade Hyperion to `0.9.37`. -- Upgrade Coroutines to `1.6.4`. -- Upgrade AppCompat to `1.6.1`. -- Upgrade ViewModel-ktx to `2.6.1`. -- Upgrade Fragment-ktx to `1.5.7`. -- Upgrade RecyclerView to `1.3.0`. -- Upgrade Material to `1.9.0`. +- Upgrade to Kotlin `1.8.21`. +- Upgrade to Coroutines `1.6.4`. +- Upgrade to Gradle `8.1.1`. +- Upgrade to Wire `4.7.0`. +- Upgrade to KotlinPoet `1.14.2`. +- Upgrade to Hyperion `0.9.37`. +- Upgrade to AppCompat `1.6.1`. +- Upgrade to ViewModel-ktx `2.6.1`. +- Upgrade to Fragment-ktx `1.5.7`. +- Upgrade to RecyclerView `1.3.0`. +- Upgrade to Material `1.9.0`. - Upgrade target and source compatibilities versions to Java 17. - Change compile and target SDK to 33. @@ -54,9 +71,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - `SharedPreferencesFeatureStorage` no marked as experimental due to coroutines. -- Upgrade Kotlin to `1.6.10`. +- Upgrade to Kotlin `1.6.10`. - Upgrade to Coroutines `1.6.0`. -- Upgrade Wire to `4.0.1`. +- Upgrade to Wire `4.0.1`. ### Fixed - Handle usage of deprecated features in a generated `OptionFactory`. diff --git a/library/docs/css/site.css b/docs/css/site.css similarity index 100% rename from library/docs/css/site.css rename to docs/css/site.css diff --git a/library/docs/gradle-plugin.md b/docs/gradle-plugin.md similarity index 98% rename from library/docs/gradle-plugin.md rename to docs/gradle-plugin.md index fe452f0d6..3c3b884d7 100644 --- a/library/docs/gradle-plugin.md +++ b/docs/gradle-plugin.md @@ -165,9 +165,6 @@ DSL for supervised feature flags is recursive allowing to nest them in `withOpti If your feature flags use multiple sources, you can configure the Gradle plugin to generate for you a quality of life extension function that returns a custom `FeatureStorage` builder. -!!! tip - Check [the sample](https://github.com/MiSikora/laboratory/tree/trunk/samples/firebase) with demo Firebase integration. - ```groovy apply plugin: "io.mehow.laboratory" @@ -376,9 +373,6 @@ private object GeneratedFeatureSourceFactory : FeatureFactory { The generation of an option factory is useful when you want to control local feature flag options remotely. Option factory aggregates all feature flags and recognizes them either by a fully qualified class name or an optional `key` property on a feature flag. Keys must be unique and cannot match fully qualified class names of other feature flags. -!!! tip - Check [the sample](https://github.com/MiSikora/laboratory/tree/trunk/samples/firebase) with demo Firebase integration. - ![type:video](https://www.youtube.com/embed/yNm2DUcZ0L4) ```groovy diff --git a/library/docs/images/hyperion_screenshot.jpg b/docs/images/hyperion_screenshot.jpg similarity index 100% rename from library/docs/images/hyperion_screenshot.jpg rename to docs/images/hyperion_screenshot.jpg diff --git a/library/docs/images/inspector_screenshot.jpg b/docs/images/inspector_screenshot.jpg similarity index 100% rename from library/docs/images/inspector_screenshot.jpg rename to docs/images/inspector_screenshot.jpg diff --git a/library/docs/images/laboratory_logo.ico b/docs/images/laboratory_logo.ico similarity index 100% rename from library/docs/images/laboratory_logo.ico rename to docs/images/laboratory_logo.ico diff --git a/library/docs/images/laboratory_logo.svg b/docs/images/laboratory_logo.svg similarity index 100% rename from library/docs/images/laboratory_logo.svg rename to docs/images/laboratory_logo.svg diff --git a/library/docs/images/laboratory_logo_menu.svg b/docs/images/laboratory_logo_menu.svg similarity index 100% rename from library/docs/images/laboratory_logo_menu.svg rename to docs/images/laboratory_logo_menu.svg diff --git a/library/docs/index.md b/docs/index.md similarity index 88% rename from library/docs/index.md rename to docs/index.md index f3971c99a..58ffe1e85 100644 --- a/library/docs/index.md +++ b/docs/index.md @@ -53,22 +53,8 @@ suspend fun main() { ## Requirements -Laboratory requires [Java 8 bytecode](https://developer.android.com/studio/write/java8-support) support. You can enable it with the following configuration in a `build.gradle` file. -```groovy -android { - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } -} -``` - -Also, you have to enable [default methods generation](https://blog.jetbrains.com/kotlin/2020/07/kotlin-1-4-m3-generating-default-methods-in-interfaces/) by Kotlin compiler. You can do this by adding a compiler flag in a `build.gradle` file. +Laboratory requires [default methods generation](https://blog.jetbrains.com/kotlin/2020/07/kotlin-1-4-m3-generating-default-methods-in-interfaces/). You can do this by adding a compiler flag in a `build.gradle` file. ```groovy android { diff --git a/library/docs/qa-module.md b/docs/qa-module.md similarity index 100% rename from library/docs/qa-module.md rename to docs/qa-module.md diff --git a/library/docs/releasing.md b/docs/releasing.md similarity index 71% rename from library/docs/releasing.md rename to docs/releasing.md index 6a8d9c6df..bf280f053 100644 --- a/library/docs/releasing.md +++ b/docs/releasing.md @@ -2,14 +2,14 @@ ## Versioning -1. Run the [prepare release script](https://github.com/MiSikora/laboratory/blob/trunk/library/prepare-release.sh) and bump the desirable version part. +1. Run the [prepare release script](https://github.com/MiSikora/laboratory/blob/trunk/prepare-release.sh) and bump the desirable version part. 2. If there are no errors `git push && git push --tags`. 3. Wait for [the CI server](https://github.com/MiSikora/laboratory/actions) to upload the artifacts. 4. Visit [Sonatype Nexus](https://oss.sonatype.org) and promote the artifacts. ## Documentation updates -Website documentation lives under [`/library/docs`](https://github.com/MiSikora/laboratory/tree/trunk/library/docs) directory and is deployed with [MkDocs](https://www.mkdocs.org/) using [Material Theme](https://squidfunk.github.io/mkdocs-material/). A new site is built and published for the latest commits on the `trunk` branch. +Website documentation lives under [`/docs`](https://github.com/MiSikora/laboratory/tree/trunk/docs) directory and is deployed with [MkDocs](https://www.mkdocs.org/) using [Material Theme](https://squidfunk.github.io/mkdocs-material/). A new site is built and published for the latest commits on the `trunk` branch. If you want to test the website locally before pushing changes, you need to follow these steps. diff --git a/library/docs/requirements.txt b/docs/requirements.txt similarity index 100% rename from library/docs/requirements.txt rename to docs/requirements.txt diff --git a/library/docs/user-guide.md b/docs/user-guide.md similarity index 100% rename from library/docs/user-guide.md rename to docs/user-guide.md diff --git a/gradle.properties b/gradle.properties index 88c77bdae..881622e30 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,24 @@ +GROUP=io.mehow.laboratory +VERSION_NAME=1.1.1-SNAPSHOT + +POM_DESCRIPTION=Library for feature flags management. + +POM_URL=https://github.com/MiSikora/laboratory +POM_SCM_URL=https://github.com/MiSikora/laboratory +POM_SCM_CONNECTION=scm:git:git://github.com/MiSikora/laboratory.git +POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/MiSikora/laboratory.git + +POM_LICENCE_NAME=The Apache Software License, Version 2.0 +POM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt +POM_LICENCE_DIST=repo + +POM_DEVELOPER_ID=michalsikora90 +POM_DEVELOPER_NAME=Michal Sikora + # Increase the build VMs heap size. Default is 512m. -org.gradle.jvmargs=-Xmx2g +org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 org.gradle.parallel=true android.useAndroidX=true + +detekt.use.worker.api = true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..80e5562db --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,55 @@ +[versions] +android = "8.4.0" +coroutines = "1.8.1" +kotest = "5.8.1" +kotlin = "1.9.24" +hyperion = "0.9.38" +detekt = "1.23.6" +ktlint = "1.2.1" + +jvmTarget = "11" +minSdk = "21" +compileSdk = "34" +targetSdk = "34" + +[libraries] +kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } +kotlinx-coroutinesAndroid = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } +kotlinPoet = { group = "com.squareup", name = "kotlinpoet", version = "1.16.0" } +androidx-dataStore = { group = "androidx.datastore", name = "datastore-core", version = "1.1.1" } +androidx-appCompat = { group = "androidx.appcompat", name = "appcompat", version = "1.6.1" } +androidx-viewModelKtx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version = "2.8.0" } +androidx-fragmentKtx = { group = "androidx.fragment", name = "fragment-ktx", version = "1.7.1" } +androidx-viewPager2 = { group = "androidx.viewpager2", name = "viewpager2", version = "1.1.0" } +androidx-recyclerView = { group = "androidx.recyclerview", name = "recyclerview", version = "1.3.2" } +android-material = { group = "com.google.android.material", name = "material", version = "1.12.0" } +hyperion-core = { group = "com.willowtreeapps.hyperion", name = "hyperion-core", version.ref = "hyperion" } +hyperion-plugin = { group = "com.willowtreeapps.hyperion", name = "hyperion-plugin", version.ref = "hyperion" } +autoServiceKsp = { group = "dev.zacsweers.autoservice", name = "auto-service-ksp", version = "1.1.0" } + +kotlinx-coroutinesTest = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } +kotest-runner-junit5 = { group = "io.kotest", name = "kotest-runner-junit5", version.ref = "kotest" } +kotest-assertions = { group = "io.kotest", name = "kotest-assertions-core", version.ref = "kotest" } +kotest-property = { group = "io.kotest", name = "kotest-property-jvm", version.ref = "kotest" } +turbine = { group = "app.cash.turbine", name = "turbine", version = "1.1.0" } +androidx-test-coreKtx = { group = "androidx.test", name = "core-ktx", version = "1.5.0" } +androidx-test-orchestrator = { group = "androidx.test", name = "orchestrator", version = "1.4.2" } +androidx-test-runner = { group = "androidx.test", name = "runner", version = "1.5.2" } +androidx-testExt-junitKtx = { group = "androidx.test.ext", name = "junit-ktx", version = "1.1.5" } + +android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "android" } # +kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } + +[plugins] +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +android-library = { id = "com.android.library", version.ref = "android" } +android-application = { id = "com.android.application", version.ref = "android" } +ksp = { id = "com.google.devtools.ksp", version = "1.9.24-1.0.20" } +wire = { id = "com.squareup.wire", version = "4.9.9" } +buildconfig = { id = "com.github.gmazzo.buildconfig", version = "5.3.5" } +mavenPublish = { id = "com.vanniktech.maven.publish", version = "0.28.0" } +spotless = { id = "com.diffplug.spotless", version = "6.25.0" } +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } +kotlinx-binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.14.0" } +dokka = { id = "org.jetbrains.dokka", version = "1.9.20" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832f..c1962a79e 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1f017e4ee..bb6c19194 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index a69d9cb6c..aeb74cbb4 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +80,10 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -143,12 +140,16 @@ fi if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,6 +194,10 @@ if "$cygwin" || "$msys" ; then done fi + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in diff --git a/gradlew.bat b/gradlew.bat index 53a6b238d..6689b85be 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% diff --git a/library/data-store/api/data-store.api b/laboratory/data-store/api/data-store.api similarity index 100% rename from library/data-store/api/data-store.api rename to laboratory/data-store/api/data-store.api diff --git a/laboratory/data-store/build.gradle.kts b/laboratory/data-store/build.gradle.kts new file mode 100644 index 000000000..6462f97dd --- /dev/null +++ b/laboratory/data-store/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.mavenPublish) + alias(libs.plugins.spotless) + alias(libs.plugins.detekt) + alias(libs.plugins.dokka) + alias(libs.plugins.wire) +} + +wire { + kotlin {} +} + +android { + namespace = "io.mehow.laboratory.datastore" + + sourceSets { + getByName("main").java.srcDirs("${layout.buildDirectory}/generated/source/wire/") + } +} + +tasks.withType().configureEach { + useJUnitPlatform() +} + +dependencies { + api(projects.laboratory.runtime) + api(libs.androidx.dataStore) + + testImplementation(libs.kotest.runner.junit5) + testImplementation(libs.kotest.assertions) + testImplementation(libs.turbine) +} diff --git a/library/data-store/gradle.properties b/laboratory/data-store/gradle.properties similarity index 62% rename from library/data-store/gradle.properties rename to laboratory/data-store/gradle.properties index 7a17d7c84..6b1d0dca5 100644 --- a/library/data-store/gradle.properties +++ b/laboratory/data-store/gradle.properties @@ -1,3 +1,3 @@ POM_ARTIFACT_ID=laboratory-data-store -POM_NAME=Laboratory (DataStore) +POM_NAME=Laboratory (Data Store) POM_PACKAGING=aar diff --git a/library/data-store/src/main/java/io/mehow/laboratory/datastore/DataStoreFeatureStorage.kt b/laboratory/data-store/src/main/kotlin/io/mehow/laboratory/datastore/DataStoreFeatureStorage.kt similarity index 96% rename from library/data-store/src/main/java/io/mehow/laboratory/datastore/DataStoreFeatureStorage.kt rename to laboratory/data-store/src/main/kotlin/io/mehow/laboratory/datastore/DataStoreFeatureStorage.kt index 8b3772a71..58ec3881e 100644 --- a/library/data-store/src/main/java/io/mehow/laboratory/datastore/DataStoreFeatureStorage.kt +++ b/laboratory/data-store/src/main/kotlin/io/mehow/laboratory/datastore/DataStoreFeatureStorage.kt @@ -11,8 +11,8 @@ internal class DataStoreFeatureStorage( private val dataStore: DataStore, ) : FeatureStorage { override fun observeFeatureName(feature: Class>) = dataStore - .data - .map { it.options[feature.name] } + .data + .map { it.options[feature.name] } override suspend fun getFeatureName(feature: Class>) = try { dataStore.data.first().options[feature.name] diff --git a/library/data-store/src/main/java/io/mehow/laboratory/datastore/FeatureFlagsSerializer.kt b/laboratory/data-store/src/main/kotlin/io/mehow/laboratory/datastore/FeatureFlagsSerializer.kt similarity index 87% rename from library/data-store/src/main/java/io/mehow/laboratory/datastore/FeatureFlagsSerializer.kt rename to laboratory/data-store/src/main/kotlin/io/mehow/laboratory/datastore/FeatureFlagsSerializer.kt index 8c0f17531..1bd5ec8be 100644 --- a/library/data-store/src/main/java/io/mehow/laboratory/datastore/FeatureFlagsSerializer.kt +++ b/laboratory/data-store/src/main/kotlin/io/mehow/laboratory/datastore/FeatureFlagsSerializer.kt @@ -15,7 +15,10 @@ public object FeatureFlagsSerializer : Serializer { return FeatureFlags.ADAPTER.decode(input) } - override suspend fun writeTo(t: FeatureFlags, output: OutputStream) { + override suspend fun writeTo( + t: FeatureFlags, + output: OutputStream, + ) { FeatureFlags.ADAPTER.encode(output, t) } } diff --git a/library/data-store/src/main/proto/io/mehow/laboratory/datastore/feature_flags.proto b/laboratory/data-store/src/main/proto/io/mehow/laboratory/datastore/feature_flags.proto similarity index 68% rename from library/data-store/src/main/proto/io/mehow/laboratory/datastore/feature_flags.proto rename to laboratory/data-store/src/main/proto/io/mehow/laboratory/datastore/feature_flags.proto index 74dc934b6..77859e67b 100644 --- a/library/data-store/src/main/proto/io/mehow/laboratory/datastore/feature_flags.proto +++ b/laboratory/data-store/src/main/proto/io/mehow/laboratory/datastore/feature_flags.proto @@ -2,8 +2,6 @@ syntax = "proto3"; package io.mehow.laboratory.datastore; -option java_package = "io.mehow.laboratory.datastore"; - message FeatureFlags { map options = 1; } diff --git a/library/data-store/src/test/java/io/mehow/laboratory/datastore/DataStoreFeatureStorageSpec.kt b/laboratory/data-store/src/test/kotlin/io/mehow/laboratory/datastore/DataStoreFeatureStorageSpec.kt similarity index 85% rename from library/data-store/src/test/java/io/mehow/laboratory/datastore/DataStoreFeatureStorageSpec.kt rename to laboratory/data-store/src/test/kotlin/io/mehow/laboratory/datastore/DataStoreFeatureStorageSpec.kt index 9e9efc2b8..825226b5c 100644 --- a/library/data-store/src/test/java/io/mehow/laboratory/datastore/DataStoreFeatureStorageSpec.kt +++ b/laboratory/data-store/src/test/kotlin/io/mehow/laboratory/datastore/DataStoreFeatureStorageSpec.kt @@ -2,7 +2,7 @@ package io.mehow.laboratory.datastore import androidx.datastore.core.DataStoreFactory import app.cash.turbine.test -import io.kotest.core.spec.style.StringSpec +import io.kotest.core.spec.style.FunSpec import io.kotest.engine.spec.tempfile import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.shouldBe @@ -10,8 +10,8 @@ import io.mehow.laboratory.FeatureStorage import io.mehow.laboratory.Laboratory import okio.ByteString.Companion.decodeHex -internal class DataStoreFeatureStorageSpec : StringSpec({ - "stored feature flag option is available as experiment" { +class DataStoreFeatureStorageSpec : FunSpec({ + test("reads stored feature option") { val tempFile = tempfile() val storage = FeatureStorage.dataStore(DataStoreFactory.create(FeatureFlagsSerializer) { tempFile }) val laboratory = Laboratory.create(storage) @@ -21,21 +21,21 @@ internal class DataStoreFeatureStorageSpec : StringSpec({ laboratory.experiment() shouldBe FeatureA.B } - "corrupted file yields default experiment" { + test("uses default option for corrupted data") { val tempFile = tempfile() val storage = FeatureStorage.dataStore(DataStoreFactory.create(FeatureFlagsSerializer) { tempFile }) val laboratory = Laboratory.create(storage) // Represents a map with a key of Feature::class.java.name and value of 1. val corruptedBytes = "0a290a25696f2e6d65686f772e6c61626f7261746f72792e6461746173746f72652e466561747572651001" - .decodeHex() - .toByteArray() + .decodeHex() + .toByteArray() tempFile.writeBytes(corruptedBytes) laboratory.experiment() shouldBe FeatureA.A } - "observes feature flag changes" { + test("emits feature option changes") { val tempFile = tempfile() val storage = FeatureStorage.dataStore(DataStoreFactory.create(FeatureFlagsSerializer) { tempFile }) @@ -55,7 +55,7 @@ internal class DataStoreFeatureStorageSpec : StringSpec({ } } - "clears feature flag options" { + test("clears storage") { val tempFile = tempfile() val storage = FeatureStorage.dataStore(DataStoreFactory.create(FeatureFlagsSerializer) { tempFile }) val laboratory = Laboratory.create(storage) diff --git a/library/data-store/src/test/java/io/mehow/laboratory/datastore/FeatureA.kt b/laboratory/data-store/src/test/kotlin/io/mehow/laboratory/datastore/FeatureA.kt similarity index 71% rename from library/data-store/src/test/java/io/mehow/laboratory/datastore/FeatureA.kt rename to laboratory/data-store/src/test/kotlin/io/mehow/laboratory/datastore/FeatureA.kt index 22524d32c..3dea5609e 100644 --- a/library/data-store/src/test/java/io/mehow/laboratory/datastore/FeatureA.kt +++ b/laboratory/data-store/src/test/kotlin/io/mehow/laboratory/datastore/FeatureA.kt @@ -2,7 +2,7 @@ package io.mehow.laboratory.datastore import io.mehow.laboratory.Feature -internal enum class FeatureA : Feature { +enum class FeatureA : Feature { A, B, ; diff --git a/laboratory/data-store/src/test/kotlin/io/mehow/laboratory/datastore/FeatureFlagsSerializerSpec.kt b/laboratory/data-store/src/test/kotlin/io/mehow/laboratory/datastore/FeatureFlagsSerializerSpec.kt new file mode 100644 index 000000000..e186459b1 --- /dev/null +++ b/laboratory/data-store/src/test/kotlin/io/mehow/laboratory/datastore/FeatureFlagsSerializerSpec.kt @@ -0,0 +1,29 @@ +package io.mehow.laboratory.datastore + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import okio.Buffer +import okio.ByteString.Companion.decodeHex + +internal class FeatureFlagsSerializerSpec : FunSpec({ + val flags = FeatureFlags(mapOf(FeatureA::class.java.toString() to FeatureA.A.name)) + val hex = "0a310a2c636c61737320696f2e6d65686f772e6c61626f7261746f72792e6461746173746f72652e4665617475726541120141" + val binaryFlags = hex.decodeHex() + + test("decodes bytes") { + val input = Buffer().write(binaryFlags).inputStream() + + val result = FeatureFlagsSerializer.readFrom(input) + + result shouldBe flags + } + + test("encodes bytes") { + val output = Buffer() + + FeatureFlagsSerializer.writeTo(flags, output.outputStream()) + val result = output.readByteString() + + result shouldBe binaryFlags + } +}) diff --git a/library/generator/api/generator.api b/laboratory/generator/api/generator.api similarity index 98% rename from library/generator/api/generator.api rename to laboratory/generator/api/generator.api index 4aec7d0ea..d17a8dfe9 100644 --- a/library/generator/api/generator.api +++ b/laboratory/generator/api/generator.api @@ -72,6 +72,7 @@ public final class io/mehow/laboratory/generator/Supervisor { public final class io/mehow/laboratory/generator/Visibility : java/lang/Enum { public static final field Internal Lio/mehow/laboratory/generator/Visibility; public static final field Public Lio/mehow/laboratory/generator/Visibility; + public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lio/mehow/laboratory/generator/Visibility; public static fun values ()[Lio/mehow/laboratory/generator/Visibility; } diff --git a/laboratory/generator/build.gradle.kts b/laboratory/generator/build.gradle.kts new file mode 100644 index 000000000..b2e2e537d --- /dev/null +++ b/laboratory/generator/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.mavenPublish) + alias(libs.plugins.spotless) + alias(libs.plugins.detekt) + alias(libs.plugins.dokka) +} + +tasks.withType().configureEach { + useJUnitPlatform() +} + +dependencies { + api(libs.kotlinPoet) + implementation(projects.laboratory.runtime) + + testImplementation(libs.kotest.runner.junit5) + testImplementation(libs.kotest.assertions) + testImplementation(libs.kotest.property) +} diff --git a/library/generator/gradle.properties b/laboratory/generator/gradle.properties similarity index 63% rename from library/generator/gradle.properties rename to laboratory/generator/gradle.properties index f2688833c..bb064064b 100644 --- a/library/generator/gradle.properties +++ b/laboratory/generator/gradle.properties @@ -1,3 +1,3 @@ POM_ARTIFACT_ID=laboratory-generator -POM_NAME=Laboratory (generator) +POM_NAME=Laboratory (Generator) POM_PACKAGING=jar diff --git a/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/ClassNames.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/ClassNames.kt new file mode 100644 index 000000000..e0e5541dd --- /dev/null +++ b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/ClassNames.kt @@ -0,0 +1,11 @@ +package io.mehow.laboratory.generator + +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.asClassName +import kotlin.reflect.KClass + +internal operator fun KClass<*>.invoke( + parameter: TypeName, + vararg parameters: TypeName, +) = asClassName().parameterizedBy(parameter, *parameters) diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/Deprecation.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/Deprecation.kt similarity index 91% rename from library/generator/src/main/java/io/mehow/laboratory/generator/Deprecation.kt rename to laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/Deprecation.kt index 0800e8fca..2b1bff626 100644 --- a/library/generator/src/main/java/io/mehow/laboratory/generator/Deprecation.kt +++ b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/Deprecation.kt @@ -14,7 +14,7 @@ public class Deprecation( ERROR, HIDDEN -> "DEPRECATION_ERROR" }.let { name -> AnnotationSpec.builder(Suppress::class) - .addMember("%S", name) - .build() + .addMember("%S", name) + .build() } } diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFactoryGenerator.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFactoryGenerator.kt similarity index 60% rename from library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFactoryGenerator.kt rename to laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFactoryGenerator.kt index ddd6b333b..c1fb8ade2 100644 --- a/library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFactoryGenerator.kt +++ b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFactoryGenerator.kt @@ -20,49 +20,51 @@ internal class FeatureFactoryGenerator( functionName: String, ) { private val featureClasses = factory.features - .map { it.className.reflectionName() } - .sorted() - .map { name -> CodeBlock.of("%T.forName(%S)", Class::class.asTypeName(), name) } - .joinToCode(prefix = "\n⇥", separator = ",\n", suffix = "⇤\n") + .map { it.className.reflectionName() } + .sorted() + .map { name -> CodeBlock.of("%T.forName(%S)", Class::class.asTypeName(), name) } + .joinToCode(prefix = "\n⇥", separator = ",\n", suffix = "⇤\n") private val suppressCast = AnnotationSpec.builder(Suppress::class) - .addMember("%S", "UNCHECKED_CAST") - .build() + .addMember("%S", "UNCHECKED_CAST") + .build() private val setOf = MemberName("kotlin.collections", "setOf") private val emptySet = MemberName("kotlin.collections", "emptySet") private val discoveryFunctionOverride = FunSpec.builder("create") - .addModifiers(OVERRIDE) - .apply { - returns(factoryReturnType) + .addModifiers(OVERRIDE) + .apply { + returns(factoryReturnType) - if (factory.features.isNotEmpty()) { - addAnnotation(suppressCast) - addStatement("return %M(%L) as %T", setOf, featureClasses, factoryReturnType) - } else addStatement("return %M<%T>()", emptySet, featureType) + if (factory.features.isNotEmpty()) { + addAnnotation(suppressCast) + addStatement("return %M(%L) as %T", setOf, featureClasses, factoryReturnType) + } else { + addStatement("return %M<%T>()", emptySet, featureType) } - .build() + } + .build() private val factoryType = TypeSpec.objectBuilder(factory.className) - .addModifiers(PRIVATE) - .addSuperinterface(FeatureFactory::class) - .addFunction(discoveryFunctionOverride) - .build() + .addModifiers(PRIVATE) + .addSuperinterface(FeatureFactory::class) + .addFunction(discoveryFunctionOverride) + .build() private val factoryExtension = FunSpec.builder(functionName) - .addModifiers(factory.visibility.modifier) - .receiver(FeatureFactory.Companion::class) - .returns(FeatureFactory::class) - .addStatement("return %N", factoryType) - .build() + .addModifiers(factory.visibility.modifier) + .receiver(FeatureFactory.Companion::class) + .returns(FeatureFactory::class) + .addStatement("return %N", factoryType) + .build() private val factoryFile = FileSpec.builder(factory.className.packageName, factory.className.simpleName) - .addFunction(factoryExtension) - .addType(factoryType) - .build() + .addFunction(factoryExtension) + .addType(factoryType) + .build() fun fileSpec() = factoryFile diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFactoryModel.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFactoryModel.kt similarity index 100% rename from library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFactoryModel.kt rename to laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFactoryModel.kt diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFlagGenerator.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFlagGenerator.kt similarity index 62% rename from library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFlagGenerator.kt rename to laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFlagGenerator.kt index 81912863b..51658317c 100644 --- a/library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFlagGenerator.kt +++ b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFlagGenerator.kt @@ -20,28 +20,28 @@ internal class FeatureFlagGenerator( ) { private val deprecated = feature.deprecation?.let { deprecation -> AnnotationSpec.builder(Deprecated::class) - .addMember("message = %S", deprecation.message) - .addMember("level = %T.%L", DeprecationLevel::class, deprecation.level) - .build() + .addMember("message = %S", deprecation.message) + .addMember("level = %T.%L", DeprecationLevel::class, deprecation.level) + .build() } private val suppressDeprecation = feature.deprecation?.suppressSpec private val defaultOptionProperty = feature.options.toList() - .single(FeatureFlagOption::isDefault) - .let { option -> - PropertySpec - .builder(defaultOptionPropertyName, feature.className, OVERRIDE) - .apply { suppressDeprecation?.let { addAnnotation(it) } } - .getter(FunSpec.getterBuilder().addCode("return %L", option.name).build()) - .build() - } + .single(FeatureFlagOption::isDefault) + .let { option -> + PropertySpec + .builder(defaultOptionPropertyName, feature.className, OVERRIDE) + .apply { suppressDeprecation?.let { addAnnotation(it) } } + .getter(FunSpec.getterBuilder().addCode("return %L", option.name).build()) + .build() + } private val sourceProperty = feature.source?.let { nestedSource -> nestedSource to PropertySpec - .builder(sourcePropertyName, featureClassType, OVERRIDE) - .initializer("%T::class.java", nestedSource.className) - .build() + .builder(sourcePropertyName, featureClassType, OVERRIDE) + .initializer("%T::class.java", nestedSource.className) + .build() } private val description: String? = feature.description.takeIf(String::isNotBlank) @@ -50,49 +50,49 @@ internal class FeatureFlagGenerator( private val descriptionProperty = description?.let { description -> PropertySpec - .builder(descriptionPropertyName, String::class, OVERRIDE) - .initializer("%S", description) - .build() + .builder(descriptionPropertyName, String::class, OVERRIDE) + .initializer("%S", description) + .build() } private val supervisorOptionProperty = feature.supervisor?.let { supervisor -> PropertySpec - .builder(supervisorOptionPropertyName, featureType, OVERRIDE) - .initializer("%T.%L", supervisor.featureFlag.className, supervisor.option.name) - .build() + .builder(supervisorOptionPropertyName, featureType, OVERRIDE) + .initializer("%T.%L", supervisor.featureFlag.className, supervisor.option.name) + .build() } private val typeSpec: TypeSpec = TypeSpec.enumBuilder(feature.className) - .apply { deprecated?.let { addAnnotation(it) } } - .addModifiers(feature.visibility.modifier) - .apply { - var parametrizedType: TypeName = feature.className - if (suppressDeprecation != null) { - parametrizedType = parametrizedType.copy(annotations = listOf(suppressDeprecation)) - } - addSuperinterface(Feature::class(parametrizedType)) + .apply { deprecated?.let { addAnnotation(it) } } + .addModifiers(feature.visibility.modifier) + .apply { + var parametrizedType: TypeName = feature.className + if (suppressDeprecation != null) { + parametrizedType = parametrizedType.copy(annotations = listOf(suppressDeprecation)) } - .addProperty(defaultOptionProperty) - .apply { - feature.options.fold(this) { builder, featureOption -> - builder.addEnumConstant(featureOption.name) - } + addSuperinterface(Feature::class(parametrizedType)) + } + .addProperty(defaultOptionProperty) + .apply { + feature.options.fold(this) { builder, featureOption -> + builder.addEnumConstant(featureOption.name) } - .apply { - sourceProperty?.let { (nestedSource, sourceWithOverride) -> - addType(FeatureFlagGenerator(nestedSource).typeSpec) - addProperty(sourceWithOverride) - } + } + .apply { + sourceProperty?.let { (nestedSource, sourceWithOverride) -> + addType(FeatureFlagGenerator(nestedSource).typeSpec) + addProperty(sourceWithOverride) } - .apply { kdocCodeBlock?.let { addKdoc(it) } } - .apply { descriptionProperty?.let { addProperty(it) } } - .apply { supervisorOptionProperty?.let { addProperty(it) } } - .build() + } + .apply { kdocCodeBlock?.let { addKdoc(it) } } + .apply { descriptionProperty?.let { addProperty(it) } } + .apply { supervisorOptionProperty?.let { addProperty(it) } } + .build() private val fileSpec = FileSpec.builder(feature.className.packageName, feature.className.simpleName) - .addType(typeSpec) - .build() + .addType(typeSpec) + .build() fun fileSpec() = fileSpec @@ -115,8 +115,8 @@ internal fun String.prepareKdocHyperlinks(): String { val regularTokens = matches.toRegularTokens(this) val linkTokens = matches.toLinkTokens() val tokens = (regularTokens + linkTokens) - .sortedBy { (_, startIndex) -> startIndex } - .map { (token, _) -> token } + .sortedBy { (_, startIndex) -> startIndex } + .map { (token, _) -> token } return buildString { for (token in tokens) { token.append(this) @@ -151,12 +151,12 @@ private fun Sequence.toLinkTokens() = map { matchResult -> } private fun Sequence.toRegularTokens(text: String) = toUnmatchedRanges(text) - .map { range -> Regular(text.substring(range)) to range.first } + .map { range -> Regular(text.substring(range)) to range.first } private fun Sequence.toUnmatchedRanges(text: String) = sequence { yield(Int.MIN_VALUE..0) yieldAll(map { it.range }.map { it.first - 1..it.last + 1 }) yield(text.length - 1..Int.MAX_VALUE) }.windowed(2, 1) - .map { (start, end) -> start.last..end.first } - .filterNot { range -> range.isEmpty() } + .map { (start, end) -> start.last..end.first } + .filterNot { range -> range.isEmpty() } diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFlagModel.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFlagModel.kt similarity index 84% rename from library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFlagModel.kt rename to laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFlagModel.kt index 954e6729c..7808b4036 100644 --- a/library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFlagModel.kt +++ b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFlagModel.kt @@ -37,14 +37,14 @@ public class FeatureFlagModel private constructor( sourceOptions: List = emptyList(), supervisor: Supervisor? = null, ) : this( - className, - options, - visibility, - key, - description, - deprecation, - createSource(visibility, className, sourceOptions), - supervisor, + className, + options, + visibility, + key, + description, + deprecation, + createSource(visibility, className, sourceOptions), + supervisor, ) public fun prepare(): FileSpec = FeatureFlagGenerator(this).fileSpec() @@ -68,12 +68,12 @@ public class FeatureFlagModel private constructor( private fun ClassName.toSourceName() = ClassName(packageName, simpleNames + "Source") private fun List.toSourceOptions() = filterNot { it.name.equals("local", ignoreCase = true) } - .takeIf { it.isNotEmpty() } - ?.let { options -> - buildList { - add(FeatureFlagOption("Local", isDefault = options.none(FeatureFlagOption::isDefault))) - addAll(options) - } + .takeIf { it.isNotEmpty() } + ?.let { options -> + buildList { + add(FeatureFlagOption("Local", isDefault = options.none(FeatureFlagOption::isDefault))) + addAll(options) } + } } } diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFlagOption.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFlagOption.kt similarity index 100% rename from library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFlagOption.kt rename to laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFlagOption.kt diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/OptionFactoryModel.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/OptionFactoryModel.kt similarity index 52% rename from library/generator/src/main/java/io/mehow/laboratory/generator/OptionFactoryModel.kt rename to laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/OptionFactoryModel.kt index d113e36a7..0a5efdcd6 100644 --- a/library/generator/src/main/java/io/mehow/laboratory/generator/OptionFactoryModel.kt +++ b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/OptionFactoryModel.kt @@ -29,12 +29,12 @@ public class OptionFactoryModel( val groupedFeatures = features.groupBy { it.key ?: it.className.canonicalName } require(groupedFeatures.size == features.size) { val duplicates = groupedFeatures - .filterValues { it.size > 1 } - .mapValues { (_, features) -> features.map(FeatureFlagModel::toString) } + .filterValues { it.size > 1 } + .mapValues { (_, features) -> features.map(FeatureFlagModel::toString) } """ |Feature flags must have unique keys. Found following duplicates: | - ${duplicates.toList().joinToString(separator = "\n - ") { (key, fqcns) -> "$key: $fqcns" }} - """.trimMargin() + """.trimMargin() } } } @@ -44,46 +44,46 @@ private class OptionFactoryGenerator( private val model: OptionFactoryModel, ) { private val nameMatcher = model.features.associateBy { it.className } - .mapValues { (className, feature) -> - val whenExpression = feature.options - .map { CodeBlock.of("%S·->·%T.%L", it.name, className, it.name) } - .joinToCode(prefix = "when·(name)·{\n⇥", separator = "\n", suffix = "\nelse·->·null⇤\n}") - val deprecation = feature.deprecation?.suppressSpec - if (deprecation != null) { - CodeBlock.of("%L·%L", deprecation, whenExpression) - } else { - whenExpression - } + .mapValues { (className, feature) -> + val whenExpression = feature.options + .map { CodeBlock.of("%S·->·%T.%L", it.name, className, it.name) } + .joinToCode(prefix = "when·(name)·{\n⇥", separator = "\n", suffix = "\nelse·->·null⇤\n}") + val deprecation = feature.deprecation?.suppressSpec + if (deprecation != null) { + CodeBlock.of("%L·%L", deprecation, whenExpression) + } else { + whenExpression } + } private val keyMatcher = model.features - .sortedWith(compareBy({ it.key == null }, { it.key }, { it.className.canonicalName })) - .map { CodeBlock.of("%S·->·%L", it.key ?: it.className.canonicalName, nameMatcher.getValue(it.className)) } - .joinToCode(prefix = "when·(key)·{\n⇥", separator = "\n", suffix = "\nelse·->·null⇤\n}") + .sortedWith(compareBy({ it.key == null }, { it.key }, { it.className.canonicalName })) + .map { CodeBlock.of("%S·->·%L", it.key ?: it.className.canonicalName, nameMatcher.getValue(it.className)) } + .joinToCode(prefix = "when·(key)·{\n⇥", separator = "\n", suffix = "\nelse·->·null⇤\n}") private val createFunctionOverride = FunSpec.builder("create") - .addModifiers(OVERRIDE) - .addParameter("key", String::class) - .addParameter("name", String::class) - .returns(Feature::class(STAR).copy(nullable = true)) - .apply { if (model.features.isEmpty()) addStatement("return null") else addStatement("return %L", keyMatcher) } - .build() + .addModifiers(OVERRIDE) + .addParameter("key", String::class) + .addParameter("name", String::class) + .returns(Feature::class(STAR).copy(nullable = true)) + .apply { if (model.features.isEmpty()) addStatement("return null") else addStatement("return %L", keyMatcher) } + .build() private val factoryType = TypeSpec.objectBuilder(model.className) - .addModifiers(PRIVATE) - .addSuperinterface(OptionFactory::class) - .addFunction(createFunctionOverride) - .build() + .addModifiers(PRIVATE) + .addSuperinterface(OptionFactory::class) + .addFunction(createFunctionOverride) + .build() private val factoryExtension = FunSpec.builder("generated") - .addModifiers(model.visibility.modifier) - .receiver(OptionFactory.Companion::class) - .returns(OptionFactory::class) - .addStatement("return %N", factoryType) - .build() + .addModifiers(model.visibility.modifier) + .receiver(OptionFactory.Companion::class) + .returns(OptionFactory::class) + .addStatement("return %N", factoryType) + .build() val fileSpec = FileSpec.builder(model.className.packageName, model.className.simpleName) - .addFunction(factoryExtension) - .addType(factoryType) - .build() + .addFunction(factoryExtension) + .addType(factoryType) + .build() } diff --git a/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/SourcedFeatureStorageGenerator.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/SourcedFeatureStorageGenerator.kt new file mode 100644 index 000000000..b939637a3 --- /dev/null +++ b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/SourcedFeatureStorageGenerator.kt @@ -0,0 +1,150 @@ +package io.mehow.laboratory.generator + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier.ABSTRACT +import com.squareup.kotlinpoet.KModifier.DATA +import com.squareup.kotlinpoet.KModifier.OVERRIDE +import com.squareup.kotlinpoet.KModifier.PRIVATE +import com.squareup.kotlinpoet.KOperator.PLUS +import com.squareup.kotlinpoet.MemberName +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.asClassName +import io.mehow.laboratory.FeatureStorage +import java.util.Locale + +internal class SourcedFeatureStorageGenerator( + storage: SourcedFeatureStorageModel, +) { + private val sourceNames = storage.sourceNames + .filterNot { featureName -> featureName.equals("local", ignoreCase = true) } + .distinct() + + private val sourced = MemberName(FeatureStorage.Companion::class.asClassName(), "sourced") + + private val emptyMap = MemberName(kotlinCollectionsSpace, "emptyMap") + + private val mapPlus = MemberName(kotlinCollectionsSpace, PLUS) + + private val infixTo = MemberName("kotlin", "to") + + private val buildingStepClassName = ClassName(storage.className.packageName, "BuildingStep") + + private val buildingStepType = TypeSpec.interfaceBuilder(buildingStepClassName) + .addModifiers(storage.visibility.modifier) + .addFunction( + FunSpec.builder("build") + .addModifiers(ABSTRACT) + .returns(FeatureStorage::class) + .build(), + ) + .build() + + private val remoteStepClassNames = sourceNames.distinct() + .sorted() + .map { ClassName(storage.className.packageName, it + stepSuffix) } + + private val remoteStepTypes = remoteStepClassNames + .windowed(size = 2, step = 1, partialWindows = true) { sources -> + val currentSourceClassName = sources.first() + val functionReturnClassName = sources.drop(1).firstOrNull() ?: buildingStepClassName + val functionName = currentSourceClassName.simpleName + .removeSuffix(stepSuffix) + .replaceFirstChar { it.lowercase(Locale.ROOT) } + "Source" + + TypeSpec.interfaceBuilder(currentSourceClassName) + .addModifiers(storage.visibility.modifier) + .addFunction( + FunSpec.builder(functionName) + .addModifiers(ABSTRACT) + .addParameter("source", FeatureStorage::class) + .returns(functionReturnClassName) + .build(), + ) + .build() + } + + private val builderType = TypeSpec.classBuilder(ClassName(storage.className.simpleName, "Builder")) + .addModifiers(PRIVATE, DATA) + .addSuperinterfaces(remoteStepClassNames + buildingStepClassName) + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter(localSourceParam, FeatureStorage::class) + .addParameter(remoteSourcesParam, stringToStorageMap) + .build(), + ) + .addProperty( + PropertySpec.builder(localSourceParam, FeatureStorage::class) + .initializer(localSourceParam) + .addModifiers(PRIVATE) + .build(), + ) + .addProperty( + PropertySpec.builder(remoteSourcesParam, stringToStorageMap) + .initializer(remoteSourcesParam) + .addModifiers(PRIVATE) + .build(), + ) + .addFunctions( + remoteStepTypes.mapIndexed { index, remoteStep -> + val function = remoteStep.funSpecs.single() + function.toBuilder() + .apply { modifiers -= ABSTRACT } + .addModifiers(OVERRIDE) + .addStatement( + "return copy(\n⇥%1L = %1L %2M (%3S %4M %5N)⇤\n)", + remoteSourcesParam, + mapPlus, + remoteStepClassNames[index].simpleName.removeSuffix(stepSuffix), + infixTo, + function.parameters.single(), + ) + .build() + }, + ) + .addFunction( + buildingStepType.funSpecs.single() + .toBuilder() + .apply { modifiers -= ABSTRACT } + .addModifiers(OVERRIDE) + .addStatement("return %M(%L, %L)", sourced, localSourceParam, remoteSourcesParam) + .build(), + ) + .build() + + private val storageBuilderExtension = FunSpec.builder("sourcedBuilder") + .addModifiers(storage.visibility.modifier) + .receiver(FeatureStorage.Companion::class) + .returns(remoteStepClassNames.firstOrNull() ?: buildingStepClassName) + .addParameter(localSourceParam, FeatureStorage::class) + .addStatement("return %N(%L, %M())", builderType, localSourceParam, emptyMap) + .build() + + private val storageFile = FileSpec.builder(storage.className.packageName, storage.className.simpleName) + .addFunction(storageBuilderExtension) + .apply { + for (type in remoteStepTypes) { + addType(type) + } + } + .addType(buildingStepType) + .addType(builderType) + .build() + + fun fileSpec() = storageFile + + private companion object { + const val stepSuffix = "Step" + const val localSourceParam = "localSource" + const val remoteSourcesParam = "remoteSources" + + const val kotlinCollectionsSpace = "kotlin.collections" + + val stringToStorageMap = Map::class( + String::class.asClassName(), + FeatureStorage::class.asClassName(), + ) + } +} diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/SourcedFeatureStorageModel.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/SourcedFeatureStorageModel.kt similarity index 100% rename from library/generator/src/main/java/io/mehow/laboratory/generator/SourcedFeatureStorageModel.kt rename to laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/SourcedFeatureStorageModel.kt diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/Supervisor.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/Supervisor.kt similarity index 100% rename from library/generator/src/main/java/io/mehow/laboratory/generator/Supervisor.kt rename to laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/Supervisor.kt diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/Visibility.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/Visibility.kt similarity index 92% rename from library/generator/src/main/java/io/mehow/laboratory/generator/Visibility.kt rename to laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/Visibility.kt index 354a418eb..f9604752c 100644 --- a/library/generator/src/main/java/io/mehow/laboratory/generator/Visibility.kt +++ b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/Visibility.kt @@ -8,5 +8,5 @@ public enum class Visibility( internal val modifier: KModifier, ) { Public(PUBLIC), - Internal(INTERNAL) + Internal(INTERNAL), } diff --git a/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/FeatureFactoryModelSpec.kt b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/FeatureFactoryModelSpec.kt new file mode 100644 index 000000000..702053844 --- /dev/null +++ b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/FeatureFactoryModelSpec.kt @@ -0,0 +1,116 @@ +package io.mehow.laboratory.generator + +import com.squareup.kotlinpoet.ClassName +import io.kotest.core.spec.style.FunSpec +import io.mehow.laboratory.generator.Visibility.Internal +import io.mehow.laboratory.generator.Visibility.Public +import io.mehow.laboratory.generator.test.shouldSpecify + +class FeatureFactoryModelSpec : FunSpec({ + val featureA = FeatureFlagModel( + className = ClassName("io.mehow", "FeatureA"), + options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + ) + + val featureB = FeatureFlagModel( + className = ClassName("io.mehow", "FeatureB"), + options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + ) + + val featureC = FeatureFlagModel( + className = ClassName("io.mehow.c", "FeatureA"), + options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + ) + + test("can be internal") { + val model = FeatureFactoryModel( + ClassName("io.mehow", "GeneratedFeatureFactory"), + listOf(featureA, featureB, featureC), + visibility = Internal, + ) + + val fileSpec = model.prepare("generated") + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import io.mehow.laboratory.FeatureFactory + |import java.lang.Class + |import kotlin.Suppress + |import kotlin.collections.Set + |import kotlin.collections.setOf + | + |internal fun FeatureFactory.Companion.generated(): FeatureFactory = GeneratedFeatureFactory + | + |private object GeneratedFeatureFactory : FeatureFactory { + | @Suppress("UNCHECKED_CAST") + | override fun create(): Set>> = setOf( + | Class.forName("io.mehow.FeatureA"), + | Class.forName("io.mehow.FeatureB"), + | Class.forName("io.mehow.c.FeatureA") + | ) as Set>> + |} + | + """.trimMargin() + } + + test("can be public") { + val model = FeatureFactoryModel( + ClassName("io.mehow", "GeneratedFeatureFactory"), + listOf(featureA, featureB, featureC), + visibility = Public, + ) + + val fileSpec = model.prepare("generated") + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import io.mehow.laboratory.FeatureFactory + |import java.lang.Class + |import kotlin.Suppress + |import kotlin.collections.Set + |import kotlin.collections.setOf + | + |public fun FeatureFactory.Companion.generated(): FeatureFactory = GeneratedFeatureFactory + | + |private object GeneratedFeatureFactory : FeatureFactory { + | @Suppress("UNCHECKED_CAST") + | override fun create(): Set>> = setOf( + | Class.forName("io.mehow.FeatureA"), + | Class.forName("io.mehow.FeatureB"), + | Class.forName("io.mehow.c.FeatureA") + | ) as Set>> + |} + | + """.trimMargin() + } + + test("is optimized when there are no features") { + val model = FeatureFactoryModel( + ClassName("io.mehow", "GeneratedFeatureFactory"), + features = emptyList(), + ) + + val fileSpec = model.prepare("generated") + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import io.mehow.laboratory.FeatureFactory + |import java.lang.Class + |import kotlin.collections.Set + |import kotlin.collections.emptySet + | + |internal fun FeatureFactory.Companion.generated(): FeatureFactory = GeneratedFeatureFactory + | + |private object GeneratedFeatureFactory : FeatureFactory { + | override fun create(): Set>> = emptySet>>() + |} + | + """.trimMargin() + } +}) diff --git a/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/FeatureFlagModelSpec.kt b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/FeatureFlagModelSpec.kt new file mode 100644 index 000000000..282a3d2c1 --- /dev/null +++ b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/FeatureFlagModelSpec.kt @@ -0,0 +1,521 @@ +package io.mehow.laboratory.generator + +import com.squareup.kotlinpoet.ClassName +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.throwable.shouldHaveMessage +import io.kotest.property.Arb +import io.kotest.property.arbitrary.stringPattern +import io.kotest.property.checkAll +import io.mehow.laboratory.generator.Visibility.Internal +import io.mehow.laboratory.generator.Visibility.Public +import io.mehow.laboratory.generator.test.shouldSpecify +import java.util.Locale +import kotlin.DeprecationLevel.ERROR +import kotlin.DeprecationLevel.HIDDEN +import kotlin.DeprecationLevel.WARNING + +class FeatureFlagModelSpec : FunSpec({ + test("can be internal") { + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + visibility = Internal, + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + | + |internal enum class FeatureA : Feature { + | First, + | Second, + | ; + | + | override val defaultOption: FeatureA + | get() = First + |} + | + """.trimMargin() + } + + test("can be public") { + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + visibility = Public, + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + | + |public enum class FeatureA : Feature { + | First, + | Second, + | ; + | + | override val defaultOption: FeatureA + | get() = First + |} + | + """.trimMargin() + } + + test("can have single option") { + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(FeatureFlagOption("First", isDefault = true)), + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + | + |public enum class FeatureA : Feature { + | First, + | ; + | + | override val defaultOption: FeatureA + | get() = First + |} + | + """.trimMargin() + } + + test("can have source") { + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + sourceOptions = listOf(FeatureFlagOption("Remote")), + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import java.lang.Class + | + |public enum class FeatureA : Feature { + | First, + | Second, + | ; + | + | override val defaultOption: FeatureA + | get() = First + | + | override val source: Class> = Source::class.java + | + | public enum class Source : Feature { + | Local, + | Remote, + | ; + | + | override val defaultOption: Source + | get() = Local + | } + |} + | + """.trimMargin() + } + + test("does not have source parameter if only source is Local") { + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + sourceOptions = listOf(FeatureFlagOption("Local")), + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + | + |public enum class FeatureA : Feature { + | First, + | Second, + | ; + | + | override val defaultOption: FeatureA + | get() = First + |} + | + """.trimMargin() + } + + test("filters out any custom local source") { + val localPermutations = (0b00000..0b11111).map { + listOf(it and 0b00001, it and 0b00010, it and 0b00100, it and 0b01000, it and 0b10000) + .map { mask -> mask != 0 } + .mapIndexed { index, mask -> + val chars = "local"[index].toString() + if (mask) chars else chars.replaceFirstChar { char -> char.titlecase(Locale.ROOT) } + }.joinToString(separator = "") + } + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + sourceOptions = (localPermutations + "Remote").map(::FeatureFlagOption), + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import java.lang.Class + | + |public enum class FeatureA : Feature { + | First, + | Second, + | ; + | + | override val defaultOption: FeatureA + | get() = First + | + | override val source: Class> = Source::class.java + | + | public enum class Source : Feature { + | Local, + | Remote, + | ; + | + | override val defaultOption: Source + | get() = Local + | } + |} + | + """.trimMargin() + } + + test("can change default source") { + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + sourceOptions = listOf(FeatureFlagOption("Remote", isDefault = true)), + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import java.lang.Class + | + |public enum class FeatureA : Feature { + | First, + | Second, + | ; + | + | override val defaultOption: FeatureA + | get() = First + | + | override val source: Class> = Source::class.java + | + | public enum class Source : Feature { + | Local, + | Remote, + | ; + | + | override val defaultOption: Source + | get() = Remote + | } + |} + | + """.trimMargin() + } + + test("copies feature visibility to source visibility") { + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + visibility = Internal, + sourceOptions = listOf(FeatureFlagOption("Remote")), + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import java.lang.Class + | + |internal enum class FeatureA : Feature { + | First, + | Second, + | ; + | + | override val defaultOption: FeatureA + | get() = First + | + | override val source: Class> = Source::class.java + | + | internal enum class Source : Feature { + | Local, + | Remote, + | ; + | + | override val defaultOption: Source + | get() = Local + | } + |} + | + """.trimMargin() + } + + test("can have supervisor") { + val supervisor = FeatureFlagModel( + ClassName("io.mehow.supervisor", "Supervisor"), + listOf(FeatureFlagOption("First", isDefault = true)), + ) + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + supervisor = Supervisor(supervisor, supervisor.options.first()), + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import io.mehow.supervisor.Supervisor + | + |public enum class FeatureA : Feature { + | First, + | Second, + | ; + | + | override val defaultOption: FeatureA + | get() = First + | + | override val supervisorOption: Feature<*> = Supervisor.First + |} + | + """.trimMargin() + } + + test("description is added as KDoc") { + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + description = "Feature description", + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import kotlin.String + | + |/** + | * Feature description + | */ + |public enum class FeatureA : Feature { + | First, + | Second, + | ; + | + | override val defaultOption: FeatureA + | get() = First + | + | override val description: String = "Feature description" + |} + | + """.trimMargin() + } + + test("description does not break hyperlinks") { + @Suppress("MaxLineLength") + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + description = "Some [long hyperlink](https://square.github.io/kotlinpoet/1.x/kotlinpoet-classinspector-elements/com.squareup.kotlinpoet.classinspector.elements/) in the KDoc.", + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import kotlin.String + | + |/** + | * Some + | * [long hyperlink](https://square.github.io/kotlinpoet/1.x/kotlinpoet-classinspector-elements/com.squareup.kotlinpoet.classinspector.elements/) + | * in the KDoc. + | */ + |public enum class FeatureA : Feature { + | First, + | Second, + | ; + | + | override val defaultOption: FeatureA + | get() = First + | + | override val description: String = + | "Some [long hyperlink](https://square.github.io/kotlinpoet/1.x/kotlinpoet-classinspector-elements/com.squareup.kotlinpoet.classinspector.elements/) in the KDoc." + |} + | + """.trimMargin() + } + + test("uses warning level as a default deprecation") { + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + deprecation = Deprecation("Deprecation message"), + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import kotlin.Deprecated + |import kotlin.DeprecationLevel + |import kotlin.Suppress + | + |@Deprecated( + | message = "Deprecation message", + | level = DeprecationLevel.WARNING, + |) + |public enum class FeatureA : Feature<@Suppress("DEPRECATION") FeatureA> { + | First, + | Second, + | ; + | + | @Suppress("DEPRECATION") + | override val defaultOption: FeatureA + | get() = First + |} + | + """.trimMargin() + } + + enumValues().forEach { level -> + test("can use explicit $level deprecation level") { + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + deprecation = Deprecation("Deprecation message", level), + ) + val suppressLevel = when (level) { + WARNING -> "DEPRECATION" + ERROR, HIDDEN -> "DEPRECATION_ERROR" + } + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import kotlin.Deprecated + |import kotlin.DeprecationLevel + |import kotlin.Suppress + | + |@Deprecated( + | message = "Deprecation message", + | level = DeprecationLevel.$level, + |) + |public enum class FeatureA : Feature<@Suppress("$suppressLevel") FeatureA> { + | First, + | Second, + | ; + | + | @Suppress("$suppressLevel") + | override val defaultOption: FeatureA + | get() = First + |} + | + """.trimMargin() + } + } + + test("fails to generate when there are no options") { + val exception = shouldThrow { + FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + options = emptyList(), + ) + } + + exception shouldHaveMessage "io.mehow.FeatureA must have at least one option" + } + + test("fails to generate when there is no default option") { + checkAll( + Arb.stringPattern("[a-z](0)([a-z]{0,10})"), + Arb.stringPattern("[a-z](1)([a-z]{0,10})"), + ) { first, second -> + val exception = shouldThrow { + FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(FeatureFlagOption(first), FeatureFlagOption(second)), + ) + } + + exception shouldHaveMessage "io.mehow.FeatureA must have exactly one default option" + } + } + + test("fails to generate when there are multiple default options") { + checkAll( + Arb.stringPattern("[a-z](0)([a-z]{0,10})"), + Arb.stringPattern("[a-z](1)([a-z]{0,10})"), + Arb.stringPattern("[a-z](2)([a-z]{0,10})"), + ) { first, second, third -> + val exception = shouldThrow { + FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf( + FeatureFlagOption(first, isDefault = true), + FeatureFlagOption(second), + FeatureFlagOption(third, isDefault = true), + ), + ) + } + + exception shouldHaveMessage "io.mehow.FeatureA must have exactly one default option" + } + } + + test("fails to supervise itself") { + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(FeatureFlagOption("First", isDefault = true)), + ) + + val exception = shouldThrow { + FeatureFlagModel( + model.className, + model.options, + supervisor = Supervisor(model, model.options.first()), + ) + } + + exception shouldHaveMessage "io.mehow.FeatureA cannot supervise itself" + } +}) diff --git a/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/OptionFactoryModelSpec.kt b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/OptionFactoryModelSpec.kt new file mode 100644 index 000000000..a5f07be7c --- /dev/null +++ b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/OptionFactoryModelSpec.kt @@ -0,0 +1,302 @@ +package io.mehow.laboratory.generator + +import com.squareup.kotlinpoet.ClassName +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.throwable.shouldHaveMessage +import io.kotest.property.Arb +import io.kotest.property.arbitrary.stringPattern +import io.kotest.property.checkAll +import io.mehow.laboratory.generator.Visibility.Internal +import io.mehow.laboratory.generator.Visibility.Public +import io.mehow.laboratory.generator.test.shouldSpecify +import kotlin.DeprecationLevel.ERROR +import kotlin.DeprecationLevel.HIDDEN +import kotlin.DeprecationLevel.WARNING + +class OptionFactoryModelSpec : FunSpec({ + test("can be internal") { + val features = listOf( + FeatureFlagModel( + className = ClassName("io.mehow", "FeatureA"), + options = listOf(FeatureFlagOption("OneA", isDefault = true), FeatureFlagOption("OneB")), + key = "FeatureA", + ), + FeatureFlagModel( + className = ClassName("io.mehow", "FeatureB"), + options = listOf(FeatureFlagOption("TwoA", isDefault = true)), + ), + FeatureFlagModel( + className = ClassName("io.mehow.c", "FeatureC"), + options = listOf(FeatureFlagOption("ThreeA", isDefault = true), FeatureFlagOption("ThreeB")), + key = "FeatureC", + ), + ) + val model = OptionFactoryModel( + ClassName("io.mehow", "GeneratedOptionFactory"), + features, + visibility = Internal, + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.c.FeatureC + |import io.mehow.laboratory.Feature + |import io.mehow.laboratory.OptionFactory + |import kotlin.String + | + |internal fun OptionFactory.Companion.generated(): OptionFactory = GeneratedOptionFactory + | + |private object GeneratedOptionFactory : OptionFactory { + | override fun create(key: String, name: String): Feature<*>? = when (key) { + | "FeatureA" -> when (name) { + | "OneA" -> FeatureA.OneA + | "OneB" -> FeatureA.OneB + | else -> null + | } + | "FeatureC" -> when (name) { + | "ThreeA" -> FeatureC.ThreeA + | "ThreeB" -> FeatureC.ThreeB + | else -> null + | } + | "io.mehow.FeatureB" -> when (name) { + | "TwoA" -> FeatureB.TwoA + | else -> null + | } + | else -> null + | } + |} + | + """.trimMargin() + } + + test("can be public") { + val features = listOf( + FeatureFlagModel( + className = ClassName("io.mehow", "FeatureA"), + options = listOf(FeatureFlagOption("OneA", isDefault = true), FeatureFlagOption("OneB")), + key = "FeatureA", + ), + FeatureFlagModel( + className = ClassName("io.mehow", "FeatureB"), + options = listOf(FeatureFlagOption("TwoA", isDefault = true)), + ), + FeatureFlagModel( + className = ClassName("io.mehow.c", "FeatureC"), + options = listOf(FeatureFlagOption("ThreeA", isDefault = true), FeatureFlagOption("ThreeB")), + key = "FeatureC", + ), + ) + val model = OptionFactoryModel( + ClassName("io.mehow", "GeneratedOptionFactory"), + features, + visibility = Public, + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.c.FeatureC + |import io.mehow.laboratory.Feature + |import io.mehow.laboratory.OptionFactory + |import kotlin.String + | + |public fun OptionFactory.Companion.generated(): OptionFactory = GeneratedOptionFactory + | + |private object GeneratedOptionFactory : OptionFactory { + | override fun create(key: String, name: String): Feature<*>? = when (key) { + | "FeatureA" -> when (name) { + | "OneA" -> FeatureA.OneA + | "OneB" -> FeatureA.OneB + | else -> null + | } + | "FeatureC" -> when (name) { + | "ThreeA" -> FeatureC.ThreeA + | "ThreeB" -> FeatureC.ThreeB + | else -> null + | } + | "io.mehow.FeatureB" -> when (name) { + | "TwoA" -> FeatureB.TwoA + | else -> null + | } + | else -> null + | } + |} + | + """.trimMargin() + } + + test("is optimized when there are no features") { + val model = OptionFactoryModel( + ClassName("io.mehow", "GeneratedOptionFactory"), + features = emptyList(), + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import io.mehow.laboratory.OptionFactory + |import kotlin.String + | + |internal fun OptionFactory.Companion.generated(): OptionFactory = GeneratedOptionFactory + | + |private object GeneratedOptionFactory : OptionFactory { + | override fun create(key: String, name: String): Feature<*>? = null + |} + | + """.trimMargin() + } + + test("suppresses usage of deprecated features") { + val features = listOf( + FeatureFlagModel( + className = ClassName("io.mehow", "FeatureA"), + options = listOf(FeatureFlagOption("OneA", isDefault = true)), + ), + FeatureFlagModel( + className = ClassName("io.mehow", "FeatureB"), + options = listOf(FeatureFlagOption("TwoA", isDefault = true)), + deprecation = Deprecation("message", WARNING), + ), + FeatureFlagModel( + className = ClassName("io.mehow", "FeatureC"), + options = listOf(FeatureFlagOption("ThreeA", isDefault = true)), + deprecation = Deprecation("message", ERROR), + ), + FeatureFlagModel( + className = ClassName("io.mehow", "FeatureD"), + options = listOf(FeatureFlagOption("FourA", isDefault = true)), + deprecation = Deprecation("message", HIDDEN), + ), + ) + val model = OptionFactoryModel( + ClassName("io.mehow", "GeneratedOptionFactory"), + features, + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import io.mehow.laboratory.OptionFactory + |import kotlin.String + |import kotlin.Suppress + | + |internal fun OptionFactory.Companion.generated(): OptionFactory = GeneratedOptionFactory + | + |private object GeneratedOptionFactory : OptionFactory { + | override fun create(key: String, name: String): Feature<*>? = when (key) { + | "io.mehow.FeatureA" -> when (name) { + | "OneA" -> FeatureA.OneA + | else -> null + | } + | "io.mehow.FeatureB" -> @Suppress("DEPRECATION") when (name) { + | "TwoA" -> FeatureB.TwoA + | else -> null + | } + | "io.mehow.FeatureC" -> @Suppress("DEPRECATION_ERROR") when (name) { + | "ThreeA" -> FeatureC.ThreeA + | else -> null + | } + | "io.mehow.FeatureD" -> @Suppress("DEPRECATION_ERROR") when (name) { + | "FourA" -> FeatureD.FourA + | else -> null + | } + | else -> null + | } + |} + | + """.trimMargin() + } + + test("fails to generate features with duplicate keys") { + checkAll( + Arb.stringPattern("[a-z](0)([a-z]{0,10})"), + Arb.stringPattern("[a-z](1)([a-z]{0,10})"), + ) { first, second -> + val features = listOf( + FeatureFlagModel( + className = ClassName("io.mehow1", first), + options = listOf(FeatureFlagOption("First", isDefault = true)), + key = "FeatureA", + ), + FeatureFlagModel( + className = ClassName("io.mehow1", second), + options = listOf(FeatureFlagOption("First", isDefault = true)), + key = "FeatureA", + ), + FeatureFlagModel( + className = ClassName("io.mehow2", "${second}A"), + options = listOf(FeatureFlagOption("First", isDefault = true)), + ), + FeatureFlagModel( + className = ClassName("io.mehow2", "${second}B"), + options = listOf(FeatureFlagOption("First", isDefault = true)), + ), + FeatureFlagModel( + className = ClassName("io.mehow2", "${second}C"), + options = listOf(FeatureFlagOption("First", isDefault = true)), + key = "FeatureB", + ), + FeatureFlagModel( + className = ClassName("io.mehow3", first), + options = listOf(FeatureFlagOption("First", isDefault = true)), + key = "FeatureC", + ), + FeatureFlagModel( + className = ClassName("io.mehow3", second), + options = listOf(FeatureFlagOption("First", isDefault = true)), + key = "FeatureB", + ), + ) + + val exception = shouldThrow { + OptionFactoryModel(ClassName("io.mehow", "GeneratedOptionFactory"), features) + } + + exception shouldHaveMessage """ + |Feature flags must have unique keys. Found following duplicates: + | - FeatureA: [io.mehow1.$first, io.mehow1.$second] + | - FeatureB: [io.mehow2.${second}C, io.mehow3.$second] + """.trimMargin() + } + } + + test("fails to generate features with keys matching fqcn") { + checkAll( + Arb.stringPattern("[a-z](0)([a-z]{0,10})"), + Arb.stringPattern("[a-z](0)([a-z]{0,10})"), + ) { packageName, simpleName -> + val features = listOf( + FeatureFlagModel( + className = ClassName(packageName, simpleName), + options = listOf(FeatureFlagOption("First", isDefault = true)), + ), + FeatureFlagModel( + className = ClassName("io.mehow", "FeatureName"), + options = listOf(FeatureFlagOption("First", isDefault = true)), + key = "$packageName.$simpleName", + ), + ) + + val exception = shouldThrow { + OptionFactoryModel(ClassName("io.mehow", "GeneratedOptionFactory"), features) + } + + exception shouldHaveMessage """ + |Feature flags must have unique keys. Found following duplicates: + | - $packageName.$simpleName: [$packageName.$simpleName, io.mehow.FeatureName] + """.trimMargin() + } + } +}) diff --git a/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/SourcedFeatureStorageModelSpec.kt b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/SourcedFeatureStorageModelSpec.kt new file mode 100644 index 000000000..d36c42fa3 --- /dev/null +++ b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/SourcedFeatureStorageModelSpec.kt @@ -0,0 +1,264 @@ +package io.mehow.laboratory.generator + +import com.squareup.kotlinpoet.ClassName +import io.kotest.core.spec.style.FunSpec +import io.mehow.laboratory.generator.Visibility.Internal +import io.mehow.laboratory.generator.Visibility.Public +import io.mehow.laboratory.generator.test.shouldSpecify +import java.util.Locale + +class SourcedFeatureStorageModelSpec : FunSpec({ + test("can be internal") { + val model = SourcedFeatureStorageModel( + ClassName("io.mehow", "SourcedGeneratedFeatureStorage"), + sourceNames = listOf("Firebase", "S3"), + visibility = Internal, + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.FeatureStorage + |import io.mehow.laboratory.FeatureStorage.Companion.sourced + |import kotlin.String + |import kotlin.collections.Map + |import kotlin.collections.emptyMap + |import kotlin.collections.plus + |import kotlin.to + | + |internal fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage): FirebaseStep = + | Builder(localSource, emptyMap()) + | + |internal interface FirebaseStep { + | public fun firebaseSource(source: FeatureStorage): S3Step + |} + | + |internal interface S3Step { + | public fun s3Source(source: FeatureStorage): BuildingStep + |} + | + |internal interface BuildingStep { + | public fun build(): FeatureStorage + |} + | + |private data class Builder( + | private val localSource: FeatureStorage, + | private val remoteSources: Map, + |) : FirebaseStep, S3Step, BuildingStep { + | override fun firebaseSource(source: FeatureStorage): S3Step = copy( + | remoteSources = remoteSources + ("Firebase" to source) + | ) + | + | override fun s3Source(source: FeatureStorage): BuildingStep = copy( + | remoteSources = remoteSources + ("S3" to source) + | ) + | + | override fun build(): FeatureStorage = sourced(localSource, remoteSources) + |} + | + """.trimMargin() + } + + test("can be public") { + val model = SourcedFeatureStorageModel( + ClassName("io.mehow", "SourcedGeneratedFeatureStorage"), + sourceNames = listOf("Firebase", "S3"), + visibility = Public, + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.FeatureStorage + |import io.mehow.laboratory.FeatureStorage.Companion.sourced + |import kotlin.String + |import kotlin.collections.Map + |import kotlin.collections.emptyMap + |import kotlin.collections.plus + |import kotlin.to + | + |public fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage): FirebaseStep = + | Builder(localSource, emptyMap()) + | + |public interface FirebaseStep { + | public fun firebaseSource(source: FeatureStorage): S3Step + |} + | + |public interface S3Step { + | public fun s3Source(source: FeatureStorage): BuildingStep + |} + | + |public interface BuildingStep { + | public fun build(): FeatureStorage + |} + | + |private data class Builder( + | private val localSource: FeatureStorage, + | private val remoteSources: Map, + |) : FirebaseStep, S3Step, BuildingStep { + | override fun firebaseSource(source: FeatureStorage): S3Step = copy( + | remoteSources = remoteSources + ("Firebase" to source) + | ) + | + | override fun s3Source(source: FeatureStorage): BuildingStep = copy( + | remoteSources = remoteSources + ("S3" to source) + | ) + | + | override fun build(): FeatureStorage = sourced(localSource, remoteSources) + |} + | + """.trimMargin() + } + + test("ignores duplicate sources") { + val model = SourcedFeatureStorageModel( + ClassName("io.mehow", "SourcedGeneratedFeatureStorage"), + sourceNames = listOf("Foo", "Bar", "Baz", "Foo", "Baz", "Foo"), + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.FeatureStorage + |import io.mehow.laboratory.FeatureStorage.Companion.sourced + |import kotlin.String + |import kotlin.collections.Map + |import kotlin.collections.emptyMap + |import kotlin.collections.plus + |import kotlin.to + | + |internal fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage): BarStep = + | Builder(localSource, emptyMap()) + | + |internal interface BarStep { + | public fun barSource(source: FeatureStorage): BazStep + |} + | + |internal interface BazStep { + | public fun bazSource(source: FeatureStorage): FooStep + |} + | + |internal interface FooStep { + | public fun fooSource(source: FeatureStorage): BuildingStep + |} + | + |internal interface BuildingStep { + | public fun build(): FeatureStorage + |} + | + |private data class Builder( + | private val localSource: FeatureStorage, + | private val remoteSources: Map, + |) : BarStep, BazStep, FooStep, BuildingStep { + | override fun barSource(source: FeatureStorage): BazStep = copy( + | remoteSources = remoteSources + ("Bar" to source) + | ) + | + | override fun bazSource(source: FeatureStorage): FooStep = copy( + | remoteSources = remoteSources + ("Baz" to source) + | ) + | + | override fun fooSource(source: FeatureStorage): BuildingStep = copy( + | remoteSources = remoteSources + ("Foo" to source) + | ) + | + | override fun build(): FeatureStorage = sourced(localSource, remoteSources) + |} + | + """.trimMargin() + } + + test("ignores local source") { + val localPermutations = (0b00000..0b11111).map { + listOf(it and 0b00001, it and 0b00010, it and 0b00100, it and 0b01000, it and 0b10000) + .map { mask -> mask != 0 } + .mapIndexed { index, mask -> + val chars = "local"[index].toString() + if (mask) chars else chars.replaceFirstChar { char -> char.titlecase(Locale.ROOT) } + }.joinToString(separator = "") + } + val model = SourcedFeatureStorageModel( + ClassName("io.mehow", "SourcedGeneratedFeatureStorage"), + sourceNames = localPermutations + "Foo", + visibility = Public, + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.FeatureStorage + |import io.mehow.laboratory.FeatureStorage.Companion.sourced + |import kotlin.String + |import kotlin.collections.Map + |import kotlin.collections.emptyMap + |import kotlin.collections.plus + |import kotlin.to + | + |public fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage): FooStep = + | Builder(localSource, emptyMap()) + | + |public interface FooStep { + | public fun fooSource(source: FeatureStorage): BuildingStep + |} + | + |public interface BuildingStep { + | public fun build(): FeatureStorage + |} + | + |private data class Builder( + | private val localSource: FeatureStorage, + | private val remoteSources: Map, + |) : FooStep, BuildingStep { + | override fun fooSource(source: FeatureStorage): BuildingStep = copy( + | remoteSources = remoteSources + ("Foo" to source) + | ) + | + | override fun build(): FeatureStorage = sourced(localSource, remoteSources) + |} + | + """.trimMargin() + } + + test("can have no external sources") { + val model = SourcedFeatureStorageModel( + ClassName("io.mehow", "SourcedGeneratedFeatureStorage"), + sourceNames = emptyList(), + visibility = Public, + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.FeatureStorage + |import io.mehow.laboratory.FeatureStorage.Companion.sourced + |import kotlin.String + |import kotlin.collections.Map + |import kotlin.collections.emptyMap + | + |public fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage): BuildingStep = + | Builder(localSource, emptyMap()) + | + |public interface BuildingStep { + | public fun build(): FeatureStorage + |} + | + |private data class Builder( + | private val localSource: FeatureStorage, + | private val remoteSources: Map, + |) : BuildingStep { + | override fun build(): FeatureStorage = sourced(localSource, remoteSources) + |} + | + """.trimMargin() + } +}) diff --git a/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/SupervisorSpec.kt b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/SupervisorSpec.kt new file mode 100644 index 000000000..0132e0ef8 --- /dev/null +++ b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/SupervisorSpec.kt @@ -0,0 +1,42 @@ +package io.mehow.laboratory.generator + +import com.squareup.kotlinpoet.ClassName +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.throwable.shouldHaveMessage +import io.kotest.property.Arb +import io.kotest.property.arbitrary.stringPattern +import io.kotest.property.checkAll + +class SupervisorSpec : FunSpec({ + test("does not fail when it has an option") { + checkAll(Arb.stringPattern("[a-z](0)([a-z]{0,10})")) { optionName -> + val option = FeatureFlagOption(optionName, isDefault = true) + val feature = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(option), + ) + + shouldNotThrowAny { + Supervisor(feature, option) + } + } + } + + test("fails when it has no options") { + checkAll(Arb.stringPattern("[a-z](0)([a-z]{0,10})")) { optionName -> + val feature = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + ) + val option = FeatureFlagOption(optionName, isDefault = true) + + val exception = shouldThrow { + Supervisor(feature, option) + } + + exception shouldHaveMessage "Feature flag io.mehow.FeatureA does not contain option $optionName" + } + } +}) diff --git a/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/test/KotlinPoetMatchers.kt b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/test/KotlinPoetMatchers.kt new file mode 100644 index 000000000..422b0e664 --- /dev/null +++ b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/test/KotlinPoetMatchers.kt @@ -0,0 +1,17 @@ +package io.mehow.laboratory.generator.test + +import com.squareup.kotlinpoet.FileSpec +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.withClue +import io.kotest.matchers.shouldBe + +infix fun FileSpec.shouldSpecify(value: String) = assertSoftly { + val actualLines = toString().split("\n") + val expectedLines = value.split("\n") + val maxSize = maxOf(actualLines.size, expectedLines.size) + repeat(maxSize) { line -> + withClue("Line $line does not match") { + actualLines.getOrNull(line) shouldBe expectedLines.getOrNull(line) + } + } +} diff --git a/library/gradle-plugin/api/gradle-plugin.api b/laboratory/gradle-plugin/api/gradle-plugin.api similarity index 98% rename from library/gradle-plugin/api/gradle-plugin.api rename to laboratory/gradle-plugin/api/gradle-plugin.api index 706a2a4ee..2c87b7e97 100644 --- a/library/gradle-plugin/api/gradle-plugin.api +++ b/laboratory/gradle-plugin/api/gradle-plugin.api @@ -6,6 +6,7 @@ public final class io/mehow/laboratory/gradle/DeprecationLevel : java/lang/Enum public static final field Error Lio/mehow/laboratory/gradle/DeprecationLevel; public static final field Hidden Lio/mehow/laboratory/gradle/DeprecationLevel; public static final field Warning Lio/mehow/laboratory/gradle/DeprecationLevel; + public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lio/mehow/laboratory/gradle/DeprecationLevel; public static fun values ()[Lio/mehow/laboratory/gradle/DeprecationLevel; } diff --git a/laboratory/gradle-plugin/build.gradle.kts b/laboratory/gradle-plugin/build.gradle.kts new file mode 100644 index 000000000..738d6636f --- /dev/null +++ b/laboratory/gradle-plugin/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + `java-gradle-plugin` + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.mavenPublish) + alias(libs.plugins.spotless) + alias(libs.plugins.detekt) + alias(libs.plugins.dokka) + alias(libs.plugins.buildconfig) +} + +val pluginName = "laboratory" + +gradlePlugin { + plugins { + create(pluginName) { + id = "io.mehow.laboratory" + implementationClass = "io.mehow.laboratory.gradle.LaboratoryPlugin" + } + } +} + +buildConfig { + useKotlinOutput { + internalVisibility = true + topLevelConstants = true + } + packageName("io.mehow.laboratory.gradle") + buildConfigField("String", "LibraryVersion", "\"${project.version}\"") + buildConfigField("String", "PluginName", "\"${pluginName}\"") +} + +tasks.withType().configureEach { + useJUnitPlatform() +} + +val fixtureClasspath: Configuration by configurations.creating + +tasks.withType().configureEach { + pluginClasspath.from(fixtureClasspath) +} + +dependencies { + compileOnly(libs.android.gradlePlugin) + + implementation(projects.laboratory.generator) + implementation(libs.kotlin.gradlePlugin) + + testImplementation(libs.kotest.runner.junit5) + testImplementation(libs.kotest.assertions) + + fixtureClasspath(libs.android.gradlePlugin) +} diff --git a/library/gradle-plugin/gradle.properties b/laboratory/gradle-plugin/gradle.properties similarity index 62% rename from library/gradle-plugin/gradle.properties rename to laboratory/gradle-plugin/gradle.properties index ef823004c..3f7284596 100644 --- a/library/gradle-plugin/gradle.properties +++ b/laboratory/gradle-plugin/gradle.properties @@ -1,3 +1,3 @@ POM_ARTIFACT_ID=laboratory-gradle-plugin -POM_NAME=Laboratory (Gradle plugin) +POM_NAME=Laboratory (Gradle Plugin) POM_PACKAGING=jar diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/ChildFeatureFlagsInput.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/ChildFeatureFlagsInput.kt similarity index 90% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/ChildFeatureFlagsInput.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/ChildFeatureFlagsInput.kt index 1c06635a6..b3d5f31dc 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/ChildFeatureFlagsInput.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/ChildFeatureFlagsInput.kt @@ -17,7 +17,10 @@ public class ChildFeatureFlagsInput internal constructor( /** * Generates a new supervised feature flag. */ - public fun feature(name: String, action: Action) { + public fun feature( + name: String, + action: Action, + ) { mutableFeatureInputs += FeatureFlagInput(name, packageNameProvider, supervisor).let { input -> action.execute(input) return@let input diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/DeprecationLevel.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/DeprecationLevel.kt similarity index 100% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/DeprecationLevel.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/DeprecationLevel.kt diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFactoryInput.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFactoryInput.kt similarity index 73% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFactoryInput.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFactoryInput.kt index d5b737b06..71fd215ff 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFactoryInput.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFactoryInput.kt @@ -22,9 +22,12 @@ public class FeatureFactoryInput internal constructor( */ public var packageName: String? = null - internal fun toModel(features: List, simpleName: String) = FeatureFactoryModel( - visibility = if (isPublic) Public else Internal, - className = ClassName(packageName ?: packageNameProvider(), simpleName), - features = features, + internal fun toModel( + features: List, + simpleName: String, + ) = FeatureFactoryModel( + visibility = if (isPublic) Public else Internal, + className = ClassName(packageName ?: packageNameProvider(), simpleName), + features = features, ) } diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFactoryTask.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFactoryTask.kt similarity index 99% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFactoryTask.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFactoryTask.kt index e659fb87b..828b2e374 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFactoryTask.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFactoryTask.kt @@ -8,10 +8,15 @@ import java.io.File public open class FeatureFactoryTask : DefaultTask() { @get:Internal internal lateinit var factory: FeatureFactoryInput + @get:Internal internal lateinit var features: List + @get:Internal internal lateinit var codeGenDir: File + @get:Internal internal lateinit var factoryClassName: String + @get:Internal internal lateinit var factoryFunctionName: String + @get:Internal internal lateinit var featureModelsMapper: (List) -> List @TaskAction public fun generateFeatureFactory() { diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFlagInput.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFlagInput.kt similarity index 82% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFlagInput.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFlagInput.kt index 74a56124f..2c9ec687a 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFlagInput.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFlagInput.kt @@ -48,7 +48,10 @@ public class FeatureFlagInput internal constructor( /** * Adds a feature option and configures features flags supervised by it. */ - public fun withOption(name: String, action: Action): Unit = + public fun withOption( + name: String, + action: Action, + ): Unit = withOption(name, isDefault = false, action) /** @@ -61,12 +64,19 @@ public class FeatureFlagInput internal constructor( * Adds a feature value that will be used as a default value and configures features flags supervised by it. * Exactly one value must be set with this method. */ - public fun withDefaultOption(name: String, action: Action): Unit = + public fun withDefaultOption( + name: String, + action: Action, + ): Unit = withOption(name, isDefault = true, action) private val childFeatureInputs = mutableListOf() - private fun withOption(name: String, isDefault: Boolean, action: Action) { + private fun withOption( + name: String, + isDefault: Boolean, + action: Action, + ) { val option = FeatureFlagOption(name, isDefault) options += option val packageNameProvider = { packageName ?: packageNameProvider() } @@ -101,19 +111,22 @@ public class FeatureFlagInput internal constructor( /** * Annotates a feature flag as deprecated. */ - @JvmOverloads public fun deprecated(message: String, level: DeprecationLevel = Warning) { + @JvmOverloads public fun deprecated( + message: String, + level: DeprecationLevel = Warning, + ) { deprecation = Deprecation(message, level.kotlinLevel) } private fun toModel() = FeatureFlagModel( - visibility = if (isPublic) Public else Internal, - className = ClassName(packageName ?: packageNameProvider(), name), - options = options, - sourceOptions = sources, - key = key, - description = description.orEmpty(), - deprecation = deprecation, - supervisor = supervisor?.invoke(), + visibility = if (isPublic) Public else Internal, + className = ClassName(packageName ?: packageNameProvider(), name), + options = options, + sourceOptions = sources, + key = key, + description = description.orEmpty(), + deprecation = deprecation, + supervisor = supervisor?.invoke(), ) internal fun toModels(): List = diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFlagsTask.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFlagsTask.kt similarity index 99% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFlagsTask.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFlagsTask.kt index 2ca95d8fd..75f0954c0 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFlagsTask.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFlagsTask.kt @@ -7,6 +7,7 @@ import java.io.File public open class FeatureFlagsTask : DefaultTask() { @get:Internal internal lateinit var features: List + @get:Internal internal lateinit var codeGenDir: File @TaskAction public fun generateFeatureFlags() { diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/LaboratoryExtension.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/LaboratoryExtension.kt similarity index 97% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/LaboratoryExtension.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/LaboratoryExtension.kt index a1951d3d6..69a782807 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/LaboratoryExtension.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/LaboratoryExtension.kt @@ -6,7 +6,6 @@ import org.gradle.api.Project /** * An entry point for configuration of feature flags code generation. */ -@Suppress("UnnecessaryAbstractClass") // Created by Gradle public abstract class LaboratoryExtension { /** * Sets package name for any factories or feature flags defined in this extension. @@ -33,7 +32,10 @@ public abstract class LaboratoryExtension { /** * Generates a new feature in this module. */ - public fun feature(name: String, action: Action) { + public fun feature( + name: String, + action: Action, + ) { mutableFeatureInputs += FeatureFlagInput(name, packageNameProvider).let { input -> action.execute(input) return@let input diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/LaboratoryPlugin.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/LaboratoryPlugin.kt similarity index 84% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/LaboratoryPlugin.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/LaboratoryPlugin.kt index c266dbfd3..7f5720c52 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/LaboratoryPlugin.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/LaboratoryPlugin.kt @@ -1,7 +1,6 @@ package io.mehow.laboratory.gradle import io.mehow.laboratory.generator.FeatureFlagModel -import io.mehow.laboratory.laboratoryVersion import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.Task @@ -9,15 +8,13 @@ import org.gradle.api.tasks.TaskProvider import java.io.File import java.util.concurrent.atomic.AtomicBoolean -private const val pluginName = "laboratory" - public class LaboratoryPlugin : Plugin { private val hasAndroid = AtomicBoolean(false) private val hasKotlin = AtomicBoolean(false) private lateinit var extension: LaboratoryExtension override fun apply(project: Project) { - extension = project.extensions.create(pluginName, LaboratoryExtension::class.java).apply { + extension = project.extensions.create(PluginName, LaboratoryExtension::class.java).apply { this.project = project } project.setUpKotlinProject() @@ -64,9 +61,9 @@ public class LaboratoryPlugin : Plugin { } private fun Project.registerFeaturesTask() = afterEvaluate { - val codeGenDir = File("$buildDir/generated/laboratory/code/feature-flags") + val codeGenDir = File("${layout.buildDirectory.get()}/generated/laboratory/code/feature-flags") val featuresTask = registerTask("generateFeatureFlags") { task -> - task.group = pluginName + task.group = PluginName task.description = "Generate Laboratory features." task.features = extension.featureInputs task.codeGenDir = codeGenDir @@ -77,9 +74,9 @@ public class LaboratoryPlugin : Plugin { private fun Project.registerFeatureFactoryTask() = afterEvaluate { val factoryInput = extension.factoryInput ?: return@afterEvaluate - val codeGenDir = File("${project.buildDir}/generated/laboratory/code/feature-factory") + val codeGenDir = File("${layout.buildDirectory.get()}/generated/laboratory/code/feature-factory") val factoryTask = registerTask("generateFeatureFactory") { task -> - task.group = pluginName + task.group = PluginName task.description = "Generate Laboratory feature factory." task.factory = factoryInput task.features = extension.factoryFeatureInputs @@ -94,9 +91,9 @@ public class LaboratoryPlugin : Plugin { private fun Project.registerSourcedFeatureStorageTask() = afterEvaluate { val storageInput = extension.storageInput ?: return@afterEvaluate - val codeGenDir = File("${project.buildDir}/generated/laboratory/code/sourced-storage") + val codeGenDir = File("${layout.buildDirectory.get()}/generated/laboratory/code/sourced-storage") val storageTask = registerTask("generateSourcedFeatureStorage") { task -> - task.group = pluginName + task.group = PluginName task.description = "Generate Laboratory sourced feature storage." task.storage = storageInput task.features = extension.factoryFeatureInputs @@ -108,9 +105,9 @@ public class LaboratoryPlugin : Plugin { private fun Project.registerFeatureSourcesFactoryTask() = afterEvaluate { val factoryInput = extension.featureSourcesFactory ?: return@afterEvaluate - val codeGenDir = File("${project.buildDir}/generated/laboratory/code/feature-source-factory") + val codeGenDir = File("${layout.buildDirectory.get()}/generated/laboratory/code/feature-source-factory") val factoryTask = registerTask("generateFeatureSourceFactory") { task -> - task.group = pluginName + task.group = PluginName task.description = "Generate Laboratory feature sources factory." task.factory = factoryInput task.features = extension.factoryFeatureInputs @@ -125,9 +122,9 @@ public class LaboratoryPlugin : Plugin { private fun Project.registerOptionFactoryTask() = afterEvaluate { val factoryInput = extension.optionFactoryInput ?: return@afterEvaluate - val codeGenDir = File("${project.buildDir}/generated/laboratory/code/option-factory") + val codeGenDir = File("${layout.buildDirectory.get()}/generated/laboratory/code/option-factory") val factoryTask = registerTask("generateOptionFactory") { task -> - task.group = pluginName + task.group = PluginName task.description = "Generate Laboratory option factory." task.factory = factoryInput task.features = extension.factoryFeatureInputs @@ -137,7 +134,7 @@ public class LaboratoryPlugin : Plugin { } private fun Project.addLaboratoryDependency() { - dependencies.add("api", "io.mehow.laboratory:laboratory:$laboratoryVersion") + dependencies.add("api", "io.mehow.laboratory:laboratory:$LibraryVersion") } private inline fun Project.registerTask( @@ -147,7 +144,10 @@ public class LaboratoryPlugin : Plugin { return tasks.register(name, T::class.java) { action(it) } } - private fun Project.addSourceSets(task: TaskProvider, dir: File) { + private fun Project.addSourceSets( + task: TaskProvider, + dir: File, + ) { if (hasAndroid.get()) { task.contributeToAndroidSourceSets(dir, this) } else { diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/OptionFactoryInput.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/OptionFactoryInput.kt similarity index 83% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/OptionFactoryInput.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/OptionFactoryInput.kt index 3366aedb1..551d9ff10 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/OptionFactoryInput.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/OptionFactoryInput.kt @@ -23,8 +23,8 @@ public class OptionFactoryInput internal constructor( public var packageName: String? = null internal fun toModel(features: List) = OptionFactoryModel( - visibility = if (isPublic) Public else Internal, - className = ClassName(packageName ?: packageNameProvider(), "GeneratedOptionFactory"), - features = features, + visibility = if (isPublic) Public else Internal, + className = ClassName(packageName ?: packageNameProvider(), "GeneratedOptionFactory"), + features = features, ) } diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/OptionFactoryTask.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/OptionFactoryTask.kt similarity index 99% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/OptionFactoryTask.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/OptionFactoryTask.kt index 0c5ca9783..3faacb353 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/OptionFactoryTask.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/OptionFactoryTask.kt @@ -7,7 +7,9 @@ import java.io.File public open class OptionFactoryTask : DefaultTask() { @get:Internal internal lateinit var factory: OptionFactoryInput + @get:Internal internal lateinit var features: List + @get:Internal internal lateinit var codeGenDir: File @TaskAction public fun generateSourcedFeatureStorage() { diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/PackageNameProvider.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/PackageNameProvider.kt similarity index 100% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/PackageNameProvider.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/PackageNameProvider.kt diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/SourceSetContribution.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/SourceSetContribution.kt similarity index 84% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/SourceSetContribution.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/SourceSetContribution.kt index 557a0efd1..55d1e39ac 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/SourceSetContribution.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/SourceSetContribution.kt @@ -3,7 +3,6 @@ package io.mehow.laboratory.gradle import com.android.build.gradle.AppExtension import com.android.build.gradle.BaseExtension import com.android.build.gradle.LibraryExtension -import com.android.build.gradle.api.BaseVariant import org.gradle.api.DomainObjectSet import org.gradle.api.GradleException import org.gradle.api.Project @@ -14,12 +13,18 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.io.File import java.util.Locale -internal fun TaskProvider.contributeToSourceSets(dir: File, project: Project) { +internal fun TaskProvider.contributeToSourceSets( + dir: File, + project: Project, +) { makeKotlinDependOnTask(project) contributeToKotlin(dir, project) } -internal fun TaskProvider.contributeToAndroidSourceSets(dir: File, project: Project) { +internal fun TaskProvider.contributeToAndroidSourceSets( + dir: File, + project: Project, +) { makeKotlinDependOnTask(project) contributeToAndroid(dir, project) } @@ -30,13 +35,19 @@ private fun TaskProvider.makeKotlinDependOnTask(project: Project) { } } -private fun contributeToKotlin(dir: File, project: Project) { +private fun contributeToKotlin( + dir: File, + project: Project, +) { val sourceSets = project.extensions.getByType(KotlinProjectExtension::class.java).sourceSets val kotlinSourceSet = sourceSets.getByName("main").kotlin kotlinSourceSet.srcDir(dir) } -private fun TaskProvider.contributeToAndroid(dir: File, project: Project) { +private fun TaskProvider.contributeToAndroid( + dir: File, + project: Project, +) { val extension = requireNotNull(project.extensions.findByType(BaseExtension::class.java)) { "Did not find BaseExtension in Android project" } @@ -62,7 +73,8 @@ private fun TaskProvider.contributeToAndroid(dir: File, project: Proje } // Copied from SQLDelight with small modifications. -private val BaseExtension.variants: DomainObjectSet +@Suppress("DEPRECATION") +private val BaseExtension.variants: DomainObjectSet get() = when (this) { is AppExtension -> applicationVariants is LibraryExtension -> libraryVariants diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/SourcedFeatureStorageInput.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/SourcedFeatureStorageInput.kt similarity index 81% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/SourcedFeatureStorageInput.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/SourcedFeatureStorageInput.kt index 54937529a..d14ae5ac8 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/SourcedFeatureStorageInput.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/SourcedFeatureStorageInput.kt @@ -22,8 +22,8 @@ public class SourcedFeatureStorageInput internal constructor( public var packageName: String? = null internal fun toModel(sourceNames: List) = SourcedFeatureStorageModel( - visibility = if (isPublic) Public else Internal, - className = ClassName(packageName ?: packageNameProvider(), "SourcedGeneratedFeatureStorage"), - sourceNames = sourceNames, + visibility = if (isPublic) Public else Internal, + className = ClassName(packageName ?: packageNameProvider(), "SourcedGeneratedFeatureStorage"), + sourceNames = sourceNames, ) } diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/SourcedFeatureStorageTask.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/SourcedFeatureStorageTask.kt similarity index 89% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/SourcedFeatureStorageTask.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/SourcedFeatureStorageTask.kt index 42df858c8..fdcc1880a 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/SourcedFeatureStorageTask.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/SourcedFeatureStorageTask.kt @@ -9,7 +9,9 @@ import java.io.File public open class SourcedFeatureStorageTask : DefaultTask() { @get:Internal internal lateinit var storage: SourcedFeatureStorageInput + @get:Internal internal lateinit var features: List + @get:Internal internal lateinit var codeGenDir: File @TaskAction public fun generateSourcedFeatureStorage() { @@ -20,7 +22,7 @@ public open class SourcedFeatureStorageTask : DefaultTask() { } private fun List.sourceNames(): List = mapNotNull(FeatureFlagModel::source) - .map(FeatureFlagModel::options) - .flatMap { it.toList() } - .map(FeatureFlagOption::name) + .map(FeatureFlagModel::options) + .flatMap { it.toList() } + .map(FeatureFlagOption::name) } diff --git a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateFeatureFactoryTaskSpec.kt b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateFeatureFactoryTaskSpec.kt similarity index 89% rename from library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateFeatureFactoryTaskSpec.kt rename to laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateFeatureFactoryTaskSpec.kt index a885f85c0..e6a50b3bf 100644 --- a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateFeatureFactoryTaskSpec.kt +++ b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateFeatureFactoryTaskSpec.kt @@ -1,6 +1,6 @@ package io.mehow.laboratory.gradle -import io.kotest.core.spec.style.StringSpec +import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.file.shouldExist import io.kotest.matchers.file.shouldNotExist import io.kotest.matchers.shouldBe @@ -9,18 +9,18 @@ import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome.FAILED import org.gradle.testkit.runner.TaskOutcome.SUCCESS -internal class GenerateFeatureFactoryTaskSpec : StringSpec({ +class GenerateFeatureFactoryTaskSpec : FunSpec({ lateinit var gradleRunner: GradleRunner cleanBuildDirs() beforeTest { gradleRunner = GradleRunner.create() - .withPluginClasspath() - .withArguments("generateFeatureFactory", "--stacktrace") + .withPluginClasspath() + .withArguments("generateFeatureFactory", "--stacktrace") } - "generates factory without any feature flags" { + test("generates factory without any feature flags") { val fixture = "factory-generate-empty".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -39,7 +39,7 @@ internal class GenerateFeatureFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory with feature flags" { + test("generates factory with feature flags") { val fixture = "factory-generate-features".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -62,7 +62,7 @@ internal class GenerateFeatureFactoryTaskSpec : StringSpec({ """.trimMargin() } - "uses implicit package name" { + test("uses implicit package name") { val fixture = "factory-package-name-implicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -73,7 +73,7 @@ internal class GenerateFeatureFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.implicit" } - "uses explicit package name" { + test("uses explicit package name") { val fixture = "factory-package-name-explicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -84,7 +84,7 @@ internal class GenerateFeatureFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.explicit" } - "overrides implicit package name" { + test("overrides implicit package name") { val fixture = "factory-package-name-explicit-override".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -95,7 +95,7 @@ internal class GenerateFeatureFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.explicit" } - "generates internal factory" { + test("generates internal factory") { val fixture = "factory-generate-internal".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -106,7 +106,7 @@ internal class GenerateFeatureFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "internal fun FeatureFactory.Companion.featureGenerated()" } - "generates public factory" { + test("generates public factory") { val fixture = "factory-generate-public".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -117,7 +117,7 @@ internal class GenerateFeatureFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "public fun FeatureFactory.Companion.featureGenerated()" } - "fails for feature flags with no options" { + test("fails for feature flags with no options") { val fixture = "factory-feature-flag-option-missing".toFixture() val result = gradleRunner.withProjectDir(fixture).buildAndFail() @@ -128,7 +128,7 @@ internal class GenerateFeatureFactoryTaskSpec : StringSpec({ fixture.featureFactoryFile("GeneratedFeatureFactory").shouldNotExist() } - "generates factory with feature flags from all modules" { + test("generates factory with feature flags from all modules") { val fixture = "factory-multi-module-generate-all".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -152,7 +152,7 @@ internal class GenerateFeatureFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory with feature flags only from included modules" { + test("generates factory with feature flags only from included modules") { val fixture = "factory-multi-module-generate-filtered".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -175,7 +175,7 @@ internal class GenerateFeatureFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory for Android project" { + test("generates factory for Android project") { val fixture = "factory-android-smoke".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -194,7 +194,7 @@ internal class GenerateFeatureFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory with supervised feature flags" { + test("generates factory with supervised feature flags") { val fixture = "factory-generate-supervised-feature-flags".toFixture() val result = gradleRunner.withProjectDir(fixture).build() diff --git a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateFeatureFlagsTaskSpec.kt b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateFeatureFlagsTaskSpec.kt similarity index 90% rename from library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateFeatureFlagsTaskSpec.kt rename to laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateFeatureFlagsTaskSpec.kt index 722ca931d..69fd288de 100644 --- a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateFeatureFlagsTaskSpec.kt +++ b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateFeatureFlagsTaskSpec.kt @@ -1,6 +1,6 @@ package io.mehow.laboratory.gradle -import io.kotest.core.spec.style.StringSpec +import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.file.shouldExist import io.kotest.matchers.file.shouldNotExist import io.kotest.matchers.shouldBe @@ -9,18 +9,18 @@ import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome.FAILED import org.gradle.testkit.runner.TaskOutcome.SUCCESS -internal class GenerateFeatureFlagsTaskSpec : StringSpec({ +class GenerateFeatureFlagsTaskSpec : FunSpec({ lateinit var gradleRunner: GradleRunner cleanBuildDirs() beforeTest { gradleRunner = GradleRunner.create() - .withPluginClasspath() - .withArguments("generateFeatureFlags", "--stacktrace") + .withPluginClasspath() + .withArguments("generateFeatureFlags", "--stacktrace") } - "generates single feature flag" { + test("generates single feature flag") { val fixture = "feature-flag-generate-single".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -42,7 +42,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "generates multiple feature flags" { + test("generates multiple feature flags") { val fixture = "feature-flag-generate-multiple".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -78,7 +78,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "generates a single feature flag with source" { + test("generates a single feature flag with source") { val fixture = "feature-flag-generate-sources-single".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -112,7 +112,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "generates an internal feature flag with source" { + test("generates an internal feature flag with source") { val fixture = "feature-flag-generate-sources-internal".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -146,7 +146,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "generates a public feature flag with source" { + test("generates a public feature flag with source") { val fixture = "feature-flag-generate-sources-public".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -180,7 +180,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "generates multiple feature flags with sources" { + test("generates multiple feature flags with sources") { val fixture = "feature-flag-generate-sources-multiple".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -254,7 +254,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "uses implicit package name" { + test("uses implicit package name") { val fixture = "feature-flag-package-name-implicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -265,7 +265,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ feature.readText() shouldContain "package io.mehow.implicit" } - "cascades implicit package name" { + test("cascades implicit package name") { val fixture = "feature-flag-package-name-implicit-cascading".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -277,7 +277,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ featureB.shouldExist() } - "uses last implicit package name for all features" { + test("uses last implicit package name for all features") { val fixture = "feature-flag-package-name-implicit-switching".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -293,7 +293,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ featureB.readText() shouldContain "package io.mehow.implicit.switch" } - "uses explicit package name" { + test("uses explicit package name") { val fixture = "feature-flag-package-name-explicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -304,7 +304,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ feature.readText() shouldContain "package io.mehow.explicit" } - "switches explicit package name" { + test("switches explicit package name") { val fixture = "feature-flag-package-name-explicit-switching".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -320,7 +320,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ featureB.readText() shouldContain "package io.mehow.explicit.switch" } - "overrides implicit package name" { + test("overrides implicit package name") { val fixture = "feature-flag-package-name-explicit-override".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -331,7 +331,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ feature.readText() shouldContain "package io.mehow.explicit" } - "generates internal feature flag" { + test("generates internal feature flag") { val fixture = "feature-flag-generate-internal".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -342,7 +342,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ feature.readText() shouldContain "internal enum class Feature" } - "generates public feature flag" { + test("generates public feature flag") { val fixture = "feature-flag-generate-public".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -353,7 +353,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ feature.readText() shouldContain "public enum class Feature" } - "generates features with the same options but different names" { + test("generates features with the same options but different names") { val fixture = "feature-flag-generate-option-name-common".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -387,7 +387,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "fails for features with no options" { + test("fails for features with no options") { val fixture = "feature-flag-option-missing".toFixture() val result = gradleRunner.withProjectDir(fixture).buildAndFail() @@ -399,7 +399,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ feature.shouldNotExist() } - "generates feature flag for Android project" { + test("generates feature flag for Android project") { val fixture = "feature-flag-android-smoke".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -420,7 +420,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "generates feature flag with a description" { + test("generates feature flag with a description") { val fixture = "feature-flag-generate-description".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -446,7 +446,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "generates deprecated feature flag" { + test("generates deprecated feature flag") { val fixture = "feature-flag-generate-deprecated".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -465,7 +465,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "generates deprecated feature flag with specified deprecation level" { + test("generates deprecated feature flag with specified deprecation level") { val fixture = "feature-flag-generate-deprecated-with-level".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -484,7 +484,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "generates supervised child feature flag" { + test("generates supervised child feature flag") { val fixture = "feature-flag-supervisor-generate-child".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -507,7 +507,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "generates supervised grandchild feature flag" { + test("generates supervised grandchild feature flag") { val fixture = "feature-flag-supervisor-generate-grandchild".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -530,7 +530,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "generates supervised multiple children feature flags" { + test("generates supervised multiple children feature flags") { val fixture = "feature-flag-supervisor-generate-multiple-children".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -568,7 +568,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "supervised feature flag uses explicit package name" { + test("supervised feature flag uses explicit package name") { val fixture = "feature-flag-supervisor-package-name-explicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -579,7 +579,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ feature.readText() shouldContain "package io.mehow.explicit" } - "supervised feature flag uses implicit package name" { + test("supervised feature flag uses implicit package name") { val fixture = "feature-flag-supervisor-package-name-explicit-override".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -590,7 +590,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ feature.readText() shouldContain "package io.mehow.explicit" } - "supervised feature flag overrides implicit package name" { + test("supervised feature flag overrides implicit package name") { val fixture = "feature-flag-supervisor-package-name-implicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -601,7 +601,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ feature.readText() shouldContain "package io.mehow.implicit" } - "fails for feature supervising itself" { + test("fails for feature supervising itself") { val fixture = "feature-flag-supervisor-self-supervision".toFixture() val result = gradleRunner.withProjectDir(fixture).buildAndFail() diff --git a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateFeatureSourceFactoryTaskSpec.kt b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateFeatureSourceFactoryTaskSpec.kt similarity index 90% rename from library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateFeatureSourceFactoryTaskSpec.kt rename to laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateFeatureSourceFactoryTaskSpec.kt index a2a3c0c72..a1227afd9 100644 --- a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateFeatureSourceFactoryTaskSpec.kt +++ b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateFeatureSourceFactoryTaskSpec.kt @@ -1,6 +1,6 @@ package io.mehow.laboratory.gradle -import io.kotest.core.spec.style.StringSpec +import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.file.shouldExist import io.kotest.matchers.file.shouldNotExist import io.kotest.matchers.shouldBe @@ -9,18 +9,18 @@ import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome.FAILED import org.gradle.testkit.runner.TaskOutcome.SUCCESS -internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ +class GenerateFeatureSourceFactoryTaskSpec : FunSpec({ lateinit var gradleRunner: GradleRunner cleanBuildDirs() beforeTest { gradleRunner = GradleRunner.create() - .withPluginClasspath() - .withArguments("generateFeatureSourceFactory", "--stacktrace") + .withPluginClasspath() + .withArguments("generateFeatureSourceFactory", "--stacktrace") } - "generates factory without any feature flags" { + test("generates factory without any feature flags") { val fixture = "source-factory-generate-empty".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -40,7 +40,7 @@ internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory with feature flags" { + test("generates factory with feature flags") { val fixture = "source-factory-generate-feature-flags".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -64,7 +64,7 @@ internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ """.trimMargin() } - "uses implicit package name" { + test("uses implicit package name") { val fixture = "source-factory-package-name-implicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -75,7 +75,7 @@ internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.implicit" } - "uses explicit package name" { + test("uses explicit package name") { val fixture = "source-factory-package-name-explicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -86,7 +86,7 @@ internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.explicit" } - "overrides implicit package name" { + test("overrides implicit package name") { val fixture = "source-factory-package-name-explicit-override".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -97,7 +97,7 @@ internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.explicit" } - "generates internal factory" { + test("generates internal factory") { val fixture = "source-factory-generate-internal".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -108,7 +108,7 @@ internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "internal fun FeatureFactory.Companion.featureSourceGenerated()" } - "generates public factory" { + test("generates public factory") { val fixture = "source-factory-generate-public".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -119,7 +119,7 @@ internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "public fun FeatureFactory.Companion.featureSourceGenerated()" } - "fails for feature flags with no options" { + test("fails for feature flags with no options") { val fixture = "source-factory-feature-flag-option-missing".toFixture() val result = gradleRunner.withProjectDir(fixture).buildAndFail() @@ -131,7 +131,7 @@ internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ feature.shouldNotExist() } - "generates factory with feature flags from all modules" { + test("generates factory with feature flags from all modules") { val fixture = "source-factory-multi-module-generate-all".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -156,7 +156,7 @@ internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory with feature flags only from included modules" { + test("generates factory with feature flags only from included modules") { val fixture = "source-factory-multi-module-generate-filtered".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -180,7 +180,7 @@ internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory for Android project" { + test("generates factory for Android project") { val fixture = "source-factory-android-smoke".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -200,7 +200,7 @@ internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory with supervised feature flag sources" { + test("generates factory with supervised feature flag sources") { val fixture = "source-factory-generate-supervised-feature-flags".toFixture() val result = gradleRunner.withProjectDir(fixture).build() diff --git a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateSourcedStorageTaskSpec.kt b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateSourcedStorageTaskSpec.kt similarity index 93% rename from library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateSourcedStorageTaskSpec.kt rename to laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateSourcedStorageTaskSpec.kt index 329cbd772..4ca550218 100644 --- a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateSourcedStorageTaskSpec.kt +++ b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateSourcedStorageTaskSpec.kt @@ -1,6 +1,6 @@ package io.mehow.laboratory.gradle -import io.kotest.core.spec.style.StringSpec +import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.file.shouldExist import io.kotest.matchers.file.shouldNotExist import io.kotest.matchers.shouldBe @@ -9,18 +9,18 @@ import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome.FAILED import org.gradle.testkit.runner.TaskOutcome.SUCCESS -internal class GenerateSourcedStorageTaskSpec : StringSpec({ +class GenerateSourcedStorageTaskSpec : FunSpec({ lateinit var gradleRunner: GradleRunner cleanBuildDirs() beforeTest { gradleRunner = GradleRunner.create() - .withPluginClasspath() - .withArguments("generateSourcedFeatureStorage", "--stacktrace") + .withPluginClasspath() + .withArguments("generateSourcedFeatureStorage", "--stacktrace") } - "generates storage with only local source" { + test("generates storage with only local source") { val fixture = "sourced-storage-generate-local".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -48,7 +48,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ """.trimMargin() } - "generates storage with sources" { + test("generates storage with sources") { val fixture = "sourced-storage-generate-sources".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -91,7 +91,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ """.trimMargin() } - "uses implicit package name" { + test("uses implicit package name") { val fixture = "sourced-storage-package-name-implicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -102,7 +102,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.implicit" } - "uses explicit package name" { + test("uses explicit package name") { val fixture = "sourced-storage-package-name-explicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -113,7 +113,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.explicit" } - "overrides implicit package name" { + test("overrides implicit package name") { val fixture = "sourced-storage-package-name-explicit-override".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -124,7 +124,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.explicit" } - "generates internal storage" { + test("generates internal storage") { val fixture = "sourced-storage-generate-internal".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -135,7 +135,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ factory.readText() shouldContain "internal fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage)" } - "generates public storage" { + test("generates public storage") { val fixture = "sourced-storage-generate-public".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -146,7 +146,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ factory.readText() shouldContain "public fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage)" } - "fails for feature flags with no options" { + test("fails for feature flags with no options") { val fixture = "sourced-storage-feature-flag-option-missing".toFixture() val result = gradleRunner.withProjectDir(fixture).buildAndFail() @@ -158,7 +158,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ feature.shouldNotExist() } - "generates storage with sourced from all modules" { + test("generates storage with sourced from all modules") { val fixture = "sourced-storage-multi-module-generate-all".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -209,7 +209,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ """.trimMargin() } - "generates storage with names only from included modules" { + test("generates storage with names only from included modules") { val fixture = "sourced-storage-multi-module-generate-filtered".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -252,7 +252,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ """.trimMargin() } - "generates storage for Android project" { + test("generates storage for Android project") { val fixture = "sourced-storage-android-smoke".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -279,7 +279,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ """.trimMargin() } - "ignores any custom variant of local sources" { + test("ignores any custom variant of local sources") { val fixture = "sourced-storage-generate-local-ignore".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -306,7 +306,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ """.trimMargin() } - "generates storage with supervised feature flag sources" { + test("generates storage with supervised feature flag sources") { val fixture = "sourced-storage-generate-supervised-feature-flags".toFixture() val result = gradleRunner.withProjectDir(fixture).build() diff --git a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/LaboratoryPluginSpec.kt b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/LaboratoryPluginSpec.kt similarity index 57% rename from library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/LaboratoryPluginSpec.kt rename to laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/LaboratoryPluginSpec.kt index 8ec82cb37..8f0d87f1b 100644 --- a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/LaboratoryPluginSpec.kt +++ b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/LaboratoryPluginSpec.kt @@ -1,13 +1,13 @@ package io.mehow.laboratory.gradle import io.kotest.assertions.throwables.shouldThrowAny -import io.kotest.core.spec.style.StringSpec +import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.string.shouldContain import org.gradle.testkit.runner.GradleRunner -internal class LaboratoryPluginSpec : StringSpec({ +class LaboratoryPluginSpec : FunSpec({ lateinit var gradleRunner: GradleRunner cleanBuildDirs() @@ -16,169 +16,169 @@ internal class LaboratoryPluginSpec : StringSpec({ gradleRunner = GradleRunner.create().withPluginClasspath() } - "fails for project without Kotlin plugin" { + test("fails for project without Kotlin plugin") { val fixture = "plugin-kotlin-missing".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateFeatureFlags", "--stacktrace") - .buildAndFail() + .withArguments("generateFeatureFlags", "--stacktrace") + .buildAndFail() result.task(":generateFeatureFlags").shouldBeNull() result.output shouldContain "Laboratory Gradle plugin requires Kotlin plugin." } - "registers feature flags task for project with Kotlin plugin" { + test("registers feature flags task for project with Kotlin plugin") { val fixture = "plugin-kotlin-present".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateFeatureFlags", "--stacktrace") - .build() + .withArguments("generateFeatureFlags", "--stacktrace") + .build() result.task(":generateFeatureFlags").shouldNotBeNull() } - "fails for Android project without Kotlin Android plugin" { + test("fails for Android project without Kotlin Android plugin") { val fixture = "plugin-kotlin-android-missing".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateFeatureFlags", "--stacktrace") - .buildAndFail() + .withArguments("generateFeatureFlags", "--stacktrace") + .buildAndFail() result.task(":generateFeatureFlags").shouldBeNull() result.output shouldContain "Laboratory Gradle plugin requires Kotlin plugin." } - "registers feature flags task for project with Kotlin Android plugin" { + test("registers feature flags task for project with Kotlin Android plugin") { val fixture = "plugin-kotlin-android-present".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateFeatureFlags", "--stacktrace") - .build() + .withArguments("generateFeatureFlags", "--stacktrace") + .build() result.task(":generateFeatureFlags").shouldNotBeNull() } - "does not register feature flags factory for project without feature flags factory extension" { + test("does not register feature flags factory for project without feature flags factory extension") { val fixture = "plugin-factory-missing".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("--stacktrace") - .build() + .withArguments("--stacktrace") + .build() result.task(":generateFeatureFactory").shouldBeNull() } - "fails for project without feature flags factory extension with feature flags factory argument" { + test("fails for project without feature flags factory extension with feature flags factory argument") { val fixture = "plugin-factory-missing".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateFeatureFactory", "--stacktrace") - .buildAndFail() + .withArguments("generateFeatureFactory", "--stacktrace") + .buildAndFail() result.task(":generateFeatureFactory").shouldBeNull() } - "registers feature flags factory for project with feature flags factory extension" { + test("registers feature flags factory for project with feature flags factory extension") { val fixture = "plugin-factory-present".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateFeatureFactory", "--stacktrace") - .build() + .withArguments("generateFeatureFactory", "--stacktrace") + .build() result.task(":generateFeatureFactory").shouldNotBeNull() } - "does not register sourced storage for project without sourced storage extension" { + test("does not register sourced storage for project without sourced storage extension") { val fixture = "plugin-sourced-storage-missing".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("--stacktrace") - .build() + .withArguments("--stacktrace") + .build() result.task(":generateSourcedFeatureStorage").shouldBeNull() } - "fails for project without sourced storage extension with factory argument" { + test("fails for project without sourced storage extension with factory argument") { val fixture = "plugin-sourced-storage-missing".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateSourcedFeatureStorage", "--stacktrace") - .buildAndFail() + .withArguments("generateSourcedFeatureStorage", "--stacktrace") + .buildAndFail() result.task(":generateSourcedFeatureStorage").shouldBeNull() } - "registers sourced storage for project with factory extension" { + test("registers sourced storage for project with factory extension") { val fixture = "plugin-sourced-storage-present".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateSourcedFeatureStorage", "--stacktrace") - .build() + .withArguments("generateSourcedFeatureStorage", "--stacktrace") + .build() result.task(":generateSourcedFeatureStorage").shouldNotBeNull() } - "does not register feature flag sources factory for project without feature flag sources factory extension" { + test("does not register feature flag sources factory for project without feature flag sources factory extension") { val fixture = "plugin-source-factory-missing".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("--stacktrace") - .build() + .withArguments("--stacktrace") + .build() result.task(":generateFeatureSourceFactory").shouldBeNull() } - "fails for project without feature sources factory extension with feature flag sources factory argument" { + test("fails for project without feature sources factory extension with feature flag sources factory argument") { val fixture = "plugin-source-factory-missing".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateFeatureSourceFactory", "--stacktrace") - .buildAndFail() + .withArguments("generateFeatureSourceFactory", "--stacktrace") + .buildAndFail() result.task(":generateFeatureSourceFactory").shouldBeNull() } - "registers feature flag sources factory for project with feature flag sources factory extension" { + test("registers feature flag sources factory for project with feature flag sources factory extension") { val fixture = "plugin-source-factory-present".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateFeatureSourceFactory", "--stacktrace") - .build() + .withArguments("generateFeatureSourceFactory", "--stacktrace") + .build() result.task(":generateFeatureSourceFactory").shouldNotBeNull() } - "does not register option factory for project without option factory extension" { + test("does not register option factory for project without option factory extension") { val fixture = "plugin-option-factory-missing".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("--stacktrace") - .build() + .withArguments("--stacktrace") + .build() result.task(":generateOptionFactory").shouldBeNull() } - "fails for project without option factory extension with option factory argument" { + test("fails for project without option factory extension with option factory argument") { val fixture = "plugin-option-factory-missing".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateOptionFactory", "--stacktrace") - .buildAndFail() + .withArguments("generateOptionFactory", "--stacktrace") + .buildAndFail() result.task(":generateOptionFactory").shouldBeNull() } - "registers option factory for project with option factory extension" { + test("registers option factory for project with option factory extension") { val fixture = "plugin-option-factory-present".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateOptionFactory", "--stacktrace") - .build() + .withArguments("generateOptionFactory", "--stacktrace") + .build() result.task(":generateOptionFactory").shouldNotBeNull() } - "fails for including dependency without laboratory plugin" { + test("fails for including dependency without laboratory plugin") { val fixture = "plugin-dependency-plugin-missing".toFixture() val exception = shouldThrowAny { gradleRunner.withProjectDir(fixture).build() } diff --git a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/OptionFactoryTaskSpec.kt b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/OptionFactoryTaskSpec.kt similarity index 89% rename from library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/OptionFactoryTaskSpec.kt rename to laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/OptionFactoryTaskSpec.kt index d39214e23..fffccefe5 100644 --- a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/OptionFactoryTaskSpec.kt +++ b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/OptionFactoryTaskSpec.kt @@ -1,6 +1,6 @@ package io.mehow.laboratory.gradle -import io.kotest.core.spec.style.StringSpec +import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.file.shouldExist import io.kotest.matchers.file.shouldNotExist import io.kotest.matchers.shouldBe @@ -9,18 +9,18 @@ import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome.FAILED import org.gradle.testkit.runner.TaskOutcome.SUCCESS -internal class OptionFactoryTaskSpec : StringSpec({ +class OptionFactoryTaskSpec : FunSpec({ lateinit var gradleRunner: GradleRunner cleanBuildDirs() beforeTest { gradleRunner = GradleRunner.create() - .withPluginClasspath() - .withArguments("generateOptionFactory", "--stacktrace") + .withPluginClasspath() + .withArguments("generateOptionFactory", "--stacktrace") } - "generates factory without any feature flags" { + test("generates factory without any feature flags") { val fixture = "option-factory-generate-empty".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -39,7 +39,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory using feature flag fqcns" { + test("generates factory using feature flag fqcns") { val fixture = "option-factory-generate-fqcn".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -69,7 +69,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory using feature flag keys" { + test("generates factory using feature flag keys") { val fixture = "option-factory-generate-key".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -99,7 +99,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ """.trimMargin() } - "fails to generate factory for feature flags with duplicate keys" { + test("fails to generate factory for feature flags with duplicate keys") { val fixture = "option-factory-duplicate-key".toFixture() val result = gradleRunner.withProjectDir(fixture).buildAndFail() @@ -114,7 +114,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ factory.shouldNotExist() } - "fails to generate factory for key duplicating another fqcn" { + test("fails to generate factory for key duplicating another fqcn") { val fixture = "option-factory-duplicate-key-fqcn".toFixture() val result = gradleRunner.withProjectDir(fixture).buildAndFail() @@ -129,7 +129,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ factory.shouldNotExist() } - "fails to generate factory for feature flags with no options" { + test("fails to generate factory for feature flags with no options") { val fixture = "option-factory-no-option".toFixture() val result = gradleRunner.withProjectDir(fixture).buildAndFail() @@ -141,7 +141,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ factory.shouldNotExist() } - "uses implicit package name" { + test("uses implicit package name") { val fixture = "option-factory-package-name-implicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -152,7 +152,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.implicit" } - "uses explicit package name" { + test("uses explicit package name") { val fixture = "option-factory-package-name-explicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -163,7 +163,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.explicit" } - "overrides implicit package name" { + test("overrides implicit package name") { val fixture = "option-factory-package-name-explicit-override".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -174,7 +174,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.explicit" } - "generates internal factory" { + test("generates internal factory") { val fixture = "option-factory-generate-internal".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -185,7 +185,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "internal fun OptionFactory.Companion.generated()" } - "generates public factory" { + test("generates public factory") { val fixture = "option-factory-generate-public".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -196,7 +196,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "public fun OptionFactory.Companion.generated()" } - "generates factory with feature flags from all modules" { + test("generates factory with feature flags from all modules") { val fixture = "option-factory-multi-module-generate-all".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -229,7 +229,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory with feature flags only from included modules" { + test("generates factory with feature flags only from included modules") { val fixture = "option-factory-multi-module-generate-filtered".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -258,7 +258,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory for Android project" { + test("generates factory for Android project") { val fixture = "option-factory-android-smoke".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -277,7 +277,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ """.trimMargin() } - "fails to generate factory for feature flags with duplicate keys in different modules" { + test("fails to generate factory for feature flags with duplicate keys in different modules") { val fixture = "option-factory-multi-module-duplicate-key".toFixture() val result = gradleRunner.withProjectDir(fixture).buildAndFail() @@ -292,7 +292,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ factory.shouldNotExist() } - "generates factory for feature flags with duplicate keys in filtered modules" { + test("generates factory for feature flags with duplicate keys in filtered modules") { val fixture = "option-factory-multi-module-filtered-duplicate-key".toFixture() val result = gradleRunner.withProjectDir(fixture).build() diff --git a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/TestExtensions.kt b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/TestExtensions.kt similarity index 86% rename from library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/TestExtensions.kt rename to laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/TestExtensions.kt index d441c4c77..173b004d3 100644 --- a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/TestExtensions.kt +++ b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/TestExtensions.kt @@ -15,10 +15,10 @@ internal fun File.featureSourceStorageFile(fqcn: String) = codeGenFile("feature- internal fun File.optionFactoryFile(fqcn: String) = codeGenFile("option-factory", fqcn) -private fun File.codeGenFile(dir: String, fqcn: String) = File( - this, - "build/generated/laboratory/code/$dir/${fqcn.replace(".", "/")}.kt" -) +private fun File.codeGenFile( + dir: String, + fqcn: String, +) = File(this, "build/generated/laboratory/code/$dir/${fqcn.replace(".", "/")}.kt") internal fun TestConfiguration.cleanBuildDirs() = beforeSpec { File("src/test/projects").getBuildDirs().forEach { it.deleteRecursively() } diff --git a/library/gradle-plugin/src/test/projects/factory-android-smoke/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-android-smoke/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-android-smoke/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-android-smoke/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-feature-flag-option-missing/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-android-smoke/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-feature-flag-option-missing/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-android-smoke/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-feature-flag-option-missing/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-feature-flag-option-missing/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-feature-flag-option-missing/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-feature-flag-option-missing/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-generate-empty/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-feature-flag-option-missing/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-generate-empty/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-feature-flag-option-missing/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-generate-empty/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-generate-empty/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-generate-empty/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-generate-empty/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-generate-features/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-generate-empty/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-generate-features/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-generate-empty/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-generate-features/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-generate-features/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-generate-features/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-generate-features/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-generate-internal/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-generate-features/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-generate-internal/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-generate-features/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-generate-internal/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-generate-internal/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-generate-internal/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-generate-internal/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-generate-public/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-generate-internal/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-generate-public/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-generate-internal/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-generate-public/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-generate-public/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-generate-public/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-generate-public/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-generate-supervised-feature-flags/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-generate-public/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-generate-supervised-feature-flags/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-generate-public/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-generate-supervised-feature-flags/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-generate-supervised-feature-flags/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-generate-supervised-feature-flags/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-generate-supervised-feature-flags/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-package-name-explicit-override/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-generate-supervised-feature-flags/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-package-name-explicit-override/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-generate-supervised-feature-flags/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-multi-module-generate-all/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-all/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-multi-module-generate-all/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-all/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-multi-module-generate-all/feature-a/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-all/feature-a/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-multi-module-generate-all/feature-a/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-all/feature-a/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-multi-module-generate-all/feature-b/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-all/feature-b/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-multi-module-generate-all/feature-b/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-all/feature-b/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-multi-module-generate-all/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-all/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-multi-module-generate-all/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-all/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/feature-a/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/feature-a/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/feature-a/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/feature-a/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/feature-b/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/feature-b/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/feature-b/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/feature-b/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-package-name-explicit-override/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-package-name-explicit-override/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-package-name-explicit-override/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-package-name-explicit-override/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-package-name-explicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-package-name-explicit-override/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-package-name-explicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-package-name-explicit-override/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-package-name-explicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-package-name-explicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-package-name-explicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-package-name-explicit/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-package-name-implicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-package-name-explicit/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-package-name-implicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-package-name-explicit/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-package-name-implicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-package-name-implicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-package-name-implicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-package-name-implicit/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-android-smoke/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-package-name-implicit/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-android-smoke/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-package-name-implicit/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-android-smoke/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-android-smoke/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-android-smoke/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-android-smoke/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-deprecated-with-level/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-android-smoke/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-deprecated-with-level/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-android-smoke/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-deprecated-with-level/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-deprecated-with-level/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-deprecated-with-level/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-deprecated-with-level/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-deprecated/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-deprecated-with-level/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-deprecated/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-deprecated-with-level/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-deprecated/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-deprecated/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-deprecated/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-deprecated/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-description/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-deprecated/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-description/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-deprecated/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-description/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-description/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-description/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-description/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-internal/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-description/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-internal/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-description/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-internal/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-internal/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-internal/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-internal/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-multiple/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-internal/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-multiple/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-internal/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-multiple/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-multiple/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-multiple/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-multiple/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-option-name-common/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-multiple/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-option-name-common/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-multiple/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-option-name-common/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-option-name-common/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-option-name-common/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-option-name-common/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-public/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-option-name-common/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-public/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-option-name-common/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-public/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-public/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-public/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-public/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-single/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-public/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-single/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-public/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-single/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-single/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-single/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-single/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-sources-internal/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-single/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-sources-internal/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-single/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-sources-internal/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-internal/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-sources-internal/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-internal/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-sources-multiple/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-internal/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-sources-multiple/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-internal/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-sources-multiple/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-multiple/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-sources-multiple/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-multiple/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-sources-public/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-multiple/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-sources-public/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-multiple/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-sources-public/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-public/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-sources-public/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-public/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-sources-single/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-public/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-sources-single/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-public/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-sources-single/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-single/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-sources-single/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-single/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-option-missing/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-single/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-option-missing/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-single/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-option-missing/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-option-missing/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-option-missing/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-option-missing/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-override/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-option-missing/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-override/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-option-missing/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-override/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-override/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-override/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-override/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-switching/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-override/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-switching/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-override/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-switching/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-switching/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-switching/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-switching/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-switching/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-switching/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-cascading/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-cascading/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-cascading/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-cascading/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-cascading/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-cascading/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-switching/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-cascading/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-switching/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-cascading/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-switching/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-switching/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-switching/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-switching/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-switching/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-switching/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-child/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-child/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-child/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-child/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-child/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-child/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-grandchild/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-child/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-grandchild/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-child/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-grandchild/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-grandchild/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-grandchild/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-grandchild/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-multiple-children/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-grandchild/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-multiple-children/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-grandchild/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-multiple-children/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-multiple-children/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-multiple-children/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-multiple-children/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit-override/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-multiple-children/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit-override/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-multiple-children/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit-override/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit-override/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit-override/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit-override/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit-override/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit-override/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-implicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-implicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-implicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-implicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-implicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-implicit/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-self-supervision/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-implicit/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-self-supervision/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-implicit/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-self-supervision/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-self-supervision/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-self-supervision/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-self-supervision/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-android-smoke/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-self-supervision/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-android-smoke/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-self-supervision/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-android-smoke/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-android-smoke/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-android-smoke/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-android-smoke/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-duplicate-key-fqcn/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-android-smoke/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-duplicate-key-fqcn/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-android-smoke/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-duplicate-key-fqcn/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-duplicate-key-fqcn/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-duplicate-key-fqcn/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-duplicate-key-fqcn/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-duplicate-key/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-duplicate-key-fqcn/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-duplicate-key/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-duplicate-key-fqcn/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-duplicate-key/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-duplicate-key/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-duplicate-key/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-duplicate-key/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-generate-empty/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-duplicate-key/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-generate-empty/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-duplicate-key/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-generate-empty/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-generate-empty/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-generate-empty/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-generate-empty/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-generate-fqcn/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-generate-empty/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-generate-fqcn/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-generate-empty/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-generate-fqcn/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-generate-fqcn/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-generate-fqcn/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-generate-fqcn/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-generate-internal/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-generate-fqcn/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-generate-internal/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-generate-fqcn/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-generate-internal/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-generate-internal/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-generate-internal/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-generate-internal/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-generate-key/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-generate-internal/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-generate-key/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-generate-internal/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-generate-key/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-generate-key/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-generate-key/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-generate-key/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-generate-public/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-generate-key/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-generate-public/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-generate-key/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-generate-public/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-generate-public/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-generate-public/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-generate-public/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-no-option/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-generate-public/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-no-option/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-generate-public/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/feature-a/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/feature-a/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/feature-a/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/feature-a/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/feature-b/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/feature-b/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/feature-b/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/feature-b/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/feature-a/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/feature-a/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/feature-a/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/feature-a/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/feature-b/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/feature-b/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/feature-b/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/feature-b/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/feature-a/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/feature-a/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/feature-a/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/feature-a/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/feature-b/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/feature-b/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/feature-b/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/feature-b/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/feature-a/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/feature-a/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/feature-a/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/feature-a/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/feature-b/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/feature-b/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/feature-b/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/feature-b/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-no-option/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-no-option/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-no-option/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-no-option/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-package-name-explicit-override/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-no-option/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-package-name-explicit-override/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-no-option/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-package-name-explicit-override/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-package-name-explicit-override/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-package-name-explicit-override/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-package-name-explicit-override/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-package-name-explicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-package-name-explicit-override/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-package-name-explicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-package-name-explicit-override/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-package-name-explicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-package-name-explicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-package-name-explicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-package-name-explicit/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-package-name-implicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-package-name-explicit/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-package-name-implicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-package-name-explicit/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-package-name-implicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-package-name-implicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-package-name-implicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-package-name-implicit/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-factory-missing/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-package-name-implicit/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-factory-missing/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-package-name-implicit/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/no-plugin/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/no-plugin/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/no-plugin/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/no-plugin/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-factory-missing/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-factory-missing/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-factory-missing/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-factory-missing/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-factory-present/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-factory-missing/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-factory-present/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-factory-missing/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-factory-present/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-factory-present/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-factory-present/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-factory-present/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-kotlin-android-missing/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-factory-present/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-kotlin-android-missing/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-factory-present/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-kotlin-android-missing/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-kotlin-android-missing/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-kotlin-android-missing/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-kotlin-android-missing/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-kotlin-android-present/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-kotlin-android-missing/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-kotlin-android-present/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-kotlin-android-missing/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-kotlin-android-present/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-kotlin-android-present/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-kotlin-android-present/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-kotlin-android-present/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-kotlin-missing/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-kotlin-android-present/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-kotlin-missing/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-kotlin-android-present/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-kotlin-missing/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-kotlin-missing/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-kotlin-missing/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-kotlin-missing/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-kotlin-present/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-kotlin-missing/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-kotlin-present/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-kotlin-missing/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-kotlin-present/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-kotlin-present/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-kotlin-present/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-kotlin-present/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-option-factory-missing/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-kotlin-present/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-option-factory-missing/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-kotlin-present/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-option-factory-missing/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-option-factory-missing/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-option-factory-missing/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-option-factory-missing/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-option-factory-present/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-option-factory-missing/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-option-factory-present/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-option-factory-missing/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-option-factory-present/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-option-factory-present/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-option-factory-present/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-option-factory-present/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-source-factory-missing/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-option-factory-present/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-source-factory-missing/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-option-factory-present/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-source-factory-missing/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-source-factory-missing/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-source-factory-missing/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-source-factory-missing/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-source-factory-present/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-source-factory-missing/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-source-factory-present/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-source-factory-missing/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-source-factory-present/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-source-factory-present/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-source-factory-present/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-source-factory-present/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-sourced-storage-missing/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-source-factory-present/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-sourced-storage-missing/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-source-factory-present/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-sourced-storage-missing/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-sourced-storage-missing/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-sourced-storage-missing/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-sourced-storage-missing/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-sourced-storage-present/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-sourced-storage-missing/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-sourced-storage-present/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-sourced-storage-missing/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-sourced-storage-present/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-sourced-storage-present/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-sourced-storage-present/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-sourced-storage-present/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-android-smoke/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-sourced-storage-present/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-android-smoke/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-sourced-storage-present/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-android-smoke/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-android-smoke/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-android-smoke/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-android-smoke/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-feature-flag-option-missing/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-android-smoke/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-feature-flag-option-missing/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-android-smoke/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-feature-flag-option-missing/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-feature-flag-option-missing/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-feature-flag-option-missing/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-feature-flag-option-missing/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-generate-empty/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-feature-flag-option-missing/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-generate-empty/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-feature-flag-option-missing/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-generate-empty/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-generate-empty/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-generate-empty/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-generate-empty/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-generate-feature-flags/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-generate-empty/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-generate-feature-flags/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-generate-empty/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-generate-feature-flags/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-generate-feature-flags/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-generate-feature-flags/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-generate-feature-flags/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-generate-internal/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-generate-feature-flags/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-generate-internal/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-generate-feature-flags/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-generate-internal/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-generate-internal/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-generate-internal/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-generate-internal/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-generate-public/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-generate-internal/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-generate-public/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-generate-internal/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-generate-public/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-generate-public/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-generate-public/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-generate-public/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-generate-supervised-feature-flags/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-generate-public/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-generate-supervised-feature-flags/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-generate-public/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-generate-supervised-feature-flags/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-generate-supervised-feature-flags/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-generate-supervised-feature-flags/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-generate-supervised-feature-flags/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-package-name-explicit-override/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-generate-supervised-feature-flags/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-package-name-explicit-override/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-generate-supervised-feature-flags/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/feature-a/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/feature-a/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/feature-a/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/feature-a/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/feature-b/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/feature-b/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/feature-b/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/feature-b/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/feature-a/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/feature-a/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/feature-a/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/feature-a/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/feature-b/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/feature-b/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/feature-b/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/feature-b/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-package-name-explicit-override/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-package-name-explicit-override/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-package-name-explicit-override/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-package-name-explicit-override/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-package-name-explicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-package-name-explicit-override/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-package-name-explicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-package-name-explicit-override/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-package-name-explicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-package-name-explicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-package-name-explicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-package-name-explicit/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-package-name-implicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-package-name-explicit/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-package-name-implicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-package-name-explicit/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-package-name-implicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-package-name-implicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-package-name-implicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-package-name-implicit/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-android-smoke/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-package-name-implicit/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-android-smoke/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-package-name-implicit/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-android-smoke/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-android-smoke/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-android-smoke/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-android-smoke/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-feature-flag-option-missing/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-android-smoke/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-feature-flag-option-missing/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-android-smoke/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-feature-flag-option-missing/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-feature-flag-option-missing/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-feature-flag-option-missing/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-feature-flag-option-missing/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-internal/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-feature-flag-option-missing/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-internal/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-feature-flag-option-missing/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-internal/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-internal/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-internal/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-internal/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-local-ignore/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-internal/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-local-ignore/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-internal/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-local-ignore/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-local-ignore/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-local-ignore/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-local-ignore/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-local/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-local-ignore/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-local/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-local-ignore/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-local/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-local/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-local/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-local/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-public/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-local/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-public/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-local/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-public/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-public/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-public/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-public/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-sources/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-public/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-sources/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-public/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-sources/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-sources/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-sources/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-sources/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-supervised-feature-flags/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-sources/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-supervised-feature-flags/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-sources/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-supervised-feature-flags/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-supervised-feature-flags/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-supervised-feature-flags/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-supervised-feature-flags/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit-override/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-supervised-feature-flags/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit-override/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-supervised-feature-flags/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/feature-a/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/feature-a/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/feature-a/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/feature-a/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/feature-b/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/feature-b/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/feature-b/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/feature-b/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/feature-a/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/feature-a/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/feature-a/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/feature-a/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/feature-b/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/feature-b/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/feature-b/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/feature-b/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit-override/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit-override/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit-override/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit-override/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit-override/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit-override/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-package-name-implicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-package-name-implicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-package-name-implicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-implicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-package-name-implicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-implicit/build.gradle diff --git a/library/hyperion-plugin/api/hyperion-plugin.api b/laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-implicit/settings.gradle similarity index 100% rename from library/hyperion-plugin/api/hyperion-plugin.api rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-implicit/settings.gradle diff --git a/laboratory/hyperion-plugin/api/hyperion-plugin.api b/laboratory/hyperion-plugin/api/hyperion-plugin.api new file mode 100644 index 000000000..e69de29bb diff --git a/laboratory/hyperion-plugin/build.gradle.kts b/laboratory/hyperion-plugin/build.gradle.kts new file mode 100644 index 000000000..bb17779ab --- /dev/null +++ b/laboratory/hyperion-plugin/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.mavenPublish) + alias(libs.plugins.spotless) + alias(libs.plugins.detekt) + alias(libs.plugins.dokka) + alias(libs.plugins.ksp) +} + +android { + namespace = "io.mehow.laboratory.hyperion" + resourcePrefix = "io_mehow_laboratory_" +} + +dependencies { + api(projects.laboratory.inspector) + api(libs.hyperion.plugin) + implementation(libs.androidx.appCompat) + ksp(libs.autoServiceKsp) +} diff --git a/library/hyperion-plugin/gradle.properties b/laboratory/hyperion-plugin/gradle.properties similarity index 61% rename from library/hyperion-plugin/gradle.properties rename to laboratory/hyperion-plugin/gradle.properties index 742b2ca81..7017ccba9 100644 --- a/library/hyperion-plugin/gradle.properties +++ b/laboratory/hyperion-plugin/gradle.properties @@ -1,3 +1,3 @@ POM_ARTIFACT_ID=laboratory-hyperion-plugin -POM_NAME=Laboratory (Hyperion plugin) +POM_NAME=Laboratory (Hyperion Plugin) POM_PACKAGING=aar diff --git a/library/hyperion-plugin/src/main/java/io/mehow/laboratory/hyperion/Plugin.kt b/laboratory/hyperion-plugin/src/main/kotlin/io/mehow/laboratory/hyperion/Plugin.kt similarity index 99% rename from library/hyperion-plugin/src/main/java/io/mehow/laboratory/hyperion/Plugin.kt rename to laboratory/hyperion-plugin/src/main/kotlin/io/mehow/laboratory/hyperion/Plugin.kt index df957744e..f0d243310 100644 --- a/library/hyperion-plugin/src/main/java/io/mehow/laboratory/hyperion/Plugin.kt +++ b/laboratory/hyperion-plugin/src/main/kotlin/io/mehow/laboratory/hyperion/Plugin.kt @@ -7,5 +7,6 @@ import com.willowtreeapps.hyperion.plugin.v1.PluginModule as HyperionPluginModul @AutoService(HyperionPlugin::class) internal class Plugin : HyperionPlugin() { override fun minimumRequiredApi() = 21 + override fun createPluginModule(): HyperionPluginModule = PluginModule() } diff --git a/library/hyperion-plugin/src/main/java/io/mehow/laboratory/hyperion/PluginModule.kt b/laboratory/hyperion-plugin/src/main/kotlin/io/mehow/laboratory/hyperion/PluginModule.kt similarity index 84% rename from library/hyperion-plugin/src/main/java/io/mehow/laboratory/hyperion/PluginModule.kt rename to laboratory/hyperion-plugin/src/main/kotlin/io/mehow/laboratory/hyperion/PluginModule.kt index de036aeba..ff9bc9a6f 100644 --- a/library/hyperion-plugin/src/main/java/io/mehow/laboratory/hyperion/PluginModule.kt +++ b/laboratory/hyperion-plugin/src/main/kotlin/io/mehow/laboratory/hyperion/PluginModule.kt @@ -9,7 +9,10 @@ import com.willowtreeapps.hyperion.plugin.v1.PluginModule as HyperionPluginModul internal class PluginModule : HyperionPluginModule() { override fun getName(): Int = R.string.io_mehow_laboratory_plugin_id - override fun createPluginView(layoutInflater: LayoutInflater, parent: ViewGroup): View { + override fun createPluginView( + layoutInflater: LayoutInflater, + parent: ViewGroup, + ): View { return layoutInflater.inflate(R.layout.io_mehow_laboratory_plugin_item, parent, false).apply { setOnClickListener { LaboratoryActivity.start(context) } } diff --git a/library/hyperion-plugin/src/main/res/drawable/io_mehow_laboratory_icon.xml b/laboratory/hyperion-plugin/src/main/res/drawable/io_mehow_laboratory_icon.xml similarity index 100% rename from library/hyperion-plugin/src/main/res/drawable/io_mehow_laboratory_icon.xml rename to laboratory/hyperion-plugin/src/main/res/drawable/io_mehow_laboratory_icon.xml diff --git a/library/hyperion-plugin/src/main/res/layout/io_mehow_laboratory_plugin_item.xml b/laboratory/hyperion-plugin/src/main/res/layout/io_mehow_laboratory_plugin_item.xml similarity index 100% rename from library/hyperion-plugin/src/main/res/layout/io_mehow_laboratory_plugin_item.xml rename to laboratory/hyperion-plugin/src/main/res/layout/io_mehow_laboratory_plugin_item.xml diff --git a/library/hyperion-plugin/src/main/res/values/public.xml b/laboratory/hyperion-plugin/src/main/res/values/public.xml similarity index 100% rename from library/hyperion-plugin/src/main/res/values/public.xml rename to laboratory/hyperion-plugin/src/main/res/values/public.xml diff --git a/library/hyperion-plugin/src/main/res/values/strings.xml b/laboratory/hyperion-plugin/src/main/res/values/strings.xml similarity index 100% rename from library/hyperion-plugin/src/main/res/values/strings.xml rename to laboratory/hyperion-plugin/src/main/res/values/strings.xml diff --git a/library/inspector/api/inspector.api b/laboratory/inspector/api/inspector.api similarity index 97% rename from library/inspector/api/inspector.api rename to laboratory/inspector/api/inspector.api index 2834e1862..ada6ba372 100644 --- a/library/inspector/api/inspector.api +++ b/laboratory/inspector/api/inspector.api @@ -1,6 +1,7 @@ public final class io/mehow/laboratory/inspector/DeprecationAlignment : java/lang/Enum { public static final field Bottom Lio/mehow/laboratory/inspector/DeprecationAlignment; public static final field Regular Lio/mehow/laboratory/inspector/DeprecationAlignment; + public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lio/mehow/laboratory/inspector/DeprecationAlignment; public static fun values ()[Lio/mehow/laboratory/inspector/DeprecationAlignment; } @@ -13,6 +14,7 @@ public final class io/mehow/laboratory/inspector/DeprecationPhenotype : java/lan public static final field Hide Lio/mehow/laboratory/inspector/DeprecationPhenotype; public static final field Show Lio/mehow/laboratory/inspector/DeprecationPhenotype; public static final field Strikethrough Lio/mehow/laboratory/inspector/DeprecationPhenotype; + public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lio/mehow/laboratory/inspector/DeprecationPhenotype; public static fun values ()[Lio/mehow/laboratory/inspector/DeprecationPhenotype; } diff --git a/laboratory/inspector/build.gradle.kts b/laboratory/inspector/build.gradle.kts new file mode 100644 index 000000000..9af733376 --- /dev/null +++ b/laboratory/inspector/build.gradle.kts @@ -0,0 +1,39 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.mavenPublish) + alias(libs.plugins.spotless) + alias(libs.plugins.detekt) + alias(libs.plugins.dokka) +} + +android { + namespace = "io.mehow.laboratory.inspector" + resourcePrefix = "io_mehow_laboratory_" + + defaultConfig { + consumerProguardFile("io-mehow-laboratory-inspector.pro") + } +} + +tasks.withType().configureEach { + useJUnitPlatform() +} + +dependencies { + api(projects.laboratory.runtime) + implementation(libs.hyperion.plugin) + implementation(libs.androidx.appCompat) + implementation(libs.androidx.fragmentKtx) + implementation(libs.androidx.viewModelKtx) + implementation(libs.androidx.recyclerView) + implementation(libs.androidx.viewPager2) + implementation(libs.android.material) + implementation(libs.kotlinx.coroutinesAndroid) + + testImplementation(libs.kotest.runner.junit5) + testImplementation(libs.kotest.assertions) + testImplementation(libs.kotest.property) + testImplementation(libs.turbine) + testImplementation(libs.kotlinx.coroutinesTest) +} diff --git a/library/inspector/gradle.properties b/laboratory/inspector/gradle.properties similarity index 63% rename from library/inspector/gradle.properties rename to laboratory/inspector/gradle.properties index 42697faac..e1bab13f4 100644 --- a/library/inspector/gradle.properties +++ b/laboratory/inspector/gradle.properties @@ -1,3 +1,3 @@ POM_ARTIFACT_ID=laboratory-inspector -POM_NAME=Laboratory (inspector) +POM_NAME=Laboratory (Inspector) POM_PACKAGING=aar diff --git a/library/inspector/io-mehow-laboratory-inspector.pro b/laboratory/inspector/io-mehow-laboratory-inspector.pro similarity index 100% rename from library/inspector/io-mehow-laboratory-inspector.pro rename to laboratory/inspector/io-mehow-laboratory-inspector.pro diff --git a/library/inspector/src/main/AndroidManifest.xml b/laboratory/inspector/src/main/AndroidManifest.xml similarity index 100% rename from library/inspector/src/main/AndroidManifest.xml rename to laboratory/inspector/src/main/AndroidManifest.xml diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/DeprecationAlignment.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/DeprecationAlignment.kt similarity index 100% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/DeprecationAlignment.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/DeprecationAlignment.kt diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/DeprecationHandler.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/DeprecationHandler.kt similarity index 100% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/DeprecationHandler.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/DeprecationHandler.kt diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/DeprecationPhenotype.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/DeprecationPhenotype.kt similarity index 100% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/DeprecationPhenotype.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/DeprecationPhenotype.kt diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureAdapter.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/FeatureAdapter.kt similarity index 68% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureAdapter.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/FeatureAdapter.kt index 57c0ba5bf..1f526d37e 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureAdapter.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/FeatureAdapter.kt @@ -13,20 +13,35 @@ internal class FeatureAdapter( ) : ListAdapter(DiffCallback) { override fun getItemViewType(position: Int) = R.layout.io_mehow_laboratory_feature_item - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FeatureViewHolder { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): FeatureViewHolder { val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) return FeatureViewHolder(view, listener) } - override fun onBindViewHolder(holder: FeatureViewHolder, position: Int) = holder.bind(getItem(position)) + override fun onBindViewHolder( + holder: FeatureViewHolder, + position: Int, + ) = holder.bind(getItem(position)) private object DiffCallback : ItemCallback() { - override fun areItemsTheSame(old: FeatureUiModel, new: FeatureUiModel) = old.type == new.type + override fun areItemsTheSame( + old: FeatureUiModel, + new: FeatureUiModel, + ) = old.type == new.type - override fun areContentsTheSame(old: FeatureUiModel, new: FeatureUiModel) = old == new + override fun areContentsTheSame( + old: FeatureUiModel, + new: FeatureUiModel, + ) = old == new // Prevent item animation change. - override fun getChangePayload(old: FeatureUiModel, new: FeatureUiModel) = Unit + override fun getChangePayload( + old: FeatureUiModel, + new: FeatureUiModel, + ) = Unit } interface Listener : OptionGroupListener, OnSelectSourceListener { diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureCoordinates.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/FeatureCoordinates.kt similarity index 100% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureCoordinates.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/FeatureCoordinates.kt diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureUiModel.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/FeatureUiModel.kt similarity index 89% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureUiModel.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/FeatureUiModel.kt index 0451c9b59..d489cda59 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureUiModel.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/FeatureUiModel.kt @@ -16,9 +16,9 @@ internal data class FeatureUiModel( val hasMultipleSources = sources.size > 1 private val isCurrentSourceLocal = sources.firstOrNull(OptionUiModel::isSelected) - ?.option - ?.name - ?.equals("Local", ignoreCase = true) ?: true + ?.option + ?.name + ?.equals("Local", ignoreCase = true) ?: true private val isSupervised = type.supervisorOption == supervisorOption @@ -28,8 +28,8 @@ internal data class FeatureUiModel( private val firstAlignmentOrdinal = DeprecationAlignment.values().first() val NaturalComparator = compareBy( - { it.deprecationAlignment ?: firstAlignmentOrdinal }, - { it.name } + { it.deprecationAlignment ?: firstAlignmentOrdinal }, + { it.name }, ) } } diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureViewHolder.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/FeatureViewHolder.kt similarity index 100% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureViewHolder.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/FeatureViewHolder.kt diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/Fragments.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/Fragments.kt similarity index 100% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/Fragments.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/Fragments.kt diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/InspectorViewModel.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/InspectorViewModel.kt similarity index 76% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/InspectorViewModel.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/InspectorViewModel.kt index 3546662c6..4219a5e8c 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/InspectorViewModel.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/InspectorViewModel.kt @@ -56,13 +56,13 @@ internal class InspectorViewModel( flow { val groups = withContext(computationDispatcher) { featureFactory.create() - .mapNotNull(metadataFactory::create) - .map { it.observeGroup(laboratory) } - .combineLatest() + .mapNotNull(metadataFactory::create) + .map { it.observeGroup(laboratory) } + .combineLatest() } val searchedGroups = combine(groups, initiatedSearchQueries) { group, query -> group.search(query) } - .map { it.sortedWith(FeatureUiModel.NaturalComparator) } - .flowOn(computationDispatcher) + .map { it.sortedWith(FeatureUiModel.NaturalComparator) } + .flowOn(computationDispatcher) emitAll(searchedGroups) }.shareIn(viewModelScope, SharingStarted.Lazily, replay = 1) } @@ -78,12 +78,12 @@ internal class InspectorViewModel( val featureCoordinatesFlow: Flow get() = mutableNavigationFlow suspend fun goTo(feature: Class>) = sectionFlows.values.asFlow().withIndex() - .mapNotNull { (sectionIndex, sectionFlow) -> - val listIndex = sectionFlow.first().map(FeatureUiModel::type).indexOf(feature) - if (listIndex == -1) null else FeatureCoordinates(sectionIndex, listIndex) - } - .firstOrNull() - ?.also { mutableNavigationFlow.emit(it) } + .mapNotNull { (sectionIndex, sectionFlow) -> + val listIndex = sectionFlow.first().map(FeatureUiModel::type).indexOf(feature) + if (listIndex == -1) null else FeatureCoordinates(sectionIndex, listIndex) + } + .firstOrNull() + ?.also { mutableNavigationFlow.emit(it) } private class FeatureMetadata( private val feature: Class>, @@ -97,31 +97,30 @@ internal class InspectorViewModel( private val sourceMetadata = feature.source?.let { FeatureMetadata(it, allFeatures, deprecationHandler) } private val deprecationLevel = feature.annotations - .filterIsInstance() - .firstOrNull() - ?.level + .filterIsInstance() + .firstOrNull() + ?.level private val deprecationPhenotype = deprecationLevel?.let(deprecationHandler::getPhenotype) private val deprecationPlacement = deprecationLevel?.let(deprecationHandler::getAlignment) - @Suppress("UNCHECKED_CAST") fun observeGroup(laboratory: Laboratory): Flow { val featureEmissions = observeOptions(laboratory) val sourceEmissions = sourceMetadata?.observeOptions(laboratory) ?: flowOf(emptyList()) val supervisorEmissions = feature.supervisorOption - ?.let { laboratory.observe(it::class.java) } - ?: flowOf(null) + ?.let { laboratory.observe(it::class.java) } + ?: flowOf(null) return combine(featureEmissions, sourceEmissions, supervisorEmissions) { features, sources, supervisor -> FeatureUiModel( - type = feature, - name = simpleReadableName, - description = feature.description.tokenize(), - models = features, - sources = sources, - deprecationAlignment = deprecationPlacement, - deprecationPhenotype = deprecationPhenotype, - supervisorOption = supervisor, + type = feature, + name = simpleReadableName, + description = feature.description.tokenize(), + models = features, + sources = sources, + deprecationAlignment = deprecationPlacement, + deprecationPhenotype = deprecationPhenotype, + supervisorOption = supervisor, ) } } @@ -139,14 +138,14 @@ internal class InspectorViewModel( ) { private val allFeatures by lazy { featureFactories.values - .flatMap { it.create() } - .filterNot { it.options.isEmpty() } + .flatMap { it.create() } + .filterNot { it.options.isEmpty() } } fun create(feature: Class>) = feature - .takeUnless { it.options.isEmpty() } - ?.let { FeatureMetadata(it, allFeatures, deprecationHandler) } - ?.takeIf { it.deprecationPhenotype != DeprecationPhenotype.Hide } + .takeUnless { it.options.isEmpty() } + ?.let { FeatureMetadata(it, allFeatures, deprecationHandler) } + ?.takeIf { it.deprecationPhenotype != DeprecationPhenotype.Hide } } } @@ -156,13 +155,14 @@ internal class InspectorViewModel( ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { require(modelClass == InspectorViewModel::class.java) { "Cannot create $modelClass" } - @Suppress("UNCHECKED_CAST") @OptIn(FlowPreview::class) + @Suppress("UNCHECKED_CAST") + @OptIn(FlowPreview::class) return InspectorViewModel( - configuration.laboratory, - searchViewModel.uiModels.debounce(200.milliseconds).map { it.query }, - configuration.featureFactories, - configuration.deprecation, - Dispatchers.Default, + configuration.laboratory, + searchViewModel.uiModels.debounce(200.milliseconds).map { it.query }, + configuration.featureFactories, + configuration.deprecation, + Dispatchers.Default, ) as T } } diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/Iterables.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/Iterables.kt similarity index 69% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/Iterables.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/Iterables.kt index 318b959f8..f7fde6d99 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/Iterables.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/Iterables.kt @@ -10,13 +10,13 @@ internal fun Iterable.containsAllInOrder( predicate: (left: T, right: T) -> Boolean, ): Boolean { val allCombinations = asSequence() - .mapIndexed(::Pair) - .flatMap { left -> other.mapIndexed { index, right -> left to (index to right) } } + .mapIndexed(::Pair) + .flatMap { left -> other.mapIndexed { index, right -> left to (index to right) } } val uniqueFinds = allCombinations - .filter { (left, right) -> predicate(left.second, right.second) } - .map { (_, right) -> right } - .distinct() - .map { (_, value) -> value } + .filter { (left, right) -> predicate(left.second, right.second) } + .map { (_, right) -> right } + .distinct() + .map { (_, value) -> value } return uniqueFinds.toList() == other.toList() } diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/LaboratoryActivity.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/LaboratoryActivity.kt similarity index 91% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/LaboratoryActivity.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/LaboratoryActivity.kt index ce727a015..2ba7fdf34 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/LaboratoryActivity.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/LaboratoryActivity.kt @@ -34,21 +34,21 @@ public class LaboratoryActivity : AppCompatActivity(R.layout.io_mehow_laboratory InspectorViewModel.Factory(configuration, searchViewModel) } - override fun onCreate(inState: Bundle?) { - super.onCreate(inState) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) setUpToolbar() setUpViewPager() } private fun setUpToolbar() { val binding = ToolbarBinding( - view = window.decorView, - onSearchEventsListener = { event -> searchViewModel.sendEvent(event) }, - onResetEventsListener = { resetFeatureFlags() }, + view = window.decorView, + onSearchEventsListener = { event -> searchViewModel.sendEvent(event) }, + onResetEventsListener = { resetFeatureFlags() }, ) searchViewModel.uiModels - .onEach { uiModel -> binding.render(uiModel) } - .launchIn(lifecycleScope) + .onEach { uiModel -> binding.render(uiModel) } + .launchIn(lifecycleScope) } private fun setUpViewPager() { @@ -69,19 +69,20 @@ public class LaboratoryActivity : AppCompatActivity(R.layout.io_mehow_laboratory } private fun observeNavigationEvents(viewPager: ViewPager2) = inspectorViewModel.featureCoordinatesFlow - .onEach { (sectionIndex, featureIndex) -> - viewPager.currentItem = sectionIndex - awaitSectionFragment(sectionNames[sectionIndex]).scrollTo(featureIndex) - } - .launchIn(lifecycleScope) + .onEach { (sectionIndex, featureIndex) -> + viewPager.currentItem = sectionIndex + awaitSectionFragment(sectionNames[sectionIndex]).scrollTo(featureIndex) + } + .launchIn(lifecycleScope) private suspend fun awaitSectionFragment(sectionName: String): SectionFragment = supportFragmentManager.fragments - .filterIsInstance() - .firstOrNull { it.sectionName == sectionName } - ?: run { - delay(100) // ¯\_(ツ)_/¯ - awaitSectionFragment(sectionName) - } + .filterIsInstance() + .firstOrNull { it.sectionName == sectionName } + ?: run { + @Suppress("MagicNumber") + delay(100) // ¯\_(ツ)_/¯ + awaitSectionFragment(sectionName) + } private fun resetFeatureFlags() = lifecycleScope.launch { val isCleared = configuration.laboratory.clear() @@ -242,10 +243,12 @@ public class LaboratoryActivity : AppCompatActivity(R.layout.io_mehow_laboratory externalFactories: Map = emptyMap(), ) { val filteredFactories = externalFactories.filterNot { it.key == featuresLabel } - configure(Configuration.create( + configure( + Configuration.create( laboratory, - featureFactories = linkedMapOf(featuresLabel to mainFactory) + filteredFactories - )) + featureFactories = linkedMapOf(featuresLabel to mainFactory) + filteredFactories, + ), + ) } /** diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/OptionUiModel.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/OptionUiModel.kt similarity index 100% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/OptionUiModel.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/OptionUiModel.kt diff --git a/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/OptionViewGroup.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/OptionViewGroup.kt new file mode 100644 index 000000000..9425addcf --- /dev/null +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/OptionViewGroup.kt @@ -0,0 +1,101 @@ +package io.mehow.laboratory.inspector + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.CompoundButton +import androidx.appcompat.content.res.AppCompatResources +import androidx.appcompat.widget.PopupMenu +import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipGroup +import io.mehow.laboratory.Feature +import com.google.android.material.R as MaterialR + +internal class OptionViewGroup + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet, + defStyle: Int = MaterialR.attr.chipGroupStyle, + ) : ChipGroup(context, attrs, defStyle) { + private val inflater = LayoutInflater.from(context) + private var listener: OptionGroupListener? = null + + init { + isSelectionRequired = true + } + + fun setOnSelectFeatureListener(listener: OptionGroupListener?) { + this.listener = listener + } + + fun render( + models: List, + isEnabled: Boolean, + ) { + chips.forEach(::removeOnCheckedChangeListener) + removeAllViews() + models.map { createChip(it, isEnabled) }.forEach(::addView) + } + + private fun createChip( + model: OptionUiModel, + isEnabled: Boolean, + ): Chip { + val chip = inflater.inflate(R.layout.io_mehow_laboratory_feature_option_chip, this, false) as Chip + return chip.apply { + text = model.option.name + isChecked = model.isSelected + if (model.supervisedFeatures.isNotEmpty()) { + chipIcon = AppCompatResources.getDrawable(context, R.drawable.io_mehow_laboratory_supervisor) + setOnLongClickListener { showSupervisedFeaturesMenu(this, model.supervisedFeatures) } + } + isActivated = isEnabled + this.isEnabled = isEnabled + setOnCheckedChangeListener(createListener(model)) + } + } + + private fun createListener(model: OptionUiModel) = CompoundButton.OnCheckedChangeListener { chip, isChecked -> + if (isChecked) { + (chip as Chip).deselectOtherChips() + listener?.onSelectOption(model.option) + } + } + + // ChipGroup.isSingleSelection does not work with initial selection from code. + private fun Chip.deselectOtherChips() { + chips.filter { it !== this }.forEach { chip -> chip.isChecked = false } + } + + private fun removeOnCheckedChangeListener(chip: Chip) = chip.setOnCheckedChangeListener(null) + + private fun showSupervisedFeaturesMenu( + anchor: Chip, + features: List>>, + ): Boolean { + PopupMenu(context, anchor).apply { + features.forEachIndexed { index, feature -> + menu.add(0, index, index, feature.simpleName) + } + setOnMenuItemClickListener { + listener?.onSelectSupervisedFeature(features[it.order]) + true + } + }.show() + return true + } + + private val chips: Sequence get() = sequence { + for (index in 0 until childCount) { + val chip = getChildAt(index) as? Chip ?: continue + yield(chip) + } + } + + interface OptionGroupListener { + fun onSelectOption(option: Feature<*>) + + fun onSelectSupervisedFeature(feature: Class>) + } + } diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SearchMode.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SearchMode.kt similarity index 95% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/SearchMode.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SearchMode.kt index c7a16c1d0..3a343931e 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SearchMode.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SearchMode.kt @@ -3,5 +3,4 @@ package io.mehow.laboratory.inspector internal enum class SearchMode { Idle, Active, - ; } diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SearchQuery.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SearchQuery.kt similarity index 90% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/SearchQuery.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SearchQuery.kt index 0905a69af..dbf9b3bab 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SearchQuery.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SearchQuery.kt @@ -4,9 +4,9 @@ import java.util.Locale internal class SearchQuery(input: String) { private val parts = input.replace(whiteSpaceRegex, " ") - .split(' ') - .flatMap { it.replace(searchNoiseRegex, "").splitToParts() } - .map { it.lowercase(Locale.ROOT) } + .split(' ') + .flatMap { it.replace(searchNoiseRegex, "").splitToParts() } + .map { it.lowercase(Locale.ROOT) } private val joinedParts = parts.joinToString("") fun isNotEmpty() = parts.isNotEmpty() diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SearchViewModel.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SearchViewModel.kt similarity index 94% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/SearchViewModel.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SearchViewModel.kt index aed6ae030..32530218f 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SearchViewModel.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SearchViewModel.kt @@ -20,8 +20,8 @@ internal class SearchViewModel : ViewModel() { private val uiModelChanges = MutableSharedFlow<(UiModel) -> UiModel>() private val sharedUiModels = uiModelChanges.scan( - initial = UiModel(Idle, SearchQuery.Empty), - operation = { currentModel, updateModel -> updateModel(currentModel) } + initial = UiModel(Idle, SearchQuery.Empty), + operation = { currentModel, updateModel -> updateModel(currentModel) }, ).shareIn(viewModelScope, SharingStarted.Lazily, replay = 1).distinctUntilChanged() val uiModels: Flow = sharedUiModels @@ -59,7 +59,9 @@ internal class SearchViewModel : ViewModel() { sealed class Event { object OpenSearch : Event() + object CloseSearch : Event() + class UpdateQuery(val query: String) : Event() } diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SectionAdapter.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SectionAdapter.kt similarity index 100% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/SectionAdapter.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SectionAdapter.kt diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SectionFragment.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SectionFragment.kt similarity index 93% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/SectionFragment.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SectionFragment.kt index 6c9aef854..77fbd0e1f 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SectionFragment.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SectionFragment.kt @@ -36,7 +36,10 @@ internal class SectionFragment : Fragment(R.layout.io_mehow_laboratory_feature_g } }) - override fun onViewCreated(view: View, inState: Bundle?) { + override fun onViewCreated( + view: View, + inState: Bundle?, + ) { view.findViewById(R.id.io_mehow_laboratory_feature_section).apply { layoutManager = SmoothScrollingLinearLayoutManager(requireActivity()).also { this@SectionFragment.layoutManager = it @@ -48,8 +51,8 @@ internal class SectionFragment : Fragment(R.layout.io_mehow_laboratory_feature_g } private fun observeGroup() = inspectorViewModel.sectionFlow(sectionName) - .onEach { featureAdapter.submitList(it) } - .launchIn(viewLifecycleOwner.lifecycleScope) + .onEach { featureAdapter.submitList(it) } + .launchIn(viewLifecycleOwner.lifecycleScope) fun scrollTo(index: Int) = layoutManager.smoothScrollTo(index) diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SmoothScrollingLinearLayoutManager.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SmoothScrollingLinearLayoutManager.kt similarity index 100% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/SmoothScrollingLinearLayoutManager.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SmoothScrollingLinearLayoutManager.kt diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SourceAdapter.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SourceAdapter.kt similarity index 87% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/SourceAdapter.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SourceAdapter.kt index 36c5886e0..52816757f 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SourceAdapter.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SourceAdapter.kt @@ -10,7 +10,11 @@ import io.mehow.laboratory.Feature internal class SourceAdapter( private val features: List>, ) : BaseAdapter() { - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + override fun getView( + position: Int, + convertView: View?, + parent: ViewGroup, + ): View { val inflater = LayoutInflater.from(parent.context) val viewHolder = if (convertView == null) { val view = inflater.inflate(R.layout.io_mehow_laboratory_feature_source_spinner_item, parent, false) @@ -22,7 +26,11 @@ internal class SourceAdapter( return viewHolder.item } - override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { + override fun getDropDownView( + position: Int, + convertView: View?, + parent: ViewGroup, + ): View { val inflater = LayoutInflater.from(parent.context) val viewHolder = if (convertView == null) { val view = inflater.inflate(R.layout.io_mehow_laboratory_feature_source_drop_down_item, parent, false) diff --git a/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SourceViewGroup.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SourceViewGroup.kt new file mode 100644 index 000000000..e58bc4b54 --- /dev/null +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SourceViewGroup.kt @@ -0,0 +1,61 @@ +package io.mehow.laboratory.inspector + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.AdapterView +import androidx.appcompat.widget.AppCompatSpinner +import io.mehow.laboratory.Feature +import androidx.appcompat.R as AppCompatR + +internal class SourceViewGroup + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet, + defStyle: Int = AppCompatR.attr.spinnerStyle, + ) : AppCompatSpinner(context, attrs, defStyle) { + internal var listener: OnSelectSourceListener? = null + + override fun getAdapter() = super.getAdapter() as? SourceAdapter + + fun setOnSelectSourceListener(listener: OnSelectSourceListener?) { + this.listener = listener + } + + fun render(models: List) { + val features = models.map(OptionUiModel::option) + val selectedFeature = models.firstOrNull(OptionUiModel::isSelected)?.option ?: return + val newAdapter = SourceAdapter(features) + onItemSelectedListener = createListener() + adapter = newAdapter + val position = newAdapter.positionOf(selectedFeature) + setSelection(position) + } + + private fun createListener() = object : OnItemSelectedListener { + var ignoreItem = true + + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long, + ) { + if (ignoreItem) { + ignoreItem = false + return + } + val item = requireNotNull(adapter) { + "Feature source adapter is not set" + }.getItem(position) + listener?.onSelectSource(item) + } + + override fun onNothingSelected(parent: AdapterView<*>?) = Unit + } + + interface OnSelectSourceListener { + fun onSelectSource(option: Feature<*>) + } + } diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SourceViewHolder.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SourceViewHolder.kt similarity index 100% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/SourceViewHolder.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SourceViewHolder.kt diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/TextToken.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/TextToken.kt similarity index 92% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/TextToken.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/TextToken.kt index 20b06b154..f256dfd29 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/TextToken.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/TextToken.kt @@ -33,9 +33,9 @@ internal fun String.tokenize(): List { val regularTokens = matches.toRegularTokens(this) val linkTokens = matches.toLinkTokens() return (regularTokens + linkTokens) - .sortedBy { (_, startIndex) -> startIndex } - .map { (token, _) -> token } - .toList() + .sortedBy { (_, startIndex) -> startIndex } + .map { (token, _) -> token } + .toList() } private fun Sequence.toLinkTokens() = map { matchResult -> @@ -44,7 +44,7 @@ private fun Sequence.toLinkTokens() = map { matchResult -> } private fun Sequence.toRegularTokens(text: String) = toUnmatchedRanges(text) - .map { range -> Regular(text.substring(range)) to range.first } + .map { range -> Regular(text.substring(range)) to range.first } private fun Sequence.toUnmatchedRanges(text: String) = sequence { yield(Int.MIN_VALUE..0) diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/ToolbarBinding.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/ToolbarBinding.kt similarity index 81% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/ToolbarBinding.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/ToolbarBinding.kt index d680eca60..763b5f4d4 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/ToolbarBinding.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/ToolbarBinding.kt @@ -25,11 +25,11 @@ internal class ToolbarBinding( private val resetFeatures = view.findViewById(R.id.io_mehow_laboratory_reset_features) private val resetFeaturesDialog = MaterialAlertDialogBuilder(view.context) - .setTitle(R.string.io_mehow_laboratory_reset_title) - .setMessage(R.string.io_mehow_laboratory_reset_message) - .setNegativeButton(R.string.io_mehow_laboratory_cancel) { _, _ -> } - .setPositiveButton(R.string.io_mehow_laboratory_reset) { _, _ -> onResetEventsListener() } - .create() + .setTitle(R.string.io_mehow_laboratory_reset_title) + .setMessage(R.string.io_mehow_laboratory_reset_message) + .setNegativeButton(R.string.io_mehow_laboratory_cancel) { _, _ -> } + .setPositiveButton(R.string.io_mehow_laboratory_reset) { _, _ -> onResetEventsListener() } + .create() init { var oldText: String? = null @@ -41,8 +41,20 @@ internal class ToolbarBinding( onSearchEventsListener(UpdateQuery(query)) } } - override fun beforeTextChanged(text: CharSequence?, start: Int, count: Int, after: Int) = Unit - override fun onTextChanged(text: CharSequence?, start: Int, before: Int, count: Int) = Unit + + override fun beforeTextChanged( + text: CharSequence?, + start: Int, + count: Int, + after: Int, + ) = Unit + + override fun onTextChanged( + text: CharSequence?, + start: Int, + before: Int, + count: Int, + ) = Unit }) openSearch.setOnClickListener { onSearchEventsListener(OpenSearch) } closeSearch.setOnClickListener { onSearchEventsListener(CloseSearch) } diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/Views.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/Views.kt similarity index 84% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/Views.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/Views.kt index f26a792a3..170375341 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/Views.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/Views.kt @@ -14,9 +14,11 @@ import kotlin.math.absoluteValue internal fun View.focusAndShowKeyboard() { fun View.showKeyboardIfFocused() { - if (isFocused) post { - val service = context.getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager - service?.showSoftInput(this, SHOW_IMPLICIT) + if (isFocused) { + post { + val service = context.getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager + service?.showSoftInput(this, SHOW_IMPLICIT) + } } } @@ -29,7 +31,9 @@ internal fun View.focusAndShowKeyboard() { } } viewTreeObserver.addOnWindowFocusChangeListener(listener) - } else showKeyboardIfFocused() + } else { + showKeyboardIfFocused() + } } internal fun View.hideKeyboard() { @@ -42,7 +46,11 @@ internal fun RecyclerView.hideKeyboardOnScroll() { val touchSlop = ViewConfiguration.get(context).scaledTouchSlop var totalDy = 0 addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + override fun onScrolled( + recyclerView: RecyclerView, + dx: Int, + dy: Int, + ) { totalDy += dy.absoluteValue if (totalDy >= touchSlop) { totalDy = 0 diff --git a/library/inspector/src/main/res/color/io_mehow_laboratory_chip_background.xml b/laboratory/inspector/src/main/res/color/io_mehow_laboratory_chip_background.xml similarity index 100% rename from library/inspector/src/main/res/color/io_mehow_laboratory_chip_background.xml rename to laboratory/inspector/src/main/res/color/io_mehow_laboratory_chip_background.xml diff --git a/library/inspector/src/main/res/color/io_mehow_laboratory_chip_text.xml b/laboratory/inspector/src/main/res/color/io_mehow_laboratory_chip_text.xml similarity index 100% rename from library/inspector/src/main/res/color/io_mehow_laboratory_chip_text.xml rename to laboratory/inspector/src/main/res/color/io_mehow_laboratory_chip_text.xml diff --git a/library/inspector/src/main/res/color/io_mehow_laboratory_hint_text.xml b/laboratory/inspector/src/main/res/color/io_mehow_laboratory_hint_text.xml similarity index 100% rename from library/inspector/src/main/res/color/io_mehow_laboratory_hint_text.xml rename to laboratory/inspector/src/main/res/color/io_mehow_laboratory_hint_text.xml diff --git a/library/inspector/src/main/res/drawable/io_mehow_laboratory_clear.xml b/laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_clear.xml similarity index 100% rename from library/inspector/src/main/res/drawable/io_mehow_laboratory_clear.xml rename to laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_clear.xml diff --git a/library/inspector/src/main/res/drawable/io_mehow_laboratory_close_serach.xml b/laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_close_serach.xml similarity index 100% rename from library/inspector/src/main/res/drawable/io_mehow_laboratory_close_serach.xml rename to laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_close_serach.xml diff --git a/library/inspector/src/main/res/drawable/io_mehow_laboratory_hint_cursor.xml b/laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_hint_cursor.xml similarity index 100% rename from library/inspector/src/main/res/drawable/io_mehow_laboratory_hint_cursor.xml rename to laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_hint_cursor.xml diff --git a/library/inspector/src/main/res/drawable/io_mehow_laboratory_open_serach.xml b/laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_open_serach.xml similarity index 100% rename from library/inspector/src/main/res/drawable/io_mehow_laboratory_open_serach.xml rename to laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_open_serach.xml diff --git a/library/inspector/src/main/res/drawable/io_mehow_laboratory_reset_features.xml b/laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_reset_features.xml similarity index 100% rename from library/inspector/src/main/res/drawable/io_mehow_laboratory_reset_features.xml rename to laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_reset_features.xml diff --git a/library/inspector/src/main/res/drawable/io_mehow_laboratory_supervisor.xml b/laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_supervisor.xml similarity index 100% rename from library/inspector/src/main/res/drawable/io_mehow_laboratory_supervisor.xml rename to laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_supervisor.xml diff --git a/library/inspector/src/main/res/layout/io_mehow_laboratory_feature_group.xml b/laboratory/inspector/src/main/res/layout/io_mehow_laboratory_feature_group.xml similarity index 100% rename from library/inspector/src/main/res/layout/io_mehow_laboratory_feature_group.xml rename to laboratory/inspector/src/main/res/layout/io_mehow_laboratory_feature_group.xml diff --git a/library/inspector/src/main/res/layout/io_mehow_laboratory_feature_item.xml b/laboratory/inspector/src/main/res/layout/io_mehow_laboratory_feature_item.xml similarity index 100% rename from library/inspector/src/main/res/layout/io_mehow_laboratory_feature_item.xml rename to laboratory/inspector/src/main/res/layout/io_mehow_laboratory_feature_item.xml diff --git a/library/inspector/src/main/res/layout/io_mehow_laboratory_feature_option_chip.xml b/laboratory/inspector/src/main/res/layout/io_mehow_laboratory_feature_option_chip.xml similarity index 100% rename from library/inspector/src/main/res/layout/io_mehow_laboratory_feature_option_chip.xml rename to laboratory/inspector/src/main/res/layout/io_mehow_laboratory_feature_option_chip.xml diff --git a/library/inspector/src/main/res/layout/io_mehow_laboratory_feature_source_drop_down_item.xml b/laboratory/inspector/src/main/res/layout/io_mehow_laboratory_feature_source_drop_down_item.xml similarity index 100% rename from library/inspector/src/main/res/layout/io_mehow_laboratory_feature_source_drop_down_item.xml rename to laboratory/inspector/src/main/res/layout/io_mehow_laboratory_feature_source_drop_down_item.xml diff --git a/library/inspector/src/main/res/layout/io_mehow_laboratory_feature_source_spinner_item.xml b/laboratory/inspector/src/main/res/layout/io_mehow_laboratory_feature_source_spinner_item.xml similarity index 100% rename from library/inspector/src/main/res/layout/io_mehow_laboratory_feature_source_spinner_item.xml rename to laboratory/inspector/src/main/res/layout/io_mehow_laboratory_feature_source_spinner_item.xml diff --git a/library/inspector/src/main/res/layout/io_mehow_laboratory_inspector.xml b/laboratory/inspector/src/main/res/layout/io_mehow_laboratory_inspector.xml similarity index 100% rename from library/inspector/src/main/res/layout/io_mehow_laboratory_inspector.xml rename to laboratory/inspector/src/main/res/layout/io_mehow_laboratory_inspector.xml diff --git a/library/inspector/src/main/res/values-night/dimens.xml b/laboratory/inspector/src/main/res/values-night/dimens.xml similarity index 100% rename from library/inspector/src/main/res/values-night/dimens.xml rename to laboratory/inspector/src/main/res/values-night/dimens.xml diff --git a/library/inspector/src/main/res/values-night/themes.xml b/laboratory/inspector/src/main/res/values-night/themes.xml similarity index 100% rename from library/inspector/src/main/res/values-night/themes.xml rename to laboratory/inspector/src/main/res/values-night/themes.xml diff --git a/library/inspector/src/main/res/values/dimens.xml b/laboratory/inspector/src/main/res/values/dimens.xml similarity index 100% rename from library/inspector/src/main/res/values/dimens.xml rename to laboratory/inspector/src/main/res/values/dimens.xml diff --git a/library/inspector/src/main/res/values/public.xml b/laboratory/inspector/src/main/res/values/public.xml similarity index 100% rename from library/inspector/src/main/res/values/public.xml rename to laboratory/inspector/src/main/res/values/public.xml diff --git a/library/inspector/src/main/res/values/strings.xml b/laboratory/inspector/src/main/res/values/strings.xml similarity index 100% rename from library/inspector/src/main/res/values/strings.xml rename to laboratory/inspector/src/main/res/values/strings.xml diff --git a/library/inspector/src/main/res/values/styles.xml b/laboratory/inspector/src/main/res/values/styles.xml similarity index 100% rename from library/inspector/src/main/res/values/styles.xml rename to laboratory/inspector/src/main/res/values/styles.xml diff --git a/library/inspector/src/main/res/values/themes.xml b/laboratory/inspector/src/main/res/values/themes.xml similarity index 100% rename from library/inspector/src/main/res/values/themes.xml rename to laboratory/inspector/src/main/res/values/themes.xml diff --git a/library/inspector/src/test/java/io/mehow/laboratory/inspector/FlowTurbines.kt b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/FlowTurbines.kt similarity index 68% rename from library/inspector/src/test/java/io/mehow/laboratory/inspector/FlowTurbines.kt rename to laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/FlowTurbines.kt index 9c16f0371..58a5891ba 100644 --- a/library/inspector/src/test/java/io/mehow/laboratory/inspector/FlowTurbines.kt +++ b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/FlowTurbines.kt @@ -1,10 +1,10 @@ package io.mehow.laboratory.inspector -import app.cash.turbine.FlowTurbine +import app.cash.turbine.TurbineTestContext import kotlinx.coroutines.withTimeout -internal suspend fun FlowTurbine.awaitItemEventually( - timeoutMs: Long = this.timeoutMs, +suspend fun TurbineTestContext.awaitItemEventually( + timeoutMs: Long = 1000L, assertion: (T) -> Unit, ) = withTimeout(timeoutMs) { while (true) { diff --git a/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelDeprecationSpec.kt b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelDeprecationSpec.kt new file mode 100644 index 000000000..6e5f9c631 --- /dev/null +++ b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelDeprecationSpec.kt @@ -0,0 +1,174 @@ +package io.mehow.laboratory.inspector + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldContainExactly +import io.mehow.laboratory.Feature +import io.mehow.laboratory.FeatureFactory +import io.mehow.laboratory.Laboratory +import io.mehow.laboratory.inspector.DeprecationAlignment.Bottom +import io.mehow.laboratory.inspector.DeprecationAlignment.Regular +import io.mehow.laboratory.inspector.DeprecationPhenotype.Hide +import io.mehow.laboratory.inspector.DeprecationPhenotype.Show +import io.mehow.laboratory.inspector.DeprecationPhenotype.Strikethrough +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.first +import kotlin.DeprecationLevel.ERROR +import kotlin.DeprecationLevel.HIDDEN +import kotlin.DeprecationLevel.WARNING + +class InspectorViewModelDeprecationSpec : FunSpec({ + setMainDispatcher() + + test("can be filtered out") { + val viewModel = InspectorViewModel( + DeprecationHandler( + phenotypeSelector = { Hide }, + alignmentSelector = { Regular }, + ), + ) + + val featureNames = viewModel.sectionFlow().first().map(FeatureUiModel::name) + + featureNames shouldContainExactly listOf("NotDeprecated") + } + + test("can be struck through") { + val viewModel = InspectorViewModel( + DeprecationHandler( + phenotypeSelector = { Strikethrough }, + alignmentSelector = { Regular }, + ), + ) + + val featureNames = viewModel.sectionFlow().first().map { it.name to it.deprecationPhenotype } + + featureNames shouldContainExactly listOf( + "DeprecatedError" to Strikethrough, + "DeprecatedHidden" to Strikethrough, + "DeprecatedWarning" to Strikethrough, + "NotDeprecated" to null, + ) + } + + test("can be shown") { + val viewModel = InspectorViewModel( + DeprecationHandler( + phenotypeSelector = { Show }, + alignmentSelector = { Regular }, + ), + ) + + val featureNames = viewModel.sectionFlow().first().map { it.name to it.deprecationPhenotype } + + featureNames shouldContainExactly listOf( + "DeprecatedError" to Show, + "DeprecatedHidden" to Show, + "DeprecatedWarning" to Show, + "NotDeprecated" to null, + ) + } + + test("can be moved to bottom") { + val viewModel = InspectorViewModel( + DeprecationHandler( + phenotypeSelector = { Show }, + alignmentSelector = { Bottom }, + ), + ) + + val featureNames = viewModel.sectionFlow().first().map { it.name to it.deprecationPhenotype } + + featureNames shouldContainExactly listOf( + "NotDeprecated" to null, + "DeprecatedError" to Show, + "DeprecatedHidden" to Show, + "DeprecatedWarning" to Show, + ) + } + + test("can be selected based on deprecation level") { + val viewModel = InspectorViewModel( + DeprecationHandler( + phenotypeSelector = { if (it == WARNING) Strikethrough else Show }, + alignmentSelector = { if (it != WARNING) Bottom else Regular }, + ), + ) + + val featureNames = viewModel.sectionFlow().first().map { it.name to it.deprecationPhenotype } + + featureNames shouldContainExactly listOf( + "DeprecatedWarning" to Strikethrough, + "NotDeprecated" to null, + "DeprecatedError" to Show, + "DeprecatedHidden" to Show, + ) + } +}) + +private object DeprecatedFeatureFactory : FeatureFactory { + override fun create(): Set>> { + @Suppress("UNCHECKED_CAST") + return setOf( + Class.forName("io.mehow.laboratory.inspector.DeprecatedWarning"), + Class.forName("io.mehow.laboratory.inspector.DeprecatedError"), + Class.forName("io.mehow.laboratory.inspector.DeprecatedHidden"), + Class.forName("io.mehow.laboratory.inspector.NotDeprecated"), + ) as Set>> + } +} + +@Deprecated("", level = WARNING) +private enum class DeprecatedWarning : Feature< + @Suppress("DEPRECATION") + DeprecatedWarning, +> { + Option, + ; + + @Suppress("DEPRECATION") + override val defaultOption: DeprecatedWarning + get() = Option +} + +@Deprecated("message", level = ERROR) +private enum class DeprecatedError : Feature< + @Suppress("DEPRECATION_ERROR") + DeprecatedError, +> { + Option, + ; + + @Suppress("DEPRECATION_ERROR") + override val defaultOption: DeprecatedError + get() = Option +} + +@Deprecated("", level = HIDDEN) +private enum class DeprecatedHidden : Feature< + @Suppress("DEPRECATION_ERROR") + DeprecatedHidden, +> { + Option, + ; + + @Suppress("DEPRECATION_ERROR") + override val defaultOption: DeprecatedHidden + get() = Option +} + +private enum class NotDeprecated : Feature { + Option, + ; + + override val defaultOption: NotDeprecated + get() = Option +} + +private fun InspectorViewModel( + deprecationHandler: DeprecationHandler, +) = InspectorViewModel( + Laboratory.inMemory(), + emptyFlow(), + DeprecatedFeatureFactory, + deprecationHandler, +) diff --git a/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelFeatureSpec.kt b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelFeatureSpec.kt new file mode 100644 index 000000000..3892e2383 --- /dev/null +++ b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelFeatureSpec.kt @@ -0,0 +1,318 @@ +package io.mehow.laboratory.inspector + +import app.cash.turbine.test +import io.kotest.assertions.fail +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.collections.shouldNotContain +import io.mehow.laboratory.DefaultOptionFactory +import io.mehow.laboratory.Feature +import io.mehow.laboratory.FeatureFactory +import io.mehow.laboratory.FeatureStorage +import io.mehow.laboratory.Laboratory +import io.mehow.laboratory.inspector.TextToken.Link +import io.mehow.laboratory.inspector.TextToken.Regular +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.first + +class InspectorViewModelFeatureSpec : FunSpec({ + setMainDispatcher() + + test("filters empty feature flag groups") { + val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) + + val featureNames = viewModel.sectionFlow().first().map(FeatureUiModel::name) + + featureNames shouldNotContain "Empty" + } + + test("orders feature flag groups by name") { + val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) + + val featureNames = viewModel.sectionFlow().first().map(FeatureUiModel::name) + + featureNames shouldContainExactly listOf("First", "Second") + } + + test("does not order feature flag options") { + val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) + + val features = viewModel.sectionFlow().first() + .map(FeatureUiModel::models) + .map { models -> models.map(OptionUiModel::option) } + + features[0] shouldContainExactly listOf(First.C, First.B, First.A) + features[1] shouldContainExactly listOf(Second.B, Second.C, Second.A) + } + + test("marks first feature flag option as selected by default") { + val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) + + viewModel.observeSelectedFeatures().first() shouldContainExactly listOf(First.C, Second.B) + } + + test("marks saved feature flag options as selected") { + val laboratory = Laboratory.inMemory().apply { + setOption(First.A) + setOption(Second.C) + } + + val viewModel = InspectorViewModel(laboratory, NoSourceFeatureFactory) + + viewModel.observeSelectedFeatures().first() shouldContainExactly listOf(First.A, Second.C) + } + + test("selects feature flag options") { + val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) + + viewModel.selectFeature(First.B) + viewModel.selectFeature(Second.A) + + viewModel.observeSelectedFeatures().first() shouldContainExactly listOf(First.B, Second.A) + } + + test("emits feature flag changes") { + val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) + + viewModel.observeSelectedFeatures().test { + awaitItem() shouldContainExactly listOf(First.C, Second.B) + + viewModel.selectFeature(First.B) + awaitItem() shouldContainExactly listOf(First.B, Second.B) + + viewModel.selectFeature(Second.C) + awaitItem() shouldContainExactly listOf(First.B, Second.C) + + cancel() + } + } + + test("emits source changes") { + val viewModel = InspectorViewModel(Laboratory.inMemory(), AllFeatureFactory) + + viewModel.observeSelectedFeaturesAndSources().test { + awaitItem() shouldContainExactly listOf( + First.C to null, + Second.B to null, + Sourced.A to Sourced.Source.Local, + ) + + viewModel.selectFeature(Sourced.Source.Remote) + + awaitItem() shouldContainExactly listOf( + First.C to null, + Second.B to null, + Sourced.A to Sourced.Source.Remote, + ) + } + } + + test("resets feature flags to default options declared in factory") { + val defaultOptionFactory = object : DefaultOptionFactory { + override fun > create(feature: T): Feature<*>? = when (feature) { + is First -> First.A + is Second -> Second.A + else -> null + } + } + val laboratory = Laboratory.builder() + .featureStorage(FeatureStorage.inMemory()) + .defaultOptionFactory(defaultOptionFactory) + .build() + val viewModel = InspectorViewModel(laboratory, NoSourceFeatureFactory) + + viewModel.observeSelectedFeatures().test { + awaitItem() shouldContainExactly listOf(First.A, Second.A) + + viewModel.selectFeature(First.B) + awaitItem() shouldContainExactly listOf(First.B, Second.A) + + viewModel.selectFeature(Second.B) + awaitItem() shouldContainExactly listOf(First.B, Second.B) + + laboratory.clear() + awaitItemEventually { it shouldContainExactly listOf(First.A, Second.A) } + + cancel() + } + } + + test("uses text tokens for feature flag description") { + val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) + + val descriptions = viewModel.sectionFlow().first().map(FeatureUiModel::description) + + descriptions shouldContainExactly listOf( + listOf( + Regular("Description with a "), + Link("link", "https://mehow.io"), + ), + listOf( + Regular("Description without a link"), + ), + ) + } + + test("emits feature flags supervision") { + val viewModel = InspectorViewModel(Laboratory.inMemory(), SupervisedFeatureFactory) + + viewModel.observeSelectedFeaturesAndEnabledState().test { + awaitItem() shouldContainExactly listOf( + Child.A to false, + Parent.Disabled to true, + ) + + viewModel.selectFeature(Parent.Enabled) + awaitItemEventually { + it shouldContainExactly listOf( + Child.A to true, + Parent.Enabled to true, + ) + } + + viewModel.selectFeature(Child.B) + awaitItem() shouldContainExactly listOf( + Child.B to true, + Parent.Enabled to true, + ) + + viewModel.selectFeature(Parent.Disabled) + awaitItemEventually { + it shouldContainExactly listOf( + Child.A to false, + Parent.Disabled to true, + ) + } + + cancel() + } + } + + test("includes supervised features to options") { + val viewModel = InspectorViewModel(Laboratory.inMemory(), SupervisedFeatureFactory) + + viewModel.supervisedFeaturesFlow().first() shouldContainExactly listOf( + Child.A to emptyList(), + Child.B to emptyList(), + Parent.Enabled to listOf(Child::class.java), + Parent.Disabled to emptyList(), + ) + } + + test("includes supervised features to options from different sections") { + val parentFactory = object : FeatureFactory { + override fun create(): Set>> = setOf(Parent::class.java) + } + val childFactory = object : FeatureFactory { + override fun create(): Set>> = setOf(Child::class.java) + } + + val viewModel = InspectorViewModel( + Laboratory.inMemory(), + searchQueries = emptyFlow(), + mapOf("Parent" to parentFactory, "Child" to childFactory), + DeprecationHandler({ fail("Unexpected call") }, { fail("Unexpected call") }), + Dispatchers.Unconfined, + ) + + viewModel.supervisedFeaturesFlow("Parent").first() shouldContainExactly listOf( + Parent.Enabled to listOf(Child::class.java), + Parent.Disabled to emptyList(), + ) + } +}) + +private object NoSourceFeatureFactory : FeatureFactory { + override fun create(): Set>> = setOf( + Second::class.java, + First::class.java, + Empty::class.java, + ) +} + +private object SourcedFeatureFactory : FeatureFactory { + override fun create(): Set>> = setOf(Sourced::class.java) +} + +private object AllFeatureFactory : FeatureFactory { + override fun create() = NoSourceFeatureFactory.create() + SourcedFeatureFactory.create() +} + +private object SupervisedFeatureFactory : FeatureFactory { + override fun create(): Set>> = setOf( + Parent::class.java, + Child::class.java, + ) +} + +private enum class First : Feature { + C, + B, + A, + ; + + override val defaultOption get() = C + + override val description = "Description with a [link](https://mehow.io)" +} + +private enum class Second : Feature { + B, + C, + A, + ; + + override val defaultOption get() = B + + override val description = "Description without a link" +} + +private enum class Empty : Feature + +private enum class Sourced : Feature { + A, + B, + C, + ; + + override val defaultOption get() = A + + override val source = Source::class.java + + enum class Source : Feature { + Local, + Remote, + ; + + override val defaultOption get() = Local + } +} + +private enum class Parent : Feature { + Enabled, + Disabled, + ; + + override val defaultOption get() = Disabled +} + +private enum class Child : Feature { + A, + B, + ; + + override val defaultOption get() = A + + override val supervisorOption get() = Parent.Enabled +} + +private fun InspectorViewModel( + laboratory: Laboratory, + factory: FeatureFactory, +) = InspectorViewModel( + laboratory, + emptyFlow(), + factory, + DeprecationHandler({ fail("Unexpected call") }, { fail("Unexpected call") }), +) diff --git a/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelFilterSpec.kt b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelFilterSpec.kt new file mode 100644 index 000000000..28368711b --- /dev/null +++ b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelFilterSpec.kt @@ -0,0 +1,260 @@ +package io.mehow.laboratory.inspector + +import app.cash.turbine.TurbineTestContext +import app.cash.turbine.test +import io.kotest.assertions.fail +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.property.Arb +import io.kotest.property.arbitrary.stringPattern +import io.kotest.property.checkAll +import io.mehow.laboratory.Feature +import io.mehow.laboratory.FeatureFactory +import io.mehow.laboratory.Laboratory +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlin.reflect.KClass + +class InspectorViewModelFilterSpec : FunSpec({ + setMainDispatcher() + + test("emits all feature flags for no search terms") { + val searchFlow = MutableSharedFlow() + InspectorViewModel(searchFlow).observeFeatureClasses().test { + expectAllFeatureFlags() + + cancel() + } + } + + test("emits all feature flags for blank search terms") { + checkAll(Arb.stringPattern("([ ]{0,10})")) { query -> + val searchFlow = MutableSharedFlow() + InspectorViewModel(searchFlow).observeFeatureClasses().test { + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery(query)) + expectNoEvents() + + cancel() + } + } + } + + test("finds feature flags by their exact name") { + val searchFlow = MutableSharedFlow() + InspectorViewModel(searchFlow).observeFeatureClasses().test { + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery("RegularNameFeature")) + awaitItem() shouldContainExactly listOf(RegularNameFeature::class) + + searchFlow.emit(SearchQuery("Numbered1NameFeature")) + awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class) + + searchFlow.emit(SearchQuery("SourcedFeature")) + awaitItem() shouldContainExactly listOf(SourcedFeature::class) + + cancel() + } + } + + test("finds feature flags by split name parts") { + val searchFlow = MutableSharedFlow() + InspectorViewModel(searchFlow).observeFeatureClasses().test { + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery("Name Feature")) + awaitItem() shouldContainExactly listOf( + Numbered1NameFeature::class, + RegularNameFeature::class, + ) + + searchFlow.emit(SearchQuery("Numbered 1Name Feature")) + awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class) + + cancel() + } + } + + // Find out why checkAll(Arb.stringPattern("[!_?@*]{10,}")) { query -> } can fail on CI. + // It fails randomly with a timeout on a second event. Re-using generator seed does not help locally. + // Pattern in generator also doesn't matter as long as it produces valid input for the test. + test("finds no feature flags for no matches") { + val searchFlow = MutableSharedFlow() + InspectorViewModel(searchFlow).observeFeatureClasses().test { + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery("???")) + awaitItem() shouldContainExactly emptyList() + + cancel() + } + } + + test("finds feature flags by their options") { + val searchFlow = MutableSharedFlow() + InspectorViewModel(searchFlow).observeFeatureClasses().test { + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery("Disabled")) + awaitItem() shouldContainExactly listOf( + Numbered1NameFeature::class, + RegularNameFeature::class, + ) + + searchFlow.emit(SearchQuery("Howdy")) + awaitItem() shouldContainExactly listOf(SourcedFeature::class) + + cancel() + } + } + + test("finds feature flags by their sources") { + val searchFlow = MutableSharedFlow() + InspectorViewModel(searchFlow).observeFeatureClasses().test { + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery("Remote")) + awaitItem() shouldContainExactly listOf(SourcedFeature::class) + + cancel() + } + } + + test("finds feature flags by partial matches") { + val searchFlow = MutableSharedFlow() + InspectorViewModel(searchFlow).observeFeatureClasses().test { + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery("me ture")) + awaitItem() shouldContainExactly listOf( + Numbered1NameFeature::class, + RegularNameFeature::class, + ) + + searchFlow.emit(SearchQuery("ature")) + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery("ed ture")) + awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class, SourcedFeature::class) + + searchFlow.emit(SearchQuery("cal")) + awaitItem() shouldContainExactly listOf(SourcedFeature::class) + + cancel() + } + } + + test("finds feature flags by ordered input") { + val searchFlow = MutableSharedFlow() + InspectorViewModel(searchFlow).observeFeatureClasses().test { + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery("Enabled Disabled")) + awaitItem() shouldContainExactly listOf(RegularNameFeature::class) + + searchFlow.emit(SearchQuery("Disabled Enabled")) + awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class) + + cancel() + } + } + + test("ignores capitalization during search") { + val searchFlow = MutableSharedFlow() + InspectorViewModel(searchFlow).observeFeatureClasses().test { + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery("enabled")) + awaitItem() shouldContainExactly listOf( + Numbered1NameFeature::class, + RegularNameFeature::class, + ) + + searchFlow.emit(SearchQuery("feature")) + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery("local")) + awaitItem() shouldContainExactly listOf(SourcedFeature::class) + + cancel() + } + } + + test("finds feature flags by partial, non-split inner search") { + val searchFlow = MutableSharedFlow() + InspectorViewModel(searchFlow).observeFeatureClasses().test { + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery("arnamefea")) + awaitItem() shouldContainExactly listOf(RegularNameFeature::class) + + searchFlow.emit(SearchQuery("d1na")) + awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class) + + cancel() + } + } +}) + +private object SearchFeatureFactory : FeatureFactory { + override fun create(): Set>> = setOf( + RegularNameFeature::class.java, + Numbered1NameFeature::class.java, + SourcedFeature::class.java, + ) +} + +private enum class RegularNameFeature : Feature { + Enabled, + Disabled, + ; + + override val defaultOption get() = Disabled +} + +private enum class Numbered1NameFeature : Feature { + Disabled, + Enabled, + ; + + override val defaultOption get() = Disabled +} + +private enum class SourcedFeature : Feature { + Howdy, + There, + Partner, + ; + + override val defaultOption get() = Howdy + + @Suppress("UNCHECKED_CAST") + override val source = Source::class.java as Class> + + enum class Source : Feature { + Local, + Remote, + ; + + override val defaultOption get() = Local + } +} + +private fun InspectorViewModel( + searchFlow: Flow, +) = InspectorViewModel( + Laboratory.inMemory(), + searchFlow, + SearchFeatureFactory, + DeprecationHandler({ fail("Unexpected call") }, { fail("Unexpected call") }), +) + +private suspend fun TurbineTestContext>>>.expectAllFeatureFlags() { + awaitItem() shouldContainExactly listOf( + Numbered1NameFeature::class, + RegularNameFeature::class, + SourcedFeature::class, + ) +} diff --git a/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelNavigationSpec.kt b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelNavigationSpec.kt new file mode 100644 index 000000000..f84b8b5fb --- /dev/null +++ b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelNavigationSpec.kt @@ -0,0 +1,125 @@ +package io.mehow.laboratory.inspector + +import app.cash.turbine.test +import io.kotest.assertions.fail +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldBeIn +import io.kotest.matchers.shouldBe +import io.mehow.laboratory.Feature +import io.mehow.laboratory.FeatureFactory +import io.mehow.laboratory.Laboratory +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf + +@Suppress("UNCHECKED_CAST") +class InspectorViewModelNavigationSpec : FunSpec({ + setMainDispatcher() + + test("finds coordinates for registered feature flags") { + val viewModel = InspectorViewModel() + + viewModel.goTo(SectionOneFeatureA::class.java as Class>) shouldBe FeatureCoordinates(0, 0) + viewModel.goTo(SectionOneFeatureB::class.java as Class>) shouldBe FeatureCoordinates(0, 1) + viewModel.goTo(SectionTwoFeature::class.java as Class>) shouldBe FeatureCoordinates(1, 0) + } + + test("does not find coordinates for unregistered feature flags") { + val viewModel = InspectorViewModel() + + viewModel.goTo(UnregisteredFeature::class.java as Class>) shouldBe null + } + + test("does not find coordinates for filtered feature flags") { + val viewModel = InspectorViewModel(searchFlow = flowOf(SearchQuery("Foo"))) + + viewModel.goTo(SectionOneFeatureA::class.java as Class>) shouldBe null + } + + test("finds multiple coordinates for a feature registered twice") { + val viewModel = InspectorViewModel(mapOf("A1" to SectionAFactory, "A2" to SectionAFactory)) + + viewModel.goTo(SectionOneFeatureA::class.java as Class>) shouldBeIn listOf( + FeatureCoordinates(0, 0), + FeatureCoordinates(1, 0), + ) + } + + test("emits coordinates") { + val viewModel = InspectorViewModel() + + viewModel.featureCoordinatesFlow.test { + expectNoEvents() + + viewModel.goTo(SectionOneFeatureA::class.java as Class>) + awaitItem() shouldBe FeatureCoordinates(0, 0) + + cancel() + } + } + + test("does not cache emitted coordinates") { + val viewModel = InspectorViewModel() + + viewModel.goTo(SectionOneFeatureA::class.java as Class>) + + viewModel.featureCoordinatesFlow.test { + cancel() + } + } +}) + +private object SectionAFactory : FeatureFactory { + override fun create(): Set>> = setOf( + SectionOneFeatureA::class.java, + SectionOneFeatureB::class.java, + ) +} + +private object SectionBFactory : FeatureFactory { + override fun create(): Set>> = setOf(SectionTwoFeature::class.java) +} + +private enum class SectionOneFeatureA : Feature { + Option, + ; + + override val defaultOption: SectionOneFeatureA + get() = Option +} + +private enum class SectionOneFeatureB : Feature { + Option, + ; + + override val defaultOption: SectionOneFeatureB + get() = Option +} + +private enum class SectionTwoFeature : Feature { + Option, + ; + + override val defaultOption: SectionTwoFeature + get() = Option +} + +private enum class UnregisteredFeature : Feature { + Option, + ; + + override val defaultOption: UnregisteredFeature + get() = Option +} + +private fun InspectorViewModel( + featureFactories: Map = mapOf("A" to SectionAFactory, "B" to SectionBFactory), + searchFlow: Flow = emptyFlow(), +) = InspectorViewModel( + Laboratory.inMemory(), + searchFlow, + featureFactories, + DeprecationHandler({ fail("Unexpected call") }, { fail("Unexpected call") }), + Dispatchers.Unconfined, +) diff --git a/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorViewModels.kt b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModels.kt similarity index 90% rename from library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorViewModels.kt rename to laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModels.kt index 41d4684ef..988cfae19 100644 --- a/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorViewModels.kt +++ b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModels.kt @@ -32,18 +32,17 @@ internal fun InspectorViewModel.observeSelectedFeaturesAndEnabledState() = secti internal val InspectorViewModel.Companion.defaultSection get() = "section" -@Suppress("TestFunctionName") internal fun InspectorViewModel( laboratory: Laboratory, searchQueries: Flow, featureFactory: FeatureFactory, deprecationHandler: DeprecationHandler, ) = InspectorViewModel( - laboratory, - searchQueries, - mapOf(InspectorViewModel.defaultSection to featureFactory), - deprecationHandler, - Dispatchers.Unconfined, + laboratory, + searchQueries, + mapOf(InspectorViewModel.defaultSection to featureFactory), + deprecationHandler, + Dispatchers.Unconfined, ) internal fun InspectorViewModel.sectionFlow() = sectionFlow(InspectorViewModel.defaultSection) diff --git a/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/SearchViewModelSpec.kt b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/SearchViewModelSpec.kt new file mode 100644 index 000000000..4e8807ff0 --- /dev/null +++ b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/SearchViewModelSpec.kt @@ -0,0 +1,88 @@ +package io.mehow.laboratory.inspector + +import app.cash.turbine.TurbineTestContext +import app.cash.turbine.test +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mehow.laboratory.inspector.SearchMode.Active +import io.mehow.laboratory.inspector.SearchMode.Idle +import io.mehow.laboratory.inspector.SearchViewModel.Event.CloseSearch +import io.mehow.laboratory.inspector.SearchViewModel.Event.OpenSearch +import io.mehow.laboratory.inspector.SearchViewModel.Event.UpdateQuery +import io.mehow.laboratory.inspector.SearchViewModel.UiModel + +class SearchViewModelSpec : FunSpec({ + setMainDispatcher() + + test("starts with an idle state") { + SearchViewModel().uiModels.test { + expectIdleModel() + + cancel() + } + } + + test("can be opened") { + val viewModel = SearchViewModel() + viewModel.uiModels.test { + expectIdleModel() + + viewModel.sendEvent(OpenSearch) + awaitItem() shouldBe UiModel(Active, SearchQuery.Empty) + + cancel() + } + } + + test("updates search queries in active mode") { + val viewModel = SearchViewModel() + viewModel.uiModels.test { + expectIdleModel() + + viewModel.sendEvent(OpenSearch) + awaitItem() + + viewModel.sendEvent(UpdateQuery("Hello")) + awaitItem() shouldBe UiModel(Active, SearchQuery("Hello")) + + viewModel.sendEvent(UpdateQuery("World")) + awaitItem() shouldBe UiModel(Active, SearchQuery("World")) + + cancel() + } + } + + test("ignores queries in idle mode") { + val viewModel = SearchViewModel() + viewModel.uiModels.test { + expectIdleModel() + + viewModel.sendEvent(UpdateQuery("Hello")) + expectNoEvents() + + cancel() + } + } + + test("clears queries when search is closed") { + val viewModel = SearchViewModel() + viewModel.uiModels.test { + expectIdleModel() + + viewModel.sendEvent(OpenSearch) + awaitItem() + + viewModel.sendEvent(UpdateQuery("Hello")) + awaitItem() shouldBe UiModel(Active, SearchQuery("Hello")) + + viewModel.sendEvent(CloseSearch) + expectIdleModel() + + cancel() + } + } +}) + +private suspend fun TurbineTestContext.expectIdleModel() { + awaitItem() shouldBe UiModel(Idle, SearchQuery.Empty) +} diff --git a/library/inspector/src/test/java/io/mehow/laboratory/inspector/TestConfigurations.kt b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/TestConfigurations.kt similarity index 90% rename from library/inspector/src/test/java/io/mehow/laboratory/inspector/TestConfigurations.kt rename to laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/TestConfigurations.kt index 570ccdd88..f162e6ec5 100644 --- a/library/inspector/src/test/java/io/mehow/laboratory/inspector/TestConfigurations.kt +++ b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/TestConfigurations.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain @OptIn(ExperimentalCoroutinesApi::class) -internal fun TestConfiguration.setMainDispatcher( +fun TestConfiguration.setMainDispatcher( dispatcher: CoroutineDispatcher = Dispatchers.Unconfined, ) { beforeSpec { Dispatchers.setMain(dispatcher) } diff --git a/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/TextTokenSpec.kt b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/TextTokenSpec.kt new file mode 100644 index 000000000..8709eaa7c --- /dev/null +++ b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/TextTokenSpec.kt @@ -0,0 +1,77 @@ +package io.mehow.laboratory.inspector + +import io.kotest.core.spec.style.FunSpec +import io.kotest.data.blocking.forAll +import io.kotest.data.row +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContainExactly +import io.mehow.laboratory.inspector.TextToken.Link +import io.mehow.laboratory.inspector.TextToken.Regular + +class TextTokenSpec : FunSpec({ + test("can be empty") { + "".tokenize().shouldBeEmpty() + } + + test("can be blank") { + " ".tokenize() shouldContainExactly listOf(Regular(" ")) + } + + test("can have regular text") { + "Hello".tokenize() shouldContainExactly listOf(Regular("Hello")) + } + + test("can have a link") { + "[Hello](https://mehow.io)".tokenize() shouldContainExactly listOf( + Link("Hello", "https://mehow.io"), + ) + } + + test("can start with regular text followed by a link") { + "Hello [there](https://github.com/MiSikora/)".tokenize() shouldContainExactly listOf( + Regular("Hello "), + Link("there", "https://github.com/MiSikora/"), + ) + } + + test("can start with a link followed by a regular text") { + "[General](https://google.com) Kenobi".tokenize() shouldContainExactly listOf( + Link("General", "https://google.com"), + Regular(" Kenobi"), + ) + } + + test("can have multiple regular texts and links") { + val input = "Hello [there](https://github.com)… [General](https://sample.org) Kenobi" + input.tokenize() shouldContainExactly listOf( + Regular("Hello "), + Link("there", "https://github.com"), + Regular("… "), + Link("General", "https://sample.org"), + Regular(" Kenobi"), + ) + } + + test("can have multiple consecutive links") { + val input = "[One,](https://one.com)[ Two](https://two.com)[, Three](https://three.com)" + input.tokenize() shouldContainExactly listOf( + Link("One,", "https://one.com"), + Link(" Two", "https://two.com"), + Link(", Three", "https://three.com"), + ) + } + + test("ignores malformed link syntax") { + forAll( + row("[One[](https://one.com)"), + row("[One](https://one.com()"), + row("[](https://one.com"), + row("[One]()"), + row("[One]((https://one.com)"), + row("[O]ne](https://one.com)"), + row("[One](h(ttps://one.com)"), + ) { + it.tokenize() shouldContainExactly listOf(Regular(it)) + } + } +}) diff --git a/library/laboratory/api/laboratory.api b/laboratory/runtime/api/runtime.api similarity index 100% rename from library/laboratory/api/laboratory.api rename to laboratory/runtime/api/runtime.api diff --git a/laboratory/runtime/build.gradle.kts b/laboratory/runtime/build.gradle.kts new file mode 100644 index 000000000..98f98e065 --- /dev/null +++ b/laboratory/runtime/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.mavenPublish) + alias(libs.plugins.spotless) + alias(libs.plugins.detekt) + alias(libs.plugins.dokka) +} + +tasks.withType().configureEach { + useJUnitPlatform() +} + +dependencies { + api(libs.kotlinx.coroutines.core) + + testImplementation(libs.kotest.runner.junit5) + testImplementation(libs.kotest.assertions) + testImplementation(libs.turbine) +} diff --git a/library/laboratory/gradle.properties b/laboratory/runtime/gradle.properties similarity index 100% rename from library/laboratory/gradle.properties rename to laboratory/runtime/gradle.properties diff --git a/library/laboratory/src/main/java/io/mehow/laboratory/BlockingIoCall.kt b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/BlockingIoCall.kt similarity index 73% rename from library/laboratory/src/main/java/io/mehow/laboratory/BlockingIoCall.kt rename to laboratory/runtime/src/main/kotlin/io/mehow/laboratory/BlockingIoCall.kt index 8c5961a9e..25f23d4a9 100644 --- a/library/laboratory/src/main/java/io/mehow/laboratory/BlockingIoCall.kt +++ b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/BlockingIoCall.kt @@ -8,9 +8,9 @@ import kotlin.annotation.AnnotationTarget.FUNCTION * Opt-in annotation denoting that the used function can potentially block a calling thread with I/O operations. */ @RequiresOptIn( - message = "" + - "Used API can block a thread with IO operations. " + - "Either opt in to its usage or use a suspending equivalent." + message = "" + + "Used API can block a thread with IO operations. " + + "Either opt in to its usage or use a suspending equivalent.", ) @Retention(BINARY) @Target(CLASS, FUNCTION) diff --git a/library/laboratory/src/main/java/io/mehow/laboratory/BlockingLaboratory.kt b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/BlockingLaboratory.kt similarity index 100% rename from library/laboratory/src/main/java/io/mehow/laboratory/BlockingLaboratory.kt rename to laboratory/runtime/src/main/kotlin/io/mehow/laboratory/BlockingLaboratory.kt diff --git a/library/laboratory/src/main/java/io/mehow/laboratory/DefaultOptionFactory.kt b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/DefaultOptionFactory.kt similarity index 100% rename from library/laboratory/src/main/java/io/mehow/laboratory/DefaultOptionFactory.kt rename to laboratory/runtime/src/main/kotlin/io/mehow/laboratory/DefaultOptionFactory.kt diff --git a/library/laboratory/src/main/java/io/mehow/laboratory/Feature.kt b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/Feature.kt similarity index 100% rename from library/laboratory/src/main/java/io/mehow/laboratory/Feature.kt rename to laboratory/runtime/src/main/kotlin/io/mehow/laboratory/Feature.kt diff --git a/library/laboratory/src/main/java/io/mehow/laboratory/FeatureFactory.kt b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/FeatureFactory.kt similarity index 100% rename from library/laboratory/src/main/java/io/mehow/laboratory/FeatureFactory.kt rename to laboratory/runtime/src/main/kotlin/io/mehow/laboratory/FeatureFactory.kt diff --git a/library/laboratory/src/main/java/io/mehow/laboratory/FeatureStorage.kt b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/FeatureStorage.kt similarity index 97% rename from library/laboratory/src/main/java/io/mehow/laboratory/FeatureStorage.kt rename to laboratory/runtime/src/main/kotlin/io/mehow/laboratory/FeatureStorage.kt index 60073ae8e..ba4556d5c 100644 --- a/library/laboratory/src/main/java/io/mehow/laboratory/FeatureStorage.kt +++ b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/FeatureStorage.kt @@ -44,6 +44,7 @@ public interface FeatureStorage { * * @return `true` if the value was set successfully, `false` otherwise. */ + @Suppress("SpreadOperator") // Implementations override this to be more efficient public suspend fun setOptions(options: Collection>): Boolean = setOptions(*options.toTypedArray()) public companion object { diff --git a/library/laboratory/src/main/java/io/mehow/laboratory/InMemoryFeatureStorage.kt b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/InMemoryFeatureStorage.kt similarity index 95% rename from library/laboratory/src/main/java/io/mehow/laboratory/InMemoryFeatureStorage.kt rename to laboratory/runtime/src/main/kotlin/io/mehow/laboratory/InMemoryFeatureStorage.kt index 7f2328186..1390fe76b 100644 --- a/library/laboratory/src/main/java/io/mehow/laboratory/InMemoryFeatureStorage.kt +++ b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/InMemoryFeatureStorage.kt @@ -10,8 +10,8 @@ internal class InMemoryFeatureStorage : FeatureStorage { private val featureFlow = MutableStateFlow(emptyMap>, String>()) override fun observeFeatureName(feature: Class>) = featureFlow - .map { it[feature] } - .distinctUntilChanged() + .map { it[feature] } + .distinctUntilChanged() override suspend fun getFeatureName(feature: Class>) = featureFlow.map { it[feature] }.first() diff --git a/library/laboratory/src/main/java/io/mehow/laboratory/Laboratory.kt b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/Laboratory.kt similarity index 96% rename from library/laboratory/src/main/java/io/mehow/laboratory/Laboratory.kt rename to laboratory/runtime/src/main/kotlin/io/mehow/laboratory/Laboratory.kt index 5f1182cb0..890f6072c 100644 --- a/library/laboratory/src/main/java/io/mehow/laboratory/Laboratory.kt +++ b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/Laboratory.kt @@ -77,7 +77,14 @@ public class Laboratory internal constructor( val activeOption = options.firstOrNull { it.name == expectedName } ?: defaultOption val parent = feature.supervisorOption ?: return activeOption - return if (activeOption.supervisorOption != experimentRaw(parent.javaClass)) defaultOption else activeOption + return if (activeOption.supervisorOption != experimentRaw( + parent.javaClass, + ) + ) { + defaultOption + } else { + activeOption + } } /** @@ -85,7 +92,7 @@ public class Laboratory internal constructor( */ @Suppress("UNCHECKED_CAST") public suspend fun > experimentIs(option: T): Boolean = experimentRaw( - option::class.java as Class> + option::class.java as Class>, ) == option /** diff --git a/library/laboratory/src/main/java/io/mehow/laboratory/OptionFactory.kt b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/OptionFactory.kt similarity index 69% rename from library/laboratory/src/main/java/io/mehow/laboratory/OptionFactory.kt rename to laboratory/runtime/src/main/kotlin/io/mehow/laboratory/OptionFactory.kt index d21210017..0c4d58ddc 100644 --- a/library/laboratory/src/main/java/io/mehow/laboratory/OptionFactory.kt +++ b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/OptionFactory.kt @@ -7,15 +7,20 @@ public interface OptionFactory { /** * Returns a feature matching class name and option name or null if no match is found. */ - public fun create(key: String, name: String): Feature<*>? + public fun create( + key: String, + name: String, + ): Feature<*>? /** * Creates a new [OptionFactory] that will first look for an option in this factory and then in the * other factory. */ public operator fun plus(factory: OptionFactory): OptionFactory = object : OptionFactory { - override fun create(key: String, name: String) = - this@OptionFactory.create(key, name) ?: factory.create(key, name) + override fun create( + key: String, + name: String, + ) = this@OptionFactory.create(key, name) ?: factory.create(key, name) } public companion object diff --git a/library/laboratory/src/main/java/io/mehow/laboratory/SafeDefaultOptionFactory.kt b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/SafeDefaultOptionFactory.kt similarity index 100% rename from library/laboratory/src/main/java/io/mehow/laboratory/SafeDefaultOptionFactory.kt rename to laboratory/runtime/src/main/kotlin/io/mehow/laboratory/SafeDefaultOptionFactory.kt diff --git a/library/laboratory/src/main/java/io/mehow/laboratory/SourcedFeatureStorage.kt b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/SourcedFeatureStorage.kt similarity index 68% rename from library/laboratory/src/main/java/io/mehow/laboratory/SourcedFeatureStorage.kt rename to laboratory/runtime/src/main/kotlin/io/mehow/laboratory/SourcedFeatureStorage.kt index a3a83e59b..ede789634 100644 --- a/library/laboratory/src/main/java/io/mehow/laboratory/SourcedFeatureStorage.kt +++ b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/SourcedFeatureStorage.kt @@ -12,17 +12,17 @@ internal class SourcedFeatureStorage( defaultOptionFactory: DefaultOptionFactory? = null, ) : FeatureStorage { private val localLaboratory = Laboratory.builder() - .featureStorage(localSource) - .let { builder -> defaultOptionFactory?.let(builder::defaultOptionFactory) ?: builder } - .build() + .featureStorage(localSource) + .let { builder -> defaultOptionFactory?.let(builder::defaultOptionFactory) ?: builder } + .build() override fun observeFeatureName(feature: Class>) = feature.observeSource() - .map { source -> remoteSources[source.name] ?: localSource } - .onEmpty { emit(localSource) } - .let { - @OptIn(ExperimentalCoroutinesApi::class) - it.flatMapLatest { storage -> storage.observeFeatureName(feature) } - } + .map { source -> remoteSources[source.name] ?: localSource } + .onEmpty { emit(localSource) } + .let { + @OptIn(ExperimentalCoroutinesApi::class) + it.flatMapLatest { storage -> storage.observeFeatureName(feature) } + } override suspend fun getFeatureName(feature: Class>): String? { val storage = feature.getSource()?.let { remoteSources[it.name] } ?: localSource @@ -36,20 +36,20 @@ internal class SourcedFeatureStorage( override suspend fun clear() = localSource.clear() private fun > Class.observeSource() = validatedSource() - ?.let { localLaboratory.observe(it) } - ?: emptyFlow() + ?.let { localLaboratory.observe(it) } + ?: emptyFlow() private suspend fun > Class.getSource() = validatedSource() - ?.let { localLaboratory.experiment(it) } + ?.let { localLaboratory.experiment(it) } private fun > Class.validatedSource() = options - .firstOrNull() - ?.source - ?.takeUnless { it.options.isEmpty() } + .firstOrNull() + ?.source + ?.takeUnless { it.options.isEmpty() } fun withDefaultOptionFactory(factory: DefaultOptionFactory) = SourcedFeatureStorage( - localSource, - remoteSources, - factory, + localSource, + remoteSources, + factory, ) } diff --git a/library/laboratory/src/main/resources/META-INF/proguard/io-mehow-laboratory.pro b/laboratory/runtime/src/main/resources/META-INF/proguard/io-mehow-laboratory.pro similarity index 100% rename from library/laboratory/src/main/resources/META-INF/proguard/io-mehow-laboratory.pro rename to laboratory/runtime/src/main/resources/META-INF/proguard/io-mehow-laboratory.pro diff --git a/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/DefaultOptionFactorySpec.kt b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/DefaultOptionFactorySpec.kt new file mode 100644 index 000000000..035cfb3e7 --- /dev/null +++ b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/DefaultOptionFactorySpec.kt @@ -0,0 +1,63 @@ +package io.mehow.laboratory + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +class DefaultOptionFactorySpec : FunSpec() { + enum class FeatureA : Feature { + A, + B, + C, + ; + + override val defaultOption get() = A + } + + enum class FeatureB : Feature { + A, + B, + ; + + override val defaultOption get() = A + } + + enum class FeatureC : Feature { + A, + ; + + override val defaultOption get() = A + } + + init { + val firstFactory = object : DefaultOptionFactory { + override fun > create(feature: T): Feature<*>? = when (feature) { + is FeatureA -> FeatureA.C + else -> null + } + } + + val secondFactory = object : DefaultOptionFactory { + override fun > create(feature: T): Feature<*>? = when (feature) { + is FeatureA -> FeatureA.B + is FeatureB -> FeatureB.B + else -> null + } + } + + context("factory created from sum") { + val factory = firstFactory + secondFactory + + test("prioritizes first factory when option is available in it") { + factory.create(FeatureA.A) shouldBe FeatureA.C + } + + test("falls back to second factory when option is not available in first factory") { + factory.create(FeatureB.A) shouldBe FeatureB.B + } + + test("does not handle options unknown to any of sub-factories") { + factory.create(FeatureC.A) shouldBe null + } + } + } +} diff --git a/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/FeatureFactorySpec.kt b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/FeatureFactorySpec.kt new file mode 100644 index 000000000..01c19402e --- /dev/null +++ b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/FeatureFactorySpec.kt @@ -0,0 +1,41 @@ +package io.mehow.laboratory + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder + +class FeatureFactorySpec : FunSpec() { + enum class FeatureA : Feature { + A, + ; + + override val defaultOption get() = A + } + + enum class FeatureB : Feature { + A, + ; + + override val defaultOption get() = A + } + + init { + val firstFactory = object : FeatureFactory { + override fun create(): Set>> = setOf(FeatureA::class.java) + } + + val secondFactory = object : FeatureFactory { + override fun create(): Set>> = setOf(FeatureB::class.java) + } + + context("factory created from sum") { + val factory = firstFactory + secondFactory + + test("return features available in all sub-factories") { + factory.create() shouldContainExactlyInAnyOrder setOf( + FeatureA::class.java, + FeatureB::class.java, + ) + } + } + } +} diff --git a/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/LaboratorySpec.kt b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/LaboratorySpec.kt new file mode 100644 index 000000000..c63258d52 --- /dev/null +++ b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/LaboratorySpec.kt @@ -0,0 +1,188 @@ +package io.mehow.laboratory + +import app.cash.turbine.test +import io.kotest.assertions.fail +import io.kotest.assertions.throwables.shouldThrowExactly +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.throwable.shouldHaveMessage +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +class LaboratorySpec : FunSpec() { + enum class NoValuesFeature : Feature + + enum class FeatureA : Feature { + A, + B, + C, + ; + + override val defaultOption get() = A + } + + enum class FeatureB : Feature { + A, + B, + ; + + override val defaultOption get() = A + } + + init { + test("reads feature option saved in storage") { + val storage = FeatureStorage.inMemory() + val laboratory = Laboratory.create(storage) + + for (option in FeatureA::class.java.options) { + storage.setOption(option) + + laboratory.experiment() shouldBe option + laboratory.experimentIs(option) shouldBe true + } + } + + test("changes feature option") { + val laboratory = Laboratory.inMemory() + + for (option in FeatureA::class.java.options) { + laboratory.setOption(option) + + laboratory.experiment() shouldBe option + } + } + + test("changes options for multiple features") { + val laboratory = Laboratory.inMemory() + + laboratory.setOptions(FeatureA.C, FeatureB.B) + + laboratory.experiment() shouldBe FeatureA.C + laboratory.experiment() shouldBe FeatureB.B + } + + test("emits feature changes") { + val laboratory = Laboratory.inMemory() + + laboratory.observe().test { + awaitItem() shouldBe FeatureA.A + + laboratory.setOption(FeatureA.B) + awaitItem() shouldBe FeatureA.B + + laboratory.setOption(FeatureA.C) + awaitItem() shouldBe FeatureA.C + + laboratory.setOption(FeatureA.C) + expectNoEvents() + + laboratory.setOption(FeatureA.B) + awaitItem() shouldBe FeatureA.B + } + } + + test("clears all features") { + val laboratory = Laboratory.inMemory() + + laboratory.setOptions(FeatureA.B, FeatureB.B) + laboratory.clear() + + laboratory.experiment() shouldBe FeatureA.A + laboratory.experiment() shouldBe FeatureB.A + } + + test("does not share instances between in memory implementations") { + val firstLaboratory = Laboratory.inMemory() + val secondLaboratory = Laboratory.inMemory() + + firstLaboratory.setOption(FeatureA.B) + firstLaboratory.experiment() shouldBe FeatureA.B + secondLaboratory.experiment() shouldBe FeatureA.A + + secondLaboratory.setOption(FeatureA.C) + firstLaboratory.experiment() shouldBe FeatureA.B + secondLaboratory.experiment() shouldBe FeatureA.C + } + + test("uses default option if no match is found") { + val nullStorage = object : FeatureStorage { + override fun observeFeatureName(feature: Class>): Flow = + flowOf(null) + + override suspend fun getFeatureName(feature: Class>): String? = null + + override suspend fun setOptions(vararg options: Feature<*>) = fail("Unexpected call") + + override suspend fun clear() = fail("Unexpected call") + } + val laboratory = Laboratory.create(nullStorage) + + laboratory.experiment() shouldBe FeatureA.A + } + + context("default options factory") { + val factory = object : DefaultOptionFactory { + override fun > create(feature: T) = when (feature) { + is FeatureA -> FeatureA.C + is FeatureB -> FeatureA.C // Intentional wrong class + else -> null + } + } + + val laboratory = Laboratory.builder() + .featureStorage(FeatureStorage.inMemory()) + .defaultOptionFactory(factory) + .build() + + beforeTest { + laboratory.clear() + } + + test("overrides default options") { + laboratory.experiment() shouldBe FeatureA.C + } + + test("does not override changed options") { + for (option in FeatureA::class.java.options) { + laboratory.setOption(option) + + laboratory.experiment() shouldBe option + } + } + + test("overrides emitted default options") { + laboratory.observe().test { + awaitItem() shouldBe FeatureA.C + + laboratory.setOption(FeatureA.B) + awaitItem() shouldBe FeatureA.B + } + } + + @Suppress("MaxLineLength") + test("fails when provided default option uses wrong type") { + shouldThrowExactly { + laboratory.experiment() + } shouldHaveMessage "Tried to use FeatureA.C as a default option for io.mehow.laboratory.LaboratorySpec.FeatureB" + } + } + + test("fails to use feature with no values") { + val throwingStorage = object : FeatureStorage { + override fun observeFeatureName(feature: Class>) = fail("Unexpected call") + + override suspend fun getFeatureName(feature: Class>) = + fail("Unexpected call") + + override suspend fun setOptions(vararg options: Feature<*>) = fail("Unexpected call") + + override suspend fun clear() = fail("Unexpected call") + } + val laboratory = Laboratory.create(throwingStorage) + + shouldThrowExactly { + laboratory.experiment() + } shouldHaveMessage "io.mehow.laboratory.LaboratorySpec.NoValuesFeature must have at least one option" + } + } +} diff --git a/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/OptionFactorySpec.kt b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/OptionFactorySpec.kt new file mode 100644 index 000000000..8965e09ba --- /dev/null +++ b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/OptionFactorySpec.kt @@ -0,0 +1,61 @@ +package io.mehow.laboratory + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +class OptionFactorySpec : FunSpec() { + enum class FeatureA : Feature { + A, + B, + ; + + override val defaultOption get() = A + } + + enum class FeatureB : Feature { + A, + B, + ; + + override val defaultOption get() = A + } + + init { + val firstFactory = object : OptionFactory { + override fun create( + key: String, + name: String, + ): Feature<*>? = when (key) { + "FeatureA" -> FeatureA.A + else -> null + } + } + + val secondFactory = object : OptionFactory { + override fun create( + key: String, + name: String, + ): Feature<*>? = when (key) { + "FeatureA" -> FeatureA.B + "FeatureB" -> FeatureB.B + else -> null + } + } + + context("factory created from sum") { + val factory = firstFactory + secondFactory + + test("prioritizes first factory when feature is available in it") { + factory.create("FeatureA", "") shouldBe FeatureA.A + } + + test("falls back to second factory when feature is not available in first factory") { + factory.create("FeatureB", "") shouldBe FeatureB.B + } + + test("does not handle features unknown to any of sub-factories") { + factory.create("Unknown", "") shouldBe null + } + } + } +} diff --git a/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/ParentChildFeatureSpec.kt b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/ParentChildFeatureSpec.kt new file mode 100644 index 000000000..db4a12fb7 --- /dev/null +++ b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/ParentChildFeatureSpec.kt @@ -0,0 +1,119 @@ +package io.mehow.laboratory + +import app.cash.turbine.test +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +class ParentChildFeatureSpec : FunSpec() { + enum class GrandParentFeature : Feature { + A, + B, + ; + + override val defaultOption get() = A + } + + enum class ParentFeature : Feature { + A, + B, + ; + + override val defaultOption get() = A + + override val supervisorOption = GrandParentFeature.A + } + + enum class ChildFeature : Feature { + A, + B, + ; + + override val defaultOption get() = A + + override val supervisorOption = ParentFeature.A + } + + enum class ChildFeature2 : Feature { + A, + B, + ; + + override val defaultOption get() = A + + override val supervisorOption = ParentFeature.B + } + + init { + context("parent feature") { + test("supervises reading child's option") { + val laboratory = Laboratory.inMemory() + + laboratory.setOption(ChildFeature.B) + laboratory.experiment() shouldBe ChildFeature.B + + laboratory.setOption(ParentFeature.B) + laboratory.experiment() shouldBe ChildFeature.A + } + + test("does not changed stored child's option") { + val laboratory = Laboratory.inMemory() + + laboratory.setOption(ChildFeature.B) + laboratory.setOption(ParentFeature.B) + laboratory.setOption(ParentFeature.A) + + laboratory.experiment() shouldBe ChildFeature.B + } + + test("does not prevent writing child's option") { + val laboratory = Laboratory.inMemory() + + laboratory.setOption(ParentFeature.B) + laboratory.setOption(ChildFeature.B) + laboratory.setOption(ParentFeature.A) + + laboratory.experiment() shouldBe ChildFeature.B + } + + test("triggers emitting child's option when they differ") { + val laboratory = Laboratory.inMemory() + + laboratory.setOption(ChildFeature.B) + + laboratory.observe().test { + awaitItem() shouldBe ChildFeature.B + + laboratory.setOption(ParentFeature.B) + awaitItem() shouldBe ChildFeature.A + + laboratory.setOption(ParentFeature.A) + awaitItem() shouldBe ChildFeature.B + } + } + + test("does not trigger emitting child's option when they do not differ") { + val laboratory = Laboratory.inMemory() + + laboratory.observe().test { + awaitItem() shouldBe ChildFeature.A + + laboratory.setOption(ParentFeature.B) + expectNoEvents() + + laboratory.setOption(ParentFeature.A) + expectNoEvents() + } + } + } + + test("supervises reading grandchild's option") { + val laboratory = Laboratory.inMemory() + + laboratory.setOption(ChildFeature2.B) + laboratory.setOption(ParentFeature.B) + laboratory.setOption(GrandParentFeature.B) + + laboratory.experiment() shouldBe ChildFeature2.A + } + } +} diff --git a/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/SourcedFeatureStorageSpec.kt b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/SourcedFeatureStorageSpec.kt new file mode 100644 index 000000000..b085225a1 --- /dev/null +++ b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/SourcedFeatureStorageSpec.kt @@ -0,0 +1,285 @@ +package io.mehow.laboratory + +import app.cash.turbine.test +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +class SourcedFeatureStorageSpec : FunSpec() { + enum class FeatureA : Feature { + A, + B, + C, + ; + + override val defaultOption get() = A + + override val source = Source::class.java + + enum class Source : Feature { + Local, + RemoteA, + ; + + override val defaultOption get() = Local + } + } + + enum class FeatureB : Feature { + A, + B, + C, + ; + + override val defaultOption get() = A + + override val source = Source::class.java + + enum class Source : Feature { + Local, + RemoteA, + RemoteB, + ; + + override val defaultOption get() = RemoteB + } + } + + enum class EmptySourceFeature : Feature { + A, + B, + C, + ; + + override val defaultOption get() = A + + override val source = Source::class.java + + enum class Source : Feature + } + + enum class UnsourcedFeature : Feature { + A, + B, + C, + ; + + override val defaultOption get() = A + } + + init { + val storageLocal = FeatureStorage.inMemory() + val storageRemoteA = FeatureStorage.inMemory() + val storageRemoteB = FeatureStorage.inMemory() + val storageSourced = SourcedFeatureStorage( + storageLocal, + mapOf("RemoteA" to storageRemoteA, "RemoteB" to storageRemoteB), + ) + + val laboratoryLocal = Laboratory.create(storageLocal) + val laboratoryRemoteA = Laboratory.create(storageRemoteA) + val laboratoryRemoteB = Laboratory.create(storageRemoteB) + val laboratorySourced = Laboratory.create(storageSourced) + + beforeTest { + laboratoryLocal.clear() + laboratoryRemoteA.clear() + laboratoryRemoteB.clear() + } + + test("uses options from default sources") { + laboratoryLocal.setOption(FeatureA.B) + laboratorySourced.experiment() shouldBe FeatureA.B + + laboratoryRemoteB.setOption(FeatureB.C) + laboratorySourced.experiment() shouldBe FeatureB.C + } + + test("emits options from default sources") { + laboratorySourced.observe().test { + awaitItem() shouldBe FeatureB.A + + laboratoryRemoteB.setOption(FeatureB.B) + awaitItem() shouldBe FeatureB.B + + laboratoryRemoteB.setOption(FeatureB.C) + awaitItem() shouldBe FeatureB.C + } + } + + test("uses options from changed sources") { + laboratorySourced.setOption(FeatureA.Source.RemoteA) + + laboratoryRemoteA.setOption(FeatureA.C) + laboratorySourced.experiment() shouldBe FeatureA.C + + laboratoryLocal.setOption(FeatureA.B) + laboratorySourced.experiment() shouldBe FeatureA.C + } + + test("emits options from changed sources") { + laboratoryLocal.setOption(FeatureB.A) + laboratoryRemoteA.setOption(FeatureB.B) + laboratoryRemoteB.setOption(FeatureB.C) + + laboratorySourced.observe().test { + awaitItem() shouldBe FeatureB.C + + laboratorySourced.setOption(FeatureB.Source.Local) + awaitItem() shouldBe FeatureB.A + + laboratoryLocal.setOption(FeatureB.C) + awaitItem() shouldBe FeatureB.C + + laboratorySourced.setOption(FeatureB.Source.RemoteA) + awaitItem() shouldBe FeatureB.B + + laboratoryRemoteA.setOption(FeatureB.A) + awaitItem() shouldBe FeatureB.A + + laboratorySourced.setOption(FeatureB.Source.RemoteB) + awaitItem() shouldBe FeatureB.C + + laboratoryRemoteB.setOption(FeatureB.B) + awaitItem() shouldBe FeatureB.B + } + } + + test("does not emit changes from inactive sources") { + laboratorySourced.observe().test { + awaitItem() shouldBe FeatureB.A + + laboratoryLocal.setOption(FeatureB.B) + expectNoEvents() + + laboratoryRemoteA.setOption(FeatureB.B) + expectNoEvents() + } + } + + test("does not mix source changes between different features") { + laboratoryLocal.setOption(FeatureB.B) + laboratoryRemoteA.setOption(FeatureB.C) + + laboratorySourced.observe().test { + awaitItem() shouldBe FeatureB.A + + laboratorySourced.setOption(FeatureA.Source.Local) + expectNoEvents() + + laboratorySourced.setOption(FeatureA.Source.RemoteA) + expectNoEvents() + } + } + + test("clears only local storage") { + laboratoryLocal.setOption(FeatureA.B) + laboratoryRemoteA.setOption(FeatureA.B) + laboratorySourced.setOption(FeatureA.Source.RemoteA) + + laboratorySourced.clear() + + laboratoryLocal.experimentIs(FeatureA.A) + laboratoryRemoteA.experimentIs(FeatureA.B) + laboratorySourced.experimentIs(FeatureA.A) + } + + test("allows to override default sources") { + val defaultOptionFactory = object : DefaultOptionFactory { + override fun > create(feature: T) = when (feature) { + is FeatureA.Source -> FeatureA.Source.RemoteA + else -> null + } + } + val laboratory = Laboratory.Builder() + .featureStorage(storageSourced) + .defaultOptionFactory(defaultOptionFactory) + .build() + + laboratoryRemoteA.setOption(FeatureA.C) + + laboratory.experiment() shouldBe FeatureA.C + } + + test("makes changes only in local storage") { + laboratoryLocal.setOption(FeatureA.A) + laboratoryRemoteA.setOption(FeatureA.A) + laboratoryRemoteB.setOption(FeatureA.A) + + laboratorySourced.setOption(FeatureA.B) + + laboratoryLocal.experiment() shouldBe FeatureA.B + laboratoryRemoteA.experiment() shouldBe FeatureA.A + laboratoryRemoteB.experiment() shouldBe FeatureA.A + } + + context("feature with empty sources") { + test("reads from a local storage") { + laboratorySourced.experiment() shouldBe EmptySourceFeature.A + + laboratoryLocal.setOption(EmptySourceFeature.C) + laboratorySourced.experiment() shouldBe EmptySourceFeature.C + } + + test("emits changes from a local storage") { + laboratorySourced.observe().test { + awaitItem() shouldBe EmptySourceFeature.A + + laboratoryLocal.setOption(EmptySourceFeature.B) + awaitItem() shouldBe EmptySourceFeature.B + + cancel() + } + } + } + + context("feature with no sources") { + test("reads from a local storage") { + laboratorySourced.experiment() shouldBe UnsourcedFeature.A + + laboratoryLocal.setOption(UnsourcedFeature.C) + laboratorySourced.experiment() shouldBe UnsourcedFeature.C + } + + test("emits changes from a local storage") { + laboratorySourced.observe().test { + awaitItem() shouldBe UnsourcedFeature.A + + laboratoryLocal.setOption(UnsourcedFeature.B) + awaitItem() shouldBe UnsourcedFeature.B + + cancel() + } + } + } + + @Suppress("NAME_SHADOWING") + context("feature with unknown source") { + val localStorage = FeatureStorage.inMemory() + val sourcedStorage = SourcedFeatureStorage(localStorage, emptyMap()) + val laboratoryLocal = Laboratory.create(localStorage) + val laboratorySourced = Laboratory.create(sourcedStorage) + + beforeTest { + laboratoryLocal.clear() + } + + test("reads from a local storage") { + laboratorySourced.experiment() shouldBe FeatureA.A + + laboratoryLocal.setOption(FeatureA.C) + laboratorySourced.experiment() shouldBe FeatureA.C + } + + test("emits changes from a local storage") { + laboratorySourced.observe().test { + awaitItem() shouldBe FeatureA.A + + laboratoryLocal.setOption(FeatureA.B) + awaitItem() shouldBe FeatureA.B + + cancel() + } + } + } + } +} diff --git a/library/shared-preferences/api/shared-preferences.api b/laboratory/shared-preferences/api/shared-preferences.api similarity index 100% rename from library/shared-preferences/api/shared-preferences.api rename to laboratory/shared-preferences/api/shared-preferences.api diff --git a/laboratory/shared-preferences/build.gradle.kts b/laboratory/shared-preferences/build.gradle.kts new file mode 100644 index 000000000..5979b3eb4 --- /dev/null +++ b/laboratory/shared-preferences/build.gradle.kts @@ -0,0 +1,39 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.mavenPublish) + alias(libs.plugins.spotless) + alias(libs.plugins.detekt) + alias(libs.plugins.dokka) +} + +android { + namespace = "io.mehow.laboratory.sharedpreferences" + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments += "clearPackageData" to "true" + } + + testOptions.execution = "ANDROIDX_TEST_ORCHESTRATOR" + testBuildType = "release" + + buildTypes { + release { + // Testing release builds requires signing them + signingConfig = signingConfigs.getByName("debug") + } + } +} + +dependencies { + api(projects.laboratory.runtime) + implementation(libs.kotlinx.coroutines.core) + + androidTestImplementation(libs.kotest.assertions) + androidTestImplementation(libs.turbine) + androidTestUtil(libs.androidx.test.orchestrator) + androidTestImplementation(libs.androidx.test.coreKtx) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.testExt.junitKtx) +} diff --git a/laboratory/shared-preferences/gradle.properties b/laboratory/shared-preferences/gradle.properties new file mode 100644 index 000000000..461e262f8 --- /dev/null +++ b/laboratory/shared-preferences/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=laboratory-data-store +POM_NAME=Laboratory (Shared Preferences) +POM_PACKAGING=aar diff --git a/library/shared-preferences/src/androidTest/java/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeaturesStorageTest.kt b/laboratory/shared-preferences/src/androidTest/kotlin/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeaturesStorageTest.kt similarity index 61% rename from library/shared-preferences/src/androidTest/java/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeaturesStorageTest.kt rename to laboratory/shared-preferences/src/androidTest/kotlin/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeaturesStorageTest.kt index 4a0f16341..fb960c12b 100644 --- a/library/shared-preferences/src/androidTest/java/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeaturesStorageTest.kt +++ b/laboratory/shared-preferences/src/androidTest/kotlin/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeaturesStorageTest.kt @@ -12,14 +12,22 @@ import io.mehow.laboratory.Laboratory import kotlinx.coroutines.runBlocking import org.junit.Test -internal class SharedPreferencesFeaturesStorageTest { +class SharedPreferencesFeaturesStorageTest { + enum class FeatureA : Feature { + A, + B, + ; + + override val defaultOption get() = A + } + private val preferences = ApplicationProvider - .getApplicationContext() - .getSharedPreferences("laboratory", MODE_PRIVATE) + .getApplicationContext() + .getSharedPreferences("laboratory", MODE_PRIVATE) private val storage = FeatureStorage.sharedPreferences(preferences) private val laboratory = Laboratory.create(storage) - @Test fun storedOptionIsAvailableAsExperiment() { + @Test fun readsStoredOption() { runBlocking { storage.setOption(FeatureA.B) @@ -27,7 +35,7 @@ internal class SharedPreferencesFeaturesStorageTest { } } - @Test fun corruptedFeatureFlagOptionYieldsDefaultExperiment() { + @Test fun usesDefaultOptionForCorruptedData() { runBlocking { storage.setOption(FeatureA.B) preferences.edit().putInt(FeatureA::class.java.name, 1).commit() @@ -36,7 +44,7 @@ internal class SharedPreferencesFeaturesStorageTest { } } - @Test fun observesFeatureFlagChanges() = runBlocking { + @Test fun emitsFeatureOptionChanges() = runBlocking { storage.observeFeatureName(FeatureA::class.java).test { awaitItem() shouldBe null @@ -51,30 +59,26 @@ internal class SharedPreferencesFeaturesStorageTest { } } - @Test fun clearsFeatureFlagOptions() = runBlocking { - storage.setOption(FeatureA.B) - storage.clear() + @Test fun clearsStorage() { + runBlocking { + storage.setOption(FeatureA.B) + storage.clear() - laboratory.experimentIs(FeatureA.A).shouldBeTrue() + laboratory.experimentIs(FeatureA.A).shouldBeTrue() + } } - @Test fun informsObserversAfterClearingFeatureFlags() = runBlocking { - storage.observeFeatureName(FeatureA::class.java).test { - awaitItem() shouldBe null + @Test fun emitsClearedState() { + runBlocking { + storage.observeFeatureName(FeatureA::class.java).test { + awaitItem() shouldBe null - storage.setOption(FeatureA.B) - awaitItem() shouldBe FeatureA.B.name + storage.setOption(FeatureA.B) + awaitItem() shouldBe FeatureA.B.name - storage.clear() - awaitItem() shouldBe null + storage.clear() + awaitItem() shouldBe null + } } } } - -private enum class FeatureA : Feature { - A, - B, - ; - - override val defaultOption get() = A -} diff --git a/library/shared-preferences/src/main/java/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeatureStorage.kt b/laboratory/shared-preferences/src/main/kotlin/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeatureStorage.kt similarity index 95% rename from library/shared-preferences/src/main/java/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeatureStorage.kt rename to laboratory/shared-preferences/src/main/kotlin/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeatureStorage.kt index 89c838637..c8fbf53f9 100644 --- a/library/shared-preferences/src/main/java/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeatureStorage.kt +++ b/laboratory/shared-preferences/src/main/kotlin/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeatureStorage.kt @@ -13,7 +13,9 @@ internal class SharedPreferencesFeatureStorage( ) : FeatureStorage { override fun observeFeatureName(feature: Class>) = callbackFlow { val listener = OnSharedPreferenceChangeListener { _, key -> - if (key == feature.name) trySend(getStringSafe(key)) + if (key != null && key == feature.name) { + trySend(getStringSafe(key)) + } } send(getStringSafe(feature.name)) preferences.registerOnSharedPreferenceChangeListener(listener) diff --git a/library/build.gradle b/library/build.gradle deleted file mode 100644 index 540640a8e..000000000 --- a/library/build.gradle +++ /dev/null @@ -1,140 +0,0 @@ -import io.gitlab.arturbosch.detekt.Detekt -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -buildscript { - repositories { - mavenCentral() - gradlePluginPortal() - google() - } - - dependencies { - classpath libs.android.gradlePlugin - classpath libs.kotlin.gradlePlugin - classpath libs.mavenPublish.gradlePlugin - classpath libs.dokka.gradlePlugin - classpath libs.detekt.gradlePlugin - classpath libs.gradleVersions.gradlePlugin - classpath libs.wireGradlePlugin - classpath libs.kotlin.x.binaryCompatibility.gradlePlugin - } -} - -apply plugin: libs.plugins.binaryCompatibility.get().pluginId - -allprojects { - group GROUP - version VERSION_NAME - - tasks.withType(Test).configureEach { - testLogging { - events "skipped", "failed", "passed" - } - } - - tasks.withType(JavaCompile).configureEach { - sourceCompatibility JavaConfig.name - targetCompatibility JavaConfig.name - } - - tasks.withType(KotlinCompile).configureEach { - kotlinOptions { - jvmTarget = JavaConfig.name - freeCompilerArgs += [ - "-progressive", - "-Xjvm-default=all", - "-Xopt-in=kotlin.RequiresOptIn", - "-Xopt-in=kotlin.time.ExperimentalTime", - // TODO: Remove API in tests https://youtrack.jetbrains.com/issue/KT-42718 - "-Xexplicit-api=strict", - ] - } - } - - pluginManager.withPlugin("com.android.library") { plugin -> - android { - compileOptions { - sourceCompatibility JavaConfig.code - targetCompatibility JavaConfig.code - } - - defaultConfig { - targetSdkVersion libs.versions.androidBuild.targetSdk.get().toInteger() - vectorDrawables.useSupportLibrary true - } - - variantFilter { variant -> - setIgnore variant.name != "release" - } - - libraryVariants.all { variant -> - variant.outputs.all { - outputFileName = "${archivesBaseName}-${version}.aar" - } - } - - testOptions.unitTests.all { - useJUnitPlatform() - } - - lintOptions { - lintConfig rootProject.file("lint.xml") - - htmlReport !isCi() - xmlReport isCi() - xmlOutput file("build/reports/lint/lint-results.xml") - - textReport true - textOutput "stdout" - explainIssues false - - checkDependencies false - checkGeneratedSources true - checkTestSources false - checkReleaseBuilds false - } - } - } -} - -apply plugin: libs.plugins.detekt.get().pluginId - -dependencies { - detekt libs.detekt.formatting - detekt libs.detekt.cli -} - -tasks.withType(Detekt).configureEach { - parallel true - config.setFrom(rootProject.files("detekt-config.yml")) - setSource(files(projectDir)) - exclude "**/test/**", "**/androidTest/**" , "**/kyrie/**" - exclude subprojects.collect { "${rootDir.toPath().relativize(it.buildDir.toPath())}/" } - reports { - xml { - enabled = isCi() - destination = file("build/reports/detekt/detekt-results.xml") - } - html.enabled = !isCi() - sarif.enabled = false - txt.enabled = false - } -} - -apply plugin: libs.plugins.gradleVersions.get().pluginId - -dependencyUpdates { - rejectVersionIf { - isNonStable(it.candidate.version) && !isNonStable(it.currentVersion) - } -} - -private static def isNonStable(String version) { - def regex = /^[0-9,.v-]+(-r)?$/ - return !(version ==~ regex) -} - -private static def isCi() { - // noinspection GroovyPointlessBoolean - return System.getenv("CI")?.toBoolean() == true -} diff --git a/library/buildSrc/build.gradle.kts b/library/buildSrc/build.gradle.kts deleted file mode 100644 index bc0172f0f..000000000 --- a/library/buildSrc/build.gradle.kts +++ /dev/null @@ -1,3 +0,0 @@ -plugins { - `kotlin-dsl` -} diff --git a/library/buildSrc/settings.gradle.kts b/library/buildSrc/settings.gradle.kts deleted file mode 100644 index 0f47ea913..000000000 --- a/library/buildSrc/settings.gradle.kts +++ /dev/null @@ -1,6 +0,0 @@ -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - mavenCentral() - } -} diff --git a/library/buildSrc/src/main/kotlin/JavaConfig.kt b/library/buildSrc/src/main/kotlin/JavaConfig.kt deleted file mode 100644 index 2d5fa00cd..000000000 --- a/library/buildSrc/src/main/kotlin/JavaConfig.kt +++ /dev/null @@ -1,6 +0,0 @@ -import org.gradle.api.JavaVersion - -object JavaConfig { - val code = JavaVersion.VERSION_17 - val name = code.toString() -} diff --git a/library/data-store/build.gradle b/library/data-store/build.gradle deleted file mode 100644 index 690817ee5..000000000 --- a/library/data-store/build.gradle +++ /dev/null @@ -1,38 +0,0 @@ -plugins { - id "com.android.library" - id "org.jetbrains.kotlin.android" - id "com.squareup.wire" -} - -wire { - kotlin { } -} - -android { - namespace "io.mehow.laboratory.datastore" - - sourceSets { - getByName("main").java.srcDirs += "$buildDir/generated/source/wire/" - } -} - -dependencies { - api project(":laboratory") - api libs.android.x.dataStore - - testImplementation libs.kotest.runnerJunit5 - testImplementation libs.kotest.assertions - testImplementation libs.turbine -} - -apply from: "$rootDir/gradle/dokka-config.gradle" - -dokkaHtml { - dokkaSourceSets { - configureEach { - sourceRoots.from(file("$buildDir/generated/source/wire/io/mehow/laboratory/datastore/FeatureFlags.kt")) - } - } -} - -apply from: "$rootDir/gradle/gradle-mvn-push.gradle" diff --git a/library/data-store/src/test/java/io/mehow/laboratory/datastore/FeatureFlagsSerializerSpec.kt b/library/data-store/src/test/java/io/mehow/laboratory/datastore/FeatureFlagsSerializerSpec.kt deleted file mode 100644 index d0832d5a6..000000000 --- a/library/data-store/src/test/java/io/mehow/laboratory/datastore/FeatureFlagsSerializerSpec.kt +++ /dev/null @@ -1,31 +0,0 @@ -package io.mehow.laboratory.datastore - -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.shouldBe -import okio.Buffer -import okio.ByteString.Companion.decodeHex - -internal class FeatureFlagsSerializerSpec : DescribeSpec({ - describe("serializer") { - val flags = FeatureFlags(mapOf(FeatureA::class.java.toString() to FeatureA.A.name)) - val hex = "0a310a2c636c61737320696f2e6d65686f772e6c61626f7261746f72792e6461746173746f72652e4665617475726541120141" - val binaryFlags = hex.decodeHex() - - it("decodes bytes") { - val input = Buffer().write(binaryFlags).inputStream() - - val result = FeatureFlagsSerializer.readFrom(input) - - result shouldBe flags - } - - it("encodes bytes") { - val output = Buffer() - - FeatureFlagsSerializer.writeTo(flags, output.outputStream()) - val result = output.readByteString() - - result shouldBe binaryFlags - } - } -}) diff --git a/library/detekt-config.yml b/library/detekt-config.yml deleted file mode 100644 index 3c6dbcbb7..000000000 --- a/library/detekt-config.yml +++ /dev/null @@ -1,311 +0,0 @@ -build: - maxIssues: 0 - weights: - -complexity: - active: true - ComplexCondition: - active: true - threshold: 3 - ComplexInterface: - active: true - ComplexMethod: - active: true - threshold: 10 - ignoreSingleWhenExpression: true - LargeClass: - active: true - threshold: 400 - LongMethod: - active: true - threshold: 20 - LongParameterList: - active: true - functionThreshold: 5 - constructorThreshold: 5 - NestedBlockDepth: - active: true - StringLiteralDuplication: - active: true - threshold: 3 - ignoreAnnotation: true - excludeStringsWithLessThan5Characters: false - -coroutines: - active: true - GlobalCoroutineUsage: - active: true - RedundantSuspendModifier: - active: true - SleepInsteadOfDelay: - active: true - SuspendFunWithFlowReturnType: - active: true - -empty-blocks: - active: true - EmptyCatchBlock: - active: true - EmptyClassBlock: - active: true - EmptyDefaultConstructor: - active: true - EmptyDoWhileBlock: - active: true - EmptyElseBlock: - active: true - EmptyFinallyBlock: - active: true - EmptyForBlock: - active: true - EmptyFunctionBlock: - active: true - EmptyIfBlock: - active: true - EmptyInitBlock: - active: true - EmptyKtFile: - active: true - EmptySecondaryConstructor: - active: true - EmptyWhenBlock: - active: true - EmptyWhileBlock: - active: true - -exceptions: - active: true - ExceptionRaisedInUnexpectedLocation: - active: true - methodNames: 'toString,hashCode,equals,finalize' - InstanceOfCheckForException: - active: true - NotImplementedDeclaration: - active: true - ObjectExtendsThrowable: - active: true - PrintStackTrace: - active: true - RethrowCaughtException: - active: true - ReturnFromFinally: - active: true - SwallowedException: - active: true - ThrowingExceptionFromFinally: - active: true - ThrowingExceptionsWithoutMessageOrCause: - active: true - ThrowingNewInstanceOfSameException: - active: true - TooGenericExceptionCaught: - active: true - TooGenericExceptionThrown: - active: true - -formatting: - active: true - ChainWrapping: - active: true - CommentSpacing: - active: true - ImportOrdering: - active: true - Indentation: - active: false # TODO: Turn on when fixed in KtLint - indentSize: 2 - continuationIndentSize: 4 - NoBlankLineBeforeRbrace: - active: true - NoConsecutiveBlankLines: - active: true - NoLineBreakBeforeAssignment: - active: true - NoMultipleSpaces: - active: true - NoSemicolons: - active: true - NoTrailingSpaces: - active: true - ParameterListWrapping: - active: true - indentSize: 2 - SpacingAroundColon: - active: true - SpacingAroundComma: - active: true - SpacingAroundCurly: - active: true - SpacingAroundKeyword: - active: true - SpacingAroundOperators: - active: true - SpacingAroundRangeOperator: - active: true - StringTemplate: - active: true - -naming: - active: true - ClassNaming: - active: true - classPattern: '^[A-Z][a-z]+(?:[A-Z][a-z]+)*$' - ConstructorParameterNaming: - active: true - parameterPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' - privateParameterPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' - EnumNaming: - active: true - enumEntryPattern: '^[A-Z][a-z]+(?:[A-Z][a-z]+)*$' - FunctionNaming: - active: true - functionPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' - FunctionParameterNaming: - active: true - parameterPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' - MatchingDeclarationName: - active: true - MemberNameEqualsClassName: - active: true - ObjectPropertyNaming: - active: true - constantPattern: '^[a-zA-Z]+(?:[A-Z][a-z]+)*$' - propertyPattern: '^[a-zA-Z]+(?:[A-Z][a-z]+)*$' - privatePropertyPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' - PackageNaming: - active: true - packagePattern: '^[a-z]+(\.[a-z][a-z]*)*$' - TopLevelPropertyNaming: - active: true - constantPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' - propertyPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' - privatePropertyPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' - VariableNaming: - active: true - variablePattern: '^[a-z]+(?:[A-Z][a-z]+)*$' - privateVariablePattern: '^[a-z]+(?:[A-Z][a-z]+)*$' - -performance: - active: true - ForEachOnRange: - active: true - UnnecessaryTemporaryInstantiation: - active: true - -potential-bugs: - active: true - DuplicateCaseInWhenExpression: - active: true - EqualsAlwaysReturnsTrueOrFalse: - active: false - EqualsWithHashCodeExist: - active: true - ExplicitGarbageCollectionCall: - active: true - InvalidRange: - active: true - IteratorHasNextCallsNextMethod: - active: true - IteratorNotThrowingNoSuchElementException: - active: true - MissingWhenCase: - active: true - RedundantElseInWhen: - active: true - UnconditionalJumpStatementInLoop: - active: true - UnreachableCode: - active: true - UnusedUnaryOperator: - active: true - UselessPostfixExpression: - active: true - WrongEqualsTypeParameter: - active: true - -style: - active: true - CollapsibleIfStatements: - active: true - DataClassShouldBeImmutable: - active: true - EqualsNullCall: - active: true - EqualsOnSignatureLine: - active: true - ExplicitItLambdaParameter: - active: true - ForbiddenVoid: - active: true - LibraryCodeMustSpecifyReturnType: - active: true - LoopWithTooManyJumpStatements: - active: false - maxJumpCount: 1 - MaxLineLength: - active: true - maxLineLength: 120 - excludePackageStatements: true - excludeImportStatements: true - excludeCommentStatements: true - MayBeConst: - active: true - ModifierOrder: - active: true - NewLineAtEndOfFile: - active: true - NoTabs: - active: true - OptionalAbstractKeyword: - active: true - OptionalWhenBraces: - active: true - PreferToOverPairSyntax: - active: true - ProtectedMemberInFinalClass: - active: true - RedundantVisibilityModifierRule: - active: false - SafeCast: - active: true - SerialVersionUIDInSerializableClass: - active: true - SpacingBetweenPackageAndImports: - active: true - TrailingWhitespace: - active: true - UnderscoresInNumericLiterals: - active: true - acceptableLength: 3 - UnnecessaryAbstractClass: - active: true - UnnecessaryApply: - active: true - UnnecessaryInheritance: - active: true - UnnecessaryLet: - active: true - UnnecessaryParentheses: - active: true - UntilInsteadOfRangeTo: - active: true - UnusedImports: - active: true - UnusedPrivateClass: - active: true - UnusedPrivateMember: - active: true - UseArrayLiteralsInAnnotations: - active: true - UseCheckOrError: - active: true - UseRequire: - active: true - UselessCallOnNotNull: - active: true - UtilityClassWithPublicConstructor: - active: true - VarCouldBeVal: - active: true - WildcardImport: - active: true diff --git a/library/generator/build.gradle b/library/generator/build.gradle deleted file mode 100644 index 8e0175541..000000000 --- a/library/generator/build.gradle +++ /dev/null @@ -1,24 +0,0 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -plugins { - id "org.jetbrains.kotlin.jvm" -} - -test.useJUnitPlatform() - -tasks.withType(KotlinCompile).configureEach { - kotlinOptions.freeCompilerArgs += [ - "-Xopt-in=kotlin.io.path.ExperimentalPathApi", - ] -} - -dependencies { - api libs.kotlinPoet - implementation project(":laboratory") - - testImplementation libs.kotest.runnerJunit5 - testImplementation libs.kotest.assertions - testImplementation libs.kotest.property -} - -apply from: "$rootDir/gradle/gradle-mvn-push.gradle" diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/ClassNames.kt b/library/generator/src/main/java/io/mehow/laboratory/generator/ClassNames.kt deleted file mode 100644 index d0a578e2a..000000000 --- a/library/generator/src/main/java/io/mehow/laboratory/generator/ClassNames.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.mehow.laboratory.generator - -import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy -import com.squareup.kotlinpoet.TypeName -import com.squareup.kotlinpoet.asClassName -import kotlin.reflect.KClass - -internal operator fun KClass<*>.invoke(parameter: TypeName, vararg parameters: TypeName) = - asClassName().parameterizedBy(parameter, *parameters) - -internal operator fun KClass<*>.invoke(parameter: KClass<*>, vararg parameters: KClass<*>) = - invoke(parameter.asClassName(), *parameters.map { it.asClassName() }.toTypedArray()) diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/SourcedFeatureStorageGenerator.kt b/library/generator/src/main/java/io/mehow/laboratory/generator/SourcedFeatureStorageGenerator.kt deleted file mode 100644 index 6bf0ef8b0..000000000 --- a/library/generator/src/main/java/io/mehow/laboratory/generator/SourcedFeatureStorageGenerator.kt +++ /dev/null @@ -1,131 +0,0 @@ -package io.mehow.laboratory.generator - -import com.squareup.kotlinpoet.ClassName -import com.squareup.kotlinpoet.FileSpec -import com.squareup.kotlinpoet.FunSpec -import com.squareup.kotlinpoet.KModifier.ABSTRACT -import com.squareup.kotlinpoet.KModifier.DATA -import com.squareup.kotlinpoet.KModifier.OVERRIDE -import com.squareup.kotlinpoet.KModifier.PRIVATE -import com.squareup.kotlinpoet.KOperator.PLUS -import com.squareup.kotlinpoet.MemberName -import com.squareup.kotlinpoet.PropertySpec -import com.squareup.kotlinpoet.TypeSpec -import com.squareup.kotlinpoet.asClassName -import io.mehow.laboratory.FeatureStorage -import java.util.Locale - -internal class SourcedFeatureStorageGenerator( - storage: SourcedFeatureStorageModel, -) { - private val sourceNames = storage.sourceNames - .filterNot { featureName -> featureName.equals("local", ignoreCase = true) } - .distinct() - - private val sourced = MemberName(FeatureStorage.Companion::class.asClassName(), "sourced") - - private val emptyMap = MemberName(kotlinCollectionsSpace, "emptyMap") - - private val mapPlus = MemberName(kotlinCollectionsSpace, PLUS) - - private val infixTo = MemberName("kotlin", "to") - - private val buildingStepClassName = ClassName(storage.className.packageName, "BuildingStep") - - private val buildingStepType = TypeSpec.interfaceBuilder(buildingStepClassName) - .addModifiers(storage.visibility.modifier) - .addFunction(FunSpec.builder("build") - .addModifiers(ABSTRACT) - .returns(FeatureStorage::class) - .build()) - .build() - - private val remoteStepClassNames = sourceNames.distinct() - .sorted() - .map { ClassName(storage.className.packageName, it + stepSuffix) } - - private val remoteStepTypes = remoteStepClassNames - .windowed(size = 2, step = 1, partialWindows = true) { sources -> - val currentSourceClassName = sources.first() - val functionReturnClassName = sources.drop(1).firstOrNull() ?: buildingStepClassName - val functionName = currentSourceClassName.simpleName - .removeSuffix(stepSuffix) - .replaceFirstChar { it.lowercase(Locale.ROOT) } + "Source" - - TypeSpec.interfaceBuilder(currentSourceClassName) - .addModifiers(storage.visibility.modifier) - .addFunction(FunSpec.builder(functionName) - .addModifiers(ABSTRACT) - .addParameter("source", FeatureStorage::class) - .returns(functionReturnClassName) - .build()) - .build() - } - - private val builderType = TypeSpec.classBuilder(ClassName(storage.className.simpleName, "Builder")) - .addModifiers(PRIVATE, DATA) - .addSuperinterfaces(remoteStepClassNames + buildingStepClassName) - .primaryConstructor(FunSpec.constructorBuilder() - .addParameter(localSourceParam, FeatureStorage::class) - .addParameter(remoteSourcesParam, Map::class(String::class, FeatureStorage::class)) - .build()) - .addProperty(PropertySpec.builder(localSourceParam, FeatureStorage::class) - .initializer(localSourceParam) - .addModifiers(PRIVATE) - .build()) - .addProperty(PropertySpec.builder(remoteSourcesParam, Map::class(String::class, FeatureStorage::class)) - .initializer(remoteSourcesParam) - .addModifiers(PRIVATE) - .build()) - .addFunctions(remoteStepTypes.mapIndexed { index, remoteStep -> - val function = remoteStep.funSpecs.single() - function.toBuilder() - .apply { modifiers -= ABSTRACT } - .addModifiers(OVERRIDE) - .addStatement( - "return copy(\n⇥%1L = %1L %2M (%3S %4M %5N)⇤\n)", - remoteSourcesParam, - mapPlus, - remoteStepClassNames[index].simpleName.removeSuffix(stepSuffix), - infixTo, - function.parameters.single(), - ) - .build() - }) - .addFunction(buildingStepType.funSpecs.single() - .toBuilder() - .apply { modifiers -= ABSTRACT } - .addModifiers(OVERRIDE) - .addStatement("return %M(%L, %L)", sourced, localSourceParam, remoteSourcesParam) - .build()) - .build() - - private val storageBuilderExtension = FunSpec.builder("sourcedBuilder") - .addModifiers(storage.visibility.modifier) - .receiver(FeatureStorage.Companion::class) - .returns(remoteStepClassNames.firstOrNull() ?: buildingStepClassName) - .addParameter(localSourceParam, FeatureStorage::class) - .addStatement("return %N(%L, %M())", builderType, localSourceParam, emptyMap) - .build() - - private val storageFile = FileSpec.builder(storage.className.packageName, storage.className.simpleName) - .addFunction(storageBuilderExtension) - .apply { - for (type in remoteStepTypes) { - addType(type) - } - } - .addType(buildingStepType) - .addType(builderType) - .build() - - fun fileSpec() = storageFile - - private companion object { - const val stepSuffix = "Step" - const val localSourceParam = "localSource" - const val remoteSourcesParam = "remoteSources" - - const val kotlinCollectionsSpace = "kotlin.collections" - } -} diff --git a/library/generator/src/test/java/io/mehow/laboratory/generator/FeatureFactoryGeneratorSpec.kt b/library/generator/src/test/java/io/mehow/laboratory/generator/FeatureFactoryGeneratorSpec.kt deleted file mode 100644 index 4d4c55e78..000000000 --- a/library/generator/src/test/java/io/mehow/laboratory/generator/FeatureFactoryGeneratorSpec.kt +++ /dev/null @@ -1,118 +0,0 @@ -package io.mehow.laboratory.generator - -import com.squareup.kotlinpoet.ClassName -import io.kotest.core.spec.style.DescribeSpec -import io.mehow.laboratory.generator.Visibility.Internal -import io.mehow.laboratory.generator.Visibility.Public -import io.mehow.laboratory.generator.test.shouldSpecify - -internal class FeatureFactoryGeneratorSpec : DescribeSpec({ - val featureA = FeatureFlagModel( - className = ClassName("io.mehow", "FeatureA"), - options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - ) - - val featureB = FeatureFlagModel( - className = ClassName("io.mehow", "FeatureB"), - options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - ) - - val featureC = FeatureFlagModel( - className = ClassName("io.mehow.c", "FeatureA"), - options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - ) - - describe("generated feature flag factory") { - it("can be internal") { - val model = FeatureFactoryModel( - ClassName("io.mehow", "GeneratedFeatureFactory"), - listOf(featureA, featureB, featureC), - visibility = Internal, - ) - - val fileSpec = model.prepare("generated") - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import io.mehow.laboratory.FeatureFactory - |import java.lang.Class - |import kotlin.Suppress - |import kotlin.collections.Set - |import kotlin.collections.setOf - | - |internal fun FeatureFactory.Companion.generated(): FeatureFactory = GeneratedFeatureFactory - | - |private object GeneratedFeatureFactory : FeatureFactory { - | @Suppress("UNCHECKED_CAST") - | override fun create(): Set>> = setOf( - | Class.forName("io.mehow.FeatureA"), - | Class.forName("io.mehow.FeatureB"), - | Class.forName("io.mehow.c.FeatureA") - | ) as Set>> - |} - | - """.trimMargin() - } - - it("can be public") { - val model = FeatureFactoryModel( - ClassName("io.mehow", "GeneratedFeatureFactory"), - listOf(featureA, featureB, featureC), - visibility = Public, - ) - - val fileSpec = model.prepare("generated") - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import io.mehow.laboratory.FeatureFactory - |import java.lang.Class - |import kotlin.Suppress - |import kotlin.collections.Set - |import kotlin.collections.setOf - | - |public fun FeatureFactory.Companion.generated(): FeatureFactory = GeneratedFeatureFactory - | - |private object GeneratedFeatureFactory : FeatureFactory { - | @Suppress("UNCHECKED_CAST") - | override fun create(): Set>> = setOf( - | Class.forName("io.mehow.FeatureA"), - | Class.forName("io.mehow.FeatureB"), - | Class.forName("io.mehow.c.FeatureA") - | ) as Set>> - |} - | - """.trimMargin() - } - - it("is optimized in case of no features") { - val model = FeatureFactoryModel( - ClassName("io.mehow", "GeneratedFeatureFactory"), - features = emptyList(), - ) - - val fileSpec = model.prepare("generated") - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import io.mehow.laboratory.FeatureFactory - |import java.lang.Class - |import kotlin.collections.Set - |import kotlin.collections.emptySet - | - |internal fun FeatureFactory.Companion.generated(): FeatureFactory = GeneratedFeatureFactory - | - |private object GeneratedFeatureFactory : FeatureFactory { - | override fun create(): Set>> = emptySet>>() - |} - | - """.trimMargin() - } - } -}) diff --git a/library/generator/src/test/java/io/mehow/laboratory/generator/FeatureFlagGeneratorSpec.kt b/library/generator/src/test/java/io/mehow/laboratory/generator/FeatureFlagGeneratorSpec.kt deleted file mode 100644 index 81ced39cb..000000000 --- a/library/generator/src/test/java/io/mehow/laboratory/generator/FeatureFlagGeneratorSpec.kt +++ /dev/null @@ -1,534 +0,0 @@ -package io.mehow.laboratory.generator - -import com.squareup.kotlinpoet.ClassName -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.throwable.shouldHaveMessage -import io.kotest.property.Arb -import io.kotest.property.arbitrary.stringPattern -import io.kotest.property.checkAll -import io.mehow.laboratory.generator.Visibility.Internal -import io.mehow.laboratory.generator.Visibility.Public -import io.mehow.laboratory.generator.test.shouldSpecify -import java.util.Locale -import kotlin.DeprecationLevel.ERROR -import kotlin.DeprecationLevel.HIDDEN -import kotlin.DeprecationLevel.WARNING - -internal class FeatureFlagGeneratorSpec : DescribeSpec({ - describe("feature flag model") { - context("options") { - it("cannot be empty") { - val exception = shouldThrow { - FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - options = emptyList(), - ) - } - - exception shouldHaveMessage "io.mehow.FeatureA must have at least one option" - } - } - - context("default") { - it("cannot have no options") { - checkAll( - Arb.stringPattern("[a-z](0)([a-z]{0,10})"), - Arb.stringPattern("[a-z](1)([a-z]{0,10})"), - ) { first, second -> - val exception = shouldThrow { - FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(FeatureFlagOption(first), FeatureFlagOption(second)), - ) - } - - exception shouldHaveMessage "io.mehow.FeatureA must have exactly one default option" - } - } - - it("cannot have multiple options") { - checkAll( - Arb.stringPattern("[a-z](0)([a-z]{0,10})"), - Arb.stringPattern("[a-z](1)([a-z]{0,10})"), - Arb.stringPattern("[a-z](2)([a-z]{0,10})"), - ) { first, second, third -> - val exception = shouldThrow { - FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf( - FeatureFlagOption(first, isDefault = true), - FeatureFlagOption(second), - FeatureFlagOption(third, isDefault = true), - ), - ) - } - - exception shouldHaveMessage "io.mehow.FeatureA must have exactly one default option" - } - } - } - - context("supervisor option") { - it("cannot supervise itself") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(FeatureFlagOption("First", isDefault = true)), - ) - - val exception = shouldThrow { - FeatureFlagModel( - model.className, - model.options, - supervisor = Supervisor(model, model.options.first()), - ) - } - - exception shouldHaveMessage "io.mehow.FeatureA cannot supervise itself" - } - } - } - - describe("generated feature flag") { - it("can be internal") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - visibility = Internal, - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - | - |internal enum class FeatureA : Feature { - | First, - | Second, - | ; - | - | override val defaultOption: FeatureA - | get() = First - |} - | - """.trimMargin() - } - - it("can be public") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - visibility = Public, - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - | - |public enum class FeatureA : Feature { - | First, - | Second, - | ; - | - | override val defaultOption: FeatureA - | get() = First - |} - | - """.trimMargin() - } - - it("can have single option") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(FeatureFlagOption("First", isDefault = true)), - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - | - |public enum class FeatureA : Feature { - | First, - | ; - | - | override val defaultOption: FeatureA - | get() = First - |} - | - """.trimMargin() - } - - it("can have source") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - sourceOptions = listOf(FeatureFlagOption("Remote")), - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import java.lang.Class - | - |public enum class FeatureA : Feature { - | First, - | Second, - | ; - | - | override val defaultOption: FeatureA - | get() = First - | - | override val source: Class> = Source::class.java - | - | public enum class Source : Feature { - | Local, - | Remote, - | ; - | - | override val defaultOption: Source - | get() = Local - | } - |} - | - """.trimMargin() - } - - it("does not have source parameter if only source is Local") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - sourceOptions = listOf(FeatureFlagOption("Local")), - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - | - |public enum class FeatureA : Feature { - | First, - | Second, - | ; - | - | override val defaultOption: FeatureA - | get() = First - |} - | - """.trimMargin() - } - - it("filters out any custom local source") { - val localPermutations = (0b00000..0b11111).map { - listOf(it and 0b00001, it and 0b00010, it and 0b00100, it and 0b01000, it and 0b10000) - .map { mask -> mask != 0 } - .mapIndexed { index, mask -> - val chars = "local"[index].toString() - if (mask) chars else chars.replaceFirstChar { char -> char.titlecase(Locale.ROOT) } - }.joinToString(separator = "") - } - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - sourceOptions = (localPermutations + "Remote").map(::FeatureFlagOption), - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import java.lang.Class - | - |public enum class FeatureA : Feature { - | First, - | Second, - | ; - | - | override val defaultOption: FeatureA - | get() = First - | - | override val source: Class> = Source::class.java - | - | public enum class Source : Feature { - | Local, - | Remote, - | ; - | - | override val defaultOption: Source - | get() = Local - | } - |} - | - """.trimMargin() - } - - it("can change default source") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - sourceOptions = listOf(FeatureFlagOption("Remote", isDefault = true)), - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import java.lang.Class - | - |public enum class FeatureA : Feature { - | First, - | Second, - | ; - | - | override val defaultOption: FeatureA - | get() = First - | - | override val source: Class> = Source::class.java - | - | public enum class Source : Feature { - | Local, - | Remote, - | ; - | - | override val defaultOption: Source - | get() = Remote - | } - |} - | - """.trimMargin() - } - - it("source visibility follows feature visibility") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - visibility = Internal, - sourceOptions = listOf(FeatureFlagOption("Remote")), - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import java.lang.Class - | - |internal enum class FeatureA : Feature { - | First, - | Second, - | ; - | - | override val defaultOption: FeatureA - | get() = First - | - | override val source: Class> = Source::class.java - | - | internal enum class Source : Feature { - | Local, - | Remote, - | ; - | - | override val defaultOption: Source - | get() = Local - | } - |} - | - """.trimMargin() - } - - context("description") { - it("is added as KDoc") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - description = "Feature description", - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import kotlin.String - | - |/** - | * Feature description - | */ - |public enum class FeatureA : Feature { - | First, - | Second, - | ; - | - | override val defaultOption: FeatureA - | get() = First - | - | override val description: String = "Feature description" - |} - | - """.trimMargin() - } - - it("does not break hyperlinks") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - description = "Some [long hyperlink](https://square.github.io/kotlinpoet/1.x/kotlinpoet-classinspector-elements/com.squareup.kotlinpoet.classinspector.elements/) in the KDoc.", - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import kotlin.String - | - |/** - | * Some - | * [long hyperlink](https://square.github.io/kotlinpoet/1.x/kotlinpoet-classinspector-elements/com.squareup.kotlinpoet.classinspector.elements/) - | * in the KDoc. - | */ - |public enum class FeatureA : Feature { - | First, - | Second, - | ; - | - | override val defaultOption: FeatureA - | get() = First - | - | override val description: String = - | "Some [long hyperlink](https://square.github.io/kotlinpoet/1.x/kotlinpoet-classinspector-elements/com.squareup.kotlinpoet.classinspector.elements/) in the KDoc." - |} - | - """.trimMargin() - } - } - - context("can be deprecated") { - it("with warning level by default") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - deprecation = Deprecation("Deprecation message"), - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import kotlin.Deprecated - |import kotlin.DeprecationLevel - |import kotlin.Suppress - | - |@Deprecated( - | message = "Deprecation message", - | level = DeprecationLevel.WARNING, - |) - |public enum class FeatureA : Feature<@Suppress("DEPRECATION") FeatureA> { - | First, - | Second, - | ; - | - | @Suppress("DEPRECATION") - | override val defaultOption: FeatureA - | get() = First - |} - | - """.trimMargin() - } - - enumValues().forEach { level -> - it("with explicit $level deprecation level") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - deprecation = Deprecation("Deprecation message", level), - ) - val suppressLevel = when (level) { - WARNING -> "DEPRECATION" - ERROR, HIDDEN -> "DEPRECATION_ERROR" - } - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import kotlin.Deprecated - |import kotlin.DeprecationLevel - |import kotlin.Suppress - | - |@Deprecated( - | message = "Deprecation message", - | level = DeprecationLevel.${level}, - |) - |public enum class FeatureA : Feature<@Suppress("$suppressLevel") FeatureA> { - | First, - | Second, - | ; - | - | @Suppress("$suppressLevel") - | override val defaultOption: FeatureA - | get() = First - |} - | - """.trimMargin() - } - } - } - - it("can have supervisor") { - val supervisor = FeatureFlagModel( - ClassName("io.mehow.supervisor", "Supervisor"), - listOf(FeatureFlagOption("First", isDefault = true)), - ) - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - supervisor = Supervisor(supervisor, supervisor.options.first()), - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import io.mehow.supervisor.Supervisor - | - |public enum class FeatureA : Feature { - | First, - | Second, - | ; - | - | override val defaultOption: FeatureA - | get() = First - | - | override val supervisorOption: Feature<*> = Supervisor.First - |} - | - """.trimMargin() - } - } -}) diff --git a/library/generator/src/test/java/io/mehow/laboratory/generator/OptionFactoryModelSpec.kt b/library/generator/src/test/java/io/mehow/laboratory/generator/OptionFactoryModelSpec.kt deleted file mode 100644 index 2c6140b8e..000000000 --- a/library/generator/src/test/java/io/mehow/laboratory/generator/OptionFactoryModelSpec.kt +++ /dev/null @@ -1,306 +0,0 @@ -package io.mehow.laboratory.generator - -import com.squareup.kotlinpoet.ClassName -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.throwable.shouldHaveMessage -import io.kotest.property.Arb -import io.kotest.property.arbitrary.stringPattern -import io.kotest.property.checkAll -import io.mehow.laboratory.generator.Visibility.Internal -import io.mehow.laboratory.generator.Visibility.Public -import io.mehow.laboratory.generator.test.shouldSpecify -import kotlin.DeprecationLevel.ERROR -import kotlin.DeprecationLevel.HIDDEN -import kotlin.DeprecationLevel.WARNING - -internal class OptionFactoryModelSpec : DescribeSpec({ - describe("option factory model") { - it("features cannot have duplicate keys") { - checkAll( - Arb.stringPattern("[a-z](0)([a-z]{0,10})"), - Arb.stringPattern("[a-z](1)([a-z]{0,10})"), - ) { first, second -> - val features = listOf( - FeatureFlagModel( - className = ClassName("io.mehow1", first), - options = listOf(FeatureFlagOption("First", isDefault = true)), - key = "FeatureA", - ), - FeatureFlagModel( - className = ClassName("io.mehow1", second), - options = listOf(FeatureFlagOption("First", isDefault = true)), - key = "FeatureA", - ), - FeatureFlagModel( - className = ClassName("io.mehow2", "${second}A"), - options = listOf(FeatureFlagOption("First", isDefault = true)), - ), - FeatureFlagModel( - className = ClassName("io.mehow2", "${second}B"), - options = listOf(FeatureFlagOption("First", isDefault = true)), - ), - FeatureFlagModel( - className = ClassName("io.mehow2", "${second}C"), - options = listOf(FeatureFlagOption("First", isDefault = true)), - key = "FeatureB" - ), - FeatureFlagModel( - className = ClassName("io.mehow3", first), - options = listOf(FeatureFlagOption("First", isDefault = true)), - key = "FeatureC", - ), - FeatureFlagModel( - className = ClassName("io.mehow3", second), - options = listOf(FeatureFlagOption("First", isDefault = true)), - key = "FeatureB", - ), - ) - - val exception = shouldThrow { - OptionFactoryModel(ClassName("io.mehow", "GeneratedOptionFactory"), features) - } - - exception shouldHaveMessage """ - |Feature flags must have unique keys. Found following duplicates: - | - FeatureA: [io.mehow1.${first}, io.mehow1.${second}] - | - FeatureB: [io.mehow2.${second}C, io.mehow3.${second}] - """.trimMargin() - } - } - - it("features cannot have keys the same as other fqcns") { - checkAll( - Arb.stringPattern("[a-z](0)([a-z]{0,10})"), - Arb.stringPattern("[a-z](0)([a-z]{0,10})"), - ) { packageName, simpleName -> - val features = listOf( - FeatureFlagModel( - className = ClassName(packageName, simpleName), - options = listOf(FeatureFlagOption("First", isDefault = true)), - ), - FeatureFlagModel( - className = ClassName("io.mehow", "FeatureName"), - options = listOf(FeatureFlagOption("First", isDefault = true)), - key = "$packageName.$simpleName" - ), - ) - - val exception = shouldThrow { - OptionFactoryModel(ClassName("io.mehow", "GeneratedOptionFactory"), features) - } - - exception shouldHaveMessage """ - |Feature flags must have unique keys. Found following duplicates: - | - $packageName.$simpleName: [$packageName.$simpleName, io.mehow.FeatureName] - """.trimMargin() - } - } - } - - describe("generated option factory") { - it("can be internal") { - val features = listOf( - FeatureFlagModel( - className = ClassName("io.mehow", "FeatureA"), - options = listOf(FeatureFlagOption("OneA", isDefault = true), FeatureFlagOption("OneB")), - key = "FeatureA", - ), - FeatureFlagModel( - className = ClassName("io.mehow", "FeatureB"), - options = listOf(FeatureFlagOption("TwoA", isDefault = true)), - ), - FeatureFlagModel( - className = ClassName("io.mehow.c", "FeatureC"), - options = listOf(FeatureFlagOption("ThreeA", isDefault = true), FeatureFlagOption("ThreeB")), - key = "FeatureC", - ), - ) - val model = OptionFactoryModel( - ClassName("io.mehow", "GeneratedOptionFactory"), - features, - visibility = Internal, - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.c.FeatureC - |import io.mehow.laboratory.Feature - |import io.mehow.laboratory.OptionFactory - |import kotlin.String - | - |internal fun OptionFactory.Companion.generated(): OptionFactory = GeneratedOptionFactory - | - |private object GeneratedOptionFactory : OptionFactory { - | override fun create(key: String, name: String): Feature<*>? = when (key) { - | "FeatureA" -> when (name) { - | "OneA" -> FeatureA.OneA - | "OneB" -> FeatureA.OneB - | else -> null - | } - | "FeatureC" -> when (name) { - | "ThreeA" -> FeatureC.ThreeA - | "ThreeB" -> FeatureC.ThreeB - | else -> null - | } - | "io.mehow.FeatureB" -> when (name) { - | "TwoA" -> FeatureB.TwoA - | else -> null - | } - | else -> null - | } - |} - | - """.trimMargin() - } - - it("can be public") { - val features = listOf( - FeatureFlagModel( - className = ClassName("io.mehow", "FeatureA"), - options = listOf(FeatureFlagOption("OneA", isDefault = true), FeatureFlagOption("OneB")), - key = "FeatureA", - ), - FeatureFlagModel( - className = ClassName("io.mehow", "FeatureB"), - options = listOf(FeatureFlagOption("TwoA", isDefault = true)), - ), - FeatureFlagModel( - className = ClassName("io.mehow.c", "FeatureC"), - options = listOf(FeatureFlagOption("ThreeA", isDefault = true), FeatureFlagOption("ThreeB")), - key = "FeatureC", - ), - ) - val model = OptionFactoryModel( - ClassName("io.mehow", "GeneratedOptionFactory"), - features, - visibility = Public, - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.c.FeatureC - |import io.mehow.laboratory.Feature - |import io.mehow.laboratory.OptionFactory - |import kotlin.String - | - |public fun OptionFactory.Companion.generated(): OptionFactory = GeneratedOptionFactory - | - |private object GeneratedOptionFactory : OptionFactory { - | override fun create(key: String, name: String): Feature<*>? = when (key) { - | "FeatureA" -> when (name) { - | "OneA" -> FeatureA.OneA - | "OneB" -> FeatureA.OneB - | else -> null - | } - | "FeatureC" -> when (name) { - | "ThreeA" -> FeatureC.ThreeA - | "ThreeB" -> FeatureC.ThreeB - | else -> null - | } - | "io.mehow.FeatureB" -> when (name) { - | "TwoA" -> FeatureB.TwoA - | else -> null - | } - | else -> null - | } - |} - | - """.trimMargin() - } - - it("is optimized in case of no features") { - val model = OptionFactoryModel( - ClassName("io.mehow", "GeneratedOptionFactory"), - features = emptyList(), - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import io.mehow.laboratory.OptionFactory - |import kotlin.String - | - |internal fun OptionFactory.Companion.generated(): OptionFactory = GeneratedOptionFactory - | - |private object GeneratedOptionFactory : OptionFactory { - | override fun create(key: String, name: String): Feature<*>? = null - |} - | - """.trimMargin() - } - - it("suppresses usage of deprecated features") { - val features = listOf( - FeatureFlagModel( - className = ClassName("io.mehow", "FeatureA"), - options = listOf(FeatureFlagOption("OneA", isDefault = true)), - ), - FeatureFlagModel( - className = ClassName("io.mehow", "FeatureB"), - options = listOf(FeatureFlagOption("TwoA", isDefault = true)), - deprecation = Deprecation("message", WARNING), - ), - FeatureFlagModel( - className = ClassName("io.mehow", "FeatureC"), - options = listOf(FeatureFlagOption("ThreeA", isDefault = true)), - deprecation = Deprecation("message", ERROR), - ), - FeatureFlagModel( - className = ClassName("io.mehow", "FeatureD"), - options = listOf(FeatureFlagOption("FourA", isDefault = true)), - deprecation = Deprecation("message", HIDDEN), - ), - ) - val model = OptionFactoryModel( - ClassName("io.mehow", "GeneratedOptionFactory"), - features, - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import io.mehow.laboratory.OptionFactory - |import kotlin.String - |import kotlin.Suppress - | - |internal fun OptionFactory.Companion.generated(): OptionFactory = GeneratedOptionFactory - | - |private object GeneratedOptionFactory : OptionFactory { - | override fun create(key: String, name: String): Feature<*>? = when (key) { - | "io.mehow.FeatureA" -> when (name) { - | "OneA" -> FeatureA.OneA - | else -> null - | } - | "io.mehow.FeatureB" -> @Suppress("DEPRECATION") when (name) { - | "TwoA" -> FeatureB.TwoA - | else -> null - | } - | "io.mehow.FeatureC" -> @Suppress("DEPRECATION_ERROR") when (name) { - | "ThreeA" -> FeatureC.ThreeA - | else -> null - | } - | "io.mehow.FeatureD" -> @Suppress("DEPRECATION_ERROR") when (name) { - | "FourA" -> FeatureD.FourA - | else -> null - | } - | else -> null - | } - |} - | - """.trimMargin() - } - } -}) diff --git a/library/generator/src/test/java/io/mehow/laboratory/generator/SourcedFeatureStorageGeneratorSpec.kt b/library/generator/src/test/java/io/mehow/laboratory/generator/SourcedFeatureStorageGeneratorSpec.kt deleted file mode 100644 index 612310e91..000000000 --- a/library/generator/src/test/java/io/mehow/laboratory/generator/SourcedFeatureStorageGeneratorSpec.kt +++ /dev/null @@ -1,266 +0,0 @@ -package io.mehow.laboratory.generator - -import com.squareup.kotlinpoet.ClassName -import io.kotest.core.spec.style.DescribeSpec -import io.mehow.laboratory.generator.Visibility.Internal -import io.mehow.laboratory.generator.Visibility.Public -import io.mehow.laboratory.generator.test.shouldSpecify -import java.util.Locale - -internal class SourcedFeatureStorageGeneratorSpec : DescribeSpec({ - describe("generated feature storage") { - it("can be internal") { - val model = SourcedFeatureStorageModel( - ClassName("io.mehow", "SourcedGeneratedFeatureStorage"), - sourceNames = listOf("Firebase", "S3"), - visibility = Internal, - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.FeatureStorage - |import io.mehow.laboratory.FeatureStorage.Companion.sourced - |import kotlin.String - |import kotlin.collections.Map - |import kotlin.collections.emptyMap - |import kotlin.collections.plus - |import kotlin.to - | - |internal fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage): FirebaseStep = - | Builder(localSource, emptyMap()) - | - |internal interface FirebaseStep { - | public fun firebaseSource(source: FeatureStorage): S3Step - |} - | - |internal interface S3Step { - | public fun s3Source(source: FeatureStorage): BuildingStep - |} - | - |internal interface BuildingStep { - | public fun build(): FeatureStorage - |} - | - |private data class Builder( - | private val localSource: FeatureStorage, - | private val remoteSources: Map, - |) : FirebaseStep, S3Step, BuildingStep { - | override fun firebaseSource(source: FeatureStorage): S3Step = copy( - | remoteSources = remoteSources + ("Firebase" to source) - | ) - | - | override fun s3Source(source: FeatureStorage): BuildingStep = copy( - | remoteSources = remoteSources + ("S3" to source) - | ) - | - | override fun build(): FeatureStorage = sourced(localSource, remoteSources) - |} - | - """.trimMargin() - } - - it("can be public") { - val model = SourcedFeatureStorageModel( - ClassName("io.mehow", "SourcedGeneratedFeatureStorage"), - sourceNames = listOf("Firebase", "S3"), - visibility = Public, - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.FeatureStorage - |import io.mehow.laboratory.FeatureStorage.Companion.sourced - |import kotlin.String - |import kotlin.collections.Map - |import kotlin.collections.emptyMap - |import kotlin.collections.plus - |import kotlin.to - | - |public fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage): FirebaseStep = - | Builder(localSource, emptyMap()) - | - |public interface FirebaseStep { - | public fun firebaseSource(source: FeatureStorage): S3Step - |} - | - |public interface S3Step { - | public fun s3Source(source: FeatureStorage): BuildingStep - |} - | - |public interface BuildingStep { - | public fun build(): FeatureStorage - |} - | - |private data class Builder( - | private val localSource: FeatureStorage, - | private val remoteSources: Map, - |) : FirebaseStep, S3Step, BuildingStep { - | override fun firebaseSource(source: FeatureStorage): S3Step = copy( - | remoteSources = remoteSources + ("Firebase" to source) - | ) - | - | override fun s3Source(source: FeatureStorage): BuildingStep = copy( - | remoteSources = remoteSources + ("S3" to source) - | ) - | - | override fun build(): FeatureStorage = sourced(localSource, remoteSources) - |} - | - """.trimMargin() - } - - it("ignores duplicate source names") { - val model = SourcedFeatureStorageModel( - ClassName("io.mehow", "SourcedGeneratedFeatureStorage"), - sourceNames = listOf("Foo", "Bar", "Baz", "Foo", "Baz", "Foo"), - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.FeatureStorage - |import io.mehow.laboratory.FeatureStorage.Companion.sourced - |import kotlin.String - |import kotlin.collections.Map - |import kotlin.collections.emptyMap - |import kotlin.collections.plus - |import kotlin.to - | - |internal fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage): BarStep = - | Builder(localSource, emptyMap()) - | - |internal interface BarStep { - | public fun barSource(source: FeatureStorage): BazStep - |} - | - |internal interface BazStep { - | public fun bazSource(source: FeatureStorage): FooStep - |} - | - |internal interface FooStep { - | public fun fooSource(source: FeatureStorage): BuildingStep - |} - | - |internal interface BuildingStep { - | public fun build(): FeatureStorage - |} - | - |private data class Builder( - | private val localSource: FeatureStorage, - | private val remoteSources: Map, - |) : BarStep, BazStep, FooStep, BuildingStep { - | override fun barSource(source: FeatureStorage): BazStep = copy( - | remoteSources = remoteSources + ("Bar" to source) - | ) - | - | override fun bazSource(source: FeatureStorage): FooStep = copy( - | remoteSources = remoteSources + ("Baz" to source) - | ) - | - | override fun fooSource(source: FeatureStorage): BuildingStep = copy( - | remoteSources = remoteSources + ("Foo" to source) - | ) - | - | override fun build(): FeatureStorage = sourced(localSource, remoteSources) - |} - | - """.trimMargin() - } - - it("ignores local source name") { - val localPermutations = (0b00000..0b11111).map { - listOf(it and 0b00001, it and 0b00010, it and 0b00100, it and 0b01000, it and 0b10000) - .map { mask -> mask != 0 } - .mapIndexed { index, mask -> - val chars = "local"[index].toString() - if (mask) chars else chars.replaceFirstChar { char -> char.titlecase(Locale.ROOT) } - }.joinToString(separator = "") - } - val model = SourcedFeatureStorageModel( - ClassName("io.mehow", "SourcedGeneratedFeatureStorage"), - sourceNames = localPermutations + "Foo", - visibility = Public, - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.FeatureStorage - |import io.mehow.laboratory.FeatureStorage.Companion.sourced - |import kotlin.String - |import kotlin.collections.Map - |import kotlin.collections.emptyMap - |import kotlin.collections.plus - |import kotlin.to - | - |public fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage): FooStep = - | Builder(localSource, emptyMap()) - | - |public interface FooStep { - | public fun fooSource(source: FeatureStorage): BuildingStep - |} - | - |public interface BuildingStep { - | public fun build(): FeatureStorage - |} - | - |private data class Builder( - | private val localSource: FeatureStorage, - | private val remoteSources: Map, - |) : FooStep, BuildingStep { - | override fun fooSource(source: FeatureStorage): BuildingStep = copy( - | remoteSources = remoteSources + ("Foo" to source) - | ) - | - | override fun build(): FeatureStorage = sourced(localSource, remoteSources) - |} - | - """.trimMargin() - } - - it("can have only local source") { - val model = SourcedFeatureStorageModel( - ClassName("io.mehow", "SourcedGeneratedFeatureStorage"), - sourceNames = emptyList(), - visibility = Public, - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.FeatureStorage - |import io.mehow.laboratory.FeatureStorage.Companion.sourced - |import kotlin.String - |import kotlin.collections.Map - |import kotlin.collections.emptyMap - | - |public fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage): BuildingStep = - | Builder(localSource, emptyMap()) - | - |public interface BuildingStep { - | public fun build(): FeatureStorage - |} - | - |private data class Builder( - | private val localSource: FeatureStorage, - | private val remoteSources: Map, - |) : BuildingStep { - | override fun build(): FeatureStorage = sourced(localSource, remoteSources) - |} - | - """.trimMargin() - } - } -}) diff --git a/library/generator/src/test/java/io/mehow/laboratory/generator/SupervisorSpec.kt b/library/generator/src/test/java/io/mehow/laboratory/generator/SupervisorSpec.kt deleted file mode 100644 index db28ebe69..000000000 --- a/library/generator/src/test/java/io/mehow/laboratory/generator/SupervisorSpec.kt +++ /dev/null @@ -1,44 +0,0 @@ -package io.mehow.laboratory.generator - -import com.squareup.kotlinpoet.ClassName -import io.kotest.assertions.throwables.shouldNotThrowAny -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.throwable.shouldHaveMessage -import io.kotest.property.Arb -import io.kotest.property.arbitrary.stringPattern -import io.kotest.property.checkAll - -internal class SupervisorSpec : DescribeSpec({ - context("supervisor option") { - it("must be present in parent") { - checkAll(Arb.stringPattern("[a-z](0)([a-z]{0,10})")) { optionName -> - val option = FeatureFlagOption(optionName, isDefault = true) - val feature = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(option), - ) - - shouldNotThrowAny { - Supervisor(feature, option) - } - } - } - - it("cannot be absent in parent") { - checkAll(Arb.stringPattern("[a-z](0)([a-z]{0,10})")) { optionName -> - val feature = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - ) - val option = FeatureFlagOption(optionName, isDefault = true) - - val exception = shouldThrow { - Supervisor(feature, option) - } - - exception shouldHaveMessage "Feature flag io.mehow.FeatureA does not contain option $optionName" - } - } - } -}) diff --git a/library/generator/src/test/java/io/mehow/laboratory/generator/test/KotlinPoetMatchers.kt b/library/generator/src/test/java/io/mehow/laboratory/generator/test/KotlinPoetMatchers.kt deleted file mode 100644 index 390d2848f..000000000 --- a/library/generator/src/test/java/io/mehow/laboratory/generator/test/KotlinPoetMatchers.kt +++ /dev/null @@ -1,6 +0,0 @@ -package io.mehow.laboratory.generator.test - -import com.squareup.kotlinpoet.FileSpec -import io.kotest.matchers.shouldBe - -internal infix fun FileSpec.shouldSpecify(value: String) = toString() shouldBe value diff --git a/library/gradle-plugin/build.gradle b/library/gradle-plugin/build.gradle deleted file mode 100644 index 69783c85f..000000000 --- a/library/gradle-plugin/build.gradle +++ /dev/null @@ -1,69 +0,0 @@ -import org.jetbrains.dokka.gradle.DokkaTask -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - - -plugins { - id "org.jetbrains.kotlin.jvm" - id "java-gradle-plugin" -} - -gradlePlugin { - plugins { - laboratory { - id = "io.mehow.laboratory" - implementationClass = "io.mehow.laboratory.gradle.LaboratoryPlugin" - } - } -} - -test.useJUnitPlatform() - -configurations { - fixtureClasspath -} - -tasks.getByName("pluginUnderTestMetadata").getPluginClasspath().from(configurations.fixtureClasspath) - -dependencies { - implementation project(":generator") - implementation libs.kotlin.gradlePlugin - compileOnly libs.android.gradlePlugin - - testImplementation libs.kotest.runnerJunit5 - testImplementation libs.kotest.assertions - - fixtureClasspath libs.kotlin.gradlePlugin - fixtureClasspath libs.android.gradlePlugin -} - -def versionDir = "${buildDir}/generated/source/laboratory" -sourceSets.main.java.srcDirs += versionDir - -def generateVersion = tasks.register("pluginVersion") { - inputs.property "version", version - outputs.dir file(versionDir) - - doLast { - def outputFile = file("$versionDir/io/mehow/laboratory/Config.kt") - outputFile.parentFile.mkdirs() - outputFile.text = """package io.mehow.laboratory - -internal const val laboratoryVersion = "$version" -""" - } -} - -tasks.withType(KotlinCompile).configureEach { - it.dependsOn(generateVersion) -} - -apply from: "$rootDir/gradle/dokka-config.gradle" -apply from: "$rootDir/gradle/gradle-mvn-push.gradle" - -tasks.withType(Jar).configureEach { - it.dependsOn(generateVersion) -} - -tasks.withType(DokkaTask).configureEach { - it.dependsOn(generateVersion) -} diff --git a/library/gradle.properties b/library/gradle.properties deleted file mode 100644 index 153ea69ae..000000000 --- a/library/gradle.properties +++ /dev/null @@ -1,29 +0,0 @@ -GROUP=io.mehow.laboratory -VERSION_NAME=1.1.1-SNAPSHOT - -POM_DESCRIPTION=Library for feature flags management. - -POM_URL=https://github.com/MiSikora/laboratory -POM_SCM_URL=https://github.com/MiSikora/laboratory -POM_SCM_CONNECTION=scm:git:git://github.com/MiSikora/laboratory.git -POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/MiSikora/laboratory.git - -POM_LICENCE_NAME=The Apache Software License, Version 2.0 -POM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt -POM_LICENCE_DIST=repo - -POM_DEVELOPER_ID=michalsikora90 -POM_DEVELOPER_NAME=Michal Sikora - -android.useAndroidX=true - -# Increase the build VMs heap size. Default is 512m. -# Increase metaspace for Dokka https://github.com/Kotlin/dokka/issues/1405 -# Illegal access: https://youtrack.jetbrains.com/issue/KT-45545 -org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=1g --illegal-access=permit -org.gradle.parallel=true - -android.defaults.buildfeatures.buildconfig=false -android.defaults.buildfeatures.aidl=false -android.defaults.buildfeatures.renderscript=false -android.defaults.buildfeatures.shaders=false diff --git a/library/gradle/dependencies.toml b/library/gradle/dependencies.toml deleted file mode 100644 index 10a178d6a..000000000 --- a/library/gradle/dependencies.toml +++ /dev/null @@ -1,73 +0,0 @@ -[versions] -androidBuild-targetSdk = "33" -androidPlugin = "8.0.2" -coroutines = "1.6.4" -kotest = "4.6.3" -hyperion = "0.9.37" -detekt = "1.22.0" - -[libraries] -android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidPlugin" } - -android-x-dataStore = { group = "androidx.datastore", name = "datastore-core", version = "1.0.0" } -android-x-appCompat = { group = "androidx.appcompat", name = "appcompat", version = "1.6.1" } -android-x-viewModelKtx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version = "2.6.1" } -android-x-fragmentKtx = { group = "androidx.fragment", name = "fragment-ktx", version = "1.5.7" } -android-x-viewPager2 = { group = "androidx.viewpager2", name = "viewpager2", version = "1.0.0" } -android-x-recyclerView = { group = "androidx.recyclerview", name = "recyclerview", version = "1.3.0" } - -android-x-test-coreKtx = { group = "androidx.test", name = "core-ktx", version = "1.5.0" } -android-x-test-orchestrator = { group = "androidx.test", name = "orchestrator", version = "1.4.2" } -android-x-test-runner = { group = "androidx.test", name = "runner", version = "1.5.2" } -android-x-testExtJunitKtx = { group = "androidx.test.ext", name = "junit-ktx", version = "1.1.5" } - -android-material = { group = "com.google.android.material", name = "material", version = "1.9.0" } - -firebase-databaseKtx = { group = "com.google.firebase", name = "firebase-database-ktx", version = "20.2.2" } - -googleServices-gradlePlugin = { group = "com.google.gms", name = "google-services", version = "4.3.15" } - -kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version = "1.8.21" } - -kotlin-x-coroutinesCore = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } -kotlin-x-coroutinesAndroid = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } -kotlin-x-coroutinesTest = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } -kotlin-x-binaryCompatibility-gradlePlugin = { group = "org.jetbrains.kotlinx", name = "binary-compatibility-validator", version = "0.13.1" } - -dokka-gradlePlugin = { group = "org.jetbrains.dokka", name = "dokka-gradle-plugin", version = "1.8.10" } - -kotest-runnerJunit5 = { group = "io.kotest", name = "kotest-runner-junit5-jvm", version.ref = "kotest" } -kotest-assertions = { group = "io.kotest", name = "kotest-assertions-core-jvm", version.ref = "kotest" } -kotest-property = { group = "io.kotest", name = "kotest-property-jvm", version.ref = "kotest" } - -hyperion-core = { group = "com.willowtreeapps.hyperion", name = "hyperion-core", version.ref = "hyperion" } -hyperion-plugin = { group = "com.willowtreeapps.hyperion", name = "hyperion-plugin", version.ref = "hyperion" } - -autoService = { group = "com.google.auto.service", name = "auto-service", version = "1.1.0" } - -mavenPublish-gradlePlugin = { group = "com.vanniktech", name = "gradle-maven-publish-plugin", version = "0.25.2" } - -detekt-gradlePlugin = { group = "io.gitlab.arturbosch.detekt", name = "detekt-gradle-plugin", version.ref = "detekt" } -detekt-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-formatting", version.ref = "detekt" } -detekt-cli = { group = "io.gitlab.arturbosch.detekt", name = "detekt-cli", version.ref = "detekt" } - -gradleVersions-gradlePlugin = { group = "com.github.ben-manes", name = "gradle-versions-plugin", version = "0.46.0" } - -kotlinPoet = { group = "com.squareup", name = "kotlinpoet", version = "1.14.2" } -wireGradlePlugin = { group = "com.squareup.wire", name = "wire-gradle-plugin", version = "4.7.0" } -turbine = { group = "app.cash.turbine", name = "turbine", version = "0.7.0" } - -laboratory-core = { module = "io.mehow.laboratory:laboratory" } -laboratory-inspector = { module = "io.mehow.laboratory:inspector" } -laboratory-hyperionPlugin = { module = "io.mehow.laboratory:hyperion-plugin" } -laboratory-sharedPreferences = { module = "io.mehow.laboratory:shared-preferences" } -laboratory-dataStore = { module = "io.mehow.laboratory:data-store" } -laboratory-generator = { module = "io.mehow.laboratory:generator" } -laboratory-gradlePlugin = { module = "io.mehow.laboratory:gradle-plugin" } - -[plugins] -gradleVersions = { id = "com.github.ben-manes.versions" } -detekt = { id = "io.gitlab.arturbosch.detekt" } -binaryCompatibility = { id = "binary-compatibility-validator", version = "0.13.1" } -dokka = { id = "org.jetbrains.dokka" } -mavenPublish = { id = "com.vanniktech.maven.publish" } diff --git a/library/gradle/dokka-config.gradle b/library/gradle/dokka-config.gradle deleted file mode 100644 index a71f61957..000000000 --- a/library/gradle/dokka-config.gradle +++ /dev/null @@ -1,14 +0,0 @@ -apply plugin: libs.plugins.dokka.get().pluginId - -dokkaHtml { - outputDirectory.set(file("$rootDir/docs/api/${project.name}")) - - dokkaSourceSets { - configureEach { - jdkVersion.set(8) - reportUndocumented.set(false) - skipDeprecated.set(true) - skipEmptyPackages.set(true) - } - } -} diff --git a/library/gradle/gradle-mvn-push.gradle b/library/gradle/gradle-mvn-push.gradle deleted file mode 100644 index 73038ee6a..000000000 --- a/library/gradle/gradle-mvn-push.gradle +++ /dev/null @@ -1,6 +0,0 @@ -apply plugin: libs.plugins.mavenPublish.get().pluginId - -mavenPublishing { - publishToMavenCentral() - signAllPublications() -} diff --git a/library/hyperion-plugin/build.gradle b/library/hyperion-plugin/build.gradle deleted file mode 100644 index 57e109524..000000000 --- a/library/hyperion-plugin/build.gradle +++ /dev/null @@ -1,19 +0,0 @@ -plugins { - id "com.android.library" - id "org.jetbrains.kotlin.android" - id "org.jetbrains.kotlin.kapt" -} - -android { - namespace "io.mehow.laboratory.hyperion" - resourcePrefix "io_mehow_laboratory_" -} - -dependencies { - api project(":inspector") - api libs.hyperion.plugin - implementation libs.android.x.appCompat - kapt libs.autoService -} - -apply from: "$rootDir/gradle/gradle-mvn-push.gradle" diff --git a/library/hyperion-plugin/src/main/AndroidManifest.xml b/library/hyperion-plugin/src/main/AndroidManifest.xml deleted file mode 100644 index 01b745f9e..000000000 --- a/library/hyperion-plugin/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/library/inspector/build.gradle b/library/inspector/build.gradle deleted file mode 100644 index 2e556f652..000000000 --- a/library/inspector/build.gradle +++ /dev/null @@ -1,34 +0,0 @@ -plugins { - id "com.android.library" - id "org.jetbrains.kotlin.android" -} - -android { - namespace "io.mehow.laboratory.inspector" - resourcePrefix "io_mehow_laboratory_" - - defaultConfig { - consumerProguardFile "io-mehow-laboratory-inspector.pro" - } -} - -dependencies { - api project(":laboratory") - implementation libs.hyperion.plugin - implementation libs.android.x.appCompat - implementation libs.android.x.fragmentKtx - implementation libs.android.x.viewModelKtx - implementation libs.android.x.recyclerView - implementation libs.android.x.viewPager2 - implementation libs.android.material - implementation libs.kotlin.x.coroutinesAndroid - - testImplementation libs.kotest.runnerJunit5 - testImplementation libs.kotest.assertions - testImplementation libs.kotest.property - testImplementation libs.turbine - testImplementation libs.kotlin.x.coroutinesTest -} - -apply from: "$rootDir/gradle/dokka-config.gradle" -apply from: "$rootDir/gradle/gradle-mvn-push.gradle" diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/OptionViewGroup.kt b/library/inspector/src/main/java/io/mehow/laboratory/inspector/OptionViewGroup.kt deleted file mode 100644 index 101ef40e7..000000000 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/OptionViewGroup.kt +++ /dev/null @@ -1,90 +0,0 @@ -package io.mehow.laboratory.inspector - -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import android.widget.CompoundButton -import androidx.appcompat.content.res.AppCompatResources -import androidx.appcompat.widget.PopupMenu -import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipGroup -import io.mehow.laboratory.Feature -import com.google.android.material.R as MaterialR - -internal class OptionViewGroup @JvmOverloads constructor( - context: Context, - attrs: AttributeSet, - defStyle: Int = MaterialR.attr.chipGroupStyle, -) : ChipGroup(context, attrs, defStyle) { - private val inflater = LayoutInflater.from(context) - private var listener: OptionGroupListener? = null - - init { - isSelectionRequired = true - } - - fun setOnSelectFeatureListener(listener: OptionGroupListener?) { - this.listener = listener - } - - fun render(models: List, isEnabled: Boolean) { - chips.forEach(::removeOnCheckedChangeListener) - removeAllViews() - models.map { createChip(it, isEnabled) }.forEach(::addView) - } - - private fun createChip(model: OptionUiModel, isEnabled: Boolean): Chip { - val chip = inflater.inflate(R.layout.io_mehow_laboratory_feature_option_chip, this, false) as Chip - return chip.apply { - text = model.option.name - isChecked = model.isSelected - if (model.supervisedFeatures.isNotEmpty()) { - chipIcon = AppCompatResources.getDrawable(context, R.drawable.io_mehow_laboratory_supervisor) - setOnLongClickListener { showSupervisedFeaturesMenu(this, model.supervisedFeatures) } - } - isActivated = isEnabled - this.isEnabled = isEnabled - setOnCheckedChangeListener(createListener(model)) - } - } - - private fun createListener(model: OptionUiModel) = CompoundButton.OnCheckedChangeListener { chip, isChecked -> - if (isChecked) { - (chip as Chip).deselectOtherChips() - listener?.onSelectOption(model.option) - } - } - - // ChipGroup.isSingleSelection does not work with initial selection from code. - private fun Chip.deselectOtherChips() { - chips.filter { it !== this }.forEach { chip -> chip.isChecked = false } - } - - private fun removeOnCheckedChangeListener(chip: Chip) = chip.setOnCheckedChangeListener(null) - - private fun showSupervisedFeaturesMenu(anchor: Chip, features: List>>): Boolean { - PopupMenu(context, anchor).apply { - features.forEachIndexed { index, feature -> - menu.add(0, index, index, feature.simpleName) - } - setOnMenuItemClickListener { - listener?.onSelectSupervisedFeature(features[it.order]) - true - } - }.show() - return true - } - - private val chips: Sequence get() = sequence { - for (index in 0 until childCount) { - val chip = getChildAt(index) as? Chip ?: continue - yield(chip) - } - } - - interface OptionGroupListener { - fun onSelectOption(option: Feature<*>) - - fun onSelectSupervisedFeature(feature: Class>) - } -} diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SourceViewGroup.kt b/library/inspector/src/main/java/io/mehow/laboratory/inspector/SourceViewGroup.kt deleted file mode 100644 index a9b620314..000000000 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SourceViewGroup.kt +++ /dev/null @@ -1,54 +0,0 @@ -package io.mehow.laboratory.inspector - -import android.content.Context -import android.util.AttributeSet -import android.view.View -import android.widget.AdapterView -import androidx.appcompat.widget.AppCompatSpinner -import io.mehow.laboratory.Feature -import androidx.appcompat.R as AppCompatR - -internal class SourceViewGroup @JvmOverloads constructor( - context: Context, - attrs: AttributeSet, - defStyle: Int = AppCompatR.attr.spinnerStyle, -) : AppCompatSpinner(context, attrs, defStyle) { - internal var listener: OnSelectSourceListener? = null - - override fun getAdapter() = super.getAdapter() as? SourceAdapter - - fun setOnSelectSourceListener(listener: OnSelectSourceListener?) { - this.listener = listener - } - - fun render(models: List) { - val features = models.map(OptionUiModel::option) - val selectedFeature = models.firstOrNull(OptionUiModel::isSelected)?.option ?: return - val newAdapter = SourceAdapter(features) - onItemSelectedListener = createListener() - adapter = newAdapter - val position = newAdapter.positionOf(selectedFeature) - setSelection(position) - } - - private fun createListener() = object : OnItemSelectedListener { - var ignoreItem = true - - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - if (ignoreItem) { - ignoreItem = false - return - } - val item = requireNotNull(adapter) { - "Feature source adapter is not set" - }.getItem(position) - listener?.onSelectSource(item) - } - - override fun onNothingSelected(parent: AdapterView<*>?) = Unit - } - - interface OnSelectSourceListener { - fun onSelectSource(option: Feature<*>) - } -} diff --git a/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelDeprecationSpec.kt b/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelDeprecationSpec.kt deleted file mode 100644 index 89fd3cdb8..000000000 --- a/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelDeprecationSpec.kt +++ /dev/null @@ -1,158 +0,0 @@ -package io.mehow.laboratory.inspector - -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.collections.shouldContainExactly -import io.mehow.laboratory.Feature -import io.mehow.laboratory.FeatureFactory -import io.mehow.laboratory.Laboratory -import io.mehow.laboratory.inspector.DeprecationAlignment.Bottom -import io.mehow.laboratory.inspector.DeprecationAlignment.Regular -import io.mehow.laboratory.inspector.DeprecationPhenotype.Hide -import io.mehow.laboratory.inspector.DeprecationPhenotype.Show -import io.mehow.laboratory.inspector.DeprecationPhenotype.Strikethrough -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.first -import kotlin.DeprecationLevel.ERROR -import kotlin.DeprecationLevel.HIDDEN -import kotlin.DeprecationLevel.WARNING - -internal class InspectorViewModelDeprecationSpec : DescribeSpec({ - setMainDispatcher() - - describe("deprecated feature flags") { - it("can be filtered out") { - val viewModel = InspectorViewModel(DeprecationHandler( - phenotypeSelector = { Hide }, - alignmentSelector = { Regular }, - )) - - val featureNames = viewModel.sectionFlow().first().map(FeatureUiModel::name) - - featureNames shouldContainExactly listOf("NotDeprecated") - } - - it("can be struck through") { - val viewModel = InspectorViewModel(DeprecationHandler( - phenotypeSelector = { Strikethrough }, - alignmentSelector = { Regular }, - )) - - val featureNames = viewModel.sectionFlow().first().map { it.name to it.deprecationPhenotype } - - featureNames shouldContainExactly listOf( - "DeprecatedError" to Strikethrough, - "DeprecatedHidden" to Strikethrough, - "DeprecatedWarning" to Strikethrough, - "NotDeprecated" to null, - ) - } - - it("can be shown") { - val viewModel = InspectorViewModel(DeprecationHandler( - phenotypeSelector = { Show }, - alignmentSelector = { Regular }, - )) - - val featureNames = viewModel.sectionFlow().first().map { it.name to it.deprecationPhenotype } - - featureNames shouldContainExactly listOf( - "DeprecatedError" to Show, - "DeprecatedHidden" to Show, - "DeprecatedWarning" to Show, - "NotDeprecated" to null, - ) - } - - it("can be moved to bottom") { - val viewModel = InspectorViewModel(DeprecationHandler( - phenotypeSelector = { Show }, - alignmentSelector = { Bottom }, - )) - - val featureNames = viewModel.sectionFlow().first().map { it.name to it.deprecationPhenotype } - - featureNames shouldContainExactly listOf( - "NotDeprecated" to null, - "DeprecatedError" to Show, - "DeprecatedHidden" to Show, - "DeprecatedWarning" to Show, - ) - } - - it("can be selected based on deprecation level") { - val viewModel = InspectorViewModel(DeprecationHandler( - phenotypeSelector = { if (it == WARNING) Strikethrough else Show }, - alignmentSelector = { if (it != WARNING) Bottom else Regular }, - )) - - val featureNames = viewModel.sectionFlow().first().map { it.name to it.deprecationPhenotype } - - featureNames shouldContainExactly listOf( - "DeprecatedWarning" to Strikethrough, - "NotDeprecated" to null, - "DeprecatedError" to Show, - "DeprecatedHidden" to Show, - ) - } - } -}) - -private object DeprecatedFeatureFactory : FeatureFactory { - override fun create(): Set>> { - @Suppress("UNCHECKED_CAST") - return setOf( - Class.forName("io.mehow.laboratory.inspector.DeprecatedWarning"), - Class.forName("io.mehow.laboratory.inspector.DeprecatedError"), - Class.forName("io.mehow.laboratory.inspector.DeprecatedHidden"), - Class.forName("io.mehow.laboratory.inspector.NotDeprecated"), - ) as Set>> - } -} - -@Deprecated("", level = WARNING) -private enum class DeprecatedWarning : Feature<@Suppress("DEPRECATION") DeprecatedWarning> { - Option, - ; - - @Suppress("DEPRECATION") - override val defaultOption: DeprecatedWarning - get() = Option -} - -@Deprecated("message", level = ERROR) -private enum class DeprecatedError : Feature<@Suppress("DEPRECATION_ERROR") DeprecatedError> { - Option, - ; - - @Suppress("DEPRECATION_ERROR") - override val defaultOption: DeprecatedError - get() = Option -} - -@Deprecated("", level = HIDDEN) -private enum class DeprecatedHidden : Feature<@Suppress("DEPRECATION_ERROR") DeprecatedHidden> { - Option, - ; - - @Suppress("DEPRECATION_ERROR") - override val defaultOption: DeprecatedHidden - get() = Option -} - -private enum class NotDeprecated : Feature { - Option, - ; - - override val defaultOption: NotDeprecated - get() = Option -} - -@Suppress("TestFunctionName") -private fun InspectorViewModel( - deprecationHandler: DeprecationHandler, -) = InspectorViewModel( - Laboratory.inMemory(), - emptyFlow(), - DeprecatedFeatureFactory, - deprecationHandler, -) diff --git a/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelFeatureSpec.kt b/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelFeatureSpec.kt deleted file mode 100644 index fc9c68691..000000000 --- a/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelFeatureSpec.kt +++ /dev/null @@ -1,321 +0,0 @@ -package io.mehow.laboratory.inspector - -import app.cash.turbine.test -import io.kotest.assertions.fail -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.collections.shouldContainExactly -import io.kotest.matchers.collections.shouldNotContain -import io.mehow.laboratory.DefaultOptionFactory -import io.mehow.laboratory.Feature -import io.mehow.laboratory.FeatureFactory -import io.mehow.laboratory.FeatureStorage -import io.mehow.laboratory.Laboratory -import io.mehow.laboratory.inspector.TextToken.Link -import io.mehow.laboratory.inspector.TextToken.Regular -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.first - -internal class InspectorViewModelFeatureSpec : DescribeSpec({ - setMainDispatcher() - - describe("view model") { - it("filters empty feature flag groups") { - val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) - - val featureNames = viewModel.sectionFlow().first().map(FeatureUiModel::name) - - featureNames shouldNotContain "Empty" - } - - it("orders feature flag groups by name") { - val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) - - val featureNames = viewModel.sectionFlow().first().map(FeatureUiModel::name) - - featureNames shouldContainExactly listOf("First", "Second") - } - - it("does not order feature flag options") { - val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) - - val features = viewModel.sectionFlow().first() - .map(FeatureUiModel::models) - .map { models -> models.map(OptionUiModel::option) } - - features[0] shouldContainExactly listOf(First.C, First.B, First.A) - features[1] shouldContainExactly listOf(Second.B, Second.C, Second.A) - } - - it("marks first feature flag option as selected by default") { - val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) - - viewModel.observeSelectedFeatures().first() shouldContainExactly listOf(First.C, Second.B) - } - - it("marks saved feature flag options as selected") { - val laboratory = Laboratory.inMemory().apply { - setOption(First.A) - setOption(Second.C) - } - - val viewModel = InspectorViewModel(laboratory, NoSourceFeatureFactory) - - viewModel.observeSelectedFeatures().first() shouldContainExactly listOf(First.A, Second.C) - } - - it("selects feature flag options") { - val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) - - viewModel.selectFeature(First.B) - viewModel.selectFeature(Second.A) - - viewModel.observeSelectedFeatures().first() shouldContainExactly listOf(First.B, Second.A) - } - - it("observes feature flag changes") { - val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) - - viewModel.observeSelectedFeatures().test { - awaitItem() shouldContainExactly listOf(First.C, Second.B) - - viewModel.selectFeature(First.B) - awaitItem() shouldContainExactly listOf(First.B, Second.B) - - viewModel.selectFeature(Second.C) - awaitItem() shouldContainExactly listOf(First.B, Second.C) - - cancel() - } - } - - it("observes source changes") { - val viewModel = InspectorViewModel(Laboratory.inMemory(), AllFeatureFactory) - - viewModel.observeSelectedFeaturesAndSources().test { - awaitItem() shouldContainExactly listOf( - First.C to null, - Second.B to null, - Sourced.A to Sourced.Source.Local - ) - - viewModel.selectFeature(Sourced.Source.Remote) - - awaitItem() shouldContainExactly listOf( - First.C to null, - Second.B to null, - Sourced.A to Sourced.Source.Remote - ) - } - } - - it("resets feature flags to default options declared in factory") { - val defaultOptionFactory = object : DefaultOptionFactory { - override fun > create(feature: T): Feature<*>? = when (feature) { - is First -> First.A - is Second -> Second.A - else -> null - } - } - val laboratory = Laboratory.builder() - .featureStorage(FeatureStorage.inMemory()) - .defaultOptionFactory(defaultOptionFactory) - .build() - val viewModel = InspectorViewModel(laboratory, NoSourceFeatureFactory) - - viewModel.observeSelectedFeatures().test { - awaitItem() shouldContainExactly listOf(First.A, Second.A) - - viewModel.selectFeature(First.B) - awaitItem() shouldContainExactly listOf(First.B, Second.A) - - viewModel.selectFeature(Second.B) - awaitItem() shouldContainExactly listOf(First.B, Second.B) - - laboratory.clear() - awaitItemEventually { it shouldContainExactly listOf(First.A, Second.A) } - - cancel() - } - } - - it("uses text tokens for feature flag description") { - val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) - - val descriptions = viewModel.sectionFlow().first().map(FeatureUiModel::description) - - descriptions shouldContainExactly listOf( - listOf( - Regular("Description with a "), - Link("link", "https://mehow.io"), - ), - listOf( - Regular("Description without a link"), - ), - ) - } - - it("observers feature flags supervision") { - val viewModel = InspectorViewModel(Laboratory.inMemory(), SupervisedFeatureFactory) - - viewModel.observeSelectedFeaturesAndEnabledState().test { - awaitItem() shouldContainExactly listOf( - Child.A to false, - Parent.Disabled to true, - ) - - viewModel.selectFeature(Parent.Enabled) - awaitItemEventually { - it shouldContainExactly listOf( - Child.A to true, - Parent.Enabled to true, - ) - } - - viewModel.selectFeature(Child.B) - awaitItem() shouldContainExactly listOf( - Child.B to true, - Parent.Enabled to true, - ) - - viewModel.selectFeature(Parent.Disabled) - awaitItemEventually { - it shouldContainExactly listOf( - Child.A to false, - Parent.Disabled to true, - ) - } - - cancel() - } - } - - it("includes supervised features to options") { - val viewModel = InspectorViewModel(Laboratory.inMemory(), SupervisedFeatureFactory) - - viewModel.supervisedFeaturesFlow().first() shouldContainExactly listOf( - Child.A to emptyList(), - Child.B to emptyList(), - Parent.Enabled to listOf(Child::class.java), - Parent.Disabled to emptyList(), - ) - } - - it("includes supervised features to options from different sections") { - val parentFactory = object : FeatureFactory { - override fun create(): Set>> = setOf(Parent::class.java) - } - val childFactory = object : FeatureFactory { - override fun create(): Set>> = setOf(Child::class.java) - } - - val viewModel = InspectorViewModel( - Laboratory.inMemory(), - searchQueries = emptyFlow(), - mapOf("Parent" to parentFactory, "Child" to childFactory), - DeprecationHandler({ fail("Unexpected call") }, { fail("Unexpected call") }), - Dispatchers.Unconfined, - ) - - viewModel.supervisedFeaturesFlow("Parent").first() shouldContainExactly listOf( - Parent.Enabled to listOf(Child::class.java), - Parent.Disabled to emptyList(), - ) - } - } -}) - -private object NoSourceFeatureFactory : FeatureFactory { - override fun create(): Set>> = setOf( - Second::class.java, - First::class.java, - Empty::class.java, - ) -} - -private object SourcedFeatureFactory : FeatureFactory { - override fun create(): Set>> = setOf(Sourced::class.java) -} - -private object AllFeatureFactory : FeatureFactory { - override fun create() = NoSourceFeatureFactory.create() + SourcedFeatureFactory.create() -} - -private object SupervisedFeatureFactory : FeatureFactory { - override fun create(): Set>> = setOf( - Parent::class.java, - Child::class.java, - ) -} - -private enum class First : Feature { - C, - B, - A, - ; - - override val defaultOption get() = C - - override val description = "Description with a [link](https://mehow.io)" -} - -private enum class Second : Feature { - B, - C, - A, - ; - - override val defaultOption get() = B - - override val description = "Description without a link" -} - -private enum class Empty : Feature - -private enum class Sourced : Feature { - A, - B, - C, - ; - - override val defaultOption get() = A - - override val source = Source::class.java - - enum class Source : Feature { - Local, - Remote, - ; - - override val defaultOption get() = Local - } -} - -private enum class Parent : Feature { - Enabled, - Disabled, - ; - - override val defaultOption get() = Disabled -} - -private enum class Child : Feature { - A, - B, - ; - - override val defaultOption get() = A - - override val supervisorOption get() = Parent.Enabled -} - -@Suppress("TestFunctionName") -private fun InspectorViewModel( - laboratory: Laboratory, - factory: FeatureFactory, -) = InspectorViewModel( - laboratory, - emptyFlow(), - factory, - DeprecationHandler({ fail("Unexpected call") }, { fail("Unexpected call") }), -) diff --git a/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelFilterSpec.kt b/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelFilterSpec.kt deleted file mode 100644 index fb6271853..000000000 --- a/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelFilterSpec.kt +++ /dev/null @@ -1,255 +0,0 @@ -package io.mehow.laboratory.inspector - -import app.cash.turbine.FlowTurbine -import app.cash.turbine.test -import io.kotest.assertions.fail -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.collections.shouldContainExactly -import io.kotest.property.Arb -import io.kotest.property.arbitrary.stringPattern -import io.kotest.property.checkAll -import io.mehow.laboratory.Feature -import io.mehow.laboratory.FeatureFactory -import io.mehow.laboratory.Laboratory -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlin.reflect.KClass - -internal class InspectorViewModelFilterSpec : DescribeSpec({ - setMainDispatcher() - - describe("feature flags filtering") { - it("emits all feature flags for no search terms") { - val searchFlow = MutableSharedFlow() - InspectorViewModel(searchFlow).observeFeatureClasses().test { - expectAllFeatureFlags() - - cancel() - } - } - - it("emits all feature flags for blank search terms") { - checkAll(Arb.stringPattern("([ ]{0,10})")) { query -> - val searchFlow = MutableSharedFlow() - InspectorViewModel(searchFlow).observeFeatureClasses().test { - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery(query)) - expectNoEvents() - - cancel() - } - } - } - - it("finds feature flags by their exact name") { - val searchFlow = MutableSharedFlow() - InspectorViewModel(searchFlow).observeFeatureClasses().test { - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery("RegularNameFeature")) - awaitItem() shouldContainExactly listOf(RegularNameFeature::class) - - searchFlow.emit(SearchQuery("Numbered1NameFeature")) - awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class) - - searchFlow.emit(SearchQuery("SourcedFeature")) - awaitItem() shouldContainExactly listOf(SourcedFeature::class) - - cancel() - } - } - - it("finds feature flags by split name parts") { - val searchFlow = MutableSharedFlow() - InspectorViewModel(searchFlow).observeFeatureClasses().test { - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery("Name Feature")) - awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class, - RegularNameFeature::class) - - searchFlow.emit(SearchQuery("Numbered 1Name Feature")) - awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class) - - cancel() - } - } - - // Find out why checkAll(Arb.stringPattern("[!_?@*]{10,}")) { query -> } can fail on CI. - // It fails randomly with a timeout on a second event. Re-using generator seed does not help locally. - // Pattern in generator also doesn't matter as long as it produces valid input for the test. - it("finds no feature flags for no matches") { - val searchFlow = MutableSharedFlow() - InspectorViewModel(searchFlow).observeFeatureClasses().test { - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery("???")) - awaitItem() shouldContainExactly emptyList() - - cancel() - } - } - - it("finds feature flags by their options") { - val searchFlow = MutableSharedFlow() - InspectorViewModel(searchFlow).observeFeatureClasses().test { - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery("Disabled")) - awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class, - RegularNameFeature::class) - - searchFlow.emit(SearchQuery("Howdy")) - awaitItem() shouldContainExactly listOf(SourcedFeature::class) - - cancel() - } - } - - it("finds feature flags by their sources") { - val searchFlow = MutableSharedFlow() - InspectorViewModel(searchFlow).observeFeatureClasses().test { - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery("Remote")) - awaitItem() shouldContainExactly listOf(SourcedFeature::class) - - cancel() - } - } - - it("finds feature flags by partial matches") { - val searchFlow = MutableSharedFlow() - InspectorViewModel(searchFlow).observeFeatureClasses().test { - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery("me ture")) - awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class, - RegularNameFeature::class) - - searchFlow.emit(SearchQuery("ature")) - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery("ed ture")) - awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class, SourcedFeature::class) - - searchFlow.emit(SearchQuery("cal")) - awaitItem() shouldContainExactly listOf(SourcedFeature::class) - - cancel() - } - } - - it("finds feature flags by ordered input") { - val searchFlow = MutableSharedFlow() - InspectorViewModel(searchFlow).observeFeatureClasses().test { - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery("Enabled Disabled")) - awaitItem() shouldContainExactly listOf(RegularNameFeature::class) - - searchFlow.emit(SearchQuery("Disabled Enabled")) - awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class) - - cancel() - } - } - - it("ignores capitalization during search") { - val searchFlow = MutableSharedFlow() - InspectorViewModel(searchFlow).observeFeatureClasses().test { - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery("enabled")) - awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class, - RegularNameFeature::class) - - searchFlow.emit(SearchQuery("feature")) - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery("local")) - awaitItem() shouldContainExactly listOf(SourcedFeature::class) - - cancel() - } - } - - it("finds feature flags by partial, non-split inner search") { - val searchFlow = MutableSharedFlow() - InspectorViewModel(searchFlow).observeFeatureClasses().test { - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery("arnamefea")) - awaitItem() shouldContainExactly listOf(RegularNameFeature::class) - - searchFlow.emit(SearchQuery("d1na")) - awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class) - - cancel() - } - } - } -}) - -private object SearchFeatureFactory : FeatureFactory { - override fun create(): Set>> = setOf( - RegularNameFeature::class.java, - Numbered1NameFeature::class.java, - SourcedFeature::class.java, - ) -} - -private enum class RegularNameFeature : Feature { - Enabled, - Disabled, - ; - - override val defaultOption get() = Disabled -} - -private enum class Numbered1NameFeature : Feature { - Disabled, - Enabled, - ; - - override val defaultOption get() = Disabled -} - -private enum class SourcedFeature : Feature { - Howdy, - There, - Partner, - ; - - override val defaultOption get() = Howdy - - @Suppress("UNCHECKED_CAST") - override val source = Source::class.java as Class> - - enum class Source : Feature { - Local, - Remote, - ; - - override val defaultOption get() = Local - } -} - -@Suppress("TestFunctionName") -private fun InspectorViewModel( - searchFlow: Flow, -) = InspectorViewModel( - Laboratory.inMemory(), - searchFlow, - SearchFeatureFactory, - DeprecationHandler({ fail("Unexpected call") }, { fail("Unexpected call") }), -) - -private suspend fun FlowTurbine>>>.expectAllFeatureFlags() { - awaitItem() shouldContainExactly listOf( - Numbered1NameFeature::class, - RegularNameFeature::class, - SourcedFeature::class, - ) -} diff --git a/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelNavigationSpec.kt b/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelNavigationSpec.kt deleted file mode 100644 index 4071f8e20..000000000 --- a/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelNavigationSpec.kt +++ /dev/null @@ -1,128 +0,0 @@ -package io.mehow.laboratory.inspector - -import app.cash.turbine.test -import io.kotest.assertions.fail -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.collections.shouldBeIn -import io.kotest.matchers.shouldBe -import io.mehow.laboratory.Feature -import io.mehow.laboratory.FeatureFactory -import io.mehow.laboratory.Laboratory -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flowOf - -@Suppress("UNCHECKED_CAST") -internal class InspectorViewModelNavigationSpec : DescribeSpec({ - setMainDispatcher() - - describe("feature flag coordinates") { - it("are found") { - val viewModel = InspectorViewModel() - - viewModel.goTo(SectionOneFeatureA::class.java as Class>) shouldBe FeatureCoordinates(0, 0) - viewModel.goTo(SectionOneFeatureB::class.java as Class>) shouldBe FeatureCoordinates(0, 1) - viewModel.goTo(SectionTwoFeature::class.java as Class>) shouldBe FeatureCoordinates(1, 0) - } - - it("are not found when feature is not registered") { - val viewModel = InspectorViewModel() - - viewModel.goTo(UnregisteredFeature::class.java as Class>) shouldBe null - } - - it("are not found when feature is filtered") { - val viewModel = InspectorViewModel(searchFlow = flowOf(SearchQuery("Foo"))) - - viewModel.goTo(SectionOneFeatureA::class.java as Class>) shouldBe null - } - - it("are found when feature is registerd twice") { - val viewModel = InspectorViewModel(mapOf("A1" to SectionAFactory, "A2" to SectionAFactory)) - - viewModel.goTo(SectionOneFeatureA::class.java as Class>) shouldBeIn listOf( - FeatureCoordinates(0, 0), - FeatureCoordinates(1, 0), - ) - } - - it("can be observed") { - val viewModel = InspectorViewModel() - - viewModel.featureCoordinatesFlow.test { - expectNoEvents() - - viewModel.goTo(SectionOneFeatureA::class.java as Class>) - awaitItem() shouldBe FeatureCoordinates(0, 0) - - cancel() - } - } - - it("do not cache emissions") { - val viewModel = InspectorViewModel() - - viewModel.goTo(SectionOneFeatureA::class.java as Class>) - - viewModel.featureCoordinatesFlow.test { - cancel() - } - } - } -}) - -private object SectionAFactory : FeatureFactory { - override fun create(): Set>> = setOf( - SectionOneFeatureA::class.java, - SectionOneFeatureB::class.java, - ) -} - -private object SectionBFactory : FeatureFactory { - override fun create(): Set>> = setOf(SectionTwoFeature::class.java) -} - -private enum class SectionOneFeatureA : Feature { - Option, - ; - - override val defaultOption: SectionOneFeatureA - get() = Option -} - -private enum class SectionOneFeatureB : Feature { - Option, - ; - - override val defaultOption: SectionOneFeatureB - get() = Option -} - -private enum class SectionTwoFeature : Feature { - Option, - ; - - override val defaultOption: SectionTwoFeature - get() = Option -} - -private enum class UnregisteredFeature : Feature { - Option, - ; - - override val defaultOption: UnregisteredFeature - get() = Option -} - -@Suppress("TestFunctionName") -private fun InspectorViewModel( - featureFactories: Map = mapOf("A" to SectionAFactory, "B" to SectionBFactory), - searchFlow: Flow = emptyFlow(), -) = InspectorViewModel( - Laboratory.inMemory(), - searchFlow, - featureFactories, - DeprecationHandler({ fail("Unexpected call") }, { fail("Unexpected call") }), - Dispatchers.Unconfined, -) diff --git a/library/inspector/src/test/java/io/mehow/laboratory/inspector/SearchViewModelSpec.kt b/library/inspector/src/test/java/io/mehow/laboratory/inspector/SearchViewModelSpec.kt deleted file mode 100644 index f8f0b7d42..000000000 --- a/library/inspector/src/test/java/io/mehow/laboratory/inspector/SearchViewModelSpec.kt +++ /dev/null @@ -1,90 +0,0 @@ -package io.mehow.laboratory.inspector - -import app.cash.turbine.FlowTurbine -import app.cash.turbine.test -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.shouldBe -import io.mehow.laboratory.inspector.SearchMode.Active -import io.mehow.laboratory.inspector.SearchMode.Idle -import io.mehow.laboratory.inspector.SearchViewModel.Event.CloseSearch -import io.mehow.laboratory.inspector.SearchViewModel.Event.OpenSearch -import io.mehow.laboratory.inspector.SearchViewModel.Event.UpdateQuery -import io.mehow.laboratory.inspector.SearchViewModel.UiModel - -internal class SearchViewModelSpec : DescribeSpec({ - setMainDispatcher() - - describe("feature flag searching") { - it("has initial idle state") { - SearchViewModel().uiModels.test { - expectIdleModel() - - cancel() - } - } - - it("can be opened") { - val viewModel = SearchViewModel() - viewModel.uiModels.test { - expectIdleModel() - - viewModel.sendEvent(OpenSearch) - awaitItem() shouldBe UiModel(Active, SearchQuery.Empty) - - cancel() - } - } - - it("updates search queries in active mode") { - val viewModel = SearchViewModel() - viewModel.uiModels.test { - expectIdleModel() - - viewModel.sendEvent(OpenSearch) - awaitItem() - - viewModel.sendEvent(UpdateQuery("Hello")) - awaitItem() shouldBe UiModel(Active, SearchQuery("Hello")) - - viewModel.sendEvent(UpdateQuery("World")) - awaitItem() shouldBe UiModel(Active, SearchQuery("World")) - - cancel() - } - } - - it("ignores queries in idle mode") { - val viewModel = SearchViewModel() - viewModel.uiModels.test { - expectIdleModel() - - viewModel.sendEvent(UpdateQuery("Hello")) - expectNoEvents() - - cancel() - } - } - - it("clears queries when search is closed") { - val viewModel = SearchViewModel() - viewModel.uiModels.test { - expectIdleModel() - - viewModel.sendEvent(OpenSearch) - awaitItem() - - viewModel.sendEvent(UpdateQuery("Hello")) - awaitItem() shouldBe UiModel(Active, SearchQuery("Hello")) - - viewModel.sendEvent(CloseSearch) - expectIdleModel() - - cancel() - } - } - } -}) - -private suspend fun FlowTurbine.expectIdleModel() { - awaitItem() shouldBe UiModel(Idle, SearchQuery.Empty) -} diff --git a/library/inspector/src/test/java/io/mehow/laboratory/inspector/TextTokenSpec.kt b/library/inspector/src/test/java/io/mehow/laboratory/inspector/TextTokenSpec.kt deleted file mode 100644 index 4f0771710..000000000 --- a/library/inspector/src/test/java/io/mehow/laboratory/inspector/TextTokenSpec.kt +++ /dev/null @@ -1,77 +0,0 @@ -package io.mehow.laboratory.inspector - -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.data.blocking.forAll -import io.kotest.data.row -import io.kotest.matchers.collections.shouldBeEmpty -import io.kotest.matchers.collections.shouldContainExactly -import io.mehow.laboratory.inspector.TextToken.Link -import io.mehow.laboratory.inspector.TextToken.Regular - -internal class TextTokenSpec : DescribeSpec({ - describe("text tokens") { - it("can be empty") { - "".tokenize().shouldBeEmpty() - } - - it("can be blank") { - " ".tokenize() shouldContainExactly listOf(Regular(" ")) - } - - it("can have regular text") { - "Hello".tokenize() shouldContainExactly listOf(Regular("Hello")) - } - - it("can have link") { - "[Hello](https://mehow.io)".tokenize() shouldContainExactly listOf(Link("Hello", "https://mehow.io")) - } - - it("can start with regular text followed by a link") { - "Hello [there](https://github.com/MiSikora/)".tokenize() shouldContainExactly listOf( - Regular("Hello "), - Link("there", "https://github.com/MiSikora/"), - ) - } - - it("can start with a link followed by a regular text") { - "[General](https://google.com) Kenobi".tokenize() shouldContainExactly listOf( - Link("General", "https://google.com"), - Regular(" Kenobi"), - ) - } - - it("can have multiple regular texts and links") { - val input = "Hello [there](https://github.com)… [General](https://sample.org) Kenobi" - input.tokenize() shouldContainExactly listOf( - Regular("Hello "), - Link("there", "https://github.com"), - Regular("… "), - Link("General", "https://sample.org"), - Regular(" Kenobi"), - ) - } - - it("can have multiple consecutive links") { - val input = "[One,](https://one.com)[ Two](https://two.com)[, Three](https://three.com)" - input.tokenize() shouldContainExactly listOf( - Link("One,", "https://one.com"), - Link(" Two", "https://two.com"), - Link(", Three", "https://three.com"), - ) - } - - it("does not use malformed link syntax") { - forAll( - row("[One[](https://one.com)"), - row("[One](https://one.com()"), - row("[](https://one.com"), - row("[One]()"), - row("[One]((https://one.com)"), - row("[O]ne](https://one.com)"), - row("[One](h(ttps://one.com)"), - ) { - it.tokenize() shouldContainExactly listOf(Regular(it)) - } - } - } -}) \ No newline at end of file diff --git a/library/laboratory/build.gradle b/library/laboratory/build.gradle deleted file mode 100644 index 62d65163d..000000000 --- a/library/laboratory/build.gradle +++ /dev/null @@ -1,16 +0,0 @@ -plugins { - id "org.jetbrains.kotlin.jvm" -} - -test.useJUnitPlatform() - -dependencies { - api libs.kotlin.x.coroutinesCore - - testImplementation libs.kotest.runnerJunit5 - testImplementation libs.kotest.assertions - testImplementation libs.turbine -} - -apply from: "$rootDir/gradle/dokka-config.gradle" -apply from: "$rootDir/gradle/gradle-mvn-push.gradle" diff --git a/library/laboratory/src/test/java/io/mehow/laboratory/DefaultOptionFactorySpec.kt b/library/laboratory/src/test/java/io/mehow/laboratory/DefaultOptionFactorySpec.kt deleted file mode 100644 index 109518350..000000000 --- a/library/laboratory/src/test/java/io/mehow/laboratory/DefaultOptionFactorySpec.kt +++ /dev/null @@ -1,39 +0,0 @@ -package io.mehow.laboratory - -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.shouldBe - -internal class DefaultOptionFactorySpec : DescribeSpec({ - val firstFactory = object : DefaultOptionFactory { - override fun > create(feature: T): Feature<*>? = when (feature) { - is FirstFeature -> FirstFeature.B - else -> null - } - } - - val secondFactory = object : DefaultOptionFactory { - override fun > create(feature: T): Feature<*>? = when (feature) { - is FirstFeature -> FirstFeature.C - is SecondFeature -> SecondFeature.C - else -> null - } - } - - describe("default option factory") { - context("when added to another") { - val factory = (firstFactory + secondFactory) - - it("uses self as a producer if available in self") { - factory.create(FirstFeature.A) shouldBe FirstFeature.B - } - - it("uses another factory as a producer when unavailable in self") { - factory.create(SecondFeature.A) shouldBe SecondFeature.C - } - - it("uses no producer feature flag is unknown to any of factories") { - factory.create(UnsourcedFeature.A) shouldBe null - } - } - } -}) diff --git a/library/laboratory/src/test/java/io/mehow/laboratory/FeatureFactorySpec.kt b/library/laboratory/src/test/java/io/mehow/laboratory/FeatureFactorySpec.kt deleted file mode 100644 index 50a73ab78..000000000 --- a/library/laboratory/src/test/java/io/mehow/laboratory/FeatureFactorySpec.kt +++ /dev/null @@ -1,24 +0,0 @@ -package io.mehow.laboratory - -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder - -internal class FeatureFactorySpec : DescribeSpec({ - val firstFactory = object : FeatureFactory { - override fun create(): Set>> = setOf(FirstFeature::class.java) - } - - val secondFactory = object : FeatureFactory { - override fun create(): Set>> = setOf(OtherFeature::class.java) - } - - describe("feature factory") { - it("when added to another factory returns sum of available features") { - (firstFactory + secondFactory).create() shouldContainExactlyInAnyOrder setOf( - FirstFeature::class.java, - OtherFeature::class.java, - ) - } - } -}) - diff --git a/library/laboratory/src/test/java/io/mehow/laboratory/Features.kt b/library/laboratory/src/test/java/io/mehow/laboratory/Features.kt deleted file mode 100644 index 37e2f21ef..000000000 --- a/library/laboratory/src/test/java/io/mehow/laboratory/Features.kt +++ /dev/null @@ -1,120 +0,0 @@ -package io.mehow.laboratory - -internal enum class FirstFeature : Feature { - A, - B, - C, - ; - - override val defaultOption get() = A - - override val source = Source::class.java - - enum class Source : Feature { - Local, - RemoteA, - ; - - override val defaultOption get() = Local - } -} - -internal enum class SecondFeature : Feature { - A, - B, - C, - ; - - override val defaultOption get() = A - - override val source = Source::class.java - - enum class Source : Feature { - Local, - RemoteA, - RemoteB, - ; - - override val defaultOption get() = RemoteB - } -} - -internal enum class EmptySourceFeature : Feature { - A, - B, - C, - ; - - override val defaultOption get() = A - - override val source = Source::class.java - - internal enum class Source : Feature -} - -internal enum class UnsourcedFeature : Feature { - A, - B, - C, - ; - - override val defaultOption get() = A -} - -internal enum class SomeFeature : Feature { - A, - B, - C, - ; - - override val defaultOption get() = B -} - -internal enum class OtherFeature : Feature { - A, - B, - C, - ; - - override val defaultOption get() = A -} - -internal enum class NoValuesFeature : Feature - -internal enum class GrandParentFeature : Feature { - A, - B, - ; - - override val defaultOption get() = A -} - -internal enum class ParentFeature : Feature { - A, - B, - ; - - override val defaultOption get() = A - - override val supervisorOption = GrandParentFeature.A -} - -internal enum class FirstChildFeature : Feature { - A, - B, - ; - - override val defaultOption get() = A - - override val supervisorOption = ParentFeature.A -} - -internal enum class SecondChildFeature : Feature { - A, - B, - ; - - override val defaultOption get() = A - - override val supervisorOption = ParentFeature.B -} diff --git a/library/laboratory/src/test/java/io/mehow/laboratory/LaboratorySpec.kt b/library/laboratory/src/test/java/io/mehow/laboratory/LaboratorySpec.kt deleted file mode 100644 index 517b01461..000000000 --- a/library/laboratory/src/test/java/io/mehow/laboratory/LaboratorySpec.kt +++ /dev/null @@ -1,195 +0,0 @@ -package io.mehow.laboratory - -import app.cash.turbine.test -import io.kotest.assertions.fail -import io.kotest.assertions.throwables.shouldThrowExactly -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.booleans.shouldBeTrue -import io.kotest.matchers.shouldBe -import io.kotest.matchers.throwable.shouldHaveMessage -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf - -internal class LaboratorySpec : DescribeSpec({ - val throwingStorage = object : FeatureStorage { - override fun observeFeatureName(feature: Class>) = fail("Unexpected call") - override suspend fun getFeatureName(feature: Class>) = fail("Unexpected call") - override suspend fun setOptions(vararg options: Feature<*>) = fail("Unexpected call") - override suspend fun clear() = fail("Unexpected call") - } - - val nullStorage = object : FeatureStorage { - override fun observeFeatureName(feature: Class>): Flow = flowOf(null) - override suspend fun getFeatureName(feature: Class>): String? = null - override suspend fun setOptions(vararg options: Feature<*>) = fail("Unexpected call") - override suspend fun clear() = fail("Unexpected call") - } - - val emptyStorage = object : FeatureStorage { - override fun observeFeatureName(feature: Class>) = flowOf("") - override suspend fun getFeatureName(feature: Class>) = "" - override suspend fun setOptions(vararg options: Feature<*>) = fail("Unexpected call") - override suspend fun clear() = fail("Unexpected call") - } - - describe("laboratory") { - it("cannot use features with no values") { - val laboratory = Laboratory.create(throwingStorage) - - shouldThrowExactly { - laboratory.experiment() - } shouldHaveMessage "io.mehow.laboratory.NoValuesFeature must have at least one option" - } - - context("for feature with single default") { - it("uses declared default value") { - val laboratory = Laboratory.create(nullStorage) - - laboratory.experiment() shouldBe SomeFeature.B - } - - it("uses declared default value if no match is found") { - val laboratory = Laboratory.create(emptyStorage) - - laboratory.experiment() shouldBe SomeFeature.B - } - } - - context("checking feature value") { - it("returns false for non-default value") { - val laboratory = Laboratory.create(nullStorage) - - laboratory.experimentIs(SomeFeature.A) shouldBe false - } - - it("returns true for default value") { - val laboratory = Laboratory.create(nullStorage) - - laboratory.experimentIs(SomeFeature.B) shouldBe true - } - } - - context("reading and writing feature flag") { - val feature = SomeFeature::class.java - - it("uses value saved in a storage") { - val storage = FeatureStorage.inMemory() - val laboratory = Laboratory.create(storage) - - for (value in feature.options) { - storage.setOption(value) - - laboratory.experiment(feature) shouldBe value - } - } - - it("can directly change the feature") { - val laboratory = Laboratory.inMemory() - - for (value in feature.options) { - laboratory.setOption(value) - - laboratory.experiment(feature) shouldBe value - } - } - } - - it("observes feature changes") { - val laboratory = Laboratory.inMemory() - - laboratory.observe().test { - awaitItem() shouldBe SomeFeature.B - - laboratory.setOption(SomeFeature.A) - awaitItem() shouldBe SomeFeature.A - - laboratory.setOption(SomeFeature.C) - awaitItem() shouldBe SomeFeature.C - - laboratory.setOption(SomeFeature.C) - expectNoEvents() - - laboratory.setOption(SomeFeature.B) - awaitItem() shouldBe SomeFeature.B - - cancel() - } - } - - it("clears all feature flags") { - val laboratory = Laboratory.inMemory() - - laboratory.setOption(SomeFeature.A) - laboratory.setOption(OtherFeature.B) - laboratory.clear() - - laboratory.experimentIs(SomeFeature.B) - laboratory.experimentIs(OtherFeature.A) - } - } - - describe("in memory laboratory") { - it("is not shared across instances") { - val firstLaboratory = Laboratory.inMemory() - val secondLaboratory = Laboratory.inMemory() - - firstLaboratory.setOption(SomeFeature.A) - firstLaboratory.experiment() shouldBe SomeFeature.A - secondLaboratory.experiment() shouldBe SomeFeature.B - - secondLaboratory.setOption(SomeFeature.C) - firstLaboratory.experiment() shouldBe SomeFeature.A - secondLaboratory.experiment() shouldBe SomeFeature.C - } - } - - describe("default options factory") { - val factory = object : DefaultOptionFactory { - override fun > create(feature: T) = when (feature) { - is SomeFeature -> OtherFeature.C // Intentional wrong class for test - is OtherFeature -> OtherFeature.C - else -> null - } - } - - it("changes default option") { - val laboratory = Laboratory.builder() - .featureStorage(FeatureStorage.inMemory()) - .defaultOptionFactory(factory) - .build() - laboratory.experimentIs(OtherFeature.C).shouldBeTrue() - } - - it("does not affect stored option") { - val laboratory = Laboratory.builder() - .featureStorage(FeatureStorage.inMemory()) - .defaultOptionFactory(factory) - .build() - laboratory.setOption(OtherFeature.B) - laboratory.experimentIs(OtherFeature.B).shouldBeTrue() - } - - it("changes default option when feature flag is observed") { - val laboratory = Laboratory.builder() - .featureStorage(FeatureStorage.inMemory()) - .defaultOptionFactory(factory) - .build() - laboratory.observe().test { - awaitItem() shouldBe OtherFeature.C - - laboratory.setOption(OtherFeature.B) - awaitItem() shouldBe OtherFeature.B - } - } - - it("fails when provided default option has wrong type") { - val laboratory = Laboratory.builder() - .featureStorage(FeatureStorage.inMemory()) - .defaultOptionFactory(factory) - .build() - shouldThrowExactly { - laboratory.experiment() - } shouldHaveMessage "Tried to use OtherFeature.C as a default option for io.mehow.laboratory.SomeFeature" - } - } -}) diff --git a/library/laboratory/src/test/java/io/mehow/laboratory/OptionFactorySpec.kt b/library/laboratory/src/test/java/io/mehow/laboratory/OptionFactorySpec.kt deleted file mode 100644 index f25d57bb3..000000000 --- a/library/laboratory/src/test/java/io/mehow/laboratory/OptionFactorySpec.kt +++ /dev/null @@ -1,39 +0,0 @@ -package io.mehow.laboratory - -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.shouldBe - -internal class OptionFactorySpec : DescribeSpec({ - val firstFactory = object : OptionFactory { - override fun create(key: String, name: String): Feature<*>? = when (key) { - "FirstFeature" -> FirstFeature.A - else -> null - } - } - - val secondFactory = object : OptionFactory { - override fun create(key: String, name: String): Feature<*>? = when (key) { - "FirstFeature" -> FirstFeature.B - "SecondFeature" -> SecondFeature.B - else -> null - } - } - - describe("option factory") { - context("when added to another") { - val factory = (firstFactory + secondFactory) - - it("uses self as a producer when feature flag is known to self") { - factory.create("FirstFeature", "whatever") shouldBe FirstFeature.A - } - - it("uses another factory as a producer when feature flag is known to other") { - factory.create("SecondFeature", "whatever") shouldBe SecondFeature.B - } - - it("uses no producer when feature flag is unknown to any of factories") { - factory.create("UnsourcedFeature", "whatever") shouldBe null - } - } - } -}) diff --git a/library/laboratory/src/test/java/io/mehow/laboratory/ParentChildFeatureSpec.kt b/library/laboratory/src/test/java/io/mehow/laboratory/ParentChildFeatureSpec.kt deleted file mode 100644 index 719d467f8..000000000 --- a/library/laboratory/src/test/java/io/mehow/laboratory/ParentChildFeatureSpec.kt +++ /dev/null @@ -1,100 +0,0 @@ -package io.mehow.laboratory - -import app.cash.turbine.test -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.shouldBe - -internal class ParentChildFeatureSpec : DescribeSpec({ - describe("parent feature") { - it("supervises experiments on child") { - val laboratory = Laboratory.inMemory() - - laboratory.setOption(FirstChildFeature.B) - laboratory.setOption(ParentFeature.B) - - laboratory.experiment() shouldBe FirstChildFeature.A - } - - it("does not change saved child option") { - val laboratory = Laboratory.inMemory() - - laboratory.setOption(FirstChildFeature.B) - laboratory.setOption(ParentFeature.B) - laboratory.setOption(ParentFeature.A) - - laboratory.experiment() shouldBe FirstChildFeature.B - } - - it("does not disable changing child option") { - val laboratory = Laboratory.inMemory() - - laboratory.setOption(ParentFeature.B) - laboratory.setOption(FirstChildFeature.B) - laboratory.setOption(ParentFeature.A) - - laboratory.experiment() shouldBe FirstChildFeature.B - } - - it("supervises observation of child") { - val laboratory = Laboratory.inMemory() - - laboratory.setOption(FirstChildFeature.B) - - laboratory.observe().test { - awaitItem() shouldBe FirstChildFeature.B - - laboratory.setOption(ParentFeature.B) - awaitItem() shouldBe FirstChildFeature.A - - laboratory.setOption(ParentFeature.A) - awaitItem() shouldBe FirstChildFeature.B - - cancel() - } - } - - it("prevents child from emitting same value twice") { - val laboratory = Laboratory.inMemory() - - laboratory.observe().test { - awaitItem() shouldBe FirstChildFeature.A - - laboratory.setOption(ParentFeature.B) - expectNoEvents() - - cancel() - } - } - } - - describe("grandparent feature") { - it("supervises experiments on grandchild") { - val laboratory = Laboratory.inMemory() - - laboratory.setOption(SecondChildFeature.B) - laboratory.setOption(ParentFeature.B) - laboratory.setOption(GrandParentFeature.B) - - laboratory.experiment() shouldBe SecondChildFeature.A - } - - it("supervises observation of grandchild") { - val laboratory = Laboratory.inMemory() - - laboratory.setOption(SecondChildFeature.B) - laboratory.setOption(ParentFeature.B) - - laboratory.observe().test { - awaitItem() shouldBe SecondChildFeature.B - - laboratory.setOption(GrandParentFeature.B) - awaitItem() shouldBe SecondChildFeature.A - - laboratory.setOption(GrandParentFeature.A) - awaitItem() shouldBe SecondChildFeature.B - - cancel() - } - } - } -}) diff --git a/library/laboratory/src/test/java/io/mehow/laboratory/SourcedFeatureStorageSpec.kt b/library/laboratory/src/test/java/io/mehow/laboratory/SourcedFeatureStorageSpec.kt deleted file mode 100644 index d7fcd977f..000000000 --- a/library/laboratory/src/test/java/io/mehow/laboratory/SourcedFeatureStorageSpec.kt +++ /dev/null @@ -1,221 +0,0 @@ -package io.mehow.laboratory - -import app.cash.turbine.test -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.shouldBe - -internal class SourcedFeatureStorageSpec : DescribeSpec({ - lateinit var localLaboratory: Laboratory - lateinit var remoteLaboratoryA: Laboratory - lateinit var remoteLaboratoryB: Laboratory - lateinit var sourcedLaboratory: Laboratory - lateinit var sourcedStorage: FeatureStorage - - beforeTest { - val localStorage = FeatureStorage.inMemory() - val remoteStorageA = FeatureStorage.inMemory() - val remoteStorageB = FeatureStorage.inMemory() - sourcedStorage = SourcedFeatureStorage( - localStorage, - mapOf( - "RemoteA" to remoteStorageA, - "RemoteB" to remoteStorageB, - ), - ) - localLaboratory = Laboratory.create(localStorage) - remoteLaboratoryA = Laboratory.create(remoteStorageA) - remoteLaboratoryB = Laboratory.create(remoteStorageB) - sourcedLaboratory = Laboratory.create(sourcedStorage) - } - - describe("sourced feature storage") { - context("with no sources set") { - it("takes initial value from a default source") { - localLaboratory.setOption(FirstFeature.B) - sourcedLaboratory.experiment() shouldBe FirstFeature.B - - remoteLaboratoryB.setOption(SecondFeature.C) - sourcedLaboratory.experiment() shouldBe SecondFeature.C - } - - it("observes changes of a default feature source") { - sourcedLaboratory.observe().test { - awaitItem() shouldBe SecondFeature.A - - remoteLaboratoryB.setOption(SecondFeature.B) - awaitItem() shouldBe SecondFeature.B - - remoteLaboratoryB.setOption(SecondFeature.C) - awaitItem() shouldBe SecondFeature.C - - cancel() - } - } - } - - it("allows to change a feature source") { - sourcedLaboratory.setOption(FirstFeature.Source.RemoteA) - - remoteLaboratoryA.setOption(FirstFeature.C) - sourcedLaboratory.experiment() shouldBe FirstFeature.C - } - - it("observes changes of a feature source") { - localLaboratory.setOption(SecondFeature.A) - remoteLaboratoryA.setOption(SecondFeature.B) - remoteLaboratoryB.setOption(SecondFeature.C) - - sourcedLaboratory.observe().test { - awaitItem() shouldBe SecondFeature.C - - sourcedLaboratory.setOption(SecondFeature.Source.Local) - awaitItem() shouldBe SecondFeature.A - - localLaboratory.setOption(SecondFeature.C) - awaitItem() shouldBe SecondFeature.C - - sourcedLaboratory.setOption(SecondFeature.Source.RemoteA) - awaitItem() shouldBe SecondFeature.B - - remoteLaboratoryA.setOption(SecondFeature.A) - awaitItem() shouldBe SecondFeature.A - - sourcedLaboratory.setOption(SecondFeature.Source.RemoteB) - awaitItem() shouldBe SecondFeature.C - - remoteLaboratoryB.setOption(SecondFeature.B) - awaitItem() shouldBe SecondFeature.B - - cancel() - } - } - - it("does not observe changes of different feature source") { - localLaboratory.setOption(SecondFeature.B) - remoteLaboratoryA.setOption(SecondFeature.C) - - sourcedLaboratory.observe().test { - awaitItem() shouldBe SecondFeature.A - - sourcedLaboratory.setOption(FirstFeature.Source.Local) - expectNoEvents() - - sourcedLaboratory.setOption(FirstFeature.Source.RemoteA) - expectNoEvents() - - cancel() - } - } - - context("for empty source") { - it("falls back to observing a local storage") { - sourcedLaboratory.observe().test { - awaitItem() shouldBe EmptySourceFeature.A - - localLaboratory.setOption(EmptySourceFeature.B) - awaitItem() shouldBe EmptySourceFeature.B - - cancel() - } - } - - it("falls backs to experimenting with a local storage") { - sourcedLaboratory.experiment() shouldBe EmptySourceFeature.A - - localLaboratory.setOption(EmptySourceFeature.C) - sourcedLaboratory.experiment() shouldBe EmptySourceFeature.C - } - } - - context("for unknown source") { - it("falls back to observing a local storage") { - val localStorage = FeatureStorage.inMemory() - val sourcedStorage = SourcedFeatureStorage(localStorage, emptyMap()) - localLaboratory = Laboratory.create(localStorage) - sourcedLaboratory = Laboratory.create(sourcedStorage) - - sourcedLaboratory.observe().test { - awaitItem() shouldBe FirstFeature.A - - localLaboratory.setOption(FirstFeature.B) - awaitItem() shouldBe FirstFeature.B - - cancel() - } - } - - it("falls backs to experimenting with a local storage") { - val localStorage = FeatureStorage.inMemory() - val sourcedStorage = SourcedFeatureStorage(localStorage, emptyMap()) - localLaboratory = Laboratory.create(localStorage) - sourcedLaboratory = Laboratory.create(sourcedStorage) - - sourcedLaboratory.experiment() shouldBe FirstFeature.A - - localLaboratory.setOption(FirstFeature.C) - sourcedLaboratory.experiment() shouldBe FirstFeature.C - } - } - - context("for unsourced feature") { - it("falls back to observing a local storage") { - sourcedLaboratory.observe().test { - awaitItem() shouldBe UnsourcedFeature.A - - localLaboratory.setOption(UnsourcedFeature.B) - awaitItem() shouldBe UnsourcedFeature.B - - cancel() - } - } - - it("falls backs to experimenting with a local storage") { - sourcedLaboratory.experiment() shouldBe UnsourcedFeature.A - - localLaboratory.setOption(UnsourcedFeature.C) - sourcedLaboratory.experiment() shouldBe UnsourcedFeature.C - } - } - - it("controls local features") { - sourcedLaboratory.observe().test { - awaitItem() shouldBe FirstFeature.A - - sourcedLaboratory.setOption(FirstFeature.B) - awaitItem() shouldBe FirstFeature.B - - sourcedLaboratory.setOption(FirstFeature.C) - awaitItem() shouldBe FirstFeature.C - } - } - - it("clears only local source") { - localLaboratory.setOption(FirstFeature.B) - remoteLaboratoryA.setOption(FirstFeature.B) - sourcedLaboratory.setOption(FirstFeature.Source.RemoteA) - - sourcedLaboratory.clear() - - localLaboratory.experimentIs(FirstFeature.A) - remoteLaboratoryA.experimentIs(FirstFeature.B) - sourcedLaboratory.experimentIs(FirstFeature.A) - } - - it("uses default options override for sources") { - val defaultOptionFactory = object : DefaultOptionFactory { - override fun > create(feature: T) = when (feature) { - is FirstFeature.Source -> FirstFeature.Source.RemoteA - else -> null - } - } - val laboratory = Laboratory.Builder() - .featureStorage(sourcedStorage) - .defaultOptionFactory(defaultOptionFactory) - .build() - - remoteLaboratoryA.setOption(FirstFeature.C) - - laboratory.experiment() shouldBe FirstFeature.C - } - } -}) diff --git a/library/settings.gradle.kts b/library/settings.gradle.kts deleted file mode 100644 index 503c1416f..000000000 --- a/library/settings.gradle.kts +++ /dev/null @@ -1,41 +0,0 @@ -import com.android.build.api.dsl.SettingsExtension - -pluginManagement { - repositories { - mavenCentral() - google() - } -} - -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - mavenCentral() - google() - gradlePluginPortal() - } - - versionCatalogs { - create("libs") { - from(files("gradle/dependencies.toml")) - } - } -} - -plugins { - id("com.android.settings") version "8.0.2" -} - -@Suppress("UnstableApiUsage") -extensions.getByType(SettingsExtension::class).apply { - compileSdk = 33 - minSdk = 21 -} - -include(":laboratory") -include(":shared-preferences") -include(":inspector") -include(":hyperion-plugin") -include(":generator") -include(":gradle-plugin") -include(":data-store") diff --git a/library/shared-preferences/build.gradle b/library/shared-preferences/build.gradle deleted file mode 100644 index 51fa70ed4..000000000 --- a/library/shared-preferences/build.gradle +++ /dev/null @@ -1,42 +0,0 @@ -plugins { - id "com.android.library" - id "org.jetbrains.kotlin.android" -} - -android { - namespace "io.mehow.laboratory.sharedpreferences" - - defaultConfig { - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - testInstrumentationRunnerArgument "clearPackageData", "true" - } - - testOptions.execution "ANDROIDX_TEST_ORCHESTRATOR" - testBuildType "release" - - buildTypes { - release { - // Since we test release build it has to be signed. - signingConfig signingConfigs.getByName("debug") - } - } - - packagingOptions { - exclude "META-INF/AL2.0" - exclude "META-INF/LGPL2.1" - } -} - -dependencies { - api project(":laboratory") - - androidTestUtil libs.android.x.test.orchestrator - androidTestImplementation libs.android.x.testExtJunitKtx - androidTestImplementation libs.android.x.test.coreKtx - androidTestImplementation libs.android.x.test.runner - androidTestImplementation libs.kotest.assertions - androidTestImplementation libs.turbine -} - -apply from: "$rootDir/gradle/dokka-config.gradle" -apply from: "$rootDir/gradle/gradle-mvn-push.gradle" diff --git a/library/shared-preferences/gradle.properties b/library/shared-preferences/gradle.properties deleted file mode 100644 index ec4821ca1..000000000 --- a/library/shared-preferences/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -POM_ARTIFACT_ID=laboratory-shared-preferences -POM_NAME=Laboratory (SharedPreferences) -POM_PACKAGING=aar diff --git a/library/lint.xml b/lint.xml similarity index 58% rename from library/lint.xml rename to lint.xml index 04d08705e..998387fbd 100644 --- a/library/lint.xml +++ b/lint.xml @@ -4,12 +4,7 @@ - - - - - + - diff --git a/library/mkdocs.yml b/mkdocs.yml similarity index 75% rename from library/mkdocs.yml rename to mkdocs.yml index 47a7f178c..501bc5f05 100644 --- a/library/mkdocs.yml +++ b/mkdocs.yml @@ -33,8 +33,6 @@ markdown_extensions: plugins: - search - - minify: - minify_html: true - mkdocs-video nav: @@ -42,11 +40,6 @@ nav: - 'User guide': user-guide.md - 'QA module': qa-module.md - 'Gradle plugin': gradle-plugin.md - - 'API': - - 'laboratory': api/laboratory/index.html - - 'gradle-plugin': api/gradle-plugin/index.html - - 'data-store': api/data-store/index.html - - 'inspector': api/inspector/index.html - - 'shared-preferences': api/shared-preferences/index.html + - 'API': api/index.html - 'Changelog': changelog.md - 'Releasing': releasing.md diff --git a/library/prepare-release.sh b/prepare-release.sh similarity index 99% rename from library/prepare-release.sh rename to prepare-release.sh index 6a2d2e9e1..cd02fd65f 100644 --- a/library/prepare-release.sh +++ b/prepare-release.sh @@ -69,7 +69,7 @@ indexFile="./docs/index.md" sed -i "" "s/$currentVersion/$newVersion/g" $indexFile # Replace current version in README.md -readmeFile="../README.md" +readmeFile="./README.md" sed -i "" "s/$currentVersion/$newVersion/g" $readmeFile git reset &> /dev/null diff --git a/samples/.editorconfig b/samples/.editorconfig new file mode 120000 index 000000000..38d9a0ce1 --- /dev/null +++ b/samples/.editorconfig @@ -0,0 +1 @@ +../.editorconfig \ No newline at end of file diff --git a/samples/basic/build.gradle b/samples/basic/build.gradle deleted file mode 100644 index 943d1dec5..000000000 --- a/samples/basic/build.gradle +++ /dev/null @@ -1,89 +0,0 @@ -plugins { - id "com.android.application" - id "org.jetbrains.kotlin.android" - id "io.mehow.laboratory" -} - -android { - namespace "io.mehow.laboratory.sample.basic" - - signingConfigs { - config { - keyAlias "mehow-io" - keyPassword "mehow-io" - storeFile file("../mehow-io.keystore") - storePassword "mehow-io" - } - } - - defaultConfig { - applicationId "io.mehow.laboratory.sample.basic" - targetSdkVersion libs.versions.androidBuild.targetSdk.get().toInteger() - - versionCode 1 - versionName "1.0.0" - - signingConfig signingConfigs.config - } - - buildFeatures { - viewBinding true - } - - buildTypes.debug.setMatchingFallbacks "release" - - variantFilter { - setIgnore it.name != "debug" - } - - compileOptions { - sourceCompatibility JavaConfig.code - targetCompatibility JavaConfig.code - } - - kotlinOptions { - jvmTarget = JavaConfig.name - freeCompilerArgs += [ - "-Xjvm-default=all", - ] - } -} - -laboratory { - packageName = "io.mehow.laboratory.sample.basic" - - featureFactory() - - feature("LogType") { - deprecated("Sample deprecation") - - withDefaultOption("Info") - withOption("Verbose") - withOption("Debug") - withOption("Warning") - withOption("Error") - } - - feature("ReportRootedDevice") { - description = - "Reports during [cold start](https://developer.android.com/topic/performance/vitals/launch-time#cold) whether device is rooted" - - withDefaultOption("Disabled") - withOption("Enabled") - } - - feature("Authentication") { - withDefaultOption("Password") - withOption("Fingerprint") - withOption("Retina") - withOption("Face") - } -} - -dependencies { - implementation libs.kotlin.x.coroutinesAndroid - implementation libs.android.material - implementation libs.hyperion.core - implementation libs.laboratory.dataStore - implementation libs.laboratory.hyperionPlugin -} diff --git a/samples/basic/build.gradle.kts b/samples/basic/build.gradle.kts new file mode 100644 index 000000000..5b009588f --- /dev/null +++ b/samples/basic/build.gradle.kts @@ -0,0 +1,49 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.laboratory) + alias(libs.plugins.detekt) + alias(libs.plugins.spotless) +} + +android { + namespace = "io.mehow.laboratory.sample.basic" +} + +laboratory { + packageName = "io.mehow.laboratory.sample.basic" + + featureFactory() + + feature("LogType") { + deprecated("Sample deprecation") + + withDefaultOption("Info") + withOption("Verbose") + withOption("Debug") + withOption("Warning") + withOption("Error") + } + + feature("ReportRootedDevice") { + description = "Reports during [cold start](https://developer.android.com/topic/performance/vitals/launch-time#cold) whether device is rooted" + + withDefaultOption("Disabled") + withOption("Enabled") + } + + feature("Authentication") { + withDefaultOption("Password") + withOption("Fingerprint") + withOption("Retina") + withOption("Face") + } +} + +dependencies { + implementation(libs.kotlinx.coroutinesAndroid) + implementation(libs.android.material) + implementation(libs.hyperion.core) + implementation(libs.laboratory.dataStore) + implementation(libs.laboratory.hyperionPlugin) +} diff --git a/samples/basic/src/main/kotlin/io/mehow/laboratory/sample/basic/Activity.kt b/samples/basic/src/main/kotlin/io/mehow/laboratory/sample/basic/Activity.kt index 0d4e1406a..83222180b 100644 --- a/samples/basic/src/main/kotlin/io/mehow/laboratory/sample/basic/Activity.kt +++ b/samples/basic/src/main/kotlin/io/mehow/laboratory/sample/basic/Activity.kt @@ -21,6 +21,7 @@ class Activity : AndroidActivity() { val binding = MainBinding.inflate(layoutInflater).apply { launchLaboratory.setOnClickListener { LaboratoryActivity.start(this@Activity) } + @Suppress("DEPRECATION") logType.observeFeature() reportRootedDevice.observeFeature() authentication.observeFeature() @@ -35,8 +36,8 @@ class Activity : AndroidActivity() { private inline fun > TextView.observeFeature() { laboratory.observe() - .map { "${it.javaClass.simpleName}: $it" } - .onEach { text = it } - .launchIn(mainScope) + .map { "${it.javaClass.simpleName}: $it" } + .onEach { text = it } + .launchIn(mainScope) } } diff --git a/samples/build.gradle.kts b/samples/build.gradle.kts new file mode 100644 index 000000000..7a3560ec6 --- /dev/null +++ b/samples/build.gradle.kts @@ -0,0 +1,139 @@ +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.gradle.AppPlugin +import com.diffplug.gradle.spotless.SpotlessExtension +import com.diffplug.gradle.spotless.SpotlessExtensionPredeclare +import com.diffplug.gradle.spotless.SpotlessPlugin +import com.diffplug.spotless.LineEnding +import io.gitlab.arturbosch.detekt.Detekt +import io.gitlab.arturbosch.detekt.DetektPlugin +import io.gitlab.arturbosch.detekt.extensions.DetektExtension +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompilerOptions +import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask + +plugins { + alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.android.application) apply false + alias(libs.plugins.laboratory) apply false + alias(libs.plugins.detekt) + alias(libs.plugins.spotless) +} + +val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get()) +val ktlintVersion = libs.versions.ktlint.get() + +allprojects { + val configureSpotless: SpotlessExtension.() -> Unit = { + lineEndings = LineEnding.UNIX + + kotlin { + target("src/**/*.kt") + trimTrailingWhitespace() + endWithNewline() + ktlint(ktlintVersion).setEditorConfigPath(rootProject.file(".editorconfig")) + } + + kotlinGradle { + target("*.kts") + trimTrailingWhitespace() + endWithNewline() + ktlint(ktlintVersion).setEditorConfigPath(rootProject.file(".editorconfig")) + } + + format("misc") { + target("*.md", "*.yml", "*.proto", "*.properties", "*.toml", ".gitignore", ".editorconfig") + trimTrailingWhitespace() + endWithNewline() + } + } + plugins.withType().configureEach { + configure { + configureSpotless() + + if (project.rootProject == project) { + predeclareDeps() + } + } + if (project.rootProject == project) { + configure { configureSpotless() } + } + } + + plugins.withType().configureEach { + configure { + toolVersion = libs.versions.detekt.get() + allRules = true + parallel = true + buildUponDefaultConfig = true + config.from(rootProject.file("detekt.yml")) + } + tasks.withType().configureEach { + jvmTarget = javaTarget.target + reports { + html.required.set(true) + xml.required.set(true) + txt.required.set(true) + } + } + } +} + +subprojects { + plugins.withType().configureEach { + tasks.withType>().configureEach { + compilerOptions { + jvmTarget.set(javaTarget) + progressiveMode.set(true) + allWarningsAsErrors.set(true) + freeCompilerArgs.addAll( + "-Xjvm-default=all", + ) + } + } + } + + tasks.withType().configureEach { + sourceCompatibility = javaTarget.target + targetCompatibility = javaTarget.target + } + + plugins.withType().configureEach { + configure { + compileOptions { + sourceCompatibility = JavaVersion.toVersion(javaTarget.target) + targetCompatibility = JavaVersion.toVersion(javaTarget.target) + } + + val dummyConfig by signingConfigs.creating { + storeFile = rootProject.file("mehow-io.keystore") + storePassword = "mehow-io" + keyAlias = "mehow-io" + keyPassword = "mehow-io" + } + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.targetSdk.get().toInt() + + versionCode = 1 + versionName = "1.0.0" + + signingConfig = dummyConfig + } + + buildFeatures { + viewBinding = true + } + + buildTypes { + debug { + applicationIdSuffix = ".debug" + matchingFallbacks.add("release") + } + } + } + } +} diff --git a/samples/ci-check/build.gradle b/samples/ci-check/build.gradle deleted file mode 100644 index 501fe8d0d..000000000 --- a/samples/ci-check/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -plugins { - id "org.jetbrains.kotlin.jvm" - id "io.mehow.laboratory" -} - -tasks.withType(JavaCompile).configureEach { - sourceCompatibility JavaConfig.name - targetCompatibility JavaConfig.name -} - -tasks.withType(KotlinCompile).configureEach { - kotlinOptions { - jvmTarget = JavaConfig.name - freeCompilerArgs += [ - "-Xjvm-default=all", - ] - } -} - -laboratory { - packageName = "io.mehow.laboratory.sample.ci" - - feature("SampleFlag") { - withDefaultOption("OptionA") - withOption("OptionB") - } -} diff --git a/samples/ci-check/src/main/kotlin/main.kt b/samples/ci-check/src/main/kotlin/main.kt deleted file mode 100644 index cc405003a..000000000 --- a/samples/ci-check/src/main/kotlin/main.kt +++ /dev/null @@ -1,7 +0,0 @@ -import io.mehow.laboratory.Laboratory -import io.mehow.laboratory.sample.ci.SampleFlag - -suspend fun main() { - val laboratory = Laboratory.inMemory() - println(laboratory.experimentIs(SampleFlag.OptionA)) -} diff --git a/samples/default-option/build.gradle b/samples/default-option/build.gradle deleted file mode 100644 index 26f761f41..000000000 --- a/samples/default-option/build.gradle +++ /dev/null @@ -1,81 +0,0 @@ -plugins { - id "com.android.application" - id "org.jetbrains.kotlin.android" - id "io.mehow.laboratory" -} - -android { - namespace "io.mehow.laboratory.sample.defaultoption" - - signingConfigs { - config { - keyAlias "mehow-io" - keyPassword "mehow-io" - storeFile file("../mehow-io.keystore") - storePassword "mehow-io" - } - } - - defaultConfig { - applicationId "io.mehow.laboratory.sample.defaultoption" - targetSdkVersion libs.versions.androidBuild.targetSdk.get().toInteger() - - versionCode 1 - versionName "1.0.0" - - signingConfig signingConfigs.config - } - - buildTypes { - debug { - applicationIdSuffix ".debug" - } - } - - buildFeatures { - viewBinding true - } - - buildTypes.debug.setMatchingFallbacks "release" - - compileOptions { - sourceCompatibility JavaConfig.code - targetCompatibility JavaConfig.code - } - - kotlinOptions { - jvmTarget = JavaConfig.name - freeCompilerArgs += [ - "-Xjvm-default=all", - ] - } -} - -laboratory { - packageName = "io.mehow.laboratory.sample.defaultoption" - - featureFactory() - - feature("ShowAds") { - withDefaultOption("Enabled") - withOption("Disabled") - } - - feature("ReportRootedDevice") { - withDefaultOption("Enabled") - withOption("Disabled") - } - - feature("RequiredFingerprint") { - withDefaultOption("Enabled") - withOption("Disabled") - } -} - -dependencies { - implementation libs.kotlin.x.coroutinesAndroid - implementation libs.android.material - implementation libs.hyperion.core - implementation libs.laboratory.dataStore - implementation libs.laboratory.hyperionPlugin -} diff --git a/samples/default-option/build.gradle.kts b/samples/default-option/build.gradle.kts new file mode 100644 index 000000000..59dc949b3 --- /dev/null +++ b/samples/default-option/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.laboratory) + alias(libs.plugins.detekt) + alias(libs.plugins.spotless) +} + +android { + namespace = "io.mehow.laboratory.sample.defaultoption" +} + +laboratory { + packageName = "io.mehow.laboratory.sample.defaultoption" + + featureFactory() + + feature("ShowAds") { + withDefaultOption("Enabled") + withOption("Disabled") + } + + feature("ReportRootedDevice") { + withDefaultOption("Enabled") + withOption("Disabled") + } + + feature("RequiredFingerprint") { + withDefaultOption("Enabled") + withOption("Disabled") + } +} + +dependencies { + implementation(libs.kotlinx.coroutinesAndroid) + implementation(libs.android.material) + implementation(libs.hyperion.core) + implementation(libs.laboratory.dataStore) + implementation(libs.laboratory.hyperionPlugin) +} diff --git a/samples/default-option/src/debug/kotlin/io/mehow.laboratory/sample/defaultoption/DefaultOptionFactory.kt b/samples/default-option/src/debug/kotlin/io/mehow.laboratory/sample/defaultoption/DefaultOptionFactory.kt index 149a00e6d..8807c1c9a 100644 --- a/samples/default-option/src/debug/kotlin/io/mehow.laboratory/sample/defaultoption/DefaultOptionFactory.kt +++ b/samples/default-option/src/debug/kotlin/io/mehow.laboratory/sample/defaultoption/DefaultOptionFactory.kt @@ -8,6 +8,6 @@ fun DefaultOptionFactory.Companion.create(): DefaultOptionFactory = DebugDefault private object DebugDefaultOptionFactory : DefaultOptionFactory { override fun > create(feature: T) = feature::class.java - .options - .firstOrNull { it.name == "Disabled" } + .options + .firstOrNull { it.name == "Disabled" } } diff --git a/samples/default-option/src/main/kotlin/io/mehow/laboratory/sample/defaultoption/Activity.kt b/samples/default-option/src/main/kotlin/io/mehow/laboratory/sample/defaultoption/Activity.kt index 15596df69..9f088d729 100644 --- a/samples/default-option/src/main/kotlin/io/mehow/laboratory/sample/defaultoption/Activity.kt +++ b/samples/default-option/src/main/kotlin/io/mehow/laboratory/sample/defaultoption/Activity.kt @@ -35,8 +35,8 @@ class Activity : AndroidActivity() { private inline fun > TextView.observeFeature() { laboratory.observe() - .map { "${it.javaClass.simpleName}: $it" } - .onEach { text = it } - .launchIn(mainScope) + .map { "${it.javaClass.simpleName}: $it" } + .onEach { text = it } + .launchIn(mainScope) } } diff --git a/samples/default-option/src/main/kotlin/io/mehow/laboratory/sample/defaultoption/Application.kt b/samples/default-option/src/main/kotlin/io/mehow/laboratory/sample/defaultoption/Application.kt index ce8fc89e2..1df6ec525 100644 --- a/samples/default-option/src/main/kotlin/io/mehow/laboratory/sample/defaultoption/Application.kt +++ b/samples/default-option/src/main/kotlin/io/mehow/laboratory/sample/defaultoption/Application.kt @@ -20,9 +20,9 @@ class Application : AndroidApplication() { val dataStore = DataStoreFactory.create(FeatureFlagsSerializer) { File(filesDir, "datastore/local") } val storage = FeatureStorage.dataStore(dataStore) laboratory = Laboratory.builder() - .featureStorage(storage) - .defaultOptionFactory(DefaultOptionFactory.create()) - .build() + .featureStorage(storage) + .defaultOptionFactory(DefaultOptionFactory.create()) + .build() LaboratoryActivity.configure(laboratory, FeatureFactory.featureGenerated()) } diff --git a/samples/detekt.yml b/samples/detekt.yml new file mode 120000 index 000000000..2b9b28fc7 --- /dev/null +++ b/samples/detekt.yml @@ -0,0 +1 @@ +../detekt.yml \ No newline at end of file diff --git a/samples/firebase/build.gradle b/samples/firebase/build.gradle deleted file mode 100644 index 2fbe06433..000000000 --- a/samples/firebase/build.gradle +++ /dev/null @@ -1,101 +0,0 @@ -plugins { - id "com.android.application" - id "org.jetbrains.kotlin.android" - id "io.mehow.laboratory" - id "com.google.gms.google-services" -} - -android { - namespace "io.mehow.laboratory.sample.firebase" - - signingConfigs { - config { - keyAlias "mehow-io" - keyPassword "mehow-io" - storeFile file("../mehow-io.keystore") - storePassword "mehow-io" - } - } - - defaultConfig { - applicationId "io.mehow.laboratory.sample.firebase" - - targetSdkVersion libs.versions.androidBuild.targetSdk.get().toInteger() - - versionCode 1 - versionName "1.0.0" - - signingConfig signingConfigs.config - } - - buildFeatures { - viewBinding true - } - - buildTypes.debug.setMatchingFallbacks "release" - - variantFilter { - setIgnore it.name != "debug" - } - - compileOptions { - sourceCompatibility JavaConfig.code - targetCompatibility JavaConfig.code - } - - kotlinOptions { - jvmTarget = JavaConfig.name - freeCompilerArgs += [ - "-Xjvm-default=all", - ] - } -} - -laboratory { - packageName = "io.mehow.laboratory.sample.firebase" - - sourcedStorage() - optionFactory() - featureFactory() - - feature("LogType") { - key = "LogType" - - withDefaultOption("Info") - withOption("Verbose") - withOption("Debug") - withOption("Warning") - withOption("Error") - - withDefaultSource("Firebase") - } - - feature("ReportRootedDevice") { - key = "ReportRootedDevice" - - withDefaultOption("Disabled") - withOption("Enabled") - - withDefaultSource("Firebase") - } - - feature("Authentication") { - key = "Authentication" - - withDefaultOption("Password") - withOption("Fingerprint") - withOption("Retina") - withOption("Face") - - withDefaultSource("Firebase") - } -} - -dependencies { - implementation libs.kotlin.x.coroutinesAndroid - implementation libs.android.material - implementation libs.firebase.databaseKtx - implementation libs.hyperion.core - implementation libs.laboratory.dataStore - implementation libs.laboratory.hyperionPlugin -} diff --git a/samples/firebase/google-services.json b/samples/firebase/google-services.json deleted file mode 100644 index 7ae670380..000000000 --- a/samples/firebase/google-services.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "project_info": { - "project_number": "851249949580", - "project_id": "laboratory-sample", - "storage_bucket": "laboratory-sample.appspot.com" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:851249949580:android:3ba6dee5e056a2e74804a9", - "android_client_info": { - "package_name": "io.mehow.laboratory.sample.firebase" - } - }, - "oauth_client": [ - { - "client_id": "851249949580-i1ls4r7hsomkkon4fpjpdj1dlagctu4j.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "io.mehow.laboratory.sample.firebase", - "certificate_hash": "232d7fb12884c4872303c219d51b0f69f524e739" - } - }, - { - "client_id": "851249949580-2sf7joehcm5emmkmut2g436bith99f4p.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyAQpuW6WWiyH-ZHsmFUACfm6N_pY91h3PU" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "851249949580-2sf7joehcm5emmkmut2g436bith99f4p.apps.googleusercontent.com", - "client_type": 3 - } - ] - } - } - } - ], - "configuration_version": "1" -} \ No newline at end of file diff --git a/samples/firebase/src/main/AndroidManifest.xml b/samples/firebase/src/main/AndroidManifest.xml deleted file mode 100644 index 4f61e533f..000000000 --- a/samples/firebase/src/main/AndroidManifest.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/samples/firebase/src/main/kotlin/io/mehow/laboratory/sample/firebase/Activity.kt b/samples/firebase/src/main/kotlin/io/mehow/laboratory/sample/firebase/Activity.kt deleted file mode 100644 index 46010f2e4..000000000 --- a/samples/firebase/src/main/kotlin/io/mehow/laboratory/sample/firebase/Activity.kt +++ /dev/null @@ -1,42 +0,0 @@ -package io.mehow.laboratory.sample.firebase - -import android.os.Bundle -import android.widget.TextView -import io.mehow.laboratory.Feature -import io.mehow.laboratory.inspector.LaboratoryActivity -import io.mehow.laboratory.sample.firebase.Application.Companion.laboratory -import io.mehow.laboratory.sample.firebase.databinding.MainBinding -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import android.app.Activity as AndroidActivity - -class Activity : AndroidActivity() { - private val mainScope = MainScope() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val binding = MainBinding.inflate(layoutInflater).apply { - launchLaboratory.setOnClickListener { LaboratoryActivity.start(this@Activity) } - logType.observeFeature() - reportRootedDevice.observeFeature() - authentication.observeFeature() - } - setContentView(binding.root) - } - - override fun onDestroy() { - mainScope.cancel() - super.onDestroy() - } - - private inline fun > TextView.observeFeature() { - laboratory.observe() - .map { "${it.javaClass.simpleName}: $it" } - .onEach { text = it } - .launchIn(mainScope) - } -} diff --git a/samples/firebase/src/main/kotlin/io/mehow/laboratory/sample/firebase/Application.kt b/samples/firebase/src/main/kotlin/io/mehow/laboratory/sample/firebase/Application.kt deleted file mode 100644 index 9f4f3638b..000000000 --- a/samples/firebase/src/main/kotlin/io/mehow/laboratory/sample/firebase/Application.kt +++ /dev/null @@ -1,50 +0,0 @@ -package io.mehow.laboratory.sample.firebase - -import android.content.Context -import androidx.datastore.core.DataStoreFactory -import com.google.firebase.database.DataSnapshot -import com.google.firebase.database.DatabaseError -import com.google.firebase.database.ValueEventListener -import com.google.firebase.database.ktx.database -import com.google.firebase.ktx.Firebase -import io.mehow.laboratory.FeatureFactory -import io.mehow.laboratory.FeatureStorage -import io.mehow.laboratory.Laboratory -import io.mehow.laboratory.OptionFactory -import io.mehow.laboratory.datastore.FeatureFlagsSerializer -import io.mehow.laboratory.datastore.dataStore -import io.mehow.laboratory.inspector.LaboratoryActivity -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import java.io.File -import android.app.Application as AndroidApplication - -class Application : AndroidApplication() { - private lateinit var laboratory: Laboratory - - override fun onCreate() { - super.onCreate() - val localDataStore = DataStoreFactory.create(FeatureFlagsSerializer) { File(filesDir, "datastore/local") } - val localStorage = FeatureStorage.dataStore(localDataStore) - val firebaseDataStore = DataStoreFactory.create(FeatureFlagsSerializer) { File(filesDir, "datastore/firebase") } - val firebaseStorage = FeatureStorage.dataStore(firebaseDataStore) - val sourcedStorage = FeatureStorage.sourcedBuilder(localStorage) - .firebaseSource(firebaseStorage) - .build() - laboratory = Laboratory.create(sourcedStorage) - LaboratoryActivity.configure(laboratory, FeatureFactory.featureGenerated()) - - val synchronizer = FirebaseSynchronizer( - databaseReference = Firebase.database(firebaseUrl).reference.child("featureFlags"), - optionFactory = OptionFactory.generated(), - featureStorage = firebaseStorage, - ) - @OptIn(DelicateCoroutinesApi::class) synchronizer.synchronize(GlobalScope) - } - - companion object { - private const val firebaseUrl = "https://laboratory-sample-default-rtdb.europe-west1.firebasedatabase.app" - - val Context.laboratory get() = (applicationContext as Application).laboratory - } -} diff --git a/samples/firebase/src/main/kotlin/io/mehow/laboratory/sample/firebase/FirebaseSynchronizer.kt b/samples/firebase/src/main/kotlin/io/mehow/laboratory/sample/firebase/FirebaseSynchronizer.kt deleted file mode 100644 index ad9a2b155..000000000 --- a/samples/firebase/src/main/kotlin/io/mehow/laboratory/sample/firebase/FirebaseSynchronizer.kt +++ /dev/null @@ -1,46 +0,0 @@ -package io.mehow.laboratory.sample.firebase - -import com.google.firebase.database.DataSnapshot -import com.google.firebase.database.DatabaseError -import com.google.firebase.database.DatabaseReference -import com.google.firebase.database.ValueEventListener -import io.mehow.laboratory.FeatureStorage -import io.mehow.laboratory.OptionFactory -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach - -class FirebaseSynchronizer( - private val databaseReference: DatabaseReference, - private val optionFactory: OptionFactory, - private val featureStorage: FeatureStorage, -) { - fun synchronize(scope: CoroutineScope) = databaseReference.asKeyValueFlow() - .map { pairs -> pairs.mapNotNull { (key, value) -> optionFactory.create(key, value) } } - .onEach(featureStorage::setOptions) - .launchIn(scope) - - private fun DatabaseReference.asKeyValueFlow() = callbackFlow { - val listener = object : ValueEventListener { - override fun onDataChange(snapshot: DataSnapshot) { - (snapshot.value as? Map<*, *>)?.mapNotNull { (key, value) -> - val stringKey = key as? String ?: return@mapNotNull null - val stringValue = value as? String ?: return@mapNotNull null - stringKey to stringValue - }?.let { trySend(it) } - } - - override fun onCancelled(error: DatabaseError) { - close(error.toException()) - } - } - addValueEventListener(listener) - - awaitClose { - removeEventListener(listener) - } - } -} diff --git a/samples/firebase/src/main/res/layout/main.xml b/samples/firebase/src/main/res/layout/main.xml deleted file mode 100644 index a0c6fdaaf..000000000 --- a/samples/firebase/src/main/res/layout/main.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - diff --git a/samples/gradle.properties b/samples/gradle.properties new file mode 100644 index 000000000..607ae1e7c --- /dev/null +++ b/samples/gradle.properties @@ -0,0 +1,5 @@ +# Increase the build VMs heap size. Default is 512m. +org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 +org.gradle.parallel=true + +android.useAndroidX=true diff --git a/library/gradle/wrapper/gradle-wrapper.jar b/samples/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from library/gradle/wrapper/gradle-wrapper.jar rename to samples/gradle/wrapper/gradle-wrapper.jar diff --git a/library/gradle/wrapper/gradle-wrapper.properties b/samples/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from library/gradle/wrapper/gradle-wrapper.properties rename to samples/gradle/wrapper/gradle-wrapper.properties diff --git a/library/gradlew b/samples/gradlew similarity index 100% rename from library/gradlew rename to samples/gradlew diff --git a/library/gradlew.bat b/samples/gradlew.bat similarity index 100% rename from library/gradlew.bat rename to samples/gradlew.bat diff --git a/samples/multi-module/build.gradle b/samples/multi-module/build.gradle deleted file mode 100644 index 3e772d3c2..000000000 --- a/samples/multi-module/build.gradle +++ /dev/null @@ -1,71 +0,0 @@ -plugins { - id "com.android.application" - id "org.jetbrains.kotlin.android" - id "io.mehow.laboratory" -} - -android { - namespace "io.mehow.laboratory.sample.multimodule" - - signingConfigs { - config { - keyAlias "mehow-io" - keyPassword "mehow-io" - storeFile file("../mehow-io.keystore") - storePassword "mehow-io" - } - } - - defaultConfig { - applicationId "io.mehow.laboratory.sample.multimodule" - - targetSdkVersion libs.versions.androidBuild.targetSdk.get().toInteger() - - versionCode 1 - versionName "1.0.0" - - signingConfig signingConfigs.config - } - - buildFeatures { - viewBinding true - } - - buildTypes.debug.setMatchingFallbacks "release" - - variantFilter { - setIgnore it.name != "debug" - } - - compileOptions { - sourceCompatibility JavaConfig.code - targetCompatibility JavaConfig.code - } - - kotlinOptions { - jvmTarget = JavaConfig.name - freeCompilerArgs += [ - "-Xjvm-default=all", - ] - } -} - -laboratory { - packageName = "io.mehow.laboratory.sample.multimodule" - - featureFactory() - - dependency(project(":samples:multi-module:multi-module-a")) - dependency(project(":samples:multi-module:multi-module-b")) -} - -dependencies { - implementation libs.kotlin.x.coroutinesAndroid - implementation libs.android.material - implementation libs.hyperion.core - implementation libs.laboratory.dataStore - implementation libs.laboratory.hyperionPlugin - implementation project(":samples:multi-module:multi-module-a") - implementation project(":samples:multi-module:multi-module-b") - implementation project(":samples:multi-module:multi-module-c") -} diff --git a/samples/multi-module/build.gradle.kts b/samples/multi-module/build.gradle.kts new file mode 100644 index 000000000..2cfae7ed8 --- /dev/null +++ b/samples/multi-module/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.laboratory) + alias(libs.plugins.detekt) + alias(libs.plugins.spotless) +} + +android { + namespace = "io.mehow.laboratory.sample.multimodule" +} + +laboratory { + packageName = "io.mehow.laboratory.sample.multimodule" + + featureFactory() + + dependency(project(":multi-module:multi-module-a")) + dependency(project(":multi-module:multi-module-b")) +} + +dependencies { + implementation(libs.kotlinx.coroutinesAndroid) + implementation(libs.android.material) + implementation(libs.hyperion.core) + implementation(libs.laboratory.dataStore) + implementation(libs.laboratory.hyperionPlugin) + implementation(projects.multiModule.multiModuleA) + implementation(projects.multiModule.multiModuleB) + implementation(projects.multiModule.multiModuleC) +} diff --git a/samples/multi-module/multi-module-a/build.gradle b/samples/multi-module/multi-module-a/build.gradle.kts similarity index 59% rename from samples/multi-module/multi-module-a/build.gradle rename to samples/multi-module/multi-module-a/build.gradle.kts index 3d411130d..659f53c13 100644 --- a/samples/multi-module/multi-module-a/build.gradle +++ b/samples/multi-module/multi-module-a/build.gradle.kts @@ -1,14 +1,8 @@ plugins { - id "org.jetbrains.kotlin.jvm" - id "io.mehow.laboratory" -} - -compileKotlin { - kotlinOptions { - freeCompilerArgs += [ - "-Xjvm-default=all", - ] - } + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.laboratory) + alias(libs.plugins.detekt) + alias(libs.plugins.spotless) } laboratory { diff --git a/samples/multi-module/multi-module-b/build.gradle b/samples/multi-module/multi-module-b/build.gradle.kts similarity index 72% rename from samples/multi-module/multi-module-b/build.gradle rename to samples/multi-module/multi-module-b/build.gradle.kts index 8680fb302..2f9079fed 100644 --- a/samples/multi-module/multi-module-b/build.gradle +++ b/samples/multi-module/multi-module-b/build.gradle.kts @@ -1,14 +1,8 @@ plugins { - id "org.jetbrains.kotlin.jvm" - id "io.mehow.laboratory" -} - -compileKotlin { - kotlinOptions { - freeCompilerArgs += [ - "-Xjvm-default=all", - ] - } + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.laboratory) + alias(libs.plugins.detekt) + alias(libs.plugins.spotless) } laboratory { diff --git a/samples/multi-module/multi-module-c/build.gradle b/samples/multi-module/multi-module-c/build.gradle deleted file mode 100644 index ac8a760e9..000000000 --- a/samples/multi-module/multi-module-c/build.gradle +++ /dev/null @@ -1,59 +0,0 @@ -plugins { - id "org.jetbrains.kotlin.jvm" - id "io.mehow.laboratory" -} - -compileKotlin { - kotlinOptions { - freeCompilerArgs += [ - "-Xjvm-default=all", - ] - } -} - -laboratory { - packageName = "io.mehow.laboratory.smaple.multimodule.c" - - featureFactory { - isPublic = true - } - - feature("Camera") { - withDefaultOption("Disabled") - - withOption("Enabled") { camera -> - camera.feature("LivestreamPreview") { livestream -> - livestream.withDefaultOption("Enabled") - livestream.withOption("Disabled") - } - - camera.feature("RecordingQuality") { quality -> - quality.withDefaultOption("SD") - quality.withOption("HD") - quality.withOption("QHD") - } - - camera.feature("RecordingDirectory") { directory -> - directory.withDefaultOption("Internal") - directory.withOption("External") - } - - camera.feature("VideoFilter") { filter -> - filter.withDefaultOption("NoFilter") - filter.withOption("Retro") - filter.withOption("Sepia") - filter.withOption("EightBit") - } - - camera.feature("MotionDetection") { motion -> - motion.withDefaultOption("Enabled") - motion.withOption("Disabled") - } - - camera.feature("NightMode") { nightMode -> - nightMode.withDefaultOption("Enabled") - nightMode.withOption("Disabled") - } - } - } -} diff --git a/samples/multi-module/multi-module-c/build.gradle.kts b/samples/multi-module/multi-module-c/build.gradle.kts new file mode 100644 index 000000000..3d98f3245 --- /dev/null +++ b/samples/multi-module/multi-module-c/build.gradle.kts @@ -0,0 +1,53 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.laboratory) + alias(libs.plugins.detekt) + alias(libs.plugins.spotless) +} + +laboratory { + packageName = "io.mehow.laboratory.smaple.multimodule.c" + + featureFactory { + isPublic = true + } + + feature("Camera") { + withDefaultOption("Disabled") + + withOption("Enabled") { + feature("LivestreamPreview") { + withDefaultOption("Enabled") + withOption("Disabled") + } + + feature("RecordingQuality") { + withDefaultOption("SD") + withOption("HD") + withOption("QHD") + } + + feature("RecordingDirectory") { + withDefaultOption("Internal") + withOption("External") + } + + feature("VideoFilter") { + withDefaultOption("NoFilter") + withOption("Retro") + withOption("Sepia") + withOption("EightBit") + } + + feature("MotionDetection") { + withDefaultOption("Enabled") + withOption("Disabled") + } + + feature("NightMode") { + withDefaultOption("Enabled") + withOption("Disabled") + } + } + } +} diff --git a/samples/multi-module/src/main/kotlin/io/mehow/laboratory/sample/multimodule/Activity.kt b/samples/multi-module/src/main/kotlin/io/mehow/laboratory/sample/multimodule/Activity.kt index 91d8babfe..14cf1c493 100644 --- a/samples/multi-module/src/main/kotlin/io/mehow/laboratory/sample/multimodule/Activity.kt +++ b/samples/multi-module/src/main/kotlin/io/mehow/laboratory/sample/multimodule/Activity.kt @@ -52,8 +52,8 @@ class Activity : AndroidActivity() { private inline fun > TextView.observeFeature() { laboratory.observe() - .map { "${it.javaClass.simpleName}: $it" } - .onEach { text = it } - .launchIn(mainScope) + .map { "${it.javaClass.simpleName}: $it" } + .onEach { text = it } + .launchIn(mainScope) } } diff --git a/samples/multi-module/src/main/kotlin/io/mehow/laboratory/sample/multimodule/Application.kt b/samples/multi-module/src/main/kotlin/io/mehow/laboratory/sample/multimodule/Application.kt index e091d27bf..4838e6979 100644 --- a/samples/multi-module/src/main/kotlin/io/mehow/laboratory/sample/multimodule/Application.kt +++ b/samples/multi-module/src/main/kotlin/io/mehow/laboratory/sample/multimodule/Application.kt @@ -21,9 +21,9 @@ class Application : AndroidApplication() { val storage = FeatureStorage.dataStore(dataStore) laboratory = Laboratory.create(storage) LaboratoryActivity.configure( - laboratory, - mainFactory = FeatureFactory.featureGenerated(), - externalFactories = mapOf("Camera" to FeatureFactory.cameraFeatureGenerated()) + laboratory, + mainFactory = FeatureFactory.featureGenerated(), + externalFactories = mapOf("Camera" to FeatureFactory.cameraFeatureGenerated()), ) } diff --git a/samples/settings.gradle.kts b/samples/settings.gradle.kts new file mode 100644 index 000000000..34ac03dce --- /dev/null +++ b/samples/settings.gradle.kts @@ -0,0 +1,51 @@ +pluginManagement { + repositories { + mavenCentral() + google() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + library("laboratory-runtime", "io.mehow.laboratory:laboratory:+") + library("laboratory-sharedPreferences", "io.mehow.laboratory:laboratory-shared-preferences:+") + library("laboratory-dataStore", "io.mehow.laboratory:laboratory-data-store:+") + library("laboratory-generator", "io.mehow.laboratory:laboratory-generator:+") + library("laboratory-inspector", "io.mehow.laboratory:laboratory-inspector:+") + library("laboratory-hyperionPlugin", "io.mehow.laboratory:laboratory-hyperion-plugin:+") + plugin("laboratory", "io.mehow.laboratory").version("+") + } + } + + repositories { + mavenCentral() + google() + } +} + +includeBuild("..") { + dependencySubstitution { + substitute(module("io.mehow.laboratory:laboratory")).using(project(":laboratory:runtime")) + substitute(module("io.mehow.laboratory:laboratory-shared-preferences")).using(project(":laboratory:shared-preferences")) + substitute(module("io.mehow.laboratory:laboratory-data-store")).using(project(":laboratory:data-store")) + substitute(module("io.mehow.laboratory:laboratory-generator")).using(project(":laboratory:generator")) + substitute(module("io.mehow.laboratory:laboratory-gradle-plugin")).using(project(":laboratory:gradle-plugin")) + substitute(module("io.mehow.laboratory:laboratory-inspector")).using(project(":laboratory:inspector")) + substitute(module("io.mehow.laboratory:laboratory-hyperion-plugin")).using(project(":laboratory:hyperion-plugin")) + } +} + +rootProject.name = "samples-root" + +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +include(":basic") +include(":default-option") +include(":supervision") +include(":multi-module") +include(":multi-module:multi-module-a") +include(":multi-module:multi-module-b") +include(":multi-module:multi-module-c") diff --git a/samples/supervision/build.gradle b/samples/supervision/build.gradle deleted file mode 100644 index db86ed265..000000000 --- a/samples/supervision/build.gradle +++ /dev/null @@ -1,105 +0,0 @@ -plugins { - id "com.android.application" - id "org.jetbrains.kotlin.android" - id "io.mehow.laboratory" -} - -android { - namespace "io.mehow.laboratory.sample.supervision" - - signingConfigs { - config { - keyAlias "mehow-io" - keyPassword "mehow-io" - storeFile file("../mehow-io.keystore") - storePassword "mehow-io" - } - } - - defaultConfig { - applicationId "io.mehow.laboratory.sample.supervision" - - targetSdkVersion libs.versions.androidBuild.targetSdk.get().toInteger() - - versionCode 1 - versionName "1.0.0" - - signingConfig signingConfigs.config - } - - buildFeatures { - viewBinding true - } - - buildTypes.debug.setMatchingFallbacks "release" - - variantFilter { - setIgnore it.name != "debug" - } - - compileOptions { - sourceCompatibility JavaConfig.code - targetCompatibility JavaConfig.code - } - - kotlinOptions { - jvmTarget = JavaConfig.name - freeCompilerArgs += [ - "-Xjvm-default=all", - ] - } -} - -laboratory { - packageName = "io.mehow.laboratory.sample.supervision" - - featureFactory() - - feature("Theming") { - withDefaultOption("Default") - - withOption("Christmas") { christmas -> - christmas.feature("ChristmasGreeting") { greeting -> - greeting.withDefaultOption("Disabled") - greeting.withOption("Hello") - greeting.withOption("HoHoHo") - } - - christmas.feature("ChristmasBackground") { background -> - background.withDefaultOption("Disabled") - background.withOption("Reindeer") - background.withOption("Snowman") - } - } - - withOption("Halloween") { halloween -> - halloween.feature("SpookyMusic") { spookyMusic -> - spookyMusic.withDefaultOption("Disabled") - spookyMusic.withOption("Graveyard") - spookyMusic.withOption("HauntedHouse") - spookyMusic.withOption("OldCastle") - } - - halloween.feature("WitchChance") { witchChance -> - witchChance.withDefaultOption("Chance00") - witchChance.withOption("Chance20") - witchChance.withOption("Chance50") - witchChance.withOption("Chance100") - } - - halloween.feature("CandyArt") { candy -> - candy.withDefaultOption("Disabled") - candy.withOption("ChocolateGhost") - candy.withOption("MarshmallowGhost") - } - } - } -} - -dependencies { - implementation libs.kotlin.x.coroutinesAndroid - implementation libs.android.material - implementation libs.hyperion.core - implementation libs.laboratory.dataStore - implementation libs.laboratory.hyperionPlugin -} diff --git a/samples/supervision/build.gradle.kts b/samples/supervision/build.gradle.kts new file mode 100644 index 000000000..92ffd6297 --- /dev/null +++ b/samples/supervision/build.gradle.kts @@ -0,0 +1,65 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.laboratory) + alias(libs.plugins.detekt) + alias(libs.plugins.spotless) +} + +android { + namespace = "io.mehow.laboratory.sample.supervision" +} + +laboratory { + packageName = "io.mehow.laboratory.sample.supervision" + + featureFactory() + + feature("Theming") { + withDefaultOption("Default") + + withOption("Christmas") { + feature("ChristmasGreeting") { + withDefaultOption("Disabled") + withOption("Hello") + withOption("HoHoHo") + } + + feature("ChristmasBackground") { + withDefaultOption("Disabled") + withOption("Reindeer") + withOption("Snowman") + } + } + + withOption("Halloween") { + feature("SpookyMusic") { + withDefaultOption("Disabled") + withOption("Graveyard") + withOption("HauntedHouse") + withOption("OldCastle") + } + + feature("WitchChance") { + withDefaultOption("Chance00") + withOption("Chance20") + withOption("Chance50") + withOption("Chance100") + } + + feature("CandyArt") { + withDefaultOption("Disabled") + withOption("ChocolateGhost") + withOption("MarshmallowGhost") + } + } + } +} + +dependencies { + implementation(libs.kotlinx.coroutinesAndroid) + implementation(libs.android.material) + implementation(libs.hyperion.core) + implementation(libs.laboratory.dataStore) + implementation(libs.laboratory.hyperionPlugin) +} diff --git a/samples/supervision/src/main/kotlin/io/mehow/laboratory/sample/supervision/Activity.kt b/samples/supervision/src/main/kotlin/io/mehow/laboratory/sample/supervision/Activity.kt index 128671ace..63e9efc02 100644 --- a/samples/supervision/src/main/kotlin/io/mehow/laboratory/sample/supervision/Activity.kt +++ b/samples/supervision/src/main/kotlin/io/mehow/laboratory/sample/supervision/Activity.kt @@ -4,8 +4,8 @@ import android.os.Bundle import android.widget.TextView import io.mehow.laboratory.Feature import io.mehow.laboratory.inspector.LaboratoryActivity -import io.mehow.laboratory.sample.supervision.databinding.MainBinding import io.mehow.laboratory.sample.supervision.Application.Companion.laboratory +import io.mehow.laboratory.sample.supervision.databinding.MainBinding import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.launchIn @@ -38,8 +38,8 @@ class Activity : AndroidActivity() { private inline fun > TextView.observeFeature() { laboratory.observe() - .map { "${it.javaClass.simpleName}: $it" } - .onEach { text = it } - .launchIn(mainScope) + .map { "${it.javaClass.simpleName}: $it" } + .onEach { text = it } + .launchIn(mainScope) } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 0dab3617e..4916ed4a1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,45 +1,26 @@ -import com.android.build.api.dsl.SettingsExtension - pluginManagement { repositories { mavenCentral() google() + gradlePluginPortal() } } dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { mavenCentral() google() - gradlePluginPortal() - } - - versionCatalogs { - create("libs") { - from(files("library/gradle/dependencies.toml")) - } } } -plugins { - id("com.android.settings") version "8.0.2" -} - -@Suppress("UnstableApiUsage") -extensions.getByType(SettingsExtension::class).apply { - compileSdk = 33 - minSdk = 21 -} +rootProject.name = "laboratory-root" -include(":samples:basic") -include(":samples:default-option") -include(":samples:multi-module") -include(":samples:multi-module:multi-module-a") -include(":samples:multi-module:multi-module-b") -include(":samples:multi-module:multi-module-c") -include(":samples:firebase") -include(":samples:supervision") -include(":samples:ci-check") +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") -includeBuild("library") +include(":laboratory:runtime") +include(":laboratory:shared-preferences") +include(":laboratory:data-store") +include(":laboratory:generator") +include(":laboratory:gradle-plugin") +include(":laboratory:inspector") +include(":laboratory:hyperion-plugin")