Java tutorial
/********************************************************************************** * $URL$ * $Id$ *********************************************************************************** * * Copyright (c) 2005, 2006 The Sakai Foundation. * * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.tool.impl; import org.apache.commons.lang.mutable.MutableLong; import org.jmock.Expectations; import org.sakaiproject.tool.api.Session; import org.sakaiproject.tool.api.ToolSession; import java.lang.reflect.Field; import java.util.concurrent.*; /** * Mostly black-box {@link SessionComponent} unit tests intended to guard against * regressions. Was specifically motivated by an effort to re-implement * session attribute storage against a distributable cache. Class * name is unfortunate, but is necessitated by the existence of * {@link SessionComponentTest} in the "main" src dir. * * @author dmccallum@unicon.net */ public class SessionComponentRegressionTest extends BaseSessionComponentTest { public void testGetSessionReturnsNullIfNoSuchSession() { assertNull(sessionComponent.getSession("COMPLETE_NONSENSE")); } /** * Ensures {@link Session} has entity semantics, i.e. the same * object is returned to each request for that object. "Always" * here is limited to non-expired sessions. * * @throws TimeoutException * @throws ExecutionException * @throws InterruptedException */ public void testGetSessionAlwaysReturnsSessionCreatedByStartSession() throws InterruptedException, ExecutionException, TimeoutException { final Session startedSession = startSessionForUser(); assertSame(startedSession, sessionComponent.getSession(startedSession.getId())); assertSame(startedSession, sessionComponent.getSession(startedSession.getId())); // intentional duplicate // all threads should get the same Session obj for a given key FutureTask<Session> asynchGet = new FutureTask<Session>(new Callable<Session>() { public Session call() { return sessionComponent.getSession(startedSession.getId()); } }); new Thread(asynchGet).start(); assertSame(startedSession, asynchGet.get(1, TimeUnit.SECONDS)); } /** * Identical to {@link #testGetSessionReturnsSessionCreatedByStartSession()} * except that it tests the overload of <code>startSession()</code> * ({@link SessionComponent#startSession(String)}. * * @throws TimeoutException * @throws ExecutionException * @throws InterruptedException */ public void testGetSessionAlwaysReturnsSessionStartedWithClientSpecifiedId() throws InterruptedException, ExecutionException, TimeoutException { final Session startedSession = sessionComponent.startSession("9876543210"); assertSame(startedSession, sessionComponent.getSession(startedSession.getId())); assertSame(startedSession, sessionComponent.getSession(startedSession.getId())); // intentional duplicate // all threads should get the same Session obj for a given key FutureTask<Session> asynchGet = new FutureTask<Session>(new Callable<Session>() { public Session call() { return sessionComponent.getSession(startedSession.getId()); } }); new Thread(asynchGet).start(); assertSame(startedSession, asynchGet.get(1, TimeUnit.SECONDS)); } /** * Verifies the end-result of session invalidation triggered by * <code>SessionComponent</code>'s internal maintenance thread. * * @see #testGetSessionReturnsNullIfSessionExplicitlyInvalidated() * @throws InterruptedException * @throws IllegalAccessException * @throws NoSuchFieldException * @throws IllegalArgumentException * @throws SecurityException */ public void testGetSessionReturnsNullIfSessionExpired() throws InterruptedException, SecurityException, IllegalArgumentException, NoSuchFieldException, IllegalAccessException { resetMaintenance("1", "1"); Session session = startSessionAndExpectItsExpiration(); awaitExpirationOrFail(session, 1); String id = session.getId(); assertNull(sessionComponent.getSession(id)); } /** * An integration test which more explicitly verifies the invalidation * callbacks from the {@link Session}s created by this <code>SessionComponent</code>. * Compare to {@link #testGetSessionReturnsNullIfSessionExpired()} which * tests the same <code>SessionComponent</code>-internal cleanup code, * but relies on its internal maintenance thread to trigger the session * invalidation. */ public void testGetSessionReturnsNullIfSessionExplicitlyInvalidated() { stopMaintenance(); // ensure that it's actually the test code triggering invalidation Session session = startSessionAndExpectItsExpiration(); String id = session.getId(); session.invalidate(); assertNull(sessionComponent.getSession(id)); } protected Session startSessionAndExpectItsExpiration() { final Session startedSession = startSessionForUser(); // Session invalidation involves de-selecting itself as "current". We // make the session appear "current" to avoid lazy alloc of another session. expectGetAndUnsetCurrentSession(startedSession); return startedSession; } /** * Verifies that the lazily-instantiated session is not stored in * the "global" session lookup table, and is therefore not findable by ID. See * {@link #testGetCurrentSessionCachesThreadScopedSession()} for verification * that the session is subsequently available to the current thread by other * means, though. */ public void testGetCurrentSessionLazilyCreatesTransientSession() { expectLazyCurrentSessionCreation(); Session session = sessionComponent.getCurrentSession(); assertNotNull("Should have allocated a new session", session); assertNull("Should not have registered lazily created \"current\" session with the global lookup table", sessionComponent.getSession(session.getId())); } /** * Verifies that a session created as a side-effect of * {@link SessionComponent#getCurrentSession()} is available in a * thread-scoped cache, i.e. that a call to {@link {@link SessionComponent#getCurrentSession()} * on a different, sessionless thread receives a different session. This is * distinct from {@link #testGetCurrentSessionLazilyCreatesTransientSession()} * which is concerned with whether or not lazily created "current" sessions * are findable by ID (they are <em>not</em>). * @throws TimeoutException * @throws ExecutionException * @throws InterruptedException */ public void testGetCurrentSessionCachesLazilyCreatedThreadScopedSession() throws InterruptedException, ExecutionException, TimeoutException { expectLazyCurrentSessionCreation("1234546789"); final Session session = sessionComponent.getCurrentSession(); assertNotNull("Should have allocated a new session", session); // Since we control the return value of the "gets" on threadLocalManager, // the important bit here is that the "get" expectation defined immediately // below is satisfied, much less so the "sameness" assertion following that. // The same basic point holds for the asynch further on down. checking(new Expectations() { { one(threadLocalManager).get(with(equal(SessionComponent.CURRENT_SESSION))); will(returnValue(session)); } }); assertSame("A thread should always receive the same \"current\" session", session, sessionComponent.getCurrentSession()); // other threads should get different "current" Session objects final SessionHolder sessionHolder = new SessionHolder(); expectLazyCurrentSessionCreation(sessionHolder, "987654321"); FutureTask<Session> asynchGet = new FutureTask<Session>(new Callable<Session>() { public Session call() { return sessionComponent.getCurrentSession(); } }); new Thread(asynchGet).start(); assertNotSame("Should have allocated a different \"current\" session for other thread", session, asynchGet.get(1, TimeUnit.SECONDS)); } /** * Verifies that the sessions created lazily by {@link SessionComponent#getCurrentSession()} * are initialized to the correct state. This effectively means that the resulting * session exhibits all the characteristics of any other "started" session. */ public void testGetCurrentSessionCachesLazilyCreatedSessionsInExpectedState() { String expectedSessionId = "123456789"; String expectedInactiveInterval = "10"; resetMaintenance("10", expectedInactiveInterval); expectLazyCurrentSessionCreation(expectedSessionId); Session session = sessionComponent.getCurrentSession(); assertStartedSessionState(session, expectedSessionId, Integer.parseInt(expectedInactiveInterval)); } /** * Verifies that {@link SessionComponent#getCurrentSession()} returns a thread- * local session if one exists. This is actually implicitly tested to some extent in * several other tests, but is included here to explicitly verify that the lazy * session creation logic is skipped when appropriate. * */ public void testGetCurrentSessionReturnsExistingThreadScopedSession() { final Session session = mock(Session.class); checking(new Expectations() { { one(threadLocalManager).get(with(equal(SessionComponent.CURRENT_SESSION))); will(returnValue(session)); } }); assertSame("Should have returned existing session", session, sessionComponent.getCurrentSession()); } /** * Verifies the state of a variety of fields on the {@link Session} * returned from {@link SessionComponent#startSession()}. To reduce * duplication, most assertions are actually implemented by * {@link #assertStartedSessionState(Session)}. */ public void testStartsSessionsInCorrectState() { resetMaintenance("10", "10"); String expectedSessionId = "123456789"; expectCreateUuidRequest("123456789"); Session startedSession = sessionComponent.startSession(); assertStartedSessionState(startedSession, expectedSessionId, 10); } /** * Identical to {@link #testStartSessionCreatesSessionInCorrectState()} * except that it tests the overload of <code>startSession()</code> * ({@link SessionComponent#startSession(String)}. */ public void testStartsSpecificSessionsInCorrectState() { resetMaintenance("10", "10"); String expectedSessionId = "987654321"; Session startedSession = sessionComponent.startSession(expectedSessionId); assertStartedSessionState(startedSession, expectedSessionId, 10); } /** * Verifies expected state of a newly "started" {@link Session}. * * @param startedSession the session to assert on * @param expectedSessionId the ID to expect on the given session (so * we can assert on sessions created by both forms of <code>startSession</code>) * @param expectedInactiveInterval since this value is not accessible * from {@link SessionComponent} except via reflection. */ protected void assertStartedSessionState(Session startedSession, String expectedSessionId, int expectedInactiveInterval) { assertEquals("Sessions should be started with the allocated or specified ID", expectedSessionId, startedSession.getId()); assertEquals("Sessions should be started with identical creation and last access times", startedSession.getCreationTime(), startedSession.getLastAccessedTime()); assertEquals("Sessions should be started with the inactive interval configured on the SessionManager", expectedInactiveInterval, startedSession.getMaxInactiveInterval()); assertNull("Sessions should be started without a user ID", startedSession.getUserId()); assertNull("Sessions should be started without a user EID", startedSession.getUserEid()); } public void testStartingSessionDoesNotSetCurrentSession() { // Currently, getCurrentSession() lazily allocates a thread-scoped // Session if such a Session does not already exist. That behavior // is tested elsewhere, but we specify "allows" here to at // least keep jMock quiet. This also explains the assertNotSame() // call, rather than assertNull() expectCreateUuidRequest("123456789"); Session startedSession = sessionComponent.startSession(); allowLazyCurrentSessionCreation(); assertNotSame(startedSession, sessionComponent.getCurrentSession()); } public void testStartingSpecificSessionDoesNotSetCurrentSession() { allowLazyCurrentSessionCreation(); Session startedSession = sessionComponent.startSession("987654321"); assertNotSame(startedSession, sessionComponent.getCurrentSession()); } public void testGetCurrentSessionUserIdRetrievesIdFromThreadScopedSession() { final Session session = mock(Session.class); final String userId = "SOME_USER_ID"; checking(new Expectations() { { one(threadLocalManager).get(SessionComponent.CURRENT_SESSION); will(returnValue(session)); one(session).getUserId(); will(returnValue(userId)); } }); assertEquals("Incorrect current session user ID", userId, sessionComponent.getCurrentSessionUserId()); } public void testGetCurrentSessionUserIdReturnsNullIfNoThreadScopedSession() { checking(new Expectations() { { one(threadLocalManager).get(SessionComponent.CURRENT_SESSION); will(returnValue(null)); } }); assertNull("Should have returned null user ID", sessionComponent.getCurrentSessionUserId()); } public void testGetCurrentToolSessionReturnsThreadScopedSession() { final ToolSession session = mock(ToolSession.class); checking(new Expectations() { { one(threadLocalManager).get(SessionComponent.CURRENT_TOOL_SESSION); will(returnValue(session)); } }); assertEquals("Should have returned the current thread-scoped ToolSession", session, sessionComponent.getCurrentToolSession()); } public void testGetCurrentToolSessionReturnsNullIfNoThreadScopedSession() { checking(new Expectations() { { one(threadLocalManager).get(SessionComponent.CURRENT_TOOL_SESSION); will(returnValue(null)); } }); assertNull("Should have returned null ToolSession", sessionComponent.getCurrentToolSession()); } /** * Verifies that the specified session is cached in the correct * <code>ThreadLocal</code> <em>and</em> that the specified session is * not stored in the "global" session lookup table, and is therefore not * findable by ID. */ public void testSetCurrentSessionCachesSessionInThreadScope() { final String sessionId = "123456789"; final Session session = mock(Session.class); checking(new Expectations() { { one(threadLocalManager).set(SessionComponent.CURRENT_SESSION, session); allowing(session).getId(); will(returnValue(sessionId)); } }); sessionComponent.setCurrentSession(session); assertNull("Should not have registered \"current\" session with the global lookup table", sessionComponent.getSession(sessionId)); } public void testSetCurrentSessionCachesNullSessionInThreadScope() { checking(new Expectations() { { one(threadLocalManager).set(SessionComponent.CURRENT_SESSION, null); } }); sessionComponent.setCurrentSession(null); } /** * Verifies that the specified tool session is cached in the correct * <code>ThreadLocal</code>. No need to verify that the tool session * isn't placed in the global lookup table b/c the API effectively * prevents this. */ public void testSetCurrentToolSessionCachesSessionInThreadScope() { final ToolSession session = mock(ToolSession.class); checking(new Expectations() { { one(threadLocalManager).set(SessionComponent.CURRENT_TOOL_SESSION, session); } }); sessionComponent.setCurrentToolSession(session); } public void testSetCurrentToolSessionCachesNullSessionInThreadScope() { checking(new Expectations() { { one(threadLocalManager).set(SessionComponent.CURRENT_TOOL_SESSION, null); } }); sessionComponent.setCurrentToolSession(null); } /** * "Activeness" is configurable by method input and refers to session * accesses not just open sessions. As currently implemented, the only * reason this method happens to work is that the internal datastructure * for storing sessions is a {@link ConcurrentHashMap}. In fact, * this test would fail with a <code>ConcurrentModificationException</code> * were {@link SessionComponent}'s internal session map implemented by * something other than a {@link ConcurrentMap}. */ public void testGetActiveUserCount() { startSessionForUser(); startSessionForUser(); startSessionForUser(); assertEquals(3, sessionComponent.getActiveUserCount(100000)); } /** * Verifies that {@link SessionComponent#getActiveUserCount(int)} * does not count any session which has not been "accessed" in the * given seconds interval. * * <p>Implementation note: We elected to implement this in a relatively * black-box fashion which involves actually waiting the "activeness * interval" before retrieving the count. Another approach would * attempt to "manually" adjust the session(s) * <code>lastAccessedTime</code>, but such an implementation would * require mocking out {@link Session} and overriding * {@link SessionComponent}'s construction thereof. There is currently * no good way to do the latter. So we live with the off * chance that this test might fail if the * {@link #startSessionForUser()} calls take an exceptionally * long time to return.</p> * * @see Session#getLastAccessedTime() * @throws InterruptedException */ public void testGetActiveUserCountFiltersInactiveSessions() throws InterruptedException { startSessionForUser(); Thread.sleep(1001); startSessionForUser(); startSessionForUser(); assertEquals(2, sessionComponent.getActiveUserCount(1)); } /** * As currently implemented a "special" user session is associated * with the super user ("admin"), the postmaster, or with a null * user ID. We elected to test this separately from "basic" * active user counting ({@link #testGetActiveUserCount()}) since * there's really two distinct concepts in play: activeness and * specialness. Misimplementation of one needn't imply misimplementation * of the other. */ public void testGetActiveUserCountFiltersSpecialUserSessions() { startSessionForUser("admin", "admin"); startSessionForUser("postmaster", "admin"); startSessionForUser(null, null); assertEquals(0, sessionComponent.getActiveUserCount(100000)); // and now to make sure a misguided impl doesn't do something // like accidentally reset the entire count upon encountering // a special user startSessionForUser(); startSessionForUser("admin", "admin"); assertEquals(1, sessionComponent.getActiveUserCount(100000)); } /** * Expiration is subtly different than inactivity. Expired sessions * will be ejected from memory altogether. Activity is determined by * a method parameter to {@link SessionComponent#getActiveUserCount(int)}. * Expired sessions should not ever be considered active, though, regardless * of that <code>int</code> value. * * @see #testGetSessionReturnsNullIfSessionExpired() * @throws InterruptedException * @throws NoSuchFieldException * @throws SecurityException * @throws IllegalAccessException * @throws IllegalArgumentException */ public void testGetActiveUserCountFiltersExpiredSessions() throws InterruptedException, SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException { resetMaintenance("1", "1"); Session toBeExpired = startSessionAndExpectItsExpiration(); awaitExpirationOrFail(toBeExpired, 1); startSessionForUser(); assertEquals(1, sessionComponent.getActiveUserCount(100000)); } private void awaitExpirationOrFail(Session toBeExpired, int inactiveInterval) throws InterruptedException, SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException { // unfortunate, but doesn't seem to be any other way around // timing problems Thread.sleep(inactiveInterval * 1500L); // ensure expiration window elapses, and then some Field isValid = toBeExpired.getClass().getDeclaredField("m_valid"); isValid.setAccessible(true); int cnt = 0; while (isValid.getBoolean(toBeExpired) && cnt++ < 10) { Thread.sleep(100); } if (isValid.getBoolean(toBeExpired)) { fail("Session should have expired"); } Thread.sleep(50); } /** * A clarifying test for dispelling possible ambiguity in * the {@link SessionComponent#getActiveUserCount(int)} method name. That * method does in fact count users, not sessions per se. */ public void testGetActiveUserCountIncludesMultipleSessionsForSameUser() { String userUuid = nextUuid(); startSessionForUser(userUuid, userUuid); startSessionForUser(userUuid, userUuid); assertEquals(1, sessionComponent.getActiveUserCount(100000)); } public void testSessionIsInvalidatedDuringMaintenance() throws InterruptedException { // 20 seconds sessionComponent.setInactiveInterval("20"); final CountDownLatch startedLatch = new CountDownLatch(1); final CountDownLatch completedLatch = new CountDownLatch(1); final CountDownLatch blockerLatch = new CountDownLatch(1); MySession session = (MySession) newSessionWithBlockableInvalidate(startedLatch, blockerLatch, completedLatch); // now minus 15 seconds session.m_accessed = System.currentTimeMillis() - (25 * 1000L); // now minus 5 seconds session.expirationTimeSuggestion.setValue(System.currentTimeMillis() - (5 * 1000L)); expectGetCurrentSessionReturnNull(session); registerSession(session); resetMaintenance("20", "20"); if (!(startedLatch.await(2, TimeUnit.SECONDS))) { fail("Took too long for the maintenance thread to start up"); } blockerLatch.countDown(); if (!(completedLatch.await(5, TimeUnit.SECONDS))) { fail("Took too long for the Session.invalidate() to complete"); } } /** * Tests that sessions can be created while a maintenance sweep is in * progress. Read on for implementation notes. * * <p>Certain operations in {@link SessionComponent} have traditionally * been implemented to iterate over the {@link SessionComponent#m_sessions} * datastructure. These operations (expiration, active user counting) need * to cope with concurrent write operations ({@link SessionComponent#startSession()}).</p> * * <p>One way to verify that this is the case would be to test * the data structure directly. However, the implementation may * choose to lock access to that map in other ways such that the * implementation of the datastructure itself is not actually relevant.</p> * * <p>As a workaround we decided to inject custom Session implementations * directly into the datastructure for which we could cause certain * operations to block on a condition. We've resisted committing this * sin elsewhere in this test (see any other asnych test) on the grounds * that it violates the black-box intentions of this test class. No other * option was really feasible for this particular test, though.</p> * @throws InterruptedException * * @see #testCanStartSessionWhileCalculatingActiveUserCount() */ public void testCanStartSessionWhilePerformingMaintenance() throws InterruptedException { final CountDownLatch maintenanceStartedLatch = new CountDownLatch(1); final CountDownLatch maintenanceCompletedLatch = new CountDownLatch(2); final CountDownLatch maintenanceBlockerLatch = new CountDownLatch(1); // need two sessions to ensure Iterator.hasNext() is invoked // twice before the test's main thread exits. It's the hasNext() // method that we expect to fail if the data structure is not // handled/specified correctly. registerSession(newSessionWithBlockableMutableLong(maintenanceStartedLatch, maintenanceBlockerLatch, maintenanceCompletedLatch)); registerSession(newSessionWithBlockableMutableLong(maintenanceStartedLatch, maintenanceBlockerLatch, maintenanceCompletedLatch)); resetMaintenance("10", "10"); // Wait for the maintenance thread to start checking for // inactive sessions. This and the other wait time below need // to be relatively low, at least lower than the maintenance // sleep window, otherwise the maintenance thread will wake up // again and defeat the test. if (!(maintenanceStartedLatch.await(2, TimeUnit.SECONDS))) { fail("Took too long for the maintenance thread to start up"); } startSessionForUser(); // the actual code exercise maintenanceBlockerLatch.countDown(); // allow the maintenance thread to proceed if (!(maintenanceCompletedLatch.await(2, TimeUnit.SECONDS))) { fail("Took too long for the maintenance thread to complete"); } } /** * Tests that sessions can be created while an active user count is in * progress. * * <p>Same as {@link #testCanStartSessionWhilePerformingMaintenance()} but * for {@link SessionComponent#getActiveUserCount(int)}</p> * @throws InterruptedException */ public void testCanStartSessionWhileCalculatingActiveUserCount() throws InterruptedException { final CountDownLatch countStartedLatch = new CountDownLatch(1); final CountDownLatch countCompletedLatch = new CountDownLatch(2); final CountDownLatch countBlockerLatch = new CountDownLatch(1); // need two sessions to ensure Iterator.hasNext() is invoked // twice before the test's main thread exits. It's the hasNext() // method that we expect to fail if the data structure is not // handled/specified correctly. registerSession(newSessionWithBlockableGetLastAccessedTimeImpl(countStartedLatch, countBlockerLatch, countCompletedLatch)); registerSession(newSessionWithBlockableGetLastAccessedTimeImpl(countStartedLatch, countBlockerLatch, countCompletedLatch)); new Thread() { public void run() { sessionComponent.getActiveUserCount(100000); } }.start(); // Wait for the counting thread to start checking for // active users. The wait time is arbitrary, but ensures // the test exits. Same for the await() call at bottom. if (!(countStartedLatch.await(2, TimeUnit.SECONDS))) { fail("Took too long for the counting thread to start up"); } startSessionForUser(); // the actual code exercise countBlockerLatch.countDown(); // allow the counting thread to proceed if (!(countCompletedLatch.await(2, TimeUnit.SECONDS))) { fail("Took too long for the counting thread to complete"); } } protected Session newSessionWithBlockableInvalidate(final CountDownLatch opStarted, final CountDownLatch opBlocker, final CountDownLatch opCompleted) { // unfortunately, the Maintenance implementation compels us to // use MySession rather than an interface. String uuid = nextUuid(); final MySession session = new MySession(sessionComponent, uuid, threadLocalManager, idManager, sessionComponent, sessionListener, sessionComponent.getInactiveInterval(), new MyNonPortableSession(), new MutableLong(System.currentTimeMillis()), null) { // Make eclipse warnings go away and define this private static final long serialVersionUID = 1L; @Override public void invalidate() { Callable<Boolean> callback = new Callable<Boolean>() { public Boolean call() throws Exception { return superInvalidate(); } }; execBlockableSessionOp(opStarted, opBlocker, opCompleted, callback); } private boolean superInvalidate() { System.out.println("**cris** invalidate"); super.invalidate(); return true; } }; return session; } protected Session newSessionWithBlockableMutableLong(final CountDownLatch opStarted, final CountDownLatch opBlocker, final CountDownLatch opCompleted) { // unfortunately, the Maintenance implementation compels us to // use MySession rather than an interface. String uuid = nextUuid(); final MutableLong expirationTimeSuggestion = new MutableLong(System.currentTimeMillis()) { @Override public long longValue() { Callable<Long> callback = new Callable<Long>() { public Long call() throws Exception { return superLongValue(); } }; Long result = execBlockableSessionOp(opStarted, opBlocker, opCompleted, callback); return result; } private long superLongValue() { return super.longValue(); } }; final MySession session = new MySession(sessionComponent, uuid, threadLocalManager, idManager, sessionComponent, sessionListener, sessionComponent.getInactiveInterval(), new MyNonPortableSession(), expirationTimeSuggestion, null); return session; } protected Session newSessionWithBlockableGetLastAccessedTimeImpl(final CountDownLatch opStarted, final CountDownLatch opBlocker, final CountDownLatch opCompleted) { // unfortunately, the getActiveUserCount() implementation compels us to // use MySession rather than an interface. String uuid = nextUuid(); final MySession session = new MySession(sessionComponent, uuid, threadLocalManager, idManager, sessionComponent, sessionListener, sessionComponent.getInactiveInterval(), new MyNonPortableSession(), new MutableLong(System.currentTimeMillis()), null) { private long superGetLastAccessedTime() { return super.getLastAccessedTime(); } @Override public long getLastAccessedTime() { Callable<Long> callback = new Callable<Long>() { public Long call() throws Exception { return superGetLastAccessedTime(); } }; Long result = execBlockableSessionOp(opStarted, opBlocker, opCompleted, callback); return result; } }; return session; } // Doesn't necessarily take less code in the client to split this method // out, but the *latch calls are error prone. protected <V> V execBlockableSessionOp(final CountDownLatch opStarted, final CountDownLatch opBlocker, final CountDownLatch opCompleted, Callable<V> callback) { try { opStarted.countDown(); if (!(opBlocker.await(10, TimeUnit.SECONDS))) { fail("Took too long"); } V result = callback.call(); // Op only completes if the super impl returns non-exceptionally. // This allows us to detect failures outside the main test thread. opCompleted.countDown(); return result; } catch (Throwable e) { e.printStackTrace(); throw new RuntimeException(e); // typically ends up in logs, at best } } /** * A back-door for registering sessions with {@link SessionComponent}'s * internal data structure. * * @param session */ @SuppressWarnings(value = { "unchecked" }) protected void registerSession(Session session) { sessionComponent.m_sessions.put(session.getId(), session); if (session instanceof MySession) { MySession mySession = (MySession) session; sessionComponent.expirationTimeSuggestionMap.put(session.getId(), mySession.expirationTimeSuggestion); } } protected Session startSessionForUser() { return startSessionForUser(nextUuid(), nextUuid()); } protected Session startSessionForUser(String userId, String userEid) { expectCreateUuidRequest(); Session session = sessionComponent.startSession(); session.setUserId(userId); session.setUserEid(userEid); return session; } protected void allowLazyCurrentSessionCreation() { allowLazyCurrentSessionCreation(null, null); } protected void allowLazyCurrentSessionCreation(SessionHolder createdSessionHolder) { allowLazyCurrentSessionCreation(createdSessionHolder, null); } protected void allowLazyCurrentSessionCreation(String sessionId) { allowLazyCurrentSessionCreation(null, sessionId); } protected void allowLazyCurrentSessionCreation(final SessionHolder createdSessionHolder, String sessionId) { final String scrubbedSessionId = sessionId == null ? "LAZY_SESSION_ID" : sessionId; checking(new Expectations() { { allowing(threadLocalManager).get(with(equal(SessionComponent.CURRENT_SESSION))); will(returnValue(null)); allowing(idManager).createUuid(); will(returnValue(scrubbedSessionId)); allowing(threadLocalManager).set(with(equal(SessionComponent.CURRENT_SESSION)), with(sessionHavingId(scrubbedSessionId, createdSessionHolder))); } }); } protected void expectLazyCurrentSessionCreation() { expectLazyCurrentSessionCreation(null, null); } protected void expectLazyCurrentSessionCreation(SessionHolder createdSessionHolder) { expectLazyCurrentSessionCreation(createdSessionHolder, null); } protected void expectLazyCurrentSessionCreation(String sessionId) { expectLazyCurrentSessionCreation(null, sessionId); } protected void expectLazyCurrentSessionCreation(final SessionHolder createdSessionHolder, String sessionId) { final String scrubbedSessionId = sessionId == null ? "LAZY_SESSION_ID" : sessionId; checking(new Expectations() { { one(threadLocalManager).get(with(equal(SessionComponent.CURRENT_SESSION))); will(returnValue(null)); one(idManager).createUuid(); will(returnValue(scrubbedSessionId)); one(threadLocalManager).set(with(equal(SessionComponent.CURRENT_SESSION)), with(sessionHavingId(scrubbedSessionId, createdSessionHolder))); } }); } protected void expectGetCurrentSessionReturnNull(final Session session) { checking(new Expectations() { { allowing(threadLocalManager).get(with(equal(SessionComponent.CURRENT_SESSION))); // exactly(2).of(threadLocalManager).get(with(any(String.class))); will(returnValue(session)); allowing(threadLocalManager).set(with(equal(SessionComponent.CURRENT_SESSION)), with(equal((Object) null))); } }); } }