DefaultTempFileServiceTest.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.internal.impl;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

import org.apache.maven.api.Constants;
import org.apache.maven.api.Session;
import org.apache.maven.api.SessionData;
import org.apache.maven.api.services.TempFileService;
import org.apache.maven.execution.MavenSession;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * Tests for DefaultTempFileService and TempFileCleanupParticipant.
 */
class DefaultTempFileServiceTest {

    @AfterEach
    void clearKeepProp() {
        System.clearProperty(Constants.MAVEN_TEMPFILE_KEEP);
    }

    @Test
    void createsFilesAndDirectoriesAndCleansThemUp() throws IOException {
        final TempFileService svc = new DefaultTempFileService();
        final SessionData data = new MapBackedSessionData();
        final Session session = mock(Session.class);
        when(session.getData()).thenReturn(data);

        final Path f1 = svc.createTempFile(session, "tfs-", ".bin");
        final Path d1 = svc.createTempDirectory(session, "tfs-");
        final Path nested = Files.createTempFile(d1, "inner-", ".tmp");

        assertTrue(Files.exists(f1), "temp file must exist");
        assertTrue(Files.exists(d1), "temp dir must exist");
        assertTrue(Files.exists(nested), "nested file must exist");

        svc.cleanup(session);

        assertFalse(Files.exists(f1), "temp file must be deleted");
        assertFalse(Files.exists(d1), "temp dir tree must be deleted");
        assertFalse(Files.exists(nested), "nested file must be deleted");
    }

    @Test
    void registerExternalPathIsAlsoDeleted() throws IOException {
        final TempFileService svc = new DefaultTempFileService();
        final SessionData data = new MapBackedSessionData();
        final Session session = mock(Session.class);
        when(session.getData()).thenReturn(data);

        final Path externalDir = Files.createTempDirectory("ext-");
        final Path nested = Files.createTempFile(externalDir, "ext-inner-", ".tmp");
        assertTrue(Files.exists(externalDir));
        assertTrue(Files.exists(nested));

        svc.register(session, externalDir);
        svc.cleanup(session);

        assertFalse(Files.exists(externalDir), "registered external dir must be deleted recursively");
        assertFalse(Files.exists(nested));
    }

    @Test
    void createsFileInSpecifiedDirectory() throws IOException {
        final TempFileService svc = new DefaultTempFileService();
        final SessionData data = new MapBackedSessionData();
        final Session session = mock(Session.class);
        when(session.getData()).thenReturn(data);

        final Path customDir = Files.createTempDirectory("custom-dir-");
        try {
            final Path f = svc.createTempFile(session, "dir-", ".tmp", customDir);
            assertTrue(Files.exists(f), "temp file must exist");
            assertTrue(f.startsWith(customDir), "temp file must be inside the specified directory");

            svc.cleanup(session);

            assertFalse(Files.exists(f), "temp file must be deleted");
        } finally {
            Files.deleteIfExists(customDir);
        }
    }

    @Test
    void createsDirectoryInSpecifiedParent() throws IOException {
        final TempFileService svc = new DefaultTempFileService();
        final SessionData data = new MapBackedSessionData();
        final Session session = mock(Session.class);
        when(session.getData()).thenReturn(data);

        final Path parentDir = Files.createTempDirectory("parent-dir-");
        try {
            final Path d = svc.createTempDirectory(session, "nested-", parentDir);
            assertTrue(Files.exists(d), "temp directory must exist");
            assertTrue(d.startsWith(parentDir), "temp directory must be inside the specified parent");

            svc.cleanup(session);

            assertFalse(Files.exists(d), "temp directory must be deleted");
        } finally {
            Files.deleteIfExists(parentDir);
        }
    }

    @Test
    void keepPropertySkipsCleanup() throws IOException {
        final TempFileService svc = new DefaultTempFileService();
        final SessionData data = new MapBackedSessionData();
        final Session session = mock(Session.class);
        when(session.getData()).thenReturn(data);

        final Path f = svc.createTempFile(session, "keep-", ".tmp");
        assertTrue(Files.exists(f));

        System.setProperty(Constants.MAVEN_TEMPFILE_KEEP, "true");
        try {
            svc.cleanup(session);
            assertTrue(Files.exists(f), "cleanup must be skipped when -Dmaven.tempfile.keep=true");
        } finally {
            System.clearProperty(Constants.MAVEN_TEMPFILE_KEEP);
        }

        // turn cleanup back on and verify it deletes
        svc.cleanup(session);
        assertFalse(Files.exists(f));
    }

    @Test
    void cleanupParticipantDelegatesCleanupToService() throws IOException {
        final TempFileService svc = new DefaultTempFileService();
        final SessionData data = new MapBackedSessionData();
        final Session apiSession = mock(Session.class);
        when(apiSession.getData()).thenReturn(data);

        final Path f = svc.createTempFile(apiSession, "participant-", ".tmp");
        assertTrue(Files.exists(f), "temp file must exist before cleanup");

        final MavenSession mavenSession = mock(MavenSession.class);
        when(mavenSession.getSession()).thenReturn(apiSession);

        final TempFileCleanupParticipant participant = new TempFileCleanupParticipant(svc);
        participant.afterSessionEnd(mavenSession);

        verify(mavenSession).getSession();
        assertFalse(Files.exists(f), "temp file must be deleted after participant runs");
    }

    @Test
    void cleanupParticipantSwallowsExceptions() {
        final TempFileService failingSvc = mock(TempFileService.class);
        try {
            org.mockito.Mockito.doThrow(new IOException("simulated failure"))
                    .when(failingSvc)
                    .cleanup(org.mockito.ArgumentMatchers.any(Session.class));
        } catch (final IOException e) {
            throw new RuntimeException(e);
        }

        final Session apiSession = mock(Session.class);
        final MavenSession mavenSession = mock(MavenSession.class);
        when(mavenSession.getSession()).thenReturn(apiSession);

        final TempFileCleanupParticipant participant = new TempFileCleanupParticipant(failingSvc);
        // Must not throw ��� lifecycle callbacks should not propagate exceptions
        participant.afterSessionEnd(mavenSession);
    }

    /**
     * Minimal, thread-safe SessionData backed by a ConcurrentHashMap.
     * Keeps generics safe at the call sites of DefaultTempFileService.
     */
    static final class MapBackedSessionData implements SessionData {
        private final ConcurrentHashMap<Key<?>, Object> map = new ConcurrentHashMap<>();

        @Override
        public <T> void set(final Key<T> key, final T value) {
            Objects.requireNonNull(key, "key");
            if (value == null) {
                map.remove(key);
            } else {
                map.put(key, value);
            }
        }

        @Override
        public <T> boolean replace(final Key<T> key, final T oldValue, final T newValue) {
            Objects.requireNonNull(key, "key");
            if (newValue == null) {
                return map.remove(key, oldValue);
            }
            return map.replace(key, oldValue, newValue);
        }

        @SuppressWarnings("unchecked")
        @Override
        public <T> T get(final Key<T> key) {
            Objects.requireNonNull(key, "key");
            return (T) map.get(key);
        }

        @SuppressWarnings("unchecked")
        @Override
        public <T> T computeIfAbsent(final Key<T> key, final Supplier<T> supplier) {
            Objects.requireNonNull(key, "key");
            Objects.requireNonNull(supplier, "supplier");
            return (T) map.computeIfAbsent(key, k -> supplier.get());
        }
    }
}