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.nio.charset.StandardCharsets;
23  import java.nio.file.Files;
24  import java.nio.file.Path;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.List;
28  import java.util.Optional;
29  import java.util.Properties;
30  import java.util.Set;
31  import java.util.concurrent.atomic.AtomicInteger;
32  import java.util.stream.Collectors;
33  
34  import org.apache.maven.AbstractCoreMavenComponentTestCase;
35  import org.apache.maven.api.Language;
36  import org.apache.maven.api.ProjectScope;
37  import org.apache.maven.api.SourceRoot;
38  import org.apache.maven.execution.MavenSession;
39  import org.apache.maven.model.Dependency;
40  import org.apache.maven.model.InputLocation;
41  import org.apache.maven.model.Plugin;
42  import org.apache.maven.model.building.ModelBuildingRequest;
43  import org.apache.maven.model.building.ModelProblem;
44  import org.codehaus.plexus.util.FileUtils;
45  import org.junit.jupiter.api.Test;
46  import org.junit.jupiter.api.io.TempDir;
47  
48  import static org.junit.jupiter.api.Assertions.assertEquals;
49  import static org.junit.jupiter.api.Assertions.assertFalse;
50  import static org.junit.jupiter.api.Assertions.assertNotNull;
51  import static org.junit.jupiter.api.Assertions.assertThrows;
52  import static org.junit.jupiter.api.Assertions.assertTrue;
53  
54  class ProjectBuilderTest extends AbstractCoreMavenComponentTestCase {
55      @Override
56      protected String getProjectsDirectory() {
57          return "src/test/projects/project-builder";
58      }
59  
60      @Test
61      void testSystemScopeDependencyIsPresentInTheCompileClasspathElements() throws Exception {
62          File pom = getProject("it0063");
63  
64          Properties eps = new Properties();
65          eps.setProperty("jre.home", new File(pom.getParentFile(), "jdk/jre").getPath());
66  
67          MavenSession session = createMavenSession(pom, eps);
68          MavenProject project = session.getCurrentProject();
69  
70          // Here we will actually not have any artifacts because the ProjectDependenciesResolver is not involved here. So
71          // right now it's not valid to ask for artifacts unless plugins require the artifacts.
72  
73          project.getCompileClasspathElements();
74      }
75  
76      @Test
77      void testBuildFromModelSource() throws Exception {
78          File pomFile = new File("src/test/resources/projects/modelsource/module01/pom.xml");
79          MavenSession mavenSession = createMavenSession(pomFile);
80          ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
81          configuration.setRepositorySession(mavenSession.getRepositorySession());
82          ProjectBuildingResult result = getContainer()
83                  .lookup(org.apache.maven.project.ProjectBuilder.class)
84                  .build(pomFile, configuration);
85  
86          assertNotNull(result.getProject().getParentFile());
87      }
88  
89      @Test
90      void testVersionlessManagedDependency() throws Exception {
91          File pomFile = new File("src/test/resources/projects/versionless-managed-dependency.xml");
92          MavenSession mavenSession = createMavenSession(null);
93          ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
94          configuration.setRepositorySession(mavenSession.getRepositorySession());
95  
96          ProjectBuildingException e = assertThrows(
97                  ProjectBuildingException.class,
98                  () -> getContainer()
99                          .lookup(org.apache.maven.project.ProjectBuilder.class)
100                         .build(pomFile, configuration));
101         assertEquals(1, e.getResults().size());
102         ProjectBuildingResultWithProblemMessageAssert.assertThat(e.getResults().get(0))
103                 .hasProblemMessage(
104                         "'dependencies.dependency.version' for groupId='org.apache.maven.its', artifactId='a', type='jar' is missing");
105         ProjectBuildingResultWithLocationAssert.assertThat(e.getResults().get(0))
106                 .hasLocation(5, 9);
107     }
108 
109     @Test
110     void testResolveDependencies() throws Exception {
111         File pomFile = new File("src/test/resources/projects/basic-resolveDependencies.xml");
112         MavenSession mavenSession = createMavenSession(null);
113         ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
114         configuration.setRepositorySession(mavenSession.getRepositorySession());
115         configuration.setResolveDependencies(true);
116 
117         // single project build entry point
118         ProjectBuildingResult result = getContainer()
119                 .lookup(org.apache.maven.project.ProjectBuilder.class)
120                 .build(pomFile, configuration);
121         assertEquals(1, result.getProject().getArtifacts().size());
122         // multi projects build entry point
123         List<ProjectBuildingResult> results = getContainer()
124                 .lookup(org.apache.maven.project.ProjectBuilder.class)
125                 .build(Collections.singletonList(pomFile), false, configuration);
126         assertEquals(1, results.size());
127         MavenProject mavenProject = results.get(0).getProject();
128         assertEquals(1, mavenProject.getArtifacts().size());
129 
130         final MavenProject project = mavenProject;
131         final AtomicInteger artifactsResultInAnotherThread = new AtomicInteger();
132         Thread t = new Thread(new Runnable() {
133             @Override
134             public void run() {
135                 artifactsResultInAnotherThread.set(project.getArtifacts().size());
136             }
137         });
138         t.start();
139         t.join();
140         assertEquals(project.getArtifacts().size(), artifactsResultInAnotherThread.get());
141     }
142 
143     @Test
144     void testDontResolveDependencies() throws Exception {
145         File pomFile = new File("src/test/resources/projects/basic-resolveDependencies.xml");
146         MavenSession mavenSession = createMavenSession(null);
147         ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
148         configuration.setRepositorySession(mavenSession.getRepositorySession());
149         configuration.setResolveDependencies(false);
150 
151         // single project build entry point
152         ProjectBuildingResult result = getContainer()
153                 .lookup(org.apache.maven.project.ProjectBuilder.class)
154                 .build(pomFile, configuration);
155         assertEquals(0, result.getProject().getArtifacts().size());
156         // multi projects build entry point
157         List<ProjectBuildingResult> results = getContainer()
158                 .lookup(org.apache.maven.project.ProjectBuilder.class)
159                 .build(Collections.singletonList(pomFile), false, configuration);
160         assertEquals(1, results.size());
161         MavenProject mavenProject = results.get(0).getProject();
162         assertEquals(0, mavenProject.getArtifacts().size());
163     }
164 
165     @Test
166     void testReadModifiedPoms(@TempDir Path tempDir) throws Exception {
167         // TODO a similar test should be created to test the dependency management (basically all usages
168         // of DefaultModelBuilder.getCache() are affected by MNG-6530
169 
170         FileUtils.copyDirectoryStructure(new File("src/test/resources/projects/grandchild-check"), tempDir.toFile());
171 
172         MavenSession mavenSession = createMavenSession(null);
173         mavenSession.getRequest().setRootDirectory(tempDir);
174         ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
175         configuration.setRepositorySession(mavenSession.getRepositorySession());
176         org.apache.maven.project.ProjectBuilder projectBuilder =
177                 getContainer().lookup(org.apache.maven.project.ProjectBuilder.class);
178         File child = new File(tempDir.toFile(), "child/pom.xml");
179         // build project once
180         projectBuilder.build(child, configuration);
181         // modify parent
182         File parent = new File(tempDir.toFile(), "pom.xml");
183         String parentContent = new String(Files.readAllBytes(parent.toPath()), StandardCharsets.UTF_8);
184         parentContent = parentContent.replace(
185                 "<packaging>pom</packaging>",
186                 "<packaging>pom</packaging><properties><addedProperty>addedValue</addedProperty></properties>");
187         Files.write(parent.toPath(), parentContent.getBytes(StandardCharsets.UTF_8));
188         // re-build pom with modified parent
189         ProjectBuildingResult result = projectBuilder.build(child, configuration);
190         assertTrue(result.getProject().getProperties().containsKey("addedProperty"));
191     }
192 
193     @Test
194     void testReadErroneousMavenProjectContainsReference() throws Exception {
195         File pomFile = new File("src/test/resources/projects/artifactMissingVersion/pom.xml").getAbsoluteFile();
196         MavenSession mavenSession = createMavenSession(null);
197         mavenSession.getRequest().setRootDirectory(pomFile.getParentFile().toPath());
198         ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
199         configuration.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL);
200         configuration.setRepositorySession(mavenSession.getRepositorySession());
201         org.apache.maven.project.ProjectBuilder projectBuilder =
202                 getContainer().lookup(org.apache.maven.project.ProjectBuilder.class);
203 
204         // single project build entry point
205         ProjectBuildingException ex1 =
206                 assertThrows(ProjectBuildingException.class, () -> projectBuilder.build(pomFile, configuration));
207 
208         assertEquals(1, ex1.getResults().size());
209         MavenProject project1 = ex1.getResults().get(0).getProject();
210         assertNotNull(project1);
211         assertEquals("testArtifactMissingVersion", project1.getArtifactId());
212         assertEquals(pomFile, project1.getFile());
213 
214         // multi projects build entry point
215         ProjectBuildingException ex2 = assertThrows(
216                 ProjectBuildingException.class,
217                 () -> projectBuilder.build(Collections.singletonList(pomFile), true, configuration));
218 
219         assertEquals(1, ex2.getResults().size());
220         MavenProject project2 = ex2.getResults().get(0).getProject();
221         assertNotNull(project2);
222         assertEquals("testArtifactMissingVersion", project2.getArtifactId());
223         assertEquals(pomFile, project2.getFile());
224     }
225 
226     @Test
227     void testReadInvalidPom() throws Exception {
228         File pomFile = new File("src/test/resources/projects/badPom.xml").getAbsoluteFile();
229         MavenSession mavenSession = createMavenSession(null);
230         ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
231         configuration.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_STRICT);
232         configuration.setRepositorySession(mavenSession.getRepositorySession());
233         org.apache.maven.project.ProjectBuilder projectBuilder =
234                 getContainer().lookup(org.apache.maven.project.ProjectBuilder.class);
235 
236         // single project build entry point
237         Exception ex = assertThrows(Exception.class, () -> projectBuilder.build(pomFile, configuration));
238         assertTrue(ex.getMessage().contains("Received non-all-whitespace CHARACTERS or CDATA event"));
239 
240         // multi projects build entry point
241         ProjectBuildingException pex = assertThrows(
242                 ProjectBuildingException.class,
243                 () -> projectBuilder.build(Collections.singletonList(pomFile), false, configuration));
244         assertEquals(1, pex.getResults().size());
245         assertNotNull(pex.getResults().get(0).getPomFile());
246         assertTrue(pex.getResults().get(0).getProblems().size() > 0);
247         ProjectBuildingResultWithProblemMessageAssert.assertThat(
248                         pex.getResults().get(0))
249                 .hasProblemMessage("Received non-all-whitespace CHARACTERS or CDATA event in nextTag()");
250     }
251 
252     @Test
253     void testReadParentAndChildWithRegularVersionSetParentFile() throws Exception {
254         List<File> toRead = new ArrayList<>(2);
255         File parentPom = getProject("MNG-6723");
256         toRead.add(parentPom);
257         toRead.add(new File(parentPom.getParentFile(), "child/pom.xml"));
258         MavenSession mavenSession = createMavenSession(null);
259         ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
260         configuration.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL);
261         configuration.setRepositorySession(mavenSession.getRepositorySession());
262         org.apache.maven.project.ProjectBuilder projectBuilder =
263                 getContainer().lookup(org.apache.maven.project.ProjectBuilder.class);
264 
265         // read poms separately
266         boolean parentFileWasFoundOnChild = false;
267         for (File file : toRead) {
268             List<ProjectBuildingResult> results =
269                     projectBuilder.build(Collections.singletonList(file), false, configuration);
270             assertResultShowNoError(results);
271             MavenProject project = findChildProject(results);
272             if (project != null) {
273                 assertEquals(parentPom, project.getParentFile());
274                 parentFileWasFoundOnChild = true;
275             }
276         }
277         assertTrue(parentFileWasFoundOnChild);
278 
279         // read projects together
280         List<ProjectBuildingResult> results = projectBuilder.build(toRead, false, configuration);
281         assertResultShowNoError(results);
282         assertEquals(parentPom, findChildProject(results).getParentFile());
283         Collections.reverse(toRead);
284         results = projectBuilder.build(toRead, false, configuration);
285         assertResultShowNoError(results);
286         assertEquals(parentPom, findChildProject(results).getParentFile());
287     }
288 
289     private MavenProject findChildProject(List<ProjectBuildingResult> results) {
290         for (ProjectBuildingResult result : results) {
291             if (result.getPomFile().getParentFile().getName().equals("child")) {
292                 return result.getProject();
293             }
294         }
295         return null;
296     }
297 
298     private void assertResultShowNoError(List<ProjectBuildingResult> results) {
299         for (ProjectBuildingResult result : results) {
300             assertTrue(result.getProblems().isEmpty());
301             assertNotNull(result.getProject());
302         }
303     }
304 
305     @Test
306     void testBuildProperties() throws Exception {
307         File file = new File(getProject("MNG-6716").getParentFile(), "project/pom.xml");
308         MavenSession mavenSession = createMavenSession(null);
309         ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
310         configuration.setRepositorySession(mavenSession.getRepositorySession());
311         configuration.setResolveDependencies(true);
312         List<ProjectBuildingResult> result =
313                 projectBuilder.build(Collections.singletonList(file), false, configuration);
314         MavenProject project = result.get(0).getProject();
315         // verify a few typical parameters are not duplicated
316         assertEquals(1, project.getTestCompileSourceRoots().size());
317         assertEquals(1, project.getCompileSourceRoots().size());
318         assertEquals(1, project.getMailingLists().size());
319         assertEquals(1, project.getResources().size());
320     }
321 
322     @Test
323     void testPropertyInPluginManagementGroupId() throws Exception {
324         File pom = getProject("MNG-6983");
325 
326         MavenSession session = createMavenSession(pom);
327         MavenProject project = session.getCurrentProject();
328 
329         for (Plugin buildPlugin : project.getBuildPlugins()) {
330             assertNotNull(buildPlugin.getVersion(), "Missing version for build plugin " + buildPlugin.getKey());
331         }
332     }
333 
334     @Test
335     void testBuildFromModelSourceResolvesBasedir() throws Exception {
336         File pomFile = new File("src/test/resources/projects/modelsourcebasedir/pom.xml");
337         MavenSession mavenSession = createMavenSession(null);
338         ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
339         configuration.setRepositorySession(mavenSession.getRepositorySession());
340         ProjectBuildingResult result = getContainer()
341                 .lookup(org.apache.maven.project.ProjectBuilder.class)
342                 .build(pomFile, configuration);
343 
344         assertEquals(
345                 pomFile.getAbsoluteFile(),
346                 result.getProject().getModel().getPomFile().getAbsoluteFile());
347         int errors = 0;
348         for (ModelProblem p : result.getProblems()) {
349             if (p.getSeverity() == ModelProblem.Severity.ERROR) {
350                 errors++;
351             }
352         }
353         assertEquals(0, errors);
354     }
355 
356     @Test
357     void testLocationTrackingResolution() throws Exception {
358         File pom = getProject("MNG-7648");
359 
360         MavenSession session = createMavenSession(pom);
361         MavenProject project = session.getCurrentProject();
362 
363         InputLocation dependencyLocation = null;
364         for (Dependency dependency : project.getDependencies()) {
365             if (dependency.getManagementKey().equals("org.apache.maven.its:a:jar")) {
366                 dependencyLocation = dependency.getLocation("version");
367             }
368         }
369         assertNotNull(dependencyLocation, "missing dependency");
370         assertEquals(
371                 "org.apache.maven.its:bom:0.1", dependencyLocation.getSource().getModelId());
372 
373         InputLocation pluginLocation = null;
374         for (Plugin plugin : project.getBuildPlugins()) {
375             if (plugin.getKey().equals("org.apache.maven.plugins:maven-clean-plugin")) {
376                 pluginLocation = plugin.getLocation("version");
377             }
378         }
379         assertNotNull(pluginLocation, "missing build plugin");
380         assertEquals(
381                 "org.apache.maven.its:parent:0.1", pluginLocation.getSource().getModelId());
382     }
383     /**
384      * Tests that a project with multiple modules defined in sources is detected as modular,
385      * and module-aware resource roots are injected for each module.
386      * <p>
387      * Acceptance Criterion: AC2 (unified source tracking for all lang/scope combinations)
388      *
389      * @see <a href="https://github.com/apache/maven/issues/11612">Issue #11612</a>
390      */
391     @Test
392     void testModularSourcesInjectResourceRoots() throws Exception {
393         File pom = getProject("modular-sources");
394 
395         MavenSession session = createMavenSession(pom);
396         MavenProject project = session.getCurrentProject();
397 
398         // Get all resource source roots for main scope
399         List<SourceRoot> mainResourceRoots = project.getEnabledSourceRoots(ProjectScope.MAIN, Language.RESOURCES)
400                 .toList();
401 
402         // Should have resource roots for both modules
403         Set<String> modules = mainResourceRoots.stream()
404                 .map(SourceRoot::module)
405                 .flatMap(Optional::stream)
406                 .collect(Collectors.toSet());
407 
408         assertEquals(2, modules.size(), "Should have resource roots for 2 modules");
409         assertTrue(modules.contains("org.foo.moduleA"), "Should have resource root for moduleA");
410         assertTrue(modules.contains("org.foo.moduleB"), "Should have resource root for moduleB");
411 
412         // Get all resource source roots for test scope
413         List<SourceRoot> testResourceRoots = project.getEnabledSourceRoots(ProjectScope.TEST, Language.RESOURCES)
414                 .toList();
415 
416         // Should have test resource roots for both modules
417         Set<String> testModules = testResourceRoots.stream()
418                 .map(SourceRoot::module)
419                 .flatMap(Optional::stream)
420                 .collect(Collectors.toSet());
421 
422         assertEquals(2, testModules.size(), "Should have test resource roots for 2 modules");
423         assertTrue(testModules.contains("org.foo.moduleA"), "Should have test resource root for moduleA");
424         assertTrue(testModules.contains("org.foo.moduleB"), "Should have test resource root for moduleB");
425     }
426 
427     /**
428      * Tests that when modular sources are configured alongside explicit legacy resources, an error is raised.
429      * <p>
430      * This verifies the behavior described in the design:
431      * - Modular projects with explicit legacy {@code <resources>} configuration should raise an error
432      * - The modular resource roots are injected instead of using the legacy configuration
433      * <p>
434      * Acceptance Criteria:
435      * - AC2 (unified source tracking for all lang/scope combinations)
436      * - AC8 (legacy directories error - supersedes AC7 which originally used WARNING)
437      *
438      * @see <a href="https://github.com/apache/maven/issues/11612">Issue #11612</a>
439      * @see <a href="https://github.com/apache/maven/issues/11701#issuecomment-3858462609">AC8 definition</a>
440      */
441     @Test
442     void testModularSourcesWithExplicitResourcesIssuesError() throws Exception {
443         File pom = getProject("modular-sources-with-explicit-resources");
444 
445         MavenSession mavenSession = createMavenSession(null);
446         ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
447         configuration.setRepositorySession(mavenSession.getRepositorySession());
448 
449         ProjectBuildingResult result = getContainer()
450                 .lookup(org.apache.maven.project.ProjectBuilder.class)
451                 .build(pom, configuration);
452 
453         MavenProject project = result.getProject();
454 
455         // Verify errors are raised for conflicting legacy resources (AC8)
456         List<ModelProblem> errors = result.getProblems().stream()
457                 .filter(p -> p.getSeverity() == ModelProblem.Severity.ERROR)
458                 .filter(p -> p.getMessage().contains("Legacy") && p.getMessage().contains("cannot be used"))
459                 .toList();
460 
461         assertEquals(2, errors.size(), "Should have 2 errors (one for resources, one for testResources)");
462         assertTrue(
463                 errors.stream().anyMatch(e -> e.getMessage().contains("<resources>")),
464                 "Should error about conflicting <resources>");
465         assertTrue(
466                 errors.stream().anyMatch(e -> e.getMessage().contains("<testResources>")),
467                 "Should error about conflicting <testResources>");
468 
469         // Verify modular resources are still injected correctly
470         List<SourceRoot> mainResourceRoots = project.getEnabledSourceRoots(ProjectScope.MAIN, Language.RESOURCES)
471                 .toList();
472 
473         assertEquals(2, mainResourceRoots.size(), "Should have 2 modular resource roots (one per module)");
474 
475         Set<String> mainModules = mainResourceRoots.stream()
476                 .map(SourceRoot::module)
477                 .flatMap(Optional::stream)
478                 .collect(Collectors.toSet());
479 
480         assertEquals(2, mainModules.size(), "Should have resource roots for 2 modules");
481         assertTrue(mainModules.contains("org.foo.moduleA"), "Should have resource root for moduleA");
482         assertTrue(mainModules.contains("org.foo.moduleB"), "Should have resource root for moduleB");
483     }
484 
485     /**
486      * Tests AC8: ALL legacy directories are rejected when {@code <sources>} is configured.
487      * <p>
488      * Modular project with Java in {@code <sources>} for MAIN scope and explicit legacy
489      * {@code <sourceDirectory>} that differs from default. The legacy directory is rejected
490      * because modular projects cannot use legacy directories (content cannot be dispatched
491      * between modules).
492      *
493      * @see <a href="https://github.com/apache/maven/issues/11701#issuecomment-3897961755">Issue #11701 (AC8/AC9)</a>
494      */
495     @Test
496     void testModularWithJavaSourcesRejectsLegacySourceDirectory() throws Exception {
497         File pom = getProject("modular-java-with-explicit-source-dir");
498 
499         MavenSession mavenSession = createMavenSession(null);
500         ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
501         configuration.setRepositorySession(mavenSession.getRepositorySession());
502 
503         ProjectBuildingResult result = getContainer()
504                 .lookup(org.apache.maven.project.ProjectBuilder.class)
505                 .build(pom, configuration);
506 
507         MavenProject project = result.getProject();
508 
509         // Verify ERROR for <sourceDirectory> (MAIN scope has Java in <sources>)
510         List<ModelProblem> errors = result.getProblems().stream()
511                 .filter(p -> p.getSeverity() == ModelProblem.Severity.ERROR)
512                 .filter(p -> p.getMessage().contains("Legacy") && p.getMessage().contains("cannot be used"))
513                 .filter(p -> p.getMessage().contains("<sourceDirectory>"))
514                 .toList();
515 
516         assertEquals(1, errors.size(), "Should have 1 error for <sourceDirectory>");
517 
518         // Verify modular source is used, not legacy
519         List<SourceRoot> mainJavaRoots = project.getEnabledSourceRoots(ProjectScope.MAIN, Language.JAVA_FAMILY)
520                 .toList();
521         assertEquals(1, mainJavaRoots.size(), "Should have 1 modular main Java source root");
522         assertEquals("org.foo.app", mainJavaRoots.get(0).module().orElse(null), "Should have module org.foo.app");
523 
524         // Legacy sourceDirectory is NOT used
525         assertFalse(
526                 mainJavaRoots.get(0).directory().toString().contains("src/custom/main/java"),
527                 "Legacy sourceDirectory should not be used");
528     }
529 
530     /**
531      * Tests AC8: Modular project rejects legacy {@code <testSourceDirectory>} even when
532      * {@code <sources>} has NO Java for TEST scope.
533      * <p>
534      * Modular project with NO Java in {@code <sources>} for TEST scope and explicit legacy
535      * {@code <testSourceDirectory>} that differs from default. The legacy directory is rejected
536      * because modular projects cannot use legacy directories (content cannot be dispatched
537      * between modules).
538      *
539      * @see <a href="https://github.com/apache/maven/issues/11701#issuecomment-3897961755">Issue #11701 (AC8/AC9)</a>
540      */
541     @Test
542     void testModularWithoutTestSourcesRejectsLegacyTestSourceDirectory() throws Exception {
543         File pom = getProject("modular-no-test-java-with-explicit-test-source-dir");
544 
545         MavenSession mavenSession = createMavenSession(null);
546         ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
547         configuration.setRepositorySession(mavenSession.getRepositorySession());
548 
549         ProjectBuildingResult result = getContainer()
550                 .lookup(org.apache.maven.project.ProjectBuilder.class)
551                 .build(pom, configuration);
552 
553         MavenProject project = result.getProject();
554 
555         // Verify ERROR for <testSourceDirectory> (modular projects reject all legacy directories)
556         List<ModelProblem> errors = result.getProblems().stream()
557                 .filter(p -> p.getSeverity() == ModelProblem.Severity.ERROR)
558                 .filter(p -> p.getMessage().contains("Legacy") && p.getMessage().contains("cannot be used"))
559                 .filter(p -> p.getMessage().contains("<testSourceDirectory>"))
560                 .toList();
561 
562         assertEquals(1, errors.size(), "Should have 1 error for <testSourceDirectory>");
563 
564         // No test Java sources (legacy rejected, none in <sources>)
565         List<SourceRoot> testJavaRoots = project.getEnabledSourceRoots(ProjectScope.TEST, Language.JAVA_FAMILY)
566                 .toList();
567         assertEquals(0, testJavaRoots.size(), "Should have no test Java sources");
568     }
569 
570     /**
571      * Tests AC9: explicit legacy directories raise an error in non-modular projects when
572      * {@code <sources>} has Java for that scope.
573      * <p>
574      * This test uses a non-modular project (no {@code <module>} attribute) with both:
575      * <ul>
576      *   <li>{@code <sources>} with main and test Java sources</li>
577      *   <li>Explicit {@code <sourceDirectory>} and {@code <testSourceDirectory>} (conflicting)</li>
578      * </ul>
579      * Both legacy directories should trigger ERROR because {@code <sources>} has Java.
580      *
581      * @see <a href="https://github.com/apache/maven/issues/11701#issuecomment-3897961755">Issue #11701 (AC8/AC9)</a>
582      */
583     @Test
584     void testClassicSourcesWithExplicitLegacyDirectories() throws Exception {
585         File pom = getProject("classic-sources-with-explicit-legacy");
586 
587         MavenSession mavenSession = createMavenSession(null);
588         ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
589         configuration.setRepositorySession(mavenSession.getRepositorySession());
590 
591         ProjectBuildingResult result = getContainer()
592                 .lookup(org.apache.maven.project.ProjectBuilder.class)
593                 .build(pom, configuration);
594 
595         // Verify errors are raised for conflicting legacy directories (AC9)
596         List<ModelProblem> errors = result.getProblems().stream()
597                 .filter(p -> p.getSeverity() == ModelProblem.Severity.ERROR)
598                 .filter(p -> p.getMessage().contains("Legacy") && p.getMessage().contains("cannot be used"))
599                 .toList();
600 
601         assertEquals(2, errors.size(), "Should have 2 errors (one for sourceDirectory, one for testSourceDirectory)");
602 
603         // Verify error messages mention the conflicting elements
604         assertTrue(
605                 errors.stream().anyMatch(e -> e.getMessage().contains("<sourceDirectory>")),
606                 "Should have error for <sourceDirectory>");
607         assertTrue(
608                 errors.stream().anyMatch(e -> e.getMessage().contains("<testSourceDirectory>")),
609                 "Should have error for <testSourceDirectory>");
610     }
611 
612     /**
613      * Tests AC9: Non-modular project with only resources in {@code <sources>} uses implicit Java fallback.
614      * <p>
615      * When {@code <sources>} contains only resources (no Java sources), the legacy
616      * {@code <sourceDirectory>} and {@code <testSourceDirectory>} are used as implicit fallback.
617      * This enables incremental adoption of {@code <sources>} - customize resources while
618      * keeping the default Java directory structure.
619      *
620      * @see <a href="https://github.com/apache/maven/issues/11701#issuecomment-3897961755">Issue #11701 (AC8/AC9)</a>
621      */
622     @Test
623     void testNonModularResourcesOnlyWithImplicitJavaFallback() throws Exception {
624         File pom = getProject("non-modular-resources-only");
625 
626         MavenSession mavenSession = createMavenSession(null);
627         ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
628         configuration.setRepositorySession(mavenSession.getRepositorySession());
629 
630         ProjectBuildingResult result = getContainer()
631                 .lookup(org.apache.maven.project.ProjectBuilder.class)
632                 .build(pom, configuration);
633 
634         MavenProject project = result.getProject();
635 
636         // Verify NO errors - legacy directories are used as fallback (AC9)
637         List<ModelProblem> errors = result.getProblems().stream()
638                 .filter(p -> p.getSeverity() == ModelProblem.Severity.ERROR)
639                 .filter(p -> p.getMessage().contains("Legacy") && p.getMessage().contains("cannot be used"))
640                 .toList();
641 
642         assertEquals(0, errors.size(), "Should have no errors - legacy directories used as fallback (AC9)");
643 
644         // Verify resources from <sources> are used
645         List<SourceRoot> mainResources = project.getEnabledSourceRoots(ProjectScope.MAIN, Language.RESOURCES)
646                 .toList();
647         assertTrue(
648                 mainResources.stream()
649                         .anyMatch(sr -> sr.directory()
650                                 .toString()
651                                 .replace(File.separatorChar, '/')
652                                 .contains("src/main/custom-resources")),
653                 "Should have custom main resources from <sources>");
654 
655         // Verify legacy Java directories are used as fallback
656         List<SourceRoot> mainJavaRoots = project.getEnabledSourceRoots(ProjectScope.MAIN, Language.JAVA_FAMILY)
657                 .toList();
658         assertEquals(1, mainJavaRoots.size(), "Should have 1 main Java source (implicit fallback)");
659         assertTrue(
660                 mainJavaRoots
661                         .get(0)
662                         .directory()
663                         .toString()
664                         .replace(File.separatorChar, '/')
665                         .endsWith("src/main/java"),
666                 "Should use default src/main/java as fallback");
667 
668         List<SourceRoot> testJavaRoots = project.getEnabledSourceRoots(ProjectScope.TEST, Language.JAVA_FAMILY)
669                 .toList();
670         assertEquals(1, testJavaRoots.size(), "Should have 1 test Java source (implicit fallback)");
671         assertTrue(
672                 testJavaRoots
673                         .get(0)
674                         .directory()
675                         .toString()
676                         .replace(File.separatorChar, '/')
677                         .endsWith("src/test/java"),
678                 "Should use default src/test/java as fallback");
679     }
680 
681     /**
682      * Tests AC9 violation: Non-modular project with only resources in {@code <sources>} and explicit legacy directories.
683      * <p>
684      * AC9 allows implicit fallback to legacy directories (when they match defaults).
685      * When legacy directories differ from the default, this is explicit configuration,
686      * which violates AC9's "implicit" requirement, so an ERROR is raised.
687      *
688      * @see <a href="https://github.com/apache/maven/issues/11701#issuecomment-3897961755">Issue #11701 (AC8/AC9)</a>
689      */
690     @Test
691     void testNonModularResourcesOnlyWithExplicitLegacyDirectoriesRejected() throws Exception {
692         File pom = getProject("non-modular-resources-only-explicit-legacy");
693 
694         MavenSession mavenSession = createMavenSession(null);
695         ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
696         configuration.setRepositorySession(mavenSession.getRepositorySession());
697 
698         ProjectBuildingResult result = getContainer()
699                 .lookup(org.apache.maven.project.ProjectBuilder.class)
700                 .build(pom, configuration);
701 
702         MavenProject project = result.getProject();
703 
704         // Verify ERRORs for explicit legacy directories (differ from default)
705         List<ModelProblem> errors = result.getProblems().stream()
706                 .filter(p -> p.getSeverity() == ModelProblem.Severity.ERROR)
707                 .filter(p -> p.getMessage().contains("Legacy") && p.getMessage().contains("cannot be used"))
708                 .toList();
709 
710         assertEquals(2, errors.size(), "Should have 2 errors for explicit legacy directories");
711         assertTrue(
712                 errors.stream().anyMatch(e -> e.getMessage().contains("<sourceDirectory>")),
713                 "Should error about <sourceDirectory>");
714         assertTrue(
715                 errors.stream().anyMatch(e -> e.getMessage().contains("<testSourceDirectory>")),
716                 "Should error about <testSourceDirectory>");
717 
718         // Verify resources from <sources> are still used
719         List<SourceRoot> mainResources = project.getEnabledSourceRoots(ProjectScope.MAIN, Language.RESOURCES)
720                 .toList();
721         assertTrue(
722                 mainResources.stream()
723                         .anyMatch(sr -> sr.directory()
724                                 .toString()
725                                 .replace(File.separatorChar, '/')
726                                 .contains("src/main/custom-resources")),
727                 "Should have custom main resources from <sources>");
728 
729         // Verify NO Java source roots (legacy was rejected, none in <sources>)
730         List<SourceRoot> mainJavaRoots = project.getEnabledSourceRoots(ProjectScope.MAIN, Language.JAVA_FAMILY)
731                 .toList();
732         assertEquals(0, mainJavaRoots.size(), "Should have no main Java sources (legacy rejected)");
733 
734         List<SourceRoot> testJavaRoots = project.getEnabledSourceRoots(ProjectScope.TEST, Language.JAVA_FAMILY)
735                 .toList();
736         assertEquals(0, testJavaRoots.size(), "Should have no test Java sources (legacy rejected)");
737     }
738 
739     /**
740      * Tests AC8: Modular project with Java in {@code <sources>} and physical default legacy directories.
741      * <p>
742      * Even when legacy directories use Super POM defaults (no explicit override),
743      * if the physical directories exist on the filesystem, an ERROR is raised.
744      * This is because modular projects use paths like {@code src/<module>/main/java},
745      * so content in {@code src/main/java} would be silently ignored.
746      *
747      * @see <a href="https://github.com/apache/maven/issues/11701#issuecomment-3897961755">Issue #11701 (AC8/AC9)</a>
748      */
749     @Test
750     void testModularWithPhysicalDefaultLegacyDirectory() throws Exception {
751         File pom = getProject("modular-with-physical-legacy");
752 
753         MavenSession mavenSession = createMavenSession(null);
754         ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
755         configuration.setRepositorySession(mavenSession.getRepositorySession());
756 
757         ProjectBuildingResult result = getContainer()
758                 .lookup(org.apache.maven.project.ProjectBuilder.class)
759                 .build(pom, configuration);
760 
761         // Verify ERRORs are raised for physical presence of default directories (AC8)
762         List<ModelProblem> errors = result.getProblems().stream()
763                 .filter(p -> p.getSeverity() == ModelProblem.Severity.ERROR)
764                 .filter(p -> p.getMessage().contains("Legacy directory")
765                         && p.getMessage().contains("exists"))
766                 .toList();
767 
768         // Should have 2 errors: one for src/main/java, one for src/test/java
769         assertEquals(2, errors.size(), "Should have 2 errors for physical legacy directories");
770         // Use File.separator for platform-independent path matching (backslash on Windows)
771         String mainJava = "src" + File.separator + "main" + File.separator + "java";
772         String testJava = "src" + File.separator + "test" + File.separator + "java";
773         assertTrue(
774                 errors.stream().anyMatch(e -> e.getMessage().contains(mainJava)),
775                 "Should error about physical src/main/java");
776         assertTrue(
777                 errors.stream().anyMatch(e -> e.getMessage().contains(testJava)),
778                 "Should error about physical src/test/java");
779     }
780 
781     /**
782      * Tests AC8: Modular project with only resources in {@code <sources>} and physical default legacy directories.
783      * <p>
784      * Even when {@code <sources>} only contains resources (no Java), if the physical
785      * default directories exist, an ERROR is raised for modular projects.
786      * Unlike non-modular projects (AC9), modular projects cannot use legacy directories as fallback.
787      *
788      * @see <a href="https://github.com/apache/maven/issues/11701#issuecomment-3897961755">Issue #11701 (AC8/AC9)</a>
789      */
790     @Test
791     void testModularResourcesOnlyWithPhysicalDefaultLegacyDirectory() throws Exception {
792         File pom = getProject("modular-resources-only-with-physical-legacy");
793 
794         MavenSession mavenSession = createMavenSession(null);
795         ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
796         configuration.setRepositorySession(mavenSession.getRepositorySession());
797 
798         ProjectBuildingResult result = getContainer()
799                 .lookup(org.apache.maven.project.ProjectBuilder.class)
800                 .build(pom, configuration);
801 
802         // Verify ERRORs are raised for physical presence of default directories (AC8)
803         // Unlike non-modular (AC9), modular projects cannot use legacy as fallback
804         List<ModelProblem> errors = result.getProblems().stream()
805                 .filter(p -> p.getSeverity() == ModelProblem.Severity.ERROR)
806                 .filter(p -> p.getMessage().contains("Legacy directory")
807                         && p.getMessage().contains("exists"))
808                 .toList();
809 
810         // Should have 2 errors: one for src/main/java, one for src/test/java
811         assertEquals(
812                 2, errors.size(), "Should have 2 errors for physical legacy directories (no AC9 fallback for modular)");
813         // Use File.separator for platform-independent path matching (backslash on Windows)
814         String mainJava = "src" + File.separator + "main" + File.separator + "java";
815         String testJava = "src" + File.separator + "test" + File.separator + "java";
816         assertTrue(
817                 errors.stream().anyMatch(e -> e.getMessage().contains(mainJava)),
818                 "Should error about physical src/main/java");
819         assertTrue(
820                 errors.stream().anyMatch(e -> e.getMessage().contains(testJava)),
821                 "Should error about physical src/test/java");
822     }
823 
824     /**
825      * Tests that mixing modular and non-modular sources within {@code <sources>} is not allowed.
826      * <p>
827      * A project must be either fully modular (all sources have a module) or fully classic
828      * (no sources have a module). Mixing them within the same project is not supported
829      * because the compiler plugin cannot handle such configurations.
830      * <p>
831      * This verifies:
832      * - An ERROR is reported when both modular and non-modular sources exist in {@code <sources>}
833      * - sourceDirectory is not used because {@code <sources>} exists
834      * <p>
835      * Acceptance Criteria:
836      * - AC1 (boolean flags eliminated - uses hasSources() for source detection)
837      * - AC6 (mixed sources error - mixing modular and classic sources within {@code <sources>}
838      *   triggers an ERROR)
839      *
840      * @see <a href="https://github.com/apache/maven/issues/11612">Issue #11612</a>
841      */
842     @Test
843     void testSourcesMixedModulesWithinSources() throws Exception {
844         File pom = getProject("sources-mixed-modules");
845 
846         MavenSession mavenSession = createMavenSession(null);
847         ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
848         configuration.setRepositorySession(mavenSession.getRepositorySession());
849 
850         ProjectBuildingResult result = getContainer()
851                 .lookup(org.apache.maven.project.ProjectBuilder.class)
852                 .build(pom, configuration);
853 
854         // Verify an ERROR is reported for mixing modular and non-modular sources
855         List<ModelProblem> errors = result.getProblems().stream()
856                 .filter(p -> p.getSeverity() == ModelProblem.Severity.ERROR)
857                 .filter(p -> p.getMessage().contains("Mixed modular and classic sources"))
858                 .toList();
859 
860         assertEquals(1, errors.size(), "Should have 1 error for mixed modular/classic configuration");
861         assertTrue(errors.get(0).getMessage().contains("lang=java"), "Error should mention java language");
862         assertTrue(errors.get(0).getMessage().contains("scope=main"), "Error should mention main scope");
863     }
864 
865     /**
866      * Tests that multiple source directories for the same (lang, scope, module) combination
867      * are allowed and all are added as source roots.
868      * <p>
869      * This is a valid use case for Phase 2: users may have generated sources alongside regular sources,
870      * both belonging to the same module. Different directories = different identities = not duplicates.
871      * <p>
872      * Acceptance Criterion: AC2 (unified source tracking - multiple directories per module supported)
873      *
874      * @see <a href="https://github.com/apache/maven/issues/11612">Issue #11612</a>
875      */
876     @Test
877     void testMultipleDirectoriesSameModule() throws Exception {
878         File pom = getProject("multiple-directories-same-module");
879 
880         MavenSession session = createMavenSession(pom);
881         MavenProject project = session.getCurrentProject();
882 
883         // Get main Java source roots
884         List<SourceRoot> mainJavaRoots = project.getEnabledSourceRoots(ProjectScope.MAIN, Language.JAVA_FAMILY)
885                 .toList();
886 
887         // Should have 2 main sources: both for com.example.app but different directories
888         assertEquals(2, mainJavaRoots.size(), "Should have 2 main Java source roots for same module");
889 
890         // Both should be for the same module
891         long moduleCount = mainJavaRoots.stream()
892                 .filter(sr -> "com.example.app".equals(sr.module().orElse(null)))
893                 .count();
894         assertEquals(2, moduleCount, "Both main sources should be for com.example.app module");
895 
896         // One should be implicit directory, one should be generated-sources
897         boolean hasImplicitDir = mainJavaRoots.stream()
898                 .anyMatch(sr -> sr.directory()
899                         .toString()
900                         .replace(File.separatorChar, '/')
901                         .contains("src/com.example.app/main/java"));
902         boolean hasGeneratedDir = mainJavaRoots.stream()
903                 .anyMatch(sr -> sr.directory()
904                         .toString()
905                         .replace(File.separatorChar, '/')
906                         .contains("target/generated-sources/com.example.app/java"));
907 
908         assertTrue(hasImplicitDir, "Should have implicit source directory for module");
909         assertTrue(hasGeneratedDir, "Should have generated-sources directory for module");
910 
911         // Get test Java source roots
912         List<SourceRoot> testJavaRoots = project.getEnabledSourceRoots(ProjectScope.TEST, Language.JAVA_FAMILY)
913                 .toList();
914 
915         // Should have 2 test sources: both for com.example.app
916         assertEquals(2, testJavaRoots.size(), "Should have 2 test Java source roots for same module");
917 
918         // Both test sources should be for the same module
919         long testModuleCount = testJavaRoots.stream()
920                 .filter(sr -> "com.example.app".equals(sr.module().orElse(null)))
921                 .count();
922         assertEquals(2, testModuleCount, "Both test sources should be for com.example.app module");
923     }
924 
925     /**
926      * Tests duplicate handling with enabled discriminator.
927      * <p>
928      * Test scenario:
929      * - Same (lang, scope, module, directory) with enabled=true appearing twice → triggers WARNING
930      * - Same identity with enabled=false → should be filtered out (disabled sources are no-ops)
931      * - Different modules should be added normally
932      * <p>
933      * Verifies:
934      * - First enabled source wins, subsequent duplicates trigger WARNING
935      * - Disabled sources don't count as duplicates
936      * - Different modules are unaffected
937      * <p>
938      * Acceptance Criteria:
939      * - AC3 (duplicate detection - duplicates trigger WARNING)
940      * - AC4 (first enabled wins - duplicates are skipped)
941      * - AC5 (disabled sources unchanged - still added but filtered by getEnabledSourceRoots)
942      *
943      * @see <a href="https://github.com/apache/maven/issues/11612">Issue #11612</a>
944      */
945     @Test
946     void testDuplicateEnabledSources() throws Exception {
947         File pom = getProject("duplicate-enabled-sources");
948 
949         MavenSession mavenSession = createMavenSession(null);
950         ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
951         configuration.setRepositorySession(mavenSession.getRepositorySession());
952 
953         ProjectBuildingResult result = getContainer()
954                 .lookup(org.apache.maven.project.ProjectBuilder.class)
955                 .build(pom, configuration);
956 
957         MavenProject project = result.getProject();
958 
959         // Verify warnings are issued for duplicate enabled sources
960         List<ModelProblem> duplicateWarnings = result.getProblems().stream()
961                 .filter(p -> p.getSeverity() == ModelProblem.Severity.WARNING)
962                 .filter(p -> p.getMessage().contains("Duplicate enabled source"))
963                 .toList();
964 
965         // We have 2 duplicate pairs: main scope and test scope for com.example.dup
966         assertEquals(2, duplicateWarnings.size(), "Should have 2 duplicate warnings (main and test scope)");
967 
968         // Get main Java source roots
969         List<SourceRoot> mainJavaRoots = project.getEnabledSourceRoots(ProjectScope.MAIN, Language.JAVA_FAMILY)
970                 .toList();
971 
972         // Should have 2 main sources: 1 for com.example.dup (first wins) + 1 for com.example.other
973         // Note: MavenProject.addSourceRoot still adds all sources, but tracking only counts first enabled
974         assertEquals(2, mainJavaRoots.size(), "Should have 2 main Java source roots");
975 
976         // Verify com.example.other module is present
977         boolean hasOtherModule = mainJavaRoots.stream()
978                 .anyMatch(sr -> "com.example.other".equals(sr.module().orElse(null)));
979         assertTrue(hasOtherModule, "Should have source root for com.example.other module");
980 
981         // Verify com.example.dup module is present (first enabled wins)
982         boolean hasDupModule = mainJavaRoots.stream()
983                 .anyMatch(sr -> "com.example.dup".equals(sr.module().orElse(null)));
984         assertTrue(hasDupModule, "Should have source root for com.example.dup module");
985 
986         // Get test Java source roots
987         List<SourceRoot> testJavaRoots = project.getEnabledSourceRoots(ProjectScope.TEST, Language.JAVA_FAMILY)
988                 .toList();
989 
990         // Test scope has 1 source for com.example.dup (first wins)
991         assertEquals(1, testJavaRoots.size(), "Should have 1 test Java source root");
992 
993         // Verify it's for the dup module
994         assertEquals(
995                 "com.example.dup",
996                 testJavaRoots.get(0).module().orElse(null),
997                 "Test source root should be for com.example.dup module");
998     }
999 
1000     @Test
1001     void testResourceTargetPathRemainsRelativeInCompatLayer() throws Exception {
1002         File pom = getProject("resource-target-path");
1003 
1004         MavenSession mavenSession = createMavenSession(null);
1005         ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
1006         configuration.setRepositorySession(mavenSession.getRepositorySession());
1007 
1008         ProjectBuildingResult result = getContainer()
1009                 .lookup(org.apache.maven.project.ProjectBuilder.class)
1010                 .build(pom, configuration);
1011 
1012         MavenProject project = result.getProject();
1013 
1014         // Verify main resources via SourceRoot API
1015         List<SourceRoot> mainResources = project.getEnabledSourceRoots(ProjectScope.MAIN, Language.RESOURCES)
1016                 .toList();
1017         assertEquals(1, mainResources.size());
1018         assertTrue(mainResources.get(0).targetPath().isPresent(), "Main resource should have a targetPath");
1019         assertFalse(
1020                 mainResources.get(0).targetPath().get().isAbsolute(),
1021                 "SourceRoot targetPath must be relative, got: "
1022                         + mainResources.get(0).targetPath().get());
1023         assertEquals(
1024                 Path.of("META-INF/tags/rdc"), mainResources.get(0).targetPath().get());
1025 
1026         // Verify test resources via SourceRoot API
1027         List<SourceRoot> testResources = project.getEnabledSourceRoots(ProjectScope.TEST, Language.RESOURCES)
1028                 .toList();
1029         assertEquals(1, testResources.size());
1030         assertTrue(testResources.get(0).targetPath().isPresent(), "Test resource should have a targetPath");
1031         assertFalse(
1032                 testResources.get(0).targetPath().get().isAbsolute(),
1033                 "SourceRoot targetPath must be relative, got: "
1034                         + testResources.get(0).targetPath().get());
1035         assertEquals(
1036                 Path.of("org/apache/maven/messages"),
1037                 testResources.get(0).targetPath().get());
1038 
1039         // Verify compat layer: MavenProject.getResources() must return relative targetPath
1040         List<org.apache.maven.model.Resource> resources = project.getResources();
1041         assertEquals(1, resources.size());
1042         assertEquals(
1043                 "META-INF" + File.separator + "tags" + File.separator + "rdc",
1044                 resources.get(0).getTargetPath(),
1045                 "Resource targetPath from getResources() must remain relative");
1046 
1047         // Verify compat layer: MavenProject.getTestResources() must return relative targetPath
1048         List<org.apache.maven.model.Resource> testResourceList = project.getTestResources();
1049         assertEquals(1, testResourceList.size());
1050         assertEquals(
1051                 "org" + File.separator + "apache" + File.separator + "maven" + File.separator + "messages",
1052                 testResourceList.get(0).getTargetPath(),
1053                 "Test resource targetPath from getTestResources() must remain relative");
1054     }
1055 
1056     @Test
1057     void testSourceTargetPathRemainsRelative() throws Exception {
1058         File pom = getProject("source-target-path");
1059 
1060         MavenSession mavenSession = createMavenSession(null);
1061         ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest();
1062         configuration.setRepositorySession(mavenSession.getRepositorySession());
1063 
1064         ProjectBuildingResult result = getContainer()
1065                 .lookup(org.apache.maven.project.ProjectBuilder.class)
1066                 .build(pom, configuration);
1067 
1068         MavenProject project = result.getProject();
1069 
1070         // Verify main resources have relative targetPath preserved
1071         List<SourceRoot> mainResources = project.getEnabledSourceRoots(ProjectScope.MAIN, Language.RESOURCES)
1072                 .toList();
1073         assertEquals(1, mainResources.size());
1074         assertTrue(mainResources.get(0).targetPath().isPresent());
1075         assertEquals(Path.of(".grammar"), mainResources.get(0).targetPath().get());
1076 
1077         // Verify test resources have relative targetPath preserved
1078         List<SourceRoot> testResources = project.getEnabledSourceRoots(ProjectScope.TEST, Language.RESOURCES)
1079                 .toList();
1080         assertEquals(1, testResources.size());
1081         assertTrue(testResources.get(0).targetPath().isPresent());
1082         assertEquals(Path.of("META-INF/test"), testResources.get(0).targetPath().get());
1083 
1084         // Verify the compat layer also returns relative targetPath
1085         List<org.apache.maven.model.Resource> resources = project.getResources();
1086         assertEquals(1, resources.size());
1087         assertEquals(".grammar", resources.get(0).getTargetPath());
1088 
1089         List<org.apache.maven.model.Resource> testResourceList = project.getTestResources();
1090         assertEquals(1, testResourceList.size());
1091         assertEquals(
1092                 "META-INF" + File.separator + "test", testResourceList.get(0).getTargetPath());
1093     }
1094 }