From 2b6da5a4d776950077bc5a189949ee2897d7d882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sikora?= Date: Sun, 12 May 2024 15:59:35 +0200 Subject: [PATCH] Restructure the project --- .editorconfig | 21 + .github/PULL_REQUEST_TEMPLATE.md | 12 +- .../workflows/gradle-wrapper-validation.yml | 8 +- .github/workflows/publish-release.yml | 18 +- .github/workflows/publish-snapshot.yml | 20 +- .github/workflows/quality-check.yml | 105 ++-- .gitignore | 6 +- .idea/codeStyles/Project.xml | 429 -------------- .idea/codeStyles/codeStyleConfig.xml | 5 - .../.mkdocs-theme => .mkdocs-theme}/404.html | 2 +- .../build.gradle.kts | 0 build-support/settings.gradle.kts | 10 + build.gradle.kts | 164 +++++- buildSrc/build.gradle.kts | 3 - buildSrc/settings.gradle.kts | 6 - buildSrc/src/main/kotlin/JavaConfig.kt | 6 - detekt.yml | 22 + {library/docs => docs}/changelog.md | 0 {library/docs => docs}/css/site.css | 0 {library/docs => docs}/gradle-plugin.md | 0 .../images/hyperion_screenshot.jpg | Bin .../images/inspector_screenshot.jpg | Bin .../docs => docs}/images/laboratory_logo.ico | Bin .../docs => docs}/images/laboratory_logo.svg | 0 .../images/laboratory_logo_menu.svg | 0 {library/docs => docs}/index.md | 0 {library/docs => docs}/qa-module.md | 0 {library/docs => docs}/releasing.md | 0 {library/docs => docs}/requirements.txt | 0 {library/docs => docs}/user-guide.md | 0 gradle.properties | 21 +- gradle/libs.versions.toml | 55 ++ gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 62076 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 19 +- gradlew.bat | 1 + .../data-store/api/data-store.api | 0 laboratory/data-store/build.gradle.kts | 34 ++ .../data-store/gradle.properties | 2 +- .../datastore/DataStoreFeatureStorage.kt | 4 +- .../datastore/FeatureFlagsSerializer.kt | 5 +- .../laboratory/datastore/feature_flags.proto | 2 - .../datastore/DataStoreFeatureStorageSpec.kt | 16 +- .../io/mehow/laboratory/datastore/FeatureA.kt | 2 +- .../datastore/FeatureFlagsSerializerSpec.kt | 29 + .../generator/api/generator.api | 1 + laboratory/generator/build.gradle.kts | 20 + .../generator/gradle.properties | 2 +- .../mehow/laboratory/generator/ClassNames.kt | 11 + .../mehow/laboratory/generator/Deprecation.kt | 4 +- .../generator/FeatureFactoryGenerator.kt | 54 +- .../generator/FeatureFactoryModel.kt | 0 .../generator/FeatureFlagGenerator.kt | 98 ++-- .../laboratory/generator/FeatureFlagModel.kt | 28 +- .../laboratory/generator/FeatureFlagOption.kt | 0 .../generator/OptionFactoryModel.kt | 68 +-- .../SourcedFeatureStorageGenerator.kt | 150 +++++ .../generator/SourcedFeatureStorageModel.kt | 0 .../mehow/laboratory/generator/Supervisor.kt | 0 .../mehow/laboratory/generator/Visibility.kt | 2 +- .../generator/FeatureFactoryModelSpec.kt | 116 ++++ .../generator/FeatureFlagModelSpec.kt | 521 +++++++++++++++++ .../generator/OptionFactoryModelSpec.kt | 302 ++++++++++ .../SourcedFeatureStorageModelSpec.kt | 264 +++++++++ .../laboratory/generator/SupervisorSpec.kt | 42 ++ .../generator/test/KotlinPoetMatchers.kt | 17 + .../gradle-plugin/api/gradle-plugin.api | 1 + laboratory/gradle-plugin/build.gradle.kts | 52 ++ .../gradle-plugin/gradle.properties | 2 +- .../gradle/ChildFeatureFlagsInput.kt | 5 +- .../laboratory/gradle/DeprecationLevel.kt | 0 .../laboratory/gradle/FeatureFactoryInput.kt | 11 +- .../laboratory/gradle/FeatureFactoryTask.kt | 5 + .../laboratory/gradle/FeatureFlagInput.kt | 37 +- .../laboratory/gradle/FeatureFlagsTask.kt | 1 + .../laboratory/gradle/LaboratoryExtension.kt | 6 +- .../laboratory/gradle/LaboratoryPlugin.kt | 32 +- .../laboratory/gradle/OptionFactoryInput.kt | 6 +- .../laboratory/gradle/OptionFactoryTask.kt | 2 + .../laboratory/gradle/PackageNameProvider.kt | 0 .../gradle/SourceSetContribution.kt | 24 +- .../gradle/SourcedFeatureStorageInput.kt | 6 +- .../gradle/SourcedFeatureStorageTask.kt | 8 +- .../gradle/GenerateFeatureFactoryTaskSpec.kt | 32 +- .../gradle/GenerateFeatureFlagsTaskSpec.kt | 62 +- .../GenerateFeatureSourceFactoryTaskSpec.kt | 32 +- .../gradle/GenerateSourcedStorageTaskSpec.kt | 34 +- .../laboratory/gradle/LaboratoryPluginSpec.kt | 102 ++-- .../gradle/OptionFactoryTaskSpec.kt | 40 +- .../mehow/laboratory/gradle/TestExtensions.kt | 8 +- .../factory-android-smoke/build.gradle | 0 .../factory-android-smoke}/settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../factory-generate-empty/build.gradle | 0 .../factory-generate-empty}/settings.gradle | 0 .../factory-generate-features/build.gradle | 0 .../settings.gradle | 0 .../factory-generate-internal/build.gradle | 0 .../settings.gradle | 0 .../factory-generate-public/build.gradle | 0 .../factory-generate-public}/settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../feature-a/build.gradle | 0 .../feature-b/build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../feature-a/build.gradle | 0 .../feature-b/build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../feature-flag-android-smoke/build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../feature-flag-generate-public/build.gradle | 0 .../settings.gradle | 0 .../feature-flag-generate-single/build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../feature-flag-option-missing/build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../option-factory-android-smoke/build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../option-factory-duplicate-key/build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../option-factory-generate-fqcn/build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../option-factory-generate-key/build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../feature-a/build.gradle | 0 .../feature-b/build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../feature-a/build.gradle | 0 .../feature-b/build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../feature-a/build.gradle | 0 .../feature-b/build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../feature-a/build.gradle | 0 .../feature-b/build.gradle | 0 .../settings.gradle | 0 .../option-factory-no-option/build.gradle | 0 .../option-factory-no-option}/settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../no-plugin/build.gradle | 0 .../settings.gradle | 0 .../plugin-factory-missing/build.gradle | 0 .../plugin-factory-missing}/settings.gradle | 0 .../plugin-factory-present/build.gradle | 0 .../plugin-factory-present}/settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../plugin-kotlin-missing/build.gradle | 0 .../plugin-kotlin-missing}/settings.gradle | 0 .../plugin-kotlin-present/build.gradle | 0 .../plugin-kotlin-present}/settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../source-factory-android-smoke/build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../feature-a/build.gradle | 0 .../feature-b/build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../feature-a/build.gradle | 0 .../feature-b/build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../feature-a/build.gradle | 0 .../feature-b/build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../feature-a/build.gradle | 0 .../feature-b/build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../build.gradle | 0 .../settings.gradle | 0 .../hyperion-plugin/api/hyperion-plugin.api | 0 laboratory/hyperion-plugin/build.gradle.kts | 21 + .../hyperion-plugin/gradle.properties | 2 +- .../io/mehow/laboratory/hyperion/Plugin.kt | 1 + .../mehow/laboratory/hyperion/PluginModule.kt | 5 +- .../res/drawable/io_mehow_laboratory_icon.xml | 0 .../io_mehow_laboratory_plugin_item.xml | 0 .../src/main/res/values/public.xml | 0 .../src/main/res/values/strings.xml | 0 .../inspector/api/inspector.api | 2 + laboratory/inspector/build.gradle.kts | 39 ++ .../inspector/gradle.properties | 2 +- .../io-mehow-laboratory-inspector.pro | 0 .../inspector/src/main/AndroidManifest.xml | 0 .../inspector/DeprecationAlignment.kt | 0 .../inspector/DeprecationHandler.kt | 0 .../inspector/DeprecationPhenotype.kt | 0 .../laboratory/inspector/FeatureAdapter.kt | 25 +- .../inspector/FeatureCoordinates.kt | 0 .../laboratory/inspector/FeatureUiModel.kt | 10 +- .../laboratory/inspector/FeatureViewHolder.kt | 0 .../mehow/laboratory/inspector/Fragments.kt | 0 .../inspector/InspectorViewModel.kt | 72 +-- .../mehow/laboratory/inspector/Iterables.kt | 12 +- .../inspector/LaboratoryActivity.kt | 45 +- .../laboratory/inspector/OptionUiModel.kt | 0 .../laboratory/inspector/OptionViewGroup.kt | 101 ++++ .../mehow/laboratory/inspector/SearchMode.kt | 1 - .../mehow/laboratory/inspector/SearchQuery.kt | 6 +- .../laboratory/inspector/SearchViewModel.kt | 6 +- .../laboratory/inspector/SectionAdapter.kt | 0 .../laboratory/inspector/SectionFragment.kt | 9 +- .../SmoothScrollingLinearLayoutManager.kt | 0 .../laboratory/inspector/SourceAdapter.kt | 12 +- .../laboratory/inspector/SourceViewGroup.kt | 61 ++ .../laboratory/inspector/SourceViewHolder.kt | 0 .../mehow/laboratory/inspector/TextToken.kt | 8 +- .../laboratory/inspector/ToolbarBinding.kt | 26 +- .../io/mehow/laboratory/inspector/Views.kt | 18 +- .../io_mehow_laboratory_chip_background.xml | 0 .../color/io_mehow_laboratory_chip_text.xml | 0 .../color/io_mehow_laboratory_hint_text.xml | 0 .../drawable/io_mehow_laboratory_clear.xml | 0 .../io_mehow_laboratory_close_serach.xml | 0 .../io_mehow_laboratory_hint_cursor.xml | 0 .../io_mehow_laboratory_open_serach.xml | 0 .../io_mehow_laboratory_reset_features.xml | 0 .../io_mehow_laboratory_supervisor.xml | 0 .../io_mehow_laboratory_feature_group.xml | 0 .../io_mehow_laboratory_feature_item.xml | 0 ...o_mehow_laboratory_feature_option_chip.xml | 0 ...boratory_feature_source_drop_down_item.xml | 0 ...laboratory_feature_source_spinner_item.xml | 0 .../layout/io_mehow_laboratory_inspector.xml | 0 .../src/main/res/values-night/dimens.xml | 0 .../src/main/res/values-night/themes.xml | 0 .../inspector/src/main/res/values/dimens.xml | 0 .../inspector/src/main/res/values/public.xml | 0 .../inspector/src/main/res/values/strings.xml | 0 .../inspector/src/main/res/values/styles.xml | 0 .../inspector/src/main/res/values/themes.xml | 0 .../laboratory/inspector/FlowTurbines.kt | 6 +- .../InspectorViewModelDeprecationSpec.kt | 174 ++++++ .../InspectorViewModelFeatureSpec.kt | 318 +++++++++++ .../inspector/InspectorViewModelFilterSpec.kt | 260 +++++++++ .../InspectorViewModelNavigationSpec.kt | 125 ++++ .../inspector/InspectorViewModels.kt | 11 +- .../inspector/SearchViewModelSpec.kt | 88 +++ .../inspector/TestConfigurations.kt | 2 +- .../laboratory/inspector/TextTokenSpec.kt | 77 +++ .../runtime/api/runtime.api | 0 laboratory/runtime/build.gradle.kts | 19 + .../runtime}/gradle.properties | 0 .../io/mehow/laboratory/BlockingIoCall.kt | 6 +- .../io/mehow/laboratory/BlockingLaboratory.kt | 0 .../mehow/laboratory/DefaultOptionFactory.kt | 0 .../kotlin}/io/mehow/laboratory/Feature.kt | 0 .../io/mehow/laboratory/FeatureFactory.kt | 0 .../io/mehow/laboratory/FeatureStorage.kt | 1 + .../laboratory/InMemoryFeatureStorage.kt | 4 +- .../kotlin}/io/mehow/laboratory/Laboratory.kt | 11 +- .../io/mehow/laboratory/OptionFactory.kt | 11 +- .../laboratory/SafeDefaultOptionFactory.kt | 0 .../mehow/laboratory/SourcedFeatureStorage.kt | 36 +- .../META-INF/proguard/io-mehow-laboratory.pro | 0 .../laboratory/DefaultOptionFactorySpec.kt | 63 +++ .../io/mehow/laboratory/FeatureFactorySpec.kt | 41 ++ .../io/mehow/laboratory/LaboratorySpec.kt | 188 ++++++ .../io/mehow/laboratory/OptionFactorySpec.kt | 61 ++ .../laboratory/ParentChildFeatureSpec.kt | 119 ++++ .../laboratory/SourcedFeatureStorageSpec.kt | 285 ++++++++++ .../api/shared-preferences.api | 0 .../shared-preferences/build.gradle.kts | 39 ++ .../shared-preferences/gradle.properties | 3 + .../SharedPreferencesFeaturesStorageTest.kt | 54 +- .../SharedPreferencesFeatureStorage.kt | 4 +- library/build.gradle | 140 ----- library/buildSrc/build.gradle.kts | 3 - library/buildSrc/settings.gradle.kts | 6 - .../buildSrc/src/main/kotlin/JavaConfig.kt | 6 - library/data-store/build.gradle | 38 -- .../datastore/FeatureFlagsSerializerSpec.kt | 31 - library/detekt-config.yml | 311 ---------- library/generator/build.gradle | 24 - .../mehow/laboratory/generator/ClassNames.kt | 12 - .../SourcedFeatureStorageGenerator.kt | 131 ----- .../generator/FeatureFactoryGeneratorSpec.kt | 118 ---- .../generator/FeatureFlagGeneratorSpec.kt | 534 ------------------ .../generator/OptionFactoryModelSpec.kt | 306 ---------- .../SourcedFeatureStorageGeneratorSpec.kt | 266 --------- .../laboratory/generator/SupervisorSpec.kt | 44 -- .../generator/test/KotlinPoetMatchers.kt | 6 - library/gradle-plugin/build.gradle | 69 --- library/gradle.properties | 29 - library/gradle/dependencies.toml | 73 --- library/gradle/dokka-config.gradle | 14 - library/gradle/gradle-mvn-push.gradle | 6 - library/hyperion-plugin/build.gradle | 19 - .../src/main/AndroidManifest.xml | 3 - library/inspector/build.gradle | 34 -- .../laboratory/inspector/OptionViewGroup.kt | 90 --- .../laboratory/inspector/SourceViewGroup.kt | 54 -- .../InspectorModelDeprecationSpec.kt | 158 ------ .../inspector/InspectorModelFeatureSpec.kt | 321 ----------- .../inspector/InspectorModelFilterSpec.kt | 255 --------- .../inspector/InspectorModelNavigationSpec.kt | 128 ----- .../inspector/SearchViewModelSpec.kt | 90 --- .../laboratory/inspector/TextTokenSpec.kt | 77 --- library/laboratory/build.gradle | 16 - .../laboratory/DefaultOptionFactorySpec.kt | 39 -- .../io/mehow/laboratory/FeatureFactorySpec.kt | 24 - .../test/java/io/mehow/laboratory/Features.kt | 120 ---- .../io/mehow/laboratory/LaboratorySpec.kt | 195 ------- .../io/mehow/laboratory/OptionFactorySpec.kt | 39 -- .../laboratory/ParentChildFeatureSpec.kt | 100 ---- .../laboratory/SourcedFeatureStorageSpec.kt | 221 -------- library/settings.gradle.kts | 41 -- library/shared-preferences/build.gradle | 42 -- library/shared-preferences/gradle.properties | 3 - library/lint.xml => lint.xml | 7 +- library/mkdocs.yml => mkdocs.yml | 9 +- .../prepare-release.sh => prepare-release.sh | 2 +- samples/.editorconfig | 1 + samples/basic/build.gradle | 89 --- samples/basic/build.gradle.kts | 49 ++ .../mehow/laboratory/sample/basic/Activity.kt | 7 +- samples/build.gradle.kts | 139 +++++ samples/ci-check/build.gradle | 29 - samples/ci-check/src/main/kotlin/main.kt | 7 - samples/default-option/build.gradle | 81 --- samples/default-option/build.gradle.kts | 40 ++ .../defaultoption/DefaultOptionFactory.kt | 4 +- .../sample/defaultoption/Activity.kt | 6 +- .../sample/defaultoption/Application.kt | 6 +- samples/detekt.yml | 1 + samples/firebase/build.gradle | 101 ---- samples/firebase/google-services.json | 47 -- samples/firebase/src/main/AndroidManifest.xml | 24 - .../laboratory/sample/firebase/Activity.kt | 42 -- .../laboratory/sample/firebase/Application.kt | 50 -- .../sample/firebase/FirebaseSynchronizer.kt | 46 -- samples/firebase/src/main/res/layout/main.xml | 43 -- samples/gradle.properties | 5 + .../gradle/wrapper/gradle-wrapper.jar | 0 .../gradle/wrapper/gradle-wrapper.properties | 0 {library => samples}/gradlew | 0 {library => samples}/gradlew.bat | 0 samples/multi-module/build.gradle | 71 --- samples/multi-module/build.gradle.kts | 31 + .../{build.gradle => build.gradle.kts} | 14 +- .../{build.gradle => build.gradle.kts} | 14 +- .../multi-module/multi-module-c/build.gradle | 59 -- .../multi-module-c/build.gradle.kts | 53 ++ .../laboratory/sample/multimodule/Activity.kt | 6 +- .../sample/multimodule/Application.kt | 6 +- samples/settings.gradle.kts | 51 ++ samples/supervision/build.gradle | 105 ---- samples/supervision/build.gradle.kts | 65 +++ .../laboratory/sample/supervision/Activity.kt | 8 +- settings.gradle.kts | 39 +- 477 files changed, 5142 insertions(+), 6179 deletions(-) create mode 100644 .editorconfig delete mode 100644 .idea/codeStyles/Project.xml delete mode 100644 .idea/codeStyles/codeStyleConfig.xml rename {library/.mkdocs-theme => .mkdocs-theme}/404.html (94%) rename library/gradle-plugin/src/test/projects/factory-android-smoke/settings.gradle => build-support/build.gradle.kts (100%) create mode 100644 build-support/settings.gradle.kts delete mode 100644 buildSrc/build.gradle.kts delete mode 100644 buildSrc/settings.gradle.kts delete mode 100644 buildSrc/src/main/kotlin/JavaConfig.kt create mode 100644 detekt.yml rename {library/docs => docs}/changelog.md (100%) rename {library/docs => docs}/css/site.css (100%) rename {library/docs => docs}/gradle-plugin.md (100%) rename {library/docs => docs}/images/hyperion_screenshot.jpg (100%) rename {library/docs => docs}/images/inspector_screenshot.jpg (100%) rename {library/docs => docs}/images/laboratory_logo.ico (100%) rename {library/docs => docs}/images/laboratory_logo.svg (100%) rename {library/docs => docs}/images/laboratory_logo_menu.svg (100%) rename {library/docs => docs}/index.md (100%) rename {library/docs => docs}/qa-module.md (100%) rename {library/docs => docs}/releasing.md (100%) rename {library/docs => docs}/requirements.txt (100%) rename {library/docs => docs}/user-guide.md (100%) create mode 100644 gradle/libs.versions.toml rename {library => laboratory}/data-store/api/data-store.api (100%) create mode 100644 laboratory/data-store/build.gradle.kts rename {library => laboratory}/data-store/gradle.properties (62%) rename {library/data-store/src/main/java => laboratory/data-store/src/main/kotlin}/io/mehow/laboratory/datastore/DataStoreFeatureStorage.kt (96%) rename {library/data-store/src/main/java => laboratory/data-store/src/main/kotlin}/io/mehow/laboratory/datastore/FeatureFlagsSerializer.kt (87%) rename {library => laboratory}/data-store/src/main/proto/io/mehow/laboratory/datastore/feature_flags.proto (68%) rename {library/data-store/src/test/java => laboratory/data-store/src/test/kotlin}/io/mehow/laboratory/datastore/DataStoreFeatureStorageSpec.kt (85%) rename {library/data-store/src/test/java => laboratory/data-store/src/test/kotlin}/io/mehow/laboratory/datastore/FeatureA.kt (71%) create mode 100644 laboratory/data-store/src/test/kotlin/io/mehow/laboratory/datastore/FeatureFlagsSerializerSpec.kt rename {library => laboratory}/generator/api/generator.api (98%) create mode 100644 laboratory/generator/build.gradle.kts rename {library => laboratory}/generator/gradle.properties (63%) create mode 100644 laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/ClassNames.kt rename {library/generator/src/main/java => laboratory/generator/src/main/kotlin}/io/mehow/laboratory/generator/Deprecation.kt (91%) rename {library/generator/src/main/java => laboratory/generator/src/main/kotlin}/io/mehow/laboratory/generator/FeatureFactoryGenerator.kt (60%) rename {library/generator/src/main/java => laboratory/generator/src/main/kotlin}/io/mehow/laboratory/generator/FeatureFactoryModel.kt (100%) rename {library/generator/src/main/java => laboratory/generator/src/main/kotlin}/io/mehow/laboratory/generator/FeatureFlagGenerator.kt (62%) rename {library/generator/src/main/java => laboratory/generator/src/main/kotlin}/io/mehow/laboratory/generator/FeatureFlagModel.kt (84%) rename {library/generator/src/main/java => laboratory/generator/src/main/kotlin}/io/mehow/laboratory/generator/FeatureFlagOption.kt (100%) rename {library/generator/src/main/java => laboratory/generator/src/main/kotlin}/io/mehow/laboratory/generator/OptionFactoryModel.kt (52%) create mode 100644 laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/SourcedFeatureStorageGenerator.kt rename {library/generator/src/main/java => laboratory/generator/src/main/kotlin}/io/mehow/laboratory/generator/SourcedFeatureStorageModel.kt (100%) rename {library/generator/src/main/java => laboratory/generator/src/main/kotlin}/io/mehow/laboratory/generator/Supervisor.kt (100%) rename {library/generator/src/main/java => laboratory/generator/src/main/kotlin}/io/mehow/laboratory/generator/Visibility.kt (92%) create mode 100644 laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/FeatureFactoryModelSpec.kt create mode 100644 laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/FeatureFlagModelSpec.kt create mode 100644 laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/OptionFactoryModelSpec.kt create mode 100644 laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/SourcedFeatureStorageModelSpec.kt create mode 100644 laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/SupervisorSpec.kt create mode 100644 laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/test/KotlinPoetMatchers.kt rename {library => laboratory}/gradle-plugin/api/gradle-plugin.api (98%) create mode 100644 laboratory/gradle-plugin/build.gradle.kts rename {library => laboratory}/gradle-plugin/gradle.properties (62%) rename {library/gradle-plugin/src/main/java => laboratory/gradle-plugin/src/main/kotlin}/io/mehow/laboratory/gradle/ChildFeatureFlagsInput.kt (90%) rename {library/gradle-plugin/src/main/java => laboratory/gradle-plugin/src/main/kotlin}/io/mehow/laboratory/gradle/DeprecationLevel.kt (100%) rename {library/gradle-plugin/src/main/java => laboratory/gradle-plugin/src/main/kotlin}/io/mehow/laboratory/gradle/FeatureFactoryInput.kt (73%) rename {library/gradle-plugin/src/main/java => laboratory/gradle-plugin/src/main/kotlin}/io/mehow/laboratory/gradle/FeatureFactoryTask.kt (99%) rename {library/gradle-plugin/src/main/java => laboratory/gradle-plugin/src/main/kotlin}/io/mehow/laboratory/gradle/FeatureFlagInput.kt (82%) rename {library/gradle-plugin/src/main/java => laboratory/gradle-plugin/src/main/kotlin}/io/mehow/laboratory/gradle/FeatureFlagsTask.kt (99%) rename {library/gradle-plugin/src/main/java => laboratory/gradle-plugin/src/main/kotlin}/io/mehow/laboratory/gradle/LaboratoryExtension.kt (97%) rename {library/gradle-plugin/src/main/java => laboratory/gradle-plugin/src/main/kotlin}/io/mehow/laboratory/gradle/LaboratoryPlugin.kt (84%) rename {library/gradle-plugin/src/main/java => laboratory/gradle-plugin/src/main/kotlin}/io/mehow/laboratory/gradle/OptionFactoryInput.kt (83%) rename {library/gradle-plugin/src/main/java => laboratory/gradle-plugin/src/main/kotlin}/io/mehow/laboratory/gradle/OptionFactoryTask.kt (99%) rename {library/gradle-plugin/src/main/java => laboratory/gradle-plugin/src/main/kotlin}/io/mehow/laboratory/gradle/PackageNameProvider.kt (100%) rename {library/gradle-plugin/src/main/java => laboratory/gradle-plugin/src/main/kotlin}/io/mehow/laboratory/gradle/SourceSetContribution.kt (84%) rename {library/gradle-plugin/src/main/java => laboratory/gradle-plugin/src/main/kotlin}/io/mehow/laboratory/gradle/SourcedFeatureStorageInput.kt (81%) rename {library/gradle-plugin/src/main/java => laboratory/gradle-plugin/src/main/kotlin}/io/mehow/laboratory/gradle/SourcedFeatureStorageTask.kt (89%) rename {library/gradle-plugin/src/test/java => laboratory/gradle-plugin/src/test/kotlin}/io/mehow/laboratory/gradle/GenerateFeatureFactoryTaskSpec.kt (89%) rename {library/gradle-plugin/src/test/java => laboratory/gradle-plugin/src/test/kotlin}/io/mehow/laboratory/gradle/GenerateFeatureFlagsTaskSpec.kt (90%) rename {library/gradle-plugin/src/test/java => laboratory/gradle-plugin/src/test/kotlin}/io/mehow/laboratory/gradle/GenerateFeatureSourceFactoryTaskSpec.kt (90%) rename {library/gradle-plugin/src/test/java => laboratory/gradle-plugin/src/test/kotlin}/io/mehow/laboratory/gradle/GenerateSourcedStorageTaskSpec.kt (93%) rename {library/gradle-plugin/src/test/java => laboratory/gradle-plugin/src/test/kotlin}/io/mehow/laboratory/gradle/LaboratoryPluginSpec.kt (57%) rename {library/gradle-plugin/src/test/java => laboratory/gradle-plugin/src/test/kotlin}/io/mehow/laboratory/gradle/OptionFactoryTaskSpec.kt (89%) rename {library/gradle-plugin/src/test/java => laboratory/gradle-plugin/src/test/kotlin}/io/mehow/laboratory/gradle/TestExtensions.kt (86%) rename {library => laboratory}/gradle-plugin/src/test/projects/factory-android-smoke/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/factory-feature-flag-option-missing => laboratory/gradle-plugin/src/test/projects/factory-android-smoke}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/factory-feature-flag-option-missing/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/factory-generate-empty => laboratory/gradle-plugin/src/test/projects/factory-feature-flag-option-missing}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/factory-generate-empty/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/factory-generate-features => laboratory/gradle-plugin/src/test/projects/factory-generate-empty}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/factory-generate-features/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/factory-generate-internal => laboratory/gradle-plugin/src/test/projects/factory-generate-features}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/factory-generate-internal/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/factory-generate-public => laboratory/gradle-plugin/src/test/projects/factory-generate-internal}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/factory-generate-public/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/factory-generate-supervised-feature-flags => laboratory/gradle-plugin/src/test/projects/factory-generate-public}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/factory-generate-supervised-feature-flags/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/factory-package-name-explicit-override => laboratory/gradle-plugin/src/test/projects/factory-generate-supervised-feature-flags}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/factory-multi-module-generate-all/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/factory-multi-module-generate-all/feature-a/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/factory-multi-module-generate-all/feature-b/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/factory-multi-module-generate-all/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/feature-a/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/feature-b/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/factory-package-name-explicit-override/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/factory-package-name-explicit => laboratory/gradle-plugin/src/test/projects/factory-package-name-explicit-override}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/factory-package-name-explicit/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/factory-package-name-implicit => laboratory/gradle-plugin/src/test/projects/factory-package-name-explicit}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/factory-package-name-implicit/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-android-smoke => laboratory/gradle-plugin/src/test/projects/factory-package-name-implicit}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-android-smoke/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-generate-deprecated-with-level => laboratory/gradle-plugin/src/test/projects/feature-flag-android-smoke}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-generate-deprecated-with-level/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-generate-deprecated => laboratory/gradle-plugin/src/test/projects/feature-flag-generate-deprecated-with-level}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-generate-deprecated/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-generate-description => laboratory/gradle-plugin/src/test/projects/feature-flag-generate-deprecated}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-generate-description/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-generate-internal => laboratory/gradle-plugin/src/test/projects/feature-flag-generate-description}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-generate-internal/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-generate-multiple => laboratory/gradle-plugin/src/test/projects/feature-flag-generate-internal}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-generate-multiple/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-generate-option-name-common => laboratory/gradle-plugin/src/test/projects/feature-flag-generate-multiple}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-generate-option-name-common/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-generate-public => laboratory/gradle-plugin/src/test/projects/feature-flag-generate-option-name-common}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-generate-public/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-generate-single => laboratory/gradle-plugin/src/test/projects/feature-flag-generate-public}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-generate-single/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-generate-sources-internal => laboratory/gradle-plugin/src/test/projects/feature-flag-generate-single}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-generate-sources-internal/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-generate-sources-multiple => laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-internal}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-generate-sources-multiple/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-generate-sources-public => laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-multiple}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-generate-sources-public/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-generate-sources-single => laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-public}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-generate-sources-single/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-option-missing => laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-single}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-option-missing/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-override => laboratory/gradle-plugin/src/test/projects/feature-flag-option-missing}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-override/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-switching => laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-override}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-switching/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit => laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-switching}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-package-name-explicit/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-cascading => laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-cascading/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-switching => laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-cascading}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-switching/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit => laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-switching}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-package-name-implicit/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-child => laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-child/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-grandchild => laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-child}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-grandchild/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-multiple-children => laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-grandchild}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-multiple-children/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit-override => laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-multiple-children}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit-override/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit => laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit-override}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-implicit => laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-implicit/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/feature-flag-supervisor-self-supervision => laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-implicit}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/feature-flag-supervisor-self-supervision/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/option-factory-android-smoke => laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-self-supervision}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-android-smoke/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/option-factory-duplicate-key-fqcn => laboratory/gradle-plugin/src/test/projects/option-factory-android-smoke}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-duplicate-key-fqcn/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/option-factory-duplicate-key => laboratory/gradle-plugin/src/test/projects/option-factory-duplicate-key-fqcn}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-duplicate-key/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/option-factory-generate-empty => laboratory/gradle-plugin/src/test/projects/option-factory-duplicate-key}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-generate-empty/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/option-factory-generate-fqcn => laboratory/gradle-plugin/src/test/projects/option-factory-generate-empty}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-generate-fqcn/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/option-factory-generate-internal => laboratory/gradle-plugin/src/test/projects/option-factory-generate-fqcn}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-generate-internal/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/option-factory-generate-key => laboratory/gradle-plugin/src/test/projects/option-factory-generate-internal}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-generate-key/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/option-factory-generate-public => laboratory/gradle-plugin/src/test/projects/option-factory-generate-key}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-generate-public/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/option-factory-no-option => laboratory/gradle-plugin/src/test/projects/option-factory-generate-public}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/feature-a/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/feature-b/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/feature-a/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/feature-b/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/feature-a/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/feature-b/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/feature-a/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/feature-b/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-no-option/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/option-factory-package-name-explicit-override => laboratory/gradle-plugin/src/test/projects/option-factory-no-option}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-package-name-explicit-override/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/option-factory-package-name-explicit => laboratory/gradle-plugin/src/test/projects/option-factory-package-name-explicit-override}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-package-name-explicit/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/option-factory-package-name-implicit => laboratory/gradle-plugin/src/test/projects/option-factory-package-name-explicit}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/option-factory-package-name-implicit/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/plugin-factory-missing => laboratory/gradle-plugin/src/test/projects/option-factory-package-name-implicit}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/no-plugin/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/plugin-factory-missing/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/plugin-factory-present => laboratory/gradle-plugin/src/test/projects/plugin-factory-missing}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/plugin-factory-present/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/plugin-kotlin-android-missing => laboratory/gradle-plugin/src/test/projects/plugin-factory-present}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/plugin-kotlin-android-missing/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/plugin-kotlin-android-present => laboratory/gradle-plugin/src/test/projects/plugin-kotlin-android-missing}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/plugin-kotlin-android-present/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/plugin-kotlin-missing => laboratory/gradle-plugin/src/test/projects/plugin-kotlin-android-present}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/plugin-kotlin-missing/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/plugin-kotlin-present => laboratory/gradle-plugin/src/test/projects/plugin-kotlin-missing}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/plugin-kotlin-present/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/plugin-option-factory-missing => laboratory/gradle-plugin/src/test/projects/plugin-kotlin-present}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/plugin-option-factory-missing/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/plugin-option-factory-present => laboratory/gradle-plugin/src/test/projects/plugin-option-factory-missing}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/plugin-option-factory-present/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/plugin-source-factory-missing => laboratory/gradle-plugin/src/test/projects/plugin-option-factory-present}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/plugin-source-factory-missing/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/plugin-source-factory-present => laboratory/gradle-plugin/src/test/projects/plugin-source-factory-missing}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/plugin-source-factory-present/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/plugin-sourced-storage-missing => laboratory/gradle-plugin/src/test/projects/plugin-source-factory-present}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/plugin-sourced-storage-missing/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/plugin-sourced-storage-present => laboratory/gradle-plugin/src/test/projects/plugin-sourced-storage-missing}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/plugin-sourced-storage-present/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/source-factory-android-smoke => laboratory/gradle-plugin/src/test/projects/plugin-sourced-storage-present}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/source-factory-android-smoke/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/source-factory-feature-flag-option-missing => laboratory/gradle-plugin/src/test/projects/source-factory-android-smoke}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/source-factory-feature-flag-option-missing/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/source-factory-generate-empty => laboratory/gradle-plugin/src/test/projects/source-factory-feature-flag-option-missing}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/source-factory-generate-empty/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/source-factory-generate-feature-flags => laboratory/gradle-plugin/src/test/projects/source-factory-generate-empty}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/source-factory-generate-feature-flags/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/source-factory-generate-internal => laboratory/gradle-plugin/src/test/projects/source-factory-generate-feature-flags}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/source-factory-generate-internal/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/source-factory-generate-public => laboratory/gradle-plugin/src/test/projects/source-factory-generate-internal}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/source-factory-generate-public/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/source-factory-generate-supervised-feature-flags => laboratory/gradle-plugin/src/test/projects/source-factory-generate-public}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/source-factory-generate-supervised-feature-flags/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/source-factory-package-name-explicit-override => laboratory/gradle-plugin/src/test/projects/source-factory-generate-supervised-feature-flags}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/feature-a/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/feature-b/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/feature-a/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/feature-b/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/source-factory-package-name-explicit-override/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/source-factory-package-name-explicit => laboratory/gradle-plugin/src/test/projects/source-factory-package-name-explicit-override}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/source-factory-package-name-explicit/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/source-factory-package-name-implicit => laboratory/gradle-plugin/src/test/projects/source-factory-package-name-explicit}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/source-factory-package-name-implicit/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/sourced-storage-android-smoke => laboratory/gradle-plugin/src/test/projects/source-factory-package-name-implicit}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/sourced-storage-android-smoke/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/sourced-storage-feature-flag-option-missing => laboratory/gradle-plugin/src/test/projects/sourced-storage-android-smoke}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/sourced-storage-feature-flag-option-missing/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/sourced-storage-generate-internal => laboratory/gradle-plugin/src/test/projects/sourced-storage-feature-flag-option-missing}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/sourced-storage-generate-internal/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/sourced-storage-generate-local-ignore => laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-internal}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/sourced-storage-generate-local-ignore/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/sourced-storage-generate-local => laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-local-ignore}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/sourced-storage-generate-local/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/sourced-storage-generate-public => laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-local}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/sourced-storage-generate-public/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/sourced-storage-generate-sources => laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-public}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/sourced-storage-generate-sources/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/sourced-storage-generate-supervised-feature-flags => laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-sources}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/sourced-storage-generate-supervised-feature-flags/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit-override => laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-supervised-feature-flags}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/feature-a/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/feature-b/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/feature-a/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/feature-b/build.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit-override/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit => laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit-override}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit/build.gradle (100%) rename {library/gradle-plugin/src/test/projects/sourced-storage-package-name-implicit => laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit}/settings.gradle (100%) rename {library => laboratory}/gradle-plugin/src/test/projects/sourced-storage-package-name-implicit/build.gradle (100%) rename library/hyperion-plugin/api/hyperion-plugin.api => laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-implicit/settings.gradle (100%) create mode 100644 laboratory/hyperion-plugin/api/hyperion-plugin.api create mode 100644 laboratory/hyperion-plugin/build.gradle.kts rename {library => laboratory}/hyperion-plugin/gradle.properties (61%) rename {library/hyperion-plugin/src/main/java => laboratory/hyperion-plugin/src/main/kotlin}/io/mehow/laboratory/hyperion/Plugin.kt (99%) rename {library/hyperion-plugin/src/main/java => laboratory/hyperion-plugin/src/main/kotlin}/io/mehow/laboratory/hyperion/PluginModule.kt (84%) rename {library => laboratory}/hyperion-plugin/src/main/res/drawable/io_mehow_laboratory_icon.xml (100%) rename {library => laboratory}/hyperion-plugin/src/main/res/layout/io_mehow_laboratory_plugin_item.xml (100%) rename {library => laboratory}/hyperion-plugin/src/main/res/values/public.xml (100%) rename {library => laboratory}/hyperion-plugin/src/main/res/values/strings.xml (100%) rename {library => laboratory}/inspector/api/inspector.api (97%) create mode 100644 laboratory/inspector/build.gradle.kts rename {library => laboratory}/inspector/gradle.properties (63%) rename {library => laboratory}/inspector/io-mehow-laboratory-inspector.pro (100%) rename {library => laboratory}/inspector/src/main/AndroidManifest.xml (100%) rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/DeprecationAlignment.kt (100%) rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/DeprecationHandler.kt (100%) rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/DeprecationPhenotype.kt (100%) rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/FeatureAdapter.kt (68%) rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/FeatureCoordinates.kt (100%) rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/FeatureUiModel.kt (89%) rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/FeatureViewHolder.kt (100%) rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/Fragments.kt (100%) rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/InspectorViewModel.kt (76%) rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/Iterables.kt (69%) rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/LaboratoryActivity.kt (91%) rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/OptionUiModel.kt (100%) create mode 100644 laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/OptionViewGroup.kt rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/SearchMode.kt (95%) rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/SearchQuery.kt (90%) rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/SearchViewModel.kt (94%) rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/SectionAdapter.kt (100%) rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/SectionFragment.kt (93%) rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/SmoothScrollingLinearLayoutManager.kt (100%) rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/SourceAdapter.kt (87%) create mode 100644 laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SourceViewGroup.kt rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/SourceViewHolder.kt (100%) rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/TextToken.kt (92%) rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/ToolbarBinding.kt (81%) rename {library/inspector/src/main/java => laboratory/inspector/src/main/kotlin}/io/mehow/laboratory/inspector/Views.kt (84%) rename {library => laboratory}/inspector/src/main/res/color/io_mehow_laboratory_chip_background.xml (100%) rename {library => laboratory}/inspector/src/main/res/color/io_mehow_laboratory_chip_text.xml (100%) rename {library => laboratory}/inspector/src/main/res/color/io_mehow_laboratory_hint_text.xml (100%) rename {library => laboratory}/inspector/src/main/res/drawable/io_mehow_laboratory_clear.xml (100%) rename {library => laboratory}/inspector/src/main/res/drawable/io_mehow_laboratory_close_serach.xml (100%) rename {library => laboratory}/inspector/src/main/res/drawable/io_mehow_laboratory_hint_cursor.xml (100%) rename {library => laboratory}/inspector/src/main/res/drawable/io_mehow_laboratory_open_serach.xml (100%) rename {library => laboratory}/inspector/src/main/res/drawable/io_mehow_laboratory_reset_features.xml (100%) rename {library => laboratory}/inspector/src/main/res/drawable/io_mehow_laboratory_supervisor.xml (100%) rename {library => laboratory}/inspector/src/main/res/layout/io_mehow_laboratory_feature_group.xml (100%) rename {library => laboratory}/inspector/src/main/res/layout/io_mehow_laboratory_feature_item.xml (100%) rename {library => laboratory}/inspector/src/main/res/layout/io_mehow_laboratory_feature_option_chip.xml (100%) rename {library => laboratory}/inspector/src/main/res/layout/io_mehow_laboratory_feature_source_drop_down_item.xml (100%) rename {library => laboratory}/inspector/src/main/res/layout/io_mehow_laboratory_feature_source_spinner_item.xml (100%) rename {library => laboratory}/inspector/src/main/res/layout/io_mehow_laboratory_inspector.xml (100%) rename {library => laboratory}/inspector/src/main/res/values-night/dimens.xml (100%) rename {library => laboratory}/inspector/src/main/res/values-night/themes.xml (100%) rename {library => laboratory}/inspector/src/main/res/values/dimens.xml (100%) rename {library => laboratory}/inspector/src/main/res/values/public.xml (100%) rename {library => laboratory}/inspector/src/main/res/values/strings.xml (100%) rename {library => laboratory}/inspector/src/main/res/values/styles.xml (100%) rename {library => laboratory}/inspector/src/main/res/values/themes.xml (100%) rename {library/inspector/src/test/java => laboratory/inspector/src/test/kotlin}/io/mehow/laboratory/inspector/FlowTurbines.kt (68%) create mode 100644 laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelDeprecationSpec.kt create mode 100644 laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelFeatureSpec.kt create mode 100644 laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelFilterSpec.kt create mode 100644 laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelNavigationSpec.kt rename {library/inspector/src/test/java => laboratory/inspector/src/test/kotlin}/io/mehow/laboratory/inspector/InspectorViewModels.kt (90%) create mode 100644 laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/SearchViewModelSpec.kt rename {library/inspector/src/test/java => laboratory/inspector/src/test/kotlin}/io/mehow/laboratory/inspector/TestConfigurations.kt (90%) create mode 100644 laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/TextTokenSpec.kt rename library/laboratory/api/laboratory.api => laboratory/runtime/api/runtime.api (100%) create mode 100644 laboratory/runtime/build.gradle.kts rename {library/laboratory => laboratory/runtime}/gradle.properties (100%) rename {library/laboratory/src/main/java => laboratory/runtime/src/main/kotlin}/io/mehow/laboratory/BlockingIoCall.kt (73%) rename {library/laboratory/src/main/java => laboratory/runtime/src/main/kotlin}/io/mehow/laboratory/BlockingLaboratory.kt (100%) rename {library/laboratory/src/main/java => laboratory/runtime/src/main/kotlin}/io/mehow/laboratory/DefaultOptionFactory.kt (100%) rename {library/laboratory/src/main/java => laboratory/runtime/src/main/kotlin}/io/mehow/laboratory/Feature.kt (100%) rename {library/laboratory/src/main/java => laboratory/runtime/src/main/kotlin}/io/mehow/laboratory/FeatureFactory.kt (100%) rename {library/laboratory/src/main/java => laboratory/runtime/src/main/kotlin}/io/mehow/laboratory/FeatureStorage.kt (97%) rename {library/laboratory/src/main/java => laboratory/runtime/src/main/kotlin}/io/mehow/laboratory/InMemoryFeatureStorage.kt (95%) rename {library/laboratory/src/main/java => laboratory/runtime/src/main/kotlin}/io/mehow/laboratory/Laboratory.kt (96%) rename {library/laboratory/src/main/java => laboratory/runtime/src/main/kotlin}/io/mehow/laboratory/OptionFactory.kt (69%) rename {library/laboratory/src/main/java => laboratory/runtime/src/main/kotlin}/io/mehow/laboratory/SafeDefaultOptionFactory.kt (100%) rename {library/laboratory/src/main/java => laboratory/runtime/src/main/kotlin}/io/mehow/laboratory/SourcedFeatureStorage.kt (68%) rename {library/laboratory => laboratory/runtime}/src/main/resources/META-INF/proguard/io-mehow-laboratory.pro (100%) create mode 100644 laboratory/runtime/src/test/kotlin/io/mehow/laboratory/DefaultOptionFactorySpec.kt create mode 100644 laboratory/runtime/src/test/kotlin/io/mehow/laboratory/FeatureFactorySpec.kt create mode 100644 laboratory/runtime/src/test/kotlin/io/mehow/laboratory/LaboratorySpec.kt create mode 100644 laboratory/runtime/src/test/kotlin/io/mehow/laboratory/OptionFactorySpec.kt create mode 100644 laboratory/runtime/src/test/kotlin/io/mehow/laboratory/ParentChildFeatureSpec.kt create mode 100644 laboratory/runtime/src/test/kotlin/io/mehow/laboratory/SourcedFeatureStorageSpec.kt rename {library => laboratory}/shared-preferences/api/shared-preferences.api (100%) create mode 100644 laboratory/shared-preferences/build.gradle.kts create mode 100644 laboratory/shared-preferences/gradle.properties rename {library/shared-preferences/src/androidTest/java => laboratory/shared-preferences/src/androidTest/kotlin}/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeaturesStorageTest.kt (61%) rename {library/shared-preferences/src/main/java => laboratory/shared-preferences/src/main/kotlin}/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeatureStorage.kt (95%) delete mode 100644 library/build.gradle delete mode 100644 library/buildSrc/build.gradle.kts delete mode 100644 library/buildSrc/settings.gradle.kts delete mode 100644 library/buildSrc/src/main/kotlin/JavaConfig.kt delete mode 100644 library/data-store/build.gradle delete mode 100644 library/data-store/src/test/java/io/mehow/laboratory/datastore/FeatureFlagsSerializerSpec.kt delete mode 100644 library/detekt-config.yml delete mode 100644 library/generator/build.gradle delete mode 100644 library/generator/src/main/java/io/mehow/laboratory/generator/ClassNames.kt delete mode 100644 library/generator/src/main/java/io/mehow/laboratory/generator/SourcedFeatureStorageGenerator.kt delete mode 100644 library/generator/src/test/java/io/mehow/laboratory/generator/FeatureFactoryGeneratorSpec.kt delete mode 100644 library/generator/src/test/java/io/mehow/laboratory/generator/FeatureFlagGeneratorSpec.kt delete mode 100644 library/generator/src/test/java/io/mehow/laboratory/generator/OptionFactoryModelSpec.kt delete mode 100644 library/generator/src/test/java/io/mehow/laboratory/generator/SourcedFeatureStorageGeneratorSpec.kt delete mode 100644 library/generator/src/test/java/io/mehow/laboratory/generator/SupervisorSpec.kt delete mode 100644 library/generator/src/test/java/io/mehow/laboratory/generator/test/KotlinPoetMatchers.kt delete mode 100644 library/gradle-plugin/build.gradle delete mode 100644 library/gradle.properties delete mode 100644 library/gradle/dependencies.toml delete mode 100644 library/gradle/dokka-config.gradle delete mode 100644 library/gradle/gradle-mvn-push.gradle delete mode 100644 library/hyperion-plugin/build.gradle delete mode 100644 library/hyperion-plugin/src/main/AndroidManifest.xml delete mode 100644 library/inspector/build.gradle delete mode 100644 library/inspector/src/main/java/io/mehow/laboratory/inspector/OptionViewGroup.kt delete mode 100644 library/inspector/src/main/java/io/mehow/laboratory/inspector/SourceViewGroup.kt delete mode 100644 library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelDeprecationSpec.kt delete mode 100644 library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelFeatureSpec.kt delete mode 100644 library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelFilterSpec.kt delete mode 100644 library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelNavigationSpec.kt delete mode 100644 library/inspector/src/test/java/io/mehow/laboratory/inspector/SearchViewModelSpec.kt delete mode 100644 library/inspector/src/test/java/io/mehow/laboratory/inspector/TextTokenSpec.kt delete mode 100644 library/laboratory/build.gradle delete mode 100644 library/laboratory/src/test/java/io/mehow/laboratory/DefaultOptionFactorySpec.kt delete mode 100644 library/laboratory/src/test/java/io/mehow/laboratory/FeatureFactorySpec.kt delete mode 100644 library/laboratory/src/test/java/io/mehow/laboratory/Features.kt delete mode 100644 library/laboratory/src/test/java/io/mehow/laboratory/LaboratorySpec.kt delete mode 100644 library/laboratory/src/test/java/io/mehow/laboratory/OptionFactorySpec.kt delete mode 100644 library/laboratory/src/test/java/io/mehow/laboratory/ParentChildFeatureSpec.kt delete mode 100644 library/laboratory/src/test/java/io/mehow/laboratory/SourcedFeatureStorageSpec.kt delete mode 100644 library/settings.gradle.kts delete mode 100644 library/shared-preferences/build.gradle delete mode 100644 library/shared-preferences/gradle.properties rename library/lint.xml => lint.xml (58%) rename library/mkdocs.yml => mkdocs.yml (75%) rename library/prepare-release.sh => prepare-release.sh (99%) create mode 120000 samples/.editorconfig delete mode 100644 samples/basic/build.gradle create mode 100644 samples/basic/build.gradle.kts create mode 100644 samples/build.gradle.kts delete mode 100644 samples/ci-check/build.gradle delete mode 100644 samples/ci-check/src/main/kotlin/main.kt delete mode 100644 samples/default-option/build.gradle create mode 100644 samples/default-option/build.gradle.kts create mode 120000 samples/detekt.yml delete mode 100644 samples/firebase/build.gradle delete mode 100644 samples/firebase/google-services.json delete mode 100644 samples/firebase/src/main/AndroidManifest.xml delete mode 100644 samples/firebase/src/main/kotlin/io/mehow/laboratory/sample/firebase/Activity.kt delete mode 100644 samples/firebase/src/main/kotlin/io/mehow/laboratory/sample/firebase/Application.kt delete mode 100644 samples/firebase/src/main/kotlin/io/mehow/laboratory/sample/firebase/FirebaseSynchronizer.kt delete mode 100644 samples/firebase/src/main/res/layout/main.xml create mode 100644 samples/gradle.properties rename {library => samples}/gradle/wrapper/gradle-wrapper.jar (100%) rename {library => samples}/gradle/wrapper/gradle-wrapper.properties (100%) rename {library => samples}/gradlew (100%) rename {library => samples}/gradlew.bat (100%) delete mode 100644 samples/multi-module/build.gradle create mode 100644 samples/multi-module/build.gradle.kts rename samples/multi-module/multi-module-a/{build.gradle => build.gradle.kts} (59%) rename samples/multi-module/multi-module-b/{build.gradle => build.gradle.kts} (72%) delete mode 100644 samples/multi-module/multi-module-c/build.gradle create mode 100644 samples/multi-module/multi-module-c/build.gradle.kts create mode 100644 samples/settings.gradle.kts delete mode 100644 samples/supervision/build.gradle create mode 100644 samples/supervision/build.gradle.kts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..94c348a9a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# https://editorconfig.org +root = true + +[*] +indent_size = 2 +indent_style = space +# Handled by Detekt, which supports @Suppress annotation +max_line_length = off + +[*.{kt,kts}] +# Different rules in Detekt +ktlint_standard_property-naming = disabled +# I don't like it +ktlint_standard_multiline-expression-wrapping = disabled + +# Don't allow any wildcard imports +ij_kotlin_packages_to_use_import_on_demand = unset + +# Prevent wildcard imports +ij_kotlin_name_count_to_use_star_import = 99 +ij_kotlin_name_count_to_use_star_import_for_members = 99 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9ca4c1c8d..1a4186d5a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,10 +2,8 @@ ## :pencil: Checklist - -- [ ] I updated the [changelog](https://github.com/MiSikora/laboratory/blob/trunk/library/docs/changelog.md). -- [ ] I updated the [documentation](https://github.com/MiSikora/laboratory/tree/trunk/library/docs). -- [ ] I updated the [sample](https://github.com/MiSikora/laboratory/tree/trunk/sample). - -## :crystal_ball: Next steps - + +- [ ] I updated the [changelog](https://github.com/MiSikora/laboratory/blob/trunk/docs/changelog.md). +- [ ] I updated the [documentation](https://github.com/MiSikora/laboratory/tree/trunk/docs). +- [ ] I updated the [samples](https://github.com/MiSikora/laboratory/tree/trunk/samples). +- \ No newline at end of file diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 49ef7bc81..40613384c 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -12,13 +12,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout latest code - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.4 - name: Configure JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4.2.1 with: distribution: zulu - java-version: 19 + java-version: 21 - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@v1.0.5 + uses: gradle/actions/wrapper-validation@v3.3.2 diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 05a718dd3..916a208a7 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -12,21 +12,21 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.4 - name: Configure JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4.2.1 with: distribution: zulu - java-version: 19 + java-version: 21 - name: Cache Gradle Dirs - uses: actions/cache@v3.0.11 + uses: actions/cache@v4.0.2 with: path: | ~/.gradle/caches/ ~/.gradle/wrapper/ - key: cache-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + key: cache-gradle-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }} restore-keys: cache-gradle- - name: Deploy Release @@ -34,19 +34,19 @@ jobs: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SONATYPE_NEXUS_SIGNING_KEY }} - run: ./gradlew -p library publishAllPublicationsToMavenCentral --no-configuration-cache --stacktrace + run: ./gradlew publish --no-configuration-cache --stacktrace - name: Stop Gradle run: ./gradlew --stop - name: Extract Release Notes id: release-notes - uses: ffurrer2/extract-release-notes@v1.16.0 + uses: ffurrer2/extract-release-notes@v2.2.0 with: - changelog_file: ./library/docs/changelog.md + changelog_file: ./docs/changelog.md - name: Create GitHub Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2.0.5 with: body: ${{ steps.release-notes.outputs.release_notes }} files: | diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml index 953fd775f..0a1e8bb45 100644 --- a/.github/workflows/publish-snapshot.yml +++ b/.github/workflows/publish-snapshot.yml @@ -12,41 +12,41 @@ jobs: runs-on: ubuntu-latest steps: - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.11.0 + uses: styfle/cancel-workflow-action@0.12.1 - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.4 - name: Configure JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4.2.1 with: distribution: zulu - java-version: 19 + java-version: 21 - name: Cache Gradle Dirs - uses: actions/cache@v3.0.11 + uses: actions/cache@v4.0.2 with: path: | ~/.gradle/caches/ ~/.gradle/wrapper/ - key: cache-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + key: cache-gradle-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }} restore-keys: cache-gradle- - name: Deploy Snapshot env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} - run: ./gradlew -p library publish --no-daemon --no-parallel --stacktrace + run: ./gradlew publish --stacktrace - name: Build HTML Docs - run: ./gradlew -p library dokkaHtml --stacktrace + run: ./gradlew dokkaHtmlMultiModule --stacktrace - name: Stop Gradle run: ./gradlew --stop - name: Publish Website - uses: mhausenblas/mkdocs-deploy-gh-pages@1.25 + uses: mhausenblas/mkdocs-deploy-gh-pages@1.26 env: CONFIG_FILE: ./library/mkdocs.yml GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REQUIREMENTS: ./library/docs/requirements.txt + REQUIREMENTS: ./docs/requirements.txt diff --git a/.github/workflows/quality-check.yml b/.github/workflows/quality-check.yml index 0b2f1d66c..177c62c8f 100644 --- a/.github/workflows/quality-check.yml +++ b/.github/workflows/quality-check.yml @@ -17,34 +17,37 @@ jobs: android-tests: if: ${{ github.repository == 'MiSikora/laboratory' }} name: Android Tests - runs-on: macos-latest + runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.4 - name: Configure JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4.2.1 with: distribution: zulu - java-version: 19 + java-version: 21 - name: Cache Gradle Dirs - uses: actions/cache@v3.0.11 + uses: actions/cache@v4.0.2 with: path: | ~/.gradle/caches/ ~/.gradle/wrapper/ - key: cache-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + key: cache-gradle-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }} restore-keys: cache-gradle- + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - name: Run Tests - uses: reactivecircus/android-emulator-runner@v2.27.0 + uses: reactivecircus/android-emulator-runner@v2.30.1 with: api-level: 29 - emulator-build: 6110076 - script: ./gradlew -p library connectedCheck --stacktrace - env: - API_LEVEL: 29 + script: ./gradlew connectedCheck --stacktrace - name: Stop Gradle run: ./gradlew --stop @@ -55,83 +58,91 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.4 - name: Configure JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4.2.1 with: distribution: zulu - java-version: 19 + java-version: 21 - name: Cache Gradle Dirs - uses: actions/cache@v3.0.11 + uses: actions/cache@v4.0.2 with: path: | ~/.gradle/caches/ ~/.gradle/wrapper/ - key: cache-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + key: cache-gradle-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }} restore-keys: cache-gradle- - name: Run Tests - run: ./gradlew -p library test --stacktrace + run: ./gradlew test --stacktrace - name: Stop Gradle run: ./gradlew --stop - build-sample: + detekt: if: ${{ github.repository == 'MiSikora/laboratory' }} - name: Build sample + name: Detekt runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.4 - name: Configure JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4.2.1 with: distribution: zulu - java-version: 19 + java-version: 21 - name: Cache Gradle Dirs - uses: actions/cache@v3.0.11 + uses: actions/cache@v4.0.2 with: path: | ~/.gradle/caches/ ~/.gradle/wrapper/ - key: cache-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + key: cache-gradle-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }} restore-keys: cache-gradle- - - name: Assemble project - run: ./gradlew :samples:ci-check:assemble --stacktrace + - name: Run Detekt + run: ./gradlew detekt --stacktrace + + - name: Run Samples Detekt + working-directory: ./samples + run: ./gradlew detekt - name: Stop Gradle run: ./gradlew --stop - detekt: + spotless: if: ${{ github.repository == 'MiSikora/laboratory' }} - name: Detekt + name: Spotless runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.4 - name: Configure JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4.2.1 with: distribution: zulu - java-version: 19 + java-version: 21 - name: Cache Gradle Dirs - uses: actions/cache@v3.0.11 + uses: actions/cache@v4.0.2 with: path: | ~/.gradle/caches/ ~/.gradle/wrapper/ - key: cache-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + key: cache-gradle-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }} restore-keys: cache-gradle- - - name: Run Detekt - run: ./gradlew -p library detekt --stacktrace + - name: Run Spotless + run: ./gradlew spotlessCheck --stacktrace + + - name: Run Samples Spotless + working-directory: ./samples + run: ./gradlew spotlessCheck - name: Stop Gradle run: ./gradlew --stop @@ -142,25 +153,25 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.4 - name: Configure JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4.2.1 with: distribution: zulu - java-version: 19 + java-version: 21 - name: Cache Gradle Dirs - uses: actions/cache@v3.0.11 + uses: actions/cache@v4.0.2 with: path: | ~/.gradle/caches/ ~/.gradle/wrapper/ - key: cache-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + key: cache-gradle-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }} restore-keys: cache-gradle- - name: Run Lint - run: ./gradlew -p library lint --stacktrace + run: ./gradlew lint --stacktrace - name: Stop Gradle run: ./gradlew --stop @@ -171,25 +182,25 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.4 - name: Configure JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4.2.1 with: distribution: zulu - java-version: 19 + java-version: 21 - name: Cache Gradle Dirs - uses: actions/cache@v3.0.11 + uses: actions/cache@v4.0.2 with: path: | ~/.gradle/caches/ ~/.gradle/wrapper/ - key: cache-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + key: cache-gradle-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }} restore-keys: cache-gradle- - name: Check ABI - run: ./gradlew -p library apiCheck --stacktrace + run: ./gradlew apiCheck --stacktrace - name: Stop Gradle run: ./gradlew --stop diff --git a/.gitignore b/.gitignore index 170a5c0de..39cc19f14 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,6 @@ # IDEA *.iml -/.idea/ -!/.idea/codeStyles/ -!/.idea/codeStyles/* +.idea/ # Gradle .gradle/ @@ -17,4 +15,4 @@ build/ .DS_Store # MkDocs -/library/docs/api +/docs/api diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index abbae65c7..000000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,429 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 79ee123c2..000000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/library/.mkdocs-theme/404.html b/.mkdocs-theme/404.html similarity index 94% rename from library/.mkdocs-theme/404.html rename to .mkdocs-theme/404.html index 24b708de5..376998554 100644 --- a/library/.mkdocs-theme/404.html +++ b/.mkdocs-theme/404.html @@ -4,4 +4,4 @@ {% block content %}

404 - Not found

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

-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/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 100% rename from library/docs/changelog.md rename to docs/changelog.md 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 100% rename from library/docs/gradle-plugin.md rename to docs/gradle-plugin.md 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 100% rename from library/docs/index.md rename to docs/index.md 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 100% rename from library/docs/releasing.md rename to docs/releasing.md 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..b662180a4 --- /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 249e5832f090a2944b7473328c07c9755baa3196..c1962a79e29d3e0ab67b14947c167a862655af9b 100644 GIT binary patch delta 39834 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 G$^QWPgI3l6 delta 38507 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.configureEach { + useJUnitPlatform() +} + +dependencies { + api(projects.laboratory.runtime) + api(libs.androidx.dataStore) + + testImplementation(libs.kotest.runner.junit5) + testImplementation(libs.kotest.assertions) + testImplementation(libs.turbine) +} diff --git a/library/data-store/gradle.properties b/laboratory/data-store/gradle.properties similarity index 62% rename from library/data-store/gradle.properties rename to laboratory/data-store/gradle.properties index 7a17d7c84..6b1d0dca5 100644 --- a/library/data-store/gradle.properties +++ b/laboratory/data-store/gradle.properties @@ -1,3 +1,3 @@ POM_ARTIFACT_ID=laboratory-data-store -POM_NAME=Laboratory (DataStore) +POM_NAME=Laboratory (Data Store) POM_PACKAGING=aar diff --git a/library/data-store/src/main/java/io/mehow/laboratory/datastore/DataStoreFeatureStorage.kt b/laboratory/data-store/src/main/kotlin/io/mehow/laboratory/datastore/DataStoreFeatureStorage.kt similarity index 96% rename from library/data-store/src/main/java/io/mehow/laboratory/datastore/DataStoreFeatureStorage.kt rename to laboratory/data-store/src/main/kotlin/io/mehow/laboratory/datastore/DataStoreFeatureStorage.kt index 8b3772a71..58ec3881e 100644 --- a/library/data-store/src/main/java/io/mehow/laboratory/datastore/DataStoreFeatureStorage.kt +++ b/laboratory/data-store/src/main/kotlin/io/mehow/laboratory/datastore/DataStoreFeatureStorage.kt @@ -11,8 +11,8 @@ internal class DataStoreFeatureStorage( private val dataStore: DataStore, ) : FeatureStorage { override fun observeFeatureName(feature: Class>) = dataStore - .data - .map { it.options[feature.name] } + .data + .map { it.options[feature.name] } override suspend fun getFeatureName(feature: Class>) = try { dataStore.data.first().options[feature.name] diff --git a/library/data-store/src/main/java/io/mehow/laboratory/datastore/FeatureFlagsSerializer.kt b/laboratory/data-store/src/main/kotlin/io/mehow/laboratory/datastore/FeatureFlagsSerializer.kt similarity index 87% rename from library/data-store/src/main/java/io/mehow/laboratory/datastore/FeatureFlagsSerializer.kt rename to laboratory/data-store/src/main/kotlin/io/mehow/laboratory/datastore/FeatureFlagsSerializer.kt index 8c0f17531..1bd5ec8be 100644 --- a/library/data-store/src/main/java/io/mehow/laboratory/datastore/FeatureFlagsSerializer.kt +++ b/laboratory/data-store/src/main/kotlin/io/mehow/laboratory/datastore/FeatureFlagsSerializer.kt @@ -15,7 +15,10 @@ public object FeatureFlagsSerializer : Serializer { return FeatureFlags.ADAPTER.decode(input) } - override suspend fun writeTo(t: FeatureFlags, output: OutputStream) { + override suspend fun writeTo( + t: FeatureFlags, + output: OutputStream, + ) { FeatureFlags.ADAPTER.encode(output, t) } } diff --git a/library/data-store/src/main/proto/io/mehow/laboratory/datastore/feature_flags.proto b/laboratory/data-store/src/main/proto/io/mehow/laboratory/datastore/feature_flags.proto similarity index 68% rename from library/data-store/src/main/proto/io/mehow/laboratory/datastore/feature_flags.proto rename to laboratory/data-store/src/main/proto/io/mehow/laboratory/datastore/feature_flags.proto index 74dc934b6..77859e67b 100644 --- a/library/data-store/src/main/proto/io/mehow/laboratory/datastore/feature_flags.proto +++ b/laboratory/data-store/src/main/proto/io/mehow/laboratory/datastore/feature_flags.proto @@ -2,8 +2,6 @@ syntax = "proto3"; package io.mehow.laboratory.datastore; -option java_package = "io.mehow.laboratory.datastore"; - message FeatureFlags { map options = 1; } diff --git a/library/data-store/src/test/java/io/mehow/laboratory/datastore/DataStoreFeatureStorageSpec.kt b/laboratory/data-store/src/test/kotlin/io/mehow/laboratory/datastore/DataStoreFeatureStorageSpec.kt similarity index 85% rename from library/data-store/src/test/java/io/mehow/laboratory/datastore/DataStoreFeatureStorageSpec.kt rename to laboratory/data-store/src/test/kotlin/io/mehow/laboratory/datastore/DataStoreFeatureStorageSpec.kt index 9e9efc2b8..825226b5c 100644 --- a/library/data-store/src/test/java/io/mehow/laboratory/datastore/DataStoreFeatureStorageSpec.kt +++ b/laboratory/data-store/src/test/kotlin/io/mehow/laboratory/datastore/DataStoreFeatureStorageSpec.kt @@ -2,7 +2,7 @@ package io.mehow.laboratory.datastore import androidx.datastore.core.DataStoreFactory import app.cash.turbine.test -import io.kotest.core.spec.style.StringSpec +import io.kotest.core.spec.style.FunSpec import io.kotest.engine.spec.tempfile import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.shouldBe @@ -10,8 +10,8 @@ import io.mehow.laboratory.FeatureStorage import io.mehow.laboratory.Laboratory import okio.ByteString.Companion.decodeHex -internal class DataStoreFeatureStorageSpec : StringSpec({ - "stored feature flag option is available as experiment" { +class DataStoreFeatureStorageSpec : FunSpec({ + test("reads stored feature option") { val tempFile = tempfile() val storage = FeatureStorage.dataStore(DataStoreFactory.create(FeatureFlagsSerializer) { tempFile }) val laboratory = Laboratory.create(storage) @@ -21,21 +21,21 @@ internal class DataStoreFeatureStorageSpec : StringSpec({ laboratory.experiment() shouldBe FeatureA.B } - "corrupted file yields default experiment" { + test("uses default option for corrupted data") { val tempFile = tempfile() val storage = FeatureStorage.dataStore(DataStoreFactory.create(FeatureFlagsSerializer) { tempFile }) val laboratory = Laboratory.create(storage) // Represents a map with a key of Feature::class.java.name and value of 1. val corruptedBytes = "0a290a25696f2e6d65686f772e6c61626f7261746f72792e6461746173746f72652e466561747572651001" - .decodeHex() - .toByteArray() + .decodeHex() + .toByteArray() tempFile.writeBytes(corruptedBytes) laboratory.experiment() shouldBe FeatureA.A } - "observes feature flag changes" { + test("emits feature option changes") { val tempFile = tempfile() val storage = FeatureStorage.dataStore(DataStoreFactory.create(FeatureFlagsSerializer) { tempFile }) @@ -55,7 +55,7 @@ internal class DataStoreFeatureStorageSpec : StringSpec({ } } - "clears feature flag options" { + test("clears storage") { val tempFile = tempfile() val storage = FeatureStorage.dataStore(DataStoreFactory.create(FeatureFlagsSerializer) { tempFile }) val laboratory = Laboratory.create(storage) diff --git a/library/data-store/src/test/java/io/mehow/laboratory/datastore/FeatureA.kt b/laboratory/data-store/src/test/kotlin/io/mehow/laboratory/datastore/FeatureA.kt similarity index 71% rename from library/data-store/src/test/java/io/mehow/laboratory/datastore/FeatureA.kt rename to laboratory/data-store/src/test/kotlin/io/mehow/laboratory/datastore/FeatureA.kt index 22524d32c..3dea5609e 100644 --- a/library/data-store/src/test/java/io/mehow/laboratory/datastore/FeatureA.kt +++ b/laboratory/data-store/src/test/kotlin/io/mehow/laboratory/datastore/FeatureA.kt @@ -2,7 +2,7 @@ package io.mehow.laboratory.datastore import io.mehow.laboratory.Feature -internal enum class FeatureA : Feature { +enum class FeatureA : Feature { A, B, ; diff --git a/laboratory/data-store/src/test/kotlin/io/mehow/laboratory/datastore/FeatureFlagsSerializerSpec.kt b/laboratory/data-store/src/test/kotlin/io/mehow/laboratory/datastore/FeatureFlagsSerializerSpec.kt new file mode 100644 index 000000000..e186459b1 --- /dev/null +++ b/laboratory/data-store/src/test/kotlin/io/mehow/laboratory/datastore/FeatureFlagsSerializerSpec.kt @@ -0,0 +1,29 @@ +package io.mehow.laboratory.datastore + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import okio.Buffer +import okio.ByteString.Companion.decodeHex + +internal class FeatureFlagsSerializerSpec : FunSpec({ + val flags = FeatureFlags(mapOf(FeatureA::class.java.toString() to FeatureA.A.name)) + val hex = "0a310a2c636c61737320696f2e6d65686f772e6c61626f7261746f72792e6461746173746f72652e4665617475726541120141" + val binaryFlags = hex.decodeHex() + + test("decodes bytes") { + val input = Buffer().write(binaryFlags).inputStream() + + val result = FeatureFlagsSerializer.readFrom(input) + + result shouldBe flags + } + + test("encodes bytes") { + val output = Buffer() + + FeatureFlagsSerializer.writeTo(flags, output.outputStream()) + val result = output.readByteString() + + result shouldBe binaryFlags + } +}) diff --git a/library/generator/api/generator.api b/laboratory/generator/api/generator.api similarity index 98% rename from library/generator/api/generator.api rename to laboratory/generator/api/generator.api index 4aec7d0ea..d17a8dfe9 100644 --- a/library/generator/api/generator.api +++ b/laboratory/generator/api/generator.api @@ -72,6 +72,7 @@ public final class io/mehow/laboratory/generator/Supervisor { public final class io/mehow/laboratory/generator/Visibility : java/lang/Enum { public static final field Internal Lio/mehow/laboratory/generator/Visibility; public static final field Public Lio/mehow/laboratory/generator/Visibility; + public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lio/mehow/laboratory/generator/Visibility; public static fun values ()[Lio/mehow/laboratory/generator/Visibility; } diff --git a/laboratory/generator/build.gradle.kts b/laboratory/generator/build.gradle.kts new file mode 100644 index 000000000..b2e2e537d --- /dev/null +++ b/laboratory/generator/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.mavenPublish) + alias(libs.plugins.spotless) + alias(libs.plugins.detekt) + alias(libs.plugins.dokka) +} + +tasks.withType().configureEach { + useJUnitPlatform() +} + +dependencies { + api(libs.kotlinPoet) + implementation(projects.laboratory.runtime) + + testImplementation(libs.kotest.runner.junit5) + testImplementation(libs.kotest.assertions) + testImplementation(libs.kotest.property) +} diff --git a/library/generator/gradle.properties b/laboratory/generator/gradle.properties similarity index 63% rename from library/generator/gradle.properties rename to laboratory/generator/gradle.properties index f2688833c..bb064064b 100644 --- a/library/generator/gradle.properties +++ b/laboratory/generator/gradle.properties @@ -1,3 +1,3 @@ POM_ARTIFACT_ID=laboratory-generator -POM_NAME=Laboratory (generator) +POM_NAME=Laboratory (Generator) POM_PACKAGING=jar diff --git a/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/ClassNames.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/ClassNames.kt new file mode 100644 index 000000000..e0e5541dd --- /dev/null +++ b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/ClassNames.kt @@ -0,0 +1,11 @@ +package io.mehow.laboratory.generator + +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.asClassName +import kotlin.reflect.KClass + +internal operator fun KClass<*>.invoke( + parameter: TypeName, + vararg parameters: TypeName, +) = asClassName().parameterizedBy(parameter, *parameters) diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/Deprecation.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/Deprecation.kt similarity index 91% rename from library/generator/src/main/java/io/mehow/laboratory/generator/Deprecation.kt rename to laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/Deprecation.kt index 0800e8fca..2b1bff626 100644 --- a/library/generator/src/main/java/io/mehow/laboratory/generator/Deprecation.kt +++ b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/Deprecation.kt @@ -14,7 +14,7 @@ public class Deprecation( ERROR, HIDDEN -> "DEPRECATION_ERROR" }.let { name -> AnnotationSpec.builder(Suppress::class) - .addMember("%S", name) - .build() + .addMember("%S", name) + .build() } } diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFactoryGenerator.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFactoryGenerator.kt similarity index 60% rename from library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFactoryGenerator.kt rename to laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFactoryGenerator.kt index ddd6b333b..c1fb8ade2 100644 --- a/library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFactoryGenerator.kt +++ b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFactoryGenerator.kt @@ -20,49 +20,51 @@ internal class FeatureFactoryGenerator( functionName: String, ) { private val featureClasses = factory.features - .map { it.className.reflectionName() } - .sorted() - .map { name -> CodeBlock.of("%T.forName(%S)", Class::class.asTypeName(), name) } - .joinToCode(prefix = "\n⇥", separator = ",\n", suffix = "⇤\n") + .map { it.className.reflectionName() } + .sorted() + .map { name -> CodeBlock.of("%T.forName(%S)", Class::class.asTypeName(), name) } + .joinToCode(prefix = "\n⇥", separator = ",\n", suffix = "⇤\n") private val suppressCast = AnnotationSpec.builder(Suppress::class) - .addMember("%S", "UNCHECKED_CAST") - .build() + .addMember("%S", "UNCHECKED_CAST") + .build() private val setOf = MemberName("kotlin.collections", "setOf") private val emptySet = MemberName("kotlin.collections", "emptySet") private val discoveryFunctionOverride = FunSpec.builder("create") - .addModifiers(OVERRIDE) - .apply { - returns(factoryReturnType) + .addModifiers(OVERRIDE) + .apply { + returns(factoryReturnType) - if (factory.features.isNotEmpty()) { - addAnnotation(suppressCast) - addStatement("return %M(%L) as %T", setOf, featureClasses, factoryReturnType) - } else addStatement("return %M<%T>()", emptySet, featureType) + if (factory.features.isNotEmpty()) { + addAnnotation(suppressCast) + addStatement("return %M(%L) as %T", setOf, featureClasses, factoryReturnType) + } else { + addStatement("return %M<%T>()", emptySet, featureType) } - .build() + } + .build() private val factoryType = TypeSpec.objectBuilder(factory.className) - .addModifiers(PRIVATE) - .addSuperinterface(FeatureFactory::class) - .addFunction(discoveryFunctionOverride) - .build() + .addModifiers(PRIVATE) + .addSuperinterface(FeatureFactory::class) + .addFunction(discoveryFunctionOverride) + .build() private val factoryExtension = FunSpec.builder(functionName) - .addModifiers(factory.visibility.modifier) - .receiver(FeatureFactory.Companion::class) - .returns(FeatureFactory::class) - .addStatement("return %N", factoryType) - .build() + .addModifiers(factory.visibility.modifier) + .receiver(FeatureFactory.Companion::class) + .returns(FeatureFactory::class) + .addStatement("return %N", factoryType) + .build() private val factoryFile = FileSpec.builder(factory.className.packageName, factory.className.simpleName) - .addFunction(factoryExtension) - .addType(factoryType) - .build() + .addFunction(factoryExtension) + .addType(factoryType) + .build() fun fileSpec() = factoryFile diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFactoryModel.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFactoryModel.kt similarity index 100% rename from library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFactoryModel.kt rename to laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFactoryModel.kt diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFlagGenerator.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFlagGenerator.kt similarity index 62% rename from library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFlagGenerator.kt rename to laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFlagGenerator.kt index 81912863b..51658317c 100644 --- a/library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFlagGenerator.kt +++ b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFlagGenerator.kt @@ -20,28 +20,28 @@ internal class FeatureFlagGenerator( ) { private val deprecated = feature.deprecation?.let { deprecation -> AnnotationSpec.builder(Deprecated::class) - .addMember("message = %S", deprecation.message) - .addMember("level = %T.%L", DeprecationLevel::class, deprecation.level) - .build() + .addMember("message = %S", deprecation.message) + .addMember("level = %T.%L", DeprecationLevel::class, deprecation.level) + .build() } private val suppressDeprecation = feature.deprecation?.suppressSpec private val defaultOptionProperty = feature.options.toList() - .single(FeatureFlagOption::isDefault) - .let { option -> - PropertySpec - .builder(defaultOptionPropertyName, feature.className, OVERRIDE) - .apply { suppressDeprecation?.let { addAnnotation(it) } } - .getter(FunSpec.getterBuilder().addCode("return %L", option.name).build()) - .build() - } + .single(FeatureFlagOption::isDefault) + .let { option -> + PropertySpec + .builder(defaultOptionPropertyName, feature.className, OVERRIDE) + .apply { suppressDeprecation?.let { addAnnotation(it) } } + .getter(FunSpec.getterBuilder().addCode("return %L", option.name).build()) + .build() + } private val sourceProperty = feature.source?.let { nestedSource -> nestedSource to PropertySpec - .builder(sourcePropertyName, featureClassType, OVERRIDE) - .initializer("%T::class.java", nestedSource.className) - .build() + .builder(sourcePropertyName, featureClassType, OVERRIDE) + .initializer("%T::class.java", nestedSource.className) + .build() } private val description: String? = feature.description.takeIf(String::isNotBlank) @@ -50,49 +50,49 @@ internal class FeatureFlagGenerator( private val descriptionProperty = description?.let { description -> PropertySpec - .builder(descriptionPropertyName, String::class, OVERRIDE) - .initializer("%S", description) - .build() + .builder(descriptionPropertyName, String::class, OVERRIDE) + .initializer("%S", description) + .build() } private val supervisorOptionProperty = feature.supervisor?.let { supervisor -> PropertySpec - .builder(supervisorOptionPropertyName, featureType, OVERRIDE) - .initializer("%T.%L", supervisor.featureFlag.className, supervisor.option.name) - .build() + .builder(supervisorOptionPropertyName, featureType, OVERRIDE) + .initializer("%T.%L", supervisor.featureFlag.className, supervisor.option.name) + .build() } private val typeSpec: TypeSpec = TypeSpec.enumBuilder(feature.className) - .apply { deprecated?.let { addAnnotation(it) } } - .addModifiers(feature.visibility.modifier) - .apply { - var parametrizedType: TypeName = feature.className - if (suppressDeprecation != null) { - parametrizedType = parametrizedType.copy(annotations = listOf(suppressDeprecation)) - } - addSuperinterface(Feature::class(parametrizedType)) + .apply { deprecated?.let { addAnnotation(it) } } + .addModifiers(feature.visibility.modifier) + .apply { + var parametrizedType: TypeName = feature.className + if (suppressDeprecation != null) { + parametrizedType = parametrizedType.copy(annotations = listOf(suppressDeprecation)) } - .addProperty(defaultOptionProperty) - .apply { - feature.options.fold(this) { builder, featureOption -> - builder.addEnumConstant(featureOption.name) - } + addSuperinterface(Feature::class(parametrizedType)) + } + .addProperty(defaultOptionProperty) + .apply { + feature.options.fold(this) { builder, featureOption -> + builder.addEnumConstant(featureOption.name) } - .apply { - sourceProperty?.let { (nestedSource, sourceWithOverride) -> - addType(FeatureFlagGenerator(nestedSource).typeSpec) - addProperty(sourceWithOverride) - } + } + .apply { + sourceProperty?.let { (nestedSource, sourceWithOverride) -> + addType(FeatureFlagGenerator(nestedSource).typeSpec) + addProperty(sourceWithOverride) } - .apply { kdocCodeBlock?.let { addKdoc(it) } } - .apply { descriptionProperty?.let { addProperty(it) } } - .apply { supervisorOptionProperty?.let { addProperty(it) } } - .build() + } + .apply { kdocCodeBlock?.let { addKdoc(it) } } + .apply { descriptionProperty?.let { addProperty(it) } } + .apply { supervisorOptionProperty?.let { addProperty(it) } } + .build() private val fileSpec = FileSpec.builder(feature.className.packageName, feature.className.simpleName) - .addType(typeSpec) - .build() + .addType(typeSpec) + .build() fun fileSpec() = fileSpec @@ -115,8 +115,8 @@ internal fun String.prepareKdocHyperlinks(): String { val regularTokens = matches.toRegularTokens(this) val linkTokens = matches.toLinkTokens() val tokens = (regularTokens + linkTokens) - .sortedBy { (_, startIndex) -> startIndex } - .map { (token, _) -> token } + .sortedBy { (_, startIndex) -> startIndex } + .map { (token, _) -> token } return buildString { for (token in tokens) { token.append(this) @@ -151,12 +151,12 @@ private fun Sequence.toLinkTokens() = map { matchResult -> } private fun Sequence.toRegularTokens(text: String) = toUnmatchedRanges(text) - .map { range -> Regular(text.substring(range)) to range.first } + .map { range -> Regular(text.substring(range)) to range.first } private fun Sequence.toUnmatchedRanges(text: String) = sequence { yield(Int.MIN_VALUE..0) yieldAll(map { it.range }.map { it.first - 1..it.last + 1 }) yield(text.length - 1..Int.MAX_VALUE) }.windowed(2, 1) - .map { (start, end) -> start.last..end.first } - .filterNot { range -> range.isEmpty() } + .map { (start, end) -> start.last..end.first } + .filterNot { range -> range.isEmpty() } diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFlagModel.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFlagModel.kt similarity index 84% rename from library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFlagModel.kt rename to laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFlagModel.kt index 954e6729c..7808b4036 100644 --- a/library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFlagModel.kt +++ b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFlagModel.kt @@ -37,14 +37,14 @@ public class FeatureFlagModel private constructor( sourceOptions: List = emptyList(), supervisor: Supervisor? = null, ) : this( - className, - options, - visibility, - key, - description, - deprecation, - createSource(visibility, className, sourceOptions), - supervisor, + className, + options, + visibility, + key, + description, + deprecation, + createSource(visibility, className, sourceOptions), + supervisor, ) public fun prepare(): FileSpec = FeatureFlagGenerator(this).fileSpec() @@ -68,12 +68,12 @@ public class FeatureFlagModel private constructor( private fun ClassName.toSourceName() = ClassName(packageName, simpleNames + "Source") private fun List.toSourceOptions() = filterNot { it.name.equals("local", ignoreCase = true) } - .takeIf { it.isNotEmpty() } - ?.let { options -> - buildList { - add(FeatureFlagOption("Local", isDefault = options.none(FeatureFlagOption::isDefault))) - addAll(options) - } + .takeIf { it.isNotEmpty() } + ?.let { options -> + buildList { + add(FeatureFlagOption("Local", isDefault = options.none(FeatureFlagOption::isDefault))) + addAll(options) } + } } } diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFlagOption.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFlagOption.kt similarity index 100% rename from library/generator/src/main/java/io/mehow/laboratory/generator/FeatureFlagOption.kt rename to laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/FeatureFlagOption.kt diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/OptionFactoryModel.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/OptionFactoryModel.kt similarity index 52% rename from library/generator/src/main/java/io/mehow/laboratory/generator/OptionFactoryModel.kt rename to laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/OptionFactoryModel.kt index d113e36a7..0a5efdcd6 100644 --- a/library/generator/src/main/java/io/mehow/laboratory/generator/OptionFactoryModel.kt +++ b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/OptionFactoryModel.kt @@ -29,12 +29,12 @@ public class OptionFactoryModel( val groupedFeatures = features.groupBy { it.key ?: it.className.canonicalName } require(groupedFeatures.size == features.size) { val duplicates = groupedFeatures - .filterValues { it.size > 1 } - .mapValues { (_, features) -> features.map(FeatureFlagModel::toString) } + .filterValues { it.size > 1 } + .mapValues { (_, features) -> features.map(FeatureFlagModel::toString) } """ |Feature flags must have unique keys. Found following duplicates: | - ${duplicates.toList().joinToString(separator = "\n - ") { (key, fqcns) -> "$key: $fqcns" }} - """.trimMargin() + """.trimMargin() } } } @@ -44,46 +44,46 @@ private class OptionFactoryGenerator( private val model: OptionFactoryModel, ) { private val nameMatcher = model.features.associateBy { it.className } - .mapValues { (className, feature) -> - val whenExpression = feature.options - .map { CodeBlock.of("%S·->·%T.%L", it.name, className, it.name) } - .joinToCode(prefix = "when·(name)·{\n⇥", separator = "\n", suffix = "\nelse·->·null⇤\n}") - val deprecation = feature.deprecation?.suppressSpec - if (deprecation != null) { - CodeBlock.of("%L·%L", deprecation, whenExpression) - } else { - whenExpression - } + .mapValues { (className, feature) -> + val whenExpression = feature.options + .map { CodeBlock.of("%S·->·%T.%L", it.name, className, it.name) } + .joinToCode(prefix = "when·(name)·{\n⇥", separator = "\n", suffix = "\nelse·->·null⇤\n}") + val deprecation = feature.deprecation?.suppressSpec + if (deprecation != null) { + CodeBlock.of("%L·%L", deprecation, whenExpression) + } else { + whenExpression } + } private val keyMatcher = model.features - .sortedWith(compareBy({ it.key == null }, { it.key }, { it.className.canonicalName })) - .map { CodeBlock.of("%S·->·%L", it.key ?: it.className.canonicalName, nameMatcher.getValue(it.className)) } - .joinToCode(prefix = "when·(key)·{\n⇥", separator = "\n", suffix = "\nelse·->·null⇤\n}") + .sortedWith(compareBy({ it.key == null }, { it.key }, { it.className.canonicalName })) + .map { CodeBlock.of("%S·->·%L", it.key ?: it.className.canonicalName, nameMatcher.getValue(it.className)) } + .joinToCode(prefix = "when·(key)·{\n⇥", separator = "\n", suffix = "\nelse·->·null⇤\n}") private val createFunctionOverride = FunSpec.builder("create") - .addModifiers(OVERRIDE) - .addParameter("key", String::class) - .addParameter("name", String::class) - .returns(Feature::class(STAR).copy(nullable = true)) - .apply { if (model.features.isEmpty()) addStatement("return null") else addStatement("return %L", keyMatcher) } - .build() + .addModifiers(OVERRIDE) + .addParameter("key", String::class) + .addParameter("name", String::class) + .returns(Feature::class(STAR).copy(nullable = true)) + .apply { if (model.features.isEmpty()) addStatement("return null") else addStatement("return %L", keyMatcher) } + .build() private val factoryType = TypeSpec.objectBuilder(model.className) - .addModifiers(PRIVATE) - .addSuperinterface(OptionFactory::class) - .addFunction(createFunctionOverride) - .build() + .addModifiers(PRIVATE) + .addSuperinterface(OptionFactory::class) + .addFunction(createFunctionOverride) + .build() private val factoryExtension = FunSpec.builder("generated") - .addModifiers(model.visibility.modifier) - .receiver(OptionFactory.Companion::class) - .returns(OptionFactory::class) - .addStatement("return %N", factoryType) - .build() + .addModifiers(model.visibility.modifier) + .receiver(OptionFactory.Companion::class) + .returns(OptionFactory::class) + .addStatement("return %N", factoryType) + .build() val fileSpec = FileSpec.builder(model.className.packageName, model.className.simpleName) - .addFunction(factoryExtension) - .addType(factoryType) - .build() + .addFunction(factoryExtension) + .addType(factoryType) + .build() } diff --git a/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/SourcedFeatureStorageGenerator.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/SourcedFeatureStorageGenerator.kt new file mode 100644 index 000000000..b939637a3 --- /dev/null +++ b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/SourcedFeatureStorageGenerator.kt @@ -0,0 +1,150 @@ +package io.mehow.laboratory.generator + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier.ABSTRACT +import com.squareup.kotlinpoet.KModifier.DATA +import com.squareup.kotlinpoet.KModifier.OVERRIDE +import com.squareup.kotlinpoet.KModifier.PRIVATE +import com.squareup.kotlinpoet.KOperator.PLUS +import com.squareup.kotlinpoet.MemberName +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.asClassName +import io.mehow.laboratory.FeatureStorage +import java.util.Locale + +internal class SourcedFeatureStorageGenerator( + storage: SourcedFeatureStorageModel, +) { + private val sourceNames = storage.sourceNames + .filterNot { featureName -> featureName.equals("local", ignoreCase = true) } + .distinct() + + private val sourced = MemberName(FeatureStorage.Companion::class.asClassName(), "sourced") + + private val emptyMap = MemberName(kotlinCollectionsSpace, "emptyMap") + + private val mapPlus = MemberName(kotlinCollectionsSpace, PLUS) + + private val infixTo = MemberName("kotlin", "to") + + private val buildingStepClassName = ClassName(storage.className.packageName, "BuildingStep") + + private val buildingStepType = TypeSpec.interfaceBuilder(buildingStepClassName) + .addModifiers(storage.visibility.modifier) + .addFunction( + FunSpec.builder("build") + .addModifiers(ABSTRACT) + .returns(FeatureStorage::class) + .build(), + ) + .build() + + private val remoteStepClassNames = sourceNames.distinct() + .sorted() + .map { ClassName(storage.className.packageName, it + stepSuffix) } + + private val remoteStepTypes = remoteStepClassNames + .windowed(size = 2, step = 1, partialWindows = true) { sources -> + val currentSourceClassName = sources.first() + val functionReturnClassName = sources.drop(1).firstOrNull() ?: buildingStepClassName + val functionName = currentSourceClassName.simpleName + .removeSuffix(stepSuffix) + .replaceFirstChar { it.lowercase(Locale.ROOT) } + "Source" + + TypeSpec.interfaceBuilder(currentSourceClassName) + .addModifiers(storage.visibility.modifier) + .addFunction( + FunSpec.builder(functionName) + .addModifiers(ABSTRACT) + .addParameter("source", FeatureStorage::class) + .returns(functionReturnClassName) + .build(), + ) + .build() + } + + private val builderType = TypeSpec.classBuilder(ClassName(storage.className.simpleName, "Builder")) + .addModifiers(PRIVATE, DATA) + .addSuperinterfaces(remoteStepClassNames + buildingStepClassName) + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter(localSourceParam, FeatureStorage::class) + .addParameter(remoteSourcesParam, stringToStorageMap) + .build(), + ) + .addProperty( + PropertySpec.builder(localSourceParam, FeatureStorage::class) + .initializer(localSourceParam) + .addModifiers(PRIVATE) + .build(), + ) + .addProperty( + PropertySpec.builder(remoteSourcesParam, stringToStorageMap) + .initializer(remoteSourcesParam) + .addModifiers(PRIVATE) + .build(), + ) + .addFunctions( + remoteStepTypes.mapIndexed { index, remoteStep -> + val function = remoteStep.funSpecs.single() + function.toBuilder() + .apply { modifiers -= ABSTRACT } + .addModifiers(OVERRIDE) + .addStatement( + "return copy(\n⇥%1L = %1L %2M (%3S %4M %5N)⇤\n)", + remoteSourcesParam, + mapPlus, + remoteStepClassNames[index].simpleName.removeSuffix(stepSuffix), + infixTo, + function.parameters.single(), + ) + .build() + }, + ) + .addFunction( + buildingStepType.funSpecs.single() + .toBuilder() + .apply { modifiers -= ABSTRACT } + .addModifiers(OVERRIDE) + .addStatement("return %M(%L, %L)", sourced, localSourceParam, remoteSourcesParam) + .build(), + ) + .build() + + private val storageBuilderExtension = FunSpec.builder("sourcedBuilder") + .addModifiers(storage.visibility.modifier) + .receiver(FeatureStorage.Companion::class) + .returns(remoteStepClassNames.firstOrNull() ?: buildingStepClassName) + .addParameter(localSourceParam, FeatureStorage::class) + .addStatement("return %N(%L, %M())", builderType, localSourceParam, emptyMap) + .build() + + private val storageFile = FileSpec.builder(storage.className.packageName, storage.className.simpleName) + .addFunction(storageBuilderExtension) + .apply { + for (type in remoteStepTypes) { + addType(type) + } + } + .addType(buildingStepType) + .addType(builderType) + .build() + + fun fileSpec() = storageFile + + private companion object { + const val stepSuffix = "Step" + const val localSourceParam = "localSource" + const val remoteSourcesParam = "remoteSources" + + const val kotlinCollectionsSpace = "kotlin.collections" + + val stringToStorageMap = Map::class( + String::class.asClassName(), + FeatureStorage::class.asClassName(), + ) + } +} diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/SourcedFeatureStorageModel.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/SourcedFeatureStorageModel.kt similarity index 100% rename from library/generator/src/main/java/io/mehow/laboratory/generator/SourcedFeatureStorageModel.kt rename to laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/SourcedFeatureStorageModel.kt diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/Supervisor.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/Supervisor.kt similarity index 100% rename from library/generator/src/main/java/io/mehow/laboratory/generator/Supervisor.kt rename to laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/Supervisor.kt diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/Visibility.kt b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/Visibility.kt similarity index 92% rename from library/generator/src/main/java/io/mehow/laboratory/generator/Visibility.kt rename to laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/Visibility.kt index 354a418eb..f9604752c 100644 --- a/library/generator/src/main/java/io/mehow/laboratory/generator/Visibility.kt +++ b/laboratory/generator/src/main/kotlin/io/mehow/laboratory/generator/Visibility.kt @@ -8,5 +8,5 @@ public enum class Visibility( internal val modifier: KModifier, ) { Public(PUBLIC), - Internal(INTERNAL) + Internal(INTERNAL), } diff --git a/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/FeatureFactoryModelSpec.kt b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/FeatureFactoryModelSpec.kt new file mode 100644 index 000000000..702053844 --- /dev/null +++ b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/FeatureFactoryModelSpec.kt @@ -0,0 +1,116 @@ +package io.mehow.laboratory.generator + +import com.squareup.kotlinpoet.ClassName +import io.kotest.core.spec.style.FunSpec +import io.mehow.laboratory.generator.Visibility.Internal +import io.mehow.laboratory.generator.Visibility.Public +import io.mehow.laboratory.generator.test.shouldSpecify + +class FeatureFactoryModelSpec : FunSpec({ + val featureA = FeatureFlagModel( + className = ClassName("io.mehow", "FeatureA"), + options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + ) + + val featureB = FeatureFlagModel( + className = ClassName("io.mehow", "FeatureB"), + options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + ) + + val featureC = FeatureFlagModel( + className = ClassName("io.mehow.c", "FeatureA"), + options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + ) + + test("can be internal") { + val model = FeatureFactoryModel( + ClassName("io.mehow", "GeneratedFeatureFactory"), + listOf(featureA, featureB, featureC), + visibility = Internal, + ) + + val fileSpec = model.prepare("generated") + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import io.mehow.laboratory.FeatureFactory + |import java.lang.Class + |import kotlin.Suppress + |import kotlin.collections.Set + |import kotlin.collections.setOf + | + |internal fun FeatureFactory.Companion.generated(): FeatureFactory = GeneratedFeatureFactory + | + |private object GeneratedFeatureFactory : FeatureFactory { + | @Suppress("UNCHECKED_CAST") + | override fun create(): Set>> = setOf( + | Class.forName("io.mehow.FeatureA"), + | Class.forName("io.mehow.FeatureB"), + | Class.forName("io.mehow.c.FeatureA") + | ) as Set>> + |} + | + """.trimMargin() + } + + test("can be public") { + val model = FeatureFactoryModel( + ClassName("io.mehow", "GeneratedFeatureFactory"), + listOf(featureA, featureB, featureC), + visibility = Public, + ) + + val fileSpec = model.prepare("generated") + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import io.mehow.laboratory.FeatureFactory + |import java.lang.Class + |import kotlin.Suppress + |import kotlin.collections.Set + |import kotlin.collections.setOf + | + |public fun FeatureFactory.Companion.generated(): FeatureFactory = GeneratedFeatureFactory + | + |private object GeneratedFeatureFactory : FeatureFactory { + | @Suppress("UNCHECKED_CAST") + | override fun create(): Set>> = setOf( + | Class.forName("io.mehow.FeatureA"), + | Class.forName("io.mehow.FeatureB"), + | Class.forName("io.mehow.c.FeatureA") + | ) as Set>> + |} + | + """.trimMargin() + } + + test("is optimized when there are no features") { + val model = FeatureFactoryModel( + ClassName("io.mehow", "GeneratedFeatureFactory"), + features = emptyList(), + ) + + val fileSpec = model.prepare("generated") + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import io.mehow.laboratory.FeatureFactory + |import java.lang.Class + |import kotlin.collections.Set + |import kotlin.collections.emptySet + | + |internal fun FeatureFactory.Companion.generated(): FeatureFactory = GeneratedFeatureFactory + | + |private object GeneratedFeatureFactory : FeatureFactory { + | override fun create(): Set>> = emptySet>>() + |} + | + """.trimMargin() + } +}) diff --git a/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/FeatureFlagModelSpec.kt b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/FeatureFlagModelSpec.kt new file mode 100644 index 000000000..282a3d2c1 --- /dev/null +++ b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/FeatureFlagModelSpec.kt @@ -0,0 +1,521 @@ +package io.mehow.laboratory.generator + +import com.squareup.kotlinpoet.ClassName +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.throwable.shouldHaveMessage +import io.kotest.property.Arb +import io.kotest.property.arbitrary.stringPattern +import io.kotest.property.checkAll +import io.mehow.laboratory.generator.Visibility.Internal +import io.mehow.laboratory.generator.Visibility.Public +import io.mehow.laboratory.generator.test.shouldSpecify +import java.util.Locale +import kotlin.DeprecationLevel.ERROR +import kotlin.DeprecationLevel.HIDDEN +import kotlin.DeprecationLevel.WARNING + +class FeatureFlagModelSpec : FunSpec({ + test("can be internal") { + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + visibility = Internal, + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + | + |internal enum class FeatureA : Feature { + | First, + | Second, + | ; + | + | override val defaultOption: FeatureA + | get() = First + |} + | + """.trimMargin() + } + + test("can be public") { + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + visibility = Public, + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + | + |public enum class FeatureA : Feature { + | First, + | Second, + | ; + | + | override val defaultOption: FeatureA + | get() = First + |} + | + """.trimMargin() + } + + test("can have single option") { + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(FeatureFlagOption("First", isDefault = true)), + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + | + |public enum class FeatureA : Feature { + | First, + | ; + | + | override val defaultOption: FeatureA + | get() = First + |} + | + """.trimMargin() + } + + test("can have source") { + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + sourceOptions = listOf(FeatureFlagOption("Remote")), + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import java.lang.Class + | + |public enum class FeatureA : Feature { + | First, + | Second, + | ; + | + | override val defaultOption: FeatureA + | get() = First + | + | override val source: Class> = Source::class.java + | + | public enum class Source : Feature { + | Local, + | Remote, + | ; + | + | override val defaultOption: Source + | get() = Local + | } + |} + | + """.trimMargin() + } + + test("does not have source parameter if only source is Local") { + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + sourceOptions = listOf(FeatureFlagOption("Local")), + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + | + |public enum class FeatureA : Feature { + | First, + | Second, + | ; + | + | override val defaultOption: FeatureA + | get() = First + |} + | + """.trimMargin() + } + + test("filters out any custom local source") { + val localPermutations = (0b00000..0b11111).map { + listOf(it and 0b00001, it and 0b00010, it and 0b00100, it and 0b01000, it and 0b10000) + .map { mask -> mask != 0 } + .mapIndexed { index, mask -> + val chars = "local"[index].toString() + if (mask) chars else chars.replaceFirstChar { char -> char.titlecase(Locale.ROOT) } + }.joinToString(separator = "") + } + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + sourceOptions = (localPermutations + "Remote").map(::FeatureFlagOption), + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import java.lang.Class + | + |public enum class FeatureA : Feature { + | First, + | Second, + | ; + | + | override val defaultOption: FeatureA + | get() = First + | + | override val source: Class> = Source::class.java + | + | public enum class Source : Feature { + | Local, + | Remote, + | ; + | + | override val defaultOption: Source + | get() = Local + | } + |} + | + """.trimMargin() + } + + test("can change default source") { + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + sourceOptions = listOf(FeatureFlagOption("Remote", isDefault = true)), + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import java.lang.Class + | + |public enum class FeatureA : Feature { + | First, + | Second, + | ; + | + | override val defaultOption: FeatureA + | get() = First + | + | override val source: Class> = Source::class.java + | + | public enum class Source : Feature { + | Local, + | Remote, + | ; + | + | override val defaultOption: Source + | get() = Remote + | } + |} + | + """.trimMargin() + } + + test("copies feature visibility to source visibility") { + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + visibility = Internal, + sourceOptions = listOf(FeatureFlagOption("Remote")), + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import java.lang.Class + | + |internal enum class FeatureA : Feature { + | First, + | Second, + | ; + | + | override val defaultOption: FeatureA + | get() = First + | + | override val source: Class> = Source::class.java + | + | internal enum class Source : Feature { + | Local, + | Remote, + | ; + | + | override val defaultOption: Source + | get() = Local + | } + |} + | + """.trimMargin() + } + + test("can have supervisor") { + val supervisor = FeatureFlagModel( + ClassName("io.mehow.supervisor", "Supervisor"), + listOf(FeatureFlagOption("First", isDefault = true)), + ) + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + supervisor = Supervisor(supervisor, supervisor.options.first()), + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import io.mehow.supervisor.Supervisor + | + |public enum class FeatureA : Feature { + | First, + | Second, + | ; + | + | override val defaultOption: FeatureA + | get() = First + | + | override val supervisorOption: Feature<*> = Supervisor.First + |} + | + """.trimMargin() + } + + test("description is added as KDoc") { + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + description = "Feature description", + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import kotlin.String + | + |/** + | * Feature description + | */ + |public enum class FeatureA : Feature { + | First, + | Second, + | ; + | + | override val defaultOption: FeatureA + | get() = First + | + | override val description: String = "Feature description" + |} + | + """.trimMargin() + } + + test("description does not break hyperlinks") { + @Suppress("MaxLineLength") + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + description = "Some [long hyperlink](https://square.github.io/kotlinpoet/1.x/kotlinpoet-classinspector-elements/com.squareup.kotlinpoet.classinspector.elements/) in the KDoc.", + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import kotlin.String + | + |/** + | * Some + | * [long hyperlink](https://square.github.io/kotlinpoet/1.x/kotlinpoet-classinspector-elements/com.squareup.kotlinpoet.classinspector.elements/) + | * in the KDoc. + | */ + |public enum class FeatureA : Feature { + | First, + | Second, + | ; + | + | override val defaultOption: FeatureA + | get() = First + | + | override val description: String = + | "Some [long hyperlink](https://square.github.io/kotlinpoet/1.x/kotlinpoet-classinspector-elements/com.squareup.kotlinpoet.classinspector.elements/) in the KDoc." + |} + | + """.trimMargin() + } + + test("uses warning level as a default deprecation") { + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + deprecation = Deprecation("Deprecation message"), + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import kotlin.Deprecated + |import kotlin.DeprecationLevel + |import kotlin.Suppress + | + |@Deprecated( + | message = "Deprecation message", + | level = DeprecationLevel.WARNING, + |) + |public enum class FeatureA : Feature<@Suppress("DEPRECATION") FeatureA> { + | First, + | Second, + | ; + | + | @Suppress("DEPRECATION") + | override val defaultOption: FeatureA + | get() = First + |} + | + """.trimMargin() + } + + enumValues().forEach { level -> + test("can use explicit $level deprecation level") { + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + deprecation = Deprecation("Deprecation message", level), + ) + val suppressLevel = when (level) { + WARNING -> "DEPRECATION" + ERROR, HIDDEN -> "DEPRECATION_ERROR" + } + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import kotlin.Deprecated + |import kotlin.DeprecationLevel + |import kotlin.Suppress + | + |@Deprecated( + | message = "Deprecation message", + | level = DeprecationLevel.$level, + |) + |public enum class FeatureA : Feature<@Suppress("$suppressLevel") FeatureA> { + | First, + | Second, + | ; + | + | @Suppress("$suppressLevel") + | override val defaultOption: FeatureA + | get() = First + |} + | + """.trimMargin() + } + } + + test("fails to generate when there are no options") { + val exception = shouldThrow { + FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + options = emptyList(), + ) + } + + exception shouldHaveMessage "io.mehow.FeatureA must have at least one option" + } + + test("fails to generate when there is no default option") { + checkAll( + Arb.stringPattern("[a-z](0)([a-z]{0,10})"), + Arb.stringPattern("[a-z](1)([a-z]{0,10})"), + ) { first, second -> + val exception = shouldThrow { + FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(FeatureFlagOption(first), FeatureFlagOption(second)), + ) + } + + exception shouldHaveMessage "io.mehow.FeatureA must have exactly one default option" + } + } + + test("fails to generate when there are multiple default options") { + checkAll( + Arb.stringPattern("[a-z](0)([a-z]{0,10})"), + Arb.stringPattern("[a-z](1)([a-z]{0,10})"), + Arb.stringPattern("[a-z](2)([a-z]{0,10})"), + ) { first, second, third -> + val exception = shouldThrow { + FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf( + FeatureFlagOption(first, isDefault = true), + FeatureFlagOption(second), + FeatureFlagOption(third, isDefault = true), + ), + ) + } + + exception shouldHaveMessage "io.mehow.FeatureA must have exactly one default option" + } + } + + test("fails to supervise itself") { + val model = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(FeatureFlagOption("First", isDefault = true)), + ) + + val exception = shouldThrow { + FeatureFlagModel( + model.className, + model.options, + supervisor = Supervisor(model, model.options.first()), + ) + } + + exception shouldHaveMessage "io.mehow.FeatureA cannot supervise itself" + } +}) diff --git a/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/OptionFactoryModelSpec.kt b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/OptionFactoryModelSpec.kt new file mode 100644 index 000000000..a5f07be7c --- /dev/null +++ b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/OptionFactoryModelSpec.kt @@ -0,0 +1,302 @@ +package io.mehow.laboratory.generator + +import com.squareup.kotlinpoet.ClassName +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.throwable.shouldHaveMessage +import io.kotest.property.Arb +import io.kotest.property.arbitrary.stringPattern +import io.kotest.property.checkAll +import io.mehow.laboratory.generator.Visibility.Internal +import io.mehow.laboratory.generator.Visibility.Public +import io.mehow.laboratory.generator.test.shouldSpecify +import kotlin.DeprecationLevel.ERROR +import kotlin.DeprecationLevel.HIDDEN +import kotlin.DeprecationLevel.WARNING + +class OptionFactoryModelSpec : FunSpec({ + test("can be internal") { + val features = listOf( + FeatureFlagModel( + className = ClassName("io.mehow", "FeatureA"), + options = listOf(FeatureFlagOption("OneA", isDefault = true), FeatureFlagOption("OneB")), + key = "FeatureA", + ), + FeatureFlagModel( + className = ClassName("io.mehow", "FeatureB"), + options = listOf(FeatureFlagOption("TwoA", isDefault = true)), + ), + FeatureFlagModel( + className = ClassName("io.mehow.c", "FeatureC"), + options = listOf(FeatureFlagOption("ThreeA", isDefault = true), FeatureFlagOption("ThreeB")), + key = "FeatureC", + ), + ) + val model = OptionFactoryModel( + ClassName("io.mehow", "GeneratedOptionFactory"), + features, + visibility = Internal, + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.c.FeatureC + |import io.mehow.laboratory.Feature + |import io.mehow.laboratory.OptionFactory + |import kotlin.String + | + |internal fun OptionFactory.Companion.generated(): OptionFactory = GeneratedOptionFactory + | + |private object GeneratedOptionFactory : OptionFactory { + | override fun create(key: String, name: String): Feature<*>? = when (key) { + | "FeatureA" -> when (name) { + | "OneA" -> FeatureA.OneA + | "OneB" -> FeatureA.OneB + | else -> null + | } + | "FeatureC" -> when (name) { + | "ThreeA" -> FeatureC.ThreeA + | "ThreeB" -> FeatureC.ThreeB + | else -> null + | } + | "io.mehow.FeatureB" -> when (name) { + | "TwoA" -> FeatureB.TwoA + | else -> null + | } + | else -> null + | } + |} + | + """.trimMargin() + } + + test("can be public") { + val features = listOf( + FeatureFlagModel( + className = ClassName("io.mehow", "FeatureA"), + options = listOf(FeatureFlagOption("OneA", isDefault = true), FeatureFlagOption("OneB")), + key = "FeatureA", + ), + FeatureFlagModel( + className = ClassName("io.mehow", "FeatureB"), + options = listOf(FeatureFlagOption("TwoA", isDefault = true)), + ), + FeatureFlagModel( + className = ClassName("io.mehow.c", "FeatureC"), + options = listOf(FeatureFlagOption("ThreeA", isDefault = true), FeatureFlagOption("ThreeB")), + key = "FeatureC", + ), + ) + val model = OptionFactoryModel( + ClassName("io.mehow", "GeneratedOptionFactory"), + features, + visibility = Public, + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.c.FeatureC + |import io.mehow.laboratory.Feature + |import io.mehow.laboratory.OptionFactory + |import kotlin.String + | + |public fun OptionFactory.Companion.generated(): OptionFactory = GeneratedOptionFactory + | + |private object GeneratedOptionFactory : OptionFactory { + | override fun create(key: String, name: String): Feature<*>? = when (key) { + | "FeatureA" -> when (name) { + | "OneA" -> FeatureA.OneA + | "OneB" -> FeatureA.OneB + | else -> null + | } + | "FeatureC" -> when (name) { + | "ThreeA" -> FeatureC.ThreeA + | "ThreeB" -> FeatureC.ThreeB + | else -> null + | } + | "io.mehow.FeatureB" -> when (name) { + | "TwoA" -> FeatureB.TwoA + | else -> null + | } + | else -> null + | } + |} + | + """.trimMargin() + } + + test("is optimized when there are no features") { + val model = OptionFactoryModel( + ClassName("io.mehow", "GeneratedOptionFactory"), + features = emptyList(), + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import io.mehow.laboratory.OptionFactory + |import kotlin.String + | + |internal fun OptionFactory.Companion.generated(): OptionFactory = GeneratedOptionFactory + | + |private object GeneratedOptionFactory : OptionFactory { + | override fun create(key: String, name: String): Feature<*>? = null + |} + | + """.trimMargin() + } + + test("suppresses usage of deprecated features") { + val features = listOf( + FeatureFlagModel( + className = ClassName("io.mehow", "FeatureA"), + options = listOf(FeatureFlagOption("OneA", isDefault = true)), + ), + FeatureFlagModel( + className = ClassName("io.mehow", "FeatureB"), + options = listOf(FeatureFlagOption("TwoA", isDefault = true)), + deprecation = Deprecation("message", WARNING), + ), + FeatureFlagModel( + className = ClassName("io.mehow", "FeatureC"), + options = listOf(FeatureFlagOption("ThreeA", isDefault = true)), + deprecation = Deprecation("message", ERROR), + ), + FeatureFlagModel( + className = ClassName("io.mehow", "FeatureD"), + options = listOf(FeatureFlagOption("FourA", isDefault = true)), + deprecation = Deprecation("message", HIDDEN), + ), + ) + val model = OptionFactoryModel( + ClassName("io.mehow", "GeneratedOptionFactory"), + features, + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.Feature + |import io.mehow.laboratory.OptionFactory + |import kotlin.String + |import kotlin.Suppress + | + |internal fun OptionFactory.Companion.generated(): OptionFactory = GeneratedOptionFactory + | + |private object GeneratedOptionFactory : OptionFactory { + | override fun create(key: String, name: String): Feature<*>? = when (key) { + | "io.mehow.FeatureA" -> when (name) { + | "OneA" -> FeatureA.OneA + | else -> null + | } + | "io.mehow.FeatureB" -> @Suppress("DEPRECATION") when (name) { + | "TwoA" -> FeatureB.TwoA + | else -> null + | } + | "io.mehow.FeatureC" -> @Suppress("DEPRECATION_ERROR") when (name) { + | "ThreeA" -> FeatureC.ThreeA + | else -> null + | } + | "io.mehow.FeatureD" -> @Suppress("DEPRECATION_ERROR") when (name) { + | "FourA" -> FeatureD.FourA + | else -> null + | } + | else -> null + | } + |} + | + """.trimMargin() + } + + test("fails to generate features with duplicate keys") { + checkAll( + Arb.stringPattern("[a-z](0)([a-z]{0,10})"), + Arb.stringPattern("[a-z](1)([a-z]{0,10})"), + ) { first, second -> + val features = listOf( + FeatureFlagModel( + className = ClassName("io.mehow1", first), + options = listOf(FeatureFlagOption("First", isDefault = true)), + key = "FeatureA", + ), + FeatureFlagModel( + className = ClassName("io.mehow1", second), + options = listOf(FeatureFlagOption("First", isDefault = true)), + key = "FeatureA", + ), + FeatureFlagModel( + className = ClassName("io.mehow2", "${second}A"), + options = listOf(FeatureFlagOption("First", isDefault = true)), + ), + FeatureFlagModel( + className = ClassName("io.mehow2", "${second}B"), + options = listOf(FeatureFlagOption("First", isDefault = true)), + ), + FeatureFlagModel( + className = ClassName("io.mehow2", "${second}C"), + options = listOf(FeatureFlagOption("First", isDefault = true)), + key = "FeatureB", + ), + FeatureFlagModel( + className = ClassName("io.mehow3", first), + options = listOf(FeatureFlagOption("First", isDefault = true)), + key = "FeatureC", + ), + FeatureFlagModel( + className = ClassName("io.mehow3", second), + options = listOf(FeatureFlagOption("First", isDefault = true)), + key = "FeatureB", + ), + ) + + val exception = shouldThrow { + OptionFactoryModel(ClassName("io.mehow", "GeneratedOptionFactory"), features) + } + + exception shouldHaveMessage """ + |Feature flags must have unique keys. Found following duplicates: + | - FeatureA: [io.mehow1.$first, io.mehow1.$second] + | - FeatureB: [io.mehow2.${second}C, io.mehow3.$second] + """.trimMargin() + } + } + + test("fails to generate features with keys matching fqcn") { + checkAll( + Arb.stringPattern("[a-z](0)([a-z]{0,10})"), + Arb.stringPattern("[a-z](0)([a-z]{0,10})"), + ) { packageName, simpleName -> + val features = listOf( + FeatureFlagModel( + className = ClassName(packageName, simpleName), + options = listOf(FeatureFlagOption("First", isDefault = true)), + ), + FeatureFlagModel( + className = ClassName("io.mehow", "FeatureName"), + options = listOf(FeatureFlagOption("First", isDefault = true)), + key = "$packageName.$simpleName", + ), + ) + + val exception = shouldThrow { + OptionFactoryModel(ClassName("io.mehow", "GeneratedOptionFactory"), features) + } + + exception shouldHaveMessage """ + |Feature flags must have unique keys. Found following duplicates: + | - $packageName.$simpleName: [$packageName.$simpleName, io.mehow.FeatureName] + """.trimMargin() + } + } +}) diff --git a/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/SourcedFeatureStorageModelSpec.kt b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/SourcedFeatureStorageModelSpec.kt new file mode 100644 index 000000000..d36c42fa3 --- /dev/null +++ b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/SourcedFeatureStorageModelSpec.kt @@ -0,0 +1,264 @@ +package io.mehow.laboratory.generator + +import com.squareup.kotlinpoet.ClassName +import io.kotest.core.spec.style.FunSpec +import io.mehow.laboratory.generator.Visibility.Internal +import io.mehow.laboratory.generator.Visibility.Public +import io.mehow.laboratory.generator.test.shouldSpecify +import java.util.Locale + +class SourcedFeatureStorageModelSpec : FunSpec({ + test("can be internal") { + val model = SourcedFeatureStorageModel( + ClassName("io.mehow", "SourcedGeneratedFeatureStorage"), + sourceNames = listOf("Firebase", "S3"), + visibility = Internal, + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.FeatureStorage + |import io.mehow.laboratory.FeatureStorage.Companion.sourced + |import kotlin.String + |import kotlin.collections.Map + |import kotlin.collections.emptyMap + |import kotlin.collections.plus + |import kotlin.to + | + |internal fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage): FirebaseStep = + | Builder(localSource, emptyMap()) + | + |internal interface FirebaseStep { + | public fun firebaseSource(source: FeatureStorage): S3Step + |} + | + |internal interface S3Step { + | public fun s3Source(source: FeatureStorage): BuildingStep + |} + | + |internal interface BuildingStep { + | public fun build(): FeatureStorage + |} + | + |private data class Builder( + | private val localSource: FeatureStorage, + | private val remoteSources: Map, + |) : FirebaseStep, S3Step, BuildingStep { + | override fun firebaseSource(source: FeatureStorage): S3Step = copy( + | remoteSources = remoteSources + ("Firebase" to source) + | ) + | + | override fun s3Source(source: FeatureStorage): BuildingStep = copy( + | remoteSources = remoteSources + ("S3" to source) + | ) + | + | override fun build(): FeatureStorage = sourced(localSource, remoteSources) + |} + | + """.trimMargin() + } + + test("can be public") { + val model = SourcedFeatureStorageModel( + ClassName("io.mehow", "SourcedGeneratedFeatureStorage"), + sourceNames = listOf("Firebase", "S3"), + visibility = Public, + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.FeatureStorage + |import io.mehow.laboratory.FeatureStorage.Companion.sourced + |import kotlin.String + |import kotlin.collections.Map + |import kotlin.collections.emptyMap + |import kotlin.collections.plus + |import kotlin.to + | + |public fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage): FirebaseStep = + | Builder(localSource, emptyMap()) + | + |public interface FirebaseStep { + | public fun firebaseSource(source: FeatureStorage): S3Step + |} + | + |public interface S3Step { + | public fun s3Source(source: FeatureStorage): BuildingStep + |} + | + |public interface BuildingStep { + | public fun build(): FeatureStorage + |} + | + |private data class Builder( + | private val localSource: FeatureStorage, + | private val remoteSources: Map, + |) : FirebaseStep, S3Step, BuildingStep { + | override fun firebaseSource(source: FeatureStorage): S3Step = copy( + | remoteSources = remoteSources + ("Firebase" to source) + | ) + | + | override fun s3Source(source: FeatureStorage): BuildingStep = copy( + | remoteSources = remoteSources + ("S3" to source) + | ) + | + | override fun build(): FeatureStorage = sourced(localSource, remoteSources) + |} + | + """.trimMargin() + } + + test("ignores duplicate sources") { + val model = SourcedFeatureStorageModel( + ClassName("io.mehow", "SourcedGeneratedFeatureStorage"), + sourceNames = listOf("Foo", "Bar", "Baz", "Foo", "Baz", "Foo"), + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.FeatureStorage + |import io.mehow.laboratory.FeatureStorage.Companion.sourced + |import kotlin.String + |import kotlin.collections.Map + |import kotlin.collections.emptyMap + |import kotlin.collections.plus + |import kotlin.to + | + |internal fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage): BarStep = + | Builder(localSource, emptyMap()) + | + |internal interface BarStep { + | public fun barSource(source: FeatureStorage): BazStep + |} + | + |internal interface BazStep { + | public fun bazSource(source: FeatureStorage): FooStep + |} + | + |internal interface FooStep { + | public fun fooSource(source: FeatureStorage): BuildingStep + |} + | + |internal interface BuildingStep { + | public fun build(): FeatureStorage + |} + | + |private data class Builder( + | private val localSource: FeatureStorage, + | private val remoteSources: Map, + |) : BarStep, BazStep, FooStep, BuildingStep { + | override fun barSource(source: FeatureStorage): BazStep = copy( + | remoteSources = remoteSources + ("Bar" to source) + | ) + | + | override fun bazSource(source: FeatureStorage): FooStep = copy( + | remoteSources = remoteSources + ("Baz" to source) + | ) + | + | override fun fooSource(source: FeatureStorage): BuildingStep = copy( + | remoteSources = remoteSources + ("Foo" to source) + | ) + | + | override fun build(): FeatureStorage = sourced(localSource, remoteSources) + |} + | + """.trimMargin() + } + + test("ignores local source") { + val localPermutations = (0b00000..0b11111).map { + listOf(it and 0b00001, it and 0b00010, it and 0b00100, it and 0b01000, it and 0b10000) + .map { mask -> mask != 0 } + .mapIndexed { index, mask -> + val chars = "local"[index].toString() + if (mask) chars else chars.replaceFirstChar { char -> char.titlecase(Locale.ROOT) } + }.joinToString(separator = "") + } + val model = SourcedFeatureStorageModel( + ClassName("io.mehow", "SourcedGeneratedFeatureStorage"), + sourceNames = localPermutations + "Foo", + visibility = Public, + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.FeatureStorage + |import io.mehow.laboratory.FeatureStorage.Companion.sourced + |import kotlin.String + |import kotlin.collections.Map + |import kotlin.collections.emptyMap + |import kotlin.collections.plus + |import kotlin.to + | + |public fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage): FooStep = + | Builder(localSource, emptyMap()) + | + |public interface FooStep { + | public fun fooSource(source: FeatureStorage): BuildingStep + |} + | + |public interface BuildingStep { + | public fun build(): FeatureStorage + |} + | + |private data class Builder( + | private val localSource: FeatureStorage, + | private val remoteSources: Map, + |) : FooStep, BuildingStep { + | override fun fooSource(source: FeatureStorage): BuildingStep = copy( + | remoteSources = remoteSources + ("Foo" to source) + | ) + | + | override fun build(): FeatureStorage = sourced(localSource, remoteSources) + |} + | + """.trimMargin() + } + + test("can have no external sources") { + val model = SourcedFeatureStorageModel( + ClassName("io.mehow", "SourcedGeneratedFeatureStorage"), + sourceNames = emptyList(), + visibility = Public, + ) + + val fileSpec = model.prepare() + + fileSpec shouldSpecify """ + |package io.mehow + | + |import io.mehow.laboratory.FeatureStorage + |import io.mehow.laboratory.FeatureStorage.Companion.sourced + |import kotlin.String + |import kotlin.collections.Map + |import kotlin.collections.emptyMap + | + |public fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage): BuildingStep = + | Builder(localSource, emptyMap()) + | + |public interface BuildingStep { + | public fun build(): FeatureStorage + |} + | + |private data class Builder( + | private val localSource: FeatureStorage, + | private val remoteSources: Map, + |) : BuildingStep { + | override fun build(): FeatureStorage = sourced(localSource, remoteSources) + |} + | + """.trimMargin() + } +}) diff --git a/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/SupervisorSpec.kt b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/SupervisorSpec.kt new file mode 100644 index 000000000..0132e0ef8 --- /dev/null +++ b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/SupervisorSpec.kt @@ -0,0 +1,42 @@ +package io.mehow.laboratory.generator + +import com.squareup.kotlinpoet.ClassName +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.throwable.shouldHaveMessage +import io.kotest.property.Arb +import io.kotest.property.arbitrary.stringPattern +import io.kotest.property.checkAll + +class SupervisorSpec : FunSpec({ + test("does not fail when it has an option") { + checkAll(Arb.stringPattern("[a-z](0)([a-z]{0,10})")) { optionName -> + val option = FeatureFlagOption(optionName, isDefault = true) + val feature = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(option), + ) + + shouldNotThrowAny { + Supervisor(feature, option) + } + } + } + + test("fails when it has no options") { + checkAll(Arb.stringPattern("[a-z](0)([a-z]{0,10})")) { optionName -> + val feature = FeatureFlagModel( + ClassName("io.mehow", "FeatureA"), + listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), + ) + val option = FeatureFlagOption(optionName, isDefault = true) + + val exception = shouldThrow { + Supervisor(feature, option) + } + + exception shouldHaveMessage "Feature flag io.mehow.FeatureA does not contain option $optionName" + } + } +}) diff --git a/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/test/KotlinPoetMatchers.kt b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/test/KotlinPoetMatchers.kt new file mode 100644 index 000000000..422b0e664 --- /dev/null +++ b/laboratory/generator/src/test/kotlin/io/mehow/laboratory/generator/test/KotlinPoetMatchers.kt @@ -0,0 +1,17 @@ +package io.mehow.laboratory.generator.test + +import com.squareup.kotlinpoet.FileSpec +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.withClue +import io.kotest.matchers.shouldBe + +infix fun FileSpec.shouldSpecify(value: String) = assertSoftly { + val actualLines = toString().split("\n") + val expectedLines = value.split("\n") + val maxSize = maxOf(actualLines.size, expectedLines.size) + repeat(maxSize) { line -> + withClue("Line $line does not match") { + actualLines.getOrNull(line) shouldBe expectedLines.getOrNull(line) + } + } +} diff --git a/library/gradle-plugin/api/gradle-plugin.api b/laboratory/gradle-plugin/api/gradle-plugin.api similarity index 98% rename from library/gradle-plugin/api/gradle-plugin.api rename to laboratory/gradle-plugin/api/gradle-plugin.api index 706a2a4ee..2c87b7e97 100644 --- a/library/gradle-plugin/api/gradle-plugin.api +++ b/laboratory/gradle-plugin/api/gradle-plugin.api @@ -6,6 +6,7 @@ public final class io/mehow/laboratory/gradle/DeprecationLevel : java/lang/Enum public static final field Error Lio/mehow/laboratory/gradle/DeprecationLevel; public static final field Hidden Lio/mehow/laboratory/gradle/DeprecationLevel; public static final field Warning Lio/mehow/laboratory/gradle/DeprecationLevel; + public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lio/mehow/laboratory/gradle/DeprecationLevel; public static fun values ()[Lio/mehow/laboratory/gradle/DeprecationLevel; } diff --git a/laboratory/gradle-plugin/build.gradle.kts b/laboratory/gradle-plugin/build.gradle.kts new file mode 100644 index 000000000..738d6636f --- /dev/null +++ b/laboratory/gradle-plugin/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + `java-gradle-plugin` + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.mavenPublish) + alias(libs.plugins.spotless) + alias(libs.plugins.detekt) + alias(libs.plugins.dokka) + alias(libs.plugins.buildconfig) +} + +val pluginName = "laboratory" + +gradlePlugin { + plugins { + create(pluginName) { + id = "io.mehow.laboratory" + implementationClass = "io.mehow.laboratory.gradle.LaboratoryPlugin" + } + } +} + +buildConfig { + useKotlinOutput { + internalVisibility = true + topLevelConstants = true + } + packageName("io.mehow.laboratory.gradle") + buildConfigField("String", "LibraryVersion", "\"${project.version}\"") + buildConfigField("String", "PluginName", "\"${pluginName}\"") +} + +tasks.withType().configureEach { + useJUnitPlatform() +} + +val fixtureClasspath: Configuration by configurations.creating + +tasks.withType().configureEach { + pluginClasspath.from(fixtureClasspath) +} + +dependencies { + compileOnly(libs.android.gradlePlugin) + + implementation(projects.laboratory.generator) + implementation(libs.kotlin.gradlePlugin) + + testImplementation(libs.kotest.runner.junit5) + testImplementation(libs.kotest.assertions) + + fixtureClasspath(libs.android.gradlePlugin) +} diff --git a/library/gradle-plugin/gradle.properties b/laboratory/gradle-plugin/gradle.properties similarity index 62% rename from library/gradle-plugin/gradle.properties rename to laboratory/gradle-plugin/gradle.properties index ef823004c..3f7284596 100644 --- a/library/gradle-plugin/gradle.properties +++ b/laboratory/gradle-plugin/gradle.properties @@ -1,3 +1,3 @@ POM_ARTIFACT_ID=laboratory-gradle-plugin -POM_NAME=Laboratory (Gradle plugin) +POM_NAME=Laboratory (Gradle Plugin) POM_PACKAGING=jar diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/ChildFeatureFlagsInput.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/ChildFeatureFlagsInput.kt similarity index 90% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/ChildFeatureFlagsInput.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/ChildFeatureFlagsInput.kt index 1c06635a6..b3d5f31dc 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/ChildFeatureFlagsInput.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/ChildFeatureFlagsInput.kt @@ -17,7 +17,10 @@ public class ChildFeatureFlagsInput internal constructor( /** * Generates a new supervised feature flag. */ - public fun feature(name: String, action: Action) { + public fun feature( + name: String, + action: Action, + ) { mutableFeatureInputs += FeatureFlagInput(name, packageNameProvider, supervisor).let { input -> action.execute(input) return@let input diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/DeprecationLevel.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/DeprecationLevel.kt similarity index 100% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/DeprecationLevel.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/DeprecationLevel.kt diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFactoryInput.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFactoryInput.kt similarity index 73% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFactoryInput.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFactoryInput.kt index d5b737b06..71fd215ff 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFactoryInput.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFactoryInput.kt @@ -22,9 +22,12 @@ public class FeatureFactoryInput internal constructor( */ public var packageName: String? = null - internal fun toModel(features: List, simpleName: String) = FeatureFactoryModel( - visibility = if (isPublic) Public else Internal, - className = ClassName(packageName ?: packageNameProvider(), simpleName), - features = features, + internal fun toModel( + features: List, + simpleName: String, + ) = FeatureFactoryModel( + visibility = if (isPublic) Public else Internal, + className = ClassName(packageName ?: packageNameProvider(), simpleName), + features = features, ) } diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFactoryTask.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFactoryTask.kt similarity index 99% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFactoryTask.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFactoryTask.kt index e659fb87b..828b2e374 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFactoryTask.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFactoryTask.kt @@ -8,10 +8,15 @@ import java.io.File public open class FeatureFactoryTask : DefaultTask() { @get:Internal internal lateinit var factory: FeatureFactoryInput + @get:Internal internal lateinit var features: List + @get:Internal internal lateinit var codeGenDir: File + @get:Internal internal lateinit var factoryClassName: String + @get:Internal internal lateinit var factoryFunctionName: String + @get:Internal internal lateinit var featureModelsMapper: (List) -> List @TaskAction public fun generateFeatureFactory() { diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFlagInput.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFlagInput.kt similarity index 82% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFlagInput.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFlagInput.kt index 74a56124f..2c9ec687a 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFlagInput.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFlagInput.kt @@ -48,7 +48,10 @@ public class FeatureFlagInput internal constructor( /** * Adds a feature option and configures features flags supervised by it. */ - public fun withOption(name: String, action: Action): Unit = + public fun withOption( + name: String, + action: Action, + ): Unit = withOption(name, isDefault = false, action) /** @@ -61,12 +64,19 @@ public class FeatureFlagInput internal constructor( * Adds a feature value that will be used as a default value and configures features flags supervised by it. * Exactly one value must be set with this method. */ - public fun withDefaultOption(name: String, action: Action): Unit = + public fun withDefaultOption( + name: String, + action: Action, + ): Unit = withOption(name, isDefault = true, action) private val childFeatureInputs = mutableListOf() - private fun withOption(name: String, isDefault: Boolean, action: Action) { + private fun withOption( + name: String, + isDefault: Boolean, + action: Action, + ) { val option = FeatureFlagOption(name, isDefault) options += option val packageNameProvider = { packageName ?: packageNameProvider() } @@ -101,19 +111,22 @@ public class FeatureFlagInput internal constructor( /** * Annotates a feature flag as deprecated. */ - @JvmOverloads public fun deprecated(message: String, level: DeprecationLevel = Warning) { + @JvmOverloads public fun deprecated( + message: String, + level: DeprecationLevel = Warning, + ) { deprecation = Deprecation(message, level.kotlinLevel) } private fun toModel() = FeatureFlagModel( - visibility = if (isPublic) Public else Internal, - className = ClassName(packageName ?: packageNameProvider(), name), - options = options, - sourceOptions = sources, - key = key, - description = description.orEmpty(), - deprecation = deprecation, - supervisor = supervisor?.invoke(), + visibility = if (isPublic) Public else Internal, + className = ClassName(packageName ?: packageNameProvider(), name), + options = options, + sourceOptions = sources, + key = key, + description = description.orEmpty(), + deprecation = deprecation, + supervisor = supervisor?.invoke(), ) internal fun toModels(): List = diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFlagsTask.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFlagsTask.kt similarity index 99% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFlagsTask.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFlagsTask.kt index 2ca95d8fd..75f0954c0 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/FeatureFlagsTask.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/FeatureFlagsTask.kt @@ -7,6 +7,7 @@ import java.io.File public open class FeatureFlagsTask : DefaultTask() { @get:Internal internal lateinit var features: List + @get:Internal internal lateinit var codeGenDir: File @TaskAction public fun generateFeatureFlags() { diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/LaboratoryExtension.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/LaboratoryExtension.kt similarity index 97% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/LaboratoryExtension.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/LaboratoryExtension.kt index a1951d3d6..69a782807 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/LaboratoryExtension.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/LaboratoryExtension.kt @@ -6,7 +6,6 @@ import org.gradle.api.Project /** * An entry point for configuration of feature flags code generation. */ -@Suppress("UnnecessaryAbstractClass") // Created by Gradle public abstract class LaboratoryExtension { /** * Sets package name for any factories or feature flags defined in this extension. @@ -33,7 +32,10 @@ public abstract class LaboratoryExtension { /** * Generates a new feature in this module. */ - public fun feature(name: String, action: Action) { + public fun feature( + name: String, + action: Action, + ) { mutableFeatureInputs += FeatureFlagInput(name, packageNameProvider).let { input -> action.execute(input) return@let input diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/LaboratoryPlugin.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/LaboratoryPlugin.kt similarity index 84% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/LaboratoryPlugin.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/LaboratoryPlugin.kt index c266dbfd3..7f5720c52 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/LaboratoryPlugin.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/LaboratoryPlugin.kt @@ -1,7 +1,6 @@ package io.mehow.laboratory.gradle import io.mehow.laboratory.generator.FeatureFlagModel -import io.mehow.laboratory.laboratoryVersion import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.Task @@ -9,15 +8,13 @@ import org.gradle.api.tasks.TaskProvider import java.io.File import java.util.concurrent.atomic.AtomicBoolean -private const val pluginName = "laboratory" - public class LaboratoryPlugin : Plugin { private val hasAndroid = AtomicBoolean(false) private val hasKotlin = AtomicBoolean(false) private lateinit var extension: LaboratoryExtension override fun apply(project: Project) { - extension = project.extensions.create(pluginName, LaboratoryExtension::class.java).apply { + extension = project.extensions.create(PluginName, LaboratoryExtension::class.java).apply { this.project = project } project.setUpKotlinProject() @@ -64,9 +61,9 @@ public class LaboratoryPlugin : Plugin { } private fun Project.registerFeaturesTask() = afterEvaluate { - val codeGenDir = File("$buildDir/generated/laboratory/code/feature-flags") + val codeGenDir = File("${layout.buildDirectory.get()}/generated/laboratory/code/feature-flags") val featuresTask = registerTask("generateFeatureFlags") { task -> - task.group = pluginName + task.group = PluginName task.description = "Generate Laboratory features." task.features = extension.featureInputs task.codeGenDir = codeGenDir @@ -77,9 +74,9 @@ public class LaboratoryPlugin : Plugin { private fun Project.registerFeatureFactoryTask() = afterEvaluate { val factoryInput = extension.factoryInput ?: return@afterEvaluate - val codeGenDir = File("${project.buildDir}/generated/laboratory/code/feature-factory") + val codeGenDir = File("${layout.buildDirectory.get()}/generated/laboratory/code/feature-factory") val factoryTask = registerTask("generateFeatureFactory") { task -> - task.group = pluginName + task.group = PluginName task.description = "Generate Laboratory feature factory." task.factory = factoryInput task.features = extension.factoryFeatureInputs @@ -94,9 +91,9 @@ public class LaboratoryPlugin : Plugin { private fun Project.registerSourcedFeatureStorageTask() = afterEvaluate { val storageInput = extension.storageInput ?: return@afterEvaluate - val codeGenDir = File("${project.buildDir}/generated/laboratory/code/sourced-storage") + val codeGenDir = File("${layout.buildDirectory.get()}/generated/laboratory/code/sourced-storage") val storageTask = registerTask("generateSourcedFeatureStorage") { task -> - task.group = pluginName + task.group = PluginName task.description = "Generate Laboratory sourced feature storage." task.storage = storageInput task.features = extension.factoryFeatureInputs @@ -108,9 +105,9 @@ public class LaboratoryPlugin : Plugin { private fun Project.registerFeatureSourcesFactoryTask() = afterEvaluate { val factoryInput = extension.featureSourcesFactory ?: return@afterEvaluate - val codeGenDir = File("${project.buildDir}/generated/laboratory/code/feature-source-factory") + val codeGenDir = File("${layout.buildDirectory.get()}/generated/laboratory/code/feature-source-factory") val factoryTask = registerTask("generateFeatureSourceFactory") { task -> - task.group = pluginName + task.group = PluginName task.description = "Generate Laboratory feature sources factory." task.factory = factoryInput task.features = extension.factoryFeatureInputs @@ -125,9 +122,9 @@ public class LaboratoryPlugin : Plugin { private fun Project.registerOptionFactoryTask() = afterEvaluate { val factoryInput = extension.optionFactoryInput ?: return@afterEvaluate - val codeGenDir = File("${project.buildDir}/generated/laboratory/code/option-factory") + val codeGenDir = File("${layout.buildDirectory.get()}/generated/laboratory/code/option-factory") val factoryTask = registerTask("generateOptionFactory") { task -> - task.group = pluginName + task.group = PluginName task.description = "Generate Laboratory option factory." task.factory = factoryInput task.features = extension.factoryFeatureInputs @@ -137,7 +134,7 @@ public class LaboratoryPlugin : Plugin { } private fun Project.addLaboratoryDependency() { - dependencies.add("api", "io.mehow.laboratory:laboratory:$laboratoryVersion") + dependencies.add("api", "io.mehow.laboratory:laboratory:$LibraryVersion") } private inline fun Project.registerTask( @@ -147,7 +144,10 @@ public class LaboratoryPlugin : Plugin { return tasks.register(name, T::class.java) { action(it) } } - private fun Project.addSourceSets(task: TaskProvider, dir: File) { + private fun Project.addSourceSets( + task: TaskProvider, + dir: File, + ) { if (hasAndroid.get()) { task.contributeToAndroidSourceSets(dir, this) } else { diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/OptionFactoryInput.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/OptionFactoryInput.kt similarity index 83% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/OptionFactoryInput.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/OptionFactoryInput.kt index 3366aedb1..551d9ff10 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/OptionFactoryInput.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/OptionFactoryInput.kt @@ -23,8 +23,8 @@ public class OptionFactoryInput internal constructor( public var packageName: String? = null internal fun toModel(features: List) = OptionFactoryModel( - visibility = if (isPublic) Public else Internal, - className = ClassName(packageName ?: packageNameProvider(), "GeneratedOptionFactory"), - features = features, + visibility = if (isPublic) Public else Internal, + className = ClassName(packageName ?: packageNameProvider(), "GeneratedOptionFactory"), + features = features, ) } diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/OptionFactoryTask.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/OptionFactoryTask.kt similarity index 99% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/OptionFactoryTask.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/OptionFactoryTask.kt index 0c5ca9783..3faacb353 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/OptionFactoryTask.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/OptionFactoryTask.kt @@ -7,7 +7,9 @@ import java.io.File public open class OptionFactoryTask : DefaultTask() { @get:Internal internal lateinit var factory: OptionFactoryInput + @get:Internal internal lateinit var features: List + @get:Internal internal lateinit var codeGenDir: File @TaskAction public fun generateSourcedFeatureStorage() { diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/PackageNameProvider.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/PackageNameProvider.kt similarity index 100% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/PackageNameProvider.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/PackageNameProvider.kt diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/SourceSetContribution.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/SourceSetContribution.kt similarity index 84% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/SourceSetContribution.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/SourceSetContribution.kt index 557a0efd1..55d1e39ac 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/SourceSetContribution.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/SourceSetContribution.kt @@ -3,7 +3,6 @@ package io.mehow.laboratory.gradle import com.android.build.gradle.AppExtension import com.android.build.gradle.BaseExtension import com.android.build.gradle.LibraryExtension -import com.android.build.gradle.api.BaseVariant import org.gradle.api.DomainObjectSet import org.gradle.api.GradleException import org.gradle.api.Project @@ -14,12 +13,18 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.io.File import java.util.Locale -internal fun TaskProvider.contributeToSourceSets(dir: File, project: Project) { +internal fun TaskProvider.contributeToSourceSets( + dir: File, + project: Project, +) { makeKotlinDependOnTask(project) contributeToKotlin(dir, project) } -internal fun TaskProvider.contributeToAndroidSourceSets(dir: File, project: Project) { +internal fun TaskProvider.contributeToAndroidSourceSets( + dir: File, + project: Project, +) { makeKotlinDependOnTask(project) contributeToAndroid(dir, project) } @@ -30,13 +35,19 @@ private fun TaskProvider.makeKotlinDependOnTask(project: Project) { } } -private fun contributeToKotlin(dir: File, project: Project) { +private fun contributeToKotlin( + dir: File, + project: Project, +) { val sourceSets = project.extensions.getByType(KotlinProjectExtension::class.java).sourceSets val kotlinSourceSet = sourceSets.getByName("main").kotlin kotlinSourceSet.srcDir(dir) } -private fun TaskProvider.contributeToAndroid(dir: File, project: Project) { +private fun TaskProvider.contributeToAndroid( + dir: File, + project: Project, +) { val extension = requireNotNull(project.extensions.findByType(BaseExtension::class.java)) { "Did not find BaseExtension in Android project" } @@ -62,7 +73,8 @@ private fun TaskProvider.contributeToAndroid(dir: File, project: Proje } // Copied from SQLDelight with small modifications. -private val BaseExtension.variants: DomainObjectSet +@Suppress("DEPRECATION") +private val BaseExtension.variants: DomainObjectSet get() = when (this) { is AppExtension -> applicationVariants is LibraryExtension -> libraryVariants diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/SourcedFeatureStorageInput.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/SourcedFeatureStorageInput.kt similarity index 81% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/SourcedFeatureStorageInput.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/SourcedFeatureStorageInput.kt index 54937529a..d14ae5ac8 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/SourcedFeatureStorageInput.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/SourcedFeatureStorageInput.kt @@ -22,8 +22,8 @@ public class SourcedFeatureStorageInput internal constructor( public var packageName: String? = null internal fun toModel(sourceNames: List) = SourcedFeatureStorageModel( - visibility = if (isPublic) Public else Internal, - className = ClassName(packageName ?: packageNameProvider(), "SourcedGeneratedFeatureStorage"), - sourceNames = sourceNames, + visibility = if (isPublic) Public else Internal, + className = ClassName(packageName ?: packageNameProvider(), "SourcedGeneratedFeatureStorage"), + sourceNames = sourceNames, ) } diff --git a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/SourcedFeatureStorageTask.kt b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/SourcedFeatureStorageTask.kt similarity index 89% rename from library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/SourcedFeatureStorageTask.kt rename to laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/SourcedFeatureStorageTask.kt index 42df858c8..fdcc1880a 100644 --- a/library/gradle-plugin/src/main/java/io/mehow/laboratory/gradle/SourcedFeatureStorageTask.kt +++ b/laboratory/gradle-plugin/src/main/kotlin/io/mehow/laboratory/gradle/SourcedFeatureStorageTask.kt @@ -9,7 +9,9 @@ import java.io.File public open class SourcedFeatureStorageTask : DefaultTask() { @get:Internal internal lateinit var storage: SourcedFeatureStorageInput + @get:Internal internal lateinit var features: List + @get:Internal internal lateinit var codeGenDir: File @TaskAction public fun generateSourcedFeatureStorage() { @@ -20,7 +22,7 @@ public open class SourcedFeatureStorageTask : DefaultTask() { } private fun List.sourceNames(): List = mapNotNull(FeatureFlagModel::source) - .map(FeatureFlagModel::options) - .flatMap { it.toList() } - .map(FeatureFlagOption::name) + .map(FeatureFlagModel::options) + .flatMap { it.toList() } + .map(FeatureFlagOption::name) } diff --git a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateFeatureFactoryTaskSpec.kt b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateFeatureFactoryTaskSpec.kt similarity index 89% rename from library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateFeatureFactoryTaskSpec.kt rename to laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateFeatureFactoryTaskSpec.kt index a885f85c0..e6a50b3bf 100644 --- a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateFeatureFactoryTaskSpec.kt +++ b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateFeatureFactoryTaskSpec.kt @@ -1,6 +1,6 @@ package io.mehow.laboratory.gradle -import io.kotest.core.spec.style.StringSpec +import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.file.shouldExist import io.kotest.matchers.file.shouldNotExist import io.kotest.matchers.shouldBe @@ -9,18 +9,18 @@ import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome.FAILED import org.gradle.testkit.runner.TaskOutcome.SUCCESS -internal class GenerateFeatureFactoryTaskSpec : StringSpec({ +class GenerateFeatureFactoryTaskSpec : FunSpec({ lateinit var gradleRunner: GradleRunner cleanBuildDirs() beforeTest { gradleRunner = GradleRunner.create() - .withPluginClasspath() - .withArguments("generateFeatureFactory", "--stacktrace") + .withPluginClasspath() + .withArguments("generateFeatureFactory", "--stacktrace") } - "generates factory without any feature flags" { + test("generates factory without any feature flags") { val fixture = "factory-generate-empty".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -39,7 +39,7 @@ internal class GenerateFeatureFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory with feature flags" { + test("generates factory with feature flags") { val fixture = "factory-generate-features".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -62,7 +62,7 @@ internal class GenerateFeatureFactoryTaskSpec : StringSpec({ """.trimMargin() } - "uses implicit package name" { + test("uses implicit package name") { val fixture = "factory-package-name-implicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -73,7 +73,7 @@ internal class GenerateFeatureFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.implicit" } - "uses explicit package name" { + test("uses explicit package name") { val fixture = "factory-package-name-explicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -84,7 +84,7 @@ internal class GenerateFeatureFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.explicit" } - "overrides implicit package name" { + test("overrides implicit package name") { val fixture = "factory-package-name-explicit-override".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -95,7 +95,7 @@ internal class GenerateFeatureFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.explicit" } - "generates internal factory" { + test("generates internal factory") { val fixture = "factory-generate-internal".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -106,7 +106,7 @@ internal class GenerateFeatureFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "internal fun FeatureFactory.Companion.featureGenerated()" } - "generates public factory" { + test("generates public factory") { val fixture = "factory-generate-public".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -117,7 +117,7 @@ internal class GenerateFeatureFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "public fun FeatureFactory.Companion.featureGenerated()" } - "fails for feature flags with no options" { + test("fails for feature flags with no options") { val fixture = "factory-feature-flag-option-missing".toFixture() val result = gradleRunner.withProjectDir(fixture).buildAndFail() @@ -128,7 +128,7 @@ internal class GenerateFeatureFactoryTaskSpec : StringSpec({ fixture.featureFactoryFile("GeneratedFeatureFactory").shouldNotExist() } - "generates factory with feature flags from all modules" { + test("generates factory with feature flags from all modules") { val fixture = "factory-multi-module-generate-all".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -152,7 +152,7 @@ internal class GenerateFeatureFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory with feature flags only from included modules" { + test("generates factory with feature flags only from included modules") { val fixture = "factory-multi-module-generate-filtered".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -175,7 +175,7 @@ internal class GenerateFeatureFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory for Android project" { + test("generates factory for Android project") { val fixture = "factory-android-smoke".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -194,7 +194,7 @@ internal class GenerateFeatureFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory with supervised feature flags" { + test("generates factory with supervised feature flags") { val fixture = "factory-generate-supervised-feature-flags".toFixture() val result = gradleRunner.withProjectDir(fixture).build() diff --git a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateFeatureFlagsTaskSpec.kt b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateFeatureFlagsTaskSpec.kt similarity index 90% rename from library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateFeatureFlagsTaskSpec.kt rename to laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateFeatureFlagsTaskSpec.kt index 722ca931d..69fd288de 100644 --- a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateFeatureFlagsTaskSpec.kt +++ b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateFeatureFlagsTaskSpec.kt @@ -1,6 +1,6 @@ package io.mehow.laboratory.gradle -import io.kotest.core.spec.style.StringSpec +import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.file.shouldExist import io.kotest.matchers.file.shouldNotExist import io.kotest.matchers.shouldBe @@ -9,18 +9,18 @@ import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome.FAILED import org.gradle.testkit.runner.TaskOutcome.SUCCESS -internal class GenerateFeatureFlagsTaskSpec : StringSpec({ +class GenerateFeatureFlagsTaskSpec : FunSpec({ lateinit var gradleRunner: GradleRunner cleanBuildDirs() beforeTest { gradleRunner = GradleRunner.create() - .withPluginClasspath() - .withArguments("generateFeatureFlags", "--stacktrace") + .withPluginClasspath() + .withArguments("generateFeatureFlags", "--stacktrace") } - "generates single feature flag" { + test("generates single feature flag") { val fixture = "feature-flag-generate-single".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -42,7 +42,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "generates multiple feature flags" { + test("generates multiple feature flags") { val fixture = "feature-flag-generate-multiple".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -78,7 +78,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "generates a single feature flag with source" { + test("generates a single feature flag with source") { val fixture = "feature-flag-generate-sources-single".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -112,7 +112,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "generates an internal feature flag with source" { + test("generates an internal feature flag with source") { val fixture = "feature-flag-generate-sources-internal".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -146,7 +146,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "generates a public feature flag with source" { + test("generates a public feature flag with source") { val fixture = "feature-flag-generate-sources-public".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -180,7 +180,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "generates multiple feature flags with sources" { + test("generates multiple feature flags with sources") { val fixture = "feature-flag-generate-sources-multiple".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -254,7 +254,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "uses implicit package name" { + test("uses implicit package name") { val fixture = "feature-flag-package-name-implicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -265,7 +265,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ feature.readText() shouldContain "package io.mehow.implicit" } - "cascades implicit package name" { + test("cascades implicit package name") { val fixture = "feature-flag-package-name-implicit-cascading".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -277,7 +277,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ featureB.shouldExist() } - "uses last implicit package name for all features" { + test("uses last implicit package name for all features") { val fixture = "feature-flag-package-name-implicit-switching".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -293,7 +293,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ featureB.readText() shouldContain "package io.mehow.implicit.switch" } - "uses explicit package name" { + test("uses explicit package name") { val fixture = "feature-flag-package-name-explicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -304,7 +304,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ feature.readText() shouldContain "package io.mehow.explicit" } - "switches explicit package name" { + test("switches explicit package name") { val fixture = "feature-flag-package-name-explicit-switching".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -320,7 +320,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ featureB.readText() shouldContain "package io.mehow.explicit.switch" } - "overrides implicit package name" { + test("overrides implicit package name") { val fixture = "feature-flag-package-name-explicit-override".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -331,7 +331,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ feature.readText() shouldContain "package io.mehow.explicit" } - "generates internal feature flag" { + test("generates internal feature flag") { val fixture = "feature-flag-generate-internal".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -342,7 +342,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ feature.readText() shouldContain "internal enum class Feature" } - "generates public feature flag" { + test("generates public feature flag") { val fixture = "feature-flag-generate-public".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -353,7 +353,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ feature.readText() shouldContain "public enum class Feature" } - "generates features with the same options but different names" { + test("generates features with the same options but different names") { val fixture = "feature-flag-generate-option-name-common".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -387,7 +387,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "fails for features with no options" { + test("fails for features with no options") { val fixture = "feature-flag-option-missing".toFixture() val result = gradleRunner.withProjectDir(fixture).buildAndFail() @@ -399,7 +399,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ feature.shouldNotExist() } - "generates feature flag for Android project" { + test("generates feature flag for Android project") { val fixture = "feature-flag-android-smoke".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -420,7 +420,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "generates feature flag with a description" { + test("generates feature flag with a description") { val fixture = "feature-flag-generate-description".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -446,7 +446,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "generates deprecated feature flag" { + test("generates deprecated feature flag") { val fixture = "feature-flag-generate-deprecated".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -465,7 +465,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "generates deprecated feature flag with specified deprecation level" { + test("generates deprecated feature flag with specified deprecation level") { val fixture = "feature-flag-generate-deprecated-with-level".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -484,7 +484,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "generates supervised child feature flag" { + test("generates supervised child feature flag") { val fixture = "feature-flag-supervisor-generate-child".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -507,7 +507,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "generates supervised grandchild feature flag" { + test("generates supervised grandchild feature flag") { val fixture = "feature-flag-supervisor-generate-grandchild".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -530,7 +530,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "generates supervised multiple children feature flags" { + test("generates supervised multiple children feature flags") { val fixture = "feature-flag-supervisor-generate-multiple-children".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -568,7 +568,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ """.trimMargin() } - "supervised feature flag uses explicit package name" { + test("supervised feature flag uses explicit package name") { val fixture = "feature-flag-supervisor-package-name-explicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -579,7 +579,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ feature.readText() shouldContain "package io.mehow.explicit" } - "supervised feature flag uses implicit package name" { + test("supervised feature flag uses implicit package name") { val fixture = "feature-flag-supervisor-package-name-explicit-override".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -590,7 +590,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ feature.readText() shouldContain "package io.mehow.explicit" } - "supervised feature flag overrides implicit package name" { + test("supervised feature flag overrides implicit package name") { val fixture = "feature-flag-supervisor-package-name-implicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -601,7 +601,7 @@ internal class GenerateFeatureFlagsTaskSpec : StringSpec({ feature.readText() shouldContain "package io.mehow.implicit" } - "fails for feature supervising itself" { + test("fails for feature supervising itself") { val fixture = "feature-flag-supervisor-self-supervision".toFixture() val result = gradleRunner.withProjectDir(fixture).buildAndFail() diff --git a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateFeatureSourceFactoryTaskSpec.kt b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateFeatureSourceFactoryTaskSpec.kt similarity index 90% rename from library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateFeatureSourceFactoryTaskSpec.kt rename to laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateFeatureSourceFactoryTaskSpec.kt index a2a3c0c72..a1227afd9 100644 --- a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateFeatureSourceFactoryTaskSpec.kt +++ b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateFeatureSourceFactoryTaskSpec.kt @@ -1,6 +1,6 @@ package io.mehow.laboratory.gradle -import io.kotest.core.spec.style.StringSpec +import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.file.shouldExist import io.kotest.matchers.file.shouldNotExist import io.kotest.matchers.shouldBe @@ -9,18 +9,18 @@ import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome.FAILED import org.gradle.testkit.runner.TaskOutcome.SUCCESS -internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ +class GenerateFeatureSourceFactoryTaskSpec : FunSpec({ lateinit var gradleRunner: GradleRunner cleanBuildDirs() beforeTest { gradleRunner = GradleRunner.create() - .withPluginClasspath() - .withArguments("generateFeatureSourceFactory", "--stacktrace") + .withPluginClasspath() + .withArguments("generateFeatureSourceFactory", "--stacktrace") } - "generates factory without any feature flags" { + test("generates factory without any feature flags") { val fixture = "source-factory-generate-empty".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -40,7 +40,7 @@ internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory with feature flags" { + test("generates factory with feature flags") { val fixture = "source-factory-generate-feature-flags".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -64,7 +64,7 @@ internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ """.trimMargin() } - "uses implicit package name" { + test("uses implicit package name") { val fixture = "source-factory-package-name-implicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -75,7 +75,7 @@ internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.implicit" } - "uses explicit package name" { + test("uses explicit package name") { val fixture = "source-factory-package-name-explicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -86,7 +86,7 @@ internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.explicit" } - "overrides implicit package name" { + test("overrides implicit package name") { val fixture = "source-factory-package-name-explicit-override".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -97,7 +97,7 @@ internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.explicit" } - "generates internal factory" { + test("generates internal factory") { val fixture = "source-factory-generate-internal".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -108,7 +108,7 @@ internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "internal fun FeatureFactory.Companion.featureSourceGenerated()" } - "generates public factory" { + test("generates public factory") { val fixture = "source-factory-generate-public".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -119,7 +119,7 @@ internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "public fun FeatureFactory.Companion.featureSourceGenerated()" } - "fails for feature flags with no options" { + test("fails for feature flags with no options") { val fixture = "source-factory-feature-flag-option-missing".toFixture() val result = gradleRunner.withProjectDir(fixture).buildAndFail() @@ -131,7 +131,7 @@ internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ feature.shouldNotExist() } - "generates factory with feature flags from all modules" { + test("generates factory with feature flags from all modules") { val fixture = "source-factory-multi-module-generate-all".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -156,7 +156,7 @@ internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory with feature flags only from included modules" { + test("generates factory with feature flags only from included modules") { val fixture = "source-factory-multi-module-generate-filtered".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -180,7 +180,7 @@ internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory for Android project" { + test("generates factory for Android project") { val fixture = "source-factory-android-smoke".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -200,7 +200,7 @@ internal class GenerateFeatureSourceFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory with supervised feature flag sources" { + test("generates factory with supervised feature flag sources") { val fixture = "source-factory-generate-supervised-feature-flags".toFixture() val result = gradleRunner.withProjectDir(fixture).build() diff --git a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateSourcedStorageTaskSpec.kt b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateSourcedStorageTaskSpec.kt similarity index 93% rename from library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateSourcedStorageTaskSpec.kt rename to laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateSourcedStorageTaskSpec.kt index 329cbd772..4ca550218 100644 --- a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/GenerateSourcedStorageTaskSpec.kt +++ b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/GenerateSourcedStorageTaskSpec.kt @@ -1,6 +1,6 @@ package io.mehow.laboratory.gradle -import io.kotest.core.spec.style.StringSpec +import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.file.shouldExist import io.kotest.matchers.file.shouldNotExist import io.kotest.matchers.shouldBe @@ -9,18 +9,18 @@ import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome.FAILED import org.gradle.testkit.runner.TaskOutcome.SUCCESS -internal class GenerateSourcedStorageTaskSpec : StringSpec({ +class GenerateSourcedStorageTaskSpec : FunSpec({ lateinit var gradleRunner: GradleRunner cleanBuildDirs() beforeTest { gradleRunner = GradleRunner.create() - .withPluginClasspath() - .withArguments("generateSourcedFeatureStorage", "--stacktrace") + .withPluginClasspath() + .withArguments("generateSourcedFeatureStorage", "--stacktrace") } - "generates storage with only local source" { + test("generates storage with only local source") { val fixture = "sourced-storage-generate-local".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -48,7 +48,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ """.trimMargin() } - "generates storage with sources" { + test("generates storage with sources") { val fixture = "sourced-storage-generate-sources".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -91,7 +91,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ """.trimMargin() } - "uses implicit package name" { + test("uses implicit package name") { val fixture = "sourced-storage-package-name-implicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -102,7 +102,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.implicit" } - "uses explicit package name" { + test("uses explicit package name") { val fixture = "sourced-storage-package-name-explicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -113,7 +113,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.explicit" } - "overrides implicit package name" { + test("overrides implicit package name") { val fixture = "sourced-storage-package-name-explicit-override".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -124,7 +124,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.explicit" } - "generates internal storage" { + test("generates internal storage") { val fixture = "sourced-storage-generate-internal".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -135,7 +135,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ factory.readText() shouldContain "internal fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage)" } - "generates public storage" { + test("generates public storage") { val fixture = "sourced-storage-generate-public".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -146,7 +146,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ factory.readText() shouldContain "public fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage)" } - "fails for feature flags with no options" { + test("fails for feature flags with no options") { val fixture = "sourced-storage-feature-flag-option-missing".toFixture() val result = gradleRunner.withProjectDir(fixture).buildAndFail() @@ -158,7 +158,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ feature.shouldNotExist() } - "generates storage with sourced from all modules" { + test("generates storage with sourced from all modules") { val fixture = "sourced-storage-multi-module-generate-all".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -209,7 +209,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ """.trimMargin() } - "generates storage with names only from included modules" { + test("generates storage with names only from included modules") { val fixture = "sourced-storage-multi-module-generate-filtered".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -252,7 +252,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ """.trimMargin() } - "generates storage for Android project" { + test("generates storage for Android project") { val fixture = "sourced-storage-android-smoke".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -279,7 +279,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ """.trimMargin() } - "ignores any custom variant of local sources" { + test("ignores any custom variant of local sources") { val fixture = "sourced-storage-generate-local-ignore".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -306,7 +306,7 @@ internal class GenerateSourcedStorageTaskSpec : StringSpec({ """.trimMargin() } - "generates storage with supervised feature flag sources" { + test("generates storage with supervised feature flag sources") { val fixture = "sourced-storage-generate-supervised-feature-flags".toFixture() val result = gradleRunner.withProjectDir(fixture).build() diff --git a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/LaboratoryPluginSpec.kt b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/LaboratoryPluginSpec.kt similarity index 57% rename from library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/LaboratoryPluginSpec.kt rename to laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/LaboratoryPluginSpec.kt index 8ec82cb37..8f0d87f1b 100644 --- a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/LaboratoryPluginSpec.kt +++ b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/LaboratoryPluginSpec.kt @@ -1,13 +1,13 @@ package io.mehow.laboratory.gradle import io.kotest.assertions.throwables.shouldThrowAny -import io.kotest.core.spec.style.StringSpec +import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.string.shouldContain import org.gradle.testkit.runner.GradleRunner -internal class LaboratoryPluginSpec : StringSpec({ +class LaboratoryPluginSpec : FunSpec({ lateinit var gradleRunner: GradleRunner cleanBuildDirs() @@ -16,169 +16,169 @@ internal class LaboratoryPluginSpec : StringSpec({ gradleRunner = GradleRunner.create().withPluginClasspath() } - "fails for project without Kotlin plugin" { + test("fails for project without Kotlin plugin") { val fixture = "plugin-kotlin-missing".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateFeatureFlags", "--stacktrace") - .buildAndFail() + .withArguments("generateFeatureFlags", "--stacktrace") + .buildAndFail() result.task(":generateFeatureFlags").shouldBeNull() result.output shouldContain "Laboratory Gradle plugin requires Kotlin plugin." } - "registers feature flags task for project with Kotlin plugin" { + test("registers feature flags task for project with Kotlin plugin") { val fixture = "plugin-kotlin-present".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateFeatureFlags", "--stacktrace") - .build() + .withArguments("generateFeatureFlags", "--stacktrace") + .build() result.task(":generateFeatureFlags").shouldNotBeNull() } - "fails for Android project without Kotlin Android plugin" { + test("fails for Android project without Kotlin Android plugin") { val fixture = "plugin-kotlin-android-missing".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateFeatureFlags", "--stacktrace") - .buildAndFail() + .withArguments("generateFeatureFlags", "--stacktrace") + .buildAndFail() result.task(":generateFeatureFlags").shouldBeNull() result.output shouldContain "Laboratory Gradle plugin requires Kotlin plugin." } - "registers feature flags task for project with Kotlin Android plugin" { + test("registers feature flags task for project with Kotlin Android plugin") { val fixture = "plugin-kotlin-android-present".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateFeatureFlags", "--stacktrace") - .build() + .withArguments("generateFeatureFlags", "--stacktrace") + .build() result.task(":generateFeatureFlags").shouldNotBeNull() } - "does not register feature flags factory for project without feature flags factory extension" { + test("does not register feature flags factory for project without feature flags factory extension") { val fixture = "plugin-factory-missing".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("--stacktrace") - .build() + .withArguments("--stacktrace") + .build() result.task(":generateFeatureFactory").shouldBeNull() } - "fails for project without feature flags factory extension with feature flags factory argument" { + test("fails for project without feature flags factory extension with feature flags factory argument") { val fixture = "plugin-factory-missing".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateFeatureFactory", "--stacktrace") - .buildAndFail() + .withArguments("generateFeatureFactory", "--stacktrace") + .buildAndFail() result.task(":generateFeatureFactory").shouldBeNull() } - "registers feature flags factory for project with feature flags factory extension" { + test("registers feature flags factory for project with feature flags factory extension") { val fixture = "plugin-factory-present".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateFeatureFactory", "--stacktrace") - .build() + .withArguments("generateFeatureFactory", "--stacktrace") + .build() result.task(":generateFeatureFactory").shouldNotBeNull() } - "does not register sourced storage for project without sourced storage extension" { + test("does not register sourced storage for project without sourced storage extension") { val fixture = "plugin-sourced-storage-missing".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("--stacktrace") - .build() + .withArguments("--stacktrace") + .build() result.task(":generateSourcedFeatureStorage").shouldBeNull() } - "fails for project without sourced storage extension with factory argument" { + test("fails for project without sourced storage extension with factory argument") { val fixture = "plugin-sourced-storage-missing".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateSourcedFeatureStorage", "--stacktrace") - .buildAndFail() + .withArguments("generateSourcedFeatureStorage", "--stacktrace") + .buildAndFail() result.task(":generateSourcedFeatureStorage").shouldBeNull() } - "registers sourced storage for project with factory extension" { + test("registers sourced storage for project with factory extension") { val fixture = "plugin-sourced-storage-present".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateSourcedFeatureStorage", "--stacktrace") - .build() + .withArguments("generateSourcedFeatureStorage", "--stacktrace") + .build() result.task(":generateSourcedFeatureStorage").shouldNotBeNull() } - "does not register feature flag sources factory for project without feature flag sources factory extension" { + test("does not register feature flag sources factory for project without feature flag sources factory extension") { val fixture = "plugin-source-factory-missing".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("--stacktrace") - .build() + .withArguments("--stacktrace") + .build() result.task(":generateFeatureSourceFactory").shouldBeNull() } - "fails for project without feature sources factory extension with feature flag sources factory argument" { + test("fails for project without feature sources factory extension with feature flag sources factory argument") { val fixture = "plugin-source-factory-missing".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateFeatureSourceFactory", "--stacktrace") - .buildAndFail() + .withArguments("generateFeatureSourceFactory", "--stacktrace") + .buildAndFail() result.task(":generateFeatureSourceFactory").shouldBeNull() } - "registers feature flag sources factory for project with feature flag sources factory extension" { + test("registers feature flag sources factory for project with feature flag sources factory extension") { val fixture = "plugin-source-factory-present".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateFeatureSourceFactory", "--stacktrace") - .build() + .withArguments("generateFeatureSourceFactory", "--stacktrace") + .build() result.task(":generateFeatureSourceFactory").shouldNotBeNull() } - "does not register option factory for project without option factory extension" { + test("does not register option factory for project without option factory extension") { val fixture = "plugin-option-factory-missing".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("--stacktrace") - .build() + .withArguments("--stacktrace") + .build() result.task(":generateOptionFactory").shouldBeNull() } - "fails for project without option factory extension with option factory argument" { + test("fails for project without option factory extension with option factory argument") { val fixture = "plugin-option-factory-missing".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateOptionFactory", "--stacktrace") - .buildAndFail() + .withArguments("generateOptionFactory", "--stacktrace") + .buildAndFail() result.task(":generateOptionFactory").shouldBeNull() } - "registers option factory for project with option factory extension" { + test("registers option factory for project with option factory extension") { val fixture = "plugin-option-factory-present".toFixture() val result = gradleRunner.withProjectDir(fixture) - .withArguments("generateOptionFactory", "--stacktrace") - .build() + .withArguments("generateOptionFactory", "--stacktrace") + .build() result.task(":generateOptionFactory").shouldNotBeNull() } - "fails for including dependency without laboratory plugin" { + test("fails for including dependency without laboratory plugin") { val fixture = "plugin-dependency-plugin-missing".toFixture() val exception = shouldThrowAny { gradleRunner.withProjectDir(fixture).build() } diff --git a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/OptionFactoryTaskSpec.kt b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/OptionFactoryTaskSpec.kt similarity index 89% rename from library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/OptionFactoryTaskSpec.kt rename to laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/OptionFactoryTaskSpec.kt index d39214e23..fffccefe5 100644 --- a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/OptionFactoryTaskSpec.kt +++ b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/OptionFactoryTaskSpec.kt @@ -1,6 +1,6 @@ package io.mehow.laboratory.gradle -import io.kotest.core.spec.style.StringSpec +import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.file.shouldExist import io.kotest.matchers.file.shouldNotExist import io.kotest.matchers.shouldBe @@ -9,18 +9,18 @@ import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome.FAILED import org.gradle.testkit.runner.TaskOutcome.SUCCESS -internal class OptionFactoryTaskSpec : StringSpec({ +class OptionFactoryTaskSpec : FunSpec({ lateinit var gradleRunner: GradleRunner cleanBuildDirs() beforeTest { gradleRunner = GradleRunner.create() - .withPluginClasspath() - .withArguments("generateOptionFactory", "--stacktrace") + .withPluginClasspath() + .withArguments("generateOptionFactory", "--stacktrace") } - "generates factory without any feature flags" { + test("generates factory without any feature flags") { val fixture = "option-factory-generate-empty".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -39,7 +39,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory using feature flag fqcns" { + test("generates factory using feature flag fqcns") { val fixture = "option-factory-generate-fqcn".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -69,7 +69,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory using feature flag keys" { + test("generates factory using feature flag keys") { val fixture = "option-factory-generate-key".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -99,7 +99,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ """.trimMargin() } - "fails to generate factory for feature flags with duplicate keys" { + test("fails to generate factory for feature flags with duplicate keys") { val fixture = "option-factory-duplicate-key".toFixture() val result = gradleRunner.withProjectDir(fixture).buildAndFail() @@ -114,7 +114,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ factory.shouldNotExist() } - "fails to generate factory for key duplicating another fqcn" { + test("fails to generate factory for key duplicating another fqcn") { val fixture = "option-factory-duplicate-key-fqcn".toFixture() val result = gradleRunner.withProjectDir(fixture).buildAndFail() @@ -129,7 +129,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ factory.shouldNotExist() } - "fails to generate factory for feature flags with no options" { + test("fails to generate factory for feature flags with no options") { val fixture = "option-factory-no-option".toFixture() val result = gradleRunner.withProjectDir(fixture).buildAndFail() @@ -141,7 +141,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ factory.shouldNotExist() } - "uses implicit package name" { + test("uses implicit package name") { val fixture = "option-factory-package-name-implicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -152,7 +152,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.implicit" } - "uses explicit package name" { + test("uses explicit package name") { val fixture = "option-factory-package-name-explicit".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -163,7 +163,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.explicit" } - "overrides implicit package name" { + test("overrides implicit package name") { val fixture = "option-factory-package-name-explicit-override".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -174,7 +174,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "package io.mehow.explicit" } - "generates internal factory" { + test("generates internal factory") { val fixture = "option-factory-generate-internal".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -185,7 +185,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "internal fun OptionFactory.Companion.generated()" } - "generates public factory" { + test("generates public factory") { val fixture = "option-factory-generate-public".toFixture() gradleRunner.withProjectDir(fixture).build() @@ -196,7 +196,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ factory.readText() shouldContain "public fun OptionFactory.Companion.generated()" } - "generates factory with feature flags from all modules" { + test("generates factory with feature flags from all modules") { val fixture = "option-factory-multi-module-generate-all".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -229,7 +229,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory with feature flags only from included modules" { + test("generates factory with feature flags only from included modules") { val fixture = "option-factory-multi-module-generate-filtered".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -258,7 +258,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ """.trimMargin() } - "generates factory for Android project" { + test("generates factory for Android project") { val fixture = "option-factory-android-smoke".toFixture() val result = gradleRunner.withProjectDir(fixture).build() @@ -277,7 +277,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ """.trimMargin() } - "fails to generate factory for feature flags with duplicate keys in different modules" { + test("fails to generate factory for feature flags with duplicate keys in different modules") { val fixture = "option-factory-multi-module-duplicate-key".toFixture() val result = gradleRunner.withProjectDir(fixture).buildAndFail() @@ -292,7 +292,7 @@ internal class OptionFactoryTaskSpec : StringSpec({ factory.shouldNotExist() } - "generates factory for feature flags with duplicate keys in filtered modules" { + test("generates factory for feature flags with duplicate keys in filtered modules") { val fixture = "option-factory-multi-module-filtered-duplicate-key".toFixture() val result = gradleRunner.withProjectDir(fixture).build() diff --git a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/TestExtensions.kt b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/TestExtensions.kt similarity index 86% rename from library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/TestExtensions.kt rename to laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/TestExtensions.kt index d441c4c77..173b004d3 100644 --- a/library/gradle-plugin/src/test/java/io/mehow/laboratory/gradle/TestExtensions.kt +++ b/laboratory/gradle-plugin/src/test/kotlin/io/mehow/laboratory/gradle/TestExtensions.kt @@ -15,10 +15,10 @@ internal fun File.featureSourceStorageFile(fqcn: String) = codeGenFile("feature- internal fun File.optionFactoryFile(fqcn: String) = codeGenFile("option-factory", fqcn) -private fun File.codeGenFile(dir: String, fqcn: String) = File( - this, - "build/generated/laboratory/code/$dir/${fqcn.replace(".", "/")}.kt" -) +private fun File.codeGenFile( + dir: String, + fqcn: String, +) = File(this, "build/generated/laboratory/code/$dir/${fqcn.replace(".", "/")}.kt") internal fun TestConfiguration.cleanBuildDirs() = beforeSpec { File("src/test/projects").getBuildDirs().forEach { it.deleteRecursively() } diff --git a/library/gradle-plugin/src/test/projects/factory-android-smoke/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-android-smoke/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-android-smoke/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-android-smoke/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-feature-flag-option-missing/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-android-smoke/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-feature-flag-option-missing/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-android-smoke/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-feature-flag-option-missing/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-feature-flag-option-missing/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-feature-flag-option-missing/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-feature-flag-option-missing/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-generate-empty/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-feature-flag-option-missing/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-generate-empty/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-feature-flag-option-missing/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-generate-empty/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-generate-empty/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-generate-empty/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-generate-empty/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-generate-features/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-generate-empty/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-generate-features/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-generate-empty/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-generate-features/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-generate-features/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-generate-features/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-generate-features/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-generate-internal/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-generate-features/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-generate-internal/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-generate-features/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-generate-internal/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-generate-internal/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-generate-internal/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-generate-internal/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-generate-public/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-generate-internal/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-generate-public/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-generate-internal/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-generate-public/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-generate-public/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-generate-public/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-generate-public/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-generate-supervised-feature-flags/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-generate-public/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-generate-supervised-feature-flags/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-generate-public/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-generate-supervised-feature-flags/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-generate-supervised-feature-flags/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-generate-supervised-feature-flags/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-generate-supervised-feature-flags/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-package-name-explicit-override/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-generate-supervised-feature-flags/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-package-name-explicit-override/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-generate-supervised-feature-flags/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-multi-module-generate-all/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-all/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-multi-module-generate-all/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-all/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-multi-module-generate-all/feature-a/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-all/feature-a/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-multi-module-generate-all/feature-a/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-all/feature-a/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-multi-module-generate-all/feature-b/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-all/feature-b/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-multi-module-generate-all/feature-b/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-all/feature-b/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-multi-module-generate-all/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-all/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-multi-module-generate-all/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-all/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/feature-a/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/feature-a/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/feature-a/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/feature-a/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/feature-b/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/feature-b/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/feature-b/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/feature-b/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-multi-module-generate-filtered/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-package-name-explicit-override/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-package-name-explicit-override/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-package-name-explicit-override/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-package-name-explicit-override/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-package-name-explicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-package-name-explicit-override/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-package-name-explicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-package-name-explicit-override/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-package-name-explicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-package-name-explicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-package-name-explicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-package-name-explicit/build.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-package-name-implicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-package-name-explicit/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-package-name-implicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-package-name-explicit/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/factory-package-name-implicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/factory-package-name-implicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/factory-package-name-implicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-package-name-implicit/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-android-smoke/settings.gradle b/laboratory/gradle-plugin/src/test/projects/factory-package-name-implicit/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-android-smoke/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/factory-package-name-implicit/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-android-smoke/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-android-smoke/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-android-smoke/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-android-smoke/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-deprecated-with-level/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-android-smoke/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-deprecated-with-level/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-android-smoke/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-deprecated-with-level/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-deprecated-with-level/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-deprecated-with-level/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-deprecated-with-level/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-deprecated/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-deprecated-with-level/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-deprecated/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-deprecated-with-level/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-deprecated/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-deprecated/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-deprecated/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-deprecated/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-description/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-deprecated/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-description/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-deprecated/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-description/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-description/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-description/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-description/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-internal/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-description/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-internal/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-description/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-internal/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-internal/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-internal/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-internal/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-multiple/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-internal/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-multiple/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-internal/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-multiple/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-multiple/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-multiple/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-multiple/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-option-name-common/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-multiple/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-option-name-common/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-multiple/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-option-name-common/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-option-name-common/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-option-name-common/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-option-name-common/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-public/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-option-name-common/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-public/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-option-name-common/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-public/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-public/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-public/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-public/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-single/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-public/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-single/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-public/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-single/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-single/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-single/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-single/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-sources-internal/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-single/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-sources-internal/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-single/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-sources-internal/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-internal/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-sources-internal/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-internal/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-sources-multiple/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-internal/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-sources-multiple/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-internal/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-sources-multiple/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-multiple/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-sources-multiple/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-multiple/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-sources-public/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-multiple/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-sources-public/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-multiple/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-sources-public/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-public/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-sources-public/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-public/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-sources-single/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-public/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-sources-single/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-public/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-generate-sources-single/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-single/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-generate-sources-single/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-single/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-option-missing/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-single/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-option-missing/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-generate-sources-single/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-option-missing/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-option-missing/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-option-missing/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-option-missing/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-override/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-option-missing/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-override/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-option-missing/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-override/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-override/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-override/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-override/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-switching/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-override/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-switching/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-override/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-switching/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-switching/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-switching/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-switching/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-switching/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit-switching/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-explicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-cascading/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-cascading/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-explicit/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-cascading/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-cascading/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-cascading/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-cascading/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-switching/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-cascading/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-switching/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-cascading/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-switching/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-switching/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-switching/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-switching/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-switching/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit-switching/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-package-name-implicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-child/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-child/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-package-name-implicit/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-child/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-child/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-child/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-child/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-grandchild/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-child/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-grandchild/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-child/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-grandchild/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-grandchild/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-grandchild/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-grandchild/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-multiple-children/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-grandchild/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-multiple-children/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-grandchild/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-multiple-children/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-multiple-children/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-multiple-children/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-multiple-children/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit-override/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-multiple-children/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit-override/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-generate-multiple-children/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit-override/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit-override/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit-override/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit-override/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit-override/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit-override/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-implicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-implicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-explicit/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-implicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-implicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-implicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-implicit/build.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-self-supervision/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-implicit/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-self-supervision/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-package-name-implicit/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/feature-flag-supervisor-self-supervision/build.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-self-supervision/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/feature-flag-supervisor-self-supervision/build.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-self-supervision/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-android-smoke/settings.gradle b/laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-self-supervision/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-android-smoke/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/feature-flag-supervisor-self-supervision/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-android-smoke/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-android-smoke/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-android-smoke/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-android-smoke/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-duplicate-key-fqcn/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-android-smoke/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-duplicate-key-fqcn/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-android-smoke/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-duplicate-key-fqcn/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-duplicate-key-fqcn/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-duplicate-key-fqcn/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-duplicate-key-fqcn/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-duplicate-key/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-duplicate-key-fqcn/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-duplicate-key/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-duplicate-key-fqcn/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-duplicate-key/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-duplicate-key/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-duplicate-key/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-duplicate-key/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-generate-empty/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-duplicate-key/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-generate-empty/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-duplicate-key/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-generate-empty/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-generate-empty/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-generate-empty/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-generate-empty/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-generate-fqcn/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-generate-empty/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-generate-fqcn/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-generate-empty/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-generate-fqcn/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-generate-fqcn/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-generate-fqcn/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-generate-fqcn/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-generate-internal/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-generate-fqcn/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-generate-internal/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-generate-fqcn/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-generate-internal/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-generate-internal/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-generate-internal/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-generate-internal/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-generate-key/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-generate-internal/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-generate-key/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-generate-internal/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-generate-key/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-generate-key/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-generate-key/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-generate-key/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-generate-public/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-generate-key/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-generate-public/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-generate-key/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-generate-public/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-generate-public/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-generate-public/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-generate-public/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-no-option/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-generate-public/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-no-option/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-generate-public/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/feature-a/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/feature-a/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/feature-a/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/feature-a/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/feature-b/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/feature-b/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/feature-b/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/feature-b/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-duplicate-key/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/feature-a/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/feature-a/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/feature-a/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/feature-a/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/feature-b/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/feature-b/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/feature-b/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/feature-b/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-filtered-duplicate-key/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/feature-a/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/feature-a/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/feature-a/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/feature-a/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/feature-b/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/feature-b/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/feature-b/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/feature-b/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-all/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/feature-a/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/feature-a/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/feature-a/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/feature-a/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/feature-b/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/feature-b/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/feature-b/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/feature-b/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-multi-module-generate-filtered/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-no-option/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-no-option/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-no-option/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-no-option/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-package-name-explicit-override/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-no-option/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-package-name-explicit-override/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-no-option/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-package-name-explicit-override/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-package-name-explicit-override/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-package-name-explicit-override/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-package-name-explicit-override/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-package-name-explicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-package-name-explicit-override/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-package-name-explicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-package-name-explicit-override/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-package-name-explicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-package-name-explicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-package-name-explicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-package-name-explicit/build.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-package-name-implicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-package-name-explicit/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-package-name-implicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-package-name-explicit/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/option-factory-package-name-implicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-package-name-implicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/option-factory-package-name-implicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-package-name-implicit/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-factory-missing/settings.gradle b/laboratory/gradle-plugin/src/test/projects/option-factory-package-name-implicit/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-factory-missing/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/option-factory-package-name-implicit/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/no-plugin/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/no-plugin/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/no-plugin/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/no-plugin/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-dependency-plugin-missing/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-factory-missing/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-factory-missing/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-factory-missing/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-factory-missing/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-factory-present/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-factory-missing/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-factory-present/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-factory-missing/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-factory-present/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-factory-present/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-factory-present/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-factory-present/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-kotlin-android-missing/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-factory-present/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-kotlin-android-missing/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-factory-present/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-kotlin-android-missing/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-kotlin-android-missing/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-kotlin-android-missing/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-kotlin-android-missing/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-kotlin-android-present/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-kotlin-android-missing/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-kotlin-android-present/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-kotlin-android-missing/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-kotlin-android-present/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-kotlin-android-present/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-kotlin-android-present/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-kotlin-android-present/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-kotlin-missing/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-kotlin-android-present/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-kotlin-missing/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-kotlin-android-present/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-kotlin-missing/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-kotlin-missing/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-kotlin-missing/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-kotlin-missing/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-kotlin-present/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-kotlin-missing/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-kotlin-present/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-kotlin-missing/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-kotlin-present/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-kotlin-present/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-kotlin-present/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-kotlin-present/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-option-factory-missing/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-kotlin-present/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-option-factory-missing/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-kotlin-present/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-option-factory-missing/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-option-factory-missing/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-option-factory-missing/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-option-factory-missing/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-option-factory-present/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-option-factory-missing/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-option-factory-present/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-option-factory-missing/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-option-factory-present/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-option-factory-present/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-option-factory-present/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-option-factory-present/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-source-factory-missing/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-option-factory-present/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-source-factory-missing/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-option-factory-present/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-source-factory-missing/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-source-factory-missing/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-source-factory-missing/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-source-factory-missing/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-source-factory-present/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-source-factory-missing/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-source-factory-present/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-source-factory-missing/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-source-factory-present/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-source-factory-present/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-source-factory-present/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-source-factory-present/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-sourced-storage-missing/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-source-factory-present/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-sourced-storage-missing/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-source-factory-present/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-sourced-storage-missing/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-sourced-storage-missing/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-sourced-storage-missing/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-sourced-storage-missing/build.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-sourced-storage-present/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-sourced-storage-missing/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-sourced-storage-present/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-sourced-storage-missing/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/plugin-sourced-storage-present/build.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-sourced-storage-present/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/plugin-sourced-storage-present/build.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-sourced-storage-present/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-android-smoke/settings.gradle b/laboratory/gradle-plugin/src/test/projects/plugin-sourced-storage-present/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-android-smoke/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/plugin-sourced-storage-present/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-android-smoke/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-android-smoke/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-android-smoke/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-android-smoke/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-feature-flag-option-missing/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-android-smoke/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-feature-flag-option-missing/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-android-smoke/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-feature-flag-option-missing/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-feature-flag-option-missing/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-feature-flag-option-missing/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-feature-flag-option-missing/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-generate-empty/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-feature-flag-option-missing/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-generate-empty/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-feature-flag-option-missing/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-generate-empty/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-generate-empty/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-generate-empty/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-generate-empty/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-generate-feature-flags/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-generate-empty/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-generate-feature-flags/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-generate-empty/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-generate-feature-flags/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-generate-feature-flags/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-generate-feature-flags/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-generate-feature-flags/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-generate-internal/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-generate-feature-flags/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-generate-internal/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-generate-feature-flags/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-generate-internal/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-generate-internal/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-generate-internal/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-generate-internal/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-generate-public/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-generate-internal/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-generate-public/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-generate-internal/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-generate-public/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-generate-public/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-generate-public/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-generate-public/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-generate-supervised-feature-flags/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-generate-public/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-generate-supervised-feature-flags/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-generate-public/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-generate-supervised-feature-flags/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-generate-supervised-feature-flags/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-generate-supervised-feature-flags/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-generate-supervised-feature-flags/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-package-name-explicit-override/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-generate-supervised-feature-flags/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-package-name-explicit-override/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-generate-supervised-feature-flags/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/feature-a/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/feature-a/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/feature-a/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/feature-a/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/feature-b/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/feature-b/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/feature-b/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/feature-b/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-all/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/feature-a/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/feature-a/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/feature-a/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/feature-a/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/feature-b/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/feature-b/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/feature-b/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/feature-b/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-multi-module-generate-filtered/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-package-name-explicit-override/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-package-name-explicit-override/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-package-name-explicit-override/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-package-name-explicit-override/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-package-name-explicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-package-name-explicit-override/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-package-name-explicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-package-name-explicit-override/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-package-name-explicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-package-name-explicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-package-name-explicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-package-name-explicit/build.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-package-name-implicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-package-name-explicit/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-package-name-implicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-package-name-explicit/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/source-factory-package-name-implicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-package-name-implicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/source-factory-package-name-implicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-package-name-implicit/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-android-smoke/settings.gradle b/laboratory/gradle-plugin/src/test/projects/source-factory-package-name-implicit/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-android-smoke/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/source-factory-package-name-implicit/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-android-smoke/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-android-smoke/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-android-smoke/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-android-smoke/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-feature-flag-option-missing/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-android-smoke/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-feature-flag-option-missing/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-android-smoke/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-feature-flag-option-missing/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-feature-flag-option-missing/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-feature-flag-option-missing/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-feature-flag-option-missing/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-internal/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-feature-flag-option-missing/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-internal/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-feature-flag-option-missing/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-internal/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-internal/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-internal/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-internal/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-local-ignore/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-internal/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-local-ignore/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-internal/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-local-ignore/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-local-ignore/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-local-ignore/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-local-ignore/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-local/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-local-ignore/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-local/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-local-ignore/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-local/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-local/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-local/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-local/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-public/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-local/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-public/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-local/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-public/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-public/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-public/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-public/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-sources/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-public/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-sources/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-public/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-sources/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-sources/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-sources/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-sources/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-supervised-feature-flags/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-sources/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-supervised-feature-flags/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-sources/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-generate-supervised-feature-flags/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-supervised-feature-flags/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-generate-supervised-feature-flags/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-supervised-feature-flags/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit-override/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-supervised-feature-flags/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit-override/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-generate-supervised-feature-flags/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/feature-a/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/feature-a/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/feature-a/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/feature-a/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/feature-b/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/feature-b/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/feature-b/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/feature-b/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-all/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/feature-a/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/feature-a/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/feature-a/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/feature-a/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/feature-b/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/feature-b/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/feature-b/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/feature-b/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-multi-module-generate-filtered/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit-override/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit-override/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit-override/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit-override/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit-override/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit-override/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit/build.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-package-name-implicit/settings.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit/settings.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-package-name-implicit/settings.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-explicit/settings.gradle diff --git a/library/gradle-plugin/src/test/projects/sourced-storage-package-name-implicit/build.gradle b/laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-implicit/build.gradle similarity index 100% rename from library/gradle-plugin/src/test/projects/sourced-storage-package-name-implicit/build.gradle rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-implicit/build.gradle diff --git a/library/hyperion-plugin/api/hyperion-plugin.api b/laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-implicit/settings.gradle similarity index 100% rename from library/hyperion-plugin/api/hyperion-plugin.api rename to laboratory/gradle-plugin/src/test/projects/sourced-storage-package-name-implicit/settings.gradle diff --git a/laboratory/hyperion-plugin/api/hyperion-plugin.api b/laboratory/hyperion-plugin/api/hyperion-plugin.api new file mode 100644 index 000000000..e69de29bb diff --git a/laboratory/hyperion-plugin/build.gradle.kts b/laboratory/hyperion-plugin/build.gradle.kts new file mode 100644 index 000000000..bb17779ab --- /dev/null +++ b/laboratory/hyperion-plugin/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.mavenPublish) + alias(libs.plugins.spotless) + alias(libs.plugins.detekt) + alias(libs.plugins.dokka) + alias(libs.plugins.ksp) +} + +android { + namespace = "io.mehow.laboratory.hyperion" + resourcePrefix = "io_mehow_laboratory_" +} + +dependencies { + api(projects.laboratory.inspector) + api(libs.hyperion.plugin) + implementation(libs.androidx.appCompat) + ksp(libs.autoServiceKsp) +} diff --git a/library/hyperion-plugin/gradle.properties b/laboratory/hyperion-plugin/gradle.properties similarity index 61% rename from library/hyperion-plugin/gradle.properties rename to laboratory/hyperion-plugin/gradle.properties index 742b2ca81..7017ccba9 100644 --- a/library/hyperion-plugin/gradle.properties +++ b/laboratory/hyperion-plugin/gradle.properties @@ -1,3 +1,3 @@ POM_ARTIFACT_ID=laboratory-hyperion-plugin -POM_NAME=Laboratory (Hyperion plugin) +POM_NAME=Laboratory (Hyperion Plugin) POM_PACKAGING=aar diff --git a/library/hyperion-plugin/src/main/java/io/mehow/laboratory/hyperion/Plugin.kt b/laboratory/hyperion-plugin/src/main/kotlin/io/mehow/laboratory/hyperion/Plugin.kt similarity index 99% rename from library/hyperion-plugin/src/main/java/io/mehow/laboratory/hyperion/Plugin.kt rename to laboratory/hyperion-plugin/src/main/kotlin/io/mehow/laboratory/hyperion/Plugin.kt index df957744e..f0d243310 100644 --- a/library/hyperion-plugin/src/main/java/io/mehow/laboratory/hyperion/Plugin.kt +++ b/laboratory/hyperion-plugin/src/main/kotlin/io/mehow/laboratory/hyperion/Plugin.kt @@ -7,5 +7,6 @@ import com.willowtreeapps.hyperion.plugin.v1.PluginModule as HyperionPluginModul @AutoService(HyperionPlugin::class) internal class Plugin : HyperionPlugin() { override fun minimumRequiredApi() = 21 + override fun createPluginModule(): HyperionPluginModule = PluginModule() } diff --git a/library/hyperion-plugin/src/main/java/io/mehow/laboratory/hyperion/PluginModule.kt b/laboratory/hyperion-plugin/src/main/kotlin/io/mehow/laboratory/hyperion/PluginModule.kt similarity index 84% rename from library/hyperion-plugin/src/main/java/io/mehow/laboratory/hyperion/PluginModule.kt rename to laboratory/hyperion-plugin/src/main/kotlin/io/mehow/laboratory/hyperion/PluginModule.kt index de036aeba..ff9bc9a6f 100644 --- a/library/hyperion-plugin/src/main/java/io/mehow/laboratory/hyperion/PluginModule.kt +++ b/laboratory/hyperion-plugin/src/main/kotlin/io/mehow/laboratory/hyperion/PluginModule.kt @@ -9,7 +9,10 @@ import com.willowtreeapps.hyperion.plugin.v1.PluginModule as HyperionPluginModul internal class PluginModule : HyperionPluginModule() { override fun getName(): Int = R.string.io_mehow_laboratory_plugin_id - override fun createPluginView(layoutInflater: LayoutInflater, parent: ViewGroup): View { + override fun createPluginView( + layoutInflater: LayoutInflater, + parent: ViewGroup, + ): View { return layoutInflater.inflate(R.layout.io_mehow_laboratory_plugin_item, parent, false).apply { setOnClickListener { LaboratoryActivity.start(context) } } diff --git a/library/hyperion-plugin/src/main/res/drawable/io_mehow_laboratory_icon.xml b/laboratory/hyperion-plugin/src/main/res/drawable/io_mehow_laboratory_icon.xml similarity index 100% rename from library/hyperion-plugin/src/main/res/drawable/io_mehow_laboratory_icon.xml rename to laboratory/hyperion-plugin/src/main/res/drawable/io_mehow_laboratory_icon.xml diff --git a/library/hyperion-plugin/src/main/res/layout/io_mehow_laboratory_plugin_item.xml b/laboratory/hyperion-plugin/src/main/res/layout/io_mehow_laboratory_plugin_item.xml similarity index 100% rename from library/hyperion-plugin/src/main/res/layout/io_mehow_laboratory_plugin_item.xml rename to laboratory/hyperion-plugin/src/main/res/layout/io_mehow_laboratory_plugin_item.xml diff --git a/library/hyperion-plugin/src/main/res/values/public.xml b/laboratory/hyperion-plugin/src/main/res/values/public.xml similarity index 100% rename from library/hyperion-plugin/src/main/res/values/public.xml rename to laboratory/hyperion-plugin/src/main/res/values/public.xml diff --git a/library/hyperion-plugin/src/main/res/values/strings.xml b/laboratory/hyperion-plugin/src/main/res/values/strings.xml similarity index 100% rename from library/hyperion-plugin/src/main/res/values/strings.xml rename to laboratory/hyperion-plugin/src/main/res/values/strings.xml diff --git a/library/inspector/api/inspector.api b/laboratory/inspector/api/inspector.api similarity index 97% rename from library/inspector/api/inspector.api rename to laboratory/inspector/api/inspector.api index 2834e1862..ada6ba372 100644 --- a/library/inspector/api/inspector.api +++ b/laboratory/inspector/api/inspector.api @@ -1,6 +1,7 @@ public final class io/mehow/laboratory/inspector/DeprecationAlignment : java/lang/Enum { public static final field Bottom Lio/mehow/laboratory/inspector/DeprecationAlignment; public static final field Regular Lio/mehow/laboratory/inspector/DeprecationAlignment; + public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lio/mehow/laboratory/inspector/DeprecationAlignment; public static fun values ()[Lio/mehow/laboratory/inspector/DeprecationAlignment; } @@ -13,6 +14,7 @@ public final class io/mehow/laboratory/inspector/DeprecationPhenotype : java/lan public static final field Hide Lio/mehow/laboratory/inspector/DeprecationPhenotype; public static final field Show Lio/mehow/laboratory/inspector/DeprecationPhenotype; public static final field Strikethrough Lio/mehow/laboratory/inspector/DeprecationPhenotype; + public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lio/mehow/laboratory/inspector/DeprecationPhenotype; public static fun values ()[Lio/mehow/laboratory/inspector/DeprecationPhenotype; } diff --git a/laboratory/inspector/build.gradle.kts b/laboratory/inspector/build.gradle.kts new file mode 100644 index 000000000..9af733376 --- /dev/null +++ b/laboratory/inspector/build.gradle.kts @@ -0,0 +1,39 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.mavenPublish) + alias(libs.plugins.spotless) + alias(libs.plugins.detekt) + alias(libs.plugins.dokka) +} + +android { + namespace = "io.mehow.laboratory.inspector" + resourcePrefix = "io_mehow_laboratory_" + + defaultConfig { + consumerProguardFile("io-mehow-laboratory-inspector.pro") + } +} + +tasks.withType().configureEach { + useJUnitPlatform() +} + +dependencies { + api(projects.laboratory.runtime) + implementation(libs.hyperion.plugin) + implementation(libs.androidx.appCompat) + implementation(libs.androidx.fragmentKtx) + implementation(libs.androidx.viewModelKtx) + implementation(libs.androidx.recyclerView) + implementation(libs.androidx.viewPager2) + implementation(libs.android.material) + implementation(libs.kotlinx.coroutinesAndroid) + + testImplementation(libs.kotest.runner.junit5) + testImplementation(libs.kotest.assertions) + testImplementation(libs.kotest.property) + testImplementation(libs.turbine) + testImplementation(libs.kotlinx.coroutinesTest) +} diff --git a/library/inspector/gradle.properties b/laboratory/inspector/gradle.properties similarity index 63% rename from library/inspector/gradle.properties rename to laboratory/inspector/gradle.properties index 42697faac..e1bab13f4 100644 --- a/library/inspector/gradle.properties +++ b/laboratory/inspector/gradle.properties @@ -1,3 +1,3 @@ POM_ARTIFACT_ID=laboratory-inspector -POM_NAME=Laboratory (inspector) +POM_NAME=Laboratory (Inspector) POM_PACKAGING=aar diff --git a/library/inspector/io-mehow-laboratory-inspector.pro b/laboratory/inspector/io-mehow-laboratory-inspector.pro similarity index 100% rename from library/inspector/io-mehow-laboratory-inspector.pro rename to laboratory/inspector/io-mehow-laboratory-inspector.pro diff --git a/library/inspector/src/main/AndroidManifest.xml b/laboratory/inspector/src/main/AndroidManifest.xml similarity index 100% rename from library/inspector/src/main/AndroidManifest.xml rename to laboratory/inspector/src/main/AndroidManifest.xml diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/DeprecationAlignment.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/DeprecationAlignment.kt similarity index 100% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/DeprecationAlignment.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/DeprecationAlignment.kt diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/DeprecationHandler.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/DeprecationHandler.kt similarity index 100% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/DeprecationHandler.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/DeprecationHandler.kt diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/DeprecationPhenotype.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/DeprecationPhenotype.kt similarity index 100% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/DeprecationPhenotype.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/DeprecationPhenotype.kt diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureAdapter.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/FeatureAdapter.kt similarity index 68% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureAdapter.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/FeatureAdapter.kt index 57c0ba5bf..1f526d37e 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureAdapter.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/FeatureAdapter.kt @@ -13,20 +13,35 @@ internal class FeatureAdapter( ) : ListAdapter(DiffCallback) { override fun getItemViewType(position: Int) = R.layout.io_mehow_laboratory_feature_item - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FeatureViewHolder { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): FeatureViewHolder { val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) return FeatureViewHolder(view, listener) } - override fun onBindViewHolder(holder: FeatureViewHolder, position: Int) = holder.bind(getItem(position)) + override fun onBindViewHolder( + holder: FeatureViewHolder, + position: Int, + ) = holder.bind(getItem(position)) private object DiffCallback : ItemCallback() { - override fun areItemsTheSame(old: FeatureUiModel, new: FeatureUiModel) = old.type == new.type + override fun areItemsTheSame( + old: FeatureUiModel, + new: FeatureUiModel, + ) = old.type == new.type - override fun areContentsTheSame(old: FeatureUiModel, new: FeatureUiModel) = old == new + override fun areContentsTheSame( + old: FeatureUiModel, + new: FeatureUiModel, + ) = old == new // Prevent item animation change. - override fun getChangePayload(old: FeatureUiModel, new: FeatureUiModel) = Unit + override fun getChangePayload( + old: FeatureUiModel, + new: FeatureUiModel, + ) = Unit } interface Listener : OptionGroupListener, OnSelectSourceListener { diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureCoordinates.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/FeatureCoordinates.kt similarity index 100% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureCoordinates.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/FeatureCoordinates.kt diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureUiModel.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/FeatureUiModel.kt similarity index 89% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureUiModel.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/FeatureUiModel.kt index 0451c9b59..d489cda59 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureUiModel.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/FeatureUiModel.kt @@ -16,9 +16,9 @@ internal data class FeatureUiModel( val hasMultipleSources = sources.size > 1 private val isCurrentSourceLocal = sources.firstOrNull(OptionUiModel::isSelected) - ?.option - ?.name - ?.equals("Local", ignoreCase = true) ?: true + ?.option + ?.name + ?.equals("Local", ignoreCase = true) ?: true private val isSupervised = type.supervisorOption == supervisorOption @@ -28,8 +28,8 @@ internal data class FeatureUiModel( private val firstAlignmentOrdinal = DeprecationAlignment.values().first() val NaturalComparator = compareBy( - { it.deprecationAlignment ?: firstAlignmentOrdinal }, - { it.name } + { it.deprecationAlignment ?: firstAlignmentOrdinal }, + { it.name }, ) } } diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureViewHolder.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/FeatureViewHolder.kt similarity index 100% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureViewHolder.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/FeatureViewHolder.kt diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/Fragments.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/Fragments.kt similarity index 100% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/Fragments.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/Fragments.kt diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/InspectorViewModel.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/InspectorViewModel.kt similarity index 76% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/InspectorViewModel.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/InspectorViewModel.kt index 3546662c6..4219a5e8c 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/InspectorViewModel.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/InspectorViewModel.kt @@ -56,13 +56,13 @@ internal class InspectorViewModel( flow { val groups = withContext(computationDispatcher) { featureFactory.create() - .mapNotNull(metadataFactory::create) - .map { it.observeGroup(laboratory) } - .combineLatest() + .mapNotNull(metadataFactory::create) + .map { it.observeGroup(laboratory) } + .combineLatest() } val searchedGroups = combine(groups, initiatedSearchQueries) { group, query -> group.search(query) } - .map { it.sortedWith(FeatureUiModel.NaturalComparator) } - .flowOn(computationDispatcher) + .map { it.sortedWith(FeatureUiModel.NaturalComparator) } + .flowOn(computationDispatcher) emitAll(searchedGroups) }.shareIn(viewModelScope, SharingStarted.Lazily, replay = 1) } @@ -78,12 +78,12 @@ internal class InspectorViewModel( val featureCoordinatesFlow: Flow get() = mutableNavigationFlow suspend fun goTo(feature: Class>) = sectionFlows.values.asFlow().withIndex() - .mapNotNull { (sectionIndex, sectionFlow) -> - val listIndex = sectionFlow.first().map(FeatureUiModel::type).indexOf(feature) - if (listIndex == -1) null else FeatureCoordinates(sectionIndex, listIndex) - } - .firstOrNull() - ?.also { mutableNavigationFlow.emit(it) } + .mapNotNull { (sectionIndex, sectionFlow) -> + val listIndex = sectionFlow.first().map(FeatureUiModel::type).indexOf(feature) + if (listIndex == -1) null else FeatureCoordinates(sectionIndex, listIndex) + } + .firstOrNull() + ?.also { mutableNavigationFlow.emit(it) } private class FeatureMetadata( private val feature: Class>, @@ -97,31 +97,30 @@ internal class InspectorViewModel( private val sourceMetadata = feature.source?.let { FeatureMetadata(it, allFeatures, deprecationHandler) } private val deprecationLevel = feature.annotations - .filterIsInstance() - .firstOrNull() - ?.level + .filterIsInstance() + .firstOrNull() + ?.level private val deprecationPhenotype = deprecationLevel?.let(deprecationHandler::getPhenotype) private val deprecationPlacement = deprecationLevel?.let(deprecationHandler::getAlignment) - @Suppress("UNCHECKED_CAST") fun observeGroup(laboratory: Laboratory): Flow { val featureEmissions = observeOptions(laboratory) val sourceEmissions = sourceMetadata?.observeOptions(laboratory) ?: flowOf(emptyList()) val supervisorEmissions = feature.supervisorOption - ?.let { laboratory.observe(it::class.java) } - ?: flowOf(null) + ?.let { laboratory.observe(it::class.java) } + ?: flowOf(null) return combine(featureEmissions, sourceEmissions, supervisorEmissions) { features, sources, supervisor -> FeatureUiModel( - type = feature, - name = simpleReadableName, - description = feature.description.tokenize(), - models = features, - sources = sources, - deprecationAlignment = deprecationPlacement, - deprecationPhenotype = deprecationPhenotype, - supervisorOption = supervisor, + type = feature, + name = simpleReadableName, + description = feature.description.tokenize(), + models = features, + sources = sources, + deprecationAlignment = deprecationPlacement, + deprecationPhenotype = deprecationPhenotype, + supervisorOption = supervisor, ) } } @@ -139,14 +138,14 @@ internal class InspectorViewModel( ) { private val allFeatures by lazy { featureFactories.values - .flatMap { it.create() } - .filterNot { it.options.isEmpty() } + .flatMap { it.create() } + .filterNot { it.options.isEmpty() } } fun create(feature: Class>) = feature - .takeUnless { it.options.isEmpty() } - ?.let { FeatureMetadata(it, allFeatures, deprecationHandler) } - ?.takeIf { it.deprecationPhenotype != DeprecationPhenotype.Hide } + .takeUnless { it.options.isEmpty() } + ?.let { FeatureMetadata(it, allFeatures, deprecationHandler) } + ?.takeIf { it.deprecationPhenotype != DeprecationPhenotype.Hide } } } @@ -156,13 +155,14 @@ internal class InspectorViewModel( ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { require(modelClass == InspectorViewModel::class.java) { "Cannot create $modelClass" } - @Suppress("UNCHECKED_CAST") @OptIn(FlowPreview::class) + @Suppress("UNCHECKED_CAST") + @OptIn(FlowPreview::class) return InspectorViewModel( - configuration.laboratory, - searchViewModel.uiModels.debounce(200.milliseconds).map { it.query }, - configuration.featureFactories, - configuration.deprecation, - Dispatchers.Default, + configuration.laboratory, + searchViewModel.uiModels.debounce(200.milliseconds).map { it.query }, + configuration.featureFactories, + configuration.deprecation, + Dispatchers.Default, ) as T } } diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/Iterables.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/Iterables.kt similarity index 69% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/Iterables.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/Iterables.kt index 318b959f8..f7fde6d99 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/Iterables.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/Iterables.kt @@ -10,13 +10,13 @@ internal fun Iterable.containsAllInOrder( predicate: (left: T, right: T) -> Boolean, ): Boolean { val allCombinations = asSequence() - .mapIndexed(::Pair) - .flatMap { left -> other.mapIndexed { index, right -> left to (index to right) } } + .mapIndexed(::Pair) + .flatMap { left -> other.mapIndexed { index, right -> left to (index to right) } } val uniqueFinds = allCombinations - .filter { (left, right) -> predicate(left.second, right.second) } - .map { (_, right) -> right } - .distinct() - .map { (_, value) -> value } + .filter { (left, right) -> predicate(left.second, right.second) } + .map { (_, right) -> right } + .distinct() + .map { (_, value) -> value } return uniqueFinds.toList() == other.toList() } diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/LaboratoryActivity.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/LaboratoryActivity.kt similarity index 91% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/LaboratoryActivity.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/LaboratoryActivity.kt index ce727a015..2ba7fdf34 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/LaboratoryActivity.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/LaboratoryActivity.kt @@ -34,21 +34,21 @@ public class LaboratoryActivity : AppCompatActivity(R.layout.io_mehow_laboratory InspectorViewModel.Factory(configuration, searchViewModel) } - override fun onCreate(inState: Bundle?) { - super.onCreate(inState) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) setUpToolbar() setUpViewPager() } private fun setUpToolbar() { val binding = ToolbarBinding( - view = window.decorView, - onSearchEventsListener = { event -> searchViewModel.sendEvent(event) }, - onResetEventsListener = { resetFeatureFlags() }, + view = window.decorView, + onSearchEventsListener = { event -> searchViewModel.sendEvent(event) }, + onResetEventsListener = { resetFeatureFlags() }, ) searchViewModel.uiModels - .onEach { uiModel -> binding.render(uiModel) } - .launchIn(lifecycleScope) + .onEach { uiModel -> binding.render(uiModel) } + .launchIn(lifecycleScope) } private fun setUpViewPager() { @@ -69,19 +69,20 @@ public class LaboratoryActivity : AppCompatActivity(R.layout.io_mehow_laboratory } private fun observeNavigationEvents(viewPager: ViewPager2) = inspectorViewModel.featureCoordinatesFlow - .onEach { (sectionIndex, featureIndex) -> - viewPager.currentItem = sectionIndex - awaitSectionFragment(sectionNames[sectionIndex]).scrollTo(featureIndex) - } - .launchIn(lifecycleScope) + .onEach { (sectionIndex, featureIndex) -> + viewPager.currentItem = sectionIndex + awaitSectionFragment(sectionNames[sectionIndex]).scrollTo(featureIndex) + } + .launchIn(lifecycleScope) private suspend fun awaitSectionFragment(sectionName: String): SectionFragment = supportFragmentManager.fragments - .filterIsInstance() - .firstOrNull { it.sectionName == sectionName } - ?: run { - delay(100) // ¯\_(ツ)_/¯ - awaitSectionFragment(sectionName) - } + .filterIsInstance() + .firstOrNull { it.sectionName == sectionName } + ?: run { + @Suppress("MagicNumber") + delay(100) // ¯\_(ツ)_/¯ + awaitSectionFragment(sectionName) + } private fun resetFeatureFlags() = lifecycleScope.launch { val isCleared = configuration.laboratory.clear() @@ -242,10 +243,12 @@ public class LaboratoryActivity : AppCompatActivity(R.layout.io_mehow_laboratory externalFactories: Map = emptyMap(), ) { val filteredFactories = externalFactories.filterNot { it.key == featuresLabel } - configure(Configuration.create( + configure( + Configuration.create( laboratory, - featureFactories = linkedMapOf(featuresLabel to mainFactory) + filteredFactories - )) + featureFactories = linkedMapOf(featuresLabel to mainFactory) + filteredFactories, + ), + ) } /** diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/OptionUiModel.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/OptionUiModel.kt similarity index 100% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/OptionUiModel.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/OptionUiModel.kt diff --git a/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/OptionViewGroup.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/OptionViewGroup.kt new file mode 100644 index 000000000..9425addcf --- /dev/null +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/OptionViewGroup.kt @@ -0,0 +1,101 @@ +package io.mehow.laboratory.inspector + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.CompoundButton +import androidx.appcompat.content.res.AppCompatResources +import androidx.appcompat.widget.PopupMenu +import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipGroup +import io.mehow.laboratory.Feature +import com.google.android.material.R as MaterialR + +internal class OptionViewGroup + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet, + defStyle: Int = MaterialR.attr.chipGroupStyle, + ) : ChipGroup(context, attrs, defStyle) { + private val inflater = LayoutInflater.from(context) + private var listener: OptionGroupListener? = null + + init { + isSelectionRequired = true + } + + fun setOnSelectFeatureListener(listener: OptionGroupListener?) { + this.listener = listener + } + + fun render( + models: List, + isEnabled: Boolean, + ) { + chips.forEach(::removeOnCheckedChangeListener) + removeAllViews() + models.map { createChip(it, isEnabled) }.forEach(::addView) + } + + private fun createChip( + model: OptionUiModel, + isEnabled: Boolean, + ): Chip { + val chip = inflater.inflate(R.layout.io_mehow_laboratory_feature_option_chip, this, false) as Chip + return chip.apply { + text = model.option.name + isChecked = model.isSelected + if (model.supervisedFeatures.isNotEmpty()) { + chipIcon = AppCompatResources.getDrawable(context, R.drawable.io_mehow_laboratory_supervisor) + setOnLongClickListener { showSupervisedFeaturesMenu(this, model.supervisedFeatures) } + } + isActivated = isEnabled + this.isEnabled = isEnabled + setOnCheckedChangeListener(createListener(model)) + } + } + + private fun createListener(model: OptionUiModel) = CompoundButton.OnCheckedChangeListener { chip, isChecked -> + if (isChecked) { + (chip as Chip).deselectOtherChips() + listener?.onSelectOption(model.option) + } + } + + // ChipGroup.isSingleSelection does not work with initial selection from code. + private fun Chip.deselectOtherChips() { + chips.filter { it !== this }.forEach { chip -> chip.isChecked = false } + } + + private fun removeOnCheckedChangeListener(chip: Chip) = chip.setOnCheckedChangeListener(null) + + private fun showSupervisedFeaturesMenu( + anchor: Chip, + features: List>>, + ): Boolean { + PopupMenu(context, anchor).apply { + features.forEachIndexed { index, feature -> + menu.add(0, index, index, feature.simpleName) + } + setOnMenuItemClickListener { + listener?.onSelectSupervisedFeature(features[it.order]) + true + } + }.show() + return true + } + + private val chips: Sequence get() = sequence { + for (index in 0 until childCount) { + val chip = getChildAt(index) as? Chip ?: continue + yield(chip) + } + } + + interface OptionGroupListener { + fun onSelectOption(option: Feature<*>) + + fun onSelectSupervisedFeature(feature: Class>) + } + } diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SearchMode.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SearchMode.kt similarity index 95% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/SearchMode.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SearchMode.kt index c7a16c1d0..3a343931e 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SearchMode.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SearchMode.kt @@ -3,5 +3,4 @@ package io.mehow.laboratory.inspector internal enum class SearchMode { Idle, Active, - ; } diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SearchQuery.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SearchQuery.kt similarity index 90% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/SearchQuery.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SearchQuery.kt index 0905a69af..dbf9b3bab 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SearchQuery.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SearchQuery.kt @@ -4,9 +4,9 @@ import java.util.Locale internal class SearchQuery(input: String) { private val parts = input.replace(whiteSpaceRegex, " ") - .split(' ') - .flatMap { it.replace(searchNoiseRegex, "").splitToParts() } - .map { it.lowercase(Locale.ROOT) } + .split(' ') + .flatMap { it.replace(searchNoiseRegex, "").splitToParts() } + .map { it.lowercase(Locale.ROOT) } private val joinedParts = parts.joinToString("") fun isNotEmpty() = parts.isNotEmpty() diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SearchViewModel.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SearchViewModel.kt similarity index 94% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/SearchViewModel.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SearchViewModel.kt index aed6ae030..32530218f 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SearchViewModel.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SearchViewModel.kt @@ -20,8 +20,8 @@ internal class SearchViewModel : ViewModel() { private val uiModelChanges = MutableSharedFlow<(UiModel) -> UiModel>() private val sharedUiModels = uiModelChanges.scan( - initial = UiModel(Idle, SearchQuery.Empty), - operation = { currentModel, updateModel -> updateModel(currentModel) } + initial = UiModel(Idle, SearchQuery.Empty), + operation = { currentModel, updateModel -> updateModel(currentModel) }, ).shareIn(viewModelScope, SharingStarted.Lazily, replay = 1).distinctUntilChanged() val uiModels: Flow = sharedUiModels @@ -59,7 +59,9 @@ internal class SearchViewModel : ViewModel() { sealed class Event { object OpenSearch : Event() + object CloseSearch : Event() + class UpdateQuery(val query: String) : Event() } diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SectionAdapter.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SectionAdapter.kt similarity index 100% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/SectionAdapter.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SectionAdapter.kt diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SectionFragment.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SectionFragment.kt similarity index 93% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/SectionFragment.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SectionFragment.kt index 6c9aef854..77fbd0e1f 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SectionFragment.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SectionFragment.kt @@ -36,7 +36,10 @@ internal class SectionFragment : Fragment(R.layout.io_mehow_laboratory_feature_g } }) - override fun onViewCreated(view: View, inState: Bundle?) { + override fun onViewCreated( + view: View, + inState: Bundle?, + ) { view.findViewById(R.id.io_mehow_laboratory_feature_section).apply { layoutManager = SmoothScrollingLinearLayoutManager(requireActivity()).also { this@SectionFragment.layoutManager = it @@ -48,8 +51,8 @@ internal class SectionFragment : Fragment(R.layout.io_mehow_laboratory_feature_g } private fun observeGroup() = inspectorViewModel.sectionFlow(sectionName) - .onEach { featureAdapter.submitList(it) } - .launchIn(viewLifecycleOwner.lifecycleScope) + .onEach { featureAdapter.submitList(it) } + .launchIn(viewLifecycleOwner.lifecycleScope) fun scrollTo(index: Int) = layoutManager.smoothScrollTo(index) diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SmoothScrollingLinearLayoutManager.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SmoothScrollingLinearLayoutManager.kt similarity index 100% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/SmoothScrollingLinearLayoutManager.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SmoothScrollingLinearLayoutManager.kt diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SourceAdapter.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SourceAdapter.kt similarity index 87% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/SourceAdapter.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SourceAdapter.kt index 36c5886e0..52816757f 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SourceAdapter.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SourceAdapter.kt @@ -10,7 +10,11 @@ import io.mehow.laboratory.Feature internal class SourceAdapter( private val features: List>, ) : BaseAdapter() { - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + override fun getView( + position: Int, + convertView: View?, + parent: ViewGroup, + ): View { val inflater = LayoutInflater.from(parent.context) val viewHolder = if (convertView == null) { val view = inflater.inflate(R.layout.io_mehow_laboratory_feature_source_spinner_item, parent, false) @@ -22,7 +26,11 @@ internal class SourceAdapter( return viewHolder.item } - override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { + override fun getDropDownView( + position: Int, + convertView: View?, + parent: ViewGroup, + ): View { val inflater = LayoutInflater.from(parent.context) val viewHolder = if (convertView == null) { val view = inflater.inflate(R.layout.io_mehow_laboratory_feature_source_drop_down_item, parent, false) diff --git a/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SourceViewGroup.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SourceViewGroup.kt new file mode 100644 index 000000000..e58bc4b54 --- /dev/null +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SourceViewGroup.kt @@ -0,0 +1,61 @@ +package io.mehow.laboratory.inspector + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.AdapterView +import androidx.appcompat.widget.AppCompatSpinner +import io.mehow.laboratory.Feature +import androidx.appcompat.R as AppCompatR + +internal class SourceViewGroup + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet, + defStyle: Int = AppCompatR.attr.spinnerStyle, + ) : AppCompatSpinner(context, attrs, defStyle) { + internal var listener: OnSelectSourceListener? = null + + override fun getAdapter() = super.getAdapter() as? SourceAdapter + + fun setOnSelectSourceListener(listener: OnSelectSourceListener?) { + this.listener = listener + } + + fun render(models: List) { + val features = models.map(OptionUiModel::option) + val selectedFeature = models.firstOrNull(OptionUiModel::isSelected)?.option ?: return + val newAdapter = SourceAdapter(features) + onItemSelectedListener = createListener() + adapter = newAdapter + val position = newAdapter.positionOf(selectedFeature) + setSelection(position) + } + + private fun createListener() = object : OnItemSelectedListener { + var ignoreItem = true + + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long, + ) { + if (ignoreItem) { + ignoreItem = false + return + } + val item = requireNotNull(adapter) { + "Feature source adapter is not set" + }.getItem(position) + listener?.onSelectSource(item) + } + + override fun onNothingSelected(parent: AdapterView<*>?) = Unit + } + + interface OnSelectSourceListener { + fun onSelectSource(option: Feature<*>) + } + } diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SourceViewHolder.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SourceViewHolder.kt similarity index 100% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/SourceViewHolder.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/SourceViewHolder.kt diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/TextToken.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/TextToken.kt similarity index 92% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/TextToken.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/TextToken.kt index 20b06b154..f256dfd29 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/TextToken.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/TextToken.kt @@ -33,9 +33,9 @@ internal fun String.tokenize(): List { val regularTokens = matches.toRegularTokens(this) val linkTokens = matches.toLinkTokens() return (regularTokens + linkTokens) - .sortedBy { (_, startIndex) -> startIndex } - .map { (token, _) -> token } - .toList() + .sortedBy { (_, startIndex) -> startIndex } + .map { (token, _) -> token } + .toList() } private fun Sequence.toLinkTokens() = map { matchResult -> @@ -44,7 +44,7 @@ private fun Sequence.toLinkTokens() = map { matchResult -> } private fun Sequence.toRegularTokens(text: String) = toUnmatchedRanges(text) - .map { range -> Regular(text.substring(range)) to range.first } + .map { range -> Regular(text.substring(range)) to range.first } private fun Sequence.toUnmatchedRanges(text: String) = sequence { yield(Int.MIN_VALUE..0) diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/ToolbarBinding.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/ToolbarBinding.kt similarity index 81% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/ToolbarBinding.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/ToolbarBinding.kt index d680eca60..763b5f4d4 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/ToolbarBinding.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/ToolbarBinding.kt @@ -25,11 +25,11 @@ internal class ToolbarBinding( private val resetFeatures = view.findViewById(R.id.io_mehow_laboratory_reset_features) private val resetFeaturesDialog = MaterialAlertDialogBuilder(view.context) - .setTitle(R.string.io_mehow_laboratory_reset_title) - .setMessage(R.string.io_mehow_laboratory_reset_message) - .setNegativeButton(R.string.io_mehow_laboratory_cancel) { _, _ -> } - .setPositiveButton(R.string.io_mehow_laboratory_reset) { _, _ -> onResetEventsListener() } - .create() + .setTitle(R.string.io_mehow_laboratory_reset_title) + .setMessage(R.string.io_mehow_laboratory_reset_message) + .setNegativeButton(R.string.io_mehow_laboratory_cancel) { _, _ -> } + .setPositiveButton(R.string.io_mehow_laboratory_reset) { _, _ -> onResetEventsListener() } + .create() init { var oldText: String? = null @@ -41,8 +41,20 @@ internal class ToolbarBinding( onSearchEventsListener(UpdateQuery(query)) } } - override fun beforeTextChanged(text: CharSequence?, start: Int, count: Int, after: Int) = Unit - override fun onTextChanged(text: CharSequence?, start: Int, before: Int, count: Int) = Unit + + override fun beforeTextChanged( + text: CharSequence?, + start: Int, + count: Int, + after: Int, + ) = Unit + + override fun onTextChanged( + text: CharSequence?, + start: Int, + before: Int, + count: Int, + ) = Unit }) openSearch.setOnClickListener { onSearchEventsListener(OpenSearch) } closeSearch.setOnClickListener { onSearchEventsListener(CloseSearch) } diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/Views.kt b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/Views.kt similarity index 84% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/Views.kt rename to laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/Views.kt index f26a792a3..170375341 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/Views.kt +++ b/laboratory/inspector/src/main/kotlin/io/mehow/laboratory/inspector/Views.kt @@ -14,9 +14,11 @@ import kotlin.math.absoluteValue internal fun View.focusAndShowKeyboard() { fun View.showKeyboardIfFocused() { - if (isFocused) post { - val service = context.getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager - service?.showSoftInput(this, SHOW_IMPLICIT) + if (isFocused) { + post { + val service = context.getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager + service?.showSoftInput(this, SHOW_IMPLICIT) + } } } @@ -29,7 +31,9 @@ internal fun View.focusAndShowKeyboard() { } } viewTreeObserver.addOnWindowFocusChangeListener(listener) - } else showKeyboardIfFocused() + } else { + showKeyboardIfFocused() + } } internal fun View.hideKeyboard() { @@ -42,7 +46,11 @@ internal fun RecyclerView.hideKeyboardOnScroll() { val touchSlop = ViewConfiguration.get(context).scaledTouchSlop var totalDy = 0 addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + override fun onScrolled( + recyclerView: RecyclerView, + dx: Int, + dy: Int, + ) { totalDy += dy.absoluteValue if (totalDy >= touchSlop) { totalDy = 0 diff --git a/library/inspector/src/main/res/color/io_mehow_laboratory_chip_background.xml b/laboratory/inspector/src/main/res/color/io_mehow_laboratory_chip_background.xml similarity index 100% rename from library/inspector/src/main/res/color/io_mehow_laboratory_chip_background.xml rename to laboratory/inspector/src/main/res/color/io_mehow_laboratory_chip_background.xml diff --git a/library/inspector/src/main/res/color/io_mehow_laboratory_chip_text.xml b/laboratory/inspector/src/main/res/color/io_mehow_laboratory_chip_text.xml similarity index 100% rename from library/inspector/src/main/res/color/io_mehow_laboratory_chip_text.xml rename to laboratory/inspector/src/main/res/color/io_mehow_laboratory_chip_text.xml diff --git a/library/inspector/src/main/res/color/io_mehow_laboratory_hint_text.xml b/laboratory/inspector/src/main/res/color/io_mehow_laboratory_hint_text.xml similarity index 100% rename from library/inspector/src/main/res/color/io_mehow_laboratory_hint_text.xml rename to laboratory/inspector/src/main/res/color/io_mehow_laboratory_hint_text.xml diff --git a/library/inspector/src/main/res/drawable/io_mehow_laboratory_clear.xml b/laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_clear.xml similarity index 100% rename from library/inspector/src/main/res/drawable/io_mehow_laboratory_clear.xml rename to laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_clear.xml diff --git a/library/inspector/src/main/res/drawable/io_mehow_laboratory_close_serach.xml b/laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_close_serach.xml similarity index 100% rename from library/inspector/src/main/res/drawable/io_mehow_laboratory_close_serach.xml rename to laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_close_serach.xml diff --git a/library/inspector/src/main/res/drawable/io_mehow_laboratory_hint_cursor.xml b/laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_hint_cursor.xml similarity index 100% rename from library/inspector/src/main/res/drawable/io_mehow_laboratory_hint_cursor.xml rename to laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_hint_cursor.xml diff --git a/library/inspector/src/main/res/drawable/io_mehow_laboratory_open_serach.xml b/laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_open_serach.xml similarity index 100% rename from library/inspector/src/main/res/drawable/io_mehow_laboratory_open_serach.xml rename to laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_open_serach.xml diff --git a/library/inspector/src/main/res/drawable/io_mehow_laboratory_reset_features.xml b/laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_reset_features.xml similarity index 100% rename from library/inspector/src/main/res/drawable/io_mehow_laboratory_reset_features.xml rename to laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_reset_features.xml diff --git a/library/inspector/src/main/res/drawable/io_mehow_laboratory_supervisor.xml b/laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_supervisor.xml similarity index 100% rename from library/inspector/src/main/res/drawable/io_mehow_laboratory_supervisor.xml rename to laboratory/inspector/src/main/res/drawable/io_mehow_laboratory_supervisor.xml diff --git a/library/inspector/src/main/res/layout/io_mehow_laboratory_feature_group.xml b/laboratory/inspector/src/main/res/layout/io_mehow_laboratory_feature_group.xml similarity index 100% rename from library/inspector/src/main/res/layout/io_mehow_laboratory_feature_group.xml rename to laboratory/inspector/src/main/res/layout/io_mehow_laboratory_feature_group.xml diff --git a/library/inspector/src/main/res/layout/io_mehow_laboratory_feature_item.xml b/laboratory/inspector/src/main/res/layout/io_mehow_laboratory_feature_item.xml similarity index 100% rename from library/inspector/src/main/res/layout/io_mehow_laboratory_feature_item.xml rename to laboratory/inspector/src/main/res/layout/io_mehow_laboratory_feature_item.xml diff --git a/library/inspector/src/main/res/layout/io_mehow_laboratory_feature_option_chip.xml b/laboratory/inspector/src/main/res/layout/io_mehow_laboratory_feature_option_chip.xml similarity index 100% rename from library/inspector/src/main/res/layout/io_mehow_laboratory_feature_option_chip.xml rename to laboratory/inspector/src/main/res/layout/io_mehow_laboratory_feature_option_chip.xml diff --git a/library/inspector/src/main/res/layout/io_mehow_laboratory_feature_source_drop_down_item.xml b/laboratory/inspector/src/main/res/layout/io_mehow_laboratory_feature_source_drop_down_item.xml similarity index 100% rename from library/inspector/src/main/res/layout/io_mehow_laboratory_feature_source_drop_down_item.xml rename to laboratory/inspector/src/main/res/layout/io_mehow_laboratory_feature_source_drop_down_item.xml diff --git a/library/inspector/src/main/res/layout/io_mehow_laboratory_feature_source_spinner_item.xml b/laboratory/inspector/src/main/res/layout/io_mehow_laboratory_feature_source_spinner_item.xml similarity index 100% rename from library/inspector/src/main/res/layout/io_mehow_laboratory_feature_source_spinner_item.xml rename to laboratory/inspector/src/main/res/layout/io_mehow_laboratory_feature_source_spinner_item.xml diff --git a/library/inspector/src/main/res/layout/io_mehow_laboratory_inspector.xml b/laboratory/inspector/src/main/res/layout/io_mehow_laboratory_inspector.xml similarity index 100% rename from library/inspector/src/main/res/layout/io_mehow_laboratory_inspector.xml rename to laboratory/inspector/src/main/res/layout/io_mehow_laboratory_inspector.xml diff --git a/library/inspector/src/main/res/values-night/dimens.xml b/laboratory/inspector/src/main/res/values-night/dimens.xml similarity index 100% rename from library/inspector/src/main/res/values-night/dimens.xml rename to laboratory/inspector/src/main/res/values-night/dimens.xml diff --git a/library/inspector/src/main/res/values-night/themes.xml b/laboratory/inspector/src/main/res/values-night/themes.xml similarity index 100% rename from library/inspector/src/main/res/values-night/themes.xml rename to laboratory/inspector/src/main/res/values-night/themes.xml diff --git a/library/inspector/src/main/res/values/dimens.xml b/laboratory/inspector/src/main/res/values/dimens.xml similarity index 100% rename from library/inspector/src/main/res/values/dimens.xml rename to laboratory/inspector/src/main/res/values/dimens.xml diff --git a/library/inspector/src/main/res/values/public.xml b/laboratory/inspector/src/main/res/values/public.xml similarity index 100% rename from library/inspector/src/main/res/values/public.xml rename to laboratory/inspector/src/main/res/values/public.xml diff --git a/library/inspector/src/main/res/values/strings.xml b/laboratory/inspector/src/main/res/values/strings.xml similarity index 100% rename from library/inspector/src/main/res/values/strings.xml rename to laboratory/inspector/src/main/res/values/strings.xml diff --git a/library/inspector/src/main/res/values/styles.xml b/laboratory/inspector/src/main/res/values/styles.xml similarity index 100% rename from library/inspector/src/main/res/values/styles.xml rename to laboratory/inspector/src/main/res/values/styles.xml diff --git a/library/inspector/src/main/res/values/themes.xml b/laboratory/inspector/src/main/res/values/themes.xml similarity index 100% rename from library/inspector/src/main/res/values/themes.xml rename to laboratory/inspector/src/main/res/values/themes.xml diff --git a/library/inspector/src/test/java/io/mehow/laboratory/inspector/FlowTurbines.kt b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/FlowTurbines.kt similarity index 68% rename from library/inspector/src/test/java/io/mehow/laboratory/inspector/FlowTurbines.kt rename to laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/FlowTurbines.kt index 9c16f0371..58a5891ba 100644 --- a/library/inspector/src/test/java/io/mehow/laboratory/inspector/FlowTurbines.kt +++ b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/FlowTurbines.kt @@ -1,10 +1,10 @@ package io.mehow.laboratory.inspector -import app.cash.turbine.FlowTurbine +import app.cash.turbine.TurbineTestContext import kotlinx.coroutines.withTimeout -internal suspend fun FlowTurbine.awaitItemEventually( - timeoutMs: Long = this.timeoutMs, +suspend fun TurbineTestContext.awaitItemEventually( + timeoutMs: Long = 1000L, assertion: (T) -> Unit, ) = withTimeout(timeoutMs) { while (true) { diff --git a/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelDeprecationSpec.kt b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelDeprecationSpec.kt new file mode 100644 index 000000000..6e5f9c631 --- /dev/null +++ b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelDeprecationSpec.kt @@ -0,0 +1,174 @@ +package io.mehow.laboratory.inspector + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldContainExactly +import io.mehow.laboratory.Feature +import io.mehow.laboratory.FeatureFactory +import io.mehow.laboratory.Laboratory +import io.mehow.laboratory.inspector.DeprecationAlignment.Bottom +import io.mehow.laboratory.inspector.DeprecationAlignment.Regular +import io.mehow.laboratory.inspector.DeprecationPhenotype.Hide +import io.mehow.laboratory.inspector.DeprecationPhenotype.Show +import io.mehow.laboratory.inspector.DeprecationPhenotype.Strikethrough +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.first +import kotlin.DeprecationLevel.ERROR +import kotlin.DeprecationLevel.HIDDEN +import kotlin.DeprecationLevel.WARNING + +class InspectorViewModelDeprecationSpec : FunSpec({ + setMainDispatcher() + + test("can be filtered out") { + val viewModel = InspectorViewModel( + DeprecationHandler( + phenotypeSelector = { Hide }, + alignmentSelector = { Regular }, + ), + ) + + val featureNames = viewModel.sectionFlow().first().map(FeatureUiModel::name) + + featureNames shouldContainExactly listOf("NotDeprecated") + } + + test("can be struck through") { + val viewModel = InspectorViewModel( + DeprecationHandler( + phenotypeSelector = { Strikethrough }, + alignmentSelector = { Regular }, + ), + ) + + val featureNames = viewModel.sectionFlow().first().map { it.name to it.deprecationPhenotype } + + featureNames shouldContainExactly listOf( + "DeprecatedError" to Strikethrough, + "DeprecatedHidden" to Strikethrough, + "DeprecatedWarning" to Strikethrough, + "NotDeprecated" to null, + ) + } + + test("can be shown") { + val viewModel = InspectorViewModel( + DeprecationHandler( + phenotypeSelector = { Show }, + alignmentSelector = { Regular }, + ), + ) + + val featureNames = viewModel.sectionFlow().first().map { it.name to it.deprecationPhenotype } + + featureNames shouldContainExactly listOf( + "DeprecatedError" to Show, + "DeprecatedHidden" to Show, + "DeprecatedWarning" to Show, + "NotDeprecated" to null, + ) + } + + test("can be moved to bottom") { + val viewModel = InspectorViewModel( + DeprecationHandler( + phenotypeSelector = { Show }, + alignmentSelector = { Bottom }, + ), + ) + + val featureNames = viewModel.sectionFlow().first().map { it.name to it.deprecationPhenotype } + + featureNames shouldContainExactly listOf( + "NotDeprecated" to null, + "DeprecatedError" to Show, + "DeprecatedHidden" to Show, + "DeprecatedWarning" to Show, + ) + } + + test("can be selected based on deprecation level") { + val viewModel = InspectorViewModel( + DeprecationHandler( + phenotypeSelector = { if (it == WARNING) Strikethrough else Show }, + alignmentSelector = { if (it != WARNING) Bottom else Regular }, + ), + ) + + val featureNames = viewModel.sectionFlow().first().map { it.name to it.deprecationPhenotype } + + featureNames shouldContainExactly listOf( + "DeprecatedWarning" to Strikethrough, + "NotDeprecated" to null, + "DeprecatedError" to Show, + "DeprecatedHidden" to Show, + ) + } +}) + +private object DeprecatedFeatureFactory : FeatureFactory { + override fun create(): Set>> { + @Suppress("UNCHECKED_CAST") + return setOf( + Class.forName("io.mehow.laboratory.inspector.DeprecatedWarning"), + Class.forName("io.mehow.laboratory.inspector.DeprecatedError"), + Class.forName("io.mehow.laboratory.inspector.DeprecatedHidden"), + Class.forName("io.mehow.laboratory.inspector.NotDeprecated"), + ) as Set>> + } +} + +@Deprecated("", level = WARNING) +private enum class DeprecatedWarning : Feature< + @Suppress("DEPRECATION") + DeprecatedWarning, +> { + Option, + ; + + @Suppress("DEPRECATION") + override val defaultOption: DeprecatedWarning + get() = Option +} + +@Deprecated("message", level = ERROR) +private enum class DeprecatedError : Feature< + @Suppress("DEPRECATION_ERROR") + DeprecatedError, +> { + Option, + ; + + @Suppress("DEPRECATION_ERROR") + override val defaultOption: DeprecatedError + get() = Option +} + +@Deprecated("", level = HIDDEN) +private enum class DeprecatedHidden : Feature< + @Suppress("DEPRECATION_ERROR") + DeprecatedHidden, +> { + Option, + ; + + @Suppress("DEPRECATION_ERROR") + override val defaultOption: DeprecatedHidden + get() = Option +} + +private enum class NotDeprecated : Feature { + Option, + ; + + override val defaultOption: NotDeprecated + get() = Option +} + +private fun InspectorViewModel( + deprecationHandler: DeprecationHandler, +) = InspectorViewModel( + Laboratory.inMemory(), + emptyFlow(), + DeprecatedFeatureFactory, + deprecationHandler, +) diff --git a/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelFeatureSpec.kt b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelFeatureSpec.kt new file mode 100644 index 000000000..3892e2383 --- /dev/null +++ b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelFeatureSpec.kt @@ -0,0 +1,318 @@ +package io.mehow.laboratory.inspector + +import app.cash.turbine.test +import io.kotest.assertions.fail +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.collections.shouldNotContain +import io.mehow.laboratory.DefaultOptionFactory +import io.mehow.laboratory.Feature +import io.mehow.laboratory.FeatureFactory +import io.mehow.laboratory.FeatureStorage +import io.mehow.laboratory.Laboratory +import io.mehow.laboratory.inspector.TextToken.Link +import io.mehow.laboratory.inspector.TextToken.Regular +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.first + +class InspectorViewModelFeatureSpec : FunSpec({ + setMainDispatcher() + + test("filters empty feature flag groups") { + val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) + + val featureNames = viewModel.sectionFlow().first().map(FeatureUiModel::name) + + featureNames shouldNotContain "Empty" + } + + test("orders feature flag groups by name") { + val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) + + val featureNames = viewModel.sectionFlow().first().map(FeatureUiModel::name) + + featureNames shouldContainExactly listOf("First", "Second") + } + + test("does not order feature flag options") { + val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) + + val features = viewModel.sectionFlow().first() + .map(FeatureUiModel::models) + .map { models -> models.map(OptionUiModel::option) } + + features[0] shouldContainExactly listOf(First.C, First.B, First.A) + features[1] shouldContainExactly listOf(Second.B, Second.C, Second.A) + } + + test("marks first feature flag option as selected by default") { + val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) + + viewModel.observeSelectedFeatures().first() shouldContainExactly listOf(First.C, Second.B) + } + + test("marks saved feature flag options as selected") { + val laboratory = Laboratory.inMemory().apply { + setOption(First.A) + setOption(Second.C) + } + + val viewModel = InspectorViewModel(laboratory, NoSourceFeatureFactory) + + viewModel.observeSelectedFeatures().first() shouldContainExactly listOf(First.A, Second.C) + } + + test("selects feature flag options") { + val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) + + viewModel.selectFeature(First.B) + viewModel.selectFeature(Second.A) + + viewModel.observeSelectedFeatures().first() shouldContainExactly listOf(First.B, Second.A) + } + + test("emits feature flag changes") { + val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) + + viewModel.observeSelectedFeatures().test { + awaitItem() shouldContainExactly listOf(First.C, Second.B) + + viewModel.selectFeature(First.B) + awaitItem() shouldContainExactly listOf(First.B, Second.B) + + viewModel.selectFeature(Second.C) + awaitItem() shouldContainExactly listOf(First.B, Second.C) + + cancel() + } + } + + test("emits source changes") { + val viewModel = InspectorViewModel(Laboratory.inMemory(), AllFeatureFactory) + + viewModel.observeSelectedFeaturesAndSources().test { + awaitItem() shouldContainExactly listOf( + First.C to null, + Second.B to null, + Sourced.A to Sourced.Source.Local, + ) + + viewModel.selectFeature(Sourced.Source.Remote) + + awaitItem() shouldContainExactly listOf( + First.C to null, + Second.B to null, + Sourced.A to Sourced.Source.Remote, + ) + } + } + + test("resets feature flags to default options declared in factory") { + val defaultOptionFactory = object : DefaultOptionFactory { + override fun > create(feature: T): Feature<*>? = when (feature) { + is First -> First.A + is Second -> Second.A + else -> null + } + } + val laboratory = Laboratory.builder() + .featureStorage(FeatureStorage.inMemory()) + .defaultOptionFactory(defaultOptionFactory) + .build() + val viewModel = InspectorViewModel(laboratory, NoSourceFeatureFactory) + + viewModel.observeSelectedFeatures().test { + awaitItem() shouldContainExactly listOf(First.A, Second.A) + + viewModel.selectFeature(First.B) + awaitItem() shouldContainExactly listOf(First.B, Second.A) + + viewModel.selectFeature(Second.B) + awaitItem() shouldContainExactly listOf(First.B, Second.B) + + laboratory.clear() + awaitItemEventually { it shouldContainExactly listOf(First.A, Second.A) } + + cancel() + } + } + + test("uses text tokens for feature flag description") { + val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) + + val descriptions = viewModel.sectionFlow().first().map(FeatureUiModel::description) + + descriptions shouldContainExactly listOf( + listOf( + Regular("Description with a "), + Link("link", "https://mehow.io"), + ), + listOf( + Regular("Description without a link"), + ), + ) + } + + test("emits feature flags supervision") { + val viewModel = InspectorViewModel(Laboratory.inMemory(), SupervisedFeatureFactory) + + viewModel.observeSelectedFeaturesAndEnabledState().test { + awaitItem() shouldContainExactly listOf( + Child.A to false, + Parent.Disabled to true, + ) + + viewModel.selectFeature(Parent.Enabled) + awaitItemEventually { + it shouldContainExactly listOf( + Child.A to true, + Parent.Enabled to true, + ) + } + + viewModel.selectFeature(Child.B) + awaitItem() shouldContainExactly listOf( + Child.B to true, + Parent.Enabled to true, + ) + + viewModel.selectFeature(Parent.Disabled) + awaitItemEventually { + it shouldContainExactly listOf( + Child.A to false, + Parent.Disabled to true, + ) + } + + cancel() + } + } + + test("includes supervised features to options") { + val viewModel = InspectorViewModel(Laboratory.inMemory(), SupervisedFeatureFactory) + + viewModel.supervisedFeaturesFlow().first() shouldContainExactly listOf( + Child.A to emptyList(), + Child.B to emptyList(), + Parent.Enabled to listOf(Child::class.java), + Parent.Disabled to emptyList(), + ) + } + + test("includes supervised features to options from different sections") { + val parentFactory = object : FeatureFactory { + override fun create(): Set>> = setOf(Parent::class.java) + } + val childFactory = object : FeatureFactory { + override fun create(): Set>> = setOf(Child::class.java) + } + + val viewModel = InspectorViewModel( + Laboratory.inMemory(), + searchQueries = emptyFlow(), + mapOf("Parent" to parentFactory, "Child" to childFactory), + DeprecationHandler({ fail("Unexpected call") }, { fail("Unexpected call") }), + Dispatchers.Unconfined, + ) + + viewModel.supervisedFeaturesFlow("Parent").first() shouldContainExactly listOf( + Parent.Enabled to listOf(Child::class.java), + Parent.Disabled to emptyList(), + ) + } +}) + +private object NoSourceFeatureFactory : FeatureFactory { + override fun create(): Set>> = setOf( + Second::class.java, + First::class.java, + Empty::class.java, + ) +} + +private object SourcedFeatureFactory : FeatureFactory { + override fun create(): Set>> = setOf(Sourced::class.java) +} + +private object AllFeatureFactory : FeatureFactory { + override fun create() = NoSourceFeatureFactory.create() + SourcedFeatureFactory.create() +} + +private object SupervisedFeatureFactory : FeatureFactory { + override fun create(): Set>> = setOf( + Parent::class.java, + Child::class.java, + ) +} + +private enum class First : Feature { + C, + B, + A, + ; + + override val defaultOption get() = C + + override val description = "Description with a [link](https://mehow.io)" +} + +private enum class Second : Feature { + B, + C, + A, + ; + + override val defaultOption get() = B + + override val description = "Description without a link" +} + +private enum class Empty : Feature + +private enum class Sourced : Feature { + A, + B, + C, + ; + + override val defaultOption get() = A + + override val source = Source::class.java + + enum class Source : Feature { + Local, + Remote, + ; + + override val defaultOption get() = Local + } +} + +private enum class Parent : Feature { + Enabled, + Disabled, + ; + + override val defaultOption get() = Disabled +} + +private enum class Child : Feature { + A, + B, + ; + + override val defaultOption get() = A + + override val supervisorOption get() = Parent.Enabled +} + +private fun InspectorViewModel( + laboratory: Laboratory, + factory: FeatureFactory, +) = InspectorViewModel( + laboratory, + emptyFlow(), + factory, + DeprecationHandler({ fail("Unexpected call") }, { fail("Unexpected call") }), +) diff --git a/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelFilterSpec.kt b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelFilterSpec.kt new file mode 100644 index 000000000..28368711b --- /dev/null +++ b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelFilterSpec.kt @@ -0,0 +1,260 @@ +package io.mehow.laboratory.inspector + +import app.cash.turbine.TurbineTestContext +import app.cash.turbine.test +import io.kotest.assertions.fail +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.property.Arb +import io.kotest.property.arbitrary.stringPattern +import io.kotest.property.checkAll +import io.mehow.laboratory.Feature +import io.mehow.laboratory.FeatureFactory +import io.mehow.laboratory.Laboratory +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlin.reflect.KClass + +class InspectorViewModelFilterSpec : FunSpec({ + setMainDispatcher() + + test("emits all feature flags for no search terms") { + val searchFlow = MutableSharedFlow() + InspectorViewModel(searchFlow).observeFeatureClasses().test { + expectAllFeatureFlags() + + cancel() + } + } + + test("emits all feature flags for blank search terms") { + checkAll(Arb.stringPattern("([ ]{0,10})")) { query -> + val searchFlow = MutableSharedFlow() + InspectorViewModel(searchFlow).observeFeatureClasses().test { + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery(query)) + expectNoEvents() + + cancel() + } + } + } + + test("finds feature flags by their exact name") { + val searchFlow = MutableSharedFlow() + InspectorViewModel(searchFlow).observeFeatureClasses().test { + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery("RegularNameFeature")) + awaitItem() shouldContainExactly listOf(RegularNameFeature::class) + + searchFlow.emit(SearchQuery("Numbered1NameFeature")) + awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class) + + searchFlow.emit(SearchQuery("SourcedFeature")) + awaitItem() shouldContainExactly listOf(SourcedFeature::class) + + cancel() + } + } + + test("finds feature flags by split name parts") { + val searchFlow = MutableSharedFlow() + InspectorViewModel(searchFlow).observeFeatureClasses().test { + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery("Name Feature")) + awaitItem() shouldContainExactly listOf( + Numbered1NameFeature::class, + RegularNameFeature::class, + ) + + searchFlow.emit(SearchQuery("Numbered 1Name Feature")) + awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class) + + cancel() + } + } + + // Find out why checkAll(Arb.stringPattern("[!_?@*]{10,}")) { query -> } can fail on CI. + // It fails randomly with a timeout on a second event. Re-using generator seed does not help locally. + // Pattern in generator also doesn't matter as long as it produces valid input for the test. + test("finds no feature flags for no matches") { + val searchFlow = MutableSharedFlow() + InspectorViewModel(searchFlow).observeFeatureClasses().test { + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery("???")) + awaitItem() shouldContainExactly emptyList() + + cancel() + } + } + + test("finds feature flags by their options") { + val searchFlow = MutableSharedFlow() + InspectorViewModel(searchFlow).observeFeatureClasses().test { + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery("Disabled")) + awaitItem() shouldContainExactly listOf( + Numbered1NameFeature::class, + RegularNameFeature::class, + ) + + searchFlow.emit(SearchQuery("Howdy")) + awaitItem() shouldContainExactly listOf(SourcedFeature::class) + + cancel() + } + } + + test("finds feature flags by their sources") { + val searchFlow = MutableSharedFlow() + InspectorViewModel(searchFlow).observeFeatureClasses().test { + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery("Remote")) + awaitItem() shouldContainExactly listOf(SourcedFeature::class) + + cancel() + } + } + + test("finds feature flags by partial matches") { + val searchFlow = MutableSharedFlow() + InspectorViewModel(searchFlow).observeFeatureClasses().test { + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery("me ture")) + awaitItem() shouldContainExactly listOf( + Numbered1NameFeature::class, + RegularNameFeature::class, + ) + + searchFlow.emit(SearchQuery("ature")) + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery("ed ture")) + awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class, SourcedFeature::class) + + searchFlow.emit(SearchQuery("cal")) + awaitItem() shouldContainExactly listOf(SourcedFeature::class) + + cancel() + } + } + + test("finds feature flags by ordered input") { + val searchFlow = MutableSharedFlow() + InspectorViewModel(searchFlow).observeFeatureClasses().test { + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery("Enabled Disabled")) + awaitItem() shouldContainExactly listOf(RegularNameFeature::class) + + searchFlow.emit(SearchQuery("Disabled Enabled")) + awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class) + + cancel() + } + } + + test("ignores capitalization during search") { + val searchFlow = MutableSharedFlow() + InspectorViewModel(searchFlow).observeFeatureClasses().test { + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery("enabled")) + awaitItem() shouldContainExactly listOf( + Numbered1NameFeature::class, + RegularNameFeature::class, + ) + + searchFlow.emit(SearchQuery("feature")) + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery("local")) + awaitItem() shouldContainExactly listOf(SourcedFeature::class) + + cancel() + } + } + + test("finds feature flags by partial, non-split inner search") { + val searchFlow = MutableSharedFlow() + InspectorViewModel(searchFlow).observeFeatureClasses().test { + expectAllFeatureFlags() + + searchFlow.emit(SearchQuery("arnamefea")) + awaitItem() shouldContainExactly listOf(RegularNameFeature::class) + + searchFlow.emit(SearchQuery("d1na")) + awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class) + + cancel() + } + } +}) + +private object SearchFeatureFactory : FeatureFactory { + override fun create(): Set>> = setOf( + RegularNameFeature::class.java, + Numbered1NameFeature::class.java, + SourcedFeature::class.java, + ) +} + +private enum class RegularNameFeature : Feature { + Enabled, + Disabled, + ; + + override val defaultOption get() = Disabled +} + +private enum class Numbered1NameFeature : Feature { + Disabled, + Enabled, + ; + + override val defaultOption get() = Disabled +} + +private enum class SourcedFeature : Feature { + Howdy, + There, + Partner, + ; + + override val defaultOption get() = Howdy + + @Suppress("UNCHECKED_CAST") + override val source = Source::class.java as Class> + + enum class Source : Feature { + Local, + Remote, + ; + + override val defaultOption get() = Local + } +} + +private fun InspectorViewModel( + searchFlow: Flow, +) = InspectorViewModel( + Laboratory.inMemory(), + searchFlow, + SearchFeatureFactory, + DeprecationHandler({ fail("Unexpected call") }, { fail("Unexpected call") }), +) + +private suspend fun TurbineTestContext>>>.expectAllFeatureFlags() { + awaitItem() shouldContainExactly listOf( + Numbered1NameFeature::class, + RegularNameFeature::class, + SourcedFeature::class, + ) +} diff --git a/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelNavigationSpec.kt b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelNavigationSpec.kt new file mode 100644 index 000000000..f84b8b5fb --- /dev/null +++ b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModelNavigationSpec.kt @@ -0,0 +1,125 @@ +package io.mehow.laboratory.inspector + +import app.cash.turbine.test +import io.kotest.assertions.fail +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldBeIn +import io.kotest.matchers.shouldBe +import io.mehow.laboratory.Feature +import io.mehow.laboratory.FeatureFactory +import io.mehow.laboratory.Laboratory +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf + +@Suppress("UNCHECKED_CAST") +class InspectorViewModelNavigationSpec : FunSpec({ + setMainDispatcher() + + test("finds coordinates for registered feature flags") { + val viewModel = InspectorViewModel() + + viewModel.goTo(SectionOneFeatureA::class.java as Class>) shouldBe FeatureCoordinates(0, 0) + viewModel.goTo(SectionOneFeatureB::class.java as Class>) shouldBe FeatureCoordinates(0, 1) + viewModel.goTo(SectionTwoFeature::class.java as Class>) shouldBe FeatureCoordinates(1, 0) + } + + test("does not find coordinates for unregistered feature flags") { + val viewModel = InspectorViewModel() + + viewModel.goTo(UnregisteredFeature::class.java as Class>) shouldBe null + } + + test("does not find coordinates for filtered feature flags") { + val viewModel = InspectorViewModel(searchFlow = flowOf(SearchQuery("Foo"))) + + viewModel.goTo(SectionOneFeatureA::class.java as Class>) shouldBe null + } + + test("finds multiple coordinates for a feature registered twice") { + val viewModel = InspectorViewModel(mapOf("A1" to SectionAFactory, "A2" to SectionAFactory)) + + viewModel.goTo(SectionOneFeatureA::class.java as Class>) shouldBeIn listOf( + FeatureCoordinates(0, 0), + FeatureCoordinates(1, 0), + ) + } + + test("emits coordinates") { + val viewModel = InspectorViewModel() + + viewModel.featureCoordinatesFlow.test { + expectNoEvents() + + viewModel.goTo(SectionOneFeatureA::class.java as Class>) + awaitItem() shouldBe FeatureCoordinates(0, 0) + + cancel() + } + } + + test("does not cache emitted coordinates") { + val viewModel = InspectorViewModel() + + viewModel.goTo(SectionOneFeatureA::class.java as Class>) + + viewModel.featureCoordinatesFlow.test { + cancel() + } + } +}) + +private object SectionAFactory : FeatureFactory { + override fun create(): Set>> = setOf( + SectionOneFeatureA::class.java, + SectionOneFeatureB::class.java, + ) +} + +private object SectionBFactory : FeatureFactory { + override fun create(): Set>> = setOf(SectionTwoFeature::class.java) +} + +private enum class SectionOneFeatureA : Feature { + Option, + ; + + override val defaultOption: SectionOneFeatureA + get() = Option +} + +private enum class SectionOneFeatureB : Feature { + Option, + ; + + override val defaultOption: SectionOneFeatureB + get() = Option +} + +private enum class SectionTwoFeature : Feature { + Option, + ; + + override val defaultOption: SectionTwoFeature + get() = Option +} + +private enum class UnregisteredFeature : Feature { + Option, + ; + + override val defaultOption: UnregisteredFeature + get() = Option +} + +private fun InspectorViewModel( + featureFactories: Map = mapOf("A" to SectionAFactory, "B" to SectionBFactory), + searchFlow: Flow = emptyFlow(), +) = InspectorViewModel( + Laboratory.inMemory(), + searchFlow, + featureFactories, + DeprecationHandler({ fail("Unexpected call") }, { fail("Unexpected call") }), + Dispatchers.Unconfined, +) diff --git a/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorViewModels.kt b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModels.kt similarity index 90% rename from library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorViewModels.kt rename to laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModels.kt index 41d4684ef..988cfae19 100644 --- a/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorViewModels.kt +++ b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/InspectorViewModels.kt @@ -32,18 +32,17 @@ internal fun InspectorViewModel.observeSelectedFeaturesAndEnabledState() = secti internal val InspectorViewModel.Companion.defaultSection get() = "section" -@Suppress("TestFunctionName") internal fun InspectorViewModel( laboratory: Laboratory, searchQueries: Flow, featureFactory: FeatureFactory, deprecationHandler: DeprecationHandler, ) = InspectorViewModel( - laboratory, - searchQueries, - mapOf(InspectorViewModel.defaultSection to featureFactory), - deprecationHandler, - Dispatchers.Unconfined, + laboratory, + searchQueries, + mapOf(InspectorViewModel.defaultSection to featureFactory), + deprecationHandler, + Dispatchers.Unconfined, ) internal fun InspectorViewModel.sectionFlow() = sectionFlow(InspectorViewModel.defaultSection) diff --git a/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/SearchViewModelSpec.kt b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/SearchViewModelSpec.kt new file mode 100644 index 000000000..4e8807ff0 --- /dev/null +++ b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/SearchViewModelSpec.kt @@ -0,0 +1,88 @@ +package io.mehow.laboratory.inspector + +import app.cash.turbine.TurbineTestContext +import app.cash.turbine.test +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mehow.laboratory.inspector.SearchMode.Active +import io.mehow.laboratory.inspector.SearchMode.Idle +import io.mehow.laboratory.inspector.SearchViewModel.Event.CloseSearch +import io.mehow.laboratory.inspector.SearchViewModel.Event.OpenSearch +import io.mehow.laboratory.inspector.SearchViewModel.Event.UpdateQuery +import io.mehow.laboratory.inspector.SearchViewModel.UiModel + +class SearchViewModelSpec : FunSpec({ + setMainDispatcher() + + test("starts with an idle state") { + SearchViewModel().uiModels.test { + expectIdleModel() + + cancel() + } + } + + test("can be opened") { + val viewModel = SearchViewModel() + viewModel.uiModels.test { + expectIdleModel() + + viewModel.sendEvent(OpenSearch) + awaitItem() shouldBe UiModel(Active, SearchQuery.Empty) + + cancel() + } + } + + test("updates search queries in active mode") { + val viewModel = SearchViewModel() + viewModel.uiModels.test { + expectIdleModel() + + viewModel.sendEvent(OpenSearch) + awaitItem() + + viewModel.sendEvent(UpdateQuery("Hello")) + awaitItem() shouldBe UiModel(Active, SearchQuery("Hello")) + + viewModel.sendEvent(UpdateQuery("World")) + awaitItem() shouldBe UiModel(Active, SearchQuery("World")) + + cancel() + } + } + + test("ignores queries in idle mode") { + val viewModel = SearchViewModel() + viewModel.uiModels.test { + expectIdleModel() + + viewModel.sendEvent(UpdateQuery("Hello")) + expectNoEvents() + + cancel() + } + } + + test("clears queries when search is closed") { + val viewModel = SearchViewModel() + viewModel.uiModels.test { + expectIdleModel() + + viewModel.sendEvent(OpenSearch) + awaitItem() + + viewModel.sendEvent(UpdateQuery("Hello")) + awaitItem() shouldBe UiModel(Active, SearchQuery("Hello")) + + viewModel.sendEvent(CloseSearch) + expectIdleModel() + + cancel() + } + } +}) + +private suspend fun TurbineTestContext.expectIdleModel() { + awaitItem() shouldBe UiModel(Idle, SearchQuery.Empty) +} diff --git a/library/inspector/src/test/java/io/mehow/laboratory/inspector/TestConfigurations.kt b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/TestConfigurations.kt similarity index 90% rename from library/inspector/src/test/java/io/mehow/laboratory/inspector/TestConfigurations.kt rename to laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/TestConfigurations.kt index 570ccdd88..f162e6ec5 100644 --- a/library/inspector/src/test/java/io/mehow/laboratory/inspector/TestConfigurations.kt +++ b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/TestConfigurations.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain @OptIn(ExperimentalCoroutinesApi::class) -internal fun TestConfiguration.setMainDispatcher( +fun TestConfiguration.setMainDispatcher( dispatcher: CoroutineDispatcher = Dispatchers.Unconfined, ) { beforeSpec { Dispatchers.setMain(dispatcher) } diff --git a/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/TextTokenSpec.kt b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/TextTokenSpec.kt new file mode 100644 index 000000000..8709eaa7c --- /dev/null +++ b/laboratory/inspector/src/test/kotlin/io/mehow/laboratory/inspector/TextTokenSpec.kt @@ -0,0 +1,77 @@ +package io.mehow.laboratory.inspector + +import io.kotest.core.spec.style.FunSpec +import io.kotest.data.blocking.forAll +import io.kotest.data.row +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContainExactly +import io.mehow.laboratory.inspector.TextToken.Link +import io.mehow.laboratory.inspector.TextToken.Regular + +class TextTokenSpec : FunSpec({ + test("can be empty") { + "".tokenize().shouldBeEmpty() + } + + test("can be blank") { + " ".tokenize() shouldContainExactly listOf(Regular(" ")) + } + + test("can have regular text") { + "Hello".tokenize() shouldContainExactly listOf(Regular("Hello")) + } + + test("can have a link") { + "[Hello](https://mehow.io)".tokenize() shouldContainExactly listOf( + Link("Hello", "https://mehow.io"), + ) + } + + test("can start with regular text followed by a link") { + "Hello [there](https://github.com/MiSikora/)".tokenize() shouldContainExactly listOf( + Regular("Hello "), + Link("there", "https://github.com/MiSikora/"), + ) + } + + test("can start with a link followed by a regular text") { + "[General](https://google.com) Kenobi".tokenize() shouldContainExactly listOf( + Link("General", "https://google.com"), + Regular(" Kenobi"), + ) + } + + test("can have multiple regular texts and links") { + val input = "Hello [there](https://github.com)… [General](https://sample.org) Kenobi" + input.tokenize() shouldContainExactly listOf( + Regular("Hello "), + Link("there", "https://github.com"), + Regular("… "), + Link("General", "https://sample.org"), + Regular(" Kenobi"), + ) + } + + test("can have multiple consecutive links") { + val input = "[One,](https://one.com)[ Two](https://two.com)[, Three](https://three.com)" + input.tokenize() shouldContainExactly listOf( + Link("One,", "https://one.com"), + Link(" Two", "https://two.com"), + Link(", Three", "https://three.com"), + ) + } + + test("ignores malformed link syntax") { + forAll( + row("[One[](https://one.com)"), + row("[One](https://one.com()"), + row("[](https://one.com"), + row("[One]()"), + row("[One]((https://one.com)"), + row("[O]ne](https://one.com)"), + row("[One](h(ttps://one.com)"), + ) { + it.tokenize() shouldContainExactly listOf(Regular(it)) + } + } +}) diff --git a/library/laboratory/api/laboratory.api b/laboratory/runtime/api/runtime.api similarity index 100% rename from library/laboratory/api/laboratory.api rename to laboratory/runtime/api/runtime.api diff --git a/laboratory/runtime/build.gradle.kts b/laboratory/runtime/build.gradle.kts new file mode 100644 index 000000000..98f98e065 --- /dev/null +++ b/laboratory/runtime/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.mavenPublish) + alias(libs.plugins.spotless) + alias(libs.plugins.detekt) + alias(libs.plugins.dokka) +} + +tasks.withType().configureEach { + useJUnitPlatform() +} + +dependencies { + api(libs.kotlinx.coroutines.core) + + testImplementation(libs.kotest.runner.junit5) + testImplementation(libs.kotest.assertions) + testImplementation(libs.turbine) +} diff --git a/library/laboratory/gradle.properties b/laboratory/runtime/gradle.properties similarity index 100% rename from library/laboratory/gradle.properties rename to laboratory/runtime/gradle.properties diff --git a/library/laboratory/src/main/java/io/mehow/laboratory/BlockingIoCall.kt b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/BlockingIoCall.kt similarity index 73% rename from library/laboratory/src/main/java/io/mehow/laboratory/BlockingIoCall.kt rename to laboratory/runtime/src/main/kotlin/io/mehow/laboratory/BlockingIoCall.kt index 8c5961a9e..25f23d4a9 100644 --- a/library/laboratory/src/main/java/io/mehow/laboratory/BlockingIoCall.kt +++ b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/BlockingIoCall.kt @@ -8,9 +8,9 @@ import kotlin.annotation.AnnotationTarget.FUNCTION * Opt-in annotation denoting that the used function can potentially block a calling thread with I/O operations. */ @RequiresOptIn( - message = "" + - "Used API can block a thread with IO operations. " + - "Either opt in to its usage or use a suspending equivalent." + message = "" + + "Used API can block a thread with IO operations. " + + "Either opt in to its usage or use a suspending equivalent.", ) @Retention(BINARY) @Target(CLASS, FUNCTION) diff --git a/library/laboratory/src/main/java/io/mehow/laboratory/BlockingLaboratory.kt b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/BlockingLaboratory.kt similarity index 100% rename from library/laboratory/src/main/java/io/mehow/laboratory/BlockingLaboratory.kt rename to laboratory/runtime/src/main/kotlin/io/mehow/laboratory/BlockingLaboratory.kt diff --git a/library/laboratory/src/main/java/io/mehow/laboratory/DefaultOptionFactory.kt b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/DefaultOptionFactory.kt similarity index 100% rename from library/laboratory/src/main/java/io/mehow/laboratory/DefaultOptionFactory.kt rename to laboratory/runtime/src/main/kotlin/io/mehow/laboratory/DefaultOptionFactory.kt diff --git a/library/laboratory/src/main/java/io/mehow/laboratory/Feature.kt b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/Feature.kt similarity index 100% rename from library/laboratory/src/main/java/io/mehow/laboratory/Feature.kt rename to laboratory/runtime/src/main/kotlin/io/mehow/laboratory/Feature.kt diff --git a/library/laboratory/src/main/java/io/mehow/laboratory/FeatureFactory.kt b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/FeatureFactory.kt similarity index 100% rename from library/laboratory/src/main/java/io/mehow/laboratory/FeatureFactory.kt rename to laboratory/runtime/src/main/kotlin/io/mehow/laboratory/FeatureFactory.kt diff --git a/library/laboratory/src/main/java/io/mehow/laboratory/FeatureStorage.kt b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/FeatureStorage.kt similarity index 97% rename from library/laboratory/src/main/java/io/mehow/laboratory/FeatureStorage.kt rename to laboratory/runtime/src/main/kotlin/io/mehow/laboratory/FeatureStorage.kt index 60073ae8e..ba4556d5c 100644 --- a/library/laboratory/src/main/java/io/mehow/laboratory/FeatureStorage.kt +++ b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/FeatureStorage.kt @@ -44,6 +44,7 @@ public interface FeatureStorage { * * @return `true` if the value was set successfully, `false` otherwise. */ + @Suppress("SpreadOperator") // Implementations override this to be more efficient public suspend fun setOptions(options: Collection>): Boolean = setOptions(*options.toTypedArray()) public companion object { diff --git a/library/laboratory/src/main/java/io/mehow/laboratory/InMemoryFeatureStorage.kt b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/InMemoryFeatureStorage.kt similarity index 95% rename from library/laboratory/src/main/java/io/mehow/laboratory/InMemoryFeatureStorage.kt rename to laboratory/runtime/src/main/kotlin/io/mehow/laboratory/InMemoryFeatureStorage.kt index 7f2328186..1390fe76b 100644 --- a/library/laboratory/src/main/java/io/mehow/laboratory/InMemoryFeatureStorage.kt +++ b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/InMemoryFeatureStorage.kt @@ -10,8 +10,8 @@ internal class InMemoryFeatureStorage : FeatureStorage { private val featureFlow = MutableStateFlow(emptyMap>, String>()) override fun observeFeatureName(feature: Class>) = featureFlow - .map { it[feature] } - .distinctUntilChanged() + .map { it[feature] } + .distinctUntilChanged() override suspend fun getFeatureName(feature: Class>) = featureFlow.map { it[feature] }.first() diff --git a/library/laboratory/src/main/java/io/mehow/laboratory/Laboratory.kt b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/Laboratory.kt similarity index 96% rename from library/laboratory/src/main/java/io/mehow/laboratory/Laboratory.kt rename to laboratory/runtime/src/main/kotlin/io/mehow/laboratory/Laboratory.kt index 5f1182cb0..890f6072c 100644 --- a/library/laboratory/src/main/java/io/mehow/laboratory/Laboratory.kt +++ b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/Laboratory.kt @@ -77,7 +77,14 @@ public class Laboratory internal constructor( val activeOption = options.firstOrNull { it.name == expectedName } ?: defaultOption val parent = feature.supervisorOption ?: return activeOption - return if (activeOption.supervisorOption != experimentRaw(parent.javaClass)) defaultOption else activeOption + return if (activeOption.supervisorOption != experimentRaw( + parent.javaClass, + ) + ) { + defaultOption + } else { + activeOption + } } /** @@ -85,7 +92,7 @@ public class Laboratory internal constructor( */ @Suppress("UNCHECKED_CAST") public suspend fun > experimentIs(option: T): Boolean = experimentRaw( - option::class.java as Class> + option::class.java as Class>, ) == option /** diff --git a/library/laboratory/src/main/java/io/mehow/laboratory/OptionFactory.kt b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/OptionFactory.kt similarity index 69% rename from library/laboratory/src/main/java/io/mehow/laboratory/OptionFactory.kt rename to laboratory/runtime/src/main/kotlin/io/mehow/laboratory/OptionFactory.kt index d21210017..0c4d58ddc 100644 --- a/library/laboratory/src/main/java/io/mehow/laboratory/OptionFactory.kt +++ b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/OptionFactory.kt @@ -7,15 +7,20 @@ public interface OptionFactory { /** * Returns a feature matching class name and option name or null if no match is found. */ - public fun create(key: String, name: String): Feature<*>? + public fun create( + key: String, + name: String, + ): Feature<*>? /** * Creates a new [OptionFactory] that will first look for an option in this factory and then in the * other factory. */ public operator fun plus(factory: OptionFactory): OptionFactory = object : OptionFactory { - override fun create(key: String, name: String) = - this@OptionFactory.create(key, name) ?: factory.create(key, name) + override fun create( + key: String, + name: String, + ) = this@OptionFactory.create(key, name) ?: factory.create(key, name) } public companion object diff --git a/library/laboratory/src/main/java/io/mehow/laboratory/SafeDefaultOptionFactory.kt b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/SafeDefaultOptionFactory.kt similarity index 100% rename from library/laboratory/src/main/java/io/mehow/laboratory/SafeDefaultOptionFactory.kt rename to laboratory/runtime/src/main/kotlin/io/mehow/laboratory/SafeDefaultOptionFactory.kt diff --git a/library/laboratory/src/main/java/io/mehow/laboratory/SourcedFeatureStorage.kt b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/SourcedFeatureStorage.kt similarity index 68% rename from library/laboratory/src/main/java/io/mehow/laboratory/SourcedFeatureStorage.kt rename to laboratory/runtime/src/main/kotlin/io/mehow/laboratory/SourcedFeatureStorage.kt index a3a83e59b..ede789634 100644 --- a/library/laboratory/src/main/java/io/mehow/laboratory/SourcedFeatureStorage.kt +++ b/laboratory/runtime/src/main/kotlin/io/mehow/laboratory/SourcedFeatureStorage.kt @@ -12,17 +12,17 @@ internal class SourcedFeatureStorage( defaultOptionFactory: DefaultOptionFactory? = null, ) : FeatureStorage { private val localLaboratory = Laboratory.builder() - .featureStorage(localSource) - .let { builder -> defaultOptionFactory?.let(builder::defaultOptionFactory) ?: builder } - .build() + .featureStorage(localSource) + .let { builder -> defaultOptionFactory?.let(builder::defaultOptionFactory) ?: builder } + .build() override fun observeFeatureName(feature: Class>) = feature.observeSource() - .map { source -> remoteSources[source.name] ?: localSource } - .onEmpty { emit(localSource) } - .let { - @OptIn(ExperimentalCoroutinesApi::class) - it.flatMapLatest { storage -> storage.observeFeatureName(feature) } - } + .map { source -> remoteSources[source.name] ?: localSource } + .onEmpty { emit(localSource) } + .let { + @OptIn(ExperimentalCoroutinesApi::class) + it.flatMapLatest { storage -> storage.observeFeatureName(feature) } + } override suspend fun getFeatureName(feature: Class>): String? { val storage = feature.getSource()?.let { remoteSources[it.name] } ?: localSource @@ -36,20 +36,20 @@ internal class SourcedFeatureStorage( override suspend fun clear() = localSource.clear() private fun > Class.observeSource() = validatedSource() - ?.let { localLaboratory.observe(it) } - ?: emptyFlow() + ?.let { localLaboratory.observe(it) } + ?: emptyFlow() private suspend fun > Class.getSource() = validatedSource() - ?.let { localLaboratory.experiment(it) } + ?.let { localLaboratory.experiment(it) } private fun > Class.validatedSource() = options - .firstOrNull() - ?.source - ?.takeUnless { it.options.isEmpty() } + .firstOrNull() + ?.source + ?.takeUnless { it.options.isEmpty() } fun withDefaultOptionFactory(factory: DefaultOptionFactory) = SourcedFeatureStorage( - localSource, - remoteSources, - factory, + localSource, + remoteSources, + factory, ) } diff --git a/library/laboratory/src/main/resources/META-INF/proguard/io-mehow-laboratory.pro b/laboratory/runtime/src/main/resources/META-INF/proguard/io-mehow-laboratory.pro similarity index 100% rename from library/laboratory/src/main/resources/META-INF/proguard/io-mehow-laboratory.pro rename to laboratory/runtime/src/main/resources/META-INF/proguard/io-mehow-laboratory.pro diff --git a/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/DefaultOptionFactorySpec.kt b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/DefaultOptionFactorySpec.kt new file mode 100644 index 000000000..035cfb3e7 --- /dev/null +++ b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/DefaultOptionFactorySpec.kt @@ -0,0 +1,63 @@ +package io.mehow.laboratory + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +class DefaultOptionFactorySpec : FunSpec() { + enum class FeatureA : Feature { + A, + B, + C, + ; + + override val defaultOption get() = A + } + + enum class FeatureB : Feature { + A, + B, + ; + + override val defaultOption get() = A + } + + enum class FeatureC : Feature { + A, + ; + + override val defaultOption get() = A + } + + init { + val firstFactory = object : DefaultOptionFactory { + override fun > create(feature: T): Feature<*>? = when (feature) { + is FeatureA -> FeatureA.C + else -> null + } + } + + val secondFactory = object : DefaultOptionFactory { + override fun > create(feature: T): Feature<*>? = when (feature) { + is FeatureA -> FeatureA.B + is FeatureB -> FeatureB.B + else -> null + } + } + + context("factory created from sum") { + val factory = firstFactory + secondFactory + + test("prioritizes first factory when option is available in it") { + factory.create(FeatureA.A) shouldBe FeatureA.C + } + + test("falls back to second factory when option is not available in first factory") { + factory.create(FeatureB.A) shouldBe FeatureB.B + } + + test("does not handle options unknown to any of sub-factories") { + factory.create(FeatureC.A) shouldBe null + } + } + } +} diff --git a/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/FeatureFactorySpec.kt b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/FeatureFactorySpec.kt new file mode 100644 index 000000000..01c19402e --- /dev/null +++ b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/FeatureFactorySpec.kt @@ -0,0 +1,41 @@ +package io.mehow.laboratory + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder + +class FeatureFactorySpec : FunSpec() { + enum class FeatureA : Feature { + A, + ; + + override val defaultOption get() = A + } + + enum class FeatureB : Feature { + A, + ; + + override val defaultOption get() = A + } + + init { + val firstFactory = object : FeatureFactory { + override fun create(): Set>> = setOf(FeatureA::class.java) + } + + val secondFactory = object : FeatureFactory { + override fun create(): Set>> = setOf(FeatureB::class.java) + } + + context("factory created from sum") { + val factory = firstFactory + secondFactory + + test("return features available in all sub-factories") { + factory.create() shouldContainExactlyInAnyOrder setOf( + FeatureA::class.java, + FeatureB::class.java, + ) + } + } + } +} diff --git a/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/LaboratorySpec.kt b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/LaboratorySpec.kt new file mode 100644 index 000000000..c63258d52 --- /dev/null +++ b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/LaboratorySpec.kt @@ -0,0 +1,188 @@ +package io.mehow.laboratory + +import app.cash.turbine.test +import io.kotest.assertions.fail +import io.kotest.assertions.throwables.shouldThrowExactly +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.throwable.shouldHaveMessage +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +class LaboratorySpec : FunSpec() { + enum class NoValuesFeature : Feature + + enum class FeatureA : Feature { + A, + B, + C, + ; + + override val defaultOption get() = A + } + + enum class FeatureB : Feature { + A, + B, + ; + + override val defaultOption get() = A + } + + init { + test("reads feature option saved in storage") { + val storage = FeatureStorage.inMemory() + val laboratory = Laboratory.create(storage) + + for (option in FeatureA::class.java.options) { + storage.setOption(option) + + laboratory.experiment() shouldBe option + laboratory.experimentIs(option) shouldBe true + } + } + + test("changes feature option") { + val laboratory = Laboratory.inMemory() + + for (option in FeatureA::class.java.options) { + laboratory.setOption(option) + + laboratory.experiment() shouldBe option + } + } + + test("changes options for multiple features") { + val laboratory = Laboratory.inMemory() + + laboratory.setOptions(FeatureA.C, FeatureB.B) + + laboratory.experiment() shouldBe FeatureA.C + laboratory.experiment() shouldBe FeatureB.B + } + + test("emits feature changes") { + val laboratory = Laboratory.inMemory() + + laboratory.observe().test { + awaitItem() shouldBe FeatureA.A + + laboratory.setOption(FeatureA.B) + awaitItem() shouldBe FeatureA.B + + laboratory.setOption(FeatureA.C) + awaitItem() shouldBe FeatureA.C + + laboratory.setOption(FeatureA.C) + expectNoEvents() + + laboratory.setOption(FeatureA.B) + awaitItem() shouldBe FeatureA.B + } + } + + test("clears all features") { + val laboratory = Laboratory.inMemory() + + laboratory.setOptions(FeatureA.B, FeatureB.B) + laboratory.clear() + + laboratory.experiment() shouldBe FeatureA.A + laboratory.experiment() shouldBe FeatureB.A + } + + test("does not share instances between in memory implementations") { + val firstLaboratory = Laboratory.inMemory() + val secondLaboratory = Laboratory.inMemory() + + firstLaboratory.setOption(FeatureA.B) + firstLaboratory.experiment() shouldBe FeatureA.B + secondLaboratory.experiment() shouldBe FeatureA.A + + secondLaboratory.setOption(FeatureA.C) + firstLaboratory.experiment() shouldBe FeatureA.B + secondLaboratory.experiment() shouldBe FeatureA.C + } + + test("uses default option if no match is found") { + val nullStorage = object : FeatureStorage { + override fun observeFeatureName(feature: Class>): Flow = + flowOf(null) + + override suspend fun getFeatureName(feature: Class>): String? = null + + override suspend fun setOptions(vararg options: Feature<*>) = fail("Unexpected call") + + override suspend fun clear() = fail("Unexpected call") + } + val laboratory = Laboratory.create(nullStorage) + + laboratory.experiment() shouldBe FeatureA.A + } + + context("default options factory") { + val factory = object : DefaultOptionFactory { + override fun > create(feature: T) = when (feature) { + is FeatureA -> FeatureA.C + is FeatureB -> FeatureA.C // Intentional wrong class + else -> null + } + } + + val laboratory = Laboratory.builder() + .featureStorage(FeatureStorage.inMemory()) + .defaultOptionFactory(factory) + .build() + + beforeTest { + laboratory.clear() + } + + test("overrides default options") { + laboratory.experiment() shouldBe FeatureA.C + } + + test("does not override changed options") { + for (option in FeatureA::class.java.options) { + laboratory.setOption(option) + + laboratory.experiment() shouldBe option + } + } + + test("overrides emitted default options") { + laboratory.observe().test { + awaitItem() shouldBe FeatureA.C + + laboratory.setOption(FeatureA.B) + awaitItem() shouldBe FeatureA.B + } + } + + @Suppress("MaxLineLength") + test("fails when provided default option uses wrong type") { + shouldThrowExactly { + laboratory.experiment() + } shouldHaveMessage "Tried to use FeatureA.C as a default option for io.mehow.laboratory.LaboratorySpec.FeatureB" + } + } + + test("fails to use feature with no values") { + val throwingStorage = object : FeatureStorage { + override fun observeFeatureName(feature: Class>) = fail("Unexpected call") + + override suspend fun getFeatureName(feature: Class>) = + fail("Unexpected call") + + override suspend fun setOptions(vararg options: Feature<*>) = fail("Unexpected call") + + override suspend fun clear() = fail("Unexpected call") + } + val laboratory = Laboratory.create(throwingStorage) + + shouldThrowExactly { + laboratory.experiment() + } shouldHaveMessage "io.mehow.laboratory.LaboratorySpec.NoValuesFeature must have at least one option" + } + } +} diff --git a/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/OptionFactorySpec.kt b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/OptionFactorySpec.kt new file mode 100644 index 000000000..8965e09ba --- /dev/null +++ b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/OptionFactorySpec.kt @@ -0,0 +1,61 @@ +package io.mehow.laboratory + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +class OptionFactorySpec : FunSpec() { + enum class FeatureA : Feature { + A, + B, + ; + + override val defaultOption get() = A + } + + enum class FeatureB : Feature { + A, + B, + ; + + override val defaultOption get() = A + } + + init { + val firstFactory = object : OptionFactory { + override fun create( + key: String, + name: String, + ): Feature<*>? = when (key) { + "FeatureA" -> FeatureA.A + else -> null + } + } + + val secondFactory = object : OptionFactory { + override fun create( + key: String, + name: String, + ): Feature<*>? = when (key) { + "FeatureA" -> FeatureA.B + "FeatureB" -> FeatureB.B + else -> null + } + } + + context("factory created from sum") { + val factory = firstFactory + secondFactory + + test("prioritizes first factory when feature is available in it") { + factory.create("FeatureA", "") shouldBe FeatureA.A + } + + test("falls back to second factory when feature is not available in first factory") { + factory.create("FeatureB", "") shouldBe FeatureB.B + } + + test("does not handle features unknown to any of sub-factories") { + factory.create("Unknown", "") shouldBe null + } + } + } +} diff --git a/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/ParentChildFeatureSpec.kt b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/ParentChildFeatureSpec.kt new file mode 100644 index 000000000..db4a12fb7 --- /dev/null +++ b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/ParentChildFeatureSpec.kt @@ -0,0 +1,119 @@ +package io.mehow.laboratory + +import app.cash.turbine.test +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +class ParentChildFeatureSpec : FunSpec() { + enum class GrandParentFeature : Feature { + A, + B, + ; + + override val defaultOption get() = A + } + + enum class ParentFeature : Feature { + A, + B, + ; + + override val defaultOption get() = A + + override val supervisorOption = GrandParentFeature.A + } + + enum class ChildFeature : Feature { + A, + B, + ; + + override val defaultOption get() = A + + override val supervisorOption = ParentFeature.A + } + + enum class ChildFeature2 : Feature { + A, + B, + ; + + override val defaultOption get() = A + + override val supervisorOption = ParentFeature.B + } + + init { + context("parent feature") { + test("supervises reading child's option") { + val laboratory = Laboratory.inMemory() + + laboratory.setOption(ChildFeature.B) + laboratory.experiment() shouldBe ChildFeature.B + + laboratory.setOption(ParentFeature.B) + laboratory.experiment() shouldBe ChildFeature.A + } + + test("does not changed stored child's option") { + val laboratory = Laboratory.inMemory() + + laboratory.setOption(ChildFeature.B) + laboratory.setOption(ParentFeature.B) + laboratory.setOption(ParentFeature.A) + + laboratory.experiment() shouldBe ChildFeature.B + } + + test("does not prevent writing child's option") { + val laboratory = Laboratory.inMemory() + + laboratory.setOption(ParentFeature.B) + laboratory.setOption(ChildFeature.B) + laboratory.setOption(ParentFeature.A) + + laboratory.experiment() shouldBe ChildFeature.B + } + + test("triggers emitting child's option when they differ") { + val laboratory = Laboratory.inMemory() + + laboratory.setOption(ChildFeature.B) + + laboratory.observe().test { + awaitItem() shouldBe ChildFeature.B + + laboratory.setOption(ParentFeature.B) + awaitItem() shouldBe ChildFeature.A + + laboratory.setOption(ParentFeature.A) + awaitItem() shouldBe ChildFeature.B + } + } + + test("does not trigger emitting child's option when they do not differ") { + val laboratory = Laboratory.inMemory() + + laboratory.observe().test { + awaitItem() shouldBe ChildFeature.A + + laboratory.setOption(ParentFeature.B) + expectNoEvents() + + laboratory.setOption(ParentFeature.A) + expectNoEvents() + } + } + } + + test("supervises reading grandchild's option") { + val laboratory = Laboratory.inMemory() + + laboratory.setOption(ChildFeature2.B) + laboratory.setOption(ParentFeature.B) + laboratory.setOption(GrandParentFeature.B) + + laboratory.experiment() shouldBe ChildFeature2.A + } + } +} diff --git a/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/SourcedFeatureStorageSpec.kt b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/SourcedFeatureStorageSpec.kt new file mode 100644 index 000000000..b085225a1 --- /dev/null +++ b/laboratory/runtime/src/test/kotlin/io/mehow/laboratory/SourcedFeatureStorageSpec.kt @@ -0,0 +1,285 @@ +package io.mehow.laboratory + +import app.cash.turbine.test +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +class SourcedFeatureStorageSpec : FunSpec() { + enum class FeatureA : Feature { + A, + B, + C, + ; + + override val defaultOption get() = A + + override val source = Source::class.java + + enum class Source : Feature { + Local, + RemoteA, + ; + + override val defaultOption get() = Local + } + } + + enum class FeatureB : Feature { + A, + B, + C, + ; + + override val defaultOption get() = A + + override val source = Source::class.java + + enum class Source : Feature { + Local, + RemoteA, + RemoteB, + ; + + override val defaultOption get() = RemoteB + } + } + + enum class EmptySourceFeature : Feature { + A, + B, + C, + ; + + override val defaultOption get() = A + + override val source = Source::class.java + + enum class Source : Feature + } + + enum class UnsourcedFeature : Feature { + A, + B, + C, + ; + + override val defaultOption get() = A + } + + init { + val storageLocal = FeatureStorage.inMemory() + val storageRemoteA = FeatureStorage.inMemory() + val storageRemoteB = FeatureStorage.inMemory() + val storageSourced = SourcedFeatureStorage( + storageLocal, + mapOf("RemoteA" to storageRemoteA, "RemoteB" to storageRemoteB), + ) + + val laboratoryLocal = Laboratory.create(storageLocal) + val laboratoryRemoteA = Laboratory.create(storageRemoteA) + val laboratoryRemoteB = Laboratory.create(storageRemoteB) + val laboratorySourced = Laboratory.create(storageSourced) + + beforeTest { + laboratoryLocal.clear() + laboratoryRemoteA.clear() + laboratoryRemoteB.clear() + } + + test("uses options from default sources") { + laboratoryLocal.setOption(FeatureA.B) + laboratorySourced.experiment() shouldBe FeatureA.B + + laboratoryRemoteB.setOption(FeatureB.C) + laboratorySourced.experiment() shouldBe FeatureB.C + } + + test("emits options from default sources") { + laboratorySourced.observe().test { + awaitItem() shouldBe FeatureB.A + + laboratoryRemoteB.setOption(FeatureB.B) + awaitItem() shouldBe FeatureB.B + + laboratoryRemoteB.setOption(FeatureB.C) + awaitItem() shouldBe FeatureB.C + } + } + + test("uses options from changed sources") { + laboratorySourced.setOption(FeatureA.Source.RemoteA) + + laboratoryRemoteA.setOption(FeatureA.C) + laboratorySourced.experiment() shouldBe FeatureA.C + + laboratoryLocal.setOption(FeatureA.B) + laboratorySourced.experiment() shouldBe FeatureA.C + } + + test("emits options from changed sources") { + laboratoryLocal.setOption(FeatureB.A) + laboratoryRemoteA.setOption(FeatureB.B) + laboratoryRemoteB.setOption(FeatureB.C) + + laboratorySourced.observe().test { + awaitItem() shouldBe FeatureB.C + + laboratorySourced.setOption(FeatureB.Source.Local) + awaitItem() shouldBe FeatureB.A + + laboratoryLocal.setOption(FeatureB.C) + awaitItem() shouldBe FeatureB.C + + laboratorySourced.setOption(FeatureB.Source.RemoteA) + awaitItem() shouldBe FeatureB.B + + laboratoryRemoteA.setOption(FeatureB.A) + awaitItem() shouldBe FeatureB.A + + laboratorySourced.setOption(FeatureB.Source.RemoteB) + awaitItem() shouldBe FeatureB.C + + laboratoryRemoteB.setOption(FeatureB.B) + awaitItem() shouldBe FeatureB.B + } + } + + test("does not emit changes from inactive sources") { + laboratorySourced.observe().test { + awaitItem() shouldBe FeatureB.A + + laboratoryLocal.setOption(FeatureB.B) + expectNoEvents() + + laboratoryRemoteA.setOption(FeatureB.B) + expectNoEvents() + } + } + + test("does not mix source changes between different features") { + laboratoryLocal.setOption(FeatureB.B) + laboratoryRemoteA.setOption(FeatureB.C) + + laboratorySourced.observe().test { + awaitItem() shouldBe FeatureB.A + + laboratorySourced.setOption(FeatureA.Source.Local) + expectNoEvents() + + laboratorySourced.setOption(FeatureA.Source.RemoteA) + expectNoEvents() + } + } + + test("clears only local storage") { + laboratoryLocal.setOption(FeatureA.B) + laboratoryRemoteA.setOption(FeatureA.B) + laboratorySourced.setOption(FeatureA.Source.RemoteA) + + laboratorySourced.clear() + + laboratoryLocal.experimentIs(FeatureA.A) + laboratoryRemoteA.experimentIs(FeatureA.B) + laboratorySourced.experimentIs(FeatureA.A) + } + + test("allows to override default sources") { + val defaultOptionFactory = object : DefaultOptionFactory { + override fun > create(feature: T) = when (feature) { + is FeatureA.Source -> FeatureA.Source.RemoteA + else -> null + } + } + val laboratory = Laboratory.Builder() + .featureStorage(storageSourced) + .defaultOptionFactory(defaultOptionFactory) + .build() + + laboratoryRemoteA.setOption(FeatureA.C) + + laboratory.experiment() shouldBe FeatureA.C + } + + test("makes changes only in local storage") { + laboratoryLocal.setOption(FeatureA.A) + laboratoryRemoteA.setOption(FeatureA.A) + laboratoryRemoteB.setOption(FeatureA.A) + + laboratorySourced.setOption(FeatureA.B) + + laboratoryLocal.experiment() shouldBe FeatureA.B + laboratoryRemoteA.experiment() shouldBe FeatureA.A + laboratoryRemoteB.experiment() shouldBe FeatureA.A + } + + context("feature with empty sources") { + test("reads from a local storage") { + laboratorySourced.experiment() shouldBe EmptySourceFeature.A + + laboratoryLocal.setOption(EmptySourceFeature.C) + laboratorySourced.experiment() shouldBe EmptySourceFeature.C + } + + test("emits changes from a local storage") { + laboratorySourced.observe().test { + awaitItem() shouldBe EmptySourceFeature.A + + laboratoryLocal.setOption(EmptySourceFeature.B) + awaitItem() shouldBe EmptySourceFeature.B + + cancel() + } + } + } + + context("feature with no sources") { + test("reads from a local storage") { + laboratorySourced.experiment() shouldBe UnsourcedFeature.A + + laboratoryLocal.setOption(UnsourcedFeature.C) + laboratorySourced.experiment() shouldBe UnsourcedFeature.C + } + + test("emits changes from a local storage") { + laboratorySourced.observe().test { + awaitItem() shouldBe UnsourcedFeature.A + + laboratoryLocal.setOption(UnsourcedFeature.B) + awaitItem() shouldBe UnsourcedFeature.B + + cancel() + } + } + } + + @Suppress("NAME_SHADOWING") + context("feature with unknown source") { + val localStorage = FeatureStorage.inMemory() + val sourcedStorage = SourcedFeatureStorage(localStorage, emptyMap()) + val laboratoryLocal = Laboratory.create(localStorage) + val laboratorySourced = Laboratory.create(sourcedStorage) + + beforeTest { + laboratoryLocal.clear() + } + + test("reads from a local storage") { + laboratorySourced.experiment() shouldBe FeatureA.A + + laboratoryLocal.setOption(FeatureA.C) + laboratorySourced.experiment() shouldBe FeatureA.C + } + + test("emits changes from a local storage") { + laboratorySourced.observe().test { + awaitItem() shouldBe FeatureA.A + + laboratoryLocal.setOption(FeatureA.B) + awaitItem() shouldBe FeatureA.B + + cancel() + } + } + } + } +} diff --git a/library/shared-preferences/api/shared-preferences.api b/laboratory/shared-preferences/api/shared-preferences.api similarity index 100% rename from library/shared-preferences/api/shared-preferences.api rename to laboratory/shared-preferences/api/shared-preferences.api diff --git a/laboratory/shared-preferences/build.gradle.kts b/laboratory/shared-preferences/build.gradle.kts new file mode 100644 index 000000000..5979b3eb4 --- /dev/null +++ b/laboratory/shared-preferences/build.gradle.kts @@ -0,0 +1,39 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.mavenPublish) + alias(libs.plugins.spotless) + alias(libs.plugins.detekt) + alias(libs.plugins.dokka) +} + +android { + namespace = "io.mehow.laboratory.sharedpreferences" + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments += "clearPackageData" to "true" + } + + testOptions.execution = "ANDROIDX_TEST_ORCHESTRATOR" + testBuildType = "release" + + buildTypes { + release { + // Testing release builds requires signing them + signingConfig = signingConfigs.getByName("debug") + } + } +} + +dependencies { + api(projects.laboratory.runtime) + implementation(libs.kotlinx.coroutines.core) + + androidTestImplementation(libs.kotest.assertions) + androidTestImplementation(libs.turbine) + androidTestUtil(libs.androidx.test.orchestrator) + androidTestImplementation(libs.androidx.test.coreKtx) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.testExt.junitKtx) +} diff --git a/laboratory/shared-preferences/gradle.properties b/laboratory/shared-preferences/gradle.properties new file mode 100644 index 000000000..461e262f8 --- /dev/null +++ b/laboratory/shared-preferences/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=laboratory-data-store +POM_NAME=Laboratory (Shared Preferences) +POM_PACKAGING=aar diff --git a/library/shared-preferences/src/androidTest/java/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeaturesStorageTest.kt b/laboratory/shared-preferences/src/androidTest/kotlin/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeaturesStorageTest.kt similarity index 61% rename from library/shared-preferences/src/androidTest/java/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeaturesStorageTest.kt rename to laboratory/shared-preferences/src/androidTest/kotlin/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeaturesStorageTest.kt index 4a0f16341..fb960c12b 100644 --- a/library/shared-preferences/src/androidTest/java/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeaturesStorageTest.kt +++ b/laboratory/shared-preferences/src/androidTest/kotlin/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeaturesStorageTest.kt @@ -12,14 +12,22 @@ import io.mehow.laboratory.Laboratory import kotlinx.coroutines.runBlocking import org.junit.Test -internal class SharedPreferencesFeaturesStorageTest { +class SharedPreferencesFeaturesStorageTest { + enum class FeatureA : Feature { + A, + B, + ; + + override val defaultOption get() = A + } + private val preferences = ApplicationProvider - .getApplicationContext() - .getSharedPreferences("laboratory", MODE_PRIVATE) + .getApplicationContext() + .getSharedPreferences("laboratory", MODE_PRIVATE) private val storage = FeatureStorage.sharedPreferences(preferences) private val laboratory = Laboratory.create(storage) - @Test fun storedOptionIsAvailableAsExperiment() { + @Test fun readsStoredOption() { runBlocking { storage.setOption(FeatureA.B) @@ -27,7 +35,7 @@ internal class SharedPreferencesFeaturesStorageTest { } } - @Test fun corruptedFeatureFlagOptionYieldsDefaultExperiment() { + @Test fun usesDefaultOptionForCorruptedData() { runBlocking { storage.setOption(FeatureA.B) preferences.edit().putInt(FeatureA::class.java.name, 1).commit() @@ -36,7 +44,7 @@ internal class SharedPreferencesFeaturesStorageTest { } } - @Test fun observesFeatureFlagChanges() = runBlocking { + @Test fun emitsFeatureOptionChanges() = runBlocking { storage.observeFeatureName(FeatureA::class.java).test { awaitItem() shouldBe null @@ -51,30 +59,26 @@ internal class SharedPreferencesFeaturesStorageTest { } } - @Test fun clearsFeatureFlagOptions() = runBlocking { - storage.setOption(FeatureA.B) - storage.clear() + @Test fun clearsStorage() { + runBlocking { + storage.setOption(FeatureA.B) + storage.clear() - laboratory.experimentIs(FeatureA.A).shouldBeTrue() + laboratory.experimentIs(FeatureA.A).shouldBeTrue() + } } - @Test fun informsObserversAfterClearingFeatureFlags() = runBlocking { - storage.observeFeatureName(FeatureA::class.java).test { - awaitItem() shouldBe null + @Test fun emitsClearedState() { + runBlocking { + storage.observeFeatureName(FeatureA::class.java).test { + awaitItem() shouldBe null - storage.setOption(FeatureA.B) - awaitItem() shouldBe FeatureA.B.name + storage.setOption(FeatureA.B) + awaitItem() shouldBe FeatureA.B.name - storage.clear() - awaitItem() shouldBe null + storage.clear() + awaitItem() shouldBe null + } } } } - -private enum class FeatureA : Feature { - A, - B, - ; - - override val defaultOption get() = A -} diff --git a/library/shared-preferences/src/main/java/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeatureStorage.kt b/laboratory/shared-preferences/src/main/kotlin/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeatureStorage.kt similarity index 95% rename from library/shared-preferences/src/main/java/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeatureStorage.kt rename to laboratory/shared-preferences/src/main/kotlin/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeatureStorage.kt index 89c838637..c8fbf53f9 100644 --- a/library/shared-preferences/src/main/java/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeatureStorage.kt +++ b/laboratory/shared-preferences/src/main/kotlin/io/mehow/laboratory/sharedpreferences/SharedPreferencesFeatureStorage.kt @@ -13,7 +13,9 @@ internal class SharedPreferencesFeatureStorage( ) : FeatureStorage { override fun observeFeatureName(feature: Class>) = callbackFlow { val listener = OnSharedPreferenceChangeListener { _, key -> - if (key == feature.name) trySend(getStringSafe(key)) + if (key != null && key == feature.name) { + trySend(getStringSafe(key)) + } } send(getStringSafe(feature.name)) preferences.registerOnSharedPreferenceChangeListener(listener) diff --git a/library/build.gradle b/library/build.gradle deleted file mode 100644 index 540640a8e..000000000 --- a/library/build.gradle +++ /dev/null @@ -1,140 +0,0 @@ -import io.gitlab.arturbosch.detekt.Detekt -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -buildscript { - repositories { - mavenCentral() - gradlePluginPortal() - google() - } - - dependencies { - classpath libs.android.gradlePlugin - classpath libs.kotlin.gradlePlugin - classpath libs.mavenPublish.gradlePlugin - classpath libs.dokka.gradlePlugin - classpath libs.detekt.gradlePlugin - classpath libs.gradleVersions.gradlePlugin - classpath libs.wireGradlePlugin - classpath libs.kotlin.x.binaryCompatibility.gradlePlugin - } -} - -apply plugin: libs.plugins.binaryCompatibility.get().pluginId - -allprojects { - group GROUP - version VERSION_NAME - - tasks.withType(Test).configureEach { - testLogging { - events "skipped", "failed", "passed" - } - } - - tasks.withType(JavaCompile).configureEach { - sourceCompatibility JavaConfig.name - targetCompatibility JavaConfig.name - } - - tasks.withType(KotlinCompile).configureEach { - kotlinOptions { - jvmTarget = JavaConfig.name - freeCompilerArgs += [ - "-progressive", - "-Xjvm-default=all", - "-Xopt-in=kotlin.RequiresOptIn", - "-Xopt-in=kotlin.time.ExperimentalTime", - // TODO: Remove API in tests https://youtrack.jetbrains.com/issue/KT-42718 - "-Xexplicit-api=strict", - ] - } - } - - pluginManager.withPlugin("com.android.library") { plugin -> - android { - compileOptions { - sourceCompatibility JavaConfig.code - targetCompatibility JavaConfig.code - } - - defaultConfig { - targetSdkVersion libs.versions.androidBuild.targetSdk.get().toInteger() - vectorDrawables.useSupportLibrary true - } - - variantFilter { variant -> - setIgnore variant.name != "release" - } - - libraryVariants.all { variant -> - variant.outputs.all { - outputFileName = "${archivesBaseName}-${version}.aar" - } - } - - testOptions.unitTests.all { - useJUnitPlatform() - } - - lintOptions { - lintConfig rootProject.file("lint.xml") - - htmlReport !isCi() - xmlReport isCi() - xmlOutput file("build/reports/lint/lint-results.xml") - - textReport true - textOutput "stdout" - explainIssues false - - checkDependencies false - checkGeneratedSources true - checkTestSources false - checkReleaseBuilds false - } - } - } -} - -apply plugin: libs.plugins.detekt.get().pluginId - -dependencies { - detekt libs.detekt.formatting - detekt libs.detekt.cli -} - -tasks.withType(Detekt).configureEach { - parallel true - config.setFrom(rootProject.files("detekt-config.yml")) - setSource(files(projectDir)) - exclude "**/test/**", "**/androidTest/**" , "**/kyrie/**" - exclude subprojects.collect { "${rootDir.toPath().relativize(it.buildDir.toPath())}/" } - reports { - xml { - enabled = isCi() - destination = file("build/reports/detekt/detekt-results.xml") - } - html.enabled = !isCi() - sarif.enabled = false - txt.enabled = false - } -} - -apply plugin: libs.plugins.gradleVersions.get().pluginId - -dependencyUpdates { - rejectVersionIf { - isNonStable(it.candidate.version) && !isNonStable(it.currentVersion) - } -} - -private static def isNonStable(String version) { - def regex = /^[0-9,.v-]+(-r)?$/ - return !(version ==~ regex) -} - -private static def isCi() { - // noinspection GroovyPointlessBoolean - return System.getenv("CI")?.toBoolean() == true -} diff --git a/library/buildSrc/build.gradle.kts b/library/buildSrc/build.gradle.kts deleted file mode 100644 index bc0172f0f..000000000 --- a/library/buildSrc/build.gradle.kts +++ /dev/null @@ -1,3 +0,0 @@ -plugins { - `kotlin-dsl` -} diff --git a/library/buildSrc/settings.gradle.kts b/library/buildSrc/settings.gradle.kts deleted file mode 100644 index 0f47ea913..000000000 --- a/library/buildSrc/settings.gradle.kts +++ /dev/null @@ -1,6 +0,0 @@ -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - mavenCentral() - } -} diff --git a/library/buildSrc/src/main/kotlin/JavaConfig.kt b/library/buildSrc/src/main/kotlin/JavaConfig.kt deleted file mode 100644 index 2d5fa00cd..000000000 --- a/library/buildSrc/src/main/kotlin/JavaConfig.kt +++ /dev/null @@ -1,6 +0,0 @@ -import org.gradle.api.JavaVersion - -object JavaConfig { - val code = JavaVersion.VERSION_17 - val name = code.toString() -} diff --git a/library/data-store/build.gradle b/library/data-store/build.gradle deleted file mode 100644 index 690817ee5..000000000 --- a/library/data-store/build.gradle +++ /dev/null @@ -1,38 +0,0 @@ -plugins { - id "com.android.library" - id "org.jetbrains.kotlin.android" - id "com.squareup.wire" -} - -wire { - kotlin { } -} - -android { - namespace "io.mehow.laboratory.datastore" - - sourceSets { - getByName("main").java.srcDirs += "$buildDir/generated/source/wire/" - } -} - -dependencies { - api project(":laboratory") - api libs.android.x.dataStore - - testImplementation libs.kotest.runnerJunit5 - testImplementation libs.kotest.assertions - testImplementation libs.turbine -} - -apply from: "$rootDir/gradle/dokka-config.gradle" - -dokkaHtml { - dokkaSourceSets { - configureEach { - sourceRoots.from(file("$buildDir/generated/source/wire/io/mehow/laboratory/datastore/FeatureFlags.kt")) - } - } -} - -apply from: "$rootDir/gradle/gradle-mvn-push.gradle" diff --git a/library/data-store/src/test/java/io/mehow/laboratory/datastore/FeatureFlagsSerializerSpec.kt b/library/data-store/src/test/java/io/mehow/laboratory/datastore/FeatureFlagsSerializerSpec.kt deleted file mode 100644 index d0832d5a6..000000000 --- a/library/data-store/src/test/java/io/mehow/laboratory/datastore/FeatureFlagsSerializerSpec.kt +++ /dev/null @@ -1,31 +0,0 @@ -package io.mehow.laboratory.datastore - -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.shouldBe -import okio.Buffer -import okio.ByteString.Companion.decodeHex - -internal class FeatureFlagsSerializerSpec : DescribeSpec({ - describe("serializer") { - val flags = FeatureFlags(mapOf(FeatureA::class.java.toString() to FeatureA.A.name)) - val hex = "0a310a2c636c61737320696f2e6d65686f772e6c61626f7261746f72792e6461746173746f72652e4665617475726541120141" - val binaryFlags = hex.decodeHex() - - it("decodes bytes") { - val input = Buffer().write(binaryFlags).inputStream() - - val result = FeatureFlagsSerializer.readFrom(input) - - result shouldBe flags - } - - it("encodes bytes") { - val output = Buffer() - - FeatureFlagsSerializer.writeTo(flags, output.outputStream()) - val result = output.readByteString() - - result shouldBe binaryFlags - } - } -}) diff --git a/library/detekt-config.yml b/library/detekt-config.yml deleted file mode 100644 index 3c6dbcbb7..000000000 --- a/library/detekt-config.yml +++ /dev/null @@ -1,311 +0,0 @@ -build: - maxIssues: 0 - weights: - -complexity: - active: true - ComplexCondition: - active: true - threshold: 3 - ComplexInterface: - active: true - ComplexMethod: - active: true - threshold: 10 - ignoreSingleWhenExpression: true - LargeClass: - active: true - threshold: 400 - LongMethod: - active: true - threshold: 20 - LongParameterList: - active: true - functionThreshold: 5 - constructorThreshold: 5 - NestedBlockDepth: - active: true - StringLiteralDuplication: - active: true - threshold: 3 - ignoreAnnotation: true - excludeStringsWithLessThan5Characters: false - -coroutines: - active: true - GlobalCoroutineUsage: - active: true - RedundantSuspendModifier: - active: true - SleepInsteadOfDelay: - active: true - SuspendFunWithFlowReturnType: - active: true - -empty-blocks: - active: true - EmptyCatchBlock: - active: true - EmptyClassBlock: - active: true - EmptyDefaultConstructor: - active: true - EmptyDoWhileBlock: - active: true - EmptyElseBlock: - active: true - EmptyFinallyBlock: - active: true - EmptyForBlock: - active: true - EmptyFunctionBlock: - active: true - EmptyIfBlock: - active: true - EmptyInitBlock: - active: true - EmptyKtFile: - active: true - EmptySecondaryConstructor: - active: true - EmptyWhenBlock: - active: true - EmptyWhileBlock: - active: true - -exceptions: - active: true - ExceptionRaisedInUnexpectedLocation: - active: true - methodNames: 'toString,hashCode,equals,finalize' - InstanceOfCheckForException: - active: true - NotImplementedDeclaration: - active: true - ObjectExtendsThrowable: - active: true - PrintStackTrace: - active: true - RethrowCaughtException: - active: true - ReturnFromFinally: - active: true - SwallowedException: - active: true - ThrowingExceptionFromFinally: - active: true - ThrowingExceptionsWithoutMessageOrCause: - active: true - ThrowingNewInstanceOfSameException: - active: true - TooGenericExceptionCaught: - active: true - TooGenericExceptionThrown: - active: true - -formatting: - active: true - ChainWrapping: - active: true - CommentSpacing: - active: true - ImportOrdering: - active: true - Indentation: - active: false # TODO: Turn on when fixed in KtLint - indentSize: 2 - continuationIndentSize: 4 - NoBlankLineBeforeRbrace: - active: true - NoConsecutiveBlankLines: - active: true - NoLineBreakBeforeAssignment: - active: true - NoMultipleSpaces: - active: true - NoSemicolons: - active: true - NoTrailingSpaces: - active: true - ParameterListWrapping: - active: true - indentSize: 2 - SpacingAroundColon: - active: true - SpacingAroundComma: - active: true - SpacingAroundCurly: - active: true - SpacingAroundKeyword: - active: true - SpacingAroundOperators: - active: true - SpacingAroundRangeOperator: - active: true - StringTemplate: - active: true - -naming: - active: true - ClassNaming: - active: true - classPattern: '^[A-Z][a-z]+(?:[A-Z][a-z]+)*$' - ConstructorParameterNaming: - active: true - parameterPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' - privateParameterPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' - EnumNaming: - active: true - enumEntryPattern: '^[A-Z][a-z]+(?:[A-Z][a-z]+)*$' - FunctionNaming: - active: true - functionPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' - FunctionParameterNaming: - active: true - parameterPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' - MatchingDeclarationName: - active: true - MemberNameEqualsClassName: - active: true - ObjectPropertyNaming: - active: true - constantPattern: '^[a-zA-Z]+(?:[A-Z][a-z]+)*$' - propertyPattern: '^[a-zA-Z]+(?:[A-Z][a-z]+)*$' - privatePropertyPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' - PackageNaming: - active: true - packagePattern: '^[a-z]+(\.[a-z][a-z]*)*$' - TopLevelPropertyNaming: - active: true - constantPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' - propertyPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' - privatePropertyPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' - VariableNaming: - active: true - variablePattern: '^[a-z]+(?:[A-Z][a-z]+)*$' - privateVariablePattern: '^[a-z]+(?:[A-Z][a-z]+)*$' - -performance: - active: true - ForEachOnRange: - active: true - UnnecessaryTemporaryInstantiation: - active: true - -potential-bugs: - active: true - DuplicateCaseInWhenExpression: - active: true - EqualsAlwaysReturnsTrueOrFalse: - active: false - EqualsWithHashCodeExist: - active: true - ExplicitGarbageCollectionCall: - active: true - InvalidRange: - active: true - IteratorHasNextCallsNextMethod: - active: true - IteratorNotThrowingNoSuchElementException: - active: true - MissingWhenCase: - active: true - RedundantElseInWhen: - active: true - UnconditionalJumpStatementInLoop: - active: true - UnreachableCode: - active: true - UnusedUnaryOperator: - active: true - UselessPostfixExpression: - active: true - WrongEqualsTypeParameter: - active: true - -style: - active: true - CollapsibleIfStatements: - active: true - DataClassShouldBeImmutable: - active: true - EqualsNullCall: - active: true - EqualsOnSignatureLine: - active: true - ExplicitItLambdaParameter: - active: true - ForbiddenVoid: - active: true - LibraryCodeMustSpecifyReturnType: - active: true - LoopWithTooManyJumpStatements: - active: false - maxJumpCount: 1 - MaxLineLength: - active: true - maxLineLength: 120 - excludePackageStatements: true - excludeImportStatements: true - excludeCommentStatements: true - MayBeConst: - active: true - ModifierOrder: - active: true - NewLineAtEndOfFile: - active: true - NoTabs: - active: true - OptionalAbstractKeyword: - active: true - OptionalWhenBraces: - active: true - PreferToOverPairSyntax: - active: true - ProtectedMemberInFinalClass: - active: true - RedundantVisibilityModifierRule: - active: false - SafeCast: - active: true - SerialVersionUIDInSerializableClass: - active: true - SpacingBetweenPackageAndImports: - active: true - TrailingWhitespace: - active: true - UnderscoresInNumericLiterals: - active: true - acceptableLength: 3 - UnnecessaryAbstractClass: - active: true - UnnecessaryApply: - active: true - UnnecessaryInheritance: - active: true - UnnecessaryLet: - active: true - UnnecessaryParentheses: - active: true - UntilInsteadOfRangeTo: - active: true - UnusedImports: - active: true - UnusedPrivateClass: - active: true - UnusedPrivateMember: - active: true - UseArrayLiteralsInAnnotations: - active: true - UseCheckOrError: - active: true - UseRequire: - active: true - UselessCallOnNotNull: - active: true - UtilityClassWithPublicConstructor: - active: true - VarCouldBeVal: - active: true - WildcardImport: - active: true diff --git a/library/generator/build.gradle b/library/generator/build.gradle deleted file mode 100644 index 8e0175541..000000000 --- a/library/generator/build.gradle +++ /dev/null @@ -1,24 +0,0 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -plugins { - id "org.jetbrains.kotlin.jvm" -} - -test.useJUnitPlatform() - -tasks.withType(KotlinCompile).configureEach { - kotlinOptions.freeCompilerArgs += [ - "-Xopt-in=kotlin.io.path.ExperimentalPathApi", - ] -} - -dependencies { - api libs.kotlinPoet - implementation project(":laboratory") - - testImplementation libs.kotest.runnerJunit5 - testImplementation libs.kotest.assertions - testImplementation libs.kotest.property -} - -apply from: "$rootDir/gradle/gradle-mvn-push.gradle" diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/ClassNames.kt b/library/generator/src/main/java/io/mehow/laboratory/generator/ClassNames.kt deleted file mode 100644 index d0a578e2a..000000000 --- a/library/generator/src/main/java/io/mehow/laboratory/generator/ClassNames.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.mehow.laboratory.generator - -import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy -import com.squareup.kotlinpoet.TypeName -import com.squareup.kotlinpoet.asClassName -import kotlin.reflect.KClass - -internal operator fun KClass<*>.invoke(parameter: TypeName, vararg parameters: TypeName) = - asClassName().parameterizedBy(parameter, *parameters) - -internal operator fun KClass<*>.invoke(parameter: KClass<*>, vararg parameters: KClass<*>) = - invoke(parameter.asClassName(), *parameters.map { it.asClassName() }.toTypedArray()) diff --git a/library/generator/src/main/java/io/mehow/laboratory/generator/SourcedFeatureStorageGenerator.kt b/library/generator/src/main/java/io/mehow/laboratory/generator/SourcedFeatureStorageGenerator.kt deleted file mode 100644 index 6bf0ef8b0..000000000 --- a/library/generator/src/main/java/io/mehow/laboratory/generator/SourcedFeatureStorageGenerator.kt +++ /dev/null @@ -1,131 +0,0 @@ -package io.mehow.laboratory.generator - -import com.squareup.kotlinpoet.ClassName -import com.squareup.kotlinpoet.FileSpec -import com.squareup.kotlinpoet.FunSpec -import com.squareup.kotlinpoet.KModifier.ABSTRACT -import com.squareup.kotlinpoet.KModifier.DATA -import com.squareup.kotlinpoet.KModifier.OVERRIDE -import com.squareup.kotlinpoet.KModifier.PRIVATE -import com.squareup.kotlinpoet.KOperator.PLUS -import com.squareup.kotlinpoet.MemberName -import com.squareup.kotlinpoet.PropertySpec -import com.squareup.kotlinpoet.TypeSpec -import com.squareup.kotlinpoet.asClassName -import io.mehow.laboratory.FeatureStorage -import java.util.Locale - -internal class SourcedFeatureStorageGenerator( - storage: SourcedFeatureStorageModel, -) { - private val sourceNames = storage.sourceNames - .filterNot { featureName -> featureName.equals("local", ignoreCase = true) } - .distinct() - - private val sourced = MemberName(FeatureStorage.Companion::class.asClassName(), "sourced") - - private val emptyMap = MemberName(kotlinCollectionsSpace, "emptyMap") - - private val mapPlus = MemberName(kotlinCollectionsSpace, PLUS) - - private val infixTo = MemberName("kotlin", "to") - - private val buildingStepClassName = ClassName(storage.className.packageName, "BuildingStep") - - private val buildingStepType = TypeSpec.interfaceBuilder(buildingStepClassName) - .addModifiers(storage.visibility.modifier) - .addFunction(FunSpec.builder("build") - .addModifiers(ABSTRACT) - .returns(FeatureStorage::class) - .build()) - .build() - - private val remoteStepClassNames = sourceNames.distinct() - .sorted() - .map { ClassName(storage.className.packageName, it + stepSuffix) } - - private val remoteStepTypes = remoteStepClassNames - .windowed(size = 2, step = 1, partialWindows = true) { sources -> - val currentSourceClassName = sources.first() - val functionReturnClassName = sources.drop(1).firstOrNull() ?: buildingStepClassName - val functionName = currentSourceClassName.simpleName - .removeSuffix(stepSuffix) - .replaceFirstChar { it.lowercase(Locale.ROOT) } + "Source" - - TypeSpec.interfaceBuilder(currentSourceClassName) - .addModifiers(storage.visibility.modifier) - .addFunction(FunSpec.builder(functionName) - .addModifiers(ABSTRACT) - .addParameter("source", FeatureStorage::class) - .returns(functionReturnClassName) - .build()) - .build() - } - - private val builderType = TypeSpec.classBuilder(ClassName(storage.className.simpleName, "Builder")) - .addModifiers(PRIVATE, DATA) - .addSuperinterfaces(remoteStepClassNames + buildingStepClassName) - .primaryConstructor(FunSpec.constructorBuilder() - .addParameter(localSourceParam, FeatureStorage::class) - .addParameter(remoteSourcesParam, Map::class(String::class, FeatureStorage::class)) - .build()) - .addProperty(PropertySpec.builder(localSourceParam, FeatureStorage::class) - .initializer(localSourceParam) - .addModifiers(PRIVATE) - .build()) - .addProperty(PropertySpec.builder(remoteSourcesParam, Map::class(String::class, FeatureStorage::class)) - .initializer(remoteSourcesParam) - .addModifiers(PRIVATE) - .build()) - .addFunctions(remoteStepTypes.mapIndexed { index, remoteStep -> - val function = remoteStep.funSpecs.single() - function.toBuilder() - .apply { modifiers -= ABSTRACT } - .addModifiers(OVERRIDE) - .addStatement( - "return copy(\n⇥%1L = %1L %2M (%3S %4M %5N)⇤\n)", - remoteSourcesParam, - mapPlus, - remoteStepClassNames[index].simpleName.removeSuffix(stepSuffix), - infixTo, - function.parameters.single(), - ) - .build() - }) - .addFunction(buildingStepType.funSpecs.single() - .toBuilder() - .apply { modifiers -= ABSTRACT } - .addModifiers(OVERRIDE) - .addStatement("return %M(%L, %L)", sourced, localSourceParam, remoteSourcesParam) - .build()) - .build() - - private val storageBuilderExtension = FunSpec.builder("sourcedBuilder") - .addModifiers(storage.visibility.modifier) - .receiver(FeatureStorage.Companion::class) - .returns(remoteStepClassNames.firstOrNull() ?: buildingStepClassName) - .addParameter(localSourceParam, FeatureStorage::class) - .addStatement("return %N(%L, %M())", builderType, localSourceParam, emptyMap) - .build() - - private val storageFile = FileSpec.builder(storage.className.packageName, storage.className.simpleName) - .addFunction(storageBuilderExtension) - .apply { - for (type in remoteStepTypes) { - addType(type) - } - } - .addType(buildingStepType) - .addType(builderType) - .build() - - fun fileSpec() = storageFile - - private companion object { - const val stepSuffix = "Step" - const val localSourceParam = "localSource" - const val remoteSourcesParam = "remoteSources" - - const val kotlinCollectionsSpace = "kotlin.collections" - } -} diff --git a/library/generator/src/test/java/io/mehow/laboratory/generator/FeatureFactoryGeneratorSpec.kt b/library/generator/src/test/java/io/mehow/laboratory/generator/FeatureFactoryGeneratorSpec.kt deleted file mode 100644 index 4d4c55e78..000000000 --- a/library/generator/src/test/java/io/mehow/laboratory/generator/FeatureFactoryGeneratorSpec.kt +++ /dev/null @@ -1,118 +0,0 @@ -package io.mehow.laboratory.generator - -import com.squareup.kotlinpoet.ClassName -import io.kotest.core.spec.style.DescribeSpec -import io.mehow.laboratory.generator.Visibility.Internal -import io.mehow.laboratory.generator.Visibility.Public -import io.mehow.laboratory.generator.test.shouldSpecify - -internal class FeatureFactoryGeneratorSpec : DescribeSpec({ - val featureA = FeatureFlagModel( - className = ClassName("io.mehow", "FeatureA"), - options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - ) - - val featureB = FeatureFlagModel( - className = ClassName("io.mehow", "FeatureB"), - options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - ) - - val featureC = FeatureFlagModel( - className = ClassName("io.mehow.c", "FeatureA"), - options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - ) - - describe("generated feature flag factory") { - it("can be internal") { - val model = FeatureFactoryModel( - ClassName("io.mehow", "GeneratedFeatureFactory"), - listOf(featureA, featureB, featureC), - visibility = Internal, - ) - - val fileSpec = model.prepare("generated") - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import io.mehow.laboratory.FeatureFactory - |import java.lang.Class - |import kotlin.Suppress - |import kotlin.collections.Set - |import kotlin.collections.setOf - | - |internal fun FeatureFactory.Companion.generated(): FeatureFactory = GeneratedFeatureFactory - | - |private object GeneratedFeatureFactory : FeatureFactory { - | @Suppress("UNCHECKED_CAST") - | override fun create(): Set>> = setOf( - | Class.forName("io.mehow.FeatureA"), - | Class.forName("io.mehow.FeatureB"), - | Class.forName("io.mehow.c.FeatureA") - | ) as Set>> - |} - | - """.trimMargin() - } - - it("can be public") { - val model = FeatureFactoryModel( - ClassName("io.mehow", "GeneratedFeatureFactory"), - listOf(featureA, featureB, featureC), - visibility = Public, - ) - - val fileSpec = model.prepare("generated") - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import io.mehow.laboratory.FeatureFactory - |import java.lang.Class - |import kotlin.Suppress - |import kotlin.collections.Set - |import kotlin.collections.setOf - | - |public fun FeatureFactory.Companion.generated(): FeatureFactory = GeneratedFeatureFactory - | - |private object GeneratedFeatureFactory : FeatureFactory { - | @Suppress("UNCHECKED_CAST") - | override fun create(): Set>> = setOf( - | Class.forName("io.mehow.FeatureA"), - | Class.forName("io.mehow.FeatureB"), - | Class.forName("io.mehow.c.FeatureA") - | ) as Set>> - |} - | - """.trimMargin() - } - - it("is optimized in case of no features") { - val model = FeatureFactoryModel( - ClassName("io.mehow", "GeneratedFeatureFactory"), - features = emptyList(), - ) - - val fileSpec = model.prepare("generated") - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import io.mehow.laboratory.FeatureFactory - |import java.lang.Class - |import kotlin.collections.Set - |import kotlin.collections.emptySet - | - |internal fun FeatureFactory.Companion.generated(): FeatureFactory = GeneratedFeatureFactory - | - |private object GeneratedFeatureFactory : FeatureFactory { - | override fun create(): Set>> = emptySet>>() - |} - | - """.trimMargin() - } - } -}) diff --git a/library/generator/src/test/java/io/mehow/laboratory/generator/FeatureFlagGeneratorSpec.kt b/library/generator/src/test/java/io/mehow/laboratory/generator/FeatureFlagGeneratorSpec.kt deleted file mode 100644 index 81ced39cb..000000000 --- a/library/generator/src/test/java/io/mehow/laboratory/generator/FeatureFlagGeneratorSpec.kt +++ /dev/null @@ -1,534 +0,0 @@ -package io.mehow.laboratory.generator - -import com.squareup.kotlinpoet.ClassName -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.throwable.shouldHaveMessage -import io.kotest.property.Arb -import io.kotest.property.arbitrary.stringPattern -import io.kotest.property.checkAll -import io.mehow.laboratory.generator.Visibility.Internal -import io.mehow.laboratory.generator.Visibility.Public -import io.mehow.laboratory.generator.test.shouldSpecify -import java.util.Locale -import kotlin.DeprecationLevel.ERROR -import kotlin.DeprecationLevel.HIDDEN -import kotlin.DeprecationLevel.WARNING - -internal class FeatureFlagGeneratorSpec : DescribeSpec({ - describe("feature flag model") { - context("options") { - it("cannot be empty") { - val exception = shouldThrow { - FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - options = emptyList(), - ) - } - - exception shouldHaveMessage "io.mehow.FeatureA must have at least one option" - } - } - - context("default") { - it("cannot have no options") { - checkAll( - Arb.stringPattern("[a-z](0)([a-z]{0,10})"), - Arb.stringPattern("[a-z](1)([a-z]{0,10})"), - ) { first, second -> - val exception = shouldThrow { - FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(FeatureFlagOption(first), FeatureFlagOption(second)), - ) - } - - exception shouldHaveMessage "io.mehow.FeatureA must have exactly one default option" - } - } - - it("cannot have multiple options") { - checkAll( - Arb.stringPattern("[a-z](0)([a-z]{0,10})"), - Arb.stringPattern("[a-z](1)([a-z]{0,10})"), - Arb.stringPattern("[a-z](2)([a-z]{0,10})"), - ) { first, second, third -> - val exception = shouldThrow { - FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf( - FeatureFlagOption(first, isDefault = true), - FeatureFlagOption(second), - FeatureFlagOption(third, isDefault = true), - ), - ) - } - - exception shouldHaveMessage "io.mehow.FeatureA must have exactly one default option" - } - } - } - - context("supervisor option") { - it("cannot supervise itself") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(FeatureFlagOption("First", isDefault = true)), - ) - - val exception = shouldThrow { - FeatureFlagModel( - model.className, - model.options, - supervisor = Supervisor(model, model.options.first()), - ) - } - - exception shouldHaveMessage "io.mehow.FeatureA cannot supervise itself" - } - } - } - - describe("generated feature flag") { - it("can be internal") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - visibility = Internal, - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - | - |internal enum class FeatureA : Feature { - | First, - | Second, - | ; - | - | override val defaultOption: FeatureA - | get() = First - |} - | - """.trimMargin() - } - - it("can be public") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - visibility = Public, - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - | - |public enum class FeatureA : Feature { - | First, - | Second, - | ; - | - | override val defaultOption: FeatureA - | get() = First - |} - | - """.trimMargin() - } - - it("can have single option") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(FeatureFlagOption("First", isDefault = true)), - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - | - |public enum class FeatureA : Feature { - | First, - | ; - | - | override val defaultOption: FeatureA - | get() = First - |} - | - """.trimMargin() - } - - it("can have source") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - sourceOptions = listOf(FeatureFlagOption("Remote")), - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import java.lang.Class - | - |public enum class FeatureA : Feature { - | First, - | Second, - | ; - | - | override val defaultOption: FeatureA - | get() = First - | - | override val source: Class> = Source::class.java - | - | public enum class Source : Feature { - | Local, - | Remote, - | ; - | - | override val defaultOption: Source - | get() = Local - | } - |} - | - """.trimMargin() - } - - it("does not have source parameter if only source is Local") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - sourceOptions = listOf(FeatureFlagOption("Local")), - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - | - |public enum class FeatureA : Feature { - | First, - | Second, - | ; - | - | override val defaultOption: FeatureA - | get() = First - |} - | - """.trimMargin() - } - - it("filters out any custom local source") { - val localPermutations = (0b00000..0b11111).map { - listOf(it and 0b00001, it and 0b00010, it and 0b00100, it and 0b01000, it and 0b10000) - .map { mask -> mask != 0 } - .mapIndexed { index, mask -> - val chars = "local"[index].toString() - if (mask) chars else chars.replaceFirstChar { char -> char.titlecase(Locale.ROOT) } - }.joinToString(separator = "") - } - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - sourceOptions = (localPermutations + "Remote").map(::FeatureFlagOption), - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import java.lang.Class - | - |public enum class FeatureA : Feature { - | First, - | Second, - | ; - | - | override val defaultOption: FeatureA - | get() = First - | - | override val source: Class> = Source::class.java - | - | public enum class Source : Feature { - | Local, - | Remote, - | ; - | - | override val defaultOption: Source - | get() = Local - | } - |} - | - """.trimMargin() - } - - it("can change default source") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - sourceOptions = listOf(FeatureFlagOption("Remote", isDefault = true)), - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import java.lang.Class - | - |public enum class FeatureA : Feature { - | First, - | Second, - | ; - | - | override val defaultOption: FeatureA - | get() = First - | - | override val source: Class> = Source::class.java - | - | public enum class Source : Feature { - | Local, - | Remote, - | ; - | - | override val defaultOption: Source - | get() = Remote - | } - |} - | - """.trimMargin() - } - - it("source visibility follows feature visibility") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - options = listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - visibility = Internal, - sourceOptions = listOf(FeatureFlagOption("Remote")), - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import java.lang.Class - | - |internal enum class FeatureA : Feature { - | First, - | Second, - | ; - | - | override val defaultOption: FeatureA - | get() = First - | - | override val source: Class> = Source::class.java - | - | internal enum class Source : Feature { - | Local, - | Remote, - | ; - | - | override val defaultOption: Source - | get() = Local - | } - |} - | - """.trimMargin() - } - - context("description") { - it("is added as KDoc") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - description = "Feature description", - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import kotlin.String - | - |/** - | * Feature description - | */ - |public enum class FeatureA : Feature { - | First, - | Second, - | ; - | - | override val defaultOption: FeatureA - | get() = First - | - | override val description: String = "Feature description" - |} - | - """.trimMargin() - } - - it("does not break hyperlinks") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - description = "Some [long hyperlink](https://square.github.io/kotlinpoet/1.x/kotlinpoet-classinspector-elements/com.squareup.kotlinpoet.classinspector.elements/) in the KDoc.", - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import kotlin.String - | - |/** - | * Some - | * [long hyperlink](https://square.github.io/kotlinpoet/1.x/kotlinpoet-classinspector-elements/com.squareup.kotlinpoet.classinspector.elements/) - | * in the KDoc. - | */ - |public enum class FeatureA : Feature { - | First, - | Second, - | ; - | - | override val defaultOption: FeatureA - | get() = First - | - | override val description: String = - | "Some [long hyperlink](https://square.github.io/kotlinpoet/1.x/kotlinpoet-classinspector-elements/com.squareup.kotlinpoet.classinspector.elements/) in the KDoc." - |} - | - """.trimMargin() - } - } - - context("can be deprecated") { - it("with warning level by default") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - deprecation = Deprecation("Deprecation message"), - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import kotlin.Deprecated - |import kotlin.DeprecationLevel - |import kotlin.Suppress - | - |@Deprecated( - | message = "Deprecation message", - | level = DeprecationLevel.WARNING, - |) - |public enum class FeatureA : Feature<@Suppress("DEPRECATION") FeatureA> { - | First, - | Second, - | ; - | - | @Suppress("DEPRECATION") - | override val defaultOption: FeatureA - | get() = First - |} - | - """.trimMargin() - } - - enumValues().forEach { level -> - it("with explicit $level deprecation level") { - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - deprecation = Deprecation("Deprecation message", level), - ) - val suppressLevel = when (level) { - WARNING -> "DEPRECATION" - ERROR, HIDDEN -> "DEPRECATION_ERROR" - } - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import kotlin.Deprecated - |import kotlin.DeprecationLevel - |import kotlin.Suppress - | - |@Deprecated( - | message = "Deprecation message", - | level = DeprecationLevel.${level}, - |) - |public enum class FeatureA : Feature<@Suppress("$suppressLevel") FeatureA> { - | First, - | Second, - | ; - | - | @Suppress("$suppressLevel") - | override val defaultOption: FeatureA - | get() = First - |} - | - """.trimMargin() - } - } - } - - it("can have supervisor") { - val supervisor = FeatureFlagModel( - ClassName("io.mehow.supervisor", "Supervisor"), - listOf(FeatureFlagOption("First", isDefault = true)), - ) - val model = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - supervisor = Supervisor(supervisor, supervisor.options.first()), - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import io.mehow.supervisor.Supervisor - | - |public enum class FeatureA : Feature { - | First, - | Second, - | ; - | - | override val defaultOption: FeatureA - | get() = First - | - | override val supervisorOption: Feature<*> = Supervisor.First - |} - | - """.trimMargin() - } - } -}) diff --git a/library/generator/src/test/java/io/mehow/laboratory/generator/OptionFactoryModelSpec.kt b/library/generator/src/test/java/io/mehow/laboratory/generator/OptionFactoryModelSpec.kt deleted file mode 100644 index 2c6140b8e..000000000 --- a/library/generator/src/test/java/io/mehow/laboratory/generator/OptionFactoryModelSpec.kt +++ /dev/null @@ -1,306 +0,0 @@ -package io.mehow.laboratory.generator - -import com.squareup.kotlinpoet.ClassName -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.throwable.shouldHaveMessage -import io.kotest.property.Arb -import io.kotest.property.arbitrary.stringPattern -import io.kotest.property.checkAll -import io.mehow.laboratory.generator.Visibility.Internal -import io.mehow.laboratory.generator.Visibility.Public -import io.mehow.laboratory.generator.test.shouldSpecify -import kotlin.DeprecationLevel.ERROR -import kotlin.DeprecationLevel.HIDDEN -import kotlin.DeprecationLevel.WARNING - -internal class OptionFactoryModelSpec : DescribeSpec({ - describe("option factory model") { - it("features cannot have duplicate keys") { - checkAll( - Arb.stringPattern("[a-z](0)([a-z]{0,10})"), - Arb.stringPattern("[a-z](1)([a-z]{0,10})"), - ) { first, second -> - val features = listOf( - FeatureFlagModel( - className = ClassName("io.mehow1", first), - options = listOf(FeatureFlagOption("First", isDefault = true)), - key = "FeatureA", - ), - FeatureFlagModel( - className = ClassName("io.mehow1", second), - options = listOf(FeatureFlagOption("First", isDefault = true)), - key = "FeatureA", - ), - FeatureFlagModel( - className = ClassName("io.mehow2", "${second}A"), - options = listOf(FeatureFlagOption("First", isDefault = true)), - ), - FeatureFlagModel( - className = ClassName("io.mehow2", "${second}B"), - options = listOf(FeatureFlagOption("First", isDefault = true)), - ), - FeatureFlagModel( - className = ClassName("io.mehow2", "${second}C"), - options = listOf(FeatureFlagOption("First", isDefault = true)), - key = "FeatureB" - ), - FeatureFlagModel( - className = ClassName("io.mehow3", first), - options = listOf(FeatureFlagOption("First", isDefault = true)), - key = "FeatureC", - ), - FeatureFlagModel( - className = ClassName("io.mehow3", second), - options = listOf(FeatureFlagOption("First", isDefault = true)), - key = "FeatureB", - ), - ) - - val exception = shouldThrow { - OptionFactoryModel(ClassName("io.mehow", "GeneratedOptionFactory"), features) - } - - exception shouldHaveMessage """ - |Feature flags must have unique keys. Found following duplicates: - | - FeatureA: [io.mehow1.${first}, io.mehow1.${second}] - | - FeatureB: [io.mehow2.${second}C, io.mehow3.${second}] - """.trimMargin() - } - } - - it("features cannot have keys the same as other fqcns") { - checkAll( - Arb.stringPattern("[a-z](0)([a-z]{0,10})"), - Arb.stringPattern("[a-z](0)([a-z]{0,10})"), - ) { packageName, simpleName -> - val features = listOf( - FeatureFlagModel( - className = ClassName(packageName, simpleName), - options = listOf(FeatureFlagOption("First", isDefault = true)), - ), - FeatureFlagModel( - className = ClassName("io.mehow", "FeatureName"), - options = listOf(FeatureFlagOption("First", isDefault = true)), - key = "$packageName.$simpleName" - ), - ) - - val exception = shouldThrow { - OptionFactoryModel(ClassName("io.mehow", "GeneratedOptionFactory"), features) - } - - exception shouldHaveMessage """ - |Feature flags must have unique keys. Found following duplicates: - | - $packageName.$simpleName: [$packageName.$simpleName, io.mehow.FeatureName] - """.trimMargin() - } - } - } - - describe("generated option factory") { - it("can be internal") { - val features = listOf( - FeatureFlagModel( - className = ClassName("io.mehow", "FeatureA"), - options = listOf(FeatureFlagOption("OneA", isDefault = true), FeatureFlagOption("OneB")), - key = "FeatureA", - ), - FeatureFlagModel( - className = ClassName("io.mehow", "FeatureB"), - options = listOf(FeatureFlagOption("TwoA", isDefault = true)), - ), - FeatureFlagModel( - className = ClassName("io.mehow.c", "FeatureC"), - options = listOf(FeatureFlagOption("ThreeA", isDefault = true), FeatureFlagOption("ThreeB")), - key = "FeatureC", - ), - ) - val model = OptionFactoryModel( - ClassName("io.mehow", "GeneratedOptionFactory"), - features, - visibility = Internal, - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.c.FeatureC - |import io.mehow.laboratory.Feature - |import io.mehow.laboratory.OptionFactory - |import kotlin.String - | - |internal fun OptionFactory.Companion.generated(): OptionFactory = GeneratedOptionFactory - | - |private object GeneratedOptionFactory : OptionFactory { - | override fun create(key: String, name: String): Feature<*>? = when (key) { - | "FeatureA" -> when (name) { - | "OneA" -> FeatureA.OneA - | "OneB" -> FeatureA.OneB - | else -> null - | } - | "FeatureC" -> when (name) { - | "ThreeA" -> FeatureC.ThreeA - | "ThreeB" -> FeatureC.ThreeB - | else -> null - | } - | "io.mehow.FeatureB" -> when (name) { - | "TwoA" -> FeatureB.TwoA - | else -> null - | } - | else -> null - | } - |} - | - """.trimMargin() - } - - it("can be public") { - val features = listOf( - FeatureFlagModel( - className = ClassName("io.mehow", "FeatureA"), - options = listOf(FeatureFlagOption("OneA", isDefault = true), FeatureFlagOption("OneB")), - key = "FeatureA", - ), - FeatureFlagModel( - className = ClassName("io.mehow", "FeatureB"), - options = listOf(FeatureFlagOption("TwoA", isDefault = true)), - ), - FeatureFlagModel( - className = ClassName("io.mehow.c", "FeatureC"), - options = listOf(FeatureFlagOption("ThreeA", isDefault = true), FeatureFlagOption("ThreeB")), - key = "FeatureC", - ), - ) - val model = OptionFactoryModel( - ClassName("io.mehow", "GeneratedOptionFactory"), - features, - visibility = Public, - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.c.FeatureC - |import io.mehow.laboratory.Feature - |import io.mehow.laboratory.OptionFactory - |import kotlin.String - | - |public fun OptionFactory.Companion.generated(): OptionFactory = GeneratedOptionFactory - | - |private object GeneratedOptionFactory : OptionFactory { - | override fun create(key: String, name: String): Feature<*>? = when (key) { - | "FeatureA" -> when (name) { - | "OneA" -> FeatureA.OneA - | "OneB" -> FeatureA.OneB - | else -> null - | } - | "FeatureC" -> when (name) { - | "ThreeA" -> FeatureC.ThreeA - | "ThreeB" -> FeatureC.ThreeB - | else -> null - | } - | "io.mehow.FeatureB" -> when (name) { - | "TwoA" -> FeatureB.TwoA - | else -> null - | } - | else -> null - | } - |} - | - """.trimMargin() - } - - it("is optimized in case of no features") { - val model = OptionFactoryModel( - ClassName("io.mehow", "GeneratedOptionFactory"), - features = emptyList(), - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import io.mehow.laboratory.OptionFactory - |import kotlin.String - | - |internal fun OptionFactory.Companion.generated(): OptionFactory = GeneratedOptionFactory - | - |private object GeneratedOptionFactory : OptionFactory { - | override fun create(key: String, name: String): Feature<*>? = null - |} - | - """.trimMargin() - } - - it("suppresses usage of deprecated features") { - val features = listOf( - FeatureFlagModel( - className = ClassName("io.mehow", "FeatureA"), - options = listOf(FeatureFlagOption("OneA", isDefault = true)), - ), - FeatureFlagModel( - className = ClassName("io.mehow", "FeatureB"), - options = listOf(FeatureFlagOption("TwoA", isDefault = true)), - deprecation = Deprecation("message", WARNING), - ), - FeatureFlagModel( - className = ClassName("io.mehow", "FeatureC"), - options = listOf(FeatureFlagOption("ThreeA", isDefault = true)), - deprecation = Deprecation("message", ERROR), - ), - FeatureFlagModel( - className = ClassName("io.mehow", "FeatureD"), - options = listOf(FeatureFlagOption("FourA", isDefault = true)), - deprecation = Deprecation("message", HIDDEN), - ), - ) - val model = OptionFactoryModel( - ClassName("io.mehow", "GeneratedOptionFactory"), - features, - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.Feature - |import io.mehow.laboratory.OptionFactory - |import kotlin.String - |import kotlin.Suppress - | - |internal fun OptionFactory.Companion.generated(): OptionFactory = GeneratedOptionFactory - | - |private object GeneratedOptionFactory : OptionFactory { - | override fun create(key: String, name: String): Feature<*>? = when (key) { - | "io.mehow.FeatureA" -> when (name) { - | "OneA" -> FeatureA.OneA - | else -> null - | } - | "io.mehow.FeatureB" -> @Suppress("DEPRECATION") when (name) { - | "TwoA" -> FeatureB.TwoA - | else -> null - | } - | "io.mehow.FeatureC" -> @Suppress("DEPRECATION_ERROR") when (name) { - | "ThreeA" -> FeatureC.ThreeA - | else -> null - | } - | "io.mehow.FeatureD" -> @Suppress("DEPRECATION_ERROR") when (name) { - | "FourA" -> FeatureD.FourA - | else -> null - | } - | else -> null - | } - |} - | - """.trimMargin() - } - } -}) diff --git a/library/generator/src/test/java/io/mehow/laboratory/generator/SourcedFeatureStorageGeneratorSpec.kt b/library/generator/src/test/java/io/mehow/laboratory/generator/SourcedFeatureStorageGeneratorSpec.kt deleted file mode 100644 index 612310e91..000000000 --- a/library/generator/src/test/java/io/mehow/laboratory/generator/SourcedFeatureStorageGeneratorSpec.kt +++ /dev/null @@ -1,266 +0,0 @@ -package io.mehow.laboratory.generator - -import com.squareup.kotlinpoet.ClassName -import io.kotest.core.spec.style.DescribeSpec -import io.mehow.laboratory.generator.Visibility.Internal -import io.mehow.laboratory.generator.Visibility.Public -import io.mehow.laboratory.generator.test.shouldSpecify -import java.util.Locale - -internal class SourcedFeatureStorageGeneratorSpec : DescribeSpec({ - describe("generated feature storage") { - it("can be internal") { - val model = SourcedFeatureStorageModel( - ClassName("io.mehow", "SourcedGeneratedFeatureStorage"), - sourceNames = listOf("Firebase", "S3"), - visibility = Internal, - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.FeatureStorage - |import io.mehow.laboratory.FeatureStorage.Companion.sourced - |import kotlin.String - |import kotlin.collections.Map - |import kotlin.collections.emptyMap - |import kotlin.collections.plus - |import kotlin.to - | - |internal fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage): FirebaseStep = - | Builder(localSource, emptyMap()) - | - |internal interface FirebaseStep { - | public fun firebaseSource(source: FeatureStorage): S3Step - |} - | - |internal interface S3Step { - | public fun s3Source(source: FeatureStorage): BuildingStep - |} - | - |internal interface BuildingStep { - | public fun build(): FeatureStorage - |} - | - |private data class Builder( - | private val localSource: FeatureStorage, - | private val remoteSources: Map, - |) : FirebaseStep, S3Step, BuildingStep { - | override fun firebaseSource(source: FeatureStorage): S3Step = copy( - | remoteSources = remoteSources + ("Firebase" to source) - | ) - | - | override fun s3Source(source: FeatureStorage): BuildingStep = copy( - | remoteSources = remoteSources + ("S3" to source) - | ) - | - | override fun build(): FeatureStorage = sourced(localSource, remoteSources) - |} - | - """.trimMargin() - } - - it("can be public") { - val model = SourcedFeatureStorageModel( - ClassName("io.mehow", "SourcedGeneratedFeatureStorage"), - sourceNames = listOf("Firebase", "S3"), - visibility = Public, - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.FeatureStorage - |import io.mehow.laboratory.FeatureStorage.Companion.sourced - |import kotlin.String - |import kotlin.collections.Map - |import kotlin.collections.emptyMap - |import kotlin.collections.plus - |import kotlin.to - | - |public fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage): FirebaseStep = - | Builder(localSource, emptyMap()) - | - |public interface FirebaseStep { - | public fun firebaseSource(source: FeatureStorage): S3Step - |} - | - |public interface S3Step { - | public fun s3Source(source: FeatureStorage): BuildingStep - |} - | - |public interface BuildingStep { - | public fun build(): FeatureStorage - |} - | - |private data class Builder( - | private val localSource: FeatureStorage, - | private val remoteSources: Map, - |) : FirebaseStep, S3Step, BuildingStep { - | override fun firebaseSource(source: FeatureStorage): S3Step = copy( - | remoteSources = remoteSources + ("Firebase" to source) - | ) - | - | override fun s3Source(source: FeatureStorage): BuildingStep = copy( - | remoteSources = remoteSources + ("S3" to source) - | ) - | - | override fun build(): FeatureStorage = sourced(localSource, remoteSources) - |} - | - """.trimMargin() - } - - it("ignores duplicate source names") { - val model = SourcedFeatureStorageModel( - ClassName("io.mehow", "SourcedGeneratedFeatureStorage"), - sourceNames = listOf("Foo", "Bar", "Baz", "Foo", "Baz", "Foo"), - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.FeatureStorage - |import io.mehow.laboratory.FeatureStorage.Companion.sourced - |import kotlin.String - |import kotlin.collections.Map - |import kotlin.collections.emptyMap - |import kotlin.collections.plus - |import kotlin.to - | - |internal fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage): BarStep = - | Builder(localSource, emptyMap()) - | - |internal interface BarStep { - | public fun barSource(source: FeatureStorage): BazStep - |} - | - |internal interface BazStep { - | public fun bazSource(source: FeatureStorage): FooStep - |} - | - |internal interface FooStep { - | public fun fooSource(source: FeatureStorage): BuildingStep - |} - | - |internal interface BuildingStep { - | public fun build(): FeatureStorage - |} - | - |private data class Builder( - | private val localSource: FeatureStorage, - | private val remoteSources: Map, - |) : BarStep, BazStep, FooStep, BuildingStep { - | override fun barSource(source: FeatureStorage): BazStep = copy( - | remoteSources = remoteSources + ("Bar" to source) - | ) - | - | override fun bazSource(source: FeatureStorage): FooStep = copy( - | remoteSources = remoteSources + ("Baz" to source) - | ) - | - | override fun fooSource(source: FeatureStorage): BuildingStep = copy( - | remoteSources = remoteSources + ("Foo" to source) - | ) - | - | override fun build(): FeatureStorage = sourced(localSource, remoteSources) - |} - | - """.trimMargin() - } - - it("ignores local source name") { - val localPermutations = (0b00000..0b11111).map { - listOf(it and 0b00001, it and 0b00010, it and 0b00100, it and 0b01000, it and 0b10000) - .map { mask -> mask != 0 } - .mapIndexed { index, mask -> - val chars = "local"[index].toString() - if (mask) chars else chars.replaceFirstChar { char -> char.titlecase(Locale.ROOT) } - }.joinToString(separator = "") - } - val model = SourcedFeatureStorageModel( - ClassName("io.mehow", "SourcedGeneratedFeatureStorage"), - sourceNames = localPermutations + "Foo", - visibility = Public, - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.FeatureStorage - |import io.mehow.laboratory.FeatureStorage.Companion.sourced - |import kotlin.String - |import kotlin.collections.Map - |import kotlin.collections.emptyMap - |import kotlin.collections.plus - |import kotlin.to - | - |public fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage): FooStep = - | Builder(localSource, emptyMap()) - | - |public interface FooStep { - | public fun fooSource(source: FeatureStorage): BuildingStep - |} - | - |public interface BuildingStep { - | public fun build(): FeatureStorage - |} - | - |private data class Builder( - | private val localSource: FeatureStorage, - | private val remoteSources: Map, - |) : FooStep, BuildingStep { - | override fun fooSource(source: FeatureStorage): BuildingStep = copy( - | remoteSources = remoteSources + ("Foo" to source) - | ) - | - | override fun build(): FeatureStorage = sourced(localSource, remoteSources) - |} - | - """.trimMargin() - } - - it("can have only local source") { - val model = SourcedFeatureStorageModel( - ClassName("io.mehow", "SourcedGeneratedFeatureStorage"), - sourceNames = emptyList(), - visibility = Public, - ) - - val fileSpec = model.prepare() - - fileSpec shouldSpecify """ - |package io.mehow - | - |import io.mehow.laboratory.FeatureStorage - |import io.mehow.laboratory.FeatureStorage.Companion.sourced - |import kotlin.String - |import kotlin.collections.Map - |import kotlin.collections.emptyMap - | - |public fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage): BuildingStep = - | Builder(localSource, emptyMap()) - | - |public interface BuildingStep { - | public fun build(): FeatureStorage - |} - | - |private data class Builder( - | private val localSource: FeatureStorage, - | private val remoteSources: Map, - |) : BuildingStep { - | override fun build(): FeatureStorage = sourced(localSource, remoteSources) - |} - | - """.trimMargin() - } - } -}) diff --git a/library/generator/src/test/java/io/mehow/laboratory/generator/SupervisorSpec.kt b/library/generator/src/test/java/io/mehow/laboratory/generator/SupervisorSpec.kt deleted file mode 100644 index db28ebe69..000000000 --- a/library/generator/src/test/java/io/mehow/laboratory/generator/SupervisorSpec.kt +++ /dev/null @@ -1,44 +0,0 @@ -package io.mehow.laboratory.generator - -import com.squareup.kotlinpoet.ClassName -import io.kotest.assertions.throwables.shouldNotThrowAny -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.throwable.shouldHaveMessage -import io.kotest.property.Arb -import io.kotest.property.arbitrary.stringPattern -import io.kotest.property.checkAll - -internal class SupervisorSpec : DescribeSpec({ - context("supervisor option") { - it("must be present in parent") { - checkAll(Arb.stringPattern("[a-z](0)([a-z]{0,10})")) { optionName -> - val option = FeatureFlagOption(optionName, isDefault = true) - val feature = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(option), - ) - - shouldNotThrowAny { - Supervisor(feature, option) - } - } - } - - it("cannot be absent in parent") { - checkAll(Arb.stringPattern("[a-z](0)([a-z]{0,10})")) { optionName -> - val feature = FeatureFlagModel( - ClassName("io.mehow", "FeatureA"), - listOf(FeatureFlagOption("First", isDefault = true), FeatureFlagOption("Second")), - ) - val option = FeatureFlagOption(optionName, isDefault = true) - - val exception = shouldThrow { - Supervisor(feature, option) - } - - exception shouldHaveMessage "Feature flag io.mehow.FeatureA does not contain option $optionName" - } - } - } -}) diff --git a/library/generator/src/test/java/io/mehow/laboratory/generator/test/KotlinPoetMatchers.kt b/library/generator/src/test/java/io/mehow/laboratory/generator/test/KotlinPoetMatchers.kt deleted file mode 100644 index 390d2848f..000000000 --- a/library/generator/src/test/java/io/mehow/laboratory/generator/test/KotlinPoetMatchers.kt +++ /dev/null @@ -1,6 +0,0 @@ -package io.mehow.laboratory.generator.test - -import com.squareup.kotlinpoet.FileSpec -import io.kotest.matchers.shouldBe - -internal infix fun FileSpec.shouldSpecify(value: String) = toString() shouldBe value diff --git a/library/gradle-plugin/build.gradle b/library/gradle-plugin/build.gradle deleted file mode 100644 index 69783c85f..000000000 --- a/library/gradle-plugin/build.gradle +++ /dev/null @@ -1,69 +0,0 @@ -import org.jetbrains.dokka.gradle.DokkaTask -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - - -plugins { - id "org.jetbrains.kotlin.jvm" - id "java-gradle-plugin" -} - -gradlePlugin { - plugins { - laboratory { - id = "io.mehow.laboratory" - implementationClass = "io.mehow.laboratory.gradle.LaboratoryPlugin" - } - } -} - -test.useJUnitPlatform() - -configurations { - fixtureClasspath -} - -tasks.getByName("pluginUnderTestMetadata").getPluginClasspath().from(configurations.fixtureClasspath) - -dependencies { - implementation project(":generator") - implementation libs.kotlin.gradlePlugin - compileOnly libs.android.gradlePlugin - - testImplementation libs.kotest.runnerJunit5 - testImplementation libs.kotest.assertions - - fixtureClasspath libs.kotlin.gradlePlugin - fixtureClasspath libs.android.gradlePlugin -} - -def versionDir = "${buildDir}/generated/source/laboratory" -sourceSets.main.java.srcDirs += versionDir - -def generateVersion = tasks.register("pluginVersion") { - inputs.property "version", version - outputs.dir file(versionDir) - - doLast { - def outputFile = file("$versionDir/io/mehow/laboratory/Config.kt") - outputFile.parentFile.mkdirs() - outputFile.text = """package io.mehow.laboratory - -internal const val laboratoryVersion = "$version" -""" - } -} - -tasks.withType(KotlinCompile).configureEach { - it.dependsOn(generateVersion) -} - -apply from: "$rootDir/gradle/dokka-config.gradle" -apply from: "$rootDir/gradle/gradle-mvn-push.gradle" - -tasks.withType(Jar).configureEach { - it.dependsOn(generateVersion) -} - -tasks.withType(DokkaTask).configureEach { - it.dependsOn(generateVersion) -} diff --git a/library/gradle.properties b/library/gradle.properties deleted file mode 100644 index 153ea69ae..000000000 --- a/library/gradle.properties +++ /dev/null @@ -1,29 +0,0 @@ -GROUP=io.mehow.laboratory -VERSION_NAME=1.1.1-SNAPSHOT - -POM_DESCRIPTION=Library for feature flags management. - -POM_URL=https://github.com/MiSikora/laboratory -POM_SCM_URL=https://github.com/MiSikora/laboratory -POM_SCM_CONNECTION=scm:git:git://github.com/MiSikora/laboratory.git -POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/MiSikora/laboratory.git - -POM_LICENCE_NAME=The Apache Software License, Version 2.0 -POM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt -POM_LICENCE_DIST=repo - -POM_DEVELOPER_ID=michalsikora90 -POM_DEVELOPER_NAME=Michal Sikora - -android.useAndroidX=true - -# Increase the build VMs heap size. Default is 512m. -# Increase metaspace for Dokka https://github.com/Kotlin/dokka/issues/1405 -# Illegal access: https://youtrack.jetbrains.com/issue/KT-45545 -org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=1g --illegal-access=permit -org.gradle.parallel=true - -android.defaults.buildfeatures.buildconfig=false -android.defaults.buildfeatures.aidl=false -android.defaults.buildfeatures.renderscript=false -android.defaults.buildfeatures.shaders=false diff --git a/library/gradle/dependencies.toml b/library/gradle/dependencies.toml deleted file mode 100644 index 10a178d6a..000000000 --- a/library/gradle/dependencies.toml +++ /dev/null @@ -1,73 +0,0 @@ -[versions] -androidBuild-targetSdk = "33" -androidPlugin = "8.0.2" -coroutines = "1.6.4" -kotest = "4.6.3" -hyperion = "0.9.37" -detekt = "1.22.0" - -[libraries] -android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidPlugin" } - -android-x-dataStore = { group = "androidx.datastore", name = "datastore-core", version = "1.0.0" } -android-x-appCompat = { group = "androidx.appcompat", name = "appcompat", version = "1.6.1" } -android-x-viewModelKtx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version = "2.6.1" } -android-x-fragmentKtx = { group = "androidx.fragment", name = "fragment-ktx", version = "1.5.7" } -android-x-viewPager2 = { group = "androidx.viewpager2", name = "viewpager2", version = "1.0.0" } -android-x-recyclerView = { group = "androidx.recyclerview", name = "recyclerview", version = "1.3.0" } - -android-x-test-coreKtx = { group = "androidx.test", name = "core-ktx", version = "1.5.0" } -android-x-test-orchestrator = { group = "androidx.test", name = "orchestrator", version = "1.4.2" } -android-x-test-runner = { group = "androidx.test", name = "runner", version = "1.5.2" } -android-x-testExtJunitKtx = { group = "androidx.test.ext", name = "junit-ktx", version = "1.1.5" } - -android-material = { group = "com.google.android.material", name = "material", version = "1.9.0" } - -firebase-databaseKtx = { group = "com.google.firebase", name = "firebase-database-ktx", version = "20.2.2" } - -googleServices-gradlePlugin = { group = "com.google.gms", name = "google-services", version = "4.3.15" } - -kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version = "1.8.21" } - -kotlin-x-coroutinesCore = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } -kotlin-x-coroutinesAndroid = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } -kotlin-x-coroutinesTest = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } -kotlin-x-binaryCompatibility-gradlePlugin = { group = "org.jetbrains.kotlinx", name = "binary-compatibility-validator", version = "0.13.1" } - -dokka-gradlePlugin = { group = "org.jetbrains.dokka", name = "dokka-gradle-plugin", version = "1.8.10" } - -kotest-runnerJunit5 = { group = "io.kotest", name = "kotest-runner-junit5-jvm", version.ref = "kotest" } -kotest-assertions = { group = "io.kotest", name = "kotest-assertions-core-jvm", version.ref = "kotest" } -kotest-property = { group = "io.kotest", name = "kotest-property-jvm", version.ref = "kotest" } - -hyperion-core = { group = "com.willowtreeapps.hyperion", name = "hyperion-core", version.ref = "hyperion" } -hyperion-plugin = { group = "com.willowtreeapps.hyperion", name = "hyperion-plugin", version.ref = "hyperion" } - -autoService = { group = "com.google.auto.service", name = "auto-service", version = "1.1.0" } - -mavenPublish-gradlePlugin = { group = "com.vanniktech", name = "gradle-maven-publish-plugin", version = "0.25.2" } - -detekt-gradlePlugin = { group = "io.gitlab.arturbosch.detekt", name = "detekt-gradle-plugin", version.ref = "detekt" } -detekt-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-formatting", version.ref = "detekt" } -detekt-cli = { group = "io.gitlab.arturbosch.detekt", name = "detekt-cli", version.ref = "detekt" } - -gradleVersions-gradlePlugin = { group = "com.github.ben-manes", name = "gradle-versions-plugin", version = "0.46.0" } - -kotlinPoet = { group = "com.squareup", name = "kotlinpoet", version = "1.14.2" } -wireGradlePlugin = { group = "com.squareup.wire", name = "wire-gradle-plugin", version = "4.7.0" } -turbine = { group = "app.cash.turbine", name = "turbine", version = "0.7.0" } - -laboratory-core = { module = "io.mehow.laboratory:laboratory" } -laboratory-inspector = { module = "io.mehow.laboratory:inspector" } -laboratory-hyperionPlugin = { module = "io.mehow.laboratory:hyperion-plugin" } -laboratory-sharedPreferences = { module = "io.mehow.laboratory:shared-preferences" } -laboratory-dataStore = { module = "io.mehow.laboratory:data-store" } -laboratory-generator = { module = "io.mehow.laboratory:generator" } -laboratory-gradlePlugin = { module = "io.mehow.laboratory:gradle-plugin" } - -[plugins] -gradleVersions = { id = "com.github.ben-manes.versions" } -detekt = { id = "io.gitlab.arturbosch.detekt" } -binaryCompatibility = { id = "binary-compatibility-validator", version = "0.13.1" } -dokka = { id = "org.jetbrains.dokka" } -mavenPublish = { id = "com.vanniktech.maven.publish" } diff --git a/library/gradle/dokka-config.gradle b/library/gradle/dokka-config.gradle deleted file mode 100644 index a71f61957..000000000 --- a/library/gradle/dokka-config.gradle +++ /dev/null @@ -1,14 +0,0 @@ -apply plugin: libs.plugins.dokka.get().pluginId - -dokkaHtml { - outputDirectory.set(file("$rootDir/docs/api/${project.name}")) - - dokkaSourceSets { - configureEach { - jdkVersion.set(8) - reportUndocumented.set(false) - skipDeprecated.set(true) - skipEmptyPackages.set(true) - } - } -} diff --git a/library/gradle/gradle-mvn-push.gradle b/library/gradle/gradle-mvn-push.gradle deleted file mode 100644 index 73038ee6a..000000000 --- a/library/gradle/gradle-mvn-push.gradle +++ /dev/null @@ -1,6 +0,0 @@ -apply plugin: libs.plugins.mavenPublish.get().pluginId - -mavenPublishing { - publishToMavenCentral() - signAllPublications() -} diff --git a/library/hyperion-plugin/build.gradle b/library/hyperion-plugin/build.gradle deleted file mode 100644 index 57e109524..000000000 --- a/library/hyperion-plugin/build.gradle +++ /dev/null @@ -1,19 +0,0 @@ -plugins { - id "com.android.library" - id "org.jetbrains.kotlin.android" - id "org.jetbrains.kotlin.kapt" -} - -android { - namespace "io.mehow.laboratory.hyperion" - resourcePrefix "io_mehow_laboratory_" -} - -dependencies { - api project(":inspector") - api libs.hyperion.plugin - implementation libs.android.x.appCompat - kapt libs.autoService -} - -apply from: "$rootDir/gradle/gradle-mvn-push.gradle" diff --git a/library/hyperion-plugin/src/main/AndroidManifest.xml b/library/hyperion-plugin/src/main/AndroidManifest.xml deleted file mode 100644 index 01b745f9e..000000000 --- a/library/hyperion-plugin/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/library/inspector/build.gradle b/library/inspector/build.gradle deleted file mode 100644 index 2e556f652..000000000 --- a/library/inspector/build.gradle +++ /dev/null @@ -1,34 +0,0 @@ -plugins { - id "com.android.library" - id "org.jetbrains.kotlin.android" -} - -android { - namespace "io.mehow.laboratory.inspector" - resourcePrefix "io_mehow_laboratory_" - - defaultConfig { - consumerProguardFile "io-mehow-laboratory-inspector.pro" - } -} - -dependencies { - api project(":laboratory") - implementation libs.hyperion.plugin - implementation libs.android.x.appCompat - implementation libs.android.x.fragmentKtx - implementation libs.android.x.viewModelKtx - implementation libs.android.x.recyclerView - implementation libs.android.x.viewPager2 - implementation libs.android.material - implementation libs.kotlin.x.coroutinesAndroid - - testImplementation libs.kotest.runnerJunit5 - testImplementation libs.kotest.assertions - testImplementation libs.kotest.property - testImplementation libs.turbine - testImplementation libs.kotlin.x.coroutinesTest -} - -apply from: "$rootDir/gradle/dokka-config.gradle" -apply from: "$rootDir/gradle/gradle-mvn-push.gradle" diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/OptionViewGroup.kt b/library/inspector/src/main/java/io/mehow/laboratory/inspector/OptionViewGroup.kt deleted file mode 100644 index 101ef40e7..000000000 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/OptionViewGroup.kt +++ /dev/null @@ -1,90 +0,0 @@ -package io.mehow.laboratory.inspector - -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import android.widget.CompoundButton -import androidx.appcompat.content.res.AppCompatResources -import androidx.appcompat.widget.PopupMenu -import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipGroup -import io.mehow.laboratory.Feature -import com.google.android.material.R as MaterialR - -internal class OptionViewGroup @JvmOverloads constructor( - context: Context, - attrs: AttributeSet, - defStyle: Int = MaterialR.attr.chipGroupStyle, -) : ChipGroup(context, attrs, defStyle) { - private val inflater = LayoutInflater.from(context) - private var listener: OptionGroupListener? = null - - init { - isSelectionRequired = true - } - - fun setOnSelectFeatureListener(listener: OptionGroupListener?) { - this.listener = listener - } - - fun render(models: List, isEnabled: Boolean) { - chips.forEach(::removeOnCheckedChangeListener) - removeAllViews() - models.map { createChip(it, isEnabled) }.forEach(::addView) - } - - private fun createChip(model: OptionUiModel, isEnabled: Boolean): Chip { - val chip = inflater.inflate(R.layout.io_mehow_laboratory_feature_option_chip, this, false) as Chip - return chip.apply { - text = model.option.name - isChecked = model.isSelected - if (model.supervisedFeatures.isNotEmpty()) { - chipIcon = AppCompatResources.getDrawable(context, R.drawable.io_mehow_laboratory_supervisor) - setOnLongClickListener { showSupervisedFeaturesMenu(this, model.supervisedFeatures) } - } - isActivated = isEnabled - this.isEnabled = isEnabled - setOnCheckedChangeListener(createListener(model)) - } - } - - private fun createListener(model: OptionUiModel) = CompoundButton.OnCheckedChangeListener { chip, isChecked -> - if (isChecked) { - (chip as Chip).deselectOtherChips() - listener?.onSelectOption(model.option) - } - } - - // ChipGroup.isSingleSelection does not work with initial selection from code. - private fun Chip.deselectOtherChips() { - chips.filter { it !== this }.forEach { chip -> chip.isChecked = false } - } - - private fun removeOnCheckedChangeListener(chip: Chip) = chip.setOnCheckedChangeListener(null) - - private fun showSupervisedFeaturesMenu(anchor: Chip, features: List>>): Boolean { - PopupMenu(context, anchor).apply { - features.forEachIndexed { index, feature -> - menu.add(0, index, index, feature.simpleName) - } - setOnMenuItemClickListener { - listener?.onSelectSupervisedFeature(features[it.order]) - true - } - }.show() - return true - } - - private val chips: Sequence get() = sequence { - for (index in 0 until childCount) { - val chip = getChildAt(index) as? Chip ?: continue - yield(chip) - } - } - - interface OptionGroupListener { - fun onSelectOption(option: Feature<*>) - - fun onSelectSupervisedFeature(feature: Class>) - } -} diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SourceViewGroup.kt b/library/inspector/src/main/java/io/mehow/laboratory/inspector/SourceViewGroup.kt deleted file mode 100644 index a9b620314..000000000 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SourceViewGroup.kt +++ /dev/null @@ -1,54 +0,0 @@ -package io.mehow.laboratory.inspector - -import android.content.Context -import android.util.AttributeSet -import android.view.View -import android.widget.AdapterView -import androidx.appcompat.widget.AppCompatSpinner -import io.mehow.laboratory.Feature -import androidx.appcompat.R as AppCompatR - -internal class SourceViewGroup @JvmOverloads constructor( - context: Context, - attrs: AttributeSet, - defStyle: Int = AppCompatR.attr.spinnerStyle, -) : AppCompatSpinner(context, attrs, defStyle) { - internal var listener: OnSelectSourceListener? = null - - override fun getAdapter() = super.getAdapter() as? SourceAdapter - - fun setOnSelectSourceListener(listener: OnSelectSourceListener?) { - this.listener = listener - } - - fun render(models: List) { - val features = models.map(OptionUiModel::option) - val selectedFeature = models.firstOrNull(OptionUiModel::isSelected)?.option ?: return - val newAdapter = SourceAdapter(features) - onItemSelectedListener = createListener() - adapter = newAdapter - val position = newAdapter.positionOf(selectedFeature) - setSelection(position) - } - - private fun createListener() = object : OnItemSelectedListener { - var ignoreItem = true - - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - if (ignoreItem) { - ignoreItem = false - return - } - val item = requireNotNull(adapter) { - "Feature source adapter is not set" - }.getItem(position) - listener?.onSelectSource(item) - } - - override fun onNothingSelected(parent: AdapterView<*>?) = Unit - } - - interface OnSelectSourceListener { - fun onSelectSource(option: Feature<*>) - } -} diff --git a/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelDeprecationSpec.kt b/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelDeprecationSpec.kt deleted file mode 100644 index 89fd3cdb8..000000000 --- a/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelDeprecationSpec.kt +++ /dev/null @@ -1,158 +0,0 @@ -package io.mehow.laboratory.inspector - -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.collections.shouldContainExactly -import io.mehow.laboratory.Feature -import io.mehow.laboratory.FeatureFactory -import io.mehow.laboratory.Laboratory -import io.mehow.laboratory.inspector.DeprecationAlignment.Bottom -import io.mehow.laboratory.inspector.DeprecationAlignment.Regular -import io.mehow.laboratory.inspector.DeprecationPhenotype.Hide -import io.mehow.laboratory.inspector.DeprecationPhenotype.Show -import io.mehow.laboratory.inspector.DeprecationPhenotype.Strikethrough -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.first -import kotlin.DeprecationLevel.ERROR -import kotlin.DeprecationLevel.HIDDEN -import kotlin.DeprecationLevel.WARNING - -internal class InspectorViewModelDeprecationSpec : DescribeSpec({ - setMainDispatcher() - - describe("deprecated feature flags") { - it("can be filtered out") { - val viewModel = InspectorViewModel(DeprecationHandler( - phenotypeSelector = { Hide }, - alignmentSelector = { Regular }, - )) - - val featureNames = viewModel.sectionFlow().first().map(FeatureUiModel::name) - - featureNames shouldContainExactly listOf("NotDeprecated") - } - - it("can be struck through") { - val viewModel = InspectorViewModel(DeprecationHandler( - phenotypeSelector = { Strikethrough }, - alignmentSelector = { Regular }, - )) - - val featureNames = viewModel.sectionFlow().first().map { it.name to it.deprecationPhenotype } - - featureNames shouldContainExactly listOf( - "DeprecatedError" to Strikethrough, - "DeprecatedHidden" to Strikethrough, - "DeprecatedWarning" to Strikethrough, - "NotDeprecated" to null, - ) - } - - it("can be shown") { - val viewModel = InspectorViewModel(DeprecationHandler( - phenotypeSelector = { Show }, - alignmentSelector = { Regular }, - )) - - val featureNames = viewModel.sectionFlow().first().map { it.name to it.deprecationPhenotype } - - featureNames shouldContainExactly listOf( - "DeprecatedError" to Show, - "DeprecatedHidden" to Show, - "DeprecatedWarning" to Show, - "NotDeprecated" to null, - ) - } - - it("can be moved to bottom") { - val viewModel = InspectorViewModel(DeprecationHandler( - phenotypeSelector = { Show }, - alignmentSelector = { Bottom }, - )) - - val featureNames = viewModel.sectionFlow().first().map { it.name to it.deprecationPhenotype } - - featureNames shouldContainExactly listOf( - "NotDeprecated" to null, - "DeprecatedError" to Show, - "DeprecatedHidden" to Show, - "DeprecatedWarning" to Show, - ) - } - - it("can be selected based on deprecation level") { - val viewModel = InspectorViewModel(DeprecationHandler( - phenotypeSelector = { if (it == WARNING) Strikethrough else Show }, - alignmentSelector = { if (it != WARNING) Bottom else Regular }, - )) - - val featureNames = viewModel.sectionFlow().first().map { it.name to it.deprecationPhenotype } - - featureNames shouldContainExactly listOf( - "DeprecatedWarning" to Strikethrough, - "NotDeprecated" to null, - "DeprecatedError" to Show, - "DeprecatedHidden" to Show, - ) - } - } -}) - -private object DeprecatedFeatureFactory : FeatureFactory { - override fun create(): Set>> { - @Suppress("UNCHECKED_CAST") - return setOf( - Class.forName("io.mehow.laboratory.inspector.DeprecatedWarning"), - Class.forName("io.mehow.laboratory.inspector.DeprecatedError"), - Class.forName("io.mehow.laboratory.inspector.DeprecatedHidden"), - Class.forName("io.mehow.laboratory.inspector.NotDeprecated"), - ) as Set>> - } -} - -@Deprecated("", level = WARNING) -private enum class DeprecatedWarning : Feature<@Suppress("DEPRECATION") DeprecatedWarning> { - Option, - ; - - @Suppress("DEPRECATION") - override val defaultOption: DeprecatedWarning - get() = Option -} - -@Deprecated("message", level = ERROR) -private enum class DeprecatedError : Feature<@Suppress("DEPRECATION_ERROR") DeprecatedError> { - Option, - ; - - @Suppress("DEPRECATION_ERROR") - override val defaultOption: DeprecatedError - get() = Option -} - -@Deprecated("", level = HIDDEN) -private enum class DeprecatedHidden : Feature<@Suppress("DEPRECATION_ERROR") DeprecatedHidden> { - Option, - ; - - @Suppress("DEPRECATION_ERROR") - override val defaultOption: DeprecatedHidden - get() = Option -} - -private enum class NotDeprecated : Feature { - Option, - ; - - override val defaultOption: NotDeprecated - get() = Option -} - -@Suppress("TestFunctionName") -private fun InspectorViewModel( - deprecationHandler: DeprecationHandler, -) = InspectorViewModel( - Laboratory.inMemory(), - emptyFlow(), - DeprecatedFeatureFactory, - deprecationHandler, -) diff --git a/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelFeatureSpec.kt b/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelFeatureSpec.kt deleted file mode 100644 index fc9c68691..000000000 --- a/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelFeatureSpec.kt +++ /dev/null @@ -1,321 +0,0 @@ -package io.mehow.laboratory.inspector - -import app.cash.turbine.test -import io.kotest.assertions.fail -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.collections.shouldContainExactly -import io.kotest.matchers.collections.shouldNotContain -import io.mehow.laboratory.DefaultOptionFactory -import io.mehow.laboratory.Feature -import io.mehow.laboratory.FeatureFactory -import io.mehow.laboratory.FeatureStorage -import io.mehow.laboratory.Laboratory -import io.mehow.laboratory.inspector.TextToken.Link -import io.mehow.laboratory.inspector.TextToken.Regular -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.first - -internal class InspectorViewModelFeatureSpec : DescribeSpec({ - setMainDispatcher() - - describe("view model") { - it("filters empty feature flag groups") { - val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) - - val featureNames = viewModel.sectionFlow().first().map(FeatureUiModel::name) - - featureNames shouldNotContain "Empty" - } - - it("orders feature flag groups by name") { - val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) - - val featureNames = viewModel.sectionFlow().first().map(FeatureUiModel::name) - - featureNames shouldContainExactly listOf("First", "Second") - } - - it("does not order feature flag options") { - val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) - - val features = viewModel.sectionFlow().first() - .map(FeatureUiModel::models) - .map { models -> models.map(OptionUiModel::option) } - - features[0] shouldContainExactly listOf(First.C, First.B, First.A) - features[1] shouldContainExactly listOf(Second.B, Second.C, Second.A) - } - - it("marks first feature flag option as selected by default") { - val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) - - viewModel.observeSelectedFeatures().first() shouldContainExactly listOf(First.C, Second.B) - } - - it("marks saved feature flag options as selected") { - val laboratory = Laboratory.inMemory().apply { - setOption(First.A) - setOption(Second.C) - } - - val viewModel = InspectorViewModel(laboratory, NoSourceFeatureFactory) - - viewModel.observeSelectedFeatures().first() shouldContainExactly listOf(First.A, Second.C) - } - - it("selects feature flag options") { - val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) - - viewModel.selectFeature(First.B) - viewModel.selectFeature(Second.A) - - viewModel.observeSelectedFeatures().first() shouldContainExactly listOf(First.B, Second.A) - } - - it("observes feature flag changes") { - val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) - - viewModel.observeSelectedFeatures().test { - awaitItem() shouldContainExactly listOf(First.C, Second.B) - - viewModel.selectFeature(First.B) - awaitItem() shouldContainExactly listOf(First.B, Second.B) - - viewModel.selectFeature(Second.C) - awaitItem() shouldContainExactly listOf(First.B, Second.C) - - cancel() - } - } - - it("observes source changes") { - val viewModel = InspectorViewModel(Laboratory.inMemory(), AllFeatureFactory) - - viewModel.observeSelectedFeaturesAndSources().test { - awaitItem() shouldContainExactly listOf( - First.C to null, - Second.B to null, - Sourced.A to Sourced.Source.Local - ) - - viewModel.selectFeature(Sourced.Source.Remote) - - awaitItem() shouldContainExactly listOf( - First.C to null, - Second.B to null, - Sourced.A to Sourced.Source.Remote - ) - } - } - - it("resets feature flags to default options declared in factory") { - val defaultOptionFactory = object : DefaultOptionFactory { - override fun > create(feature: T): Feature<*>? = when (feature) { - is First -> First.A - is Second -> Second.A - else -> null - } - } - val laboratory = Laboratory.builder() - .featureStorage(FeatureStorage.inMemory()) - .defaultOptionFactory(defaultOptionFactory) - .build() - val viewModel = InspectorViewModel(laboratory, NoSourceFeatureFactory) - - viewModel.observeSelectedFeatures().test { - awaitItem() shouldContainExactly listOf(First.A, Second.A) - - viewModel.selectFeature(First.B) - awaitItem() shouldContainExactly listOf(First.B, Second.A) - - viewModel.selectFeature(Second.B) - awaitItem() shouldContainExactly listOf(First.B, Second.B) - - laboratory.clear() - awaitItemEventually { it shouldContainExactly listOf(First.A, Second.A) } - - cancel() - } - } - - it("uses text tokens for feature flag description") { - val viewModel = InspectorViewModel(Laboratory.inMemory(), NoSourceFeatureFactory) - - val descriptions = viewModel.sectionFlow().first().map(FeatureUiModel::description) - - descriptions shouldContainExactly listOf( - listOf( - Regular("Description with a "), - Link("link", "https://mehow.io"), - ), - listOf( - Regular("Description without a link"), - ), - ) - } - - it("observers feature flags supervision") { - val viewModel = InspectorViewModel(Laboratory.inMemory(), SupervisedFeatureFactory) - - viewModel.observeSelectedFeaturesAndEnabledState().test { - awaitItem() shouldContainExactly listOf( - Child.A to false, - Parent.Disabled to true, - ) - - viewModel.selectFeature(Parent.Enabled) - awaitItemEventually { - it shouldContainExactly listOf( - Child.A to true, - Parent.Enabled to true, - ) - } - - viewModel.selectFeature(Child.B) - awaitItem() shouldContainExactly listOf( - Child.B to true, - Parent.Enabled to true, - ) - - viewModel.selectFeature(Parent.Disabled) - awaitItemEventually { - it shouldContainExactly listOf( - Child.A to false, - Parent.Disabled to true, - ) - } - - cancel() - } - } - - it("includes supervised features to options") { - val viewModel = InspectorViewModel(Laboratory.inMemory(), SupervisedFeatureFactory) - - viewModel.supervisedFeaturesFlow().first() shouldContainExactly listOf( - Child.A to emptyList(), - Child.B to emptyList(), - Parent.Enabled to listOf(Child::class.java), - Parent.Disabled to emptyList(), - ) - } - - it("includes supervised features to options from different sections") { - val parentFactory = object : FeatureFactory { - override fun create(): Set>> = setOf(Parent::class.java) - } - val childFactory = object : FeatureFactory { - override fun create(): Set>> = setOf(Child::class.java) - } - - val viewModel = InspectorViewModel( - Laboratory.inMemory(), - searchQueries = emptyFlow(), - mapOf("Parent" to parentFactory, "Child" to childFactory), - DeprecationHandler({ fail("Unexpected call") }, { fail("Unexpected call") }), - Dispatchers.Unconfined, - ) - - viewModel.supervisedFeaturesFlow("Parent").first() shouldContainExactly listOf( - Parent.Enabled to listOf(Child::class.java), - Parent.Disabled to emptyList(), - ) - } - } -}) - -private object NoSourceFeatureFactory : FeatureFactory { - override fun create(): Set>> = setOf( - Second::class.java, - First::class.java, - Empty::class.java, - ) -} - -private object SourcedFeatureFactory : FeatureFactory { - override fun create(): Set>> = setOf(Sourced::class.java) -} - -private object AllFeatureFactory : FeatureFactory { - override fun create() = NoSourceFeatureFactory.create() + SourcedFeatureFactory.create() -} - -private object SupervisedFeatureFactory : FeatureFactory { - override fun create(): Set>> = setOf( - Parent::class.java, - Child::class.java, - ) -} - -private enum class First : Feature { - C, - B, - A, - ; - - override val defaultOption get() = C - - override val description = "Description with a [link](https://mehow.io)" -} - -private enum class Second : Feature { - B, - C, - A, - ; - - override val defaultOption get() = B - - override val description = "Description without a link" -} - -private enum class Empty : Feature - -private enum class Sourced : Feature { - A, - B, - C, - ; - - override val defaultOption get() = A - - override val source = Source::class.java - - enum class Source : Feature { - Local, - Remote, - ; - - override val defaultOption get() = Local - } -} - -private enum class Parent : Feature { - Enabled, - Disabled, - ; - - override val defaultOption get() = Disabled -} - -private enum class Child : Feature { - A, - B, - ; - - override val defaultOption get() = A - - override val supervisorOption get() = Parent.Enabled -} - -@Suppress("TestFunctionName") -private fun InspectorViewModel( - laboratory: Laboratory, - factory: FeatureFactory, -) = InspectorViewModel( - laboratory, - emptyFlow(), - factory, - DeprecationHandler({ fail("Unexpected call") }, { fail("Unexpected call") }), -) diff --git a/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelFilterSpec.kt b/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelFilterSpec.kt deleted file mode 100644 index fb6271853..000000000 --- a/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelFilterSpec.kt +++ /dev/null @@ -1,255 +0,0 @@ -package io.mehow.laboratory.inspector - -import app.cash.turbine.FlowTurbine -import app.cash.turbine.test -import io.kotest.assertions.fail -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.collections.shouldContainExactly -import io.kotest.property.Arb -import io.kotest.property.arbitrary.stringPattern -import io.kotest.property.checkAll -import io.mehow.laboratory.Feature -import io.mehow.laboratory.FeatureFactory -import io.mehow.laboratory.Laboratory -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlin.reflect.KClass - -internal class InspectorViewModelFilterSpec : DescribeSpec({ - setMainDispatcher() - - describe("feature flags filtering") { - it("emits all feature flags for no search terms") { - val searchFlow = MutableSharedFlow() - InspectorViewModel(searchFlow).observeFeatureClasses().test { - expectAllFeatureFlags() - - cancel() - } - } - - it("emits all feature flags for blank search terms") { - checkAll(Arb.stringPattern("([ ]{0,10})")) { query -> - val searchFlow = MutableSharedFlow() - InspectorViewModel(searchFlow).observeFeatureClasses().test { - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery(query)) - expectNoEvents() - - cancel() - } - } - } - - it("finds feature flags by their exact name") { - val searchFlow = MutableSharedFlow() - InspectorViewModel(searchFlow).observeFeatureClasses().test { - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery("RegularNameFeature")) - awaitItem() shouldContainExactly listOf(RegularNameFeature::class) - - searchFlow.emit(SearchQuery("Numbered1NameFeature")) - awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class) - - searchFlow.emit(SearchQuery("SourcedFeature")) - awaitItem() shouldContainExactly listOf(SourcedFeature::class) - - cancel() - } - } - - it("finds feature flags by split name parts") { - val searchFlow = MutableSharedFlow() - InspectorViewModel(searchFlow).observeFeatureClasses().test { - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery("Name Feature")) - awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class, - RegularNameFeature::class) - - searchFlow.emit(SearchQuery("Numbered 1Name Feature")) - awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class) - - cancel() - } - } - - // Find out why checkAll(Arb.stringPattern("[!_?@*]{10,}")) { query -> } can fail on CI. - // It fails randomly with a timeout on a second event. Re-using generator seed does not help locally. - // Pattern in generator also doesn't matter as long as it produces valid input for the test. - it("finds no feature flags for no matches") { - val searchFlow = MutableSharedFlow() - InspectorViewModel(searchFlow).observeFeatureClasses().test { - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery("???")) - awaitItem() shouldContainExactly emptyList() - - cancel() - } - } - - it("finds feature flags by their options") { - val searchFlow = MutableSharedFlow() - InspectorViewModel(searchFlow).observeFeatureClasses().test { - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery("Disabled")) - awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class, - RegularNameFeature::class) - - searchFlow.emit(SearchQuery("Howdy")) - awaitItem() shouldContainExactly listOf(SourcedFeature::class) - - cancel() - } - } - - it("finds feature flags by their sources") { - val searchFlow = MutableSharedFlow() - InspectorViewModel(searchFlow).observeFeatureClasses().test { - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery("Remote")) - awaitItem() shouldContainExactly listOf(SourcedFeature::class) - - cancel() - } - } - - it("finds feature flags by partial matches") { - val searchFlow = MutableSharedFlow() - InspectorViewModel(searchFlow).observeFeatureClasses().test { - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery("me ture")) - awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class, - RegularNameFeature::class) - - searchFlow.emit(SearchQuery("ature")) - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery("ed ture")) - awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class, SourcedFeature::class) - - searchFlow.emit(SearchQuery("cal")) - awaitItem() shouldContainExactly listOf(SourcedFeature::class) - - cancel() - } - } - - it("finds feature flags by ordered input") { - val searchFlow = MutableSharedFlow() - InspectorViewModel(searchFlow).observeFeatureClasses().test { - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery("Enabled Disabled")) - awaitItem() shouldContainExactly listOf(RegularNameFeature::class) - - searchFlow.emit(SearchQuery("Disabled Enabled")) - awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class) - - cancel() - } - } - - it("ignores capitalization during search") { - val searchFlow = MutableSharedFlow() - InspectorViewModel(searchFlow).observeFeatureClasses().test { - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery("enabled")) - awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class, - RegularNameFeature::class) - - searchFlow.emit(SearchQuery("feature")) - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery("local")) - awaitItem() shouldContainExactly listOf(SourcedFeature::class) - - cancel() - } - } - - it("finds feature flags by partial, non-split inner search") { - val searchFlow = MutableSharedFlow() - InspectorViewModel(searchFlow).observeFeatureClasses().test { - expectAllFeatureFlags() - - searchFlow.emit(SearchQuery("arnamefea")) - awaitItem() shouldContainExactly listOf(RegularNameFeature::class) - - searchFlow.emit(SearchQuery("d1na")) - awaitItem() shouldContainExactly listOf(Numbered1NameFeature::class) - - cancel() - } - } - } -}) - -private object SearchFeatureFactory : FeatureFactory { - override fun create(): Set>> = setOf( - RegularNameFeature::class.java, - Numbered1NameFeature::class.java, - SourcedFeature::class.java, - ) -} - -private enum class RegularNameFeature : Feature { - Enabled, - Disabled, - ; - - override val defaultOption get() = Disabled -} - -private enum class Numbered1NameFeature : Feature { - Disabled, - Enabled, - ; - - override val defaultOption get() = Disabled -} - -private enum class SourcedFeature : Feature { - Howdy, - There, - Partner, - ; - - override val defaultOption get() = Howdy - - @Suppress("UNCHECKED_CAST") - override val source = Source::class.java as Class> - - enum class Source : Feature { - Local, - Remote, - ; - - override val defaultOption get() = Local - } -} - -@Suppress("TestFunctionName") -private fun InspectorViewModel( - searchFlow: Flow, -) = InspectorViewModel( - Laboratory.inMemory(), - searchFlow, - SearchFeatureFactory, - DeprecationHandler({ fail("Unexpected call") }, { fail("Unexpected call") }), -) - -private suspend fun FlowTurbine>>>.expectAllFeatureFlags() { - awaitItem() shouldContainExactly listOf( - Numbered1NameFeature::class, - RegularNameFeature::class, - SourcedFeature::class, - ) -} diff --git a/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelNavigationSpec.kt b/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelNavigationSpec.kt deleted file mode 100644 index 4071f8e20..000000000 --- a/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelNavigationSpec.kt +++ /dev/null @@ -1,128 +0,0 @@ -package io.mehow.laboratory.inspector - -import app.cash.turbine.test -import io.kotest.assertions.fail -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.collections.shouldBeIn -import io.kotest.matchers.shouldBe -import io.mehow.laboratory.Feature -import io.mehow.laboratory.FeatureFactory -import io.mehow.laboratory.Laboratory -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flowOf - -@Suppress("UNCHECKED_CAST") -internal class InspectorViewModelNavigationSpec : DescribeSpec({ - setMainDispatcher() - - describe("feature flag coordinates") { - it("are found") { - val viewModel = InspectorViewModel() - - viewModel.goTo(SectionOneFeatureA::class.java as Class>) shouldBe FeatureCoordinates(0, 0) - viewModel.goTo(SectionOneFeatureB::class.java as Class>) shouldBe FeatureCoordinates(0, 1) - viewModel.goTo(SectionTwoFeature::class.java as Class>) shouldBe FeatureCoordinates(1, 0) - } - - it("are not found when feature is not registered") { - val viewModel = InspectorViewModel() - - viewModel.goTo(UnregisteredFeature::class.java as Class>) shouldBe null - } - - it("are not found when feature is filtered") { - val viewModel = InspectorViewModel(searchFlow = flowOf(SearchQuery("Foo"))) - - viewModel.goTo(SectionOneFeatureA::class.java as Class>) shouldBe null - } - - it("are found when feature is registerd twice") { - val viewModel = InspectorViewModel(mapOf("A1" to SectionAFactory, "A2" to SectionAFactory)) - - viewModel.goTo(SectionOneFeatureA::class.java as Class>) shouldBeIn listOf( - FeatureCoordinates(0, 0), - FeatureCoordinates(1, 0), - ) - } - - it("can be observed") { - val viewModel = InspectorViewModel() - - viewModel.featureCoordinatesFlow.test { - expectNoEvents() - - viewModel.goTo(SectionOneFeatureA::class.java as Class>) - awaitItem() shouldBe FeatureCoordinates(0, 0) - - cancel() - } - } - - it("do not cache emissions") { - val viewModel = InspectorViewModel() - - viewModel.goTo(SectionOneFeatureA::class.java as Class>) - - viewModel.featureCoordinatesFlow.test { - cancel() - } - } - } -}) - -private object SectionAFactory : FeatureFactory { - override fun create(): Set>> = setOf( - SectionOneFeatureA::class.java, - SectionOneFeatureB::class.java, - ) -} - -private object SectionBFactory : FeatureFactory { - override fun create(): Set>> = setOf(SectionTwoFeature::class.java) -} - -private enum class SectionOneFeatureA : Feature { - Option, - ; - - override val defaultOption: SectionOneFeatureA - get() = Option -} - -private enum class SectionOneFeatureB : Feature { - Option, - ; - - override val defaultOption: SectionOneFeatureB - get() = Option -} - -private enum class SectionTwoFeature : Feature { - Option, - ; - - override val defaultOption: SectionTwoFeature - get() = Option -} - -private enum class UnregisteredFeature : Feature { - Option, - ; - - override val defaultOption: UnregisteredFeature - get() = Option -} - -@Suppress("TestFunctionName") -private fun InspectorViewModel( - featureFactories: Map = mapOf("A" to SectionAFactory, "B" to SectionBFactory), - searchFlow: Flow = emptyFlow(), -) = InspectorViewModel( - Laboratory.inMemory(), - searchFlow, - featureFactories, - DeprecationHandler({ fail("Unexpected call") }, { fail("Unexpected call") }), - Dispatchers.Unconfined, -) diff --git a/library/inspector/src/test/java/io/mehow/laboratory/inspector/SearchViewModelSpec.kt b/library/inspector/src/test/java/io/mehow/laboratory/inspector/SearchViewModelSpec.kt deleted file mode 100644 index f8f0b7d42..000000000 --- a/library/inspector/src/test/java/io/mehow/laboratory/inspector/SearchViewModelSpec.kt +++ /dev/null @@ -1,90 +0,0 @@ -package io.mehow.laboratory.inspector - -import app.cash.turbine.FlowTurbine -import app.cash.turbine.test -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.shouldBe -import io.mehow.laboratory.inspector.SearchMode.Active -import io.mehow.laboratory.inspector.SearchMode.Idle -import io.mehow.laboratory.inspector.SearchViewModel.Event.CloseSearch -import io.mehow.laboratory.inspector.SearchViewModel.Event.OpenSearch -import io.mehow.laboratory.inspector.SearchViewModel.Event.UpdateQuery -import io.mehow.laboratory.inspector.SearchViewModel.UiModel - -internal class SearchViewModelSpec : DescribeSpec({ - setMainDispatcher() - - describe("feature flag searching") { - it("has initial idle state") { - SearchViewModel().uiModels.test { - expectIdleModel() - - cancel() - } - } - - it("can be opened") { - val viewModel = SearchViewModel() - viewModel.uiModels.test { - expectIdleModel() - - viewModel.sendEvent(OpenSearch) - awaitItem() shouldBe UiModel(Active, SearchQuery.Empty) - - cancel() - } - } - - it("updates search queries in active mode") { - val viewModel = SearchViewModel() - viewModel.uiModels.test { - expectIdleModel() - - viewModel.sendEvent(OpenSearch) - awaitItem() - - viewModel.sendEvent(UpdateQuery("Hello")) - awaitItem() shouldBe UiModel(Active, SearchQuery("Hello")) - - viewModel.sendEvent(UpdateQuery("World")) - awaitItem() shouldBe UiModel(Active, SearchQuery("World")) - - cancel() - } - } - - it("ignores queries in idle mode") { - val viewModel = SearchViewModel() - viewModel.uiModels.test { - expectIdleModel() - - viewModel.sendEvent(UpdateQuery("Hello")) - expectNoEvents() - - cancel() - } - } - - it("clears queries when search is closed") { - val viewModel = SearchViewModel() - viewModel.uiModels.test { - expectIdleModel() - - viewModel.sendEvent(OpenSearch) - awaitItem() - - viewModel.sendEvent(UpdateQuery("Hello")) - awaitItem() shouldBe UiModel(Active, SearchQuery("Hello")) - - viewModel.sendEvent(CloseSearch) - expectIdleModel() - - cancel() - } - } - } -}) - -private suspend fun FlowTurbine.expectIdleModel() { - awaitItem() shouldBe UiModel(Idle, SearchQuery.Empty) -} diff --git a/library/inspector/src/test/java/io/mehow/laboratory/inspector/TextTokenSpec.kt b/library/inspector/src/test/java/io/mehow/laboratory/inspector/TextTokenSpec.kt deleted file mode 100644 index 4f0771710..000000000 --- a/library/inspector/src/test/java/io/mehow/laboratory/inspector/TextTokenSpec.kt +++ /dev/null @@ -1,77 +0,0 @@ -package io.mehow.laboratory.inspector - -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.data.blocking.forAll -import io.kotest.data.row -import io.kotest.matchers.collections.shouldBeEmpty -import io.kotest.matchers.collections.shouldContainExactly -import io.mehow.laboratory.inspector.TextToken.Link -import io.mehow.laboratory.inspector.TextToken.Regular - -internal class TextTokenSpec : DescribeSpec({ - describe("text tokens") { - it("can be empty") { - "".tokenize().shouldBeEmpty() - } - - it("can be blank") { - " ".tokenize() shouldContainExactly listOf(Regular(" ")) - } - - it("can have regular text") { - "Hello".tokenize() shouldContainExactly listOf(Regular("Hello")) - } - - it("can have link") { - "[Hello](https://mehow.io)".tokenize() shouldContainExactly listOf(Link("Hello", "https://mehow.io")) - } - - it("can start with regular text followed by a link") { - "Hello [there](https://github.com/MiSikora/)".tokenize() shouldContainExactly listOf( - Regular("Hello "), - Link("there", "https://github.com/MiSikora/"), - ) - } - - it("can start with a link followed by a regular text") { - "[General](https://google.com) Kenobi".tokenize() shouldContainExactly listOf( - Link("General", "https://google.com"), - Regular(" Kenobi"), - ) - } - - it("can have multiple regular texts and links") { - val input = "Hello [there](https://github.com)… [General](https://sample.org) Kenobi" - input.tokenize() shouldContainExactly listOf( - Regular("Hello "), - Link("there", "https://github.com"), - Regular("… "), - Link("General", "https://sample.org"), - Regular(" Kenobi"), - ) - } - - it("can have multiple consecutive links") { - val input = "[One,](https://one.com)[ Two](https://two.com)[, Three](https://three.com)" - input.tokenize() shouldContainExactly listOf( - Link("One,", "https://one.com"), - Link(" Two", "https://two.com"), - Link(", Three", "https://three.com"), - ) - } - - it("does not use malformed link syntax") { - forAll( - row("[One[](https://one.com)"), - row("[One](https://one.com()"), - row("[](https://one.com"), - row("[One]()"), - row("[One]((https://one.com)"), - row("[O]ne](https://one.com)"), - row("[One](h(ttps://one.com)"), - ) { - it.tokenize() shouldContainExactly listOf(Regular(it)) - } - } - } -}) \ No newline at end of file diff --git a/library/laboratory/build.gradle b/library/laboratory/build.gradle deleted file mode 100644 index 62d65163d..000000000 --- a/library/laboratory/build.gradle +++ /dev/null @@ -1,16 +0,0 @@ -plugins { - id "org.jetbrains.kotlin.jvm" -} - -test.useJUnitPlatform() - -dependencies { - api libs.kotlin.x.coroutinesCore - - testImplementation libs.kotest.runnerJunit5 - testImplementation libs.kotest.assertions - testImplementation libs.turbine -} - -apply from: "$rootDir/gradle/dokka-config.gradle" -apply from: "$rootDir/gradle/gradle-mvn-push.gradle" diff --git a/library/laboratory/src/test/java/io/mehow/laboratory/DefaultOptionFactorySpec.kt b/library/laboratory/src/test/java/io/mehow/laboratory/DefaultOptionFactorySpec.kt deleted file mode 100644 index 109518350..000000000 --- a/library/laboratory/src/test/java/io/mehow/laboratory/DefaultOptionFactorySpec.kt +++ /dev/null @@ -1,39 +0,0 @@ -package io.mehow.laboratory - -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.shouldBe - -internal class DefaultOptionFactorySpec : DescribeSpec({ - val firstFactory = object : DefaultOptionFactory { - override fun > create(feature: T): Feature<*>? = when (feature) { - is FirstFeature -> FirstFeature.B - else -> null - } - } - - val secondFactory = object : DefaultOptionFactory { - override fun > create(feature: T): Feature<*>? = when (feature) { - is FirstFeature -> FirstFeature.C - is SecondFeature -> SecondFeature.C - else -> null - } - } - - describe("default option factory") { - context("when added to another") { - val factory = (firstFactory + secondFactory) - - it("uses self as a producer if available in self") { - factory.create(FirstFeature.A) shouldBe FirstFeature.B - } - - it("uses another factory as a producer when unavailable in self") { - factory.create(SecondFeature.A) shouldBe SecondFeature.C - } - - it("uses no producer feature flag is unknown to any of factories") { - factory.create(UnsourcedFeature.A) shouldBe null - } - } - } -}) diff --git a/library/laboratory/src/test/java/io/mehow/laboratory/FeatureFactorySpec.kt b/library/laboratory/src/test/java/io/mehow/laboratory/FeatureFactorySpec.kt deleted file mode 100644 index 50a73ab78..000000000 --- a/library/laboratory/src/test/java/io/mehow/laboratory/FeatureFactorySpec.kt +++ /dev/null @@ -1,24 +0,0 @@ -package io.mehow.laboratory - -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder - -internal class FeatureFactorySpec : DescribeSpec({ - val firstFactory = object : FeatureFactory { - override fun create(): Set>> = setOf(FirstFeature::class.java) - } - - val secondFactory = object : FeatureFactory { - override fun create(): Set>> = setOf(OtherFeature::class.java) - } - - describe("feature factory") { - it("when added to another factory returns sum of available features") { - (firstFactory + secondFactory).create() shouldContainExactlyInAnyOrder setOf( - FirstFeature::class.java, - OtherFeature::class.java, - ) - } - } -}) - diff --git a/library/laboratory/src/test/java/io/mehow/laboratory/Features.kt b/library/laboratory/src/test/java/io/mehow/laboratory/Features.kt deleted file mode 100644 index 37e2f21ef..000000000 --- a/library/laboratory/src/test/java/io/mehow/laboratory/Features.kt +++ /dev/null @@ -1,120 +0,0 @@ -package io.mehow.laboratory - -internal enum class FirstFeature : Feature { - A, - B, - C, - ; - - override val defaultOption get() = A - - override val source = Source::class.java - - enum class Source : Feature { - Local, - RemoteA, - ; - - override val defaultOption get() = Local - } -} - -internal enum class SecondFeature : Feature { - A, - B, - C, - ; - - override val defaultOption get() = A - - override val source = Source::class.java - - enum class Source : Feature { - Local, - RemoteA, - RemoteB, - ; - - override val defaultOption get() = RemoteB - } -} - -internal enum class EmptySourceFeature : Feature { - A, - B, - C, - ; - - override val defaultOption get() = A - - override val source = Source::class.java - - internal enum class Source : Feature -} - -internal enum class UnsourcedFeature : Feature { - A, - B, - C, - ; - - override val defaultOption get() = A -} - -internal enum class SomeFeature : Feature { - A, - B, - C, - ; - - override val defaultOption get() = B -} - -internal enum class OtherFeature : Feature { - A, - B, - C, - ; - - override val defaultOption get() = A -} - -internal enum class NoValuesFeature : Feature - -internal enum class GrandParentFeature : Feature { - A, - B, - ; - - override val defaultOption get() = A -} - -internal enum class ParentFeature : Feature { - A, - B, - ; - - override val defaultOption get() = A - - override val supervisorOption = GrandParentFeature.A -} - -internal enum class FirstChildFeature : Feature { - A, - B, - ; - - override val defaultOption get() = A - - override val supervisorOption = ParentFeature.A -} - -internal enum class SecondChildFeature : Feature { - A, - B, - ; - - override val defaultOption get() = A - - override val supervisorOption = ParentFeature.B -} diff --git a/library/laboratory/src/test/java/io/mehow/laboratory/LaboratorySpec.kt b/library/laboratory/src/test/java/io/mehow/laboratory/LaboratorySpec.kt deleted file mode 100644 index 517b01461..000000000 --- a/library/laboratory/src/test/java/io/mehow/laboratory/LaboratorySpec.kt +++ /dev/null @@ -1,195 +0,0 @@ -package io.mehow.laboratory - -import app.cash.turbine.test -import io.kotest.assertions.fail -import io.kotest.assertions.throwables.shouldThrowExactly -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.booleans.shouldBeTrue -import io.kotest.matchers.shouldBe -import io.kotest.matchers.throwable.shouldHaveMessage -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf - -internal class LaboratorySpec : DescribeSpec({ - val throwingStorage = object : FeatureStorage { - override fun observeFeatureName(feature: Class>) = fail("Unexpected call") - override suspend fun getFeatureName(feature: Class>) = fail("Unexpected call") - override suspend fun setOptions(vararg options: Feature<*>) = fail("Unexpected call") - override suspend fun clear() = fail("Unexpected call") - } - - val nullStorage = object : FeatureStorage { - override fun observeFeatureName(feature: Class>): Flow = flowOf(null) - override suspend fun getFeatureName(feature: Class>): String? = null - override suspend fun setOptions(vararg options: Feature<*>) = fail("Unexpected call") - override suspend fun clear() = fail("Unexpected call") - } - - val emptyStorage = object : FeatureStorage { - override fun observeFeatureName(feature: Class>) = flowOf("") - override suspend fun getFeatureName(feature: Class>) = "" - override suspend fun setOptions(vararg options: Feature<*>) = fail("Unexpected call") - override suspend fun clear() = fail("Unexpected call") - } - - describe("laboratory") { - it("cannot use features with no values") { - val laboratory = Laboratory.create(throwingStorage) - - shouldThrowExactly { - laboratory.experiment() - } shouldHaveMessage "io.mehow.laboratory.NoValuesFeature must have at least one option" - } - - context("for feature with single default") { - it("uses declared default value") { - val laboratory = Laboratory.create(nullStorage) - - laboratory.experiment() shouldBe SomeFeature.B - } - - it("uses declared default value if no match is found") { - val laboratory = Laboratory.create(emptyStorage) - - laboratory.experiment() shouldBe SomeFeature.B - } - } - - context("checking feature value") { - it("returns false for non-default value") { - val laboratory = Laboratory.create(nullStorage) - - laboratory.experimentIs(SomeFeature.A) shouldBe false - } - - it("returns true for default value") { - val laboratory = Laboratory.create(nullStorage) - - laboratory.experimentIs(SomeFeature.B) shouldBe true - } - } - - context("reading and writing feature flag") { - val feature = SomeFeature::class.java - - it("uses value saved in a storage") { - val storage = FeatureStorage.inMemory() - val laboratory = Laboratory.create(storage) - - for (value in feature.options) { - storage.setOption(value) - - laboratory.experiment(feature) shouldBe value - } - } - - it("can directly change the feature") { - val laboratory = Laboratory.inMemory() - - for (value in feature.options) { - laboratory.setOption(value) - - laboratory.experiment(feature) shouldBe value - } - } - } - - it("observes feature changes") { - val laboratory = Laboratory.inMemory() - - laboratory.observe().test { - awaitItem() shouldBe SomeFeature.B - - laboratory.setOption(SomeFeature.A) - awaitItem() shouldBe SomeFeature.A - - laboratory.setOption(SomeFeature.C) - awaitItem() shouldBe SomeFeature.C - - laboratory.setOption(SomeFeature.C) - expectNoEvents() - - laboratory.setOption(SomeFeature.B) - awaitItem() shouldBe SomeFeature.B - - cancel() - } - } - - it("clears all feature flags") { - val laboratory = Laboratory.inMemory() - - laboratory.setOption(SomeFeature.A) - laboratory.setOption(OtherFeature.B) - laboratory.clear() - - laboratory.experimentIs(SomeFeature.B) - laboratory.experimentIs(OtherFeature.A) - } - } - - describe("in memory laboratory") { - it("is not shared across instances") { - val firstLaboratory = Laboratory.inMemory() - val secondLaboratory = Laboratory.inMemory() - - firstLaboratory.setOption(SomeFeature.A) - firstLaboratory.experiment() shouldBe SomeFeature.A - secondLaboratory.experiment() shouldBe SomeFeature.B - - secondLaboratory.setOption(SomeFeature.C) - firstLaboratory.experiment() shouldBe SomeFeature.A - secondLaboratory.experiment() shouldBe SomeFeature.C - } - } - - describe("default options factory") { - val factory = object : DefaultOptionFactory { - override fun > create(feature: T) = when (feature) { - is SomeFeature -> OtherFeature.C // Intentional wrong class for test - is OtherFeature -> OtherFeature.C - else -> null - } - } - - it("changes default option") { - val laboratory = Laboratory.builder() - .featureStorage(FeatureStorage.inMemory()) - .defaultOptionFactory(factory) - .build() - laboratory.experimentIs(OtherFeature.C).shouldBeTrue() - } - - it("does not affect stored option") { - val laboratory = Laboratory.builder() - .featureStorage(FeatureStorage.inMemory()) - .defaultOptionFactory(factory) - .build() - laboratory.setOption(OtherFeature.B) - laboratory.experimentIs(OtherFeature.B).shouldBeTrue() - } - - it("changes default option when feature flag is observed") { - val laboratory = Laboratory.builder() - .featureStorage(FeatureStorage.inMemory()) - .defaultOptionFactory(factory) - .build() - laboratory.observe().test { - awaitItem() shouldBe OtherFeature.C - - laboratory.setOption(OtherFeature.B) - awaitItem() shouldBe OtherFeature.B - } - } - - it("fails when provided default option has wrong type") { - val laboratory = Laboratory.builder() - .featureStorage(FeatureStorage.inMemory()) - .defaultOptionFactory(factory) - .build() - shouldThrowExactly { - laboratory.experiment() - } shouldHaveMessage "Tried to use OtherFeature.C as a default option for io.mehow.laboratory.SomeFeature" - } - } -}) diff --git a/library/laboratory/src/test/java/io/mehow/laboratory/OptionFactorySpec.kt b/library/laboratory/src/test/java/io/mehow/laboratory/OptionFactorySpec.kt deleted file mode 100644 index f25d57bb3..000000000 --- a/library/laboratory/src/test/java/io/mehow/laboratory/OptionFactorySpec.kt +++ /dev/null @@ -1,39 +0,0 @@ -package io.mehow.laboratory - -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.shouldBe - -internal class OptionFactorySpec : DescribeSpec({ - val firstFactory = object : OptionFactory { - override fun create(key: String, name: String): Feature<*>? = when (key) { - "FirstFeature" -> FirstFeature.A - else -> null - } - } - - val secondFactory = object : OptionFactory { - override fun create(key: String, name: String): Feature<*>? = when (key) { - "FirstFeature" -> FirstFeature.B - "SecondFeature" -> SecondFeature.B - else -> null - } - } - - describe("option factory") { - context("when added to another") { - val factory = (firstFactory + secondFactory) - - it("uses self as a producer when feature flag is known to self") { - factory.create("FirstFeature", "whatever") shouldBe FirstFeature.A - } - - it("uses another factory as a producer when feature flag is known to other") { - factory.create("SecondFeature", "whatever") shouldBe SecondFeature.B - } - - it("uses no producer when feature flag is unknown to any of factories") { - factory.create("UnsourcedFeature", "whatever") shouldBe null - } - } - } -}) diff --git a/library/laboratory/src/test/java/io/mehow/laboratory/ParentChildFeatureSpec.kt b/library/laboratory/src/test/java/io/mehow/laboratory/ParentChildFeatureSpec.kt deleted file mode 100644 index 719d467f8..000000000 --- a/library/laboratory/src/test/java/io/mehow/laboratory/ParentChildFeatureSpec.kt +++ /dev/null @@ -1,100 +0,0 @@ -package io.mehow.laboratory - -import app.cash.turbine.test -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.shouldBe - -internal class ParentChildFeatureSpec : DescribeSpec({ - describe("parent feature") { - it("supervises experiments on child") { - val laboratory = Laboratory.inMemory() - - laboratory.setOption(FirstChildFeature.B) - laboratory.setOption(ParentFeature.B) - - laboratory.experiment() shouldBe FirstChildFeature.A - } - - it("does not change saved child option") { - val laboratory = Laboratory.inMemory() - - laboratory.setOption(FirstChildFeature.B) - laboratory.setOption(ParentFeature.B) - laboratory.setOption(ParentFeature.A) - - laboratory.experiment() shouldBe FirstChildFeature.B - } - - it("does not disable changing child option") { - val laboratory = Laboratory.inMemory() - - laboratory.setOption(ParentFeature.B) - laboratory.setOption(FirstChildFeature.B) - laboratory.setOption(ParentFeature.A) - - laboratory.experiment() shouldBe FirstChildFeature.B - } - - it("supervises observation of child") { - val laboratory = Laboratory.inMemory() - - laboratory.setOption(FirstChildFeature.B) - - laboratory.observe().test { - awaitItem() shouldBe FirstChildFeature.B - - laboratory.setOption(ParentFeature.B) - awaitItem() shouldBe FirstChildFeature.A - - laboratory.setOption(ParentFeature.A) - awaitItem() shouldBe FirstChildFeature.B - - cancel() - } - } - - it("prevents child from emitting same value twice") { - val laboratory = Laboratory.inMemory() - - laboratory.observe().test { - awaitItem() shouldBe FirstChildFeature.A - - laboratory.setOption(ParentFeature.B) - expectNoEvents() - - cancel() - } - } - } - - describe("grandparent feature") { - it("supervises experiments on grandchild") { - val laboratory = Laboratory.inMemory() - - laboratory.setOption(SecondChildFeature.B) - laboratory.setOption(ParentFeature.B) - laboratory.setOption(GrandParentFeature.B) - - laboratory.experiment() shouldBe SecondChildFeature.A - } - - it("supervises observation of grandchild") { - val laboratory = Laboratory.inMemory() - - laboratory.setOption(SecondChildFeature.B) - laboratory.setOption(ParentFeature.B) - - laboratory.observe().test { - awaitItem() shouldBe SecondChildFeature.B - - laboratory.setOption(GrandParentFeature.B) - awaitItem() shouldBe SecondChildFeature.A - - laboratory.setOption(GrandParentFeature.A) - awaitItem() shouldBe SecondChildFeature.B - - cancel() - } - } - } -}) diff --git a/library/laboratory/src/test/java/io/mehow/laboratory/SourcedFeatureStorageSpec.kt b/library/laboratory/src/test/java/io/mehow/laboratory/SourcedFeatureStorageSpec.kt deleted file mode 100644 index d7fcd977f..000000000 --- a/library/laboratory/src/test/java/io/mehow/laboratory/SourcedFeatureStorageSpec.kt +++ /dev/null @@ -1,221 +0,0 @@ -package io.mehow.laboratory - -import app.cash.turbine.test -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.shouldBe - -internal class SourcedFeatureStorageSpec : DescribeSpec({ - lateinit var localLaboratory: Laboratory - lateinit var remoteLaboratoryA: Laboratory - lateinit var remoteLaboratoryB: Laboratory - lateinit var sourcedLaboratory: Laboratory - lateinit var sourcedStorage: FeatureStorage - - beforeTest { - val localStorage = FeatureStorage.inMemory() - val remoteStorageA = FeatureStorage.inMemory() - val remoteStorageB = FeatureStorage.inMemory() - sourcedStorage = SourcedFeatureStorage( - localStorage, - mapOf( - "RemoteA" to remoteStorageA, - "RemoteB" to remoteStorageB, - ), - ) - localLaboratory = Laboratory.create(localStorage) - remoteLaboratoryA = Laboratory.create(remoteStorageA) - remoteLaboratoryB = Laboratory.create(remoteStorageB) - sourcedLaboratory = Laboratory.create(sourcedStorage) - } - - describe("sourced feature storage") { - context("with no sources set") { - it("takes initial value from a default source") { - localLaboratory.setOption(FirstFeature.B) - sourcedLaboratory.experiment() shouldBe FirstFeature.B - - remoteLaboratoryB.setOption(SecondFeature.C) - sourcedLaboratory.experiment() shouldBe SecondFeature.C - } - - it("observes changes of a default feature source") { - sourcedLaboratory.observe().test { - awaitItem() shouldBe SecondFeature.A - - remoteLaboratoryB.setOption(SecondFeature.B) - awaitItem() shouldBe SecondFeature.B - - remoteLaboratoryB.setOption(SecondFeature.C) - awaitItem() shouldBe SecondFeature.C - - cancel() - } - } - } - - it("allows to change a feature source") { - sourcedLaboratory.setOption(FirstFeature.Source.RemoteA) - - remoteLaboratoryA.setOption(FirstFeature.C) - sourcedLaboratory.experiment() shouldBe FirstFeature.C - } - - it("observes changes of a feature source") { - localLaboratory.setOption(SecondFeature.A) - remoteLaboratoryA.setOption(SecondFeature.B) - remoteLaboratoryB.setOption(SecondFeature.C) - - sourcedLaboratory.observe().test { - awaitItem() shouldBe SecondFeature.C - - sourcedLaboratory.setOption(SecondFeature.Source.Local) - awaitItem() shouldBe SecondFeature.A - - localLaboratory.setOption(SecondFeature.C) - awaitItem() shouldBe SecondFeature.C - - sourcedLaboratory.setOption(SecondFeature.Source.RemoteA) - awaitItem() shouldBe SecondFeature.B - - remoteLaboratoryA.setOption(SecondFeature.A) - awaitItem() shouldBe SecondFeature.A - - sourcedLaboratory.setOption(SecondFeature.Source.RemoteB) - awaitItem() shouldBe SecondFeature.C - - remoteLaboratoryB.setOption(SecondFeature.B) - awaitItem() shouldBe SecondFeature.B - - cancel() - } - } - - it("does not observe changes of different feature source") { - localLaboratory.setOption(SecondFeature.B) - remoteLaboratoryA.setOption(SecondFeature.C) - - sourcedLaboratory.observe().test { - awaitItem() shouldBe SecondFeature.A - - sourcedLaboratory.setOption(FirstFeature.Source.Local) - expectNoEvents() - - sourcedLaboratory.setOption(FirstFeature.Source.RemoteA) - expectNoEvents() - - cancel() - } - } - - context("for empty source") { - it("falls back to observing a local storage") { - sourcedLaboratory.observe().test { - awaitItem() shouldBe EmptySourceFeature.A - - localLaboratory.setOption(EmptySourceFeature.B) - awaitItem() shouldBe EmptySourceFeature.B - - cancel() - } - } - - it("falls backs to experimenting with a local storage") { - sourcedLaboratory.experiment() shouldBe EmptySourceFeature.A - - localLaboratory.setOption(EmptySourceFeature.C) - sourcedLaboratory.experiment() shouldBe EmptySourceFeature.C - } - } - - context("for unknown source") { - it("falls back to observing a local storage") { - val localStorage = FeatureStorage.inMemory() - val sourcedStorage = SourcedFeatureStorage(localStorage, emptyMap()) - localLaboratory = Laboratory.create(localStorage) - sourcedLaboratory = Laboratory.create(sourcedStorage) - - sourcedLaboratory.observe().test { - awaitItem() shouldBe FirstFeature.A - - localLaboratory.setOption(FirstFeature.B) - awaitItem() shouldBe FirstFeature.B - - cancel() - } - } - - it("falls backs to experimenting with a local storage") { - val localStorage = FeatureStorage.inMemory() - val sourcedStorage = SourcedFeatureStorage(localStorage, emptyMap()) - localLaboratory = Laboratory.create(localStorage) - sourcedLaboratory = Laboratory.create(sourcedStorage) - - sourcedLaboratory.experiment() shouldBe FirstFeature.A - - localLaboratory.setOption(FirstFeature.C) - sourcedLaboratory.experiment() shouldBe FirstFeature.C - } - } - - context("for unsourced feature") { - it("falls back to observing a local storage") { - sourcedLaboratory.observe().test { - awaitItem() shouldBe UnsourcedFeature.A - - localLaboratory.setOption(UnsourcedFeature.B) - awaitItem() shouldBe UnsourcedFeature.B - - cancel() - } - } - - it("falls backs to experimenting with a local storage") { - sourcedLaboratory.experiment() shouldBe UnsourcedFeature.A - - localLaboratory.setOption(UnsourcedFeature.C) - sourcedLaboratory.experiment() shouldBe UnsourcedFeature.C - } - } - - it("controls local features") { - sourcedLaboratory.observe().test { - awaitItem() shouldBe FirstFeature.A - - sourcedLaboratory.setOption(FirstFeature.B) - awaitItem() shouldBe FirstFeature.B - - sourcedLaboratory.setOption(FirstFeature.C) - awaitItem() shouldBe FirstFeature.C - } - } - - it("clears only local source") { - localLaboratory.setOption(FirstFeature.B) - remoteLaboratoryA.setOption(FirstFeature.B) - sourcedLaboratory.setOption(FirstFeature.Source.RemoteA) - - sourcedLaboratory.clear() - - localLaboratory.experimentIs(FirstFeature.A) - remoteLaboratoryA.experimentIs(FirstFeature.B) - sourcedLaboratory.experimentIs(FirstFeature.A) - } - - it("uses default options override for sources") { - val defaultOptionFactory = object : DefaultOptionFactory { - override fun > create(feature: T) = when (feature) { - is FirstFeature.Source -> FirstFeature.Source.RemoteA - else -> null - } - } - val laboratory = Laboratory.Builder() - .featureStorage(sourcedStorage) - .defaultOptionFactory(defaultOptionFactory) - .build() - - remoteLaboratoryA.setOption(FirstFeature.C) - - laboratory.experiment() shouldBe FirstFeature.C - } - } -}) diff --git a/library/settings.gradle.kts b/library/settings.gradle.kts deleted file mode 100644 index 503c1416f..000000000 --- a/library/settings.gradle.kts +++ /dev/null @@ -1,41 +0,0 @@ -import com.android.build.api.dsl.SettingsExtension - -pluginManagement { - repositories { - mavenCentral() - google() - } -} - -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - mavenCentral() - google() - gradlePluginPortal() - } - - versionCatalogs { - create("libs") { - from(files("gradle/dependencies.toml")) - } - } -} - -plugins { - id("com.android.settings") version "8.0.2" -} - -@Suppress("UnstableApiUsage") -extensions.getByType(SettingsExtension::class).apply { - compileSdk = 33 - minSdk = 21 -} - -include(":laboratory") -include(":shared-preferences") -include(":inspector") -include(":hyperion-plugin") -include(":generator") -include(":gradle-plugin") -include(":data-store") diff --git a/library/shared-preferences/build.gradle b/library/shared-preferences/build.gradle deleted file mode 100644 index 51fa70ed4..000000000 --- a/library/shared-preferences/build.gradle +++ /dev/null @@ -1,42 +0,0 @@ -plugins { - id "com.android.library" - id "org.jetbrains.kotlin.android" -} - -android { - namespace "io.mehow.laboratory.sharedpreferences" - - defaultConfig { - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - testInstrumentationRunnerArgument "clearPackageData", "true" - } - - testOptions.execution "ANDROIDX_TEST_ORCHESTRATOR" - testBuildType "release" - - buildTypes { - release { - // Since we test release build it has to be signed. - signingConfig signingConfigs.getByName("debug") - } - } - - packagingOptions { - exclude "META-INF/AL2.0" - exclude "META-INF/LGPL2.1" - } -} - -dependencies { - api project(":laboratory") - - androidTestUtil libs.android.x.test.orchestrator - androidTestImplementation libs.android.x.testExtJunitKtx - androidTestImplementation libs.android.x.test.coreKtx - androidTestImplementation libs.android.x.test.runner - androidTestImplementation libs.kotest.assertions - androidTestImplementation libs.turbine -} - -apply from: "$rootDir/gradle/dokka-config.gradle" -apply from: "$rootDir/gradle/gradle-mvn-push.gradle" diff --git a/library/shared-preferences/gradle.properties b/library/shared-preferences/gradle.properties deleted file mode 100644 index ec4821ca1..000000000 --- a/library/shared-preferences/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -POM_ARTIFACT_ID=laboratory-shared-preferences -POM_NAME=Laboratory (SharedPreferences) -POM_PACKAGING=aar diff --git a/library/lint.xml b/lint.xml similarity index 58% rename from library/lint.xml rename to lint.xml index 04d08705e..998387fbd 100644 --- a/library/lint.xml +++ b/lint.xml @@ -4,12 +4,7 @@ - - - - - + - diff --git a/library/mkdocs.yml b/mkdocs.yml similarity index 75% rename from library/mkdocs.yml rename to mkdocs.yml index 47a7f178c..501bc5f05 100644 --- a/library/mkdocs.yml +++ b/mkdocs.yml @@ -33,8 +33,6 @@ markdown_extensions: plugins: - search - - minify: - minify_html: true - mkdocs-video nav: @@ -42,11 +40,6 @@ nav: - 'User guide': user-guide.md - 'QA module': qa-module.md - 'Gradle plugin': gradle-plugin.md - - 'API': - - 'laboratory': api/laboratory/index.html - - 'gradle-plugin': api/gradle-plugin/index.html - - 'data-store': api/data-store/index.html - - 'inspector': api/inspector/index.html - - 'shared-preferences': api/shared-preferences/index.html + - 'API': api/index.html - 'Changelog': changelog.md - 'Releasing': releasing.md diff --git a/library/prepare-release.sh b/prepare-release.sh similarity index 99% rename from library/prepare-release.sh rename to prepare-release.sh index 6a2d2e9e1..cd02fd65f 100644 --- a/library/prepare-release.sh +++ b/prepare-release.sh @@ -69,7 +69,7 @@ indexFile="./docs/index.md" sed -i "" "s/$currentVersion/$newVersion/g" $indexFile # Replace current version in README.md -readmeFile="../README.md" +readmeFile="./README.md" sed -i "" "s/$currentVersion/$newVersion/g" $readmeFile git reset &> /dev/null diff --git a/samples/.editorconfig b/samples/.editorconfig new file mode 120000 index 000000000..38d9a0ce1 --- /dev/null +++ b/samples/.editorconfig @@ -0,0 +1 @@ +../.editorconfig \ No newline at end of file diff --git a/samples/basic/build.gradle b/samples/basic/build.gradle deleted file mode 100644 index 943d1dec5..000000000 --- a/samples/basic/build.gradle +++ /dev/null @@ -1,89 +0,0 @@ -plugins { - id "com.android.application" - id "org.jetbrains.kotlin.android" - id "io.mehow.laboratory" -} - -android { - namespace "io.mehow.laboratory.sample.basic" - - signingConfigs { - config { - keyAlias "mehow-io" - keyPassword "mehow-io" - storeFile file("../mehow-io.keystore") - storePassword "mehow-io" - } - } - - defaultConfig { - applicationId "io.mehow.laboratory.sample.basic" - targetSdkVersion libs.versions.androidBuild.targetSdk.get().toInteger() - - versionCode 1 - versionName "1.0.0" - - signingConfig signingConfigs.config - } - - buildFeatures { - viewBinding true - } - - buildTypes.debug.setMatchingFallbacks "release" - - variantFilter { - setIgnore it.name != "debug" - } - - compileOptions { - sourceCompatibility JavaConfig.code - targetCompatibility JavaConfig.code - } - - kotlinOptions { - jvmTarget = JavaConfig.name - freeCompilerArgs += [ - "-Xjvm-default=all", - ] - } -} - -laboratory { - packageName = "io.mehow.laboratory.sample.basic" - - featureFactory() - - feature("LogType") { - deprecated("Sample deprecation") - - withDefaultOption("Info") - withOption("Verbose") - withOption("Debug") - withOption("Warning") - withOption("Error") - } - - feature("ReportRootedDevice") { - description = - "Reports during [cold start](https://developer.android.com/topic/performance/vitals/launch-time#cold) whether device is rooted" - - withDefaultOption("Disabled") - withOption("Enabled") - } - - feature("Authentication") { - withDefaultOption("Password") - withOption("Fingerprint") - withOption("Retina") - withOption("Face") - } -} - -dependencies { - implementation libs.kotlin.x.coroutinesAndroid - implementation libs.android.material - implementation libs.hyperion.core - implementation libs.laboratory.dataStore - implementation libs.laboratory.hyperionPlugin -} diff --git a/samples/basic/build.gradle.kts b/samples/basic/build.gradle.kts new file mode 100644 index 000000000..5b009588f --- /dev/null +++ b/samples/basic/build.gradle.kts @@ -0,0 +1,49 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.laboratory) + alias(libs.plugins.detekt) + alias(libs.plugins.spotless) +} + +android { + namespace = "io.mehow.laboratory.sample.basic" +} + +laboratory { + packageName = "io.mehow.laboratory.sample.basic" + + featureFactory() + + feature("LogType") { + deprecated("Sample deprecation") + + withDefaultOption("Info") + withOption("Verbose") + withOption("Debug") + withOption("Warning") + withOption("Error") + } + + feature("ReportRootedDevice") { + description = "Reports during [cold start](https://developer.android.com/topic/performance/vitals/launch-time#cold) whether device is rooted" + + withDefaultOption("Disabled") + withOption("Enabled") + } + + feature("Authentication") { + withDefaultOption("Password") + withOption("Fingerprint") + withOption("Retina") + withOption("Face") + } +} + +dependencies { + implementation(libs.kotlinx.coroutinesAndroid) + implementation(libs.android.material) + implementation(libs.hyperion.core) + implementation(libs.laboratory.dataStore) + implementation(libs.laboratory.hyperionPlugin) +} diff --git a/samples/basic/src/main/kotlin/io/mehow/laboratory/sample/basic/Activity.kt b/samples/basic/src/main/kotlin/io/mehow/laboratory/sample/basic/Activity.kt index 0d4e1406a..83222180b 100644 --- a/samples/basic/src/main/kotlin/io/mehow/laboratory/sample/basic/Activity.kt +++ b/samples/basic/src/main/kotlin/io/mehow/laboratory/sample/basic/Activity.kt @@ -21,6 +21,7 @@ class Activity : AndroidActivity() { val binding = MainBinding.inflate(layoutInflater).apply { launchLaboratory.setOnClickListener { LaboratoryActivity.start(this@Activity) } + @Suppress("DEPRECATION") logType.observeFeature() reportRootedDevice.observeFeature() authentication.observeFeature() @@ -35,8 +36,8 @@ class Activity : AndroidActivity() { private inline fun > TextView.observeFeature() { laboratory.observe() - .map { "${it.javaClass.simpleName}: $it" } - .onEach { text = it } - .launchIn(mainScope) + .map { "${it.javaClass.simpleName}: $it" } + .onEach { text = it } + .launchIn(mainScope) } } diff --git a/samples/build.gradle.kts b/samples/build.gradle.kts new file mode 100644 index 000000000..7a3560ec6 --- /dev/null +++ b/samples/build.gradle.kts @@ -0,0 +1,139 @@ +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.gradle.AppPlugin +import com.diffplug.gradle.spotless.SpotlessExtension +import com.diffplug.gradle.spotless.SpotlessExtensionPredeclare +import com.diffplug.gradle.spotless.SpotlessPlugin +import com.diffplug.spotless.LineEnding +import io.gitlab.arturbosch.detekt.Detekt +import io.gitlab.arturbosch.detekt.DetektPlugin +import io.gitlab.arturbosch.detekt.extensions.DetektExtension +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompilerOptions +import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask + +plugins { + alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.android.application) apply false + alias(libs.plugins.laboratory) apply false + alias(libs.plugins.detekt) + alias(libs.plugins.spotless) +} + +val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get()) +val ktlintVersion = libs.versions.ktlint.get() + +allprojects { + val configureSpotless: SpotlessExtension.() -> Unit = { + lineEndings = LineEnding.UNIX + + kotlin { + target("src/**/*.kt") + trimTrailingWhitespace() + endWithNewline() + ktlint(ktlintVersion).setEditorConfigPath(rootProject.file(".editorconfig")) + } + + kotlinGradle { + target("*.kts") + trimTrailingWhitespace() + endWithNewline() + ktlint(ktlintVersion).setEditorConfigPath(rootProject.file(".editorconfig")) + } + + format("misc") { + target("*.md", "*.yml", "*.proto", "*.properties", "*.toml", ".gitignore", ".editorconfig") + trimTrailingWhitespace() + endWithNewline() + } + } + plugins.withType().configureEach { + configure { + configureSpotless() + + if (project.rootProject == project) { + predeclareDeps() + } + } + if (project.rootProject == project) { + configure { configureSpotless() } + } + } + + plugins.withType().configureEach { + configure { + toolVersion = libs.versions.detekt.get() + allRules = true + parallel = true + buildUponDefaultConfig = true + config.from(rootProject.file("detekt.yml")) + } + tasks.withType().configureEach { + jvmTarget = javaTarget.target + reports { + html.required.set(true) + xml.required.set(true) + txt.required.set(true) + } + } + } +} + +subprojects { + plugins.withType().configureEach { + tasks.withType>().configureEach { + compilerOptions { + jvmTarget.set(javaTarget) + progressiveMode.set(true) + allWarningsAsErrors.set(true) + freeCompilerArgs.addAll( + "-Xjvm-default=all", + ) + } + } + } + + tasks.withType().configureEach { + sourceCompatibility = javaTarget.target + targetCompatibility = javaTarget.target + } + + plugins.withType().configureEach { + configure { + compileOptions { + sourceCompatibility = JavaVersion.toVersion(javaTarget.target) + targetCompatibility = JavaVersion.toVersion(javaTarget.target) + } + + val dummyConfig by signingConfigs.creating { + storeFile = rootProject.file("mehow-io.keystore") + storePassword = "mehow-io" + keyAlias = "mehow-io" + keyPassword = "mehow-io" + } + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.targetSdk.get().toInt() + + versionCode = 1 + versionName = "1.0.0" + + signingConfig = dummyConfig + } + + buildFeatures { + viewBinding = true + } + + buildTypes { + debug { + applicationIdSuffix = ".debug" + matchingFallbacks.add("release") + } + } + } + } +} diff --git a/samples/ci-check/build.gradle b/samples/ci-check/build.gradle deleted file mode 100644 index 501fe8d0d..000000000 --- a/samples/ci-check/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -plugins { - id "org.jetbrains.kotlin.jvm" - id "io.mehow.laboratory" -} - -tasks.withType(JavaCompile).configureEach { - sourceCompatibility JavaConfig.name - targetCompatibility JavaConfig.name -} - -tasks.withType(KotlinCompile).configureEach { - kotlinOptions { - jvmTarget = JavaConfig.name - freeCompilerArgs += [ - "-Xjvm-default=all", - ] - } -} - -laboratory { - packageName = "io.mehow.laboratory.sample.ci" - - feature("SampleFlag") { - withDefaultOption("OptionA") - withOption("OptionB") - } -} diff --git a/samples/ci-check/src/main/kotlin/main.kt b/samples/ci-check/src/main/kotlin/main.kt deleted file mode 100644 index cc405003a..000000000 --- a/samples/ci-check/src/main/kotlin/main.kt +++ /dev/null @@ -1,7 +0,0 @@ -import io.mehow.laboratory.Laboratory -import io.mehow.laboratory.sample.ci.SampleFlag - -suspend fun main() { - val laboratory = Laboratory.inMemory() - println(laboratory.experimentIs(SampleFlag.OptionA)) -} diff --git a/samples/default-option/build.gradle b/samples/default-option/build.gradle deleted file mode 100644 index 26f761f41..000000000 --- a/samples/default-option/build.gradle +++ /dev/null @@ -1,81 +0,0 @@ -plugins { - id "com.android.application" - id "org.jetbrains.kotlin.android" - id "io.mehow.laboratory" -} - -android { - namespace "io.mehow.laboratory.sample.defaultoption" - - signingConfigs { - config { - keyAlias "mehow-io" - keyPassword "mehow-io" - storeFile file("../mehow-io.keystore") - storePassword "mehow-io" - } - } - - defaultConfig { - applicationId "io.mehow.laboratory.sample.defaultoption" - targetSdkVersion libs.versions.androidBuild.targetSdk.get().toInteger() - - versionCode 1 - versionName "1.0.0" - - signingConfig signingConfigs.config - } - - buildTypes { - debug { - applicationIdSuffix ".debug" - } - } - - buildFeatures { - viewBinding true - } - - buildTypes.debug.setMatchingFallbacks "release" - - compileOptions { - sourceCompatibility JavaConfig.code - targetCompatibility JavaConfig.code - } - - kotlinOptions { - jvmTarget = JavaConfig.name - freeCompilerArgs += [ - "-Xjvm-default=all", - ] - } -} - -laboratory { - packageName = "io.mehow.laboratory.sample.defaultoption" - - featureFactory() - - feature("ShowAds") { - withDefaultOption("Enabled") - withOption("Disabled") - } - - feature("ReportRootedDevice") { - withDefaultOption("Enabled") - withOption("Disabled") - } - - feature("RequiredFingerprint") { - withDefaultOption("Enabled") - withOption("Disabled") - } -} - -dependencies { - implementation libs.kotlin.x.coroutinesAndroid - implementation libs.android.material - implementation libs.hyperion.core - implementation libs.laboratory.dataStore - implementation libs.laboratory.hyperionPlugin -} diff --git a/samples/default-option/build.gradle.kts b/samples/default-option/build.gradle.kts new file mode 100644 index 000000000..59dc949b3 --- /dev/null +++ b/samples/default-option/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.laboratory) + alias(libs.plugins.detekt) + alias(libs.plugins.spotless) +} + +android { + namespace = "io.mehow.laboratory.sample.defaultoption" +} + +laboratory { + packageName = "io.mehow.laboratory.sample.defaultoption" + + featureFactory() + + feature("ShowAds") { + withDefaultOption("Enabled") + withOption("Disabled") + } + + feature("ReportRootedDevice") { + withDefaultOption("Enabled") + withOption("Disabled") + } + + feature("RequiredFingerprint") { + withDefaultOption("Enabled") + withOption("Disabled") + } +} + +dependencies { + implementation(libs.kotlinx.coroutinesAndroid) + implementation(libs.android.material) + implementation(libs.hyperion.core) + implementation(libs.laboratory.dataStore) + implementation(libs.laboratory.hyperionPlugin) +} diff --git a/samples/default-option/src/debug/kotlin/io/mehow.laboratory/sample/defaultoption/DefaultOptionFactory.kt b/samples/default-option/src/debug/kotlin/io/mehow.laboratory/sample/defaultoption/DefaultOptionFactory.kt index 149a00e6d..8807c1c9a 100644 --- a/samples/default-option/src/debug/kotlin/io/mehow.laboratory/sample/defaultoption/DefaultOptionFactory.kt +++ b/samples/default-option/src/debug/kotlin/io/mehow.laboratory/sample/defaultoption/DefaultOptionFactory.kt @@ -8,6 +8,6 @@ fun DefaultOptionFactory.Companion.create(): DefaultOptionFactory = DebugDefault private object DebugDefaultOptionFactory : DefaultOptionFactory { override fun > create(feature: T) = feature::class.java - .options - .firstOrNull { it.name == "Disabled" } + .options + .firstOrNull { it.name == "Disabled" } } diff --git a/samples/default-option/src/main/kotlin/io/mehow/laboratory/sample/defaultoption/Activity.kt b/samples/default-option/src/main/kotlin/io/mehow/laboratory/sample/defaultoption/Activity.kt index 15596df69..9f088d729 100644 --- a/samples/default-option/src/main/kotlin/io/mehow/laboratory/sample/defaultoption/Activity.kt +++ b/samples/default-option/src/main/kotlin/io/mehow/laboratory/sample/defaultoption/Activity.kt @@ -35,8 +35,8 @@ class Activity : AndroidActivity() { private inline fun > TextView.observeFeature() { laboratory.observe() - .map { "${it.javaClass.simpleName}: $it" } - .onEach { text = it } - .launchIn(mainScope) + .map { "${it.javaClass.simpleName}: $it" } + .onEach { text = it } + .launchIn(mainScope) } } diff --git a/samples/default-option/src/main/kotlin/io/mehow/laboratory/sample/defaultoption/Application.kt b/samples/default-option/src/main/kotlin/io/mehow/laboratory/sample/defaultoption/Application.kt index ce8fc89e2..1df6ec525 100644 --- a/samples/default-option/src/main/kotlin/io/mehow/laboratory/sample/defaultoption/Application.kt +++ b/samples/default-option/src/main/kotlin/io/mehow/laboratory/sample/defaultoption/Application.kt @@ -20,9 +20,9 @@ class Application : AndroidApplication() { val dataStore = DataStoreFactory.create(FeatureFlagsSerializer) { File(filesDir, "datastore/local") } val storage = FeatureStorage.dataStore(dataStore) laboratory = Laboratory.builder() - .featureStorage(storage) - .defaultOptionFactory(DefaultOptionFactory.create()) - .build() + .featureStorage(storage) + .defaultOptionFactory(DefaultOptionFactory.create()) + .build() LaboratoryActivity.configure(laboratory, FeatureFactory.featureGenerated()) } diff --git a/samples/detekt.yml b/samples/detekt.yml new file mode 120000 index 000000000..2b9b28fc7 --- /dev/null +++ b/samples/detekt.yml @@ -0,0 +1 @@ +../detekt.yml \ No newline at end of file diff --git a/samples/firebase/build.gradle b/samples/firebase/build.gradle deleted file mode 100644 index 2fbe06433..000000000 --- a/samples/firebase/build.gradle +++ /dev/null @@ -1,101 +0,0 @@ -plugins { - id "com.android.application" - id "org.jetbrains.kotlin.android" - id "io.mehow.laboratory" - id "com.google.gms.google-services" -} - -android { - namespace "io.mehow.laboratory.sample.firebase" - - signingConfigs { - config { - keyAlias "mehow-io" - keyPassword "mehow-io" - storeFile file("../mehow-io.keystore") - storePassword "mehow-io" - } - } - - defaultConfig { - applicationId "io.mehow.laboratory.sample.firebase" - - targetSdkVersion libs.versions.androidBuild.targetSdk.get().toInteger() - - versionCode 1 - versionName "1.0.0" - - signingConfig signingConfigs.config - } - - buildFeatures { - viewBinding true - } - - buildTypes.debug.setMatchingFallbacks "release" - - variantFilter { - setIgnore it.name != "debug" - } - - compileOptions { - sourceCompatibility JavaConfig.code - targetCompatibility JavaConfig.code - } - - kotlinOptions { - jvmTarget = JavaConfig.name - freeCompilerArgs += [ - "-Xjvm-default=all", - ] - } -} - -laboratory { - packageName = "io.mehow.laboratory.sample.firebase" - - sourcedStorage() - optionFactory() - featureFactory() - - feature("LogType") { - key = "LogType" - - withDefaultOption("Info") - withOption("Verbose") - withOption("Debug") - withOption("Warning") - withOption("Error") - - withDefaultSource("Firebase") - } - - feature("ReportRootedDevice") { - key = "ReportRootedDevice" - - withDefaultOption("Disabled") - withOption("Enabled") - - withDefaultSource("Firebase") - } - - feature("Authentication") { - key = "Authentication" - - withDefaultOption("Password") - withOption("Fingerprint") - withOption("Retina") - withOption("Face") - - withDefaultSource("Firebase") - } -} - -dependencies { - implementation libs.kotlin.x.coroutinesAndroid - implementation libs.android.material - implementation libs.firebase.databaseKtx - implementation libs.hyperion.core - implementation libs.laboratory.dataStore - implementation libs.laboratory.hyperionPlugin -} diff --git a/samples/firebase/google-services.json b/samples/firebase/google-services.json deleted file mode 100644 index 7ae670380..000000000 --- a/samples/firebase/google-services.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "project_info": { - "project_number": "851249949580", - "project_id": "laboratory-sample", - "storage_bucket": "laboratory-sample.appspot.com" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:851249949580:android:3ba6dee5e056a2e74804a9", - "android_client_info": { - "package_name": "io.mehow.laboratory.sample.firebase" - } - }, - "oauth_client": [ - { - "client_id": "851249949580-i1ls4r7hsomkkon4fpjpdj1dlagctu4j.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "io.mehow.laboratory.sample.firebase", - "certificate_hash": "232d7fb12884c4872303c219d51b0f69f524e739" - } - }, - { - "client_id": "851249949580-2sf7joehcm5emmkmut2g436bith99f4p.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyAQpuW6WWiyH-ZHsmFUACfm6N_pY91h3PU" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "851249949580-2sf7joehcm5emmkmut2g436bith99f4p.apps.googleusercontent.com", - "client_type": 3 - } - ] - } - } - } - ], - "configuration_version": "1" -} \ No newline at end of file diff --git a/samples/firebase/src/main/AndroidManifest.xml b/samples/firebase/src/main/AndroidManifest.xml deleted file mode 100644 index 4f61e533f..000000000 --- a/samples/firebase/src/main/AndroidManifest.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/samples/firebase/src/main/kotlin/io/mehow/laboratory/sample/firebase/Activity.kt b/samples/firebase/src/main/kotlin/io/mehow/laboratory/sample/firebase/Activity.kt deleted file mode 100644 index 46010f2e4..000000000 --- a/samples/firebase/src/main/kotlin/io/mehow/laboratory/sample/firebase/Activity.kt +++ /dev/null @@ -1,42 +0,0 @@ -package io.mehow.laboratory.sample.firebase - -import android.os.Bundle -import android.widget.TextView -import io.mehow.laboratory.Feature -import io.mehow.laboratory.inspector.LaboratoryActivity -import io.mehow.laboratory.sample.firebase.Application.Companion.laboratory -import io.mehow.laboratory.sample.firebase.databinding.MainBinding -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import android.app.Activity as AndroidActivity - -class Activity : AndroidActivity() { - private val mainScope = MainScope() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val binding = MainBinding.inflate(layoutInflater).apply { - launchLaboratory.setOnClickListener { LaboratoryActivity.start(this@Activity) } - logType.observeFeature() - reportRootedDevice.observeFeature() - authentication.observeFeature() - } - setContentView(binding.root) - } - - override fun onDestroy() { - mainScope.cancel() - super.onDestroy() - } - - private inline fun > TextView.observeFeature() { - laboratory.observe() - .map { "${it.javaClass.simpleName}: $it" } - .onEach { text = it } - .launchIn(mainScope) - } -} diff --git a/samples/firebase/src/main/kotlin/io/mehow/laboratory/sample/firebase/Application.kt b/samples/firebase/src/main/kotlin/io/mehow/laboratory/sample/firebase/Application.kt deleted file mode 100644 index 9f4f3638b..000000000 --- a/samples/firebase/src/main/kotlin/io/mehow/laboratory/sample/firebase/Application.kt +++ /dev/null @@ -1,50 +0,0 @@ -package io.mehow.laboratory.sample.firebase - -import android.content.Context -import androidx.datastore.core.DataStoreFactory -import com.google.firebase.database.DataSnapshot -import com.google.firebase.database.DatabaseError -import com.google.firebase.database.ValueEventListener -import com.google.firebase.database.ktx.database -import com.google.firebase.ktx.Firebase -import io.mehow.laboratory.FeatureFactory -import io.mehow.laboratory.FeatureStorage -import io.mehow.laboratory.Laboratory -import io.mehow.laboratory.OptionFactory -import io.mehow.laboratory.datastore.FeatureFlagsSerializer -import io.mehow.laboratory.datastore.dataStore -import io.mehow.laboratory.inspector.LaboratoryActivity -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import java.io.File -import android.app.Application as AndroidApplication - -class Application : AndroidApplication() { - private lateinit var laboratory: Laboratory - - override fun onCreate() { - super.onCreate() - val localDataStore = DataStoreFactory.create(FeatureFlagsSerializer) { File(filesDir, "datastore/local") } - val localStorage = FeatureStorage.dataStore(localDataStore) - val firebaseDataStore = DataStoreFactory.create(FeatureFlagsSerializer) { File(filesDir, "datastore/firebase") } - val firebaseStorage = FeatureStorage.dataStore(firebaseDataStore) - val sourcedStorage = FeatureStorage.sourcedBuilder(localStorage) - .firebaseSource(firebaseStorage) - .build() - laboratory = Laboratory.create(sourcedStorage) - LaboratoryActivity.configure(laboratory, FeatureFactory.featureGenerated()) - - val synchronizer = FirebaseSynchronizer( - databaseReference = Firebase.database(firebaseUrl).reference.child("featureFlags"), - optionFactory = OptionFactory.generated(), - featureStorage = firebaseStorage, - ) - @OptIn(DelicateCoroutinesApi::class) synchronizer.synchronize(GlobalScope) - } - - companion object { - private const val firebaseUrl = "https://laboratory-sample-default-rtdb.europe-west1.firebasedatabase.app" - - val Context.laboratory get() = (applicationContext as Application).laboratory - } -} diff --git a/samples/firebase/src/main/kotlin/io/mehow/laboratory/sample/firebase/FirebaseSynchronizer.kt b/samples/firebase/src/main/kotlin/io/mehow/laboratory/sample/firebase/FirebaseSynchronizer.kt deleted file mode 100644 index ad9a2b155..000000000 --- a/samples/firebase/src/main/kotlin/io/mehow/laboratory/sample/firebase/FirebaseSynchronizer.kt +++ /dev/null @@ -1,46 +0,0 @@ -package io.mehow.laboratory.sample.firebase - -import com.google.firebase.database.DataSnapshot -import com.google.firebase.database.DatabaseError -import com.google.firebase.database.DatabaseReference -import com.google.firebase.database.ValueEventListener -import io.mehow.laboratory.FeatureStorage -import io.mehow.laboratory.OptionFactory -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach - -class FirebaseSynchronizer( - private val databaseReference: DatabaseReference, - private val optionFactory: OptionFactory, - private val featureStorage: FeatureStorage, -) { - fun synchronize(scope: CoroutineScope) = databaseReference.asKeyValueFlow() - .map { pairs -> pairs.mapNotNull { (key, value) -> optionFactory.create(key, value) } } - .onEach(featureStorage::setOptions) - .launchIn(scope) - - private fun DatabaseReference.asKeyValueFlow() = callbackFlow { - val listener = object : ValueEventListener { - override fun onDataChange(snapshot: DataSnapshot) { - (snapshot.value as? Map<*, *>)?.mapNotNull { (key, value) -> - val stringKey = key as? String ?: return@mapNotNull null - val stringValue = value as? String ?: return@mapNotNull null - stringKey to stringValue - }?.let { trySend(it) } - } - - override fun onCancelled(error: DatabaseError) { - close(error.toException()) - } - } - addValueEventListener(listener) - - awaitClose { - removeEventListener(listener) - } - } -} diff --git a/samples/firebase/src/main/res/layout/main.xml b/samples/firebase/src/main/res/layout/main.xml deleted file mode 100644 index a0c6fdaaf..000000000 --- a/samples/firebase/src/main/res/layout/main.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - diff --git a/samples/gradle.properties b/samples/gradle.properties new file mode 100644 index 000000000..607ae1e7c --- /dev/null +++ b/samples/gradle.properties @@ -0,0 +1,5 @@ +# Increase the build VMs heap size. Default is 512m. +org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 +org.gradle.parallel=true + +android.useAndroidX=true diff --git a/library/gradle/wrapper/gradle-wrapper.jar b/samples/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from library/gradle/wrapper/gradle-wrapper.jar rename to samples/gradle/wrapper/gradle-wrapper.jar diff --git a/library/gradle/wrapper/gradle-wrapper.properties b/samples/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from library/gradle/wrapper/gradle-wrapper.properties rename to samples/gradle/wrapper/gradle-wrapper.properties diff --git a/library/gradlew b/samples/gradlew similarity index 100% rename from library/gradlew rename to samples/gradlew diff --git a/library/gradlew.bat b/samples/gradlew.bat similarity index 100% rename from library/gradlew.bat rename to samples/gradlew.bat diff --git a/samples/multi-module/build.gradle b/samples/multi-module/build.gradle deleted file mode 100644 index 3e772d3c2..000000000 --- a/samples/multi-module/build.gradle +++ /dev/null @@ -1,71 +0,0 @@ -plugins { - id "com.android.application" - id "org.jetbrains.kotlin.android" - id "io.mehow.laboratory" -} - -android { - namespace "io.mehow.laboratory.sample.multimodule" - - signingConfigs { - config { - keyAlias "mehow-io" - keyPassword "mehow-io" - storeFile file("../mehow-io.keystore") - storePassword "mehow-io" - } - } - - defaultConfig { - applicationId "io.mehow.laboratory.sample.multimodule" - - targetSdkVersion libs.versions.androidBuild.targetSdk.get().toInteger() - - versionCode 1 - versionName "1.0.0" - - signingConfig signingConfigs.config - } - - buildFeatures { - viewBinding true - } - - buildTypes.debug.setMatchingFallbacks "release" - - variantFilter { - setIgnore it.name != "debug" - } - - compileOptions { - sourceCompatibility JavaConfig.code - targetCompatibility JavaConfig.code - } - - kotlinOptions { - jvmTarget = JavaConfig.name - freeCompilerArgs += [ - "-Xjvm-default=all", - ] - } -} - -laboratory { - packageName = "io.mehow.laboratory.sample.multimodule" - - featureFactory() - - dependency(project(":samples:multi-module:multi-module-a")) - dependency(project(":samples:multi-module:multi-module-b")) -} - -dependencies { - implementation libs.kotlin.x.coroutinesAndroid - implementation libs.android.material - implementation libs.hyperion.core - implementation libs.laboratory.dataStore - implementation libs.laboratory.hyperionPlugin - implementation project(":samples:multi-module:multi-module-a") - implementation project(":samples:multi-module:multi-module-b") - implementation project(":samples:multi-module:multi-module-c") -} diff --git a/samples/multi-module/build.gradle.kts b/samples/multi-module/build.gradle.kts new file mode 100644 index 000000000..2cfae7ed8 --- /dev/null +++ b/samples/multi-module/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.laboratory) + alias(libs.plugins.detekt) + alias(libs.plugins.spotless) +} + +android { + namespace = "io.mehow.laboratory.sample.multimodule" +} + +laboratory { + packageName = "io.mehow.laboratory.sample.multimodule" + + featureFactory() + + dependency(project(":multi-module:multi-module-a")) + dependency(project(":multi-module:multi-module-b")) +} + +dependencies { + implementation(libs.kotlinx.coroutinesAndroid) + implementation(libs.android.material) + implementation(libs.hyperion.core) + implementation(libs.laboratory.dataStore) + implementation(libs.laboratory.hyperionPlugin) + implementation(projects.multiModule.multiModuleA) + implementation(projects.multiModule.multiModuleB) + implementation(projects.multiModule.multiModuleC) +} diff --git a/samples/multi-module/multi-module-a/build.gradle b/samples/multi-module/multi-module-a/build.gradle.kts similarity index 59% rename from samples/multi-module/multi-module-a/build.gradle rename to samples/multi-module/multi-module-a/build.gradle.kts index 3d411130d..659f53c13 100644 --- a/samples/multi-module/multi-module-a/build.gradle +++ b/samples/multi-module/multi-module-a/build.gradle.kts @@ -1,14 +1,8 @@ plugins { - id "org.jetbrains.kotlin.jvm" - id "io.mehow.laboratory" -} - -compileKotlin { - kotlinOptions { - freeCompilerArgs += [ - "-Xjvm-default=all", - ] - } + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.laboratory) + alias(libs.plugins.detekt) + alias(libs.plugins.spotless) } laboratory { diff --git a/samples/multi-module/multi-module-b/build.gradle b/samples/multi-module/multi-module-b/build.gradle.kts similarity index 72% rename from samples/multi-module/multi-module-b/build.gradle rename to samples/multi-module/multi-module-b/build.gradle.kts index 8680fb302..2f9079fed 100644 --- a/samples/multi-module/multi-module-b/build.gradle +++ b/samples/multi-module/multi-module-b/build.gradle.kts @@ -1,14 +1,8 @@ plugins { - id "org.jetbrains.kotlin.jvm" - id "io.mehow.laboratory" -} - -compileKotlin { - kotlinOptions { - freeCompilerArgs += [ - "-Xjvm-default=all", - ] - } + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.laboratory) + alias(libs.plugins.detekt) + alias(libs.plugins.spotless) } laboratory { diff --git a/samples/multi-module/multi-module-c/build.gradle b/samples/multi-module/multi-module-c/build.gradle deleted file mode 100644 index ac8a760e9..000000000 --- a/samples/multi-module/multi-module-c/build.gradle +++ /dev/null @@ -1,59 +0,0 @@ -plugins { - id "org.jetbrains.kotlin.jvm" - id "io.mehow.laboratory" -} - -compileKotlin { - kotlinOptions { - freeCompilerArgs += [ - "-Xjvm-default=all", - ] - } -} - -laboratory { - packageName = "io.mehow.laboratory.smaple.multimodule.c" - - featureFactory { - isPublic = true - } - - feature("Camera") { - withDefaultOption("Disabled") - - withOption("Enabled") { camera -> - camera.feature("LivestreamPreview") { livestream -> - livestream.withDefaultOption("Enabled") - livestream.withOption("Disabled") - } - - camera.feature("RecordingQuality") { quality -> - quality.withDefaultOption("SD") - quality.withOption("HD") - quality.withOption("QHD") - } - - camera.feature("RecordingDirectory") { directory -> - directory.withDefaultOption("Internal") - directory.withOption("External") - } - - camera.feature("VideoFilter") { filter -> - filter.withDefaultOption("NoFilter") - filter.withOption("Retro") - filter.withOption("Sepia") - filter.withOption("EightBit") - } - - camera.feature("MotionDetection") { motion -> - motion.withDefaultOption("Enabled") - motion.withOption("Disabled") - } - - camera.feature("NightMode") { nightMode -> - nightMode.withDefaultOption("Enabled") - nightMode.withOption("Disabled") - } - } - } -} diff --git a/samples/multi-module/multi-module-c/build.gradle.kts b/samples/multi-module/multi-module-c/build.gradle.kts new file mode 100644 index 000000000..3d98f3245 --- /dev/null +++ b/samples/multi-module/multi-module-c/build.gradle.kts @@ -0,0 +1,53 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.laboratory) + alias(libs.plugins.detekt) + alias(libs.plugins.spotless) +} + +laboratory { + packageName = "io.mehow.laboratory.smaple.multimodule.c" + + featureFactory { + isPublic = true + } + + feature("Camera") { + withDefaultOption("Disabled") + + withOption("Enabled") { + feature("LivestreamPreview") { + withDefaultOption("Enabled") + withOption("Disabled") + } + + feature("RecordingQuality") { + withDefaultOption("SD") + withOption("HD") + withOption("QHD") + } + + feature("RecordingDirectory") { + withDefaultOption("Internal") + withOption("External") + } + + feature("VideoFilter") { + withDefaultOption("NoFilter") + withOption("Retro") + withOption("Sepia") + withOption("EightBit") + } + + feature("MotionDetection") { + withDefaultOption("Enabled") + withOption("Disabled") + } + + feature("NightMode") { + withDefaultOption("Enabled") + withOption("Disabled") + } + } + } +} diff --git a/samples/multi-module/src/main/kotlin/io/mehow/laboratory/sample/multimodule/Activity.kt b/samples/multi-module/src/main/kotlin/io/mehow/laboratory/sample/multimodule/Activity.kt index 91d8babfe..14cf1c493 100644 --- a/samples/multi-module/src/main/kotlin/io/mehow/laboratory/sample/multimodule/Activity.kt +++ b/samples/multi-module/src/main/kotlin/io/mehow/laboratory/sample/multimodule/Activity.kt @@ -52,8 +52,8 @@ class Activity : AndroidActivity() { private inline fun > TextView.observeFeature() { laboratory.observe() - .map { "${it.javaClass.simpleName}: $it" } - .onEach { text = it } - .launchIn(mainScope) + .map { "${it.javaClass.simpleName}: $it" } + .onEach { text = it } + .launchIn(mainScope) } } diff --git a/samples/multi-module/src/main/kotlin/io/mehow/laboratory/sample/multimodule/Application.kt b/samples/multi-module/src/main/kotlin/io/mehow/laboratory/sample/multimodule/Application.kt index e091d27bf..4838e6979 100644 --- a/samples/multi-module/src/main/kotlin/io/mehow/laboratory/sample/multimodule/Application.kt +++ b/samples/multi-module/src/main/kotlin/io/mehow/laboratory/sample/multimodule/Application.kt @@ -21,9 +21,9 @@ class Application : AndroidApplication() { val storage = FeatureStorage.dataStore(dataStore) laboratory = Laboratory.create(storage) LaboratoryActivity.configure( - laboratory, - mainFactory = FeatureFactory.featureGenerated(), - externalFactories = mapOf("Camera" to FeatureFactory.cameraFeatureGenerated()) + laboratory, + mainFactory = FeatureFactory.featureGenerated(), + externalFactories = mapOf("Camera" to FeatureFactory.cameraFeatureGenerated()), ) } diff --git a/samples/settings.gradle.kts b/samples/settings.gradle.kts new file mode 100644 index 000000000..34ac03dce --- /dev/null +++ b/samples/settings.gradle.kts @@ -0,0 +1,51 @@ +pluginManagement { + repositories { + mavenCentral() + google() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + library("laboratory-runtime", "io.mehow.laboratory:laboratory:+") + library("laboratory-sharedPreferences", "io.mehow.laboratory:laboratory-shared-preferences:+") + library("laboratory-dataStore", "io.mehow.laboratory:laboratory-data-store:+") + library("laboratory-generator", "io.mehow.laboratory:laboratory-generator:+") + library("laboratory-inspector", "io.mehow.laboratory:laboratory-inspector:+") + library("laboratory-hyperionPlugin", "io.mehow.laboratory:laboratory-hyperion-plugin:+") + plugin("laboratory", "io.mehow.laboratory").version("+") + } + } + + repositories { + mavenCentral() + google() + } +} + +includeBuild("..") { + dependencySubstitution { + substitute(module("io.mehow.laboratory:laboratory")).using(project(":laboratory:runtime")) + substitute(module("io.mehow.laboratory:laboratory-shared-preferences")).using(project(":laboratory:shared-preferences")) + substitute(module("io.mehow.laboratory:laboratory-data-store")).using(project(":laboratory:data-store")) + substitute(module("io.mehow.laboratory:laboratory-generator")).using(project(":laboratory:generator")) + substitute(module("io.mehow.laboratory:laboratory-gradle-plugin")).using(project(":laboratory:gradle-plugin")) + substitute(module("io.mehow.laboratory:laboratory-inspector")).using(project(":laboratory:inspector")) + substitute(module("io.mehow.laboratory:laboratory-hyperion-plugin")).using(project(":laboratory:hyperion-plugin")) + } +} + +rootProject.name = "samples-root" + +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +include(":basic") +include(":default-option") +include(":supervision") +include(":multi-module") +include(":multi-module:multi-module-a") +include(":multi-module:multi-module-b") +include(":multi-module:multi-module-c") diff --git a/samples/supervision/build.gradle b/samples/supervision/build.gradle deleted file mode 100644 index db86ed265..000000000 --- a/samples/supervision/build.gradle +++ /dev/null @@ -1,105 +0,0 @@ -plugins { - id "com.android.application" - id "org.jetbrains.kotlin.android" - id "io.mehow.laboratory" -} - -android { - namespace "io.mehow.laboratory.sample.supervision" - - signingConfigs { - config { - keyAlias "mehow-io" - keyPassword "mehow-io" - storeFile file("../mehow-io.keystore") - storePassword "mehow-io" - } - } - - defaultConfig { - applicationId "io.mehow.laboratory.sample.supervision" - - targetSdkVersion libs.versions.androidBuild.targetSdk.get().toInteger() - - versionCode 1 - versionName "1.0.0" - - signingConfig signingConfigs.config - } - - buildFeatures { - viewBinding true - } - - buildTypes.debug.setMatchingFallbacks "release" - - variantFilter { - setIgnore it.name != "debug" - } - - compileOptions { - sourceCompatibility JavaConfig.code - targetCompatibility JavaConfig.code - } - - kotlinOptions { - jvmTarget = JavaConfig.name - freeCompilerArgs += [ - "-Xjvm-default=all", - ] - } -} - -laboratory { - packageName = "io.mehow.laboratory.sample.supervision" - - featureFactory() - - feature("Theming") { - withDefaultOption("Default") - - withOption("Christmas") { christmas -> - christmas.feature("ChristmasGreeting") { greeting -> - greeting.withDefaultOption("Disabled") - greeting.withOption("Hello") - greeting.withOption("HoHoHo") - } - - christmas.feature("ChristmasBackground") { background -> - background.withDefaultOption("Disabled") - background.withOption("Reindeer") - background.withOption("Snowman") - } - } - - withOption("Halloween") { halloween -> - halloween.feature("SpookyMusic") { spookyMusic -> - spookyMusic.withDefaultOption("Disabled") - spookyMusic.withOption("Graveyard") - spookyMusic.withOption("HauntedHouse") - spookyMusic.withOption("OldCastle") - } - - halloween.feature("WitchChance") { witchChance -> - witchChance.withDefaultOption("Chance00") - witchChance.withOption("Chance20") - witchChance.withOption("Chance50") - witchChance.withOption("Chance100") - } - - halloween.feature("CandyArt") { candy -> - candy.withDefaultOption("Disabled") - candy.withOption("ChocolateGhost") - candy.withOption("MarshmallowGhost") - } - } - } -} - -dependencies { - implementation libs.kotlin.x.coroutinesAndroid - implementation libs.android.material - implementation libs.hyperion.core - implementation libs.laboratory.dataStore - implementation libs.laboratory.hyperionPlugin -} diff --git a/samples/supervision/build.gradle.kts b/samples/supervision/build.gradle.kts new file mode 100644 index 000000000..92ffd6297 --- /dev/null +++ b/samples/supervision/build.gradle.kts @@ -0,0 +1,65 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.laboratory) + alias(libs.plugins.detekt) + alias(libs.plugins.spotless) +} + +android { + namespace = "io.mehow.laboratory.sample.supervision" +} + +laboratory { + packageName = "io.mehow.laboratory.sample.supervision" + + featureFactory() + + feature("Theming") { + withDefaultOption("Default") + + withOption("Christmas") { + feature("ChristmasGreeting") { + withDefaultOption("Disabled") + withOption("Hello") + withOption("HoHoHo") + } + + feature("ChristmasBackground") { + withDefaultOption("Disabled") + withOption("Reindeer") + withOption("Snowman") + } + } + + withOption("Halloween") { + feature("SpookyMusic") { + withDefaultOption("Disabled") + withOption("Graveyard") + withOption("HauntedHouse") + withOption("OldCastle") + } + + feature("WitchChance") { + withDefaultOption("Chance00") + withOption("Chance20") + withOption("Chance50") + withOption("Chance100") + } + + feature("CandyArt") { + withDefaultOption("Disabled") + withOption("ChocolateGhost") + withOption("MarshmallowGhost") + } + } + } +} + +dependencies { + implementation(libs.kotlinx.coroutinesAndroid) + implementation(libs.android.material) + implementation(libs.hyperion.core) + implementation(libs.laboratory.dataStore) + implementation(libs.laboratory.hyperionPlugin) +} diff --git a/samples/supervision/src/main/kotlin/io/mehow/laboratory/sample/supervision/Activity.kt b/samples/supervision/src/main/kotlin/io/mehow/laboratory/sample/supervision/Activity.kt index 128671ace..63e9efc02 100644 --- a/samples/supervision/src/main/kotlin/io/mehow/laboratory/sample/supervision/Activity.kt +++ b/samples/supervision/src/main/kotlin/io/mehow/laboratory/sample/supervision/Activity.kt @@ -4,8 +4,8 @@ import android.os.Bundle import android.widget.TextView import io.mehow.laboratory.Feature import io.mehow.laboratory.inspector.LaboratoryActivity -import io.mehow.laboratory.sample.supervision.databinding.MainBinding import io.mehow.laboratory.sample.supervision.Application.Companion.laboratory +import io.mehow.laboratory.sample.supervision.databinding.MainBinding import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.launchIn @@ -38,8 +38,8 @@ class Activity : AndroidActivity() { private inline fun > TextView.observeFeature() { laboratory.observe() - .map { "${it.javaClass.simpleName}: $it" } - .onEach { text = it } - .launchIn(mainScope) + .map { "${it.javaClass.simpleName}: $it" } + .onEach { text = it } + .launchIn(mainScope) } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 0dab3617e..4916ed4a1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,45 +1,26 @@ -import com.android.build.api.dsl.SettingsExtension - pluginManagement { repositories { mavenCentral() google() + gradlePluginPortal() } } dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { mavenCentral() google() - gradlePluginPortal() - } - - versionCatalogs { - create("libs") { - from(files("library/gradle/dependencies.toml")) - } } } -plugins { - id("com.android.settings") version "8.0.2" -} - -@Suppress("UnstableApiUsage") -extensions.getByType(SettingsExtension::class).apply { - compileSdk = 33 - minSdk = 21 -} +rootProject.name = "laboratory-root" -include(":samples:basic") -include(":samples:default-option") -include(":samples:multi-module") -include(":samples:multi-module:multi-module-a") -include(":samples:multi-module:multi-module-b") -include(":samples:multi-module:multi-module-c") -include(":samples:firebase") -include(":samples:supervision") -include(":samples:ci-check") +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") -includeBuild("library") +include(":laboratory:runtime") +include(":laboratory:shared-preferences") +include(":laboratory:data-store") +include(":laboratory:generator") +include(":laboratory:gradle-plugin") +include(":laboratory:inspector") +include(":laboratory:hyperion-plugin")