Merge "Refactor isoping and add unit tests."
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) );