/*************************GO-LICENSE-START*********************************
* Copyright 2014 ThoughtWorks, Inc.
*
* Licensed 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.
*************************GO-LICENSE-END***********************************/
package com.thoughtworks.go.server.transaction;
import java.io.IOException;
import com.thoughtworks.go.util.ReflectionUtil;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNull.nullValue;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
"classpath:WEB-INF/applicationContext-global.xml",
"classpath:WEB-INF/applicationContext-dataLocalAccess.xml",
"classpath:WEB-INF/applicationContext-acegi-security.xml"
})
public class TransactionTemplateTest {
@Autowired private TransactionTemplate goTransactionTemplate;
@Autowired private TransactionSynchronizationManager transactionSynchronizationManager;
private boolean txnCommited;
private boolean txnCompleted;
private org.springframework.transaction.support.TransactionTemplate transactionTemplate;//set in setup
@Before
public void setUp() {
txnCommited = false;
txnCompleted = false;
transactionTemplate = (org.springframework.transaction.support.TransactionTemplate) ReflectionUtil.getField(goTransactionTemplate, "transactionTemplate");
}
@Test
public void shouldBubbleRuntimeExceptionToActualTransactionTemplateForCallback() throws Exception {
TransactionTemplate template = new TransactionTemplate(transactionTemplate);
try {
template.executeWithExceptionHandling(new TransactionCallback() {
@Override public Object doInTransaction(TransactionStatus status) throws Exception {
registerSynchronization();
throw new Exception("foo");
}
});
fail("should have thrown exception");
} catch (Exception e) {
assertThat(e.getMessage(), is("foo"));
}
assertThat(txnCommited, is(false));
assertThat(txnCompleted, is(true));
}
@Test
public void shouldBubbleRuntimeExceptionToActualTransactionTemplateForCallbackWithoutReturnValue() throws Exception {
TransactionTemplate template = new TransactionTemplate(transactionTemplate);
try {
template.executeWithExceptionHandling(new TransactionCallbackWithoutResult() {
@Override public void doInTransactionWithoutResult(TransactionStatus status) throws Exception {
registerSynchronization();
throw new Exception("foo");
}
});
fail("should have thrown exception");
} catch (Exception e) {
assertThat(e.getMessage(), is("foo"));
}
assertThat(txnCommited, is(false));
assertThat(txnCompleted, is(true));
}
@Test
public void shouldReturnValueReturnedByCallback() throws Exception {
TransactionTemplate template = new TransactionTemplate(transactionTemplate);
String returnVal = (String) template.executeWithExceptionHandling(new TransactionCallback() {
@Override public Object doInTransaction(TransactionStatus status) throws Exception {
registerSynchronization();
return "foo";
}
});
assertThat(txnCommited, is(true));
assertThat(txnCompleted, is(true));
assertThat(returnVal, is("foo"));
}
@Test
public void shouldReturnNullForCallbackWithoutReturn() throws Exception {
TransactionTemplate template = new TransactionTemplate(transactionTemplate);
Object returnVal = template.executeWithExceptionHandling(new TransactionCallbackWithoutResult() {
@Override public void doInTransactionWithoutResult(TransactionStatus status) throws Exception {
registerSynchronization();
}
});
assertThat(txnCommited, is(true));
assertThat(txnCompleted, is(true));
assertThat(returnVal, nullValue());
}
@Test
public void shouldExecuteTransactionCallback() throws Exception {
TransactionTemplate template = new TransactionTemplate(transactionTemplate);
String returnVal = (String) template.execute(new org.springframework.transaction.support.TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
registerSynchronization();
return "foo";
}
});
assertThat(txnCommited, is(true));
assertThat(txnCompleted, is(true));
assertThat(returnVal, is("foo"));
}
@Test
public void shouldUnderstand_InTransaction() {
TransactionTemplate template = new TransactionTemplate(transactionTemplate);
final boolean[] inTransactionInBody = new boolean[] {false};
final boolean[] inTransactionInAfterCommit = new boolean[] {true};
final boolean[] inTransactionInAfterComplete = new boolean[] {true};
String returnVal = (String) template.execute(new org.springframework.transaction.support.TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
setTxnBodyActiveFlag(inTransactionInBody, inTransactionInAfterCommit, inTransactionInAfterComplete, 0);
return "foo";
}
});
assertThat(inTransactionInBody[0], is(true));
assertThat(inTransactionInAfterCommit[0], is(false));
assertThat(inTransactionInAfterComplete[0], is(false));
assertThat(returnVal, is("foo"));
}
@Test
public void shouldAllowRegistrationOfTransactionSynchronization_inTransactionSurroundingBlock_andExecuteAppropriateHooks() {
final TransactionTemplate template = new TransactionTemplate(transactionTemplate);
final boolean[] afterCommitHappened = new boolean[1];
final boolean[] transactionWasActiveInSurrounding = new boolean[1];
final boolean[] transactionWasActiveInTransaction = new boolean[1];
String returnVal = (String) template.transactionSurrounding(new TransactionTemplate.TransactionSurrounding<RuntimeException>() {
public Object surrounding() {
transactionWasActiveInSurrounding[0] = transactionSynchronizationManager.isTransactionBodyExecuting();
transactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override public void afterCommit() {
afterCommitHappened[0] = true;
}
});
return template.execute(new org.springframework.transaction.support.TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
transactionWasActiveInTransaction[0] = transactionSynchronizationManager.isTransactionBodyExecuting();
return "foo";
}
});
}
});
assertThat(returnVal, is("foo"));
assertThat(afterCommitHappened[0], is(true));
assertThat(transactionWasActiveInSurrounding[0], is(false));
assertThat(transactionWasActiveInTransaction[0], is(true));
}
@Test
public void shouldAllowRegistrationOfTransactionSynchronization_inTransactionSurroundingBlock_andNotExecuteSynchronizationIfTransactionNeverHappens() {
TransactionTemplate template = new TransactionTemplate(transactionTemplate);
final boolean[] afterCommitHappened = new boolean[1];
String returnVal = (String) template.transactionSurrounding(new TransactionTemplate.TransactionSurrounding<RuntimeException>() {
public Object surrounding() {
transactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override public void afterCommit() {
afterCommitHappened[0] = true;
}
});
return "bar";
}
});
assertThat(returnVal, is("bar"));
assertThat(afterCommitHappened[0], is(false));
}
@Test
public void should_NOT_useSynchronizationsFromOneSurroundingBlockInAnother() {
final TransactionTemplate template = new TransactionTemplate(transactionTemplate);
final boolean[] afterCommitHappened = new boolean[1];
String returnVal = (String) template.transactionSurrounding(new TransactionTemplate.TransactionSurrounding<RuntimeException>() {
public Object surrounding() {
transactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override public void afterCommit() {
afterCommitHappened[0] = true;
}
});
return "foo";
}
});
assertThat(returnVal, is("foo"));
assertThat(afterCommitHappened[0], is(false));//because no transaction happened
returnVal = (String) template.transactionSurrounding(new TransactionTemplate.TransactionSurrounding<RuntimeException>() {
public Object surrounding() {
return template.execute(new org.springframework.transaction.support.TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
return "bar";
}
});
}
});
assertThat(returnVal, is("bar"));
assertThat(afterCommitHappened[0], is(false));//because it registered no synchronization
}
@Test
public void shouldPropagateExceptionsOutOfTransactionSurrounding() throws IOException {
TransactionTemplate template = new TransactionTemplate(transactionTemplate);
final boolean[] afterCommitHappened = new boolean[1];
String returnVal = null;
try {
returnVal = (String) template.transactionSurrounding(new TransactionTemplate.TransactionSurrounding<IOException>() {
public Object surrounding() throws IOException {
transactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override public void afterCommit() {
afterCommitHappened[0] = true;
}
});
throw new IOException("boo ha!");
}
});
fail("should have propagated exception");
} catch (IOException e) {
assertThat(e.getMessage(), is("boo ha!"));
}
assertThat(returnVal, nullValue());
assertThat(afterCommitHappened[0], is(false));
}
@Test
public void shouldNotRegisterSynchronizationMultipleTimes() {
final TransactionTemplate template = new TransactionTemplate(transactionTemplate);
final int[] numberOfTimesAfterCommitHappened = new int[1];
numberOfTimesAfterCommitHappened[0] = 0;
String returnVal = (String) template.transactionSurrounding(new TransactionTemplate.TransactionSurrounding<RuntimeException>() {
public Object surrounding() {
transactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override public void afterCommit() {
numberOfTimesAfterCommitHappened[0]++;
}
});
return template.execute(new org.springframework.transaction.support.TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
return goTransactionTemplate.execute(new org.springframework.transaction.support.TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
return "foo";
}
});
}
});
}
});
assertThat(returnVal, is("foo"));
assertThat(numberOfTimesAfterCommitHappened[0], is(1));
}
@Test
public void should_NOT_AllowMoreThanOneTransactionInsideSurrounding() {
final TransactionTemplate template = new TransactionTemplate(transactionTemplate);
String returnVal = null;
try {
returnVal = (String) template.transactionSurrounding(new TransactionTemplate.TransactionSurrounding<RuntimeException>() {
public Object surrounding() {
template.execute(new org.springframework.transaction.support.TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
return "foo";
}
});
return template.execute(new org.springframework.transaction.support.TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
return "bar";
}
});
}
});
fail("should not have allowed multiple top-level transactions");//this can cause assumptions of registered-synchronization to become invalid -jj
} catch (RuntimeException e) {
assertThat(e.getMessage(), is("Multiple independent transactions are not permitted inside single transaction surrounding."));
}
assertThat(returnVal, nullValue());
}
@Test
public void shouldAllowMoreThanOneTransactionInsideSurrounding_ifSurroundingIsInsideTransactionAlready() {
final TransactionTemplate template = new TransactionTemplate(transactionTemplate);
final boolean[] firstNestedTransactionHappened = new boolean[1];
final boolean[] secondNestedTransactionHappened = new boolean[1];
final boolean[] firstNestedTransactionCalledTransactionSynchronization = new boolean[1];
final boolean[] secondNestedTransactionCalledTransactionSynchronization = new boolean[1];
final int[] numberOfTimesSynchronizationWasCalled = new int[1];
numberOfTimesSynchronizationWasCalled[0] = 0;
String returnVal = (String) template.execute(new org.springframework.transaction.support.TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
return template.transactionSurrounding(new TransactionTemplate.TransactionSurrounding<RuntimeException>() {
public Object surrounding() {
transactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override public void afterCommit() {
numberOfTimesSynchronizationWasCalled[0]++;
}
});
template.execute(new org.springframework.transaction.support.TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
firstNestedTransactionHappened[0] = true;
return "foo";
}
});
firstNestedTransactionCalledTransactionSynchronization[0] = numberOfTimesSynchronizationWasCalled[0] > 0;
Object ret = template.execute(new org.springframework.transaction.support.TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
secondNestedTransactionHappened[0] = true;
return "bar";
}
});
secondNestedTransactionCalledTransactionSynchronization[0] = numberOfTimesSynchronizationWasCalled[0] > 0;
return ret;
}
});
}
});
assertThat(returnVal, is("bar"));
assertThat(numberOfTimesSynchronizationWasCalled[0], is(1));
assertThat(firstNestedTransactionHappened[0], is(true));
assertThat(firstNestedTransactionCalledTransactionSynchronization[0], is(false));
assertThat(secondNestedTransactionHappened[0], is(true));
assertThat(secondNestedTransactionCalledTransactionSynchronization[0], is(false));
}
@Test
public void shouldUnderstand_InTransaction_AcrossNestedInvocations() {
final TransactionTemplate template = new TransactionTemplate(transactionTemplate);
final boolean[] inTransactionInBody = new boolean[] {false, false, false, false};
final boolean[] inTransactionInAfterCommit = new boolean[] {true, true, true, true};
final boolean[] inTransactionInAfterComplete = new boolean[] {true, true, true, true};
String returnVal = (String) template.execute(new org.springframework.transaction.support.TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
setTxnBodyActiveFlag(inTransactionInBody, inTransactionInAfterCommit, inTransactionInAfterComplete, 0);
return template.execute(new org.springframework.transaction.support.TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
setTxnBodyActiveFlag(inTransactionInBody, inTransactionInAfterCommit, inTransactionInAfterComplete, 1);
try {
return template.executeWithExceptionHandling(new TransactionCallback() {
@Override public Object doInTransaction(TransactionStatus status) throws Exception {
setTxnBodyActiveFlag(inTransactionInBody, inTransactionInAfterCommit, inTransactionInAfterComplete, 2);
return template.execute(new org.springframework.transaction.support.TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
setTxnBodyActiveFlag(inTransactionInBody, inTransactionInAfterCommit, inTransactionInAfterComplete, 3);
return "baz";
}
});
}
});
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
}
});
for (int i = 0; i < 4; i++) {
assertThat(inTransactionInBody[i], is(true));
assertThat(inTransactionInAfterCommit[i], is(false));
assertThat(inTransactionInAfterComplete[i], is(false));
}
assertThat(returnVal, is("baz"));
}
private void setTxnBodyActiveFlag(final boolean[] inTransactionInBody, final boolean[] inTransactionInAfterCommit, final boolean[] inTransactionInAfterComplete, final int depth) {
inTransactionInBody[depth] = transactionSynchronizationManager.isTransactionBodyExecuting();
transactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override public void afterCommit() {
inTransactionInAfterCommit[depth] = transactionSynchronizationManager.isTransactionBodyExecuting();
}
@Override public void afterCompletion(int status) {
inTransactionInAfterComplete[depth] = transactionSynchronizationManager.isTransactionBodyExecuting();
}
});
}
@Test
public void shouldUnderstand_InTransaction_ForTransactionWithExceptionHandling() throws Exception {
TransactionTemplate template = new TransactionTemplate(transactionTemplate);
final boolean[] inTransactionInBody = new boolean[] {false};
final boolean[] inTransactionInAfterCommit = new boolean[] {true};
final boolean[] inTransactionInAfterComplete = new boolean[] {true};
String returnVal = (String) template.executeWithExceptionHandling(new TransactionCallback() {
@Override public Object doInTransaction(TransactionStatus status) throws Exception {
setTxnBodyActiveFlag(inTransactionInBody, inTransactionInAfterCommit, inTransactionInAfterComplete, 0);
return "foo";
}
});
assertThat(inTransactionInBody[0], is(true));
assertThat(inTransactionInAfterCommit[0], is(false));
assertThat(inTransactionInAfterComplete[0], is(false));
assertThat(returnVal, is("foo"));
}
@Test
public void shouldNotFailWhenNoTransactionStarted() throws InterruptedException {
final boolean[] transactionBodyIn = new boolean[] {true};
Thread thd = new Thread(new Runnable() {
public void run() {
transactionBodyIn[0] = goTransactionTemplate.isTransactionBodyExecuting();
}
});
thd.start();
thd.join();
assertThat(transactionBodyIn[0], is(false));
}
private void registerSynchronization() {
transactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override public void afterCommit() {
txnCommited = true;
}
@Override public void afterCompletion(int status) {
txnCompleted = true;
}
});
}
}