InjectorDiagnosticOutputTest.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.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.maven.di.impl.InjectorDiagnosticsFixture.RecordingInjector;
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.parallel.ResourceLock;

import static org.apache.maven.di.impl.InjectorDiagnosticsFixture.body;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
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 InjectorDiagnosticOutputTest {
    private InjectorDiagnosticsFixture environment;

    @BeforeEach
    void enableDiagnostics() {
        environment = new InjectorDiagnosticsFixture();
    }

    @AfterEach
    void restoreEnvironment() {
        environment.close();
    }

    @Test
    void outputChangesAndFailedDeltasRestartWithFullSnapshots() {
        AtomicInteger attempts = new AtomicInteger();
        RecordingInjector injector = new RecordingInjector() {
            @Override
            void writeDump(PrintStream output, String dump) {
                if (attempts.incrementAndGet() == 2) {
                    throw new IllegalStateException("delta output failed");
                }
                super.writeDump(output, dump);
            }
        };
        injector.bindInstance(String.class, "value");
        injector.checkpoint();
        injector.bindInstance(Integer.class, 1);
        injector.checkpoint();
        assertEquals(1, injector.dumps.size());
        injector.checkpoint();
        assertEquals(2, injector.dumps.size());
        assertFalse(injector.dumps.get(1).contains(", delta"));
        assertTrue(injector.dumps.get(1).contains("candidate: java.lang.String"));
        try (PrintStream other = new PrintStream(new ByteArrayOutputStream())) {
            System.setOut(other);
            injector.checkpoint();
            assertEquals(3, injector.dumps.size());
            assertFalse(injector.dumps.get(2).contains(", delta"));
            assertEquals(body(injector.dumps.get(1)), body(injector.dumps.get(2)));
        }
    }

    @Test
    void printStreamErrorsDoNotCommitAnUnwrittenSnapshot() {
        ByteArrayOutputStream captured = new ByteArrayOutputStream();
        AtomicInteger writes = new AtomicInteger();
        try (PrintStream sink =
                new PrintStream(
                        new OutputStream() {
                            @Override
                            public void write(int value) throws IOException {
                                if (writes.getAndIncrement() == 0) {
                                    throw new IOException("output unavailable");
                                }
                                captured.write(value);
                            }
                        },
                        true,
                        StandardCharsets.UTF_8) {
                    @Override
                    public boolean checkError() {
                        boolean failed = super.checkError();
                        clearError();
                        return failed;
                    }
                }) {
            System.setOut(sink);
            InjectorImpl injector = new InjectorImpl();
            injector.bindInstance(String.class, "value");
            assertEquals("value", injector.getInstance(String.class));
            captured.reset();
            assertEquals("value", injector.getInstance(String.class));
            String retry = captured.toString(StandardCharsets.UTF_8);
            assertTrue(retry.contains("candidate: java.lang.String"), retry);
            assertFalse(retry.contains(", delta"), retry);
        }
    }

    @Test
    void stickyPrintStreamErrorKeepsRetryingFullSnapshotsUntilOutputChanges() {
        ByteArrayOutputStream failed = new ByteArrayOutputStream();
        ByteArrayOutputStream recovered = new ByteArrayOutputStream();
        try (PrintStream sink = new PrintStream(failed, true, StandardCharsets.UTF_8) {
                    {
                        setError();
                    }
                };
                PrintStream recovery = new PrintStream(recovered, true, StandardCharsets.UTF_8)) {
            InjectorImpl injector = new InjectorImpl();
            injector.bindInstance(String.class, "fixture");
            System.setOut(sink);
            injector.debugBindings();
            injector.debugBindings();
            String attempts = failed.toString(StandardCharsets.UTF_8);
            assertEquals(
                    2,
                    attempts.lines()
                            .filter(line -> line.startsWith("Maven DI native registry "))
                            .count());
            assertFalse(attempts.contains(", delta"), attempts);
            System.setOut(recovery);
            injector.debugBindings();
            String success = recovered.toString(StandardCharsets.UTF_8);
            assertTrue(success.contains("candidate: java.lang.String"), success);
            assertFalse(success.contains(", delta"), success);
        }
    }
}