From ab7394c39371ea02f80e9a0a17b950cbf26e505c Mon Sep 17 00:00:00 2001 From: Mike Cumings Date: Tue, 20 Aug 2024 14:47:00 -0700 Subject: [PATCH] Address Issue #258: Configuration predicate to control dependency graph creation Adds a new configuration option to allow the consumer to provision of a `Predicate` instance to customize what `Configuration` instances should be considered when building the dependency graph. --- README.md | 6 ++ .../AffectedModuleConfiguration.kt | 16 ++- .../AffectedModuleDetectorPlugin.kt | 2 +- .../AlwaysConfigurationPredicate.kt | 14 +++ .../DependencyTracker.kt | 34 ++++--- .../AffectedModuleConfigurationTest.kt | 29 +++++- .../AffectedModuleDetectorImplTest.kt | 59 ++++++++++- .../AffectedModuleDetectorPluginTest.kt | 12 +-- .../mocks/MockObjectFactory.kt | 98 ++++++++++++++++++ .../mocks/MockProperty.kt | 99 +++++++++++++++++++ .../mocks/PropertySource.kt | 23 +++++ 11 files changed, 366 insertions(+), 26 deletions(-) create mode 100644 affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AlwaysConfigurationPredicate.kt create mode 100644 affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/mocks/MockObjectFactory.kt create mode 100644 affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/mocks/MockProperty.kt create mode 100644 affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/mocks/PropertySource.kt diff --git a/README.md b/README.md index 371df4fc..85c77d73 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,11 @@ affectedModuleDetector { "Run static analysis tool without auto-correction by Impact analysis" ) ] + configurationPredicate.set(new Predicate() { + boolean test(Configuration configuration) { + return !configuration.name.contains("somethingToExclude") + } + }) } ``` @@ -124,6 +129,7 @@ affectedModuleDetector { - `includeUncommitted`: If uncommitted files should be considered affected - `top`: The top of the git log to use. Must be used in combination with configuration `includeUncommitted = false` - `customTasks`: set of [CustomTask](https://github.com/dropbox/AffectedModuleDetector/blob/main/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleConfiguration.kt) + - `configurationPredicate`: A predicate to filter configurations that should be considered for the dependency graph. By default, all configurations are considered. By default, the Detector will look for `assembleAndroidDebugTest`, `connectedAndroidDebugTest`, and `testDebug`. Modules can specify a configuration block to specify which variant tests to run: ```groovy diff --git a/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleConfiguration.kt b/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleConfiguration.kt index 7d309c33..4eb785d2 100644 --- a/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleConfiguration.kt +++ b/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleConfiguration.kt @@ -1,9 +1,13 @@ package com.dropbox.affectedmoduledetector import com.dropbox.affectedmoduledetector.util.toOsSpecificPath +import org.gradle.api.artifacts.Configuration +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property import java.io.File +import java.util.function.Predicate -class AffectedModuleConfiguration { +class AffectedModuleConfiguration(objectFactory: ObjectFactory) { /** * Implementation of [AffectedModuleTaskType] for easy adding of custom gradle task to @@ -44,6 +48,16 @@ class AffectedModuleConfiguration { */ var customTasks = emptySet() + /** + * Predicate to determine if a configuration should be considered or ignored. This predicate + * will be called for every configuration defined by each project module. By default, + * all configurations are considered. + */ + @Suppress("UNCHECKED_CAST") // Erasure in the API results in: Property> + val configurationPredicate: Property> = + (objectFactory.property(Predicate::class.java) as Property>) + .convention(AlwaysConfigurationPredicate()) + /** * Folder to place the log in */ diff --git a/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorPlugin.kt b/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorPlugin.kt index cc6960b3..25d03a7c 100644 --- a/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorPlugin.kt +++ b/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorPlugin.kt @@ -62,7 +62,7 @@ class AffectedModuleDetectorPlugin : Plugin { private fun registerMainConfiguration(project: Project) { project.extensions.add( AffectedModuleConfiguration.name, - AffectedModuleConfiguration() + AffectedModuleConfiguration(project.objects) ) } diff --git a/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AlwaysConfigurationPredicate.kt b/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AlwaysConfigurationPredicate.kt new file mode 100644 index 00000000..a80d206f --- /dev/null +++ b/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AlwaysConfigurationPredicate.kt @@ -0,0 +1,14 @@ +package com.dropbox.affectedmoduledetector + +import org.gradle.api.artifacts.Configuration +import java.util.function.Predicate + +/** + * Default implementation of a [Configuration] [Predicate] that always returns true, indicating + * that all configurations should be considered. + */ +internal class AlwaysConfigurationPredicate : Predicate { + override fun test(t: Configuration): Boolean { + return true + } +} diff --git a/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/DependencyTracker.kt b/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/DependencyTracker.kt index 3d235cfd..0b79e7fe 100644 --- a/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/DependencyTracker.kt +++ b/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/DependencyTracker.kt @@ -32,24 +32,30 @@ class DependencyTracker constructor( private val rootProject: Project, private val logger: Logger? ) { + private val configuration: AffectedModuleConfiguration by lazy { + rootProject.extensions.getByType(AffectedModuleConfiguration::class.java) + } + private val dependentList: Map> by lazy { val result = mutableMapOf>() rootProject.subprojects.forEach { project -> logger?.info("checking ${project.path} for dependencies") - project.configurations.forEach { config -> - logger?.info("checking config ${project.path}/$config for dependencies") - config - .dependencies - .filterIsInstance(ProjectDependency::class.java) - .forEach { - logger?.info( - "there is a dependency from ${project.path} to " + - it.dependencyProject.path - ) - result.getOrPut(it.dependencyProject) { mutableSetOf() } - .add(project) - } - } + project.configurations + .filter(configuration.configurationPredicate.get()::test) + .forEach { config -> + logger?.info("checking config ${project.path}/$config for dependencies") + config + .dependencies + .filterIsInstance(ProjectDependency::class.java) + .forEach { + logger?.info( + "there is a dependency from ${project.path} to " + + it.dependencyProject.path + ) + result.getOrPut(it.dependencyProject) { mutableSetOf() } + .add(project) + } + } } result } diff --git a/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleConfigurationTest.kt b/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleConfigurationTest.kt index ccb14f9e..2168af53 100644 --- a/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleConfigurationTest.kt +++ b/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleConfigurationTest.kt @@ -1,6 +1,8 @@ package com.dropbox.affectedmoduledetector +import com.dropbox.affectedmoduledetector.mocks.MockObjectFactory import com.google.common.truth.Truth.assertThat +import org.gradle.api.artifacts.Configuration import org.junit.Assert.fail import org.junit.Before import org.junit.Rule @@ -9,6 +11,7 @@ import org.junit.rules.TemporaryFolder import org.junit.runner.RunWith import org.junit.runners.JUnit4 import java.io.File +import java.util.function.Predicate @RunWith(JUnit4::class) class AffectedModuleConfigurationTest { @@ -26,7 +29,7 @@ class AffectedModuleConfigurationTest { @Before fun setup() { - config = AffectedModuleConfiguration() + config = AffectedModuleConfiguration(MockObjectFactory()) } @Test @@ -324,4 +327,28 @@ class AffectedModuleConfigurationTest { assert(actual.first().taskDescription == "Description of fake task") } + + @Test + fun `GIVEN AffectedModuleConfiguration WHEN configuration predicate is set THEN is configuration predicate`() { + // GIVEN + val expected = Predicate { false } + config.configurationPredicate.set(expected) + + // WHEN + val predicate = config.configurationPredicate.get() + + // THEN + assertThat(predicate).isSameInstanceAs(expected) + } + + @Test + fun `GIVEN AffectedModuleConfiguration WHEN configuration predicate is not set THEN is default`() { + // GIVEN default configuration + + // WHEN + val predicate = config.configurationPredicate.get() + + // THEN + assertThat(predicate).isInstanceOf(AlwaysConfigurationPredicate::class.java) + } } diff --git a/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorImplTest.kt b/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorImplTest.kt index ee272554..f41a4d05 100644 --- a/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorImplTest.kt +++ b/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorImplTest.kt @@ -1,5 +1,6 @@ package com.dropbox.affectedmoduledetector +import com.dropbox.affectedmoduledetector.mocks.MockObjectFactory import com.google.common.truth.Truth import org.gradle.api.Project import org.gradle.api.plugins.ExtraPropertiesExtension @@ -220,10 +221,13 @@ class AffectedModuleDetectorImplTest { val p19config = p19.configurations.create("p19config") p19config.dependencies.add(p19.dependencies.project(mutableMapOf("path" to ":p18"))) - affectedModuleConfiguration = AffectedModuleConfiguration().also { + affectedModuleConfiguration = AffectedModuleConfiguration(MockObjectFactory()).also { it.baseDir = tmpDir.absolutePath it.pathsAffectingAllModules = pathsAffectingAllModules } + listOf(root, root2, root3).forEach { rootProject -> + rootProject.extensions.add(AffectedModuleConfiguration.name, affectedModuleConfiguration) + } } @Test @@ -1329,7 +1333,7 @@ class AffectedModuleDetectorImplTest { @Test fun `GIVEN affected module configuration WHEN invalid path THEN throw exception`() { // GIVEN - val config = AffectedModuleConfiguration().also { + val config = AffectedModuleConfiguration(MockObjectFactory()).also { it.baseDir = tmpFolder.root.absolutePath } @@ -1350,7 +1354,7 @@ class AffectedModuleDetectorImplTest { @Test fun `GIVEN affected module configuration WHEN valid paths THEN return paths`() { // GIVEN - val config = AffectedModuleConfiguration().also { + val config = AffectedModuleConfiguration(MockObjectFactory()).also { it.baseDir = tmpFolder.root.absolutePath } @@ -1522,6 +1526,55 @@ class AffectedModuleDetectorImplTest { ) } + @Test + fun `GIVEN upward configuration reference from p2 to p6 WHEN no predicate is supplied THEN p2 is affected`() { + p2.configurations.create("p2-upward-p6") { config -> + config.dependencies.add(p2.dependencies.project(mapOf("path" to p6.path))) + } + val detector = AffectedModuleDetectorImpl( + rootProject = root, + logger = logger, + ignoreUnknownProjects = false, + projectSubset = ProjectSubset.ALL_AFFECTED_PROJECTS, + modules = null, + injectedGitClient = MockGitClient( + changedFiles = listOf( + convertToFilePath("d1/d3/d6", "foo.java") + ), + tmpFolder = tmpFolder.root + ), + config = affectedModuleConfiguration + ) + Truth.assertThat(detector.shouldInclude(p2)).isTrue() + Truth.assertThat(detector.shouldInclude(p6)).isTrue() + } + + @Test + fun `GIVEN upward configuration reference from p2 to p6 WHEN predicate filtered THEN p2 is unaffected`() { + p2.configurations.create("p2-upward-p6") { config -> + config.dependencies.add(p2.dependencies.project(mapOf("path" to p6.path))) + } + affectedModuleConfiguration.configurationPredicate.set { configuration -> + !configuration.name.contains("-upward-") + } + val detector = AffectedModuleDetectorImpl( + rootProject = root, + logger = logger, + ignoreUnknownProjects = false, + projectSubset = ProjectSubset.ALL_AFFECTED_PROJECTS, + modules = null, + injectedGitClient = MockGitClient( + changedFiles = listOf( + convertToFilePath("d1/d3/d6", "foo.java") + ), + tmpFolder = tmpFolder.root + ), + config = affectedModuleConfiguration + ) + Truth.assertThat(detector.shouldInclude(p2)).isFalse() + Truth.assertThat(detector.shouldInclude(p6)).isTrue() + } + // For both Linux/Windows fun convertToFilePath(vararg list: String): String { return list.toList().joinToString(File.separator) diff --git a/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorPluginTest.kt b/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorPluginTest.kt index 3ac25fd2..5bfd884e 100644 --- a/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorPluginTest.kt +++ b/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorPluginTest.kt @@ -1,5 +1,6 @@ package com.dropbox.affectedmoduledetector +import com.dropbox.affectedmoduledetector.mocks.MockObjectFactory import com.google.common.truth.Truth.assertThat import org.gradle.api.Project import org.gradle.api.internal.plugins.PluginApplicationException @@ -10,7 +11,6 @@ import org.junit.Test import org.junit.rules.TemporaryFolder import org.junit.runner.RunWith import org.junit.runners.JUnit4 -import java.lang.IllegalStateException @RunWith(JUnit4::class) class AffectedModuleDetectorPluginTest { @@ -102,7 +102,7 @@ class AffectedModuleDetectorPluginTest { @Test fun `GIVEN affected module detector plugin WHEN register_custom_task is called AND AffectedModuleConfiguration customTask is not empty THEN task is added`() { // GIVEN - val configuration = AffectedModuleConfiguration() + val configuration = AffectedModuleConfiguration(MockObjectFactory()) configuration.customTasks = setOf(fakeTask) rootProject.extensions.add(AffectedModuleConfiguration.name, configuration) @@ -122,7 +122,7 @@ class AffectedModuleDetectorPluginTest { @Test fun `GIVEN affected module detector plugin WHEN registerCustomTasks is called AND AffectedModuleConfiguration customTask is empty THEN task isn't added`() { // GIVEN - val configuration = AffectedModuleConfiguration() + val configuration = AffectedModuleConfiguration(MockObjectFactory()) rootProject.extensions.add(AffectedModuleConfiguration.name, configuration) val plugin = AffectedModuleDetectorPlugin() @@ -144,7 +144,7 @@ class AffectedModuleDetectorPluginTest { @Test fun `GIVEN affected module detector plugin WHEN registerTestTasks THEN task all task added`() { // GIVEN - val configuration = AffectedModuleConfiguration() + val configuration = AffectedModuleConfiguration(MockObjectFactory()) rootProject.extensions.add(AffectedModuleConfiguration.name, configuration) val plugin = AffectedModuleDetectorPlugin() @@ -168,7 +168,7 @@ class AffectedModuleDetectorPluginTest { @Test fun `GIVEN affected module detector plugin WHEN registerTestTasks called THEN added all tasks from InternalTaskType`() { // GIVEN - val configuration = AffectedModuleConfiguration() + val configuration = AffectedModuleConfiguration(MockObjectFactory()) rootProject.extensions.add(AffectedModuleConfiguration.name, configuration) val plugin = AffectedModuleDetectorPlugin() val availableTaskVariants = 3 // runAffectedAndroidTests, assembleAffectedAndroidTests and runAffectedUnitTests @@ -187,7 +187,7 @@ class AffectedModuleDetectorPluginTest { fun `GIVEN affected module detector plugin WHEN registerCustomTasks called THEN added all tasks from FakeTaskType`() { // GIVEN val givenCustomTasks = setOf(fakeTask, fakeTask.copy(commandByImpact = "otherCommand")) - val configuration = AffectedModuleConfiguration() + val configuration = AffectedModuleConfiguration(MockObjectFactory()) configuration.customTasks = givenCustomTasks rootProject.extensions.add(AffectedModuleConfiguration.name, configuration) val plugin = AffectedModuleDetectorPlugin() diff --git a/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/mocks/MockObjectFactory.kt b/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/mocks/MockObjectFactory.kt new file mode 100644 index 00000000..925dbd80 --- /dev/null +++ b/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/mocks/MockObjectFactory.kt @@ -0,0 +1,98 @@ +package com.dropbox.affectedmoduledetector.mocks + +import org.gradle.api.DomainObjectSet +import org.gradle.api.ExtensiblePolymorphicDomainObjectContainer +import org.gradle.api.Named +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.NamedDomainObjectFactory +import org.gradle.api.NamedDomainObjectList +import org.gradle.api.NamedDomainObjectSet +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.ConfigurableFileTree +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.file.SourceDirectorySet +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty + +/** + * Mock implementation of Gradle's [ObjectFactory] for testing purposes. + */ +class MockObjectFactory : ObjectFactory { + override fun named(type: Class, name: String): T { + TODO("Not yet implemented") + } + + override fun newInstance(type: Class, vararg parameters: Any): T { + TODO("Not yet implemented") + } + + override fun sourceDirectorySet(name: String, displayName: String): SourceDirectorySet { + TODO("Not yet implemented") + } + + override fun fileCollection(): ConfigurableFileCollection { + TODO("Not yet implemented") + } + + override fun fileTree(): ConfigurableFileTree { + TODO("Not yet implemented") + } + + override fun domainObjectContainer(elementType: Class): NamedDomainObjectContainer { + TODO("Not yet implemented") + } + + override fun domainObjectContainer( + elementType: Class, + factory: NamedDomainObjectFactory + ): NamedDomainObjectContainer { + TODO("Not yet implemented") + } + + override fun polymorphicDomainObjectContainer(elementType: Class): ExtensiblePolymorphicDomainObjectContainer { + TODO("Not yet implemented") + } + + override fun domainObjectSet(elementType: Class): DomainObjectSet { + TODO("Not yet implemented") + } + + override fun namedDomainObjectSet(elementType: Class): NamedDomainObjectSet { + TODO("Not yet implemented") + } + + override fun namedDomainObjectList(elementType: Class): NamedDomainObjectList { + TODO("Not yet implemented") + } + + override fun property(valueType: Class): Property { + return MockProperty() + } + + override fun listProperty(elementType: Class): ListProperty { + TODO("Not yet implemented") + } + + override fun setProperty(elementType: Class): SetProperty { + TODO("Not yet implemented") + } + + override fun mapProperty( + keyType: Class, + valueType: Class + ): MapProperty { + TODO("Not yet implemented") + } + + override fun directoryProperty(): DirectoryProperty { + TODO("Not yet implemented") + } + + override fun fileProperty(): RegularFileProperty { + TODO("Not yet implemented") + } +} diff --git a/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/mocks/MockProperty.kt b/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/mocks/MockProperty.kt new file mode 100644 index 00000000..ecb6e8c2 --- /dev/null +++ b/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/mocks/MockProperty.kt @@ -0,0 +1,99 @@ +package com.dropbox.affectedmoduledetector.mocks + +import org.gradle.api.Transformer +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import java.util.concurrent.atomic.AtomicReference +import java.util.function.BiFunction + +/** + * Mock implementation of the Gradle's [ObjectFactory] for testing purposes. + */ +class MockProperty : Property { + private val valueSourceRef = AtomicReference?>() + private val conventionRef = AtomicReference?>() + + override fun get(): T { + return getOrNull() ?: throw IllegalStateException("Property value is not set") + } + + override fun getOrNull(): T? { + return valueSourceRef.get()?.get() + ?: conventionRef.get()?.get() + } + + override fun isPresent(): Boolean { + return getOrNull() != null + } + + @Deprecated("super is deprecated", ReplaceWith("modern stuff")) + override fun forUseAtConfigurationTime(): Provider = this + + override fun finalizeValue() { + // No-op + } + + override fun finalizeValueOnRead() { + // No-op + } + + override fun disallowChanges() { + // No-op + } + + override fun disallowUnsafeRead() { + // No-op + } + + override fun convention(provider: Provider): Property = apply { + conventionRef.set(ProviderSource(provider)) + } + + override fun convention(value: T?): Property = apply { + conventionRef.set(ValueSource(value)) + } + + override fun value(provider: Provider): Property = apply { + set(provider) + } + + override fun value(value: T?): Property = apply { + set(value) + } + + override fun set(provider: Provider) { + valueSourceRef.set(ProviderSource(provider)) + } + + override fun set(value: T?) { + valueSourceRef.set(ValueSource(value)) + } + + override fun zip( + provider: Provider, + biFunction: BiFunction + ): Provider { + TODO("Not yet implemented") + } + + override fun orElse(provider: Provider): Provider = apply { + conventionRef.set(ProviderSource(provider)) + } + + override fun orElse(value: T): Provider = apply { + conventionRef.set(ValueSource(value)) + } + + override fun flatMap(transformer: Transformer, in T>): Provider { + TODO("Not yet implemented") + } + + override fun map(transformer: Transformer): Provider { + TODO("Not yet implemented") + } + + override fun getOrElse(defaultValue: T): T { + return getOrNull() ?: defaultValue + } +} diff --git a/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/mocks/PropertySource.kt b/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/mocks/PropertySource.kt new file mode 100644 index 00000000..7c774390 --- /dev/null +++ b/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/mocks/PropertySource.kt @@ -0,0 +1,23 @@ +package com.dropbox.affectedmoduledetector.mocks + +import org.gradle.api.provider.Provider + +/** + * A property source that can be used to provide a value for a property. This makes it + * a bit easier for [MockProperty] to manage. + */ +sealed interface PropertySource { + fun get(): T? +} + +class ProviderSource(private val provider: Provider) : PropertySource { + override fun get(): T? { + return provider.get() + } +} + +class ValueSource(private val value: T?) : PropertySource { + override fun get(): T? { + return value + } +}