InjectorDiagnosticsTest.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.di.impl;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Member;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Enumeration;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Supplier;
import org.apache.maven.api.di.Provides;
import org.apache.maven.di.Key;
import org.apache.maven.di.impl.InjectorDiagnosticsFixture.Hostile;
import org.apache.maven.di.impl.InjectorDiagnosticsFixture.RecordingInjector;
import org.apache.maven.di.impl.InjectorDiagnosticsFixture.Unconstructed;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.api.parallel.ResourceLock;
import static org.apache.maven.di.impl.InjectorDiagnosticsFixture.MAVEN_DI_DEBUG;
import static org.apache.maven.di.impl.InjectorDiagnosticsFixture.bindingRows;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.parallel.Resources.SYSTEM_OUT;
import static org.junit.jupiter.api.parallel.Resources.SYSTEM_PROPERTIES;
@ResourceLock(SYSTEM_PROPERTIES)
@ResourceLock(SYSTEM_OUT)
@Timeout(30)
class InjectorDiagnosticsTest {
private static final String SISU_DEBUG = "sisu.debug";
private InjectorDiagnosticsFixture environment;
@BeforeEach
void enableDiagnostics() {
environment = new InjectorDiagnosticsFixture();
}
@AfterEach
void restoreEnvironment() {
environment.close();
}
@Test
void sisuFlagDoesNotEnableNativeDiagnostics() {
String previous = System.getProperty(SISU_DEBUG);
try {
System.setProperty(SISU_DEBUG, "true");
RecordingInjector injector = new RecordingInjector();
injector.bindInstance(String.class, "value");
System.clearProperty(MAVEN_DI_DEBUG);
assertEquals("value", injector.getInstance(String.class));
assertTrue(injector.dumps.isEmpty());
System.setProperty(MAVEN_DI_DEBUG, "false");
injector.checkpoint();
assertTrue(injector.dumps.isEmpty());
} finally {
if (previous == null) {
System.clearProperty(SISU_DEBUG);
} else {
System.setProperty(SISU_DEBUG, previous);
}
}
}
@Test
void flagIsReadAgainForReusedInjector() {
RecordingInjector injector = new RecordingInjector();
AtomicInteger descriptions = new AtomicInteger();
Binding<String> binding = new Binding.BindingToInstance<>("value") {
@Override
List<String> getDiagnosticSourceLines(Function<Member, List<String>> memberSource) {
descriptions.incrementAndGet();
return super.getDiagnosticSourceLines(memberSource);
}
}.withKey(Key.of(String.class));
injector.bind(Key.of(String.class), binding);
injector.bind(Key.of(String.class, "alias"), binding);
for (String value : List.of("false", "yes", "1")) {
System.setProperty(MAVEN_DI_DEBUG, value);
assertEquals("value", injector.getInstance(String.class));
}
System.clearProperty(MAVEN_DI_DEBUG);
injector.checkpoint();
assertTrue(injector.dumps.isEmpty());
assertEquals(0, descriptions.get());
for (String value : List.of("", "TRUE", "true")) {
System.setProperty(MAVEN_DI_DEBUG, value);
injector.checkpoint();
injector.checkpoint();
System.setProperty(MAVEN_DI_DEBUG, "false");
injector.checkpoint();
}
assertEquals(3, injector.dumps.size());
assertEquals(3, descriptions.get());
}
@Test
void discoveryPrintsOnceAndPreservesPartialRegistryOnFailure(@TempDir Path directory) throws Exception {
RecordingInjector injector = new RecordingInjector();
try (URLClassLoader loader = indexLoader(directory, Unconstructed.class.getName())) {
injector.discover(loader);
assertEquals(1, injector.dumps.size());
assertTrue(injector.dumps.get(0).contains("discovery"));
injector.discover(loader);
injector.checkpoint();
assertEquals(1, injector.dumps.size());
}
try (URLClassLoader loader =
indexLoader(directory.resolve("broken"), Hostile.class.getName() + "\nmissing.Type")) {
DIException failure = assertThrows(DIException.class, () -> injector.discover(loader));
assertTrue(failure.getCause() instanceof ClassNotFoundException);
assertEquals(2, injector.dumps.size());
assertTrue(injector.dumps.get(1).contains("partial"));
assertTrue(injector.dumps.get(1).contains(Hostile.class.getName()));
}
}
@Test
void registrationAndOutputFailuresPreserveThePrimaryError() {
RecordingInjector injector = new RecordingInjector();
DIException failure = assertThrows(DIException.class, () -> injector.bindImplicit(BrokenProvider.class));
assertTrue(failure.getMessage().contains("Parameterized method"));
assertTrue(injector.dumps.get(0).contains("partial"));
assertTrue(injector.dumps.get(0).contains(BrokenProvider.class.getName()));
AtomicInteger attempts = new AtomicInteger();
RecordingInjector brokenSink = new RecordingInjector() {
@Override
void writeDump(PrintStream output, String dump) {
if (attempts.getAndIncrement() == 0) {
throw new IllegalStateException("output failed");
}
super.writeDump(output, dump);
}
};
DIException original = assertThrows(DIException.class, () -> brokenSink.bindImplicit(BrokenProvider.class));
assertEquals(failure.getMessage(), original.getMessage());
assertTrue(brokenSink.dumps.isEmpty());
assertSame(
BrokenProvider.class,
brokenSink.getInstance(BrokenProvider.class).getClass());
assertEquals(2, attempts.get());
assertEquals(1, brokenSink.dumps.size());
assertTrue(brokenSink.dumps.get(0).contains(BrokenProvider.class.getName()));
}
@Test
void ambiguousModeFiltersByCandidateCountAndResetsOnModeChanges() {
RecordingInjector injector = new RecordingInjector();
injector.bindInstance(String.class, "single");
injector.bindImplicit(Runnable.class);
System.setProperty(MAVEN_DI_DEBUG, "ambiguous");
injector.checkpoint();
assertTrue(injector.dumps.get(0).contains(", ambiguous"));
assertTrue(injector.dumps.get(0).contains("key: java.lang.Runnable"));
assertFalse(injector.dumps.get(0).contains("java.lang.String"));
injector.bind(Key.of(String.class), Binding.toInstance("second").withKey(Key.of(String.class)));
injector.checkpoint();
assertTrue(injector.dumps.get(1).contains("+ key: java.lang.String"));
assertEquals(2, bindingRows(injector.dumps.get(1)).size());
injector.bindInstance(Runnable.class, () -> {});
injector.checkpoint();
assertTrue(injector.dumps.get(2).contains("- key: java.lang.Runnable"));
System.setProperty(MAVEN_DI_DEBUG, "true");
injector.checkpoint();
assertFalse(injector.dumps.get(3).contains(", delta"));
assertTrue(injector.dumps.get(3).contains("key: java.lang.Runnable"));
System.setProperty(MAVEN_DI_DEBUG, "AMBIGUOUS");
injector.checkpoint();
assertFalse(injector.dumps.get(4).contains(", delta"));
assertFalse(injector.dumps.get(4).contains("java.lang.Runnable"));
}
@Test
void changesHiddenByAmbiguousFilterDoNotPrintEmptyDeltas() {
System.setProperty(MAVEN_DI_DEBUG, "ambiguous");
RecordingInjector injector = new RecordingInjector();
injector.checkpoint();
injector.bindInstance(String.class, "single");
injector.checkpoint();
injector.checkpoint();
assertEquals(1, injector.dumps.size());
injector.bindImplicit(Runnable.class);
injector.checkpoint();
assertEquals(2, injector.dumps.size());
assertTrue(injector.dumps.get(1).contains("+ key: java.lang.Runnable"));
}
@Test
void captureFailureRetainsTheLastSuccessfulBaseline() {
AtomicInteger captures = new AtomicInteger();
RecordingInjector injector = new RecordingInjector();
injector.bindInstance(String.class, "first");
injector.checkpoint();
Key<Integer> key = Key.of(Integer.class);
injector.bind(key, new Binding<Integer>(key, Set.of()) {
@Override
public Supplier<Integer> compile(Function<Dependency<?>, Supplier<?>> compiler) {
throw new AssertionError("compiled for diagnostics");
}
@Override
List<String> getDiagnosticSourceLines(Function<Member, List<String>> source) {
if (captures.getAndIncrement() == 0) {
throw new IllegalStateException("capture unavailable");
}
return List.of("test source");
}
});
injector.checkpoint();
assertEquals(1, injector.dumps.size());
injector.checkpoint();
assertEquals(2, injector.dumps.size());
assertTrue(injector.dumps.get(1).contains(", delta"));
assertTrue(injector.dumps.get(1).contains("+ key: java.lang.Integer"));
}
private static URLClassLoader indexLoader(Path directory, String content) throws Exception {
Path index = directory.resolve("META-INF/maven/org.apache.maven.api.di.Inject");
Files.createDirectories(index.getParent());
Files.writeString(index, content, StandardCharsets.UTF_8);
return new URLClassLoader(
new URL[] {directory.toUri().toURL()}, InjectorDiagnosticsTest.class.getClassLoader()) {
@Override
public Enumeration<URL> getResources(String name) throws IOException {
return findResources(name);
}
};
}
static class BrokenProvider {
@Provides
<T> T provide() {
return null;
}
}
}