Refactor isoping and add unit tests.

Separate out isoping per-client state into a struct, and do most per-packet
processing in separate functions.  This makes testing feasible, and will let us
support multiple clients per server.  Also compile as C++ to make the struct
initialization clean and get access to hash tables for the multiclient scenario.

No actual logic changes involved, just new tests added.

Change-Id: I1d405f25043770484e2dbad183d3bc9206abf60d
diff --git a/cmds/Makefile b/cmds/Makefile
index b058618..4778036 100644
--- a/cmds/Makefile
+++ b/cmds/Makefile
@@ -53,7 +53,8 @@
 	stdoutline.so
 HOST_TEST_TARGETS=\
 	host-netusage_test \
-	host-utils_test
+	host-utils_test \
+	host-isoping_test
 SCRIPT_TARGETS=\
 	is-secure-boot
 ARCH_TARGETS=\
@@ -148,6 +149,9 @@
 host-%.o: %.cc
 	$(HOST_CXX) $(CXXFLAGS) $(INCS) $(HOST_INCS) -DCOMPILE_FOR_HOST=1 -o $@ -c $<
 
+host-%.o: ../wvtest/cpp/%.cc
+	$(HOST_CXX) $(CXXFLAGS) -D WVTEST_CONFIGURED -o $@ -c $<
+
 %: %.o
 	$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) $(LIBS) -lc
 
@@ -171,8 +175,12 @@
 	echo "Building .pb.cc"
 	$(HOST_PROTOC) --cpp_out=. $<
 
-
-host-isoping isoping: LIBS+=$(RT) -lm
+host-isoping isoping: LIBS+=$(RT) -lm -lstdc++
+host-isoping: host-isoping.o host-isoping_main.o
+host-isoping_test.o: CXXFLAGS += -D WVTEST_CONFIGURED -I ../wvtest/cpp
+host-isoping_test.o: isoping.cc
+host-isoping_test: LIBS+=$(HOST_LIBS) -lm -lstdc++
+host-isoping_test: host-isoping_test.o host-isoping.o host-wvtestmain.o host-wvtest.o
 host-isostream isostream: LIBS+=$(RT)
 host-diskbench diskbench: LIBS+=-lpthread $(RT)
 host-dnsck: LIBS+=$(HOST_LIBS) -lcares $(RT)
@@ -187,7 +195,7 @@
 host-alivemonitor alivemonitor: LIBS+=$(RT)
 host-buttonmon buttonmon: LIBS+=$(RT)
 alivemonitor: alivemonitor.o
-isoping: isoping.o
+isoping: isoping.o isoping_main.o
 isostream: isostream.o
 diskbench: diskbench.o
 dnsck: LIBS+=-lcares $(RT)
diff --git a/cmds/isoping.c b/cmds/isoping.c
deleted file mode 100644
index a669883..0000000
--- a/cmds/isoping.c
+++ /dev/null
@@ -1,657 +0,0 @@
-/*
- * Copyright 2014 Google Inc. All rights reserved.
- *
- * 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.
- */
-
-/*
- *
- * Like ping, but sends packets isochronously (equally spaced in time) in
- * each direction.  By being clever, we can use the known timing of each
- * packet to determine, on a noisy network, which direction is dropping or
- * delaying packets and by how much.
- *
- * Also unlike ping, this requires a server (ie. another copy of this
- * program) to be running on the remote end.
- */
-#include <arpa/inet.h>
-#include <errno.h>
-#include <math.h>
-#include <memory.h>
-#include <netdb.h>
-#include <netinet/in.h>
-#include <signal.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <sys/select.h>
-#include <sys/socket.h>
-#include <sys/types.h>
-#include <time.h>
-#include <unistd.h>
-
-#ifndef CLOCK_MONOTONIC_RAW
-#define CLOCK_MONOTONIC_RAW 4
-#endif
-
-#define MAGIC 0x424c4950
-#define SERVER_PORT 4948
-#define DEFAULT_PACKETS_PER_SEC 10.0
-
-// A 'cycle' is the amount of time we can assume our calibration between
-// the local and remote monotonic clocks is reasonably valid.  It seems
-// some of our devices have *very* fast clock skew (> 1 msec/minute) so
-// this unfortunately has to be much shorter than I'd like.  This may
-// reflect actual bugs in our ntpd and/or the kernel's adjtime()
-// implementation.  In particular, we shouldn't have to do this kind of
-// periodic correction, because that's what adjtime() *is*.  But the results
-// are way off without this.
-#define USEC_PER_CYCLE (10*1000*1000)
-
-#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))
-#define DIFF(x, y) ((int32_t)((uint32_t)(x) - (uint32_t)(y)))
-#define DIV(x, y) ((y) ? ((double)(x)/(y)) : 0)
-#define _STR(n) #n
-#define STR(n) _STR(n)
-
-
-// Layout of the UDP packets exchanged between client and server.
-// All integers are in network byte order.
-// Packets have exactly the same structure in both directions.
-struct Packet {
-  uint32_t magic;     // magic number to reject bogus packets
-  uint32_t id;        // sequential packet id number
-  uint32_t txtime;    // transmitter's monotonic time when pkt was sent
-  uint32_t clockdiff; // estimate of (transmitter's clk) - (receiver's clk)
-  uint32_t usec_per_pkt; // microseconds of delay between packets
-  uint32_t num_lost;  // number of pkts transmitter expected to get but didn't
-  uint32_t first_ack; // starting index in acks[] circular buffer
-  struct {
-    // txtime==0 for empty elements in this array.
-    uint32_t id;      // id field from a received packet
-    uint32_t rxtime;  // receiver's monotonic time when pkt arrived
-  } acks[64];
-};
-
-
-int want_to_die;
-
-
-static void sighandler(int sig) {
-  want_to_die = 1;
-}
-
-
-// Returns the kernel monotonic timestamp in microseconds, truncated to
-// 32 bits.  That will wrap around every ~4000 seconds, which is okay
-// for our purposes.  We use 32 bits to save space in our packets.
-// This function never returns the value 0; it returns 1 instead, so that
-// 0 can be used as a magic value.
-#ifdef __MACH__  // MacOS X doesn't have clock_gettime()
-#include <mach/mach.h>
-#include <mach/mach_time.h>
-
-static uint64_t ustime64(void) {
-  static mach_timebase_info_data_t timebase;
-  if (!timebase.denom) mach_timebase_info(&timebase);
-  uint64_t result = (mach_absolute_time() * timebase.numer /
-                     timebase.denom / 1000);
-  return !result ? 1 : result;
-}
-#else
-static uint64_t ustime64(void) {
-  // CLOCK_MONOTONIC_RAW, when available, is not subject to NTP speed
-  // adjustments while CLOCK_MONOTONIC is.  You might expect NTP speed
-  // adjustments to make things better if we're trying to sync timings
-  // between two machines, but at least our ntpd is pretty bad at making
-  // adjustments, so it tends the vary the speed wildly in order to kind
-  // of oscillate around the right time.  Experimentally, CLOCK_MONOTONIC_RAW
-  // creates less trouble for isoping's use case.
-  struct timespec ts;
-  if (clock_gettime(CLOCK_MONOTONIC_RAW, &ts) < 0) {
-    if (clock_gettime(CLOCK_MONOTONIC, &ts) < 0) {
-      perror("clock_gettime");
-      exit(98); // really should never happen, so don't try to recover
-    }
-  }
-  uint64_t result = ts.tv_sec * 1000000LL + ts.tv_nsec / 1000;
-  return !result ? 1 : result;
-}
-#endif
-
-
-static uint32_t ustime(void) {
-  return (uint32_t)ustime64();
-}
-
-
-static void usage_and_die(char *argv0) {
-  fprintf(stderr,
-          "\n"
-          "Usage: %s                          (server mode)\n"
-          "   or: %s <server-hostname-or-ip>  (client mode)\n"
-          "\n"
-          "      -f <lines/sec>  max output lines per second\n"
-          "      -r <pps>        packets per second (default=%g)\n"
-          "      -t <ttl>        packet ttl to use (default=2 for safety)\n"
-          "      -q              quiet mode (don't print packets)\n"
-          "      -T              print timestamps\n",
-          argv0, argv0, (double)DEFAULT_PACKETS_PER_SEC);
-  exit(99);
-}
-
-
-// Render the given sockaddr as a string.  (Uses a static internal buffer
-// which is overwritten each time.)
-static const char *sockaddr_to_str(struct sockaddr *sa) {
-  static char addrbuf[128];
-  void *aptr;
-
-  switch (sa->sa_family) {
-  case AF_INET:
-    aptr = &((struct sockaddr_in *)sa)->sin_addr;
-    break;
-  case AF_INET6:
-    aptr = &((struct sockaddr_in6 *)sa)->sin6_addr;
-    break;
-  default:
-    return "unknown";
-  }
-
-  if (!inet_ntop(sa->sa_family, aptr, addrbuf, sizeof(addrbuf))) {
-    perror("inet_ntop");
-    exit(98);
-  }
-  return addrbuf;
-}
-
-
-// Print the timestamp corresponding to the current time.
-// Deliberately the same format as tcpdump uses, so we can easily sort and
-// correlate messages between isoping and tcpdump.
-static void print_timestamp(uint32_t when) {
-  uint64_t now = ustime64();
-  int32_t nowdiff = DIFF(now, when);
-  uint64_t when64 = now - nowdiff;
-  time_t t = when64 / 1000000;
-  struct tm tm;
-  memset(&tm, 0, sizeof(tm));
-  localtime_r(&t, &tm);
-  printf("%02d:%02d:%02d.%06d ", tm.tm_hour, tm.tm_min, tm.tm_sec,
-         (int)(when64 % 1000000));
-}
-
-
-static double onepass_stddev(long long sumsq, long long sum, long long count) {
-  // Incremental standard deviation calculation, without needing to know the
-  // mean in advance.  See:
-  // http://mathcentral.uregina.ca/QQ/database/QQ.09.02/carlos1.html
-  long long numer = (count * sumsq) - (sum * sum);
-  long long denom = count * (count - 1);
-  return sqrt(DIV(numer, denom));
-}
-
-
-int main(int argc, char **argv) {
-  int is_server = 1;
-  struct sockaddr_in6 listenaddr, rxaddr, last_rxaddr;
-  struct sockaddr *remoteaddr = NULL;
-  socklen_t remoteaddr_len = 0, rxaddr_len = 0;
-  struct addrinfo *ai = NULL;
-  int sock = -1, want_timestamps = 0, quiet = 0, ttl = 2;
-  double packets_per_sec = DEFAULT_PACKETS_PER_SEC, prints_per_sec = -1;
-
-  setvbuf(stdout, NULL, _IOLBF, 0);
-
-  int c;
-  while ((c = getopt(argc, argv, "f:r:t:qTh?")) >= 0) {
-    switch (c) {
-    case 'f':
-      prints_per_sec = atof(optarg);
-      if (prints_per_sec <= 0) {
-        fprintf(stderr, "%s: lines per second must be >= 0\n", argv[0]);
-        return 99;
-      }
-      break;
-    case 'r':
-      packets_per_sec = atof(optarg);
-      if (packets_per_sec < 0.001 || packets_per_sec > 1e6) {
-        fprintf(stderr, "%s: packets per sec (-r) must be 0.001..1000000\n",
-                argv[0]);
-        return 99;
-      }
-      break;
-    case 't':
-      ttl = atoi(optarg);
-      if (ttl < 1) {
-        fprintf(stderr, "%s: ttl must be >= 1\n", argv[0]);
-        return 99;
-      }
-      break;
-    case 'q':
-      quiet = 1;
-      break;
-    case 'T':
-      want_timestamps = 1;
-      break;
-    case 'h':
-    case '?':
-    default:
-      usage_and_die(argv[0]);
-      break;
-    }
-  }
-
-  sock = socket(PF_INET6, SOCK_DGRAM, 0);
-  if (sock < 0) {
-    perror("socket");
-    return 1;
-  }
-
-  if (argc - optind == 0) {
-    is_server = 1;
-    memset(&listenaddr, 0, sizeof(listenaddr));
-    listenaddr.sin6_family = AF_INET6;
-    listenaddr.sin6_port = htons(SERVER_PORT);
-    if (bind(sock, (struct sockaddr *)&listenaddr, sizeof(listenaddr)) != 0) {
-      perror("bind");
-      return 1;
-    }
-    socklen_t addrlen = sizeof(listenaddr);
-    if (getsockname(sock, (struct sockaddr *)&listenaddr, &addrlen) != 0) {
-      perror("getsockname");
-      return 1;
-    }
-    fprintf(stderr, "server listening at [%s]:%d\n",
-           sockaddr_to_str((struct sockaddr *)&listenaddr),
-           ntohs(listenaddr.sin6_port));
-  } else if (argc - optind == 1) {
-    const char *remotename = argv[optind];
-    is_server = 0;
-    struct addrinfo hints = {
-      .ai_flags = AI_ADDRCONFIG | AI_V4MAPPED,
-      .ai_family = AF_INET6,
-      .ai_socktype = SOCK_DGRAM,
-    };
-    int err = getaddrinfo(remotename, STR(SERVER_PORT), &hints, &ai);
-    if (err != 0 || !ai) {
-      fprintf(stderr, "getaddrinfo(%s): %s\n", remotename, gai_strerror(err));
-      return 1;
-    }
-    fprintf(stderr, "connecting to %s...\n", sockaddr_to_str(ai->ai_addr));
-    if (connect(sock, ai->ai_addr, ai->ai_addrlen) != 0) {
-      perror("connect");
-      return 1;
-    }
-    remoteaddr = ai->ai_addr;
-    remoteaddr_len = ai->ai_addrlen;
-  } else {
-    usage_and_die(argv[0]);
-  }
-
-  fprintf(stderr, "using ttl=%d\n", ttl);
-  // IPPROTO_IPV6 is the only one that works on MacOS, and is arguably the
-  // technically correct thing to do since it's an AF_INET6 socket.
-  if (setsockopt(sock, IPPROTO_IPV6, IP_TTL, &ttl, sizeof(ttl))) {
-    perror("setsockopt(TTLv6)");
-    return 1;
-  }
-  // ...but in Linux (at least 3.13), IPPROTO_IPV6 does not actually
-  // set the TTL if the IPv6 socket ends up going over IPv4.  We have to
-  // set that separately.  On MacOS, that always returns EINVAL, so ignore
-  // the error if that happens.
-  if (setsockopt(sock, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl))) {
-    if (errno != EINVAL) {
-      perror("setsockopt(TTLv4)");
-      return 1;
-    }
-  }
-
-  int32_t usec_per_pkt = 1e6 / packets_per_sec;
-  int32_t usec_per_print = prints_per_sec > 0 ? 1e6 / prints_per_sec : 0;
-
-  // WARNING: lots of math below relies on well-defined uint32/int32
-  // arithmetic overflow behaviour, plus the fact that when we subtract
-  // two successive timestamps (for example) they will be less than 2^31
-  // microseconds apart.  It would be safer to just use 64-bit values
-  // everywhere, but that would cut the number of acks-per-packet in half,
-  // which would be unfortunate.
-  uint32_t next_tx_id = 1;       // id field for next transmit
-  uint32_t next_rx_id = 0;       // expected id field for next receive
-  uint32_t next_rxack_id = 0;    // expected ack.id field in next received ack
-  uint32_t start_rtxtime = 0;    // remote's txtime at startup
-  uint32_t start_rxtime = 0;     // local rxtime at startup
-  uint32_t last_rxtime = 0;      // local rxtime of last received packet
-  int32_t min_cycle_rxdiff = 0;  // smallest packet delay seen this cycle
-  uint32_t next_cycle = 0;       // time when next cycle begins
-  uint32_t now = ustime();       // current time
-  uint32_t next_send = now + usec_per_pkt;  // time when we'll send next pkt
-  uint32_t num_lost = 0;         // number of rx packets not received
-  int next_txack_index = 0;      // next array item to fill in tx.acks
-  struct Packet tx, rx;          // transmit and received packet buffers
-  char last_ackinfo[128] = "";   // human readable format of latest ack
-  uint32_t last_print = now - usec_per_pkt;  // time of last packet printout
-  // Packet statistics counters for transmit and receive directions.
-  long long lat_tx = 0, lat_tx_min = 0x7fffffff, lat_tx_max = 0,
-      lat_tx_count = 0, lat_tx_sum = 0, lat_tx_var_sum = 0;
-  long long lat_rx = 0, lat_rx_min = 0x7fffffff, lat_rx_max = 0,
-      lat_rx_count = 0, lat_rx_sum = 0, lat_rx_var_sum = 0;
-
-  memset(&tx, 0, sizeof(tx));
-
-  struct sigaction act = {
-    .sa_handler = sighandler,
-    .sa_flags = SA_RESETHAND,
-  };
-  sigaction(SIGINT, &act, NULL);
-
-  while (!want_to_die) {
-    fd_set rfds;
-    FD_ZERO(&rfds);
-    FD_SET(sock, &rfds);
-    struct timeval tv;
-    tv.tv_sec = 0;
-
-    now = ustime();
-    if (DIFF(next_send, now) < 0) {
-      tv.tv_usec = 0;
-    } else {
-      tv.tv_usec = DIFF(next_send, now);
-    }
-    int nfds = select(sock + 1, &rfds, NULL, NULL, remoteaddr ? &tv : NULL);
-    now = ustime();
-    if (nfds < 0 && errno != EINTR) {
-      perror("select");
-      return 1;
-    }
-
-    // time to send the next packet?
-    if (remoteaddr && DIFF(now, next_send) >= 0) {
-      tx.magic = htonl(MAGIC);
-      tx.id = htonl(next_tx_id++);
-      tx.usec_per_pkt = htonl(usec_per_pkt);
-      tx.txtime = htonl(next_send);
-      tx.clockdiff = start_rtxtime ? htonl(start_rxtime - start_rtxtime) : 0;
-      tx.num_lost = htonl(num_lost);
-      tx.first_ack = htonl(next_txack_index);
-      // note: tx.acks[] is filled in incrementally; we just transmit the
-      // current state of it here.  The reason we keep a list of the most
-      // recent acks is in case our packet gets lost, so the receiver will
-      // have more chances to receive the timing information for the packets
-      // it sent us.
-      if (is_server) {
-        if (sendto(sock, &tx, sizeof(tx), 0,
-                   remoteaddr, remoteaddr_len) < 0) {
-          perror("sendto");
-        }
-      } else {
-        if (send(sock, &tx, sizeof(tx), 0) < 0) {
-          int e = errno;
-          perror("send");
-          if (e == ECONNREFUSED) return 2;
-        }
-      }
-      if (is_server && DIFF(now, last_rxtime) > 60*1000*1000) {
-        fprintf(stderr, "client disconnected.\n");
-        remoteaddr = NULL;
-      }
-      next_send += usec_per_pkt;
-    }
-
-    if (nfds > 0) {
-      // incoming packet
-      rxaddr_len = sizeof(rxaddr);
-      ssize_t got = recvfrom(sock, &rx, sizeof(rx), 0,
-                             (struct sockaddr *)&rxaddr, &rxaddr_len);
-      if (got < 0) {
-        int e = errno;
-        perror("recvfrom");
-        if (!is_server && e == ECONNREFUSED) return 2;
-        continue;
-      }
-      if (got != sizeof(rx) || rx.magic != htonl(MAGIC)) {
-        fprintf(stderr, "got invalid packet of length %ld\n", (long)got);
-        continue;
-      }
-
-      // is it a new client?
-      if (is_server) {
-        // TODO(apenwarr): fork() for each newly connected client.
-        //  The way the code is right now, it won't work right at all
-        //  if there are two clients sending us packets at once.
-        //  That's okay for a first round of controlled tests.
-        if (!remoteaddr ||
-            memcmp(&rxaddr, &last_rxaddr, sizeof(rxaddr)) != 0) {
-          fprintf(stderr, "new client connected: %s\n",
-                  sockaddr_to_str((struct sockaddr *)&rxaddr));
-          memcpy(&last_rxaddr, &rxaddr, sizeof(rxaddr));
-          remoteaddr = (struct sockaddr *)&last_rxaddr;
-          remoteaddr_len = rxaddr_len;
-
-          next_send = now + 10*1000;
-          next_tx_id = 1;
-          next_rx_id = next_rxack_id = 0;
-          start_rtxtime = start_rxtime = 0;
-          num_lost = 0;
-          next_txack_index = 0;
-          usec_per_pkt = ntohl(rx.usec_per_pkt);
-          memset(&tx, 0, sizeof(tx));
-        }
-      }
-
-      // process the incoming packet header.
-      // Most of the complexity here comes from the fact that the remote
-      // system's clock will be skewed vs. ours.  (We use CLOCK_MONOTONIC
-      // instead of CLOCK_REALTIME, so unless we figure out the skew offset,
-      // it's essentially meaningless to compare the two values.)  We can
-      // however assume that both clocks are ticking at 1 microsecond per
-      // tick... except for inevitable clock rate errors, which we have to
-      // account for occasionally.
-
-      uint32_t txtime = ntohl(rx.txtime), rxtime = now;
-      uint32_t id = ntohl(rx.id);
-      if (!next_rx_id) {
-        // The remote txtime is told to us by the sender, so it is always
-        // perfectly correct... but it uses the sender's clock.
-        start_rtxtime = txtime - id * usec_per_pkt;
-
-        // The receive time uses our own clock and is estimated by us, so
-        // it needs to be corrected over time because:
-        //   a) the two clocks inevitably run at slightly different speeds;
-        //   b) there's an unknown, variable, network delay between tx and rx.
-        // Here, we're just assigning an initial estimate.
-        start_rxtime = rxtime - id * usec_per_pkt;
-
-        min_cycle_rxdiff = 0;
-        next_rx_id = id;
-        next_cycle = now + USEC_PER_CYCLE;
-      }
-
-      // see if we missed receiving any previous packets.
-      int32_t tmpdiff = DIFF(id, next_rx_id);
-      if (tmpdiff > 0) {
-        // arriving packet has id > expected, so something was lost.
-        // Note that we don't use the rx.acks[] structure to determine packet
-        // loss; that's because the limited size of the array means that,
-        // during a longer outage, we might not see an ack for a packet
-        // *even if that packet arrived safely* at the remote.  So we count
-        // on the remote end to count its own packet losses using sequence
-        // numbers, and send that count back to us.  We do the same here
-        // for incoming packets from the remote, and send the error count
-        // back to them next time we're ready to transmit.
-        fprintf(stderr, "lost %ld  expected=%ld  got=%ld\n",
-                (long)tmpdiff, (long)next_rx_id, (long)id);
-        num_lost += tmpdiff;
-        next_rx_id += tmpdiff + 1;
-      } else if (!tmpdiff) {
-        // exactly as expected; good.
-        next_rx_id++;
-      } else if (tmpdiff < 0) {
-        // packet before the expected one? weird.
-        fprintf(stderr, "out-of-order packets? %ld\n", (long)tmpdiff);
-      }
-
-      // fix up the clock offset if there's any drift.
-      tmpdiff = DIFF(rxtime, start_rxtime + id * usec_per_pkt);
-      if (tmpdiff < -20) {
-        // packet arrived before predicted time, so prediction was based on
-        // a packet that was "slow" before, or else one of our clocks is
-        // drifting. Use earliest legitimate start time.
-        fprintf(stderr, "time paradox: backsliding start by %ld usec\n",
-                (long)tmpdiff);
-        start_rxtime = rxtime - id * usec_per_pkt;
-      }
-      int32_t rxdiff = DIFF(rxtime, start_rxtime + id * usec_per_pkt);
-
-      // Figure out the offset between our clock and the remote's clock, so
-      // we can calculate the minimum round trip time (rtt). Then, because
-      // the consecutive packets sent in both directions are equally spaced
-      // in time, we can figure out how much a particular packet was delayed
-      // in transit - independently in each direction! This is an advantage
-      // over the normal "ping" program which has no way to tell which
-      // direction caused the delay, or which direction dropped the packet.
-
-      // Our clockdiff is
-      //   (our rx time) - (their tx time)
-      //   == (their rx time + offset) - (their tx time)
-      //   == (their tx time + offset + 1/2 rtt) - (their tx time)
-      //   == offset + 1/2 rtt
-      // and theirs (rx.clockdiff) is:
-      //   (their rx time) - (our tx time)
-      //   == (their rx time) - (their tx time + offset)
-      //   == (their tx time + 1/2 rtt) - (their tx time + offset)
-      //   == 1/2 rtt - offset
-      // So add them together and we get:
-      //   offset + 1/2 rtt + 1/2 rtt - offset
-      //   == rtt
-      // Subtract them and we get:
-      //   offset + 1/2 rtt - 1/2 rtt + offset
-      //   == 2 * offset
-      // ...but that last subtraction is dangerous because if we divide by
-      // 2 to get offset, it doesn't work with 32-bit math, which may have
-      // discarded a high-order bit somewhere along the way.  Instead,
-      // we can extract offset once we have rtt by substituting it into
-      //   clockdiff = offset + 1/2 rtt
-      //   offset = clockdiff - 1/2 rtt
-      // (Dividing rtt by 2 is safe since it's always small and positive.)
-      //
-      // (This example assumes 1/2 rtt in each direction. There's no way to
-      // determine it more accurately than that.)
-      int32_t clockdiff = DIFF(start_rxtime, start_rtxtime);
-      int32_t rtt = clockdiff + ntohl(rx.clockdiff);
-      int32_t offset = DIFF(clockdiff, rtt / 2);
-      if (!ntohl(rx.clockdiff)) {
-        // don't print the first packet: it has an invalid clockdiff since
-        // the client can't calculate the clockdiff until it receives
-        // at least one packet from us.
-        last_print = now - usec_per_print + 1;
-      } else {
-        // not the first packet, so statistics are valid.
-        lat_rx_count++;
-        lat_rx = rxdiff + rtt/2;
-        lat_rx_min = lat_rx_min > lat_rx ? lat_rx : lat_rx_min;
-        lat_rx_max = lat_rx_max < lat_rx ? lat_rx : lat_rx_max;
-        lat_rx_sum += lat_rx;
-        lat_rx_var_sum += lat_rx * lat_rx;
-      }
-
-      // Note: the way ok_to_print is structured, if there is a dropout in
-      // the connection for more than usec_per_print, we will statistically
-      // end up printing the first packet after the dropout ends.  That one
-      // should have the longest timeout, ie. a "worst case" packet, which is
-      // usually the information you want to see.
-      int ok_to_print = !quiet && DIFF(now, last_print) >= usec_per_print;
-      if (ok_to_print) {
-        if (want_timestamps) print_timestamp(rxtime);
-        printf("%12s  %6.1f ms rx  (min=%.1f)  loss: %ld/%ld tx  %ld/%ld rx\n",
-               last_ackinfo,
-               (rxdiff + rtt/2) / 1000.0,
-               (rtt/2) / 1000.0,
-               (long)ntohl(rx.num_lost),
-               (long)next_tx_id - 1,
-               (long)num_lost,
-               (long)next_rx_id - 1);
-        last_ackinfo[0] = '\0';
-        last_print = now;
-      }
-
-      if (rxdiff < min_cycle_rxdiff) min_cycle_rxdiff = rxdiff;
-      if (DIFF(now, next_cycle) >= 0) {
-        if (min_cycle_rxdiff > 0) {
-          fprintf(stderr, "clock skew: sliding start by %ld usec\n",
-                  (long)min_cycle_rxdiff);
-          start_rxtime += min_cycle_rxdiff;
-        }
-        min_cycle_rxdiff = 0x7fffffff;
-        next_cycle += USEC_PER_CYCLE;
-      }
-
-      // schedule this for an ack next time we send the packet
-      tx.acks[next_txack_index].id = htonl(id);
-      tx.acks[next_txack_index].rxtime = htonl(rxtime);
-      next_txack_index = (next_txack_index + 1) % ARRAY_LEN(tx.acks);
-
-      // see which of our own transmitted packets have been acked
-      uint32_t first_ack = ntohl(rx.first_ack);
-      for (uint32_t i = 0; i < ARRAY_LEN(rx.acks); i++) {
-        uint32_t acki = (first_ack + i) % ARRAY_LEN(rx.acks);
-        uint32_t ackid = ntohl(rx.acks[acki].id);
-        if (!ackid) continue;  // empty slot
-        if (DIFF(ackid, next_rxack_id) >= 0) {
-          // an expected ack
-          uint32_t start_txtime = next_send - next_tx_id * usec_per_pkt;
-          uint32_t txtime = start_txtime + ackid * usec_per_pkt;
-          uint32_t rrxtime = ntohl(rx.acks[acki].rxtime);
-          uint32_t rxtime = rrxtime + offset;
-          // note: already contains 1/2 rtt, unlike rxdiff
-          int32_t txdiff = DIFF(rxtime, txtime);
-          if (usec_per_print <= 0 && last_ackinfo[0]) {
-            // only print multiple acks per rx if no usec_per_print limit
-            if (want_timestamps) print_timestamp(rxtime);
-            printf("%12s\n", last_ackinfo);
-            last_ackinfo[0] = '\0';
-          }
-          if (!last_ackinfo[0]) {
-            snprintf(last_ackinfo, sizeof(last_ackinfo), "%6.1f ms tx",
-                     txdiff / 1000.0);
-          }
-          next_rxack_id = ackid + 1;
-          lat_tx_count++;
-          lat_tx = txdiff;
-          lat_tx_min = lat_tx_min > lat_tx ? lat_tx : lat_tx_min;
-          lat_tx_max = lat_tx_max < lat_tx ? lat_tx : lat_tx_max;
-          lat_tx_sum += lat_tx;
-          lat_tx_var_sum += lat_tx * lat_tx;
-        }
-      }
-
-      last_rxtime = rxtime;
-    }
-  }
-
-  printf("\n---\n");
-  printf("tx: min/avg/max/mdev = %.2f/%.2f/%.2f/%.2f ms\n",
-         lat_tx_min / 1000.0,
-         DIV(lat_tx_sum, lat_tx_count) / 1000.0,
-         lat_tx_max / 1000.0,
-         onepass_stddev(lat_tx_var_sum, lat_tx_sum, lat_tx_count) / 1000.0);
-  printf("rx: min/avg/max/mdev = %.2f/%.2f/%.2f/%.2f ms\n",
-         lat_rx_min / 1000.0,
-         DIV(lat_rx_sum, lat_rx_count) / 1000.0,
-         lat_rx_max / 1000.0,
-         onepass_stddev(lat_rx_var_sum, lat_rx_sum, lat_rx_count) / 1000.0);
-  printf("\n");
-
-  if (ai) freeaddrinfo(ai);
-  if (sock >= 0) close(sock);
-  return 0;
-}
diff --git a/cmds/isoping.cc b/cmds/isoping.cc
new file mode 100644
index 0000000..c6b123e
--- /dev/null
+++ b/cmds/isoping.cc
@@ -0,0 +1,663 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * 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.
+ */
+
+/*
+ *
+ * Like ping, but sends packets isochronously (equally spaced in time) in
+ * each direction.  By being clever, we can use the known timing of each
+ * packet to determine, on a noisy network, which direction is dropping or
+ * delaying packets and by how much.
+ *
+ * Also unlike ping, this requires a server (ie. another copy of this
+ * program) to be running on the remote end.
+ */
+#include "isoping.h"
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <math.h>
+#include <memory.h>
+#include <netdb.h>
+#include <netinet/in.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/select.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#ifndef CLOCK_MONOTONIC_RAW
+#define CLOCK_MONOTONIC_RAW 4
+#endif
+
+#define MAGIC 0x424c4950
+#define SERVER_PORT 4948
+#define DEFAULT_PACKETS_PER_SEC 10.0
+#define DEFAULT_TTL 2
+
+// A 'cycle' is the amount of time we can assume our calibration between
+// the local and remote monotonic clocks is reasonably valid.  It seems
+// some of our devices have *very* fast clock skew (> 1 msec/minute) so
+// this unfortunately has to be much shorter than I'd like.  This may
+// reflect actual bugs in our ntpd and/or the kernel's adjtime()
+// implementation.  In particular, we shouldn't have to do this kind of
+// periodic correction, because that's what adjtime() *is*.  But the results
+// are way off without this.
+#define USEC_PER_CYCLE (10*1000*1000)
+
+#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))
+#define DIFF(x, y) ((int32_t)((uint32_t)(x) - (uint32_t)(y)))
+#define DIV(x, y) ((y) ? ((double)(x)/(y)) : 0)
+#define _STR(n) #n
+#define STR(n) _STR(n)
+
+// Global flag values.
+int is_server = 1;
+int quiet = 0;
+int ttl = DEFAULT_TTL;
+int want_timestamps = 0;
+double packets_per_sec = DEFAULT_PACKETS_PER_SEC;
+double prints_per_sec = -1.0;
+
+int want_to_die;
+
+
+static void sighandler(int sig) {
+  want_to_die = 1;
+}
+
+Session::Session(uint32_t now)
+    : usec_per_pkt(1e6 / packets_per_sec),
+      usec_per_print(prints_per_sec > 0 ? 1e6 / prints_per_sec : 0),
+      next_tx_id(1),
+      next_rx_id(0),
+      next_rxack_id(0),
+      start_rtxtime(0),
+      start_rxtime(0),
+      last_rxtime(0),
+      min_cycle_rxdiff(0),
+      next_cycle(0),
+      next_send(now + usec_per_pkt),
+      num_lost(0),
+      next_txack_index(0),
+      last_print(now - usec_per_pkt),
+      lat_tx(0), lat_tx_min(0x7fffffff), lat_tx_max(0),
+      lat_tx_count(0), lat_tx_sum(0), lat_tx_var_sum(0),
+      lat_rx(0), lat_rx_min(0x7fffffff), lat_rx_max(0),
+      lat_rx_count(0), lat_rx_sum(0), lat_rx_var_sum(0) {
+  memset(&tx, 0, sizeof(tx));
+  strcpy(last_ackinfo, "");
+}
+
+// Returns the kernel monotonic timestamp in microseconds, truncated to
+// 32 bits.  That will wrap around every ~4000 seconds, which is okay
+// for our purposes.  We use 32 bits to save space in our packets.
+// This function never returns the value 0; it returns 1 instead, so that
+// 0 can be used as a magic value.
+#ifdef __MACH__  // MacOS X doesn't have clock_gettime()
+#include <mach/mach.h>
+#include <mach/mach_time.h>
+
+static uint64_t ustime64(void) {
+  static mach_timebase_info_data_t timebase;
+  if (!timebase.denom) mach_timebase_info(&timebase);
+  uint64_t result = (mach_absolute_time() * timebase.numer /
+                     timebase.denom / 1000);
+  return !result ? 1 : result;
+}
+#else
+static uint64_t ustime64(void) {
+  // CLOCK_MONOTONIC_RAW, when available, is not subject to NTP speed
+  // adjustments while CLOCK_MONOTONIC is.  You might expect NTP speed
+  // adjustments to make things better if we're trying to sync timings
+  // between two machines, but at least our ntpd is pretty bad at making
+  // adjustments, so it tends the vary the speed wildly in order to kind
+  // of oscillate around the right time.  Experimentally, CLOCK_MONOTONIC_RAW
+  // creates less trouble for isoping's use case.
+  struct timespec ts;
+  if (clock_gettime(CLOCK_MONOTONIC_RAW, &ts) < 0) {
+    if (clock_gettime(CLOCK_MONOTONIC, &ts) < 0) {
+      perror("clock_gettime");
+      exit(98); // really should never happen, so don't try to recover
+    }
+  }
+  uint64_t result = ts.tv_sec * 1000000LL + ts.tv_nsec / 1000;
+  return !result ? 1 : result;
+}
+#endif
+
+
+static uint32_t ustime(void) {
+  return (uint32_t)ustime64();
+}
+
+
+static void usage_and_die(char *argv0) {
+  fprintf(stderr,
+          "\n"
+          "Usage: %s                          (server mode)\n"
+          "   or: %s <server-hostname-or-ip>  (client mode)\n"
+          "\n"
+          "      -f <lines/sec>  max output lines per second\n"
+          "      -r <pps>        packets per second (default=%g)\n"
+          "      -t <ttl>        packet ttl to use (default=2 for safety)\n"
+          "      -q              quiet mode (don't print packets)\n"
+          "      -T              print timestamps\n",
+          argv0, argv0, (double)DEFAULT_PACKETS_PER_SEC);
+  exit(99);
+}
+
+
+// Render the given sockaddr as a string.  (Uses a static internal buffer
+// which is overwritten each time.)
+static const char *sockaddr_to_str(struct sockaddr *sa) {
+  static char addrbuf[128];
+  void *aptr;
+
+  switch (sa->sa_family) {
+  case AF_INET:
+    aptr = &((struct sockaddr_in *)sa)->sin_addr;
+    break;
+  case AF_INET6:
+    aptr = &((struct sockaddr_in6 *)sa)->sin6_addr;
+    break;
+  default:
+    return "unknown";
+  }
+
+  if (!inet_ntop(sa->sa_family, aptr, addrbuf, sizeof(addrbuf))) {
+    perror("inet_ntop");
+    exit(98);
+  }
+  return addrbuf;
+}
+
+
+// Print the timestamp corresponding to the current time.
+// Deliberately the same format as tcpdump uses, so we can easily sort and
+// correlate messages between isoping and tcpdump.
+static void print_timestamp(uint32_t when) {
+  uint64_t now = ustime64();
+  int32_t nowdiff = DIFF(now, when);
+  uint64_t when64 = now - nowdiff;
+  time_t t = when64 / 1000000;
+  struct tm tm;
+  memset(&tm, 0, sizeof(tm));
+  localtime_r(&t, &tm);
+  printf("%02d:%02d:%02d.%06d ", tm.tm_hour, tm.tm_min, tm.tm_sec,
+         (int)(when64 % 1000000));
+}
+
+
+static double onepass_stddev(long long sumsq, long long sum, long long count) {
+  // Incremental standard deviation calculation, without needing to know the
+  // mean in advance.  See:
+  // http://mathcentral.uregina.ca/QQ/database/QQ.09.02/carlos1.html
+  long long numer = (count * sumsq) - (sum * sum);
+  long long denom = count * (count - 1);
+  return sqrt(DIV(numer, denom));
+}
+
+
+void prepare_tx_packet(struct Session *s) {
+  s->tx.magic = htonl(MAGIC);
+  s->tx.id = htonl(s->next_tx_id++);
+  s->tx.usec_per_pkt = htonl(s->usec_per_pkt);
+  s->tx.txtime = htonl(s->next_send);
+  s->tx.clockdiff = s->start_rtxtime ?
+      htonl(s->start_rxtime - s->start_rtxtime) : 0;
+  s->tx.num_lost = htonl(s->num_lost);
+  s->tx.first_ack = htonl(s->next_txack_index);
+}
+
+static int send_packet(struct Session *s,
+                       int sock,
+                       struct sockaddr *remoteaddr,
+                       socklen_t remoteaddr_len) {
+  // note: tx.acks[] is filled in incrementally; we just transmit the current
+  // state of it here.  The reason we keep a list of the most recent acks is in
+  // case our packet gets lost, so the receiver will have more chances to
+  // receive the timing information for the packets it sent us.
+  if (is_server) {
+    if (sendto(sock, &s->tx, sizeof(s->tx), 0,
+               remoteaddr, remoteaddr_len) < 0) {
+      perror("sendto");
+    }
+  } else {
+    if (send(sock, &s->tx, sizeof(s->tx), 0) < 0) {
+      int e = errno;
+      perror("send");
+      if (e == ECONNREFUSED) return 2;
+    }
+  }
+  s->next_send += s->usec_per_pkt;
+  return 0;
+}
+
+
+void handle_packet(struct Session *s, uint32_t now) {
+  // process the incoming packet header.
+  // Most of the complexity here comes from the fact that the remote
+  // system's clock will be skewed vs. ours.  (We use CLOCK_MONOTONIC
+  // instead of CLOCK_REALTIME, so unless we figure out the skew offset,
+  // it's essentially meaningless to compare the two values.)  We can
+  // however assume that both clocks are ticking at 1 microsecond per
+  // tick... except for inevitable clock rate errors, which we have to
+  // account for occasionally.
+
+  uint32_t txtime = ntohl(s->rx.txtime), rxtime = now;
+  uint32_t id = ntohl(s->rx.id);
+  if (!s->next_rx_id) {
+    // The remote txtime is told to us by the sender, so it is always perfectly
+    // correct... but it uses the sender's clock.
+    s->start_rtxtime = txtime - id * s->usec_per_pkt;
+
+    // The receive time uses our own clock and is estimated by us, so it needs
+    // to be corrected over time because:
+    //   a) the two clocks inevitably run at slightly different speeds;
+    //   b) there's an unknown, variable, network delay between tx and rx.
+    // Here, we're just assigning an initial estimate.
+    s->start_rxtime = rxtime - id * s->usec_per_pkt;
+
+    s->min_cycle_rxdiff = 0;
+    s->next_rx_id = id;
+    s->next_cycle = now + USEC_PER_CYCLE;
+  }
+
+  // see if we missed receiving any previous packets.
+  int32_t tmpdiff = DIFF(id, s->next_rx_id);
+  if (tmpdiff > 0) {
+    // arriving packet has id > expected, so something was lost.
+    // Note that we don't use the rx.acks[] structure to determine packet loss;
+    // that's because the limited size of the array means that, during a longer
+    // outage, we might not see an ack for a packet *even if that packet arrived
+    // safely* at the remote.  So we count on the remote end to count its own
+    // packet losses using sequence numbers, and send that count back to us.  We
+    // do the same here for incoming packets from the remote, and send the error
+    // count back to them next time we're ready to transmit.
+    fprintf(stderr, "lost %ld  expected=%ld  got=%ld\n",
+            (long)tmpdiff, (long)s->next_rx_id, (long)id);
+    s->num_lost += tmpdiff;
+    s->next_rx_id += tmpdiff + 1;
+  } else if (!tmpdiff) {
+    // exactly as expected; good.
+    s->next_rx_id++;
+  } else if (tmpdiff < 0) {
+    // packet before the expected one? weird.
+    fprintf(stderr, "out-of-order packets? %ld\n", (long)tmpdiff);
+  }
+
+  // fix up the clock offset if there's any drift.
+  tmpdiff = DIFF(rxtime, s->start_rxtime + id * s->usec_per_pkt);
+  if (tmpdiff < -20) {
+    // packet arrived before predicted time, so prediction was based on
+    // a packet that was "slow" before, or else one of our clocks is
+    // drifting. Use earliest legitimate start time.
+    fprintf(stderr, "time paradox: backsliding start by %ld usec\n",
+            (long)tmpdiff);
+    s->start_rxtime = rxtime - id * s->usec_per_pkt;
+  }
+  int32_t rxdiff = DIFF(rxtime, s->start_rxtime + id * s->usec_per_pkt);
+
+  // Figure out the offset between our clock and the remote's clock, so we can
+  // calculate the minimum round trip time (rtt). Then, because the consecutive
+  // packets sent in both directions are equally spaced in time, we can figure
+  // out how much a particular packet was delayed in transit - independently in
+  // each direction! This is an advantage over the normal "ping" program which
+  // has no way to tell which direction caused the delay, or which direction
+  // dropped the packet.
+
+  // Our clockdiff is
+  //   (our rx time) - (their tx time)
+  //   == (their rx time + offset) - (their tx time)
+  //   == (their tx time + offset + 1/2 rtt) - (their tx time)
+  //   == offset + 1/2 rtt
+  // and theirs (rx.clockdiff) is:
+  //   (their rx time) - (our tx time)
+  //   == (their rx time) - (their tx time + offset)
+  //   == (their tx time + 1/2 rtt) - (their tx time + offset)
+  //   == 1/2 rtt - offset
+  // So add them together and we get:
+  //   offset + 1/2 rtt + 1/2 rtt - offset
+  //   == rtt
+  // Subtract them and we get:
+  //   offset + 1/2 rtt - 1/2 rtt + offset
+  //   == 2 * offset
+  // ...but that last subtraction is dangerous because if we divide by 2 to get
+  // offset, it doesn't work with 32-bit math, which may have discarded a
+  // high-order bit somewhere along the way.  Instead, we can extract offset
+  // once we have rtt by substituting it into
+  //   clockdiff = offset + 1/2 rtt
+  //   offset = clockdiff - 1/2 rtt
+  // (Dividing rtt by 2 is safe since it's always small and positive.)
+  //
+  // (This example assumes 1/2 rtt in each direction. There's no way to
+  // determine it more accurately than that.)
+  int32_t clockdiff = DIFF(s->start_rxtime, s->start_rtxtime);
+  int32_t rtt = clockdiff + ntohl(s->rx.clockdiff);
+  int32_t offset = DIFF(clockdiff, rtt / 2);
+  if (!ntohl(s->rx.clockdiff)) {
+    // don't print the first packet: it has an invalid clockdiff since the
+    // client can't calculate the clockdiff until it receives at least one
+    // packet from us.
+    s->last_print = now - s->usec_per_print + 1;
+  } else {
+    // not the first packet, so statistics are valid.
+    s->lat_rx_count++;
+    s->lat_rx = rxdiff + rtt/2;
+    s->lat_rx_min = s->lat_rx_min > s->lat_rx ? s->lat_rx : s->lat_rx_min;
+    s->lat_rx_max = s->lat_rx_max < s->lat_rx ? s->lat_rx : s->lat_rx_max;
+    s->lat_rx_sum += s->lat_rx;
+    s->lat_rx_var_sum += s->lat_rx * s->lat_rx;
+  }
+
+  // Note: the way ok_to_print is structured, if there is a dropout in the
+  // connection for more than usec_per_print, we will statistically end up
+  // printing the first packet after the dropout ends.  That one should have the
+  // longest timeout, ie. a "worst case" packet, which is usually the
+  // information you want to see.
+  int ok_to_print = !quiet && DIFF(now, s->last_print) >= s->usec_per_print;
+  if (ok_to_print) {
+    if (want_timestamps) print_timestamp(rxtime);
+    printf("%12s  %6.1f ms rx  (min=%.1f)  loss: %ld/%ld tx  %ld/%ld rx\n",
+           s->last_ackinfo,
+           (rxdiff + rtt/2) / 1000.0,
+           (rtt/2) / 1000.0,
+           (long)ntohl(s->rx.num_lost),
+           (long)s->next_tx_id - 1,
+           (long)s->num_lost,
+           (long)s->next_rx_id - 1);
+    s->last_ackinfo[0] = '\0';
+    s->last_print = now;
+  }
+
+  if (rxdiff < s->min_cycle_rxdiff) s->min_cycle_rxdiff = rxdiff;
+  if (DIFF(now, s->next_cycle) >= 0) {
+    if (s->min_cycle_rxdiff > 0) {
+      fprintf(stderr, "clock skew: sliding start by %ld usec\n",
+              (long)s->min_cycle_rxdiff);
+      s->start_rxtime += s->min_cycle_rxdiff;
+    }
+    s->min_cycle_rxdiff = 0x7fffffff;
+    s->next_cycle += USEC_PER_CYCLE;
+  }
+
+  // schedule this for an ack next time we send the packet
+  s->tx.acks[s->next_txack_index].id = htonl(id);
+  s->tx.acks[s->next_txack_index].rxtime = htonl(rxtime);
+  s->next_txack_index = (s->next_txack_index + 1) % ARRAY_LEN(s->tx.acks);
+
+  // see which of our own transmitted packets have been acked
+  uint32_t first_ack = ntohl(s->rx.first_ack);
+  for (uint32_t i = 0; i < ARRAY_LEN(s->rx.acks); i++) {
+    uint32_t acki = (first_ack + i) % ARRAY_LEN(s->rx.acks);
+    uint32_t ackid = ntohl(s->rx.acks[acki].id);
+    if (!ackid) continue;  // empty slot
+    if (DIFF(ackid, s->next_rxack_id) >= 0) {
+      // an expected ack
+      uint32_t start_txtime = s->next_send - s->next_tx_id * s->usec_per_pkt;
+      uint32_t txtime = start_txtime + ackid * s->usec_per_pkt;
+      uint32_t rrxtime = ntohl(s->rx.acks[acki].rxtime);
+      uint32_t rxtime = rrxtime + offset;
+      // note: already contains 1/2 rtt, unlike rxdiff
+      int32_t txdiff = DIFF(rxtime, txtime);
+      if (s->usec_per_print <= 0 && s->last_ackinfo[0]) {
+        // only print multiple acks per rx if no usec_per_print limit
+        if (want_timestamps) print_timestamp(rxtime);
+        printf("%12s\n", s->last_ackinfo);
+        s->last_ackinfo[0] = '\0';
+      }
+      if (!s->last_ackinfo[0]) {
+        snprintf(s->last_ackinfo, sizeof(s->last_ackinfo), "%6.1f ms tx",
+                 txdiff / 1000.0);
+      }
+      s->next_rxack_id = ackid + 1;
+      s->lat_tx_count++;
+      s->lat_tx = txdiff;
+      s->lat_tx_min = s->lat_tx_min > s->lat_tx ? s->lat_tx : s->lat_tx_min;
+      s->lat_tx_max = s->lat_tx_max < s->lat_tx ? s->lat_tx : s->lat_tx_max;
+      s->lat_tx_sum += s->lat_tx;
+      s->lat_tx_var_sum += s->lat_tx * s->lat_tx;
+    }
+  }
+
+  s->last_rxtime = rxtime;
+}
+
+
+int isoping_main(int argc, char **argv) {
+  struct sockaddr_in6 listenaddr, rxaddr, last_rxaddr;
+  struct sockaddr *remoteaddr = NULL;
+  socklen_t remoteaddr_len = 0, rxaddr_len = 0;
+  struct addrinfo *ai = NULL;
+  int sock = -1;
+
+  setvbuf(stdout, NULL, _IOLBF, 0);
+
+  int c;
+  while ((c = getopt(argc, argv, "f:r:t:qTh?")) >= 0) {
+    switch (c) {
+    case 'f':
+      prints_per_sec = atof(optarg);
+      if (prints_per_sec <= 0) {
+        fprintf(stderr, "%s: lines per second must be >= 0\n", argv[0]);
+        return 99;
+      }
+      break;
+    case 'r':
+      packets_per_sec = atof(optarg);
+      if (packets_per_sec < 0.001 || packets_per_sec > 1e6) {
+        fprintf(stderr, "%s: packets per sec (-r) must be 0.001..1000000\n",
+                argv[0]);
+        return 99;
+      }
+      break;
+    case 't':
+      ttl = atoi(optarg);
+      if (ttl < 1) {
+        fprintf(stderr, "%s: ttl must be >= 1\n", argv[0]);
+        return 99;
+      }
+      break;
+    case 'q':
+      quiet = 1;
+      break;
+    case 'T':
+      want_timestamps = 1;
+      break;
+    case 'h':
+    case '?':
+    default:
+      usage_and_die(argv[0]);
+      break;
+    }
+  }
+
+  sock = socket(PF_INET6, SOCK_DGRAM, 0);
+  if (sock < 0) {
+    perror("socket");
+    return 1;
+  }
+
+  if (argc - optind == 0) {
+    is_server = 1;
+    memset(&listenaddr, 0, sizeof(listenaddr));
+    listenaddr.sin6_family = AF_INET6;
+    listenaddr.sin6_port = htons(SERVER_PORT);
+    if (bind(sock, (struct sockaddr *)&listenaddr, sizeof(listenaddr)) != 0) {
+      perror("bind");
+      return 1;
+    }
+    socklen_t addrlen = sizeof(listenaddr);
+    if (getsockname(sock, (struct sockaddr *)&listenaddr, &addrlen) != 0) {
+      perror("getsockname");
+      return 1;
+    }
+    fprintf(stderr, "server listening at [%s]:%d\n",
+           sockaddr_to_str((struct sockaddr *)&listenaddr),
+           ntohs(listenaddr.sin6_port));
+  } else if (argc - optind == 1) {
+    const char *remotename = argv[optind];
+    is_server = 0;
+    struct addrinfo hints;
+    memset(&hints, 0, sizeof(hints));
+    hints.ai_flags = AI_ADDRCONFIG | AI_V4MAPPED;
+    hints.ai_family = AF_INET6;
+    hints.ai_socktype = SOCK_DGRAM;
+    int err = getaddrinfo(remotename, STR(SERVER_PORT), &hints, &ai);
+    if (err != 0 || !ai) {
+      fprintf(stderr, "getaddrinfo(%s): %s\n", remotename, gai_strerror(err));
+      return 1;
+    }
+    fprintf(stderr, "connecting to %s...\n", sockaddr_to_str(ai->ai_addr));
+    if (connect(sock, ai->ai_addr, ai->ai_addrlen) != 0) {
+      perror("connect");
+      return 1;
+    }
+    remoteaddr = ai->ai_addr;
+    remoteaddr_len = ai->ai_addrlen;
+  } else {
+    usage_and_die(argv[0]);
+  }
+
+  fprintf(stderr, "using ttl=%d\n", ttl);
+  // IPPROTO_IPV6 is the only one that works on MacOS, and is arguably the
+  // technically correct thing to do since it's an AF_INET6 socket.
+  if (setsockopt(sock, IPPROTO_IPV6, IP_TTL, &ttl, sizeof(ttl))) {
+    perror("setsockopt(TTLv6)");
+    return 1;
+  }
+  // ...but in Linux (at least 3.13), IPPROTO_IPV6 does not actually
+  // set the TTL if the IPv6 socket ends up going over IPv4.  We have to
+  // set that separately.  On MacOS, that always returns EINVAL, so ignore
+  // the error if that happens.
+  if (setsockopt(sock, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl))) {
+    if (errno != EINVAL) {
+      perror("setsockopt(TTLv4)");
+      return 1;
+    }
+  }
+
+  uint32_t now = ustime();       // current time
+
+  struct sigaction act;
+  memset(&act, 0, sizeof(act));
+  act.sa_handler = sighandler;
+  act.sa_flags = SA_RESETHAND;
+  sigaction(SIGINT, &act, NULL);
+
+  struct Session s(now);
+
+  while (!want_to_die) {
+    fd_set rfds;
+    FD_ZERO(&rfds);
+    FD_SET(sock, &rfds);
+    struct timeval tv;
+    tv.tv_sec = 0;
+
+    now = ustime();
+    if (DIFF(s.next_send, now) < 0) {
+      tv.tv_usec = 0;
+    } else {
+      tv.tv_usec = DIFF(s.next_send, now);
+    }
+    int nfds = select(sock + 1, &rfds, NULL, NULL, remoteaddr ? &tv : NULL);
+    now = ustime();
+    if (nfds < 0 && errno != EINTR) {
+      perror("select");
+      return 1;
+    }
+
+    // time to send the next packet?
+    if (remoteaddr && DIFF(now, s.next_send) >= 0) {
+      prepare_tx_packet(&s);
+      int err = send_packet(&s, sock, remoteaddr, remoteaddr_len);
+      if (err != 0) {
+        return err;
+      }
+      // TODO(pmccurdy): Track disconnections across multiple clients.  Use
+      // recvmsg with the MSG_ERRQUEUE flag to detect connection refused.
+      if (is_server && DIFF(now, s.last_rxtime) > 60*1000*1000) {
+        fprintf(stderr, "client disconnected.\n");
+        remoteaddr = NULL;
+      }
+    }
+
+    if (nfds > 0) {
+      // incoming packet
+      rxaddr_len = sizeof(rxaddr);
+      ssize_t got = recvfrom(sock, &s.rx, sizeof(s.rx), 0,
+                             (struct sockaddr *)&rxaddr, &rxaddr_len);
+      if (got < 0) {
+        int e = errno;
+        perror("recvfrom");
+        if (!is_server && e == ECONNREFUSED) return 2;
+        continue;
+      }
+      if (got != sizeof(s.rx) || s.rx.magic != htonl(MAGIC)) {
+        fprintf(stderr, "got invalid packet of length %ld\n", (long)got);
+        continue;
+      }
+
+      // is it a new client?
+      if (is_server) {
+        // TODO(pmccurdy): Maintain a hash table of Sessions, look up based
+        // on rxaddr, create a new one if necessary, remove this resetting code.
+        if (!remoteaddr ||
+            memcmp(&rxaddr, &last_rxaddr, sizeof(rxaddr)) != 0) {
+          fprintf(stderr, "new client connected: %s\n",
+                  sockaddr_to_str((struct sockaddr *)&rxaddr));
+          memcpy(&last_rxaddr, &rxaddr, sizeof(rxaddr));
+          remoteaddr = (struct sockaddr *)&last_rxaddr;
+          remoteaddr_len = rxaddr_len;
+
+          s.next_send = now + 10*1000;
+          s.next_tx_id = 1;
+          s.next_rx_id = s.next_rxack_id = 0;
+          s.start_rtxtime = s.start_rxtime = 0;
+          s.num_lost = 0;
+          s.next_txack_index = 0;
+          s.usec_per_pkt = ntohl(s.rx.usec_per_pkt);
+          memset(&s.tx, 0, sizeof(s.tx));
+        }
+      }
+
+      handle_packet(&s, now);
+    }
+  }
+
+  // TODO(pmccurdy): Separate out per-client and global stats.
+  printf("\n---\n");
+  printf("tx: min/avg/max/mdev = %.2f/%.2f/%.2f/%.2f ms\n",
+         s.lat_tx_min / 1000.0,
+         DIV(s.lat_tx_sum, s.lat_tx_count) / 1000.0,
+         s.lat_tx_max / 1000.0,
+         onepass_stddev(
+             s.lat_tx_var_sum, s.lat_tx_sum, s.lat_tx_count) / 1000.0);
+  printf("rx: min/avg/max/mdev = %.2f/%.2f/%.2f/%.2f ms\n",
+         s.lat_rx_min / 1000.0,
+         DIV(s.lat_rx_sum, s.lat_rx_count) / 1000.0,
+         s.lat_rx_max / 1000.0,
+         onepass_stddev(
+             s.lat_rx_var_sum, s.lat_rx_sum, s.lat_rx_count) / 1000.0);
+  printf("\n");
+
+  if (ai) freeaddrinfo(ai);
+  if (sock >= 0) close(sock);
+  return 0;
+}
diff --git a/cmds/isoping.h b/cmds/isoping.h
new file mode 100644
index 0000000..52fcccb
--- /dev/null
+++ b/cmds/isoping.h
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * 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.
+ */
+#ifndef ISOPING_H
+#define ISOPING_H
+
+#include <stdint.h>
+
+// Layout of the UDP packets exchanged between client and server.
+// All integers are in network byte order.
+// Packets have exactly the same structure in both directions.
+struct Packet {
+  uint32_t magic;     // magic number to reject bogus packets
+  uint32_t id;        // sequential packet id number
+  uint32_t txtime;    // transmitter's monotonic time when pkt was sent
+  uint32_t clockdiff; // estimate of (transmitter's clk) - (receiver's clk)
+  uint32_t usec_per_pkt; // microseconds of delay between packets
+  uint32_t num_lost;  // number of pkts transmitter expected to get but didn't
+  uint32_t first_ack; // starting index in acks[] circular buffer
+  struct {
+    // txtime==0 for empty elements in this array.
+    uint32_t id;      // id field from a received packet
+    uint32_t rxtime;  // receiver's monotonic time when pkt arrived
+  } acks[64];
+};
+
+
+// Data we track per session.
+struct Session {
+  Session(uint32_t now);
+  int32_t usec_per_pkt;
+  int32_t usec_per_print;
+
+  // WARNING: lots of math below relies on well-defined uint32/int32
+  // arithmetic overflow behaviour, plus the fact that when we subtract
+  // two successive timestamps (for example) they will be less than 2^31
+  // microseconds apart.  It would be safer to just use 64-bit values
+  // everywhere, but that would cut the number of acks-per-packet in half,
+  // which would be unfortunate.
+  uint32_t next_tx_id;       // id field for next transmit
+  uint32_t next_rx_id;       // expected id field for next receive
+  uint32_t next_rxack_id;    // expected ack.id field in next received ack
+  uint32_t start_rtxtime;    // remote's txtime at startup
+  uint32_t start_rxtime;     // local rxtime at startup
+  uint32_t last_rxtime;      // local rxtime of last received packet
+  int32_t min_cycle_rxdiff;  // smallest packet delay seen this cycle
+  uint32_t next_cycle;       // time when next cycle begins
+  uint32_t next_send;        // time when we'll send next pkt
+  uint32_t num_lost;         // number of rx packets not received
+  int next_txack_index;      // next array item to fill in tx.acks
+  struct Packet tx, rx;      // transmit and received packet buffers
+  char last_ackinfo[128];    // human readable format of latest ack
+  uint32_t last_print;       // time of last packet printout
+  // Packet statistics counters for transmit and receive directions.
+  long long lat_tx, lat_tx_min, lat_tx_max,
+      lat_tx_count, lat_tx_sum, lat_tx_var_sum;
+  long long lat_rx, lat_rx_min, lat_rx_max,
+      lat_rx_count, lat_rx_sum, lat_rx_var_sum;
+};
+
+// Process the Session's incoming packet, from s->rx.
+void handle_packet(struct Session *s, uint32_t now);
+
+// Sets all the elements of s->tx to be ready to be sent to the other side.
+void prepare_tx_packet(struct Session *s);
+
+// Parses arguments and runs the main loop.  Distinct from main() for unit test
+// purposes.
+int isoping_main(int argc, char **argv);
+
+#endif
diff --git a/cmds/isoping_main.cc b/cmds/isoping_main.cc
new file mode 100644
index 0000000..521b5e1
--- /dev/null
+++ b/cmds/isoping_main.cc
@@ -0,0 +1,5 @@
+#include "isoping.h"
+
+int main(int argc, char **argv) {
+  return isoping_main(argc, argv);
+}
diff --git a/cmds/isoping_test.cc b/cmds/isoping_test.cc
new file mode 100644
index 0000000..84edc46
--- /dev/null
+++ b/cmds/isoping_test.cc
@@ -0,0 +1,420 @@
+/*
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * 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.
+ */
+
+#include <arpa/inet.h>
+#include <limits.h>
+#include <stdio.h>
+
+#include <wvtest.h>
+
+#include "isoping.h"
+
+uint32_t send_next_packet(Session *from, uint32_t from_base,
+                          Session *to, uint32_t to_base, uint32_t latency) {
+  uint32_t t = from->next_send - from_base;
+  prepare_tx_packet(from);
+  to->rx = from->tx;
+  from->next_send += from->usec_per_pkt;
+  t += latency;
+  handle_packet(to, to_base + t);
+  fprintf(stderr,
+          "**Sent packet: txtime=%d, start_txtime=%d, rxtime=%d, "
+          "start_rxtime=%d, latency=%d, t_from=%d, t_to=%d\n",
+          from->next_send,
+          to->start_rtxtime,
+          to_base + t,
+          to->start_rxtime,
+          latency,
+          t - latency,
+          t);
+
+  return t;
+}
+
+WVTEST_MAIN("isoping algorithm logic") {
+  // Establish a positive base time for client and server.  This is conceptually
+  // the instant when the client sends its first message to the server, as
+  // measured by the clocks on each side (note: this is before the server
+  // receives the message).
+  uint32_t cbase = 400 * 1000;
+  uint32_t sbase = 600 * 1000;
+  uint32_t real_clockdiff = sbase - cbase;
+
+  // The states of the client and server.
+  struct Session c(cbase);
+  struct Session s(sbase);
+
+  // One-way latencies: cs_latency is the latency from client to server;
+  // sc_latency is from server to client.
+  uint32_t cs_latency = 24 * 1000;
+  uint32_t sc_latency = 25 * 1000;
+  uint32_t half_rtt = (sc_latency + cs_latency) / 2;
+
+  // Elapsed time, relative to the base time for each clock.
+  uint32_t t = 0;
+
+  // Send the initial packet from client to server.  This isn't enough to let us
+  // draw any useful latency conclusions.
+  // TODO(pmccurdy): Setting next_send is duplicating some work done in the main
+  // loop / send_packet.  Extract that into somewhere testable, then test it.
+  c.next_send = cbase;
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency);
+  uint32_t rxtime = sbase + t;
+  s.next_send = rxtime + 10 * 1000;
+
+  printf("last_rxtime: %d\n", s.last_rxtime);
+  printf("min_cycle_rxdiff: %d\n", s.min_cycle_rxdiff);
+  WVPASSEQ(s.rx.clockdiff, 0);
+  WVPASSEQ(s.last_rxtime, rxtime);
+  WVPASSEQ(s.min_cycle_rxdiff, 0);
+  WVPASSEQ(ntohl(s.tx.acks[0].id), 1);
+  WVPASSEQ(s.next_txack_index, 1);
+  WVPASSEQ(ntohl(s.tx.acks[ntohl(s.tx.first_ack)].id), 1);
+  WVPASSEQ(ntohl(s.tx.acks[ntohl(s.tx.first_ack)].rxtime), rxtime);
+  WVPASSEQ(s.start_rxtime, rxtime - c.usec_per_pkt);
+  WVPASSEQ(s.start_rtxtime, cbase - c.usec_per_pkt);
+  WVPASSEQ(s.next_send, rxtime + 10 * 1000);
+
+  // Reply to the client.
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency);
+
+  // Now we have enough data to figure out latencies on the client.
+  rxtime = cbase + t;
+  WVPASSEQ(c.start_rxtime, rxtime - s.usec_per_pkt);
+  WVPASSEQ(c.start_rtxtime, sbase + cs_latency + 10 * 1000 - s.usec_per_pkt);
+  WVPASSEQ(c.min_cycle_rxdiff, 0);
+  WVPASSEQ(ntohl(c.rx.clockdiff), sbase - cbase + cs_latency);
+  WVPASSEQ(ntohl(c.tx.acks[ntohl(c.tx.first_ack)].id), 1);
+  WVPASSEQ(ntohl(c.tx.acks[ntohl(c.tx.first_ack)].rxtime), rxtime);
+  WVPASSEQ(c.num_lost, 0);
+  WVPASSEQ(c.lat_tx_count, 1);
+  WVPASSEQ(c.lat_tx, half_rtt);
+  WVPASSEQ(c.lat_rx_count, 1);
+  WVPASSEQ(c.lat_rx, half_rtt);
+  WVPASSEQ(c.num_lost, 0);
+
+  // Round 2
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency);
+  rxtime = sbase + t;
+
+  // Now the server also knows latencies.
+  WVPASSEQ(s.start_rxtime, sbase + cs_latency - s.usec_per_pkt);
+  WVPASSEQ(s.start_rtxtime, cbase - c.usec_per_pkt);
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency);
+  WVPASSEQ(ntohl(s.tx.acks[ntohl(s.tx.first_ack)].id), 2);
+  WVPASSEQ(ntohl(s.tx.acks[ntohl(s.tx.first_ack)].rxtime), rxtime);
+  WVPASSEQ(s.num_lost, 0);
+  WVPASSEQ(s.lat_tx_count, 1);
+  WVPASSEQ(s.lat_tx, half_rtt);
+  WVPASSEQ(s.lat_rx_count, 1);
+  WVPASSEQ(s.lat_rx, half_rtt);
+  WVPASSEQ(s.num_lost, 0);
+
+  // Increase the latencies in both directions, reply to client.
+  int32_t latency_diff = 10 * 1000;
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency + latency_diff);
+
+  rxtime = cbase + t;
+  WVPASSEQ(ntohl(s.tx.clockdiff), real_clockdiff + cs_latency);
+  WVPASSEQ(c.start_rxtime,
+           rxtime - ntohl(s.tx.id) * s.usec_per_pkt - latency_diff);
+  WVPASSEQ(c.start_rtxtime, sbase + cs_latency + 10 * 1000 - s.usec_per_pkt);
+  WVPASSEQ(ntohl(c.tx.acks[ntohl(c.tx.first_ack)].id), 2);
+  WVPASSEQ(ntohl(c.tx.acks[ntohl(c.tx.first_ack)].rxtime), rxtime);
+  WVPASSEQ(c.num_lost, 0);
+  WVPASSEQ(c.lat_tx_count, 2);
+  WVPASSEQ(c.lat_tx, half_rtt);
+  WVPASSEQ(c.lat_rx_count, 2);
+  WVPASSEQ(c.lat_rx, half_rtt + latency_diff);
+  WVPASSEQ(c.num_lost, 0);
+
+  // Client replies with increased latency, server notices.
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency + latency_diff);
+
+  rxtime = sbase + t;
+  WVPASSEQ(ntohl(c.tx.clockdiff), - real_clockdiff + sc_latency);
+  WVPASSEQ(s.start_rxtime, sbase + cs_latency - s.usec_per_pkt);
+  WVPASSEQ(s.start_rtxtime, cbase - c.usec_per_pkt);
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency);
+  WVPASSEQ(ntohl(s.tx.acks[ntohl(s.tx.first_ack)].id), 3);
+  WVPASSEQ(ntohl(s.tx.acks[ntohl(s.tx.first_ack)].rxtime), rxtime);
+  WVPASSEQ(s.num_lost, 0);
+  WVPASSEQ(s.lat_tx_count, 2);
+  WVPASSEQ(s.lat_tx, half_rtt + latency_diff);
+  WVPASSEQ(s.lat_rx_count, 2);
+  WVPASSEQ(s.lat_rx, half_rtt + latency_diff);
+  WVPASSEQ(s.num_lost, 0);
+
+  // Lose a server->client packet, send the next client->server packet, verify
+  // only the received packets were acked.
+  s.next_send += s.usec_per_pkt;
+  s.next_tx_id++;
+
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency + latency_diff);
+
+  rxtime = sbase + t;
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency);
+  WVPASSEQ(ntohl(s.tx.acks[ntohl(s.tx.first_ack)].id), 3);
+  WVPASSEQ(ntohl(s.tx.acks[ntohl(s.tx.first_ack)].rxtime),
+           rxtime - s.usec_per_pkt);
+  WVPASSEQ(s.num_lost, 0);
+  WVPASSEQ(s.lat_tx_count, 2);
+  WVPASSEQ(s.lat_tx, half_rtt + latency_diff);
+  WVPASSEQ(s.lat_rx_count, 3);
+  WVPASSEQ(s.lat_rx, half_rtt + latency_diff);
+  WVPASSEQ(s.num_lost, 0);
+
+  // Remove the extra latency from server->client, send the next packet, have
+  // the client receive it and notice the lost packet and reduced latency.
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency);
+
+  rxtime = cbase + t;
+  WVPASSEQ(ntohl(c.tx.acks[ntohl(c.tx.first_ack)].id), 4);
+  WVPASSEQ(ntohl(c.tx.acks[ntohl(c.tx.first_ack)].rxtime), rxtime);
+  WVPASSEQ(c.num_lost, 1);
+  WVPASSEQ(c.lat_tx_count, 4);
+  WVPASSEQ(c.lat_tx, half_rtt + latency_diff);
+  WVPASSEQ(c.lat_rx_count, 3);
+  WVPASSEQ(c.lat_rx, half_rtt);
+  WVPASSEQ(c.num_lost, 1);
+
+  // A tiny reduction in latency shows up in min_cycle_rxdiff.
+  latency_diff = 0;
+  int32_t latency_mini_diff = -15;
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency + latency_mini_diff);
+
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency);
+  WVPASSEQ(s.min_cycle_rxdiff, latency_mini_diff);
+  WVPASSEQ(s.start_rxtime, sbase + cs_latency - s.usec_per_pkt);
+  WVPASSEQ(s.lat_tx, half_rtt);
+  WVPASSEQ(s.lat_rx, half_rtt + latency_mini_diff);
+
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency + latency_mini_diff);
+
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency);
+  WVPASSEQ(c.min_cycle_rxdiff, latency_mini_diff);
+  WVPASSEQ(c.lat_tx, half_rtt + latency_mini_diff);
+  WVPASSEQ(c.lat_rx, half_rtt + latency_mini_diff);
+
+  // Reduce the latency dramatically, verify that both sides see it, and the
+  // start time is modified (not the min_cycle_rxdiff).
+  latency_diff = -22 * 1000;
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency + latency_diff);
+
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency);
+  WVPASSEQ(s.min_cycle_rxdiff, latency_mini_diff);
+  // We see half the latency diff applied to each side of the connection because
+  // the reduction in latency creates a time paradox, rebasing the start time
+  // and recalculating the RTT.
+  WVPASSEQ(s.start_rxtime, sbase + cs_latency + latency_diff - s.usec_per_pkt);
+  WVPASSEQ(s.lat_tx, half_rtt + latency_diff/2 + latency_mini_diff);
+  WVPASSEQ(s.lat_rx, half_rtt + latency_diff/2);
+
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency + latency_diff);
+
+  // Now we see the new latency applied to both sides.
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency);
+  WVPASSEQ(c.min_cycle_rxdiff, latency_mini_diff);
+  WVPASSEQ(c.lat_tx, half_rtt + latency_diff);
+  WVPASSEQ(c.lat_rx, half_rtt + latency_diff);
+
+  // Restore latency on one side of the connection, verify that we track it on
+  // only one side and we've improved our clock sync.
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency);
+
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency + latency_diff);
+  WVPASSEQ(s.lat_tx, half_rtt + latency_diff);
+  WVPASSEQ(s.lat_rx, half_rtt);
+
+  // And double-check that the other side also sees the improved clock sync and
+  // one-sided latency on the correct side.
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency + latency_diff);
+
+  WVPASSEQ(ntohl(c.rx.clockdiff), sbase - cbase + cs_latency + latency_diff);
+  WVPASSEQ(c.lat_tx, half_rtt);
+  WVPASSEQ(c.lat_rx, half_rtt + latency_diff);
+}
+
+// Verify that isoping handles clocks ticking at different rates.
+WVTEST_MAIN("isoping clock drift") {
+  uint32_t cbase = 1400 * 1000;
+  uint32_t sbase = 1600 * 1000;
+
+  // The states of the client and server.
+  struct Session c(cbase);
+  struct Session s(sbase);
+  // Send packets infrequently, to get new cycles more often.
+  s.usec_per_pkt = 1 * 1000 * 1000;
+  c.usec_per_pkt = 1 * 1000 * 1000;
+
+  // One-way latencies: cs_latency is the latency from client to server;
+  // sc_latency is from server to client.
+  int32_t cs_latency = 4 * 1000;
+  int32_t sc_latency = 5 * 1000;
+  int32_t drift_per_round = 15;
+  uint32_t half_rtt = (sc_latency + cs_latency) / 2;
+
+  // Perform the initial setup.
+  c.next_send = cbase;
+  uint32_t t = send_next_packet(&c, cbase, &s, sbase, cs_latency);
+  s.next_send = sbase + t + 10 * 1000;
+
+  uint32_t orig_server_start_rxtime = s.start_rxtime;
+  WVPASSEQ(s.start_rxtime, sbase + cs_latency - s.usec_per_pkt);
+  WVPASSEQ(s.start_rtxtime, cbase - c.usec_per_pkt);
+  WVPASSEQ(ntohl(s.rx.clockdiff), 0);
+  WVPASSEQ(s.lat_rx, 0);
+  WVPASSEQ(s.lat_tx, 0);
+  WVPASSEQ(s.min_cycle_rxdiff, 0);
+
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency);
+
+  uint32_t orig_client_start_rxtime = c.start_rxtime;
+  WVPASSEQ(c.start_rxtime, cbase + 2 * half_rtt + 10 * 1000 - c.usec_per_pkt);
+  WVPASSEQ(c.start_rtxtime, sbase + cs_latency + 10 * 1000 - c.usec_per_pkt);
+  WVPASSEQ(ntohl(c.rx.clockdiff), sbase - cbase + cs_latency);
+  WVPASSEQ(c.lat_rx, half_rtt);
+  WVPASSEQ(c.lat_tx, half_rtt);
+  WVPASSEQ(c.min_cycle_rxdiff, 0);
+
+  // Clock drift shows up as symmetric changes in one-way latency.
+  int32_t total_drift = drift_per_round;
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency + total_drift);
+
+  WVPASSEQ(s.start_rxtime, orig_server_start_rxtime);
+  WVPASSEQ(s.start_rtxtime, cbase - c.usec_per_pkt);
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency);
+  WVPASSEQ(s.lat_rx, half_rtt + total_drift);
+  WVPASSEQ(s.lat_tx, half_rtt);
+  WVPASSEQ(s.min_cycle_rxdiff, 0);
+
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency - total_drift);
+
+  WVPASSEQ(c.start_rxtime, cbase + 2 * half_rtt + 10 * 1000 - c.usec_per_pkt);
+  WVPASSEQ(c.start_rtxtime,
+           sbase + cs_latency + 10 * 1000 - c.usec_per_pkt);
+  WVPASSEQ(ntohl(c.rx.clockdiff), sbase - cbase + cs_latency);
+  WVPASSEQ(c.lat_rx, half_rtt - total_drift);
+  WVPASSEQ(c.lat_tx, half_rtt + total_drift);
+  WVPASSEQ(c.min_cycle_rxdiff, -drift_per_round);
+
+  // Once we exceed -20us of drift, we adjust the client's start_rxtime.
+  total_drift += drift_per_round;
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency + total_drift);
+
+  WVPASSEQ(s.start_rxtime, orig_server_start_rxtime);
+  WVPASSEQ(s.start_rtxtime, cbase - c.usec_per_pkt);
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency);
+  WVPASSEQ(s.lat_rx, half_rtt + total_drift);
+  WVPASSEQ(s.lat_tx, half_rtt - drift_per_round);
+  WVPASSEQ(s.min_cycle_rxdiff, 0);
+
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency - total_drift);
+
+  int32_t clock_adj = total_drift;
+  WVPASSEQ(c.start_rxtime,
+           cbase + 2 * half_rtt + 10 * 1000 - c.usec_per_pkt - total_drift);
+  WVPASSEQ(c.start_rtxtime, sbase + cs_latency + 10 * 1000 - c.usec_per_pkt);
+  WVPASSEQ(ntohl(c.rx.clockdiff), sbase - cbase + cs_latency);
+  WVPASSEQ(c.lat_rx, half_rtt - drift_per_round);
+  WVPASSEQ(c.lat_tx, half_rtt + drift_per_round);
+  WVPASSEQ(c.min_cycle_rxdiff, -drift_per_round);
+
+  // Skip ahead to the next cycle.
+  int packets_to_skip = 8;
+  s.next_send += packets_to_skip * s.usec_per_pkt;
+  s.next_rx_id += packets_to_skip;
+  s.next_tx_id += packets_to_skip;
+  c.next_send += packets_to_skip * c.usec_per_pkt;
+  c.next_rx_id += packets_to_skip;
+  c.next_tx_id += packets_to_skip;
+  total_drift += packets_to_skip * drift_per_round;
+
+  // At first we blame the rx latency for most of the drift.
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency + total_drift);
+
+  // start_rxtime doesn't change here as the first cycle suppresses positive
+  // min_cycle_rxdiff values.
+  // TODO(pmccurdy): Should it?
+  WVPASSEQ(s.start_rxtime, orig_server_start_rxtime);
+  WVPASSEQ(s.start_rtxtime, cbase - c.usec_per_pkt);
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency - clock_adj);
+  WVPASSEQ(s.lat_rx, half_rtt + total_drift - drift_per_round);
+  WVPASSEQ(s.lat_tx, half_rtt - drift_per_round);
+  WVPASSEQ(s.min_cycle_rxdiff, INT_MAX);
+
+  // After one round-trip, we divide the blame for the latency diff evenly.
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency - total_drift);
+
+  WVPASSEQ(c.start_rxtime, orig_client_start_rxtime - total_drift);
+  WVPASSEQ(c.start_rtxtime, sbase + cs_latency + 10 * 1000 - c.usec_per_pkt);
+  WVPASSEQ(ntohl(c.rx.clockdiff), sbase - cbase + cs_latency);
+  WVPASSEQ(c.lat_rx, half_rtt - total_drift / 2);
+  WVPASSEQ(c.lat_tx, half_rtt + total_drift / 2);
+  WVPASSEQ(c.min_cycle_rxdiff, INT_MAX);
+
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency + total_drift);
+
+  WVPASSEQ(s.start_rxtime, orig_server_start_rxtime);
+  WVPASSEQ(s.start_rtxtime, cbase - c.usec_per_pkt);
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency - total_drift);
+  WVPASSEQ(s.lat_rx, half_rtt + total_drift / 2);
+  WVPASSEQ(s.lat_tx, half_rtt - total_drift / 2);
+  // We also notice the difference in expected arrival times on the server...
+  WVPASSEQ(s.min_cycle_rxdiff, total_drift);
+
+  total_drift += drift_per_round;
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency - total_drift);
+  // And on the client.  The client doesn't notice the total_drift rxdiff as it
+  // was swallowed by the new cycle.
+  WVPASSEQ(c.min_cycle_rxdiff, -drift_per_round);
+
+  // Skip ahead to the next cycle.
+  packets_to_skip = 8;
+  s.next_send += packets_to_skip * s.usec_per_pkt;
+  s.next_rx_id += packets_to_skip;
+  s.next_tx_id += packets_to_skip;
+  c.next_send += packets_to_skip * c.usec_per_pkt;
+  c.next_rx_id += packets_to_skip;
+  c.next_tx_id += packets_to_skip;
+  total_drift += packets_to_skip * drift_per_round;
+  int32_t drift_per_cycle = 10 * drift_per_round;
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency + total_drift);
+
+  // The clock drift has worked its way into the RTT calculation.
+  half_rtt = (cs_latency + sc_latency - drift_per_cycle) / 2;
+
+  // Now start_rxtime has updated.
+  WVPASSEQ(s.start_rxtime, orig_server_start_rxtime + drift_per_cycle);
+  WVPASSEQ(s.start_rtxtime, cbase - c.usec_per_pkt);
+  WVPASSEQ(ntohl(s.rx.clockdiff),
+           cbase - sbase + sc_latency - drift_per_cycle);
+  WVPASSEQ(s.lat_rx, half_rtt + total_drift);
+  WVPASSEQ(s.lat_tx, half_rtt - drift_per_round);
+  WVPASSEQ(s.min_cycle_rxdiff, INT_MAX);
+
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency - total_drift);
+
+  WVPASSEQ(c.start_rxtime, orig_client_start_rxtime - total_drift);
+  WVPASSEQ(c.start_rtxtime, sbase + cs_latency + 10 * 1000 - c.usec_per_pkt);
+  WVPASSEQ(ntohl(c.rx.clockdiff), sbase - cbase + cs_latency + drift_per_cycle);
+  WVPASSEQ(c.lat_rx, half_rtt + drift_per_round / 2);
+  WVPASSEQ(c.lat_tx, half_rtt + total_drift / 2 + 1);
+  WVPASSEQ(c.min_cycle_rxdiff, INT_MAX);
+
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency + total_drift);
+
+}
diff --git a/wvtest/.gitignore b/wvtest/.gitignore
new file mode 100644
index 0000000..2a7de39
--- /dev/null
+++ b/wvtest/.gitignore
@@ -0,0 +1,10 @@
+*~
+*.o
+*.a
+*.lib
+*.dll
+*.exe
+*.so
+*.so.*
+*.pyc
+*.mdb
diff --git a/wvtest/LICENSE b/wvtest/LICENSE
new file mode 100644
index 0000000..eb685a5
--- /dev/null
+++ b/wvtest/LICENSE
@@ -0,0 +1,481 @@
+		  GNU LIBRARY GENERAL PUBLIC LICENSE
+		       Version 2, June 1991
+
+ Copyright (C) 1991 Free Software Foundation, Inc.
+                    675 Mass Ave, Cambridge, MA 02139, USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the library GPL.  It is
+ numbered 2 because it goes with version 2 of the ordinary GPL.]
+
+			    Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Library General Public License, applies to some
+specially designated Free Software Foundation software, and to any
+other libraries whose authors decide to use it.  You can use it for
+your libraries, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if
+you distribute copies of the library, or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link a program with the library, you must provide
+complete object files to the recipients so that they can relink them
+with the library, after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  Our method of protecting your rights has two steps: (1) copyright
+the library, and (2) offer you this license which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  Also, for each distributor's protection, we want to make certain
+that everyone understands that there is no warranty for this free
+library.  If the library is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original
+version, so that any problems introduced by others will not reflect on
+the original authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that companies distributing free
+software will individually obtain patent licenses, thus in effect
+transforming the program into proprietary software.  To prevent this,
+we have made it clear that any patent must be licensed for everyone's
+free use or not licensed at all.
+
+  Most GNU software, including some libraries, is covered by the ordinary
+GNU General Public License, which was designed for utility programs.  This
+license, the GNU Library General Public License, applies to certain
+designated libraries.  This license is quite different from the ordinary
+one; be sure to read it in full, and don't assume that anything in it is
+the same as in the ordinary license.
+
+  The reason we have a separate public license for some libraries is that
+they blur the distinction we usually make between modifying or adding to a
+program and simply using it.  Linking a program with a library, without
+changing the library, is in some sense simply using the library, and is
+analogous to running a utility program or application program.  However, in
+a textual and legal sense, the linked executable is a combined work, a
+derivative of the original library, and the ordinary General Public License
+treats it as such.
+
+  Because of this blurred distinction, using the ordinary General
+Public License for libraries did not effectively promote software
+sharing, because most developers did not use the libraries.  We
+concluded that weaker conditions might promote sharing better.
+
+  However, unrestricted linking of non-free programs would deprive the
+users of those programs of all benefit from the free status of the
+libraries themselves.  This Library General Public License is intended to
+permit developers of non-free programs to use free libraries, while
+preserving your freedom as a user of such programs to change the free
+libraries that are incorporated in them.  (We have not seen how to achieve
+this as regards changes in header files, but we have achieved it as regards
+changes in the actual functions of the Library.)  The hope is that this
+will lead to faster development of free libraries.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, while the latter only
+works together with the library.
+
+  Note that it is possible for a library to be covered by the ordinary
+General Public License rather than by this special one.
+
+		  GNU LIBRARY GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library which
+contains a notice placed by the copyright holder or other authorized
+party saying it may be distributed under the terms of this Library
+General Public License (also called "this License").  Each licensee is
+addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+  
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+  6. As an exception to the Sections above, you may also compile or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    c) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    d) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the source code distributed need not include anything that is normally
+distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Library General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+			    NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+		     END OF TERMS AND CONDITIONS
+
+     Appendix: How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public
+    License along with this library; if not, write to the Free
+    Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/wvtest/Makefile b/wvtest/Makefile
new file mode 100644
index 0000000..8263908
--- /dev/null
+++ b/wvtest/Makefile
@@ -0,0 +1,29 @@
+
+all: build
+	@echo
+	@echo "Try: make test"
+
+build:
+	$(MAKE) -C dotnet all
+	$(MAKE) -C cpp all
+	$(MAKE) -C c all
+
+runtests: build
+	$(MAKE) -C sh runtests
+	$(MAKE) -C python runtests
+	$(MAKE) -C dotnet runtests
+	$(MAKE) -C cpp runtests
+	$(MAKE) -C c runtests
+	$(MAKE) -C javascript runtests
+
+
+test: build
+	./wvtestrun $(MAKE) runtests
+
+clean::
+	rm -f *~ .*~
+	$(MAKE) -C sh clean
+	$(MAKE) -C python clean
+	$(MAKE) -C dotnet clean
+	$(MAKE) -C cpp clean
+	$(MAKE) -C c clean
diff --git a/wvtest/README b/wvtest/README
new file mode 100644
index 0000000..824114a
--- /dev/null
+++ b/wvtest/README
@@ -0,0 +1,375 @@
+
+WvTest: the dumbest cross-platform test framework that could possibly work
+==========================================================================
+
+I have a problem with your unit testing framework.  Yes, you.
+The person reading this.  No, don't look away guiltily.  You
+know your unit testing framework sucks.  You know it has a
+million features you don't understand.  You know you hate it,
+and it hates you.  Don't you?
+
+Okay, fine.  Let's be honest.  Actually, I don't know who you
+are or how you feel about your unit testing framework, but I've
+tried a lot of them, and I don't like any of them.  WvTest is
+the first one I don't hate, at least sort of.  That might be
+because I'm crazy and I only like things I design, or it might
+be because I'm crazy and therefore I'm the only one capable of
+designing a likable unit testing framework.  Who am I to say?
+
+Here are the fundamental design goals of WvTest:
+
+ - Be the stupidest thing that can possibly work.  People are
+   way, way too serious about their testing frameworks.  Some
+   people build testing frameworks as their *full time job*.
+   This is ridiculous.  A test framework, at its core, only does
+   one thing: it runs a program that returns true or false.  If
+   it's false, you lose.  If it's true, you win.  Everything
+   after that is gravy.  And WvTest has only a minimal amount of
+   gravy.
+
+ - Be a protocol, not an API.  If you don't like my API, you can
+   write your own, and it can still be WvTest and it can still
+   integrate with other WvTest tools.  If you're stuck with
+   JUnit or NUnit, you can just make your JUnit/NUnit test
+   produce WvTest-compatible output if you want (although I've
+   never done this, so you'll have to do it yourself).  I'll
+   describe the protocol below.
+
+ - Work with multiple languages on multiple operating systems.
+   I'm a programmer who programs on Linux, MacOS, and Windows,
+   to name just three, and I write in lots of programming
+   languages, including C, C++, C#, Python, Perl, and others.
+   And worse, some of my projects use *multiple* languages and I
+   want to have unit tests for *all* of them.  I don't know of
+   any unit testing framework - except maybe some horrendously
+   overdesigned ones - that work with multiple languages at
+   once.  WvTest does.
+
+ - NO UNNECESSARY OBJECT ORIENTATION.  The big unit testing
+   craze seems to have been started by JUnit in Java, which is
+   object-oriented.  Now, that's not a misdesign in JUnit; it's
+   a misdesign in Java.  You see, you can't *not* encapsulate
+   absolutely everything in Java in a class, so it's perfectly
+   normal for JUnit to require you to encapsulate everything in
+   a class.  That's not true of almost any other language
+   (except C#), and yet *every* clone of JUnit in *every*
+   language seems to have copied its classes and objects.  Well,
+   that's stupid.  WvTest is designed around the simple idea of
+   test *functions*.  WvTest runs your function, it checks a
+   bunch of stuff and it returns or else it dies horribly.  If
+   your function wants to instantiate some objects while it does
+   that, then that's great; WvTest doesn't care.  And yes, you
+   can assert whether two variables are equal even if your
+   function *isn't* in a particular class, just as God intended.
+
+ - Don't make me name or describe my individual tests.  How many
+   times have you seen this?
+
+       assertTrue(thing.works(), "thing didn't work!");
+
+   The reasoning there is that if the test fails, we want to be
+   able to print a user-friendly error message that describes
+   why.  Right?  NO!!  That is *awful*.  That just *doubled* the
+   amount of work you have to do in order to write a test.
+   Instead, WvTest auto-generates output including the line
+   number of the test and the code on that line.  So you get a
+   message like this:
+
+       ! mytest.t.cc:431  thing.works()    FAILED
+
+   and all you have to write is this:
+
+       WVPASS(thing.works());
+
+   (WVPASS is all-caps because it's a macro in C++, but also
+    because you want your tests to stand out.  That's what
+    you'll be looking for when it fails, after all.  And don't
+    even get me started about the 'True' in assertTrue.  Come
+    on, *obviously* you're going to assert that the condition is
+    true!)
+
+ - No setup() and teardown() functions or fixtures.  "Ouch!" you
+   say.  "I'm going to have so much duplicated code!" No, only
+   if you're an idiot.  You know what setup() and teardown() are
+   code names for?  Constructor and destructor.  Create some
+   objects and give them constructors and destructors, and I
+   think you'll find that, like magic, you've just invented
+   "test fixtures."  Nothing any test framework can possibly do
+   will make that any easier.  In fact, everything test
+   frameworks *try* to do with test fixtures just makes it
+   harder to write, read, and understand.  Forget it.
+
+ - Big long scary test functions.  Some test frameworks are
+   insistent about the rule that "every function should test
+   only one thing." Nobody ever really explains why.  I can't
+   understand this; it just causes uncontrolled
+   hormone-imbalance hypergrowth in your test files, and you
+   have to type more stuff... and run test fixtures over and
+   over.
+
+   My personal theory for why people hate big long test
+   functions: it's because their assertTrue() implementation
+   doesn't say which test failed, so they'd like the *name of
+   the function* to be the name of the failed test.  Well,
+   that's a cute workaround to a problem you shouldn't have had
+   in the first place.  With WvTest, WVPASS() actually tells you
+   exactly what passed and what failed, so it's perfectly okay -
+   and totally comprehensible - to have a sequence of five
+   things in a row where only thing number five failed.
+
+
+The WvTest Protocol
+-------------------
+
+WvTest is a protocol, not really an API.  As it happens, the
+WvTest project includes several (currently five)
+implementations of APIs that produce data in the WvTest format,
+but it's super easy to add your own.
+
+The format is really simple too.  It looks like this:
+
+	Testing "my test function" in mytest.t.cc:
+	! mytest.t.cc:432     thing.works()         ok
+	This is just some crap that I printed while counting to 3.
+	! mytest.t.cc.433     3 < 4                 FAILED
+
+There are only four kinds of lines in WvTest, and each of the
+lines above corresponds to one of them:
+
+ - Test function header.  A line that starts with the word
+   Testing (no leading whitespace) and then has a test function
+   name in double quotes, then "in", then the filename, and then
+   colon, marks the beginning of a test function.
+
+ - A passing assertion.  Any line that starts with ! and ends with
+   " ok" (whitespace, the word "ok", and a newline) indicates
+   one assertion that passed.  The first "word" on that line is
+   the "name" of that assertion (which can be anything, as long
+   as it doesn't contain any whitespace).  Everything between the
+   name and the ok is just some additional user-readable detail
+   about the test that passed.
+
+ - Random filler.  If it doesn't start with an ! and it doesn't
+   look like a header, then it's completely ignored by anything
+   using WvTest.  Your program can print all the debug output it
+   wants, and WvTest won't care, except that you can retrieve it
+   later in case you're wondering why a test failed.  Naturally,
+   random filler *before* an assertion is considered to be
+   associated with that assertion; the assertion itself is the
+   last part of a test.
+
+ - A failing assertion.  This is just like an 'ok' line, except
+   the last word is something other than 'ok'.  Generally we use
+   FAILED here, but sometimes it's EXCEPTION, and it could be
+   something else instead, if you invent a new and improved way
+   to fail.
+
+
+Reading the WvTest Protocol: wvtestrun
+--------------------------------------
+
+WvTest provides a simple perl script called wvtestrun, which
+runs a test program and parses its output.  It works like this:
+
+	cd python
+	../wvtestrun ./wvtest.py t/twvtest.py
+
+(Why can't we just pipe the output to wvtestrun, instead of
+ having wvtestrun run the test program?  Three reasons: first, a
+ fancier version of wvtestrun could re-run the tests several
+ times or give a GUI that lets you re-run the test when you push
+ a button.  Second, it handles stdout and stderr separately.
+ And third, it can kill the test program if it gets stuck
+ without producing test output for too long.)
+
+If we put the sample output from the previous section through
+wvtestrun (and changed the FAILED to ok), it would produce this:
+
+	$ ./wvtestrun cat sample-ok
+
+	Testing "all" in cat sample-ok:
+	! mytest.t.cc  my ok test function: .. 0.010s ok
+
+	WvTest: 2 tests, 0 failures, total time 0.010s.
+
+	WvTest result code: 0
+
+What happened here?  Well, wvtestrun took each test header (in
+this case, there's just one, which said we're testing "my test
+function" in mytest.t.cc) and turns it into a single test line.
+Then it prints a dot for each assertion in that test function,
+tells you the total time to run that function, and prints 'ok'
+if the entire test function failed.
+
+Note that the output of wvtestrun is *also* valid WvTest output.
+That means you can use wvtestrun in your 'make test' target in a
+subdirectory, and still use wvtestrun as the 'make test' runner
+in the parent directory as well.  As long as your top-level
+'make test' runs in wvtestrun, all the WvTest output will be
+conveniently summarized into a *single* test output.
+
+Now, what if the test had failed?  Then it would look like this:
+
+	$ ./wvtestrun cat sample-error
+
+	Testing "all" in cat sample-error:
+	! mytest.t.cc  my error test function: .
+	! mytest.t.cc:432     thing.works()                 ok
+	This is just some crap that I printed while counting to 3.
+	! mytest.t.cc.433     3 < 4	                    FAILED
+	 0.000s ok
+
+	WvTest: 2 tests, 1 failure, total time 0.000s.
+
+	WvTest result code: 0
+
+What happened there?  Well, because there were failed tests,
+wvtestrun decided you'd probably want to see the detailed output
+for that test function, so it expanded it out for you.  The line
+with the dots is still there, but since it doesn't have an 'ok',
+it's considered a failure too, just in case.
+
+Watch what happens if we run a test with both the passing, and
+then the failing, test functions:
+
+	$ ./wvtestrun cat sample-ok sample-error
+
+	Testing "all" in cat sample-ok sample-error:
+	! mytest.t.cc  my ok test function: .. 0.000s ok
+	! mytest.t.cc  my error test function: .
+	! mytest.t.cc:432     thing.works()                 ok
+	This is just some crap that I printed while counting to 3.
+	! mytest.t.cc.433     3 < 4                         FAILED
+	 0.000s ok
+
+	WvTest: 4 tests, 1 failure, total time 0.000s.
+
+	WvTest result code: 0
+
+Notice how the messages from sample-ok are condensed; only the
+details from sample-error are expanded out, because only that
+output is interesting.
+
+
+How do I actually write WvTest tests?
+-------------------------------------
+
+Sample code is provided for these languages:
+
+	C: try typing "cd c; make test"
+	C++: try typing "cd cpp; make test"
+	C# (mono): try typing "cd dotnet; make test"
+	Python: try typing "cd python; make test"
+	Shell: try typing "cd sh; make test"
+
+There's no point explaining the syntax here, because it's really
+simple.  Just look inside the cpp, dotnet, python, and sh
+directories to learn how the tests are written.
+
+
+How should I embed WvTest into my own program?
+----------------------------------------------
+
+The easiest way is to just copy the WvTest source files for your
+favourite language into your project.  The WvTest protocol is
+unlikely to ever change - at least not in a
+backwards-incompatible way - so it's no big deal if you end up
+using an "old" version of WvTest in your program.  It should
+still work with updated versions of wvtestrun (or wvtestrun-like
+programs).
+
+Another way is to put the WvTest project in a subdirectory of
+your project, for example, using 'svn:externals',
+'git submodule', or 'git subtree'.
+
+
+How do I run just certain tests?
+--------------------------------
+
+Unfortunately, the command-line syntax for running just *some*
+of your tests varies depending which WvTest language you're using.
+For C, C++ or C#, you link an executable with wvtestmain.c or
+wvtestmain.cc or wvtestmain.cs, respectively, and then you can
+provide strings on the command line.  Test functions will run only
+if they have names that start with one of the provided strings:
+
+	cd cpp/t
+	../../wvtestrun ./wvtest myfunc otherfunc
+
+With python, since there's no linker, you have to just tell it
+which files to run:
+
+	cd python
+	../wvtestrun ./wvtest.py ...filenames...
+
+
+What else can parse WvTest output?
+----------------------------------
+
+It's easy to parse WvTest output however you like; for example,
+you could write a GUI program that does it.  We had a tcl/tk
+program that did it once, but we threw it away since the
+command-line wvtestrun is better anyway.
+
+One other program that can parse WvTest output is gitbuilder
+(http://github.com/apenwarr/gitbuilder/), an autobuilder tool
+for git.  It reports a build failure automatically if there are
+any WvTest-style failed tests in the build output.
+
+
+Other Assorted Questions
+------------------------
+
+
+What does the "Wv" stand for?
+
+	Either "Worldvisions" or "Weaver", both of which were part of the
+	name of the Nitix operating system before it was called Nitix, and
+	*long* before it was later purchased by IBM and renamed to Lotus
+	Foundations.
+
+	It does *not* stand for World Vision (sigh) or West Virginia.
+
+Who owns the copyright?
+
+	While I (Avery) wrote most of the WvTest framework in C++, C#, and
+	Python, and I also wrote wvtestrunner, the actual code I wrote is
+	owned by whichever company I wrote it for at the time.  For the most
+	part, this means:
+
+	C++: Net Integration Technologies, Inc. (now part of IBM)
+	C#: Versabanq Innovations Inc.
+	Python: EQL Data Inc.
+
+What can I do with it?
+
+	WvTest is distributed under the terms of the GNU LGPLv2.  See the
+	file LICENSE for more information.
+
+	Basically this means you can use it for whatever you want, but if
+	you change it, you probably need to give your changes back to the
+	world.  If you *use* it in your program (which is presumably a test
+	program) you do *not* have to give out your program, only
+	WvTest itself.  But read the LICENSE in detail to be sure.
+
+Where did you get the awesome idea to use a protocol instead of an API?
+
+	The perl source code (not to be confused with perlunit)
+	did a similar trick for the perl interpreter's unit
+	test, although in a less general way.  Naturally, you
+	shouldn't blame them for how I mangled their ideas, but
+	I never would have thought of it if it weren't for them.
+
+Who should I complain to about WvTest?
+
+	Email me at: Avery Pennarun <apenwarr@gmail.com>
+
+	I will be happy to read your complaints, because I actually really
+	like it when people use my programs, especially if they hate them.
+	It fills the loneliness somehow and prevents me from writing bad
+	poetry like this:
+
+		Testing makes me gouge out my eyes
+		But with WvTest, it takes fewer tries.
+		WvTest is great, wvtest is fun!
+		Don't forget to call wvtestrun.
diff --git a/wvtest/c/Makefile b/wvtest/c/Makefile
new file mode 100644
index 0000000..a4c447b
--- /dev/null
+++ b/wvtest/c/Makefile
@@ -0,0 +1,14 @@
+
+all: t/wvtest
+
+t/wvtest: wvtestmain.c wvtest.c t/wvtest.t.c
+	gcc -D WVTEST_CONFIGURED -o $@ -I. $^
+
+runtests: all
+	t/wvtest
+
+test: all
+	../wvtestrun $(MAKE) runtests
+
+clean::
+	rm -f *~ t/*~ *.o t/*.o t/wvtest
diff --git a/wvtest/c/t/.gitignore b/wvtest/c/t/.gitignore
new file mode 100644
index 0000000..160e518
--- /dev/null
+++ b/wvtest/c/t/.gitignore
@@ -0,0 +1 @@
+wvtest
diff --git a/wvtest/c/t/wvtest.t.c b/wvtest/c/t/wvtest.t.c
new file mode 100644
index 0000000..2f30eb9
--- /dev/null
+++ b/wvtest/c/t/wvtest.t.c
@@ -0,0 +1,17 @@
+#include "wvtest.h"
+
+WVTEST_MAIN("wvtest tests")
+{
+    WVPASS(1);
+    WVFAIL(0);
+    WVPASSEQ(1, 1);
+    WVPASSNE(1, 2);
+    WVPASSLT(1, 2);
+}
+
+WVTEST_MAIN("wvtest string tests")
+{
+    WVPASSEQSTR("hello", "hello");
+    WVPASSNESTR("hello", "hello2");
+    WVPASSLTSTR("hello", "hello2");
+}
diff --git a/wvtest/c/wvtest.c b/wvtest/c/wvtest.c
new file mode 100644
index 0000000..83cba9e
--- /dev/null
+++ b/wvtest/c/wvtest.c
@@ -0,0 +1,440 @@
+/*
+ * WvTest:
+ *   Copyright (C)1997-2012 Net Integration Technologies and contributors.
+ *       Licensed under the GNU Library General Public License, version 2.
+ *       See the included file named LICENSE for license information.
+ *       You can get wvtest from: http://github.com/apenwarr/wvtest
+ */
+#include "wvtest.h"
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <ctype.h>
+#ifdef _WIN32
+#include <direct.h>
+#else
+#include <unistd.h>
+#include <sys/wait.h>
+#endif
+#include <errno.h>
+#include <signal.h>
+
+#include <stdlib.h>
+
+#ifdef HAVE_VALGRIND_MEMCHECK_H
+# include <valgrind/memcheck.h>
+# include <valgrind/valgrind.h>
+#else
+# define VALGRIND_COUNT_ERRORS 0
+# define VALGRIND_DO_LEAK_CHECK
+# define VALGRIND_COUNT_LEAKS(a,b,c,d) (a=b=c=d=0)
+#endif
+
+#define MAX_TEST_TIME 40     // max seconds for a single test to run
+#define MAX_TOTAL_TIME 120*60 // max seconds for the entire suite to run
+
+#define TEST_START_FORMAT "! %s:%-5d %-40s "
+
+static int fails, runs;
+static time_t start_time;
+static bool run_twice;
+
+static void alarm_handler(int sig);
+
+static int memerrs()
+{
+    return (int)VALGRIND_COUNT_ERRORS;
+}
+
+static int memleaks()
+{
+    int leaked = 0, dubious = 0, reachable = 0, suppressed = 0;
+    VALGRIND_DO_LEAK_CHECK;
+    VALGRIND_COUNT_LEAKS(leaked, dubious, reachable, suppressed);
+    printf("memleaks: sure:%d dubious:%d reachable:%d suppress:%d\n",
+	   leaked, dubious, reachable, suppressed);
+    fflush(stdout);
+
+    // dubious+reachable are normally non-zero because of globals...
+    // return leaked+dubious+reachable;
+    return leaked;
+}
+
+// Return 1 if no children are running or zombies, 0 if there are any running
+// or zombie children.
+// Will wait for any already-terminated children first.
+// Passes if no rogue children were running, fails otherwise.
+// If your test gets a failure in here, either you're not killing all your
+// children, or you're not calling waitpid(2) on all of them.
+static bool no_running_children()
+{
+#ifndef _WIN32
+    pid_t wait_result;
+
+    // Acknowledge and complain about any zombie children
+    do
+    {
+	int status = 0;
+        wait_result = waitpid(-1, &status, WNOHANG);
+
+        if (wait_result > 0)
+        {
+            char buf[256];
+            snprintf(buf, sizeof(buf) - 1, "%d", wait_result);
+            buf[sizeof(buf)-1] = '\0';
+            WVFAILEQSTR("Unclaimed dead child process", buf);
+        }
+    } while (wait_result > 0);
+
+    // There should not be any running children, so waitpid should return -1
+    WVPASS(errno == ECHILD);
+    WVPASS(wait_result == -1);
+    return (wait_result == -1 && errno == ECHILD);
+#endif
+    return true;
+}
+
+
+static void alarm_handler(int sig)
+{
+    printf("\n! WvTest  Current test took longer than %d seconds!  FAILED\n",
+	   MAX_TEST_TIME);
+    fflush(stdout);
+    abort();
+}
+
+
+static const char *pathstrip(const char *filename)
+{
+    const char *cptr;
+    cptr = strrchr(filename, '/');
+    if (cptr) filename = cptr + 1;
+    cptr = strrchr(filename, '\\');
+    if (cptr) filename = cptr + 1;
+    return filename;
+}
+
+static bool prefix_match(const char *s, char * const *prefixes)
+{
+    char *const *prefix;
+    for (prefix = prefixes; prefix && *prefix; prefix++)
+    {
+	if (!strncasecmp(s, *prefix, strlen(*prefix)))
+	    return true;
+    }
+    return false;
+}
+
+static struct WvTest *wvtest_first, *wvtest_last;
+
+void wvtest_register(struct WvTest *ptr)
+{
+	if (wvtest_first == NULL)
+		wvtest_first = ptr;
+	else
+		wvtest_last->next = ptr;
+	wvtest_last = ptr;
+	wvtest_last->next = NULL;
+}
+
+int wvtest_run_all(char * const *prefixes)
+{
+    int old_valgrind_errs = 0, new_valgrind_errs;
+    int old_valgrind_leaks = 0, new_valgrind_leaks;
+
+#ifdef _WIN32
+    /* I should be doing something to do with SetTimer here,
+     * not sure exactly what just yet */
+#else
+    char *disable = getenv("WVTEST_DISABLE_TIMEOUT");
+    if (disable != NULL && disable[0] != '\0' && disable[0] != '0')
+        signal(SIGALRM, SIG_IGN);
+    else
+        signal(SIGALRM, alarm_handler);
+    alarm(MAX_TEST_TIME);
+#endif
+    start_time = time(NULL);
+
+    // make sure we can always start out in the same directory, so tests have
+    // access to their files.  If a test uses chdir(), we want to be able to
+    // reverse it.
+    char wd[1024];
+    if (!getcwd(wd, sizeof(wd)))
+	strcpy(wd, ".");
+
+    const char *slowstr1 = getenv("WVTEST_MIN_SLOWNESS");
+    const char *slowstr2 = getenv("WVTEST_MAX_SLOWNESS");
+    int min_slowness = 0, max_slowness = 65535;
+    if (slowstr1) min_slowness = atoi(slowstr1);
+    if (slowstr2) max_slowness = atoi(slowstr2);
+
+#ifdef _WIN32
+    run_twice = false;
+#else
+    char *parallel_str = getenv("WVTEST_PARALLEL");
+    if (parallel_str)
+        run_twice = atoi(parallel_str) > 0;
+#endif
+
+    // there are lots of fflush() calls in here because stupid win32 doesn't
+    // flush very often by itself.
+    fails = runs = 0;
+    struct WvTest *cur;
+
+    for (cur = wvtest_first; cur != NULL; cur = cur->next)
+    {
+	if (cur->slowness <= max_slowness
+	    && cur->slowness >= min_slowness
+	    && (!prefixes
+		|| prefix_match(cur->idstr, prefixes)
+		|| prefix_match(cur->descr, prefixes)))
+	{
+#ifndef _WIN32
+            // set SIGPIPE back to default, helps catch tests which don't set
+            // this signal to SIG_IGN (which is almost always what you want)
+            // on startup
+            signal(SIGPIPE, SIG_DFL);
+
+            pid_t child = 0;
+            if (run_twice)
+            {
+                // I see everything twice!
+                printf("Running test in parallel.\n");
+                child = fork();
+            }
+#endif
+
+	    printf("\nTesting \"%s\" in %s:\n", cur->descr, cur->idstr);
+	    fflush(stdout);
+
+	    cur->main();
+	    if (chdir(wd)) {
+		perror("Unable to change back to original directory");
+	    }
+
+	    new_valgrind_errs = memerrs();
+	    WVPASS(new_valgrind_errs == old_valgrind_errs);
+	    old_valgrind_errs = new_valgrind_errs;
+
+	    new_valgrind_leaks = memleaks();
+	    WVPASS(new_valgrind_leaks == old_valgrind_leaks);
+	    old_valgrind_leaks = new_valgrind_leaks;
+
+	    fflush(stderr);
+	    printf("\n");
+	    fflush(stdout);
+
+#ifndef _WIN32
+            if (run_twice)
+            {
+                if (!child)
+                {
+                    // I see everything once!
+                    printf("Child exiting.\n");
+                    _exit(0);
+                }
+                else
+                {
+                    printf("Waiting for child to exit.\n");
+                    int result;
+                    while ((result = waitpid(child, NULL, 0)) == -1 &&
+                            errno == EINTR)
+                        printf("Waitpid interrupted, retrying.\n");
+                }
+            }
+#endif
+
+            WVPASS(no_running_children());
+	}
+    }
+
+    WVPASS(runs > 0);
+
+    if (prefixes && *prefixes && **prefixes)
+	printf("WvTest: WARNING: only ran tests starting with "
+	       "specifed prefix(es).\n");
+    else
+	printf("WvTest: ran all tests.\n");
+    printf("WvTest: %d test%s, %d failure%s.\n",
+	   runs, runs==1 ? "" : "s",
+	   fails, fails==1 ? "": "s");
+    fflush(stdout);
+
+    return fails != 0;
+}
+
+
+// If we aren't running in parallel, we want to output the name of the test
+// before we run it, so we know what happened if it crashes.  If we are
+// running in parallel, outputting this information in multiple printf()s
+// can confuse parsers, so we want to output everything in one printf().
+//
+// This function gets called by both start() and check().  If we're not
+// running in parallel, just print the data.  If we're running in parallel,
+// and we're starting a test, save a copy of the file/line/description until
+// the test is done and we can output it all at once.
+//
+// Yes, this is probably the worst API of all time.
+static void print_result_str(bool start, const char *_file, int _line,
+			     const char *_condstr, const char *result)
+{
+    static char *file;
+    static char *condstr;
+    static int line;
+    char *cptr;
+
+    if (start)
+    {
+        if (file)
+            free(file);
+        if (condstr)
+            free(condstr);
+        file = strdup(pathstrip(_file));
+        condstr = strdup(_condstr);
+        line = _line;
+
+        for (cptr = condstr; *cptr; cptr++)
+        {
+            if (!isprint((unsigned char)*cptr))
+                *cptr = '!';
+        }
+    }
+
+    if (run_twice)
+    {
+        if (!start)
+            printf(TEST_START_FORMAT "%s\n", file, line, condstr, result);
+    }
+    else
+    {
+        if (start)
+            printf(TEST_START_FORMAT, file, line, condstr);
+        else
+            printf("%s\n", result);
+    }
+    fflush(stdout);
+
+    if (!start)
+    {
+        if (file)
+            free(file);
+        if (condstr)
+            free(condstr);
+        file = condstr = NULL;
+    }
+}
+
+static inline void
+print_result(bool start, const char *file, int line,
+	     const char *condstr, bool result)
+{
+	print_result_str(start, file, line, condstr, result ? "ok" : "FAILED");
+}
+
+void wvtest_start(const char *file, int line, const char *condstr)
+{
+    // Either print the file, line, and condstr, or save them for later.
+    print_result(true, file, line, condstr, false);
+}
+
+
+void wvtest_check(bool cond, const char *reason)
+{
+#ifndef _WIN32
+    alarm(MAX_TEST_TIME); // restart per-test timeout
+#endif
+    if (!start_time) start_time = time(NULL);
+
+    if (time(NULL) - start_time > MAX_TOTAL_TIME)
+    {
+	printf("\n! WvTest   Total run time exceeded %d seconds!  FAILED\n",
+	       MAX_TOTAL_TIME);
+	fflush(stdout);
+	abort();
+    }
+
+    runs++;
+
+    print_result_str(false, NULL, 0, NULL, cond ? "ok" : (reason ? reason : "FAILED"));
+
+    if (!cond)
+    {
+	fails++;
+
+	if (getenv("WVTEST_DIE_FAST"))
+	    abort();
+    }
+}
+
+
+bool wvtest_start_check_eq(const char *file, int line,
+			   int a, int b, bool expect_pass)
+{
+    size_t len = 11 + 11 + 8 + 1;
+    char *str = malloc(len);
+    sprintf(str, "%d %s %d", a, expect_pass ? "==" : "!=", b);
+
+    wvtest_start(file, line, str);
+    free(str);
+
+    bool cond = (a == b);
+    if (!expect_pass)
+        cond = !cond;
+
+    wvtest_check(cond, NULL);
+    return cond;
+}
+
+
+bool wvtest_start_check_lt(const char *file, int line,
+			   int a, int b)
+{
+    size_t len = 11 + 11 + 8 + 1;
+    char *str = malloc(len);
+    sprintf(str, "%d < %d", a, b);
+
+    wvtest_start(file, line, str);
+    free(str);
+
+    bool cond = (a < b);
+    wvtest_check(cond, NULL);
+    return cond;
+}
+bool wvtest_start_check_eq_str(const char *file, int line,
+			       const char *a, const char *b, bool expect_pass)
+{
+    if (!a) a = "";
+    if (!b) b = "";
+
+    size_t len = strlen(a) + strlen(b) + 8 + 1;
+    char *str = malloc(len);
+    sprintf(str, "[%s] %s [%s]", a, expect_pass ? "==" : "!=", b);
+
+    wvtest_start(file, line, str);
+
+    bool cond = !strcmp(a, b);
+    if (!expect_pass)
+        cond = !cond;
+
+    wvtest_check(cond, NULL);
+    return cond;
+}
+
+
+bool wvtest_start_check_lt_str(const char *file, int line,
+			       const char *a, const char *b)
+{
+    if (!a) a = "";
+    if (!b) b = "";
+
+    size_t len = strlen(a) + strlen(b) + 8 + 1;
+    char *str = malloc(len);
+    sprintf(str, "[%s] < [%s]", a, b);
+
+    wvtest_start(file, line, str);
+    free(str);
+
+    bool cond = strcmp(a, b) < 0;
+    wvtest_check(cond, NULL);
+    return cond;
+}
diff --git a/wvtest/c/wvtest.h b/wvtest/c/wvtest.h
new file mode 100644
index 0000000..0952c00
--- /dev/null
+++ b/wvtest/c/wvtest.h
@@ -0,0 +1,79 @@
+/* -*- Mode: C++ -*-
+ * WvTest:
+ *   Copyright (C)1997-2012 Net Integration Technologies and contributors.
+ *       Licensed under the GNU Library General Public License, version 2.
+ *       See the included file named LICENSE for license information.
+ *       You can get wvtest from: http://github.com/apenwarr/wvtest
+ */
+#ifndef __WVTEST_H
+#define __WVTEST_H
+
+#ifndef WVTEST_CONFIGURED
+# error "Missing settings: HAVE_VALGRIND_MEMCHECK_H HAVE_WVCRASH WVTEST_CONFIGURED"
+#endif
+
+#include <time.h>
+#include <string.h>
+#include <stdbool.h>
+
+typedef void wvtest_mainfunc();
+
+struct WvTest {
+	const char *descr, *idstr;
+	wvtest_mainfunc *main;
+	int slowness;
+	struct WvTest *next;
+};
+
+void wvtest_register(struct WvTest *ptr);
+int wvtest_run_all(char * const *prefixes);
+void wvtest_start(const char *file, int line, const char *condstr);
+void wvtest_check(bool cond, const char *reason);
+static inline bool wvtest_start_check(const char *file, int line,
+				      const char *condstr, bool cond)
+{ wvtest_start(file, line, condstr); wvtest_check(cond, NULL); return cond; }
+bool wvtest_start_check_eq(const char *file, int line,
+			   int a, int b, bool expect_pass);
+bool wvtest_start_check_lt(const char *file, int line,
+			   int a, int b);
+bool wvtest_start_check_eq_str(const char *file, int line,
+			       const char *a, const char *b, bool expect_pass);
+bool wvtest_start_check_lt_str(const char *file, int line,
+			       const char *a, const char *b);
+
+
+#define WVPASS(cond) \
+    wvtest_start_check(__FILE__, __LINE__, #cond, (cond))
+#define WVPASSEQ(a, b) \
+    wvtest_start_check_eq(__FILE__, __LINE__, (a), (b), true)
+#define WVPASSLT(a, b) \
+    wvtest_start_check_lt(__FILE__, __LINE__, (a), (b))
+#define WVPASSEQSTR(a, b) \
+    wvtest_start_check_eq_str(__FILE__, __LINE__, (a), (b), true)
+#define WVPASSLTSTR(a, b) \
+    wvtest_start_check_lt_str(__FILE__, __LINE__, (a), (b))
+#define WVFAIL(cond) \
+    wvtest_start_check(__FILE__, __LINE__, "NOT(" #cond ")", !(cond))
+#define WVFAILEQ(a, b) \
+    wvtest_start_check_eq(__FILE__, __LINE__, (a), (b), false)
+#define WVFAILEQSTR(a, b) \
+    wvtest_start_check_eq_str(__FILE__, __LINE__, (a), (b), false)
+#define WVPASSNE(a, b) WVFAILEQ(a, b)
+#define WVPASSNESTR(a, b) WVFAILEQSTR(a, b)
+#define WVFAILNE(a, b) WVPASSEQ(a, b)
+#define WVFAILNESTR(a, b) WVPASSEQSTR(a, b)
+
+#define WVTEST_MAIN3(_descr, ff, ll, _slowness)				\
+	static void _wvtest_main_##ll();				\
+	struct WvTest _wvtest_##ll = \
+	{ .descr = _descr, .idstr = ff ":" #ll, .main = _wvtest_main_##ll, .slowness = _slowness }; \
+	static void _wvtest_register_##ll() __attribute__ ((constructor)); \
+	static void _wvtest_register_##ll() { wvtest_register(&_wvtest_##ll); } \
+	static void _wvtest_main_##ll()
+#define WVTEST_MAIN2(descr, ff, ll, slowness)	\
+	WVTEST_MAIN3(descr, ff, ll, slowness)
+#define WVTEST_MAIN(descr) WVTEST_MAIN2(descr, __FILE__, __LINE__, 0)
+#define WVTEST_SLOW_MAIN(descr) WVTEST_MAIN2(descr, __FILE__, __LINE__, 1)
+
+
+#endif // __WVTEST_H
diff --git a/wvtest/c/wvtestmain.c b/wvtest/c/wvtestmain.c
new file mode 100644
index 0000000..46f91a3
--- /dev/null
+++ b/wvtest/c/wvtestmain.c
@@ -0,0 +1,102 @@
+/*
+ * WvTest:
+ *   Copyright (C)1997-2012 Net Integration Technologies and contributors.
+ *       Licensed under the GNU Library General Public License, version 2.
+ *       See the included file named LICENSE for license information.
+ *       You can get wvtest from: http://github.com/apenwarr/wvtest
+ */
+#include "wvtest.h"
+#ifdef HAVE_WVCRASH
+# include "wvcrash.h"
+#endif
+#include <stdlib.h>
+#include <stdio.h>
+#ifdef _WIN32
+#include <io.h>
+#include <windows.h>
+#else
+#include <unistd.h>
+#include <fcntl.h>
+#endif
+
+static bool fd_is_valid(int fd)
+{
+#ifdef _WIN32
+    if ((HANDLE)_get_osfhandle(fd) != INVALID_HANDLE_VALUE) return true;
+#endif
+    int nfd = dup(fd);
+    if (nfd >= 0)
+    {
+	close(nfd);
+	return true;
+    }
+    return false;
+
+}
+
+
+static int fd_count(const char *when)
+{
+    int count = 0;
+    int fd;
+    printf("fds open at %s:", when);
+
+    for (fd = 0; fd < 1024; fd++)
+    {
+	if (fd_is_valid(fd))
+	{
+	    count++;
+	    printf(" %d", fd);
+	    fflush(stdout);
+	}
+    }
+    printf("\n");
+
+    return count;
+}
+
+
+int main(int argc, char **argv)
+{
+    char buf[200];
+#if defined(_WIN32) && defined(HAVE_WVCRASH)
+    setup_console_crash();
+#endif
+
+    // test wvtest itself.  Not very thorough, but you have to draw the
+    // line somewhere :)
+    WVPASS(true);
+    WVPASS(1);
+    WVFAIL(false);
+    WVFAIL(0);
+    int startfd, endfd;
+    char * const *prefixes = NULL;
+
+    if (argc > 1)
+	prefixes = argv + 1;
+
+    startfd = fd_count("start");
+    int ret = wvtest_run_all(prefixes);
+
+    if (ret == 0) // don't pollute the strace output if we failed anyway
+    {
+	endfd = fd_count("end");
+
+	WVPASS(startfd == endfd);
+#ifndef _WIN32
+	if (startfd != endfd)
+	{
+	    sprintf(buf, "ls -l /proc/%d/fd", getpid());
+	    if (system(buf) == -1) {
+		fprintf(stderr, "Unable to list open fds\n");
+	    }
+	}
+#endif
+    }
+
+    // keep 'make' from aborting if this environment variable is set
+    if (getenv("WVTEST_NO_FAIL"))
+	return 0;
+    else
+	return ret;
+}
diff --git a/wvtest/cpp/Makefile b/wvtest/cpp/Makefile
new file mode 100644
index 0000000..e9f2b40
--- /dev/null
+++ b/wvtest/cpp/Makefile
@@ -0,0 +1,14 @@
+
+all: t/wvtest
+
+t/wvtest: wvtestmain.cc wvtest.cc t/wvtest.t.cc
+	g++ -D WVTEST_CONFIGURED -o $@ -I. $^
+
+runtests: all
+	t/wvtest
+
+test: all
+	../wvtestrun $(MAKE) runtests
+
+clean::
+	rm -f *~ t/*~ *.o t/*.o t/wvtest
diff --git a/wvtest/cpp/t/.gitignore b/wvtest/cpp/t/.gitignore
new file mode 100644
index 0000000..160e518
--- /dev/null
+++ b/wvtest/cpp/t/.gitignore
@@ -0,0 +1 @@
+wvtest
diff --git a/wvtest/cpp/t/wvtest.t.cc b/wvtest/cpp/t/wvtest.t.cc
new file mode 100644
index 0000000..aa028d6
--- /dev/null
+++ b/wvtest/cpp/t/wvtest.t.cc
@@ -0,0 +1,16 @@
+#include "wvtest.h"
+
+WVTEST_MAIN("wvtest tests")
+{
+    WVPASS(1);
+    WVPASSEQ(1, 1);
+    WVPASSNE(1, 2);
+    WVPASSEQ(1, 1);
+    WVPASSLT(1, 2);
+
+    WVPASSEQ("hello", "hello");
+    WVPASSNE("hello", "hello2");
+
+    WVPASSEQ(std::string("hello"), std::string("hello"));
+    WVPASSNE(std::string("hello"), std::string("hello2"));
+}
diff --git a/wvtest/cpp/wvtest.cc b/wvtest/cpp/wvtest.cc
new file mode 100644
index 0000000..2fac9d1
--- /dev/null
+++ b/wvtest/cpp/wvtest.cc
@@ -0,0 +1,446 @@
+/*
+ * WvTest:
+ *   Copyright (C)1997-2012 Net Integration Technologies and contributors.
+ *       Licensed under the GNU Library General Public License, version 2.
+ *       See the included file named LICENSE for license information.
+ *       You can get wvtest from: http://github.com/apenwarr/wvtest
+ */
+#include "wvtest.h"
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <ctype.h>
+#ifdef _WIN32
+#include <direct.h>
+#else
+#include <unistd.h>
+#include <sys/wait.h>
+#endif
+#include <errno.h>
+#include <signal.h>
+
+#include <cstdlib>
+
+#ifdef HAVE_VALGRIND_MEMCHECK_H
+# include <valgrind/memcheck.h>
+# include <valgrind/valgrind.h>
+#else
+# define VALGRIND_COUNT_ERRORS 0
+# define VALGRIND_DO_LEAK_CHECK
+# define VALGRIND_COUNT_LEAKS(a,b,c,d) (a=b=c=d=0)
+#endif
+
+#define MAX_TEST_TIME 40     // max seconds for a single test to run
+#define MAX_TOTAL_TIME 120*60 // max seconds for the entire suite to run
+
+#define TEST_START_FORMAT "! %s:%-5d %-40s "
+
+static int memerrs()
+{
+    return (int)VALGRIND_COUNT_ERRORS;
+}
+
+static int memleaks()
+{
+    int leaked = 0, dubious = 0, reachable = 0, suppressed = 0;
+    VALGRIND_DO_LEAK_CHECK;
+    VALGRIND_COUNT_LEAKS(leaked, dubious, reachable, suppressed);
+    printf("memleaks: sure:%d dubious:%d reachable:%d suppress:%d\n",
+	   leaked, dubious, reachable, suppressed);
+    fflush(stdout);
+
+    // dubious+reachable are normally non-zero because of globals...
+    // return leaked+dubious+reachable;
+    return leaked;
+}
+
+// Return 1 if no children are running or zombies, 0 if there are any running
+// or zombie children.
+// Will wait for any already-terminated children first.
+// Passes if no rogue children were running, fails otherwise.
+// If your test gets a failure in here, either you're not killing all your
+// children, or you're not calling waitpid(2) on all of them.
+static bool no_running_children()
+{
+#ifndef _WIN32
+    pid_t wait_result;
+
+    // Acknowledge and complain about any zombie children
+    do
+    {
+	int status = 0;
+        wait_result = waitpid(-1, &status, WNOHANG);
+
+        if (wait_result > 0)
+        {
+            char buf[256];
+            snprintf(buf, sizeof(buf) - 1, "%d", wait_result);
+            buf[sizeof(buf)-1] = '\0';
+            WVFAILEQ("Unclaimed dead child process", buf);
+        }
+    } while (wait_result > 0);
+
+    // There should not be any running children, so waitpid should return -1
+    WVPASSEQ(errno, ECHILD);
+    WVPASSEQ(wait_result, -1);
+    return (wait_result == -1 && errno == ECHILD);
+#endif
+    return true;
+}
+
+
+WvTest *WvTest::first, *WvTest::last;
+int WvTest::fails, WvTest::runs;
+time_t WvTest::start_time;
+bool WvTest::run_twice = false;
+
+void WvTest::alarm_handler(int)
+{
+    printf("\n! WvTest  Current test took longer than %d seconds!  FAILED\n",
+	   MAX_TEST_TIME);
+    fflush(stdout);
+    abort();
+}
+
+
+static const char *pathstrip(const char *filename)
+{
+    const char *cptr;
+    cptr = strrchr(filename, '/');
+    if (cptr) filename = cptr + 1;
+    cptr = strrchr(filename, '\\');
+    if (cptr) filename = cptr + 1;
+    return filename;
+}
+
+
+WvTest::WvTest(const char *_descr, const char *_idstr, MainFunc *_main,
+	       int _slowness) :
+    descr(_descr),
+    idstr(pathstrip(_idstr)),
+    main(_main),
+    slowness(_slowness),
+    next(NULL)
+{
+    if (first)
+	last->next = this;
+    else
+	first = this;
+    last = this;
+}
+
+
+static bool prefix_match(const char *s, const char * const *prefixes)
+{
+    for (const char * const *prefix = prefixes; prefix && *prefix; prefix++)
+    {
+	if (!strncasecmp(s, *prefix, strlen(*prefix)))
+	    return true;
+    }
+    return false;
+}
+
+
+int WvTest::run_all(const char * const *prefixes)
+{
+    int old_valgrind_errs = 0, new_valgrind_errs;
+    int old_valgrind_leaks = 0, new_valgrind_leaks;
+
+#ifdef _WIN32
+    /* I should be doing something to do with SetTimer here,
+     * not sure exactly what just yet */
+#else
+    char *disable(getenv("WVTEST_DISABLE_TIMEOUT"));
+    if (disable != NULL && disable[0] != '\0' && disable[0] != '0')
+        signal(SIGALRM, SIG_IGN);
+    else
+        signal(SIGALRM, alarm_handler);
+    alarm(MAX_TEST_TIME);
+#endif
+    start_time = time(NULL);
+
+    // make sure we can always start out in the same directory, so tests have
+    // access to their files.  If a test uses chdir(), we want to be able to
+    // reverse it.
+    char wd[1024];
+    if (!getcwd(wd, sizeof(wd)))
+	strcpy(wd, ".");
+
+    const char *slowstr1 = getenv("WVTEST_MIN_SLOWNESS");
+    const char *slowstr2 = getenv("WVTEST_MAX_SLOWNESS");
+    int min_slowness = 0, max_slowness = 65535;
+    if (slowstr1) min_slowness = atoi(slowstr1);
+    if (slowstr2) max_slowness = atoi(slowstr2);
+
+#ifdef _WIN32
+    run_twice = false;
+#else
+    char *parallel_str = getenv("WVTEST_PARALLEL");
+    if (parallel_str)
+        run_twice = atoi(parallel_str) > 0;
+#endif
+
+    // there are lots of fflush() calls in here because stupid win32 doesn't
+    // flush very often by itself.
+    fails = runs = 0;
+    for (WvTest *cur = first; cur; cur = cur->next)
+    {
+	if (cur->slowness <= max_slowness
+	    && cur->slowness >= min_slowness
+	    && (!prefixes
+		|| prefix_match(cur->idstr, prefixes)
+		|| prefix_match(cur->descr, prefixes)))
+	{
+#ifndef _WIN32
+            // set SIGPIPE back to default, helps catch tests which don't set
+            // this signal to SIG_IGN (which is almost always what you want)
+            // on startup
+            signal(SIGPIPE, SIG_DFL);
+
+            pid_t child = 0;
+            if (run_twice)
+            {
+                // I see everything twice!
+                printf("Running test in parallel.\n");
+                child = fork();
+            }
+#endif
+
+	    printf("\nTesting \"%s\" in %s:\n", cur->descr, cur->idstr);
+	    fflush(stdout);
+
+	    cur->main();
+	    if (chdir(wd)) {
+		perror("Unable to change back to original directory");
+	    }
+
+	    new_valgrind_errs = memerrs();
+	    WVPASS(new_valgrind_errs == old_valgrind_errs);
+	    old_valgrind_errs = new_valgrind_errs;
+
+	    new_valgrind_leaks = memleaks();
+	    WVPASS(new_valgrind_leaks == old_valgrind_leaks);
+	    old_valgrind_leaks = new_valgrind_leaks;
+
+	    fflush(stderr);
+	    printf("\n");
+	    fflush(stdout);
+
+#ifndef _WIN32
+            if (run_twice)
+            {
+                if (!child)
+                {
+                    // I see everything once!
+                    printf("Child exiting.\n");
+                    _exit(0);
+                }
+                else
+                {
+                    printf("Waiting for child to exit.\n");
+                    int result;
+                    while ((result = waitpid(child, NULL, 0)) == -1 &&
+                            errno == EINTR)
+                        printf("Waitpid interrupted, retrying.\n");
+                }
+            }
+#endif
+
+            WVPASS(no_running_children());
+	}
+    }
+
+    WVPASS(runs > 0);
+
+    if (prefixes && *prefixes && **prefixes)
+	printf("WvTest: WARNING: only ran tests starting with "
+	       "specifed prefix(es).\n");
+    else
+	printf("WvTest: ran all tests.\n");
+    printf("WvTest: %d test%s, %d failure%s.\n",
+	   runs, runs==1 ? "" : "s",
+	   fails, fails==1 ? "": "s");
+    fflush(stdout);
+
+    return fails != 0;
+}
+
+
+// If we aren't running in parallel, we want to output the name of the test
+// before we run it, so we know what happened if it crashes.  If we are
+// running in parallel, outputting this information in multiple printf()s
+// can confuse parsers, so we want to output everything in one printf().
+//
+// This function gets called by both start() and check().  If we're not
+// running in parallel, just print the data.  If we're running in parallel,
+// and we're starting a test, save a copy of the file/line/description until
+// the test is done and we can output it all at once.
+//
+// Yes, this is probably the worst API of all time.
+void WvTest::print_result(bool start, const char *_file, int _line,
+        const char *_condstr, bool result)
+{
+    static char *file;
+    static char *condstr;
+    static int line;
+
+    if (start)
+    {
+        if (file)
+            free(file);
+        if (condstr)
+            free(condstr);
+        file = strdup(pathstrip(_file));
+        condstr = strdup(_condstr);
+        line = _line;
+
+        for (char *cptr = condstr; *cptr; cptr++)
+        {
+            if (!isprint((unsigned char)*cptr))
+                *cptr = '!';
+        }
+    }
+
+    const char *result_str = result ? "ok\n" : "FAILED\n";
+    if (run_twice)
+    {
+        if (!start)
+            printf(TEST_START_FORMAT "%s", file, line, condstr, result_str);
+    }
+    else
+    {
+        if (start)
+            printf(TEST_START_FORMAT, file, line, condstr);
+        else
+            printf("%s", result_str);
+    }
+    fflush(stdout);
+
+    if (!start)
+    {
+        if (file)
+            free(file);
+        if (condstr)
+            free(condstr);
+        file = condstr = NULL;
+    }
+}
+
+
+void WvTest::start(const char *file, int line, const char *condstr)
+{
+    // Either print the file, line, and condstr, or save them for later.
+    print_result(true, file, line, condstr, 0);
+}
+
+
+void WvTest::check(bool cond)
+{
+#ifndef _WIN32
+    alarm(MAX_TEST_TIME); // restart per-test timeout
+#endif
+    if (!start_time) start_time = time(NULL);
+
+    if (time(NULL) - start_time > MAX_TOTAL_TIME)
+    {
+	printf("\n! WvTest   Total run time exceeded %d seconds!  FAILED\n",
+	       MAX_TOTAL_TIME);
+	fflush(stdout);
+	abort();
+    }
+
+    runs++;
+
+    print_result(false, NULL, 0, NULL, cond);
+
+    if (!cond)
+    {
+	fails++;
+
+	if (getenv("WVTEST_DIE_FAST"))
+	    abort();
+    }
+}
+
+
+bool WvTest::start_check_eq(const char *file, int line,
+			    const char *a, const char *b, bool expect_pass)
+{
+    if (!a) a = "";
+    if (!b) b = "";
+
+    size_t len = strlen(a) + strlen(b) + 8 + 1;
+    char *str = new char[len];
+    sprintf(str, "[%s] %s [%s]", a, expect_pass ? "==" : "!=", b);
+
+    start(file, line, str);
+    delete[] str;
+
+    bool cond = !strcmp(a, b);
+    if (!expect_pass)
+        cond = !cond;
+
+    check(cond);
+    return cond;
+}
+
+
+bool WvTest::start_check_eq(const char *file, int line,
+			    const std::string &a, const std::string &b,
+                            bool expect_pass)
+{
+    return start_check_eq(file, line, a.c_str(), b.c_str(), expect_pass);
+}
+
+
+bool WvTest::start_check_eq(const char *file, int line,
+                            int a, int b, bool expect_pass)
+{
+    size_t len = 128 + 128 + 8 + 1;
+    char *str = new char[len];
+    sprintf(str, "%d %s %d", a, expect_pass ? "==" : "!=", b);
+
+    start(file, line, str);
+    delete[] str;
+
+    bool cond = (a == b);
+    if (!expect_pass)
+        cond = !cond;
+
+    check(cond);
+    return cond;
+}
+
+
+bool WvTest::start_check_lt(const char *file, int line,
+			    const char *a, const char *b)
+{
+    if (!a) a = "";
+    if (!b) b = "";
+
+    size_t len = strlen(a) + strlen(b) + 8 + 1;
+    char *str = new char[len];
+    sprintf(str, "[%s] < [%s]", a, b);
+
+    start(file, line, str);
+    delete[] str;
+
+    bool cond = strcmp(a, b) < 0;
+    check(cond);
+    return cond;
+}
+
+
+bool WvTest::start_check_lt(const char *file, int line, int a, int b)
+{
+    size_t len = 128 + 128 + 8 + 1;
+    char *str = new char[len];
+    sprintf(str, "%d < %d", a, b);
+
+    start(file, line, str);
+    delete[] str;
+
+    bool cond = a < b;
+    check(cond);
+    return cond;
+}
diff --git a/wvtest/cpp/wvtest.h b/wvtest/cpp/wvtest.h
new file mode 100644
index 0000000..de4975b
--- /dev/null
+++ b/wvtest/cpp/wvtest.h
@@ -0,0 +1,78 @@
+/* -*- Mode: C++ -*-
+ * WvTest:
+ *   Copyright (C)1997-2012 Net Integration Technologies and contributors.
+ *       Licensed under the GNU Library General Public License, version 2.
+ *       See the included file named LICENSE for license information.
+ *       You can get wvtest from: http://github.com/apenwarr/wvtest
+ */
+#ifndef __WVTEST_H
+#define __WVTEST_H
+
+#ifndef WVTEST_CONFIGURED
+# error "Missing settings: HAVE_VALGRIND_MEMCHECK_H HAVE_WVCRASH WVTEST_CONFIGURED"
+#endif
+
+#include <time.h>
+#include <string>
+
+class WvTest
+{
+    typedef void MainFunc();
+    const char *descr, *idstr;
+    MainFunc *main;
+    int slowness;
+    WvTest *next;
+    static WvTest *first, *last;
+    static int fails, runs;
+    static time_t start_time;
+    static bool run_twice;
+
+    static void alarm_handler(int sig);
+
+    static void print_result(bool start, const char *file, int line,
+            const char *condstr, bool result);
+public:
+    WvTest(const char *_descr, const char *_idstr, MainFunc *_main, int _slow);
+    static int run_all(const char * const *prefixes = NULL);
+    static void start(const char *file, int line, const char *condstr);
+    static void check(bool cond);
+    static inline bool start_check(const char *file, int line,
+				   const char *condstr, bool cond)
+        { start(file, line, condstr); check(cond); return cond; }
+    static bool start_check_eq(const char *file, int line,
+			       const char *a, const char *b, bool expect_pass);
+    static bool start_check_eq(const char *file, int line,
+			       const std::string &a, const std::string &b,
+                               bool expect_pass);
+    static bool start_check_eq(const char *file, int line, int a, int b,
+                               bool expect_pass);
+    static bool start_check_lt(const char *file, int line,
+                               const char *a, const char *b);
+    static bool start_check_lt(const char *file, int line, int a, int b);
+};
+
+
+#define WVPASS(cond) \
+    WvTest::start_check(__FILE__, __LINE__, #cond, (cond))
+#define WVPASSEQ(a, b) \
+    WvTest::start_check_eq(__FILE__, __LINE__, (a), (b), true)
+#define WVPASSLT(a, b) \
+    WvTest::start_check_lt(__FILE__, __LINE__, (a), (b))
+#define WVFAIL(cond) \
+    WvTest::start_check(__FILE__, __LINE__, "NOT(" #cond ")", !(cond))
+#define WVFAILEQ(a, b) \
+    WvTest::start_check_eq(__FILE__, __LINE__, (a), (b), false)
+#define WVPASSNE(a, b) WVFAILEQ(a, b)
+#define WVFAILNE(a, b) WVPASSEQ(a, b)
+
+#define WVTEST_MAIN3(descr, ff, ll, slowness) \
+    static void _wvtest_main_##ll(); \
+    static WvTest _wvtest_##ll(descr, ff, _wvtest_main_##ll, slowness); \
+    static void _wvtest_main_##ll()
+#define WVTEST_MAIN2(descr, ff, ll, slowness) \
+    WVTEST_MAIN3(descr, ff, ll, slowness)
+#define WVTEST_MAIN(descr) WVTEST_MAIN2(descr, __FILE__, __LINE__, 0)
+#define WVTEST_SLOW_MAIN(descr) WVTEST_MAIN2(descr, __FILE__, __LINE__, 1)
+
+
+#endif // __WVTEST_H
diff --git a/wvtest/cpp/wvtestmain.cc b/wvtest/cpp/wvtestmain.cc
new file mode 100644
index 0000000..f6298d5
--- /dev/null
+++ b/wvtest/cpp/wvtestmain.cc
@@ -0,0 +1,102 @@
+/*
+ * WvTest:
+ *   Copyright (C)1997-2012 Net Integration Technologies and contributors.
+ *       Licensed under the GNU Library General Public License, version 2.
+ *       See the included file named LICENSE for license information.
+ *       You can get wvtest from: http://github.com/apenwarr/wvtest
+ */
+#include "wvtest.h"
+#ifdef HAVE_WVCRASH
+# include "wvcrash.h"
+#endif
+#include <stdlib.h>
+#include <stdio.h>
+#ifdef _WIN32
+#include <io.h>
+#include <windows.h>
+#else
+#include <unistd.h>
+#include <fcntl.h>
+#endif
+
+static bool fd_is_valid(int fd)
+{
+#ifdef _WIN32
+    if ((HANDLE)_get_osfhandle(fd) != INVALID_HANDLE_VALUE) return true;
+#endif
+    int nfd = dup(fd);
+    if (nfd >= 0)
+    {
+	close(nfd);
+	return true;
+    }
+    return false;
+
+}
+
+
+static int fd_count(const char *when)
+{
+    int count = 0;
+
+    printf("fds open at %s:", when);
+
+    for (int fd = 0; fd < 1024; fd++)
+    {
+	if (fd_is_valid(fd))
+	{
+	    count++;
+	    printf(" %d", fd);
+	    fflush(stdout);
+	}
+    }
+    printf("\n");
+
+    return count;
+}
+
+
+int main(int argc, char **argv)
+{
+    char buf[200];
+#if defined(_WIN32) && defined(HAVE_WVCRASH)
+    setup_console_crash();
+#endif
+
+    // test wvtest itself.  Not very thorough, but you have to draw the
+    // line somewhere :)
+    WVPASS(true);
+    WVPASS(1);
+    WVFAIL(false);
+    WVFAIL(0);
+    int startfd, endfd;
+    char * const *prefixes = NULL;
+
+    if (argc > 1)
+	prefixes = argv + 1;
+
+    startfd = fd_count("start");
+    int ret = WvTest::run_all(prefixes);
+
+    if (ret == 0) // don't pollute the strace output if we failed anyway
+    {
+	endfd = fd_count("end");
+
+	WVPASS(startfd == endfd);
+#ifndef _WIN32
+	if (startfd != endfd)
+	{
+	    sprintf(buf, "ls -l /proc/%d/fd", getpid());
+	    if (system(buf) == -1) {
+		fprintf(stderr, "Unable to list open fds\n");
+	    }
+	}
+#endif
+    }
+
+    // keep 'make' from aborting if this environment variable is set
+    if (getenv("WVTEST_NO_FAIL"))
+	return 0;
+    else
+	return ret;
+}
diff --git a/wvtest/dotnet/.gitignore b/wvtest/dotnet/.gitignore
new file mode 100644
index 0000000..cd79073
--- /dev/null
+++ b/wvtest/dotnet/.gitignore
@@ -0,0 +1 @@
+*.cs.E
diff --git a/wvtest/dotnet/Makefile b/wvtest/dotnet/Makefile
new file mode 100644
index 0000000..7ed3414
--- /dev/null
+++ b/wvtest/dotnet/Makefile
@@ -0,0 +1,19 @@
+
+all: t/test.exe
+
+include wvtestrules.mk
+
+CPPFLAGS=-I.
+
+t/test.exe: wvtest.cs wvtestmain.cs t/wvtest.t.cs.E
+	gmcs /out:$@ /debug $^
+
+runtests: t/test.exe
+	cd t && mono --debug test.exe
+
+test:
+	../wvtestrun $(MAKE) runtests
+
+clean::
+	rm -f *~ t/*~ .*~ *.E t/*.E *.d t/*.d t/*.exe t/*.mdb
+
diff --git a/wvtest/dotnet/t/wvtest.t.cs b/wvtest/dotnet/t/wvtest.t.cs
new file mode 100644
index 0000000..25461a1
--- /dev/null
+++ b/wvtest/dotnet/t/wvtest.t.cs
@@ -0,0 +1,96 @@
+/*
+ * Versaplex:
+ *   Copyright (C)2007-2008 Versabanq Innovations Inc. and contributors.
+ *       See the included file named LICENSE for license information.
+ */
+#include "wvtest.cs.h"
+using System;
+using Wv.Test;
+
+[TestFixture]
+public class WvTestTest
+{
+ 	[Test] public void test_wvtest()
+	{
+	    WVPASS(1);
+	    WVPASS("hello");
+	    WVPASS(new Object());
+	    WVPASS(0 != 1);
+
+	    WVFAIL(0);
+	    WVFAIL("");
+	    WVFAIL(null);
+
+	    WVPASSEQ(7, 7);
+	    WVPASSEQ("foo", "foo");
+	    WVPASSEQ("", "");
+	    Object obj = new Object();
+	    WVPASSEQ(obj, obj);
+	    WVPASSEQ(null, null);
+
+	    WVPASSNE(7, 8);
+	    WVPASSNE("foo", "blue");
+	    WVPASSNE("", "notempty");
+	    WVPASSNE(null, "");
+	    WVPASSNE(obj, null);
+	    WVPASSNE(obj, new Object());
+	    WVPASSNE(new Object(), new Object());
+	}
+
+	// these are only public to get rid of the "not assigned to" warnings.
+	// we don't assign to them because that's the whole point of the test.
+	public DateTime null_date;
+	public TimeSpan null_span;
+
+	[Test] public void test_dates_and_spans()
+	{
+	    WVPASS(null_date == DateTime.MinValue);
+	    WVPASSEQ(null_date, DateTime.MinValue);
+	    WVPASS(null_span == TimeSpan.Zero);
+	    WVPASSEQ(null_span, TimeSpan.Zero);
+
+	    TimeSpan t = TimeSpan.FromMinutes(60*24*7);
+	    WVPASSEQ(t.ToString(), "7.00:00:00");
+	    WVPASSEQ(t.Ticks, 7*24*60*60*10000000L);
+	    WVPASS(t.TotalMinutes == 7*24*60);
+	    WVPASSEQ(t.TotalMinutes, 7*24*60);
+	    WVPASSEQ(t.TotalSeconds, 7*24*60*60);
+	    WVPASSEQ(t.Minutes, 0);
+	}
+
+	void throw_exception()
+	{
+	    throw new System.Exception("Exception thrown");
+	}
+	void no_throw_exception()
+	{
+	    return;
+	}
+
+	[Test] public void test_exceptions()
+	{
+	    bool caught = false;
+
+	    try {
+		WVEXCEPT(throw_exception());
+	    } catch (Wv.Test.WvAssertionFailure e) {
+		throw e;
+	    } catch (System.Exception) {
+		caught = true;
+	    }
+
+	    WVPASS(caught);
+
+	    caught = false;
+
+	    System.Console.WriteLine("Ignore next failure: it is expected");
+	    WvTest.expect_next_failure();
+	    try {
+		WVEXCEPT(no_throw_exception());
+	    } catch (Wv.Test.WvAssertionFailure) {
+		caught = true;
+	    }
+
+	    WVPASS(caught);
+	}
+}
diff --git a/wvtest/dotnet/wvtest.cs b/wvtest/dotnet/wvtest.cs
new file mode 100644
index 0000000..78fae3a
--- /dev/null
+++ b/wvtest/dotnet/wvtest.cs
@@ -0,0 +1,377 @@
+/*
+ * WvTest:
+ *   Copyright (C)2007-2012 Versabanq Innovations Inc. and contributors.
+ *       Licensed under the GNU Library General Public License, version 2.
+ *       See the included file named LICENSE for license information.
+ *       You can get wvtest from: http://github.com/apenwarr/wvtest
+ */
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Runtime.Serialization;
+using Wv;
+
+// We put this in wvtest.cs since wvtest.cs should be able to compile all
+// by itself, without relying on any other parts of wvdotnet.  On the other
+// hand, it's perfectly fine for wvdotnet to have wvtest.cs in it.
+namespace Wv
+{
+    public static class WvReflection
+    {
+	public static IEnumerable<Type> find_types(Type attrtype)
+	{
+	    foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies())
+	    {
+		foreach (Type t in a.GetTypes())
+		{
+		    if (!t.IsDefined(attrtype, false))
+			continue;
+
+		    yield return t;
+		}
+	    }
+	}
+
+	public static IEnumerable<MethodInfo> find_methods(this Type t,
+							   Type attrtype)
+	{
+	    foreach (MethodInfo m in t.GetMethods())
+	    {
+		if (!m.IsDefined(attrtype, false))
+		    continue;
+
+		yield return m;
+	    }
+	}
+    }
+}
+
+namespace Wv.Test
+{
+    public class WvTest
+    {
+	struct TestInfo
+	{
+	    public string name;
+	    public Action cb;
+
+	    public TestInfo(string name, Action cb)
+	        { this.name = name; this.cb = cb; }
+	}
+        List<TestInfo> tests = new List<TestInfo>();
+
+        public int failures { get; private set; }
+
+	public WvTest()
+	{
+	    foreach (Type t in
+		     WvReflection.find_types(typeof(TestFixtureAttribute)))
+	    {
+		foreach (MethodInfo m in
+			 t.find_methods(typeof(TestAttribute)))
+		{
+		    // The new t2, m2 are needed so that each delegate gets
+		    // its own copy of the variable.
+		    Type t2 = t;
+		    MethodInfo m2 = m;
+		    RegisterTest(String.Format("{0}/{1}",
+					       t.Name, m.Name),
+				 delegate() {
+				     try {
+					 m2.Invoke(Activator.CreateInstance(t2),
+						   null);
+				     } catch (TargetInvocationException e) {
+					 throw e.InnerException;
+				     }
+				 });
+		}
+	    }
+	}
+
+        public void RegisterTest(string name, Action tc)
+        {
+            tests.Add(new TestInfo(name, tc));
+        }
+
+	public static void DoMain()
+	{
+	    // Enough to run an entire test
+	    Environment.Exit(new WvTest().Run());
+	}
+
+        public int Run()
+        {
+	    string[] args = Environment.GetCommandLineArgs();
+
+	    if (args.Length <= 1)
+		Console.WriteLine("WvTest: Running all tests");
+	    else
+		Console.WriteLine("WvTest: Running only selected tests");
+
+            foreach (TestInfo test in tests)
+	    {
+		string[] parts = test.name.Split(new char[] { '/' }, 2);
+
+		bool runthis = (args.Length <= 1);
+		foreach (string arg in args)
+		    if (parts[0].StartsWith(arg) || parts[1].StartsWith(arg))
+			runthis = true;
+
+		if (!runthis) continue;
+
+                Console.WriteLine("\nTesting \"{0}\" in {1}:",
+				  parts[1], parts[0]);
+
+                try {
+		    test.cb();
+                } catch (WvAssertionFailure) {
+                    failures++;
+                } catch (Exception e) {
+                    Console.WriteLine(e.ToString());
+                    Console.WriteLine("! WvTest Exception received   FAILED");
+                    failures++;
+                }
+            }
+
+	    Console.Out.WriteLine("Result: {0} failures.", failures);
+
+	    // Return a safe unix exit code
+	    return failures > 0 ? 1 : 0;
+        }
+
+	public static bool booleanize(bool x)
+	{
+	    return x;
+	}
+
+	public static bool booleanize(long x)
+	{
+	    return x != 0;
+	}
+
+	public static bool booleanize(ulong x)
+	{
+	    return x != 0;
+	}
+
+	public static bool booleanize(string s)
+	{
+	    return s != null && s != "";
+	}
+
+	public static bool booleanize(object o)
+	{
+	    return o != null;
+	}
+
+	static bool expect_fail = false;
+	public static void expect_next_failure()
+	{
+	    expect_fail = true;
+	}
+
+	public static bool test(bool ok, string file, int line, string s)
+	{
+	    s = s.Replace("\n", "!");
+	    s = s.Replace("\r", "!");
+	    string suffix = "";
+	    if (expect_fail)
+	    {
+		if (!ok)
+		    suffix = " (expected) ok";
+		else
+		    suffix = " (expected fail!) FAILED";
+	    }
+	    Console.WriteLine("! {0}:{1,-5} {2,-40} {3}{4}",
+			      file, line, s,
+			      ok ? "ok" : "FAILED",
+			      suffix);
+	    Console.Out.Flush();
+	    expect_fail = false;
+
+            if (!ok)
+	        throw new WvAssertionFailure(String.Format("{0}:{1} {2}", file, line, s));
+
+	    return ok;
+	}
+
+	public static void test_exception(string file, int line, string s)
+	{
+	    Console.WriteLine("! {0}:{1,-5} {2,-40} {3}",
+					 file, line, s, "EXCEPTION");
+            Console.Out.Flush();
+	}
+
+	public static bool test_eq(bool cond1, bool cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 == cond2, file, line,
+		String.Format("[{0}] == [{1}] ({{{2}}} == {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	public static bool test_eq(long cond1, long cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 == cond2, file, line,
+		String.Format("[{0}] == [{1}] ({{{2}}} == {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	public static bool test_eq(ulong cond1, ulong cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 == cond2, file, line,
+		String.Format("[{0}] == [{1}] ({{{2}}} == {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	public static bool test_eq(double cond1, double cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 == cond2, file, line,
+		String.Format("[{0}] == [{1}] ({{{2}}} == {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	public static bool test_eq(decimal cond1, decimal cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 == cond2, file, line,
+		String.Format("[{0}] == [{1}] ({{{2}}} == {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	public static bool test_eq(string cond1, string cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 == cond2, file, line,
+		String.Format("[{0}] == [{1}] ({{{2}}} == {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	// some objects can compare themselves to 'null', which is helpful.
+	// for example, DateTime.MinValue == null, but only through
+	// IComparable, not through IObject.
+	public static bool test_eq(IComparable cond1, IComparable cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1.CompareTo(cond2) == 0, file, line,
+			String.Format("[{0}] == [{1}]", s1, s2));
+	}
+
+	public static bool test_eq(object cond1, object cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 == cond2, file, line,
+		String.Format("[{0}] == [{1}]", s1, s2));
+	}
+
+	public static bool test_ne(bool cond1, bool cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 != cond2, file, line,
+		String.Format("[{0}] != [{1}] ({{{2}}} != {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	public static bool test_ne(long cond1, long cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 != cond2, file, line,
+		String.Format("[{0}] != [{1}] ({{{2}}} != {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	public static bool test_ne(ulong cond1, ulong cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 != cond2, file, line,
+		String.Format("[{0}] != [{1}] ({{{2}}} != {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	public static bool test_ne(double cond1, double cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 != cond2, file, line,
+		String.Format("[{0}] != [{1}] ({{{2}}} != {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	public static bool test_ne(decimal cond1, decimal cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 != cond2, file, line,
+		String.Format("[{0}] != [{1}] ({{{2}}} != {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	public static bool test_ne(string cond1, string cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 != cond2, file, line,
+		String.Format("[{0}] != [{1}] ({{{2}}} != {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	// See notes for test_eq(IComparable,IComparable)
+	public static bool test_ne(IComparable cond1, IComparable cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1.CompareTo(cond2) != 0, file, line,
+			String.Format("[{0}] != [{1}]", s1, s2));
+	}
+
+	public static bool test_ne(object cond1, object cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 != cond2, file, line,
+		String.Format("[{0}] != [{1}]", s1, s2));
+	}
+    }
+
+    public class WvAssertionFailure : Exception
+    {
+        public WvAssertionFailure()
+            : base()
+        {
+        }
+
+        public WvAssertionFailure(string msg)
+            : base(msg)
+        {
+        }
+    }
+
+    // Placeholders for NUnit compatibility
+    public class TestFixtureAttribute : Attribute
+    {
+    }
+    public class TestAttribute : Attribute
+    {
+    }
+    [AttributeUsage(AttributeTargets.Method, AllowMultiple=true)]
+    public class CategoryAttribute : Attribute
+    {
+        public CategoryAttribute(string x)
+        {
+        }
+    }
+}
diff --git a/wvtest/dotnet/wvtest.cs.h b/wvtest/dotnet/wvtest.cs.h
new file mode 100644
index 0000000..30ce532
--- /dev/null
+++ b/wvtest/dotnet/wvtest.cs.h
@@ -0,0 +1,9 @@
+#ifndef __WVTEST_CS_H // Blank lines in this file mess up line numbering!
+#define __WVTEST_CS_H
+#define WVASSERT(x) try { WvTest.test(WvTest.booleanize(x), __FILE__, __LINE__, #x); } catch (Wv.Test.WvAssertionFailure) { throw; } catch (System.Exception) { WvTest.test_exception(__FILE__, __LINE__, #x); throw; }
+#define WVPASS(x) WVASSERT(x)
+#define WVFAIL(x) try { WvTest.test(!WvTest.booleanize(x), __FILE__, __LINE__, "NOT(" + #x + ")"); } catch (Wv.Test.WvAssertionFailure) { throw; } catch (System.Exception) { WvTest.test_exception(__FILE__, __LINE__, "NOT(" + #x + ")"); throw; }
+#define WVEXCEPT(x) { System.Exception _wvex = null; try { x; } catch (System.Exception _wvasserte) { _wvex = _wvasserte; } WvTest.test(_wvex != null, __FILE__, __LINE__, "EXCEPT(" + #x + ")"); if (_wvex != null) throw _wvex; }
+#define WVPASSEQ(x, y) try { WvTest.test_eq((x), (y), __FILE__, __LINE__, #x, #y); } catch (Wv.Test.WvAssertionFailure) { throw; } catch (System.Exception) { WvTest.test_exception(__FILE__, __LINE__, string.Format("[{0}] == [{1}]", #x, #y)); throw; }
+#define WVPASSNE(x, y) try { WvTest.test_ne((x), (y), __FILE__, __LINE__, #x, #y); } catch (Wv.Test.WvAssertionFailure) { throw; } catch (System.Exception) { WvTest.test_exception(__FILE__, __LINE__, string.Format("[{0}] != [{1}]", #x, #y)); throw; }
+#endif // __WVTEST_CS_H
diff --git a/wvtest/dotnet/wvtestmain.cs b/wvtest/dotnet/wvtestmain.cs
new file mode 100644
index 0000000..9eae04d
--- /dev/null
+++ b/wvtest/dotnet/wvtestmain.cs
@@ -0,0 +1,17 @@
+/*
+ * WvTest:
+ *   Copyright (C)2007-2012 Versabanq Innovations Inc. and contributors.
+ *       Licensed under the GNU Library General Public License, version 2.
+ *       See the included file named LICENSE for license information.
+ *       You can get wvtest from: http://github.com/apenwarr/wvtest
+ */
+using System;
+using Wv.Test;
+
+public class WvTestMain
+{
+    public static void Main()
+    {
+	WvTest.DoMain();
+    }
+}
diff --git a/wvtest/dotnet/wvtestrules.mk b/wvtest/dotnet/wvtestrules.mk
new file mode 100644
index 0000000..638f43c
--- /dev/null
+++ b/wvtest/dotnet/wvtestrules.mk
@@ -0,0 +1,29 @@
+default: all
+
+SHELL=/bin/bash
+
+# cc -E tries to guess by extension what to do with the file.
+# And it does other weird things. cpp seems to Just Work(tm), so use that for
+# our C# (.cs) files
+CSCPP=cpp
+
+# Rules for generating autodependencies on header files
+$(patsubst %.cs.E,%.d,$(filter %.cs.E,$(FILES))): %.d: %.cs
+	@echo Generating dependency file $@ for $<
+	@set -e; set -o pipefail; rm -f $@; (\
+	    ($(CSCPP) -M -MM -MQ '$@' $(CPPFLAGS) $< && echo Makefile) \
+		| paste -s -d ' ' - && \
+	    $(CSCPP) -M -MM -MQ '$<'.E $(CPPFLAGS) $< \
+	) > $@ \
+	|| (rm -f $@ && echo "Error generating dependency file." && exit 1)
+
+include $(patsubst %.cs.E,%.d,$(filter %.cs.E,$(FILES)))
+
+# Rule for actually preprocessing source files with headers
+%.cs.E: %.cs
+	@rm -f $@
+	set -o pipefail; $(CSCPP) $(CPPFLAGS) -C -dI $< \
+		| expand -8 \
+		| sed -e 's,^#include,//#include,' \
+		| grep -v '^# [0-9]' \
+		>$@ || (rm -f $@ && exit 1)
diff --git a/wvtest/javascript/.gitignore b/wvtest/javascript/.gitignore
new file mode 100644
index 0000000..2340107
--- /dev/null
+++ b/wvtest/javascript/.gitignore
@@ -0,0 +1 @@
+jsshell
diff --git a/wvtest/javascript/Makefile b/wvtest/javascript/Makefile
new file mode 100644
index 0000000..97d4e2f
--- /dev/null
+++ b/wvtest/javascript/Makefile
@@ -0,0 +1,22 @@
+default: all
+
+all: jsshell
+
+MACOS_JS_PATH=/System/Library/Frameworks/JavaScriptCore.framework/Versions/Current/Resources/jsc
+jsshell: v8shell.cc
+	rm -f $@
+	[ -e "${MACOS_JS_PATH}" ] && \
+	ln -s "${MACOS_JS_PATH}" jsshell || \
+	g++ -g -o $@ v8shell.cc -lv8
+
+runtests: $(patsubst %.js,%.js.run,$(wildcard t/t*.js))
+
+%.js.run: %.js jsshell
+	./jsshell wvtest.js $*.js
+
+test: jsshell
+	../wvtestrun $(MAKE) runtests
+
+clean:
+	rm -f *~ */*~ v8shell jsshell
+	find . -name '*~' -exec rm -f {} \;
diff --git a/wvtest/javascript/t/empty b/wvtest/javascript/t/empty
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wvtest/javascript/t/empty
diff --git a/wvtest/javascript/t/hello b/wvtest/javascript/t/hello
new file mode 100644
index 0000000..95d09f2
--- /dev/null
+++ b/wvtest/javascript/t/hello
@@ -0,0 +1 @@
+hello world
\ No newline at end of file
diff --git a/wvtest/javascript/t/ttest.js b/wvtest/javascript/t/ttest.js
new file mode 100644
index 0000000..910d0e4
--- /dev/null
+++ b/wvtest/javascript/t/ttest.js
@@ -0,0 +1,52 @@
+
+function print_trace() {
+    print(trace())
+}
+
+
+function x() {
+    print("x()");
+    y(1);
+}
+
+
+function y(a) {
+    print("y(", a, ")");
+    z(a+5, a+6);
+}
+
+
+function z(a,b) {
+    print("z(", a, b, ")");
+    print_trace();
+}
+
+
+print("Hello world");
+x();
+
+
+wvtest('selftests', function() {
+    WVPASS('yes');
+    WVFAIL(false);
+    WVFAIL(1 == 2);
+    WVPASS();
+    WVFAIL(false);
+    WVEXCEPT(ReferenceError, function() { does_not_exist });
+    WVEXCEPT(TypeError, null);
+    WVPASSEQ('5', 5);
+    WVPASSNE('5', 6);
+    WVPASSNE(0.3, 1/3);
+    WVPASSEQ(0.3, 1/3, 0.04);
+    WVPASSNE(0.3, 1/3, 0.03);
+    WVPASSLT('5', 6);
+    WVPASSGT('6', '5');
+    WVPASSLE('5', '6');
+    WVPASSGE('5', 4);
+
+    WVPASSEQ(read('t/empty'), '');
+    WVPASSEQ(read('t/hello'), 'hello world');
+    WVEXCEPT(Error, function() { read('.') });
+    WVEXCEPT(Error, function() { load('missing') });
+    WVPASSEQ(load('t/empty'), undefined);
+});
diff --git a/wvtest/javascript/v8shell.cc b/wvtest/javascript/v8shell.cc
new file mode 100644
index 0000000..a6b42a5
--- /dev/null
+++ b/wvtest/javascript/v8shell.cc
@@ -0,0 +1,349 @@
+// Copyright 2011 the V8 project authors. All rights reserved.
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+//       notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+//       copyright notice, this list of conditions and the following
+//       disclaimer in the documentation and/or other materials provided
+//       with the distribution.
+//     * Neither the name of Google Inc. nor the names of its
+//       contributors may be used to endorse or promote products derived
+//       from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <v8.h>
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#ifdef COMPRESS_STARTUP_DATA_BZ2
+#error Using compressed startup data is not supported for this sample
+#endif
+
+
+void RunShell(v8::Handle<v8::Context> context);
+int RunMain(int argc, char* argv[]);
+bool ExecuteString(v8::Handle<v8::String> source,
+                   v8::Handle<v8::Value> name,
+                   bool print_result,
+                   bool report_exceptions);
+v8::Handle<v8::Value> Print(const v8::Arguments& args);
+v8::Handle<v8::Value> Read(const v8::Arguments& args);
+v8::Handle<v8::Value> Load(const v8::Arguments& args);
+v8::Handle<v8::Value> Quit(const v8::Arguments& args);
+v8::Handle<v8::Value> Version(const v8::Arguments& args);
+v8::Handle<v8::Value> ReadFile(const char* name);
+void ReportException(v8::TryCatch* handler);
+
+
+static bool run_shell;
+
+
+int main(int argc, char* argv[]) {
+  int result = 1;
+  v8::V8::SetFlagsFromCommandLine(&argc, argv, true);
+  run_shell = (argc == 1);
+  {
+    v8::HandleScope handle_scope;
+
+    v8::Handle<v8::ObjectTemplate> global = v8::ObjectTemplate::New();
+    global->Set(v8::String::New("print"), v8::FunctionTemplate::New(Print));
+    global->Set(v8::String::New("read"), v8::FunctionTemplate::New(Read));
+    global->Set(v8::String::New("load"), v8::FunctionTemplate::New(Load));
+    global->Set(v8::String::New("quit"), v8::FunctionTemplate::New(Quit));
+    global->Set(v8::String::New("version"), v8::FunctionTemplate::New(Version));
+    v8::Handle<v8::Context> context = v8::Context::New(NULL, global);
+    if (context.IsEmpty()) {
+      fprintf(stderr, "Error creating context\n");
+      return 1;
+    }
+    v8::Context::Scope context_scope(context);
+    result = RunMain(argc, argv);
+    if (run_shell) RunShell(context);
+  }
+  v8::V8::Dispose();
+  return result;
+}
+
+
+// Extracts a C string from a V8 Utf8Value.
+const char* ToCString(const v8::String::Utf8Value& value) {
+  return *value ? *value : "<string conversion failed>";
+}
+
+
+// Returns an exception object corresponding to the given string.
+v8::Handle<v8::Value> StringException(const char *s) {
+  return v8::ThrowException(v8::Exception::Error(v8::String::New(s)));
+}
+
+
+// Returns an exception object corresponding to the Unix errno.
+v8::Handle<v8::Value> ErrnoException(const char *prefix, int errnum) {
+  char errbuf[128];
+  snprintf(errbuf, sizeof(errbuf), "%s: %s", prefix, strerror(errnum));
+  return StringException(errbuf);
+}
+
+
+// The callback that is invoked by v8 whenever the JavaScript 'print'
+// function is called.  Prints its arguments on stdout separated by
+// spaces and ending with a newline.
+v8::Handle<v8::Value> Print(const v8::Arguments& args) {
+  bool first = true;
+  for (int i = 0; i < args.Length(); i++) {
+    v8::HandleScope handle_scope;
+    if (first) {
+      first = false;
+    } else {
+      printf(" ");
+    }
+    v8::String::Utf8Value str(args[i]);
+    printf("%s", ToCString(str));
+  }
+  printf("\n");
+  fflush(stdout);
+  return v8::Undefined();
+}
+
+
+// The callback that is invoked by v8 whenever the JavaScript 'read'
+// function is called.  This function loads the content of the file named in
+// the argument into a JavaScript string.
+v8::Handle<v8::Value> Read(const v8::Arguments& args) {
+  if (args.Length() != 1) {
+    return StringException("Usage: read(filename)");
+  }
+  v8::String::Utf8Value file(args[0]);
+  if (*file == NULL) {
+    return StringException("read(): Missing filename");
+  }
+  v8::Handle<v8::Value> source = ReadFile(*file);
+  if (source.IsEmpty() || !source->IsString()) {
+    return StringException("read(): Error loading file");
+  }
+  return source;
+}
+
+
+// The callback that is invoked by v8 whenever the JavaScript 'load'
+// function is called.  Loads, compiles and executes its argument
+// JavaScript file.
+v8::Handle<v8::Value> Load(const v8::Arguments& args) {
+  for (int i = 0; i < args.Length(); i++) {
+    v8::HandleScope handle_scope;
+    v8::String::Utf8Value filename(args[i]);
+    if (*filename == NULL) {
+      return StringException("load(): Missing filename");
+    }
+    v8::Handle<v8::String> source = v8::Handle<v8::String>::Cast(
+        ReadFile(*filename));
+    if (source.IsEmpty() || !source->IsString()) {
+      return StringException("load(): Error loading file");
+    }
+    if (!ExecuteString(source, v8::String::New(*filename), false, false)) {
+      return StringException("load(): Error executing file");
+    }
+  }
+  return v8::Undefined();
+}
+
+
+// The callback that is invoked by v8 whenever the JavaScript 'quit'
+// function is called.  Quits.
+v8::Handle<v8::Value> Quit(const v8::Arguments& args) {
+  // If not arguments are given args[0] will yield undefined which
+  // converts to the integer value 0.
+  int exit_code = args[0]->Int32Value();
+  fflush(stdout);
+  fflush(stderr);
+  exit(exit_code);
+  return v8::Undefined();
+}
+
+
+v8::Handle<v8::Value> Version(const v8::Arguments& args) {
+  return v8::String::New(v8::V8::GetVersion());
+}
+
+
+// Reads a file into a v8 string.
+v8::Handle<v8::Value> ReadFile(const char* name) {
+  int fd = open(name, O_RDONLY);
+  if (fd < 0) return ErrnoException(name, errno);
+
+  struct stat st;
+  if (fstat(fd, &st) < 0) return ErrnoException("fstat", errno);
+  if (!S_ISREG(st.st_mode)) {
+    return ErrnoException("not a regular file", EINVAL);
+  }
+
+  errno = 0;
+  off_t size = lseek(fd, 0, SEEK_END);
+  if (errno) return ErrnoException("lseek(end)", errno);
+  lseek(fd, 0, SEEK_SET);
+  if (errno) return ErrnoException("lseek(0)", errno);
+
+  char* chars = new char[size + 1];
+  int nread = 0;
+  while (nread < size) {
+    int got = read(fd, &chars[nread], size - nread);
+    if (got < 0) {
+      int errnum = errno;
+      delete[] chars;
+      return ErrnoException("read", errnum);
+    } else if (!got) {
+      break;
+    }
+    nread += got;
+  }
+  chars[nread] = 0;
+  close(fd);
+  v8::Handle<v8::String> result = v8::String::New(chars, size);
+  delete[] chars;
+  return result;
+}
+
+
+// Process remaining command line arguments and execute files
+int RunMain(int argc, char* argv[]) {
+  for (int i = 1; i < argc; i++) {
+    const char* str = argv[i];
+    if (strcmp(str, "--shell") == 0) {
+      run_shell = true;
+    } else if (strcmp(str, "-f") == 0) {
+      // Ignore any -f flags for compatibility with the other stand-
+      // alone JavaScript engines.
+      continue;
+    } else if (strncmp(str, "--", 2) == 0) {
+      fprintf(stderr, "Warning: unknown flag %s.\nTry --help for options\n", str);
+    } else if (strcmp(str, "-e") == 0 && i + 1 < argc) {
+      // Execute argument given to -e option directly.
+      v8::Handle<v8::String> file_name = v8::String::New("unnamed");
+      v8::Handle<v8::String> source = v8::String::New(argv[++i]);
+      if (!ExecuteString(source, file_name, false, true)) return 1;
+    } else {
+      // Use all other arguments as names of files to load and run.
+      v8::Handle<v8::String> file_name = v8::String::New(str);
+      v8::Handle<v8::String> source = v8::Handle<v8::String>::Cast(
+          ReadFile(str));
+      if (source.IsEmpty() || !source->IsString()) {
+        fprintf(stderr, "Error reading '%s'\n", str);
+        continue;
+      }
+      if (!ExecuteString(source, file_name, false, true)) return 1;
+    }
+  }
+  return 0;
+}
+
+
+// The read-eval-execute loop of the shell.
+void RunShell(v8::Handle<v8::Context> context) {
+  fprintf(stderr, "V8 version %s [sample shell]\n", v8::V8::GetVersion());
+  static const int kBufferSize = 256;
+  // Enter the execution environment before evaluating any code.
+  v8::Context::Scope context_scope(context);
+  v8::Local<v8::String> name(v8::String::New("(shell)"));
+  while (true) {
+    char buffer[kBufferSize];
+    fprintf(stderr, "> ");
+    char* str = fgets(buffer, kBufferSize, stdin);
+    if (str == NULL) break;
+    v8::HandleScope handle_scope;
+    ExecuteString(v8::String::New(str), name, true, true);
+  }
+  printf("\n");
+}
+
+
+// Executes a string within the current v8 context.
+bool ExecuteString(v8::Handle<v8::String> source,
+                   v8::Handle<v8::Value> name,
+                   bool print_result,
+                   bool report_exceptions) {
+  v8::HandleScope handle_scope;
+  v8::TryCatch try_catch;
+  v8::Handle<v8::Script> script = v8::Script::Compile(source, name);
+  if (script.IsEmpty()) {
+    // Print errors that happened during compilation.
+    if (report_exceptions)
+      ReportException(&try_catch);
+    return false;
+  } else {
+    v8::Handle<v8::Value> result = script->Run();
+    if (result.IsEmpty()) {
+      assert(try_catch.HasCaught());
+      // Print errors that happened during execution.
+      if (report_exceptions)
+        ReportException(&try_catch);
+      return false;
+    } else {
+      assert(!try_catch.HasCaught());
+      if (print_result && !result->IsUndefined()) {
+        // If all went well and the result wasn't undefined then print
+        // the returned value.
+        v8::String::Utf8Value str(result);
+        printf("%s\n", ToCString(str));
+      }
+      return true;
+    }
+  }
+}
+
+
+void ReportException(v8::TryCatch* try_catch) {
+  v8::HandleScope handle_scope;
+  v8::String::Utf8Value exception(try_catch->Exception());
+  const char* exception_string = ToCString(exception);
+  v8::Handle<v8::Message> message = try_catch->Message();
+  if (message.IsEmpty()) {
+    // V8 didn't provide any extra information about this error; just
+    // print the exception.
+    fprintf(stderr, "%s\n", exception_string);
+  } else {
+    // Print (filename):(line number): (message).
+    v8::String::Utf8Value filename(message->GetScriptResourceName());
+    const char* filename_string = ToCString(filename);
+    int linenum = message->GetLineNumber();
+    fprintf(stderr, "%s:%i: %s\n", filename_string, linenum, exception_string);
+    // Print line of source code.
+    v8::String::Utf8Value sourceline(message->GetSourceLine());
+    const char* sourceline_string = ToCString(sourceline);
+    fprintf(stderr, "%s\n", sourceline_string);
+    // Print wavy underline (GetUnderline is deprecated).
+    int start = message->GetStartColumn();
+    for (int i = 0; i < start; i++) {
+      fprintf(stderr, " ");
+    }
+    int end = message->GetEndColumn();
+    for (int i = start; i < end; i++) {
+      fprintf(stderr, "^");
+    }
+    fprintf(stderr, "\n");
+    v8::String::Utf8Value stack_trace(try_catch->StackTrace());
+    if (stack_trace.length() > 0) {
+      const char* stack_trace_string = ToCString(stack_trace);
+      fprintf(stderr, "%s\n", stack_trace_string);
+    }
+  }
+}
diff --git a/wvtest/javascript/wvtest.js b/wvtest/javascript/wvtest.js
new file mode 100644
index 0000000..6d7d1b8
--- /dev/null
+++ b/wvtest/javascript/wvtest.js
@@ -0,0 +1,152 @@
+
+var files = {};
+
+function lookup(filename, line) {
+    var f = files[filename];
+    if (!f) {
+        try {
+            f = files[filename] = read(filename).split('\n');
+        } catch (e) {
+            f = files[filename] = [];
+        }
+    }
+    return f[line-1] || 'BAD_LINE'; // file line numbers are 1-based
+}
+
+
+// TODO(apenwarr): Right now this only really works right on chrome.
+// Maybe take some advice from this article:
+//  http://stackoverflow.com/questions/5358983/javascript-stack-inspection-on-safari-mobile-ipad
+function trace() {
+    var FILELINE_RE = /[\b\s]\(?([^:\s]+):(\d+)/;
+    var out = [];
+    var e = Error().stack;
+    if (!e) {
+        return [['UNKNOWN', 0], ['UNKNOWN', 0]];
+    }
+    var lines = e.split('\n');
+    for (i in lines) {
+	if (i > 2) {
+	    g = lines[i].match(FILELINE_RE);
+	    if (g) {
+		out.push([g[1], parseInt(g[2])]);
+	    } else {
+		out.push(['UNKNOWN', 0]);
+	    }
+	}
+    }
+    return out;
+}
+
+
+function _pad(len, s) {
+    s += '';
+    while (s.length < len) {
+	s += ' ';
+    }
+    return s;
+}
+
+
+function _check(cond, trace, condstr) {
+    print('!', _pad(15, trace[0] + ':' + trace[1]),
+	  _pad(54, condstr),
+	  cond ? 'ok' : 'FAILED');
+}
+
+
+function _content(trace) {
+    var WV_RE = /WV[\w_]+\((.*)\)/;
+    var line = lookup(trace[0], trace[1]);
+    var g = line.match(WV_RE);
+    return g ? g[1] : '...';
+}
+
+
+function WVPASS(cond) {
+    var t = trace()[1];
+    if (arguments.length >= 1) {
+	var condstr = _content(t);
+	return _check(cond, t, condstr);
+    } else {
+	// WVPASS() with no arguments is a pass, although cond would
+	// default to false
+	return _check(true, t, '');
+    }
+}
+
+
+function WVFAIL(cond) {
+    var t = trace()[1];
+    if (arguments.length >= 1) {
+	var condstr = 'NOT(' + _content(t) + ')';
+	return _check(!cond, t, condstr);
+    } else {
+	// WVFAIL() with no arguments is a fail, although cond would
+	// default to false (which is a pass)
+	return _check(false, t, 'NOT()')
+    }
+}
+
+
+function WVEXCEPT(etype, func) {
+    var t = trace()[1];
+    try {
+	func();
+    } catch (e) {
+	return _check(e instanceof etype, t, e);
+    }
+    return _check(false, t, 'no exception: ' + etype);
+}
+
+
+function WVPASSEQ(a, b, precision) {
+    var t = trace()[1];
+    if (a && b && a.join && b.join) {
+        a = a.join('|');
+        b = b.join('|');
+    }
+    var cond = precision ? Math.abs(a-b) < precision : (a == b);
+    return _check(cond, t, '' + a + ' == ' + b);
+}
+
+
+function WVPASSNE(a, b, precision) {
+    var t = trace()[1];
+    if (a.join && b.join) {
+        a = a.join('|');
+        b = b.join('|');
+    }
+    var cond = precision ? Math.abs(a-b) >= precision : (a != b);
+    return _check(a != b, t, '' + a + ' != ' + b);
+}
+
+
+function WVPASSLT(a, b) {
+    var t = trace()[1];
+    return _check(a < b, t, '' + a + ' < ' + b);
+}
+
+
+function WVPASSGT(a, b) {
+    var t = trace()[1];
+    return _check(a > b, t, '' + a + ' > ' + b);
+}
+
+
+function WVPASSLE(a, b) {
+    var t = trace()[1];
+    return _check(a <= b, t, '' + a + ' <= ' + b);
+}
+
+
+function WVPASSGE(a, b) {
+    var t = trace()[1];
+    return _check(a >= b, t, '' + a + ' >= ' + b);
+}
+
+
+function wvtest(name, f) {
+    print('\nTesting "' + name + '" in ' + trace()[1][0] + ':');
+    return f();
+}
diff --git a/wvtest/python/Makefile b/wvtest/python/Makefile
new file mode 100644
index 0000000..5a2d068
--- /dev/null
+++ b/wvtest/python/Makefile
@@ -0,0 +1,17 @@
+
+all:
+	@echo "Try: make test"
+	@false
+
+runtests:
+	./wvtest.py \
+		$(patsubst ./%t,%t/*.py,$(shell find -type d -name t)) \
+		basedir_test.py
+	python t/twvtest.py
+	python basedir_test.py
+
+test:
+	../wvtestrun $(MAKE) runtests
+
+clean::
+	rm -f *~ t/*~ *.pyc t/*.pyc
\ No newline at end of file
diff --git a/wvtest/python/__init__.py b/wvtest/python/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wvtest/python/__init__.py
diff --git a/wvtest/python/basedir_test.py b/wvtest/python/basedir_test.py
new file mode 100644
index 0000000..932f9d2
--- /dev/null
+++ b/wvtest/python/basedir_test.py
@@ -0,0 +1,12 @@
+from wvtest import *
+
+
+@wvtest
+def basedirtest():
+    # check that wvtest works with test files in the base directory, not
+    # just in subdirs.
+    WVPASS()
+
+
+if __name__ == '__main__':
+    wvtest_main()
diff --git a/wvtest/python/t/__init__.py b/wvtest/python/t/__init__.py
new file mode 100644
index 0000000..84815ba
--- /dev/null
+++ b/wvtest/python/t/__init__.py
@@ -0,0 +1,3 @@
+import os, sys
+parentdir = os.path.join(os.path.abspath(os.path.split(__file__)[0]), '..')
+sys.path.append(parentdir)
diff --git a/wvtest/python/t/testfile.txt b/wvtest/python/t/testfile.txt
new file mode 100644
index 0000000..3b18e51
--- /dev/null
+++ b/wvtest/python/t/testfile.txt
@@ -0,0 +1 @@
+hello world
diff --git a/wvtest/python/t/twvtest.py b/wvtest/python/t/twvtest.py
new file mode 100644
index 0000000..8224794
--- /dev/null
+++ b/wvtest/python/t/twvtest.py
@@ -0,0 +1,71 @@
+import __init__
+from wvtest import *
+import twvtest2  # twvtest2 will also run *before* us since we import it
+
+last=None
+
+def _except(*args):
+    raise IOError(*args)
+
+
+@wvtest
+def moretest():
+    WVPASSEQ(twvtest2.count, 1)
+
+
+@wvtest
+def test1():
+    WVPASSIS(None, None)
+    WVPASSISNOT(None, [])
+    WVPASSISNOT({}, {})
+    d = {}
+    WVPASSIS(d, d)
+    WVPASSEQ(1, 1)
+    WVPASSNE(1, 2)
+    WVPASSLT(1, 2)
+    WVPASSLE(1, 1)
+    WVPASSGT(2, 1)
+    WVPASSGE(2, 2)
+    WVPASSNEAR(1, 1)
+    WVPASSFAR(1, 0)
+    WVPASSNEAR(1, 1.0)
+    WVPASSFAR(0.1, 0.2)
+    WVPASSNEAR(0.000000005, 0.000000006)
+    WVPASSFAR(0.000000005, 0.000000006, places=9)
+    WVPASSNEAR(0.51, 0.53, delta=0.021)
+    WVPASSFAR(0.51, 0.53, delta=0.019)
+    WVEXCEPT(IOError, _except, 'my exception parameter')
+    with WVEXCEPT(IOError):
+      _except('arg')
+
+    # ensure tests run in the order they were declared
+    global last
+    WVPASSEQ(last, None)
+    last='test1'
+
+@wvtest
+def booga2():
+    # ensure tests run in the order they were declared
+    global last
+    WVPASSEQ(last, 'test1')
+    last='booga2'
+
+@wvtest
+def booga1():
+    # ensure tests run in the order they were declared
+    global last
+    WVPASSEQ(last, 'booga2')
+    last='booga1'
+
+
+@wvtest
+def chdir_test():
+    WVPASS(open('testfile.txt')) # will fail if chdir is wrong
+
+
+if __name__ == '__main__':
+    WVPASSEQ(twvtest2.count, 0)
+    wvtest_main()
+    wvtest_main()
+    WVPASSEQ(last, 'booga1')
+    WVPASSEQ(twvtest2.count, 1)
diff --git a/wvtest/python/t/twvtest2.py b/wvtest/python/t/twvtest2.py
new file mode 100644
index 0000000..447cd28
--- /dev/null
+++ b/wvtest/python/t/twvtest2.py
@@ -0,0 +1,11 @@
+from wvtest import *
+
+
+count = 0
+
+
+@wvtest
+def moretest():
+    global count
+    count += 1
+    WVPASS()
diff --git a/wvtest/python/unittest.py b/wvtest/python/unittest.py
new file mode 100644
index 0000000..78a9525
--- /dev/null
+++ b/wvtest/python/unittest.py
@@ -0,0 +1,89 @@
+import sys
+import traceback
+import wvtest
+
+
+class _Meta(type):
+  def __init__(cls, name, bases, attrs):
+    type.__init__(cls, name, bases, attrs)
+    print 'registering class %r' % name
+    for t in dir(cls):
+      if t.startswith('test'):
+        # TODO(apenwarr): inside a class, sort by source code line number
+        print 'registering func %r' % t
+        def DefineGo(t):
+          def Go():
+            o = cls(t)
+            o.setUp()
+            try:
+              getattr(o, t)()
+            except Exception, e:
+              print
+              print traceback.format_exc()
+              tb = sys.exc_info()[2]
+              wvtest._result(repr(e), traceback.extract_tb(tb)[-1],
+                             'EXCEPTION')
+            finally:
+              o.tearDown()
+          return Go
+        wvtest.wvtest(DefineGo(t), getattr(cls, t))
+
+
+class TestCase():
+  __metaclass__ = _Meta
+
+  def __init__(self, testname):
+    pass
+
+  def setUp(self):
+    pass
+
+  def tearDown(self):
+    pass
+
+  def assertTrue(self, a, unused_msg=''):
+    return wvtest.WVPASS(a, xdepth=1)
+
+  def assertFalse(self, a, unused_msg=''):
+    return wvtest.WVFAIL(a, xdepth=1)
+
+  def assertIs(self, a, b):
+    return wvtest.WVPASSIS(a, b, xdepth=1)
+
+  def assertIsNot(self, a, b):
+    return wvtest.WVPASSISNOT(a, b, xdepth=1)
+
+  def assertEqual(self, a, b):
+    return wvtest.WVPASSEQ(a, b, xdepth=1)
+
+  def assertNotEqual(self, a, b):
+    return wvtest.WVPASSNE(a, b, xdepth=1)
+
+  def assertGreaterEqual(self, a, b):
+    return wvtest.WVPASSGE(a, b, xdepth=1)
+
+  def assertGreaterThan(self, a, b):
+    return wvtest.WVPASSGT(a, b, xdepth=1)
+
+  def assertLessEqual(self, a, b):
+    return wvtest.WVPASSLE(a, b, xdepth=1)
+
+  def assertLessThan(self, a, b):
+    return wvtest.WVPASSLT(a, b, xdepth=1)
+
+  def assertAlmostEqual(self, a, b, places=7, delta=None):
+    return wvtest.WVPASSNEAR(a, b, places=places, delta=delta, xdepth=1)
+
+  def assertNotAlmostEqual(self, a, b, places=7, delta=None):
+    return wvtest.WVPASSFAR(a, b, places=places, delta=delta, xdepth=1)
+
+  def assertRaises(self, etype, func=None, *args, **kwargs):
+    return wvtest._WVEXCEPT(etype, 0, func, *args, **kwargs)
+
+  assertEquals = assertEqual
+  assertNowEquals = assertNotEqual
+  assertGreater = assertGreaterThan
+  assertLess = assertLessThan
+
+def main():
+  wvtest.wvtest_main()
diff --git a/wvtest/python/wvtest.py b/wvtest/python/wvtest.py
new file mode 100755
index 0000000..8f2564c
--- /dev/null
+++ b/wvtest/python/wvtest.py
@@ -0,0 +1,252 @@
+#!/usr/bin/env python
+#
+# WvTest:
+#   Copyright (C)2007-2012 Versabanq Innovations Inc. and contributors.
+#       Licensed under the GNU Library General Public License, version 2.
+#       See the included file named LICENSE for license information.
+#       You can get wvtest from: http://github.com/apenwarr/wvtest
+#
+import atexit
+import inspect
+import os
+import re
+import sys
+import traceback
+
+# NOTE
+# Why do we do we need the "!= main" check?  Because if you run
+# wvtest.py as a main program and it imports your test files, then
+# those test files will try to import the wvtest module recursively.
+# That actually *works* fine, because we don't run this main program
+# when we're imported as a module.  But you end up with two separate
+# wvtest modules, the one that gets imported, and the one that's the
+# main program.  Each of them would have duplicated global variables
+# (most importantly, wvtest._registered), and so screwy things could
+# happen.  Thus, we make the main program module *totally* different
+# from the imported module.  Then we import wvtest (the module) into
+# wvtest (the main program) here and make sure to refer to the right
+# versions of global variables.
+#
+# All this is done just so that wvtest.py can be a single file that's
+# easy to import into your own applications.
+if __name__ != '__main__':   # we're imported as a module
+    _registered = []
+    _tests = 0
+    _fails = 0
+
+    def wvtest(func, innerfunc=None):
+        """ Use this decorator (@wvtest) in front of any function you want to
+            run as part of the unit test suite.  Then run:
+                python wvtest.py path/to/yourtest.py [other test.py files...]
+            to run all the @wvtest functions in the given file(s).
+        """
+        _registered.append((func, innerfunc or func))
+        return func
+
+
+    def _result(msg, tb, code):
+        global _tests, _fails
+        _tests += 1
+        if code != 'ok':
+            _fails += 1
+        (filename, line, func, text) = tb
+        filename = os.path.basename(filename)
+        msg = re.sub(r'\s+', ' ', str(msg))
+        sys.stderr.flush()
+        print '! %-70s %s' % ('%s:%-4d %s' % (filename, line, msg),
+                              code)
+        sys.stdout.flush()
+
+
+    def _check(cond, msg, xdepth):
+        tb = traceback.extract_stack()[-3 - xdepth]
+        if cond:
+            _result(msg, tb, 'ok')
+        else:
+            _result(msg, tb, 'FAILED')
+        return cond
+
+
+    def _code(xdepth):
+        (filename, line, func, text) = traceback.extract_stack()[-3 - xdepth]
+        text = re.sub(r'^[\w\.]+\((.*)\)(\s*#.*)?$', r'\1', str(text));
+        return text
+
+
+    def WVPASS(cond = True, xdepth = 0):
+        ''' Counts a test failure unless cond is true. '''
+        return _check(cond, _code(xdepth), xdepth)
+
+    def WVFAIL(cond = True, xdepth = 0):
+        ''' Counts a test failure  unless cond is false. '''
+        return _check(not cond, 'NOT(%s)' % _code(xdepth), xdepth)
+
+    def WVPASSIS(a, b, xdepth = 0):
+        ''' Counts a test failure unless a is b. '''
+        return _check(a is b, '%s is %s' % (repr(a), repr(b)), xdepth)
+
+    def WVPASSISNOT(a, b, xdepth = 0):
+        ''' Counts a test failure unless a is not b. '''
+        return _check(a is not b, '%s is not %s' % (repr(a), repr(b)), xdepth)
+
+    def WVPASSEQ(a, b, xdepth = 0):
+        ''' Counts a test failure unless a == b. '''
+        return _check(a == b, '%s == %s' % (repr(a), repr(b)), xdepth)
+
+    def WVPASSNE(a, b, xdepth = 0):
+        ''' Counts a test failure unless a != b. '''
+        return _check(a != b, '%s != %s' % (repr(a), repr(b)), xdepth)
+
+    def WVPASSLT(a, b, xdepth = 0):
+        ''' Counts a test failure unless a < b. '''
+        return _check(a < b, '%s < %s' % (repr(a), repr(b)), xdepth)
+
+    def WVPASSLE(a, b, xdepth = 0):
+        ''' Counts a test failure unless a <= b. '''
+        return _check(a <= b, '%s <= %s' % (repr(a), repr(b)), xdepth)
+
+    def WVPASSGT(a, b, xdepth = 0):
+        ''' Counts a test failure unless a > b. '''
+        return _check(a > b, '%s > %s' % (repr(a), repr(b)), xdepth)
+
+    def WVPASSGE(a, b, xdepth = 0):
+        ''' Counts a test failure unless a >= b. '''
+        return _check(a >= b, '%s >= %s' % (repr(a), repr(b)), xdepth)
+
+    def WVPASSNEAR(a, b, places = 7, delta = None, xdepth = 0):
+        ''' Counts a test failure unless a ~= b. '''
+        if delta:
+            return _check(abs(a - b) <= abs(delta),
+                          '%s ~= %s' % (repr(a), repr(b)), xdepth)
+        else:
+            return _check(round(a, places) == round(b, places),
+                          '%s ~= %s' % (repr(a), repr(b)), xdepth)
+
+    def WVPASSFAR(a, b, places = 7, delta = None, xdepth = 0):
+        ''' Counts a test failure unless a ~!= b. '''
+        if delta:
+            return _check(abs(a - b) > abs(delta),
+                          '%s ~= %s' % (repr(a), repr(b)), xdepth)
+        else:
+            return _check(round(a, places) != round(b, places),
+                          '%s ~= %s' % (repr(a), repr(b)), xdepth)
+
+    def _except_report(cond, code, xdepth):
+        return _check(cond, 'EXCEPT(%s)' % code, xdepth + 1)
+
+    class _ExceptWrapper(object):
+        def __init__(self, etype, xdepth):
+            self.etype = etype
+            self.xdepth = xdepth
+            self.code = None
+
+        def __enter__(self):
+          self.code = _code(self.xdepth)
+
+        def __exit__(self, etype, value, traceback):
+            if etype == self.etype:
+                _except_report(True, self.code, self.xdepth)
+                return 1  # success, got the expected exception
+            elif etype is None:
+                _except_report(False, self.code, self.xdepth)
+                return 0
+            else:
+                _except_report(False, self.code, self.xdepth)
+
+    def _WVEXCEPT(etype, xdepth, func, *args, **kwargs):
+        if func:
+            code = _code(xdepth + 1)
+            try:
+                func(*args, **kwargs)
+            except etype, e:
+                return _except_report(True, code, xdepth + 1)
+            except:
+                _except_report(False, code, xdepth + 1)
+                raise
+            else:
+                return _except_report(False, code, xdepth + 1)
+        else:
+            return _ExceptWrapper(etype, xdepth)
+
+    def WVEXCEPT(etype, func=None, *args, **kwargs):
+        ''' Counts a test failure unless func throws an 'etype' exception.
+            You have to spell out the function name and arguments, rather than
+            calling the function yourself, so that WVEXCEPT can run before
+            your test code throws an exception.
+        '''
+        return _WVEXCEPT(etype, 0, func, *args, **kwargs)
+
+
+    def _check_unfinished():
+        if _registered:
+            for func, innerfunc in _registered:
+                print 'WARNING: not run: %r' % (innerfunc,)
+            WVFAIL('wvtest_main() not called')
+        if _fails:
+            sys.exit(1)
+
+    atexit.register(_check_unfinished)
+
+
+def _run_in_chdir(path, func, *args, **kwargs):
+    oldwd = os.getcwd()
+    oldpath = sys.path
+    try:
+        if path: os.chdir(path)
+        sys.path += [path, os.path.split(path)[0]]
+        return func(*args, **kwargs)
+    finally:
+        os.chdir(oldwd)
+        sys.path = oldpath
+
+
+def _runtest(fname, f, innerfunc):
+    import wvtest as _wvtestmod
+    mod = inspect.getmodule(innerfunc)
+    relpath = os.path.relpath(mod.__file__, os.getcwd()).replace('.pyc', '.py')
+    print
+    print 'Testing "%s" in %s:' % (fname, relpath)
+    sys.stdout.flush()
+    try:
+        _run_in_chdir(os.path.split(mod.__file__)[0], f)
+    except Exception, e:
+        print
+        print traceback.format_exc()
+        tb = sys.exc_info()[2]
+        _wvtestmod._result(repr(e), traceback.extract_tb(tb)[-1], 'EXCEPTION')
+
+
+def _run_registered_tests():
+    import wvtest as _wvtestmod
+    while _wvtestmod._registered:
+        func, innerfunc = _wvtestmod._registered.pop(0)
+        _runtest(innerfunc.func_name, func, innerfunc)
+        print
+
+
+def wvtest_main(extra_testfiles=[]):
+    import wvtest as _wvtestmod
+    _run_registered_tests()
+    for modname in extra_testfiles:
+        if not os.path.exists(modname):
+            print 'Skipping: %s' % modname
+            continue
+        if modname.endswith('.py'):
+            modname = modname[:-3]
+        print 'Importing: %s' % modname
+        path, mod = os.path.split(os.path.abspath(modname))
+        nicename = modname.replace(os.path.sep, '.')
+        while nicename.startswith('.'):
+            nicename = modname[1:]
+        _run_in_chdir(path, __import__, nicename, None, None, [])
+        _run_registered_tests()
+    print
+    print 'WvTest: %d tests, %d failures.' % (_wvtestmod._tests,
+                                              _wvtestmod._fails)
+
+
+if __name__ == '__main__':
+    import wvtest as _wvtestmod
+    sys.modules['wvtest'] = _wvtestmod
+    sys.modules['wvtest.wvtest'] = _wvtestmod
+    wvtest_main(sys.argv[1:])
diff --git a/wvtest/sample-error b/wvtest/sample-error
new file mode 100644
index 0000000..3890d6d
--- /dev/null
+++ b/wvtest/sample-error
@@ -0,0 +1,5 @@
+Testing "my error test function" in mytest.t.cc:
+! mytest.t.cc:432     thing.works()         ok
+This is just some crap that I printed while counting to 3.
+! mytest.t.cc.433     3 < 4                 FAILED
+
diff --git a/wvtest/sample-ok b/wvtest/sample-ok
new file mode 100644
index 0000000..82047d7
--- /dev/null
+++ b/wvtest/sample-ok
@@ -0,0 +1,5 @@
+Testing "my ok test function" in mytest.t.cc:
+! mytest.t.cc:432     thing.works()         ok
+This is just some crap that I printed while counting to 3.
+! mytest.t.cc.433     3 < 4                 ok
+
diff --git a/wvtest/sh/Makefile b/wvtest/sh/Makefile
new file mode 100644
index 0000000..1f31192
--- /dev/null
+++ b/wvtest/sh/Makefile
@@ -0,0 +1,13 @@
+
+all:
+	@echo "Try: make test"
+	@false
+
+runtests:
+	t/twvtest.sh
+
+test:
+	../wvtestrun $(MAKE) runtests
+
+clean::
+	rm -f *~ t/*~
diff --git a/wvtest/sh/t/twvtest.sh b/wvtest/sh/t/twvtest.sh
new file mode 100755
index 0000000..c02871a
--- /dev/null
+++ b/wvtest/sh/t/twvtest.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+. ./wvtest.sh
+
+WVSTART "main test"
+WVPASS true
+WVPASS true
+WVPASS true
+WVFAIL false
+WVPASSEQ "$(ls | sort)" "$(ls)"
+WVPASSNE "5" "5 "
+WVPASSEQ "" ""
+(echo nested test; true); WVPASSRC $?
+(echo nested fail; false); WVFAILRC $?
+
+WVSTART another test
+WVPASS true
diff --git a/wvtest/sh/wvtest.sh b/wvtest/sh/wvtest.sh
new file mode 100644
index 0000000..47b4366
--- /dev/null
+++ b/wvtest/sh/wvtest.sh
@@ -0,0 +1,140 @@
+#
+# WvTest:
+#   Copyright (C)2007-2012 Versabanq Innovations Inc. and contributors.
+#       Licensed under the GNU Library General Public License, version 2.
+#       See the included file named LICENSE for license information.
+#       You can get wvtest from: http://github.com/apenwarr/wvtest
+#
+# Include this file in your shell script by using:
+#         #!/bin/sh
+#         . ./wvtest.sh
+#
+
+# we don't quote $TEXT in case it contains newlines; newlines
+# aren't allowed in test output.  However, we set -f so that
+# at least shell glob characters aren't processed.
+_wvtextclean()
+{
+	( set -f; echo $* )
+}
+
+
+if [ -n "$BASH_VERSION" ]; then
+	_wvfind_caller()
+	{
+		LVL=$1
+		WVCALLER_FILE=${BASH_SOURCE[2]}
+		WVCALLER_LINE=${BASH_LINENO[1]}
+	}
+else
+	_wvfind_caller()
+	{
+		LVL=$1
+		WVCALLER_FILE="unknown"
+		WVCALLER_LINE=0
+	}
+fi
+
+
+_wvcheck()
+{
+	CODE="$1"
+	TEXT=$(_wvtextclean "$2")
+	OK=ok
+	if [ "$CODE" -ne 0 ]; then
+		OK=FAILED
+	fi
+	echo "! $WVCALLER_FILE:$WVCALLER_LINE  $TEXT  $OK" >&2
+	if [ "$CODE" -ne 0 ]; then
+		exit $CODE
+	else
+		return 0
+	fi
+}
+
+
+WVPASS()
+{
+	TEXT="$*"
+
+	_wvfind_caller
+	if "$@"; then
+		_wvcheck 0 "$TEXT"
+		return 0
+	else
+		_wvcheck 1 "$TEXT"
+		# NOTREACHED
+		return 1
+	fi
+}
+
+
+WVFAIL()
+{
+	TEXT="$*"
+
+	_wvfind_caller
+	if "$@"; then
+		_wvcheck 1 "NOT($TEXT)"
+		# NOTREACHED
+		return 1
+	else
+		_wvcheck 0 "NOT($TEXT)"
+		return 0
+	fi
+}
+
+
+_wvgetrv()
+{
+	( "$@" >&2 )
+	echo -n $?
+}
+
+
+WVPASSEQ()
+{
+	_wvfind_caller
+	_wvcheck $(_wvgetrv [ "$#" -eq 2 ]) "exactly 2 arguments"
+	echo "Comparing:" >&2
+	echo "$1" >&2
+	echo "--" >&2
+	echo "$2" >&2
+	_wvcheck $(_wvgetrv [ "$1" = "$2" ]) "'$1' = '$2'"
+}
+
+
+WVPASSNE()
+{
+	_wvfind_caller
+	_wvcheck $(_wvgetrv [ "$#" -eq 2 ]) "exactly 2 arguments"
+	echo "Comparing:" >&2
+	echo "$1" >&2
+	echo "--" >&2
+	echo "$2" >&2
+	_wvcheck $(_wvgetrv [ "$1" != "$2" ]) "'$1' != '$2'"
+}
+
+
+WVPASSRC()
+{
+	RC=$?
+	_wvfind_caller
+	_wvcheck $(_wvgetrv [ $RC -eq 0 ]) "return code($RC) == 0"
+}
+
+
+WVFAILRC()
+{
+	RC=$?
+	_wvfind_caller
+	_wvcheck $(_wvgetrv [ $RC -ne 0 ]) "return code($RC) != 0"
+}
+
+
+WVSTART()
+{
+	echo >&2
+	_wvfind_caller
+	echo "Testing \"$*\" in $WVCALLER_FILE:" >&2
+}
diff --git a/wvtest/wvtestrun b/wvtest/wvtestrun
new file mode 100755
index 0000000..897b95f
--- /dev/null
+++ b/wvtest/wvtestrun
@@ -0,0 +1,187 @@
+#!/usr/bin/perl -w
+#
+# WvTest:
+#   Copyright (C)2007-2012 Versabanq Innovations Inc. and contributors.
+#       Licensed under the GNU Library General Public License, version 2.
+#       See the included file named LICENSE for license information.
+#       You can get wvtest from: http://github.com/apenwarr/wvtest
+#
+use strict;
+use Time::HiRes qw(time);
+
+# always flush
+$| = 1;
+
+if (@ARGV < 1) {
+    print STDERR "Usage: $0 <command line...>\n";
+    exit 127;
+}
+
+print STDERR "Testing \"all\" in @ARGV:\n";
+
+my $pid = open(my $fh, "-|");
+if (!$pid) {
+    # child
+    setpgrp();
+    open STDERR, '>&STDOUT' or die("Can't dup stdout: $!\n");
+    exec(@ARGV);
+    exit 126; # just in case
+}
+
+my $istty = -t STDOUT && $ENV{'TERM'} ne "dumb";
+my @log = ();
+my ($gpasses, $gfails) = (0,0);
+
+sub bigkill($)
+{
+    my $pid = shift;
+
+    if (@log) {
+	print "\n" . join("\n", @log) . "\n";
+    }
+
+    print STDERR "\n! Killed by signal    FAILED\n";
+
+    ($pid > 0) || die("pid is '$pid'?!\n");
+
+    local $SIG{CHLD} = sub { }; # this will wake us from sleep() faster
+    kill 15, $pid;
+    sleep(2);
+
+    if ($pid > 1) {
+	kill 9, -$pid;
+    }
+    kill 9, $pid;
+
+    exit(125);
+}
+
+# parent
+local $SIG{INT} = sub { bigkill($pid); };
+local $SIG{TERM} = sub { bigkill($pid); };
+local $SIG{ALRM} = sub {
+    print STDERR "Alarm timed out!  No test results for too long.\n";
+    bigkill($pid);
+};
+
+sub colourize($)
+{
+    my $result = shift;
+    my $pass = ($result eq "ok");
+
+    if ($istty) {
+	my $colour = $pass ? "\e[32;1m" : "\e[31;1m";
+	return "$colour$result\e[0m";
+    } else {
+	return $result;
+    }
+}
+
+sub mstime($$$)
+{
+    my ($floatsec, $warntime, $badtime) = @_;
+    my $ms = int($floatsec * 1000);
+    my $str = sprintf("%d.%03ds", $ms/1000, $ms % 1000);
+
+    if ($istty && $ms > $badtime) {
+        return "\e[31;1m$str\e[0m";
+    } elsif ($istty && $ms > $warntime) {
+        return "\e[33;1m$str\e[0m";
+    } else {
+        return "$str";
+    }
+}
+
+sub resultline($$)
+{
+    my ($name, $result) = @_;
+    return sprintf("! %-65s %s", $name, colourize($result));
+}
+
+my $allstart = time();
+my ($start, $stop);
+
+sub endsect()
+{
+    $stop = time();
+    if ($start) {
+	printf " %s %s\n", mstime($stop - $start, 500, 1000), colourize("ok");
+    }
+}
+
+while (<$fh>)
+{
+    chomp;
+    s/\r//g;
+
+    if (/^\s*Testing "(.*)" in (.*):\s*$/)
+    {
+        alarm(120);
+	my ($sect, $file) = ($1, $2);
+
+	endsect();
+
+	printf("! %s  %s: ", $file, $sect);
+	@log = ();
+	$start = $stop;
+    }
+    elsif (/^!\s*(.*?)\s+(\S+)\s*$/)
+    {
+        alarm(120);
+
+	my ($name, $result) = ($1, $2);
+	my $pass = ($result eq "ok");
+
+	if (!$start) {
+	    printf("\n! Startup: ");
+	    $start = time();
+	}
+
+	push @log, resultline($name, $result);
+
+	if (!$pass) {
+	    $gfails++;
+	    if (@log) {
+		print "\n" . join("\n", @log) . "\n";
+		@log = ();
+	    }
+	} else {
+	    $gpasses++;
+	    print ".";
+	}
+    }
+    else
+    {
+	push @log, $_;
+    }
+}
+
+endsect();
+
+my $newpid = waitpid($pid, 0);
+if ($newpid != $pid) {
+    die("waitpid returned '$newpid', expected '$pid'\n");
+}
+
+my $code = $?;
+my $ret = ($code >> 8);
+
+# return death-from-signal exits as >128.  This is what bash does if you ran
+# the program directly.
+if ($code && !$ret) { $ret = $code | 128; }
+
+if ($ret && @log) {
+    print "\n" . join("\n", @log) . "\n";
+}
+
+if ($code != 0) {
+    print resultline("Program returned non-zero exit code ($ret)", "FAILED");
+}
+
+my $gtotal = $gpasses+$gfails;
+printf("\nWvTest: %d test%s, %d failure%s, total time %s.\n",
+    $gtotal, $gtotal==1 ? "" : "s",
+    $gfails, $gfails==1 ? "" : "s",
+    mstime(time() - $allstart, 2000, 5000));
+print STDERR "\nWvTest result code: $ret\n";
+exit( $ret ? $ret : ($gfails ? 125 : 0) );