Make path, channel and unit handling smarter.

This fixes issues with reports that aren't in the same directory as
report.py, with running report.py and sample.py from other
directories, and with Iperf automatically scaling units.

It doesn't do some of the other things I've talked about, like
converting Iperf units to more sensible ones, or finding channel
boundaries correctly on Linux VHT channels. I'm thinking that both are
clever enough they're best done in a later FiberCL.

Change-Id: Ia8dcee3a04c26e53475c686203ffbc325405d611
diff --git a/wifitables/report.py b/wifitables/report.py
index ffd6b7e..6289cd5 100755
--- a/wifitables/report.py
+++ b/wifitables/report.py
@@ -22,13 +22,17 @@
 channels = {}
 
 
+def _Resource(name):
+  return os.path.join(os.path.dirname(os.path.abspath(__file__)), name)
+
+
 def LoadNRates():
   """Loads 802.11n coding and data rates into a global variable."""
   if nrates: return
 
   raw = []
 
-  with open(NFILE, 'rb') as csvfile:
+  with open(_Resource(NFILE), 'rb') as csvfile:
     reader = csv.reader(csvfile, delimiter='\t')
     next(reader)  # skip header row when reading by machine
     for mcs, width, gi, rate in reader:
@@ -42,8 +46,9 @@
 
 def LoadChannels():
   """Load 802.11n channels and frequencies into a global variable."""
+  if channels: return
 
-  with open(CHANNELFILE, 'rb') as csvfile:
+  with open(_Resource(CHANNELFILE), 'rb') as csvfile:
     reader = csv.reader(csvfile, delimiter='\t')
     next(reader)
 
@@ -53,6 +58,7 @@
 
 def ParseMCSFile(outfile, width=20):
   """Extract MCS and PHY rate statistics from an MCS report file."""
+  LoadNRates()
 
   # assume long guard interval
   guard = 800
@@ -87,7 +93,13 @@
     match = iperf_re.match(line)
     if match:
       iperf = match.groupdict()
-      iperf['bandwidth'] = float(iperf['bandwidth'].split()[0])
+      bval, bunit = iperf['bandwidth'].split()
+      iperf['bandwidth'] = float(bval)
+      iperf['bandwidth_unit'] = bunit
+
+      tval, tunit = iperf['transfer'].split()
+      iperf['transfer'] = float(tval)
+      iperf['transfer_unit'] = tunit
       return iperf
 
   return {}
@@ -108,11 +120,13 @@
 
 def Channel(text_channel):
   """Given a text channel spec like 149,+1 return the central freq and width."""
+  LoadChannels()
+
   if ',' in text_channel:
     base, offset = text_channel.split(',')
     freq = channels[int(base)]
     offset = int(offset)
-    return (freq + offset * 20) / 2, 40
+    return (2 * freq + offset * 20) / 2, 40
   else:
     return channels[int(text_channel)], 20
 
@@ -227,7 +241,7 @@
 
     rssi = airport['agrCtlRSSI']
     noise = airport['agrCtlNoise']
-    line += [rssi, noise, shared, overlap - shared]
+    line += [channel, width, rssi, noise, shared, overlap - shared]
 
   else:
     # assume the report was generated on Linux.
@@ -235,14 +249,15 @@
       iwlink = ParseIwLink(il.read())
 
     signal = int(iwlink.get('signal', '0 dBm').split()[0])
-    line += [signal, '', '', '']  # Noise and contention not yet gathered in
-                                  # samples run on Linux systems.
-
+    channel = int(iwlink.get('freq'))
     width = 20
-    m = re.search(r'(\d+)Mhz', iwlink.get('tx bitrate'))
+    m = re.search(r'(\d+)MHz', iwlink.get('tx bitrate'), flags=re.I)
     if m:
       width = int(m.group(1))
 
+    # Noise and contention not yet gathered in samples run on Linux systems.
+    line += [channel, width, signal, '', '', '']
+
   mpath = os.path.join(report_dir, 'mcs')
   if os.path.isfile(mpath):
     with open(os.path.join(report_dir, 'mcs')) as mf:
@@ -255,7 +270,8 @@
 
   tcp_perf = ParseIperfTCP(it.read())
   udp_perf = ParseIperfUDP(iu.read())
-  line += [tcp_perf.get('bandwidth'), udp_perf.get('bandwidth')]
+  line += [tcp_perf.get('bandwidth'), tcp_perf.get('bandwidth_unit'),
+           udp_perf.get('bandwidth'), udp_perf.get('bandwidth_unit')]
 
   iu.close()
   it.close()
@@ -277,16 +293,20 @@
     lines += [ReportLine(opt.report_dir)]
 
   if extra:
-    with open(extra[0]) as journal:
-      for line in journal:
-        lines += [ReportLine(line.strip())]
+    for jname in extra[:1]:
+      jname = os.path.realpath(jname)
+      with open(jname) as journal:
+        for line in journal:
+          lines += [ReportLine(os.path.join(os.path.dirname(jname),
+                                            line.strip()))]
 
   if len(lines) < 1:
     o.fatal("didn't find any samples. did you supply at least one report dir"
             ' or journal?')
 
-  print '\t'.join(['Steps', 'RSSI', 'Noise', 'Shared', 'Interfering', 'MCS',
-                   'PHY', 'TCP', 'UDP'])
+  print '\t'.join(['Steps', 'Channel', 'Width', 'RSSI', 'Noise', 'Shared',
+                   'Interfering', 'MCS', 'PHY', 'TCP BW', '(Units)', 'UDP BW',
+                   '(Units)'])
   for line in lines:
     print '\t'.join(str(i) for i in line)
 
diff --git a/wifitables/report_test.py b/wifitables/report_test.py
index 31aff8f..54c1723 100644
--- a/wifitables/report_test.py
+++ b/wifitables/report_test.py
@@ -3,7 +3,8 @@
 import os
 
 import report
-from wvtest import *
+from wvtest import *  # pylint: disable=wildcard-import
+
 
 @wvtest
 def LoadExternalData():
@@ -47,30 +48,48 @@
     WVPASSEQ(data.get('SSID'), 'GSAFNS1441P0208_TestWifi')
     WVPASSEQ(data.get('BSSID'), 'f4:f5:e8:80:f3:d0')
 
-  steps, rssi, _, _, _, _, _, tcperf, udperf = report.ReportLine(rpt)
+  line = report.ReportLine(rpt)
+  print ('Checking report. Implemented measures: steps, channel, width, rssi, '
+         'TCP performance, UDP performance')
+  print line
 
-  print ('Checking report. Implemented measures: steps, rssi, TCP performance, '
-         'UDP performance')
+  (steps, ch, width, rssi, _, _, _, _, _,
+   tcperf, tcpbw, udperf, udpbw) = line
+
   WVPASSEQ(steps, 10)
+  WVPASSEQ(ch, 5745)
+  WVPASSEQ(width, 80)
   WVPASSEQ(rssi, -39)
   WVPASSEQ(tcperf, 92.1)
+  WVPASSEQ(tcpbw, 'Mbits/sec')
   WVPASSEQ(udperf, 91.2)
+  WVPASSEQ(udpbw, 'Mbits/sec')
+
 
 @wvtest
 def MacbookReport():
   rpt = 'testdata/wifi-1424744066.47-0010'
-  print ('Checking report. Implemented measures: steps, rssi, noise, devices '
-         'on channel, off channel, MCS, PHY rate, TCP performance, UDP '
-         'performance.')
+  line = report.ReportLine(rpt)
 
-  (steps, rssi, noise, shared, conflict,
-   mcs, phy, tcperf, udperf) = report.ReportLine(rpt)
+  print ('Checking report. Implemented measures: steps, channel, width, rssi, '
+         'noise, devices on channel, off channel, MCS, PHY rate, '
+         'TCP performance, UDP performance.')
+  print line
+
+  (steps, ch, width, rssi, noise, shared, conflict,
+   mcs, phy, tcperf, tcpbw, udperf, udpbw) = line
+
   WVPASSEQ(steps, 10)
+  WVPASSEQ(ch, 5755)
+  WVPASSEQ(width, 40)
   WVPASSEQ(rssi, -29)
   WVPASSEQ(noise, -90)
-  WVPASSEQ(shared, 16)
+  WVPASSEQ(shared, 17)
   WVPASSEQ(conflict, 10)
   WVPASSEQ(mcs, 21)
   WVPASS(abs(phy - 340.9) < 0.1)
   WVPASSEQ(tcperf, 196.0)
+  WVPASSEQ(tcpbw, 'Mbits/sec')
   WVPASSEQ(udperf, 260.0)
+  WVPASSEQ(udpbw, 'Mbits/sec')
+
diff --git a/wifitables/sample.py b/wifitables/sample.py
index 92839bf..53a3c62 100755
--- a/wifitables/sample.py
+++ b/wifitables/sample.py
@@ -95,6 +95,15 @@
   return out
 
 
+def IwScan(devname):
+  out = tempfile.NamedTemporaryFile(prefix='iwlink')
+  subprocess.check_call(['iw', 'dev', devname, 'scan', 'dump'], stdout=out)
+  if os.fstat(out.file.fileno()).st_size: return out
+
+  subprocess.check_call(['iw', 'dev', devname, 'scan'], stdout=out)
+  return out
+
+
 def IpAddr():
   out = tempfile.NamedTemporaryFile(prefix='ipaddr')
   subprocess.check_call(['ip', '-o', '-f', 'inet', 'addr'], stdout=out)
@@ -165,7 +174,9 @@
     il = IwLink(opt.interface)
     il.seek(0)
     bssid = report.ParseIwLink(il.read())['BSSID']
-    outputs += [ip, il]
+
+    ic = IwScan(opt.interface)
+    outputs += [ip, il, ic]
   else:
     raise OSError('This script requires Mac OS X or Linux.')
 
@@ -188,16 +199,19 @@
   if opt.monitor:
     subprocess.check_call(['sudo', 'kill', str(sudo_tcpdump.pid)])
 
-  report_dir = 'wifi-{}-{:04}'.format(time.time(), opt.steps)
+  report_name = 'wifi-{}-{:04}'.format(time.time(), opt.steps)
+  if opt.journal:
+    with open(opt.journal, 'a') as journal:
+      print >> journal, report_name
+    report_dir = os.path.join(os.path.dirname(opt.journal), report_name)
+  else:
+    report_dir = report_name
+
   os.mkdir(report_dir)
   for page in outputs:
     shutil.copy(page.name,
                 os.path.join(report_dir, os.path.basename(page.name[:-6])))
 
-  if opt.journal:
-    with open(opt.journal, 'a') as journal:
-      print >> journal, report_dir
-
   print 'Report written to', report_dir