BindingSnapshotTest.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.lang.reflect.GenericDeclaration;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;

import org.apache.maven.api.di.Singleton;
import org.apache.maven.di.Key;
import org.apache.maven.di.Scope;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

class BindingSnapshotTest {
    private final DiagnosticIdentity identities = new DiagnosticIdentity();
    private final Object injector = new Object();

    @Test
    void deltaAddsCandidateToExistingKeyWithoutRepeatingUnchangedCandidates() {
        Key<String> key = Key.of(String.class);
        Binding<String> first = Binding.toInstance("first").withKey(key);
        Binding<String> second = Binding.toInstance("second").withKey(key);
        Map<Key<?>, Set<Binding<?>>> registry = new HashMap<>();
        registry.put(key, new HashSet<>(Set.of(first)));
        BindingSnapshot before = capture(registry, 1);
        registry.get(key).add(second);
        BindingSnapshot after = capture(registry, 2);
        String delta = render(after, before);
        assertTrue(delta.contains("revision=1->2"), delta);
        assertTrue(delta.contains("~ key: java.lang.String"), delta);
        assertTrue(delta.contains("+ candidate: java.lang.String"), delta);
        assertFalse(delta.contains(Binding.identityString(((Binding.BindingToInstance<?>) first).instance)), delta);
        assertEquals(
                1, delta.lines().filter(line -> line.contains("candidate:")).count());
        assertNull(render(capture(registry, 3), after));
    }

    @Test
    void metadataChangesAndRemovalsRemainVisible() {
        Key<String> key = Key.of(String.class);
        Binding<String> first = Binding.toInstance("first").withKey(key);
        Binding<String> second = Binding.toInstance("second").withKey(key);
        Binding<Integer> other = Binding.toInstance(1).withKey(Key.of(Integer.class));
        Map<Key<?>, Set<Binding<?>>> registry = new HashMap<>();
        registry.put(key, new HashSet<>(Set.of(first, second)));
        registry.put(Key.of(Integer.class), Set.of(other));
        BindingSnapshot before = capture(registry, 1);
        first.prioritize(42);
        registry.get(key).remove(second);
        registry.remove(Key.of(Integer.class));
        BindingSnapshot after = capture(registry, 2);
        String delta = render(after, before);
        assertTrue(delta.contains("~ candidate: java.lang.String"), delta);
        assertTrue(delta.contains("priority: 42"), delta);
        assertTrue(delta.contains("- candidate: java.lang.String"), delta);
        assertTrue(delta.contains("- key: java.lang.Integer"), delta);
        assertFalse(render(before, null).contains("priority: 42"));
        registry.get(key).clear();
        String empty = render(capture(registry, 3), after);
        assertTrue(empty.contains("- candidate: java.lang.String"), empty);
        assertTrue(empty.contains("<no candidates>"), empty);
    }

    @Test
    void earlierKeysFromNewRealmsDoNotRenumberExistingLoaderReferences() throws Exception {
        byte[] bytes;
        try (var input = Sample.class.getResourceAsStream("BindingSnapshotTest$Sample.class")) {
            bytes = input.readAllBytes();
        }
        class Realm extends ClassLoader {
            Class<?> define() {
                return defineClass(Sample.class.getName(), bytes, 0, bytes.length);
            }
        }
        Key<Object> oldKey = Key.ofType(new Realm().define(), "z");
        Key<Object> newKey = Key.ofType(new Realm().define(), "a");
        Map<Key<?>, Set<Binding<?>>> registry = new HashMap<>();
        registry.put(oldKey, Set.of(Binding.toInstance(new Object()).withKey(oldKey)));
        BindingSnapshot before = capture(registry, 1);
        registry.put(newKey, Set.of(Binding.toInstance(new Object()).withKey(newKey)));
        BindingSnapshot after = capture(registry, 2);
        String delta = render(after, before);
        assertTrue(delta.contains("+ key: @Named(\"a\") " + Sample.class.getName() + " [loader=L2]"), delta);
        assertFalse(delta.contains("@Named(\"z\")"), delta);
        assertTrue(delta.contains("L1:"), delta);
        assertTrue(delta.contains("L2:"), delta);
    }

    @Test
    void stylingDoesNotChangeSnapshotComparisonOrEscapedContent() {
        Key<String> key = Key.of(String.class, "quote\"\n");
        Binding<String> binding = Binding.toInstance("unused").withKey(key);
        Map<Key<?>, Set<Binding<?>>> registry = Map.of(key, Set.of(binding));
        BindingSnapshot snapshot = capture(registry, 1);
        BindingStyler styled = new BindingStyler() {
            @Override
            public String key(String text) {
                return "<K>" + text + "</K>";
            }

            @Override
            public String qualifier(String text) {
                return "<Q>" + text + "</Q>";
            }

            @Override
            public String header(String text) {
                return "<H>" + text + "</H>";
            }
        };
        String plain = render(snapshot, null);
        String colored = snapshot.render(injector, "test", false, null, styled);
        assertTrue(colored.contains("<K>java.lang.String [loader=bootstrap]</K>"), colored);
        assertTrue(colored.contains("<Q>@Named("), colored);
        assertTrue(colored.contains("<H>key:</H>"), colored);
        assertEquals(plain, colored.replaceAll("</?[KQH]>", ""));
        assertTrue(plain.contains("quote\\\"\\u000a"), plain);
        assertNull(capture(registry, 2).render(injector, "test", false, snapshot, styled));
    }

    @Test
    void scopeChangesAreComparedIndependentlyOfBindings() {
        var singleton = Singleton.class;
        Supplier<Scope> first = () -> {
            throw new AssertionError("scope provisioned");
        };
        Supplier<Scope> second = () -> {
            throw new AssertionError("scope provisioned");
        };
        BindingSnapshot before = BindingSnapshot.capture(Map.of(), Map.of(singleton, first), identities, false, 1);
        BindingSnapshot after = BindingSnapshot.capture(Map.of(), Map.of(singleton, second), identities, false, 2);
        String delta = render(after, before);
        assertTrue(delta.contains("- scopes:"), delta);
        assertTrue(delta.contains("+ scopes:"), delta);
        assertTrue(delta.contains(Binding.identityString(first)), delta);
        assertTrue(delta.contains(Binding.identityString(second)), delta);
    }

    @Test
    void collidingHandlesPreserveCandidatesWithEqualDescriptions() {
        DiagnosticIdentity colliding = new DiagnosticIdentity(object -> 1);
        Key<String> key = Key.of(String.class);
        Binding.TupleConstructorN<String> factory = args -> "unused";
        Binding<String> first = Binding.to(key, factory, new Class<?>[0]);
        Binding<String> second = Binding.to(key, factory, new Class<?>[0]);
        Binding<String> third = Binding.to(key, factory, new Class<?>[0]);
        BindingSnapshot initial = BindingSnapshot.capture(Map.of(key, Set.of(first)), Map.of(), colliding, false, 1);
        BindingSnapshot before =
                BindingSnapshot.capture(Map.of(key, Set.of(first, second)), Map.of(), colliding, false, 2);
        String added = render(before, initial);
        assertEquals(
                1, added.lines().filter(line -> line.contains("+ candidate:")).count());
        assertFalse(added.contains("- candidate:"));
        BindingSnapshot after = BindingSnapshot.capture(Map.of(key, Set.of(second)), Map.of(), colliding, false, 3);
        String removed = render(after, before);
        assertEquals(
                1, removed.lines().filter(line -> line.contains("- candidate:")).count());
        BindingSnapshot replacement =
                BindingSnapshot.capture(Map.of(key, Set.of(third)), Map.of(), colliding, false, 4);
        String replaced = render(replacement, after);
        assertEquals(
                1,
                replaced.lines().filter(line -> line.contains("- candidate:")).count());
        assertEquals(
                1,
                replaced.lines().filter(line -> line.contains("+ candidate:")).count());
    }

    @Test
    void repeatedMethodAndConstructorDeclarationsHaveTheSameTypeIdentity() throws Exception {
        for (int kind = 0; kind < 2; kind++) {
            GenericDeclaration first =
                    kind == 0 ? Variables.class.getDeclaredMethod("method") : Variables.class.getDeclaredConstructor();
            GenericDeclaration second =
                    kind == 0 ? Variables.class.getDeclaredMethod("method") : Variables.class.getDeclaredConstructor();
            Type firstType = ownerWith(first.getTypeParameters()[0]);
            Type secondType = ownerWith(second.getTypeParameters()[0]);
            Binding<Object> binding = Binding.toInstance(new Object());
            BindingSnapshot before = capture(Map.of(Key.ofType(firstType), Set.of(binding)), 1);
            BindingSnapshot after = capture(Map.of(Key.ofType(secondType), Set.of(binding)), 2);
            assertNull(render(after, before));
        }
    }

    private static Type ownerWith(Type variable) {
        Type owner = Types.parameterizedType(Outer.class, variable);
        return Types.parameterizedType(owner, Outer.Inner.class, new Type[] {String.class});
    }

    static class Variables {
        <T> Variables() {}

        <T> void method() {}
    }

    @Test
    void dependencyComparisonUsesSemanticKeysAndIgnoresSetIterationOrder() throws Exception {
        Key<Object> first = Key.ofType(First.class.getDeclaredField("field").getGenericType());
        Key<Object> second = Key.ofType(Second.class.getDeclaredField("field").getGenericType());
        Dependency<?> firstDependency = new Dependency<>(first, false);
        Dependency<?> secondDependency = new Dependency<>(second, false);
        Set<Dependency<?>> dependencies = new LinkedHashSet<>(List.of(firstDependency, secondDependency));
        Key<String> key = Key.of(String.class);
        Binding<String> binding = new Binding<>(key, dependencies) {
            @Override
            public Supplier<String> compile(Function<Dependency<?>, Supplier<?>> compiler) {
                throw new AssertionError("compiled for diagnostics");
            }
        };
        BindingSnapshot before = capture(Map.of(key, Set.of(binding)), 1);
        dependencies.clear();
        dependencies.add(secondDependency);
        dependencies.add(firstDependency);
        BindingSnapshot reordered = capture(Map.of(key, Set.of(binding)), 2);
        assertNull(render(reordered, before));
        dependencies.remove(firstDependency);
        BindingSnapshot changed = capture(Map.of(key, Set.of(binding)), 3);
        String delta = render(changed, reordered);
        assertTrue(delta != null && delta.contains("~ candidate:"), String.valueOf(delta));
    }

    @Test
    void opaqueQualifiersAreComparedWithoutCallingTheirMethods() {
        class Qualifier {
            boolean armed;

            @Override
            public int hashCode() {
                if (armed) {
                    throw new AssertionError("qualifier hashCode");
                }
                return 1;
            }

            @Override
            public boolean equals(Object object) {
                if (armed) {
                    throw new AssertionError("qualifier equals");
                }
                return this == object;
            }

            @Override
            public String toString() {
                throw new AssertionError("qualifier toString");
            }
        }
        Qualifier first = new Qualifier();
        Qualifier second = new Qualifier();
        Key<String> firstKey = Key.of(String.class, first);
        Key<String> secondKey = Key.of(String.class, second);
        Binding<String> binding = Binding.toInstance("unused");
        Map<Key<?>, Set<Binding<?>>> registry = Map.of(firstKey, Set.of(binding), secondKey, Set.of(binding));
        first.armed = true;
        second.armed = true;
        BindingSnapshot before = capture(registry, 1);
        BindingSnapshot after = capture(registry, 2);
        assertNull(render(after, before));
        assertEquals(
                2,
                render(before, null)
                        .lines()
                        .filter(line -> line.startsWith("  key:"))
                        .count());
    }

    @Test
    void ownerTypeVariablesFromDifferentDeclarationsRemainDistinct() throws Exception {
        Key<Object> first = Key.ofType(First.class.getDeclaredField("field").getGenericType());
        Key<Object> second = Key.ofType(Second.class.getDeclaredField("field").getGenericType());
        Binding<Object> binding = Binding.toInstance(new Object());
        BindingSnapshot before = capture(Map.of(first, Set.of(binding)), 1);
        BindingSnapshot after = capture(Map.of(second, Set.of(binding)), 2);
        String delta = render(after, before);
        assertTrue(delta != null && delta.contains("- key:") && delta.contains("+ key:"), String.valueOf(delta));
    }

    static class Outer<T> {
        class Inner<U> {}
    }

    static class First<T> {
        Outer<T>.Inner<String> field;
    }

    static class Second<T> {
        Outer<T>.Inner<String> field;
    }

    private BindingSnapshot capture(Map<Key<?>, Set<Binding<?>>> registry, long revision) {
        return BindingSnapshot.capture(registry, Map.of(), identities, false, revision);
    }

    private String render(BindingSnapshot snapshot, BindingSnapshot previous) {
        return snapshot.render(injector, "test", false, previous, BindingStyler.NONE);
    }

    static class Sample {}
}