diff --git a/impl/maven-core/pom.xml b/impl/maven-core/pom.xml index 534838b51d7e..f01da88b941d 100644 --- a/impl/maven-core/pom.xml +++ b/impl/maven-core/pom.xml @@ -31,6 +31,11 @@ under the License. Maven 4 Core Maven Core classes. + + + FileLength + + diff --git a/impl/maven-core/src/test/java/org/apache/maven/project/PomConstructionTest.java b/impl/maven-core/src/test/java/org/apache/maven/project/PomConstructionTest.java index a6513e409183..d53247b53b9a 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/project/PomConstructionTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/project/PomConstructionTest.java @@ -1534,6 +1534,82 @@ void testProfilePluginMngDependencies() throws Exception { assertEquals("a", pom.getValue("build/plugins[1]/dependencies[1]/artifactId")); } + /* MNG-3309 */ + @Test + void testCascadingProfileActivation() throws Exception { + Properties props = new Properties(); + props.put("trigger", "start"); + + PomTestWrapper pom = buildPom("cascading-profile-activation", props, null); + + // Verify that cascading profile activation works + // profile1 should be activated by trigger=start + // profile2 should be activated by profile1's cascade.level1=activate property + // profile3 should be activated by profile2's cascade.level2=activate property + + List activeProfiles = + pom.getMavenProject().getActiveProfiles(); + + // Should have 3 active profiles (profile1, profile2, profile3) + assertEquals(3, activeProfiles.size()); + + // Verify specific profiles are active + assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId()))); + assertTrue(activeProfiles.stream().anyMatch(p -> "profile2".equals(p.getId()))); + assertTrue(activeProfiles.stream().anyMatch(p -> "profile3".equals(p.getId()))); + + // Verify profile4 is NOT active (no trigger) + assertTrue(activeProfiles.stream().noneMatch(p -> "profile4".equals(p.getId()))); + + // Verify properties are set correctly (last profile wins) + assertEquals("profile3", pom.getValue("properties/test.property")); + assertEquals("true", pom.getValue("properties/profile1.activated")); + assertEquals("true", pom.getValue("properties/profile2.activated")); + assertEquals("true", pom.getValue("properties/profile3.activated")); + } + + /* MNG-3309 - Test circular dependency handling */ + @Test + void testCascadingProfileActivationCircular() throws Exception { + Properties props = new Properties(); + props.put("circular", "test"); + + PomTestWrapper pom = buildPom("cascading-profile-activation", props, null); + + // Verify that circular dependencies are handled gracefully + // profile5 sets trigger=start which would activate profile1 + // But this should not cause infinite loops + + List activeProfiles = + pom.getMavenProject().getActiveProfiles(); + + // Should have profile5 and profile1 active (and cascaded profiles) + assertTrue(activeProfiles.stream().anyMatch(p -> "profile5".equals(p.getId()))); + assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId()))); + + // Verify no infinite loop occurred (test should complete) + assertTrue(activeProfiles.size() >= 2); + } + + /* MNG-3309 - Test no cascading baseline */ + @Test + void testNoCascadingProfileActivation() throws Exception { + // Test with no trigger properties - no profiles should be activated + PomTestWrapper pom = buildPom("cascading-profile-activation"); + + List activeProfiles = + pom.getMavenProject().getActiveProfiles(); + + // No profiles should be active + assertEquals(0, activeProfiles.size()); + + // Properties should remain at default values + assertEquals("default", pom.getValue("properties/test.property")); + assertEquals("false", pom.getValue("properties/profile1.activated")); + assertEquals("false", pom.getValue("properties/profile2.activated")); + assertEquals("false", pom.getValue("properties/profile3.activated")); + } + /** MNG-4116 */ @Test void testPercentEncodedUrlsMustNotBeDecoded() throws Exception { diff --git a/impl/maven-core/src/test/resources-project-builder/cascading-profile-activation/pom.xml b/impl/maven-core/src/test/resources-project-builder/cascading-profile-activation/pom.xml new file mode 100644 index 000000000000..a94ef85cb8df --- /dev/null +++ b/impl/maven-core/src/test/resources-project-builder/cascading-profile-activation/pom.xml @@ -0,0 +1,145 @@ + + + + + 4.0.0 + + org.apache.maven.its.mng3309 + cascading-profile-activation + 1.0-SNAPSHOT + jar + + Maven Integration Test :: MNG-3309 + + Test cascading profile activation where one profile's properties trigger the activation of other profiles. + + + + + default + false + false + false + + + + + + profile1 + + + trigger + start + + + + true + + activate + profile1 + + + + + + profile2 + + + cascade.level1 + activate + + + + true + + activate + profile2 + + + + + + profile3 + + + cascade.level2 + activate + + + + true + profile3 + + + + + + profile4 + + + never.set + never + + + + true + profile4 + + + + + + profile5 + + + circular + test + + + + true + + start + profile5 + + + + + + + + + org.apache.maven.plugins + maven-help-plugin + 3.4.0 + + + show-profiles + validate + + active-profiles + + + + + + + diff --git a/impl/maven-impl/src/main/java/org/apache/maven/api/services/model/ProfileActivationContext.java b/impl/maven-impl/src/main/java/org/apache/maven/api/services/model/ProfileActivationContext.java index b7951ea3e122..43fec5176afc 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/api/services/model/ProfileActivationContext.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/api/services/model/ProfileActivationContext.java @@ -18,9 +18,12 @@ */ package org.apache.maven.api.services.model; +import java.util.Collection; + import org.apache.maven.api.annotations.Nonnull; import org.apache.maven.api.annotations.Nullable; import org.apache.maven.api.model.Model; +import org.apache.maven.api.model.Profile; import org.apache.maven.api.services.InterpolatorException; import org.apache.maven.api.services.ModelBuilderException; @@ -130,4 +133,12 @@ public interface ProfileActivationContext { * @throws InterpolatorException if an error occurs during interpolation */ boolean exists(@Nullable String path, boolean glob); + + /** + * Inject properties from newly activated profiles in order to trigger the cascading mechanism. + * This method allows profiles to contribute properties that can trigger the activation of other profiles. + * + * @param activatedProfiles The collection of profiles that have been activated that may trigger the cascading effect. + */ + void addProfileProperties(Collection activatedProfiles); } diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/SettingsUtilsV4.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/SettingsUtilsV4.java index 0290464ebb77..fabcc214a221 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/SettingsUtilsV4.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/SettingsUtilsV4.java @@ -268,7 +268,7 @@ public static org.apache.maven.api.model.Profile convertFromSettingsProfile(Prof } org.apache.maven.api.model.Profile value = profile.build(); - value.setSource("settings.xml"); + value.setSource(org.apache.maven.api.model.Profile.SOURCE_SETTINGS); return value; } diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultProfileActivationContext.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultProfileActivationContext.java index 9d9ba94546c8..8cb36789364c 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultProfileActivationContext.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultProfileActivationContext.java @@ -27,6 +27,7 @@ import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -35,6 +36,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.apache.maven.api.model.Model; +import org.apache.maven.api.model.Profile; import org.apache.maven.api.services.Interpolator; import org.apache.maven.api.services.InterpolatorException; import org.apache.maven.api.services.ModelBuilderException; @@ -169,6 +171,7 @@ private boolean matchesExists(Map exists, DefaultProfileA private Map systemProperties = Collections.emptyMap(); private Map userProperties = Collections.emptyMap(); private Model model; + private Map cascadingProperties = Collections.emptyMap(); final Record record; public DefaultProfileActivationContext( @@ -326,10 +329,21 @@ public String getModelPackaging() { @Override public String getModelProperty(String key) { if (record != null) { - return record.usedModelProperties.computeIfAbsent( - key, k -> model.getProperties().get(k)); + return record.usedModelProperties.computeIfAbsent(key, k -> { + // Check cascading properties first, then model properties + String value = cascadingProperties.get(k); + if (value == null && model.getProperties() != null) { + value = model.getProperties().get(k); + } + return value; + }); } else { - return model.getProperties().get(key); + // Check cascading properties first, then model properties + String value = cascadingProperties.get(key); + if (value == null && model.getProperties() != null) { + value = model.getProperties().get(key); + } + return value; } } @@ -461,4 +475,25 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { private static Map unmodifiable(Map map) { return map != null ? Collections.unmodifiableMap(map) : Collections.emptyMap(); } + + // Cascading profile activation methods + + @Override + public void addProfileProperties(Collection activatedProfiles) { + // Inject properties from activated profiles into cascading properties + // This enables cascading profile activation without modifying the underlying model + if (activatedProfiles != null && !activatedProfiles.isEmpty()) { + Map newCascadingProperties = new HashMap<>(cascadingProperties); + + // Add properties from each activated profile + for (Profile profile : activatedProfiles) { + if (profile.getProperties() != null) { + newCascadingProperties.putAll(profile.getProperties()); + } + } + + // Update cascading properties for future profile activation checks + this.cascadingProperties = Collections.unmodifiableMap(newCascadingProperties); + } + } } diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultProfileSelector.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultProfileSelector.java index 407d35a71c23..19ae7a6492d6 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultProfileSelector.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultProfileSelector.java @@ -63,32 +63,49 @@ public DefaultProfileSelector addProfileActivator(ProfileActivator profileActiva @Override public List getActiveProfiles( Collection profiles, ProfileActivationContext context, ModelProblemCollector problems) { - List activeProfiles = new ArrayList<>(profiles.size()); + + List activeSettingsProfiles = new ArrayList<>(); + List activePomProfiles = new ArrayList<>(); List activePomProfilesByDefault = new ArrayList<>(); - boolean activatedPomProfileNotByDefault = false; - for (Profile profile : profiles) { - if (!context.isProfileInactive(profile.getId())) { - if (context.isProfileActive(profile.getId()) || isActive(profile, context, problems)) { - activeProfiles.add(profile); - if (Profile.SOURCE_POM.equals(profile.getSource())) { - activatedPomProfileNotByDefault = true; - } - } else if (isActiveByDefault(profile)) { - if (Profile.SOURCE_POM.equals(profile.getSource())) { - activePomProfilesByDefault.add(profile); - } else { - activeProfiles.add(profile); + // Cascading mode: iterate until no more profiles are activated + List remainingProfiles = new ArrayList<>(profiles); + List activatedProfiles; + do { + activatedProfiles = new ArrayList<>(); + for (Profile profile : List.copyOf(remainingProfiles)) { + if (!context.isProfileInactive(profile.getId())) { + boolean activated = context.isProfileActive(profile.getId()); + boolean active = isActive(profile, context, problems); + boolean activeByDefault = isActiveByDefault(profile); + if (activated || active || activeByDefault) { + if (Profile.SOURCE_POM.equals(profile.getSource())) { + if (activated || active) { + activePomProfiles.add(profile); + } else { + activePomProfilesByDefault.add(profile); + } + } else { + activeSettingsProfiles.add(profile); + } + remainingProfiles.remove(profile); + activatedProfiles.add(profile); } } } - } + // Add profile properties for cascading activation + context.addProfileProperties(activatedProfiles); + } while (!activatedProfiles.isEmpty()); - if (!activatedPomProfileNotByDefault) { - activeProfiles.addAll(activePomProfilesByDefault); + List allActivated = new ArrayList<>(); + if (activePomProfiles.isEmpty()) { + allActivated.addAll(activePomProfilesByDefault); + } else { + allActivated.addAll(activePomProfiles); } + allActivated.addAll(activeSettingsProfiles); - return activeProfiles; + return allActivated; } private boolean isActive(Profile profile, ProfileActivationContext context, ModelProblemCollector problems) { diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/PropertyProfileActivator.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/PropertyProfileActivator.java index 8b2114ad3633..95c0e1a9aa14 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/PropertyProfileActivator.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/PropertyProfileActivator.java @@ -73,6 +73,9 @@ public boolean isActive(Profile profile, ProfileActivationContext context, Model if (sysValue == null && "packaging".equals(name)) { sysValue = context.getModelPackaging(); } + if (sysValue == null) { + sysValue = context.getModelProperty(name); + } if (sysValue == null) { sysValue = context.getSystemProperty(name); } diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultProfileSelectorTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultProfileSelectorTest.java new file mode 100644 index 000000000000..96a0a0c43bfd --- /dev/null +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultProfileSelectorTest.java @@ -0,0 +1,428 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.impl.model; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.apache.maven.api.model.Activation; +import org.apache.maven.api.model.ActivationProperty; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.model.Profile; +import org.apache.maven.api.services.model.ProfileActivationContext; +import org.apache.maven.api.services.model.ProfileActivator; +import org.apache.maven.impl.model.profile.PropertyProfileActivator; +import org.apache.maven.impl.model.profile.SimpleProblemCollector; +import org.apache.maven.impl.model.rootlocator.DefaultRootLocator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests {@link DefaultProfileSelector} with focus on cascading activation behavior. + */ +public class DefaultProfileSelectorTest { + + private DefaultProfileSelector selector; + private SimpleProblemCollector problems; + + @BeforeEach + void setUp() { + selector = new DefaultProfileSelector(); + // Add a simple property-based activator for testing + selector.addProfileActivator(new PropertyProfileActivator()); + problems = new SimpleProblemCollector(); + } + + @Test + void testNonCascadingActivation() { + // Create profiles with property-based activation + Profile profile1 = createProfile("profile1", "prop1", "value1", Profile.SOURCE_POM); + Profile profile2 = createProfile("profile2", "prop2", "value2", Profile.SOURCE_POM); + + List profiles = Arrays.asList(profile1, profile2); + + // Create context with prop1 set + DefaultProfileActivationContext context = new DefaultProfileActivationContext( + new DefaultPathTranslator(), new DefaultRootLocator(), new DefaultInterpolator()); + context.setModel(Model.newInstance()); + context.setSystemProperties(Map.of("prop1", "value1")); + + // Test cascading mode (current implementation only supports cascading) + List activeProfiles = selector.getActiveProfiles(profiles, context, problems); + + assertEquals(1, activeProfiles.size()); + assertEquals("profile1", activeProfiles.get(0).getId()); + assertTrue(problems.getErrors().isEmpty()); + } + + @Test + void testCascadingActivation() { + // Create profiles where one activates another through properties + Profile profile1 = createProfileWithProperties( + "profile1", "prop1", "value1", Map.of("prop2", "value2"), Profile.SOURCE_POM); + Profile profile2 = createProfile("profile2", "prop2", "value2", Profile.SOURCE_POM); + + List profiles = Arrays.asList(profile1, profile2); + + // Create context with prop1 set (should activate profile1, which sets prop2, which activates profile2) + DefaultProfileActivationContext context = new DefaultProfileActivationContext( + new DefaultPathTranslator(), new DefaultRootLocator(), new DefaultInterpolator()); + context.setSystemProperties(Map.of("prop1", "value1")); + context.setModel(Model.newInstance()); // Set a model for property injection + + // Test cascading mode + List activeProfiles = selector.getActiveProfiles(profiles, context, problems); + + assertEquals(2, activeProfiles.size()); + assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId()))); + assertTrue(activeProfiles.stream().anyMatch(p -> "profile2".equals(p.getId()))); + assertTrue(problems.getErrors().isEmpty()); + } + + @Test + void testCascadingVsNonCascadingDifference() { + // Create profiles where cascading would activate more profiles + Profile profile1 = createProfileWithProperties( + "profile1", "prop1", "value1", Map.of("prop2", "value2"), Profile.SOURCE_POM); + Profile profile2 = createProfile("profile2", "prop2", "value2", Profile.SOURCE_POM); + + List profiles = Arrays.asList(profile1, profile2); + + DefaultProfileActivationContext context = new DefaultProfileActivationContext( + new DefaultPathTranslator(), new DefaultRootLocator(), new DefaultInterpolator()); + context.setSystemProperties(Map.of("prop1", "value1")); + context.setModel(Model.newInstance()); // Set a model for property injection + + // Cascading should activate both profile1 and profile2 + List cascading = selector.getActiveProfiles(profiles, context, problems); + assertEquals(2, cascading.size()); + } + + @Test + void testActiveByDefaultProfiles() { + Profile defaultProfile = createActiveByDefaultProfile("default-profile", Profile.SOURCE_POM); + Profile conditionalProfile = createProfile("conditional", "prop1", "value1", Profile.SOURCE_POM); + + List profiles = Arrays.asList(defaultProfile, conditionalProfile); + + DefaultProfileActivationContext context = new DefaultProfileActivationContext( + new DefaultPathTranslator(), new DefaultRootLocator(), new DefaultInterpolator()); + context.setModel(Model.newInstance()); + + // Should activate default profile when no conditions are met + List activeProfiles = selector.getActiveProfiles(profiles, context, problems); + assertEquals(1, activeProfiles.size()); + assertEquals("default-profile", activeProfiles.get(0).getId()); + + // Should not activate default profile when conditional profile is active + context.setSystemProperties(Map.of("prop1", "value1")); + activeProfiles = selector.getActiveProfiles(profiles, context, problems); + assertEquals(1, activeProfiles.size()); + assertEquals("conditional", activeProfiles.get(0).getId()); + } + + @Test + void testMixedSourceProfiles() { + Profile pomProfile = createProfile("pom-profile", "prop1", "value1", Profile.SOURCE_POM); + Profile settingsProfile = createProfile("settings-profile", "prop2", "value2", Profile.SOURCE_SETTINGS); + + List profiles = Arrays.asList(pomProfile, settingsProfile); + + DefaultProfileActivationContext context = new DefaultProfileActivationContext( + new DefaultPathTranslator(), new DefaultRootLocator(), new DefaultInterpolator()); + context.setModel(Model.newInstance()); + context.setSystemProperties(Map.of("prop1", "value1", "prop2", "value2")); + + List activeProfiles = selector.getActiveProfiles(profiles, context, problems); + assertEquals(2, activeProfiles.size()); + + // Settings profiles should come after POM profiles in the result + assertEquals("pom-profile", activeProfiles.get(0).getId()); + assertEquals("settings-profile", activeProfiles.get(1).getId()); + } + + @Test + void testEmptyProfilesList() { + List profiles = Collections.emptyList(); + DefaultProfileActivationContext context = new DefaultProfileActivationContext( + new DefaultPathTranslator(), new DefaultRootLocator(), new DefaultInterpolator()); + context.setModel(Model.newInstance()); + + List activeProfiles = selector.getActiveProfiles(profiles, context, problems); + assertTrue(activeProfiles.isEmpty()); + } + + @Test + void testExplicitlyActivatedProfiles() { + Profile profile1 = createProfile("profile1", "nonexistent", "value", Profile.SOURCE_POM); + Profile profile2 = createProfile("profile2", "prop2", "value2", Profile.SOURCE_POM); + + List profiles = Arrays.asList(profile1, profile2); + + DefaultProfileActivationContext context = new DefaultProfileActivationContext( + new DefaultPathTranslator(), + new DefaultRootLocator(), + new DefaultInterpolator(), + List.of("profile1"), + List.of(), + Map.of("prop2", "value2"), + Map.of(), + Model.newInstance()); + + List activeProfiles = selector.getActiveProfiles(profiles, context, problems); + assertEquals(2, activeProfiles.size()); + assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId()))); + assertTrue(activeProfiles.stream().anyMatch(p -> "profile2".equals(p.getId()))); + } + + @Test + void testCascadingActivationChain() { + // Create a chain of profiles: profile1 -> profile2 -> profile3 + Profile profile1 = createProfileWithProperties( + "profile1", "prop1", "value1", Map.of("prop2", "value2"), Profile.SOURCE_POM); + Profile profile2 = createProfileWithProperties( + "profile2", "prop2", "value2", Map.of("prop3", "value3"), Profile.SOURCE_POM); + Profile profile3 = createProfile("profile3", "prop3", "value3", Profile.SOURCE_POM); + + List profiles = Arrays.asList(profile1, profile2, profile3); + + // Create context with prop1 set + DefaultProfileActivationContext context = new DefaultProfileActivationContext( + new DefaultPathTranslator(), new DefaultRootLocator(), new DefaultInterpolator()); + context.setSystemProperties(Map.of("prop1", "value1")); + context.setModel(Model.newInstance()); + + // Test cascading mode + List activeProfiles = selector.getActiveProfiles(profiles, context, problems); + + // All three profiles should be activated through cascading + assertEquals(3, activeProfiles.size()); + assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId()))); + assertTrue(activeProfiles.stream().anyMatch(p -> "profile2".equals(p.getId()))); + assertTrue(activeProfiles.stream().anyMatch(p -> "profile3".equals(p.getId()))); + assertTrue(problems.getErrors().isEmpty()); + } + + @Test + void testCascadingStopCondition() { + // Test that cascading stops when no more profiles can be activated + Profile profile1 = createProfileWithProperties( + "profile1", "prop1", "value1", Map.of("prop2", "value2"), Profile.SOURCE_POM); + Profile profile2 = createProfileWithProperties( + "profile2", "prop2", "value2", Map.of("prop3", "value3"), Profile.SOURCE_POM); + // profile3 requires prop4 which is never set, so cascading should stop + Profile profile3 = createProfile("profile3", "prop4", "value4", Profile.SOURCE_POM); + + List profiles = Arrays.asList(profile1, profile2, profile3); + + // Create context with prop1 set + DefaultProfileActivationContext context = new DefaultProfileActivationContext( + new DefaultPathTranslator(), new DefaultRootLocator(), new DefaultInterpolator()); + context.setSystemProperties(Map.of("prop1", "value1")); + context.setModel(Model.newInstance()); + + // Test cascading mode + List activeProfiles = selector.getActiveProfiles(profiles, context, problems); + + // Only profile1 and profile2 should be activated, profile3 should not + assertEquals(2, activeProfiles.size()); + assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId()))); + assertTrue(activeProfiles.stream().anyMatch(p -> "profile2".equals(p.getId()))); + assertTrue(activeProfiles.stream().noneMatch(p -> "profile3".equals(p.getId()))); + assertTrue(problems.getErrors().isEmpty()); + } + + @Test + void testCascadingWithCircularDependency() { + // Test that cascading handles circular dependencies gracefully + Profile profile1 = createProfileWithProperties( + "profile1", "prop1", "value1", Map.of("prop2", "value2"), Profile.SOURCE_POM); + Profile profile2 = createProfileWithProperties( + "profile2", "prop2", "value2", Map.of("prop1", "value1"), Profile.SOURCE_POM); + + List profiles = Arrays.asList(profile1, profile2); + + // Create context with prop1 set + DefaultProfileActivationContext context = new DefaultProfileActivationContext( + new DefaultPathTranslator(), new DefaultRootLocator(), new DefaultInterpolator()); + context.setSystemProperties(Map.of("prop1", "value1")); + context.setModel(Model.newInstance()); + + // Test cascading mode + List activeProfiles = selector.getActiveProfiles(profiles, context, problems); + + // Both profiles should be activated, but cascading should stop after first iteration + assertEquals(2, activeProfiles.size()); + assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId()))); + assertTrue(activeProfiles.stream().anyMatch(p -> "profile2".equals(p.getId()))); + assertTrue(problems.getErrors().isEmpty()); + } + + @Test + void testCascadingWithInactiveProfile() { + // Create profiles where one would activate another, but the second is explicitly deactivated + Profile profile1 = createProfileWithProperties( + "profile1", "prop1", "value1", Map.of("prop2", "value2"), Profile.SOURCE_POM); + Profile profile2 = createProfile("profile2", "prop2", "value2", Profile.SOURCE_POM); + + List profiles = Arrays.asList(profile1, profile2); + + // Create context with prop1 set and profile2 explicitly deactivated + DefaultProfileActivationContext context = new DefaultProfileActivationContext( + new DefaultPathTranslator(), + new DefaultRootLocator(), + new DefaultInterpolator(), + List.of(), + List.of("profile2"), + Map.of("prop1", "value1"), + Map.of(), + Model.newInstance()); + + // Test cascading mode + List activeProfiles = selector.getActiveProfiles(profiles, context, problems); + + // Only profile1 should be activated, profile2 should be deactivated despite cascading + assertEquals(1, activeProfiles.size()); + assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId()))); + assertTrue(activeProfiles.stream().noneMatch(p -> "profile2".equals(p.getId()))); + assertTrue(problems.getErrors().isEmpty()); + } + + @Test + void testCascadingWithRecordImmutability() { + // Test that profile records remain immutable during cascading + Profile originalProfile1 = createProfileWithProperties( + "profile1", "prop1", "value1", Map.of("prop2", "value2"), Profile.SOURCE_POM); + Profile originalProfile2 = createProfile("profile2", "prop2", "value2", Profile.SOURCE_POM); + + List profiles = Arrays.asList(originalProfile1, originalProfile2); + + // Create context with prop1 set + DefaultProfileActivationContext context = new DefaultProfileActivationContext( + new DefaultPathTranslator(), new DefaultRootLocator(), new DefaultInterpolator()); + context.setSystemProperties(Map.of("prop1", "value1")); + context.setModel(Model.newInstance()); + + // Test cascading mode + List activeProfiles = selector.getActiveProfiles(profiles, context, problems); + + // Verify that original profiles are unchanged (immutable records) + assertEquals("profile1", originalProfile1.getId()); + assertEquals("profile2", originalProfile2.getId()); + assertEquals(Map.of("prop2", "value2"), originalProfile1.getProperties()); + assertEquals(Map.of(), originalProfile2.getProperties()); + + // Verify activation worked + assertEquals(2, activeProfiles.size()); + assertTrue(problems.getErrors().isEmpty()); + } + + // Helper methods for creating test profiles + + private Profile createProfile(String id, String propName, String propValue, String source) { + Profile profile = Profile.newBuilder() + .id(id) + .activation(Activation.newBuilder() + .property(ActivationProperty.newBuilder() + .name(propName) + .value(propValue) + .build()) + .build()) + .build(); + profile.setSource(source); + return profile; + } + + private Profile createProfileWithProperties( + String id, String propName, String propValue, Map profileProperties, String source) { + Profile profile = Profile.newBuilder() + .id(id) + .activation(Activation.newBuilder() + .property(ActivationProperty.newBuilder() + .name(propName) + .value(propValue) + .build()) + .build()) + .properties(profileProperties) + .build(); + profile.setSource(source); + return profile; + } + + private Profile createActiveByDefaultProfile(String id, String source) { + Profile profile = Profile.newBuilder() + .id(id) + .activation(Activation.newBuilder().activeByDefault(true).build()) + .build(); + profile.setSource(source); + return profile; + } + + /** + * Simple property-based profile activator for testing. + */ + private static class PropertyProfileActivator implements ProfileActivator { + @Override + public boolean isActive( + Profile profile, + ProfileActivationContext context, + org.apache.maven.api.services.ModelProblemCollector problems) { + Activation activation = profile.getActivation(); + if (activation == null || activation.getProperty() == null) { + return false; + } + + ActivationProperty property = activation.getProperty(); + String name = property.getName(); + String expectedValue = property.getValue(); + + if (name == null) { + return false; + } + + // Check user properties first, then model properties (for cascading), then system properties + String actualValue = context.getUserProperty(name); + if (actualValue == null) { + actualValue = context.getModelProperty(name); + } + if (actualValue == null) { + actualValue = context.getSystemProperty(name); + } + + if (expectedValue == null || expectedValue.isEmpty()) { + return actualValue != null; + } + + return expectedValue.equals(actualValue); + } + + @Override + public boolean presentInConfig( + Profile profile, + ProfileActivationContext context, + org.apache.maven.api.services.ModelProblemCollector problems) { + return profile.getActivation() != null && profile.getActivation().getProperty() != null; + } + } +}