View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.project;
20  
21  import java.io.File;
22  import java.io.InputStream;
23  import java.nio.file.Files;
24  import java.nio.file.Path;
25  import java.nio.file.StandardCopyOption;
26  import java.util.List;
27  
28  import org.apache.maven.api.model.InputLocation;
29  import org.apache.maven.api.model.InputSource;
30  import org.apache.maven.artifact.Artifact;
31  import org.apache.maven.artifact.repository.ArtifactRepository;
32  import org.apache.maven.impl.InternalSession;
33  import org.apache.maven.internal.impl.DefaultProject;
34  import org.apache.maven.internal.impl.InternalMavenSession;
35  import org.apache.maven.model.Profile;
36  import org.junit.jupiter.api.BeforeEach;
37  import org.junit.jupiter.api.Disabled;
38  import org.junit.jupiter.api.Test;
39  import org.junit.jupiter.api.io.TempDir;
40  import org.mockito.Mockito;
41  
42  import static org.apache.maven.project.ProjectBuildingResultWithProblemMessageMatcher.projectBuildingResultWithProblemMessage;
43  import static org.codehaus.plexus.testing.PlexusExtension.getTestFile;
44  import static org.hamcrest.MatcherAssert.assertThat;
45  import static org.hamcrest.Matchers.contains;
46  import static org.hamcrest.Matchers.containsString;
47  import static org.hamcrest.Matchers.greaterThan;
48  import static org.hamcrest.Matchers.is;
49  import static org.junit.jupiter.api.Assertions.assertEquals;
50  import static org.junit.jupiter.api.Assertions.assertFalse;
51  import static org.junit.jupiter.api.Assertions.assertNotNull;
52  import static org.junit.jupiter.api.Assertions.assertNull;
53  import static org.junit.jupiter.api.Assertions.assertThrows;
54  import static org.junit.jupiter.api.Assertions.assertTrue;
55  import static org.junit.jupiter.api.Assertions.fail;
56  
57  class DefaultMavenProjectBuilderTest extends AbstractMavenProjectTestCase {
58      @TempDir
59      File localRepoDir;
60  
61      // only use by reread()
62      @TempDir
63      Path projectRoot;
64  
65      @Override
66      @BeforeEach
67      public void setUp() throws Exception {
68          projectBuilder = getContainer().lookup(ProjectBuilder.class);
69      }
70  
71      protected MavenProject getProject(Artifact pom, boolean allowStub) throws Exception {
72          ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
73          configuration.setLocalRepository(getLocalRepository());
74          initRepoSession(configuration);
75  
76          return projectBuilder.build(pom, allowStub, configuration).getProject();
77      }
78  
79      /**
80       * Check that we can build ok from the middle pom of a (parent,child,grandchild) hierarchy
81       *
82       * @throws Exception in case of issue
83       */
84      @Test
85      void testBuildFromMiddlePom() throws Exception {
86          File f1 = getTestFile("src/test/resources/projects/grandchild-check/child/pom.xml");
87          File f2 = getTestFile("src/test/resources/projects/grandchild-check/child/grandchild/pom.xml");
88  
89          getProject(f1);
90  
91          // it's the building of the grandchild project, having already cached the child project
92          // (but not the parent project), which causes the problem.
93          getProject(f2);
94      }
95  
96      @Disabled("Maven 4 does not allow duplicate plugin declarations")
97      @Test
98      void testDuplicatePluginDefinitionsMerged() throws Exception {
99          File f1 = getTestFile("src/test/resources/projects/duplicate-plugins-merged-pom.xml");
100 
101         MavenProject project = getProject(f1);
102         assertEquals(2, project.getBuildPlugins().get(0).getDependencies().size());
103         assertEquals(2, project.getBuildPlugins().get(0).getExecutions().size());
104         assertEquals(
105                 "first", project.getBuildPlugins().get(0).getExecutions().get(0).getId());
106     }
107 
108     @Test
109     void testFutureModelVersion() throws Exception {
110         File f1 = getTestFile("src/test/resources/projects/future-model-version-pom.xml");
111 
112         ProjectBuildingException e = assertThrows(
113                 ProjectBuildingException.class, () -> getProject(f1), "Expected to fail for future versions");
114         assertThat(e.getMessage(), containsString("Building this project requires a newer version of Maven"));
115     }
116 
117     @Test
118     void testPastModelVersion() throws Exception {
119         // a Maven 1.x pom will not even
120         // update the resource if we stop supporting modelVersion 4.0.0
121         File f1 = getTestFile("src/test/resources/projects/past-model-version-pom.xml");
122 
123         ProjectBuildingException e = assertThrows(
124                 ProjectBuildingException.class, () -> getProject(f1), "Expected to fail for past versions");
125         assertThat(e.getMessage(), containsString("Building this project requires an older version of Maven"));
126     }
127 
128     @Test
129     void testFutureSchemaModelVersion() throws Exception {
130         File f1 = getTestFile("src/test/resources/projects/future-schema-model-version-pom.xml");
131 
132         ProjectBuildingException e = assertThrows(
133                 ProjectBuildingException.class, () -> getProject(f1), "Expected to fail for future versions");
134         assertThat(e.getMessage(), containsString("Building this project requires a newer version of Maven"));
135     }
136 
137     @Test
138     void testBuildStubModelForMissingRemotePom() throws Exception {
139         Artifact pom = repositorySystem.createProjectArtifact("org.apache.maven.its", "missing", "0.1");
140         MavenProject project = getProject(pom, true);
141 
142         assertNotNull(project.getArtifactId());
143 
144         assertNotNull(project.getRemoteArtifactRepositories());
145         assertTrue(project.getRemoteArtifactRepositories().isEmpty());
146 
147         assertNotNull(project.getPluginArtifactRepositories());
148         assertTrue(project.getPluginArtifactRepositories().isEmpty());
149 
150         assertNull(project.getParent());
151         assertNull(project.getParentArtifact());
152 
153         assertFalse(project.isExecutionRoot());
154     }
155 
156     @Override
157     protected ArtifactRepository getLocalRepository() throws Exception {
158         return repositorySystem.createLocalRepository(getLocalRepositoryPath());
159     }
160 
161     @Test
162     void testPartialResultUponBadDependencyDeclaration() throws Exception {
163         File pomFile = getTestFile("src/test/resources/projects/bad-dependency.xml");
164 
165         ProjectBuildingRequest request = newBuildingRequest();
166         request.setProcessPlugins(false);
167         request.setResolveDependencies(true);
168         ProjectBuildingException e = assertThrows(
169                 ProjectBuildingException.class,
170                 () -> projectBuilder.build(pomFile, request),
171                 "Project building did not fail despite invalid POM");
172         List<ProjectBuildingResult> results = e.getResults();
173         assertNotNull(results);
174         assertEquals(1, results.size());
175         ProjectBuildingResult result = results.get(0);
176         assertNotNull(result);
177         assertNotNull(result.getProject());
178         assertEquals(1, result.getProblems().size());
179         assertEquals(1, result.getProject().getArtifacts().size());
180         assertNotNull(result.getDependencyResolutionResult());
181     }
182 
183     /**
184      * Tests whether local version range parent references are build correctly.
185      *
186      * @throws Exception in case of issue
187      */
188     @Test
189     void testBuildValidParentVersionRangeLocally() throws Exception {
190         File f1 = getTestFile("src/test/resources/projects/parent-version-range-local-valid/child/pom.xml");
191 
192         final MavenProject childProject = getProject(f1);
193 
194         assertNotNull(childProject.getParentArtifact());
195         assertEquals(childProject.getParentArtifact().getVersion(), "1");
196         assertNotNull(childProject.getParent());
197         assertEquals(childProject.getParent().getVersion(), "1");
198         assertNotNull(childProject.getModel().getParent());
199         assertEquals(childProject.getModel().getParent().getVersion(), "[1,10]");
200     }
201 
202     /**
203      * Tests whether local version range parent references are build correctly.
204      *
205      * @throws Exception in case of issue
206      */
207     @Test
208     void testBuildParentVersionRangeLocallyWithoutChildVersion() throws Exception {
209         File f1 = getTestFile(
210                 "src/test/resources/projects/parent-version-range-local-child-without-version/child/pom.xml");
211 
212         ProjectBuildingException e = assertThrows(
213                 ProjectBuildingException.class,
214                 () -> getProject(f1),
215                 "Expected 'ProjectBuildingException' not thrown.");
216         assertThat(e.getResults(), contains(projectBuildingResultWithProblemMessage("Version must be a constant")));
217     }
218 
219     /**
220      * Tests whether local version range parent references are build correctly.
221      *
222      * @throws Exception in case of issue
223      */
224     @Test
225     void testBuildParentVersionRangeLocallyWithChildProjectVersionExpression() throws Exception {
226         File f1 = getTestFile(
227                 "src/test/resources/projects/parent-version-range-local-child-project-version-expression/child/pom.xml");
228 
229         ProjectBuildingException e = assertThrows(
230                 ProjectBuildingException.class,
231                 () -> getProject(f1),
232                 "Expected 'ProjectBuildingException' not thrown.");
233         assertThat(e.getResults(), contains(projectBuildingResultWithProblemMessage("Version must be a constant")));
234     }
235 
236     /**
237      * Tests whether local version range parent references are build correctly.
238      *
239      * @throws Exception
240      */
241     public void testBuildParentVersionRangeLocallyWithChildProjectParentVersionExpression() throws Exception {
242         File f1 = getTestFile(
243                 "src/test/resources/projects/parent-version-range-local-child-project-parent-version-expression/child/pom.xml");
244 
245         try {
246             getProject(f1);
247             fail("Expected 'ProjectBuildingException' not thrown.");
248         } catch (final ProjectBuildingException e) {
249             assertNotNull(e.getMessage());
250             assertThat(e.getMessage(), containsString("Version must be a constant"));
251         }
252     }
253 
254     /**
255      * Tests whether local version range parent references are build correctly.
256      *
257      * @throws Exception
258      */
259     public void testBuildParentVersionRangeLocallyWithChildRevisionExpression() throws Exception {
260         File f1 = getTestFile(
261                 "src/test/resources/projects/parent-version-range-local-child-revision-expression/child/pom.xml");
262 
263         MavenProject mp = this.getProjectFromRemoteRepository(f1);
264 
265         assertEquals("1.0-SNAPSHOT", mp.getVersion());
266     }
267 
268     /**
269      * Tests whether external version range parent references are build correctly.
270      *
271      * @throws Exception in case of issue
272      */
273     @Test
274     void testBuildParentVersionRangeExternally() throws Exception {
275         File f1 = getTestFile("src/test/resources/projects/parent-version-range-external-valid/pom.xml");
276 
277         final MavenProject childProject = this.getProjectFromRemoteRepository(f1);
278 
279         assertNotNull(childProject.getParentArtifact());
280         assertEquals(childProject.getParentArtifact().getVersion(), "1");
281         assertNotNull(childProject.getParent());
282         assertEquals(childProject.getParent().getVersion(), "1");
283         assertNotNull(childProject.getModel().getParent());
284         assertEquals(childProject.getModel().getParent().getVersion(), "[1,1]");
285     }
286 
287     /**
288      * Tests whether external version range parent references are build correctly.
289      *
290      * @throws Exception in case of issue
291      */
292     @Test
293     void testBuildParentVersionRangeExternallyWithoutChildVersion() throws Exception {
294         File f1 =
295                 getTestFile("src/test/resources/projects/parent-version-range-external-child-without-version/pom.xml");
296 
297         ProjectBuildingException e = assertThrows(
298                 ProjectBuildingException.class,
299                 () -> getProjectFromRemoteRepository(f1),
300                 "Expected 'ProjectBuildingException' not thrown.");
301         assertThat(e.getResults(), contains(projectBuildingResultWithProblemMessage("Version must be a constant")));
302     }
303 
304     /**
305      * Tests whether external version range parent references are build correctly.
306      *
307      * @throws Exception in case of issue
308      */
309     @Test
310     void testBuildParentVersionRangeExternallyWithChildProjectVersionExpression() throws Exception {
311         File f1 = getTestFile(
312                 "src/test/resources/projects/parent-version-range-external-child-project-version-expression/pom.xml");
313 
314         ProjectBuildingException e = assertThrows(
315                 ProjectBuildingException.class,
316                 () -> getProjectFromRemoteRepository(f1),
317                 "Expected 'ProjectBuildingException' not thrown.");
318         assertThat(e.getResults(), contains(projectBuildingResultWithProblemMessage("Version must be a constant")));
319     }
320 
321     /**
322      * Ensure that when re-reading a pom, it should not use the cached Model
323      *
324      * @throws Exception in case of issue
325      */
326     @Test
327     void rereadPom_mng7063() throws Exception {
328         final Path pom = projectRoot.resolve("pom.xml");
329         final ProjectBuildingRequest buildingRequest = newBuildingRequest();
330 
331         InternalMavenSession.from(InternalSession.from(buildingRequest.getRepositorySession()))
332                 .getMavenSession()
333                 .getRequest()
334                 .setRootDirectory(projectRoot);
335 
336         try (InputStream pomResource =
337                 DefaultMavenProjectBuilderTest.class.getResourceAsStream("/projects/reread/pom1.xml")) {
338             Files.copy(pomResource, pom, StandardCopyOption.REPLACE_EXISTING);
339         }
340 
341         MavenProject project =
342                 projectBuilder.build(pom.toFile(), buildingRequest).getProject();
343         assertThat(project.getName(), is("aid")); // inherited from artifactId
344 
345         try (InputStream pomResource =
346                 DefaultMavenProjectBuilderTest.class.getResourceAsStream("/projects/reread/pom2.xml")) {
347             Files.copy(pomResource, pom, StandardCopyOption.REPLACE_EXISTING);
348         }
349 
350         project = projectBuilder.build(pom.toFile(), buildingRequest).getProject();
351         assertThat(project.getName(), is("PROJECT NAME"));
352     }
353 
354     @Test
355     void testActivatedProfileBySource() throws Exception {
356         File testPom = getTestFile("src/test/resources/projects/pom-with-profiles/pom.xml");
357 
358         ProjectBuildingRequest request = newBuildingRequest();
359         request.setLocalRepository(getLocalRepository());
360         request.setActiveProfileIds(List.of("profile1"));
361 
362         MavenProject project = projectBuilder.build(testPom, request).getProject();
363 
364         assertTrue(project.getInjectedProfileIds().keySet().containsAll(List.of("external", project.getId())));
365         assertTrue(project.getInjectedProfileIds().get("external").isEmpty());
366         assertTrue(project.getInjectedProfileIds().get(project.getId()).stream().anyMatch("profile1"::equals));
367         assertTrue(project.getInjectedProfileIds().get(project.getId()).stream().noneMatch("profile2"::equals));
368         assertTrue(
369                 project.getInjectedProfileIds().get(project.getId()).stream().noneMatch("active-by-default"::equals));
370     }
371 
372     @Test
373     void testActivatedDefaultProfileBySource() throws Exception {
374         File testPom = getTestFile("src/test/resources/projects/pom-with-profiles/pom.xml");
375 
376         ProjectBuildingRequest request = newBuildingRequest();
377         request.setLocalRepository(getLocalRepository());
378 
379         MavenProject project = projectBuilder.build(testPom, request).getProject();
380 
381         assertTrue(project.getInjectedProfileIds().keySet().containsAll(List.of("external", project.getId())));
382         assertTrue(project.getInjectedProfileIds().get("external").isEmpty());
383         assertTrue(project.getInjectedProfileIds().get(project.getId()).stream().noneMatch("profile1"::equals));
384         assertTrue(project.getInjectedProfileIds().get(project.getId()).stream().noneMatch("profile2"::equals));
385         assertTrue(project.getInjectedProfileIds().get(project.getId()).stream().anyMatch("active-by-default"::equals));
386 
387         InternalMavenSession session = Mockito.mock(InternalMavenSession.class);
388         List<org.apache.maven.api.model.Profile> activeProfiles =
389                 new DefaultProject(session, project).getDeclaredActiveProfiles();
390         assertEquals(1, activeProfiles.size());
391         org.apache.maven.api.model.Profile profile = activeProfiles.get(0);
392         assertEquals("active-by-default", profile.getId());
393         InputLocation location = profile.getLocation("");
394         assertNotNull(location);
395         assertThat(location.getLineNumber(), greaterThan(0));
396         assertThat(location.getColumnNumber(), greaterThan(0));
397         assertNotNull(location.getSource());
398         assertThat(location.getSource().getLocation(), containsString("pom-with-profiles/pom.xml"));
399     }
400 
401     @Test
402     void testActivatedExternalProfileBySource() throws Exception {
403         File testPom = getTestFile("src/test/resources/projects/pom-with-profiles/pom.xml");
404 
405         ProjectBuildingRequest request = newBuildingRequest();
406         request.setLocalRepository(getLocalRepository());
407 
408         final Profile externalProfile = new Profile();
409         externalProfile.setLocation(
410                 "",
411                 new org.apache.maven.model.InputLocation(
412                         1, 1, new org.apache.maven.model.InputSource(new InputSource(null, "settings.xml", null))));
413         externalProfile.setId("external-profile");
414         request.addProfile(externalProfile);
415         request.setActiveProfileIds(List.of(externalProfile.getId()));
416 
417         MavenProject project = projectBuilder.build(testPom, request).getProject();
418 
419         assertTrue(project.getInjectedProfileIds().keySet().containsAll(List.of("external", project.getId())));
420         assertTrue(project.getInjectedProfileIds().get("external").stream().anyMatch("external-profile"::equals));
421         assertTrue(project.getInjectedProfileIds().get(project.getId()).stream().noneMatch("profile1"::equals));
422         assertTrue(project.getInjectedProfileIds().get(project.getId()).stream().noneMatch("profile2"::equals));
423         assertTrue(project.getInjectedProfileIds().get(project.getId()).stream().anyMatch("active-by-default"::equals));
424 
425         InternalMavenSession session = Mockito.mock(InternalMavenSession.class);
426         List<org.apache.maven.api.model.Profile> activeProfiles =
427                 new DefaultProject(session, project).getDeclaredActiveProfiles();
428         assertEquals(2, activeProfiles.size());
429         org.apache.maven.api.model.Profile profile = activeProfiles.get(0);
430         assertEquals("active-by-default", profile.getId());
431         InputLocation location = profile.getLocation("");
432         assertNotNull(location);
433         assertThat(location.getLineNumber(), greaterThan(0));
434         assertThat(location.getColumnNumber(), greaterThan(0));
435         assertNotNull(location.getSource());
436         assertThat(location.getSource().getLocation(), containsString("pom-with-profiles/pom.xml"));
437         profile = activeProfiles.get(1);
438         assertEquals("external-profile", profile.getId());
439         location = profile.getLocation("");
440         assertNotNull(location);
441         assertThat(location.getLineNumber(), greaterThan(0));
442         assertThat(location.getColumnNumber(), greaterThan(0));
443         assertNotNull(location.getSource());
444         assertThat(location.getSource().getLocation(), containsString("settings.xml"));
445     }
446 
447     @Test
448     void testActivatedProfileIsResolved() throws Exception {
449         File testPom = getTestFile("src/test/resources/projects/pom-with-profiles/pom.xml");
450 
451         ProjectBuildingRequest request = newBuildingRequest();
452         request.setLocalRepository(getLocalRepository());
453         request.setActiveProfileIds(List.of("profile1"));
454 
455         MavenProject project = projectBuilder.build(testPom, request).getProject();
456 
457         assertEquals(1, project.getActiveProfiles().size());
458         assertTrue(project.getActiveProfiles().stream().anyMatch(p -> "profile1".equals(p.getId())));
459         assertTrue(project.getActiveProfiles().stream().noneMatch(p -> "profile2".equals(p.getId())));
460         assertTrue(project.getActiveProfiles().stream().noneMatch(p -> "active-by-default".equals(p.getId())));
461     }
462 
463     @Test
464     void testActivatedProfileByDefaultIsResolved() throws Exception {
465         File testPom = getTestFile("src/test/resources/projects/pom-with-profiles/pom.xml");
466 
467         ProjectBuildingRequest request = newBuildingRequest();
468         request.setLocalRepository(getLocalRepository());
469 
470         MavenProject project = projectBuilder.build(testPom, request).getProject();
471 
472         assertEquals(1, project.getActiveProfiles().size());
473         assertTrue(project.getActiveProfiles().stream().noneMatch(p -> "profile1".equals(p.getId())));
474         assertTrue(project.getActiveProfiles().stream().noneMatch(p -> "profile2".equals(p.getId())));
475         assertTrue(project.getActiveProfiles().stream().anyMatch(p -> "active-by-default".equals(p.getId())));
476     }
477 
478     /**
479      * Tests whether external version range parent references are build correctly.
480      *
481      * @throws Exception
482      */
483     public void testBuildParentVersionRangeExternallyWithChildPomVersionExpression() throws Exception {
484         File f1 = getTestFile(
485                 "src/test/resources/projects/parent-version-range-external-child-pom-version-expression/pom.xml");
486 
487         try {
488             this.getProjectFromRemoteRepository(f1);
489             fail("Expected 'ProjectBuildingException' not thrown.");
490         } catch (final ProjectBuildingException e) {
491             assertNotNull(e.getMessage());
492             assertThat(e.getMessage(), containsString("Version must be a constant"));
493         }
494     }
495 
496     /**
497      * Tests whether external version range parent references are build correctly.
498      *
499      * @throws Exception
500      */
501     public void testBuildParentVersionRangeExternallyWithChildPomParentVersionExpression() throws Exception {
502         File f1 = getTestFile(
503                 "src/test/resources/projects/parent-version-range-external-child-pom-parent-version-expression/pom.xml");
504 
505         try {
506             this.getProjectFromRemoteRepository(f1);
507             fail("Expected 'ProjectBuildingException' not thrown.");
508         } catch (final ProjectBuildingException e) {
509             assertNotNull(e.getMessage());
510             assertThat(e.getMessage(), containsString("Version must be a constant"));
511         }
512     }
513 
514     /**
515      * Tests whether external version range parent references are build correctly.
516      *
517      * @throws Exception
518      */
519     public void testBuildParentVersionRangeExternallyWithChildProjectParentVersionExpression() throws Exception {
520         File f1 = getTestFile(
521                 "src/test/resources/projects/parent-version-range-external-child-project-parent-version-expression/pom.xml");
522 
523         try {
524             this.getProjectFromRemoteRepository(f1);
525             fail("Expected 'ProjectBuildingException' not thrown.");
526         } catch (final ProjectBuildingException e) {
527             assertNotNull(e.getMessage());
528             assertThat(e.getMessage(), containsString("Version must be a constant"));
529         }
530     }
531 
532     /**
533      * Tests whether external version range parent references are build correctly.
534      *
535      * @throws Exception
536      */
537     public void testBuildParentVersionRangeExternallyWithChildRevisionExpression() throws Exception {
538         File f1 = getTestFile(
539                 "src/test/resources/projects/parent-version-range-external-child-revision-expression/pom.xml");
540 
541         MavenProject mp = this.getProjectFromRemoteRepository(f1);
542 
543         assertEquals("1.0-SNAPSHOT", mp.getVersion());
544     }
545 
546     @Test
547     public void testSubprojectDiscovery() throws Exception {
548         File pom = getTestFile("src/test/resources/projects/subprojects-discover/pom.xml");
549         ProjectBuildingRequest configuration = newBuildingRequest();
550         InternalSession internalSession = InternalSession.from(configuration.getRepositorySession());
551         InternalMavenSession mavenSession = InternalMavenSession.from(internalSession);
552         mavenSession
553                 .getMavenSession()
554                 .getRequest()
555                 .setRootDirectory(pom.toPath().getParent());
556 
557         List<ProjectBuildingResult> results = projectBuilder.build(List.of(pom), true, configuration);
558         assertEquals(2, results.size());
559         MavenProject p1 = results.get(0).getProject();
560         MavenProject p2 = results.get(1).getProject();
561         MavenProject parent = p1.getArtifactId().equals("parent") ? p1 : p2;
562         MavenProject child = p1.getArtifactId().equals("parent") ? p2 : p1;
563         assertEquals(List.of("child"), parent.getModel().getDelegate().getSubprojects());
564     }
565 }