Merge "hnvram: Added option to choose target file + tests"
diff --git a/cmds/Makefile b/cmds/Makefile
index b058618..45580b2 100644
--- a/cmds/Makefile
+++ b/cmds/Makefile
@@ -33,6 +33,7 @@
dnsck \
freemegs \
gfhd254_reboot \
+ gflldpd \
gstatic \
http_bouncer \
ionice \
@@ -52,8 +53,10 @@
LIB_TARGETS=\
stdoutline.so
HOST_TEST_TARGETS=\
+ host-gflldpd_test \
host-netusage_test \
- host-utils_test
+ host-utils_test \
+ host-isoping_test
SCRIPT_TARGETS=\
is-secure-boot
ARCH_TARGETS=\
@@ -148,6 +151,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 +177,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 +197,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)
@@ -264,6 +274,10 @@
anonid: anonid.o
host-anonid: host-anonid.o
anonid host-anonid: LIBS += -lcrypto
+host-gflldpd_test.o: CXXFLAGS += -D WVTEST_CONFIGURED -I ../wvtest/cpp
+host-gflldpd_test.o: gflldpd.c
+host-gflldpd_test: LIBS+=$(HOST_LIBS) -lm -lstdc++
+host-gflldpd_test: host-gflldpd_test.o host-wvtestmain.o host-wvtest.o
TESTS = $(wildcard test-*.sh) $(wildcard test-*.py) $(wildcard *_test.py) $(TEST_TARGETS)
diff --git a/cmds/anonid.c b/cmds/anonid.c
index e7854ad..106f5fc 100644
--- a/cmds/anonid.c
+++ b/cmds/anonid.c
@@ -177,6 +177,7 @@
usage(argv[0]);
}
+ memset(anonid, 0, sizeof(anonid));
get_anonid_for_mac(addr, anonid);
printf("%s\n", anonid);
diff --git a/cmds/castcheck b/cmds/castcheck
index 70080ec..40c90cc 100755
--- a/cmds/castcheck
+++ b/cmds/castcheck
@@ -13,7 +13,7 @@
while IFS=";" read ip; do
cast_devices="$cast_devices $ip"
done<<EOT
-$($AVAHI -tpvlr _googlecast._tcp | grep "^=" | cut -d";" -f8 | sort)
+$(timeout 10 $AVAHI -tpvlr _googlecast._tcp | grep "^=" | cut -d";" -f8 | sort)
EOT
echo "Cast responses from:$cast_devices"
diff --git a/cmds/dialcheck.cc b/cmds/dialcheck.cc
index 17f8fbd..d5ea202 100644
--- a/cmds/dialcheck.cc
+++ b/cmds/dialcheck.cc
@@ -290,7 +290,7 @@
int s4, s6;
setlinebuf(stdout);
- alarm(30);
+ alarm(10);
while ((c = getopt(argc, argv, "t:")) != -1) {
switch(c) {
diff --git a/cmds/gflldpd.c b/cmds/gflldpd.c
new file mode 100644
index 0000000..44c2dd2
--- /dev/null
+++ b/cmds/gflldpd.c
@@ -0,0 +1,246 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013 Keichi Takahashi keichi.t@me.com
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+/* Substantially derived from * https://github.com/keichi/tiny-lldpd
+ * also under the MIT license */
+
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syslog.h>
+#include <unistd.h>
+
+#include <net/ethernet.h>
+#include <net/if.h>
+#include <netinet/if_ether.h>
+#include <netinet/in.h>
+#include <netpacket/packet.h>
+#include <sys/socket.h>
+
+#define MAXINTERFACES 8
+const char *ifnames[MAXINTERFACES] = {0};
+int ninterfaces = 0;
+
+uint8_t sendbuf[1024];
+
+const uint8_t lldpaddr[ETH_ALEN] = {0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e};
+#define ETH_P_LLDP 0x88cc
+#define TLV_END 0
+#define TLV_CHASSIS_ID 1
+#define TLV_PORT_ID 2
+#define TLV_TTL 3
+#define TLV_PORT_DESCRIPTION 4
+#define TLV_SYSTEM_NAME 5
+
+#define CHASSIS_ID_MAC_ADDRESS 4
+#define PORT_ID_MAC_ADDRESS 3
+
+static int write_lldp_tlv_header(void *p, int type, int length)
+{
+ *((uint16_t *)p) = htons((type & 0x7f) << 9 | (length & 0x1ff));
+ return 2;
+}
+
+
+static int write_lldp_type_subtype_tlv(size_t offset,
+ uint8_t type, uint8_t subtype, int length, const void *data)
+{
+ uint8_t *p = sendbuf + offset;
+
+ if ((offset + 2 + 1 + length) > sizeof(sendbuf)) {
+ fprintf(stderr, "LLDP frame too large %zd > %zd\n",
+ (offset + 2 + 1 + length), sizeof(sendbuf));
+ exit(1);
+ }
+
+ p += write_lldp_tlv_header(p, type, length + 1);
+ *p++ = subtype;
+ memcpy(p, data, length);
+ p += length;
+
+ return (p - sendbuf);
+}
+
+
+static int write_lldp_type_tlv(size_t offset, uint8_t type,
+ int length, const void *data)
+{
+ uint8_t *p = sendbuf + offset;
+
+ if ((offset + 2 + length) > sizeof(sendbuf)) {
+ fprintf(stderr, "LLDP frame too large %zd > %zd\n",
+ (offset + 2 + length), sizeof(sendbuf));
+ exit(1);
+ }
+
+ p += write_lldp_tlv_header(p, type, length);
+ memcpy(p, data, length);
+ p += length;
+
+ return (p - sendbuf);
+}
+
+
+static int write_lldp_end_tlv(size_t offset)
+{
+ uint8_t *p = sendbuf + offset;
+
+ if ((offset + 2) > sizeof(sendbuf)) {
+ fprintf(stderr, "LLDP frame too large %zd > %zd\n",
+ (offset + 2), sizeof(sendbuf));
+ exit(1);
+ }
+
+ offset += write_lldp_tlv_header(p, TLV_END, 0);
+ return offset;
+}
+
+
+static void mac_str_to_bytes(const char *macstr, uint8_t *mac)
+{
+ if (sscanf(macstr, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx",
+ &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5]) != 6) {
+ fprintf(stderr, "Invalid MAC address: %s\n", macstr);
+ exit(1);
+ }
+}
+
+
+static size_t format_lldp_packet(const char *macaddr, const char *ifname,
+ const char *serial)
+{
+ uint8_t saddr[ETH_ALEN];
+ size_t offset = 0;
+ struct ether_header *eh = (struct ether_header *)sendbuf;
+ uint16_t ttl;
+
+ mac_str_to_bytes(macaddr, saddr);
+ memset(sendbuf, 0, sizeof(sendbuf));
+
+ eh = (struct ether_header *)sendbuf;
+ memcpy(eh->ether_shost, saddr, sizeof(eh->ether_shost));
+ memcpy(eh->ether_dhost, lldpaddr, sizeof(eh->ether_dhost));
+ eh->ether_type = htons(ETH_P_LLDP);
+ offset = sizeof(*eh);
+
+ offset = write_lldp_type_subtype_tlv(offset,
+ TLV_CHASSIS_ID, CHASSIS_ID_MAC_ADDRESS, ETH_ALEN, saddr);
+ offset = write_lldp_type_subtype_tlv(offset,
+ TLV_PORT_ID, PORT_ID_MAC_ADDRESS, ETH_ALEN, saddr);
+
+ ttl = htons(120);
+ offset = write_lldp_type_tlv(offset, TLV_TTL, sizeof(ttl), &ttl);
+
+ offset = write_lldp_type_tlv(offset,
+ TLV_PORT_DESCRIPTION, strlen(ifname), ifname);
+ offset = write_lldp_type_tlv(offset,
+ TLV_SYSTEM_NAME, strlen(serial), serial);
+ offset = write_lldp_end_tlv(offset);
+
+ return offset;
+}
+
+
+#ifndef UNIT_TESTS
+static void send_lldp_packet(int s, size_t len, const char *ifname)
+{
+ struct sockaddr_ll sll;
+
+ memset(&sll, 0, sizeof(sll));
+ sll.sll_family = PF_PACKET;
+ sll.sll_ifindex = if_nametoindex(ifname);
+ sll.sll_hatype = ARPHRD_ETHER;
+ sll.sll_halen = ETH_ALEN;
+ sll.sll_pkttype = PACKET_OTHERHOST;
+ memcpy(sll.sll_addr, lldpaddr, ETH_ALEN);
+ if (sendto(s, sendbuf, len, 0, (struct sockaddr*)&sll, sizeof(sll)) < 0) {
+ fprintf(stderr, "LLDP sendto failed\n");
+ exit(1);
+ }
+}
+
+
+static void usage(const char *progname)
+{
+ fprintf(stderr, "usage: %s -i eth# -m 00:11:22:33:44:55 -s G0123456789\n",
+ progname);
+ exit(1);
+}
+
+
+int main(int argc, char *argv[])
+{
+ const char *macaddr = NULL;
+ const char *serial = NULL;
+ int c;
+ int s;
+
+ while ((c = getopt(argc, argv, "i:m:s:")) != -1) {
+ switch (c) {
+ case 'i':
+ if (ninterfaces == (MAXINTERFACES - 1)) {
+ usage(argv[0]);
+ }
+ ifnames[ninterfaces++] = optarg;
+ break;
+ case 'm':
+ macaddr = optarg;
+ break;
+ case 's':
+ serial = optarg;
+ break;
+ default:
+ usage(argv[0]);
+ break;
+ }
+ }
+
+ if (ninterfaces == 0 || macaddr == NULL || serial == NULL) {
+ usage(argv[0]);
+ }
+
+ if ((s = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) < 0) {
+ fprintf(stderr, "socket(PF_PACKET) failed\n");
+ exit(1);
+ }
+
+ while (1) {
+ int i;
+
+ for (i = 0; i < ninterfaces; ++i) {
+ if (ifnames[i] != NULL) {
+ size_t len = format_lldp_packet(macaddr, ifnames[i], serial);
+ send_lldp_packet(s, len, ifnames[i]);
+ }
+ usleep(10000 + (rand() % 80000));
+ }
+
+ usleep(500000 + (rand() % 1000000));
+ }
+
+ return 0;
+}
+#endif /* UNIT_TESTS */
diff --git a/cmds/gflldpd_test.cc b/cmds/gflldpd_test.cc
new file mode 100644
index 0000000..21f33df
--- /dev/null
+++ b/cmds/gflldpd_test.cc
@@ -0,0 +1,61 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013 Keichi Takahashi keichi.t@me.com
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include <netinet/if_ether.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <wvtest.h>
+
+
+#define UNIT_TESTS
+#include "gflldpd.c"
+
+
+WVTEST_MAIN("mac_str_to_bytes") {
+ uint8_t expected_mac[] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55 };
+ uint8_t mac[ETH_ALEN];
+
+ mac_str_to_bytes("00:11:22:33:44:55", mac);
+ WVPASSEQ(memcmp(mac, expected_mac, ETH_ALEN), 0);
+}
+
+
+WVTEST_MAIN("format_lldp_packet") {
+ size_t siz;
+ uint8_t expected[] = {
+ 0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e, 0x00, 0x11,
+ 0x22, 0x33, 0x44, 0x55, 0x88, 0xcc, 0x02, 0x07,
+ 0x04, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x04,
+ 0x07, 0x03, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55,
+ 0x06, 0x02, 0x00, 0x78, 0x08, 0x04, 0x65, 0x74,
+ 0x68, 0x30, 0x0a, 0x0b, 0x47, 0x30, 0x31, 0x32,
+ 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x00,
+ 0x00
+ };
+
+ siz = format_lldp_packet("00:11:22:33:44:55", "eth0", "G0123456789");
+ WVPASSEQ(siz, sizeof(expected));
+ WVPASSEQ(memcmp(sendbuf, expected, siz), 0);
+}
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/cmds/soft_rc.py b/cmds/soft_rc.py
index 78bf5bc..685a069 100755
--- a/cmds/soft_rc.py
+++ b/cmds/soft_rc.py
@@ -142,7 +142,8 @@
LOG_VERB = 3
LOG_ALL = 99
-SLEEP_BEFORE_RELEASE_TIME = 0.1 # secs
+SLEEP_BEFORE_RELEASE_TIME = 0.1 # secs
+SLEEP_BETWEEN_DIGITS_TIME = 0.25 # secs
optspec = """
soft_rc.py [options]
@@ -523,6 +524,7 @@
for d in token:
tok = "DIGIT_" + d
self.SendKeyCode(tok, keymap.get(tok))
+ time.sleep(SLEEP_BETWEEN_DIGITS_TIME)
self.SendKeyCode("OK", keymap.get("OK"))
# regular key
diff --git a/cmds/statpitcher.cc b/cmds/statpitcher.cc
index 0a19443..27dd171 100644
--- a/cmds/statpitcher.cc
+++ b/cmds/statpitcher.cc
@@ -173,7 +173,7 @@
if (pipe) {
char buffer[128];
if (fgets(buffer, 128, pipe.get()) != NULL) {
- std::istringstream(buffer) >> ret;
+ std::istringstream(buffer) >> std::hex >> ret;
}
}
return ret;
diff --git a/conman/Makefile b/conman/Makefile
index 0faf301..6126f84 100644
--- a/conman/Makefile
+++ b/conman/Makefile
@@ -15,7 +15,7 @@
%.test: %_test.py
echo ./$<
- PYTHONPATH=..:./test/fake_wpactrl:$(PYTHONPATH) ./$<
+ PYTHONPATH=..:./test/fake_python:$(PYTHONPATH) ./$<
runtests: \
$(patsubst %_test.py,%.test,$(wildcard *_test.py))
diff --git a/conman/connection_manager.py b/conman/connection_manager.py
index f43ee8d..e484e67 100755
--- a/conman/connection_manager.py
+++ b/conman/connection_manager.py
@@ -74,7 +74,7 @@
WIFI_SETCLIENT = ['wifi', 'setclient', '--persist']
WIFI_STOPCLIENT = ['wifi', 'stopclient', '--persist']
- def __init__(self, band, wifi, command_lines, wpa_control_interface):
+ def __init__(self, band, wifi, command_lines):
self.band = band
self.wifi = wifi
self.command = command_lines.splitlines()
@@ -83,7 +83,6 @@
self.passphrase = None
self.interface_suffix = None
self.access_point = None
- self._wpa_control_interface = wpa_control_interface
binwifi_option_attrs = {
'-s': 'ssid',
@@ -106,16 +105,12 @@
if self.ssid is None:
raise ValueError('Command file does not specify SSID')
- if self.wifi.initial_ssid == self.ssid:
+ if self.client_up:
logging.info('Connected to WLAN at startup')
@property
def client_up(self):
- wpa_status = self.wifi.wpa_status()
- return (wpa_status.get('wpa_state') == 'COMPLETED'
- # NONE indicates we're on a provisioning network; anything else
- # suggests we're already on the WLAN.
- and wpa_status.get('key_mgmt') != 'NONE')
+ return self.ssid and self.ssid == self.wifi.current_secure_ssid()
def start_access_point(self):
"""Start an access point."""
@@ -163,7 +158,8 @@
return
if self._actually_start_client():
- self._post_start_client()
+ self.wifi.status.connected_to_wlan = True
+ logging.info('Started wifi client on %s GHz', self.band)
def _actually_start_client(self):
"""Actually run wifi setclient.
@@ -187,26 +183,18 @@
return True
- def _post_start_client(self):
- self.wifi.handle_wpa_events()
- self.wifi.status.connected_to_wlan = True
- logging.info('Started wifi client on %s GHz', self.band)
- self.wifi.attach_wpa_control(self._wpa_control_interface)
-
def stop_client(self):
if not self.client_up:
logging.debug('Wifi client already stopped on %s GHz', self.band)
return
- self.wifi.detach_wpa_control()
-
try:
subprocess.check_output(self.WIFI_STOPCLIENT + ['-b', self.band],
stderr=subprocess.STDOUT)
# TODO(rofrankel): Make this work for dual-radio devices.
self.wifi.status.connected_to_wlan = False
logging.info('Stopped wifi client on %s GHz', self.band)
- self.wifi.handle_wpa_events()
+ self.wifi.update()
except subprocess.CalledProcessError as e:
logging.error('Failed to stop wifi client: %s', e.output)
@@ -242,10 +230,10 @@
tmp_dir='/tmp/conman',
config_dir='/config/conman',
moca_tmp_dir='/tmp/cwmp/monitoring/moca2',
- wpa_control_interface='/var/run/wpa_supplicant',
run_duration_s=1, interface_update_period=5,
wifi_scan_period_s=120, wlan_retry_s=120, associate_wait_s=15,
- dhcp_wait_s=10, acs_start_wait_s=20, acs_finish_wait_s=120,
+ dhcp_wait_s=10, acs_connection_check_wait_s=1,
+ acs_start_wait_s=20, acs_finish_wait_s=120,
bssid_cycle_length_s=30):
self._tmp_dir = tmp_dir
@@ -253,13 +241,13 @@
self._interface_status_dir = os.path.join(tmp_dir, 'interfaces')
self._status_dir = os.path.join(tmp_dir, 'status')
self._moca_tmp_dir = moca_tmp_dir
- self._wpa_control_interface = wpa_control_interface
self._run_duration_s = run_duration_s
self._interface_update_period = interface_update_period
self._wifi_scan_period_s = wifi_scan_period_s
self._wlan_retry_s = wlan_retry_s
self._associate_wait_s = associate_wait_s
self._dhcp_wait_s = dhcp_wait_s
+ self._acs_connection_check_wait_s = acs_connection_check_wait_s
self._acs_start_wait_s = acs_start_wait_s
self._acs_finish_wait_s = acs_finish_wait_s
self._bssid_cycle_length_s = bssid_cycle_length_s
@@ -308,17 +296,13 @@
self.ifplugd_action('eth0', ethernet_up)
self.bridge.ethernet = ethernet_up
- # Do the same for wifi interfaces , but rather than explicitly setting that
- # the wpa_supplicant link is up, attempt to attach to the wpa_supplicant
- # control interface.
+ # Do the same for wifi interfaces.
for wifi in self.wifi:
+ wifi_up = self.is_interface_up(wifi.name)
+ wifi.wpa_supplicant = wifi_up
if not os.path.exists(
os.path.join(self._interface_status_dir, wifi.name)):
- wifi_up = self.is_interface_up(wifi.name)
self.ifplugd_action(wifi.name, wifi_up)
- if wifi_up:
- wifi.status.attached_to_wpa_supplicant = wifi.attach_wpa_control(
- self._wpa_control_interface)
for path, prefix in ((self._tmp_dir, self.GATEWAY_FILE_PREFIX),
(self._tmp_dir, self.SUBNET_FILE_PREFIX),
@@ -366,7 +350,10 @@
ratchet.Condition('trying_open', wifi.connected_to_open,
self._associate_wait_s,
callback=wifi.expire_connection_status_cache),
- ratchet.Condition('waiting_for_dhcp', wifi.gateway, self._dhcp_wait_s,
+ ratchet.Condition('waiting_for_dhcp', wifi.gateway,
+ self._dhcp_wait_s),
+ ratchet.Condition('acs_connection_check', wifi.acs,
+ self._acs_connection_check_wait_s,
callback=self.cwmp_wakeup),
ratchet.FileTouchedCondition('waiting_for_cwmp_wakeup',
os.path.join(CWMP_PATH, 'acscontact'),
@@ -433,8 +420,6 @@
while True:
self.run_once()
finally:
- for wifi in self.wifi:
- wifi.detach_wpa_control()
self.notifier.stop()
def run_once(self):
@@ -445,16 +430,18 @@
1. Process any changes in watched files.
2. Check interfaces for changed connectivity, if
update_interfaces_and_routes is true.
- 3. Start, stop, or restart access points as appropriate. If running an
+ 3. Try to upload logs, if we just joined a new open network.
+ 4. Start, stop, or restart access points as appropriate. If running an
access point, skip all remaining wifi steps for that band.
- 3. Handle any wpa_supplicant events.
- 4. Periodically, perform a wifi scan.
- 5. If not connected to the WLAN or to the ACS, try to connect to something.
- 6. If connected to the ACS but not the WLAN, and enough time has passed
+ 5. Handle any wpa_supplicant events.
+ 6. Periodically, perform a wifi scan.
+ 7. If not connected to the WLAN or to the ACS, try to connect to something.
+ 8. If connected to the ACS but not the WLAN, and enough time has passed
since connecting that we should expect a current WLAN configuration, try
to join the WLAN again.
- 7. Sleep for the rest of the duration of _run_duration_s.
+ 9. Sleep for the rest of the duration of _run_duration_s.
"""
+
start_time = _gettime()
self.notifier.process_events()
while self.notifier.check_events():
@@ -466,13 +453,22 @@
self._interface_update_counter = 0
self._update_interfaces_and_routes()
+ if self.acs() and self._try_to_upload_logs:
+ self._try_upload_logs()
+ self._try_to_upload_logs = False
+
for wifi in self.wifi:
- continue_wifi = False
- provisioning_failed = self.provisioning_failed(wifi)
if self.currently_provisioning(wifi):
+ logging.debug('Currently provisioning, nothing else to do.')
continue
provisioning_failed = self.provisioning_failed(wifi)
+ if provisioning_failed and (
+ getattr(wifi, 'last_attempted_bss_info', None) ==
+ getattr(wifi, 'last_successful_bss_info', None)):
+ wifi.last_successful_bss_info = None
+
+ continue_wifi = False
# Only one wlan_configuration per interface will have access_point ==
# True. Try 5 GHz first, then 2.4 GHz. If both bands are supported by
@@ -494,12 +490,7 @@
if wlan_configuration.access_point_up:
continue_wifi = True
- if not wifi.attached():
- logging.debug('Attempting to attach to wpa control interface for %s',
- wifi.name)
- wifi.status.attached_to_wpa_supplicant = wifi.attach_wpa_control(
- self._wpa_control_interface)
- wifi.handle_wpa_events()
+ wifi.update()
if continue_wifi:
logging.debug('Running AP on %s, nothing else to do.', wifi.name)
@@ -550,9 +541,6 @@
wifi.status.connected_to_wlan = False
if self.acs():
logging.debug('Connected to ACS')
- if self._try_to_upload_logs:
- self._try_upload_logs()
- self._try_to_upload_logs = False
if wifi.acs():
wifi.last_successful_bss_info = getattr(wifi,
@@ -722,8 +710,7 @@
wifi = self.wifi_for_band(band)
if wifi:
self._update_wlan_configuration(
- self.WLANConfiguration(band, wifi, contents,
- self._wpa_control_interface))
+ self.WLANConfiguration(band, wifi, contents))
elif filename.startswith(self.ACCESS_POINT_FILE_PREFIX):
match = re.match(self.ACCESS_POINT_FILE_REGEXP, filename)
if match:
@@ -831,8 +818,7 @@
self.start_provisioning(wifi)
connected = self._try_bssid(wifi, bss_info)
if connected:
- wifi.attach_wpa_control(self._wpa_control_interface)
- wifi.handle_wpa_events()
+ wifi.update()
wifi.status.connected_to_open = True
now = _gettime()
wifi.complain_about_acs_at = now + 5
@@ -859,7 +845,7 @@
wifi.last_attempted_bss_info = bss_info
return subprocess.call(self.WIFI_SETCLIENT +
['--ssid', bss_info.ssid,
- '--band', wifi.bands[0],
+ '--band', bss_info.band,
'--bssid', bss_info.bssid]) == 0
def _connected_to_wlan(self, wifi):
@@ -877,6 +863,7 @@
band = wlan_configuration.band
current = self._wlan_configuration.get(band, None)
if current is None or wlan_configuration.command != current.command:
+ logging.debug('Received new WLAN configuration for band %s', band)
if current is not None:
wlan_configuration.access_point = current.access_point
else:
@@ -960,6 +947,7 @@
wifi.provisioning_ratchet.check()
if wifi.provisioning_ratchet.done_after:
wifi.status.provisioning_completed = True
+ wifi.provisioning_ratchet.stop()
logging.info('%s successfully provisioned', wifi.name)
return False
except ratchet.TimeoutException:
diff --git a/conman/connection_manager_test.py b/conman/connection_manager_test.py
index d7d0a40..1e447d0 100755
--- a/conman/connection_manager_test.py
+++ b/conman/connection_manager_test.py
@@ -5,8 +5,10 @@
import logging
import os
import shutil
+import subprocess # Fake subprocess module in test/fake_python.
import tempfile
import time
+import traceback
import connection_manager
import experiment_testutils
@@ -29,92 +31,26 @@
}
"""
-WIFI_SHOW_OUTPUT_MARVELL8897 = """Band: 2.4
-RegDomain: US
-Interface: wlan0 # 2.4 GHz ap
-Channel: 149
-BSSID: f4:f5:e8:81:1b:a0
-AutoChannel: True
-AutoType: NONDFS
-Station List for band: 2.4
-Client Interface: wcli0 # 2.4 GHz client
-Client BSSID: f4:f5:e8:81:1b:a1
-
-Band: 5
-RegDomain: US
-Interface: wlan0 # 5 GHz ap
-Channel: 149
-BSSID: f4:f5:e8:81:1b:a0
-AutoChannel: True
-AutoType: NONDFS
-Station List for band: 5
-
-Client Interface: wcli0 # 5 GHz client
-Client BSSID: f4:f5:e8:81:1b:a1
-"""
-
-WIFI_SHOW_OUTPUT_ATH9K_ATH10K = """Band: 2.4
-RegDomain: US
-Interface: wlan0 # 2.4 GHz ap
-Channel: 149
-BSSID: f4:f5:e8:81:1b:a0
-AutoChannel: True
-AutoType: NONDFS
-Station List for band: 2.4
-
-Client Interface: wcli0 # 2.4 GHz client
-Client BSSID: f4:f5:e8:81:1b:a1
-
-Band: 5
-RegDomain: US
-Interface: wlan1 # 5 GHz ap
-Channel: 149
-BSSID: f4:f5:e8:81:1b:a0
-AutoChannel: True
-AutoType: NONDFS
-Station List for band: 5
-
-Client Interface: wcli1 # 5 GHz client
-Client BSSID: f4:f5:e8:81:1b:a1
-"""
-
+WIFI_SHOW_OUTPUT_MARVELL8897 = (
+ subprocess.wifi.MockInterface(phynum='0', bands=['2.4', '5'],
+ driver='cfg80211'),
+)
+WIFI_SHOW_OUTPUT_ATH9K_ATH10K = (
+ subprocess.wifi.MockInterface(phynum='0', bands=['2.4'], driver='cfg80211'),
+ subprocess.wifi.MockInterface(phynum='1', bands=['5'], driver='cfg80211'),
+)
# See b/27328894.
-WIFI_SHOW_OUTPUT_MARVELL8897_NO_5GHZ = """Band: 2.4
-RegDomain: 00
-Interface: wlan0 # 2.4 GHz ap
-BSSID: 00:50:43:02:fe:01
-AutoChannel: False
-Station List for band: 2.4
-
-Client Interface: wcli0 # 2.4 GHz client
-Client BSSID: 00:50:43:02:fe:02
-
-Band: 5
-RegDomain: 00
-"""
-
-WIFI_SHOW_OUTPUT_ATH9K_FRENZY = """Band: 2.4
-RegDomain: US
-Interface: wlan0 # 2.4 GHz ap
-Channel: 149
-BSSID: f4:f5:e8:81:1b:a0
-AutoChannel: True
-AutoType: NONDFS
-Station List for band: 2.4
-
-Client Interface: wcli0 # 2.4 GHz client
-Client BSSID: f4:f5:e8:81:1b:a1
-
-Band: 5
-RegDomain: 00
-"""
-
-WIFI_SHOW_OUTPUT_FRENZY = """Band: 2.4
-RegDomain: 00
-Band: 5
-RegDomain: 00
-"""
+WIFI_SHOW_OUTPUT_MARVELL8897_NO_5GHZ = (
+ subprocess.wifi.MockInterface(phynum='0', bands=['2.4'], driver='cfg80211'),
+)
+WIFI_SHOW_OUTPUT_ATH9K_FRENZY = (
+ subprocess.wifi.MockInterface(phynum='0', bands=['2.4'], driver='cfg80211'),
+ subprocess.wifi.MockInterface(phynum='1', bands=['5'], driver='frenzy'),
+)
+WIFI_SHOW_OUTPUT_FRENZY = (
+ subprocess.wifi.MockInterface(phynum='0', bands=['5'], driver='frenzy'),
+)
IW_SCAN_DEFAULT_OUTPUT = """BSS 00:11:22:33:44:55(on wcli0)
SSID: s1
@@ -133,22 +69,13 @@
@wvtest.wvtest
def get_client_interfaces_test():
"""Test get_client_interfaces."""
- wifi_show = None
- quantenna_interfaces = None
+ subprocess.reset()
- # pylint: disable=protected-access
- old_wifi_show = connection_manager._wifi_show
- old_get_quantenna_interfaces = connection_manager._get_quantenna_interfaces
- connection_manager._wifi_show = lambda: wifi_show
- connection_manager._get_quantenna_interfaces = lambda: quantenna_interfaces
-
- wifi_show = WIFI_SHOW_OUTPUT_MARVELL8897
- quantenna_interfaces = []
+ subprocess.mock('wifi', 'interfaces', *WIFI_SHOW_OUTPUT_MARVELL8897)
wvtest.WVPASSEQ(connection_manager.get_client_interfaces(),
{'wcli0': {'bands': set(['2.4', '5'])}})
- wifi_show = WIFI_SHOW_OUTPUT_ATH9K_ATH10K
- quantenna_interfaces = []
+ subprocess.mock('wifi', 'interfaces', *WIFI_SHOW_OUTPUT_ATH9K_ATH10K)
wvtest.WVPASSEQ(connection_manager.get_client_interfaces(), {
'wcli0': {'bands': set(['2.4'])},
'wcli1': {'bands': set(['5'])}
@@ -156,113 +83,30 @@
# Test Quantenna devices.
- # 2.4 GHz cfg80211 radio + 5 GHz Frenzy (Optimus Prime).
- wifi_show = WIFI_SHOW_OUTPUT_ATH9K_FRENZY
- quantenna_interfaces = ['wlan1', 'wlan1_portal', 'wcli1']
+ # 2.4 GHz cfg80211 radio + 5 GHz Frenzy (e.g. Optimus Prime).
+ subprocess.mock('wifi', 'interfaces', *WIFI_SHOW_OUTPUT_ATH9K_FRENZY)
wvtest.WVPASSEQ(connection_manager.get_client_interfaces(), {
'wcli0': {'bands': set(['2.4'])},
'wcli1': {'frenzy': True, 'bands': set(['5'])}
})
# Only Frenzy (e.g. Lockdown).
- wifi_show = WIFI_SHOW_OUTPUT_FRENZY
- quantenna_interfaces = ['wlan0', 'wlan0_portal', 'wcli0']
+ subprocess.mock('wifi', 'interfaces', *WIFI_SHOW_OUTPUT_FRENZY)
wvtest.WVPASSEQ(connection_manager.get_client_interfaces(),
{'wcli0': {'frenzy': True, 'bands': set(['5'])}})
- connection_manager._wifi_show = old_wifi_show
- connection_manager._get_quantenna_interfaces = old_get_quantenna_interfaces
-
-
-class WLANConfiguration(connection_manager.WLANConfiguration):
- """WLANConfiguration subclass for testing."""
-
- WIFI_STOPAP = ['echo', 'stopap']
- WIFI_SETCLIENT = ['echo', 'setclient']
- WIFI_STOPCLIENT = ['echo', 'stopclient']
-
- def __init__(self, *args, **kwargs):
- super(WLANConfiguration, self).__init__(*args, **kwargs)
- self.stale = False
-
- def _actually_start_client(self):
- self.client_was_up = self.client_up
- self.was_attached = self.wifi.attached()
- self.wifi._secure_testonly = True
-
- super(WLANConfiguration, self)._actually_start_client()
-
- if not self.client_was_up and not self.was_attached:
- self.wifi._initial_ssid_testonly = self.ssid
- self.wifi.start_wpa_supplicant_testonly(self._wpa_control_interface)
-
- if self.wifi._wpa_control:
- self.wifi._wpa_control.connected = not self.stale
- return not self.stale
-
- def _post_start_client(self):
- if not self.client_was_up:
- self.wifi.set_connection_check_result('succeed')
-
- if self.was_attached:
- self.wifi._wpa_control.ssid_testonly = self.ssid
- self.wifi._wpa_control.secure_testonly = True
- self.wifi.add_connected_event()
-
- # Normally, wpa_supplicant would bring up the client interface, which
- # would trigger ifplugd, which would run ifplugd.action, which would do
- # two things:
- #
- # 1) Write an interface status file.
- # 2) Call run-dhclient, which would call dhclient-script, which would
- # call ipapply, which would write gateway and subnet files.
- #
- # Fake both of these things instead.
- self.write_interface_status_file('1')
- self.write_gateway_file()
- self.write_subnet_file()
-
- def stop_client(self):
- client_was_up = self.client_up
-
- super(WLANConfiguration, self).stop_client()
-
- if client_was_up:
- self.wifi.add_terminating_event()
- self.wifi.set_connection_check_result('fail')
-
- # See comments in start_client.
- self.write_interface_status_file('0')
-
- def write_gateway_file(self):
- gateway_file = os.path.join(self.tmp_dir,
- self.gateway_file_prefix + self.wifi.name)
- with open(gateway_file, 'w') as f:
- # This value doesn't matter to conman, so it's fine to hard code it here.
- f.write('192.168.1.1')
-
- def write_subnet_file(self):
- subnet_file = os.path.join(self.tmp_dir,
- self.subnet_file_prefix + self.wifi.name)
- with open(subnet_file, 'w') as f:
- # This value doesn't matter to conman, so it's fine to hard code it here.
- f.write('192.168.1.0/24')
-
- def write_interface_status_file(self, value):
- status_file = os.path.join(self.interface_status_dir, self.wifi.name)
- with open(status_file, 'w') as f:
- # This value doesn't matter to conman, so it's fine to hard code it here.
- f.write(value)
-
@wvtest.wvtest
def WLANConfigurationParseTest(): # pylint: disable=invalid-name
"""Test WLANConfiguration parsing."""
+ subprocess.reset()
+
cmd = '\n'.join([
'WIFI_PSK=abcdWIFI_PSK=qwer', 'wifi', 'set', '-P', '-b', '5',
'--bridge=br0', '-s', 'my ssid=1', '--interface-suffix', '_suffix',
])
- config = WLANConfiguration('5', interface_test.Wifi('wcli0', 20), cmd, None)
+ config = connection_manager.WLANConfiguration(
+ '5', interface_test.Wifi('wcli0', 20), cmd)
wvtest.WVPASSEQ('my ssid=1', config.ssid)
wvtest.WVPASSEQ('abcdWIFI_PSK=qwer', config.passphrase)
@@ -290,150 +134,37 @@
Bridge = interface_test.Bridge
Wifi = Wifi
FrenzyWifi = FrenzyWifi
- WLANConfiguration = WLANConfiguration
-
- WIFI_SETCLIENT = ['echo', 'setclient']
- IFUP = ['echo', 'ifup']
- IFPLUGD_ACTION = ['echo', 'ifplugd.action']
- BINWIFI = ['echo', 'wifi']
- UPLOAD_LOGS_AND_WAIT = ['echo', 'upload-logs-and-wait']
- CWMP_WAKEUP = ['echo', 'cwmp', 'wakeup']
def __init__(self, *args, **kwargs):
self._binwifi_commands = []
- self.interfaces_already_up = kwargs.pop('__test_interfaces_already_up',
- ['eth0'])
-
- self.wifi_interfaces_already_up = [ifc for ifc in self.interfaces_already_up
- if ifc.startswith('w')]
- for wifi in self.wifi_interfaces_already_up:
- # wcli1 is always 5 GHz. wcli0 always *includes* 2.4. wlan* client
- # interfaces are Frenzy interfaces and therefore 5 GHz-only.
- band = '5' if wifi in ('wlan0', 'wlan1', 'wcli1') else '2.4'
- # This will happen in the super function, but in order for
- # write_wlan_config to work we have to do it now. This has to happen
- # before the super function so that the files exist before the inotify
- # registration.
- self._config_dir = kwargs['config_dir']
- self.write_wlan_config(band, 'my ssid', 'passphrase')
-
- # Also create the wpa_supplicant socket to which to attach.
- open(os.path.join(kwargs['wpa_control_interface'], wifi), 'w')
+ for interface_name in kwargs.pop('__test_interfaces_already_up', ['eth0']):
+ subprocess.call(['ifup', interface_name])
+ if interface_name.startswith('w'):
+ phynum = interface_name[-1]
+ for band, interface in subprocess.wifi.INTERFACE_FOR_BAND.iteritems():
+ if interface.phynum == phynum:
+ break
+ else:
+ raise ValueError('Could not find matching interface for '
+ '__test_interfaces_already_up')
+ ssid = 'my ssid'
+ psk = 'passphrase'
+ # If band is undefined then a ValueError will be raised above. pylint
+ # isn't smart enough to figure that out.
+ # pylint: disable=undefined-loop-variable
+ subprocess.mock('cwmp', band, ssid=ssid, psk=psk, write_now=True)
+ subprocess.mock('wifi', 'remote_ap', band=band, ssid=ssid, psk=psk,
+ bssid='00:00:00:00:00:00')
super(ConnectionManager, self).__init__(*args, **kwargs)
- self.interface_with_scan_results = None
- self.scan_results_include_hidden = False
- # Should we be able to connect to open network s2?
- self.can_connect_to_s2 = True
- self.can_connect_to_s3 = True
- # Will s2 fail rather than providing ACS access?
- self.s2_fail = False
- # Will s3 fail to acquire a DHCP lease?
- self.dhcp_failure_on_s3 = False
- self.log_upload_count = 0
- self.acs_session_fails = False
- for wifi in self.wifi:
- wifi.bssids_tried_testonly = 0
-
- def create_wifi_interfaces(self):
- super(ConnectionManager, self).create_wifi_interfaces()
- for wifi in self.wifi_interfaces_already_up:
- # pylint: disable=protected-access
- self.interface_by_name(wifi)._initial_ssid_testonly = 'my ssid'
- self.interface_by_name(wifi)._secure_testonly = True
-
- @property
- def IP_LINK(self):
- return ['echo'] + ['%s LOWER_UP' % ifc
- for ifc in self.interfaces_already_up]
-
- def _update_access_point(self, wlan_configuration):
- client_was_up = wlan_configuration.client_up
- super(ConnectionManager, self)._update_access_point(wlan_configuration)
- if wlan_configuration.access_point_up:
- if client_was_up:
- wifi = self.wifi_for_band(wlan_configuration.band)
- wifi.add_terminating_event()
-
- def _try_bssid(self, wifi, bss_info):
- if wifi.wpa_status().get('wpa_state', None) == 'COMPLETED':
- wifi.add_disconnected_event()
- self.last_provisioning_attempt = bss_info
-
- super(ConnectionManager, self)._try_bssid(wifi, bss_info)
-
- wifi.bssids_tried_testonly += 1
-
- def connect(connection_check_result, dhcp_failure=False):
- # pylint: disable=protected-access
- if wifi.attached():
- wifi._wpa_control.ssid_testonly = bss_info.ssid
- wifi._wpa_control.secure_testonly = False
- wifi.add_connected_event()
- else:
- wifi._initial_ssid_testonly = bss_info.ssid
- wifi._secure_testonly = False
- wifi.start_wpa_supplicant_testonly(self._wpa_control_interface)
- wifi.set_connection_check_result(connection_check_result)
- self.ifplugd_action(wifi.name, True, dhcp_failure)
-
- if bss_info and bss_info.ssid == 's1':
- connect('fail')
- return True
-
- if bss_info and bss_info.ssid == 's2' and self.can_connect_to_s2:
- connect('fail' if self.s2_fail else 'succeed')
- return True
-
- if bss_info and bss_info.ssid == 's3' and self.can_connect_to_s3:
- connect('restricted', self.dhcp_failure_on_s3)
- return True
-
- return False
-
- # pylint: disable=unused-argument,protected-access
- def _find_bssids(self, band):
- scan_output = ''
- if (self.interface_with_scan_results and
- band in self.interface_by_name(self.interface_with_scan_results).bands):
- scan_output = IW_SCAN_DEFAULT_OUTPUT
- if self.scan_results_include_hidden:
- scan_output += IW_SCAN_HIDDEN_OUTPUT
- iw._scan = lambda interface: scan_output
- return super(ConnectionManager, self)._find_bssids(band)
-
- def _update_wlan_configuration(self, wlan_configuration):
- wlan_configuration.command.insert(0, 'echo')
- wlan_configuration._wpa_control_interface = self._wpa_control_interface
- wlan_configuration.tmp_dir = self._tmp_dir
- wlan_configuration.interface_status_dir = self._interface_status_dir
- wlan_configuration.gateway_file_prefix = self.GATEWAY_FILE_PREFIX
- wlan_configuration.subnet_file_prefix = self.SUBNET_FILE_PREFIX
-
- super(ConnectionManager, self)._update_wlan_configuration(
- wlan_configuration)
-
# Just looking for last_wifi_scan_time to change doesn't work because the
# tests run too fast.
def _wifi_scan(self, wifi):
super(ConnectionManager, self)._wifi_scan(wifi)
wifi.wifi_scan_counter += 1
- def ifplugd_action(self, interface_name, up, dhcp_failure=False):
- # Typically, when moca comes up, conman calls ifplugd.action, which writes
- # this file. Also, when conman starts, it calls ifplugd.action for eth0.
- self.write_interface_status_file(interface_name, '1' if up else '0')
-
- # ifplugd calls run-dhclient, which results in a gateway file if the link is
- # up (and working).
- if up and not dhcp_failure:
- self.write_gateway_file('br0' if interface_name in ('eth0', 'moca0')
- else interface_name)
- self.write_subnet_file('br0' if interface_name in ('eth0', 'moca0')
- else interface_name)
-
def _binwifi(self, *command):
super(ConnectionManager, self)._binwifi(*command)
self._binwifi_commands.append(command)
@@ -452,50 +183,7 @@
return self._wlan_configuration[band].client_up
- def _try_upload_logs(self):
- self.log_upload_count += 1
- return super(ConnectionManager, self)._try_upload_logs()
-
- # Test methods
-
- def tried_to_upload_logs(self):
- result = getattr(self, 'last_log_upload_count', 0) < self.log_upload_count
- self.last_log_upload_count = self.log_upload_count
- return result
-
- def delete_wlan_config(self, band):
- delete_wlan_config(self._config_dir, band)
-
- def write_wlan_config(self, *args, **kwargs):
- write_wlan_config(self._config_dir, *args, **kwargs)
-
- def enable_access_point(self, band):
- enable_access_point(self._config_dir, band)
-
- def disable_access_point(self, band):
- disable_access_point(self._config_dir, band)
-
- def write_gateway_file(self, interface_name):
- gateway_file = os.path.join(self._tmp_dir,
- self.GATEWAY_FILE_PREFIX + interface_name)
- with open(gateway_file, 'w') as f:
- logging.debug('Writing gateway file %s', gateway_file)
- # This value doesn't matter to conman, so it's fine to hard code it here.
- f.write('192.168.1.1')
-
- def write_subnet_file(self, interface_name):
- subnet_file = os.path.join(self._tmp_dir,
- self.SUBNET_FILE_PREFIX + interface_name)
- with open(subnet_file, 'w') as f:
- logging.debug('Writing subnet file %s', subnet_file)
- # This value doesn't matter to conman, so it's fine to hard code it here.
- f.write('192.168.1.0/24')
-
- def write_interface_status_file(self, interface_name, value):
- status_file = os.path.join(self._interface_status_dir, interface_name)
- with open(status_file, 'w') as f:
- # This value doesn't matter to conman, so it's fine to hard code it here.
- f.write(value)
+ # # Test methods
def set_ethernet(self, up):
self.ifplugd_action('eth0', up)
@@ -514,6 +202,7 @@
self.run_once()
def run_until_scan(self, band):
+ logging.debug('running until scan on band %r', band)
wifi = self.wifi_for_band(band)
wifi_scan_counter = wifi.wifi_scan_counter
while wifi_scan_counter == wifi.wifi_scan_counter:
@@ -529,58 +218,12 @@
def has_status_files(self, files):
return not set(files) - set(os.listdir(self._status_dir))
- def cwmp_wakeup(self):
- super(ConnectionManager, self).cwmp_wakeup()
- self.write_acscontact()
- if self.acs():
- self.write_acsconnected()
-
- def write_acscontact(self):
- open(os.path.join(connection_manager.CWMP_PATH, 'acscontact'), 'w')
-
- def write_acsconnected(self):
- if not self.acs_session_fails:
- open(os.path.join(connection_manager.CWMP_PATH, 'acsconnected'), 'w')
-
-
-def wlan_config_filename(path, band):
- return os.path.join(path, 'command.%s' % band)
-
-
-def access_point_filename(path, band):
- return os.path.join(path, 'access_point.%s' % band)
-
-
-def write_wlan_config(path, band, ssid, psk, atomic=False):
- final_filename = wlan_config_filename(path, band)
- filename = final_filename + ('.tmp' if atomic else '')
- with open(filename, 'w') as f:
- f.write('\n'.join(['env', 'WIFI_PSK=%s' % psk,
- 'wifi', 'set', '-b', band, '--ssid', ssid]))
- if atomic:
- os.rename(filename, final_filename)
-
-
-def delete_wlan_config(path, band):
- os.unlink(wlan_config_filename(path, band))
-
-
-def enable_access_point(path, band):
- open(access_point_filename(path, band), 'w')
-
-
-def disable_access_point(path, band):
- ap_filename = access_point_filename(path, band)
- if os.path.isfile(ap_filename):
- os.unlink(ap_filename)
-
def check_tmp_hosts(expected_contents):
wvtest.WVPASSEQ(open(connection_manager.TMP_HOSTS).read(), expected_contents)
-def connection_manager_test(radio_config, wlan_configs=None,
- quantenna_interfaces=None, **cm_kwargs):
+def connection_manager_test(radio_config, wlan_configs=None, **cm_kwargs):
"""Returns a decorator that does ConnectionManager test boilerplate."""
if wlan_configs is None:
wlan_configs = {}
@@ -589,38 +232,38 @@
"""The actual decorator."""
def actual_test():
"""The actual test function."""
+ subprocess.reset()
+
run_duration_s = .01
interface_update_period = 5
wifi_scan_period = 15
wifi_scan_period_s = run_duration_s * wifi_scan_period
associate_wait_s = 0
dhcp_wait_s = .5
+ acs_cc_wait_s = 0
acs_start_wait_s = 0
- acs_finish_wait_s = 0
+ acs_finish_wait_s = 0.25
- # pylint: disable=protected-access
- old_wifi_show = connection_manager._wifi_show
- connection_manager._wifi_show = lambda: radio_config
-
- old_gqi = connection_manager._get_quantenna_interfaces
- connection_manager._get_quantenna_interfaces = (
- lambda: quantenna_interfaces or [])
+ subprocess.mock('wifi', 'interfaces', *radio_config)
try:
# No initial state.
connection_manager.TMP_HOSTS = tempfile.mktemp()
tmp_dir = tempfile.mkdtemp()
config_dir = tempfile.mkdtemp()
- os.mkdir(os.path.join(tmp_dir, 'interfaces'))
+ interfaces_dir = os.path.join(tmp_dir, 'interfaces')
+ if not os.path.exists(interfaces_dir):
+ os.mkdir(interfaces_dir)
moca_tmp_dir = tempfile.mkdtemp()
wpa_control_interface = tempfile.mkdtemp()
- FrenzyWifi.WPACtrl.WIFIINFO_PATH = tempfile.mkdtemp()
+ subprocess.mock('wifi', 'wpa_path', wpa_control_interface)
connection_manager.CWMP_PATH = tempfile.mkdtemp()
+ subprocess.set_conman_paths(tmp_dir, config_dir,
+ connection_manager.CWMP_PATH)
for band, access_point in wlan_configs.iteritems():
- write_wlan_config(config_dir, band, 'initial ssid', 'initial psk')
- if access_point:
- open(os.path.join(config_dir, 'access_point.%s' % band), 'w')
+ subprocess.mock('cwmp', band, ssid='initial ssid', psk='initial psk',
+ access_point=access_point, write_now=True)
# Test that missing directories are created by ConnectionManager.
shutil.rmtree(tmp_dir)
@@ -628,19 +271,23 @@
c = ConnectionManager(tmp_dir=tmp_dir,
config_dir=config_dir,
moca_tmp_dir=moca_tmp_dir,
- wpa_control_interface=wpa_control_interface,
run_duration_s=run_duration_s,
interface_update_period=interface_update_period,
wlan_retry_s=0,
wifi_scan_period_s=wifi_scan_period_s,
associate_wait_s=associate_wait_s,
dhcp_wait_s=dhcp_wait_s,
+ acs_connection_check_wait_s=acs_cc_wait_s,
acs_start_wait_s=acs_start_wait_s,
acs_finish_wait_s=acs_finish_wait_s,
bssid_cycle_length_s=1,
**cm_kwargs)
f(c)
+ except Exception:
+ logging.error('Uncaught exception!')
+ traceback.print_exc()
+ raise
finally:
if os.path.exists(connection_manager.TMP_HOSTS):
os.unlink(connection_manager.TMP_HOSTS)
@@ -648,11 +295,7 @@
shutil.rmtree(config_dir)
shutil.rmtree(moca_tmp_dir)
shutil.rmtree(wpa_control_interface)
- shutil.rmtree(FrenzyWifi.WPACtrl.WIFIINFO_PATH)
shutil.rmtree(connection_manager.CWMP_PATH)
- # pylint: disable=protected-access
- connection_manager._wifi_show = old_wifi_show
- connection_manager._get_quantenna_interfaces = old_gqi
actual_test.func_name = f.func_name
return actual_test
@@ -660,6 +303,20 @@
return inner
+def _enable_basic_scan_results(band):
+ for bssid, ssid, ccr in (('00:11:22:33:44:55', 's1', 'fail'),
+ ('66:77:88:99:aa:bb', 's1', 'fail'),
+ ('01:23:45:67:89:ab', 's2', 'succeed')):
+ subprocess.mock('wifi', 'remote_ap', bssid=bssid, ssid=ssid,
+ band=band, security=None, connection_check_result=ccr)
+
+
+def _disable_basic_scan_results(band):
+ for bssid in (('00:11:22:33:44:55'), ('66:77:88:99:aa:bb'),
+ ('01:23:45:67:89:ab')):
+ subprocess.mock('wifi', 'remote_ap_remove', bssid=bssid, band=band)
+
+
def connection_manager_test_generic(c, band):
"""Test ConnectionManager for things independent of radio configuration.
@@ -754,16 +411,28 @@
check_tmp_hosts('127.0.0.1 localhost')
# Now there are some scan results.
- c.interface_with_scan_results = c.wifi_for_band(band).name
- # Wait for a scan, plus 3 cycles, so that s2 will have been tried.
+ _enable_basic_scan_results(band)
+
+ # Create a WLAN configuration which should eventually be connected to.
+ ssid = 'wlan'
+ psk = 'password'
+ subprocess.mock('wifi', 'remote_ap',
+ bssid='11:22:33:44:55:66',
+ ssid=ssid, psk=psk, band=band, security='WPA2')
+ subprocess.mock('cwmp', band, ssid=ssid, psk=psk, access_point=False)
+
+ wvtest.WVFAIL(subprocess.upload_logs_and_wait.uploaded_logs())
+ # Wait for a scan, then until s2 is tried.
c.run_until_scan(band)
- wvtest.WVPASSEQ(c.log_upload_count, 0)
- c.wifi_for_band(band).ip_testonly = '192.168.1.100'
+ subprocess.call(['ip', 'addr', 'add', '192.168.1.100',
+ 'dev', c.wifi_for_band(band).name])
for _ in range(len(c.wifi_for_band(band).cycler)):
c.run_once()
wvtest.WVPASS(c.has_status_files([status.P.CONNECTED_TO_OPEN]))
+ last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
+ if last_bss_info.ssid == 's2':
+ break
- last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
wvtest.WVPASSEQ(last_bss_info.ssid, 's2')
wvtest.WVPASSEQ(last_bss_info.bssid, '01:23:45:67:89:ab')
@@ -773,17 +442,10 @@
wvtest.WVPASS(c.internet())
wvtest.WVFAIL(c.client_up(band))
wvtest.WVPASS(c.wifi_for_band(band).current_routes())
- wvtest.WVPASS(c.tried_to_upload_logs())
- # Disable scan results again.
- c.interface_with_scan_results = None
+ wvtest.WVPASS(subprocess.upload_logs_and_wait.uploaded_logs())
c.run_until_interface_update()
check_tmp_hosts('192.168.1.100 %s\n127.0.0.1 localhost' % hostname)
- # Now, create a WLAN configuration which should be connected to.
- ssid = 'wlan'
- psk = 'password'
- c.write_wlan_config(band, ssid, psk)
- c.disable_access_point(band)
c.run_once()
wvtest.WVPASS(c.client_up(band))
wvtest.WVPASS(c.wifi_for_band(band).current_routes())
@@ -792,7 +454,7 @@
# Kill wpa_supplicant. conman should restart it.
wvtest.WVPASS(c.client_up(band))
wvtest.WVPASS(c._connected_to_wlan(c.wifi_for_band(band)))
- c.wifi_for_band(band).kill_wpa_supplicant_testonly(c._wpa_control_interface)
+ subprocess.mock('wifi', 'kill_wpa_supplicant', band)
wvtest.WVFAIL(c.client_up(band))
wvtest.WVFAIL(c._connected_to_wlan(c.wifi_for_band(band)))
# Make sure we stay connected to s2, rather than disconnecting and
@@ -806,18 +468,22 @@
# The AP restarts with a new configuration, kicking us off. We should
# reprovision.
- c._wlan_configuration[band].stale = True
- c.wifi_for_band(band).add_disconnected_event()
+ ssid = 'wlan2'
+ psk = 'password2'
+ subprocess.mock('cwmp', band, ssid=ssid, psk=psk)
+ # Overwrites previous one due to same BSSID.
+ subprocess.mock('wifi', 'remote_ap',
+ bssid='11:22:33:44:55:66',
+ ssid=ssid, psk=psk, band=band, security='WPA2')
+ subprocess.mock('wifi', 'disconnected_event', band)
c.run_once()
wvtest.WVFAIL(c.client_up(band))
wvtest.WVPASS(c._connected_to_open(c.wifi_for_band(band)))
- wvtest.WVPASSEQ(c.last_provisioning_attempt.ssid, 's2')
+ wvtest.WVPASSEQ(c.wifi_for_band(band).last_attempted_bss_info.ssid, 's2')
- # Now that we're on the provisioning network, create the new WLAN
- # configuration, which should be connected to.
- ssid = 'wlan2'
- psk = 'password2'
- c.write_wlan_config(band, ssid, psk)
+ # Run once for cwmp wakeup to get called, then once more for the new config to
+ # be received.
+ c.run_once()
c.run_once()
wvtest.WVPASS(c.client_up(band))
wvtest.WVPASS(c.wifi_for_band(band).wpa_status()['ssid'] == ssid)
@@ -829,23 +495,26 @@
# add the user's WLAN to the scan results, and scan again. This time, the
# first SSID tried should be 's3', which is now present in the scan results
# (with its SSID hidden, but included via vendor IE).
- c.delete_wlan_config(band)
- c.can_connect_to_s2 = False
- c.interface_with_scan_results = c.wifi_for_band(band).name
- c.scan_results_include_hidden = True
- c.run_until_interface_update_and_scan(band)
- wvtest.WVFAIL(c.has_status_files([status.P.CONNECTED_TO_WLAN]))
- c.run_until_interface_update()
- wvtest.WVPASS(c.has_status_files([status.P.CONNECTED_TO_OPEN]))
- wvtest.WVPASSEQ(c.last_provisioning_attempt.ssid, 's3')
- wvtest.WVPASSEQ(c.last_provisioning_attempt.bssid, 'ff:ee:dd:cc:bb:aa')
- # The log upload happens on the next main loop after joining s3.
- c.run_once()
- wvtest.WVPASS(c.tried_to_upload_logs())
+ subprocess.mock('cwmp', band, delete_config=True, write_now=True)
+ del c.wifi_for_band(band).cycler
+ _enable_basic_scan_results(band)
+ # Remove s2.
+ subprocess.mock('wifi', 'remote_ap_remove',
+ bssid='01:23:45:67:89:ab', band=band)
+ # Create s3.
+ subprocess.mock('wifi', 'remote_ap', bssid='ff:ee:dd:cc:bb:aa', ssid='s3',
+ band=band, security=None, hidden=True,
+ vendor_ies=(('f4:f5:e8', '01'), ('f4:f5:e8', '03 73 33')))
+ #### Now, recreate the same WLAN configuration, which should be connected to.
+ subprocess.mock('cwmp', band, ssid=ssid, psk=psk)
- # Now, recreate the same WLAN configuration, which should be connected to.
- # Also, test that atomic writes/renames work.
- c.write_wlan_config(band, ssid, psk, atomic=True)
+ c.run_until_interface_update_and_scan(band)
+ c.run_until_interface_update()
+ wvtest.WVPASSEQ(c.wifi_for_band(band).last_attempted_bss_info.ssid, 's3')
+ wvtest.WVPASSEQ(c.wifi_for_band(band).last_attempted_bss_info.bssid,
+ 'ff:ee:dd:cc:bb:aa')
+ wvtest.WVPASS(subprocess.upload_logs_and_wait.uploaded_logs())
+
c.run_once()
wvtest.WVPASS(c.client_up(band))
wvtest.WVPASS(c.wifi_for_band(band).current_routes())
@@ -853,7 +522,7 @@
# Now enable the AP. Since we have no wired connection, this should have no
# effect.
- c.enable_access_point(band)
+ subprocess.mock('cwmp', band, access_point=True, write_now=True)
c.run_once()
wvtest.WVPASS(c.client_up(band))
wvtest.WVPASS(c.wifi_for_band(band).current_routes())
@@ -865,7 +534,7 @@
# an AP.
c.set_ethernet(True)
c.bridge.set_connection_check_result('succeed')
- c.bridge.ip_testonly = '192.168.1.101'
+ subprocess.call(['ip', 'addr', 'add', '192.168.1.101', 'dev', c.bridge.name])
c.run_until_interface_update()
wvtest.WVPASS(c.access_point_up(band))
wvtest.WVFAIL(c.client_up(band))
@@ -876,7 +545,7 @@
# Now move (rather than delete) the configuration file. The AP should go
# away, and we should not be able to join the WLAN. Routes should not be
# affected.
- filename = wlan_config_filename(c._config_dir, band)
+ filename = subprocess.cwmp.wlan_config_filename(band)
other_filename = filename + '.bak'
os.rename(filename, other_filename)
c.run_once()
@@ -896,7 +565,7 @@
# Now delete the config and bring down the bridge and make sure we reprovision
# via the last working BSS.
- c.delete_wlan_config(band)
+ subprocess.mock('cwmp', band, delete_config=True, write_now=True)
c.bridge.set_connection_check_result('fail')
scan_count_for_band = c.wifi_for_band(band).wifi_scan_counter
c.run_until_interface_update()
@@ -907,6 +576,7 @@
check_tmp_hosts('192.168.1.101 %s\n127.0.0.1 localhost' % hostname)
# s3 is not what the cycler would suggest trying next.
wvtest.WVPASSNE('s3', c.wifi_for_band(band).cycler.peek())
+ subprocess.mock('cwmp', band, ssid=ssid, psk=psk)
# Run only once, so that only one BSS can be tried. It should be the s3 one,
# since that worked previously.
c.run_once()
@@ -914,18 +584,23 @@
# Make sure we didn't scan on `band`.
wvtest.WVPASSEQ(scan_count_for_band, c.wifi_for_band(band).wifi_scan_counter)
c.run_once()
- wvtest.WVPASS(c.tried_to_upload_logs())
+ wvtest.WVPASS(subprocess.upload_logs_and_wait.uploaded_logs())
- # Now re-create the WLAN config, connect to the WLAN, and make sure that s3 is
- # unset as last_successful_bss_info, since it is no longer available.
- c.write_wlan_config(band, ssid, psk)
+ # Now wait for the WLAN config to be created, connect to the WLAN, and make
+ # sure that s3 is unset as last_successful_bss_info, since it is no longer
+ # available.
c.run_once()
wvtest.WVPASS(c.acs())
wvtest.WVPASS(c.internet())
- c.can_connect_to_s3 = False
- c.scan_results_include_hidden = False
- c.delete_wlan_config(band)
+ # Remove s3.
+ subprocess.mock('wifi', 'remote_ap_remove',
+ bssid='ff:ee:dd:cc:bb:aa', band=band)
+ # Bring s2 back.
+ subprocess.mock('wifi', 'remote_ap', bssid='01:23:45:67:89:ab', ssid='s2',
+ band=band, security=None)
+
+ subprocess.mock('cwmp', band, delete_config=True, write_now=True)
c.run_once()
wvtest.WVPASSEQ(c.wifi_for_band(band).last_successful_bss_info, None)
@@ -939,7 +614,7 @@
# disconnecting.
# 4) Connect to s2 but get no ACS access; see that last_successful_bss_info is
# unset.
- c.write_wlan_config(band, ssid, psk)
+ subprocess.mock('cwmp', band, ssid=ssid, psk=psk, write_now=True)
# Connect
c.run_once()
# Process DHCP results
@@ -947,31 +622,37 @@
wvtest.WVPASS(c.acs())
wvtest.WVPASS(c.internet())
- c.delete_wlan_config(band)
+ subprocess.mock('cwmp', band, delete_config=True, write_now=True)
c.run_once()
wvtest.WVFAIL(c.wifi_for_band(band).acs())
- c.can_connect_to_s2 = True
# Give it time to try all BSSIDs. This means sleeping long enough that
# everything in the cycler is active, then doing n+1 loops (the n+1st loop is
# when we decide that the SSID in the nth loop was successful).
time.sleep(c._bssid_cycle_length_s)
+ subprocess.mock('cwmp', band, ssid=ssid, psk=psk)
for _ in range(len(c.wifi_for_band(band).cycler) + 1):
c.run_once()
- s2_bss = iw.BssInfo('01:23:45:67:89:ab', 's2')
+ s2_bss = iw.BssInfo(bssid='01:23:45:67:89:ab', ssid='s2', band=band)
wvtest.WVPASSEQ(c.wifi_for_band(band).last_successful_bss_info, s2_bss)
c.run_once()
- wvtest.WVPASS(c.tried_to_upload_logs())
+ wvtest.WVPASS(subprocess.upload_logs_and_wait.uploaded_logs())
- c.s2_fail = True
- c.write_wlan_config(band, ssid, psk)
+ # Make s2's connection check fail.
+ subprocess.mock('wifi', 'remote_ap', bssid='01:23:45:67:89:ab', ssid='s2',
+ band=band, security=None, connection_check_result='fail')
+
c.run_once()
wvtest.WVPASS(c.acs())
wvtest.WVPASS(c.internet())
-
wvtest.WVPASSEQ(c.wifi_for_band(band).last_successful_bss_info, s2_bss)
- c._wlan_configuration[band].stale = True
- c.wifi_for_band(band).add_disconnected_event()
+ # Disconnect.
+ ssid = 'wlan3'
+ psk = 'password3'
+ subprocess.mock('wifi', 'remote_ap',
+ bssid='11:22:33:44:55:66',
+ ssid=ssid, psk=psk, band=band, security='WPA2')
+ subprocess.mock('wifi', 'disconnected_event', band)
# Run once so that c will reconnect to s2.
c.run_once()
wvtest.WVPASS(c.wifi_for_band(band).connected_to_open())
@@ -986,16 +667,18 @@
# which lets us force a timeout and proceed to the next AP. Having a stale
# WLAN configuration shouldn't interrupt provisioning.
del c.wifi_for_band(band).cycler
- c.interface_with_scan_results = c.wifi_for_band(band).name
- c.scan_results_include_hidden = True
- c.can_connect_to_s3 = True
- c.dhcp_failure_on_s3 = True
+ subprocess.mock('wifi', 'remote_ap', bssid='ff:ee:dd:cc:bb:aa', ssid='s3',
+ band=band, security=None, hidden=True,
+ vendor_ies=(('f4:f5:e8', '01'), ('f4:f5:e8', '03 73 33')))
+ subprocess.mock('run-dhclient', c.wifi_for_band(band).name, failure=True)
+
# First iteration: check that we try s3.
c.run_until_scan(band)
last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
wvtest.WVPASSEQ(last_bss_info.ssid, 's3')
wvtest.WVPASSEQ(last_bss_info.bssid, 'ff:ee:dd:cc:bb:aa')
- c.write_wlan_config(band, ssid, psk)
+ # Attempt to interrupt provisioning, make sure it doesn't work.
+ c._try_wlan_after[band] = 0
# Second iteration: check that we try s3 again since there's no gateway yet.
c.run_once()
last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
@@ -1011,7 +694,8 @@
wvtest.WVPASSNE(last_bss_info.bssid, 'ff:ee:dd:cc:bb:aa')
# We can delete the stale WLAN config now, to simplify subsequent tests.
- c.delete_wlan_config(band)
+ # c.delete_wlan_config(band)
+ subprocess.mock('cwmp', band, delete_config=True, write_now=True)
# Now repeat the above, but for an ACS session that takes a while. We don't
# necessarily want to leave if it fails (so we don't want the third check),
@@ -1020,12 +704,13 @@
# Unlike DHCP, which we can always simulate working immediately above, it is
# wrong to simulate ACS sessions working for connections without ACS access.
# This means we can either always wait for the ACS session timeout in every
- # test above, making the tests much slower, or we can set that timeout to 0
- # and then be a little gross here and change it. The latter is unfortunately
- # the lesser evil, because slow tests are bad.
+ # test above, making the tests much slower, or we can set that timeout very
+ # low and then be a little gross here and change it. The latter is
+ # unfortunately the lesser evil, because slow tests are bad.
del c.wifi_for_band(band).cycler
- c.dhcp_failure_on_s3 = False
- c.acs_session_fails = True
+ subprocess.mock('run-dhclient', c.wifi_for_band(band).name, failure=False)
+ subprocess.mock('cwmp', band, acs_session_fails=True)
+
# First iteration: check that we try s3.
c.run_until_scan(band)
last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
@@ -1050,8 +735,7 @@
# Finally, test successful provisioning.
del c.wifi_for_band(band).cycler
- c.dhcp_failure_on_s3 = False
- c.acs_session_fails = False
+ subprocess.mock('cwmp', band, acs_session_fails=False)
# First iteration: check that we try s3.
c.run_until_scan(band)
last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
@@ -1092,25 +776,19 @@
@wvtest.wvtest
-@connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_FRENZY,
- quantenna_interfaces=['wlan1', 'wlan1_portal', 'wcli1']
- )
+@connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_FRENZY)
def connection_manager_test_generic_ath9k_frenzy_2g(c):
connection_manager_test_generic(c, '2.4')
@wvtest.wvtest
-@connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_FRENZY,
- quantenna_interfaces=['wlan1', 'wlan1_portal', 'wcli1']
- )
+@connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_FRENZY)
def connection_manager_test_generic_ath9k_frenzy_5g(c):
connection_manager_test_generic(c, '5')
@wvtest.wvtest
-@connection_manager_test(WIFI_SHOW_OUTPUT_FRENZY,
- quantenna_interfaces=['wlan0', 'wlan0_portal', 'wcli0']
- )
+@connection_manager_test(WIFI_SHOW_OUTPUT_FRENZY)
def connection_manager_test_generic_frenzy_5g(c):
connection_manager_test_generic(c, '5')
@@ -1123,24 +801,28 @@
Args:
c: The ConnectionManager set up by @connection_manager_test.
"""
+ ssid = 'my ssid'
+ psk = 'passphrase'
+
wvtest.WVPASSEQ(len(c._binwifi_commands), 2)
+
for band in ['2.4', '5']:
wvtest.WVPASS(('stop', '--band', band, '--persist') in c._binwifi_commands)
+ subprocess.mock('wifi', 'remote_ap',
+ bssid='11:22:33:44:55:66',
+ ssid=ssid, psk=psk, band=band, security='WPA2')
+
# Bring up ethernet, access.
c.set_ethernet(True)
c.run_once()
wvtest.WVPASS(c.acs())
wvtest.WVPASS(c.internet())
- ssid = 'my ssid'
- psk = 'passphrase'
-
# Bring up both access points.
- c.write_wlan_config('2.4', ssid, psk)
- c.enable_access_point('2.4')
- c.write_wlan_config('5', ssid, psk)
- c.enable_access_point('5')
+ for band in ('2.4', '5'):
+ subprocess.mock('cwmp', band, ssid=ssid, psk=psk, access_point=True,
+ write_now=True)
c.run_once()
wvtest.WVPASS(c.access_point_up('2.4'))
wvtest.WVPASS(c.access_point_up('5'))
@@ -1152,7 +834,7 @@
# Disable the 2.4 GHz AP, make sure the 5 GHz AP stays up. 2.4 GHz should
# join the WLAN.
- c.disable_access_point('2.4')
+ subprocess.mock('cwmp', '2.4', access_point=False, write_now=True)
c.run_until_interface_update()
wvtest.WVFAIL(c.access_point_up('2.4'))
wvtest.WVPASS(c.access_point_up('5'))
@@ -1163,7 +845,7 @@
# Delete the 2.4 GHz WLAN configuration; it should leave the WLAN but nothing
# else should change.
- c.delete_wlan_config('2.4')
+ subprocess.mock('cwmp', '2.4', delete_config=True, write_now=True)
c.run_until_interface_update()
wvtest.WVFAIL(c.access_point_up('2.4'))
wvtest.WVPASS(c.access_point_up('5'))
@@ -1175,7 +857,7 @@
# Disable the wired connection and remove the WLAN configurations. Both
# radios should scan. Wait for 5 GHz to scan, then enable scan results for
# 2.4. This should lead to ACS access.
- c.delete_wlan_config('5')
+ subprocess.mock('cwmp', '5', delete_config=True, write_now=True)
c.set_ethernet(False)
c.run_once()
wvtest.WVFAIL(c.acs())
@@ -1193,7 +875,7 @@
wvtest.WVFAIL(c.wifi_for_band('5').current_routes_normal_testonly())
# The next 2.4 GHz scan will have results.
- c.interface_with_scan_results = c.wifi_for_band('2.4').name
+ _enable_basic_scan_results('2.4')
c.run_until_scan('2.4')
# Now run for enough cycles that s2 will have been tried.
for _ in range(len(c.wifi_for_band('2.4').cycler)):
@@ -1204,7 +886,7 @@
wvtest.WVPASS(c.wifi_for_band('2.4').current_routes())
wvtest.WVFAIL(c.wifi_for_band('5').current_routes_normal_testonly())
c.run_once()
- wvtest.WVPASS(c.tried_to_upload_logs())
+ wvtest.WVPASS(subprocess.upload_logs_and_wait.uploaded_logs())
@wvtest.wvtest
@@ -1214,9 +896,7 @@
@wvtest.wvtest
-@connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_FRENZY,
- quantenna_interfaces=['wlan1', 'wlan1_portal', 'wcli1']
- )
+@connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_FRENZY)
def connection_manager_test_dual_band_two_radios_ath9k_frenzy(c):
connection_manager_test_dual_band_two_radios(c)
@@ -1243,10 +923,10 @@
psk = 'passphrase'
# Enable both access points. Only 5 should be up.
- c.write_wlan_config('2.4', ssid, psk)
- c.enable_access_point('2.4')
- c.write_wlan_config('5', ssid, psk)
- c.enable_access_point('5')
+ subprocess.mock('cwmp', '2.4', ssid=ssid, psk=psk, access_point=True,
+ write_now=True)
+ subprocess.mock('cwmp', '5', ssid=ssid, psk=psk, access_point=True,
+ write_now=True)
c.run_once()
wvtest.WVFAIL(c.access_point_up('2.4'))
wvtest.WVPASS(c.access_point_up('5'))
@@ -1256,7 +936,7 @@
# Disable the 2.4 GHz AP; nothing should change. The 2.4 GHz client should
# not be up because the same radio is being used to run a 5 GHz AP.
- c.disable_access_point('2.4')
+ subprocess.mock('cwmp', '2.4', access_point=False, write_now=True)
c.run_until_interface_update()
wvtest.WVFAIL(c.access_point_up('2.4'))
wvtest.WVPASS(c.access_point_up('5'))
@@ -1266,7 +946,7 @@
wvtest.WVFAIL(c.wifi_for_band('5').current_routes_normal_testonly())
# Delete the 2.4 GHz WLAN configuration; nothing should change.
- c.delete_wlan_config('2.4')
+ subprocess.mock('cwmp', '2.4', delete_config=True, write_now=True)
c.run_once()
wvtest.WVFAIL(c.access_point_up('2.4'))
wvtest.WVPASS(c.access_point_up('5'))
@@ -1279,7 +959,7 @@
# should be a single scan that leads to ACS access. (It doesn't matter which
# band we specify in run_until_scan, since both bands point to the same
# interface.)
- c.delete_wlan_config('5')
+ subprocess.mock('cwmp', '5', delete_config=True, write_now=True)
c.set_ethernet(False)
c.run_once()
wvtest.WVFAIL(c.acs())
@@ -1288,7 +968,7 @@
wvtest.WVFAIL(c.wifi_for_band('5').current_routes_normal_testonly())
# The scan will have results that will lead to ACS access.
- c.interface_with_scan_results = c.wifi_for_band('2.4').name
+ _enable_basic_scan_results('2.4')
c.run_until_scan('5')
for _ in range(len(c.wifi_for_band('2.4').cycler)):
c.run_once()
@@ -1298,7 +978,7 @@
wvtest.WVPASS(c.wifi_for_band('2.4').current_routes())
wvtest.WVPASS(c.wifi_for_band('5').current_routes())
c.run_once()
- wvtest.WVPASSEQ(c.log_upload_count, 1)
+ wvtest.WVPASS(subprocess.upload_logs_and_wait.uploaded_logs())
@wvtest.wvtest
@@ -1320,6 +1000,12 @@
"""
# Make sure we've correctly set up the test; that there is no 5 GHz wifi
# interface.
+ ssid = 'my ssid'
+ psk = 'my psk'
+ subprocess.mock('wifi', 'remote_ap', band='2.4', ssid=ssid, psk=psk,
+ bssid='00:00:00:00:00:00', security='WPA2',
+ connection_check_result='succeed')
+
wvtest.WVPASSEQ(c.wifi_for_band('5'), None)
c.set_ethernet(True)
@@ -1327,17 +1013,17 @@
wvtest.WVPASS(c.internet())
# Make sure this doesn't crash.
- c.write_wlan_config('5', 'my ssid', 'my psk')
+ subprocess.mock('cwmp', '5', ssid=ssid, psk=psk, write_now=True)
c.run_once()
- c.enable_access_point('5')
+ subprocess.mock('cwmp', '5', access_point=True, write_now=True)
c.run_once()
- c.disable_access_point('5')
+ subprocess.mock('cwmp', '5', access_point=False, write_now=True)
c.run_once()
- c.delete_wlan_config('5')
+ subprocess.mock('cwmp', '5', delete_config=True, write_now=True)
c.run_once()
# Make sure 2.4 still works.
- c.write_wlan_config('2.4', 'my ssid', 'my psk')
+ subprocess.mock('cwmp', '2.4', ssid=ssid, psk=psk, write_now=True)
# Connect
c.run_once()
# Process DHCP results
@@ -1394,19 +1080,23 @@
# First, establish that we connect on 2.4 without the experiment, to make sure
# this test doesn't spuriously pass.
- c.write_wlan_config('2.4', 'my ssid', 'my psk')
+ ssid = 'my ssid'
+ psk = 'my psk'
+ subprocess.mock('wifi', 'remote_ap', ssid=ssid, psk=psk, band='2.4',
+ bssid='00:00:00:00:00:00')
+ subprocess.mock('cwmp', '2.4', ssid=ssid, psk=psk, write_now=True)
c.run_once()
wvtest.WVPASS(c.client_up('2.4'))
# Now, force a disconnect by deleting the config.
- c.delete_wlan_config('2.4')
+ subprocess.mock('cwmp', '2.4', delete_config=True, write_now=True)
c.run_once()
wvtest.WVFAIL(c.client_up('2.4'))
# Now enable the experiment, recreate the config, and make sure we don't
# connect.
experiment_testutils.enable('WifiNo2GClient')
- c.write_wlan_config('2.4', 'my ssid', 'my psk')
+ subprocess.mock('cwmp', '2.4', ssid=ssid, psk=psk, write_now=True)
c.run_once()
wvtest.WVFAIL(c.client_up('2.4'))
diff --git a/conman/interface.py b/conman/interface.py
index e71a322..68aa35b 100755
--- a/conman/interface.py
+++ b/conman/interface.py
@@ -12,7 +12,6 @@
logging.basicConfig(level=logging.DEBUG)
import experiment
-import wpactrl
METRIC_5GHZ = 20
METRIC_24GHZ_5GHZ = 21
@@ -229,15 +228,12 @@
try:
logging.debug('%s calling ip route %s', self.name, ' '.join(args))
- return self._really_ip_route(*args)
+ return subprocess.check_output(self.IP_ROUTE + list(args))
except subprocess.CalledProcessError as e:
logging.error('Failed to call "ip route" with args %r: %s', args,
e.message)
return ''
- def _really_ip_route(self, *args):
- return subprocess.check_output(self.IP_ROUTE + list(args))
-
def _ip_addr_show(self):
try:
return subprocess.check_output(self.IP_ADDR_SHOW + [self.name])
@@ -431,57 +427,19 @@
class Wifi(Interface):
"""Represents a wireless interface."""
- WPA_EVENT_RE = re.compile(r'<\d+>CTRL-EVENT-(?P<event>[A-Z\-]+).*')
- # pylint: disable=invalid-name
- WPACtrl = wpactrl.WPACtrl
-
def __init__(self, *args, **kwargs):
self.bands = kwargs.pop('bands', [])
super(Wifi, self).__init__(*args, **kwargs)
- self._wpa_control = None
- self.initial_ssid = None
@property
def wpa_supplicant(self):
+ self.update()
return 'wpa_supplicant' in self.links
@wpa_supplicant.setter
def wpa_supplicant(self, is_up):
self._set_link_status('wpa_supplicant', is_up)
- def attached(self):
- return self._wpa_control and self._wpa_control.attached
-
- def attach_wpa_control(self, path):
- """Attach to the wpa_supplicant control interface.
-
- Args:
- path: The path containing the wpa_supplicant control interface socket.
-
- Returns:
- Whether attaching was successful.
- """
- if self.attached():
- return True
-
- socket = os.path.join(path, self.name)
- logging.debug('%s socket is %s', self.name, socket)
- try:
- self._wpa_control = self.get_wpa_control(socket)
- self._wpa_control.attach()
- logging.debug('%s successfully attached', self.name)
- except (wpactrl.error, OSError) as e:
- logging.error('Error attaching to wpa_supplicant: %s', e)
- return False
-
- status = self.wpa_status()
- logging.debug('%s status after attaching is %s', self.name, status)
- self.wpa_supplicant = status.get('wpa_state') == 'COMPLETED'
- if not self._initialized:
- self.initial_ssid = status.get('ssid')
-
- return True
-
def wpa_status(self):
"""Parse the STATUS response from the wpa_supplicant control interface.
@@ -491,80 +449,41 @@
"""
status = {}
- if self._wpa_control and self._wpa_control.attached:
- logging.debug('%s ctrl_iface_path %s',
- self, self._wpa_control.ctrl_iface_path)
- lines = []
- try:
- lines = self._wpa_control.request('STATUS').splitlines()
- except (wpactrl.error, OSError) as e:
- logging.error('wpa_control STATUS request failed %s args %s',
- e.message, e.args)
- lines = self.wpa_cli_status().splitlines()
- for line in lines:
- if '=' not in line:
- continue
- k, v = line.strip().split('=', 1)
- status[k] = v
-
- return status
-
- def get_wpa_control(self, socket):
- return self.WPACtrl(socket)
-
- def detach_wpa_control(self):
- if self.attached():
- try:
- self._wpa_control.detach()
- except (wpactrl.error, OSError):
- logging.error('Failed to detach from wpa_supplicant interface. This '
- 'may mean something else killed wpa_supplicant.')
- self._wpa_control = None
-
- self.wpa_supplicant = False
-
- def handle_wpa_events(self):
- if not self.attached():
- self.wpa_supplicant = False
- return
-
- while self._wpa_control.pending():
- match = self.WPA_EVENT_RE.match(self._wpa_control.recv())
- if match:
- event = match.group('event')
- logging.debug('%s got wpa_supplicant event %s', self.name, event)
- if event == 'CONNECTED':
- self.wpa_supplicant = True
- elif event in ('DISCONNECTED', 'TERMINATING', 'ASSOC-REJECT',
- 'SSID-TEMP-DISABLED', 'AUTH-REJECT'):
- self.wpa_supplicant = False
- if event == 'TERMINATING':
- self.detach_wpa_control()
- break
-
- self.update_routes()
-
- def initialize(self):
- """Unset self.initial_ssid, which is only relevant during initialization."""
-
- self.initial_ssid = None
- super(Wifi, self).initialize()
-
- def connected_to_open(self):
- return (self.wpa_status().get('wpa_state', None) == 'COMPLETED' and
- self.wpa_status().get('key_mgmt', None) == 'NONE')
-
- # TODO(rofrankel): Remove this if and when the wpactrl failures are fixed.
- def wpa_cli_status(self):
- """Fallback for wpa_supplicant control interface status requests."""
try:
- return subprocess.check_output(['wpa_cli', '-i', self.name, 'status'])
+ lines = subprocess.check_output(['wpa_cli', '-i', self.name,
+ 'status']).splitlines()
except subprocess.CalledProcessError:
logging.error('wpa_cli status request failed')
- return ''
+ return {}
+
+ for line in lines:
+ if '=' not in line:
+ continue
+ k, v = line.strip().split('=', 1)
+ status[k] = v
+
+ logging.debug('wpa_status is %r', status)
+ return status
+
+ def update(self):
+ self.wpa_supplicant = self.wpa_status().get('wpa_state', '') == 'COMPLETED'
+
+ def connected_to_open(self):
+ status = self.wpa_status()
+ return (status.get('wpa_state', None) == 'COMPLETED' and
+ status.get('key_mgmt', None) == 'NONE')
+
+ def current_secure_ssid(self):
+ """Returns SSID if connected to a secure network, False otherwise."""
+ status = self.wpa_status()
+ return (status.get('wpa_state', None) == 'COMPLETED' and
+ # NONE indicates we're on a provisioning network; anything else
+ # suggests we're already on the WLAN.
+ status.get('key_mgmt', None) != 'NONE' and
+ status.get('ssid'))
-class FrenzyWPACtrl(object):
+class FrenzyWifi(Wifi):
"""A WPACtrl for Frenzy devices.
Implements the same functions used on the normal WPACtrl, using a combination
@@ -572,19 +491,6 @@
diffing saved state with current system state.
"""
- WIFIINFO_PATH = '/tmp/wifi/wifiinfo'
-
- def __init__(self, socket):
- self.ctrl_iface_path, self._interface = os.path.split(socket)
-
- # State from QCSAPI and wifi_files.
- self._client_mode = False
- self._ssid = None
- self._status = None
- self._security = None
-
- self._events = []
-
def _qcsapi(self, *command):
try:
return subprocess.check_output(['qcsapi'] + list(command)).strip()
@@ -592,79 +498,27 @@
logging.error('QCSAPI call failed: %s: %s', e, e.output)
raise
- def attach(self):
- self._update()
-
- @property
- def attached(self):
- return self._client_mode
-
- def detach(self):
- raise wpactrl.error('Real WPACtrl always raises this when detaching.')
-
- def pending(self):
- self._update()
- return bool(self._events)
-
- def _update(self):
+ def wpa_status(self):
"""Generate and cache events, update state."""
try:
client_mode = self._qcsapi('get_mode', 'wifi0') == 'Station'
ssid = self._qcsapi('get_ssid', 'wifi0')
- status = self._qcsapi('get_status', 'wifi0')
security = (self._qcsapi('ssid_get_authentication_mode', 'wifi0', ssid)
if ssid else None)
except subprocess.CalledProcessError:
- # If QCSAPI failed, skip update.
- return
+ # If QCSAPI failed, don't crash.
+ return {}
- # If we have an SSID and are in client mode, and at least one of those is
- # new, then we have just connected.
- if client_mode and ssid and (not self._client_mode or ssid != self._ssid):
- self._events.append('<2>CTRL-EVENT-CONNECTED')
+ up = bool(client_mode and ssid)
+ self.wpa_supplicant = up
- # If we are in client mode but lost SSID, we disconnected.
- if client_mode and self._ssid and not ssid:
- self._events.append('<2>CTRL-EVENT-DISCONNECTED')
-
- # If there is an auth/assoc failure, then status (above) is 'Error'. We
- # really want the converse of this implication (i.e. that 'Error' implies an
- # auth/assoc failure), but due to limited documentation this will have to
- # do. It should be good enough: if something else causes get_status to
- # return 'Error', we are probably not connected, and we don't do anything
- # special with auth/assoc failures specifically.
- if client_mode and status == 'Error' and self._status != 'Error':
- self._events.append('<2>CTRL-EVENT-SSID-TEMP-DISABLED')
-
- # If we left client mode, wpa_supplicant has terminated.
- if self._client_mode and not client_mode:
- self._events.append('<2>CTRL-EVENT-TERMINATING')
-
- self._client_mode = client_mode
- self._ssid = ssid
- self._status = status
- self._security = security
-
- def recv(self):
- return self._events.pop(0)
-
- def request(self, request_type):
- """Partial implementation of WPACtrl.request."""
-
- if request_type != 'STATUS':
- return ''
-
- self._update()
-
- if not self._client_mode or not self._ssid:
- return ''
-
- return ('wpa_state=COMPLETED\nssid=%s\nkey_mgmt=%s' %
- (self._ssid, self._security or 'NONE'))
-
-
-class FrenzyWifi(Wifi):
- """Represents a Frenzy wireless interface."""
-
- # pylint: disable=invalid-name
- WPACtrl = FrenzyWPACtrl
+ if up:
+ return {
+ 'wpa_state': 'COMPLETED',
+ 'ssid': ssid,
+ 'key_mgmt': security or 'NONE',
+ }
+ else:
+ return {
+ 'wpa_state': 'SCANNING',
+ }
diff --git a/conman/interface_test.py b/conman/interface_test.py
index 74363b8..f080285 100755
--- a/conman/interface_test.py
+++ b/conman/interface_test.py
@@ -5,8 +5,6 @@
import logging
import os
import shutil
-import socket
-import struct
import subprocess
import tempfile
import time
@@ -15,11 +13,6 @@
# pylint: disable=g-import-not-at-top
logging.basicConfig(level=logging.DEBUG)
-# This is in site-packages on the device, but not when running tests, and so
-# raises lint errors.
-# pylint: disable=g-bad-import-order
-import wpactrl
-
import experiment_testutils
import interface
from wvtest import wvtest
@@ -38,87 +31,13 @@
def __init__(self, *args, **kwargs):
super(FakeInterfaceMixin, self).__init__(*args, **kwargs)
self.set_connection_check_result('succeed')
- self.routing_table = {}
- self.ip_testonly = None
-
- def _connection_check(self, *args, **kwargs):
- result = super(FakeInterfaceMixin, self)._connection_check(*args, **kwargs)
- if not self.links:
- return False
- if (self.current_routes().get('default', {}).get('via', None) !=
- self._gateway_ip):
- return False
- return result
+ subprocess.ip.register_testonly(self.name)
def set_connection_check_result(self, result):
if result in ['succeed', 'fail', 'restricted']:
- # pylint: disable=invalid-name
- self.CONNECTION_CHECK = './test/' + result
+ subprocess.mock(self.CONNECTION_CHECK, self.name, result)
else:
- raise ValueError('Invalid fake connection_check script.')
-
- def _really_ip_route(self, *args):
- def can_add_route():
- def ip_to_int(ip):
- return struct.unpack('!I', socket.inet_pton(socket.AF_INET, ip))[0]
-
- if args[1] != 'default':
- return True
-
- via = ip_to_int(args[args.index('via') + 1])
- for (ifc, route, _), _ in self.routing_table.iteritems():
- if ifc != self.name:
- continue
-
- netmask = 0
- if '/' in route:
- route, netmask = route.split('/')
- netmask = 32 - int(netmask)
- route = ip_to_int(route)
-
- if (route >> netmask) == (via >> netmask):
- return True
-
- return False
-
- if not args:
- return '\n'.join(self.routing_table.values() +
- ['1.2.3.4/24 dev fake0 proto kernel scope link',
- # Non-subnet route, e.g. to NFS host.
- '1.2.3.1 dev %s proto kernel scope link' % self.name,
- 'default via 1.2.3.4 dev fake0',
- 'random junk'])
-
- metric = None
- if 'metric' in args:
- metric = args[args.index('metric') + 1]
- if args[0] in ('add', 'del'):
- route = args[1]
- key = (self.name, route, metric)
- if args[0] == 'add' and key not in self.routing_table:
- if not can_add_route():
- raise subprocess.CalledProcessError(
- 'Tried to add default route without subnet route: %r',
- self.routing_table)
- logging.debug('Adding route for %r', key)
- self.routing_table[key] = ' '.join(args[1:])
- elif args[0] == 'del':
- if key in self.routing_table:
- logging.debug('Deleting route for %r', key)
- del self.routing_table[key]
- elif key[2] is None:
- # pylint: disable=g-builtin-op
- for k in self.routing_table.keys():
- if k[:-1] == key[:-1]:
- logging.debug('Deleting route for %r (generalized from %s)', k, key)
- del self.routing_table[k]
- break
-
- def _ip_addr_show(self):
- if self.ip_testonly:
- return _IP_ADDR_SHOW_TPL.format(name=self.name, ip=self.ip_testonly)
-
- return ''
+ raise ValueError('Invalid fake connection_check value.')
def current_routes_normal_testonly(self):
result = self.current_routes()
@@ -129,244 +48,13 @@
pass
-class FakeWPACtrl(object):
- """Fake wpactrl.WPACtrl."""
-
- # pylint: disable=unused-argument
- def __init__(self, wpa_socket):
- self._socket = wpa_socket
- self.events = []
- self.attached = False
- self.connected = False
- self.ssid_testonly = None
- self.secure_testonly = False
- self.request_status_fails = False
-
- def pending(self):
- self.check_socket_exists('pending: socket does not exist')
- return bool(self.events)
-
- def recv(self):
- self.check_socket_exists('recv: socket does not exist')
- return self.events.pop(0)
-
- def attach(self):
- if not os.path.exists(self._socket):
- raise wpactrl.error('wpactrl_attach failed')
- self.attached = True
-
- def detach(self):
- self.attached = False
- self.ssid_testonly = None
- self.secure_testonly = False
- self.connected = False
- self.check_socket_exists('wpactrl_detach failed')
-
- def request(self, request_type):
- if request_type == 'STATUS':
- if self.request_status_fails:
- raise wpactrl.error('test error')
- return self.wpa_cli_status_testonly()
- else:
- raise ValueError('Invalid request_type %s' % request_type)
-
- @property
- def ctrl_iface_path(self):
- return os.path.split(self._socket)[0]
-
- # Below methods are not part of WPACtrl.
-
- def add_event(self, event):
- self.events.append(event)
-
- def add_connected_event(self):
- self.connected = True
- self.add_event(Wifi.CONNECTED_EVENT)
-
- def add_disconnected_event(self):
- self.connected = False
- self.add_event(Wifi.DISCONNECTED_EVENT)
-
- def add_terminating_event(self):
- self.connected = False
- self.add_event(Wifi.TERMINATING_EVENT)
-
- def check_socket_exists(self, msg='Fake socket does not exist'):
- if not os.path.exists(self._socket):
- raise wpactrl.error(msg)
-
- def wpa_cli_status_testonly(self):
- if self.connected:
- return ('foo\nwpa_state=COMPLETED\nssid=%s\nkey_mgmt=%s\nbar' %
- (self.ssid_testonly,
- 'WPA2-PSK' if self.secure_testonly else 'NONE'))
- else:
- return 'wpa_state=SCANNING\naddress=12:34:56:78:90:ab'
-
-
class Wifi(FakeInterfaceMixin, interface.Wifi):
"""Fake Wifi for testing."""
-
- CONNECTED_EVENT = '<2>CTRL-EVENT-CONNECTED'
- DISCONNECTED_EVENT = '<2>CTRL-EVENT-DISCONNECTED'
- TERMINATING_EVENT = '<2>CTRL-EVENT-TERMINATING'
-
- WPACtrl = FakeWPACtrl
-
- def __init__(self, *args, **kwargs):
- super(Wifi, self).__init__(*args, **kwargs)
- self._initial_ssid_testonly = None
- self._secure_testonly = False
-
- def attach_wpa_control(self, path):
- if self._initial_ssid_testonly and self._wpa_control:
- self._wpa_control.connected = True
- super(Wifi, self).attach_wpa_control(path)
-
- def get_wpa_control(self, *args, **kwargs):
- result = super(Wifi, self).get_wpa_control(*args, **kwargs)
- if self._initial_ssid_testonly:
- result.connected = True
- result.ssid_testonly = self._initial_ssid_testonly
- result.secure_testonly = self._secure_testonly
- return result
-
- def add_connected_event(self):
- if self.attached():
- self._wpa_control.add_connected_event()
-
- def add_disconnected_event(self):
- self._initial_ssid_testonly = None
- self._secure_testonly = False
- if self.attached():
- self._wpa_control.add_disconnected_event()
-
- def add_terminating_event(self):
- self._initial_ssid_testonly = None
- self._secure_testonly = False
- if self.attached():
- self._wpa_control.add_terminating_event()
-
- def detach_wpa_control(self):
- self._initial_ssid_testonly = None
- self._secure_testonly = False
- super(Wifi, self).detach_wpa_control()
-
- def wpa_cli_status(self):
- # This is just a convenient way of keeping things dry; the actual wpa_cli
- # status makes a subprocess call which returns the same string.
- return self._wpa_control.wpa_cli_status_testonly()
-
- def start_wpa_supplicant_testonly(self, path):
- wpa_socket = os.path.join(path, self.name)
- logging.debug('Starting fake wpa_supplicant for %s: %s',
- self.name, wpa_socket)
- open(wpa_socket, 'w')
-
- def kill_wpa_supplicant_testonly(self, path):
- logging.debug('Killing fake wpa_supplicant for %s', self.name)
- if self.attached():
- self.detach_wpa_control()
- os.unlink(os.path.join(path, self.name))
- else:
- raise RuntimeError('Trying to kill wpa_supplicant while not attached')
-
-
-class FrenzyWPACtrl(interface.FrenzyWPACtrl):
-
- def __init__(self, *args, **kwargs):
- super(FrenzyWPACtrl, self).__init__(*args, **kwargs)
- self.ssid_testonly = None
- self.secure_testonly = False
- self.request_status_fails = False
-
- def _qcsapi(self, *command):
- return self.fake_qcsapi.get(command[0], None)
-
- def add_connected_event(self):
- self.fake_qcsapi['get_mode'] = 'Station'
- self.fake_qcsapi['get_ssid'] = self.ssid_testonly
- security = 'PSKAuthentication' if self.secure_testonly else 'NONE'
- self.fake_qcsapi['ssid_get_authentication_mode'] = security
-
- def add_disconnected_event(self):
- self.ssid_testonly = None
- self.secure_testonly = False
- self.fake_qcsapi['get_ssid'] = None
- self.fake_qcsapi['ssid_get_authentication_mode'] = 'NONE'
-
- def add_terminating_event(self):
- self.ssid_testonly = None
- self.secure_testonly = False
- self.fake_qcsapi['get_ssid'] = None
- self.fake_qcsapi['get_mode'] = 'AP'
- self.fake_qcsapi['ssid_get_authentication_mode'] = 'NONE'
-
- def detach(self):
- self.add_terminating_event()
- super(FrenzyWPACtrl, self).detach()
+ pass
class FrenzyWifi(FakeInterfaceMixin, interface.FrenzyWifi):
- WPACtrl = FrenzyWPACtrl
-
- def __init__(self, *args, **kwargs):
- super(FrenzyWifi, self).__init__(*args, **kwargs)
- self._initial_ssid_testonly = None
- self._secure_testonly = False
- self.fake_qcsapi = {}
-
- def attach_wpa_control(self, *args, **kwargs):
- super(FrenzyWifi, self).attach_wpa_control(*args, **kwargs)
- if self._wpa_control:
- self._wpa_control.ssid_testonly = self._initial_ssid_testonly
- self._wpa_control.secure_testonly = self._secure_testonly
- if self._initial_ssid_testonly:
- self._wpa_control.add_connected_event()
-
- def get_wpa_control(self, *args, **kwargs):
- result = super(FrenzyWifi, self).get_wpa_control(*args, **kwargs)
- result.fake_qcsapi = self.fake_qcsapi
- if self._initial_ssid_testonly:
- result.fake_qcsapi['get_mode'] = 'Station'
- result.ssid_testonly = self._initial_ssid_testonly
- result.secure_testonly = self._secure_testonly
- result.add_connected_event()
- return result
-
- def add_connected_event(self):
- if self.attached():
- self._wpa_control.add_connected_event()
-
- def add_disconnected_event(self):
- self._initial_ssid_testonly = None
- self._secure_testonly = False
- if self.attached():
- self._wpa_control.add_disconnected_event()
-
- def add_terminating_event(self):
- self._initial_ssid_testonly = None
- self._secure_testonly = False
- if self.attached():
- self._wpa_control.add_terminating_event()
-
- def detach_wpa_control(self):
- self._initial_ssid_testonly = None
- self._secure_testonly = False
- super(FrenzyWifi, self).detach_wpa_control()
-
- def start_wpa_supplicant_testonly(self, unused_path):
- logging.debug('Starting fake wpa_supplicant for %s', self.name)
- self.fake_qcsapi['get_mode'] = 'Station'
-
- def kill_wpa_supplicant_testonly(self, unused_path):
- logging.debug('Killing fake wpa_supplicant for %s', self.name)
- if self.attached():
- # This happens to do what we need.
- self.add_terminating_event()
- self.detach_wpa_control()
- else:
- raise RuntimeError('Trying to kill wpa_supplicant while not attached')
+ pass
@wvtest.wvtest
@@ -443,7 +131,7 @@
wvtest.WVPASS(os.path.exists(autoprov_filepath))
wvtest.WVFAIL(b.get_ip_address())
- b.ip_testonly = '192.168.1.100'
+ subprocess.call(['ip', 'addr', 'add', '192.168.1.100', 'dev', b.name])
wvtest.WVPASSEQ(b.get_ip_address(), '192.168.1.100')
# Get a new gateway/subnet (e.g. due to joining a new network).
@@ -487,49 +175,25 @@
def generic_wifi_test(w, wpa_path):
# Not currently connected.
- w.start_wpa_supplicant_testonly(wpa_path)
- w.attach_wpa_control(wpa_path)
+ subprocess.wifi.WPA_PATH = wpa_path
wvtest.WVFAIL(w.wpa_supplicant)
- # pylint: disable=protected-access
- wpa_control = w._wpa_control
-
# wpa_supplicant connects.
- wpa_control.ssid_testonly = 'my=ssid'
- wpa_control.add_connected_event()
- wvtest.WVFAIL(w.wpa_supplicant)
- w.handle_wpa_events()
+ ssid = 'my=ssid'
+ psk = 'passphrase'
+ subprocess.mock('wifi', 'remote_ap', ssid=ssid, psk=psk, band='5',
+ bssid='00:00:00:00:00:00', connection_check_result='succeed')
+ subprocess.check_call(['wifi', 'setclient', '--ssid', ssid, '--band', '5'],
+ env={'WIFI_CLIENT_PSK': psk})
wvtest.WVPASS(w.wpa_supplicant)
w.set_gateway_ip('192.168.1.1')
# wpa_supplicant disconnects.
- wpa_control.add_disconnected_event()
- w.handle_wpa_events()
+ subprocess.mock('wifi', 'disconnected_event', '5')
wvtest.WVFAIL(w.wpa_supplicant)
- # Now, start over so we can test what happens when wpa_supplicant is already
- # connected when we attach.
- w.detach_wpa_control()
- # pylint: disable=protected-access
- w._initial_ssid_testonly = 'my=ssid'
- w._initialized = False
- w.attach_wpa_control(wpa_path)
- wpa_control = w._wpa_control
-
- # wpa_supplicant was already connected when we attached.
- wvtest.WVPASS(w.wpa_supplicant)
- wvtest.WVPASSEQ(w.initial_ssid, 'my=ssid')
- w.initialize()
- wvtest.WVPASSEQ(w.initial_ssid, None)
-
- wvtest.WVPASSNE(w.wpa_status(), {})
- w._wpa_control.request_status_fails = True
- wvtest.WVPASSNE(w.wpa_status(), {})
-
# The wpa_supplicant process disconnects and terminates.
- wpa_control.add_disconnected_event()
- wpa_control.add_terminating_event()
- w.handle_wpa_events()
+ subprocess.check_call(['wifi', 'stopclient', '--band', '5'])
wvtest.WVFAIL(w.wpa_supplicant)
@@ -537,33 +201,40 @@
def wifi_test():
"""Test Wifi."""
w = Wifi('wcli0', '21')
- w.set_connection_check_result('succeed')
w.initialize()
try:
wpa_path = tempfile.mkdtemp()
+ conman_path = tempfile.mkdtemp()
+ subprocess.set_conman_paths(conman_path, None)
+ subprocess.mock('wifi', 'interfaces',
+ subprocess.wifi.MockInterface(phynum='0', bands=['5'],
+ driver='cfg80211'))
generic_wifi_test(w, wpa_path)
finally:
shutil.rmtree(wpa_path)
+ shutil.rmtree(conman_path)
@wvtest.wvtest
def frenzy_wifi_test():
"""Test FrenzyWifi."""
w = FrenzyWifi('wlan0', '20')
- w.set_connection_check_result('succeed')
w.initialize()
try:
wpa_path = tempfile.mkdtemp()
- FrenzyWifi.WPACtrl.WIFIINFO_PATH = wifiinfo_path = tempfile.mkdtemp()
-
+ conman_path = tempfile.mkdtemp()
+ subprocess.set_conman_paths(conman_path, None)
+ subprocess.mock('wifi', 'interfaces',
+ subprocess.wifi.MockInterface(phynum='0', bands=['5'],
+ driver='frenzy'))
generic_wifi_test(w, wpa_path)
finally:
shutil.rmtree(wpa_path)
- shutil.rmtree(wifiinfo_path)
+ shutil.rmtree(conman_path)
@wvtest.wvtest
diff --git a/conman/iw.py b/conman/iw.py
index f2e15d8..8d80010 100755
--- a/conman/iw.py
+++ b/conman/iw.py
@@ -15,6 +15,7 @@
_BSSID_RE = r'BSS (?P<BSSID>([0-9a-f]{2}:?){6})\(on .*\)'
_SSID_RE = r'SSID: (?P<SSID>.*)'
_RSSI_RE = r'signal: (?P<RSSI>.*) dBm'
+_FREQ_RE = r'freq: (?P<freq>\d+)'
_VENDOR_IE_RE = (r'Vendor specific: OUI (?P<OUI>([0-9a-f]{2}:?){3}), '
'data:(?P<data>( [0-9a-f]{2})+)')
@@ -29,16 +30,17 @@
class BssInfo(object):
"""Contains info about a BSS, parsed from 'iw scan'."""
- def __init__(self, bssid='', ssid='', rssi=-100, security=None,
+ def __init__(self, bssid='', ssid='', rssi=0, band=None, security=None,
vendor_ies=None):
self.bssid = bssid
self.ssid = ssid
self.rssi = rssi
+ self.band = band
self.vendor_ies = vendor_ies or []
self.security = security or []
def __attrs(self):
- return (self.bssid, self.ssid, tuple(sorted(self.vendor_ies)),
+ return (self.bssid, self.ssid, self.band, tuple(sorted(self.vendor_ies)),
tuple(sorted(self.security)), self.rssi)
def __eq__(self, other):
@@ -52,9 +54,9 @@
return hash(self.__attrs())
def __repr__(self):
- return '<BssInfo: SSID=%s BSSID=%s Security=%s Vendor IEs=%s>' % (
- self.ssid, self.bssid, ','.join(self.security),
- ','.join('|'.join(ie) for ie in self.vendor_ies))
+ return ('<BssInfo: SSID=%s BSSID=%s Band=%s Security=%s Vendor IEs=%s>'
+ % (self.ssid, self.bssid, self.band, ','.join(self.security),
+ ','.join('|'.join(ie) for ie in self.vendor_ies)))
# TODO(rofrankel): waveguide also scans. Can we find a way to avoid two programs
@@ -79,6 +81,10 @@
if match:
bss_info.rssi = float(match.group('RSSI'))
continue
+ match = re.match(_FREQ_RE, line)
+ if match:
+ bss_info.band = '2.4' if match.group('freq').startswith('2') else '5'
+ continue
match = re.match(_VENDOR_IE_RE, line)
if match:
bss_info.vendor_ies.append((match.group('OUI'),
diff --git a/conman/iw_test.py b/conman/iw_test.py
index 55b2e7b..202d10c 100755
--- a/conman/iw_test.py
+++ b/conman/iw_test.py
@@ -2,633 +2,48 @@
"""Tests for iw.py."""
+import subprocess
+
import iw
from wvtest import wvtest
-SCAN_OUTPUT = """BSS 00:23:97:57:f4:d8(on wcli0)
- TSF: 1269828266773 usec (14d, 16:43:48)
- freq: 2437
- beacon interval: 100 TUs
- capability: ESS Privacy ShortSlotTime (0x0411)
- signal: -60.00 dBm
- last seen: 2190 ms ago
- Information elements from Probe Response frame:
- Vendor specific: OUI 00:11:22, data: 01 23 45 67
- SSID: short scan result
- Supported rates: 1.0* 2.0* 5.5* 11.0* 18.0 24.0 36.0 54.0
- DS Parameter set: channel 6
- ERP: <no flags>
- ERP D4.0: <no flags>
- Privacy: WEP
- Extended supported rates: 6.0 9.0 12.0 48.0
-BSS 94:b4:0f:f1:02:a0(on wcli0)
- TSF: 16233722683 usec (0d, 04:30:33)
- freq: 2412
- beacon interval: 100 TUs
- capability: ESS Privacy ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1531)
- signal: -54.00 dBm
- last seen: 2490 ms ago
- Information elements from Probe Response frame:
- SSID: Google
- Supported rates: 36.0* 48.0 54.0
- DS Parameter set: channel 1
- Country: US Environment: Indoor/Outdoor
- Channels [1 - 11] @ 36 dBm
- Power constraint: 0 dB
- TPC report: TX power: 3 dBm
- ERP: <no flags>
- RSN: * Version: 1
- * Group cipher: CCMP
- * Pairwise ciphers: CCMP
- * Authentication suites: IEEE 802.1X
- * Capabilities: 4-PTKSA-RC 4-GTKSA-RC (0x0028)
- BSS Load:
- * station count: 0
- * channel utilisation: 33/255
- * available admission capacity: 25625 [*32us]
- HT capabilities:
- Capabilities: 0x19ad
- RX LDPC
- HT20
- SM Power Save disabled
- RX HT20 SGI
- TX STBC
- RX STBC 1-stream
- Max AMSDU length: 7935 bytes
- DSSS/CCK HT40
- Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
- Minimum RX AMPDU time spacing: 4 usec (0x05)
- HT RX MCS rate indexes supported: 0-23
- HT TX MCS rate indexes are undefined
- HT operation:
- * primary channel: 1
- * secondary channel offset: no secondary
- * STA channel width: 20 MHz
- * RIFS: 1
- * HT protection: nonmember
- * non-GF present: 0
- * OBSS non-GF present: 1
- * dual beacon: 0
- * dual CTS protection: 0
- * STBC beacon: 0
- * L-SIG TXOP Prot: 0
- * PCO active: 0
- * PCO phase: 0
- Overlapping BSS scan params:
- * passive dwell: 20 TUs
- * active dwell: 10 TUs
- * channel width trigger scan interval: 300 s
- * scan passive total per channel: 200 TUs
- * scan active total per channel: 20 TUs
- * BSS width channel transition delay factor: 5
- * OBSS Scan Activity Threshold: 0.25 %
- Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
- WMM: * Parameter version 1
- * u-APSD
- * BE: CW 15-1023, AIFSN 3
- * BK: CW 15-1023, AIFSN 7
- * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
- * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
-BSS 94:b4:0f:f1:35:60(on wcli0)
- TSF: 1739987968 usec (0d, 00:28:59)
- freq: 2462
- beacon interval: 100 TUs
- capability: ESS Privacy ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1531)
- signal: -39.00 dBm
- last seen: 1910 ms ago
- Information elements from Probe Response frame:
- SSID: Google
- Supported rates: 36.0* 48.0 54.0
- DS Parameter set: channel 11
- Country: US Environment: Indoor/Outdoor
- Channels [1 - 11] @ 36 dBm
- Power constraint: 0 dB
- TPC report: TX power: 3 dBm
- ERP: <no flags>
- RSN: * Version: 1
- * Group cipher: CCMP
- * Pairwise ciphers: CCMP
- * Authentication suites: IEEE 802.1X
- * Capabilities: 4-PTKSA-RC 4-GTKSA-RC (0x0028)
- BSS Load:
- * station count: 0
- * channel utilisation: 49/255
- * available admission capacity: 26875 [*32us]
- HT capabilities:
- Capabilities: 0x19ad
- RX LDPC
- HT20
- SM Power Save disabled
- RX HT20 SGI
- TX STBC
- RX STBC 1-stream
- Max AMSDU length: 7935 bytes
- DSSS/CCK HT40
- Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
- Minimum RX AMPDU time spacing: 4 usec (0x05)
- HT RX MCS rate indexes supported: 0-23
- HT TX MCS rate indexes are undefined
- HT operation:
- * primary channel: 11
- * secondary channel offset: no secondary
- * STA channel width: 20 MHz
- * RIFS: 1
- * HT protection: nonmember
- * non-GF present: 1
- * OBSS non-GF present: 1
- * dual beacon: 0
- * dual CTS protection: 0
- * STBC beacon: 0
- * L-SIG TXOP Prot: 0
- * PCO active: 0
- * PCO phase: 0
- Overlapping BSS scan params:
- * passive dwell: 20 TUs
- * active dwell: 10 TUs
- * channel width trigger scan interval: 300 s
- * scan passive total per channel: 200 TUs
- * scan active total per channel: 20 TUs
- * BSS width channel transition delay factor: 5
- * OBSS Scan Activity Threshold: 0.25 %
- Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
- WMM: * Parameter version 1
- * u-APSD
- * BE: CW 15-1023, AIFSN 3
- * BK: CW 15-1023, AIFSN 7
- * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
- * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
-BSS 94:b4:0f:f1:35:61(on wcli0)
- TSF: 1739988134 usec (0d, 00:28:59)
- freq: 2462
- beacon interval: 100 TUs
- capability: ESS ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1521)
- signal: -38.00 dBm
- last seen: 1910 ms ago
- Information elements from Probe Response frame:
- SSID: GoogleGuest
- Supported rates: 36.0* 48.0 54.0
- DS Parameter set: channel 11
- Country: US Environment: Indoor/Outdoor
- Channels [1 - 11] @ 36 dBm
- Power constraint: 0 dB
- TPC report: TX power: 3 dBm
- ERP: <no flags>
- BSS Load:
- * station count: 1
- * channel utilisation: 49/255
- * available admission capacity: 26875 [*32us]
- HT capabilities:
- Capabilities: 0x19ad
- RX LDPC
- HT20
- SM Power Save disabled
- RX HT20 SGI
- TX STBC
- RX STBC 1-stream
- Max AMSDU length: 7935 bytes
- DSSS/CCK HT40
- Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
- Minimum RX AMPDU time spacing: 4 usec (0x05)
- HT RX MCS rate indexes supported: 0-23
- HT TX MCS rate indexes are undefined
- HT operation:
- * primary channel: 11
- * secondary channel offset: no secondary
- * STA channel width: 20 MHz
- * RIFS: 1
- * HT protection: nonmember
- * non-GF present: 1
- * OBSS non-GF present: 1
- * dual beacon: 0
- * dual CTS protection: 0
- * STBC beacon: 0
- * L-SIG TXOP Prot: 0
- * PCO active: 0
- * PCO phase: 0
- Overlapping BSS scan params:
- * passive dwell: 20 TUs
- * active dwell: 10 TUs
- * channel width trigger scan interval: 300 s
- * scan passive total per channel: 200 TUs
- * scan active total per channel: 20 TUs
- * BSS width channel transition delay factor: 5
- * OBSS Scan Activity Threshold: 0.25 %
- Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
- WMM: * Parameter version 1
- * u-APSD
- * BE: CW 15-1023, AIFSN 3
- * BK: CW 15-1023, AIFSN 7
- * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
- * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
-BSS 94:b4:0f:f1:3a:e0(on wcli0)
- TSF: 24578560051 usec (0d, 06:49:38)
- freq: 2437
- beacon interval: 100 TUs
- capability: ESS Privacy ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1531)
- signal: -55.00 dBm
- last seen: 2310 ms ago
- Information elements from Probe Response frame:
- SSID: Google
- Supported rates: 36.0* 48.0 54.0
- DS Parameter set: channel 6
- TIM: DTIM Count 0 DTIM Period 1 Bitmap Control 0x0 Bitmap[0] 0x0
- Country: US Environment: Indoor/Outdoor
- Channels [1 - 11] @ 36 dBm
- Power constraint: 0 dB
- TPC report: TX power: 3 dBm
- ERP: <no flags>
- RSN: * Version: 1
- * Group cipher: CCMP
- * Pairwise ciphers: CCMP
- * Authentication suites: IEEE 802.1X
- * Capabilities: 4-PTKSA-RC 4-GTKSA-RC (0x0028)
- BSS Load:
- * station count: 1
- * channel utilisation: 21/255
- * available admission capacity: 28125 [*32us]
- HT capabilities:
- Capabilities: 0x19ad
- RX LDPC
- HT20
- SM Power Save disabled
- RX HT20 SGI
- TX STBC
- RX STBC 1-stream
- Max AMSDU length: 7935 bytes
- DSSS/CCK HT40
- Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
- Minimum RX AMPDU time spacing: 4 usec (0x05)
- HT RX MCS rate indexes supported: 0-23
- HT TX MCS rate indexes are undefined
- HT operation:
- * primary channel: 6
- * secondary channel offset: no secondary
- * STA channel width: 20 MHz
- * RIFS: 1
- * HT protection: nonmember
- * non-GF present: 1
- * OBSS non-GF present: 1
- * dual beacon: 0
- * dual CTS protection: 0
- * STBC beacon: 0
- * L-SIG TXOP Prot: 0
- * PCO active: 0
- * PCO phase: 0
- Overlapping BSS scan params:
- * passive dwell: 20 TUs
- * active dwell: 10 TUs
- * channel width trigger scan interval: 300 s
- * scan passive total per channel: 200 TUs
- * scan active total per channel: 20 TUs
- * BSS width channel transition delay factor: 5
- * OBSS Scan Activity Threshold: 0.25 %
- Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
- WMM: * Parameter version 1
- * u-APSD
- * BE: CW 15-1023, AIFSN 3
- * BK: CW 15-1023, AIFSN 7
- * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
- * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
-BSS 94:b4:0f:f1:3a:e1(on wcli0)
- TSF: 24578576547 usec (0d, 06:49:38)
- freq: 2437
- beacon interval: 100 TUs
- capability: ESS ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1521)
- signal: -65.00 dBm
- last seen: 80 ms ago
- Information elements from Probe Response frame:
- SSID: GoogleGuest
- Supported rates: 36.0* 48.0 54.0
- DS Parameter set: channel 6
- Country: US Environment: Indoor/Outdoor
- Channels [1 - 11] @ 36 dBm
- Power constraint: 0 dB
- TPC report: TX power: 3 dBm
- ERP: <no flags>
- BSS Load:
- * station count: 2
- * channel utilisation: 21/255
- * available admission capacity: 28125 [*32us]
- HT capabilities:
- Capabilities: 0x19ad
- RX LDPC
- HT20
- SM Power Save disabled
- RX HT20 SGI
- TX STBC
- RX STBC 1-stream
- Max AMSDU length: 7935 bytes
- DSSS/CCK HT40
- Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
- Minimum RX AMPDU time spacing: 4 usec (0x05)
- HT RX MCS rate indexes supported: 0-23
- HT TX MCS rate indexes are undefined
- HT operation:
- * primary channel: 6
- * secondary channel offset: no secondary
- * STA channel width: 20 MHz
- * RIFS: 1
- * HT protection: nonmember
- * non-GF present: 1
- * OBSS non-GF present: 1
- * dual beacon: 0
- * dual CTS protection: 0
- * STBC beacon: 0
- * L-SIG TXOP Prot: 0
- * PCO active: 0
- * PCO phase: 0
- Overlapping BSS scan params:
- * passive dwell: 20 TUs
- * active dwell: 10 TUs
- * channel width trigger scan interval: 300 s
- * scan passive total per channel: 200 TUs
- * scan active total per channel: 20 TUs
- * BSS width channel transition delay factor: 5
- * OBSS Scan Activity Threshold: 0.25 %
- Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
- WMM: * Parameter version 1
- * u-APSD
- * BE: CW 15-1023, AIFSN 3
- * BK: CW 15-1023, AIFSN 7
- * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
- * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
-
-BSS 94:b4:0f:f1:36:41(on wcli0)
- TSF: 12499149351 usec (0d, 03:28:19)
- freq: 2437
- beacon interval: 100 TUs
- capability: ESS ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1521)
- signal: -67.00 dBm
- last seen: 80 ms ago
- Information elements from Probe Response frame:
- SSID: GoogleGuest
- Supported rates: 36.0* 48.0 54.0
- DS Parameter set: channel 6
- Country: US Environment: Indoor/Outdoor
- Channels [1 - 11] @ 36 dBm
- Power constraint: 0 dB
- TPC report: TX power: 3 dBm
- ERP: <no flags>
- BSS Load:
- * station count: 1
- * channel utilisation: 28/255
- * available admission capacity: 27500 [*32us]
- HT capabilities:
- Capabilities: 0x19ad
- RX LDPC
- HT20
- SM Power Save disabled
- RX HT20 SGI
- TX STBC
- RX STBC 1-stream
- Max AMSDU length: 7935 bytes
- DSSS/CCK HT40
- Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
- Minimum RX AMPDU time spacing: 4 usec (0x05)
- HT RX MCS rate indexes supported: 0-23
- HT TX MCS rate indexes are undefined
- HT operation:
- * primary channel: 6
- * secondary channel offset: no secondary
- * STA channel width: 20 MHz
- * RIFS: 1
- * HT protection: nonmember
- * non-GF present: 1
- * OBSS non-GF present: 1
- * dual beacon: 0
- * dual CTS protection: 0
- * STBC beacon: 0
- * L-SIG TXOP Prot: 0
- * PCO active: 0
- * PCO phase: 0
- Overlapping BSS scan params:
- * passive dwell: 20 TUs
- * active dwell: 10 TUs
- * channel width trigger scan interval: 300 s
- * scan passive total per channel: 200 TUs
- * scan active total per channel: 20 TUs
- * BSS width channel transition delay factor: 5
- * OBSS Scan Activity Threshold: 0.25 %
- Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
- WMM: * Parameter version 1
- * u-APSD
- * BE: CW 15-1023, AIFSN 3
- * BK: CW 15-1023, AIFSN 7
- * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
- * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
-BSS 94:b4:0f:f1:36:40(on wcli0)
- TSF: 12499150000 usec (0d, 03:28:19)
- freq: 2437
- beacon interval: 100 TUs
- capability: ESS Privacy ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1531)
- signal: -66.00 dBm
- last seen: 2350 ms ago
- Information elements from Probe Response frame:
- SSID: Google
- Supported rates: 36.0* 48.0 54.0
- DS Parameter set: channel 6
- TIM: DTIM Count 0 DTIM Period 1 Bitmap Control 0x0 Bitmap[0] 0x0
- Country: US Environment: Indoor/Outdoor
- Channels [1 - 11] @ 36 dBm
- Power constraint: 0 dB
- TPC report: TX power: 3 dBm
- ERP: <no flags>
- RSN: * Version: 1
- * Group cipher: CCMP
- * Pairwise ciphers: CCMP
- * Authentication suites: IEEE 802.1X
- * Capabilities: 4-PTKSA-RC 4-GTKSA-RC (0x0028)
- BSS Load:
- * station count: 0
- * channel utilisation: 28/255
- * available admission capacity: 27500 [*32us]
- HT capabilities:
- Capabilities: 0x19ad
- RX LDPC
- HT20
- SM Power Save disabled
- RX HT20 SGI
- TX STBC
- RX STBC 1-stream
- Max AMSDU length: 7935 bytes
- DSSS/CCK HT40
- Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
- Minimum RX AMPDU time spacing: 4 usec (0x05)
- HT RX MCS rate indexes supported: 0-23
- HT TX MCS rate indexes are undefined
- HT operation:
- * primary channel: 6
- * secondary channel offset: no secondary
- * STA channel width: 20 MHz
- * RIFS: 1
- * HT protection: nonmember
- * non-GF present: 1
- * OBSS non-GF present: 1
- * dual beacon: 0
- * dual CTS protection: 0
- * STBC beacon: 0
- * L-SIG TXOP Prot: 0
- * PCO active: 0
- * PCO phase: 0
- Overlapping BSS scan params:
- * passive dwell: 20 TUs
- * active dwell: 10 TUs
- * channel width trigger scan interval: 300 s
- * scan passive total per channel: 200 TUs
- * scan active total per channel: 20 TUs
- * BSS width channel transition delay factor: 5
- * OBSS Scan Activity Threshold: 0.25 %
- Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
- WMM: * Parameter version 1
- * u-APSD
- * BE: CW 15-1023, AIFSN 3
- * BK: CW 15-1023, AIFSN 7
- * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
- * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
-BSS 94:b4:0f:f1:36:42(on wcli0)
- TSF: 12499150000 usec (0d, 03:28:19)
- freq: 2437
- beacon interval: 100 TUs
- capability: ESS Privacy ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1531)
- signal: -66.00 dBm
- last seen: 2350 ms ago
- Information elements from Probe Response frame:
- SSID:
- Supported rates: 36.0* 48.0 54.0
- DS Parameter set: channel 6
- TIM: DTIM Count 0 DTIM Period 1 Bitmap Control 0x0 Bitmap[0] 0x0
- Country: US Environment: Indoor/Outdoor
- Channels [1 - 11] @ 36 dBm
- Power constraint: 0 dB
- TPC report: TX power: 3 dBm
- ERP: <no flags>
- BSS Load:
- * station count: 0
- * channel utilisation: 28/255
- * available admission capacity: 27500 [*32us]
- HT capabilities:
- Capabilities: 0x19ad
- RX LDPC
- HT20
- SM Power Save disabled
- RX HT20 SGI
- TX STBC
- RX STBC 1-stream
- Max AMSDU length: 7935 bytes
- DSSS/CCK HT40
- Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
- Minimum RX AMPDU time spacing: 4 usec (0x05)
- HT RX MCS rate indexes supported: 0-23
- HT TX MCS rate indexes are undefined
- HT operation:
- * primary channel: 6
- * secondary channel offset: no secondary
- * STA channel width: 20 MHz
- * RIFS: 1
- * HT protection: nonmember
- * non-GF present: 1
- * OBSS non-GF present: 1
- * dual beacon: 0
- * dual CTS protection: 0
- * STBC beacon: 0
- * L-SIG TXOP Prot: 0
- * PCO active: 0
- * PCO phase: 0
- Overlapping BSS scan params:
- * passive dwell: 20 TUs
- * active dwell: 10 TUs
- * channel width trigger scan interval: 300 s
- * scan passive total per channel: 200 TUs
- * scan active total per channel: 20 TUs
- * BSS width channel transition delay factor: 5
- * OBSS Scan Activity Threshold: 0.25 %
- Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
- WMM: * Parameter version 1
- * u-APSD
- * BE: CW 15-1023, AIFSN 3
- * BK: CW 15-1023, AIFSN 7
- * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
- * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
- Vendor specific: OUI 00:11:22, data: 01 23 45 67
- Vendor specific: OUI f4:f5:e8, data: 01
- Vendor specific: OUI f4:f5:e8, data: 03 47 46 69 62 65 72 53 65 74 75 70 41 75 74 6f 6d 61 74 69 6f 6e
-BSS 00:1a:11:f1:36:43(on wcli0)
- TSF: 12499150000 usec (0d, 03:28:19)
- freq: 2437
- beacon interval: 100 TUs
- capability: ESS Privacy ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1531)
- signal: -66.00 dBm
- last seen: 2350 ms ago
- Information elements from Probe Response frame:
- SSID:
- Supported rates: 36.0* 48.0 54.0
- DS Parameter set: channel 6
- TIM: DTIM Count 0 DTIM Period 1 Bitmap Control 0x0 Bitmap[0] 0x0
- Country: US Environment: Indoor/Outdoor
- Channels [1 - 11] @ 36 dBm
- Power constraint: 0 dB
- TPC report: TX power: 3 dBm
- ERP: <no flags>
- BSS Load:
- * station count: 0
- * channel utilisation: 28/255
- * available admission capacity: 27500 [*32us]
- HT capabilities:
- Capabilities: 0x19ad
- RX LDPC
- HT20
- SM Power Save disabled
- RX HT20 SGI
- TX STBC
- RX STBC 1-stream
- Max AMSDU length: 7935 bytes
- DSSS/CCK HT40
- Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
- Minimum RX AMPDU time spacing: 4 usec (0x05)
- HT RX MCS rate indexes supported: 0-23
- HT TX MCS rate indexes are undefined
- HT operation:
- * primary channel: 6
- * secondary channel offset: no secondary
- * STA channel width: 20 MHz
- * RIFS: 1
- * HT protection: nonmember
- * non-GF present: 1
- * OBSS non-GF present: 1
- * dual beacon: 0
- * dual CTS protection: 0
- * STBC beacon: 0
- * L-SIG TXOP Prot: 0
- * PCO active: 0
- * PCO phase: 0
- Overlapping BSS scan params:
- * passive dwell: 20 TUs
- * active dwell: 10 TUs
- * channel width trigger scan interval: 300 s
- * scan passive total per channel: 200 TUs
- * scan active total per channel: 20 TUs
- * BSS width channel transition delay factor: 5
- * OBSS Scan Activity Threshold: 0.25 %
- Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
- WMM: * Parameter version 1
- * u-APSD
- * BE: CW 15-1023, AIFSN 3
- * BK: CW 15-1023, AIFSN 7
- * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
- * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
-"""
-
-
-# pylint: disable=unused-argument,protected-access
-def fake_scan(*args, **kwargs):
- return SCAN_OUTPUT
-iw._scan = fake_scan
+SCAN_RESULTS = (
+ {'rssi': -60, 'ssid': 'short scan result', 'bssid': '00:23:97:57:f4:d8',
+ 'vendor_ies': [('00:11:22', '01 23 45 67')], 'security': 'WEP'},
+ {'rssi': -54, 'ssid': 'Google', 'bssid': '94:b4:0f:f1:02:a0',
+ 'vendor_ies': [], 'security': 'WPA2'},
+ {'rssi': -39, 'ssid': 'Google', 'bssid': '94:b4:0f:f1:35:60',
+ 'vendor_ies': [], 'security': 'WPA2'},
+ {'rssi': -38, 'ssid': 'GoogleGuest', 'bssid': '94:b4:0f:f1:35:61',
+ 'vendor_ies': [], 'security': None},
+ {'rssi': -55, 'ssid': 'Google', 'bssid': '94:b4:0f:f1:3a:e0',
+ 'vendor_ies': [], 'security': 'WPA2'},
+ {'rssi': -65, 'ssid': 'GoogleGuest', 'bssid': '94:b4:0f:f1:3a:e1',
+ 'vendor_ies': [], 'security': None},
+ {'rssi': -67, 'ssid': 'GoogleGuest', 'bssid': '94:b4:0f:f1:36:41',
+ 'vendor_ies': [], 'security': None},
+ {'rssi': -66, 'ssid': 'Google', 'bssid': '94:b4:0f:f1:36:40',
+ 'vendor_ies': [], 'security': 'WPA2'},
+ {'rssi': -66, 'ssid': '', 'bssid': '94:b4:0f:f1:36:42',
+ 'vendor_ies': [('00:11:22', '01 23 45 67'), ('f4:f5:e8', '01'),
+ ('f4:f5:e8', '03 47 46 69 62 65 72 53 65 74 75 70 41 75 74 '
+ '6f 6d 61 74 69 6f 6e')], 'security': None},
+ {'rssi': -66, 'ssid': '', 'bssid': '00:1a:11:f1:36:43',
+ 'vendor_ies': [], 'security': None},
+)
@wvtest.wvtest
def find_bssids_test():
"""Test iw.find_bssids."""
+ subprocess.mock('wifi', 'interfaces',
+ subprocess.wifi.MockInterface(phynum='0', bands=['2.4', '5'],
+ driver='cfg80211'))
+ subprocess.call(['ifup', 'wcli0'])
+ for scan_result in SCAN_RESULTS:
+ subprocess.mock('wifi', 'remote_ap', band='5', **scan_result)
+
test_ie = ('00:11:22', '01 23 45 67')
provisioning_ie = ('f4:f5:e8', '01')
ssid_ie = (
@@ -637,48 +52,51 @@
)
short_scan_result = iw.BssInfo(ssid='short scan result',
bssid='00:23:97:57:f4:d8',
+ band='5',
rssi=-60,
security=['WEP'],
vendor_ies=[test_ie])
provisioning_bss_info = iw.BssInfo(ssid=iw.DEFAULT_GFIBERSETUP_SSID,
bssid='94:b4:0f:f1:36:42',
+ band='5',
rssi=-66,
vendor_ies=[test_ie, provisioning_ie,
ssid_ie])
provisioning_bss_info_frenzy = iw.BssInfo(ssid=iw.DEFAULT_GFIBERSETUP_SSID,
bssid='00:1a:11:f1:36:43',
+ band='5',
rssi=-66)
wvtest.WVPASSEQ(
- set(iw.find_bssids('wcli0', True)),
+ set(iw.find_bssids('2.4', True)),
set([(short_scan_result, 2.4),
(provisioning_bss_info, 5.34),
(provisioning_bss_info_frenzy, 4.34),
- (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:36:41', rssi=-67),
- 2.33),
- (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:3a:e1', rssi=-65),
- 2.35),
- (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:35:61', rssi=-38),
- 2.62),
- (iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:36:40', rssi=-66,
- security=['WPA2']), 2.34),
- (iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:3a:e0', rssi=-55,
- security=['WPA2']), 2.45),
- (iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:35:60', rssi=-39,
- security=['WPA2']), 2.61),
- (iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:02:a0', rssi=-54,
- security=['WPA2']), 2.46)]))
+ (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:36:41',
+ band='5', rssi=-67), 2.33),
+ (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:3a:e1',
+ band='5', rssi=-65), 2.35),
+ (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:35:61',
+ band='5', rssi=-38), 2.62),
+ (iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:36:40', band='5',
+ rssi=-66, security=['WPA2']), 2.34),
+ (iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:3a:e0', band='5',
+ rssi=-55, security=['WPA2']), 2.45),
+ (iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:35:60', band='5',
+ rssi=-39, security=['WPA2']), 2.61),
+ (iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:02:a0', band='5',
+ rssi=-54, security=['WPA2']), 2.46)]))
wvtest.WVPASSEQ(
- set(iw.find_bssids('wcli0', False)),
+ set(iw.find_bssids('2.4', False)),
set([(provisioning_bss_info, 5.34),
(provisioning_bss_info_frenzy, 4.34),
- (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:36:41', rssi=-67),
- 2.33),
- (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:3a:e1', rssi=-65),
- 2.35),
- (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:35:61', rssi=-38),
- 2.62)]))
+ (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:36:41', band='5',
+ rssi=-67), 2.33),
+ (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:3a:e1', band='5',
+ rssi=-65), 2.35),
+ (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:35:61', band='5',
+ rssi=-38), 2.62)]))
if __name__ == '__main__':
wvtest.wvtest_main()
diff --git a/conman/ratchet.py b/conman/ratchet.py
index 07e61a8..61e8705 100644
--- a/conman/ratchet.py
+++ b/conman/ratchet.py
@@ -15,6 +15,7 @@
except AttributeError:
_gettime = time.time
+
# This has to be called before another module calls it with a higher log level.
# pylint: disable=g-import-not-at-top
logging.basicConfig(level=logging.DEBUG)
@@ -69,7 +70,7 @@
if now > self.start_at + self.timeout:
self.timed_out = True
self.logger.info('%s timed out after %.2f seconds',
- self.name, now - self.t0)
+ self.name, now - self.start_at)
raise TimeoutException()
self.not_done_before = _gettime()
@@ -81,7 +82,7 @@
self.done_after = self.not_done_before
self.done_by = _gettime()
self.logger.info('%s completed after %.2f seconds',
- self.name, self.done_by - self.t0)
+ self.name, self.done_by - self.start_at)
if self.callback:
self.callback()
@@ -90,47 +91,35 @@
class FileExistsCondition(Condition):
"""A condition that checks for the existence of a file."""
- def __init__(self, name, filename, timeout):
- self._filename = filename
+ def __init__(self, name, filepath, timeout):
+ self._filepath = filepath
super(FileExistsCondition, self).__init__(name, None, timeout)
def evaluate(self):
- return os.path.exists(self._filename)
-
- def mtime(self):
- if os.path.exists(self._filename):
- return os.stat(self._filename).st_mtime
-
- return None
-
- def mark_done(self):
- super(FileExistsCondition, self).mark_done()
- # We have to check this because the file could have been deleted while this
- # was being called. But this condition should almost always be true.
- mtime = self.mtime()
- if mtime:
- self.done_after = self.done_by = mtime
+ return os.path.exists(self._filepath)
class FileTouchedCondition(FileExistsCondition):
- """A condition that checks that a file was touched after a certain time."""
+ """A condition that checks that a file is touched.
- def reset(self, t0=None, start_at=None):
- mtime = self.mtime
- if t0 and mtime and mtime < t0:
- self.initial_mtime = self.mtime()
- else:
- self.initial_mtime = None
- super(FileTouchedCondition, self).reset(t0, start_at)
+ Because the clock may be adjusted, we can't compare the file's mtime to a
+ timestamp. So just look for mtime changes instead. This means that t0 and
+ start_at aren't respected; instead, look for touches after whenever the
+ FileTouchedCondition is reset.
+ """
+
+ def reset(self, *args, **kwargs):
+ super(FileTouchedCondition, self).reset(*args, **kwargs)
+ self.initial_mtime = self.mtime()
def evaluate(self):
if not super(FileTouchedCondition, self).evaluate():
return False
+ return self.mtime() != self.initial_mtime
- if self.initial_mtime:
- return self.mtime() > self.initial_mtime
-
- return self.mtime() >= self.t0
+ def mtime(self):
+ if os.path.exists(self._filepath):
+ return os.stat(self._filepath).st_mtime
class Ratchet(Condition):
@@ -146,6 +135,7 @@
def reset(self):
self._current_step = 0
+ self.active = False
for step in self.steps:
step.reset()
self._set_step_status(step, False)
@@ -153,11 +143,18 @@
def start(self):
self.reset()
+ self.active = True
self._set_current_step_status(True)
+ def stop(self):
+ self.active = False
+
# Override check rather than evaluate because we don't want the Ratchet to
# time out unless one of its steps does.
def check(self):
+ if not self.active:
+ return
+
if not self.done_after:
while self.current_step().check():
if not self.advance():
diff --git a/conman/ratchet_test.py b/conman/ratchet_test.py
index 97f7c94..48b693c 100755
--- a/conman/ratchet_test.py
+++ b/conman/ratchet_test.py
@@ -45,38 +45,29 @@
@wvtest.wvtest
def file_condition_test():
"""Test File*Condition functionality."""
- try:
- _, filename = tempfile.mkstemp()
- c_exists = ratchet.FileExistsCondition('c exists', filename, 0.1)
- c_mtime = ratchet.FileTouchedCondition('c mtime', filename, 0.1)
- wvtest.WVPASS(c_exists.check())
- wvtest.WVFAIL(c_mtime.check())
- # mtime precision is too low to notice that we're touching the file *after*
- # capturing its initial mtime rather than at the same time, so take a short
- # nap before touching it.
- time.sleep(0.01)
- open(filename, 'w')
- wvtest.WVPASS(c_mtime.check())
+ _, filename = tempfile.mkstemp()
+ c_exists = ratchet.FileExistsCondition('c exists', filename, 0.1)
+ c_touched = ratchet.FileTouchedCondition('c touched', filename, 0.1)
+ wvtest.WVPASS(c_exists.check())
+ wvtest.WVFAIL(c_touched.check())
+ # File mtime resolution isn't fine enough to see the difference between this
+ # write and the previous one, so sleep for a short time before writing to
+ # ensure a different mtime.
+ time.sleep(0.01)
+ open(filename, 'w')
+ wvtest.WVPASS(c_touched.check())
- # Test that old mtimes don't count.
- time.sleep(0.01)
- c_mtime.reset()
- wvtest.WVFAIL(c_mtime.check())
- time.sleep(0.1)
- wvtest.WVEXCEPT(ratchet.TimeoutException, c_mtime.check)
+ # Test that pre-existing files don't count.
+ c_touched.reset()
+ wvtest.WVFAIL(c_touched.check())
+ time.sleep(0.1)
+ wvtest.WVEXCEPT(ratchet.TimeoutException, c_touched.check)
- # Test t0 and start_at.
- os.unlink(filename)
- now = time.time()
- c_mtime.reset(t0=now, start_at=now + 0.2)
- wvtest.WVFAIL(c_mtime.check())
- time.sleep(0.15)
- wvtest.WVFAIL(c_mtime.check())
- open(filename, 'w')
- wvtest.WVPASS(c_mtime.check())
-
- finally:
- os.unlink(filename)
+ # Test that deleting files doesn't count.
+ c_touched.reset()
+ wvtest.WVFAIL(c_touched.check())
+ os.unlink(filename)
+ wvtest.WVFAIL(c_touched.check())
@wvtest.wvtest
@@ -122,7 +113,7 @@
wvtest.WVEXCEPT(ratchet.TimeoutException, r.check)
x = y = z = 1
- r.reset()
+ r.start()
wvtest.WVPASS(r.check())
finally:
shutil.rmtree(status_export_path)
diff --git a/conman/status.py b/conman/status.py
index 8f8d3a1..b5b1bae 100644
--- a/conman/status.py
+++ b/conman/status.py
@@ -30,10 +30,10 @@
COULD_REACH_ACS = 'COULD_REACH_ACS'
CAN_REACH_INTERNET = 'CAN_REACH_INTERNET'
PROVISIONING_FAILED = 'PROVISIONING_FAILED'
- ATTACHED_TO_WPA_SUPPLICANT = 'ATTACHED_TO_WPA_SUPPLICANT'
WAITING_FOR_PROVISIONING = 'WAITING_FOR_PROVISIONING'
WAITING_FOR_DHCP = 'WAITING_FOR_DHCP'
+ ACS_CONNECTION_CHECK = 'ACS_CONNECTION_CHECK'
WAITING_FOR_CWMP_WAKEUP = 'WAITING_FOR_CWMP_WAKEUP'
WAITING_FOR_ACS_SESSION = 'WAITING_FOR_ACS_SESSION'
PROVISIONING_COMPLETED = 'PROVISIONING_COMPLETED'
@@ -81,10 +81,6 @@
(P.HAVE_CONFIG,),
(),
),
- P.ATTACHED_TO_WPA_SUPPLICANT: (
- (),
- (),
- ),
P.WAITING_FOR_PROVISIONING: (
(P.CONNECTED_TO_OPEN,),
(),
@@ -93,13 +89,18 @@
(P.WAITING_FOR_PROVISIONING,),
(P.WAITING_FOR_CWMP_WAKEUP, P.WAITING_FOR_ACS_SESSION),
),
+ P.ACS_CONNECTION_CHECK: (
+ (P.WAITING_FOR_PROVISIONING,),
+ (P.WAITING_FOR_DHCP, P.WAITING_FOR_CWMP_WAKEUP,
+ P.WAITING_FOR_ACS_SESSION),
+ ),
P.WAITING_FOR_CWMP_WAKEUP: (
(P.WAITING_FOR_PROVISIONING,),
- (P.WAITING_FOR_DHCP, P.WAITING_FOR_ACS_SESSION),
+ (P.WAITING_FOR_DHCP, P.ACS_CONNECTION_CHECK, P.WAITING_FOR_ACS_SESSION),
),
P.WAITING_FOR_ACS_SESSION: (
(P.WAITING_FOR_PROVISIONING,),
- (P.WAITING_FOR_DHCP, P.WAITING_FOR_CWMP_WAKEUP),
+ (P.WAITING_FOR_DHCP, P.ACS_CONNECTION_CHECK, P.WAITING_FOR_CWMP_WAKEUP),
),
P.PROVISIONING_COMPLETED: (
(),
diff --git a/conman/status_test.py b/conman/status_test.py
index befebbf..38dda83 100755
--- a/conman/status_test.py
+++ b/conman/status_test.py
@@ -137,8 +137,11 @@
check_exported(True, False, status.P.CONNECTED_TO_OPEN)
check_exported(True, False, status.P.WAITING_FOR_PROVISIONING)
check_exported(True, False, status.P.WAITING_FOR_DHCP)
- s.waiting_for_cwmp_wakeup = True
+ s.acs_connection_check = True
check_exported(False, False, status.P.WAITING_FOR_DHCP)
+ check_exported(True, False, status.P.ACS_CONNECTION_CHECK)
+ s.waiting_for_cwmp_wakeup = True
+ check_exported(False, False, status.P.ACS_CONNECTION_CHECK)
check_exported(True, False, status.P.WAITING_FOR_CWMP_WAKEUP)
s.waiting_for_acs_session = True
check_exported(False, False, status.P.WAITING_FOR_DHCP)
@@ -147,6 +150,7 @@
s.provisioning_completed = True
check_exported(False, False, status.P.WAITING_FOR_PROVISIONING)
check_exported(False, False, status.P.WAITING_FOR_DHCP)
+ check_exported(False, False, status.P.ACS_CONNECTION_CHECK)
check_exported(False, False, status.P.WAITING_FOR_CWMP_WAKEUP)
check_exported(False, False, status.P.WAITING_FOR_CWMP_WAKEUP)
diff --git a/conman/test/fail b/conman/test/fail
deleted file mode 100755
index 2bb8d86..0000000
--- a/conman/test/fail
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-
-exit 1
diff --git a/conman/test/fake_python/subprocess/__init__.py b/conman/test/fake_python/subprocess/__init__.py
new file mode 100644
index 0000000..3d73d4d
--- /dev/null
+++ b/conman/test/fake_python/subprocess/__init__.py
@@ -0,0 +1,149 @@
+#!/usr/bin/python
+
+"""subprocess replacement that implements specific programs in Python."""
+
+import importlib
+import logging
+import os
+import types
+
+logger = logging.getLogger('subprocess')
+logger.setLevel(logging.DEBUG)
+
+
+# Values are only for when the module name does not match the command name.
+_COMMAND_NAMES = {
+ 'connection_check': None,
+ 'cwmp': None,
+ 'get_quantenna_interfaces': 'get-quantenna-interfaces',
+ 'ifdown': None,
+ 'ifplugd_action': '/etc/ifplugd/ifplugd.action',
+ 'ifup': None,
+ 'ip': None,
+ 'register_experiment': None,
+ 'run_dhclient': 'run-dhclient',
+ 'qcsapi': None,
+ 'upload_logs_and_wait': 'upload-logs-and-wait',
+ 'wifi': None,
+ 'wpa_cli': None,
+}
+_COMMANDS = {v or k: importlib.import_module('.' + k, __name__)
+ for k, v in _COMMAND_NAMES.iteritems()}
+
+STDOUT = 1
+STDERR = 2
+
+
+class CalledProcessError(Exception):
+
+ def __init__(self, returncode, cmd, output):
+ super(CalledProcessError, self).__init__()
+ self.returncode = returncode
+ self.cmd = cmd
+ self.output = output
+
+ def __repr__(self):
+ return ('CalledProcessError: '
+ 'Command "%r" returned non-zero exit status %d: %s'
+ % (self.cmd, self.returncode, self.output))
+
+
+def _call(command, **kwargs):
+ """Fake subprocess call."""
+ if type(command) not in (tuple, list):
+ raise Exception('Fake subprocess.call only supports list/tuple commands, '
+ 'got: %s', command)
+
+ ignored_kwargs = ('stdout', 'stderr')
+ for ignored_kwarg in ignored_kwargs:
+ kwargs.pop(ignored_kwarg, None)
+ extra_env = kwargs.pop('env', {})
+ if kwargs:
+ raise Exception('Fake subprocess.call does not support these kwargs: %s'
+ % kwargs.keys())
+
+ logger.debug('%r%s', command, (', env %r' % extra_env) if extra_env else '')
+
+ command, args = command[0], command[1:]
+
+ if command not in _COMMANDS:
+ raise Exception('Fake subprocess.call does not support %r, supports %r' %
+ (command, _COMMANDS.keys()))
+
+ impl = _COMMANDS[command]
+ if isinstance(impl, types.ModuleType):
+ impl = impl.call
+
+ forwarded_kwargs = {}
+ if extra_env:
+ forwarded_kwargs['env'] = extra_env
+ return impl(*args, **forwarded_kwargs)
+
+
+def call(command, **kwargs):
+ rc, _ = _call(command, **kwargs)
+ return rc
+
+
+def check_call(command, **kwargs):
+ rc, output = _call(command, **kwargs)
+ if rc:
+ raise CalledProcessError(rc, command, output)
+ return True
+
+
+def check_output(command, **kwargs):
+ rc, output = _call(command, **kwargs)
+ if rc != 0:
+ raise CalledProcessError(rc, command, output)
+ return output
+
+
+def mock(command, *args, **kwargs):
+ _COMMANDS[command].mock(*args, **kwargs)
+
+
+def reset():
+ """Reset any module-level state."""
+ for command in _COMMANDS.itervalues():
+ if isinstance(command, types.ModuleType):
+ reload(command)
+
+
+def set_conman_paths(tmp_path=None, config_path=None, cwmp_path=None):
+ for command in ('run-dhclient', '/etc/ifplugd/ifplugd.action'):
+ _COMMANDS[command].CONMAN_PATH = tmp_path
+
+ for command in ('cwmp',):
+ _COMMANDS[command].CONMAN_CONFIG_PATH = config_path
+
+ for command in ('cwmp',):
+ _COMMANDS[command].CWMP_PATH = cwmp_path
+
+ # Make sure <tmp_path>/interfaces exists.
+ tmp_interfaces_path = os.path.join(tmp_path, 'interfaces')
+ if not os.path.exists(tmp_interfaces_path):
+ os.mkdir(tmp_interfaces_path)
+
+
+# Some tiny fake implementations don't need their own file.
+
+
+def echo(*s):
+ return 0, ' '.join(s)
+
+
+def env(extra_env, *command, **kwargs):
+ final_env = kwargs.get('env', {})
+ k, v = extra_env.split('=')
+ final_env[k] = v
+ kwargs['env'] = final_env
+ return _call(command, **kwargs)
+
+
+def timeout(unused_t, *command, **kwargs):
+ """Just a transparent pass-through."""
+ return _call(command, **kwargs)
+
+
+_COMMANDS.update({'echo': echo, 'env': env, 'timeout': timeout,})
diff --git a/conman/test/fake_python/subprocess/connection_check.py b/conman/test/fake_python/subprocess/connection_check.py
new file mode 100644
index 0000000..8c23c50
--- /dev/null
+++ b/conman/test/fake_python/subprocess/connection_check.py
@@ -0,0 +1,19 @@
+#!/usr/bin/python
+
+"""Fake connection_check implementation."""
+
+RESULTS = {}
+
+
+def mock(interface, result):
+ RESULTS[interface] = result
+
+
+def call(*args):
+ interface = args[args.index('-I') + 1]
+ result = RESULTS.get(interface, 'fail')
+
+ if result == 'restricted' and '-a' in args:
+ result = 'succeed'
+
+ return (0 if result == 'succeed' else 1), ''
diff --git a/conman/test/fake_python/subprocess/cwmp.py b/conman/test/fake_python/subprocess/cwmp.py
new file mode 100644
index 0000000..f34cd2f
--- /dev/null
+++ b/conman/test/fake_python/subprocess/cwmp.py
@@ -0,0 +1,148 @@
+#!/usr/bin/python
+
+"""Fake catawampus implementation."""
+
+import logging
+import os
+
+import connection_check
+
+
+logger = logging.getLogger('subprocess.cwmp')
+
+CONMAN_CONFIG_PATH = None
+CWMP_PATH = None
+CONFIG = {}
+ACCESS_POINT = {}
+ACS_SESSION_FAILS = False
+
+
+def call(command, env=None):
+ if command == 'wakeup':
+ if not CONMAN_CONFIG_PATH:
+ raise ValueError('Call subprocess.set_conman_paths before calling '
+ '"cwmp wakeup".')
+
+ write_acscontact()
+
+ if ACS_SESSION_FAILS:
+ return 0, ''
+
+ if ((env and 'write_now_testonly' in env) or
+ [result for result in connection_check.RESULTS.itervalues()
+ if result in ('restricted', 'succeed')]):
+ for band in ('2.4', '5'):
+ if CONFIG.get(band, None):
+ write_wlan_config(band)
+ else:
+ delete_wlan_config(band)
+ disable_access_point(band)
+
+ if ACCESS_POINT.get(band, False):
+ enable_access_point(band)
+ else:
+ disable_access_point(band)
+
+ logger.debug('Fake ACS session completing')
+ write_acsconnected()
+ else:
+ logger.debug('ACS session failed due to no working connections')
+
+ return 0, ''
+
+ raise ValueError('Fake cwmp only supports "wakeup" command.')
+
+
+def wlan_config_filename(band):
+ return os.path.join(CONMAN_CONFIG_PATH, 'command.%s' % band)
+
+
+def access_point_filename(band):
+ return os.path.join(CONMAN_CONFIG_PATH, 'access_point.%s' % band)
+
+
+def write_wlan_config(band):
+ final_filename = wlan_config_filename(band)
+ logger.debug('Writing config for band %s: %s', band, final_filename)
+ # We don't care which writes are atomic, as long as some but not all are.
+ # Making it depend on band achieves this.
+ atomic = band == '2.4'
+ filename = final_filename + ('.tmp' if atomic else '')
+ with open(filename, 'w') as f:
+ f.write('\n'.join(['env', 'WIFI_PSK=%s' % CONFIG[band]['psk'],
+ 'wifi', 'set', '--band', band,
+ '--ssid', CONFIG[band]['ssid']]))
+ logger.debug( 'wrote to filename %s', filename)
+ if atomic:
+ logger.debug( 'moving from %s to %s', filename, final_filename)
+ os.rename(filename, final_filename)
+
+
+def enable_access_point(band):
+ logger.debug('Enabling AP for band %s', band)
+ open(access_point_filename(band), 'w')
+
+
+def delete_wlan_config(band):
+ config_filename = wlan_config_filename(band)
+ if os.path.exists(config_filename):
+ logger.debug('Deleting config for band %s', band)
+ os.unlink(config_filename)
+
+
+def disable_access_point(band):
+ ap_filename = access_point_filename(band)
+ if os.path.isfile(ap_filename):
+ logger.debug('Disabling AP for band %s', band)
+ os.unlink(ap_filename)
+
+
+def write_acscontact():
+ logger.debug('ACS session started')
+ open(os.path.join(CWMP_PATH, 'acscontact'), 'w')
+
+
+def write_acsconnected():
+ logger.debug('ACS session completed')
+ open(os.path.join(CWMP_PATH, 'acsconnected'), 'w')
+
+
+def mock(band, access_point=None, delete_config=False, ssid=None, psk=None,
+ write_now=False, acs_session_fails=None):
+ """Mock the config written by catawampus.
+
+ Args:
+ band: The band for which things are being mocked.
+ access_point: Set to True or False to enable/disable the AP.
+ delete_config: Set to True to delete the config.
+ ssid: If updating config, the ssid to use. psk must also be set.
+ psk: If updating config, the psk to use. ssid must also be set.
+ write_now: If updating config, write it immediately.
+
+ Raises:
+ ValueError: If invalid values are specified.
+ """
+ if acs_session_fails is not None:
+ global ACS_SESSION_FAILS
+ ACS_SESSION_FAILS = acs_session_fails
+
+ if access_point is not None:
+ if access_point not in (True, False):
+ raise ValueError('access_point should only be mocked as True/False')
+ ACCESS_POINT[band] = access_point
+ logger.debug('AP mocked %s', access_point)
+
+ if delete_config:
+ logger.debug('Config mock removed for band %s', band)
+ CONFIG[band] = None
+ elif ssid and psk:
+ logger.debug('Config mock updated for band %s', band)
+ CONFIG[band] = {'ssid': ssid, 'psk': psk}
+ elif ssid or psk:
+ raise ValueError('Cannot set only one of ssid (%r) and psk (%r).',
+ ssid, psk)
+
+ if write_now:
+ call('wakeup', env={'write_now_testonly': True})
+
+
diff --git a/conman/test/fake_python/subprocess/get_quantenna_interfaces.py b/conman/test/fake_python/subprocess/get_quantenna_interfaces.py
new file mode 100644
index 0000000..7316139
--- /dev/null
+++ b/conman/test/fake_python/subprocess/get_quantenna_interfaces.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python -S
+
+"""Fake get-quantenna-interfaces implementation."""
+
+_INTERFACES = []
+
+
+def call(*unused_args, **unused_kwargs):
+ return 0, '\n'.join(_INTERFACES)
+
+
+def mock(interfaces):
+ global _INTERFACES
+ _INTERFACES = list(interfaces)
diff --git a/conman/test/fake_python/subprocess/ifdown.py b/conman/test/fake_python/subprocess/ifdown.py
new file mode 100644
index 0000000..0677e4f
--- /dev/null
+++ b/conman/test/fake_python/subprocess/ifdown.py
@@ -0,0 +1,10 @@
+#!/usr/bin/python
+
+"""Fake ifdown implementation."""
+
+import ifup
+
+
+def call(interface):
+ ifup.INTERFACE_STATE[interface] = False
+ return 0, ''
diff --git a/conman/test/fake_python/subprocess/ifplugd_action.py b/conman/test/fake_python/subprocess/ifplugd_action.py
new file mode 100644
index 0000000..ab6a97a
--- /dev/null
+++ b/conman/test/fake_python/subprocess/ifplugd_action.py
@@ -0,0 +1,27 @@
+#!/usr/bin/python
+
+"""Fake ifplugd.action implementation."""
+
+import os
+
+import run_dhclient
+
+CONMAN_PATH = None
+
+
+def call(interface, state):
+ if CONMAN_PATH is None:
+ raise ValueError('Need to set subprocess.ifplugd_action.CONMAN_PATH')
+
+ if state not in ('up', 'down'):
+ raise ValueError('state should be "up" or "down"')
+
+ status_file = os.path.join(CONMAN_PATH, 'interfaces', interface)
+ with open(status_file, 'w') as f:
+ # This value doesn't matter to conman, so it's fine to hard code it here.
+ f.write('1' if state == 'up' else '0')
+
+ # ifplugd.action calls run-dhclient.
+ run_dhclient.call('br0' if interface in ('eth0', 'moca0') else interface)
+
+ return 0, ''
diff --git a/conman/test/fake_python/subprocess/ifup.py b/conman/test/fake_python/subprocess/ifup.py
new file mode 100644
index 0000000..7669555
--- /dev/null
+++ b/conman/test/fake_python/subprocess/ifup.py
@@ -0,0 +1,10 @@
+#!/usr/bin/python
+
+"""Fake ifup implementation."""
+
+INTERFACE_STATE = {}
+
+
+def call(interface):
+ INTERFACE_STATE[interface] = True
+ return 0, ''
diff --git a/conman/test/fake_python/subprocess/ip.py b/conman/test/fake_python/subprocess/ip.py
new file mode 100644
index 0000000..d8baaf3
--- /dev/null
+++ b/conman/test/fake_python/subprocess/ip.py
@@ -0,0 +1,136 @@
+#!/usr/bin/python
+
+"""Fake ip route implementation."""
+
+import logging
+import socket
+import struct
+
+import ifup
+
+
+_ROUTING_TABLE = {}
+_IP_TABLE = {}
+
+
+def call(subcommand, *args):
+ """Fake ip command."""
+ subcommands = {
+ 'route': _ip_route,
+ 'addr': _ip_addr,
+ 'link': _link,
+ }
+
+ if subcommand not in subcommands:
+ return 1, 'ip subcommand %r not supported' % subcommand
+
+ return subcommands[subcommand](args)
+
+
+def register_testonly(interface):
+ if interface not in _IP_TABLE:
+ _IP_TABLE[interface] = set()
+
+
+def _ip_route(args):
+ def can_add_route(dev):
+ def ip_to_int(ip_addr):
+ return struct.unpack('!I', socket.inet_pton(socket.AF_INET, ip_addr))[0]
+
+ if args[1] != 'default':
+ return True
+
+ via = ip_to_int(args[args.index('via') + 1])
+ for (ifc, route, _), _ in _ROUTING_TABLE.iteritems():
+ if ifc != dev:
+ continue
+
+ netmask = 0
+ if '/' in route:
+ route, netmask = route.split('/')
+ netmask = 32 - int(netmask)
+ route = ip_to_int(route)
+
+ if (route >> netmask) == (via >> netmask):
+ return True
+
+ return False
+
+ if not args:
+ return 0, '\n'.join(_ROUTING_TABLE.values())
+
+ if 'dev' not in args:
+ raise Exception('fake ip route got no dev')
+
+ dev = args[args.index('dev') + 1]
+
+ metric = None
+ if 'metric' in args:
+ metric = args[args.index('metric') + 1]
+ if args[0] in ('add', 'del'):
+ route = args[1]
+ key = (dev, route, metric)
+ if args[0] == 'add' and key not in _ROUTING_TABLE:
+ if not can_add_route(dev):
+ return (1, 'Tried to add default route without subnet route: %r' %
+ _ROUTING_TABLE)
+ logging.debug('Adding route for %r', key)
+ _ROUTING_TABLE[key] = ' '.join(args[1:])
+ elif args[0] == 'del':
+ if key in _ROUTING_TABLE:
+ logging.debug('Deleting route for %r', key)
+ del _ROUTING_TABLE[key]
+ elif key[2] is None:
+ # pylint: disable=g-builtin-op
+ for k in _ROUTING_TABLE.keys():
+ if k[:-1] == key[:-1]:
+ logging.debug('Deleting route for %r (generalized from %s)', k, key)
+ del _ROUTING_TABLE[k]
+ break
+
+ return 0, ''
+
+
+# pylint: disable=line-too-long
+_IP_ADDR_SHOW_TPL = """4: {name}: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
+ link/ether fe:fb:01:80:1b:74 brd ff:ff:ff:ff:ff:ff
+{ips}
+"""
+
+_IP_ADDR_SHOW_IP_TPL = """ inet {ip}/24 brd 100.100.255.255 scope global {name}
+ valid_lft forever preferred_lft forever
+"""
+
+
+def _ip_addr(args):
+ if 'dev' not in args:
+ raise Exception('fake ip addr show got no dev')
+
+ dev = args[args.index('dev') + 1]
+ if dev not in _IP_TABLE:
+ return 255, 'Device "%r" does not exist' % dev
+
+ if 'show' in args:
+ ips = '\n'.join(_IP_ADDR_SHOW_IP_TPL.format(name=dev, ip=addr)
+ for addr in _IP_TABLE[dev])
+ return 0, _IP_ADDR_SHOW_TPL.format(name=dev, ips=ips)
+
+ if 'add' in args:
+ add = args[args.index('add') + 1]
+ _IP_TABLE[dev].add(add)
+ return 0, ''
+
+ if 'del' in args:
+ remove = args[args.index('del') + 1]
+ if remove in _IP_TABLE[dev]:
+ _IP_TABLE[dev].remove(remove)
+ return 0, ''
+ return 254, 'RTNETLINK answers: Cannot assign requested address'
+
+ raise Exception('no recognized ip addr command in %r' % args)
+
+
+def _link(args):
+ return 0, '\n'.join('%s LOWER_UP' % interface
+ for interface, state in ifup.INTERFACE_STATE.iteritems()
+ if state)
diff --git a/conman/test/fake_python/subprocess/qcsapi.py b/conman/test/fake_python/subprocess/qcsapi.py
new file mode 100644
index 0000000..3625772
--- /dev/null
+++ b/conman/test/fake_python/subprocess/qcsapi.py
@@ -0,0 +1,25 @@
+#!/usr/bin/python -S
+
+"""Fake QCSAPI implementation."""
+
+
+STATE = {}
+
+
+def call(*args):
+ if args not in STATE:
+ return 1, 'No mocked value for args %r' % (args,)
+
+ return 0, STATE[args]
+
+
+def mock(*args, **kwargs):
+ import logging
+ if 'value' not in kwargs:
+ raise ValueError('Must specify value for mock qcsapi call %r' % args)
+ value = kwargs['value']
+ logging.debug ('qcsapi %r mocked: %r', args, value)
+ if value is None and args in STATE:
+ del STATE[args]
+ else:
+ STATE[args] = value
diff --git a/conman/test/fake_python/subprocess/register_experiment.py b/conman/test/fake_python/subprocess/register_experiment.py
new file mode 100644
index 0000000..a2dab49
--- /dev/null
+++ b/conman/test/fake_python/subprocess/register_experiment.py
@@ -0,0 +1,11 @@
+#!/usr/bin/python
+
+"""Fake register_experiment implementation."""
+
+
+REGISTERED_EXPERIMENTS = set()
+
+
+def call(experiment):
+ REGISTERED_EXPERIMENTS.add(experiment)
+ return 0, ''
diff --git a/conman/test/fake_python/subprocess/run_dhclient.py b/conman/test/fake_python/subprocess/run_dhclient.py
new file mode 100644
index 0000000..a1bffb3
--- /dev/null
+++ b/conman/test/fake_python/subprocess/run_dhclient.py
@@ -0,0 +1,36 @@
+#!/usr/bin/python
+
+"""Fake run-dhclient implementation."""
+
+import os
+
+
+CONMAN_PATH = None
+_FAILURE = {}
+
+
+def mock(interface, failure=False):
+ _FAILURE[interface] = failure
+
+
+def call(interface):
+ if CONMAN_PATH is None:
+ raise ValueError('Need to set subprocess.ifplugd_action.CONMAN_PATH')
+
+ if not _FAILURE.get(interface, False):
+ _write_subnet_file(interface)
+ _write_gateway_file(interface)
+
+
+def _write_gateway_file(interface):
+ gateway_file = os.path.join(CONMAN_PATH, 'gateway.' + interface)
+ with open(gateway_file, 'w') as f:
+ # This value doesn't matter to conman, so it's fine to hard code it here.
+ f.write('192.168.1.1')
+
+
+def _write_subnet_file(interface):
+ subnet_file = os.path.join(CONMAN_PATH, 'subnet.' + interface)
+ with open(subnet_file, 'w') as f:
+ # This value doesn't matter to conman, so it's fine to hard code it here.
+ f.write('192.168.1.0/24')
diff --git a/conman/test/fake_python/subprocess/upload_logs_and_wait.py b/conman/test/fake_python/subprocess/upload_logs_and_wait.py
new file mode 100644
index 0000000..6c45f87
--- /dev/null
+++ b/conman/test/fake_python/subprocess/upload_logs_and_wait.py
@@ -0,0 +1,18 @@
+#!/usr/bin/python
+
+"""Fake upload-logs-and-wait implementation."""
+
+UPLOADED = False
+
+
+def call():
+ global UPLOADED
+ UPLOADED = True
+ return 0, ''
+
+
+def uploaded_logs():
+ global UPLOADED
+ result = UPLOADED
+ UPLOADED = False
+ return result
diff --git a/conman/test/fake_python/subprocess/wifi.py b/conman/test/fake_python/subprocess/wifi.py
new file mode 100644
index 0000000..900b908
--- /dev/null
+++ b/conman/test/fake_python/subprocess/wifi.py
@@ -0,0 +1,331 @@
+#!/usr/bin/python
+
+"""Fake /bin/wifi implementation."""
+
+import collections
+import os
+import random
+
+import connection_check
+import get_quantenna_interfaces
+import ifplugd_action
+import ifup
+import qcsapi
+import wpa_cli
+
+
+MockInterface = collections.namedtuple('MockInterface',
+ ['phynum', 'bands', 'driver'])
+
+
+# A randomly selceted wifi scan result with the interesting stuff templated.
+WIFI_SCAN_TPL = '''BSS {bssid}(on wcli0)
+ TSF: 1269828266773 usec (14d, 16:43:48)
+ freq: {freq}
+ beacon interval: 100 TUs
+ capability: ESS Privacy ShortSlotTime (0x0411)
+ signal: {rssi}
+ last seen: 2190 ms ago
+ Information elements from Probe Response frame:
+ {vendor_ies}
+ SSID: {ssid}
+ Supported rates: 1.0* 2.0* 5.5* 11.0* 18.0 24.0 36.0 54.0
+ DS Parameter set: channel 6
+ ERP: <no flags>
+ ERP D4.0: <no flags>
+ {security}
+ Extended supported rates: 6.0 9.0 12.0 48.0
+'''
+
+VENDOR_IE_TPL = ' Vendor specific: OUI {oui}, data: {data}'
+
+
+WIFI_SHOW_TPL = '''Band: {band}
+RegDomain: US
+Interface: wlan{phynum} # {band} GHz ap
+BSSID: f4:f5:e8:81:1b:a0
+AutoChannel: True
+AutoType: NONDFS
+Station List for band: {band}
+
+Client Interface: wcli{phynum} # {band} GHz client
+Client BSSID: f4:f5:e8:81:1b:a1
+'''
+
+WIFI_SHOW_NO_RADIO_TPL = '''Band: {band}
+RegDomain: 00
+'''
+
+WPA_PATH = None
+REMOTE_ACCESS_POINTS = collections.defaultdict(dict)
+INTERFACE_FOR_BAND = collections.defaultdict(lambda: None)
+INTERFACE_EVENTS = collections.defaultdict(list)
+LOCAL_ACCESS_POINTS = {}
+CLIENT_ASSOCIATIONS = {}
+
+
+class AccessPoint(object):
+
+ def __init__(self, **kwargs):
+ self._attrs = ('ssid', 'psk', 'band', 'bssid', 'security', 'rssi',
+ 'vendor_ies', 'connection_check_result', 'hidden')
+ for attr in self._attrs:
+ setattr(self, attr, kwargs.get(attr, None))
+
+ def scan_str(self):
+ security_strs = {
+ 'WEP': ' Privacy: WEP',
+ 'WPA': ' WPA:',
+ 'WPA2': ' RSN: * Version: 1',
+ }
+ return WIFI_SCAN_TPL.format(
+ ssid=self.ssid if not self.hidden else '',
+ freq='2437' if self.band == '2.4' else '5160',
+ bssid=self.bssid,
+ vendor_ies='\n'.join(VENDOR_IE_TPL.format(oui=oui, data=data)
+ for oui, data in (self.vendor_ies or [])),
+ rssi='%.2f dBm' % (self.rssi or 0),
+ security=security_strs.get(self.security, ''))
+
+ def __str__(self):
+ return 'AccessPoint<%s>' % ' '.join('%s=%s' % (attr, getattr(self, attr))
+ for attr in self._attrs)
+
+ def __repr__(self):
+ return str(self)
+
+
+def call(*args, **kwargs):
+ wifi_commands = {
+ 'scan': _scan,
+ 'set': _set,
+ 'stopap': _stopap,
+ 'setclient': _setclient,
+ 'stopclient': _stopclient,
+ 'stop': _stop,
+ 'show': _show,
+ }
+
+ if WPA_PATH is None and args[0].endswith('client'):
+ raise ValueError('Set subprocess.wifi.WPA_PATH before calling a fake '
+ '"wifi *client" command')
+
+ if args[0] in wifi_commands:
+ return wifi_commands[args[0]](args[1:], env=kwargs.get('env', {}))
+
+ return 99, 'unrecognized command %s' % args[0]
+
+
+def _set(args, env=None):
+ band = _get_flag(args, ('-b', '--band'))
+ LOCAL_ACCESS_POINTS[band] = args, env
+ return 0, ''
+
+
+def _stopap(args, env=None):
+ bands = _get_flag(args, ('-b', '--band')) or '2.4 5'
+ for band in bands.split():
+ if band in LOCAL_ACCESS_POINTS:
+ del LOCAL_ACCESS_POINTS[band]
+
+ return 0, ''
+
+
+def _setclient(args, env=None):
+ env = env or {}
+
+ band = _get_flag(args, ('-b', '--band'))
+ bssid = _get_flag(args, ('--bssid',))
+ ssid = _get_flag(args, ('S', '--ssid',))
+
+ if band not in INTERFACE_FOR_BAND:
+ raise ValueError('No interface for band %r' % band)
+
+ interface = INTERFACE_FOR_BAND[band]
+ interface_name = 'wcli%s' % interface.phynum
+
+ if bssid:
+ ap = REMOTE_ACCESS_POINTS[band].get(bssid, None)
+ if not ap or ap.ssid != ssid:
+ _setclient_error_not_found(interface_name, ssid, interface.driver)
+ return 1, ('AP with band %r and BSSID %r and ssid %s not found: %s'
+ % (band, bssid, ssid, REMOTE_ACCESS_POINTS))
+ elif ssid:
+ candidates = [ap for ap in REMOTE_ACCESS_POINTS[band].itervalues()
+ if ap.ssid == ssid]
+ if not candidates:
+ _setclient_error_not_found(interface_name, ssid, interface.driver)
+ return 1, 'AP with SSID %r not found: %s' % (ssid, REMOTE_ACCESS_POINTS)
+ ap = random.choice(candidates)
+ else:
+ raise ValueError('Did not specify BSSID or SSID in %r' % args)
+
+ psk = env.get('WIFI_CLIENT_PSK', None)
+ if psk != ap.psk:
+ _setclient_error_auth(interface_name, ssid, interface.driver)
+ return 1, 'Wrong PSK, got %r, expected %r' % (psk, ap.psk)
+
+ _setclient_success(interface_name, ssid, bssid, psk, interface.driver, ap,
+ band)
+
+ return 0, ''
+
+
+def _setclient_error_not_found(interface_name, ssid, driver):
+ if driver == 'cfg80211':
+ wpa_cli.mock(interface_name, wpa_state='SCANNING')
+ elif driver == 'frenzy':
+ qcsapi.mock('get_mode', 'wifi0', value='Station')
+ qcsapi.mock('get_ssid', 'wifi0', value='')
+ qcsapi.mock('ssid_get_authentication_mode', 'wifi0', ssid, value='')
+ qcsapi.mock('get_status', 'wifi0', value='Error')
+
+ CLIENT_ASSOCIATIONS[interface_name] = None
+
+
+def _setclient_error_auth(interface_name, ssid, driver):
+ if driver == 'cfg80211':
+ # This is what our version of wpa_supplicant does for auth failures.
+ INTERFACE_EVENTS[interface_name].append('<2>CTRL-EVENT-SSID-TEMP-DISABLED')
+ wpa_cli.mock(interface_name, wpa_state='SCANNING')
+ elif driver == 'frenzy':
+ qcsapi.mock('get_mode', 'wifi0', value='Station')
+ qcsapi.mock('get_ssid', 'wifi0', value='')
+ qcsapi.mock('ssid_get_authentication_mode', 'wifi0', ssid, value='')
+ qcsapi.mock('get_status', 'wifi0', value='Error')
+
+ CLIENT_ASSOCIATIONS[interface_name] = None
+
+
+def _setclient_success(interface_name, ssid, bssid, psk, driver, ap, band):
+ if CLIENT_ASSOCIATIONS.get(interface_name, None):
+ _disconnected_event(band)
+ if driver == 'cfg80211':
+ # Make sure the wpa_supplicant socket exists.
+ open(os.path.join(WPA_PATH, interface_name), 'w')
+
+ # Tell wpa_cli what to return.
+ key_mgmt = 'WPA2-PSK' if psk else 'NONE'
+ wpa_cli.mock(interface_name, wpa_state='COMPLETED', ssid=ssid, bssid=bssid,
+ key_mgmt=key_mgmt)
+
+ # Send the CONNECTED event.
+ INTERFACE_EVENTS[interface_name].append('<2>CTRL-EVENT-CONNECTED')
+
+ elif driver == 'frenzy':
+ qcsapi.mock('get_mode', 'wifi0', value='Station')
+ qcsapi.mock('get_ssid', 'wifi0', value=ssid)
+ qcsapi.mock('ssid_get_authentication_mode', 'wifi0', ssid,
+ value='PSKAuthentication' if psk else 'NONE')
+ qcsapi.mock('get_status', 'wifi0', value='')
+
+ CLIENT_ASSOCIATIONS[interface_name] = ap
+ connection_check.mock(interface_name, ap.connection_check_result or 'succeed')
+
+ # Call ifplugd.action for the interface coming up (wifi/quantenna.py does this
+ # manually).
+ ifplugd_action.call(interface_name, 'up')
+
+
+def _disconnected_event(band):
+ interface = INTERFACE_FOR_BAND[band]
+ interface_name = 'wcli%s' % interface.phynum
+ if interface.driver == 'cfg80211':
+ INTERFACE_EVENTS[interface_name].append('<2>CTRL-EVENT-DISCONNECTED')
+ wpa_cli.mock(interface_name, wpa_state='SCANNING')
+ else:
+ qcsapi.mock('get_ssid', 'wifi0', value='')
+ qcsapi.mock('get_status', 'wifi0', value='Error')
+
+ CLIENT_ASSOCIATIONS[interface_name] = None
+
+
+def _stopclient(args, env=None):
+ bands = _get_flag(args, ('-b', '--band')) or '2.4 5'
+ for band in bands.split():
+ interface = INTERFACE_FOR_BAND[band]
+ interface_name = 'wcli%s' % interface.phynum
+
+ if interface.driver == 'cfg80211':
+ # Send the DISCONNECTED and TERMINATING events.
+ INTERFACE_EVENTS[interface_name].append('<2>CTRL-EVENT-DISCONNECTED')
+ INTERFACE_EVENTS[interface_name].append('<2>CTRL-EVENT-TERMINATING')
+
+ # Clear the wpa_cli status response.
+ wpa_cli.mock(interface_name)
+
+ # Make sure the wpa_supplicant socket does not.
+ if os.path.exists(os.path.join(WPA_PATH, interface_name)):
+ os.unlink(os.path.join(WPA_PATH, interface_name))
+
+ elif interface.driver == 'frenzy':
+ qcsapi.mock('get_ssid', 'wifi0', value='')
+ qcsapi.mock('get_status', 'wifi0', value='Error')
+
+ CLIENT_ASSOCIATIONS[interface_name] = None
+
+ # Call ifplugd.action for the interface going down (wifi/quantenna.py does this
+ # manually).
+ ifplugd_action.call(interface_name, 'down')
+
+ return 0, ''
+
+
+def _stop(*args, **kwargs):
+ _stopap(*args, **kwargs)
+ _stopclient(*args, **kwargs)
+ return 0, ''
+
+
+def _kill_wpa_supplicant(band):
+ # From conman's perspective, there's no difference between someone running
+ # 'wifi stopclient' and the process dying for some other reason.
+ _stopclient(['--band', band])
+
+
+def _scan(args, **unused_kwargs):
+ band_flag = _get_flag(args, ('-b', '--band'))
+ interface = INTERFACE_FOR_BAND[band_flag]
+ interface_name = 'wcli%s' % interface.phynum
+ if not ifup.INTERFACE_STATE.get(interface_name, False):
+ return 1, 'interface down'
+
+ return 0, '\n'.join(ap.scan_str()
+ for band in interface.bands
+ for ap in REMOTE_ACCESS_POINTS[band].itervalues())
+
+
+def _show(unused_args, **unused_kwargs):
+ return 0, '\n\n'.join(WIFI_SHOW_TPL.format(band=band, **interface._asdict()) if interface
+ else WIFI_SHOW_NO_RADIO_TPL.format(band)
+ for band, interface in INTERFACE_FOR_BAND.iteritems())
+
+
+def _get_flag(args, flags):
+ for flag in flags:
+ if flag in args:
+ return args[args.index(flag) + 1]
+
+
+def mock(command, *args, **kwargs):
+ if command == 'remote_ap':
+ remote_ap = AccessPoint(**kwargs)
+ REMOTE_ACCESS_POINTS[kwargs['band']][kwargs['bssid']] = remote_ap
+ elif command == 'remote_ap_remove':
+ del REMOTE_ACCESS_POINTS[kwargs['band']][kwargs['bssid']]
+ elif command == 'interfaces':
+ INTERFACE_FOR_BAND.clear()
+ for interface in args:
+ for band in interface.bands:
+ INTERFACE_FOR_BAND[band] = interface
+ if interface.driver == 'frenzy':
+ get_quantenna_interfaces.mock(
+ fmt % interface.phynum
+ for fmt in ('wlan%s', 'wlan%s_portal', 'wcli%s'))
+ elif command == 'wpa_path':
+ global WPA_PATH
+ WPA_PATH = args[0]
+ elif command == 'disconnected_event':
+ _disconnected_event(args[0])
+ elif command == 'kill_wpa_supplicant':
+ _kill_wpa_supplicant(args[0])
diff --git a/conman/test/fake_python/subprocess/wpa_cli.py b/conman/test/fake_python/subprocess/wpa_cli.py
new file mode 100644
index 0000000..c1849a9
--- /dev/null
+++ b/conman/test/fake_python/subprocess/wpa_cli.py
@@ -0,0 +1,37 @@
+#!/usr/bin/python
+
+"""Fake wpa_cli implementation. Used by fake WPACtrl too."""
+
+import ifdown
+import ifup
+
+
+_INTERFACE_STATE = {}
+
+
+def call(*args, **unused_kwargs):
+ if 'status' not in args:
+ raise ValueError('Fake wpa_cli can only do status requests.')
+
+ if '-i' not in args:
+ raise ValueError('Must specify interface with -i.')
+
+ interface = args[args.index('-i') + 1]
+
+ # Fails for not present or empty dict.
+ if not _INTERFACE_STATE.get(interface, None):
+ return 1, ('Failed to connect to non-global ctrl_ifname: %r '
+ 'error: No such file or directory' % interface)
+
+ state = _INTERFACE_STATE[interface]
+
+ return 0, '\n'.join('%s=%s' % (k, v) for k, v in state.iteritems())
+
+
+# Pass no kwargs to "kill" wpa_supplicant.
+def mock(interface, **kwargs):
+ _INTERFACE_STATE[interface] = {k: v for k, v in kwargs.iteritems() if v}
+ if kwargs:
+ ifup.call(interface)
+ else:
+ ifdown.call(interface)
diff --git a/conman/test/fake_wpactrl/wpactrl/__init__.py b/conman/test/fake_wpactrl/wpactrl/__init__.py
deleted file mode 100644
index b8ce1fd..0000000
--- a/conman/test/fake_wpactrl/wpactrl/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-class error(Exception):
- pass
-
-class WPACtrl(object):
- pass
diff --git a/conman/test/restricted b/conman/test/restricted
deleted file mode 100755
index 9bd8fc3..0000000
--- a/conman/test/restricted
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/sh
-
-echo "$@" | grep -q -- "-a"
-
diff --git a/conman/test/succeed b/conman/test/succeed
deleted file mode 100755
index c52d3c2..0000000
--- a/conman/test/succeed
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-
-exit 0
diff --git a/diags/chameleon/sfp.c b/diags/chameleon/sfp.c
index e17e02d..1974d27 100644
--- a/diags/chameleon/sfp.c
+++ b/diags/chameleon/sfp.c
@@ -175,7 +175,7 @@
temp = value[0] + ((float)value[1]) / 256.0;
}
vcc = ((float)((value[2] << 8) + value[3])) / 10000.0;
- tx_bias = ((float)((value[4] << 8) + value[5])) / 1000.0;
+ tx_bias = (((float)((value[4] << 8) + value[5])) * 2) / 1000.0;
tx_power = ((float)((value[6] << 8) + value[7])) / 10000.0;
rx_power = ((float)((value[8] << 8) + value[9])) / 10000.0;
mod_curr = ((float)((value[12] << 8) + value[13])) / 1000.0;
diff --git a/ginstall/ginstall.py b/ginstall/ginstall.py
index 1fc0976..67f196a 100755
--- a/ginstall/ginstall.py
+++ b/ginstall/ginstall.py
@@ -62,10 +62,13 @@
F = {
'ETCPLATFORM': '/etc/platform',
+ 'ETCOS': '/etc/os',
'ETCVERSION': '/etc/version',
'DEV': '/dev',
'MMCBLK0': '/dev/mmcblk0',
+ 'MMCBLK0-ANDROID': '/dev/block/mmcblk0',
'MTD_PREFIX': '/dev/mtd',
+ 'MTD_PREFIX-ANDROID': '/dev/mtd/mtd',
'PROC_CMDLINE': '/proc/cmdline',
'PROC_MTD': '/proc/mtd',
'SECUREBOOT': '/tmp/gpio/ledcontrol/secure_boot',
@@ -74,12 +77,22 @@
'SYSBLOCK': '/sys/block',
'MMCBLK0BOOT0': '/dev/mmcblk0boot0',
'MMCBLK0BOOT1': '/dev/mmcblk0boot1',
+ 'MMCBLK0BOOT0-ANDROID': '/dev/block/mmcblk0boot0',
+ 'MMCBLK0BOOT1-ANDROID': '/dev/block/mmcblk0boot1',
'MEMINFO': '/proc/meminfo',
}
+ANDROID_BSU_PARTITION = 'bsu'
+ANDROID_BOOT_PARTITIONS = ['boot_a', 'boot_b']
+ANDROID_SYSTEM_PARTITIONS = ['system_a', 'system_b']
+ANDROID_IMAGES = ['boot.img', 'system.img.raw']
+ANDROID_IMG_SUFFIX = ['a', 'b']
+
MMC_RO_LOCK = {
'MMCBLK0BOOT0': '/sys/block/mmcblk0boot0/force_ro',
'MMCBLK0BOOT1': '/sys/block/mmcblk0boot1/force_ro',
+ 'MMCBLK0BOOT0-ANDROID': '/sys/block/mmcblk0boot0/force_ro',
+ 'MMCBLK0BOOT1-ANDROID': '/sys/block/mmcblk0boot1/force_ro',
}
# Verbosity of output
@@ -131,6 +144,26 @@
return open(F['ETCPLATFORM']).read().strip()
+def GetOs():
+ # not all platforms provide ETCOS, default to 'fiberos' in that case
+ try:
+ return open(F['ETCOS']).read().strip()
+ except IOError:
+ return 'fiberos'
+
+
+def GetMtdPrefix():
+ if GetOs() == 'android':
+ return F['MTD_PREFIX-ANDROID']
+ return F['MTD_PREFIX']
+
+
+def GetMmcblk0Prefix():
+ if GetOs() == 'android':
+ return F['MMCBLK0-ANDROID']
+ return F['MMCBLK0']
+
+
def GetVersion():
return open(F['ETCVERSION']).read().strip()
@@ -154,17 +187,47 @@
return None
-def SetBootPartition(partition):
- VerbosePrint('Setting boot partition to kernel%d\n', partition)
- cmd = [HNVRAM, '-q', '-w', 'ACTIVATED_KERNEL_NAME=kernel%d' % partition]
- return subprocess.call(cmd)
+def SetBootPartition(target_os, partition):
+ """Set active boot partition for the given OS and switch the OS if needed.
+
+ Args:
+ target_os: 'fiberos' or 'android'
+ partition: 0 or 1
+
+ Returns:
+ 0 if successful, else an error code.
+ """
+ if target_os == 'android':
+ param = 'ANDROID_ACTIVE_PARTITION=%s' % ANDROID_IMG_SUFFIX[partition]
+ else:
+ param = 'ACTIVATED_KERNEL_NAME=kernel%d' % partition
+
+ VerbosePrint('Setting boot partition: %s\n', param)
+ try:
+ ret = subprocess.call([HNVRAM, '-q', '-w', param])
+ except OSError:
+ ret = 127
+ if ret:
+ VerbosePrint('Failed setting boot partition!\n')
+ return ret
+
+ if target_os != GetOs():
+ VerbosePrint('Switch OS to %s\n', target_os)
+ try:
+ ret = subprocess.call([HNVRAM, '-q', '-w', 'BOOT_TARGET=%s' % target_os])
+ except OSError:
+ ret = 127
+ if ret:
+ VerbosePrint('Failed switching OS!\n')
+
+ return ret
def GetBootedPartition():
"""Get the role of partition where the running system is booted from.
Returns:
- 0 or 1 for rootfs0 and rootfs1, or None if not booted from flash.
+ 0 or 1, or None if not booted from flash.
"""
try:
with open(F['PROC_CMDLINE']) as f:
@@ -184,6 +247,41 @@
return 0
elif partition == 'kernel1':
return 1
+ elif arg.startswith('androidboot.gfiber_system_img='):
+ partition = arg.split('=')[1]
+ if partition == ANDROID_SYSTEM_PARTITIONS[0]:
+ return 0
+ elif partition == ANDROID_SYSTEM_PARTITIONS[1]:
+ return 1
+ return None
+
+
+def GetActivePartitionFromHNVRAM(target_os):
+ """Get the active partion for the given OS as set in HNVRAM.
+
+ Args:
+ target_os: 'fiberos' or 'android'
+
+ Returns:
+ 0 or 1 if the active partition could be determined, None if not.
+ """
+ if target_os == 'fiberos':
+ cmd = [HNVRAM, '-q', '-r', 'ACTIVATED_KERNEL_NAME']
+ elif target_os == 'android':
+ cmd = [HNVRAM, '-q', '-r', 'ANDROID_ACTIVE_PARTITION']
+ else:
+ return None
+
+ try:
+ partition_name = subprocess.check_output(cmd).strip()
+ except subprocess.CalledProcessError:
+ return None
+
+ if partition_name in ['0', 'a']:
+ return 0
+ elif partition_name in ['1', 'b']:
+ return 1
+
return None
@@ -219,12 +317,12 @@
if len(fields) >= 4 and fields[3] == quotedname:
assert fields[0].startswith('mtd')
assert fields[0].endswith(':')
- return '%s%d' % (F['MTD_PREFIX'], int(fields[0][3:-1]))
+ return '%s%d' % (GetMtdPrefix(), int(fields[0][3:-1]))
return None # no match
def IsMtdNand(mtddevname):
- mtddevname = re.sub(r'^' + F['MTD_PREFIX'], 'mtd', mtddevname)
+ mtddevname = re.sub(r'^' + GetMtdPrefix(), 'mtd', mtddevname)
path = F['SYSCLASSMTD'] + '/{0}/type'.format(mtddevname)
data = open(path).read()
return 'nand' in data
@@ -275,7 +373,8 @@
Returns:
Device file of named partition
"""
- cmd = [SGDISK, '-p', blk_dev]
+ # Note: Android doesn't support '-p' option, need to use '--print'
+ cmd = [SGDISK, '--print', blk_dev]
devnull = open('/dev/null', 'w')
try:
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=devnull)
@@ -641,6 +740,24 @@
return True
+def GetOsFromManifest(manifest):
+ """Determine which OS (FiberOS, Android) the image is for from the manifest.
+
+ Args:
+ manifest: the manifest from an image file
+
+ Returns:
+ 'android' if any Android specific image name is found in the manifest,
+ otherwise it returns 'fiberos' (default).
+
+ """
+ for key in manifest.keys():
+ if key.endswith('-sha1'):
+ if key[:-5] in ANDROID_IMAGES:
+ return 'android'
+ return 'fiberos'
+
+
class ProgressBar(object):
"""Progress bar that prints one dot per 1MB."""
@@ -695,26 +812,37 @@
Log('W: psback/logos unavailable for tracing.\n')
-def GetPartition(opt):
- """Return the partiton to install to, given the command line options."""
- if opt.partition == 'other':
- boot = GetBootedPartition()
+def GetPartition(partition_name, target_os):
+ """Return the partition to install to.
+
+ Args:
+ partition_name: partition name from command-line
+ {'primary', 'secondary', 'other'}
+ target_os: 'fiberos' or 'android'
+
+ Returns:
+ 0 or 1
+
+ Raises:
+ Fatal: if no partition could be determined
+ """
+ if partition_name == 'other':
+ if target_os == GetOs():
+ boot = GetBootedPartition()
+ else:
+ boot = GetActivePartitionFromHNVRAM(target_os)
assert boot in [None, 0, 1]
if boot is None:
# Policy decision: if we're booted from NFS, install to secondary
return 1
else:
return boot ^ 1
- elif opt.partition in ['primary', 0]:
+ elif partition_name in ['primary', 0]:
return 0
- elif opt.partition in ['secondary', 1]:
+ elif partition_name in ['secondary', 1]:
return 1
- elif opt.partition:
- raise Fatal('--partition must be one of: primary, secondary, other')
- elif opt.tar:
- raise Fatal('A --partition option must be provided with --tar')
else:
- return None
+ raise Fatal('--partition must be one of: primary, secondary, other')
def InstallKernel(kern, partition):
@@ -729,7 +857,7 @@
partition_name = 'kernel%d' % partition
mtd = GetMtdDevForNameOrNone(partition_name)
- gpt = GetGptPartitionForName(F['MMCBLK0'], partition_name)
+ gpt = GetGptPartitionForName(GetMmcblk0Prefix(), partition_name)
if mtd:
VerbosePrint('Writing kernel to %r\n' % mtd)
InstallToMtd(kern, mtd)
@@ -759,7 +887,7 @@
if gpt:
mtd = None
else:
- gpt = GetGptPartitionForName(F['MMCBLK0'], partition_name)
+ gpt = GetGptPartitionForName(GetMmcblk0Prefix(), partition_name)
if mtd:
if GetPlatform().startswith('GFMN'):
VerbosePrint('Writing rootfs to %r\n' % mtd)
@@ -775,6 +903,72 @@
raise Fatal('no partition named %r is available' % partition_name)
+def InstallAndroidBoot(boot, partition):
+ """Install an Android boot.img file.
+
+ Args:
+ boot: a FileWithSecureHash object.
+ partition: the partition to install to, 0 or 1.
+
+ Raises:
+ Fatal: if install fails
+ """
+
+ partition_name = ANDROID_BOOT_PARTITIONS[partition]
+ gpt = GetGptPartitionForName(GetMmcblk0Prefix(), partition_name)
+ if gpt:
+ VerbosePrint('Writing boot.img to %r\n' % gpt)
+ InstallToFile(boot, gpt)
+ else:
+ raise Fatal('no partition named %r is available' % partition_name)
+
+
+def InstallAndroidSystem(system, partition):
+ """Install an Android system.img file.
+
+ Args:
+ system: a FileWithSecureHash object.
+ partition: the partition to install to, 0 or 1.
+
+ Raises:
+ Fatal: if install fails
+ """
+
+ partition_name = ANDROID_SYSTEM_PARTITIONS[partition]
+ gpt = GetGptPartitionForName(GetMmcblk0Prefix(), partition_name)
+ if gpt:
+ VerbosePrint('Writing system.img.raw to %r\n' % gpt)
+ InstallToFile(system, gpt)
+ else:
+ raise Fatal('no partition named %r is available' % partition_name)
+
+
+def InstallAndroidBsu(bsu):
+ """Install an Android BSU file.
+
+ Args:
+ bsu: a FileWithSecureHash object.
+
+ Raises:
+ Fatal: if install fails
+ """
+
+ is_bsu_current = False
+ gpt = GetGptPartitionForName(GetMmcblk0Prefix(), ANDROID_BSU_PARTITION)
+ if gpt:
+ with open(gpt, 'rb') as gptfile:
+ VerbosePrint('Checking if android_bsu is up to date.\n')
+ is_bsu_current = IsIdentical('android_bsu', bsu.filelike, gptfile)
+ if is_bsu_current:
+ VerbosePrint('android_bsu is the latest.\n')
+ else:
+ bsu.filelike.seek(0, os.SEEK_SET)
+ VerbosePrint('Writing android_bsu.elf to %r\n' % gpt)
+ InstallToFile(bsu, gpt)
+ else:
+ raise Fatal('no partition named %r is available' % ANDROID_BSU_PARTITION)
+
+
def UnlockMMC(mmc_name):
if mmc_name in MMC_RO_LOCK:
with open(MMC_RO_LOCK[mmc_name], 'w') as f:
@@ -805,7 +999,11 @@
WriteLoaderToMtd(loader, loader_start, mtd, 'loader')
installed = True
# For hd254 we also write the loader to the emmc boot partitions.
- for emmc_name in ['MMCBLK0BOOT0', 'MMCBLK0BOOT1']:
+ if GetOs() == 'android':
+ emmc_list = ['MMCBLK0BOOT0-ANDROID', 'MMCBLK0BOOT1-ANDROID']
+ else:
+ emmc_list = ['MMCBLK0BOOT0', 'MMCBLK0BOOT1']
+ for emmc_name in emmc_list:
emmc_dev = F[emmc_name]
if os.path.exists(emmc_dev):
UnlockMMC(emmc_name)
@@ -843,19 +1041,23 @@
WriteLoaderToMtd(uloader, uloader_start, mtd, 'uloader')
-def InstallImage(f, partition, skiploader=False, skiploadersig=False):
+def InstallImage(opt):
"""Install an image.
Args:
- f: a file-like objected expected to provide a stream in tar format
- partition: integer 0 or 1 of the partition to install into
- skiploader: skip installation of a bootloader
- skiploadersig: skip checking of bootloader signature
+ opt: command-line options
+ Returns:
+ 0 for success, else an error code
Raises:
Fatal: if install fails
"""
+ if not opt.partition:
+ # default to the safe option if not given
+ opt.partition = 'other'
+
+ f = OpenPathOrUrl(opt.tar)
tar = tarfile.open(mode='r|*', fileobj=f)
first = tar.next()
@@ -879,15 +1081,30 @@
CheckMinimumVersion(manifest)
CheckMisc(manifest)
+ target_os = GetOsFromManifest(manifest)
+ partition = GetPartition(opt.partition, target_os)
+
loader_bin_list = ['loader.img', 'loader.bin']
loader_sig_list = ['loader.sig']
if CheckMultiLoader(manifest):
loader_bin_list = ['loader.%s.bin' % GetPlatform().lower()]
loader_sig_list = ['loader.%s.sig' % GetPlatform().lower()]
- uloader = loader = None
+ uloader = loader = android_bsu = None
uloadersig = FileWithSecureHash(StringIO.StringIO(''), 'badsig')
- loadersig = FileWithSecureHash(StringIO.StringIO(''), 'badsig')
+
+ # TODO(cgibson): Modern ginstall images contain a loadersig. However, some
+ # releases, such as 42.33 for the FiberJack, do not have a loadersig. In 42.33
+ # this was okay since cwmp calls ginstall with the '--skiploadersig' flag.
+ # However, in later versions this flag was removed. Now if a new ginstall
+ # were to be used to downgrade to an older ginstall image, the install would
+ # fail. This seems to only affect the FiberJack platform, which is still
+ # running 42.33. This can safely be removed once all FiberJacks have been
+ # upgraded to gfiber-47 and are not anticipated to need to be downgraded back
+ # to 42.33.
+ loadersig = None
+ if not GetPlatform().startswith('GFLT'):
+ loadersig = FileWithSecureHash(StringIO.StringIO(''), 'badsig')
for ti in tar:
secure_hash = manifest.get('%s-sha1' % ti.name)
@@ -895,11 +1112,29 @@
# already processed
pass
elif ti.name in ['kernel.img', 'vmlinuz', 'vmlinux', 'uImage']:
- fh = FileWithSecureHash(tar.extractfile(ti), secure_hash)
- InstallKernel(fh, partition)
+ if target_os != 'fiberos':
+ VerbosePrint('Cannot install kernel img in Android!\n')
+ else:
+ fh = FileWithSecureHash(tar.extractfile(ti), secure_hash)
+ InstallKernel(fh, partition)
elif ti.name.startswith('rootfs.'):
- fh = FileWithSecureHash(tar.extractfile(ti), secure_hash)
- InstallRootfs(fh, partition)
+ if target_os != 'fiberos':
+ VerbosePrint('Cannot install rootfs img in Android!\n')
+ else:
+ fh = FileWithSecureHash(tar.extractfile(ti), secure_hash)
+ InstallRootfs(fh, partition)
+ elif ti.name == 'boot.img':
+ if target_os != 'android':
+ VerbosePrint('Cannot install boot img in FiberOS!\n')
+ else:
+ fh = FileWithSecureHash(tar.extractfile(ti), secure_hash)
+ InstallAndroidBoot(fh, partition)
+ elif ti.name == 'system.img.raw':
+ if target_os != 'android':
+ VerbosePrint('Cannot install system img in FiberOS!\n')
+ else:
+ fh = FileWithSecureHash(tar.extractfile(ti), secure_hash)
+ InstallAndroidSystem(fh, partition)
elif ti.name in loader_bin_list:
buf = StringIO.StringIO(tar.extractfile(ti).read())
loader = FileWithSecureHash(buf, secure_hash)
@@ -912,34 +1147,49 @@
elif ti.name == 'uloader.sig':
buf = StringIO.StringIO(tar.extractfile(ti).read())
uloadersig = FileWithSecureHash(buf, secure_hash)
+ elif ti.name == 'android_bsu.elf':
+ buf = StringIO.StringIO(tar.extractfile(ti).read())
+ android_bsu = FileWithSecureHash(buf, secure_hash)
else:
print 'Unknown install file %s' % ti.name
- if skiploadersig:
+ if opt.skiploadersig:
loadersig = uloadersig = None
key = GetKey()
- if loadersig and loader and not skiploader:
+ if loadersig and loader and not opt.skiploader:
if not Verify(loader.filelike, loadersig.filelike, key):
raise Fatal('Loader signing check failed.')
loader.filelike.seek(0, os.SEEK_SET)
- if uloadersig and uloader and not skiploader:
+ if uloadersig and uloader and not opt.skiploader:
if not Verify(uloader.filelike, uloadersig.filelike, key):
raise Fatal('Uloader signing check failed.')
uloader.filelike.seek(0, os.SEEK_SET)
if loader:
- if skiploader:
+ if opt.skiploader:
VerbosePrint('Skipping loader installation.\n')
else:
InstallLoader(loader)
if uloader:
- if skiploader:
+ if opt.skiploader:
VerbosePrint('Skipping uloader installation.\n')
else:
InstallUloader(uloader)
+ if android_bsu:
+ if opt.skiploader:
+ VerbosePrint('Skipping android_bsu installation.\n')
+ else:
+ InstallAndroidBsu(android_bsu)
+
+ if SetBootPartition(target_os, partition) != 0:
+ VerbosePrint('Unable to set boot partition\n')
+ return HNVRAM_ERR
+
+ return 0
+
def OpenPathOrUrl(path):
"""Try to open path as a URL and as a local file."""
@@ -968,6 +1218,16 @@
if not (opt.drm or opt.tar or opt.partition):
o.fatal('Expected at least one of --partition, --tar, or --drm')
+ # handle 'ginstall -p <partition>' separately
+ if not opt.drm and not opt.tar:
+ partition = GetPartition(opt, GetOs())
+ if SetBootPartition(GetOs(), partition) != 0:
+ VerbosePrint('Unable to set boot partition\n')
+ return HNVRAM_ERR
+ return 0
+
+ # from here: ginstall [-t <tarfile>] [--drm <blob>] [options...]
+
quiet = opt.quiet
if opt.basepath:
@@ -977,21 +1237,11 @@
if opt.drm:
WriteDrm(opt)
- if opt.tar and not opt.partition:
- # default to the safe option if not given
- opt.partition = 'other'
-
- partition = GetPartition(opt)
+ ret = 0
if opt.tar:
- f = OpenPathOrUrl(opt.tar)
- InstallImage(f, partition, skiploader=opt.skiploader,
- skiploadersig=opt.skiploadersig)
+ ret = InstallImage(opt)
- if partition is not None and SetBootPartition(partition) != 0:
- VerbosePrint('Unable to set boot partition\n')
- return HNVRAM_ERR
-
- return 0
+ return ret
def BroadcomDeviceIsSecure():
diff --git a/ginstall/ginstall_test.py b/ginstall/ginstall_test.py
index 07cb294..5b335ea 100755
--- a/ginstall/ginstall_test.py
+++ b/ginstall/ginstall_test.py
@@ -40,13 +40,16 @@
def setUp(self):
self.tmpdir = tempfile.mkdtemp()
+ self.hnvram_dir = self.tmpdir + '/hnvram'
self.script_out = self.tmpdir + '/out'
self.old_path = os.environ['PATH']
self.old_bufsize = ginstall.BUFSIZE
self.old_files = ginstall.F
+ os.environ['GINSTALL_HNVRAM_DIR'] = self.hnvram_dir
os.environ['GINSTALL_OUT_FILE'] = self.script_out
os.environ['GINSTALL_TEST_FAIL'] = ''
os.environ['PATH'] = 'testdata/bin:' + self.old_path
+ os.makedirs(self.hnvram_dir)
os.makedirs(self.tmpdir + '/dev')
ginstall.F['ETCPLATFORM'] = 'testdata/etc/platform'
ginstall.F['DEV'] = self.tmpdir + '/dev'
@@ -69,6 +72,9 @@
ginstall.MMC_RO_LOCK['MMCBLK0BOOT1'] = (
self.tmpdir + '/mmcblk0boot1/force_ro')
+ # default OS to 'fiberos'
+ self.WriteOsFile('fiberos')
+
def tearDown(self):
os.environ['PATH'] = self.old_path
shutil.rmtree(self.tmpdir, ignore_errors=True)
@@ -80,6 +86,23 @@
open(filename, 'w').write(version)
ginstall.F['ETCVERSION'] = filename
+ def WriteOsFile(self, os_name):
+ """Create a fake /etc/os file in /tmp."""
+ filename = self.tmpdir + '/os'
+ open(filename, 'w').write(os_name)
+ ginstall.F['ETCOS'] = filename
+
+ def WriteHnvramAttr(self, attr, val):
+ filename = self.hnvram_dir + '/%s' % attr
+ open(filename, 'w').write(val)
+
+ def ReadHnvramAttr(self, attr):
+ filename = self.hnvram_dir + '/%s' % attr
+ try:
+ return open(filename).read()
+ except IOError:
+ return None
+
def testVerify(self):
self.assertTrue(ginstall.Verify(
open('testdata/img/loader.bin'),
@@ -158,11 +181,44 @@
origfile, 'mtd0.tmp')
def testSetBootPartition(self):
- ginstall.SetBootPartition(0)
- ginstall.SetBootPartition(1)
+ self.WriteOsFile('fiberos')
+ ginstall.SetBootPartition('fiberos', 0)
+ self.assertEqual('kernel0', self.ReadHnvramAttr('ACTIVATED_KERNEL_NAME'))
+ ginstall.SetBootPartition('fiberos', 1)
+ self.assertEqual('kernel1', self.ReadHnvramAttr('ACTIVATED_KERNEL_NAME'))
+ ginstall.SetBootPartition('android', 0)
+ self.assertEqual('a', self.ReadHnvramAttr('ANDROID_ACTIVE_PARTITION'))
+ self.assertEqual('android', self.ReadHnvramAttr('BOOT_TARGET'))
+ ginstall.SetBootPartition('android', 1)
+ self.assertEqual('b', self.ReadHnvramAttr('ANDROID_ACTIVE_PARTITION'))
+ self.assertEqual('android', self.ReadHnvramAttr('BOOT_TARGET'))
+
+ self.WriteOsFile('android')
+ ginstall.SetBootPartition('fiberos', 0)
+ self.assertEqual('kernel0', self.ReadHnvramAttr('ACTIVATED_KERNEL_NAME'))
+ self.assertEqual('fiberos', self.ReadHnvramAttr('BOOT_TARGET'))
+ ginstall.SetBootPartition('fiberos', 1)
+ self.assertEqual('kernel1', self.ReadHnvramAttr('ACTIVATED_KERNEL_NAME'))
+ self.assertEqual('fiberos', self.ReadHnvramAttr('BOOT_TARGET'))
+ ginstall.SetBootPartition('android', 0)
+ self.assertEqual('a', self.ReadHnvramAttr('ANDROID_ACTIVE_PARTITION'))
+ ginstall.SetBootPartition('android', 1)
+ self.assertEqual('b', self.ReadHnvramAttr('ANDROID_ACTIVE_PARTITION'))
+
+ # also verify the hnvram command history for good measures
out = open(self.script_out).read().splitlines()
self.assertEqual(out[0], 'hnvram -q -w ACTIVATED_KERNEL_NAME=kernel0')
self.assertEqual(out[1], 'hnvram -q -w ACTIVATED_KERNEL_NAME=kernel1')
+ self.assertEqual(out[2], 'hnvram -q -w ANDROID_ACTIVE_PARTITION=a')
+ self.assertEqual(out[3], 'hnvram -q -w BOOT_TARGET=android')
+ self.assertEqual(out[4], 'hnvram -q -w ANDROID_ACTIVE_PARTITION=b')
+ self.assertEqual(out[5], 'hnvram -q -w BOOT_TARGET=android')
+ self.assertEqual(out[6], 'hnvram -q -w ACTIVATED_KERNEL_NAME=kernel0')
+ self.assertEqual(out[7], 'hnvram -q -w BOOT_TARGET=fiberos')
+ self.assertEqual(out[8], 'hnvram -q -w ACTIVATED_KERNEL_NAME=kernel1')
+ self.assertEqual(out[9], 'hnvram -q -w BOOT_TARGET=fiberos')
+ self.assertEqual(out[10], 'hnvram -q -w ANDROID_ACTIVE_PARTITION=a')
+ self.assertEqual(out[11], 'hnvram -q -w ANDROID_ACTIVE_PARTITION=b')
def testParseManifest(self):
l = ('installer_version: 99\nimage_type: fake\n'
@@ -189,6 +245,34 @@
manifest = ginstall.ParseManifest(in_f)
self.assertTrue(ginstall.CheckPlatform(manifest))
+ def testGetOs(self):
+ self.WriteOsFile('fiberos')
+ self.assertEqual('fiberos', ginstall.GetOs())
+ self.WriteOsFile('android')
+ self.assertEqual('android', ginstall.GetOs())
+ # in case file doesn't exist, default is 'fiberos'
+ os.remove(self.tmpdir + '/os')
+ self.assertEqual('fiberos', ginstall.GetOs())
+
+ def testGetMtdPrefix(self):
+ self.WriteOsFile('fiberos')
+ self.assertEqual(ginstall.F['MTD_PREFIX'], ginstall.GetMtdPrefix())
+ self.WriteOsFile('android')
+ self.assertEqual(ginstall.F['MTD_PREFIX-ANDROID'], ginstall.GetMtdPrefix())
+ # unknown OS returns 'fiberos'
+ self.WriteOsFile('windows')
+ self.assertEqual(ginstall.F['MTD_PREFIX'], ginstall.GetMtdPrefix())
+
+ def testGetMmcblk0Prefix(self):
+ self.WriteOsFile('fiberos')
+ self.assertEqual(ginstall.F['MMCBLK0'], ginstall.GetMmcblk0Prefix())
+ self.WriteOsFile('android')
+ self.assertEqual(ginstall.F['MMCBLK0-ANDROID'],
+ ginstall.GetMmcblk0Prefix())
+ # unknown OS returns 'fiberos'
+ self.WriteOsFile('windows')
+ self.assertEqual(ginstall.F['MMCBLK0'], ginstall.GetMmcblk0Prefix())
+
def testGetInternalHarddisk(self):
self.assertEqual(ginstall.GetInternalHarddisk(), None)
@@ -268,20 +352,141 @@
manifest = {'version': v}
self.assertRaises(ginstall.Fatal, ginstall.CheckMisc, manifest)
- def testGetBootedFromCmdLine(self):
- ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline1'
+ def MakeManifestWithFilenameSha1s(self, filename):
+ m = ('installer_version: 4\n'
+ 'image_type: unlocked\n'
+ 'version: gftv254-48-pre2-1100-g25ff8d0-ck\n'
+ 'platforms: [ GFHD254 ]\n')
+ if filename is not None:
+ m += '%s-sha1: 9b5236c282b8c11b38a630361b6c690d6aaa50cb\n' % filename
+
+ in_f = StringIO.StringIO(m)
+ return ginstall.ParseManifest(in_f)
+
+ def testGetOsFromManifest(self):
+ # android specific image names return 'android'
+ for img in ginstall.ANDROID_IMAGES:
+ manifest = self.MakeManifestWithFilenameSha1s(img)
+ self.assertEqual('android', ginstall.GetOsFromManifest(manifest))
+
+ # fiberos image names or anything non-android returns 'fiberos'
+ for img in ['rootfs.img', 'kernel.img', 'whatever.img']:
+ manifest = self.MakeManifestWithFilenameSha1s(img)
+ self.assertEqual('fiberos', ginstall.GetOsFromManifest(manifest))
+
+ # no sha1 entry in the manifest returns 'fiberos'
+ manifest = self.MakeManifestWithFilenameSha1s(None)
+ self.assertEqual('fiberos', ginstall.GetOsFromManifest(manifest))
+
+ def testGetBootedPartition(self):
+ ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.none'
+ self.assertEqual(None, ginstall.GetBootedPartition())
+ ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.0'
+ self.assertEqual(0, ginstall.GetBootedPartition())
+ ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.1'
+ self.assertEqual(1, ginstall.GetBootedPartition())
+
+ # Android
+ ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.android.none'
+ self.assertEqual(None, ginstall.GetBootedPartition())
+ ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.android.0'
+ self.assertEqual(0, ginstall.GetBootedPartition())
+ ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.android.1'
+ self.assertEqual(1, ginstall.GetBootedPartition())
+
+ # Prowl
+ ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.prowl.none'
self.assertEqual(ginstall.GetBootedPartition(), None)
- ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline2'
+ ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.prowl.0'
self.assertEqual(ginstall.GetBootedPartition(), 0)
- ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline3'
+ ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.prowl.1'
self.assertEqual(ginstall.GetBootedPartition(), 1)
+ def testGetActivePartitionFromHNVRAM(self):
+ # FiberOS looks at ACTIVATED_KERNEL_NAME, not ANDROID_ACTIVE_PARTITION
+ # 0
+ self.WriteHnvramAttr('ACTIVATED_KERNEL_NAME', '0')
+ self.assertEqual(0, ginstall.GetActivePartitionFromHNVRAM('fiberos'))
+ self.WriteHnvramAttr('ANDROID_ACTIVE_PARTITION', '0')
+ self.assertEqual(0, ginstall.GetActivePartitionFromHNVRAM('fiberos'))
+ self.WriteHnvramAttr('ANDROID_ACTIVE_PARTITION', '1')
+ self.assertEqual(0, ginstall.GetActivePartitionFromHNVRAM('fiberos'))
+ # 1
+ self.WriteHnvramAttr('ACTIVATED_KERNEL_NAME', '1')
+ self.assertEqual(1, ginstall.GetActivePartitionFromHNVRAM('fiberos'))
+ self.WriteHnvramAttr('ANDROID_ACTIVE_PARTITION', '0')
+ self.assertEqual(1, ginstall.GetActivePartitionFromHNVRAM('fiberos'))
+
+ # Android looks at ANDROID_ACTIVE_PARTITION, not ACTIVATED_KERNEL_NAME
+ # 0
+ self.WriteHnvramAttr('ANDROID_ACTIVE_PARTITION', '0')
+ self.assertEqual(0, ginstall.GetActivePartitionFromHNVRAM('android'))
+ self.WriteHnvramAttr('ACTIVATED_KERNEL_NAME', '0')
+ self.assertEqual(0, ginstall.GetActivePartitionFromHNVRAM('android'))
+ self.WriteHnvramAttr('ACTIVATED_KERNEL_NAME', '1')
+ self.assertEqual(0, ginstall.GetActivePartitionFromHNVRAM('android'))
+ # 1
+ self.WriteHnvramAttr('ANDROID_ACTIVE_PARTITION', '1')
+ self.assertEqual(1, ginstall.GetActivePartitionFromHNVRAM('android'))
+ self.WriteHnvramAttr('ACTIVATED_KERNEL_NAME', '0')
+ self.assertEqual(1, ginstall.GetActivePartitionFromHNVRAM('android'))
+
+ def TestGetPartition(self):
+ self.assertEqual(0, ginstall.GetPartition('primary', 'fiberos'))
+ self.assertEqual(0, ginstall.GetPartition(0, 'fiberos'))
+ self.assertEqual(1, ginstall.GetPartition('secondary', 'fiberos'))
+ self.assertEqual(1, ginstall.GetPartition(1, 'fiberos'))
+ self.assertEqual(0, ginstall.GetPartition('primary', 'android'))
+ self.assertEqual(0, ginstall.GetPartition(0, 'android'))
+ self.assertEqual(1, ginstall.GetPartition('secondary', 'android'))
+ self.assertEqual(1, ginstall.GetPartition(1, 'android'))
+
+ # other: FiberOS->FiberOS
+ self.WriteOsFile('fiberos')
+ ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.none'
+ self.assertEqual(1, ginstall.GetPartition('other', 'fiberos'))
+ ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.0'
+ self.assertEqual(1, ginstall.GetPartition('other', 'fiberos'))
+ ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.1'
+ self.assertEqual(0, ginstall.GetPartition('other', 'fiberos'))
+ ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.prowl.none'
+ self.assertEqual(1, ginstall.GetPartition('other', 'fiberos'))
+ ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.prowl.0'
+ self.assertEqual(1, ginstall.GetPartition('other', 'fiberos'))
+ ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.prowl.1'
+ self.assertEqual(0, ginstall.GetPartition('other', 'fiberos'))
+
+ # other: FiberOS->Android
+ self.WriteHnvramAttr('ANDROID_ACTIVE_PARTITION', 'a')
+ self.assertEqual(1, ginstall.GetPartition('other', 'android'))
+ self.WriteHnvramAttr('ANDROID_ACTIVE_PARTITION', 'b')
+ self.assertEqual(0, ginstall.GetPartition('other', 'android'))
+ self.WriteHnvramAttr('ANDROID_ACTIVE_PARTITION', 'bla')
+ self.assertEqual(1, ginstall.GetPartition('other', 'android'))
+
+ # other: Android->FiberOS
+ self.WriteOsFile('android')
+ self.WriteHnvramAttr('ACTIVATED_KERNEL_NAME', '0')
+ self.assertEqual(1, ginstall.GetPartition('other', 'fiberos'))
+ self.WriteHnvramAttr('ACTIVATED_KERNEL_NAME', '1')
+ self.assertEqual(0, ginstall.GetPartition('other', 'fiberos'))
+ self.WriteHnvramAttr('ACTIVATED_KERNEL_NAME', 'bla')
+ self.assertEqual(1, ginstall.GetPartition('other', 'fiberos'))
+
+ # other: Android->Android
+ ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.android.none'
+ self.assertEqual(1, ginstall.GetPartition('other', 'android'))
+ ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.android.0'
+ self.assertEqual(1, ginstall.GetPartition('other', 'android'))
+ ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.android.1'
+ self.assertEqual(0, ginstall.GetPartition('other', 'android'))
+
# Test prowl and gfactive
- ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline4'
+ ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.prowl.none'
self.assertEqual(ginstall.GetBootedPartition(), None)
- ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline5'
+ ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.prowl.0'
self.assertEqual(ginstall.GetBootedPartition(), 0)
- ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline6'
+ ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.prowl.1'
self.assertEqual(ginstall.GetBootedPartition(), 1)
def testUloaderSigned(self):
diff --git a/ginstall/install_test.sh b/ginstall/install_test.sh
index ec372c5..910971d 100755
--- a/ginstall/install_test.sh
+++ b/ginstall/install_test.sh
@@ -4,20 +4,29 @@
tmpdir="$(mktemp -d)"
export PATH="$tmpdir/bin:${PATH}"
+export GINSTALL_HNVRAM_DIR="$tmpdir/hnvram"
export GINSTALL_OUT_FILE="$tmpdir/out"
+export GINSTALL_PLATFORM_FILE="$tmpdir/etc/platform"
psiz=$(stat --format=%s testdata/img/loader.gflt110.bin)
lsiz=$(stat --format=%s testdata/img/loader.img)
ksiz=$(stat --format=%s testdata/img/kernel.img)
rsiz=$(stat --format=%s testdata/img/rootfs.img)
usiz=$(stat --format=%s testdata/img/uloader.img)
+bsiz=$(stat --format=%s testdata/img/boot.img)
+ssiz=$(stat --format=%s testdata/img/system.img.raw)
+asiz=$(stat --format=%s testdata/img/android_bsu.elf)
testdata/bin/http_server "$tmpdir/http_ctrl" &
setup_fakeroot() {
platform="$1"
+ running_os="fiberos"
+ [ $# -gt 1 ] && running_os="$2"
rm -f "$GINSTALL_OUT_FILE"
rm -rf "$tmpdir/*"
mkdir -p "$tmpdir/bin" "$tmpdir/dev" "$tmpdir/etc"
+ mkdir -p "$tmpdir/dev/block" "$tmpdir/dev/mtd"
mkdir -p "$tmpdir/sys/block/sda"
+ mkdir -p "$GINSTALL_HNVRAM_DIR"
cp -r testdata/bin "$tmpdir"
cp -r testdata/proc "$tmpdir"
cp -r testdata/img "$tmpdir"
@@ -30,10 +39,18 @@
echo 0123456789abcdef0123456789abcdef >"$tmpdir/dev/mtd3"
echo 0123456789abcdef0123456789abcdef >"$tmpdir/dev/mtd4"
+ # write a pre-existing android_bsu
+ echo 0123456789abcdef0123456789abcdef >"$tmpdir/dev/mmcblk0p2"
+ echo 0123456789abcdef0123456789abcdef >"$tmpdir/dev/block/mmcblk0p2"
+
for i in {5..31}; do touch "$tmpdir/dev/mtd$i"; done
+ # duplicate /dev/mtd* to /dev/mtd/mtd* (used by Android)
+ cp ${tmpdir}/dev/mtd[0-9]* ${tmpdir}/dev/mtd/
+
cp "testdata/proc/mtd.$platform" "$tmpdir/proc/mtd"
echo "$platform" >"$tmpdir/etc/platform"
+ echo "$running_os" >"$tmpdir/etc/os"
echo 0123456789abcdef0123456789abcdef >"$tmpdir/etc/gfiber_public.der"
}
@@ -150,6 +167,74 @@
+# kernel in NAND, raw no bbt
+# rootfs on eMMC
+# (GFHD254)
+echo; echo; echo "GFHD254 (fiberos->fiberos)"
+setup_fakeroot GFHD254 fiberos
+expected="\
+psback
+logos ginstall
+flash_erase --quiet ${tmpdir}/dev/mtd0 0 0
+hnvram -q -w ACTIVATED_KERNEL_NAME=kernel1"
+
+WVPASS ./ginstall.py --basepath="$tmpdir" --tar=./testdata/img/image_v4.gi --partition=secondary --skiploadersig
+WVPASSEQ "$expected" "$(cat $GINSTALL_OUT_FILE)"
+WVPASS cmp --bytes="$lsiz" "${tmpdir}/dev/mtd0" testdata/img/loader.img
+WVPASS cmp --bytes="$ksiz" "${tmpdir}/dev/mmcblk0p14" testdata/img/kernel.img
+WVPASS cmp --bytes="$rsiz" "${tmpdir}/dev/mmcblk0p15" testdata/img/rootfs.img
+
+# FiberOS->Android (GFHD254)
+echo; echo; echo "GFHD254 (fiberos->android)"
+setup_fakeroot GFHD254 fiberos
+expected="\
+psback
+logos ginstall
+flash_erase --quiet ${tmpdir}/dev/mtd0 0 0
+hnvram -q -w ANDROID_ACTIVE_PARTITION=b
+hnvram -q -w BOOT_TARGET=android"
+
+WVPASS ./ginstall.py --basepath="$tmpdir" --tar=./testdata/img/image_android_v4.gi --partition=secondary --skiploadersig
+WVPASSEQ "$expected" "$(cat $GINSTALL_OUT_FILE)"
+WVPASS cmp --bytes="$lsiz" "${tmpdir}/dev/mtd0" testdata/img/loader.img
+WVPASS cmp --bytes="$asiz" "${tmpdir}/dev/mmcblk0p2" testdata/img/android_bsu.elf
+WVPASS cmp --bytes="$bsiz" "${tmpdir}/dev/mmcblk0p6" testdata/img/boot.img
+WVPASS cmp --bytes="$ssiz" "${tmpdir}/dev/mmcblk0p10" testdata/img/system.img.raw
+
+# Android->Android (GFHD254)
+echo; echo; echo "GFHD254 (android->android)"
+setup_fakeroot GFHD254 android
+expected="\
+psback
+logos ginstall
+flash_erase --quiet ${tmpdir}/dev/mtd/mtd0 0 0
+hnvram -q -w ANDROID_ACTIVE_PARTITION=a"
+
+WVPASS ./ginstall.py --basepath="$tmpdir" --tar=./testdata/img/image_android_v4.gi --partition=primary --skiploadersig
+WVPASSEQ "$expected" "$(cat $GINSTALL_OUT_FILE)"
+WVPASS cmp --bytes="$lsiz" "${tmpdir}/dev/mtd/mtd0" testdata/img/loader.img
+WVPASS cmp --bytes="$asiz" "${tmpdir}/dev/block/mmcblk0p2" testdata/img/android_bsu.elf
+WVPASS cmp --bytes="$bsiz" "${tmpdir}/dev/block/mmcblk0p5" testdata/img/boot.img
+WVPASS cmp --bytes="$ssiz" "${tmpdir}/dev/block/mmcblk0p9" testdata/img/system.img.raw
+
+# Android->FiberOS (GFHD254)
+echo; echo; echo "GFHD254 (android->fiberos)"
+setup_fakeroot GFHD254 android
+expected="\
+psback
+logos ginstall
+flash_erase --quiet ${tmpdir}/dev/mtd/mtd0 0 0
+hnvram -q -w ACTIVATED_KERNEL_NAME=kernel0
+hnvram -q -w BOOT_TARGET=fiberos"
+
+WVPASS ./ginstall.py --basepath="$tmpdir" --tar=./testdata/img/image_v4.gi --partition=primary --skiploadersig
+WVPASSEQ "$expected" "$(cat $GINSTALL_OUT_FILE)"
+WVPASS cmp --bytes="$lsiz" "${tmpdir}/dev/mtd/mtd0" testdata/img/loader.img
+WVPASS cmp --bytes="$ksiz" "${tmpdir}/dev/block/mmcblk0p12" testdata/img/kernel.img
+WVPASS cmp --bytes="$rsiz" "${tmpdir}/dev/block/mmcblk0p13" testdata/img/rootfs.img
+
+
+
# kernel in NOR, raw
# rootfs in NOR, raw
# loader in NOR, raw
diff --git a/ginstall/testdata/bin/hnvram b/ginstall/testdata/bin/hnvram
deleted file mode 120000
index 3c2bde7..0000000
--- a/ginstall/testdata/bin/hnvram
+++ /dev/null
@@ -1 +0,0 @@
-write_args_to_file
\ No newline at end of file
diff --git a/ginstall/testdata/bin/hnvram b/ginstall/testdata/bin/hnvram
new file mode 100755
index 0000000..6faa2b6
--- /dev/null
+++ b/ginstall/testdata/bin/hnvram
@@ -0,0 +1,38 @@
+#!/bin/sh
+
+# log into out file
+exe=$(basename "$0")
+echo "$exe" $* >> "$GINSTALL_OUT_FILE"
+
+# simple cmdline parser
+for i in "$@"; do
+ if [ "$i" == "-q" ]; then
+ continue
+ elif [ "$i" == "-r" ]; then
+ read=1
+ elif [ "$i" == "-w" ]; then
+ write=1
+ else
+ attr_val="$i"
+ fi
+done
+
+IFS='=' read attr val <<< "$attr_val"
+
+GINSTALL_ATTR_FILE="${GINSTALL_HNVRAM_DIR}/${attr}"
+
+if [ -n "$write" ]; then
+ echo -n "$val" > "$GINSTALL_ATTR_FILE"
+elif [ -n "$read" ]; then
+ if [ ! -r "$GINSTALL_ATTR_FILE" ]; then
+ exit 1
+ else
+ cat "$GINSTALL_ATTR_FILE"
+ fi
+fi
+
+if [ ! -z "$GINSTALL_TEST_FAIL" ]; then
+ exit 1
+fi
+
+exit 0
diff --git a/ginstall/testdata/bin/sgdisk b/ginstall/testdata/bin/sgdisk
index 69c640f..7e9392a 100755
--- a/ginstall/testdata/bin/sgdisk
+++ b/ginstall/testdata/bin/sgdisk
@@ -1,6 +1,6 @@
#!/bin/sh
-echo "Disk /dev/mmcblk0: 7634944 sectors, 3.6 GiB
+default="Disk /dev/mmcblk0: 7634944 sectors, 3.6 GiB
Logical sector size: 512 bytes
Disk identifier (GUID): FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF
Partition table holds up to 128 entries
@@ -15,3 +15,37 @@
19 954368 1740799 384.0 MiB FFFF rootfs1
20 1740800 1871871 64.0 MiB FFFF emergency
21 1871872 7114751 2.5 GiB 8300 data+ext4"
+
+gfhd254="Disk /dev/mmcblk0: 61071360 sectors, 29.1 GiB
+Logical sector size: 512 bytes
+Disk identifier (GUID): BE51F469-09A6-4A05-9A3B-D09855D3E5A1
+Partition table holds up to 128 entries
+First usable sector is 34, last usable sector is 61071326
+Partitions will be aligned on 2-sector boundaries
+Total free space is 0 sectors (0 bytes)
+
+Number Start (sector) End (sector) Size Code Name
+ 1 34 161 64.0 KiB 8300 nvram
+ 2 162 673 256.0 KiB 8300 bsu
+ 3 674 2721 1024.0 KiB 8300 misc
+ 4 2722 4769 1024.0 KiB 8300 hwcfg
+ 5 4770 70305 32.0 MiB 8300 boot_a
+ 6 70306 135841 32.0 MiB 8300 boot_b
+ 7 135842 152225 8.0 MiB 8300 metadata
+ 8 152226 156321 2.0 MiB 8300 eio
+ 9 156322 4350625 2.0 GiB 8300 system_a
+ 10 4350626 8544929 2.0 GiB 8300 system_b
+ 11 8544930 8547873 1.4 MiB 8300 hnvram
+ 12 8547874 8613409 32.0 MiB 8300 kernel0
+ 13 8613410 9399841 384.0 MiB 8300 rootfs0
+ 14 9399842 9465377 32.0 MiB 8300 kernel1
+ 15 9465378 10251809 384.0 MiB 8300 rootfs1
+ 16 10251810 10382881 64.0 MiB 8300 emergency
+ 17 10382882 61071326 24.2 GiB 8300 userdata"
+
+platform="$(cat $GINSTALL_PLATFORM_FILE)"
+if [ "$platform" == "GFHD254" ]; then
+ echo "$gfhd254"
+else
+ echo "$default"
+fi
diff --git a/ginstall/testdata/img/MANIFEST b/ginstall/testdata/img/MANIFEST
index de8c93b..5e839f4 100644
--- a/ginstall/testdata/img/MANIFEST
+++ b/ginstall/testdata/img/MANIFEST
@@ -1,7 +1,7 @@
installer_version: 4
image_type: unittest
version: gftv200-40.1
-platforms: [ GFUNITTEST, GFSC100, GFMN100, GFHD200, GFHD100, GFMS100, GFRG210, GFRG200 ]
+platforms: [ GFUNITTEST, GFSC100, GFMN100, GFHD200, GFHD254, GFHD100, GFMS100, GFRG210, GFRG200 ]
loader.img-sha1: 228d83f86ba967704a94afce92e1e4cbba3b24a6
uloader.img-sha1: 171e9a2e524c1f3a64f43ef7d254b85e764f1096
kernel.img-sha1: fbd0ad4af43303c6b3001920689db2eb4d5212d0
diff --git a/ginstall/testdata/img/android_bsu.elf b/ginstall/testdata/img/android_bsu.elf
new file mode 100644
index 0000000..44687e7
--- /dev/null
+++ b/ginstall/testdata/img/android_bsu.elf
@@ -0,0 +1 @@
+android_bsu.elf
\ No newline at end of file
diff --git a/ginstall/testdata/img/boot.img b/ginstall/testdata/img/boot.img
new file mode 100644
index 0000000..7afe48a
--- /dev/null
+++ b/ginstall/testdata/img/boot.img
@@ -0,0 +1 @@
+boot.img
\ No newline at end of file
diff --git a/ginstall/testdata/img/image_android_v4.gi b/ginstall/testdata/img/image_android_v4.gi
new file mode 100644
index 0000000..601138a
--- /dev/null
+++ b/ginstall/testdata/img/image_android_v4.gi
Binary files differ
diff --git a/ginstall/testdata/img/image_v4.gi b/ginstall/testdata/img/image_v4.gi
index 11aac14..7d66211 100644
--- a/ginstall/testdata/img/image_v4.gi
+++ b/ginstall/testdata/img/image_v4.gi
Binary files differ
diff --git a/ginstall/testdata/img/system.img.raw b/ginstall/testdata/img/system.img.raw
new file mode 100644
index 0000000..3fcfe41
--- /dev/null
+++ b/ginstall/testdata/img/system.img.raw
@@ -0,0 +1 @@
+system.img.raw
\ No newline at end of file
diff --git a/ginstall/testdata/proc/cmdline2 b/ginstall/testdata/proc/cmdline.0
similarity index 100%
rename from ginstall/testdata/proc/cmdline2
rename to ginstall/testdata/proc/cmdline.0
diff --git a/ginstall/testdata/proc/cmdline3 b/ginstall/testdata/proc/cmdline.1
similarity index 100%
rename from ginstall/testdata/proc/cmdline3
rename to ginstall/testdata/proc/cmdline.1
diff --git a/ginstall/testdata/proc/cmdline.android.0 b/ginstall/testdata/proc/cmdline.android.0
new file mode 100644
index 0000000..8fb7943
--- /dev/null
+++ b/ginstall/testdata/proc/cmdline.android.0
@@ -0,0 +1 @@
+mem=1024m@0m mem=1024m@2048m bmem=336m@672m bmem=256m@2048m brcm_cma=768m@2304m ramoops.mem_address=0x3F800000 ramoops.mem_size=0x800000 ramoops.console_size=0x400000 pmem=8m@1016m androidboot.selinux=permissive androidboot.hardware=gfhd254 androidboot.gfiber_system_img=system_a bootreason=main_chip_input
diff --git a/ginstall/testdata/proc/cmdline.android.1 b/ginstall/testdata/proc/cmdline.android.1
new file mode 100644
index 0000000..4da7328
--- /dev/null
+++ b/ginstall/testdata/proc/cmdline.android.1
@@ -0,0 +1 @@
+mem=1024m@0m mem=1024m@2048m bmem=336m@672m bmem=256m@2048m brcm_cma=768m@2304m ramoops.mem_address=0x3F800000 ramoops.mem_size=0x800000 ramoops.console_size=0x400000 pmem=8m@1016m androidboot.selinux=permissive androidboot.hardware=gfhd254 bootreason=main_chip_input androidboot.gfiber_system_img=system_b
diff --git a/ginstall/testdata/proc/cmdline.android.none b/ginstall/testdata/proc/cmdline.android.none
new file mode 100644
index 0000000..9fb507c
--- /dev/null
+++ b/ginstall/testdata/proc/cmdline.android.none
@@ -0,0 +1 @@
+mem=1024m@0m mem=1024m@2048m bmem=336m@672m bmem=256m@2048m brcm_cma=768m@2304m ramoops.mem_address=0x3F800000 ramoops.mem_size=0x800000 ramoops.console_size=0x400000 pmem=8m@1016m androidboot.selinux=permissive androidboot.hardware=gfhd254 bootreason=main_chip_input
diff --git a/ginstall/testdata/proc/cmdline1 b/ginstall/testdata/proc/cmdline.none
similarity index 100%
rename from ginstall/testdata/proc/cmdline1
rename to ginstall/testdata/proc/cmdline.none
diff --git a/ginstall/testdata/proc/cmdline5 b/ginstall/testdata/proc/cmdline.prowl.0
similarity index 100%
rename from ginstall/testdata/proc/cmdline5
rename to ginstall/testdata/proc/cmdline.prowl.0
diff --git a/ginstall/testdata/proc/cmdline6 b/ginstall/testdata/proc/cmdline.prowl.1
similarity index 100%
rename from ginstall/testdata/proc/cmdline6
rename to ginstall/testdata/proc/cmdline.prowl.1
diff --git a/ginstall/testdata/proc/cmdline4 b/ginstall/testdata/proc/cmdline.prowl.none
similarity index 100%
rename from ginstall/testdata/proc/cmdline4
rename to ginstall/testdata/proc/cmdline.prowl.none
diff --git a/ginstall/testdata/proc/mtd.GFHD254 b/ginstall/testdata/proc/mtd.GFHD254
new file mode 100644
index 0000000..21616d1
--- /dev/null
+++ b/ginstall/testdata/proc/mtd.GFHD254
@@ -0,0 +1,3 @@
+dev: size erasesize name
+mtd0: 00400000 00001000 "flash0.bolt"
+mtd1: 00400000 00001000 "flash0"
diff --git a/rcu_audio/ti-rcu-audio.cc b/rcu_audio/ti-rcu-audio.cc
index e174477..803ce3c 100644
--- a/rcu_audio/ti-rcu-audio.cc
+++ b/rcu_audio/ti-rcu-audio.cc
@@ -96,11 +96,19 @@
*
* The first three bytes of payload are some kind of RAS header.
* RAS_Decode() says the data pointer must include the three bytes
- * of header, but the length must not include those 3 bytes.
- * So the length is decremented by 6+1+1+3 for a total of 11.
+ * of header, but that the length must not include those 3 bytes.
+ *
+ * HOWEVER, the documentation is a filthy liar. RAS_Decode decrements
+ * its inputLength by three to account for the header length.
+ * If we obey the instructions and omit those 3 bytes from the
+ * length, the audio quality is noticeably worse because it starts
+ * throwing away three bytes of audio data.
+ *
+ * So both the pointer and the length passed to RAS_Decode *include*
+ * the header bytes.
*/
data = &ibuf[6 + 1 + 1];
- data_len = ilen - (6 + 1 + 1 + 3);
+ data_len = ilen - (6 + 1 + 1);
if (RAS_Decode(RAS_DECODE_TI_TYPE1, data, data_len, obuf, &olen) == 0) {
rcaudio::AudioSamples samples;
char bdaddr[18];
diff --git a/taxonomy/dhcp.py b/taxonomy/dhcp.py
index 26ecb82..e415126 100644
--- a/taxonomy/dhcp.py
+++ b/taxonomy/dhcp.py
@@ -31,6 +31,7 @@
'1,33,3,6,15,28,51,58,59': ['android'],
'1,3,6,28,33,51,58,59,121': ['android'],
'1,121,33,3,6,15,28,51,58,59,119': ['android'],
+ '1,3,6,15,26,28,51,58,59,43': ['android'],
'1,3,6,15,112,113,78,79,95,252': ['appletv1'],
diff --git a/taxonomy/ethernet.py b/taxonomy/ethernet.py
index 85a4522..295652d 100644
--- a/taxonomy/ethernet.py
+++ b/taxonomy/ethernet.py
@@ -53,12 +53,18 @@
'58:67:1a': ['barnes&noble'],
+ '2c:b0:5d': ['dish'],
+
'30:8c:fb': ['dropcam'],
'00:1a:11': ['google'],
+ '3c:5a:b4': ['google'],
'54:60:09': ['google'],
+ '94:95:a0': ['google'],
'94:eb:2c': ['google'],
'a4:77:33': ['google'],
+ 'f4:03:04': ['google'],
+ 'f4:f5:d8': ['google'],
'f4:f5:e8': ['google'],
'f8:8f:ca': ['google'],
@@ -192,12 +198,14 @@
'10:30:47': ['samsung'],
'14:32:d1': ['samsung'],
'14:49:e0': ['samsung'],
+ '18:21:95': ['samsung'],
'18:22:7e': ['samsung'],
'20:55:31': ['samsung'],
'20:6e:9c': ['samsung'],
'24:4b:81': ['samsung'],
'28:27:bf': ['samsung'],
'28:ba:b5': ['samsung'],
+ '2c:0e:3d': ['samsung'],
'2c:ae:2b': ['samsung'],
'30:19:66': ['samsung'],
'34:23:ba': ['samsung'],
@@ -236,10 +244,12 @@
'94:35:0a': ['samsung'],
'94:b1:0a': ['samsung'],
'a0:0b:ba': ['samsung'],
+ 'a4:08:ea': ['samsung'],
'a8:06:00': ['samsung'],
'ac:36:13': ['samsung'],
'ac:5f:3e': ['samsung'],
'b0:47:bf': ['samsung'],
+ 'b0:c5:59': ['samsung'],
'b0:df:3a': ['samsung'],
'b0:ec:71': ['samsung'],
'b4:07:f9': ['samsung'],
@@ -273,6 +283,7 @@
'f0:25:b7': ['samsung'],
'f4:09:d8': ['samsung'],
'f8:04:2e': ['samsung'],
+ 'fc:19:10': ['samsung'],
'fc:f1:36': ['samsung'],
'00:d9:d1': ['sony'],
@@ -281,9 +292,11 @@
'40:b8:37': ['sony'],
'58:48:22': ['sony'],
'b4:52:7e': ['sony'],
+ 'f8:d0:ac': ['sony'],
'10:08:c1': ['toshiba'],
+ '00:6b:9e': ['vizio'],
'a4:8d:3b': ['vizio'],
'b4:79:a7': ['wink'],
diff --git a/taxonomy/pcaptest.py b/taxonomy/pcaptest.py
index 3e2ea1f..116b4d1 100644
--- a/taxonomy/pcaptest.py
+++ b/taxonomy/pcaptest.py
@@ -51,6 +51,8 @@
('', './testdata/pcaps/Samsung Infuse 5GHz.pcap'),
('', './testdata/pcaps/Samsung Vibrant 2.4GHz.pcap'),
('', './testdata/pcaps/Sony Ericsson Xperia X10 2.4GHz.pcap'),
+ ('', './testdata/pcaps/Sony NSX-48GT1 2.4GHz Broadcast Probe.pcap'),
+ ('', './testdata/pcaps/Sony NSX-48GT1 2.4GHz Specific Probe.pcap'),
# Names where the identified species doesn't exactly match the filename,
# usually because multiple devices are too similar to distinguish. We name
@@ -65,16 +67,15 @@
('Amazon Kindle', './testdata/pcaps/Amazon Kindle Voyage 2.4GHz B054.pcap'),
('iPad 1st or 2nd gen', './testdata/pcaps/iPad 1st gen 5GHz.pcap'),
('iPad 1st or 2nd gen', './testdata/pcaps/iPad 2nd gen 5GHz.pcap'),
- ('iPad 4th gen or Air 1st gen', './testdata/pcaps/iPad (4th gen) 5GHz.pcap'),
- ('iPad 4th gen or Air 1st gen', './testdata/pcaps/iPad (4th gen) 2.4GHz.pcap'),
- ('iPad 4th gen or Air 1st gen', './testdata/pcaps/iPad Air 5GHz.pcap'),
- ('iPad 4th gen or Air 1st gen', './testdata/pcaps/iPad Air 2.4GHz.pcap'),
- ('iPhone 6/6+', './testdata/pcaps/iPhone 6 5GHz.pcap'),
- ('iPhone 6/6+', './testdata/pcaps/iPhone 6+ 5GHz.pcap'),
+ ('iPhone 6/6+', './testdata/pcaps/iPhone 6 5GHz iOS 9.pcap'),
+ ('iPhone 6/6+', './testdata/pcaps/iPhone 6+ 5GHz iOS 9.pcap'),
('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s 2.4GHz.pcap'),
+ ('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s 5GHz.pcap'),
('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s+ 2.4GHz.pcap'),
('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s+ 2.4GHz RRM.pcap'),
- ('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s 5GHz.pcap'),
+ ('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s 2.4GHz MKRD2LL iOS 10.0.2 Specific Probe.pcap'),
+ ('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s+ 2.4GHz iOS 10.0.2 Broadcast Probe.pcap'),
+ ('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s+ 2.4GHz iOS 10.0.2 Specific Probe.pcap'),
('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s+ 5GHz.pcap'),
('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s+ 5GHz RRM.pcap'),
('iPod Touch 1st or 2nd gen', './testdata/pcaps/iPod Touch 1st gen 2.4GHz.pcap'),
diff --git a/taxonomy/testdata/dhcp.leases b/taxonomy/testdata/dhcp.leases
index 51d4538..f46e1f1 100644
--- a/taxonomy/testdata/dhcp.leases
+++ b/taxonomy/testdata/dhcp.leases
@@ -12,7 +12,7 @@
1432237016 c8:69:cd:5e:b5:43 192.168.42.5 Apple-TV *
1432237016 6c:29:95:7c:25:fe 192.168.42.6 * *
1432237016 b0:34:95:02:66:83 192.168.42.7 iPaad-4th-gen *
-1432237016 04:69:f8:6b:99:5e 192.168.42.8 iPad-Air-2nd-gen *
+1432237016 04:69:f8:00:00:00 192.168.42.8 iPad-Air-2nd-gen *
1432237016 1c:e6:2b:9b:41:91 192.168.42.9 iPaad-Mini-1st-gen *
1432237016 84:8e:0c:99:48:d5 192.168.42.10 iPaad-Mini-2nd-gen *
1432237016 24:ab:81:e4:74:bc 192.168.42.11 iPhoone-4 *
@@ -22,14 +22,14 @@
1432237016 f0:db:e2:61:db:fa 192.168.42.13 iPhoone-6 *
1432237016 c8:85:50:e9:74:58 192.168.42.14 iPhoone-6+ *
1432237016 00:cd:fe:a7:47:96 192.168.42.15 iPhoone-6s *
-1432237016 68:db:ca:37:10:d8 192.168.42.16 iPhoone-6s+ *
+1432237016 68:db:ca:00:00:00 192.168.42.16 iPhoone-6s+ *
1432237016 00:1d:4f:0f:ee:14 192.168.42.17 iPood-Touch-1 *
1432237016 f0:b4:79:9d:28:0d 192.168.42.18 iPood-Touch-4 *
1432237016 3c:15:c2:d0:1b:0e 192.168.42.19 MacBoookPro2013 *
1432237016 10:2f:6b:ec:78:ff 192.168.42.20 NokiaLumia635 *
1432237016 08:05:81:21:68:57 192.168.42.21 Roku4 *
1432237016 5c:f6:dc:16:6a:17 192.168.42.22 SamsungSmartTV *
-1432237016 6c:40:08:55:76:8a 192.168.42.23 iPhoone-5s *
+1432237016 6c:40:08:00:00:00 192.168.42.23 iPhoone-5s *
1432237016 00:23:12:28:de:6e 192.168.42.24 AppleTV1
1432237016 28:cf:da:24:f4:ab 192.168.42.25 AppleTV2
1432237016 68:64:4b:11:ce:2b 192.168.42.26 AppleTV3A
@@ -69,7 +69,12 @@
1432237016 a4:d1:d2:00:00:00 192.168.42.58 iPaadOldiOS
1432237016 70:48:0f:00:00:00 192.168.42.59 iPadPro12_9
1432237016 6c:c2:17:00:00:00 192.168.42.60 HPPrinter
-1432237016 dc:2b:2a:95:bc:77 192.168.42.61 iPhoone 6s+
+1432237016 dc:2b:2a:00:00:00 192.168.42.61 iPhoone 6s+
1432237016 2c:33:61:00:00:00 192.168.42.62 iPhoone 7
1432237016 58:bd:a3:00:00:00 192.168.42.63 Wii
1432237016 28:0d:fc:00:00:00 192.168.42.64 Playstation 3
+1432237016 2c:1f:23:00:00:00 192.168.42.65 iPaadAir2ndGen
+1432237016 e0:b5:2d:00:00:00 192.168.42.66 iPhoone-6+
+1432237016 6c:72:e7:00:00:00 192.168.42.67 iPhoone-6s
+1432237016 f0:db:e2:00:00:00 192.168.42.68 iPhoone-6
+1432237016 b8:53:ac:00:00:00 192.168.42.67 iPhoone-7
diff --git a/taxonomy/testdata/dhcp.signatures b/taxonomy/testdata/dhcp.signatures
index 614f33d..5afd0d6 100644
--- a/taxonomy/testdata/dhcp.signatures
+++ b/taxonomy/testdata/dhcp.signatures
@@ -4,7 +4,7 @@
c8:69:cd:5e:b5:43 1,121,3,6,15,119,252
6c:29:95:7c:25:fe 1,121,33,3,6,12,15,26,28,51,54,58,59,119,252
b0:34:95:02:66:83 1,3,6,15,119,252
-04:69:f8:6b:99:5e 1,3,6,15,119,252
+04:69:f8:00:00:00 1,3,6,15,119,252
1c:e6:2b:9b:41:91 1,3,6,15,119,252
84:8e:0c:99:48:d5 1,3,6,15,119,252
24:ab:81:e4:74:bc 1,3,6,15,119,252
@@ -14,14 +14,14 @@
f0:db:e2:61:db:fa 1,3,6,15,119,252
c8:85:50:e9:74:58 1,3,6,15,119,252
00:cd:fe:a7:47:96 1,3,6,15,119,252
-68:db:ca:37:10:d8 1,3,6,15,119,252
+68:db:ca:00:00:00 1,3,6,15,119,252
00:1d:4f:0f:ee:14 1,3,6,15,119,95,252,44,46,47
f0:b4:79:9d:28:0d 1,3,6,15,119,252
3c:15:c2:d0:1b:0e 1,3,6,15,119,95,252,44,46
10:2f:6b:ec:78:ff 1,15,3,6,44,46,47,31,33,121,249,252,43
08:05:81:21:68:57 1,3,6,15,12
5c:f6:dc:16:6a:17 1,3,6,12,15,28,42,125
-6c:40:08:55:76:8a 1,3,6,15,119,252
+6c:40:08:00:00:00 1,3,6,15,119,252
00:23:12:28:de:6e 1,3,6,15,112,113,78,79,95,252
28:cf:da:24:f4:ab 1,3,6,15,119,252
68:64:4b:11:ce:2b 1,3,6,15,119,252
@@ -61,7 +61,12 @@
a4:d1:d2:00:00:00 1,3,6,15,119,252
70:48:0f:00:00:00 1,3,6,15,119,252
6c:c2:17:00:00:00 6,3,1,15,66,67,13,44,12,81,252
-dc:2b:2a:95:bc:77 1,3,6,15,119,252
+dc:2b:2a:00:00:00 1,3,6,15,119,252
2c:33:61:00:00:00 1,3,6,15,119,252
58:bd:a3:00:00:00 1,3,6,15,28,33
28:0d:fc:00:00:00 1,3,15,6
+2c:1f:23:00:00:00 1,121,3,6,15,119,252
+e0:b5:2d:00:00:00 1,121,3,6,15,119,252
+6c:72:e7:00:00:00 1,121,3,6,15,119,252
+f0:db:e2:00:00:00 1,121,3,6,15,119,252
+b8:53:ac:00:00:00 1,121,3,6,15,119,252
diff --git a/taxonomy/testdata/pcaps/Chromecast Ultra 2.4GHz.pcap b/taxonomy/testdata/pcaps/Chromecast Ultra 2.4GHz.pcap
new file mode 100644
index 0000000..0cbd07f
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Chromecast Ultra 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Chromecast Ultra 5GHz.pcap b/taxonomy/testdata/pcaps/Chromecast Ultra 5GHz.pcap
new file mode 100644
index 0000000..3166217
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Chromecast Ultra 5GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Chromecast v2 2.4GHz Audio.pcap b/taxonomy/testdata/pcaps/Chromecast v2 2.4GHz Audio.pcap
new file mode 100644
index 0000000..1913498
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Chromecast v2 2.4GHz Audio.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Chromecast v2 5GHz Audio.pcap b/taxonomy/testdata/pcaps/Chromecast v2 5GHz Audio.pcap
new file mode 100644
index 0000000..d0af810
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Chromecast v2 5GHz Audio.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Dish Network Receiver 2.4GHz ViP722k Broadcast Probe.pcap b/taxonomy/testdata/pcaps/Dish Network Receiver 2.4GHz ViP722k Broadcast Probe.pcap
new file mode 100644
index 0000000..d3c7ea8
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Dish Network Receiver 2.4GHz ViP722k Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Dish Network Receiver 2.4GHz ViP722k Specific Probe.pcap b/taxonomy/testdata/pcaps/Dish Network Receiver 2.4GHz ViP722k Specific Probe.pcap
new file mode 100644
index 0000000..3985f98
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Dish Network Receiver 2.4GHz ViP722k Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Google Home 2.4GHz.pcap b/taxonomy/testdata/pcaps/Google Home 2.4GHz.pcap
new file mode 100644
index 0000000..11cfde3
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Google Home 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Google Home 5GHz.pcap b/taxonomy/testdata/pcaps/Google Home 5GHz.pcap
new file mode 100644
index 0000000..bdbecc8
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Google Home 5GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Pixel Phone 2.4GHz XL.pcap b/taxonomy/testdata/pcaps/Pixel Phone 2.4GHz XL.pcap
new file mode 100644
index 0000000..386fa90
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Pixel Phone 2.4GHz XL.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Pixel Phone 2.4GHz.pcap b/taxonomy/testdata/pcaps/Pixel Phone 2.4GHz.pcap
new file mode 100644
index 0000000..3ff791b
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Pixel Phone 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Pixel Phone 5GHz XL.pcap b/taxonomy/testdata/pcaps/Pixel Phone 5GHz XL.pcap
new file mode 100644
index 0000000..c07af7b
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Pixel Phone 5GHz XL.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Pixel Phone 5GHz.pcap b/taxonomy/testdata/pcaps/Pixel Phone 5GHz.pcap
new file mode 100644
index 0000000..4091128
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Pixel Phone 5GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Sony NSX-48GT1 2.4GHz Broadcast Probe.pcap b/taxonomy/testdata/pcaps/Sony NSX-48GT1 2.4GHz Broadcast Probe.pcap
new file mode 100644
index 0000000..dbf2886
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Sony NSX-48GT1 2.4GHz Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Sony NSX-48GT1 2.4GHz Specific Probe.pcap b/taxonomy/testdata/pcaps/Sony NSX-48GT1 2.4GHz Specific Probe.pcap
new file mode 100644
index 0000000..ca5728d
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Sony NSX-48GT1 2.4GHz Specific Probe.pcap
Binary files differ
diff --git "a/taxonomy/testdata/pcaps/iPad \0504th gen\051 5GHz.pcap" "b/taxonomy/testdata/pcaps/iPad \0504th gen\051 5GHz.pcap"
deleted file mode 100644
index f7158b7..0000000
--- "a/taxonomy/testdata/pcaps/iPad \0504th gen\051 5GHz.pcap"
+++ /dev/null
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 1st gen 2.4GHz ME906LL iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPad Air 1st gen 2.4GHz ME906LL iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..a671244
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 1st gen 2.4GHz ME906LL iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 1st gen 2.4GHz ME906LL iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPad Air 1st gen 2.4GHz ME906LL iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..72bd72e
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 1st gen 2.4GHz ME906LL iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git "a/taxonomy/testdata/pcaps/iPad \0504th gen\051 2.4GHz.pcap" b/taxonomy/testdata/pcaps/iPad Air 1st gen 2.4GHz ME906LL iOS 9 Specific Probe.pcap
similarity index 83%
rename from "taxonomy/testdata/pcaps/iPad \0504th gen\051 2.4GHz.pcap"
rename to taxonomy/testdata/pcaps/iPad Air 1st gen 2.4GHz ME906LL iOS 9 Specific Probe.pcap
index 7eb0924..21a1acc 100644
--- "a/taxonomy/testdata/pcaps/iPad \0504th gen\051 2.4GHz.pcap"
+++ b/taxonomy/testdata/pcaps/iPad Air 1st gen 2.4GHz ME906LL iOS 9 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2.4GHz.pcap b/taxonomy/testdata/pcaps/iPad Air 1st gen 2.4GHz.pcap
similarity index 100%
rename from taxonomy/testdata/pcaps/iPad Air 2.4GHz.pcap
rename to taxonomy/testdata/pcaps/iPad Air 1st gen 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 1st gen 5GHz ME906LL iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPad Air 1st gen 5GHz ME906LL iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..4aea434
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 1st gen 5GHz ME906LL iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 1st gen 5GHz ME906LL iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPad Air 1st gen 5GHz ME906LL iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..b73645e
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 1st gen 5GHz ME906LL iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 5GHz.pcap b/taxonomy/testdata/pcaps/iPad Air 1st gen 5GHz.pcap
similarity index 100%
rename from taxonomy/testdata/pcaps/iPad Air 5GHz.pcap
rename to taxonomy/testdata/pcaps/iPad Air 1st gen 5GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MGTX2LL iOS 10.0.2.pcap b/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MGTX2LL iOS 10.0.2.pcap
new file mode 100644
index 0000000..a5a99aa
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MGTX2LL iOS 10.0.2.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MGTX2LL.pcap b/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MGTX2LL.pcap
new file mode 100644
index 0000000..cf87d66
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MGTX2LL.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MH1J2LL iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MH1J2LL iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..6426214
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MH1J2LL iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MH1J2LL iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MH1J2LL iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..7fe499a
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MH1J2LL iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz.pcap b/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz.pcap
deleted file mode 100644
index d900fed..0000000
--- a/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz.pcap
+++ /dev/null
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MGTX2LL iOS 10.0.2.pcap b/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MGTX2LL iOS 10.0.2.pcap
new file mode 100644
index 0000000..e6ffc18
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MGTX2LL iOS 10.0.2.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MGTX2LL.pcap b/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MGTX2LL.pcap
new file mode 100644
index 0000000..06513ca
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MGTX2LL.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MH1J2LL iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MH1J2LL iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..9a22b8a
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MH1J2LL iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MH1J2LL iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MH1J2LL iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..a398f73
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MH1J2LL iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz.pcap b/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz.pcap
deleted file mode 100644
index 67ff550..0000000
--- a/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz.pcap
+++ /dev/null
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Mini 1st gen 2.4GHz.pcap b/taxonomy/testdata/pcaps/iPad Mini 1st gen 2.4GHz MD528LL.pcap
similarity index 100%
rename from taxonomy/testdata/pcaps/iPad Mini 1st gen 2.4GHz.pcap
rename to taxonomy/testdata/pcaps/iPad Mini 1st gen 2.4GHz MD528LL.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Mini 1st gen 5GHz.pcap b/taxonomy/testdata/pcaps/iPad Mini 1st gen 5GHz MD528LL.pcap
similarity index 100%
rename from taxonomy/testdata/pcaps/iPad Mini 1st gen 5GHz.pcap
rename to taxonomy/testdata/pcaps/iPad Mini 1st gen 5GHz MD528LL.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 5 2.4GHz MD654LL iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPhone 5 2.4GHz MD654LL iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..0a0a5ec
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 5 2.4GHz MD654LL iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 5 2.4GHz MD654LL iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPhone 5 2.4GHz MD654LL iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..d715d41
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 5 2.4GHz MD654LL iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 5 5GHz MD654LL iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPhone 5 5GHz MD654LL iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..0e05542
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 5 5GHz MD654LL iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 5 5GHz MD654LL iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPhone 5 5GHz MD654LL iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..4f624a7
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 5 5GHz MD654LL iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 5s 2.4GHz iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPhone 5s 2.4GHz iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..097cbc1
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 5s 2.4GHz iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 5s 2.4GHz iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPhone 5s 2.4GHz iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..6ae7358
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 5s 2.4GHz iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 5s 2.4GHz.pcap b/taxonomy/testdata/pcaps/iPhone 5s 2.4GHz.pcap
index 4a966ac..d8f7ed4 100644
--- a/taxonomy/testdata/pcaps/iPhone 5s 2.4GHz.pcap
+++ b/taxonomy/testdata/pcaps/iPhone 5s 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 5s 5GHz iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPhone 5s 5GHz iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..1ded7bc
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 5s 5GHz iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 5s 5GHz iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPhone 5s 5GHz iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..ac2757b
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 5s 5GHz iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 5s 5GHz.pcap b/taxonomy/testdata/pcaps/iPhone 5s 5GHz.pcap
index 23cd7de..15bffe0 100644
--- a/taxonomy/testdata/pcaps/iPhone 5s 5GHz.pcap
+++ b/taxonomy/testdata/pcaps/iPhone 5s 5GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6 2.4GHz MG552LL iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6 2.4GHz MG552LL iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..7ff9271
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6 2.4GHz MG552LL iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6 2.4GHz MG552LL iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6 2.4GHz MG552LL iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..a592849
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6 2.4GHz MG552LL iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6 2.4GHz.pcap b/taxonomy/testdata/pcaps/iPhone 6 2.4GHz iOS 9.pcap
similarity index 100%
rename from taxonomy/testdata/pcaps/iPhone 6 2.4GHz.pcap
rename to taxonomy/testdata/pcaps/iPhone 6 2.4GHz iOS 9.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6 5GHz MG552LL iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6 5GHz MG552LL iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..1d83763
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6 5GHz MG552LL iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6 5GHz.pcap b/taxonomy/testdata/pcaps/iPhone 6 5GHz iOS 9.pcap
similarity index 100%
rename from taxonomy/testdata/pcaps/iPhone 6 5GHz.pcap
rename to taxonomy/testdata/pcaps/iPhone 6 5GHz iOS 9.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6+ 2.4GHz MGC02LL iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6+ 2.4GHz MGC02LL iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..1ca5f73
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6+ 2.4GHz MGC02LL iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6+ 2.4GHz MGC02LL iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6+ 2.4GHz MGC02LL iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..f5a8f47
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6+ 2.4GHz MGC02LL iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6+ 2.4GHz.pcap b/taxonomy/testdata/pcaps/iPhone 6+ 2.4GHz iOS 9.pcap
similarity index 100%
rename from taxonomy/testdata/pcaps/iPhone 6+ 2.4GHz.pcap
rename to taxonomy/testdata/pcaps/iPhone 6+ 2.4GHz iOS 9.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6+ 5GHz MGC02LL iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6+ 5GHz MGC02LL iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..a2cee5c
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6+ 5GHz MGC02LL iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6+ 5GHz.pcap b/taxonomy/testdata/pcaps/iPhone 6+ 5GHz iOS 9.pcap
similarity index 100%
rename from taxonomy/testdata/pcaps/iPhone 6+ 5GHz.pcap
rename to taxonomy/testdata/pcaps/iPhone 6+ 5GHz iOS 9.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6s 2.4GHz MKRD2LL iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6s 2.4GHz MKRD2LL iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..20776c4
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6s 2.4GHz MKRD2LL iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6s 5GHz GHz MKRD2LL iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6s 5GHz GHz MKRD2LL iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..36cac21
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6s 5GHz GHz MKRD2LL iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6s 5GHz GHz MKRD2LL iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6s 5GHz GHz MKRD2LL iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..e8a9eda
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6s 5GHz GHz MKRD2LL iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz RRM.pcap b/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz RRM.pcap
index 28cbed8..21e2825 100644
--- a/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz RRM.pcap
+++ b/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz RRM.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..f2e77cf
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..7e4d2ed
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz.pcap b/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz.pcap
index 6b0e932..b67817c 100644
--- a/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz.pcap
+++ b/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz RRM.pcap b/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz RRM.pcap
index 1ba43ad..4da3754 100644
--- a/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz RRM.pcap
+++ b/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz RRM.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..74364bf
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..6cb8b42
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz.pcap b/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz.pcap
index fdb82f9..cd9f32f 100644
--- a/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz.pcap
+++ b/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 7+ 2.4GHz.pcap b/taxonomy/testdata/pcaps/iPhone 7+ 2.4GHz.pcap
new file mode 100644
index 0000000..7e8ed23
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 7+ 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 7+ 5GHz.pcap b/taxonomy/testdata/pcaps/iPhone 7+ 5GHz.pcap
new file mode 100644
index 0000000..dc6971a
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 7+ 5GHz.pcap
Binary files differ
diff --git a/taxonomy/wifi.py b/taxonomy/wifi.py
index 36c92fc..3b497e2 100644
--- a/taxonomy/wifi.py
+++ b/taxonomy/wifi.py
@@ -176,6 +176,8 @@
('Chromecast', 'v1', '2.4GHz'),
'wifi4|probe:0,1,45,50,127,191,htcap:0062,htagg:03,htmcs:00000000,vhtcap:33c07030,vhtrxmcs:0124fffc,vhttxmcs:0124fffc,extcap:0000000000000040|assoc:0,1,48,127,221(0050f2,2),45,191,htcap:006e,htagg:03,htmcs:000000ff,vhtcap:33c07030,vhtrxmcs:0186fffe,vhttxmcs:0186fffe,extcap:0400000000000140|oui:google':
('Chromecast', 'v2', '5GHz'),
+ 'wifi4|probe:0,1,45,50,127,191,htcap:0062,htagg:03,htmcs:00000000,vhtcap:33c07030,vhtrxmcs:0124fffc,vhttxmcs:0124fffc,extcap:0000000000000040|assoc:0,1,48,127,221(0050f2,2),45,191,htcap:006e,htagg:03,htmcs:000000ff,vhtcap:33c07030,vhtrxmcs:0186fffe,vhttxmcs:0186fffe,extcap:0100000000000040|oui:google':
+ ('Chromecast', 'v2', '5GHz'),
'wifi4|probe:0,1,45,50,127,191,htcap:0062,htagg:03,htmcs:00000000,vhtcap:33c07030,vhtrxmcs:0124fffc,vhttxmcs:0124fffc,extcap:0000000000000040|assoc:0,1,33,36,48,127,221(0050f2,2),45,191,htcap:006e,htagg:03,htmcs:000000ff,vhtcap:33c07030,vhtrxmcs:0186fffe,vhttxmcs:0186fffe,extcap:0400000000000140|oui:google':
('Chromecast', 'v2', '5GHz'),
'wifi4|probe:0,1,45,50,127,191,htcap:0062,htagg:03,htmcs:00000000,vhtcap:33c07030,vhtrxmcs:0124fffc,vhttxmcs:0124fffc,extcap:0000000000000040|assoc:0,1,33,36,48,127,221(0050f2,2),45,191,htcap:006e,htagg:03,htmcs:000000ff,vhtcap:33c07030,vhtrxmcs:0186fffe,vhttxmcs:0186fffe,txpow:1308,extcap:0400000000000140|oui:google':
@@ -183,11 +185,20 @@
'wifi4|probe:0,1,3,45,50,127,191,htcap:0062,htagg:03,htmcs:00000000,vhtcap:33c07030,vhtrxmcs:0124fffc,vhttxmcs:0124fffc,extcap:0000000000000040|assoc:0,1,48,50,127,221(0050f2,2),45,htcap:002c,htagg:03,htmcs:000000ff,extcap:0000000000000140|oui:google':
('Chromecast', 'v2', '2.4GHz'),
+ 'wifi4|probe:0,1,45,50,59,127,191,htcap:0163,htagg:03,htmcs:00000000,vhtcap:33d071b0,vhtrxmcs:0168fffa,vhttxmcs:0168fffa,extcap:040000000100004000|assoc:0,1,48,59,127,221(0050f2,2),45,191,199,htcap:016f,htagg:03,htmcs:0000ffff,vhtcap:33d071b0,vhtrxmcs:009cfffa,vhttxmcs:009cfffa,extcap:050000000000004000|oui:google':
+ ('Chromecast', 'Ultra', '5GHz'),
+ 'wifi4|probe:0,1,3,45,50,59,127,191,htcap:0163,htagg:03,htmcs:00000000,vhtcap:33d071b0,vhtrxmcs:0168fffa,vhttxmcs:0168fffa,extcap:040000000100004000|assoc:0,1,33,48,50,59,70,127,221(0050f2,2),45,199,htcap:012d,htagg:03,htmcs:0000ffff,txpow:1400,extcap:040000000000014000|oui:google':
+ ('Chromecast', 'Ultra', '2.4GHz'),
+
'wifi4|probe:0,1,45,221(001018,2),221(00904c,51),htcap:007c,htagg:1a,htmcs:0000ffff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:007c,htagg:1a,htmcs:0000ffff,txpow:1408|os:directv':
('DirecTV', 'HR44 or HD54', '5GHz'),
'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:107c,htagg:1a,htmcs:0000ffff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:107c,htagg:1a,htmcs:0000ffff,txpow:1608|os:directv':
('DirecTV', 'HR44 or HF54', '2.4GHz'),
+ # Noted from a ViP722k, likely matches other models
+ 'wifi4|probe:0,1,50,45,221(001018,2),221(00904c,51),htcap:186c,htagg:1a,htmcs:0000ffff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:186c,htagg:1a,htmcs:0000ffff,txpow:1408|oui:dish':
+ ('Dish Network Receiver', '', '2.4GHz'),
+
'wifi4|probe:0,1,45,htcap:106e,htagg:01,htmcs:000000ff|assoc:0,1,45,33,36,48,221(0050f2,2),htcap:106e,htagg:01,htmcs:000000ff,txpow:0e00|oui:dropcam':
('Dropcam', '', '5GHz'),
'wifi4|probe:0,1,50,45,htcap:002c,htagg:01,htmcs:000000ff|assoc:0,1,50,45,48,221(0050f2,2),htcap:002c,htagg:01,htmcs:000000ff|oui:dropcam':
@@ -205,6 +216,11 @@
'wifi4|probe:0,1,50,3,45,221(001018,2),221(00904c,51),htcap:0020,htagg:1a,htmcs:000000ff|assoc:0,1,48,50,45,221(001018,2),221(0050f2,2),htcap:0020,htagg:1a,htmcs:000000ff|os:epsonprinter':
('Epson Printer', '', '2.4GHz'),
+ 'wifi4|probe:0,1,45,50,127,191,htcap:0062,htagg:03,htmcs:00000000,vhtcap:33c07030,vhtrxmcs:0124fffc,vhttxmcs:0124fffc,extcap:0000000020000040|assoc:0,1,48,127,221(0050f2,2),45,191,htcap:006e,htagg:03,htmcs:000000ff,vhtcap:33c07030,vhtrxmcs:0186fffe,vhttxmcs:0186fffe,extcap:0000000020000040|oui:google':
+ ('Google Home', '', '5GHz'),
+ 'wifi4|probe:0,1,3,45,50,127,191,htcap:0062,htagg:03,htmcs:00000000,vhtcap:33c07030,vhtrxmcs:0124fffc,vhttxmcs:0124fffc,extcap:0000000020000040|assoc:0,1,48,50,127,221(0050f2,2),45,htcap:002c,htagg:03,htmcs:000000ff,extcap:0000000020000040|oui:google':
+ ('Google Home', '', '2.4GHz'),
+
'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:100c,htagg:19,htmcs:000000ff|os:hpprinter':
('HP Printer', '', '2.4GHz'),
'wifi4|probe:0,1,50,45,221(001018,2),221(00904c,51),htcap:102c,htagg:1b,htmcs:000000ff|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:102c,htagg:1b,htmcs:000000ff|os:hpprinter':
@@ -298,17 +314,29 @@
'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:0100,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0100,htagg:19,htmcs:000000ff,txpow:150c|os:ios':
('iPad', '3rd gen', '2.4GHz'),
+ # iPad Air 1st gen with iOS 9
'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01fe,htagg:1b,htmcs:0000ffff,extcap:00000804|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:01fe,htagg:1b,htmcs:0000ffff,txpow:e708|os:ios':
- ('iPad', '4th gen or Air 1st gen', '5GHz'),
+ ('iPad', 'Air 1st gen', '5GHz'),
'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01fe,htagg:1b,htmcs:0000ffff,extcap:00000804|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:01fe,htagg:1b,htmcs:0000ffff,txpow:e708|os:ios':
- ('iPad', '4th gen or Air 1st gen', '5GHz'),
+ ('iPad', 'Air 1st gen', '5GHz'),
'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01fe,htagg:1b,htmcs:0000ffff,extcap:00000004|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:01fe,htagg:1b,htmcs:0000ffff,txpow:e708|os:ios':
- ('iPad', '4th gen or Air 1st gen', '5GHz'),
+ ('iPad', 'Air 1st gen', '5GHz'),
'wifi4|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01bc,htagg:1b,htmcs:0000ffff,extcap:00000804|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:01bc,htagg:1b,htmcs:0000ffff,txpow:1805|os:ios':
- ('iPad', '4th gen or Air 1st gen', '2.4GHz'),
+ ('iPad', 'Air 1st gen', '2.4GHz'),
'wifi4|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01bc,htagg:1b,htmcs:0000ffff,extcap:00000804|assoc:0,1,33,36,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:01bc,htagg:1b,htmcs:0000ffff,txpow:1805|os:ios':
- ('iPad', '4th gen or Air 1st gen', '2.4GHz'),
+ ('iPad', 'Air 1st gen', '2.4GHz'),
+ # iPad Air 1st gen with iOS 10
+ 'wifi4|probe:0,1,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01fe,htagg:1b,htmcs:0000ffff,extcap:00000804|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:01fe,htagg:1b,htmcs:0000ffff,txpow:e708|os:ios':
+ ('iPad', 'Air 1st gen', '5GHz'),
+ 'wifi4|probe:0,1,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01fe,htagg:1b,htmcs:0000ffff,extcap:00000804|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:01fe,htagg:1b,htmcs:0000ffff,txpow:e708|os:ios':
+ ('iPad', 'Air 1st gen', '5GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01bc,htagg:1b,htmcs:0000ffff,extcap:00000804|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:01bc,htagg:1b,htmcs:0000ffff,txpow:1805|os:ios':
+ ('iPad', 'Air 1st gen', '2.4GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01bc,htagg:1b,htmcs:0000ffff,extcap:00000804|assoc:0,1,33,36,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:01bc,htagg:1b,htmcs:0000ffff,txpow:1805|os:ios':
+ ('iPad', 'Air 1st gen', '2.4GHz'),
+
+ # iPad Air 2nd gen with iOS 9. Signatures identical to iPhone 6s, use name to distinguish them.
'wifi4|probe:0,1,45,127,107,191,221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e002,extcap:0400000000000040|name:ipad':
('iPad', 'Air 2nd gen', '5GHz'),
'wifi4|probe:0,1,45,127,107,191,221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e002,extcap:0400000000000040|name:ipad':
@@ -328,6 +356,16 @@
'wifi4|probe:0,1,50,3,45,127,107,221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1502,extcap:0000000000000040|os:ios':
('iPad', 'Air 2nd gen', '2.4GHz'),
+ # iPad Air 2nd gen with iOS 10 changed the 5GHz tx power, no longer identical to iPhone 6s.
+ 'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0400088400000040|assoc:0,1,33,36,48,45,127,191,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:1302,extcap:0400000000000040|os:ios':
+ ('iPad', 'Air 2nd gen', '5GHz'),
+ 'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:1302,extcap:0400000000000040|os:ios':
+ ('iPad', 'Air 2nd gen', '5GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,107,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0400088400000040|assoc:0,1,50,33,36,48,45,127,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1502,extcap:0000000000000040|os:ios':
+ ('iPad', 'Air 2nd gen', '2.4GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,107,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1502,extcap:0000000000000040|os:ios':
+ ('iPad', 'Air 2nd gen', '2.4GHz'),
+
'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1807|os:ios':
('iPad Mini', '1st gen', '5GHz'),
'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1807|os:ios':
@@ -377,6 +415,7 @@
'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:0100,htagg:19,htmcs:000000ff|assoc:0,1,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0100,htagg:19,htmcs:000000ff|os:ios':
('iPhone 4s', '', '2.4GHz'),
+ # iPhone 5 with iOS 9 and prior.
'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1504|os:ios':
('iPhone 5', '', '5GHz'),
'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1504|os:ios':
@@ -386,6 +425,16 @@
'wifi4|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0020,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0020,htagg:1a,htmcs:000000ff,txpow:1403|os:ios':
('iPhone 5', '', '2.4GHz'),
+ # iPhone 5 with iOS 10.
+ 'wifi4|probe:0,1,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1504|os:ios':
+ ('iPhone 5', '', '5GHz'),
+ 'wifi4|probe:0,1,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1504|os:ios':
+ ('iPhone 5', '', '5GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0020,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:0020,htagg:1a,htmcs:000000ff,txpow:1403|os:ios':
+ ('iPhone 5', '', '2.4GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0020,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:0020,htagg:1a,htmcs:000000ff,txpow:1403|os:ios':
+ ('iPhone 5', '', '2.4GHz'),
+
'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1805|os:ios':
('iPhone 5c', '', '5GHz'),
'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000804|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1805|os:ios':
@@ -405,10 +454,18 @@
('iPhone 5s', '', '5GHz'),
'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1603|os:ios':
('iPhone 5s', '', '5GHz'),
+ 'wifi4|probe:0,1,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000804|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1603|os:ios':
+ ('iPhone 5s', '', '5GHz'),
+ 'wifi4|probe:0,1,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000804|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1603|os:ios':
+ ('iPhone 5s', '', '5GHz'),
'wifi4|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0020,htagg:1a,htmcs:000000ff,extcap:00000804|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0020,htagg:1a,htmcs:000000ff,txpow:1805|os:ios':
('iPhone 5s', '', '2.4GHz'),
'wifi4|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0020,htagg:1a,htmcs:000000ff,extcap:00000804|assoc:0,1,33,36,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0020,htagg:1a,htmcs:000000ff,txpow:1805|os:ios':
('iPhone 5s', '', '2.4GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0020,htagg:1a,htmcs:000000ff,extcap:00000804|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:0020,htagg:1a,htmcs:000000ff,txpow:1805|os:ios':
+ ('iPhone 5s', '', '2.4GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0020,htagg:1a,htmcs:000000ff,extcap:00000804|assoc:0,1,33,36,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:0020,htagg:1a,htmcs:000000ff,txpow:1805|os:ios':
+ ('iPhone 5s', '', '2.4GHz'),
'wifi4|probe:0,1,45,127,107,191,221(0050f2,8),221(001018,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(001018,2),221(0050f2,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:e002,extcap:0400000000000040|os:ios':
('iPhone 6/6+', '', '5GHz'),
@@ -433,6 +490,41 @@
'wifi4|probe:0,1,50,3,45,127,107,221(00904c,51),221(0050f2,8),221(001018,2),htcap:0021,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(001018,2),221(0050f2,2),htcap:0021,htagg:17,htmcs:000000ff,txpow:1402,extcap:0000000000000040|os:ios':
('iPhone 6+', '', '2.4GHz'),
+ # iPhone 6 with iOS 10 changed txpow, now distinguishable from iPhone 6+.
+ 'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,45,127,191,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:1202,extcap:0400000000000040|os:ios':
+ ('iPhone 6', '', '5GHz'),
+ 'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:1202,extcap:0400000000000040|os:ios':
+ ('iPhone 6', '', '5GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,107,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:0021,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,45,127,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:0021,htagg:17,htmcs:000000ff,txpow:1302,extcap:0000000000000040|os:ios':
+ ('iPhone 6', '', '2.4GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,107,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:0021,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:0021,htagg:17,htmcs:000000ff,txpow:1302,extcap:0000000000000040|os:ios':
+ ('iPhone 6', '', '2.4GHz'),
+
+ # iPhone 6+ with iOS 10 changed txpow, now distinguishable from iPhone 6.
+ 'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,45,127,191,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:1302,extcap:0400000000000040|os:ios':
+ ('iPhone 6+', '', '5GHz'),
+ 'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:1302,extcap:0400000000000040|os:ios':
+ ('iPhone 6+', '', '5GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,107,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:0021,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,45,127,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:0021,htagg:17,htmcs:000000ff,txpow:1402,extcap:0000000000000040|os:ios':
+ ('iPhone 6+', '', '2.4GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,107,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:0021,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:0021,htagg:17,htmcs:000000ff,txpow:1402,extcap:0000000000000040|os:ios':
+ ('iPhone 6+', '', '2.4GHz'),
+
+ # iPhone 6s/6s+ with iOS 10 changed txpow, now distinguishable on 5GHz. 2.4GHz signatures are identical.
+ 'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f815832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,45,127,191,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:1302,extcap:0400000000000040|os:ios':
+ ('iPhone 6s', '', '5GHz'),
+ 'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f815832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:1302,extcap:0400000000000040|os:ios':
+ ('iPhone 6s', '', '5GHz'),
+ 'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f815832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,45,127,191,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:1102,extcap:0400000000000040|os:ios':
+ ('iPhone 6s+', '', '5GHz'),
+ 'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f815832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:1102,extcap:0400000000000040|os:ios':
+ ('iPhone 6s+', '', '5GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,107,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,45,127,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1202,extcap:0000000000000040|os:ios':
+ ('iPhone 6s/6s+', '', '2.4GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,107,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1202,extcap:0000000000000040|os:ios':
+ ('iPhone 6s/6s+', '', '2.4GHz'),
+
+ # iOS 9 and earlier signature is identical between iPhone 6s and 6s+
'wifi4|probe:0,1,45,127,107,191,221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e002,extcap:0400000000000040|os:ios':
('iPhone 6s/6s+', '', '5GHz'),
'wifi4|probe:0,1,45,127,107,191,221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e002,extcap:0400000000000040|os:ios':
@@ -472,6 +564,10 @@
'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f807032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:00000884|assoc:0,1,33,36,48,70,54,45,127,191,199,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f811032,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:13f9,extcap:000008|os:ios':
('iPhone 7', '', '5GHz'),
+ 'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f817032,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:00000884|assoc:0,1,33,36,48,45,191,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f817032,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:13f9|os:ios':
+ ('iPhone 7+', '', '5GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,107,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:000000ff,extcap:00000884|assoc:0,1,50,33,36,48,70,45,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:13f9|os:ios':
+ ('iPhone 7+', '', '2.4GHz'),
'wifi4|probe:0,1,45,127,107,191,221(0050f2,8),221(001018,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,45,127,221(001018,2),221(0050f2,2),htcap:0063,htagg:17,htmcs:000000ff,txpow:e002,extcap:000008|os:ios':
('iPhone SE', '', '5GHz'),
@@ -789,6 +885,15 @@
'wifi4|probe:0,1,50,45,221(0050f2,4),htcap:01ad,htagg:02,htmcs:0000ffff,wps:WPS_SUPPLICANT_STATION|assoc:0,1,50,45,48,221(0050f2,2),htcap:01ad,htagg:02,htmcs:0000ffff|os:panasonictv':
('Panasonic TV', '', '2.4GHz'),
+ 'wifi4|probe:0,1,45,221(0050f2,8),191,127,htcap:01ef,htagg:1f,htmcs:0000ffff,vhtcap:339071b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:040000000000004080|assoc:0,1,33,36,48,70,45,221(0050f2,2),191,127,htcap:01ef,htagg:1f,htmcs:0000ffff,vhtcap:339071b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,txpow:1e08,extcap:04000a020100004080|oui:htc':
+ ('Pixel Phone', '', '5GHz'),
+ 'wifi4|probe:0,1,45,191,221(0050f2,8),127,htcap:01ef,htagg:df,htmcs:0000ffff,vhtcap:338001b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:04000a020100004080|assoc:0,1,48,45,221(0050f2,2),191,127,htcap:01ef,htagg:1f,htmcs:0000ffff,vhtcap:339071b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:04000a020100004080|oui:htc':
+ ('Pixel Phone', '', '5GHz'),
+ 'wifi4|probe:0,1,50,3,45,221(0050f2,8),127,htcap:01ad,htagg:1f,htmcs:0000ffff,extcap:040000000000000080|assoc:0,1,50,33,48,70,45,221(0050f2,2),127,htcap:01ad,htagg:1f,htmcs:0000ffff,txpow:1e08,extcap:04000a020100000080|oui:htc':
+ ('Pixel Phone', '', '2.4GHz'),
+ 'wifi4|probe:0,1,50,45,191,221(0050f2,8),3,127,htcap:01ef,htagg:df,htmcs:0000ffff,vhtcap:33800192,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:04000a020100004080|assoc:0,1,50,48,45,221(0050f2,2),127,htcap:01ad,htagg:1f,htmcs:0000ffff,extcap:04000a020100000080|oui:htc':
+ ('Pixel Phone', '', '2.4GHz'),
+
'wifi4|probe:0,1|assoc:0,1,221(005043,1)|os:playstation':
('Playstation', '3', '2.4GHz'),
diff --git a/wifi/configs.py b/wifi/configs.py
index 97a27ce..a4f20b8 100644
--- a/wifi/configs.py
+++ b/wifi/configs.py
@@ -22,6 +22,7 @@
'WifiHostapdDebug',
'WifiShortAggTimeout',
'WifiNoAggTimeout',
+ 'WifiNoAliveMonitor',
]
for _i in EXPERIMENTS:
experiment.register(_i)
diff --git a/wifi/qca9880_cal.py b/wifi/qca9880_cal.py
new file mode 100755
index 0000000..4e5cc0c
--- /dev/null
+++ b/wifi/qca9880_cal.py
@@ -0,0 +1,240 @@
+#!/usr/bin/python -S
+
+"""Check and fix mis-calibrated QCA9880 modules on gfrg200/gfrg210.
+
+ Some modules were delivered to customers mis-calibrated. This script will
+ check if the module is affected, and if so, generate a patch that will be
+ used after driver reload.
+"""
+import glob
+import os
+import os.path
+import experiment
+import utils
+
+NO_CAL_EXPERIMENT = 'WifiNoCalibrationPatch'
+PLATFORM_FILE = '/etc/platform'
+CALIBRATION_DIR = '/tmp/ath10k_cal'
+CAL_PATCH_FILE = 'cal_data_patch.bin'
+ATH10K_CAL_DATA = '/sys/kernel/debug/ieee80211/phy[0-9]*/ath10k/cal_data'
+OUI_OFFSET = 6
+OUI_LEN = 3
+VERSION_OFFSET = 45
+VERSION_LEN = 3
+SUSPECT_OUIS = ((0x28, 0x24, 0xff), (0x48, 0xa9, 0xd2), (0x60, 0x02, 0xb4),
+ (0xbc, 0x30, 0x7d), (0xbc, 0x30, 0x7e))
+MISCALIBRATED_VERSION_FIELD = (0x0, 0x0, 0x0)
+MODULE_PATH = '/sys/class/net/{}/device/driver/module'
+
+
+def _log(msg):
+ utils.log('ath10k calibration: {}'.format(msg))
+
+
+def _is_ath10k(interface):
+ """Check if interface is driven by the ath10k driver.
+
+ Args:
+ interface: The interface to be checked. eg wlan1
+
+ Returns:
+ True if ath10k, otherwise False.
+ """
+ try:
+ return os.readlink(MODULE_PATH.format(interface)).find('ath10k')
+ except OSError:
+ return False
+
+
+def _oui_string(oui):
+ """Convert OUI from bytes to a string.
+
+ Args:
+ oui: OUI in byte format.
+
+ Returns:
+ OUI is string format separated by ':'. Eg. 88:dc:96.
+ """
+ return ':'.join('{:02x}'.format(ord(b)) for b in oui)
+
+
+def _version_string(version):
+ """Convert version from bytes to a string.
+
+ Args:
+ version: version in byte format.
+
+ Returns:
+ Three byte version string in hex format: 0x00 0x00 0x00
+ """
+
+ return ' '.join('0x{:02x}'.format(ord(b)) for b in version)
+
+
+def _is_module_miscalibrated():
+ """Check the QCA8990 module to see if it is improperly calibrated.
+
+ There are two manufacturers of the modules, Senao and Wistron of which only
+ Wistron modules are suspect. Wistron provided a list of OUIs manufactured
+ which are listed in SUSPECT_OUIS. Modules manufactured by Winstron containing
+ V02 at offset VERSION_OFFSET have been corrected, while those containing 3
+ zero's at this offset are still suspect and will be considered mis-calibrated.
+
+ Returns:
+ True if module is mis-calibrated, None if it can't be determined, and False
+ otherwise.
+ """
+
+ try:
+ cal_data_path = _ath10k_cal_data_path()
+ if cal_data_path is None:
+ return None
+
+ with open(cal_data_path, mode='rb') as f:
+ f.seek(OUI_OFFSET)
+ oui = f.read(OUI_LEN)
+ f.seek(VERSION_OFFSET)
+ version = f.read(VERSION_LEN)
+
+ except IOError as e:
+ _log('unable to open cal_data {}: {}'.format(cal_data_path, e.strerror))
+ return None
+
+ if oui not in (bytearray(s) for s in SUSPECT_OUIS):
+ _log('OUI {} is properly calibrated.'.format(_oui_string(oui)))
+ return False
+
+ if version != (bytearray(MISCALIBRATED_VERSION_FIELD)):
+ _log('version field {} signals proper calibration.'.
+ format(_version_string(version)))
+ return False
+
+ _log('May be mis-calibrated. OUI: {} version: {}'.
+ format(_oui_string(oui), _version_string(version)))
+
+ return True
+
+
+def _is_previously_calibrated():
+ """Check if this calibration script already ran since the last boot.
+
+ Returns:
+ True if calibration checks already ran, False otherwise.
+ """
+ return os.path.exists(CALIBRATION_DIR)
+
+
+def _create_calibration_dir():
+ """Create calibration directory.
+
+ Calibration directory contains the calibration patch file.
+ If the directory is empty it signals that calibration checks have already
+ run.
+
+ Returns:
+ True if directory exists or is created, false if any error.
+ """
+ try:
+ if not os.path.isdir(CALIBRATION_DIR):
+ os.makedirs(CALIBRATION_DIR)
+ return True
+ except OSError as e:
+ _log('unable to create calibration dir {}: {}.'.
+ format(CALIBRATION_DIR, e.strerror))
+ return False
+
+ return True
+
+
+def _ath10k_cal_data_path():
+ """Find the current path to cal data.
+
+ This path encodes the phy number, which is usually phy1, but if the
+ driver load order changed or if this runs after a reload, the phy
+ number will change.
+
+ Returns:
+ Path to cal_data in debugfs.
+ """
+
+ return glob.glob(ATH10K_CAL_DATA)[0]
+
+
+def _generate_calibration_patch():
+ """Create calibration patch and write to storage.
+
+ Returns:
+ True for success or False for failure.
+ """
+ try:
+ with open(_ath10k_cal_data_path(), mode='rb') as f:
+ cal_data = bytearray(f.read())
+ except IOError as e:
+ _log('cal patch: unable to open for read {}: {}.'.
+ format(_ath10k_cal_data_path(), e.strerror))
+ return False
+
+ # Patch cal_data here once we get the actual calibration data.
+ # For now just return False until we get the data.
+ _log('patch not generated as data not supplied yet.')
+ # pylint: disable=unreachable
+ return False
+
+ if not _create_calibration_dir():
+ return False
+
+ try:
+ patched_file = os.path.join(CALIBRATION_DIR, CAL_PATCH_FILE)
+ open(patched_file, 'wb').write(cal_data)
+ except IOError as e:
+ _log('unable to open for writing {}: {}.'.format(patched_file, e.strerror))
+ return False
+
+ return True
+
+
+def _reload_driver():
+ """Reload the ath10k driver so it picks up modified calibration file."""
+ ret = utils.subprocess_quiet(('rmmod', 'ath10k_pci'))
+ if ret != 0:
+ _log('rmmod ath10k_pci failed: {}.'.format(ret))
+ return
+
+ ret = utils.subprocess_quiet(('modprobe', 'ath10k_pci'))
+ if ret != 0:
+ _log('modprobe ath10k_pci failed: {}.'.format(ret))
+ return
+
+ _log('reload ath10k driver complete')
+
+
+def qca8990_calibration():
+ """Main QCA8990 calibration check."""
+
+ if experiment.enabled(NO_CAL_EXPERIMENT):
+ _log('experiment {} on. Skip calibration check.'.format(NO_CAL_EXPERIMENT))
+ return
+
+ if _is_previously_calibrated():
+ _log('calibration check completed earlier.')
+ return
+
+ if not _is_ath10k('wlan1'):
+ _log('this platform does not use ath10k.')
+ return
+
+ cal_result = _is_module_miscalibrated()
+ if cal_result is None:
+ _log('unknown if miscalibrated.')
+ elif not cal_result:
+ _log('module is NOT miscalibrated.')
+ # Creating an empty directory signals that this script has already run.
+ _create_calibration_dir()
+ else:
+ if _generate_calibration_patch():
+ _log('generated new patch.')
+ _reload_driver()
+
+
+if __name__ == '__main__':
+ qca8990_calibration()
diff --git a/wifi/quantenna.py b/wifi/quantenna.py
index f3f96ef..9e0d26b 100755
--- a/wifi/quantenna.py
+++ b/wifi/quantenna.py
@@ -11,7 +11,7 @@
def _get_quantenna_interfaces():
- return subprocess.check_output(['get-quantenna-interfaces']).split()
+ return utils.read_or_empty('/sys/class/net/quantenna/vlan')
def _qcsapi(*args):
@@ -27,13 +27,6 @@
return ':'.join(octets)
-def _get_vlan(hif):
- m = re.search(r'VID: (\d+)', utils.read_or_empty('/proc/net/vlan/%s' % hif))
- if m:
- return int(m.group(1))
- raise utils.BinWifiException('no VLAN ID for interface %s' % hif)
-
-
def _get_interfaces(mode, suffix):
# Each host interface (hif) maps to exactly one LHOST interface (lif) based on
# the VLAN ID as follows: the lif is wifiX where X is the VLAN ID - 2 (VLAN
@@ -41,11 +34,12 @@
# VLAN ID 2.
prefix = 'wlan' if mode == 'ap' else 'wcli'
suffix = r'.*' if suffix == 'ALL' else suffix
- for hif in _get_quantenna_interfaces():
+ for line in _get_quantenna_interfaces().splitlines():
+ hif, vlan = line.split()
+ vlan = int(vlan)
+ lif = 'wifi%d' % (vlan - 2)
+ mac = _get_external_mac(hif)
if re.match(r'^' + prefix + r'\d*' + suffix + r'$', hif):
- vlan = _get_vlan(hif)
- lif = 'wifi%d' % (vlan - 2)
- mac = _get_external_mac(hif)
yield hif, lif, mac, vlan
@@ -54,18 +48,21 @@
def _set_link_state(hif, state):
- subprocess.check_output(['if' + state, hif])
+ subprocess.check_call(['if' + state, hif])
def _ifplugd_action(hif, state):
- subprocess.check_output(['/etc/ifplugd/ifplugd.action', hif, state])
+ subprocess.check_call(['/etc/ifplugd/ifplugd.action', hif, state])
def _parse_scan_result(line):
# Scan result format:
#
- # "Quantenna1" 00:26:86:00:11:5f 60 56 1 2 1 2 0 15 80
- # | | | | | | | | | | |
+ # "Quantenna1" 00:26:86:00:11:5f 60 56 1 2 1 2 0 15 80 100 1 Infrastructure
+ # | | | | | | | | | | | | | |
+ # | | | | | | | | | | | | | Mode
+ # | | | | | | | | | | | | DTIM interval
+ # | | | | | | | | | | | Beacon interval
# | | | | | | | | | | Maximum bandwidth
# | | | | | | | | | WPS flags
# | | | | | | | | Qhop flags
@@ -80,7 +77,7 @@
#
# The SSID may contain quotes and spaces. Split on whitespace from the right,
# making at most 10 splits, to preserve spaces in the SSID.
- sp = line.strip().rsplit(None, 10)
+ sp = line.strip().rsplit(None, 13)
return sp[0][1:-1], sp[1], int(sp[2]), -float(sp[3]), int(sp[4]), int(sp[5])
diff --git a/wifi/quantenna_test.py b/wifi/quantenna_test.py
index b0fb485..00400ed 100755
--- a/wifi/quantenna_test.py
+++ b/wifi/quantenna_test.py
@@ -16,37 +16,25 @@
@wvtest.wvtest
-def get_vlan_test():
- old_read_or_empty = utils.read_or_empty
- utils.read_or_empty = lambda _: 'wlan0 VID: 3 REORDER_HDR: 1'
- wvtest.WVPASSEQ(quantenna._get_vlan('wlan0'), 3)
- utils.read_or_empty = lambda _: ''
- wvtest.WVEXCEPT(utils.BinWifiException, quantenna._get_vlan, 'wlan0')
- utils.read_or_empty = old_read_or_empty
-
-
-@wvtest.wvtest
def get_interface_test():
old_get_quantenna_interfaces = quantenna._get_quantenna_interfaces
old_get_external_mac = quantenna._get_external_mac
- old_get_vlan = quantenna._get_vlan
- quantenna._get_quantenna_interfaces = lambda: ['wlan0', 'wlan0_portal']
+ quantenna._get_quantenna_interfaces = lambda: 'wlan0 3\nwlan0_portal 4\n'
quantenna._get_external_mac = lambda _: '00:00:00:00:00:00'
- quantenna._get_vlan = lambda _: 3
wvtest.WVPASSEQ(quantenna._get_interface('ap', ''),
('wlan0', 'wifi1', '00:00:00:00:00:00', 3))
wvtest.WVPASSEQ(quantenna._get_interface('ap', '_portal'),
- ('wlan0_portal', 'wifi1', '00:00:00:00:00:00', 3))
+ ('wlan0_portal', 'wifi2', '00:00:00:00:00:00', 4))
wvtest.WVPASSEQ(quantenna._get_interface('sta', ''),
(None, None, None, None))
- quantenna._get_vlan = old_get_vlan
quantenna._get_external_mac = old_get_external_mac
quantenna._get_quantenna_interfaces = old_get_quantenna_interfaces
@wvtest.wvtest
def parse_scan_result_test():
- result = ' " ssid with "quotes" " 00:11:22:33:44:55 40 25 0 0 0 0 0 1 40 '
+ result = (' " ssid with "quotes" " 00:11:22:33:44:55 40 25 0 0 0 0 0 1 40 '
+ '100 1 Infrastructure')
wvtest.WVPASSEQ(quantenna._parse_scan_result(result),
(' ssid with "quotes" ', '00:11:22:33:44:55', 40, -25, 0, 0))
diff --git a/wifi/wifi.py b/wifi/wifi.py
index 4204791..3773f03 100755
--- a/wifi/wifi.py
+++ b/wifi/wifi.py
@@ -19,6 +19,7 @@
import iw
import options
import persist
+import qca9880_cal
import quantenna
import utils
@@ -60,6 +61,7 @@
_FINGERPRINTS_DIRECTORY = '/tmp/wifi/fingerprints'
_LOCKFILE = '/tmp/wifi/wifi'
+_PLATFORM_FILE = '/etc/platform'
lockfile_taken = False
@@ -254,6 +256,9 @@
'no wifi interface found for band=%s channel=%s suffix=%s',
band, channel, opt.interface_suffix)
+ # Check for calibration errors on ath10k.
+ qca9880_cal.qca8990_calibration()
+
found_active_config = False
for other_interface in (set(iw.find_all_interfaces_from_phy(phy)) -
set([interface])):
@@ -532,12 +537,12 @@
raise utils.BinWifiException('No client interface for band %s', band)
scan_args = []
+ if opt.scan_freq:
+ scan_args += ['freq', str(opt.scan_freq)]
if opt.scan_ap_force:
scan_args += ['ap-force']
if opt.scan_passive:
scan_args += ['passive']
- if opt.scan_freq:
- scan_args += ['freq', opt.scan_freq]
print(iw.scan(interface, scan_args))
@@ -546,7 +551,7 @@
def _is_hostapd_running(interface):
return utils.subprocess_quiet(
- ('hostapd_cli', '-i', interface, 'status'), no_stdout=True) == 0
+ ('hostapd_cli', '-i', interface, 'quit'), no_stdout=True) == 0
def _wpa_cli(program, interface, command):
@@ -583,6 +588,18 @@
return None
+def _is_wind_charger():
+ try:
+ etc_platform = open(_PLATFORM_FILE).read()
+ if etc_platform[:-1] == 'GFMN100':
+ return True
+ else:
+ return False
+ except IOError as e:
+ print('_is_wind_charger: cant open %s: %s' % (_PLATFORM_FILE, e.strerror))
+ return False
+
+
def _start_hostapd(interface, config_filename, band, ssid):
"""Starts a babysat hostapd.
@@ -618,9 +635,15 @@
alivemonitor_filename = utils.get_filename(
'hostapd', utils.FILENAME_KIND.alive, interface, tmp=True)
+ # Don't use alivemonitor on Windcharger since no waveguide. b/32376077
+ if _is_wind_charger() or experiment.enabled('WifiNoAliveMonitor'):
+ alive_monitor = []
+ else:
+ alive_monitor = ['alivemonitor', alivemonitor_filename, '30', '2', '65']
+
utils.log('Starting hostapd.')
- utils.babysit(['alivemonitor', alivemonitor_filename, '30', '2', '65',
- 'hostapd',
+ utils.babysit(alive_monitor +
+ ['hostapd',
'-A', alivemonitor_filename,
'-F', _FINGERPRINTS_DIRECTORY] +
bandsteering.hostapd_options(band, ssid) +
@@ -894,8 +917,12 @@
"Couldn't stop hostapd to start wpa_supplicant.")
if already_running:
+ subprocess.check_call(['ifdown', interface])
+ subprocess.check_call(['/etc/ifplugd/ifplugd.action', interface, 'down'])
if not _reconfigure_wpa_supplicant(interface):
raise utils.BinWifiException('Failed to reconfigure wpa_supplicant.')
+ subprocess.check_call(['ifup', interface])
+ subprocess.check_call(['/etc/ifplugd/ifplugd.action', interface, 'up'])
elif not _start_wpa_supplicant(interface, tmp_config_filename):
raise utils.BinWifiException(
'wpa_supplicant failed to start. Look at wpa_supplicant logs for '
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) );