Repository: quarkusio/quarkus
Issue: Bean created three times instead of one after renaming test package

Cloning from https://github.com/apache/camel-quarkus/issues/24 so that this can be assigned to a milestone (hopefully 0.19.0). @aloubyansky has a fix that I have verified.

Changed files:
core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java
core/deployment/src/main/java/io/quarkus/runner/RuntimeRunner.java
core/deployment/src/main/java/io/quarkus/deployment/builditem/ArchiveRootBuildItem.java
test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java
core/deployment/src/test/java/io/quarkus/deployment/index/ApplicationArchiveBuildStepTestCase.java
core/deployment/src/main/java/io/quarkus/deployment/index/ApplicationArchiveBuildStep.java
devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java

diff --git a/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java b/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java
index 9238e8801fa..d203d022ce9 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java
@@ -5,6 +5,8 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Properties;
@@ -42,6 +44,7 @@ public class QuarkusAugmentor {
     private final List<Consumer<BuildChainBuilder>> buildChainCustomizers;
     private final LaunchMode launchMode;
     private final List<Path> additionalApplicationArchives;
+    private final Collection<Path> excludedFromIndexing;
     private final LiveReloadBuildItem liveReloadBuildItem;
     private final Properties buildSystemProperties;
 
@@ -53,6 +56,7 @@ public class QuarkusAugmentor {
         this.buildChainCustomizers = new ArrayList<>(builder.buildChainCustomizers);
         this.launchMode = builder.launchMode;
         this.additionalApplicationArchives = new ArrayList<>(builder.additionalApplicationArchives);
+        this.excludedFromIndexing = builder.excludedFromIndexing;
         this.liveReloadBuildItem = builder.liveReloadState;
         this.buildSystemProperties = builder.buildSystemProperties;
     }
@@ -101,7 +105,7 @@ public BuildResult run() throws Exception {
             BuildExecutionBuilder execBuilder = chain.createExecutionBuilder("main")
                     .produce(QuarkusConfig.INSTANCE)
                     .produce(liveReloadBuildItem)
-                    .produce(new ArchiveRootBuildItem(root, rootFs == null ? root : rootFs.getPath("/")))
+                    .produce(new ArchiveRootBuildItem(root, rootFs == null ? root : rootFs.getPath("/"), excludedFromIndexing))
                     .produce(new ClassOutputBuildItem(output))
                     .produce(new ShutdownContextBuildItem())
                     .produce(new LaunchModeBuildItem(launchMode))
@@ -144,6 +148,7 @@ public static Builder builder() {
     public static final class Builder {
 
         List<Path> additionalApplicationArchives = new ArrayList<>();
+        Collection<Path> excludedFromIndexing = Collections.emptySet();
         ClassOutput output;
         ClassLoader classLoader;
         Path root;
@@ -167,6 +172,11 @@ public Builder addAdditionalApplicationArchive(Path archive) {
             return this;
         }
 
+        public Builder excludeFromIndexing(Collection<Path> excludedFromIndexing) {
+            this.excludedFromIndexing = excludedFromIndexing;
+            return this;
+        }
+
         public ClassOutput getOutput() {
             return output;
         }
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/ArchiveRootBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/ArchiveRootBuildItem.java
index fa935eb1eee..35a241d2cb8 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/ArchiveRootBuildItem.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/ArchiveRootBuildItem.java
@@ -2,6 +2,8 @@
 
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
 
 import io.quarkus.builder.item.SimpleBuildItem;
 
@@ -9,17 +11,23 @@ public final class ArchiveRootBuildItem extends SimpleBuildItem {
 
     private final Path archiveLocation;
     private final Path archiveRoot;
+    private final Collection<Path> excludedFromIndexing;
 
     public ArchiveRootBuildItem(Path appClassesDir) {
         this(appClassesDir, appClassesDir);
     }
 
     public ArchiveRootBuildItem(Path archiveLocation, Path archiveRoot) {
+        this(archiveLocation, archiveRoot, Collections.emptySet());
+    }
+
+    public ArchiveRootBuildItem(Path archiveLocation, Path archiveRoot, Collection<Path> excludedFromIndexing) {
         this.archiveLocation = archiveLocation;
         if (!Files.isDirectory(archiveRoot)) {
             throw new IllegalArgumentException(archiveRoot + " does not point to the application classes directory");
         }
         this.archiveRoot = archiveRoot;
+        this.excludedFromIndexing = excludedFromIndexing;
     }
 
     /**
@@ -48,4 +56,8 @@ public Path getArchiveLocation() {
     public Path getArchiveRoot() {
         return archiveRoot;
     }
+
+    public boolean isExcludedFromIndexing(Path p) {
+        return excludedFromIndexing.contains(p);
+    }
 }
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/index/ApplicationArchiveBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/index/ApplicationArchiveBuildStep.java
index d13b71c424d..52a2001af8a 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/index/ApplicationArchiveBuildStep.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/index/ApplicationArchiveBuildStep.java
@@ -84,25 +84,26 @@ ApplicationArchivesBuildItem build(ArchiveRootBuildItem root, ApplicationIndexBu
         }
 
         List<ApplicationArchive> applicationArchives = scanForOtherIndexes(Thread.currentThread().getContextClassLoader(),
-                markerFiles, root.getArchiveLocation(), additionalApplicationArchiveBuildItem, indexCache);
+                markerFiles, root, additionalApplicationArchiveBuildItem, indexCache);
         return new ApplicationArchivesBuildItem(
                 new ApplicationArchiveImpl(appindex.getIndex(), root.getArchiveRoot(), null, false, root.getArchiveLocation()),
                 applicationArchives);
     }
 
     private List<ApplicationArchive> scanForOtherIndexes(ClassLoader classLoader, Set<String> applicationArchiveFiles,
-            Path appRoot, List<AdditionalApplicationArchiveBuildItem> additionalApplicationArchives, IndexCache indexCache)
+            ArchiveRootBuildItem root, List<AdditionalApplicationArchiveBuildItem> additionalApplicationArchives,
+            IndexCache indexCache)
             throws IOException {
         Set<Path> dependenciesToIndex = new HashSet<>();
         //get paths that are included via index-dependencies
-        dependenciesToIndex.addAll(getIndexDependencyPaths(classLoader));
+        dependenciesToIndex.addAll(getIndexDependencyPaths(classLoader, root));
         //get paths that are included via marker files
         Set<String> markers = new HashSet<>(applicationArchiveFiles);
         markers.add(JANDEX_INDEX);
-        dependenciesToIndex.addAll(getMarkerFilePaths(classLoader, markers));
+        dependenciesToIndex.addAll(getMarkerFilePaths(classLoader, markers, root));
 
         //we don't index the application root, this is handled elsewhere
-        dependenciesToIndex.remove(appRoot);
+        dependenciesToIndex.remove(root.getArchiveLocation());
 
         for (AdditionalApplicationArchiveBuildItem i : additionalApplicationArchives) {
             dependenciesToIndex.add(i.getPath());
@@ -111,7 +112,7 @@ private List<ApplicationArchive> scanForOtherIndexes(ClassLoader classLoader, Se
         return indexPaths(dependenciesToIndex, classLoader, indexCache);
     }
 
-    public List<Path> getIndexDependencyPaths(ClassLoader classLoader) {
+    public List<Path> getIndexDependencyPaths(ClassLoader classLoader, ArchiveRootBuildItem root) {
         ArtifactIndex artifactIndex = new ArtifactIndex(new ClassPathArtifactResolver(classLoader));
         try {
             List<Path> ret = new ArrayList<>();
@@ -124,7 +125,9 @@ public List<Path> getIndexDependencyPaths(ClassLoader classLoader) {
                     path = artifactIndex.getPath(entry.getValue().groupId, entry.getValue().artifactId,
                             entry.getValue().classifier);
                 }
-                ret.add(path);
+                if (!root.isExcludedFromIndexing(path)) {
+                    ret.add(path);
+                }
             }
             return ret;
         } catch (Exception e) {
@@ -152,29 +155,39 @@ private static List<ApplicationArchive> indexPaths(Set<Path> dependenciesToIndex
         return ret;
     }
 
-    private static Collection<? extends Path> getMarkerFilePaths(ClassLoader classLoader, Set<String> applicationArchiveFiles)
+    private static Collection<? extends Path> getMarkerFilePaths(ClassLoader classLoader, Set<String> applicationArchiveFiles,
+            ArchiveRootBuildItem root)
             throws IOException {
         List<Path> ret = new ArrayList<>();
         for (String file : applicationArchiveFiles) {
             Enumeration<URL> e = classLoader.getResources(file);
             while (e.hasMoreElements()) {
-                URL url = e.nextElement();
-                ret.add(urlToPath(url));
+                final URL url = e.nextElement();
+                final Path path = urlToPath(url, file);
+                if (!root.isExcludedFromIndexing(path)) {
+                    ret.add(path);
+                }
             }
         }
         return ret;
     }
 
     // package protected for testing purpose
-    static Path urlToPath(URL url) {
+    static Path urlToPath(URL url, String resource) {
         try {
             if (url.getProtocol().equals("jar")) {
                 String jarPath = url.getPath().substring(0, url.getPath().lastIndexOf('!'));
                 return Paths.get(new URI(jarPath));
             } else if (url.getProtocol().equals("file")) {
                 final Path path = Paths.get(url.toURI());
+                if (resource.isEmpty()) {
+                    return path;
+                }
                 final String filePath = path.toString();
-                final int index = filePath.lastIndexOf(File.separator + "META-INF");
+                if (File.separatorChar != '/') {
+                    resource = resource.replace('/', File.separatorChar);
+                }
+                final int index = filePath.lastIndexOf(File.separator + resource);
                 if (index == -1) {
                     return path;
                 }
@@ -222,26 +235,24 @@ private static Index handleJarPath(Path path, IndexCache indexCache) throws IOEx
         return indexCache.cache.computeIfAbsent(path, new Function<Path, Index>() {
             @Override
             public Index apply(Path path) {
-                try {
-                    try (JarFile file = new JarFile(path.toFile())) {
-                        ZipEntry existing = file.getEntry(JANDEX_INDEX);
-                        if (existing != null) {
-                            try (InputStream in = file.getInputStream(existing)) {
-                                IndexReader reader = new IndexReader(in);
-                                if (reader.getIndexVersion() < REQUIRED_INDEX_VERSION) {
-                                    LOGGER.warnf(
-                                            "Re-indexing %s - at least Jandex 2.1 must be used to index an application dependency",
-                                            path);
-                                    return indexJar(file);
-                                } else {
-                                    return reader.read();
-                                }
+                try (JarFile file = new JarFile(path.toFile())) {
+                    ZipEntry existing = file.getEntry(JANDEX_INDEX);
+                    if (existing != null) {
+                        try (InputStream in = file.getInputStream(existing)) {
+                            IndexReader reader = new IndexReader(in);
+                            if (reader.getIndexVersion() < REQUIRED_INDEX_VERSION) {
+                                LOGGER.warnf(
+                                        "Re-indexing %s - at least Jandex 2.1 must be used to index an application dependency",
+                                        path);
+                                return indexJar(file);
+                            } else {
+                                return reader.read();
                             }
                         }
-                        return indexJar(file);
                     }
+                    return indexJar(file);
                 } catch (IOException e) {
-                    throw new RuntimeException(e);
+                    throw new RuntimeException("Failed to process " + path, e);
                 }
             }
         });
diff --git a/core/deployment/src/main/java/io/quarkus/runner/RuntimeRunner.java b/core/deployment/src/main/java/io/quarkus/runner/RuntimeRunner.java
index 60cb49c135d..943c803aefd 100644
--- a/core/deployment/src/main/java/io/quarkus/runner/RuntimeRunner.java
+++ b/core/deployment/src/main/java/io/quarkus/runner/RuntimeRunner.java
@@ -7,10 +7,13 @@
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
+import java.util.Set;
 import java.util.function.BiFunction;
 import java.util.function.Consumer;
 import java.util.logging.Handler;
@@ -41,6 +44,7 @@ public class RuntimeRunner implements Runnable, Closeable {
     private final TransformerTarget transformerTarget;
     private Closeable closeTask;
     private final List<Path> additionalArchives;
+    private final Collection<Path> excludedFromIndexing;
     private final List<Consumer<BuildChainBuilder>> chainCustomizers;
     private final LaunchMode launchMode;
     private final LiveReloadBuildItem liveReloadState;
@@ -49,6 +53,7 @@ public class RuntimeRunner implements Runnable, Closeable {
     public RuntimeRunner(Builder builder) {
         this.target = builder.target;
         this.additionalArchives = new ArrayList<>(builder.additionalArchives);
+        this.excludedFromIndexing = builder.excludedFromIndexing;
         this.chainCustomizers = new ArrayList<>(builder.chainCustomizers);
         this.launchMode = builder.launchMode;
         this.liveReloadState = builder.liveReloadState;
@@ -92,6 +97,7 @@ public void run() {
             for (Path i : additionalArchives) {
                 builder.addAdditionalApplicationArchive(i);
             }
+            builder.excludeFromIndexing(excludedFromIndexing);
             for (Consumer<BuildChainBuilder> i : chainCustomizers) {
                 builder.addBuildChainCustomizer(i);
             }
@@ -153,6 +159,8 @@ public static class Builder {
         private Path transformerCache;
         private LaunchMode launchMode = LaunchMode.NORMAL;
         private final List<Path> additionalArchives = new ArrayList<>();
+        private Set<Path> excludedFromIndexing = Collections.emptySet();
+
         /**
          * additional classes directories that may be hot deployed
          */
@@ -213,6 +221,14 @@ public Builder addChainCustomizers(Collection<Consumer<BuildChainBuilder>> chain
             return this;
         }
 
+        public Builder excludeFromIndexing(Path p) {
+            if (excludedFromIndexing.isEmpty()) {
+                excludedFromIndexing = new HashSet<>(1);
+            }
+            excludedFromIndexing.add(p);
+            return this;
+        }
+
         public Builder setLaunchMode(LaunchMode launchMode) {
             this.launchMode = launchMode;
             return this;
@@ -249,7 +265,9 @@ Path getWiringClassesDir() {
         }
 
         public RuntimeRunner build() {
-            return new RuntimeRunner(this);
+            final RuntimeRunner runtimeRunner = new RuntimeRunner(this);
+            excludedFromIndexing = Collections.emptySet();
+            return runtimeRunner;
         }
     }
 }
diff --git a/core/deployment/src/test/java/io/quarkus/deployment/index/ApplicationArchiveBuildStepTestCase.java b/core/deployment/src/test/java/io/quarkus/deployment/index/ApplicationArchiveBuildStepTestCase.java
index 16211109c1b..6f573a3ea8a 100644
--- a/core/deployment/src/test/java/io/quarkus/deployment/index/ApplicationArchiveBuildStepTestCase.java
+++ b/core/deployment/src/test/java/io/quarkus/deployment/index/ApplicationArchiveBuildStepTestCase.java
@@ -12,18 +12,18 @@
 public class ApplicationArchiveBuildStepTestCase {
     @Test
     public void testUrlToPath() throws MalformedURLException {
-        assertEquals(Paths.get("/a/path"), urlToPath(new URL("jar:file:/a/path!/META-INF/services/my.Service")));
+        assertEquals(Paths.get("/a/path"), urlToPath(new URL("jar:file:/a/path!/META-INF/services/my.Service"), "META-INF"));
         assertEquals(Paths.get("/a/path with whitespace"),
-                urlToPath(new URL("jar:file:/a/path%20with%20whitespace!/META-INF/services/my.Service")));
-        assertEquals(Paths.get("/a/path"), urlToPath(new URL("file:/a/path/META-INF/services/my.Service")));
+                urlToPath(new URL("jar:file:/a/path%20with%20whitespace!/META-INF/services/my.Service"), "META-INF"));
+        assertEquals(Paths.get("/a/path"), urlToPath(new URL("file:/a/path/META-INF/services/my.Service"), "META-INF"));
         assertEquals(Paths.get("/a/path with whitespace"),
-                urlToPath(new URL("file:/a/path%20with%20whitespace/META-INF/services/my.Service")));
-        assertEquals(Paths.get("/a/path"), urlToPath(new URL("file:/a/path")));
-        assertEquals(Paths.get("/a/path with whitespace"), urlToPath(new URL("file:/a/path%20with%20whitespace")));
+                urlToPath(new URL("file:/a/path%20with%20whitespace/META-INF/services/my.Service"), "META-INF"));
+        assertEquals(Paths.get("/a/path"), urlToPath(new URL("file:/a/path"), ""));
+        assertEquals(Paths.get("/a/path with whitespace"), urlToPath(new URL("file:/a/path%20with%20whitespace"), ""));
     }
 
     @Test(expected = RuntimeException.class)
     public void testUrlToPathWithWrongProtocol() throws MalformedURLException {
-        urlToPath(new URL("http://a/path"));
+        urlToPath(new URL("http://a/path"), "");
     }
 }
diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java
index f8b5949760c..656d958f532 100644
--- a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java
+++ b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java
@@ -423,7 +423,7 @@ private void addToClassPaths(StringBuilder classPathManifest, DevModeContext cla
         }
         String path = uri.getRawPath();
         classPathManifest.append(path);
-        if (file.isDirectory()) {
+        if (file.isDirectory() && path.charAt(path.length() - 1) != '/') {
             classPathManifest.append("/");
         }
         classPathManifest.append(" ");
diff --git a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java
index 315b3faec87..d8a86bc12a6 100644
--- a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java
+++ b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java
@@ -223,11 +223,14 @@ public void execute(BuildContext context) {
                 //ignore
             }
 
+            final Path testLocation = PathTestHelper.getTestClassesLocation(testClass);
+
             runtimeRunner = RuntimeRunner.builder()
                     .setLaunchMode(LaunchMode.TEST)
                     .setClassLoader(testClass.getClassLoader())
                     .setTarget(deploymentDir)
-                    .setFrameworkClassesPath(PathTestHelper.getTestClassesLocation(testClass))
+                    .excludeFromIndexing(testLocation)
+                    .setFrameworkClassesPath(testLocation)
                     .addChainCustomizers(customiers)
                     .build();