I spent yesterday and today doing test driven development on an interesting algorithm. I'd
like to post the code, because it's kind of a neat problem. Actually, I wish I had thought of it
earlier, and I would have taken snapshots of the code and the unit test as I went along, so I
could post an example of TDD in a real world environment, on a non-trivial algorithm. However, I'm
done except for a little clean up so I'll probably just finish it and then post it. My wife
reminded me that I need to find out what my company's policy is before I do. She's right, of
course. I don't think the company stands to lose anything, but it's better to ask than to get in
trouble.
(( Note: I am including everything on this page, even though some of the work is from
yesterday and some is from tomorrow. ))
A TDD Experience Report
The Scenario
Assume I am maintianing a set of objects by receiving refresh update
events (complete set snapshots) and change update events (set deltas). In the special case I
am tackling here, the objects in the set also
have validity periods. What I'd like to receive is an event stream so that I maintain a set of
currently valid objects. For example, when time has passed causing an object to become
invalid, I need some event to tell me to remove the object from the set. An example of where this
would be useful was described on 2004-07-01.
I realized I could do this by creating a filter on the normal event stream. Here are my
whiteboard notes from yesterday:
I realized I could simplify the problem by leaving out all synchronization. The filter object
generally does not have enough knowledge of the surrounding threading issues to do an adequate
job of synchronization anyway. By pushing generation of asynchronous timer events to an outer
level, I could make a robust filter using TDD while leaving the threading issues to be solved
at an outer level without having to worry (in that outer level) about how the filter did its job.
How I Wrote It
Note: I got approval to post the code, so I have included it
at the bottom of this page. You may want to open it in another window so you can follow along.
Note: For the most part I wrote the tests (and then the code
to make them pass) in the order you
see them in the file. The main exception is that the last test
testUpdateFailure was written concurrently with testChangeUpdate
- mostly because I realized there were error conditions possible when I was writing code for
testChangeUpdate and I wanted to test my error handling code immediately.
Also, testAdvanceToTimeBackwards was added at the end chronologically but fit
better in the middle of the file.
I started by defining the basic interface. I knew I would be writing a
filter, so the class would implement an event stream Listener and take a reference
to a downstream Listener to pass the events along to.
I needed to be able to get validity periods for the object snapshots in
the update events.
Since I didn't want to restrict the type of the snapshots (for example, by making them
implement an interface with methods to return the validity begin and end), I immediately created
an interface for an evaluator object that could determine the validity begin and end of a given
snapshot - ValidityEvaluator.
Interestingly, I initially didn't have the equals
and hashCode methods
because I didn't need (or know I needed) them until I started implementing
testChangeUpdate much later. My first test, testValidAlways validated
this initial interface by checking a guaranteed straight-pass-though situation.
I was able to implement testRefresh and testRefreshNoInitialUpdate
by just sticking snapshots in a single List. The trick was in coming up with good
test cases and then the corresponding logic.
Note: in this code, a null validity begin
or end means "indefinite" or "infinity". Also ranges are in standard CS half-open form. Thus you
can easily represent validity periods like (∞, -1),
[-1, 1), and [1, ∞). For comparison, previously
I had used zero as a special value meaning "indefinite". The downside was null was
an illegal value and there was a hole in my range. With the new model, there are no illegal values
(which saves error handling and the associated tests) and there are no holes in the range that
some user entered data could stumble into at some inopportune moment.
It
wasn't until I implemented testAdvanceToTime that I ran into something that
wasn't in my whiteboard diagram. There actually two
types of time based events for a snapshot - validity begin events and validity end events.
Once I had to start consuming the data I had stashed, I
found I couldn't just use a List.
I needed a data structure where I could quickly find /
add / remove events keyed by time, and where I could quickly find when the next (first) time event
was. A SortedMap fit the bill.
I was able to implement the necessary (minimum) functionalty to make the current tests pass
by creating a TimeSlot class containing two Lists, one of begin events
(actually just the snapshot that would be dispatched as the core of that event) and one of end
events.
Things really got interesting when I started on testChangeUpdate. I had
come up with a good set of twelve test cases for testRefresh. For
testChangeUpdate, I wanted to test all twelve secondary states against all twelve
initial states. That's one hundred and fourty four test cases! And each test case would have
muliple steps, since I would have to run a full advanceToTime sequence to see if
the change really had the proper effect - about five steps, each with validations, per case.
How could I write the unit test? Copy and paste is NOT the answer.
That's where I quit last night.
I came up with my answer while lying in bed. I could try to
come up with an algorithm that
would predict the correct responses for each validation. However, to be an effective test
it would have to be different from the algorithm
I would write in the real class. I didn't even have one algorithm
figured out yet, let alone two! One thing I do a lot for repetitive test cases is use object
arrays to store the paramaters for each case - input parameters and expected results - making the
test effectively table driven. However,
with two validations per step, five steps, that's ten
objects per test case. There are 144 test cases, so that's 1440 times I'd have to type
new Something() - far more than I wanted to. My inspiration was to pack
all ten input parameters into one string. I've got
plenty of utilities for unpacking strings laying
around, and that brings me to only 144 array elements. I figured this was at least doable.
This morning I came in and coded it up. I wrote out the code to parse the string and execute
a single test case with all its steps.
Then I created strings for all the secondary states of the first initial
state. I implemented code until I had all those test cases working: the first initial state
was actually pretty degenerate so I didn't have to do too much to make it pass. Then I worked on
the strings for
the second initial state. I quickly noticed that all the test case strings were the same for the
first few initial states. Intead of copy and pasting, I realized I could take advantage of object
arrays. I dropped an Integer into the array as a reference to the index of the
element that held the actual test case strings. As testing progressed, I found a bunch more
duplicate rows. My 12 states actually had some duplicates - I probably could have gotten away
with 4, but then I wouldn't have had as good of coverage of the corner cases.
Also in retrospect, I could have extracted out rows of the array as constants and
then referred to the constant multiple times rather than doing the funky Integer
trick.
Implementing the code for all the test cases of testChangeUpdate took a bunch
of changes. Notably, I had to add equals and hashCode to
ValidityEvaluator,
and create a special Data object to hold the snapshots and expose the custom
equals and hashCode. Previously I had been using an already existing
data object that I happened to have on hand.
(Too bad the Java collection classes don't have an equals and hashCode
equivalent to the Comparable interface. If you want to redefine those methods for
a particular collection, you have to create wrappers.)
I switched the Lists of the
TimeSlot to Sets to make lookups fast when dequeueing an event.
Specifically, I used LinkedHashSet to guarantee that the order of iteration was
deterministic so that the validations in my tests would not have to account for multiple possible
result orderings.
I finished all this by the end of the day. I realized that the last thing I needed was a
notification when the externally maintained timer should be changed. I decided to create a
separate interface for that because I didn't want to require that it be the same object as the
data update listener, and I didn't want it to be part of the evaluator because the rest of the
evaluator's responibilities could be fulfilled by a single static instance. Here I ran out of
time and had to catch my plane.
Note to self: in writing this, I realized there's another thing I need to test if I'm not
doing so already:
- test - duplicate updates in a single refresh update should fail
The Next Day
I ended up calling the
interface NextEventTimestampListener, though it took me a while to come up with
a name I liked. Names are important, and that can make them a challenge. As an interesting point,
I violated strict TDD (no surprise there) and wrote the interface before
the tests. I had the method as
public Date nextEventTimestamp();
when I stopped to catch my plane. The next day when I started writing tests, I
immediately realized I had goofed! Since
this is a listener, the date must be passed in, not returned!
public void nextEventTimestamp(Date timestamp);
If I had written the test first like I was supposed to, I couldn't have made that mistake.
Peel the carrot on me!
I added the mock object for NextEventTimestampListener
and added extra validations to
the existing tests, starting with testRefresh
and working forward. I was going to skip adding
them to testChangeUpdate because I didn't want to change all the strings.
When I finished updating all the tests but that one, I found
that I had made all the tests pass without actually putting any code
to send nextEventTimestamp notifications in the change update handler!
(Interestingly, this is known to happen occasionally when you are doing TDD correctly.)
The reason all the tests passed even without code changes was that
the only notification test after a change update was in
testChangeUpdateMultiple and in that case there should be no notification.
I either
needed to add a new test for change updates to force a notification that could be validated,
or I could alter my
mega-test and all its strings. I realized I only really needed to add one validation after the
change update step, rather than a validation after every step. Since I would only have to modify
each string once (instead of five times), and since it would give me lots of test cases, I
decided to go for it and update all the strings.
After that, I was basically done and just trying to think of any holes in my test coverage.
I added a test for advanceToTime trying to go backwards, and I added the missing
update failure case mentioned in the note above.
I also added more comments, since I planned to post this on the web and wanted to look good. :)
I find I'm writing fewer comments than I used to. I don't know if that's because I'm writing
clearer code or just getting lazy. It's probably laziness. :(
Ta-da! Done. Here are the results:
TimeReleaseListener.java
1 /**
2 * Copyright (c) 2005 by Deephaven Capital Management.
3 * All Rights Reserved Worldwide.
4 */
5 package com.deephaven.common.session;
6
7 import java.util.Date;
8 import java.util.LinkedList;
9 import java.util.List;
10 import java.util.SortedMap;
11 import java.util.TreeMap;
12 import java.util.Iterator;
13 import java.util.Set;
14 import java.util.LinkedHashSet;
15
16 import com.deephaven.common.verify.Require;
17 import com.deephaven.common.verify.Assert;
18 import com.deephaven.common.util.EqualityUtil;
19 import com.deephaven.common.util.LogUtil;
20 import com.protomatter.syslog.Syslog;
21
22 //--------------------------------------------------------------------
23 /** Filters, queues, and generates and refresh and change events so
24 * that only snapshots that are presently valid are presented to the
25 * downstream listener. All validity evaluation is done against an
26 * internal "clock" which must be advanced manually.
27 * <P><B>Threading:</B> No synchonization is done in this class,
28 * because proper behavior strictly dependent on synchronization
29 * provided by the user. All methods should be called exclusively by
30 * the one thread (currently) in the event dispatcher role.
31 * @author Created by Louis Thomas on Feb 2, 2005 */
32 public class TimeReleaseListener implements MultiTableSession.Listener {
33
34 private final MultiTableSession.Listener m_listener;
35 private final ValidityEvaluator m_validityEvaluator;
36 private final NextEventTimestampListener m_nextEventTimestampListener;
37 private long m_tsNow;
38 private final SortedMap m_queue=new TreeMap();
39
40 public interface ValidityEvaluator {
41 /** Returns the timestamp when the snapshot first becomes
42 * interesting, or null if the snapshot has always been
43 * interesting. */
44 public Date getValidityBegin(String sTableName, Object snapshot);
45 /** Returns the timestamp when the snapshot finally becomes
46 * uninteresting, or null if the snapshot will always be
47 * interesting. */
48 public Date getValidityEnd(String sTableName, Object snapshot);
49
50 /** Returns the hashcode for the given snapshot. */
51 public int hashCode(String sTableName, Object snapshot);
52 /** Returns true if the given snapshots are equal. The
53 * snapshots will be from the same table; snapshots from
54 * different tables are never considered equal. */
55 public boolean equals(String sTableName, Object leftSnapshot, Object rightSnapshot);
56 }
57
58 public interface NextEventTimestampListener {
59 /** Called whenever the time for the next event changes. Will
60 * be called from whichever thread called
61 * {@link TimeReleaseListener#processRefreshUpdate},
62 * {@link TimeReleaseListener#processChangeUpdate},
63 * or {@link TimeReleaseListener#advanceToTime}. */
64 public void nextEventTimestamp(Date timestamp);
65 }
66
67 //################################################################
68
69 public class TestingAccessor {
70 public boolean isQueueEmpty() { return 0==m_queue.size(); }
71 public long getNow() { return m_tsNow; }
72 }
73 public TestingAccessor getTestingAccessor() {
74 return new TestingAccessor();
75 }
76
77 //################################################################
78 // public api
79
80 //----------------------------------------------------------------
81 /** Creates a new TimeReleaseListener with its internal clock set
82 * to the given timestamp. */
83 public TimeReleaseListener(MultiTableSession.Listener listener, ValidityEvaluator validityEvaluator, NextEventTimestampListener nextEventTimestampListener, long tsNow) {
84 Require.neqNull(listener, "listener");
85 Require.neqNull(validityEvaluator, "validityEvaluator");
86 Require.neqNull(nextEventTimestampListener, "nextEventTimestampListener");
87 m_listener=listener;
88 m_validityEvaluator=validityEvaluator;
89 m_nextEventTimestampListener=nextEventTimestampListener;
90 m_tsNow=tsNow;
91 }
92
93 //----------------------------------------------------------------
94 // from MultiTableSession.Listener
95 public void processRefreshUpdate(MultiTableSession.RefreshUpdate[] refreshUpdates) {
96 Date lastEventTimestamp=getNextEventTimestamp();
97
98 // a refresh always contains our entire state, so clear the queue so we can start fresh
99 clearQueue();
100
101 // filter the refresh updates to figure out which should be dropped, queued for later, or sent now
102 List nowRefreshUpdates=new LinkedList();
103 for (int nIndex=0; nIndex<refreshUpdates.length; nIndex++) {
104 MultiTableSession.RefreshUpdate refreshUpdate=refreshUpdates[nIndex];
105 Object snapshot=refreshUpdate.getSnapshot();
106 String sTableName=refreshUpdate.getTableName();
107
108 Date validityBegin=m_validityEvaluator.getValidityBegin(sTableName, snapshot);
109 Date validityEnd=m_validityEvaluator.getValidityEnd(sTableName, snapshot);
110
111 if (null!=validityBegin && null!=validityEnd && validityEnd.getTime()<=validityBegin.getTime()) {
112 // invalid validity period, drop update
113 continue;
114 }
115 if (null!=validityEnd && validityEnd.getTime()<=m_tsNow) {
116 // validity end in the past, drop
117 continue;
118 } else if (null!=validityEnd) {
119 // validity will end in the future, enqueue
120 enqueueEndEvent(validityEnd, sTableName, snapshot);
121 }
122 if (null!=validityBegin && m_tsNow<validityBegin.getTime()) {
123 // validity will begin in the future, enqueue
124 enqueueBeginEvent(validityBegin, sTableName, snapshot);
125 } else {
126 // valid now
127 nowRefreshUpdates.add(refreshUpdate);
128 }
129 }
130
131 // send out the refresh updates that are effective now
132 if (nowRefreshUpdates.size()>0) {
133 m_listener.processRefreshUpdate((MultiTableSession.RefreshUpdate[])nowRefreshUpdates.toArray(new MultiTableSession.RefreshUpdate[nowRefreshUpdates.size()]));
134 }
135
136 // notify if the next event timestamp has changed
137 Date nextEventTimestamp=getNextEventTimestamp();
138 if (!EqualityUtil.nullsafeEquals(lastEventTimestamp, nextEventTimestamp)) {
139 m_nextEventTimestampListener.nextEventTimestamp(nextEventTimestamp);
140 }
141 }
142
143 //----------------------------------------------------------------
144 // from MultiTableSession.Listener
145 public void processChangeUpdate(MultiTableSession.ChangeUpdate[] changeUpdates) {
146 Date lastEventTimestamp=getNextEventTimestamp();
147
148 // filter the change updates to figure out which should be dropped, queued for later, or sent now
149 List nowChangeUpdates=new LinkedList();
150 for (int nIndex=0; nIndex<changeUpdates.length; nIndex++) {
151 MultiTableSession.ChangeUpdate changeUpdate=changeUpdates[nIndex];
152 String sTableName=changeUpdate.getTableName();
153 Object oldSnapshot=changeUpdate.getOldSnapshot();
154 Object newSnapshot=changeUpdate.getNewSnapshot();
155
156 Date oldValidityBegin=null==oldSnapshot?null:m_validityEvaluator.getValidityBegin(sTableName, oldSnapshot);
157 Date oldValidityEnd=null==oldSnapshot?null:m_validityEvaluator.getValidityEnd(sTableName, oldSnapshot);
158 Date newValidityBegin=null==newSnapshot?null:m_validityEvaluator.getValidityBegin(sTableName, newSnapshot);
159 Date newValidityEnd=null==newSnapshot?null:m_validityEvaluator.getValidityEnd(sTableName, newSnapshot);
160
161 Object resultOldSnapshot=null;
162 Object resultNewSnapshot=null;
163
164 do {
165 if (null!=oldValidityBegin && null!=oldValidityEnd && oldValidityEnd.getTime()<=oldValidityBegin.getTime()) {
166 // invalid validity period, drop update
167 continue;
168 }
169 if (null!=oldValidityEnd && oldValidityEnd.getTime()<=m_tsNow) {
170 // validity end in the past, drop
171 continue;
172 } else if (null!=oldValidityEnd) {
173 // validity will end in the future, enqueue
174 dequeueEndEvent(oldValidityEnd, sTableName, oldSnapshot);
175 }
176 if (null!=oldValidityBegin && m_tsNow<oldValidityBegin.getTime()) {
177 // validity will begin in the future, enqueue
178 dequeueBeginEvent(oldValidityBegin, sTableName, oldSnapshot);
179 } else {
180 // valid now
181 resultOldSnapshot=oldSnapshot;
182 }
183 } while (false);
184
185 do {
186 if (null!=newValidityBegin && null!=newValidityEnd && newValidityEnd.getTime()<=newValidityBegin.getTime()) {
187 // invalid validity period, drop update
188 continue;
189 }
190 if (null!=newValidityEnd && newValidityEnd.getTime()<=m_tsNow) {
191 // validity end in the past, drop
192 continue;
193 } else if (null!=newValidityEnd) {
194 // validity will end in the future, enqueue
195 enqueueEndEvent(newValidityEnd, sTableName, newSnapshot);
196 }
197 if (null!=newValidityBegin && m_tsNow<newValidityBegin.getTime()) {
198 // validity will begin in the future, enqueue
199 enqueueBeginEvent(newValidityBegin, sTableName, newSnapshot);
200 } else {
201 // valid now
202 resultNewSnapshot=newSnapshot;
203 }
204 } while (false);
205
206 if (null!=resultOldSnapshot || null!=resultNewSnapshot) {
207 nowChangeUpdates.add(new MultiTableSession.ChangeUpdate(sTableName, resultOldSnapshot, resultNewSnapshot));
208 }
209 }
210
211 // send out the change updates that are effective now
212 if (nowChangeUpdates.size()>0) {
213 m_listener.processChangeUpdate((MultiTableSession.ChangeUpdate[])nowChangeUpdates.toArray(new MultiTableSession.ChangeUpdate[nowChangeUpdates.size()]));
214 }
215
216 // notify if the next event timestamp has changed
217 Date nextEventTimestamp=getNextEventTimestamp();
218 if (!EqualityUtil.nullsafeEquals(lastEventTimestamp, nextEventTimestamp)) {
219 m_nextEventTimestampListener.nextEventTimestamp(nextEventTimestamp);
220 }
221 }
222
223 //----------------------------------------------------------------
224 // from SessionState.Listener
225 public void sessionStateNotification(SessionState sessionState) {
226 m_listener.sessionStateNotification(sessionState);
227 }
228
229 //----------------------------------------------------------------
230 /** Advances the internal clock to the given timestamp, firing all
231 * necessary change updates for snapshots that have become valid
232 * or invalid in the mean time.
233 * <P><B>Note:</B> The internal clock may not be advanced
234 * backwards. (Doing so would violate internal invariants.)
235 * Attempts to do so are ignored with a warning message. */
236 public void advanceToTime(long tsNow) {
237
238 // skip invalid updates
239 if (tsNow<=m_tsNow) {
240 Syslog.warningToChannel(this, LogUtil.DEV_NOTIFY, "Ignored attempt to advance time backwards (had "+m_tsNow+", got "+tsNow+").");
241 return;
242 }
243
244 Date lastEventTimestamp=getNextEventTimestamp();
245
246 // send out any change events that have happened between then and now
247 while (m_queue.size()>0 && ((Date)m_queue.firstKey()).getTime()<=tsNow) {
248 processQueueHead();
249 }
250 m_tsNow=tsNow;
251
252 // notify if the next event timestamp has changed
253 Date nextEventTimestamp=getNextEventTimestamp();
254 if (!EqualityUtil.nullsafeEquals(lastEventTimestamp, nextEventTimestamp)) {
255 m_nextEventTimestampListener.nextEventTimestamp(nextEventTimestamp);
256 }
257 }
258
259 //################################################################
260
261 //----------------------------------------------------------------
262 private Date getNextEventTimestamp() {
263 return 0==m_queue.size()?null:((Date)m_queue.firstKey());
264 }
265
266 //----------------------------------------------------------------
267 private void clearQueue() {
268 m_queue.clear();
269 }
270
271 //----------------------------------------------------------------
272 private void enqueueEndEvent(Date validityEnd, String sTableName, Object snapshot) {
273 TimeSlot timeSlot=getOrCreateTimeSlot(validityEnd);
274 timeSlot.enqueueEndEvent(new Update(sTableName, snapshot));
275 }
276
277 //----------------------------------------------------------------
278 private void enqueueBeginEvent(Date validityEnd, String sTableName, Object snapshot) {
279 TimeSlot timeSlot=getOrCreateTimeSlot(validityEnd);
280 timeSlot.enqueueBeginEvent(new Update(sTableName, snapshot));
281 }
282
283 //----------------------------------------------------------------
284 private void dequeueEndEvent(Date validityEnd, String sTableName, Object snapshot) {
285 TimeSlot timeSlot=getExistingTimeSlot(validityEnd);
286 timeSlot.dequeueEndEvent(new Update(sTableName, snapshot));
287 cleanUpTimeSlotIfEmpty(timeSlot, validityEnd);
288 }
289
290 //----------------------------------------------------------------
291 private void dequeueBeginEvent(Date validityBegin, String sTableName, Object snapshot) {
292 TimeSlot timeSlot=getExistingTimeSlot(validityBegin);
293 timeSlot.dequeueBeginEvent(new Update(sTableName, snapshot));
294 cleanUpTimeSlotIfEmpty(timeSlot, validityBegin);
295 }
296
297 //----------------------------------------------------------------
298 private void processQueueHead() {
299 Date eventTimestamp=(Date)m_queue.firstKey();
300 TimeSlot timeSlot=(TimeSlot)m_queue.remove(eventTimestamp);
301 m_listener.processChangeUpdate(timeSlot.getAsChangeUpdates());
302 }
303
304 //----------------------------------------------------------------
305 private TimeSlot getOrCreateTimeSlot(Date date) {
306 TimeSlot timeSlot=(TimeSlot)m_queue.get(date);
307 if (null==timeSlot) {
308 timeSlot=new TimeSlot();
309 m_queue.put(date, timeSlot);
310 }
311 return timeSlot;
312 }
313
314 //----------------------------------------------------------------
315 private TimeSlot getExistingTimeSlot(Date date) {
316 TimeSlot timeSlot=(TimeSlot)m_queue.get(date);
317 Require.neqNull(timeSlot, "timeSlot", 2);
318 return timeSlot;
319 }
320
321 //----------------------------------------------------------------
322 private void cleanUpTimeSlotIfEmpty(TimeSlot timeSlot, Date validityBegin) {
323 if (true==timeSlot.isEmpty()) {
324 Object removed=m_queue.remove(validityBegin);
325 Assert.eq(removed, "removed", timeSlot, "timeSlot");
326 }
327 }
328
329 //################################################################
330
331 //----------------------------------------------------------------
332 private class Update {
333 private final String m_sTableName;
334 private final Object m_snapshot;
335 private final int m_nHashCode;
336
337 //------------------------------------------------------------
338 public Update(String sTableName, Object snapshot) {
339 Require.equalsNonempty(sTableName, "sTableName");
340 Require.neqNull(snapshot, "snapshot");
341 m_sTableName=sTableName;
342 m_snapshot=snapshot;
343 m_nHashCode=calcHashCode();
344 }
345
346 //------------------------------------------------------------
347 public String getTableName() {
348 return m_sTableName;
349 }
350
351 //------------------------------------------------------------
352 public Object getSnapshot() {
353 return m_snapshot;
354 }
355
356 //------------------------------------------------------------
357 public boolean equals(Object that) {
358 if (this==that) {
359 return true;
360 }
361 if (!(that instanceof Update)) {
362 return false;
363 }
364
365 final Update update=(Update)that;
366
367 if (m_nHashCode!=update.m_nHashCode) {
368 return false;
369 }
370 if (!m_sTableName.equals(update.m_sTableName)) {
371 return false;
372 }
373 if (!m_validityEvaluator.equals(m_sTableName, m_snapshot, update.m_snapshot)) {
374 return false;
375 }
376
377 return true;
378 }
379
380 //------------------------------------------------------------
381 private int calcHashCode() {
382 int result;
383 result=m_sTableName.hashCode();
384 result=29*result+m_validityEvaluator.hashCode(m_sTableName, m_snapshot);
385 return result;
386 }
387
388 //------------------------------------------------------------
389 public int hashCode() {
390 return m_nHashCode;
391 }
392 }
393
394 //----------------------------------------------------------------
395 private static class TimeSlot {
396 private final Set m_validityBeginUpdates=new LinkedHashSet();
397 private final Set m_validityEndUpdates=new LinkedHashSet();
398
399 //------------------------------------------------------------
400 public void enqueueBeginEvent(Update update) {
401 boolean bWasInSet=!m_validityBeginUpdates.add(update);
402 Require.eqFalse(bWasInSet, "bWasInSet", 2);
403 }
404
405 //------------------------------------------------------------
406 public void enqueueEndEvent(Update update) {
407 boolean bWasInSet=!m_validityEndUpdates.add(update);
408 Require.eqFalse(bWasInSet, "bWasInSet", 2);
409 }
410
411 //------------------------------------------------------------
412 public MultiTableSession.ChangeUpdate[] getAsChangeUpdates() {
413 MultiTableSession.ChangeUpdate[] changeUpdates=new MultiTableSession.ChangeUpdate[m_validityBeginUpdates.size()+m_validityEndUpdates.size()];
414 Assert.gtZero(changeUpdates.length, "changeUpdates.length");
415 int nIndex=0;
416 for (Iterator itr=m_validityBeginUpdates.iterator(); itr.hasNext();) {
417 Update update=(Update)itr.next();
418 changeUpdates[nIndex]=new MultiTableSession.ChangeUpdate(update.getTableName(), null, update.getSnapshot());
419 nIndex++;
420 }
421 for (Iterator itr=m_validityEndUpdates.iterator(); itr.hasNext();) {
422 Update update=(Update)itr.next();
423 changeUpdates[nIndex]=new MultiTableSession.ChangeUpdate(update.getTableName(), update.getSnapshot(), null);
424 nIndex++;
425 }
426 return changeUpdates;
427 }
428
429 //------------------------------------------------------------
430 public void dequeueBeginEvent(Update update) {
431 boolean bWasInSet=m_validityBeginUpdates.remove(update);
432 Require.eqTrue(bWasInSet, "bWasInSet", 2);
433 }
434
435 //------------------------------------------------------------
436 public void dequeueEndEvent(Update update) {
437 boolean bWasInSet=m_validityEndUpdates.remove(update);
438 Require.eqTrue(bWasInSet, "bWasInSet", 2);
439 }
440
441 //------------------------------------------------------------
442 private boolean isEmpty() {
443 return 0==m_validityBeginUpdates.size() && 0==m_validityEndUpdates.size();
444 }
445 }
446 }
TestTimeReleaseListener.java
1 /**
2 * Copyright (c) 2005 by Deephaven Capital Management.
3 * All Rights Reserved Worldwide.
4 */
5 package com.deephaven.common.session.test;
6
7 import java.util.Date;
8
9 import com.deephaven.common.session.TimeReleaseListener;
10 import com.deephaven.common.session.SessionState;
11 import com.deephaven.common.session.MultiTableSession;
12 import com.deephaven.common.verify.Require;
13 import com.deephaven.common.verify.RequirementFailureRuntimeException;
14 import com.deephaven.common.util.BloombergTokenizer;
15
16 //--------------------------------------------------------------------
17 /** Tests for {@link TimeReleaseListener}.
18 * @author Created by Louis Thomas on Feb 2, 2005 */
19 public class TestTimeReleaseListener extends BaseTradingSystemTest {
20
21 private static final TimeReleaseListener.ValidityEvaluator VALID_ALWAYS=new TimeReleaseListener.ValidityEvaluator() {
22 public Date getValidityBegin(String sTableName, Object snapshot) {
23 return null;
24 }
25
26 public Date getValidityEnd(String sTableName, Object snapshot) {
27 return null;
28 }
29
30 public int hashCode(String sTableName, Object snapshot) {
31 return 0;
32 }
33
34 public boolean equals(String sTableName, Object leftSnapshot, Object rightSnapshot) {
35 return false;
36 }
37 };
38
39 private static final MockMultiTableSessionListener.ThumbprinterAdapter TO_STRING_THUMBPRINTER=new MockMultiTableSessionListener.ThumbprinterAdapter() {
40 public String getSnapshotThumbprint(Object snapshot) {
41 return null==snapshot?"null":snapshot.toString();
42 }
43 };
44
45 private static final String TABLE_NAME="T";
46
47 //----------------------------------------------------------------
48 public void testValidAlways() {
49 MockMultiTableSessionListener mockMultiTableSessionListener=new MockMultiTableSessionListener(TO_STRING_THUMBPRINTER);
50 TimeReleaseListener timeReleaseListener=new TimeReleaseListener(mockMultiTableSessionListener, VALID_ALWAYS, new MockNextEventTimestampListener(), 0);
51
52 timeReleaseListener.sessionStateNotification(new SessionState(SessionState.STATE_CONNECTED, 0, "test"));
53 assertEquals("sSN([STATE_CONNECTED,0,test])", mockMultiTableSessionListener.getActivityRecordAndReset());
54
55 timeReleaseListener.processRefreshUpdate(new MultiTableSession.RefreshUpdate[] {new MultiTableSession.RefreshUpdate(TABLE_NAME, "snapshot")});
56 assertEquals("pRU(0:[r/snapshot])", mockMultiTableSessionListener.getActivityRecordAndReset());
57
58 timeReleaseListener.processChangeUpdate(new MultiTableSession.ChangeUpdate[] {new MultiTableSession.ChangeUpdate(TABLE_NAME, "old", "new")});
59 assertEquals("pCU(0:[c/old/new])", mockMultiTableSessionListener.getActivityRecordAndReset());
60 }
61
62 //----------------------------------------------------------------
63 public void testRefresh() {
64 MockMultiTableSessionListener mockMultiTableSessionListener=new MockMultiTableSessionListener(TO_STRING_THUMBPRINTER);
65 MockNextEventTimestampListener mockNextEventTimestampListener=new MockNextEventTimestampListener();
66 TimeReleaseListener timeReleaseListener=new TimeReleaseListener(mockMultiTableSessionListener, Data.VALIDITY_EVALUATOR, mockNextEventTimestampListener, 0);
67 TimeReleaseListener.TestingAccessor testingAccessor=timeReleaseListener.getTestingAccessor();
68
69 MultiTableSession.RefreshUpdate[] refreshUpdates=new MultiTableSession.RefreshUpdate[] {
70 // test refresh - updates with equal or crossed validity are dropped
71 new MultiTableSession.RefreshUpdate(TABLE_NAME, new Data("A", new Date(0), new Date(0))),
72 new MultiTableSession.RefreshUpdate(TABLE_NAME, new Data("B", new Date(1), new Date(-1))),
73
74 // test refresh - updates valid in the past are dropped
75 new MultiTableSession.RefreshUpdate(TABLE_NAME, new Data("C", new Date(-5), new Date(-1))),
76 new MultiTableSession.RefreshUpdate(TABLE_NAME, new Data("D", new Date(-5), new Date(0))),
77 new MultiTableSession.RefreshUpdate(TABLE_NAME, new Data("E", null, new Date(-5))),
78
79 // test refresh - updates valid now are passed
80 new MultiTableSession.RefreshUpdate(TABLE_NAME, new Data("F", new Date(-5), new Date(5))),
81 new MultiTableSession.RefreshUpdate(TABLE_NAME, new Data("G", new Date(0), new Date(5))),
82 new MultiTableSession.RefreshUpdate(TABLE_NAME, new Data("H", null, new Date(5))),
83 new MultiTableSession.RefreshUpdate(TABLE_NAME, new Data("I", null, null)),
84 new MultiTableSession.RefreshUpdate(TABLE_NAME, new Data("J", new Date(-5), null)),
85
86 // test refresh - updates valid in the future are queued
87 new MultiTableSession.RefreshUpdate(TABLE_NAME, new Data("K", new Date(1), new Date(5))),
88 new MultiTableSession.RefreshUpdate(TABLE_NAME, new Data("L", new Date(1), null)),
89 };
90
91 timeReleaseListener.processRefreshUpdate(refreshUpdates);
92 assertEquals("pRU(0:[r/F]1:[r/G]2:[r/H]3:[r/I]4:[r/J])", mockMultiTableSessionListener.getActivityRecordAndReset());
93 assertFalse(testingAccessor.isQueueEmpty());
94 assertEquals("nET(1)", mockNextEventTimestampListener.getActivityRecordAndReset());
95
96 // test refresh - the queue is cleared
97 timeReleaseListener.processRefreshUpdate(new MultiTableSession.RefreshUpdate[] { new MultiTableSession.RefreshUpdate(TABLE_NAME, new Data("M", null, null))});
98 assertEquals("pRU(0:[r/M])", mockMultiTableSessionListener.getActivityRecordAndReset());
99 assertTrue(testingAccessor.isQueueEmpty());
100 assertEquals("nET(null)", mockNextEventTimestampListener.getActivityRecordAndReset());
101 }
102
103 //----------------------------------------------------------------
104 public void test