Skip to content

Commit

Permalink
feat(sandbox): Add a custom robolectric sandbox builder which reusing…
Browse files Browse the repository at this point in the history
… the class loader (#3)
  • Loading branch information
warnyul authored Mar 8, 2024
1 parent 9662e18 commit beb1b84
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 3 deletions.
12 changes: 12 additions & 0 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions .idea/codeStyles/codeStyleConfig.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ junit5Bom = { module = "org.junit:junit-bom", version.ref = "junit5" }
junit5JupiterApi = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "sources" }
junit5JupiterEngine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "sources" }
junit5PlatformLauncher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "sources" }
kotlinTest = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlinTestJUnit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5", version.ref = "kotlin" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }

[plugins]
Expand All @@ -23,5 +23,5 @@ kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
[bundles]
junit5 = [
"junit5JupiterApi",
"junit5PlatformLauncher"
"junit5PlatformLauncher",
]
2 changes: 1 addition & 1 deletion robolectric-extension/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ dependencies {
implementation libs.junit4
implementation platform(libs.junit5Bom)
implementation libs.bundles.junit5
testImplementation libs.kotlinTest
testImplementation libs.kotlinTestJUnit5
implementation(libs.guava) {
because "CVE-2023-2976 7.1 " +
"Transitive Files or Directories Accessible to External Parties vulnerability with High severity found"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package tech.apter.junit.jupiter.robolectric.internal

import org.robolectric.ApkLoader
import org.robolectric.annotation.SQLiteMode
import org.robolectric.internal.AndroidSandbox
import org.robolectric.internal.AndroidSandbox.SdkSandboxClassLoader
import org.robolectric.internal.ResourcesMode
import org.robolectric.internal.SandboxManager
import org.robolectric.internal.bytecode.ClassInstrumentor
import org.robolectric.internal.bytecode.InstrumentationConfiguration
import org.robolectric.internal.bytecode.ShadowProviders
import org.robolectric.pluginapi.Sdk
import java.util.*
import javax.inject.Inject

internal class JUnit5RobolectricSandboxBuilder @Inject constructor(
private val apkLoader: ApkLoader,
@Suppress("VisibleForTests")
private val testEnvironmentSpec: AndroidSandbox.TestEnvironmentSpec,
private val shadowProviders: ShadowProviders,
) : SandboxManager.SandboxBuilder {
private val logger get() = createLogger()

override fun build(
instrumentationConfig: InstrumentationConfiguration,
runtimeSdk: Sdk,
compileSdk: Sdk,
resourcesMode: ResourcesMode,
sqLiteMode: SQLiteMode.Mode,
): AndroidSandbox {
val sdkSandboxClassLoader = getOrCreateClassLoader(instrumentationConfig, runtimeSdk)
return AndroidSandbox(
runtimeSdk,
compileSdk,
resourcesMode,
apkLoader,
testEnvironmentSpec,
sdkSandboxClassLoader,
shadowProviders,
sqLiteMode,
)
}

private fun getOrCreateClassLoader(
instrumentationConfig: InstrumentationConfiguration,
runtimeSdk: Sdk,
): SdkSandboxClassLoader {
val key = Key(instrumentationConfig, runtimeSdk)
return classLoaderCache.getOrPut(key) {
logger.debug { "${SdkSandboxClassLoader::class.simpleName} instance created" }
SdkSandboxClassLoader(instrumentationConfig, runtimeSdk, ClassInstrumentor())
}
}

private data class Key(private val configuration: InstrumentationConfiguration, private val runtimeSdk: Sdk)

private companion object {
@JvmStatic
private val classLoaderCache = Collections.synchronizedMap<Key, SdkSandboxClassLoader>(mutableMapOf())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package tech.apter.junit.jupiter.robolectric

class RobolectricExtensionTest {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package tech.apter.junit.jupiter.robolectric.internal

import org.junit.jupiter.api.extension.ExtendWith
import org.robolectric.ApkLoader
import org.robolectric.annotation.SQLiteMode
import org.robolectric.internal.AndroidSandbox
import org.robolectric.internal.ResourcesMode
import org.robolectric.internal.bytecode.InstrumentationConfiguration
import org.robolectric.internal.bytecode.ShadowProviders
import org.robolectric.internal.dependency.MavenDependencyResolver
import org.robolectric.pluginapi.Sdk
import org.robolectric.plugins.DefaultSdkProvider
import org.robolectric.versioning.AndroidVersions.T
import org.robolectric.versioning.AndroidVersions.U
import tech.apter.junit.jupiter.robolectric.RobolectricExtension
import kotlin.test.Ignore
import kotlin.test.Test
import kotlin.test.assertIs
import kotlin.test.assertNotSame
import kotlin.test.assertSame

@ExtendWith(RobolectricExtension::class)
@Ignore
class JUnit5RobolectricSandboxBuilderTest {

private val dependencyResolver = MavenDependencyResolver()
private val defaultSdkProvider = DefaultSdkProvider(dependencyResolver)

@Test
fun `Given the same arguments when call build twice then should return two different sandboxes with the same classloader `() {
// Given
val instrumentationConfiguration = createInstrumentationConfiguration()
val runtimeSdk: Sdk = defaultSdkProvider.DefaultSdk(U.SDK_INT, "14", "10818077", "REL", 17)
val compileSdk: Sdk = defaultSdkProvider.DefaultSdk(U.SDK_INT, "14", "10818077", "REL", 17)
val resourcesMode: ResourcesMode = ResourcesMode.BINARY
val sqLiteMode: SQLiteMode.Mode = SQLiteMode.Mode.NATIVE

subjectUnderTest {
// When
val sandbox1 = build(instrumentationConfiguration, runtimeSdk, compileSdk, resourcesMode, sqLiteMode)
// And
val sandbox2 = build(instrumentationConfiguration, runtimeSdk, compileSdk, resourcesMode, sqLiteMode)

// Then
assertIs<AndroidSandbox>(sandbox1)
assertIs<AndroidSandbox>(sandbox2)
assertNotSame(sandbox1, sandbox2)
assertSame(sandbox1.robolectricClassLoader, sandbox2.robolectricClassLoader)
}
}

@Test
fun `Given different arguments when call build twice then should return two different sandboxes with different classloaders`() {
// Given
val instrumentationConfiguration1 = createInstrumentationConfiguration()
val runtimeSdk1: Sdk = defaultSdkProvider.DefaultSdk(U.SDK_INT, "14", "10818077", "REL", 17)
val compileSdk1: Sdk = defaultSdkProvider.DefaultSdk(U.SDK_INT, "14", "10818077", "REL", 17)
val instrumentationConfiguration2 = createInstrumentationConfiguration()
val runtimeSdk2: Sdk = defaultSdkProvider.DefaultSdk(T.SDK_INT, "13", "9030017", "Tiramisu", 9)
val compileSdk2: Sdk = defaultSdkProvider.DefaultSdk(T.SDK_INT, "13", "9030017", "Tiramisu", 9)
val resourcesMode: ResourcesMode = ResourcesMode.BINARY
val sqLiteMode: SQLiteMode.Mode = SQLiteMode.Mode.NATIVE

subjectUnderTest {
val sandbox1 = build(instrumentationConfiguration1, runtimeSdk1, compileSdk1, resourcesMode, sqLiteMode)
val sandbox2 = build(instrumentationConfiguration2, runtimeSdk2, compileSdk2, resourcesMode, sqLiteMode)

assertIs<AndroidSandbox>(sandbox1)
assertIs<AndroidSandbox>(sandbox2)
assertNotSame(sandbox1, sandbox2)
assertNotSame(sandbox1.robolectricClassLoader, sandbox2.robolectricClassLoader)
}
}

private fun subjectUnderTest(
action: JUnit5RobolectricSandboxBuilder.() -> Unit
): JUnit5RobolectricSandboxBuilder = JUnit5RobolectricSandboxBuilder(
ApkLoader(),
AndroidSandbox.TestEnvironmentSpec(),
ShadowProviders(emptyList()),
).apply {
action()
}

companion object {
private fun createInstrumentationConfiguration() =
InstrumentationConfiguration.newBuilder().doNotAcquirePackage("java.")
.doNotAcquirePackage("jdk.internal.")
.doNotAcquirePackage("sun.")
.doNotAcquirePackage("org.robolectric.annotation.")
.doNotAcquirePackage("org.robolectric.internal.")
.doNotAcquirePackage("org.robolectric.pluginapi.")
.doNotAcquirePackage("org.robolectric.util.")
.doNotAcquirePackage("org.junit")
.build()
}
}

0 comments on commit beb1b84

Please sign in to comment.