test: extend sharkd tests to cover all requests
authorPeter Wu <peter@lekensteyn.nl>
Tue, 20 Nov 2018 15:27:56 +0000 (16:27 +0100)
committerAnders Broman <a.broman58@gmail.com>
Wed, 21 Nov 2018 04:36:20 +0000 (04:36 +0000)
All request types have a corresponding test_sharkd_req_* test names
which tests the current (documented) behavior. The frame and download
tests are not very comprehensive though, but it's better than nothing.

(The original test_sharkd_hello_dhcp_pcap test is replaced by
test_sharkd_req_status and test_sharkd_req_frames, although the latter
does not literally check for the "DHCP" column anymore.)

Change-Id: Ic39b954fc50065345ac46e96a7057b7aba2a09e3
Reviewed-on: https://code.wireshark.org/review/30743
Petri-Dish: Peter Wu <peter@lekensteyn.nl>
Tested-by: Petri Dish Buildbot
Reviewed-by: Anders Broman <a.broman58@gmail.com>
test/matchers.py [new file with mode: 0644]
test/suite_sharkd.py

diff --git a/test/matchers.py b/test/matchers.py
new file mode 100644 (file)
index 0000000..5696d64
--- /dev/null
@@ -0,0 +1,67 @@
+#
+# -*- coding: utf-8 -*-
+# Wireshark tests
+#
+# Copyright (c) 2018 Peter Wu <peter@lekensteyn.nl>
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+'''Helpers for matching test results.'''
+
+import re
+
+class MatchAny(object):
+    '''Matches any other value.'''
+
+    def __init__(self, type=None):
+        self.type = type
+
+    def __eq__(self, other):
+        return self.type is None or self.type == type(other)
+
+    def __repr__(self):
+        return '<MatchAny type=%s>' % (self.type.__name__,)
+
+
+class MatchObject(object):
+    '''Matches all expected fields of an object, ignoring excess others.'''
+
+    def __init__(self, fields):
+        self.fields = fields
+
+    def __eq__(self, other):
+        return all(other.get(k) == v for k, v in self.fields.items())
+
+    def __repr__(self):
+        return '<MatchObject fields=%r>' % (self.fields,)
+
+
+class MatchList(object):
+    '''Matches elements of a list. Optionally checks list length.'''
+
+    def __init__(self, item, n=None, match_element=all):
+        self.item = item
+        self.n = n
+        self.match_element = match_element
+
+    def __eq__(self, other):
+        if self.n is not None and len(other) != self.n:
+            return False
+        return self.match_element(self.item == elm for elm in other)
+
+    def __repr__(self):
+        return '<MatchList item=%r n=%r match_element=%s>' % \
+                (self.item, self.n, self.match_element.__name__)
+
+
+class MatchRegExp(object):
+    '''Matches a string against a regular expression.'''
+
+    def __init__(self, pattern):
+        self.pattern = pattern
+
+    def __eq__(self, other):
+        return type(other) == str and re.match(self.pattern, other)
+
+    def __repr__(self):
+        return '<MatchRegExp pattern=%r>' % (self.pattern)
index e69fdf5..b39e950 100644 (file)
@@ -3,14 +3,18 @@
 # Wireshark tests
 # By Gerald Combs <gerald@wireshark.org>
 #
+# Copyright (c) 2018 Peter Wu <peter@lekensteyn.nl>
+#
 # SPDX-License-Identifier: GPL-2.0-or-later
 #
 '''sharkd tests'''
 
 import json
 import subprocess
+import unittest
 import subprocesstest
 import fixtures
+from matchers import *
 
 
 @fixtures.fixture(scope='session')
@@ -18,55 +22,434 @@ def cmd_sharkd(program):
     return program('sharkd')
 
 
-@fixtures.mark_usefixtures('test_env')
+@fixtures.fixture
+def run_sharkd_session(cmd_sharkd, request):
+    self = request.instance
+
+    def run_sharkd_session_real(sharkd_commands):
+        sharkd_proc = self.startProcess(
+            (cmd_sharkd, '-'), stdin=subprocess.PIPE)
+        sharkd_proc.stdin.write('\n'.join(sharkd_commands).encode('utf8'))
+        self.waitProcess(sharkd_proc)
+
+        self.assertIn('Hello in child.', sharkd_proc.stderr_str)
+
+        outputs = []
+        for line in sharkd_proc.stdout_str.splitlines():
+            line = line.strip()
+            if not line:
+                continue
+            try:
+                jdata = json.loads(line)
+            except json.JSONDecodeError:
+                self.fail('Invalid JSON: %r' % line)
+            outputs.append(jdata)
+        return tuple(outputs)
+    return run_sharkd_session_real
+
+
+@fixtures.fixture
+def check_sharkd_session(run_sharkd_session, request):
+    self = request.instance
+
+    def check_sharkd_session_real(sharkd_commands, expected_outputs):
+        sharkd_commands = [json.dumps(x) for x in sharkd_commands]
+        actual_outputs = run_sharkd_session(sharkd_commands)
+        self.assertEqual(expected_outputs, actual_outputs)
+    return check_sharkd_session_real
+
+
+@fixtures.mark_usefixtures('base_env')
 @fixtures.uses_fixtures
 class case_sharkd(subprocesstest.SubprocessTestCase):
-    def test_sharkd_hello_no_pcap(self, cmd_sharkd):
-        '''sharkd hello message, no capture file'''
-        sharkd_proc = self.startProcess((cmd_sharkd, '-'),
-            stdin=subprocess.PIPE
-        )
-
-        sharkd_commands = b'{"req":"status"}\n'
-        sharkd_proc.stdin.write(sharkd_commands)
-        self.waitProcess(sharkd_proc)
+    def test_sharkd_req_load_bad_pcap(self, check_sharkd_session, capture_file):
+        check_sharkd_session((
+            {"req": "load", "file": capture_file('non-existant.pcap')},
+        ), (
+            {"err": 2},
+        ))
 
-        self.assertEqual(self.countOutput('Hello in child.', count_stdout=False, count_stderr=True), 1, 'No hello message.')
+    def test_sharkd_req_status_no_pcap(self, check_sharkd_session):
+        check_sharkd_session((
+            {"req": "status"},
+        ), (
+            {"frames": 0, "duration": 0.0},
+        ))
 
-        try:
-            jdata = json.loads(sharkd_proc.stdout_str)
-            self.assertEqual(jdata['duration'], 0.0, 'Missing duration.')
-        except:
-            self.fail('Invalid JSON: "{}"'.format(sharkd_proc.stdout_str))
+    def test_sharkd_req_status(self, check_sharkd_session, capture_file):
+        check_sharkd_session((
+            {"req": "load", "file": capture_file('dhcp.pcap')},
+            {"req": "status"},
+        ), (
+            {"err": 0},
+            {"frames": 4, "duration": 0.070345000,
+                "filename": "dhcp.pcap", "filesize": 1400},
+        ))
 
-    def test_sharkd_hello_dhcp_pcap(self, cmd_sharkd, capture_file):
-        '''sharkd hello message, simple capture file'''
-        sharkd_proc = self.startProcess((cmd_sharkd, '-'),
-            stdin=subprocess.PIPE
-        )
+    def test_sharkd_req_analyse(self, check_sharkd_session, capture_file):
+        check_sharkd_session((
+            {"req": "load", "file": capture_file('dhcp.pcap')},
+            {"req": "analyse"},
+        ), (
+            {"err": 0},
+            {"frames": 4, "protocols": ["frame", "eth", "ethertype", "ip", "udp",
+                                        "dhcp"], "first": 1102274184.317452908, "last": 1102274184.387798071},
+        ))
 
-        sharkd_commands = b'{"req":"load","file":'
-        sharkd_commands += json.dumps(capture_file('dhcp.pcap')).encode('utf8')
-        sharkd_commands += b'}\n'
-        sharkd_commands += b'{"req":"status"}\n'
-        sharkd_commands += b'{"req":"frames"}\n'
+    def test_sharkd_req_info(self, check_sharkd_session):
+        matchTapNameList = MatchList(
+            {"tap": MatchAny(str), "name": MatchAny(str)})
+        check_sharkd_session((
+            {"req": "info"},
+        ), (
+            {
+                "version": MatchAny(str),
+                "columns": MatchList({"format": MatchAny(str), "name": MatchAny(str)}),
+                "stats": matchTapNameList,
+                "convs": matchTapNameList,
+                "eo": matchTapNameList,
+                "srt": matchTapNameList,
+                "rtd": matchTapNameList,
+                "seqa": matchTapNameList,
+                "taps": matchTapNameList,
+                "follow": matchTapNameList,
+                "ftypes": MatchList(MatchAny(str)),
+                "nstat": matchTapNameList,
+            },
+        ))
 
-        sharkd_proc.stdin.write(sharkd_commands)
-        self.waitProcess(sharkd_proc)
+    def test_sharkd_req_check(self, check_sharkd_session, capture_file):
+        check_sharkd_session((
+            {"req": "load", "file": capture_file('dhcp.pcap')},
+            {"req": "check"},
+            {"req": "check", "filter": "garbage filter"},
+            {"req": "check", "field": "garbage field"},
+            {"req": "check", "filter": "ip", "field": "ip"},
+        ), (
+            {"err": 0},
+            {"err": 0},
+            {"err": 0, "filter": '"filter" was unexpected in this context.'},
+            {"err": 0, "field": "notfound"},
+            {"err": 0, "filter": "ok", "field": "ok"},
+        ))
 
-        has_dhcp = False
-        for line in sharkd_proc.stdout_str.splitlines():
-            line = line.strip()
-            if not line: continue
-            try:
-                jdata = json.loads(line)
-            except:
-                self.fail('Invalid JSON for "{}"'.format(line))
+    def test_sharkd_req_complete_field(self, check_sharkd_session):
+        check_sharkd_session((
+            {"req": "complete"},
+            {"req": "complete", "field": "frame.le"},
+            {"req": "complete", "field": "garbage.nothing.matches"},
+        ), (
+            {"err": 0},
+            {"err": 0, "field": MatchList(
+                {"f": "frame.len", "t": 7, "n": "Frame length on the wire"}, match_element=any)},
+            {"err": 0, "field": []},
+        ))
 
-            try:
-                if 'DHCP' in jdata[0]['c']:
-                    has_dhcp = True
-            except:
-                pass
+    def test_sharkd_req_complete_pref(self, check_sharkd_session):
+        check_sharkd_session((
+            {"req": "complete", "pref": "tcp."},
+            {"req": "complete", "pref": "garbage.nothing.matches"},
+        ), (
+            {"err": 0, "pref": MatchList(
+                {"f": "tcp.check_checksum", "d": "Validate the TCP checksum if possible"}, match_element=any)},
+            {"err": 0, "pref": []},
+        ))
+
+    def test_sharkd_req_frames(self, check_sharkd_session, capture_file):
+        # XXX need test for optional input parameters, ignored/marked/commented
+        check_sharkd_session((
+            {"req": "load", "file": capture_file('dhcp.pcap')},
+            {"req": "frames"},
+        ), (
+            {"err": 0},
+            MatchList({
+                "c": MatchList(MatchAny(str)),
+                "num": MatchAny(int),
+                "bg": MatchAny(str),
+                "fg": MatchAny(str),
+            }),
+        ))
+
+    def test_sharkd_req_tap_invalid(self, check_sharkd_session, capture_file):
+        # XXX Unrecognized taps result in an empty line, modify
+        #     run_sharkd_session such that checking for it is possible.
+        check_sharkd_session((
+            {"req": "load", "file": capture_file('dhcp.pcap')},
+            {"req": "tap"},
+            {"req": "tap", "tap0": "garbage tap"},
+        ), (
+            {"err": 0},
+        ))
+
+    def test_sharkd_req_tap(self, check_sharkd_session, capture_file):
+        check_sharkd_session((
+            {"req": "load", "file": capture_file('dhcp.pcap')},
+            {"req": "tap"},
+            {"req": "tap", "tap0": "conv:Ethernet", "tap1": "endpt:TCP"},
+        ), (
+            {"err": 0},
+            {
+                "err": 0,
+                "taps": [
+                    {
+                        "tap": "endpt:TCP",
+                        "type": "host",
+                        "proto": "TCP",
+                        "geoip": MatchAny(bool),
+                        "hosts": [],
+                    },
+                    {
+                        "tap": "conv:Ethernet",
+                        "type": "conv",
+                        "proto": "Ethernet",
+                        "geoip": MatchAny(bool),
+                        "convs": [
+                            {
+                                "saddr": MatchAny(str),
+                                "daddr": "Broadcast",
+                                "txf": 2,
+                                "txb": 628,
+                                "rxf": 0,
+                                "rxb": 0,
+                                "start": 0,
+                                "stop": 0.070031,
+                                "filter": "eth.addr==00:0b:82:01:fc:42 && eth.addr==ff:ff:ff:ff:ff:ff",
+                            },
+                            {
+                                "saddr": MatchAny(str),
+                                "daddr": MatchAny(str),
+                                "rxf": 0,
+                                "rxb": 0,
+                                "txf": 2,
+                                "txb": 684,
+                                "start": 0.000295,
+                                "stop": 0.070345,
+                                "filter": "eth.addr==00:08:74:ad:f1:9b && eth.addr==00:0b:82:01:fc:42",
+                            }
+                        ],
+                    },
+                    # XXX remove the last null element, it is not part of the interface.
+                    None
+                ]
+            },
+        ))
+
+    def test_sharkd_req_follow_bad(self, check_sharkd_session, capture_file):
+        # Unrecognized taps currently produce no output (not even err).
+        check_sharkd_session((
+            {"req": "load", "file": capture_file('dhcp.pcap')},
+            {"req": "follow"},
+            {"req": "follow", "follow": "garbage follow", "filter": "ip"},
+            {"req": "follow", "follow": "HTTP", "filter": "garbage filter"},
+        ), (
+            {"err": 0},
+        ))
+
+    def test_sharkd_req_follow_no_match(self, check_sharkd_session, capture_file):
+        check_sharkd_session((
+            {"req": "load", "file": capture_file('dhcp.pcap')},
+            {"req": "follow", "follow": "HTTP", "filter": "ip"},
+        ), (
+            {"err": 0},
+            {"err": 0, "shost": "NONE", "sport": "0", "sbytes": 0,
+             "chost": "NONE", "cport": "0", "cbytes": 0},
+        ))
+
+    def test_sharkd_req_follow_udp(self, check_sharkd_session, capture_file):
+        check_sharkd_session((
+            {"req": "load", "file": capture_file('dhcp.pcap')},
+            {"req": "follow", "follow": "UDP", "filter": "frame.number==1"},
+        ), (
+            {"err": 0},
+            {"err": 0,
+             "shost": "255.255.255.255", "sport": "67", "sbytes": 272,
+             "chost": "0.0.0.0", "cport": "68", "cbytes": 0,
+             "payloads": [
+                 {"n": 1, "d": MatchRegExp(r'AQEGAAAAPR0A[a-zA-Z0-9]{330}AANwQBAwYq/wAAAAAAAAA=')}]},
+        ))
+
+    def test_sharkd_req_iograph_bad(self, check_sharkd_session, capture_file):
+        check_sharkd_session((
+            {"req": "load", "file": capture_file('dhcp.pcap')},
+            {"req": "iograph"},
+            {"req": "iograph", "graph0": "garbage graph name"},
+        ), (
+            {"err": 0},
+            {"iograph": []},
+            {"iograph": []},
+        ))
+
+    def test_sharkd_req_iograph_basic(self, check_sharkd_session, capture_file):
+        check_sharkd_session((
+            {"req": "load", "file": capture_file('dhcp.pcap')},
+            {"req": "iograph", "graph0": "max:udp.length", "filter0": "udp.length"},
+            {"req": "iograph", "graph0": "packets", "graph1": "bytes"},
+            {"req": "iograph", "graph0": "packets", "filter0": "garbage filter"},
+        ), (
+            {"err": 0},
+            {"iograph": [{"items": [308.000000]}]},
+            {"iograph": [{"items": [4.000000]}, {"items": [1312.000000]}]},
+            {"iograph": [
+                {"errmsg": 'Filter "garbage filter" is invalid - "filter" was unexpected in this context.'}]},
+        ))
+
+    def test_sharkd_req_intervals_bad(self, check_sharkd_session, capture_file):
+        check_sharkd_session((
+            {"req": "load", "file": capture_file('dhcp.pcap')},
+            {"req": "intervals", "filter": "garbage filter"},
+        ), (
+            {"err": 0},
+        ))
+
+    def test_sharkd_req_intervals_basic(self, check_sharkd_session, capture_file):
+        check_sharkd_session((
+            {"req": "load", "file": capture_file('dhcp.pcap')},
+            {"req": "intervals"},
+            {"req": "intervals", "interval": 1},
+            {"req": "intervals", "filter": "frame.number <= 2"},
+        ), (
+            {"err": 0},
+            {"intervals": [[0, 4, 1312]], "last": 0,
+                "frames": 4, "bytes": 1312},
+            {"intervals": [[0, 2, 656], [70, 2, 656]],
+                "last": 70, "frames": 4, "bytes": 1312},
+            {"intervals": [[0, 2, 656]], "last": 0, "frames": 2, "bytes": 656},
+        ))
+
+    def test_sharkd_req_frame_basic(self, check_sharkd_session, capture_file):
+        # XXX add more tests for other options (ref_frame, prev_frame, columns, color, bytes, hidden)
+        check_sharkd_session((
+            {"req": "load", "file": capture_file('dhcp.pcap')},
+            {"req": "frame", "frame": 2},
+        ), (
+            {"err": 0},
+            # XXX remove the first 0 element, it is not part of the interface.
+            {"err": 0, "fol": [0, ["UDP", "udp.stream eq 1"]]},
+        ))
+
+    def test_sharkd_req_frame_proto(self, check_sharkd_session, capture_file):
+        # Check proto tree output (including an UTF-8 value).
+        check_sharkd_session((
+            {"req": "load", "file": capture_file('dhcp.pcap')},
+            {"req": "frame", "frame": 2, "proto": True},
+        ), (
+            {"err": 0},
+            MatchObject({
+                "tree": MatchList({
+                    "l": "Dynamic Host Configuration Protocol (Offer)",
+                    "t": "proto",
+                    "f": "dhcp",
+                    "e": MatchAny(int),
+                    "n": MatchList({
+                        "l": "Padding: 000000000000000000000000000000000000000000000000…",
+                        "h": [316, 26],
+                        "f": "dhcp.option.padding == 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00"
+                    }, match_element=any),  # match one element from 'n'
+                    "h": [42, 300],
+                }, match_element=any),  # match one element from 'tree'
+            }),
+        ))
+
+    def test_sharkd_req_setcomment(self, check_sharkd_session, capture_file):
+        check_sharkd_session((
+            {"req": "load", "file": capture_file('dhcp.pcap')},
+            # invalid frame number returns early.
+            {"req": "setcomment", "frame": 99999, "comment": "meh\nbaz"},
+            {"req": "setcomment", "frame": 3, "comment": "foo\nbar"},
+            {"req": "frame", "frame": 3},
+
+        ), (
+            {"err": 0},
+            {"err": 0},
+            {"err": 0, "comment": "foo\nbar", "fol": MatchAny(list)},
+        ))
+
+    def test_sharkd_req_setconf_bad(self, check_sharkd_session):
+        check_sharkd_session((
+            {"req": "setconf", "name": "uat:garbage-pref", "value": "\"\""},
+        ), (
+            {"err": 1, "errmsg": "Unknown preference"},
+        ))
+
+    def test_sharkd_req_dumpconf_bad(self, check_sharkd_session):
+        check_sharkd_session((
+            {"req": "dumpconf", "pref": "invalid-garbage-preference"},
+            {"req": "dumpconf", "pref": "uat:custom_http_header_fields"},
+        ), ())
+
+    def test_sharkd_req_dumpconf_all(self, check_sharkd_session):
+        check_sharkd_session((
+            {"req": "dumpconf"},
+        ), (
+            {"prefs": MatchObject({"tcp.check_checksum": {"b": 0}})},
+        ))
+
+    def test_sharkd_req_download_tls_secrets(self, check_sharkd_session, capture_file):
+        # XXX test download for eo: and rtp: too
+        check_sharkd_session((
+            {"req": "load", "file": capture_file('tls12-dsb.pcapng')},
+            {"req": "download", "token": "ssl-secrets"},
+        ), (
+            {"err": 0},
+            # TODO remove "RSA Session-ID:" support and support "CLIENT_RANDOM "... only
+            {"file": "keylog.txt", "mime": "text/plain",
+                "data": MatchRegExp(r'UlNBIFNlc3Npb24tSUQ6.+')},
+        ))
+
+    def test_sharkd_req_bye(self, check_sharkd_session):
+        check_sharkd_session((
+            {"req": "bye"},
+        ), (
+        ))
+
+    def test_sharkd_bad_request(self, check_sharkd_session):
+        check_sharkd_session((
+            {"req": 1337},
+        ), (
+        ))
+
+    def test_sharkd_config(self, check_sharkd_session):
+        check_sharkd_session((
+            {"req": "setconf", "name": "uat:custom_http_header_fields",
+                "value": "\"X-Header-Name\", \"Description\""},
+            {"req": "setconf", "name": "tcp.check_checksum", "value": "TRUE"},
+            {"req": "dumpconf", "pref": "tcp.check_checksum"},
+            {"req": "setconf", "name": "tcp.check_checksum", "value": "FALSE"},
+            {"req": "dumpconf", "pref": "tcp.check_checksum"},
+        ), (
+            # Check that the UAT preference is set. There is no way to query it
+            # (other than testing for side-effects in dissection).
+            {"err": 0},
+            {"err": 0},
+            {"prefs": {"tcp.check_checksum": {"b": 1}}},
+            {"err": 0},
+            {"prefs": {"tcp.check_checksum": {"b": 0}}},
+        ))
 
-        self.assertTrue(has_dhcp, 'Failed to find DHCP in JSON output')
+    def test_sharkd_config_enum(self, check_sharkd_session):
+        '''Dump default enum preference value, change it and restore it.'''
+        check_sharkd_session((
+            {"req": "dumpconf", "pref": "wlan.ignore_wep"},
+            {"req": "setconf", "name": "wlan.ignore_wep", "value": "Yes - with IV"},
+            {"req": "dumpconf", "pref": "wlan.ignore_wep"},
+            {"req": "setconf", "name": "wlan.ignore_wep", "value": "No"},
+            {"req": "dumpconf", "pref": "wlan.ignore_wep"},
+        ), (
+            {"prefs": {"wlan.ignore_wep": {"e": [
+                {"v": 0, "s": 1, "d": "No"},
+                {"v": 1, "d": "Yes - without IV"},
+                {"v": 2, "d": "Yes - with IV"}
+            ]}}},
+            {"err": 0},
+            {"prefs": {"wlan.ignore_wep": {"e": [
+                {"v": 0, "d": "No"},
+                {"v": 1, "d": "Yes - without IV"},
+                {"v": 2, "s": 1, "d": "Yes - with IV"}
+            ]}}},
+            {"err": 0},
+            {"prefs": {"wlan.ignore_wep": {"e": [
+                {"v": 0, "s": 1, "d": "No"},
+                {"v": 1, "d": "Yes - without IV"},
+                {"v": 2, "d": "Yes - with IV"}
+            ]}}},
+        ))