From c3cf29438e3d65d6ee5c5726f8611af99d9a649a Mon Sep 17 00:00:00 2001 From: Maarten Mulders Date: Fri, 12 Feb 2021 15:14:02 +0100 Subject: [PATCH] [MNG-6511] Optional project selection Closes #446. --- .../maven/execution/ActivationSettings.java | 58 +++++ .../DefaultBuildResumptionDataRepository.java | 2 +- .../DefaultMavenExecutionRequest.java | 37 +--- .../execution/MavenExecutionRequest.java | 18 ++ .../maven/execution/ProfileActivation.java | 31 --- .../maven/execution/ProjectActivation.java | 202 ++++++++++++++++++ .../maven/graph/DefaultGraphBuilder.java | 107 +++++++--- ...aultBuildResumptionDataRepositoryTest.java | 6 +- .../maven/graph/DefaultGraphBuilderTest.java | 201 +++++++++++++---- .../java/org/apache/maven/cli/MavenCli.java | 90 +++----- .../org/apache/maven/cli/MavenCliTest.java | 38 ++-- 11 files changed, 575 insertions(+), 215 deletions(-) create mode 100644 maven-core/src/main/java/org/apache/maven/execution/ActivationSettings.java create mode 100644 maven-core/src/main/java/org/apache/maven/execution/ProjectActivation.java diff --git a/maven-core/src/main/java/org/apache/maven/execution/ActivationSettings.java b/maven-core/src/main/java/org/apache/maven/execution/ActivationSettings.java new file mode 100644 index 0000000000..4e8f8a2d1e --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/execution/ActivationSettings.java @@ -0,0 +1,58 @@ +package org.apache.maven.execution; + +/* + * 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. + */ + +/** + * Describes whether a target should be activated or not, and if that is required or optional. + */ +enum ActivationSettings +{ + ACTIVATION_OPTIONAL( true, true ), + ACTIVATION_REQUIRED( true, false ), + DEACTIVATION_OPTIONAL( false, true ), + DEACTIVATION_REQUIRED( false, false ); + + /** + * Should the target be active? + */ + final boolean active; + /** + * Should the build continue if the target is not present? + */ + final boolean optional; + + ActivationSettings( final boolean active, final boolean optional ) + { + this.active = active; + this.optional = optional; + } + + static ActivationSettings of( final boolean active, final boolean optional ) + { + if ( optional ) + { + return active ? ACTIVATION_OPTIONAL : DEACTIVATION_OPTIONAL; + } + else + { + return active ? ACTIVATION_REQUIRED : DEACTIVATION_REQUIRED; + } + } +} diff --git a/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionDataRepository.java b/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionDataRepository.java index 02704c5902..73a0c1aeb0 100644 --- a/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionDataRepository.java +++ b/maven-core/src/main/java/org/apache/maven/execution/DefaultBuildResumptionDataRepository.java @@ -132,7 +132,7 @@ public class DefaultBuildResumptionDataRepository implements BuildResumptionData String propertyValue = properties.getProperty( REMAINING_PROJECTS ); Stream.of( propertyValue.split( PROPERTY_DELIMITER ) ) .filter( StringUtils::isNotEmpty ) - .forEach( request.getSelectedProjects()::add ); + .forEach( request.getProjectActivation()::activateOptionalProject ); LOGGER.info( "Resuming from {} due to the --resume / -r feature.", propertyValue ); } } diff --git a/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequest.java b/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequest.java index 554928b2a7..066d27f155 100644 --- a/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequest.java +++ b/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequest.java @@ -76,6 +76,7 @@ public class DefaultMavenExecutionRequest private List profiles; + private final ProjectActivation projectActivation = new ProjectActivation(); private final ProfileActivation profileActivation = new ProfileActivation(); private List pluginGroups; @@ -114,10 +115,6 @@ public class DefaultMavenExecutionRequest private String reactorFailureBehavior = REACTOR_FAIL_FAST; - private List selectedProjects; - - private List excludedProjects; - private boolean resume = false; private String resumeFrom; @@ -281,23 +278,13 @@ public class DefaultMavenExecutionRequest @Override public List getSelectedProjects() { - if ( selectedProjects == null ) - { - selectedProjects = new ArrayList<>(); - } - - return selectedProjects; + return this.projectActivation.getSelectedProjects(); } @Override public List getExcludedProjects() { - if ( excludedProjects == null ) - { - excludedProjects = new ArrayList<>(); - } - - return excludedProjects; + return this.projectActivation.getExcludedProjects(); } @Override @@ -358,6 +345,12 @@ public class DefaultMavenExecutionRequest return this; } + @Override + public ProjectActivation getProjectActivation() + { + return this.projectActivation; + } + @Override public ProfileActivation getProfileActivation() { @@ -569,11 +562,7 @@ public class DefaultMavenExecutionRequest { if ( selectedProjects != null ) { - this.selectedProjects = new ArrayList<>( selectedProjects ); - } - else - { - this.selectedProjects = null; + this.projectActivation.overwriteActiveProjects( selectedProjects ); } return this; @@ -584,11 +573,7 @@ public class DefaultMavenExecutionRequest { if ( excludedProjects != null ) { - this.excludedProjects = new ArrayList<>( excludedProjects ); - } - else - { - this.excludedProjects = null; + this.projectActivation.overwriteInactiveProjects( excludedProjects ); } return this; diff --git a/maven-core/src/main/java/org/apache/maven/execution/MavenExecutionRequest.java b/maven-core/src/main/java/org/apache/maven/execution/MavenExecutionRequest.java index d0ac0f1bd0..3989c5fa92 100644 --- a/maven-core/src/main/java/org/apache/maven/execution/MavenExecutionRequest.java +++ b/maven-core/src/main/java/org/apache/maven/execution/MavenExecutionRequest.java @@ -154,21 +154,33 @@ public interface MavenExecutionRequest String getReactorFailureBehavior(); + /** + * @deprecated Since Maven 4: use {@link #getProjectActivation()}. + */ + @Deprecated MavenExecutionRequest setSelectedProjects( List projects ); + /** + * @deprecated Since Maven 4: use {@link #getProjectActivation()}. + */ + @Deprecated List getSelectedProjects(); /** * @param projects the projects to exclude * @return this MavenExecutionRequest * @since 3.2 + * @deprecated Since Maven 4: use {@link #getProjectActivation()}. */ + @Deprecated MavenExecutionRequest setExcludedProjects( List projects ); /** * @return the excluded projects, never {@code null} * @since 3.2 + * @deprecated Since Maven 4: use {@link #getProjectActivation()}. */ + @Deprecated List getExcludedProjects(); /** @@ -327,6 +339,12 @@ public interface MavenExecutionRequest @Deprecated List getInactiveProfiles(); + /** + * Return the requested activation(s) of project(s) in this execution. + * @return requested (de-)activation(s) of project(s) in this execution. Never {@code null}. + */ + ProjectActivation getProjectActivation(); + /** * Return the requested activation(s) of profile(s) in this execution. * @return requested (de-)activation(s) of profile(s) in this execution. Never {@code null}. diff --git a/maven-core/src/main/java/org/apache/maven/execution/ProfileActivation.java b/maven-core/src/main/java/org/apache/maven/execution/ProfileActivation.java index 1837696a4d..52f5e06a91 100644 --- a/maven-core/src/main/java/org/apache/maven/execution/ProfileActivation.java +++ b/maven-core/src/main/java/org/apache/maven/execution/ProfileActivation.java @@ -35,37 +35,6 @@ import static java.util.stream.Collectors.toSet; */ public class ProfileActivation { - private enum ActivationSettings - { - ACTIVATION_OPTIONAL( true, true ), - ACTIVATION_REQUIRED( true, false ), - DEACTIVATION_OPTIONAL( false, true ), - DEACTIVATION_REQUIRED( false, false ); - - /** Should the profile be active? */ - final boolean active; - /** Should the build continue if the profile is not present? */ - final boolean optional; - - ActivationSettings( final boolean active, final boolean optional ) - { - this.active = active; - this.optional = optional; - } - - static ActivationSettings of( final boolean active, final boolean optional ) - { - if ( optional ) - { - return active ? ACTIVATION_OPTIONAL : DEACTIVATION_OPTIONAL; - } - else - { - return active ? ACTIVATION_REQUIRED : DEACTIVATION_REQUIRED; - } - } - } - private final Map activations = new HashMap<>(); /** diff --git a/maven-core/src/main/java/org/apache/maven/execution/ProjectActivation.java b/maven-core/src/main/java/org/apache/maven/execution/ProjectActivation.java new file mode 100644 index 0000000000..579ab18194 --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/execution/ProjectActivation.java @@ -0,0 +1,202 @@ +package org.apache.maven.execution; + +/* + * 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. + */ + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toSet; + +/** + * Container for storing the request from the user to activate or deactivate certain projects and optionally fail the + * build if those projects do not exist. + */ +public class ProjectActivation +{ + private static class ProjectActivationSettings + { + /** + * The selector of a project. This can be the project directory, [groupId]:[artifactId] or :[artifactId]. + */ + final String selector; + + /** + * This describes how/when to active or deactivate the project. + */ + final ActivationSettings activationSettings; + + ProjectActivationSettings( String selector, ActivationSettings activationSettings ) + { + this.selector = selector; + this.activationSettings = activationSettings; + } + } + + /** + * List of activated and deactivated projects. + */ + private final List activations = new ArrayList<>(); + + /** + * Adds a project activation to the request. + * @param selector The selector of the project. + * @param active Should the project be activated? + * @param optional Can the build continue if the project does not exist? + */ + public void addProjectActivation( String selector, boolean active, boolean optional ) + { + final ActivationSettings settings = ActivationSettings.of( active, optional ); + this.activations.add( new ProjectActivationSettings( selector, settings ) ); + } + + private Stream getProjects( final Predicate predicate ) + { + return this.activations.stream() + .filter( activation -> predicate.test( activation.activationSettings ) ); + } + + private Set getProjectSelectors( final Predicate predicate ) + { + return getProjects( predicate ) + .map( activation -> activation.selector ) + .collect( toSet() ); + } + + /** + * @return Required active project selectors, never {@code null}. + */ + public Set getRequiredActiveProjectSelectors() + { + return getProjectSelectors( pa -> !pa.optional && pa.active ); + } + + /** + * @return Optional active project selectors, never {@code null}. + */ + public Set getOptionalActiveProjectSelectors() + { + return getProjectSelectors( pa -> pa.optional && pa.active ); + } + + /** + * @return Required inactive project selectors, never {@code null}. + */ + public Set getRequiredInactiveProjectSelectors() + { + return getProjectSelectors( pa -> !pa.optional && !pa.active ); + } + + /** + * @return Optional inactive project selectors, never {@code null}. + */ + public Set getOptionalInactiveProjectSelectors() + { + return getProjectSelectors( pa -> pa.optional && !pa.active ); + } + + /** + * Mimics the pre-Maven 4 "selected projects" list. + * @deprecated Use {@link #getRequiredActiveProjectSelectors()} and {@link #getOptionalActiveProjectSelectors()} + * instead. + */ + @Deprecated + public List getSelectedProjects() + { + return Collections.unmodifiableList( new ArrayList<>( getProjectSelectors( pa -> pa.active ) ) ); + } + + /** + * Mimics the pre-Maven 4 "excluded projects" list. + * @deprecated Use {@link #getRequiredInactiveProjectSelectors()} and {@link #getOptionalInactiveProjectSelectors()} + * instead. + */ + @Deprecated + public List getExcludedProjects() + { + return Collections.unmodifiableList( new ArrayList<>( getProjectSelectors( pa -> !pa.active ) ) ); + } + + /** + * Overwrites the active projects based on a pre-Maven 4 "active projects" list. + * @param activeProjectSelectors A {@link List} of project selectors that must be activated. + * @deprecated Use {@link #activateOptionalProject(String)} or {@link #activateRequiredProject(String)} instead. + */ + @Deprecated + public void overwriteActiveProjects( List activeProjectSelectors ) + { + List projects = getProjects( pa -> pa.active ).collect( Collectors.toList() ); + this.activations.removeAll( projects ); + activeProjectSelectors.forEach( this::activateOptionalProject ); + } + + /** + * Overwrites the inactive projects based on a pre-Maven 4 "inactive projects" list. + * @param inactiveProjectSelectors A {@link List} of project selectors that must be deactivated. + * @deprecated Use {@link #deactivateOptionalProject(String)} or {@link #deactivateRequiredProject(String)} instead. + */ + @Deprecated + public void overwriteInactiveProjects( List inactiveProjectSelectors ) + { + List projects = getProjects( pa -> !pa.active ).collect( Collectors.toList() ); + this.activations.removeAll( projects ); + inactiveProjectSelectors.forEach( this::deactivateOptionalProject ); + } + + /** + * Mark a project as required and activated. + * @param selector The selector of the project. + */ + public void activateRequiredProject( String selector ) + { + this.activations.add( new ProjectActivationSettings( selector, ActivationSettings.ACTIVATION_REQUIRED ) ); + } + + /** + * Mark a project as optional and activated. + * @param selector The selector of the project. + */ + public void activateOptionalProject( String selector ) + { + this.activations.add( new ProjectActivationSettings( selector, ActivationSettings.ACTIVATION_OPTIONAL ) ); + } + + /** + * Mark a project as required and deactivated. + * @param selector The selector of the project. + */ + public void deactivateRequiredProject( String selector ) + { + this.activations.add( new ProjectActivationSettings( selector, ActivationSettings.DEACTIVATION_REQUIRED ) ); + } + + /** + * Mark a project as optional and deactivated. + * @param selector The selector of the project. + */ + public void deactivateOptionalProject( String selector ) + { + this.activations.add( new ProjectActivationSettings( selector, ActivationSettings.DEACTIVATION_OPTIONAL ) ); + } +} diff --git a/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java b/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java index 49d52af421..e747868394 100644 --- a/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java +++ b/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java @@ -21,7 +21,6 @@ package org.apache.maven.graph; import java.io.File; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -29,6 +28,7 @@ import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import javax.inject.Inject; @@ -41,6 +41,7 @@ import org.apache.maven.artifact.ArtifactUtils; import org.apache.maven.execution.BuildResumptionDataRepository; import org.apache.maven.execution.MavenExecutionRequest; import org.apache.maven.execution.MavenSession; +import org.apache.maven.execution.ProjectActivation; import org.apache.maven.execution.ProjectDependencyGraph; import org.apache.maven.model.Plugin; import org.apache.maven.model.building.DefaultModelProblem; @@ -176,40 +177,71 @@ public class DefaultGraphBuilder { List result = projects; - if ( !request.getSelectedProjects().isEmpty() ) + ProjectActivation projectActivation = request.getProjectActivation(); + Set requiredSelectors = projectActivation.getRequiredActiveProjectSelectors(); + Set optionalSelectors = projectActivation.getOptionalActiveProjectSelectors(); + if ( !requiredSelectors.isEmpty() || !optionalSelectors.isEmpty() ) { - File reactorDirectory = getReactorDirectory( request ); + Set selectedProjects = new HashSet<>( requiredSelectors.size() + optionalSelectors.size() ); + selectedProjects.addAll( getProjectsBySelectors( request, projects, requiredSelectors, true ) ); + selectedProjects.addAll( getProjectsBySelectors( request, projects, optionalSelectors, false ) ); - Collection selectedProjects = new LinkedHashSet<>(); - - for ( String selector : request.getSelectedProjects() ) + // it can be empty when an optional project is missing from the reactor, fallback to returning all projects + if ( !selectedProjects.isEmpty() ) { - MavenProject selectedProject = projects.stream() - .filter( project -> isMatchingProject( project, selector, reactorDirectory ) ) - .findFirst() - .orElseThrow( () -> new MavenExecutionException( - "Could not find the selected project in the reactor: " + selector, request.getPom() ) ); - selectedProjects.add( selectedProject ); + result = new ArrayList<>( selectedProjects ); - List children = selectedProject.getCollectedProjects(); - if ( children != null ) - { - selectedProjects.addAll( children ); - } + result = includeAlsoMakeTransitively( result, request, graph ); + + // Order the new list in the original order + List sortedProjects = graph.getSortedProjects(); + result.sort( comparing( sortedProjects::indexOf ) ); } - - result = new ArrayList<>( selectedProjects ); - - result = includeAlsoMakeTransitively( result, request, graph ); - - // Order the new list in the original order - List sortedProjects = graph.getSortedProjects(); - result.sort( comparing( sortedProjects::indexOf ) ); } return result; } + private Set getProjectsBySelectors( MavenExecutionRequest request, List projects, + Set projectSelectors, boolean required ) + throws MavenExecutionException + { + Set selectedProjects = new LinkedHashSet<>(); + File reactorDirectory = getReactorDirectory( request ); + + for ( String selector : projectSelectors ) + { + Optional optSelectedProject = projects.stream() + .filter( project -> isMatchingProject( project, selector, reactorDirectory ) ) + .findFirst(); + if ( !optSelectedProject.isPresent() ) + { + String message = "Could not find the selected project in the reactor: " + selector; + if ( required ) + { + throw new MavenExecutionException( message, request.getPom() ); + } + else + { + LOGGER.info( message ); + break; + } + } + + MavenProject selectedProject = optSelectedProject.get(); + + selectedProjects.add( selectedProject ); + + List children = selectedProject.getCollectedProjects(); + if ( children != null ) + { + selectedProjects.addAll( children ); + } + } + + return selectedProjects; + } + private List trimResumedProjects( List projects, ProjectDependencyGraph graph, MavenExecutionRequest request ) throws MavenExecutionException @@ -242,20 +274,19 @@ public class DefaultGraphBuilder { List result = projects; - if ( !request.getExcludedProjects().isEmpty() ) + ProjectActivation projectActivation = request.getProjectActivation(); + Set requiredSelectors = projectActivation.getRequiredInactiveProjectSelectors(); + Set optionalSelectors = projectActivation.getOptionalInactiveProjectSelectors(); + if ( !requiredSelectors.isEmpty() || !optionalSelectors.isEmpty() ) { - File reactorDirectory = getReactorDirectory( request ); + Set excludedProjects = new HashSet<>( requiredSelectors.size() + optionalSelectors.size() ); + excludedProjects.addAll( getProjectsBySelectors( request, projects, requiredSelectors, true ) ); + excludedProjects.addAll( getProjectsBySelectors( request, projects, optionalSelectors, false ) ); result = new ArrayList<>( projects ); - for ( String selector : request.getExcludedProjects() ) + for ( MavenProject excludedProject : excludedProjects ) { - MavenProject excludedProject = projects.stream() - .filter( project -> isMatchingProject( project, selector, reactorDirectory ) ) - .findFirst() - .orElseThrow( () -> new MavenExecutionException( "Could not find the selected project in " - + "the reactor: " + selector, request.getPom() ) ); - boolean isExcludedProjectRemoved = result.remove( excludedProject ); if ( isExcludedProjectRemoved ) @@ -267,6 +298,14 @@ public class DefaultGraphBuilder } } } + + if ( result.isEmpty() ) + { + boolean isPlural = excludedProjects.size() > 1; + String message = String.format( "The project exclusion%s in --projects/-pl resulted in an " + + "empty reactor, please correct %s.", isPlural ? "s" : "", isPlural ? "them" : "it" ); + throw new MavenExecutionException( message, request.getPom() ); + } } return result; diff --git a/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionDataRepositoryTest.java b/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionDataRepositoryTest.java index 5667418cf4..7a03f85d10 100644 --- a/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionDataRepositoryTest.java +++ b/maven-core/src/test/java/org/apache/maven/execution/DefaultBuildResumptionDataRepositoryTest.java @@ -29,7 +29,7 @@ import java.util.Properties; import static java.util.Arrays.asList; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; @@ -74,7 +74,7 @@ public class DefaultBuildResumptionDataRepositoryTest repository.applyResumptionProperties( request, properties ); - assertThat( request.getSelectedProjects(), contains( ":module-a", ":module-b", ":module-c" ) ); + assertThat( request.getSelectedProjects(), containsInAnyOrder( ":module-a", ":module-b", ":module-c" ) ); } @Test @@ -100,6 +100,6 @@ public class DefaultBuildResumptionDataRepositoryTest repository.applyResumptionData( request, rootProject ); - assertThat( request.getSelectedProjects(), contains( "example:module-c" ) ); + assertThat( request.getSelectedProjects(), containsInAnyOrder( "example:module-c" ) ); } } diff --git a/maven-core/src/test/java/org/apache/maven/graph/DefaultGraphBuilderTest.java b/maven-core/src/test/java/org/apache/maven/graph/DefaultGraphBuilderTest.java index e82c735b3f..1a5565d0af 100644 --- a/maven-core/src/test/java/org/apache/maven/graph/DefaultGraphBuilderTest.java +++ b/maven-core/src/test/java/org/apache/maven/graph/DefaultGraphBuilderTest.java @@ -19,9 +19,11 @@ package org.apache.maven.graph; * under the License. */ +import org.apache.maven.MavenExecutionException; import org.apache.maven.execution.BuildResumptionDataRepository; import org.apache.maven.execution.MavenExecutionRequest; import org.apache.maven.execution.MavenSession; +import org.apache.maven.execution.ProjectActivation; import org.apache.maven.execution.ProjectDependencyGraph; import org.apache.maven.model.Dependency; import org.apache.maven.model.Parent; @@ -45,6 +47,7 @@ import org.junit.jupiter.params.provider.MethodSource; import java.io.File; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; @@ -55,9 +58,11 @@ import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toList; import static org.apache.maven.execution.MavenExecutionRequest.REACTOR_MAKE_DOWNSTREAM; import static org.apache.maven.execution.MavenExecutionRequest.REACTOR_MAKE_UPSTREAM; import static org.apache.maven.graph.DefaultGraphBuilderTest.ScenarioBuilder.scenario; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -65,7 +70,7 @@ import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class DefaultGraphBuilderTest +class DefaultGraphBuilderTest { /* The multi-module structure in this project is displayed as follows: @@ -78,6 +83,7 @@ public class DefaultGraphBuilderTest └─── module-c-1 module-c-2 (depends on module-b) */ + private static final String GROUP_ID = "unittest"; private static final String PARENT_MODULE = "module-parent"; private static final String INDEPENDENT_MODULE = "module-independent"; private static final String MODULE_A = "module-a"; @@ -109,23 +115,59 @@ public class DefaultGraphBuilderTest scenario( "Full reactor in order" ) .expectResult( PARENT_MODULE, MODULE_C, MODULE_C_1, MODULE_A, MODULE_B, MODULE_C_2, INDEPENDENT_MODULE ), scenario( "Selected project" ) - .selectedProjects( MODULE_B ) + .activeRequiredProjects( MODULE_B ) .expectResult( MODULE_B ), scenario( "Selected project (including child modules)" ) - .selectedProjects( MODULE_C ) + .activeRequiredProjects( MODULE_C ) .expectResult( MODULE_C, MODULE_C_1, MODULE_C_2 ), + scenario( "Selected optional project" ) + .activeOptionalProjects( MODULE_B ) + .expectResult( MODULE_B ), + scenario( "Selected missing optional project" ) + .activeOptionalProjects( "non-existing-module" ) + .expectResult( PARENT_MODULE, MODULE_C, MODULE_C_1, MODULE_A, MODULE_B, MODULE_C_2, INDEPENDENT_MODULE ), + scenario( "Selected missing optional and required project" ) + .activeOptionalProjects( "non-existing-module" ) + .activeRequiredProjects( MODULE_B ) + .expectResult( MODULE_B ), scenario( "Excluded project" ) - .excludedProjects( MODULE_B ) + .inactiveRequiredProjects( MODULE_B ) .expectResult( PARENT_MODULE, MODULE_C, MODULE_C_1, MODULE_A, MODULE_C_2, INDEPENDENT_MODULE ), + scenario( "Excluded optional project" ) + .inactiveOptionalProjects( MODULE_B ) + .expectResult( PARENT_MODULE, MODULE_C, MODULE_C_1, MODULE_A, MODULE_C_2, INDEPENDENT_MODULE ), + scenario( "Excluded missing optional project" ) + .inactiveOptionalProjects( "non-existing-module" ) + .expectResult( PARENT_MODULE, MODULE_C, MODULE_C_1, MODULE_A, MODULE_B, MODULE_C_2, INDEPENDENT_MODULE ), + scenario( "Excluded missing optional and required project" ) + .inactiveOptionalProjects( "non-existing-module" ) + .inactiveRequiredProjects( MODULE_B ) + .expectResult( PARENT_MODULE, MODULE_C, MODULE_C_1, MODULE_A, MODULE_C_2, INDEPENDENT_MODULE ), + scenario( "Selected and excluded same project" ) + .activeRequiredProjects( MODULE_A ) + .inactiveRequiredProjects( MODULE_A ) + .expectResult( MavenExecutionException.class, "empty reactor" ), + scenario( "Project selected with different selector resolves to same project" ) + .activeRequiredProjects( GROUP_ID + ":" + MODULE_A ) + .inactiveRequiredProjects( MODULE_A ) + .expectResult( MavenExecutionException.class, "empty reactor" ), + scenario( "Selected and excluded same project, but also selected another project" ) + .activeRequiredProjects( MODULE_A, MODULE_B ) + .inactiveRequiredProjects( MODULE_A ) + .expectResult( MODULE_B ), + scenario( "Selected missing project as required and as optional" ) + .activeRequiredProjects( "non-existing-module" ) + .activeOptionalProjects( "non-existing-module" ) + .expectResult( MavenExecutionException.class, "not find the selected project" ), scenario( "Resuming from project" ) .resumeFrom( MODULE_B ) .expectResult( MODULE_B, MODULE_C_2, INDEPENDENT_MODULE ), scenario( "Selected project with also make dependencies" ) - .selectedProjects( MODULE_C_2 ) + .activeRequiredProjects( MODULE_C_2 ) .makeBehavior( REACTOR_MAKE_UPSTREAM ) .expectResult( PARENT_MODULE, MODULE_C, MODULE_A, MODULE_B, MODULE_C_2 ), scenario( "Selected project with also make dependents" ) - .selectedProjects( MODULE_B ) + .activeRequiredProjects( MODULE_B ) .makeBehavior( REACTOR_MAKE_DOWNSTREAM ) .expectResult( MODULE_B, MODULE_C_2 ), scenario( "Resuming from project with also make dependencies" ) @@ -133,42 +175,42 @@ public class DefaultGraphBuilderTest .resumeFrom( MODULE_C_2 ) .expectResult( PARENT_MODULE, MODULE_C, MODULE_A, MODULE_B, MODULE_C_2, INDEPENDENT_MODULE ), scenario( "Selected project with resume from and also make dependency (MNG-4960 IT#1)" ) - .selectedProjects( MODULE_C_2 ) + .activeRequiredProjects( MODULE_C_2 ) .resumeFrom( MODULE_B ) .makeBehavior( REACTOR_MAKE_UPSTREAM ) .expectResult( PARENT_MODULE, MODULE_C, MODULE_A, MODULE_B, MODULE_C_2 ), scenario( "Selected project with resume from and also make dependent (MNG-4960 IT#2)" ) - .selectedProjects( MODULE_B ) + .activeRequiredProjects( MODULE_B ) .resumeFrom( MODULE_C_2 ) .makeBehavior( REACTOR_MAKE_DOWNSTREAM ) .expectResult( MODULE_C_2 ), scenario( "Excluding an also make dependency from selectedProject does take its transitive dependency" ) - .selectedProjects( MODULE_C_2 ) - .excludedProjects( MODULE_B ) + .activeRequiredProjects( MODULE_C_2 ) + .inactiveRequiredProjects( MODULE_B ) .makeBehavior( REACTOR_MAKE_UPSTREAM ) .expectResult( PARENT_MODULE, MODULE_C, MODULE_A, MODULE_C_2 ), scenario( "Excluding a project also excludes its children" ) - .excludedProjects( MODULE_C ) + .inactiveRequiredProjects( MODULE_C ) .expectResult( PARENT_MODULE, MODULE_A, MODULE_B, INDEPENDENT_MODULE ), scenario( "Excluding an also make dependency from resumeFrom does take its transitive dependency" ) .resumeFrom( MODULE_C_2 ) - .excludedProjects( MODULE_B ) + .inactiveRequiredProjects( MODULE_B ) .makeBehavior( REACTOR_MAKE_UPSTREAM ) .expectResult( PARENT_MODULE, MODULE_C, MODULE_A, MODULE_C_2, INDEPENDENT_MODULE ), scenario( "Resume from exclude project downstream" ) .resumeFrom( MODULE_A ) - .excludedProjects( MODULE_B ) + .inactiveRequiredProjects( MODULE_B ) .expectResult( MODULE_A, MODULE_C_2, INDEPENDENT_MODULE ), scenario( "Exclude the project we are resuming from (as proposed in MNG-6676)" ) .resumeFrom( MODULE_B ) - .excludedProjects( MODULE_B ) + .inactiveRequiredProjects( MODULE_B ) .expectResult( MODULE_C_2, INDEPENDENT_MODULE ), scenario( "Selected projects in wrong order are resumed correctly in order" ) - .selectedProjects( MODULE_C_2, MODULE_B, MODULE_A ) + .activeRequiredProjects( MODULE_C_2, MODULE_B, MODULE_A ) .resumeFrom( MODULE_B ) .expectResult( MODULE_B, MODULE_C_2 ), scenario( "Duplicate projects are filtered out" ) - .selectedProjects( MODULE_A, MODULE_A ) + .activeRequiredProjects( MODULE_A, MODULE_A ) .expectResult( MODULE_A ), scenario( "Select reactor by specific pom" ) .requestedPom( MODULE_C ) @@ -184,23 +226,49 @@ public class DefaultGraphBuilderTest ); } + interface ExpectedResult { + + } + static class SelectedProjectsResult implements ExpectedResult { + final List projectNames; + + public SelectedProjectsResult( List projectSelectors ) + { + this.projectNames = projectSelectors; + } + } + static class ExceptionThrown implements ExpectedResult { + final Class expected; + final String partOfMessage; + + public ExceptionThrown( final Class expected, final String partOfMessage ) + { + this.expected = expected; + this.partOfMessage = partOfMessage; + } + } + @ParameterizedTest @MethodSource("parameters") - public void testGetReactorProjects( + void testGetReactorProjects( String parameterDescription, - List parameterSelectedProjects, - List parameterExcludedProjects, + List parameterActiveRequiredProjects, + List parameterActiveOptionalProjects, + List parameterInactiveRequiredProjects, + List parameterInactiveOptionalProjects, String parameterResumeFrom, String parameterMakeBehavior, - List parameterExpectedResult, + ExpectedResult parameterExpectedResult, File parameterRequestedPom) { // Given - List selectedProjects = parameterSelectedProjects.stream().map( p -> ":" + p ).collect( Collectors.toList() ); - List excludedProjects = parameterExcludedProjects.stream().map( p -> ":" + p ).collect( Collectors.toList() ); + ProjectActivation projectActivation = new ProjectActivation(); + parameterActiveRequiredProjects.forEach( projectActivation::activateRequiredProject ); + parameterActiveOptionalProjects.forEach( projectActivation::activateOptionalProject ); + parameterInactiveRequiredProjects.forEach( projectActivation::deactivateRequiredProject ); + parameterInactiveOptionalProjects.forEach( projectActivation::deactivateOptionalProject ); - when( mavenExecutionRequest.getSelectedProjects() ).thenReturn( selectedProjects ); - when( mavenExecutionRequest.getExcludedProjects() ).thenReturn( excludedProjects ); + when( mavenExecutionRequest.getProjectActivation() ).thenReturn( projectActivation ); when( mavenExecutionRequest.getMakeBehavior() ).thenReturn( parameterMakeBehavior ); when( mavenExecutionRequest.getPom() ).thenReturn( parameterRequestedPom ); if ( StringUtils.isNotEmpty( parameterResumeFrom ) ) @@ -212,11 +280,27 @@ public class DefaultGraphBuilderTest Result result = graphBuilder.build( session ); // Then - List actualReactorProjects = result.get().getSortedProjects(); - List expectedReactorProjects = parameterExpectedResult.stream() - .map( artifactIdProjectMap::get ) - .collect( Collectors.toList()); - assertEquals( expectedReactorProjects, actualReactorProjects, parameterDescription ); + if ( parameterExpectedResult instanceof SelectedProjectsResult ) + { + assertThat( result.hasErrors() ).isFalse(); + List expectedProjectNames = ((SelectedProjectsResult) parameterExpectedResult).projectNames; + List actualReactorProjects = result.get().getSortedProjects(); + List expectedReactorProjects = expectedProjectNames.stream() + .map( artifactIdProjectMap::get ) + .collect( toList() ); + assertEquals( expectedReactorProjects, actualReactorProjects, parameterDescription ); + } + else + { + assertThat( result.hasErrors() ).isTrue(); + Class expectedException = ((ExceptionThrown) parameterExpectedResult).expected; + String partOfMessage = ((ExceptionThrown) parameterExpectedResult).partOfMessage; + + assertThat( result.getProblems() ).hasSize( 1 ); + result.getProblems().forEach( p -> + assertThat( p.getException() ).isInstanceOf( expectedException ).hasMessageContaining( partOfMessage ) + ); + } } @BeforeEach @@ -268,7 +352,7 @@ public class DefaultGraphBuilderTest private MavenProject getMavenProject( String artifactId ) { MavenProject mavenProject = new MavenProject(); - mavenProject.setGroupId( "unittest" ); + mavenProject.setGroupId( GROUP_ID ); mavenProject.setArtifactId( artifactId ); mavenProject.setVersion( "1.0" ); mavenProject.setPomFile( new File ( artifactId, "pom.xml" ) ); @@ -293,14 +377,16 @@ public class DefaultGraphBuilderTest when( result.getProject() ).thenReturn( project ); return result; } ) - .collect( Collectors.toList() ); + .collect( toList() ); } static class ScenarioBuilder { private String description; - private List selectedProjects = emptyList(); - private List excludedProjects = emptyList(); + private List activeRequiredProjects = emptyList(); + private List activeOptionalProjects = emptyList(); + private List inactiveRequiredProjects = emptyList(); + private List inactiveOptionalProjects = emptyList(); private String resumeFrom = ""; private String makeBehavior = ""; private File requestedPom = new File( PARENT_MODULE, "pom.xml" ); @@ -314,15 +400,27 @@ public class DefaultGraphBuilderTest return scenarioBuilder; } - public ScenarioBuilder selectedProjects( String... selectedProjects ) + public ScenarioBuilder activeRequiredProjects( String... activeRequiredProjects ) { - this.selectedProjects = asList( selectedProjects ); + this.activeRequiredProjects = prependWithColonIfNeeded( activeRequiredProjects ); return this; } - public ScenarioBuilder excludedProjects( String... excludedProjects ) + public ScenarioBuilder activeOptionalProjects( String... activeOptionalProjects ) { - this.excludedProjects = asList( excludedProjects ); + this.activeOptionalProjects = prependWithColonIfNeeded( activeOptionalProjects ); + return this; + } + + public ScenarioBuilder inactiveRequiredProjects( String... inactiveRequiredProjects ) + { + this.inactiveRequiredProjects = prependWithColonIfNeeded( inactiveRequiredProjects ); + return this; + } + + public ScenarioBuilder inactiveOptionalProjects( String... inactiveOptionalProjects ) + { + this.inactiveOptionalProjects = prependWithColonIfNeeded( inactiveOptionalProjects ); return this; } @@ -346,9 +444,32 @@ public class DefaultGraphBuilderTest public Arguments expectResult( String... expectedReactorProjects ) { - return Arguments.arguments( - description, selectedProjects, excludedProjects, resumeFrom, makeBehavior, asList( expectedReactorProjects ), requestedPom - ); + ExpectedResult expectedResult = new SelectedProjectsResult( asList( expectedReactorProjects ) ); + return createTestArguments( expectedResult ); + } + + public Arguments expectResult( Class expected, final String partOfMessage ) + { + ExpectedResult expectedResult = new ExceptionThrown( expected, partOfMessage ); + return createTestArguments( expectedResult ); + } + + private Arguments createTestArguments( ExpectedResult expectedResult ) + { + return Arguments.arguments( description, activeRequiredProjects, activeOptionalProjects, + inactiveRequiredProjects, inactiveOptionalProjects, resumeFrom, makeBehavior, expectedResult, + requestedPom ); + } + + private List prependWithColonIfNeeded( String[] selectors ) + { + return Arrays.stream( selectors ) + .map( this::prependWithColonIfNeeded ) + .collect( toList() ); + } + + private String prependWithColonIfNeeded( String selector ) { + return selector.indexOf( ':' ) == -1 ? ":" + selector : selector; } } } \ No newline at end of file diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java b/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java index c217c9ea66..c3dc3b7fad 100644 --- a/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java +++ b/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java @@ -55,6 +55,7 @@ import org.apache.maven.execution.MavenExecutionRequestPopulationException; import org.apache.maven.execution.MavenExecutionRequestPopulator; import org.apache.maven.execution.MavenExecutionResult; import org.apache.maven.execution.ProfileActivation; +import org.apache.maven.execution.ProjectActivation; import org.apache.maven.execution.scope.internal.MojoExecutionScopeModule; import org.apache.maven.extension.internal.CoreExports; import org.apache.maven.extension.internal.CoreExtensionEntry; @@ -113,7 +114,6 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; -import java.util.StringTokenizer; import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -1382,10 +1382,7 @@ public class MavenCli request.setCacheNotFound( true ); request.setCacheTransferError( false ); - final ProjectActivation projectActivation = determineProjectActivation( commandLine ); - request.setSelectedProjects( projectActivation.activeProjects ); - request.setExcludedProjects( projectActivation.inactiveProjects ); - + performProjectActivation( commandLine, request.getProjectActivation() ); performProfileActivation( commandLine, request.getProfileActivation() ); final String localRepositoryPath = determineLocalRepositoryPath( request ); @@ -1472,48 +1469,44 @@ public class MavenCli } // Visible for testing - static ProjectActivation determineProjectActivation ( final CommandLine commandLine ) + static void performProjectActivation( final CommandLine commandLine, final ProjectActivation projectActivation ) { - final ProjectActivation projectActivation = new ProjectActivation(); - if ( commandLine.hasOption( CLIManager.PROJECT_LIST ) ) { - String[] projectOptionValues = commandLine.getOptionValues( CLIManager.PROJECT_LIST ); + final String[] optionValues = commandLine.getOptionValues( CLIManager.PROJECT_LIST ); - if ( projectOptionValues != null ) + if ( optionValues == null || optionValues.length == 0 ) { - for ( String projectOptionValue : projectOptionValues ) - { - StringTokenizer projectTokens = new StringTokenizer( projectOptionValue, "," ); - - while ( projectTokens.hasMoreTokens() ) - { - String projectAction = projectTokens.nextToken().trim(); - - if ( projectAction.startsWith( "-" ) || projectAction.startsWith( "!" ) ) - { - projectActivation.deactivate( projectAction.substring( 1 ) ); - } - else if ( projectAction.startsWith( "+" ) ) - { - projectActivation.activate( projectAction.substring( 1 ) ); - } - else - { - projectActivation.activate( projectAction ); - } - } - } + return; } - } + for ( final String optionValue : optionValues ) + { + for ( String token : optionValue.split( "," ) ) + { + String selector = token.trim(); + boolean active = true; + if ( selector.charAt( 0 ) == '-' || selector.charAt( 0 ) == '!' ) + { + active = false; + selector = selector.substring( 1 ); + } + else if ( token.charAt( 0 ) == '+' ) + { + selector = selector.substring( 1 ); + } - return projectActivation; + boolean optional = selector.charAt( 0 ) == '?'; + selector = selector.substring( optional ? 1 : 0 ); + + projectActivation.addProjectActivation( selector, active, optional ); + } + } + } } // Visible for testing - static void performProfileActivation( final CommandLine commandLine, - final ProfileActivation profileActivation ) + static void performProfileActivation( final CommandLine commandLine, final ProfileActivation profileActivation ) { if ( commandLine.hasOption( CLIManager.ACTIVATE_PROFILES ) ) { @@ -1800,29 +1793,4 @@ public class MavenCli { return container.lookup( ModelProcessor.class ); } - - // Visible for testing - static class ProjectActivation - { - List activeProjects; - List inactiveProjects; - - public void deactivate( final String project ) - { - if ( inactiveProjects == null ) - { - inactiveProjects = new ArrayList<>(); - } - inactiveProjects.add( project ); - } - - public void activate( final String project ) - { - if ( activeProjects == null ) - { - activeProjects = new ArrayList<>(); - } - activeProjects.add( project ); - } - } } diff --git a/maven-embedder/src/test/java/org/apache/maven/cli/MavenCliTest.java b/maven-embedder/src/test/java/org/apache/maven/cli/MavenCliTest.java index de7485cd4c..f9dbf08ae4 100644 --- a/maven-embedder/src/test/java/org/apache/maven/cli/MavenCliTest.java +++ b/maven-embedder/src/test/java/org/apache/maven/cli/MavenCliTest.java @@ -21,19 +21,18 @@ package org.apache.maven.cli; import static java.util.Arrays.asList; import static org.apache.maven.cli.MavenCli.performProfileActivation; -import static org.apache.maven.cli.MavenCli.determineProjectActivation; +import static org.apache.maven.cli.MavenCli.performProjectActivation; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.mockito.ArgumentMatchers.any; -import static org.hamcrest.collection.IsIterableContainingInOrder.contains; -import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -52,6 +51,7 @@ import org.apache.maven.Maven; import org.apache.maven.eventspy.internal.EventSpyDispatcher; import org.apache.maven.execution.MavenExecutionRequest; import org.apache.maven.execution.ProfileActivation; +import org.apache.maven.execution.ProjectActivation; import org.apache.maven.project.MavenProject; import org.apache.maven.shared.utils.logging.MessageUtils; import org.apache.maven.toolchain.building.ToolchainsBuildingRequest; @@ -119,27 +119,27 @@ public class MavenCliTest @Test public void testDetermineProjectActivation() throws ParseException { - MavenCli.ProjectActivation result; - Options options = new Options(); + final Parser parser = new GnuParser(); + + final Options options = new Options(); options.addOption( Option.builder( CLIManager.PROJECT_LIST ).hasArg().build() ); - result = determineProjectActivation( new GnuParser().parse( options, new String[0] ) ); - assertThat( result.activeProjects, is( nullValue() ) ); - assertThat( result.inactiveProjects, is( nullValue() ) ); + ProjectActivation activation; - result = determineProjectActivation( new GnuParser().parse( options, new String[]{ "-pl", "test1,+test2" } ) ); - assertThat( result.activeProjects.size(), is( 2 ) ); - assertThat( result.activeProjects, contains( "test1", "test2" ) ); + activation = new ProjectActivation(); + performProjectActivation( parser.parse( options, new String[]{ "-pl", "test1,+test2,?test3,+?test4" } ), activation ); + assertThat( activation.getRequiredActiveProjectSelectors(), containsInAnyOrder( "test1", "test2" ) ); + assertThat( activation.getOptionalActiveProjectSelectors(), containsInAnyOrder( "test3", "test4" ) ); - result = determineProjectActivation( new GnuParser().parse( options, new String[]{ "-pl", "!test1,-test2" } ) ); - assertThat( result.inactiveProjects.size(), is( 2 ) ); - assertThat( result.inactiveProjects, contains( "test1", "test2" ) ); + activation = new ProjectActivation(); + performProjectActivation( parser.parse( options, new String[]{ "-pl", "!test1,-test2,-?test3,!?test4" } ), activation ); + assertThat( activation.getRequiredInactiveProjectSelectors(), containsInAnyOrder( "test1", "test2" ) ); + assertThat( activation.getOptionalInactiveProjectSelectors(), containsInAnyOrder( "test3", "test4" ) ); - result = determineProjectActivation( new GnuParser().parse( options, new String[]{ "-pl" ,"-test1,+test2" } ) ); - assertThat( result.activeProjects.size(), is( 1 ) ); - assertThat( result.activeProjects, contains( "test2" ) ); - assertThat( result.inactiveProjects.size(), is( 1 ) ); - assertThat( result.inactiveProjects, contains( "test1" ) ); + activation = new ProjectActivation(); + performProjectActivation( parser.parse( options, new String[]{ "-pl", "-test1,+test2" } ), activation ); + assertThat( activation.getRequiredActiveProjectSelectors(), containsInAnyOrder( "test2" ) ); + assertThat( activation.getRequiredInactiveProjectSelectors(), containsInAnyOrder( "test1" ) ); } @Test