Skip to content

Commit 513a738

Browse files
antonisclaude
andcommitted
test(android): add Gradle test-repro for task-realization regressions
Adds a self-contained Android project under packages/core/test-repro/ that verifies two canary regressions: - CANARY 1 (#5236, react-native-legal): sentry.gradle must not realize lazily-registered tasks by iterating the task container (tasks.findAll). - CANARY 2 (#5698, Fullstory): sentry.gradle must not configure the fullstoryTransformRelease task before AGP's onVariants wires it via toTransformMany(), otherwise the APK lands in build/intermediates/ instead of build/outputs/. Includes stubs for multiple approaches under test: - sentry-main.gradle → tasks.findAll in onVariants (❌ both canaries fail) - sentry-noop.gradle → baseline no-op (✅ both canaries pass) - sentry-named.gradle → tasks.names.contains + tasks.named (✅ our fix) - sentry-configureEach.gradle → tasks.configureEach alternative (✅) - sentry-afterEvaluate.gradle → afterEvaluate + tasks.named (✅) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2b4333b commit 513a738

17 files changed

Lines changed: 654 additions & 0 deletions
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.gradle/
2+
build/
3+
local.properties
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import com.android.build.api.artifact.SingleArtifact
2+
import com.android.build.api.artifact.ArtifactTransformationRequest
3+
4+
apply plugin: 'com.android.application'
5+
6+
// ---------------------------------------------------------------------------
7+
// CANARY 1 — react-native-legal regression (issue #5236)
8+
// Simulates a lazily-registered task from a third-party plugin such as
9+
// AboutLibraries (used by react-native-legal). If sentry.gradle's task
10+
// search realizes this task as a side effect the build should report a
11+
// regression.
12+
// ---------------------------------------------------------------------------
13+
def canaryConfigured = false
14+
tasks.register("sentryCanaryTask") {
15+
// This configuration action must NOT run unless the task is in the
16+
// execution graph — NOT as a side-effect of iterating the task container.
17+
canaryConfigured = true
18+
project.logger.warn("[CANARY] sentryCanaryTask was configured!")
19+
doLast { println "[CANARY] sentryCanaryTask executed" }
20+
}
21+
22+
// ---------------------------------------------------------------------------
23+
// CANARY 2 — Fullstory AGP Artifacts API regression (issue #5698)
24+
// Simulates how Fullstory (and similar plugins) PRE-REGISTER their APK
25+
// transform task, then wire it inside onVariants via toTransformMany().
26+
//
27+
// The key timing check: the task's configuration action should only run
28+
// AFTER Fullstory's onVariants callback has called toTransformMany() — i.e.,
29+
// AGP is allowed to realize the task as part of wiring the artifact chain.
30+
// What must NOT happen is the task being configured BEFORE toTransformMany()
31+
// is called (i.e., as a side-effect of sentry's tasks.findAll).
32+
//
33+
// If the task is configured prematurely (before toTransformMany), its
34+
// input/output directory properties are not yet set by AGP and the APK
35+
// ends up in build/intermediates/ rather than build/outputs/.
36+
// ---------------------------------------------------------------------------
37+
def fullstoryOnVariantsReached = false
38+
def fullstoryConfiguredBeforeWiring = false
39+
def fullstoryTaskProvider = tasks.register("fullstoryTransformRelease", FullstorySimTask) {
40+
// This configuration action runs when the task is realized.
41+
// Correct: runs AFTER fullstoryOnVariantsReached is true (wired by toTransformMany).
42+
// Broken: runs BEFORE fullstoryOnVariantsReached is true (premature, via tasks.findAll).
43+
if (!fullstoryOnVariantsReached) {
44+
fullstoryConfiguredBeforeWiring = true
45+
project.logger.warn("[FULLSTORY-SIM] PREMATURE: fullstoryTransformRelease configured before toTransformMany!")
46+
}
47+
}
48+
49+
// ---------------------------------------------------------------------------
50+
// Apply the sentry approach under test (swap the comment to compare).
51+
// sentry-main.gradle → current main: tasks.findAll in onVariants (❌ expected)
52+
// sentry-pr5690.gradle → PR #5690: tasks.matching + each in afterEvaluate (❌ expected)
53+
// sentry-named.gradle → PR #5714 v1: tasks.named() in onVariants (✅ expected)
54+
// sentry-configureEach.gradle → PR #5714 v2: tasks.configureEach in onVariants (✅ expected)
55+
// sentry-afterEvaluate.gradle → PR #5714 v3: afterEvaluate + tasks.named() (✅ expected)
56+
// ---------------------------------------------------------------------------
57+
// apply from: '../sentry-afterEvaluate.gradle'
58+
// apply from: '../sentry-pr5690.gradle'
59+
// apply from: '../sentry-configureEach.gradle'
60+
// apply from: '../sentry-afterEvaluate.gradle'
61+
apply from: '../sentry-named.gradle'
62+
63+
// ---------------------------------------------------------------------------
64+
// Fullstory's onVariants callback — wires the pre-registered task into the
65+
// APK artifact chain. This callback runs AFTER sentry's onVariants callback
66+
// (because this code is evaluated after the apply above).
67+
// If sentry already realized fullstoryTransformRelease via tasks.findAll the
68+
// task has been configured outside of the artifact API lifecycle and
69+
// toTransformMany() may not wire it correctly (APK lands in intermediates/).
70+
// ---------------------------------------------------------------------------
71+
abstract class FullstorySimTask extends DefaultTask {
72+
@org.gradle.api.tasks.InputDirectory
73+
abstract org.gradle.api.file.DirectoryProperty getInputApkDir()
74+
75+
@org.gradle.api.tasks.OutputDirectory
76+
abstract org.gradle.api.file.DirectoryProperty getOutputApkDir()
77+
78+
// toTransformMany() returns an ArtifactTransformationRequest that must be
79+
// stored on the task and called via submit() in the task action.
80+
@org.gradle.api.tasks.Internal
81+
abstract org.gradle.api.provider.Property<ArtifactTransformationRequest<FullstorySimTask>> getTransformRequest()
82+
83+
@org.gradle.api.tasks.TaskAction
84+
void transform() {
85+
transformRequest.get().submit(this) { builtArtifact ->
86+
// builtArtifact.outputFile is the path of the INPUT apk for this transform.
87+
// We copy it to the output directory and return the output file.
88+
def src = new File(builtArtifact.outputFile)
89+
def dst = new File(outputApkDir.asFile.get(), src.name)
90+
dst.bytes = src.bytes
91+
println "[FULLSTORY-SIM] APK transform ran → ${dst.absolutePath}"
92+
dst
93+
}
94+
}
95+
}
96+
97+
plugins.withId('com.android.application') {
98+
def androidComponents = extensions.getByName("androidComponents")
99+
androidComponents.onVariants(androidComponents.selector().all()) { v ->
100+
if (!v.name.toLowerCase().contains("debug")) {
101+
// Set the flag BEFORE calling toTransformMany so we can detect
102+
// whether the task was configured prematurely (before this point).
103+
fullstoryOnVariantsReached = true
104+
def transformRequest = v.artifacts.use(fullstoryTaskProvider)
105+
.wiredWithDirectories({ t -> t.inputApkDir }, { t -> t.outputApkDir })
106+
.toTransformMany(SingleArtifact.APK.INSTANCE)
107+
// Store the request on the task so the @TaskAction can call submit()
108+
fullstoryTaskProvider.configure { t -> t.transformRequest.set(transformRequest) }
109+
}
110+
}
111+
}
112+
113+
// ---------------------------------------------------------------------------
114+
// Final report
115+
// ---------------------------------------------------------------------------
116+
gradle.buildFinished {
117+
println ""
118+
println "=== REGRESSION REPORT ==="
119+
println ""
120+
if (canaryConfigured) {
121+
println "❌ CANARY 1 (react-native-legal #5236): sentry.gradle realized the canary task."
122+
println " tasks.findAll iterated the container and configured sentryCanaryTask."
123+
} else {
124+
println "✅ CANARY 1 (react-native-legal #5236): sentry.gradle did NOT realize the canary task."
125+
}
126+
println ""
127+
if (fullstoryConfiguredBeforeWiring) {
128+
println "❌ CANARY 2 (Fullstory / AGP Artifacts API #5698): sentry.gradle realized"
129+
println " fullstoryTransformRelease BEFORE Fullstory's onVariants wired it."
130+
println " The task was configured without its AGP-injected artifact paths — APK"
131+
println " may land in build/intermediates/ instead of build/outputs/."
132+
} else {
133+
println "✅ CANARY 2 (Fullstory / AGP Artifacts API #5698): fullstoryTransformRelease"
134+
println " was only configured after toTransformMany() — correct AGP lifecycle."
135+
}
136+
}
137+
138+
android {
139+
namespace 'com.sentry.repro'
140+
compileSdk 34
141+
defaultConfig {
142+
applicationId 'com.sentry.repro'
143+
minSdk 24
144+
targetSdk 34
145+
versionCode 1
146+
versionName '1.0'
147+
}
148+
buildTypes {
149+
release { minifyEnabled false }
150+
}
151+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
<application android:label="SentryRepro" />
4+
</manifest>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
buildscript {
2+
repositories {
3+
google()
4+
mavenCentral()
5+
}
6+
dependencies {
7+
classpath 'com.android.tools.build:gradle:8.2.2'
8+
}
9+
}
Binary file not shown.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
distributionBase=GRADLE_USER_HOME
2+
distributionPath=wrapper/dists
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
4+
networkTimeout=10000
5+
validateDistributionUrl=true
6+
zipStoreBase=GRADLE_USER_HOME
7+
zipStorePath=wrapper/dists
42.7 KB
Binary file not shown.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
distributionBase=GRADLE_USER_HOME
2+
distributionPath=wrapper/dists
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
4+
zipStoreBase=GRADLE_USER_HOME
5+
zipStorePath=wrapper/dists

0 commit comments

Comments
 (0)