diff --git a/common/src/main/java/org/implab/gradle/common/sources/BuildVariant.java b/common/src/main/java/org/implab/gradle/common/sources/BuildVariant.java --- a/common/src/main/java/org/implab/gradle/common/sources/BuildVariant.java +++ b/common/src/main/java/org/implab/gradle/common/sources/BuildVariant.java @@ -120,7 +120,10 @@ public abstract class BuildVariant imple public LayerLink link(String from, String to, String kind) { ensureMutable("add links"); - var link = new LayerLink(from, to, kind); + var link = new LayerLink( + requireLinkValue("from", from), + requireLinkValue("to", to), + requireLinkValue("kind", kind)); links.add(link); return link; } @@ -191,6 +194,13 @@ public abstract class BuildVariant imple throw new InvalidUserDataException("Variant '" + name + "' is finalized and cannot " + operation); } + private static String requireLinkValue(String field, String value) { + if (value == null || value.trim().isEmpty()) + throw new InvalidUserDataException("Link '" + field + "' must not be null or blank"); + + return value.trim(); + } + public final class RolesSpec { public BuildRole role(String name, Action configure) { return BuildVariant.this.role(name, configure); diff --git a/common/src/main/java/org/implab/gradle/common/sources/BuildVariantsExtension.java b/common/src/main/java/org/implab/gradle/common/sources/BuildVariantsExtension.java --- a/common/src/main/java/org/implab/gradle/common/sources/BuildVariantsExtension.java +++ b/common/src/main/java/org/implab/gradle/common/sources/BuildVariantsExtension.java @@ -158,8 +158,18 @@ public abstract class BuildVariantsExten var errors = new ArrayList(); var layersByName = new LinkedHashMap(); - for (var layer : layers) - layersByName.put(layer.getName(), layer); + for (var layer : layers) { + var layerName = normalize(layer.getName()); + if (layerName == null) { + errors.add("Layer name must not be blank"); + continue; + } + + var previous = layersByName.putIfAbsent(layerName, layer); + if (previous != null) { + errors.add("Layer '" + layerName + "' is declared more than once"); + } + } for (var variant : variants) validateVariant(variant, layersByName, errors); @@ -174,28 +184,69 @@ public abstract class BuildVariantsExten } private static void validateVariant(BuildVariant variant, Map layersByName, List errors) { + var variantName = normalize(variant.getName()); + if (variantName == null) { + errors.add("Variant name must not be blank"); + return; + } + + validateRoleAndArtifactNames(variant, errors); var variantLayers = validateRoleMappings(variant, layersByName, errors); validateLinks(variant, variantLayers, errors); } + private static void validateRoleAndArtifactNames(BuildVariant variant, List errors) { + var roleNames = new LinkedHashSet(); + for (var role : variant.getRoles()) { + var roleName = normalize(role.getName()); + if (roleName == null) { + errors.add("Variant '" + variant.getName() + "' contains blank role name"); + continue; + } + if (!roleNames.add(roleName)) { + errors.add("Variant '" + variant.getName() + "' contains duplicated role name '" + roleName + "'"); + } + } + + var slotNames = new LinkedHashSet(); + for (var slot : variant.getArtifactSlots()) { + var slotName = normalize(slot.getName()); + if (slotName == null) { + errors.add("Variant '" + variant.getName() + "' contains blank artifact slot name"); + continue; + } + if (!slotNames.add(slotName)) { + errors.add("Variant '" + variant.getName() + "' contains duplicated artifact slot name '" + slotName + "'"); + } + } + } + private static Set validateRoleMappings(BuildVariant variant, Map layersByName, List errors) { var variantLayers = new LinkedHashSet(); for (var role : variant.getRoles()) { + var seenLayers = new LinkedHashSet(); for (var layerName : role.getLayers().getOrElse(List.of())) { - if (isBlank(layerName)) { + var normalizedLayerName = normalize(layerName); + if (normalizedLayerName == null) { errors.add("Variant '" + variant.getName() + "', role '" + role.getName() + "' contains blank layer name"); continue; } - var layer = layersByName.get(layerName); + var layer = layersByName.get(normalizedLayerName); if (layer == null) { - errors.add("Variant '" + variant.getName() + "' references unknown layer '" + layerName + "'"); + errors.add("Variant '" + variant.getName() + "' references unknown layer '" + normalizedLayerName + "'"); continue; } - variantLayers.add(layerName); + if (!seenLayers.add(normalizedLayerName)) { + errors.add("Variant '" + variant.getName() + "', role '" + role.getName() + + "' contains duplicated layer reference '" + normalizedLayerName + "'"); + continue; + } + + variantLayers.add(normalizedLayerName); } } diff --git a/common/src/main/java/org/implab/gradle/common/sources/VariantSourcesExtension.java b/common/src/main/java/org/implab/gradle/common/sources/VariantSourcesExtension.java --- a/common/src/main/java/org/implab/gradle/common/sources/VariantSourcesExtension.java +++ b/common/src/main/java/org/implab/gradle/common/sources/VariantSourcesExtension.java @@ -40,6 +40,7 @@ public abstract class VariantSourcesExte private final List boundContexts = new ArrayList<>(); private final LinkedHashMap> sourceSetsByName = new LinkedHashMap<>(); private final LinkedHashMap sourceSetLayersByName = new LinkedHashMap<>(); + private boolean sourceSetsRegistered; @Inject public VariantSourcesExtension(ObjectFactory objects) { @@ -137,6 +138,10 @@ public abstract class VariantSourcesExte } void registerSourceSets(BuildVariantsExtension variants, NamedDomainObjectContainer sources) { + if (sourceSetsRegistered) { + throw new InvalidUserDataException("variantSources source sets are already registered"); + } + validateBindings(variants); var usages = layerUsages(variants).toList(); @@ -157,6 +162,8 @@ public abstract class VariantSourcesExte registeredContexts.size() - registeredBefore, boundContexts.size() - boundBefore, sourceSetsByName.size()); + + sourceSetsRegistered = true; } private Stream layerUsages(BuildVariantsExtension variants) { diff --git a/common/src/test/java/org/implab/gradle/common/sources/VariantsPluginFunctionalTest.java b/common/src/test/java/org/implab/gradle/common/sources/VariantsPluginFunctionalTest.java --- a/common/src/test/java/org/implab/gradle/common/sources/VariantsPluginFunctionalTest.java +++ b/common/src/test/java/org/implab/gradle/common/sources/VariantsPluginFunctionalTest.java @@ -156,7 +156,7 @@ class VariantsPluginFunctionalTest { link('a', 'b', null) } } - """, "has incomplete link (from/to/kind are required)"); + """, "Link 'kind' must not be null or blank"); } @Test @@ -224,6 +224,25 @@ class VariantsPluginFunctionalTest { } @Test + void failsOnDuplicatedLayerReferenceInRole() throws Exception { + assertBuildFails(""" + plugins { + id 'org.implab.gradle-variants' + } + + variants { + layer('a') + + variant('browser') { + role('main') { + layers('a', 'a') + } + } + } + """, "contains duplicated layer reference 'a'"); + } + + @Test void failsOnLateLayerMutationAfterFinalize() throws Exception { assertBuildFails(""" plugins {