org.sonar.db.DBSessionsImplTest.java Source code

Java tutorial

Introduction

Here is the source code for org.sonar.db.DBSessionsImplTest.java

Source

/*
 * SonarQube
 * Copyright (C) 2009-2018 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package org.sonar.db;

import com.google.common.base.Throwables;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mockito;
import org.sonar.api.utils.log.LogTester;
import org.sonar.api.utils.log.LoggerLevel;
import org.sonar.core.util.stream.MoreCollectors;

import static java.lang.Math.abs;
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

public class DBSessionsImplTest {
    @Rule
    public LogTester logTester = new LogTester();

    private final MyBatis myBatis = mock(MyBatis.class);
    private final DbSession myBatisDbSession = mock(DbSession.class);
    private final Random random = new Random();
    private final DBSessionsImpl underTest = new DBSessionsImpl(myBatis);

    @After
    public void tearDown() {
        underTest.disableCaching();
    }

    @Test
    public void openSession_without_caching_always_returns_a_new_regular_session_when_parameter_is_false() {
        DbSession[] expected = { mock(DbSession.class), mock(DbSession.class), mock(DbSession.class),
                mock(DbSession.class) };
        when(myBatis.openSession(false)).thenReturn(expected[0]).thenReturn(expected[1]).thenReturn(expected[2])
                .thenReturn(expected[3]).thenThrow(oneCallTooMuch());

        assertThat(Arrays.stream(expected).map(ignored -> underTest.openSession(false))
                .collect(MoreCollectors.toList())).containsExactly(expected);
    }

    @Test
    public void openSession_without_caching_always_returns_a_new_batch_session_when_parameter_is_true() {
        DbSession[] expected = { mock(DbSession.class), mock(DbSession.class), mock(DbSession.class),
                mock(DbSession.class) };
        when(myBatis.openSession(true)).thenReturn(expected[0]).thenReturn(expected[1]).thenReturn(expected[2])
                .thenReturn(expected[3]).thenThrow(oneCallTooMuch());

        assertThat(Arrays.stream(expected).map(ignored -> underTest.openSession(true))
                .collect(MoreCollectors.toList())).containsExactly(expected);
    }

    @Test
    public void openSession_with_caching_always_returns_the_same_regular_session_when_parameter_is_false() {
        DbSession expected = mock(DbSession.class);
        when(myBatis.openSession(false)).thenReturn(expected).thenThrow(oneCallTooMuch());
        underTest.enableCaching();

        int size = 1 + abs(random.nextInt(10));
        Set<DbSession> dbSessions = IntStream.range(0, size).mapToObj(ignored -> underTest.openSession(false))
                .collect(MoreCollectors.toSet());
        assertThat(dbSessions).hasSize(size);
        assertThat(getWrappedDbSessions(dbSessions)).hasSize(1).containsOnly(expected);
    }

    @Test
    public void openSession_with_caching_always_returns_the_same_batch_session_when_parameter_is_true() {
        DbSession expected = mock(DbSession.class);
        when(myBatis.openSession(true)).thenReturn(expected).thenThrow(oneCallTooMuch());
        underTest.enableCaching();

        int size = 1 + abs(random.nextInt(10));
        Set<DbSession> dbSessions = IntStream.range(0, size).mapToObj(ignored -> underTest.openSession(true))
                .collect(MoreCollectors.toSet());
        assertThat(dbSessions).hasSize(size);
        assertThat(getWrappedDbSessions(dbSessions)).hasSize(1).containsOnly(expected);
    }

    @Test
    public void openSession_with_caching_returns_a_session_per_thread() {
        boolean batchOrRegular = random.nextBoolean();
        DbSession[] expected = { mock(DbSession.class), mock(DbSession.class), mock(DbSession.class),
                mock(DbSession.class) };
        when(myBatis.openSession(batchOrRegular)).thenReturn(expected[0]).thenReturn(expected[1])
                .thenReturn(expected[2]).thenReturn(expected[3]).thenThrow(oneCallTooMuch());
        List<DbSession> collector = new ArrayList<>();
        Runnable runnable = () -> {
            underTest.enableCaching();
            collector.add(underTest.openSession(batchOrRegular));
            underTest.disableCaching();
        };
        Thread[] threads = { new Thread(runnable, "T1"), new Thread(runnable, "T2"), new Thread(runnable, "T3") };

        executeThreadsAndCurrent(runnable, threads);

        // verify each DbSession was closed and then reset mock for next verification
        Arrays.stream(expected).forEach(s -> {
            verify(s).close();
            reset(s);
        });
        // verify whether each thread got the expected DbSession from MyBatis
        // by verifying to which each returned DbSession delegates to
        DbSession[] dbSessions = collector.toArray(new DbSession[0]);
        for (int i = 0; i < expected.length; i++) {
            dbSessions[i].rollback();
            verify(expected[i]).rollback();

            List<DbSession> sub = new ArrayList<>(Arrays.asList(expected));
            sub.remove(expected[i]);
            sub.forEach(Mockito::verifyNoMoreInteractions);
            reset(expected);
        }
    }

    private static void executeThreadsAndCurrent(Runnable runnable, Thread[] threads) {
        Arrays.stream(threads).forEach(thread -> {
            thread.start();
            try {
                thread.join();
            } catch (InterruptedException e) {
                Throwables.propagate(e);
            }
        });
        runnable.run();
    }

    @Test
    public void openSession_with_caching_returns_wrapper_of_MyBatis_DbSession_which_delegates_all_methods_but_close() {
        boolean batchOrRegular = random.nextBoolean();

        underTest.enableCaching();

        verifyFirstDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            dbSession.rollback();
            verify(myBatisDbSession).rollback();
        });
        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            boolean flag = random.nextBoolean();
            dbSession.rollback(flag);
            verify(myBatisDbSession).rollback(flag);
        });

        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            dbSession.commit();
            verify(myBatisDbSession).commit();
        });
        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            boolean flag = random.nextBoolean();
            dbSession.commit(flag);
            verify(myBatisDbSession).commit(flag);
        });

        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            String str = randomAlphabetic(10);
            dbSession.selectOne(str);
            verify(myBatisDbSession).selectOne(str);
        });
        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            String str = randomAlphabetic(10);
            Object object = new Object();
            dbSession.selectOne(str, object);
            verify(myBatisDbSession).selectOne(str, object);
        });

        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            String str = randomAlphabetic(10);
            dbSession.selectList(str);
            verify(myBatisDbSession).selectList(str);
        });
        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            String str = randomAlphabetic(10);
            Object object = new Object();
            dbSession.selectList(str, object);
            verify(myBatisDbSession).selectList(str, object);
        });
        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            String str = randomAlphabetic(10);
            Object parameter = new Object();
            RowBounds rowBounds = new RowBounds();
            dbSession.selectList(str, parameter, rowBounds);
            verify(myBatisDbSession).selectList(str, parameter, rowBounds);
        });

        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            String str = randomAlphabetic(10);
            String mapKey = randomAlphabetic(10);
            dbSession.selectMap(str, mapKey);
            verify(myBatisDbSession).selectMap(str, mapKey);
        });
        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            String str = randomAlphabetic(10);
            Object parameter = new Object();
            String mapKey = randomAlphabetic(10);
            dbSession.selectMap(str, parameter, mapKey);
            verify(myBatisDbSession).selectMap(str, parameter, mapKey);
        });
        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            String str = randomAlphabetic(10);
            Object parameter = new Object();
            String mapKey = randomAlphabetic(10);
            RowBounds rowBounds = new RowBounds();
            dbSession.selectMap(str, parameter, mapKey, rowBounds);
            verify(myBatisDbSession).selectMap(str, parameter, mapKey, rowBounds);
        });

        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            String str = randomAlphabetic(10);
            ResultHandler handler = mock(ResultHandler.class);
            dbSession.select(str, handler);
            verify(myBatisDbSession).select(str, handler);
        });
        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            String str = randomAlphabetic(10);
            Object parameter = new Object();
            ResultHandler handler = mock(ResultHandler.class);
            dbSession.select(str, parameter, handler);
            verify(myBatisDbSession).select(str, parameter, handler);
        });
        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            String str = randomAlphabetic(10);
            Object parameter = new Object();
            ResultHandler handler = mock(ResultHandler.class);
            RowBounds rowBounds = new RowBounds();
            dbSession.select(str, parameter, rowBounds, handler);
            verify(myBatisDbSession).select(str, parameter, rowBounds, handler);
        });

        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            String str = randomAlphabetic(10);
            dbSession.insert(str);
            verify(myBatisDbSession).insert(str);
        });
        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            String str = randomAlphabetic(10);
            Object object = new Object();
            dbSession.insert(str, object);
            verify(myBatisDbSession).insert(str, object);
        });

        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            String str = randomAlphabetic(10);
            dbSession.update(str);
            verify(myBatisDbSession).update(str);
        });
        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            String str = randomAlphabetic(10);
            Object object = new Object();
            dbSession.update(str, object);
            verify(myBatisDbSession).update(str, object);
        });

        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            String str = randomAlphabetic(10);
            dbSession.delete(str);
            verify(myBatisDbSession).delete(str);
        });
        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            String str = randomAlphabetic(10);
            Object object = new Object();
            dbSession.delete(str, object);
            verify(myBatisDbSession).delete(str, object);
        });

        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            dbSession.flushStatements();
            verify(myBatisDbSession).flushStatements();
        });

        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            dbSession.clearCache();
            verify(myBatisDbSession).clearCache();
        });

        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            Configuration expected = mock(Configuration.class);
            when(myBatisDbSession.getConfiguration()).thenReturn(expected);
            assertThat(dbSession.getConfiguration()).isSameAs(expected);
            verify(myBatisDbSession).getConfiguration();
        });

        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            Class<Object> clazz = Object.class;
            Object expected = new Object();
            when(myBatisDbSession.getMapper(clazz)).thenReturn(expected);
            assertThat(dbSession.getMapper(clazz)).isSameAs(expected);
            verify(myBatisDbSession).getMapper(clazz);
        });

        verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> {
            Connection connection = mock(Connection.class);
            when(myBatisDbSession.getConnection()).thenReturn(connection);
            assertThat(dbSession.getConnection()).isSameAs(connection);
            verify(myBatisDbSession).getConnection();
        });
    }

    @Test
    public void openSession_with_caching_returns_DbSession_that_rolls_back_on_close_if_any_mutation_call_was_not_followed_by_commit_nor_rollback()
            throws SQLException {
        DbSession dbSession = openSessionAndDoSeveralMutatingAndNeutralCalls();

        dbSession.close();

        verify(myBatisDbSession).rollback();
    }

    @Test
    public void openSession_with_caching_returns_DbSession_that_does_not_roll_back_on_close_if_any_mutation_call_was_followed_by_commit()
            throws SQLException {
        DbSession dbSession = openSessionAndDoSeveralMutatingAndNeutralCalls();
        COMMIT_CALLS[random.nextBoolean() ? 0 : 1].consume(dbSession);

        dbSession.close();

        verify(myBatisDbSession, times(0)).rollback();
    }

    @Test
    public void openSession_with_caching_returns_DbSession_that_does_not_roll_back_on_close_if_any_mutation_call_was_followed_by_rollback_without_parameters()
            throws SQLException {
        DbSession dbSession = openSessionAndDoSeveralMutatingAndNeutralCalls();
        dbSession.rollback();

        dbSession.close();

        verify(myBatisDbSession, times(1)).rollback();
    }

    @Test
    public void openSession_with_caching_returns_DbSession_that_does_not_roll_back_on_close_if_any_mutation_call_was_followed_by_rollback_with_parameters()
            throws SQLException {
        boolean force = random.nextBoolean();
        DbSession dbSession = openSessionAndDoSeveralMutatingAndNeutralCalls();
        dbSession.rollback(force);

        dbSession.close();

        verify(myBatisDbSession, times(1)).rollback(force);
        verify(myBatisDbSession, times(0)).rollback();
    }

    private DbSession openSessionAndDoSeveralMutatingAndNeutralCalls() throws SQLException {
        boolean batchOrRegular = random.nextBoolean();
        when(myBatis.openSession(batchOrRegular)).thenReturn(myBatisDbSession).thenThrow(oneCallTooMuch());
        underTest.enableCaching();
        DbSession dbSession = underTest.openSession(batchOrRegular);

        int dirtyingCallsCount = 1 + abs(random.nextInt(5));
        int neutralCallsCount = abs(random.nextInt(5));
        int[] dirtyCallIndices = IntStream.range(0, dirtyingCallsCount)
                .map(ignored -> abs(random.nextInt(DIRTYING_CALLS.length))).toArray();
        int[] neutralCallsIndices = IntStream.range(0, neutralCallsCount)
                .map(ignored -> abs(random.nextInt(NEUTRAL_CALLS.length))).toArray();
        for (int index : dirtyCallIndices) {
            DIRTYING_CALLS[index].consume(dbSession);
        }
        for (int index : neutralCallsIndices) {
            NEUTRAL_CALLS[index].consume(dbSession);
        }
        return dbSession;
    }

    private static DbSessionCaller[] DIRTYING_CALLS = { session -> session.insert(randomAlphabetic(3)),
            session -> session.insert(randomAlphabetic(2), new Object()),
            session -> session.update(randomAlphabetic(3)),
            session -> session.update(randomAlphabetic(3), new Object()),
            session -> session.delete(randomAlphabetic(3)),
            session -> session.delete(randomAlphabetic(3), new Object()), };

    private static DbSessionCaller[] COMMIT_CALLS = { session -> session.commit(),
            session -> session.commit(new Random().nextBoolean()), };

    private static DbSessionCaller[] ROLLBACK_CALLS = { session -> session.rollback(),
            session -> session.rollback(new Random().nextBoolean()), };

    private static DbSessionCaller[] NEUTRAL_CALLS = { session -> session.selectOne(randomAlphabetic(3)),
            session -> session.selectOne(randomAlphabetic(3), new Object()),
            session -> session.select(randomAlphabetic(3), mock(ResultHandler.class)),
            session -> session.select(randomAlphabetic(3), new Object(), mock(ResultHandler.class)),
            session -> session.select(randomAlphabetic(3), new Object(), new RowBounds(),
                    mock(ResultHandler.class)),
            session -> session.selectList(randomAlphabetic(3)),
            session -> session.selectList(randomAlphabetic(3), new Object()),
            session -> session.selectList(randomAlphabetic(3), new Object(), new RowBounds()),
            session -> session.selectMap(randomAlphabetic(3), randomAlphabetic(3)),
            session -> session.selectMap(randomAlphabetic(3), new Object(), randomAlphabetic(3)),
            session -> session.selectMap(randomAlphabetic(3), new Object(), randomAlphabetic(3), new RowBounds()),
            session -> session.getMapper(Object.class), session -> session.getConfiguration(),
            session -> session.getConnection(), session -> session.clearCache(),
            session -> session.flushStatements() };

    private interface DbSessionCaller {
        void consume(DbSession t) throws SQLException;
    }

    @Test
    public void disableCaching_does_not_open_DB_connection_if_openSession_was_never_called() {
        when(myBatis.openSession(anyBoolean())).thenThrow(oneCallTooMuch());
        underTest.enableCaching();

        underTest.disableCaching();

        verifyNoMoreInteractions(myBatis);
    }

    @Test
    public void disableCaching_has_no_effect_if_enabledCaching_has_not_been_called() {
        underTest.disableCaching();

        verifyNoMoreInteractions(myBatis);
    }

    @Test
    public void disableCaching_does_not_fail_but_logs_if_closing_MyBatis_session_close_throws_an_exception() {
        boolean batchOrRegular = random.nextBoolean();
        IllegalStateException toBeThrown = new IllegalStateException("Faking MyBatisSession#close failing");

        when(myBatis.openSession(batchOrRegular)).thenReturn(myBatisDbSession).thenThrow(oneCallTooMuch());
        Mockito.doThrow(toBeThrown).when(myBatisDbSession).close();
        underTest.enableCaching();
        underTest.openSession(batchOrRegular);

        underTest.disableCaching();

        List<String> errorLogs = logTester.logs(LoggerLevel.ERROR);
        assertThat(errorLogs).hasSize(1).containsOnly("Failed to close " + (batchOrRegular ? "batch" : "regular")
                + " connection in " + Thread.currentThread());
    }

    private void verifyFirstDelegation(boolean batchOrRegular, BiConsumer<DbSession, DbSession> r) {
        verifyDelegation(batchOrRegular, true, r);
    }

    private void verifyDelegation(boolean batchOrRegular, BiConsumer<DbSession, DbSession> r) {
        verifyDelegation(batchOrRegular, false, r);
    }

    private void verifyDelegation(boolean batchOrRegular, boolean firstCall, BiConsumer<DbSession, DbSession> r) {
        when(myBatis.openSession(batchOrRegular)).thenReturn(myBatisDbSession).thenThrow(oneCallTooMuch());
        r.accept(myBatisDbSession, underTest.openSession(batchOrRegular));
        verify(myBatisDbSession, times(firstCall ? 1 : 0)).getSqlSession();
        verifyNoMoreInteractions(myBatisDbSession);
        reset(myBatis, myBatisDbSession);
    }

    private static IllegalStateException oneCallTooMuch() {
        return new IllegalStateException("one call too much");
    }

    private Set<DbSession> getWrappedDbSessions(Set<DbSession> dbSessions) {
        return dbSessions.stream().filter(NonClosingDbSession.class::isInstance)
                .map(NonClosingDbSession.class::cast).map(NonClosingDbSession::getDelegate)
                .collect(Collectors.toSet());
    }
}