Merge pull request #2685 from cortesi/longout

Add a data viewer for command output
This commit is contained in:
Aldo Cortesi 2017-12-17 10:37:22 +13:00 committed by GitHub
commit 49142883e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 307 additions and 179 deletions

View File

@ -17,14 +17,6 @@ def headername(spec: str):
return spec[len("header["):-1].strip()
flow_shortcuts = {
"q": "request",
"s": "response",
"cc": "client_conn",
"sc": "server_conn",
}
def is_addr(v):
return isinstance(v, tuple) and len(v) > 1
@ -35,8 +27,6 @@ def extract(cut: str, f: flow.Flow) -> typing.Union[str, bytes]:
for i, spec in enumerate(path):
if spec.startswith("_"):
raise exceptions.CommandError("Can't access internal attribute %s" % spec)
if isinstance(current, flow.Flow):
spec = flow_shortcuts.get(spec, spec)
part = getattr(current, spec, None)
if i == len(path) - 1:
@ -56,49 +46,36 @@ def extract(cut: str, f: flow.Flow) -> typing.Union[str, bytes]:
return str(current or "")
def parse_cutspec(s: str) -> typing.Tuple[str, typing.Sequence[str]]:
"""
Returns (flowspec, [cuts]).
Raises exceptions.CommandError if input is invalid.
"""
parts = s.split("|", maxsplit=1)
flowspec = "@all"
if len(parts) == 2:
flowspec = parts[1].strip()
cuts = parts[0]
cutparts = [i.strip() for i in cuts.split(",") if i.strip()]
if len(cutparts) == 0:
raise exceptions.CommandError("Invalid cut specification.")
return flowspec, cutparts
class Cut:
@command.command("cut")
def cut(self, cutspec: str) -> command.Cuts:
def cut(
self,
flows: typing.Sequence[flow.Flow],
cuts: typing.Sequence[command.Cut]
) -> command.Cuts:
"""
Resolve a cut specification of the form "cuts|flowspec". The cuts
are a comma-separated list of cut snippets. Cut snippets are
attribute paths from the base of the flow object, with a few
conveniences - "q", "s", "cc" and "sc" are shortcuts for request,
response, client_conn and server_conn, "port" and "host" retrieve
parts of an address tuple, ".header[key]" retrieves a header value.
Return values converted sensibly: SSL certicates are converted to PEM
format, bools are "true" or "false", "bytes" are preserved, and all
other values are converted to strings. The flowspec is optional, and
if it is not specified, it is assumed to be @all.
Cut data from a set of flows. Cut specifications are attribute paths
from the base of the flow object, with a few conveniences - "port"
and "host" retrieve parts of an address tuple, ".header[key]"
retrieves a header value. Return values converted to strings or
bytes: SSL certicates are converted to PEM format, bools are "true"
or "false", "bytes" are preserved, and all other values are
converted to strings.
"""
flowspec, cuts = parse_cutspec(cutspec)
flows = ctx.master.commands.call_args("view.resolve", [flowspec])
ret = []
for f in flows:
ret.append([extract(c, f) for c in cuts])
return ret
@command.command("cut.save")
def save(self, cuts: command.Cuts, path: command.Path) -> None:
def save(
self,
flows: typing.Sequence[flow.Flow],
cuts: typing.Sequence[command.Cut],
path: command.Path
) -> None:
"""
Save cuts to file. If there are multiple rows or columns, the format
Save cuts to file. If there are multiple flows or cuts, the format
is UTF-8 encoded CSV. If there is exactly one row and one column,
the data is written to file as-is, with raw bytes preserved. If the
path is prefixed with a "+", values are appended if there is an
@ -108,12 +85,12 @@ class Cut:
if path.startswith("+"):
append = True
path = command.Path(path[1:])
if len(cuts) == 1 and len(cuts[0]) == 1:
if len(cuts) == 1 and len(flows) == 1:
with open(path, "ab" if append else "wb") as fp:
if fp.tell() > 0:
# We're appending to a file that already exists and has content
fp.write(b"\n")
v = cuts[0][0]
v = extract(cuts[0], flows[0])
if isinstance(v, bytes):
fp.write(v)
else:
@ -122,20 +99,27 @@ class Cut:
else:
with open(path, "a" if append else "w", newline='', encoding="utf8") as fp:
writer = csv.writer(fp)
for r in cuts:
for f in flows:
vals = [extract(c, f) for c in cuts]
writer.writerow(
[strutils.always_str(c) or "" for c in r] # type: ignore
[strutils.always_str(x) or "" for x in vals] # type: ignore
)
ctx.log.alert("Saved %s cuts as CSV." % len(cuts))
ctx.log.alert("Saved %s cuts over %d flows as CSV." % (len(cuts), len(flows)))
@command.command("cut.clip")
def clip(self, cuts: command.Cuts) -> None:
def clip(
self,
flows: typing.Sequence[flow.Flow],
cuts: typing.Sequence[command.Cut],
) -> None:
"""
Send cuts to the system clipboard.
Send cuts to the clipboard. If there are multiple flows or cuts, the
format is UTF-8 encoded CSV. If there is exactly one row and one
column, the data is written to file as-is, with raw bytes preserved.
"""
fp = io.StringIO(newline="")
if len(cuts) == 1 and len(cuts[0]) == 1:
v = cuts[0][0]
if len(cuts) == 1 and len(flows) == 1:
v = extract(cuts[0], flows[0])
if isinstance(v, bytes):
fp.write(strutils.always_str(v))
else:
@ -143,9 +127,10 @@ class Cut:
ctx.log.alert("Clipped single cut.")
else:
writer = csv.writer(fp)
for r in cuts:
for f in flows:
vals = [extract(c, f) for c in cuts]
writer.writerow(
[strutils.always_str(c) or "" for c in r] # type: ignore
[strutils.always_str(v) or "" for v in vals] # type: ignore
)
ctx.log.alert("Clipped %s cuts as CSV." % len(cuts))
pyperclip.copy(fp.getvalue())

View File

@ -29,6 +29,49 @@ Cuts = typing.Sequence[
]
class Cut(str):
# This is an awkward location for these values, but it's better than having
# the console core import and depend on an addon. FIXME: Add a way for
# addons to add custom types and manage their completion and validation.
valid_prefixes = [
"request.method",
"request.scheme",
"request.host",
"request.http_version",
"request.port",
"request.path",
"request.url",
"request.text",
"request.content",
"request.raw_content",
"request.timestamp_start",
"request.timestamp_end",
"request.header[",
"response.status_code",
"response.reason",
"response.text",
"response.content",
"response.timestamp_start",
"response.timestamp_end",
"response.raw_content",
"response.header[",
"client_conn.address.port",
"client_conn.address.host",
"client_conn.tls_version",
"client_conn.sni",
"client_conn.ssl_established",
"server_conn.address.port",
"server_conn.address.host",
"server_conn.ip_address.host",
"server_conn.tls_version",
"server_conn.sni",
"server_conn.ssl_established",
]
class Path(str):
pass
@ -49,11 +92,13 @@ def typename(t: type, ret: bool) -> str:
if isinstance(t, Choice):
return "choice"
elif t == typing.Sequence[flow.Flow]:
return "[flow]" if ret else "flowspec"
return "[flow]"
elif t == typing.Sequence[str]:
return "[str]"
elif t == typing.Sequence[Cut]:
return "[cut]"
elif t == Cuts:
return "[cuts]" if ret else "cutspec"
return "[cuts]"
elif t == flow.Flow:
return "flow"
elif issubclass(t, (str, int, bool)):
@ -125,7 +170,7 @@ class Command:
if chk:
pargs.extend(remainder)
else:
raise exceptions.CommandError("Invalid value type.")
raise exceptions.CommandError("Invalid value type: %s - expected %s" % (remainder, self.paramtypes[-1]))
with self.manager.master.handlecontext():
ret = self.func(*pargs)
@ -264,7 +309,7 @@ def parsearg(manager: CommandManager, spec: str, argtype: type) -> typing.Any:
"Command requires one flow, specification matched %s." % len(flows)
)
return flows[0]
elif argtype == typing.Sequence[str]:
elif argtype in (typing.Sequence[str], typing.Sequence[Cut]):
return [i.strip() for i in spec.split(",")]
else:
raise exceptions.CommandError("Unsupported argument type: %s" % argtype)

View File

@ -53,6 +53,8 @@ def twebsocketflow(client_conn=True, server_conn=True, messages=True, err=None,
sec_websocket_version="13",
sec_websocket_key="1234",
),
timestamp_start=1,
timestamp_end=2,
content=b''
)
resp = http.HTTPResponse(
@ -64,6 +66,8 @@ def twebsocketflow(client_conn=True, server_conn=True, messages=True, err=None,
upgrade='websocket',
sec_websocket_accept=b'',
),
timestamp_start=1,
timestamp_end=2,
content=b'',
)
handshake_flow = http.HTTPFlow(client_conn, server_conn)

View File

@ -1,4 +1,3 @@
import time
from io import BytesIO
from mitmproxy.utils import data
@ -31,7 +30,9 @@ def treq(**kwargs):
path=b"/path",
http_version=b"HTTP/1.1",
headers=http.Headers(((b"header", b"qvalue"), (b"content-length", b"7"))),
content=b"content"
content=b"content",
timestamp_start=1,
timestamp_end=2,
)
default.update(kwargs)
return http.Request(**default)
@ -48,8 +49,8 @@ def tresp(**kwargs):
reason=b"OK",
headers=http.Headers(((b"header-response", b"svalue"), (b"content-length", b"7"))),
content=b"message",
timestamp_start=time.time(),
timestamp_end=time.time(),
timestamp_start=1,
timestamp_end=2,
)
default.update(kwargs)
return http.Response(**default)

View File

@ -113,6 +113,19 @@ class CommandBuffer():
),
parse = parts,
)
if last.type == typing.Sequence[mitmproxy.command.Cut]:
spec = parts[-1].value.split(",")
opts = []
for pref in mitmproxy.command.Cut.valid_prefixes:
spec[-1] = pref
opts.append(",".join(spec))
self.completion = CompletionState(
completer = ListCompleter(
parts[-1].value,
opts,
),
parse = parts,
)
elif isinstance(last.type, mitmproxy.command.Choice):
self.completion = CompletionState(
completer = ListCompleter(

View File

@ -2,6 +2,8 @@ import typing
from mitmproxy import exceptions
from mitmproxy import flow
from mitmproxy.tools.console import overlay
from mitmproxy.tools.console import signals
@ -21,9 +23,11 @@ class CommandExecutor:
signals.status_message.send(
message="Command returned %s flows" % len(ret)
)
elif len(str(ret)) < 50:
signals.status_message.send(message=str(ret))
else:
signals.status_message.send(
message="Command returned too much data to display."
self.master.overlay(
overlay.DataViewerOverlay(
self.master,
ret,
),
valign="top"
)

View File

@ -31,7 +31,7 @@ def map(km):
km.add("A", "flow.resume @all", ["flowlist", "flowview"], "Resume all intercepted flows")
km.add("a", "flow.resume @focus", ["flowlist", "flowview"], "Resume this intercepted flow")
km.add(
"b", "console.command cut.save s.content|@focus ''",
"b", "console.command cut.save @focus response.content ",
["flowlist", "flowview"],
"Save response body to file"
)
@ -157,7 +157,7 @@ def map(km):
)
km.add("e", "console.grideditor.editor", ["grideditor"], "Edit in external editor")
km.add("z", "console.eventlog.clear", ["eventlog"], "Clear")
km.add("z", "eventstore.clear", ["eventlog"], "Clear")
km.add(
"a",

View File

@ -0,0 +1,67 @@
import typing
import urwid
from mitmproxy.tools.console import signals
from mitmproxy.tools.console.grideditor import base
from mitmproxy.utils import strutils
strbytes = typing.Union[str, bytes]
class Column(base.Column):
def Display(self, data):
return Display(data)
def Edit(self, data):
return Edit(data)
def blank(self):
return ""
def keypress(self, key, editor):
if key in ["m_select"]:
editor.walker.start_edit()
else:
return key
class Display(base.Cell):
def __init__(self, data: strbytes) -> None:
self.data = data
if isinstance(data, bytes):
escaped = strutils.bytes_to_escaped_str(data)
else:
escaped = data.encode()
w = urwid.Text(escaped, wrap="any")
super().__init__(w)
def get_data(self) -> strbytes:
return self.data
class Edit(base.Cell):
def __init__(self, data: strbytes) -> None:
if isinstance(data, bytes):
escaped = strutils.bytes_to_escaped_str(data)
else:
escaped = data.encode()
self.type = type(data) # type: typing.Type
w = urwid.Edit(edit_text=escaped, wrap="any", multiline=True)
w = urwid.AttrWrap(w, "editfield")
super().__init__(w)
def get_data(self) -> strbytes:
txt = self._w.get_text()[0].strip()
try:
if self.type == bytes:
return strutils.escaped_str_to_bytes(txt)
else:
return txt.decode()
except ValueError:
signals.status_message.send(
self,
message="Invalid Python-style string encoding.",
expire=1000
)
raise

View File

@ -46,7 +46,7 @@ class Edit(base.Cell):
except ValueError:
signals.status_message.send(
self,
message="Invalid Python-style string encoding.",
message="Invalid data.",
expire=1000
)
raise

View File

@ -2,6 +2,7 @@
from mitmproxy import exceptions
from mitmproxy.tools.console import layoutwidget
from mitmproxy.tools.console.grideditor import base
from mitmproxy.tools.console.grideditor import col
from mitmproxy.tools.console.grideditor import col_text
from mitmproxy.tools.console.grideditor import col_bytes
from mitmproxy.tools.console.grideditor import col_subgrid
@ -169,3 +170,20 @@ class OptionsEditor(base.GridEditor, layoutwidget.LayoutWidget):
def is_error(self, col, val):
pass
class DataViewer(base.GridEditor, layoutwidget.LayoutWidget):
title = None # type: str
def __init__(self, master, vals):
if vals:
if not isinstance(vals[0], list):
vals = [[i] for i in vals]
self.columns = [col.Column("")] * len(vals[0])
super().__init__(master, vals, self.callback)
def callback(self, vals):
pass
def is_error(self, col, val):
pass

View File

@ -1,5 +1,5 @@
import typing
from mitmproxy.tools.console import commandeditor
from mitmproxy.tools.console import commandexecutor
from mitmproxy.tools.console import signals
@ -35,7 +35,7 @@ class Binding:
class Keymap:
def __init__(self, master):
self.executor = commandeditor.CommandExecutor(master)
self.executor = commandexecutor.CommandExecutor(master)
self.keys = {}
for c in Contexts:
self.keys[c] = {}

View File

@ -148,3 +148,30 @@ class OptionsOverlay(urwid.WidgetWrap, layoutwidget.LayoutWidget):
def layout_popping(self):
return self.ge.layout_popping()
class DataViewerOverlay(urwid.WidgetWrap, layoutwidget.LayoutWidget):
keyctx = "grideditor"
def __init__(self, master, vals):
"""
vspace: how much vertical space to keep clear
"""
cols, rows = master.ui.get_cols_rows()
self.ge = grideditor.DataViewer(master, vals)
super().__init__(
urwid.AttrWrap(
urwid.LineBox(
urwid.BoxAdapter(self.ge, rows - 5),
title="Data viewer"
),
"background"
)
)
self.width = math.ceil(cols * 0.8)
def key_responder(self):
return self.ge.key_responder()
def layout_popping(self):
return self.ge.layout_popping()

View File

@ -4,7 +4,7 @@ import urwid
from mitmproxy.tools.console import common
from mitmproxy.tools.console import signals
from mitmproxy.tools.console import commandeditor
from mitmproxy.tools.console import commandexecutor
import mitmproxy.tools.console.master # noqa
from mitmproxy.tools.console.commander import commander
@ -68,7 +68,7 @@ class ActionBar(urwid.WidgetWrap):
def sig_prompt_command(self, sender, partial=""):
signals.focus.send(self, section="footer")
self._w = commander.CommandEdit(self.master, partial)
self.prompting = commandeditor.CommandExecutor(self.master)
self.prompting = commandexecutor.CommandExecutor(self.master)
def sig_prompt_onekey(self, sender, prompt, keys, callback, args=()):
"""

View File

@ -13,36 +13,41 @@ from unittest import mock
def test_extract():
tf = tflow.tflow(resp=True)
tests = [
["q.method", "GET"],
["q.scheme", "http"],
["q.host", "address"],
["q.port", "22"],
["q.path", "/path"],
["q.url", "http://address:22/path"],
["q.text", "content"],
["q.content", b"content"],
["q.raw_content", b"content"],
["q.header[header]", "qvalue"],
["request.method", "GET"],
["request.scheme", "http"],
["request.host", "address"],
["request.http_version", "HTTP/1.1"],
["request.port", "22"],
["request.path", "/path"],
["request.url", "http://address:22/path"],
["request.text", "content"],
["request.content", b"content"],
["request.raw_content", b"content"],
["request.timestamp_start", "1"],
["request.timestamp_end", "2"],
["request.header[header]", "qvalue"],
["s.status_code", "200"],
["s.reason", "OK"],
["s.text", "message"],
["s.content", b"message"],
["s.raw_content", b"message"],
["s.header[header-response]", "svalue"],
["response.status_code", "200"],
["response.reason", "OK"],
["response.text", "message"],
["response.content", b"message"],
["response.raw_content", b"message"],
["response.header[header-response]", "svalue"],
["response.timestamp_start", "1"],
["response.timestamp_end", "2"],
["cc.address.port", "22"],
["cc.address.host", "127.0.0.1"],
["cc.tls_version", "TLSv1.2"],
["cc.sni", "address"],
["cc.ssl_established", "false"],
["client_conn.address.port", "22"],
["client_conn.address.host", "127.0.0.1"],
["client_conn.tls_version", "TLSv1.2"],
["client_conn.sni", "address"],
["client_conn.ssl_established", "false"],
["sc.address.port", "22"],
["sc.address.host", "address"],
["sc.ip_address.host", "192.168.0.1"],
["sc.tls_version", "TLSv1.2"],
["sc.sni", "address"],
["sc.ssl_established", "false"],
["server_conn.address.port", "22"],
["server_conn.address.host", "address"],
["server_conn.ip_address.host", "192.168.0.1"],
["server_conn.tls_version", "TLSv1.2"],
["server_conn.sni", "address"],
["server_conn.ssl_established", "false"],
]
for t in tests:
ret = cut.extract(t[0], tf)
@ -53,43 +58,7 @@ def test_extract():
d = f.read()
c1 = certs.SSLCert.from_pem(d)
tf.server_conn.cert = c1
assert "CERTIFICATE" in cut.extract("sc.cert", tf)
def test_parse_cutspec():
tests = [
("", None, True),
("req.method", ("@all", ["req.method"]), False),
(
"req.method,req.host",
("@all", ["req.method", "req.host"]),
False
),
(
"req.method,req.host|~b foo",
("~b foo", ["req.method", "req.host"]),
False
),
(
"req.method,req.host|~b foo | ~b bar",
("~b foo | ~b bar", ["req.method", "req.host"]),
False
),
(
"req.method, req.host | ~b foo | ~b bar",
("~b foo | ~b bar", ["req.method", "req.host"]),
False
),
]
for cutspec, output, err in tests:
try:
assert cut.parse_cutspec(cutspec) == output
except exceptions.CommandError:
if not err:
raise
else:
if err:
raise AssertionError("Expected error.")
assert "CERTIFICATE" in cut.extract("server_conn.cert", tf)
def test_headername():
@ -110,69 +79,64 @@ def test_cut_clip():
v.add([tflow.tflow(resp=True)])
with mock.patch('pyperclip.copy') as pc:
tctx.command(c.clip, "q.method|@all")
tctx.command(c.clip, "@all", "request.method")
assert pc.called
with mock.patch('pyperclip.copy') as pc:
tctx.command(c.clip, "q.content|@all")
tctx.command(c.clip, "@all", "request.content")
assert pc.called
with mock.patch('pyperclip.copy') as pc:
tctx.command(c.clip, "q.method,q.content|@all")
tctx.command(c.clip, "@all", "request.method,request.content")
assert pc.called
def test_cut_file(tmpdir):
def test_cut_save(tmpdir):
f = str(tmpdir.join("path"))
v = view.View()
c = cut.Cut()
with taddons.context() as tctx:
tctx.master.addons.add(v, c)
v.add([tflow.tflow(resp=True)])
tctx.command(c.save, "q.method|@all", f)
tctx.command(c.save, "@all", "request.method", f)
assert qr(f) == b"GET"
tctx.command(c.save, "q.content|@all", f)
tctx.command(c.save, "@all", "request.content", f)
assert qr(f) == b"content"
tctx.command(c.save, "q.content|@all", "+" + f)
tctx.command(c.save, "@all", "request.content", "+" + f)
assert qr(f) == b"content\ncontent"
v.add([tflow.tflow(resp=True)])
tctx.command(c.save, "q.method|@all", f)
tctx.command(c.save, "@all", "request.method", f)
assert qr(f).splitlines() == [b"GET", b"GET"]
tctx.command(c.save, "q.method,q.content|@all", f)
tctx.command(c.save, "@all", "request.method,request.content", f)
assert qr(f).splitlines() == [b"GET,content", b"GET,content"]
def test_cut():
v = view.View()
c = cut.Cut()
with taddons.context() as tctx:
v.add([tflow.tflow(resp=True)])
tctx.master.addons.add(v, c)
assert c.cut("q.method|@all") == [["GET"]]
assert c.cut("q.scheme|@all") == [["http"]]
assert c.cut("q.host|@all") == [["address"]]
assert c.cut("q.port|@all") == [["22"]]
assert c.cut("q.path|@all") == [["/path"]]
assert c.cut("q.url|@all") == [["http://address:22/path"]]
assert c.cut("q.content|@all") == [[b"content"]]
assert c.cut("q.header[header]|@all") == [["qvalue"]]
assert c.cut("q.header[unknown]|@all") == [[""]]
with taddons.context():
tflows = [tflow.tflow(resp=True)]
assert c.cut(tflows, ["request.method"]) == [["GET"]]
assert c.cut(tflows, ["request.scheme"]) == [["http"]]
assert c.cut(tflows, ["request.host"]) == [["address"]]
assert c.cut(tflows, ["request.port"]) == [["22"]]
assert c.cut(tflows, ["request.path"]) == [["/path"]]
assert c.cut(tflows, ["request.url"]) == [["http://address:22/path"]]
assert c.cut(tflows, ["request.content"]) == [[b"content"]]
assert c.cut(tflows, ["request.header[header]"]) == [["qvalue"]]
assert c.cut(tflows, ["request.header[unknown]"]) == [[""]]
assert c.cut("s.status_code|@all") == [["200"]]
assert c.cut("s.reason|@all") == [["OK"]]
assert c.cut("s.content|@all") == [[b"message"]]
assert c.cut("s.header[header-response]|@all") == [["svalue"]]
assert c.cut("moo") == [[""]]
assert c.cut(tflows, ["response.status_code"]) == [["200"]]
assert c.cut(tflows, ["response.reason"]) == [["OK"]]
assert c.cut(tflows, ["response.content"]) == [[b"message"]]
assert c.cut(tflows, ["response.header[header-response]"]) == [["svalue"]]
assert c.cut(tflows, ["moo"]) == [[""]]
with pytest.raises(exceptions.CommandError):
assert c.cut("__dict__") == [[""]]
assert c.cut(tflows, ["__dict__"]) == [[""]]
v = view.View()
c = cut.Cut()
with taddons.context() as tctx:
tctx.master.addons.add(v, c)
v.add([tflow.ttcpflow()])
assert c.cut("q.method|@all") == [[""]]
assert c.cut("s.status|@all") == [[""]]
with taddons.context():
tflows = [tflow.ttcpflow()]
assert c.cut(tflows, ["request.method"]) == [[""]]
assert c.cut(tflows, ["response.status"]) == [[""]]

View File

@ -30,7 +30,7 @@ def test_order_refresh():
with taddons.context() as tctx:
tctx.configure(v, view_order="time")
v.add([tf])
tf.request.timestamp_start = 1
tf.request.timestamp_start = 10
assert not sargs
v.update([tf])
assert sargs
@ -41,7 +41,7 @@ def test_order_generators():
tf = tflow.tflow(resp=True)
rs = view.OrderRequestStart(v)
assert rs.generate(tf) == 0
assert rs.generate(tf) == 1
rm = view.OrderRequestMethod(v)
assert rm.generate(tf) == tf.request.method

View File

@ -150,10 +150,10 @@ class TestResponseUtils:
n = time.time()
r.headers["date"] = email.utils.formatdate(n)
pre = r.headers["date"]
r.refresh(n)
r.refresh(1)
assert pre == r.headers["date"]
r.refresh(n + 60)
r.refresh(61)
d = email.utils.parsedate_tz(r.headers["date"])
d = email.utils.mktime_tz(d)
# Weird that this is not exact...

View File

@ -153,10 +153,10 @@ def test_simple():
def test_typename():
assert command.typename(str, True) == "str"
assert command.typename(typing.Sequence[flow.Flow], True) == "[flow]"
assert command.typename(typing.Sequence[flow.Flow], False) == "flowspec"
assert command.typename(typing.Sequence[flow.Flow], False) == "[flow]"
assert command.typename(command.Cuts, False) == "cutspec"
assert command.typename(command.Cuts, True) == "[cuts]"
assert command.typename(typing.Sequence[command.Cut], False) == "[cut]"
assert command.typename(flow.Flow, False) == "flow"
assert command.typename(typing.Sequence[str], False) == "[str]"

View File

@ -322,7 +322,7 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
ws_client2 = yield websocket.websocket_connect(ws_url)
ws_client2.close()
def test_generate_tflow_js(self):
def _test_generate_tflow_js(self):
_tflow = app.flow_to_json(tflow.tflow(resp=True, err=True))
# Set some value as constant, so that _tflow.js would not change every time.
_tflow['client_conn']['id'] = "4a18d1a0-50a1-48dd-9aa6-d45d74282939"