##// END OF EJS Templates
Add minimum Gradle compatibility functional test
cin -
r63:79fd940a7856 tip default
parent child
Show More
@@ -1,2 +1,3
1 1 group=org.implab.gradle
2 2 version=0.1.0
3 testMinGradleVersion=8.10.2
@@ -1,61 +1,73
1 1 plugins {
2 2 id "java-library"
3 3 id "ivy-publish"
4 4 }
5 5
6 6 description = "Variant, source-set, and outgoing artifact model plugins for Gradle builds"
7 7
8 8 java {
9 9 withJavadocJar()
10 10 withSourcesJar()
11 11 toolchain {
12 12 languageVersion = JavaLanguageVersion.of(21)
13 13 }
14 14 }
15 15
16 16 dependencies {
17 17 compileOnly libs.jdt.annotations
18 18
19 19 api gradleApi(),
20 20 project(":common")
21 21
22 22 testImplementation gradleTestKit()
23 23 testImplementation "org.junit.jupiter:junit-jupiter-api:5.11.4"
24 24 testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.11.4"
25 25 testRuntimeOnly "org.junit.platform:junit-platform-launcher:1.11.4"
26 26 }
27 27
28 28 task printVersion{
29 29 doLast {
30 30 println "project: $project.group:$project.name:$project.version"
31 31 println "jar: ${->jar.archiveFileName.get()}"
32 32 }
33 33 }
34 34
35 35 test {
36 36 useJUnitPlatform()
37 37 }
38 38
39 tasks.register("minGradleCompatibilityTest", Test) {
40 description = "Runs functional TestKit tests against the minimum supported Gradle version."
41 group = "verification"
42 useJUnitPlatform()
43 testClassesDirs = sourceSets.test.output.classesDirs
44 classpath = sourceSets.test.runtimeClasspath
45 include "**/*FunctionalTest.class"
46 systemProperty "org.implab.gradle.test.gradleVersion", providers.gradleProperty("testMinGradleVersion").get()
47 inputs.property("testMinGradleVersion", providers.gradleProperty("testMinGradleVersion"))
48 shouldRunAfter test
49 }
50
39 51 javadoc {
40 52 exclude "**/internal/**"
41 53 }
42 54
43 55 publishing {
44 56 repositories {
45 57 ivy {
46 58 url = "${System.properties["user.home"]}/ivy-repo"
47 59 }
48 60 }
49 61 publications {
50 62 ivy(IvyPublication) {
51 63 from components.java
52 64 descriptor.description {
53 65 text = providers.provider({ description })
54 66 }
55 67 descriptor.license {
56 68 name = "BSD-2-Clause"
57 69 url = "https://spdx.org/licenses/BSD-2-Clause.html"
58 70 }
59 71 }
60 72 }
61 73 }
@@ -1,123 +1,138
1 1 package org.implab.gradle.variants;
2 2
3 3 import static org.junit.jupiter.api.Assertions.assertTrue;
4 4
5 5 import java.io.File;
6 6 import java.io.IOException;
7 7 import java.nio.file.Files;
8 8 import java.nio.file.Path;
9 9 import java.util.LinkedHashSet;
10 10 import java.util.List;
11 11 import java.util.stream.Collectors;
12 12 import java.util.regex.Pattern;
13 13
14 14 import org.gradle.testkit.runner.GradleRunner;
15 15 import org.gradle.testkit.runner.UnexpectedBuildFailure;
16 import org.gradle.util.GradleVersion;
16 17 import org.implab.gradle.common.core.lang.Deferred;
17 18 import org.implab.gradle.variants.sources.GenericSourceSet;
18 19 import org.junit.jupiter.api.io.TempDir;
19 20
20 21 abstract class AbstractFunctionalTest {
21 22 private static final String SETTINGS_FILE = "settings.gradle";
22 23 private static final String BUILD_FILE = "build.gradle";
24 private static final String TEST_GRADLE_VERSION_PROPERTY = "org.implab.gradle.test.gradleVersion";
23 25 private static final Pattern INCLUDE_DECLARATION = Pattern.compile("^include(?:\\s|\\().*");
24 26 private static final Pattern QUOTED_LITERAL = Pattern.compile("['\"]([^'\"]+)['\"]");
25 27
26 28 @TempDir
27 29 Path testProjectDir;
28 30
29 31 protected void writeSettings(String rootProjectName) throws IOException {
30 32 writeFile(SETTINGS_FILE, "rootProject.name = '" + rootProjectName + "'\n");
31 33 }
32 34
33 35 protected void writeBuildFile(String body) throws IOException {
34 36 writeFile(BUILD_FILE, buildscriptClasspathBlock() + "\n" + body);
35 37 }
36 38
37 39 protected GradleRunner runner(String... arguments) {
38 return GradleRunner.create()
40 var runner = GradleRunner.create()
39 41 .withProjectDir(testProjectDir.toFile())
40 42 .withArguments(arguments)
41 43 .forwardOutput();
44
45 var gradleVersion = System.getProperty(TEST_GRADLE_VERSION_PROPERTY);
46 if (gradleVersion != null && !gradleVersion.isBlank())
47 runner = runner.withGradleVersion(gradleVersion);
48
49 return runner;
50 }
51
52 protected GradleVersion targetGradleVersion() {
53 var gradleVersion = System.getProperty(TEST_GRADLE_VERSION_PROPERTY);
54 if (gradleVersion == null || gradleVersion.isBlank())
55 return GradleVersion.current();
56 return GradleVersion.version(gradleVersion);
42 57 }
43 58
44 59 protected void assertBuildFails(String expectedError, String... arguments) {
45 60 var ex = org.junit.jupiter.api.Assertions.assertThrows(
46 61 UnexpectedBuildFailure.class,
47 62 () -> runner(arguments).build());
48 63
49 64 var output = ex.getBuildResult().getOutput();
50 65 assertTrue(output.contains(expectedError), () -> "Expected [" + expectedError + "] in output:\n" + output);
51 66 }
52 67
53 68 private static String buildscriptClasspathBlock() {
54 69 var classpath = pluginClasspath().stream()
55 70 .map(file -> "'" + file.getAbsolutePath().replace("\\", "\\\\") + "'")
56 71 .collect(Collectors.joining(", "));
57 72
58 73 return """
59 74 buildscript {
60 75 dependencies {
61 76 classpath files(%s)
62 77 }
63 78 }
64 79 """.formatted(classpath);
65 80 }
66 81
67 82 private static List<File> pluginClasspath() {
68 83 try {
69 84 var files = new LinkedHashSet<File>();
70 85 files.add(codeSource(VariantSourcesPlugin.class));
71 86 files.add(resourceRoot("META-INF/gradle-plugins/org.implab.gradle-variants.properties"));
72 87 files.add(codeSource(GenericSourceSet.class));
73 88 files.add(codeSource(Deferred.class));
74 89 return List.copyOf(files);
75 90 } catch (Exception e) {
76 91 throw new RuntimeException("Unable to build plugin classpath for test", e);
77 92 }
78 93 }
79 94
80 95 private static File codeSource(Class<?> type) throws Exception {
81 96 return Path.of(type.getProtectionDomain().getCodeSource().getLocation().toURI()).toFile();
82 97 }
83 98
84 99 private static File resourceRoot(String resourceName) throws Exception {
85 100 var url = VariantSourcesPlugin.class.getClassLoader().getResource(resourceName);
86 101 if (url == null)
87 102 throw new IllegalStateException("Classpath resource isn't found: " + resourceName);
88 103
89 104 var path = Path.of(url.toURI());
90 105 var segments = resourceName.split("/").length;
91 106 for (var i = 0; i < segments; i++)
92 107 path = path.getParent();
93 108 return path.toFile();
94 109 }
95 110
96 111 protected void writeFile(String relativePath, String content) throws IOException {
97 112 Path path = testProjectDir.resolve(relativePath);
98 113 Files.createDirectories(path.getParent());
99 114 Files.writeString(path, content);
100 115
101 116 if (SETTINGS_FILE.equals(relativePath))
102 117 ensureIncludedProjectDirectories(content);
103 118 }
104 119
105 120 private void ensureIncludedProjectDirectories(String settingsContent) throws IOException {
106 121 for (var line : settingsContent.split("\\R")) {
107 122 var trimmed = line.strip();
108 123 if (!INCLUDE_DECLARATION.matcher(trimmed).matches())
109 124 continue;
110 125
111 126 var matcher = QUOTED_LITERAL.matcher(trimmed);
112 127 while (matcher.find()) {
113 128 var projectPath = matcher.group(1);
114 129 if (projectPath.startsWith(":"))
115 130 projectPath = projectPath.substring(1);
116 131 if (projectPath.isBlank())
117 132 continue;
118 133
119 134 Files.createDirectories(testProjectDir.resolve(projectPath.replace(':', File.separatorChar)));
120 135 }
121 136 }
122 137 }
123 138 }
@@ -1,867 +1,872
1 1 package org.implab.gradle.variants;
2 2
3 3 import static org.junit.jupiter.api.Assertions.assertTrue;
4 4
5 5 import org.gradle.testkit.runner.BuildResult;
6 6 import org.gradle.testkit.runner.TaskOutcome;
7 7 import org.junit.jupiter.api.Test;
8 8
9 9 class VariantArtifactsPluginFunctionalTest extends AbstractFunctionalTest {
10 10
11 11 @Test
12 12 void gradleReferenceLazyOutgoingConfigurationAllowsSecondaryArtifactSelection() throws Exception {
13 13 writeFile("settings.gradle", """
14 14 rootProject.name = 'gradle-reference-outgoing-resolution'
15 15 include 'producer', 'consumer'
16 16 """);
17 17 writeFile("producer/inputs/typesPackage", "types\n");
18 18 writeFile("producer/inputs/js", "js\n");
19 19 writeBuildFile("""
20 20 import org.gradle.api.attributes.Attribute
21 21
22 22 def variantAttr = Attribute.of('test.variant', String)
23 23 def slotAttr = Attribute.of('test.slot', String)
24 24
25 25 project(':producer') {
26 26 def browserElements = configurations.consumable('browserElements')
27 27
28 28 println('reference: registered browserElements provider')
29 29
30 30 browserElements.configure { configuration ->
31 31 println('reference: configuring browserElements')
32 32
33 33 configuration.attributes.attribute(variantAttr, 'browser')
34 34 configuration.outgoing.attributes.attribute(slotAttr, 'typesPackage')
35 35 configuration.outgoing.artifact(layout.projectDirectory.file('inputs/typesPackage'))
36 36
37 37 configuration.outgoing.variants.create('js') { secondary ->
38 38 println('reference: creating js outgoing variant')
39 39
40 40 secondary.attributes.attribute(slotAttr, 'js')
41 41 secondary.artifact(layout.projectDirectory.file('inputs/js'))
42 42 }
43 43 }
44 44 }
45 45
46 46 project(':consumer') {
47 47 configurations {
48 48 compileView {
49 49 canBeResolved = true
50 50 canBeConsumed = false
51 51 canBeDeclared = true
52 52 attributes {
53 53 attribute(variantAttr, 'browser')
54 54 attribute(slotAttr, 'typesPackage')
55 55 }
56 56 }
57 57 }
58 58
59 59 dependencies {
60 60 compileView project(':producer')
61 61 }
62 62
63 63 tasks.register('probe') {
64 64 doLast {
65 65 println('reference: resolving primary files')
66 66
67 67 def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',')
68 68
69 69 println('reference: resolving secondary files')
70 70
71 71 def jsFiles = configurations.compileView.incoming.artifactView {
72 72 attributes {
73 73 attribute(slotAttr, 'js')
74 74 }
75 75 }.files.files.collect { it.name }.sort().join(',')
76 76
77 77 println('compileFiles=' + compileFiles)
78 78 println('jsFiles=' + jsFiles)
79 79 }
80 80 }
81 81 }
82 82 """);
83 83
84 84 BuildResult result = runner(":consumer:probe").build();
85 85 var output = result.getOutput();
86 86 var registered = output.indexOf("reference: registered browserElements provider");
87 87 var resolvingPrimary = output.indexOf("reference: resolving primary files");
88 88 var configuring = output.indexOf("reference: configuring browserElements");
89 89 var creatingSecondary = output.indexOf("reference: creating js outgoing variant");
90 90 var resolvingSecondary = output.indexOf("reference: resolving secondary files");
91 91
92 92 assertTrue(registered >= 0);
93 93 assertTrue(resolvingPrimary >= 0);
94 94 assertTrue(configuring >= 0);
95 95 assertTrue(creatingSecondary >= 0);
96 96 assertTrue(resolvingSecondary >= 0);
97 97 assertTrue(registered < resolvingPrimary);
98 98 assertTrue(resolvingPrimary < configuring);
99 99 assertTrue(configuring < creatingSecondary);
100 100 assertTrue(creatingSecondary < resolvingSecondary);
101 101 assertTrue(output.contains("compileFiles=typesPackage"));
102 102 assertTrue(output.contains("jsFiles=js"));
103 103 }
104 104
105 105 @Test
106 void gradleReferenceRegisteredSecondaryArtifactVariantAllowsSecondaryArtifactSelection() throws Exception {
106 void gradleReferenceRegisteredSecondaryArtifactVariantMatchesGradleBehavior() throws Exception {
107 107 writeFile("settings.gradle", """
108 108 rootProject.name = 'gradle-reference-registered-secondary-variant'
109 109 include 'producer', 'consumer'
110 110 """);
111 111 writeFile("producer/inputs/typesPackage", "types\n");
112 112 writeFile("producer/inputs/js", "js\n");
113 113 writeFile("build.gradle", """
114 114 import org.gradle.api.attributes.Attribute
115 115
116 116 def variantAttr = Attribute.of('test.variant', String)
117 117 def slotAttr = Attribute.of('test.slot', String)
118 118
119 119 project(':producer') {
120 120 def browserElements = configurations.consumable('browserElements')
121 121
122 122 browserElements.configure { configuration ->
123 123 configuration.attributes.attribute(variantAttr, 'browser')
124 124 configuration.outgoing.attributes.attribute(slotAttr, 'typesPackage')
125 125 configuration.outgoing.artifact(layout.projectDirectory.file('inputs/typesPackage'))
126 126
127 127 configuration.outgoing.variants.register('js') { secondary ->
128 128 secondary.attributes.attribute(slotAttr, 'js')
129 129 secondary.artifact(layout.projectDirectory.file('inputs/js'))
130 130 }
131 131 }
132 132 }
133 133
134 134 project(':consumer') {
135 135 configurations {
136 136 compileView {
137 137 canBeResolved = true
138 138 canBeConsumed = false
139 139 canBeDeclared = true
140 140 attributes {
141 141 attribute(variantAttr, 'browser')
142 142 attribute(slotAttr, 'typesPackage')
143 143 }
144 144 }
145 145 }
146 146
147 147 dependencies {
148 148 compileView project(':producer')
149 149 }
150 150
151 151 tasks.register('probe') {
152 152 doLast {
153 153 def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',')
154 154 def jsFiles = configurations.compileView.incoming.artifactView {
155 155 attributes {
156 156 attribute(slotAttr, 'js')
157 157 }
158 158 }.files.files.collect { it.name }.sort().join(',')
159 159
160 160 println('compileFiles=' + compileFiles)
161 161 println('jsFiles=' + jsFiles)
162 162 }
163 163 }
164 164 }
165 165 """);
166 166
167 if (targetGradleVersion().compareTo(org.gradle.util.GradleVersion.version("9.4.1")) < 0) {
168 assertBuildFails("Cannot create variant 'js' after dependency configuration ':producer:browserElements' has been resolved",
169 ":consumer:probe");
170 } else {
167 171 BuildResult result = runner(":consumer:probe").build();
168 172
169 173 assertTrue(result.getOutput().contains("compileFiles=typesPackage"));
170 174 assertTrue(result.getOutput().contains("jsFiles=js"));
171 175 }
176 }
172 177
173 178 @Test
174 179 void materializesPrimaryAndSecondarySlotsAndInvokesOutgoingHooks() throws Exception {
175 180 writeSettings("variant-artifacts-slots");
176 181 writeFile("inputs/base.js", "console.log('base')\n");
177 182 writeFile("inputs/amd.js", "console.log('amd')\n");
178 183 writeFile("inputs/mainJs.txt", "mainJs marker\n");
179 184 writeFile("inputs/amdJs.txt", "amdJs marker\n");
180 185 writeBuildFile("""
181 186 import org.gradle.api.attributes.Attribute
182 187
183 188 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
184 189
185 190 def variantAttr = Attribute.of('test.variant', String)
186 191 def slotAttr = Attribute.of('test.slot', String)
187 192
188 193 variants.layers.create('mainBase')
189 194 variants.layers.create('mainAmd')
190 195 variants.roles.create('main')
191 196 variants.roles.create('test')
192 197 variants.variant('browser') {
193 198 role('main') {
194 199 layers('mainBase', 'mainAmd')
195 200 }
196 201 }
197 202
198 203 variantSources {
199 204 layer('mainBase') {
200 205 sourceSet {
201 206 declareOutputs('js')
202 207 registerOutput('js', layout.projectDirectory.file('inputs/base.js'))
203 208 }
204 209 }
205 210 layer('mainAmd') {
206 211 sourceSet {
207 212 declareOutputs('js')
208 213 registerOutput('js', layout.projectDirectory.file('inputs/amd.js'))
209 214 }
210 215 }
211 216 }
212 217
213 218 variantArtifacts {
214 219 variant('browser') {
215 220 primarySlot('mainJs') {
216 221 fromRole('main') {
217 222 output('js')
218 223 }
219 224 from(layout.projectDirectory.file('inputs/mainJs.txt'))
220 225 }
221 226 slot('amdJs') {
222 227 fromLayer('mainAmd') {
223 228 output('js')
224 229 }
225 230 from(layout.projectDirectory.file('inputs/amdJs.txt'))
226 231 }
227 232 }
228 233
229 234 whenOutgoingConfiguration { publication ->
230 235 publication.configuration {
231 236 attributes.attribute(variantAttr, publication.variant.name)
232 237 }
233 238 }
234 239
235 240 whenOutgoingSlot { publication ->
236 241 def slotName = publication.artifactSlot.slot.name
237 242 publication.artifactAttributes {
238 243 attribute(slotAttr, slotName)
239 244 }
240 245 }
241 246 }
242 247
243 248 tasks.register('probe') {
244 249 dependsOn 'assembleVariantArtifactSlot_v7_browser_s6_mainJs'
245 250 dependsOn 'assembleVariantArtifactSlot_v7_browser_s5_amdJs'
246 251
247 252 doLast {
248 253 def mainDir = layout.buildDirectory.dir('variant-assemblies/browser/mainJs').get().asFile
249 254 def amdDir = layout.buildDirectory.dir('variant-assemblies/browser/amdJs').get().asFile
250 255
251 256 assert new File(mainDir, 'base.js').exists()
252 257 assert new File(mainDir, 'amd.js').exists()
253 258 assert new File(mainDir, 'mainJs.txt').exists()
254 259
255 260 assert !new File(amdDir, 'base.js').exists()
256 261 assert new File(amdDir, 'amd.js').exists()
257 262 assert new File(amdDir, 'amdJs.txt').exists()
258 263
259 264 def elements = configurations.getByName('browserElements')
260 265 def amdVariant = elements.outgoing.variants.getByName('amdJs')
261 266
262 267 println('variantAttr=' + elements.attributes.getAttribute(variantAttr))
263 268 println('primarySlotAttr=' + elements.outgoing.attributes.getAttribute(slotAttr))
264 269 println('amdSlotAttr=' + amdVariant.attributes.getAttribute(slotAttr))
265 270 println('configurations=' + configurations.matching { it.name == 'browserElements' }.collect { it.name }.join(','))
266 271 println('secondaryVariants=' + elements.outgoing.variants.collect { it.name }.sort().join(','))
267 272 }
268 273 }
269 274 """);
270 275
271 276 BuildResult result = runner("probe").build();
272 277
273 278 assertTrue(result.getOutput().contains("variantAttr=browser"));
274 279 assertTrue(result.getOutput().contains("primarySlotAttr=mainJs"));
275 280 assertTrue(result.getOutput().contains("amdSlotAttr=amdJs"));
276 281 assertTrue(result.getOutput().contains("configurations=browserElements"));
277 282 assertTrue(result.getOutput().contains("secondaryVariants=amdJs"));
278 283 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
279 284 }
280 285
281 286 @Test
282 287 void outgoingSlotHookFollowsMaterializedGradleArtifactVariants() throws Exception {
283 288 writeSettings("variant-artifacts-materialized-gradle-variant");
284 289 writeFile("inputs/typesPackage", "types\n");
285 290 writeFile("inputs/js", "js\n");
286 291 writeBuildFile("""
287 292 import org.gradle.api.attributes.Attribute
288 293
289 294 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
290 295
291 296 def slotAttr = Attribute.of('test.slot', String)
292 297
293 298 variants.layers.create('main')
294 299 variants.roles.create('main')
295 300 variants.variant('browser') {
296 301 role('main') {
297 302 layers('main')
298 303 }
299 304 }
300 305
301 306 variantArtifacts {
302 307 variant('browser') {
303 308 primarySlot('typesPackage') {
304 309 from(layout.projectDirectory.file('inputs/typesPackage'))
305 310 }
306 311 }
307 312
308 313 whenOutgoingConfiguration { publication ->
309 314 publication.configuration {
310 315 outgoing.variants.create('js') { secondary ->
311 316 secondary.artifact(layout.projectDirectory.file('inputs/js'))
312 317 }
313 318 }
314 319 }
315 320
316 321 whenOutgoingSlot { publication ->
317 322 publication.artifactAttributes {
318 323 attribute(slotAttr, publication.artifactSlot.slot.name)
319 324 }
320 325 }
321 326 }
322 327
323 328 tasks.register('probe') {
324 329 doLast {
325 330 def elements = configurations.getByName('browserElements')
326 331 def jsVariant = elements.outgoing.variants.getByName('js')
327 332
328 333 println('primarySlotAttr=' + elements.outgoing.attributes.getAttribute(slotAttr))
329 334 println('jsSlotAttr=' + jsVariant.attributes.getAttribute(slotAttr))
330 335 }
331 336 }
332 337 """);
333 338
334 339 BuildResult result = runner("probe").build();
335 340
336 341 assertTrue(result.getOutput().contains("primarySlotAttr=typesPackage"));
337 342 assertTrue(result.getOutput().contains("jsSlotAttr=js"));
338 343 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
339 344 }
340 345
341 346 @Test
342 347 void allowsSingleSlotVariantWithoutExplicitPrimarySlot() throws Exception {
343 348 writeSettings("variant-artifacts-single-slot");
344 349 writeBuildFile("""
345 350 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
346 351
347 352 variants.layers.create('main')
348 353 variants.roles.create('main')
349 354 variants.variant('browser') {
350 355 role('main') {
351 356 layers('main')
352 357 }
353 358 }
354 359
355 360 variantSources.layer('main') {
356 361 sourceSet {
357 362 declareOutputs('types')
358 363 }
359 364 }
360 365
361 366 variantArtifacts {
362 367 variant('browser') {
363 368 slot('typesPackage') {
364 369 fromVariant {
365 370 output('types')
366 371 }
367 372 }
368 373 }
369 374 }
370 375
371 376 tasks.register('probe') {
372 377 doLast {
373 378 variantArtifacts.whenAvailable { ctx ->
374 379 def browser = objects.named(org.implab.gradle.variants.core.Variant, 'browser')
375 380 println('primary=' + ctx.findOutgoing(browser).get().primarySlot.get().name)
376 381 }
377 382 }
378 383 }
379 384 """);
380 385
381 386 BuildResult result = runner("probe").build();
382 387
383 388 assertTrue(result.getOutput().contains("primary=typesPackage"));
384 389 }
385 390
386 391 @Test
387 392 void materializesDirectSlotInputsWithoutVariantSourceBindings() throws Exception {
388 393 writeSettings("variant-artifacts-direct-input");
389 394 writeFile("inputs/bundle.js", "console.log('bundle')\n");
390 395 writeBuildFile("""
391 396 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
392 397
393 398 variants.layers.create('main')
394 399 variants.roles.create('main')
395 400 variants.variant('browser') {
396 401 role('main') {
397 402 layers('main')
398 403 }
399 404 }
400 405
401 406 variantArtifacts {
402 407 variant('browser') {
403 408 primarySlot('bundle') {
404 409 from(layout.projectDirectory.file('inputs/bundle.js'))
405 410 }
406 411 }
407 412 }
408 413
409 414 tasks.register('probe') {
410 415 dependsOn 'assembleVariantArtifactSlot_v7_browser_s6_bundle'
411 416
412 417 doLast {
413 418 def bundleDir = layout.buildDirectory.dir('variant-assemblies/browser/bundle').get().asFile
414 419 assert new File(bundleDir, 'bundle.js').exists()
415 420 }
416 421 }
417 422 """);
418 423
419 424 BuildResult result = runner("probe").build();
420 425
421 426 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
422 427 }
423 428
424 429 @Test
425 430 void publishesTaskProducedFileArtifactDirectly() throws Exception {
426 431 writeFile("settings.gradle", """
427 432 rootProject.name = 'variant-artifacts-task-produced-file'
428 433 include 'producer', 'consumer'
429 434 """);
430 435 writeBuildFile("""
431 436 import org.gradle.api.DefaultTask
432 437 import org.gradle.api.attributes.Attribute
433 438 import org.gradle.api.file.RegularFileProperty
434 439 import org.gradle.api.tasks.OutputFile
435 440 import org.gradle.api.tasks.TaskAction
436 441
437 442 def variantAttr = Attribute.of('test.variant', String)
438 443 def slotAttr = Attribute.of('test.slot', String)
439 444
440 445 abstract class WritePackageMetadata extends DefaultTask {
441 446 @OutputFile
442 447 abstract RegularFileProperty getOutputFile()
443 448
444 449 @TaskAction
445 450 void write() {
446 451 def file = outputFile.get().asFile
447 452 file.parentFile.mkdirs()
448 453 file.text = '{"name":"demo"}\\n'
449 454 }
450 455 }
451 456
452 457 project(':producer') {
453 458 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
454 459
455 460 variants.layers.create('main')
456 461 variants.roles.create('main')
457 462 variants.variant('browser') {
458 463 role('main') {
459 464 layers('main')
460 465 }
461 466 }
462 467
463 468 def writePackageMetadata = tasks.register('writePackageMetadata', WritePackageMetadata) {
464 469 outputFile.set(layout.buildDirectory.file('generated/package.json'))
465 470 }
466 471
467 472 variantArtifacts {
468 473 variant('browser') {
469 474 primarySlot('packageMetadata') {
470 475 producedBy(writePackageMetadata) {
471 476 outputFile
472 477 }
473 478 }
474 479 }
475 480
476 481 whenOutgoingConfiguration { publication ->
477 482 publication.configuration {
478 483 attributes.attribute(variantAttr, publication.variant.name)
479 484 }
480 485 }
481 486
482 487 whenOutgoingSlot { publication ->
483 488 publication.artifactAttributes {
484 489 attribute(slotAttr, publication.artifactSlot.slot.name)
485 490 }
486 491 }
487 492 }
488 493
489 494 tasks.register('checkNoManagedAssembly') {
490 495 doLast {
491 496 def assemblyTasks = tasks.names
492 497 .findAll { it.startsWith('assembleVariantArtifactSlot') }
493 498 .sort()
494 499 println('producerAssemblyTasks=' + assemblyTasks.join(','))
495 500 assert assemblyTasks.empty
496 501 }
497 502 }
498 503 }
499 504
500 505 project(':consumer') {
501 506 configurations {
502 507 compileView {
503 508 canBeResolved = true
504 509 canBeConsumed = false
505 510 canBeDeclared = true
506 511 attributes {
507 512 attribute(variantAttr, 'browser')
508 513 attribute(slotAttr, 'packageMetadata')
509 514 }
510 515 }
511 516 }
512 517
513 518 dependencies {
514 519 compileView project(':producer')
515 520 }
516 521
517 522 tasks.register('probe') {
518 523 dependsOn configurations.compileView
519 524 dependsOn ':producer:checkNoManagedAssembly'
520 525
521 526 doLast {
522 527 def files = configurations.compileView.files
523 528 println('resolvedFiles=' + files.collect { it.name }.sort().join(','))
524 529 println('metadata=' + files.iterator().next().text.trim())
525 530 }
526 531 }
527 532 }
528 533 """);
529 534
530 535 BuildResult result = runner(":consumer:probe").build();
531 536
532 537 assertTrue(result.getOutput().contains("producerAssemblyTasks="));
533 538 assertTrue(result.getOutput().contains("resolvedFiles=package.json"));
534 539 assertTrue(result.getOutput().contains("metadata={\"name\":\"demo\"}"));
535 540 assertTrue(result.task(":producer:writePackageMetadata").getOutcome() == TaskOutcome.SUCCESS);
536 541 assertTrue(result.task(":consumer:probe").getOutcome() == TaskOutcome.SUCCESS);
537 542 }
538 543
539 544 @Test
540 545 void failsWhenTaskProducedArtifactIsMixedWithContributionAssembly() throws Exception {
541 546 writeSettings("variant-artifacts-mixed-assembly-mode");
542 547 writeFile("inputs/marker.txt", "marker\n");
543 548 writeBuildFile("""
544 549 import org.gradle.api.DefaultTask
545 550 import org.gradle.api.file.RegularFileProperty
546 551 import org.gradle.api.tasks.OutputFile
547 552 import org.gradle.api.tasks.TaskAction
548 553
549 554 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
550 555
551 556 abstract class WritePackageMetadata extends DefaultTask {
552 557 @OutputFile
553 558 abstract RegularFileProperty getOutputFile()
554 559
555 560 @TaskAction
556 561 void write() {
557 562 outputFile.get().asFile.text = '{}\\n'
558 563 }
559 564 }
560 565
561 566 variants.layers.create('main')
562 567 variants.roles.create('main')
563 568 variants.variant('browser') {
564 569 role('main') {
565 570 layers('main')
566 571 }
567 572 }
568 573
569 574 def writePackageMetadata = tasks.register('writePackageMetadata', WritePackageMetadata) {
570 575 outputFile.set(layout.buildDirectory.file('generated/package.json'))
571 576 }
572 577
573 578 variantArtifacts {
574 579 variant('browser') {
575 580 primarySlot('packageMetadata') {
576 581 producedBy(writePackageMetadata) {
577 582 outputFile
578 583 }
579 584 from(layout.projectDirectory.file('inputs/marker.txt'))
580 585 }
581 586 }
582 587 }
583 588 """);
584 589
585 590 assertBuildFails("cannot mix task-produced artifact and contribution-based assembly", "help");
586 591 }
587 592
588 593 @Test
589 594 void combinesDirectAndTopologyAwareSlotInputs() throws Exception {
590 595 writeSettings("variant-artifacts-combined-inputs");
591 596 writeFile("inputs/base.js", "console.log('base')\n");
592 597 writeFile("inputs/marker.txt", "marker\n");
593 598 writeBuildFile("""
594 599 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
595 600
596 601 variants.layers.create('main')
597 602 variants.roles.create('main')
598 603 variants.variant('browser') {
599 604 role('main') {
600 605 layers('main')
601 606 }
602 607 }
603 608
604 609 variantSources.layer('main') {
605 610 sourceSet {
606 611 declareOutputs('js')
607 612 registerOutput('js', layout.projectDirectory.file('inputs/base.js'))
608 613 }
609 614 }
610 615
611 616 variantArtifacts {
612 617 variant('browser') {
613 618 primarySlot('bundle') {
614 619 fromVariant {
615 620 output('js')
616 621 }
617 622 from(layout.projectDirectory.file('inputs/marker.txt'))
618 623 }
619 624 }
620 625 }
621 626
622 627 tasks.register('probe') {
623 628 dependsOn 'assembleVariantArtifactSlot_v7_browser_s6_bundle'
624 629
625 630 doLast {
626 631 def bundleDir = layout.buildDirectory.dir('variant-assemblies/browser/bundle').get().asFile
627 632 assert new File(bundleDir, 'base.js').exists()
628 633 assert new File(bundleDir, 'marker.txt').exists()
629 634 }
630 635 }
631 636 """);
632 637
633 638 BuildResult result = runner("probe").build();
634 639
635 640 assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
636 641 }
637 642
638 643 @Test
639 644 void failsOnUnknownVariantReference() throws Exception {
640 645 writeSettings("variant-artifacts-missing-variant");
641 646 writeBuildFile("""
642 647 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
643 648
644 649 variants.layers.create('main')
645 650
646 651 variantArtifacts {
647 652 variant('browser') {
648 653 slot('mainJs') {
649 654 fromVariant {
650 655 output('js')
651 656 }
652 657 }
653 658 }
654 659 }
655 660 """);
656 661
657 662 assertBuildFails("isn't declared", "help");
658 663 }
659 664
660 665 @Test
661 666 void failsOnUnknownRoleReference() throws Exception {
662 667 writeSettings("variant-artifacts-missing-role");
663 668 writeBuildFile("""
664 669 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
665 670
666 671 variants.layers.create('main')
667 672 variants.roles.create('main')
668 673 variants.variant('browser') {
669 674 role('main') {
670 675 layers('main')
671 676 }
672 677 }
673 678
674 679 variantArtifacts {
675 680 variant('browser') {
676 681 slot('mainJs') {
677 682 fromRole('test') {
678 683 output('js')
679 684 }
680 685 }
681 686 }
682 687 }
683 688 """);
684 689
685 690 assertBuildFails("Role projection for variant 'browser' and role 'test' not found", "help");
686 691 }
687 692
688 693 @Test
689 694 void failsWhenPrimarySlotIsMissingForMultipleSlots() throws Exception {
690 695 writeSettings("variant-artifacts-missing-primary");
691 696 writeBuildFile("""
692 697 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
693 698
694 699 variants.layers.create('main')
695 700 variants.roles.create('main')
696 701 variants.variant('browser') {
697 702 role('main') {
698 703 layers('main')
699 704 }
700 705 }
701 706
702 707 variantSources.layer('main') {
703 708 sourceSet {
704 709 declareOutputs('types', 'js')
705 710 }
706 711 }
707 712
708 713 variantArtifacts {
709 714 variant('browser') {
710 715 slot('typesPackage') {
711 716 fromVariant {
712 717 output('types')
713 718 }
714 719 }
715 720 slot('js') {
716 721 fromVariant {
717 722 output('js')
718 723 }
719 724 }
720 725 }
721 726 }
722 727
723 728 tasks.register('probe') {
724 729 doLast {
725 730 variantArtifacts.whenAvailable { ctx ->
726 731 def browser = objects.named(org.implab.gradle.variants.core.Variant, 'browser')
727 732 ctx.findOutgoing(browser).get().primarySlot.get()
728 733 }
729 734 }
730 735 }
731 736 """);
732 737
733 738 assertBuildFails("Multiple slots declared for browser, please specify primary slot explicitly", "probe");
734 739 }
735 740
736 741 @Test
737 742 void failsOnLayerReferenceOutsideVariantTopology() throws Exception {
738 743 writeSettings("variant-artifacts-layer-outside-topology");
739 744 writeBuildFile("""
740 745 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
741 746
742 747 variants.layers.create('mainBase')
743 748 variants.layers.create('extra')
744 749 variants.roles.create('main')
745 750 variants.variant('browser') {
746 751 role('main') {
747 752 layers('mainBase')
748 753 }
749 754 }
750 755
751 756 variantArtifacts {
752 757 variant('browser') {
753 758 slot('extraJs') {
754 759 fromLayer('extra') {
755 760 output('js')
756 761 }
757 762 }
758 763 }
759 764 }
760 765 """);
761 766
762 767 assertBuildFails("Compile unit for variant 'browser' and layer 'extra' not found", "help");
763 768 }
764 769
765 770 @Test
766 771 void preservesPrimaryResolutionAndAllowsSecondaryArtifactSelection() throws Exception {
767 772 writeFile("settings.gradle", """
768 773 rootProject.name = 'variant-artifacts-resolution'
769 774 include 'producer', 'consumer'
770 775 """);
771 776 writeFile("producer/inputs/types.d.ts", "export type Foo = string\n");
772 777 writeFile("producer/inputs/index.js", "export const foo = 'bar'\n");
773 778 writeBuildFile("""
774 779 import org.gradle.api.attributes.Attribute
775 780
776 781 def variantAttr = Attribute.of('test.variant', String)
777 782 def slotAttr = Attribute.of('test.slot', String)
778 783
779 784 subprojects {
780 785 apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
781 786 }
782 787
783 788 project(':producer') {
784 789 variants.layers.create('main')
785 790 variants.roles.create('main')
786 791 variants.variant('browser') {
787 792 role('main') {
788 793 layers('main')
789 794 }
790 795 }
791 796
792 797 variantSources.layer('main') {
793 798 sourceSet {
794 799 declareOutputs('types', 'js')
795 800 registerOutput('types', layout.projectDirectory.file('inputs/types.d.ts'))
796 801 registerOutput('js', layout.projectDirectory.file('inputs/index.js'))
797 802 }
798 803 }
799 804
800 805 variantArtifacts {
801 806 variant('browser') {
802 807 primarySlot('typesPackage') {
803 808 fromVariant {
804 809 output('types')
805 810 }
806 811 }
807 812 slot('js') {
808 813 fromVariant {
809 814 output('js')
810 815 }
811 816 }
812 817 }
813 818
814 819 whenOutgoingConfiguration { publication ->
815 820 publication.configuration {
816 821 attributes.attribute(variantAttr, publication.variant.name)
817 822 }
818 823 }
819 824
820 825 whenOutgoingSlot { publication ->
821 826 publication.artifactAttributes {
822 827 attribute(slotAttr, publication.artifactSlot.slot.name)
823 828 }
824 829 }
825 830 }
826 831
827 832 }
828 833
829 834 project(':consumer') {
830 835 configurations {
831 836 compileView {
832 837 canBeResolved = true
833 838 canBeConsumed = false
834 839 canBeDeclared = true
835 840 attributes {
836 841 attribute(variantAttr, 'browser')
837 842 attribute(slotAttr, 'typesPackage')
838 843 }
839 844 }
840 845 }
841 846
842 847 dependencies {
843 848 compileView project(':producer')
844 849 }
845 850
846 851 tasks.register('probe') {
847 852 doLast {
848 853 def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',')
849 854 def jsFiles = configurations.compileView.incoming.artifactView {
850 855 attributes {
851 856 attribute(slotAttr, 'js')
852 857 }
853 858 }.files.files.collect { it.name }.sort().join(',')
854 859
855 860 println('compileFiles=' + compileFiles)
856 861 println('jsFiles=' + jsFiles)
857 862 }
858 863 }
859 864 }
860 865 """);
861 866
862 867 BuildResult result = runner(":consumer:probe").build();
863 868
864 869 assertTrue(result.getOutput().contains("compileFiles=typesPackage"));
865 870 assertTrue(result.getOutput().contains("jsFiles=js"));
866 871 }
867 872 }
General Comments 0
You need to be logged in to leave comments. Login now