/* * Copyright 2014, The Sporting Exchange Limited * * 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. */ package com.betfair.cougar.transport.socket; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.betfair.cougar.netutil.nio.HeapDelta; import com.betfair.cougar.netutil.nio.TerminateSubscription; import com.betfair.cougar.netutil.nio.connected.InitialUpdate; import com.betfair.cougar.netutil.nio.connected.Update; import com.betfair.cougar.netutil.nio.connected.UpdateAction; import com.betfair.cougar.transport.api.protocol.CougarObjectOutput; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; /** * Implementation of CougarObjectOutput usable for using complex expectations on multi-threaded code */ public class ExpectingOutput implements CougarObjectOutput, Runnable { private static Logger LOGGER = LoggerFactory.getLogger(ExpectingOutput.class); private List<Update> expectedUpdates; private int expectedUpdatesCurrentIndex = 0; private int currentUpdateNextIndex = 0; private long maxTimeBetween; private AtomicReference<CountDownLatch> latchRef = new AtomicReference<CountDownLatch>(); private List<Object> allValues = new ArrayList<Object>(); private List<TerminateSubscription> subTerminations = new ArrayList<TerminateSubscription>(); private List<TerminateSubscription> expectedSubTerminations = new ArrayList<TerminateSubscription>(); public ExpectingOutput(long maxTimeBetween) { this.maxTimeBetween = maxTimeBetween; latchRef.set(new CountDownLatch(1)); } @Override public void writeObject(Object object) throws IOException { allValues.add(object); if (!(object instanceof HeapDelta)) { if (!(object instanceof TerminateSubscription)) { notifyFailure("Attempt to write unexpected object: "+object); } else { subTerminations.add((TerminateSubscription)object); } return; } HeapDelta delta = (HeapDelta) object; if (expectedUpdates == null) { return; } if (expectedUpdatesCurrentIndex >= expectedUpdates.size()) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Received unexpected update: "+delta); } notifyFailure("Received unexpected update: "+delta); return; } List<Update> updates = delta.getUpdates(); Update nextExpected = expectedUpdates.get(expectedUpdatesCurrentIndex); if (nextExpected instanceof InitialUpdate) { // initial updates are always atomic expectedUpdatesCurrentIndex++; InitialUpdate expectedInitial = (InitialUpdate) nextExpected; if (updates.size() > 1) { notifyFailure("Received >1 update for initial update: "+delta); return; } if (updates.isEmpty()) { notifyFailure("Received zero length update list for initial update: "+delta); return; } Update actualInitial = updates.get(0); if (!expectedInitial.equals(actualInitial)) { notifyFailure("Expected: "+expectedInitial+", got: "+actualInitial); return; } else { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Found expected initial: "+actualInitial); } } // if more to come then move along, otherwise we can stop now.. if (expectedUpdatesCurrentIndex < expectedUpdates.size()) { latchRef.getAndSet(new CountDownLatch(1)).countDown(); } else { latchRef.getAndSet(null).countDown(); } } // ok, not an the initial update, now we're interested in the updates else { List<UpdateAction> allActions = new ArrayList<UpdateAction>(); for (Update u : updates) { for (UpdateAction ua : u.getActions()) { allActions.add(ua); } } if (expectedUpdates != null) { int waitingFor = nextExpected.getActions().size() - currentUpdateNextIndex; if (waitingFor < allActions.size()) { notifyFailure("Found more actions than I'm waiting for: "+allActions); return; } // now check each in turn for (int i=0; i<allActions.size(); i++) { UpdateAction expected = nextExpected.getActions().get(currentUpdateNextIndex + i); UpdateAction actual = allActions.get(i); if (!expected.equals(actual)) { notifyFailure("Expected: "+expected+", got: "+actual); return; } else { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Found expected update: " + actual); } } } currentUpdateNextIndex += allActions.size(); if (currentUpdateNextIndex < nextExpected.getActions().size()) { latchRef.getAndSet(new CountDownLatch(1)).countDown(); } else { expectedUpdatesCurrentIndex++; currentUpdateNextIndex=0; // if more to come then move along, otherwise we can stop now.. if (expectedUpdatesCurrentIndex < expectedUpdates.size()) { latchRef.getAndSet(new CountDownLatch(1)).countDown(); } else { latchRef.getAndSet(null).countDown(); } } } } } public void run() { while (true) { CountDownLatch latch = latchRef.get(); // null latch means all over if (latch != null) { boolean success = false; try { // latch completing means we received the update in time success = latch.await(maxTimeBetween, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { } if (!success) { notifyFailure("Didn't receive write within "+maxTimeBetween+"ms"); } } else { notifyComplete(); break; } } } private List<ExpectingOutputListener> listeners = new CopyOnWriteArrayList<ExpectingOutputListener>(); public void addListener(ExpectingOutputListener l) { listeners.add(l); } private void notifyFailure(String s) { new Exception(s).printStackTrace(); for (ExpectingOutputListener l : listeners) { l.failure(s); } } private void notifyComplete() { for (ExpectingOutputListener l : listeners) { l.complete(); } } public void start() { new Thread(this).start(); } @Override public void writeString(String arg0) throws IOException { throw new UnsupportedOperationException(); } @Override public void writeBoolean(boolean b) throws IOException { throw new UnsupportedOperationException(); } @Override public void close() throws IOException { } @Override public void flush() throws IOException { } @Override public void writeBytes(byte[] arg0, int arg1, int arg2) throws IOException { throw new UnsupportedOperationException(); } @Override public void writeBytes(byte[] buffer) throws IOException { throw new UnsupportedOperationException(); } @Override public void writeDouble(double value) throws IOException { throw new UnsupportedOperationException(); } @Override public void writeInt(int value) throws IOException { throw new UnsupportedOperationException(); } @Override public void writeLong(long value) throws IOException { throw new UnsupportedOperationException(); } public List<Object> getAllValues() { return allValues; } public void setExpectedUpdates(List<Update> expectedUpdates) { this.expectedUpdates = normaliseUpdates(expectedUpdates); } public void setExpectedSubTerminations(List<TerminateSubscription> expectedSubTerminations) { this.expectedSubTerminations = expectedSubTerminations; } public List<TerminateSubscription> getExpectedSubTerminations() { return expectedSubTerminations; } public List<TerminateSubscription> getSubTerminations() { return subTerminations; } private List<Update> normaliseUpdates(List<Update> expectedUpdates) { List<Update> ret = new ArrayList<Update>(); Update lastNormalUpdate = null; for (Update u : expectedUpdates) { if (u instanceof InitialUpdate) { ret.add(u); lastNormalUpdate = null; continue; } if (lastNormalUpdate == null) { ret.add(u); lastNormalUpdate = u; continue; } lastNormalUpdate.getActions().addAll(u.getActions()); } return ret; } public static interface ExpectingOutputListener { void failure(String s); void complete(); } }