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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- xmlns:android
-
-
-
-
-
-
-
-
-
- xmlns:.*
-
-
-
- BY_NAME
-
-
-
-
-
-
- .*:id
-
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- .*:name
-
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- .*:layout_width
-
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- .*:layout_height
-
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- .*:layout_.*
-
- http://schemas.android.com/apk/res/android
-
-
- BY_NAME
-
-
-
-
-
-
- .*
-
- http://schemas.android.com/apk/res/android
-
-
- BY_NAME
-
-
-
-
-
-
- app:layout_.*
-
- http://schemas.android.com/apk/res-auto
-
-
- BY_NAME
-
-
-
-
-
-
- .*(?<!style)$
-
-
-
- BY_NAME
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ 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