Skip to content

Commit 840d8ec

Browse files
committed
Multi module test.
1 parent 8681a40 commit 840d8ec

File tree

3 files changed

+266
-1
lines changed

3 files changed

+266
-1
lines changed

src/functionalTest/groovy/com/autonomousapps/jvm/TypeUsageSpec.groovy

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package com.autonomousapps.jvm
44

55
import com.autonomousapps.jvm.projects.TypeUsageProject
66
import com.autonomousapps.jvm.projects.TypeUsageWithFiltersProject
7+
import com.autonomousapps.jvm.projects.TypeUsageMultiModuleProject
78

89
import static com.autonomousapps.utils.Runner.build
910
import static com.google.common.truth.Truth.assertThat
@@ -97,4 +98,99 @@ final class TypeUsageSpec extends AbstractJvmSpec {
9798
where:
9899
gradleVersion << gradleVersions()
99100
}
101+
102+
def "tracks type usage across multiple modules (#gradleVersion)"() {
103+
given:
104+
def project = new TypeUsageMultiModuleProject()
105+
gradleProject = project.gradleProject
106+
107+
when:
108+
build(gradleVersion, gradleProject.rootDir, 'computeTypeUsageMain')
109+
110+
then: 'app module tracks project dependencies'
111+
def appUsage = project.actualTypeUsageFor(':app')
112+
assertThat(appUsage.projectPath).isEqualTo(':app')
113+
assertThat(appUsage.projectDependencies).hasSize(2)
114+
assertThat(appUsage.projectDependencies).containsKey(':core')
115+
assertThat(appUsage.projectDependencies).containsKey(':utils')
116+
117+
and: 'app tracks types from core'
118+
def coreTypes = appUsage.projectDependencies[':core']
119+
assertThat(coreTypes).containsKey('com.example.core.UserRepository')
120+
assertThat(coreTypes).containsKey('com.example.core.User')
121+
122+
and: 'app tracks types from utils'
123+
def utilsTypes = appUsage.projectDependencies[':utils']
124+
assertThat(utilsTypes).containsKey('com.example.utils.Logger')
125+
126+
and: 'app tracks library dependencies'
127+
assertThat(appUsage.libraryDependencies).containsKey('org.apache.commons:commons-collections4')
128+
129+
and: 'app tracks internal types'
130+
assertThat(appUsage.internal).containsKey('com.example.app.MainActivity')
131+
132+
where:
133+
gradleVersion << gradleVersions()
134+
}
135+
136+
def "core module tracks its dependencies (#gradleVersion)"() {
137+
given:
138+
def project = new TypeUsageMultiModuleProject()
139+
gradleProject = project.gradleProject
140+
141+
when:
142+
build(gradleVersion, gradleProject.rootDir, 'computeTypeUsageMain')
143+
144+
then: 'core tracks utils as project dependency'
145+
def coreUsage = project.actualTypeUsageFor(':core')
146+
assertThat(coreUsage.projectPath).isEqualTo(':core')
147+
assertThat(coreUsage.projectDependencies).hasSize(1)
148+
assertThat(coreUsage.projectDependencies).containsKey(':utils')
149+
150+
and: 'core tracks Logger from utils'
151+
def utilsTypes = coreUsage.projectDependencies[':utils']
152+
assertThat(utilsTypes).containsKey('com.example.utils.Logger')
153+
154+
and: 'core tracks its own internal types'
155+
assertThat(coreUsage.internal).containsKey('com.example.core.User')
156+
157+
and: 'core has no library dependencies beyond kotlin stdlib'
158+
def nonKotlinLibs = coreUsage.libraryDependencies.keySet().findAll {
159+
!it.startsWith('org.jetbrains')
160+
}
161+
assertThat(nonKotlinLibs).isEmpty()
162+
163+
where:
164+
gradleVersion << gradleVersions()
165+
}
166+
167+
def "utils module has only library dependencies (#gradleVersion)"() {
168+
given:
169+
def project = new TypeUsageMultiModuleProject()
170+
gradleProject = project.gradleProject
171+
172+
when:
173+
build(gradleVersion, gradleProject.rootDir, 'computeTypeUsageMain')
174+
175+
then: 'utils has no project dependencies'
176+
def utilsUsage = project.actualTypeUsageFor(':utils')
177+
assertThat(utilsUsage.projectPath).isEqualTo(':utils')
178+
assertThat(utilsUsage.projectDependencies).isEmpty()
179+
180+
and: 'utils tracks commons-io'
181+
assertThat(utilsUsage.libraryDependencies).containsKey('commons-io:commons-io')
182+
def commonsIoTypes = utilsUsage.libraryDependencies['commons-io:commons-io']
183+
assertThat(commonsIoTypes).containsKey('org.apache.commons.io.FileUtils')
184+
185+
and: 'utils tracks internal Logger type'
186+
assertThat(utilsUsage.internal).containsKey('com.example.utils.Logger')
187+
188+
and: 'summary counts are correct'
189+
assertThat(utilsUsage.summary.projectDependencies).isEqualTo(0)
190+
assertThat(utilsUsage.summary.libraryDependencies).isGreaterThan(0)
191+
assertThat(utilsUsage.summary.internalTypes).isEqualTo(utilsUsage.internal.size())
192+
193+
where:
194+
gradleVersion << gradleVersions()
195+
}
100196
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// Copyright (c) 2026. Tony Robalik.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package com.autonomousapps.jvm.projects
4+
5+
import com.autonomousapps.AbstractProject
6+
import com.autonomousapps.kit.GradleProject
7+
import com.autonomousapps.kit.Source
8+
import com.autonomousapps.kit.SourceType
9+
import com.autonomousapps.model.ProjectTypeUsage
10+
11+
import static com.autonomousapps.kit.gradle.Dependency.project
12+
import static com.autonomousapps.kit.gradle.dependencies.Dependencies.*
13+
14+
final class TypeUsageMultiModuleProject extends AbstractProject {
15+
16+
final GradleProject gradleProject
17+
18+
TypeUsageMultiModuleProject() {
19+
this.gradleProject = build()
20+
}
21+
22+
private GradleProject build() {
23+
return newGradleProjectBuilder()
24+
.withSubproject('app') { s ->
25+
s.sources = appSources
26+
s.withBuildScript { bs ->
27+
bs.plugins = kotlin
28+
bs.dependencies = [
29+
project('implementation', ':core'),
30+
project('implementation', ':utils'),
31+
commonsCollections('implementation'),
32+
kotlinStdLib('implementation')
33+
]
34+
}
35+
}
36+
.withSubproject('core') { s ->
37+
s.sources = coreSources
38+
s.withBuildScript { bs ->
39+
bs.plugins = kotlin
40+
bs.dependencies = [
41+
project('implementation', ':utils'),
42+
kotlinStdLib('implementation')
43+
]
44+
}
45+
}
46+
.withSubproject('utils') { s ->
47+
s.sources = utilsSources
48+
s.withBuildScript { bs ->
49+
bs.plugins = kotlin
50+
bs.dependencies = [
51+
commonsIO('implementation'),
52+
kotlinStdLib('implementation')
53+
]
54+
}
55+
}
56+
.write()
57+
}
58+
59+
private appSources = [
60+
new Source(
61+
SourceType.KOTLIN, "MainActivity", "com/example/app",
62+
"""\
63+
package com.example.app
64+
65+
import com.example.core.UserRepository
66+
import com.example.core.User
67+
import com.example.utils.Logger
68+
import org.apache.commons.collections4.bag.HashBag
69+
70+
class MainActivity {
71+
private val repository = UserRepository()
72+
private val logger = Logger()
73+
private val cache = HashBag<String>()
74+
75+
fun loadUsers() {
76+
val users = repository.getUsers()
77+
logger.log("Loaded \${users.size} users")
78+
users.forEach { cache.add(it.name) }
79+
}
80+
}
81+
""".stripIndent()
82+
),
83+
new Source(
84+
SourceType.KOTLIN, "AppHelper", "com/example/app",
85+
"""\
86+
package com.example.app
87+
88+
internal class AppHelper {
89+
fun getMainActivity() = MainActivity()
90+
}
91+
""".stripIndent()
92+
)
93+
]
94+
95+
private coreSources = [
96+
new Source(
97+
SourceType.KOTLIN, "UserRepository", "com/example/core",
98+
"""\
99+
package com.example.core
100+
101+
import com.example.utils.Logger
102+
103+
class UserRepository {
104+
private val logger = Logger()
105+
106+
fun getUsers(): List<User> {
107+
logger.log("Fetching users")
108+
return listOf(User("Alice"), User("Bob"))
109+
}
110+
}
111+
""".stripIndent()
112+
),
113+
new Source(
114+
SourceType.KOTLIN, "User", "com/example/core",
115+
"""\
116+
package com.example.core
117+
118+
data class User(val name: String)
119+
""".stripIndent()
120+
),
121+
new Source(
122+
SourceType.KOTLIN, "CoreInternal", "com/example/core",
123+
"""\
124+
package com.example.core
125+
126+
internal class CoreInternal {
127+
fun createUser(name: String) = User(name)
128+
}
129+
""".stripIndent()
130+
)
131+
]
132+
133+
private utilsSources = [
134+
new Source(
135+
SourceType.KOTLIN, "Logger", "com/example/utils",
136+
"""\
137+
package com.example.utils
138+
139+
import org.apache.commons.io.FileUtils
140+
import java.io.File
141+
142+
class Logger {
143+
fun log(message: String) {
144+
FileUtils.write(File("log.txt"), message, "UTF-8", true)
145+
}
146+
}
147+
""".stripIndent()
148+
),
149+
new Source(
150+
SourceType.KOTLIN, "UtilsHelper", "com/example/utils",
151+
"""\
152+
package com.example.utils
153+
154+
internal class UtilsHelper {
155+
fun createLogger() = Logger()
156+
}
157+
""".stripIndent()
158+
)
159+
]
160+
161+
ProjectTypeUsage actualTypeUsageFor(String projectPath) {
162+
def typeUsage = gradleProject.singleArtifact(projectPath,
163+
com.autonomousapps.internal.OutputPathsKt.getTypeUsagePath('main'))
164+
def adapter = com.autonomousapps.internal.utils.MoshiUtils.MOSHI
165+
.adapter(com.autonomousapps.model.ProjectTypeUsage)
166+
return adapter.fromJson(typeUsage.asPath.text)
167+
}
168+
}

src/main/kotlin/com/autonomousapps/tasks/ComputeTypeUsageTask.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ public abstract class ComputeTypeUsageTask @Inject constructor(
115115
}
116116

117117
explodedJars.forEach { jar ->
118-
val identifier = jar.coordinates.identifier
118+
// Normalize identifier to handle included builds (convert "build:project" to ":project")
119+
val identifier = jar.coordinates.normalizedIdentifier(":")
119120
jar.binaryClasses.forEach { binaryClass ->
120121
map[binaryClass.className] = identifier
121122
}

0 commit comments

Comments
 (0)