Repository: quarkusio/quarkus
Issue: Continuous testing fails when a dependency brings in an optional dependency and a Jandex index

### Describe the bug

When in a multi-module maven project, with a quarkus module `A` depending on a regular java module `B`. If module `B` contains both an optional dependecy (not declared by `A` too) and a Jandex index, then continuous testing fails with a `BuildException`.

It makes no difference whether the classes in `B` using the optional dependencies are used by the quarkus module or its tests.

Regular testing via maven still works correctly, though.

### Expected behavior

Continuous testing should build and execute the tests.

### Actual behavior

A `BuildException` is thrown:

```
[INFO] --- quarkus-maven-plugin:2.8.0.Final:test (default-cli) @ code-with-quarkus ---
[INFO] Invoking org.apache.maven.plugins:maven-resources-plugin:2.6:resources) @ code-with-quarkus
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 2 resources
[INFO] Invoking io.quarkus.platform:quarkus-maven-plugin:2.8.0.Final:generate-code) @ code-with-quarkus
[INFO] Invoking org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile) @ code-with-quarkus
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to /home/bnazare/Projects/quarkus-21226-reproducer/code-with-quarkus/target/classes
[INFO] Invoking org.apache.maven.plugins:maven-resources-plugin:2.6:testResources) @ code-with-quarkus
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /home/bnazare/Projects/quarkus-21226-reproducer/code-with-quarkus/src/test/resources
[INFO] Invoking io.quarkus.platform:quarkus-maven-plugin:2.8.0.Final:generate-code-tests) @ code-with-quarkus
[INFO] Invoking org.apache.maven.plugins:maven-compiler-plugin:3.8.1:testCompile) @ code-with-quarkus
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 2 source files to /home/bnazare/Projects/quarkus-21226-reproducer/code-with-quarkus/target/test-classes
[INFO] /home/bnazare/Projects/quarkus-21226-reproducer/code-with-quarkus/src/test/java/org/acme/NativeGreetingResourceIT.java: /home/bnazare/Projects/quarkus-21226-reproducer/code-with-quarkus/src/test/java/org/acme/NativeGreetingResourceIT.java uses or overrides a deprecated API that is marked for removal.
[INFO] /home/bnazare/Projects/quarkus-21226-reproducer/code-with-quarkus/src/test/java/org/acme/NativeGreetingResourceIT.java: Recompile with -Xlint:removal for details.
Listening for transport dt_socket at address: 5005
__  ____  __  _____   ___  __ ____  ______ 
 --/ __ \\/ / / / _ | / _ \\/ //_/ / / / __/ 
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\\ \\   
--\\___\\_\\____/_/ |_/_/|_/_/|_|\\____/___/   
2022-04-14 16:59:51,823 INFO  [io.qua.test] (main) Quarkus continuous testing mode started

2022-04-14 16:59:52,032 INFO  [io.qua.test] (Test runner thread) Running 1/1. Running: #JUnit Jupiter
2022-04-14 16:59:52,041 INFO  [io.qua.test] (Test runner thread) Running 1/1. Running: #JUnit Jupiter
2022-04-14 16:59:52,051 INFO  [io.qua.test] (Test runner thread) Running 1/1. Running: org.acme.GreetingResourceTest#GreetingResourceTest
2022-04-14 16:59:52,220 INFO  [org.jbo.threads] (Test runner thread) JBoss Threads version 3.4.2.Final
2022-04-14 16:59:52,684 INFO  [io.qua.test] (Test runner thread) Running 1/1. Running: org.acme.GreetingResourceTest#testHelloEndpoint()
2022-04-14 16:59:52,690 ERROR [io.qua.test] (Test runner thread) ==================== TEST REPORT #1 ====================
2022-04-14 16:59:52,691 ERROR [io.qua.test] (Test runner thread) Test GreetingResourceTest#testHelloEndpoint() failed 
: java.lang.RuntimeException: java.lang.RuntimeException: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
        [error]: Build step io.quarkus.deployment.steps.ClassTransformingBuildStep#handleClassTransformation threw an exception: java.lang.IllegalStateException: java.util.concurrent.ExecutionException: java.lang.TypeNotPresentException: Type com/itextpdf/text/DocumentException not present
        at io.quarkus.deployment.ExtensionLoader$3.execute(ExtensionLoader.java:934)
        at io.quarkus.builder.BuildContext.run(BuildContext.java:277)
        at org.jboss.threads.ContextHandler$1.runWith(ContextHandler.java:18)
        at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2449)
        at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1478)
        at java.base/java.lang.Thread.run(Thread.java:829)
        at org.jboss.threads.JBossThread.run(JBossThread.java:501)
Caused by: java.util.concurrent.ExecutionException: java.lang.TypeNotPresentException: Type com/itextpdf/text/DocumentException not present
        at java.base/java.util.concurrent.FutureTask.report(FutureTask.java:122)
        at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:191)
        at io.quarkus.deployment.steps.ClassTransformingBuildStep.handleClassTransformation(ClassTransformingBuildStep.java:234)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:566)
        at io.quarkus.deployment.ExtensionLoader$3.execute(ExtensionLoader.java:925)
        ... 6 more
```
(the stack trace goes on for around 300 lines, but this is the most relevant part)

### How to Reproduce?

Reproducer: https://github.com/linkareti/quarkus-optional-dep-test-issue-reproducer

Steps to reproduce the behaviour:
1. run `mvn clean install` in the project's root
2. change to folder `code-with-quarkus`
3. run `mvn clean quarkus:test`

### Output of `uname -a` or `ver`

Linux [...] 5.16.18-200.fc35.x86_64 #1 SMP PREEMPT Mon Mar 28 14:10:07 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

### Output of `java -version`

openjdk version "11.0.14.1" 2022-02-08
OpenJDK Runtime Environment 18.9 (build 11.0.14.1+1)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.14.1+1, mixed mode, sharing)

### GraalVM version (if different from Java)

n/a

### Quarkus version or git rev

2.8.0.Final, but I've seen the issue since 2.6.3.Final

### Build tool (ie. output of `mvnw --version` or `gradlew --version`)

Apache Maven 3.8.1 (05c21c65bdfed0f71a2f2ada8b84da59348c4c5d)
Maven home: /home/bnazare/tools/apache-maven-3.8.1
Java version: 11.0.14.1, vendor: Red Hat, Inc., runtime: /usr/lib/jvm/java-11-openjdk-11.0.14.1.1-5.fc35.x86_64
Default locale: pt_PT, platform encoding: UTF-8
OS name: "linux", version: "5.16.18-200.fc35.x86_64", arch: "amd64", family: "unix"

### Additional information

_No response_

Changed files:
core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java
core/deployment/src/main/java/io/quarkus/deployment/steps/ClassTransformingBuildStep.java
core/deployment/src/main/java/io/quarkus/deployment/builditem/BytecodeTransformerBuildItem.java

diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/BytecodeTransformerBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/BytecodeTransformerBuildItem.java
index 80a9a321498..ca5eba39618 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/BytecodeTransformerBuildItem.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/BytecodeTransformerBuildItem.java
@@ -42,6 +42,8 @@ public final class BytecodeTransformerBuildItem extends MultiBuildItem {
 
     final int classReaderOptions;
 
+    final boolean continueOnFailure;
+
     public BytecodeTransformerBuildItem(String classToTransform,
             BiFunction<String, ClassVisitor, ClassVisitor> visitorFunction) {
         this(classToTransform, visitorFunction, null);
@@ -78,6 +80,7 @@ public BytecodeTransformerBuildItem(boolean eager, String classToTransform,
         this.cacheable = cacheable;
         this.inputTransformer = null;
         this.classReaderOptions = 0;
+        this.continueOnFailure = false;
     }
 
     public BytecodeTransformerBuildItem(Builder builder) {
@@ -88,6 +91,7 @@ public BytecodeTransformerBuildItem(Builder builder) {
         this.cacheable = builder.cacheable;
         this.inputTransformer = builder.inputTransformer;
         this.classReaderOptions = builder.classReaderOptions;
+        this.continueOnFailure = builder.continueOnFailure;
         if (visitorFunction == null && inputTransformer == null) {
             throw new IllegalArgumentException("One of either visitorFunction or inputTransformer must be set");
         }
@@ -121,8 +125,13 @@ public BiFunction<String, byte[], byte[]> getInputTransformer() {
         return inputTransformer;
     }
 
+    public boolean isContinueOnFailure() {
+        return continueOnFailure;
+    }
+
     public static class Builder {
         public BiFunction<String, byte[], byte[]> inputTransformer;
+        public boolean continueOnFailure;
         private String classToTransform;
         private BiFunction<String, ClassVisitor, ClassVisitor> visitorFunction;
         private Set<String> requireConstPoolEntry = null;
@@ -130,6 +139,11 @@ public static class Builder {
         private boolean cacheable = false;
         private int classReaderOptions = 0;
 
+        public Builder setContinueOnFailure(boolean continueOnFailure) {
+            this.continueOnFailure = continueOnFailure;
+            return this;
+        }
+
         public Builder setInputTransformer(BiFunction<String, byte[], byte[]> inputTransformer) {
             this.inputTransformer = inputTransformer;
             return this;
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java
index e64c13d3d67..0d72f7a3b04 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java
@@ -122,13 +122,18 @@ public void instrumentTestClasses(CombinedIndexBuildItem combinedIndexBuildItem,
         for (ClassInfo clazz : combinedIndexBuildItem.getIndex().getKnownClasses()) {
             String theClassName = clazz.name().toString();
             if (isAppClass(theClassName)) {
-                transformerProducer.produce(new BytecodeTransformerBuildItem(false, theClassName,
-                        new BiFunction<String, ClassVisitor, ClassVisitor>() {
-                            @Override
-                            public ClassVisitor apply(String s, ClassVisitor classVisitor) {
-                                return new TracingClassVisitor(classVisitor, theClassName);
-                            }
-                        }, true));
+                transformerProducer.produce(new BytecodeTransformerBuildItem.Builder().setEager(false)
+                        .setClassToTransform(theClassName)
+                        .setVisitorFunction(
+                                new BiFunction<String, ClassVisitor, ClassVisitor>() {
+                                    @Override
+                                    public ClassVisitor apply(String s, ClassVisitor classVisitor) {
+                                        return new TracingClassVisitor(classVisitor, theClassName);
+                                    }
+                                })
+                        .setCacheable(true)
+                        .setContinueOnFailure(true)
+                        .build());
             }
         }
 
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassTransformingBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassTransformingBuildStep.java
index d9ab5a0d55f..95517d41d08 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassTransformingBuildStep.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassTransformingBuildStep.java
@@ -125,6 +125,9 @@ public byte[] apply(String className, byte[] originalBytes) {
                 if (classTransformers == null) {
                     return originalBytes;
                 }
+                boolean continueOnFailure = classTransformers.stream()
+                        .filter(a -> !a.isContinueOnFailure())
+                        .findAny().isEmpty();
                 List<BiFunction<String, ClassVisitor, ClassVisitor>> visitors = classTransformers.stream()
                         .map(BytecodeTransformerBuildItem::getVisitorFunction).filter(Objects::nonNull)
                         .collect(Collectors.toList());
@@ -154,6 +157,17 @@ public byte[] apply(String className, byte[] originalBytes) {
                     } else {
                         return originalBytes;
                     }
+                } catch (Throwable e) {
+                    if (continueOnFailure) {
+                        if (log.isDebugEnabled()) {
+                            log.errorf(e, "Failed to transform %s", className);
+                        } else {
+                            log.errorf("Failed to transform %s", className);
+                        }
+                        return originalBytes;
+                    } else {
+                        throw e;
+                    }
                 } finally {
                     Thread.currentThread().setContextClassLoader(old);
                 }
@@ -184,6 +198,10 @@ public byte[] apply(String className, byte[] originalBytes) {
                                 entry.getKey());
                         continue;
                     }
+
+                    boolean continueOnFailure = entry.getValue().stream()
+                            .filter(a -> !a.isContinueOnFailure())
+                            .findAny().isEmpty();
                     List<BiFunction<String, ClassVisitor, ClassVisitor>> visitors = entry.getValue().stream()
                             .map(BytecodeTransformerBuildItem::getVisitorFunction).filter(Objects::nonNull)
                             .collect(Collectors.toList());
@@ -196,9 +214,9 @@ public byte[] apply(String className, byte[] originalBytes) {
                         public TransformedClassesBuildItem.TransformedClass call() throws Exception {
                             ClassLoader old = Thread.currentThread().getContextClassLoader();
                             try {
+                                byte[] classData = classPathElement.getResource(classFileName).getData();
                                 Thread.currentThread().setContextClassLoader(transformCl);
                                 Set<String> constValues = constScanning.get(className);
-                                byte[] classData = classPathElement.getResource(classFileName).getData();
                                 if (constValues != null && !noConstScanning.contains(className)) {
                                     if (!ConstPoolScanner.constPoolEntryPresent(classData, constValues)) {
                                         return null;
@@ -214,6 +232,17 @@ public TransformedClassesBuildItem.TransformedClass call() throws Exception {
                                     transformedClassesCache.put(className, transformedClass);
                                 }
                                 return transformedClass;
+                            } catch (Throwable e) {
+                                if (continueOnFailure) {
+                                    if (log.isDebugEnabled()) {
+                                        log.errorf(e, "Failed to transform %s", className);
+                                    } else {
+                                        log.errorf("Failed to transform %s", className);
+                                    }
+                                    return null;
+                                } else {
+                                    throw e;
+                                }
                             } finally {
                                 Thread.currentThread().setContextClassLoader(old);
                             }