Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 11 additions & 40 deletions allure-kotlin-android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,17 @@ description = "Allure Kotlin Android Integration"

plugins {
id("com.android.library")
kotlin("android")
`maven-publish`
signing
}

apply(plugin = "maven-publish")

android {
compileSdkVersion(Versions.Android.compileSdk)
namespace = "io.qameta.allure.android"
compileSdk = Versions.Android.compileSdk
defaultConfig {
minSdkVersion(Versions.Android.minSdk)
targetSdkVersion(Versions.Android.targetSdk)
versionCode = 1
versionName = version as String
minSdk = Versions.Android.minSdk

consumerProguardFiles("consumer-rules.pro")
}
Expand All @@ -26,6 +23,13 @@ android {
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}

publishing {
singleVariant("release") {
withSourcesJar()
withJavadocJar()
}
}
}

dependencies {
Expand All @@ -35,47 +39,14 @@ dependencies {
implementation("androidx.test:runner:${Versions.Android.Test.runner}")
implementation("androidx.multidex:multidex:${Versions.Android.multiDex}")
implementation("androidx.test.uiautomator:uiautomator:${Versions.Android.Test.uiAutomator}")
}

tasks.register<Javadoc>("androidJavadocs") {
val androidLibrary = project.the(com.android.build.gradle.LibraryExtension::class)

source(androidLibrary.sourceSets["main"].java.srcDirs)
classpath += project.files(androidLibrary.bootClasspath.joinToString(File.pathSeparator))
androidLibrary.libraryVariants.find { it.name == "release" }?.apply {
classpath += javaCompileProvider.get().classpath
}

exclude("**/R.html", "**/R.*.html", "**/index.html")

val stdOptions = options as StandardJavadocDocletOptions
stdOptions.addBooleanOption("Xdoclint:-missing", true)
stdOptions.links(
"http://docs.oracle.com/javase/7/docs/api/",
"http://developer.android.com/reference/",
"http://hc.apache.org/httpcomponents-client-5.0.x/httpclient5/apidocs/",
"http://hc.apache.org/httpcomponents-core-5.0.x/httpcore5/apidocs/")
}

tasks.register<Jar>("androidJavadocsJar") {
val javadocTask = tasks.getByName<Javadoc>("androidJavadocs")
dependsOn(javadocTask)
archiveClassifier.set("javadoc")
from(javadocTask.destinationDir)
}

tasks.register<Jar>("androidSourcesJar") {
archiveClassifier.set("sources")
from(android.sourceSets["main"].java.srcDirs)
compileOnly("org.robolectric:robolectric:${Versions.Android.Test.robolectric}")
}

afterEvaluate {
publishing {
publications {
create<MavenPublication>("maven") {
from(components["release"])
artifact(tasks.getByName<Jar>("androidJavadocsJar"))
artifact(tasks.getByName<Jar>("androidSourcesJar"))

pom {
name.set(project.name)
Expand Down
2 changes: 1 addition & 1 deletion allure-kotlin-android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<manifest package="io.qameta.allure.android" />
<manifest />
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ internal val uiDevice: UiDevice?

internal fun createTemporaryFile(prefix: String = "temp", suffix: String? = null): File {
val cacheDir = InstrumentationRegistry.getInstrumentation().targetContext.cacheDir
return createTempFile(prefix, suffix, cacheDir)
return File.createTempFile(prefix, suffix, cacheDir)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,25 @@ import io.qameta.allure.kotlin.util.PropertiesUtils
import org.junit.runner.*
import org.junit.runner.manipulation.*
import org.junit.runner.notification.*
import org.junit.runners.model.FrameworkMethod
import org.robolectric.RobolectricTestRunner
import org.robolectric.internal.bytecode.InstrumentationConfiguration

/**
* Wrapper over [AndroidJUnit4] that attaches the [AllureJunit4] listener
* Wrapper that attaches the [AllureJunit4] listener.
*
* For device tests, delegates to [AndroidJUnit4].
* For Robolectric tests, delegates to [AllureRobolectricRunner] which excludes core
* allure packages from Robolectric's SandboxClassLoader so that the test code and
* the listener share the same [Allure] singleton.
*/
open class AllureAndroidJUnit4(clazz: Class<*>) : Runner(), Filterable, Sortable {

private val delegate = AndroidJUnit4(clazz)
private val delegate: Runner = if (isDeviceTest()) {
AndroidJUnit4(clazz)
} else {
AllureRobolectricRunner(clazz)
}

override fun run(notifier: RunNotifier?) {
createListener()?.let {
Expand Down Expand Up @@ -65,9 +77,9 @@ open class AllureAndroidJUnit4(clazz: Class<*>) : Runner(), Filterable, Sortable

override fun getDescription(): Description = delegate.description

override fun filter(filter: Filter?) = delegate.filter(filter)
override fun filter(filter: Filter?) = (delegate as Filterable).filter(filter)

override fun sort(sorter: Sorter?) = delegate.sort(sorter)
override fun sort(sorter: Sorter?) = (delegate as Sortable).sort(sorter)
}

/**
Expand Down Expand Up @@ -115,3 +127,27 @@ private val useTestStorage: Boolean
.getProperty("allure.results.useTestStorage", "false")
.toBoolean()

/**
* Custom [RobolectricTestRunner] that excludes core allure packages from Robolectric's
* SandboxClassLoader.
*
* By default, Robolectric loads all non-system classes via its SandboxClassLoader,
* creating separate class instances from the system ClassLoader. This causes the
* [Allure] singleton in the test code to be a different instance from the one used
* by the JUnit listener, so steps and attachments are not recorded.
*
* Only `io.qameta.allure.kotlin` is excluded (not the entire `io.qameta.allure`
* package) because `io.qameta.allure.android` references `androidx.test` classes.
* Excluding it from the sandbox would cause `androidx.test` to be loaded by the
* parent ClassLoader, leading to classloader conflicts (e.g.
* [java.util.ServiceConfigurationError] with `ActivityInvoker`).
*/
private open class AllureRobolectricRunner(clazz: Class<*>) : RobolectricTestRunner(clazz) {

override fun createClassLoaderConfig(method: FrameworkMethod): InstrumentationConfiguration {
return InstrumentationConfiguration.Builder(super.createClassLoaderConfig(method))
.doNotAcquirePackage("io.qameta.allure.kotlin")
.build()
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,6 @@ open class AllureLifecycle @JvmOverloads constructor(
* @param uuid the uuid of test case to start.
*/
fun startTestCase(uuid: String) {
threadContext.clear()
val testResult = storage.getTestResult(uuid) ?: return Unit.also {
LOGGER.error("Could not start test case: test case with uuid $uuid is not scheduled")
}
Expand All @@ -264,7 +263,7 @@ open class AllureLifecycle @JvmOverloads constructor(
stage = Stage.RUNNING
start = System.currentTimeMillis()
}
threadContext.start(uuid)
threadContext.startRoot(uuid)
notifier.afterTestStart(testResult)
}

Expand Down Expand Up @@ -313,7 +312,7 @@ open class AllureLifecycle @JvmOverloads constructor(
stage = Stage.FINISHED
stop = System.currentTimeMillis()
}
threadContext.clear()
threadContext.stopRoot(uuid)
notifier.afterTestStop(testResult)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,81 @@ import java.util.*
/**
* Storage that stores information about not finished tests and steps.
*
* Handles cross-thread scenarios (e.g. Robolectric) where the test case is started
* on one thread but the test body runs on another. The active root UUID is tracked
* in a shared volatile field, and the per-thread step stack falls back to it
* when the thread-local context is empty or stale.
*/
class AllureThreadContext {
private val context = Context()
@Volatile
private var activeRoot: String? = null

/**
* Returns last (most recent) uuid.
* Returns last (most recent) uuid — current step, or test case if no steps.
* Falls back to the active root when the thread-local context is empty or stale.
*/
val current: String?
get() = context.get().firstOrNull()
get() {
val steps = context.get()
if (steps.isNotEmpty()) {
if (steps.last() == activeRoot) {
return steps.first()
}
context.remove()
}
return activeRoot
}

/**
* Returns first (oldest) uuid.
* Returns first (oldest) uuid — the root test case.
* Falls back to the active root when the thread-local context is empty or stale.
*/
val root: String?
get() = context.get().lastOrNull()
get() {
val steps = context.get()
if (steps.isNotEmpty()) {
if (steps.last() == activeRoot) {
return steps.last()
}
context.remove()
}
return activeRoot
}

/**
* Adds new uuid.
* Registers a root context (test case) and initializes the thread-local stack.
*/
fun start(uuid: String) {
fun startRoot(uuid: String) {
activeRoot = uuid
context.remove()
context.get().push(uuid)
}

/**
* Unregisters a root context (test case) and clears the thread-local stack.
*/
fun stopRoot(uuid: String) {
if (activeRoot == uuid) {
activeRoot = null
}
context.remove()
}

/**
* Adds new uuid (step) to the current thread's stack.
* If the stack is empty or stale (cross-thread scenario), injects the active root first.
*/
fun start(uuid: String) {
val steps = context.get()
val root = activeRoot
if (steps.isEmpty() || steps.last() != root) {
steps.clear()
root?.let { steps.push(it) }
}
steps.push(uuid)
}

/**
* Removes latest added uuid. Ignores empty context.
*
Expand Down
22 changes: 12 additions & 10 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
java
signing
`maven-publish`

id("io.github.gradle-nexus.publish-plugin") version "1.1.0"
id("io.github.gradle-nexus.publish-plugin") version "2.0.0"

kotlin("jvm") version Versions.kotlin
kotlin("plugin.serialization") version Versions.kotlin
Expand All @@ -16,7 +19,6 @@ buildscript {
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}")
classpath("org.jetbrains.kotlin:kotlin-android-extensions:${Versions.kotlin}")
classpath("com.android.tools.build:gradle:${Versions.Android.gradlePlugin}")
}
}
Expand All @@ -38,9 +40,9 @@ allprojects {
google()
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_6.toString()
tasks.withType<KotlinCompile>().configureEach {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
}
Expand All @@ -57,9 +59,9 @@ configure(subprojects
implementation(kotlin("stdlib"))
}

configure<JavaPluginConvention> {
sourceCompatibility = JavaVersion.VERSION_1_6
targetCompatibility = JavaVersion.VERSION_1_6
configure<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}

tasks.jar {
Expand All @@ -71,12 +73,12 @@ configure(subprojects
}
}

val sourceJar by tasks.creating(Jar::class) {
val sourceJar by tasks.registering(Jar::class) {
from(sourceSets.getByName("main").allSource)
archiveClassifier.set("sources")
}

val javadocJar by tasks.creating(Jar::class) {
val javadocJar by tasks.registering(Jar::class) {
from(tasks.getByName("javadoc"))
archiveClassifier.set("javadoc")
}
Expand Down
4 changes: 0 additions & 4 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,3 @@ repositories {
plugins {
`kotlin-dsl`
}

kotlinDslPluginOptions {
experimentalWarning.set(false)
}
Loading