diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/VariantSourcesExtension.java b/variants/src/main/java/org/implab/gradle/variants/sources/VariantSourcesExtension.java --- a/variants/src/main/java/org/implab/gradle/variants/sources/VariantSourcesExtension.java +++ b/variants/src/main/java/org/implab/gradle/variants/sources/VariantSourcesExtension.java @@ -90,6 +90,10 @@ public interface VariantSourcesExtension */ void unit(String variantName, String layerName, Action action); + default void unit(String variantName, String layerName, Closure closure) { + unit(variantName, layerName, Closures.action(closure)); + } + /** * Invoked when finalized variants-derived source context becomes available. * diff --git a/variants/src/test/java/org/implab/gradle/variants/AbstractFunctionalTest.java b/variants/src/test/java/org/implab/gradle/variants/AbstractFunctionalTest.java new file mode 100644 --- /dev/null +++ b/variants/src/test/java/org/implab/gradle/variants/AbstractFunctionalTest.java @@ -0,0 +1,83 @@ +package org.implab.gradle.variants; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.stream.Collectors; + +import org.gradle.testkit.runner.GradleRunner; +import org.gradle.testkit.runner.UnexpectedBuildFailure; +import org.implab.gradle.common.sources.GenericSourceSet; +import org.junit.jupiter.api.io.TempDir; + +abstract class AbstractFunctionalTest { + private static final String SETTINGS_FILE = "settings.gradle"; + private static final String BUILD_FILE = "build.gradle"; + + @TempDir + Path testProjectDir; + + protected void writeSettings(String rootProjectName) throws IOException { + writeFile(SETTINGS_FILE, "rootProject.name = '" + rootProjectName + "'\n"); + } + + protected void writeBuildFile(String body) throws IOException { + writeFile(BUILD_FILE, buildscriptClasspathBlock() + "\n" + body); + } + + protected GradleRunner runner(String... arguments) { + return GradleRunner.create() + .withProjectDir(testProjectDir.toFile()) + .withArguments(arguments) + .forwardOutput(); + } + + protected void assertBuildFails(String expectedError, String... arguments) { + var ex = org.junit.jupiter.api.Assertions.assertThrows( + UnexpectedBuildFailure.class, + () -> runner(arguments).build()); + + var output = ex.getBuildResult().getOutput(); + assertTrue(output.contains(expectedError), () -> "Expected [" + expectedError + "] in output:\n" + output); + } + + private static String buildscriptClasspathBlock() { + var classpath = pluginClasspath().stream() + .map(file -> "'" + file.getAbsolutePath().replace("\\", "\\\\") + "'") + .collect(Collectors.joining(", ")); + + return """ + buildscript { + dependencies { + classpath files(%s) + } + } + """.formatted(classpath); + } + + private static List pluginClasspath() { + try { + var files = new LinkedHashSet(); + files.add(codeSource(VariantSourcesPlugin.class)); + files.add(codeSource(GenericSourceSet.class)); + return List.copyOf(files); + } catch (Exception e) { + throw new RuntimeException("Unable to build plugin classpath for test", e); + } + } + + private static File codeSource(Class type) throws Exception { + return Path.of(type.getProtectionDomain().getCodeSource().getLocation().toURI()).toFile(); + } + + private void writeFile(String relativePath, String content) throws IOException { + Path path = testProjectDir.resolve(relativePath); + Files.createDirectories(path.getParent()); + Files.writeString(path, content); + } +} diff --git a/variants/src/test/java/org/implab/gradle/variants/VariantSourcesPluginFunctionalTest.java b/variants/src/test/java/org/implab/gradle/variants/VariantSourcesPluginFunctionalTest.java new file mode 100644 --- /dev/null +++ b/variants/src/test/java/org/implab/gradle/variants/VariantSourcesPluginFunctionalTest.java @@ -0,0 +1,431 @@ +package org.implab.gradle.variants; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.gradle.testkit.runner.BuildResult; +import org.junit.jupiter.api.Test; + +class VariantSourcesPluginFunctionalTest extends AbstractFunctionalTest { + + @Test + void exposesDerivedViewsAndStableSourceSetProvider() throws Exception { + writeSettings("variant-sources-derived-views"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantSourcesPlugin + + def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension) + variantsExt.layers.create('main') + variantsExt.layers.create('test') + variantsExt.roles.create('production') + variantsExt.roles.create('test') + + variantsExt.variant('browser') { + role('production') { layers('main') } + role('test') { layers('main', 'test') } + } + + def lines = [] + + variantSources.whenFinalized { ctx -> + lines << "units=" + ctx.compileUnits.units + .collect { "${it.variant().name}:${it.layer().name}" } + .sort() + .join(',') + + def browser = ctx.variants.variants.find { it.name == 'browser' } + def production = ctx.variants.roles.find { it.name == 'production' } + def mainLayer = ctx.variants.layers.find { it.name == 'main' } + def projection = ctx.roleProjections.getProjection(browser, production) + def unit = ctx.compileUnits.getUnit(browser, mainLayer) + + def left = ctx.sourceSets.getSourceSet(unit) + def right = ctx.sourceSets.getSourceSet(unit) + + lines << "projectionUnits=" + ctx.roleProjections.getUnits(projection) + .collect { it.layer().name } + .sort() + .join(',') + lines << "mainSourceSet=" + left.name + lines << "sameProvider=" + left.is(right) + } + + afterEvaluate { + variantSources.whenFinalized { ctx -> + lines << "late:variants=" + ctx.variants.variants.collect { it.name }.sort().join(',') + } + } + + tasks.register('probe') { + doLast { + lines.each { println(it) } + } + } + """); + + BuildResult result = runner("probe").build(); + + assertTrue(result.getOutput().contains("units=browser:main,browser:test")); + assertTrue(result.getOutput().contains("projectionUnits=main")); + assertTrue(result.getOutput().contains("mainSourceSet=browserMain")); + assertTrue(result.getOutput().contains("sameProvider=true")); + assertTrue(result.getOutput().contains("late:variants=browser")); + } + + @Test + void appliesSelectorPrecedenceForFutureMaterialization() throws Exception { + writeSettings("variant-sources-precedence"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantSourcesPlugin + + def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension) + variantsExt.layers.create('main') + variantsExt.layers.create('test') + variantsExt.roles.create('production') + variantsExt.roles.create('test') + + variantsExt.variant('browser') { + role('production') { layers('main') } + role('test') { layers('main', 'test') } + } + + variantsExt.variant('node') { + role('production') { layers('main') } + } + + def events = [] + + variantSources { + variant('browser') { + events << "variant:" + name + } + layer('main') { + events << "layer:" + name + } + unit('browser', 'main') { + events << "unit:" + name + } + } + + afterEvaluate { + variantSources.whenFinalized { ctx -> + def browser = ctx.variants.variants.find { it.name == 'browser' } + def node = ctx.variants.variants.find { it.name == 'node' } + def mainLayer = ctx.variants.layers.find { it.name == 'main' } + def testLayer = ctx.variants.layers.find { it.name == 'test' } + + def browserMain = ctx.sourceSets.getSourceSet(ctx.compileUnits.getUnit(browser, mainLayer)).get() + def browserTest = ctx.sourceSets.getSourceSet(ctx.compileUnits.getUnit(browser, testLayer)).get() + def nodeMain = ctx.sourceSets.getSourceSet(ctx.compileUnits.getUnit(node, mainLayer)).get() + def bySourceSet = events.groupBy { it.split(':', 2)[1] } + + println("browserMain=" + bySourceSet[browserMain.name].collect { it.split(':', 2)[0] }.join(',')) + println("browserTest=" + bySourceSet[browserTest.name].collect { it.split(':', 2)[0] }.join(',')) + println("nodeMain=" + bySourceSet[nodeMain.name].collect { it.split(':', 2)[0] }.join(',')) + } + } + """); + + BuildResult result = runner("help").build(); + + assertTrue(result.getOutput().contains("browserMain=variant,layer,unit")); + assertTrue(result.getOutput().contains("browserTest=variant")); + assertTrue(result.getOutput().contains("nodeMain=layer")); + } + + @Test + void failsLateConfigurationByDefaultAfterMaterialization() throws Exception { + writeSettings("variant-sources-late-fail"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantSourcesPlugin + + def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension) + variantsExt.layers.create('main') + variantsExt.roles.create('production') + variantsExt.variant('browser') { + role('production') { layers('main') } + } + + afterEvaluate { + variantSources.whenFinalized { ctx -> + def browser = ctx.variants.variants.find { it.name == 'browser' } + def mainLayer = ctx.variants.layers.find { it.name == 'main' } + def unit = ctx.compileUnits.getUnit(browser, mainLayer) + + ctx.sourceSets.getSourceSet(unit).get() + variantSources.layer('main') { + declareOutputs('late') + } + } + } + """); + + assertBuildFails("Source sets for [layer=main] layer already materialized", "help"); + } + + @Test + void allowsLateConfigurationWhenSelectedBeforeFirstSelector() throws Exception { + writeSettings("variant-sources-late-allow"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantSourcesPlugin + + def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension) + variantsExt.layers.create('main') + variantsExt.roles.create('production') + variantsExt.variant('browser') { + role('production') { layers('main') } + } + + variantSources { + lateConfigurationPolicy { + allowLateConfiguration() + } + } + + afterEvaluate { + variantSources.whenFinalized { ctx -> + def browser = ctx.variants.variants.find { it.name == 'browser' } + def mainLayer = ctx.variants.layers.find { it.name == 'main' } + def unit = ctx.compileUnits.getUnit(browser, mainLayer) + + def sourceSet = ctx.sourceSets.getSourceSet(unit).get() + variantSources.layer('main') { + declareOutputs('late') + } + sourceSet.output('late') + println('lateAllowed=ok') + } + } + """); + + BuildResult result = runner("help").build(); + assertTrue(result.getOutput().contains("lateAllowed=ok")); + } + + @Test + void warnsAndAppliesLateConfigurationWhenWarnModeSelected() throws Exception { + writeSettings("variant-sources-late-warn"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantSourcesPlugin + + def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension) + variantsExt.layers.create('main') + variantsExt.roles.create('production') + variantsExt.variant('browser') { + role('production') { layers('main') } + } + + variantSources { + lateConfigurationPolicy { + warnOnLateConfiguration() + } + } + + afterEvaluate { + variantSources.whenFinalized { ctx -> + def browser = ctx.variants.variants.find { it.name == 'browser' } + def mainLayer = ctx.variants.layers.find { it.name == 'main' } + def unit = ctx.compileUnits.getUnit(browser, mainLayer) + + def sourceSet = ctx.sourceSets.getSourceSet(unit).get() + variantSources.layer('main') { + declareOutputs('late') + } + sourceSet.output('late') + println('lateWarn=ok') + } + } + """); + + BuildResult result = runner("help").build(); + + assertTrue(result.getOutput().contains("Source sets for [layer=main] layer already materialized")); + assertTrue(result.getOutput().contains("lateWarn=ok")); + } + + @Test + void rejectsChangingLateConfigurationPolicyAfterFirstSelector() throws Exception { + writeSettings("variant-sources-late-policy-fixed"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantSourcesPlugin + + def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension) + variantsExt.layers.create('main') + variantsExt.roles.create('production') + variantsExt.variant('browser') { + role('production') { layers('main') } + } + + variantSources { + variant('browser') { + declareOutputs('js') + } + lateConfigurationPolicy { + allowLateConfiguration() + } + } + """); + + assertBuildFails("Lazy configuration policy already applied", "help"); + } + + @Test + void failsOnProjectedNameCollisionByDefault() throws Exception { + writeSettings("variant-sources-name-collision-fail"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantSourcesPlugin + + def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension) + variantsExt.layers.create('variantBar') + variantsExt.layers.create('bar') + variantsExt.roles.create('production') + + variantsExt.variant('foo') { + role('production') { layers('variantBar') } + } + variantsExt.variant('fooVariant') { + role('production') { layers('bar') } + } + """); + + assertBuildFails("The same source set names are produced by different compile units", "help"); + } + + @Test + void resolvesProjectedNameCollisionDeterministicallyWhenConfigured() throws Exception { + writeSettings("variant-sources-name-collision-resolve"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantSourcesPlugin + + def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension) + variantsExt.layers.create('variantBar') + variantsExt.layers.create('bar') + variantsExt.roles.create('production') + + variantsExt.variant('foo') { + role('production') { layers('variantBar') } + } + variantsExt.variant('fooVariant') { + role('production') { layers('bar') } + } + + variantSources { + namingPolicy { + resolveNameCollision() + } + } + + afterEvaluate { + variantSources.whenFinalized { ctx -> + def foo = ctx.variants.variants.find { it.name == 'foo' } + def fooVariant = ctx.variants.variants.find { it.name == 'fooVariant' } + def variantBar = ctx.variants.layers.find { it.name == 'variantBar' } + def bar = ctx.variants.layers.find { it.name == 'bar' } + + def later = ctx.compileUnits.getUnit(fooVariant, bar) + def earlier = ctx.compileUnits.getUnit(foo, variantBar) + + println("map1=" + later.variant().name + ":" + later.layer().name + "->" + ctx.sourceSets.getSourceSet(later).name) + println("map2=" + earlier.variant().name + ":" + earlier.layer().name + "->" + ctx.sourceSets.getSourceSet(earlier).name) + } + } + """); + + BuildResult result = runner("help").build(); + + assertTrue(result.getOutput().contains("map1=fooVariant:bar->fooVariantBar2")); + assertTrue(result.getOutput().contains("map2=foo:variantBar->fooVariantBar")); + } + + @Test + void failsOnUnknownVariantSelectorTarget() throws Exception { + writeSettings("variant-sources-missing-variant"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantSourcesPlugin + + def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension) + variantsExt.layers.create('main') + variantsExt.roles.create('production') + variantsExt.variant('browser') { + role('production') { layers('main') } + } + + variantSources { + variant('missing') { + declareOutputs('js') + } + } + """); + + assertBuildFails("Variant 'missing' is't declared", "help"); + } + + @Test + void failsOnUnknownLayerSelectorTarget() throws Exception { + writeSettings("variant-sources-missing-layer"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantSourcesPlugin + + def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension) + variantsExt.layers.create('main') + variantsExt.roles.create('production') + variantsExt.variant('browser') { + role('production') { layers('main') } + } + + variantSources { + layer('missing') { + declareOutputs('js') + } + } + """); + + assertBuildFails("Layer 'missing' isn't declared", "help"); + } + + @Test + void failsOnUndeclaredCompileUnitSelectorTarget() throws Exception { + writeSettings("variant-sources-missing-unit"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantSourcesPlugin + + def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension) + variantsExt.layers.create('main') + variantsExt.layers.create('test') + variantsExt.roles.create('production') + variantsExt.variant('browser') { + role('production') { layers('main') } + } + + variantSources { + unit('browser', 'test') { + declareOutputs('js') + } + } + """); + + assertBuildFails("The CompileUnit isn't declared for variant 'browser', layer 'test'", "help"); + } + + @Test + void rejectsChangingNamingPolicyAfterContextBecomesObservable() throws Exception { + writeSettings("variant-sources-name-policy-fixed"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantSourcesPlugin + + def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension) + variantsExt.layers.create('main') + variantsExt.roles.create('production') + variantsExt.variant('browser') { + role('production') { layers('main') } + } + + variantSources.whenFinalized { + variantSources.namingPolicy { + resolveNameCollision() + } + } + """); + + assertBuildFails("Naming policy already applied", "help"); + } +} diff --git a/variants/src/test/java/org/implab/gradle/variants/VariantsPluginFunctionalTest.java b/variants/src/test/java/org/implab/gradle/variants/VariantsPluginFunctionalTest.java new file mode 100644 --- /dev/null +++ b/variants/src/test/java/org/implab/gradle/variants/VariantsPluginFunctionalTest.java @@ -0,0 +1,136 @@ +package org.implab.gradle.variants; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.gradle.testkit.runner.BuildResult; +import org.junit.jupiter.api.Test; + +class VariantsPluginFunctionalTest extends AbstractFunctionalTest { + + @Test + void replaysFinalizedViewAndExposesResolvedEntries() throws Exception { + writeSettings("variants-current-contract"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantsPlugin + + def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension) + variantsExt.layers.create('main') + variantsExt.layers.create('test') + variantsExt.roles.create('production') + variantsExt.roles.create('test') + + variantsExt.variant('browser') { + role('production') { layers('main') } + role('test') { layers('main', 'test') } + } + + def events = [] + + variantsExt.whenFinalized { view -> + events << "early:entries=" + view.entries + .collect { "${it.variant().name}:${it.role().name}:${it.layer().name}" } + .sort() + .join('|') + events << "early:layers=" + view.layers.collect { it.name }.sort().join(',') + def production = view.roles.find { it.name == 'production' } + def mainLayer = view.layers.find { it.name == 'main' } + events << "early:productionEntries=" + view.getEntriesForRole(production).size() + events << "early:mainEntries=" + view.getEntriesForLayer(mainLayer).size() + } + + afterEvaluate { + variantsExt.whenFinalized { view -> + events << "late:variants=" + view.variants.collect { it.name }.sort().join(',') + } + } + + tasks.register('probe') { + doLast { + events.each { println(it) } + } + } + """); + + BuildResult result = runner("probe").build(); + + assertTrue(result.getOutput().contains( + "early:entries=browser:production:main|browser:test:main|browser:test:test")); + assertTrue(result.getOutput().contains("early:layers=main,test")); + assertTrue(result.getOutput().contains("early:productionEntries=1")); + assertTrue(result.getOutput().contains("early:mainEntries=2")); + assertTrue(result.getOutput().contains("late:variants=browser")); + } + + @Test + void exposesDeclaredButUnboundIdentitiesInFinalizedView() throws Exception { + writeSettings("variants-unbound-identities"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantsPlugin + + def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension) + variantsExt.layers.create('main') + variantsExt.layers.create('test') + variantsExt.roles.create('production') + variantsExt.roles.create('test') + + variantsExt.variant('browser') { + role('production') { layers('main') } + } + variantsExt.variant('node') + + tasks.register('probe') { + doLast { + variantsExt.whenFinalized { view -> + def testRole = view.roles.find { it.name == 'test' } + def testLayer = view.layers.find { it.name == 'test' } + + println("variants=" + view.variants.collect { it.name }.sort().join(',')) + println("entriesForTestRole=" + view.getEntriesForRole(testRole).size()) + println("entriesForTestLayer=" + view.getEntriesForLayer(testLayer).size()) + } + } + } + """); + + BuildResult result = runner("probe").build(); + + assertTrue(result.getOutput().contains("variants=browser,node")); + assertTrue(result.getOutput().contains("entriesForTestRole=0")); + assertTrue(result.getOutput().contains("entriesForTestLayer=0")); + } + + @Test + void failsOnUnknownLayerReferenceDuringFinalization() throws Exception { + writeSettings("variants-missing-layer"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantsPlugin + + def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension) + variantsExt.layers.create('main') + variantsExt.roles.create('production') + + variantsExt.variant('browser') { + role('production') { layers('main', 'missingLayer') } + } + """); + + assertBuildFails("Layer 'missingLayer' isn't declared, referenced in 'browser' variant with 'production' role", "help"); + } + + @Test + void failsOnUnknownRoleReferenceDuringFinalization() throws Exception { + writeSettings("variants-missing-role"); + writeBuildFile(""" + apply plugin: org.implab.gradle.variants.VariantsPlugin + + def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension) + variantsExt.layers.create('main') + + variantsExt.variant('browser') { + role('production') { layers('main') } + } + """); + + assertBuildFails("Role 'production' isn't declared, referenced in 'browser' variant", "help"); + } +} diff --git a/variants/src/test/java/org/implab/gradle/variants/sources/CompileUnitsViewTest.java b/variants/src/test/java/org/implab/gradle/variants/sources/CompileUnitsViewTest.java new file mode 100644 --- /dev/null +++ b/variants/src/test/java/org/implab/gradle/variants/sources/CompileUnitsViewTest.java @@ -0,0 +1,104 @@ +package org.implab.gradle.variants.sources; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Constructor; +import java.util.Set; + +import org.implab.gradle.variants.core.Layer; +import org.implab.gradle.variants.core.Role; +import org.implab.gradle.variants.core.Variant; +import org.implab.gradle.variants.core.VariantsView; +import org.implab.gradle.variants.core.VariantsView.VariantRoleLayer; +import org.junit.jupiter.api.Test; + +class CompileUnitsViewTest { + @Test + void deduplicatesCompileUnitsAcrossRolesAndExposesParticipatingRoles() { + var browser = new TestVariant("browser"); + var main = new TestLayer("main"); + var test = new TestLayer("test"); + var production = new TestRole("production"); + var qa = new TestRole("test"); + + var view = view( + Set.of(main, test), + Set.of(production, qa), + Set.of(browser), + Set.of( + new VariantRoleLayer(browser, production, main), + new VariantRoleLayer(browser, qa, main), + new VariantRoleLayer(browser, qa, test))); + + var units = CompileUnitsView.of(view); + var browserMain = units.getUnit(browser, main); + var browserTest = units.getUnit(browser, test); + + assertEquals(2, units.getUnits().size()); + assertEquals(Set.of(browserMain, browserTest), units.getUnitsForVariant(browser)); + assertEquals(Set.of(production, qa), units.getRoles(browserMain)); + assertEquals(Set.of(qa), units.getRoles(browserTest)); + assertTrue(units.contains(browser, main)); + assertTrue(units.contains(browser, test)); + } + + @Test + void rejectsMissingCompileUnitLookup() { + var browser = new TestVariant("browser"); + var main = new TestLayer("main"); + var missing = new TestLayer("missing"); + var production = new TestRole("production"); + + var view = view( + Set.of(main), + Set.of(production), + Set.of(browser), + Set.of(new VariantRoleLayer(browser, production, main))); + + var units = CompileUnitsView.of(view); + + var ex = assertThrows(IllegalArgumentException.class, () -> units.getUnit(browser, missing)); + assertTrue(ex.getMessage().contains("Compile unit for variant 'browser' and layer 'missing' not found")); + } + + private static VariantsView view( + Set layers, + Set roles, + Set variants, + Set entries) { + try { + Constructor ctor = VariantsView.class.getDeclaredConstructor( + Set.class, + Set.class, + Set.class, + Set.class); + ctor.setAccessible(true); + return ctor.newInstance(layers, roles, variants, entries); + } catch (Exception e) { + throw new RuntimeException("Unable to create VariantsView fixture", e); + } + } + + private record TestVariant(String value) implements Variant { + @Override + public String getName() { + return value; + } + } + + private record TestLayer(String value) implements Layer { + @Override + public String getName() { + return value; + } + } + + private record TestRole(String value) implements Role { + @Override + public String getName() { + return value; + } + } +} diff --git a/variants/src/test/java/org/implab/gradle/variants/sources/RoleProjectionsViewTest.java b/variants/src/test/java/org/implab/gradle/variants/sources/RoleProjectionsViewTest.java new file mode 100644 --- /dev/null +++ b/variants/src/test/java/org/implab/gradle/variants/sources/RoleProjectionsViewTest.java @@ -0,0 +1,105 @@ +package org.implab.gradle.variants.sources; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Constructor; +import java.util.Set; + +import org.implab.gradle.variants.core.Layer; +import org.implab.gradle.variants.core.Role; +import org.implab.gradle.variants.core.Variant; +import org.implab.gradle.variants.core.VariantsView; +import org.implab.gradle.variants.core.VariantsView.VariantRoleLayer; +import org.junit.jupiter.api.Test; + +class RoleProjectionsViewTest { + @Test + void exposesRoleProjectionsAndTheirCompileUnits() { + var browser = new TestVariant("browser"); + var main = new TestLayer("main"); + var test = new TestLayer("test"); + var production = new TestRole("production"); + var qa = new TestRole("test"); + + var view = view( + Set.of(main, test), + Set.of(production, qa), + Set.of(browser), + Set.of( + new VariantRoleLayer(browser, production, main), + new VariantRoleLayer(browser, qa, main), + new VariantRoleLayer(browser, qa, test))); + + var projections = RoleProjectionsView.of(view); + var productionProjection = projections.getProjection(browser, production); + var qaProjection = projections.getProjection(browser, qa); + + assertEquals(Set.of(productionProjection, qaProjection), projections.getProjections()); + assertEquals(Set.of(productionProjection, qaProjection), projections.getProjectionsForVariant(browser)); + assertEquals(Set.of(qaProjection), projections.getProjectionsForRole(qa)); + assertEquals(Set.of(new CompileUnit(browser, main)), projections.getUnits(productionProjection)); + assertEquals(Set.of(new CompileUnit(browser, main), new CompileUnit(browser, test)), projections.getUnits(qaProjection)); + assertEquals(Set.of(main, test), projections.getLayers(qaProjection)); + assertTrue(projections.contains(browser, production)); + } + + @Test + void rejectsMissingProjectionLookup() { + var browser = new TestVariant("browser"); + var node = new TestVariant("node"); + var main = new TestLayer("main"); + var production = new TestRole("production"); + + var view = view( + Set.of(main), + Set.of(production), + Set.of(browser, node), + Set.of(new VariantRoleLayer(browser, production, main))); + + var projections = RoleProjectionsView.of(view); + + var ex = assertThrows(IllegalArgumentException.class, () -> projections.getProjection(node, production)); + assertTrue(ex.getMessage().contains("Role projection for variant 'node' and role 'production' not found")); + } + + private static VariantsView view( + Set layers, + Set roles, + Set variants, + Set entries) { + try { + Constructor ctor = VariantsView.class.getDeclaredConstructor( + Set.class, + Set.class, + Set.class, + Set.class); + ctor.setAccessible(true); + return ctor.newInstance(layers, roles, variants, entries); + } catch (Exception e) { + throw new RuntimeException("Unable to create VariantsView fixture", e); + } + } + + private record TestVariant(String value) implements Variant { + @Override + public String getName() { + return value; + } + } + + private record TestLayer(String value) implements Layer { + @Override + public String getName() { + return value; + } + } + + private record TestRole(String value) implements Role { + @Override + public String getName() { + return value; + } + } +} diff --git a/variants/src/test/java/org/implab/gradle/variants/sources/internal/CompileUnitNamerTest.java b/variants/src/test/java/org/implab/gradle/variants/sources/internal/CompileUnitNamerTest.java new file mode 100644 --- /dev/null +++ b/variants/src/test/java/org/implab/gradle/variants/sources/internal/CompileUnitNamerTest.java @@ -0,0 +1,90 @@ +package org.implab.gradle.variants.sources.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.gradle.api.InvalidUserDataException; +import org.implab.gradle.variants.core.Layer; +import org.implab.gradle.variants.core.Variant; +import org.implab.gradle.variants.sources.CompileUnit; +import org.junit.jupiter.api.Test; + +class CompileUnitNamerTest { + @Test + void resolvesProjectedNameForUniqueCompileUnit() { + var unit = unit("browser", "main"); + + var namer = CompileUnitNamer.builder() + .addUnits(List.of(unit)) + .build(); + + assertEquals("browserMain", namer.resolveName(unit)); + } + + @Test + void failsOnProjectedNameCollisionInFailMode() { + var left = unit("foo", "variantBar"); + var right = unit("fooVariant", "bar"); + + var ex = assertThrows( + InvalidUserDataException.class, + () -> CompileUnitNamer.builder() + .addUnits(List.of(left, right)) + .nameCollisionPolicy(NameCollisionPolicy.FAIL) + .build()); + + assertTrue(ex.getMessage().contains("The same source set names are produced by different compile units")); + assertTrue(ex.getMessage().contains("fooVariantBar")); + } + + @Test + void resolvesProjectedNameCollisionDeterministicallyInCanonicalOrder() { + var earlier = unit("foo", "variantBar"); + var later = unit("fooVariant", "bar"); + + var namer = CompileUnitNamer.builder() + .addUnits(List.of(later, earlier)) + .nameCollisionPolicy(NameCollisionPolicy.RESOLVE) + .build(); + + assertEquals("fooVariantBar", namer.resolveName(earlier)); + assertEquals("fooVariantBar2", namer.resolveName(later)); + } + + @Test + void rejectsUnknownCompileUnitLookup() { + var known = unit("browser", "main"); + var unknown = unit("browser", "test"); + + var namer = CompileUnitNamer.builder() + .addUnits(List.of(known)) + .build(); + + var ex = assertThrows(IllegalArgumentException.class, () -> namer.resolveName(unknown)); + assertTrue(ex.getMessage().contains("Compile unit")); + assertTrue(ex.getMessage().contains("associated name")); + } + + private static CompileUnit unit(String variantName, String layerName) { + return new CompileUnit( + new TestVariant(variantName), + new TestLayer(layerName)); + } + + private record TestVariant(String value) implements Variant { + @Override + public String getName() { + return value; + } + } + + private record TestLayer(String value) implements Layer { + @Override + public String getName() { + return value; + } + } +} diff --git a/variants/src/test/java/org/implab/gradle/variants/sources/internal/DefaultCompileUnitNamingPolicyTest.java b/variants/src/test/java/org/implab/gradle/variants/sources/internal/DefaultCompileUnitNamingPolicyTest.java new file mode 100644 --- /dev/null +++ b/variants/src/test/java/org/implab/gradle/variants/sources/internal/DefaultCompileUnitNamingPolicyTest.java @@ -0,0 +1,42 @@ +package org.implab.gradle.variants.sources.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class DefaultCompileUnitNamingPolicyTest { + @Test + void usesFailPolicyByDefault() { + var policy = new DefaultCompileUnitNamingPolicy(); + + assertEquals(NameCollisionPolicy.FAIL, policy.policy()); + } + + @Test + void switchesToResolvePolicyWhenSelected() { + var policy = new DefaultCompileUnitNamingPolicy(); + + policy.resolveNameCollision(); + + assertEquals(NameCollisionPolicy.RESOLVE, policy.policy()); + } + + @Test + void rejectsChangingPolicyAfterItWasSelected() { + var policy = new DefaultCompileUnitNamingPolicy(); + + policy.resolveNameCollision(); + + assertThrows(IllegalStateException.class, policy::failOnNameCollision); + } + + @Test + void rejectsChangingPolicyAfterItWasFinalized() { + var policy = new DefaultCompileUnitNamingPolicy(); + + policy.finalizePolicy(); + + assertThrows(IllegalStateException.class, policy::resolveNameCollision); + } +} diff --git a/variants/src/test/java/org/implab/gradle/variants/sources/internal/DefaultLateConfigurationPolicySpecTest.java b/variants/src/test/java/org/implab/gradle/variants/sources/internal/DefaultLateConfigurationPolicySpecTest.java new file mode 100644 --- /dev/null +++ b/variants/src/test/java/org/implab/gradle/variants/sources/internal/DefaultLateConfigurationPolicySpecTest.java @@ -0,0 +1,51 @@ +package org.implab.gradle.variants.sources.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class DefaultLateConfigurationPolicySpecTest { + @Test + void usesFailModeByDefault() { + var policy = new DefaultLateConfigurationPolicySpec(); + + assertEquals(LateConfigurationMode.FAIL, policy.mode()); + } + + @Test + void switchesToWarnModeWhenSelected() { + var policy = new DefaultLateConfigurationPolicySpec(); + + policy.warnOnLateConfiguration(); + + assertEquals(LateConfigurationMode.WARN, policy.mode()); + } + + @Test + void switchesToApplyModeWhenSelected() { + var policy = new DefaultLateConfigurationPolicySpec(); + + policy.allowLateConfiguration(); + + assertEquals(LateConfigurationMode.APPLY, policy.mode()); + } + + @Test + void rejectsChangingPolicyAfterItWasSelected() { + var policy = new DefaultLateConfigurationPolicySpec(); + + policy.warnOnLateConfiguration(); + + assertThrows(IllegalStateException.class, policy::failOnLateConfiguration); + } + + @Test + void rejectsChangingPolicyAfterItWasFinalized() { + var policy = new DefaultLateConfigurationPolicySpec(); + + policy.finalizePolicy(); + + assertThrows(IllegalStateException.class, policy::allowLateConfiguration); + } +}