Skip to content

Commit de9cd6e

Browse files
committed
improved assertions
1 parent 150959a commit de9cd6e

File tree

8 files changed

+196
-52
lines changed

8 files changed

+196
-52
lines changed

core/src/main/kotlin/xyz/block/artifactswap/core/module_selector/ArtifactSwapModuleSelector.kt

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ import xyz.block.artifactswap.core.repository.InstalledArtifact
1818
import xyz.block.artifactswap.core.repository.LocalArtifactRepository
1919
import xyz.block.artifactswap.core.shared_services.git.SquareGit
2020

21+
/** Reason a module was included or excluded during artifact swap selection. */
22+
enum class InclusionReason {
23+
EXPLICITLY_REQUESTED,
24+
ALWAYS_KEEP,
25+
LOCAL_CHANGES,
26+
MISSING_ARTIFACT,
27+
EXCLUDED,
28+
}
29+
2130
/** Metrics about project selection decisions. */
2231
data class SelectionMetrics(
2332
val totalCandidates: Int,
@@ -203,6 +212,11 @@ class RealArtifactSwapModuleSelector(
203212
}
204213
}
205214

215+
// Log each project's inclusion decision at info level
216+
for ((project, reason) in projectDecisions) {
217+
LOGGER.info("Artifact Swap decision: {} -> {}", project.path, reason)
218+
}
219+
206220
val decisionCounts = projectDecisions.groupingBy { it.second }.eachCount()
207221
val selectedProjects =
208222
projectDecisions.filter { it.second != InclusionReason.EXCLUDED }.map { it.first }.toSet()
@@ -221,14 +235,6 @@ class RealArtifactSwapModuleSelector(
221235
selectedProjects to metrics
222236
}
223237

224-
private enum class InclusionReason {
225-
EXPLICITLY_REQUESTED,
226-
ALWAYS_KEEP,
227-
LOCAL_CHANGES,
228-
MISSING_ARTIFACT,
229-
EXCLUDED,
230-
}
231-
232238
sealed class ModuleSelectorException(
233239
val result: ModuleSelectionEventResult,
234240
message: String,

gradle-plugin/src/functionalTest/kotlin/xyz/block/artifactswap/functionaltest/BasicArtifactSwapTest.kt

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
package xyz.block.artifactswap.functionaltest
22

33
import com.autonomousapps.kit.GradleProject
4-
import com.autonomousapps.kit.truth.BuildTaskSubject.Companion.assertThat
54
import com.google.common.truth.Truth.assertThat
65
import java.nio.file.Path
76
import org.gradle.testkit.runner.TaskOutcome
87
import org.junit.jupiter.api.BeforeEach
98
import org.junit.jupiter.api.Test
109
import org.junit.jupiter.api.io.TempDir
10+
import xyz.block.artifactswap.core.module_selector.InclusionReason.ALWAYS_KEEP
11+
import xyz.block.artifactswap.core.module_selector.InclusionReason.EXCLUDED
12+
import xyz.block.artifactswap.core.module_selector.InclusionReason.EXPLICITLY_REQUESTED
13+
import xyz.block.artifactswap.core.module_selector.InclusionReason.LOCAL_CHANGES
14+
import xyz.block.artifactswap.core.module_selector.InclusionReason.MISSING_ARTIFACT
1115
import xyz.block.artifactswap.functionaltest.fixtures.ArtifactSwapTestProject
16+
import xyz.block.artifactswap.functionaltest.fixtures.artifactSwapSelection
1217
import xyz.block.artifactswap.functionaltest.fixtures.ideSync
1318
import xyz.block.artifactswap.functionaltest.fixtures.writeFile
1419

@@ -46,9 +51,14 @@ class BasicArtifactSwapTest {
4651
// Then: Should use artifact swap and swap :lib to artifact
4752
assertThat(result.task(":help")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
4853
assertThat(result.output).contains("Using Artifact Swap!")
49-
assertThat(result.output).contains("Artifact Swap module selection")
50-
assertThat(result.output).contains("1 selected out of 2 candidates")
51-
assertThat(result.output).contains("excluded: 1")
54+
55+
val selection = result.artifactSwapSelection()
56+
assertThat(selection.totalSelected).isEqualTo(1)
57+
assertThat(selection.totalCandidates).isEqualTo(2)
58+
assertThat(selection.explicitCount).isEqualTo(1)
59+
assertThat(selection.excludedCount).isEqualTo(1)
60+
assertThat(selection.decisionFor(":app")).isEqualTo(EXPLICITLY_REQUESTED)
61+
assertThat(selection.decisionFor(":lib")).isEqualTo(EXCLUDED)
5262

5363
// Dependency tree should show lib as a maven artifact, not a project dependency
5464
assertThat(result.output).contains("${testProject.mavenGroup}:lib")
@@ -81,10 +91,15 @@ class BasicArtifactSwapTest {
8191
// Then: lib should be included (not swapped) due to local changes
8292
assertThat(result.task(":help")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
8393
assertThat(result.output).contains("Using Artifact Swap!")
84-
assertThat(result.output).contains("local changes:")
85-
// Both modules selected: :app (explicit) and :lib (local changes)
86-
assertThat(result.output).contains("2 selected out of 2 candidates")
87-
assertThat(result.output).contains("excluded: 0")
94+
95+
val selection = result.artifactSwapSelection()
96+
assertThat(selection.totalSelected).isEqualTo(2)
97+
assertThat(selection.totalCandidates).isEqualTo(2)
98+
assertThat(selection.explicitCount).isEqualTo(1)
99+
assertThat(selection.localChangesCount).isEqualTo(1)
100+
assertThat(selection.excludedCount).isEqualTo(0)
101+
assertThat(selection.decisionFor(":app")).isEqualTo(EXPLICITLY_REQUESTED)
102+
assertThat(selection.decisionFor(":lib")).isEqualTo(LOCAL_CHANGES)
88103
}
89104

90105
@Test
@@ -104,9 +119,15 @@ class BasicArtifactSwapTest {
104119
// Then: Both modules selected: :app (explicit) and :lib (missing artifact prevents swap)
105120
assertThat(result.task(":help")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
106121
assertThat(result.output).contains("Using Artifact Swap!")
107-
assertThat(result.output).contains("missing artifact:")
108-
assertThat(result.output).contains("2 selected out of 2 candidates")
109-
assertThat(result.output).contains("excluded: 0")
122+
123+
val selection = result.artifactSwapSelection()
124+
assertThat(selection.totalSelected).isEqualTo(2)
125+
assertThat(selection.totalCandidates).isEqualTo(2)
126+
assertThat(selection.explicitCount).isEqualTo(1)
127+
assertThat(selection.missingArtifactCount).isEqualTo(1)
128+
assertThat(selection.excludedCount).isEqualTo(0)
129+
assertThat(selection.decisionFor(":app")).isEqualTo(EXPLICITLY_REQUESTED)
130+
assertThat(selection.decisionFor(":lib")).isEqualTo(MISSING_ARTIFACT)
110131
}
111132

112133
@Test
@@ -129,8 +150,15 @@ class BasicArtifactSwapTest {
129150
// Then: Both selected: :app (explicit) and :lib (always-keep prevents swap)
130151
assertThat(result.task(":help")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
131152
assertThat(result.output).contains("Using Artifact Swap!")
132-
assertThat(result.output).contains("2 selected out of 2 candidates")
133-
assertThat(result.output).contains("excluded: 0")
153+
154+
val selection = result.artifactSwapSelection()
155+
assertThat(selection.totalSelected).isEqualTo(2)
156+
assertThat(selection.totalCandidates).isEqualTo(2)
157+
assertThat(selection.explicitCount).isEqualTo(1)
158+
assertThat(selection.alwaysKeepCount).isEqualTo(1)
159+
assertThat(selection.excludedCount).isEqualTo(0)
160+
assertThat(selection.decisionFor(":app")).isEqualTo(EXPLICITLY_REQUESTED)
161+
assertThat(selection.decisionFor(":lib")).isEqualTo(ALWAYS_KEEP)
134162
}
135163

136164
@Test
@@ -151,6 +179,12 @@ class BasicArtifactSwapTest {
151179
assertThat(result.task(":help")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
152180
assertThat(result.task(":app:dependencies")).isNotNull()
153181

182+
val selection = result.artifactSwapSelection()
183+
assertThat(selection.explicitCount).isEqualTo(1)
184+
assertThat(selection.excludedCount).isEqualTo(1)
185+
assertThat(selection.decisionFor(":app")).isEqualTo(EXPLICITLY_REQUESTED)
186+
assertThat(selection.decisionFor(":lib")).isEqualTo(EXCLUDED)
187+
154188
// The dependency tree should show the artifact notation for swapped projects
155189
// This verifies that project(':lib') was actually swapped to artifact
156190
assertThat(result.output).contains("${testProject.mavenGroup}:lib")

gradle-plugin/src/functionalTest/kotlin/xyz/block/artifactswap/functionaltest/DependencyAnalysisExcludeTest.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
package xyz.block.artifactswap.functionaltest
22

33
import com.autonomousapps.kit.GradleProject
4-
import com.autonomousapps.kit.truth.BuildTaskSubject.Companion.assertThat
54
import com.google.common.truth.Truth.assertThat
65
import java.nio.file.Path
76
import org.gradle.testkit.runner.TaskOutcome
87
import org.junit.jupiter.api.BeforeEach
98
import org.junit.jupiter.api.Test
109
import org.junit.jupiter.api.io.TempDir
10+
import xyz.block.artifactswap.core.module_selector.InclusionReason.EXCLUDED
11+
import xyz.block.artifactswap.core.module_selector.InclusionReason.EXPLICITLY_REQUESTED
1112
import xyz.block.artifactswap.functionaltest.fixtures.ArtifactSwapTestProject
13+
import xyz.block.artifactswap.functionaltest.fixtures.artifactSwapSelection
1214
import xyz.block.artifactswap.functionaltest.fixtures.ideSync
1315

1416
/**
@@ -51,7 +53,12 @@ class DependencyAnalysisExcludeTest {
5153
// Then: Build should succeed with artifact swap active
5254
assertThat(result.task(":help")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
5355
assertThat(result.output).contains("Using Artifact Swap!")
54-
assertThat(result.output).contains("Artifact Swap module selection")
56+
57+
val selection = result.artifactSwapSelection()
58+
assertThat(selection.explicitCount).isEqualTo(1)
59+
assertThat(selection.excludedCount).isEqualTo(1)
60+
assertThat(selection.decisionFor(":app")).isEqualTo(EXPLICITLY_REQUESTED)
61+
assertThat(selection.decisionFor(":common-ui")).isEqualTo(EXCLUDED)
5562

5663
// Verify no errors about missing exclude() method — this confirms
5764
// DAGP's exclude(':common-ui') still works when artifact swap is active

gradle-plugin/src/functionalTest/kotlin/xyz/block/artifactswap/functionaltest/EndToEndCliTest.kt

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@ import org.gradle.testkit.runner.TaskOutcome
99
import org.junit.jupiter.api.BeforeEach
1010
import org.junit.jupiter.api.Test
1111
import org.junit.jupiter.api.io.TempDir
12+
import xyz.block.artifactswap.core.module_selector.InclusionReason.EXCLUDED
13+
import xyz.block.artifactswap.core.module_selector.InclusionReason.EXPLICITLY_REQUESTED
14+
import xyz.block.artifactswap.core.module_selector.InclusionReason.LOCAL_CHANGES
15+
import xyz.block.artifactswap.core.module_selector.InclusionReason.MISSING_ARTIFACT
1216
import xyz.block.artifactswap.functionaltest.fixtures.ArtifactSwapTestProject
1317
import xyz.block.artifactswap.functionaltest.fixtures.CliRunner
1418
import xyz.block.artifactswap.functionaltest.fixtures.CliRunner.Companion.parseHashFile
19+
import xyz.block.artifactswap.functionaltest.fixtures.artifactSwapSelection
1520
import xyz.block.artifactswap.functionaltest.fixtures.build
1621
import xyz.block.artifactswap.functionaltest.fixtures.ideSync
1722

@@ -109,11 +114,14 @@ class EndToEndCliTest {
109114
// Then: IDE sync should succeed and show artifact swap is active
110115
assertThat(syncResult.task(":help")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
111116
assertThat(syncResult.output).contains("Using Artifact Swap!")
112-
assertThat(syncResult.output).contains("Artifact Swap module selection")
113117

114-
// Verify :lib was excluded (swapped to artifact) - only :app should be selected
115-
assertThat(syncResult.output).contains("1 selected out of 2 candidates")
116-
assertThat(syncResult.output).contains("excluded: 1")
118+
val selection = syncResult.artifactSwapSelection()
119+
assertThat(selection.totalSelected).isEqualTo(1)
120+
assertThat(selection.totalCandidates).isEqualTo(2)
121+
assertThat(selection.explicitCount).isEqualTo(1)
122+
assertThat(selection.excludedCount).isEqualTo(1)
123+
assertThat(selection.decisionFor(":app")).isEqualTo(EXPLICITLY_REQUESTED)
124+
assertThat(selection.decisionFor(":lib")).isEqualTo(EXCLUDED)
117125

118126
// The dependency tree should show the maven artifact for lib (swapped)
119127
assertThat(syncResult.output).contains("${testProject.mavenGroup}:lib")
@@ -160,10 +168,15 @@ class EndToEndCliTest {
160168
// Then: Both modules selected: :app (explicit) and :lib (local changes prevent swap)
161169
assertThat(syncResult.task(":help")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
162170
assertThat(syncResult.output).contains("Using Artifact Swap!")
163-
assertThat(syncResult.output).contains("Artifact Swap module selection")
164-
assertThat(syncResult.output).contains("local changes:")
165-
assertThat(syncResult.output).contains("2 selected out of 2 candidates")
166-
assertThat(syncResult.output).contains("excluded: 0")
171+
172+
val selection = syncResult.artifactSwapSelection()
173+
assertThat(selection.totalSelected).isEqualTo(2)
174+
assertThat(selection.totalCandidates).isEqualTo(2)
175+
assertThat(selection.explicitCount).isEqualTo(1)
176+
assertThat(selection.localChangesCount).isEqualTo(1)
177+
assertThat(selection.excludedCount).isEqualTo(0)
178+
assertThat(selection.decisionFor(":app")).isEqualTo(EXPLICITLY_REQUESTED)
179+
assertThat(selection.decisionFor(":lib")).isEqualTo(LOCAL_CHANGES)
167180
}
168181

169182
@Test
@@ -222,20 +235,17 @@ class EndToEndCliTest {
222235
assertThat(bomResult.isSuccess).isTrue()
223236

224237
// When: Run IDE sync WITHOUT any local changes
225-
// With :lib having a published artifact and not in ide-projects.txt, it should be excluded
226238
val syncResult = project.ideSync(":app:dependencies", "--configuration", "runtimeClasspath")
227239

228240
// Then: IDE sync should succeed with artifact swap active
229241
assertThat(syncResult.task(":help")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
230242
assertThat(syncResult.output).contains("Using Artifact Swap!")
231-
assertThat(syncResult.output).contains("Artifact Swap module selection")
232243

233-
// Verify that :lib was excluded (swapped to artifact) because:
234-
// - It's not in ide-projects.txt
235-
// - It has a published artifact with matching content hash
236-
// - It has no local changes
237-
// The selection should show "excluded: 1" for :lib being swapped
238-
assertThat(syncResult.output).contains("excluded: 1")
244+
val selection = syncResult.artifactSwapSelection()
245+
assertThat(selection.explicitCount).isEqualTo(1)
246+
assertThat(selection.excludedCount).isEqualTo(1)
247+
assertThat(selection.decisionFor(":app")).isEqualTo(EXPLICITLY_REQUESTED)
248+
assertThat(selection.decisionFor(":lib")).isEqualTo(EXCLUDED)
239249

240250
// The dependency tree should show the maven artifact for lib (not project :lib)
241251
assertThat(syncResult.output).contains("${testProject.mavenGroup}:lib")
@@ -273,13 +283,15 @@ class EndToEndCliTest {
273283
// Then: IDE sync should succeed with artifact swap active
274284
assertThat(syncResult.task(":help")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
275285
assertThat(syncResult.output).contains("Using Artifact Swap!")
276-
assertThat(syncResult.output).contains("Artifact Swap module selection")
277286

278-
// Verify :lib was included due to missing artifact (not excluded/swapped)
279-
// Both modules should be selected: :app (explicit) and :lib (missing artifact)
280-
assertThat(syncResult.output).contains("2 selected out of 2 candidates")
281-
assertThat(syncResult.output).contains("missing artifact: 1")
282-
assertThat(syncResult.output).contains("excluded: 0")
287+
val selection = syncResult.artifactSwapSelection()
288+
assertThat(selection.totalSelected).isEqualTo(2)
289+
assertThat(selection.totalCandidates).isEqualTo(2)
290+
assertThat(selection.explicitCount).isEqualTo(1)
291+
assertThat(selection.missingArtifactCount).isEqualTo(1)
292+
assertThat(selection.excludedCount).isEqualTo(0)
293+
assertThat(selection.decisionFor(":app")).isEqualTo(EXPLICITLY_REQUESTED)
294+
assertThat(selection.decisionFor(":lib")).isEqualTo(MISSING_ARTIFACT)
283295

284296
// The dependency tree should show :lib as a project dependency (not artifact)
285297
assertThat(syncResult.output).contains("project :lib")

gradle-plugin/src/functionalTest/kotlin/xyz/block/artifactswap/functionaltest/SqlDelightProjectAccessorTest.kt

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
package xyz.block.artifactswap.functionaltest
22

33
import com.autonomousapps.kit.GradleProject
4-
import com.autonomousapps.kit.truth.BuildTaskSubject.Companion.assertThat
54
import com.google.common.truth.Truth.assertThat
65
import java.nio.file.Path
76
import org.gradle.testkit.runner.TaskOutcome
87
import org.junit.jupiter.api.BeforeEach
98
import org.junit.jupiter.api.Test
109
import org.junit.jupiter.api.io.TempDir
10+
import xyz.block.artifactswap.core.module_selector.InclusionReason.ALWAYS_KEEP
11+
import xyz.block.artifactswap.core.module_selector.InclusionReason.EXPLICITLY_REQUESTED
1112
import xyz.block.artifactswap.functionaltest.fixtures.ArtifactSwapTestProject
13+
import xyz.block.artifactswap.functionaltest.fixtures.artifactSwapSelection
1214
import xyz.block.artifactswap.functionaltest.fixtures.ideSync
1315

1416
/**
@@ -51,13 +53,16 @@ class SqlDelightProjectAccessorTest {
5153
val result = project.ideSync()
5254

5355
// Then: Build should succeed with both modules included
54-
assertThat(result.task(":help")).isNotNull()
5556
assertThat(result.task(":help")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
5657
assertThat(result.output).contains("Using Artifact Swap!")
5758

58-
// :db kept via always-keep, :consumer via explicit request — nothing swapped
59-
assertThat(result.output).contains("2 selected out of 2 candidates")
60-
assertThat(result.output).contains("excluded: 0")
59+
val selection = result.artifactSwapSelection()
60+
assertThat(selection.totalSelected).isEqualTo(2)
61+
assertThat(selection.totalCandidates).isEqualTo(2)
62+
assertThat(selection.alwaysKeepCount).isEqualTo(1)
63+
assertThat(selection.excludedCount).isEqualTo(0)
64+
assertThat(selection.decisionFor(":consumer")).isEqualTo(EXPLICITLY_REQUESTED)
65+
assertThat(selection.decisionFor(":db")).isEqualTo(ALWAYS_KEEP)
6166

6267
// Verify no errors about missing dependency() method — this confirms
6368
// SQLDelight's `dependency projects.db` received a real project accessor
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package xyz.block.artifactswap.functionaltest.fixtures
2+
3+
import org.gradle.testkit.runner.BuildResult
4+
import xyz.block.artifactswap.core.module_selector.InclusionReason
5+
6+
/**
7+
* Parsed artifact swap module selection results from a Gradle build output.
8+
*
9+
* Extracts both the summary counts and per-module decisions from the build log, enabling clean
10+
* assertions in functional tests via standard Truth matchers:
11+
* ```
12+
* val selection = result.artifactSwapSelection()
13+
* assertThat(selection.totalCandidates).isEqualTo(2)
14+
* assertThat(selection.decisionFor(":lib")).isEqualTo(InclusionReason.EXCLUDED)
15+
* ```
16+
*/
17+
data class ArtifactSwapSelectionResult(
18+
val totalSelected: Int,
19+
val totalCandidates: Int,
20+
val explicitCount: Int,
21+
val alwaysKeepCount: Int,
22+
val localChangesCount: Int,
23+
val missingArtifactCount: Int,
24+
val excludedCount: Int,
25+
/** Map of module path to its inclusion/exclusion decision. */
26+
val moduleDecisions: Map<String, InclusionReason>,
27+
) {
28+
29+
/** Returns the decision for a specific module, failing if the module wasn't found. */
30+
fun decisionFor(modulePath: String): InclusionReason =
31+
moduleDecisions[modulePath]
32+
?: throw AssertionError(
33+
"No decision found for module '$modulePath'. " +
34+
"Available modules: ${moduleDecisions.keys}"
35+
)
36+
37+
companion object {
38+
private val SUMMARY_PATTERN =
39+
Regex(
40+
"""(\d+) selected out of (\d+) candidates \(explicit: (\d+), always-keep: (\d+), local changes: (\d+), missing artifact: (\d+), excluded: (\d+)\)"""
41+
)
42+
43+
private val DECISION_PATTERN = Regex("""Artifact Swap decision: (:\S+) -> (\S+)""")
44+
45+
fun parse(output: String): ArtifactSwapSelectionResult {
46+
val summaryMatch =
47+
SUMMARY_PATTERN.find(output)
48+
?: throw AssertionError(
49+
"Artifact Swap module selection summary not found in build output. " +
50+
"Make sure --info is passed and artifact swap is active."
51+
)
52+
53+
val decisions =
54+
DECISION_PATTERN.findAll(output).associate { match ->
55+
match.groupValues[1] to InclusionReason.valueOf(match.groupValues[2])
56+
}
57+
58+
return ArtifactSwapSelectionResult(
59+
totalSelected = summaryMatch.groupValues[1].toInt(),
60+
totalCandidates = summaryMatch.groupValues[2].toInt(),
61+
explicitCount = summaryMatch.groupValues[3].toInt(),
62+
alwaysKeepCount = summaryMatch.groupValues[4].toInt(),
63+
localChangesCount = summaryMatch.groupValues[5].toInt(),
64+
missingArtifactCount = summaryMatch.groupValues[6].toInt(),
65+
excludedCount = summaryMatch.groupValues[7].toInt(),
66+
moduleDecisions = decisions,
67+
)
68+
}
69+
}
70+
}
71+
72+
/** Parses artifact swap selection results from the build output. */
73+
fun BuildResult.artifactSwapSelection(): ArtifactSwapSelectionResult =
74+
ArtifactSwapSelectionResult.parse(output)

0 commit comments

Comments
 (0)