Skip to content

Commit

Permalink
New sensor function for quick use of probes in test suites.
Browse files Browse the repository at this point in the history
  • Loading branch information
aminusfu committed Sep 23, 2019
1 parent c1534ba commit 483c458
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 42 deletions.
22 changes: 21 additions & 1 deletion src/diagnose/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Diagnose, a library for instrumenting code at runtime."""

from contextlib import contextmanager

from . import managers
from . import instruments
from . import probes
Expand All @@ -11,4 +13,22 @@
# decides to replace diagnose.manager with an instance of a subclass.
manager = managers.InstrumentManager()

__all__ = ("probes", "instruments", "manager", "managers")
__all__ = ("probes", "instruments", "manager", "managers", "sensor")


@contextmanager
def sensor(target, value="result", name="test_instrument", event="return", mgr=None):
"""Attach a probe to the given target and yield a ProbeTestInstrument."""
probe = probes.attach_to(target)
probe.instruments[name] = i = instruments.ProbeTestInstrument(
name, value, event, expires=None, mgr=mgr
)
probe.start()

yield i

probe.instruments.pop(name)
if not probe.instruments:
p = probes.active_probes.pop(target, None)
if p is not None:
p.stop()
10 changes: 8 additions & 2 deletions src/diagnose/instruments.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ class Instrument(object):
other information for filtering events, set points for
closed-loop controllers, or other information specific
to the kind of instrument.
* mgr: an InstrumentManager instance. If None, defaults to the global
diagnose.manager
"""

error_expiration = datetime.datetime(1970, 1, 1)
Expand Down Expand Up @@ -194,9 +196,13 @@ class ProbeTestInstrument(Instrument):

def __init__(self, *args, **kwargs):
Instrument.__init__(self, *args, **kwargs)
self.results = []
self.log = []

@property
def results(self):
return [result for tags, result in self.log]

def fire(self, _globals, _locals):
v = self.evaluate(self.value, _globals, _locals)
tags = self.merge_tags(_globals, _locals)
self.results.append((tags, v))
self.log.append((tags, v))
97 changes: 58 additions & 39 deletions tests/test_probes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
MockPatch = mock._mock._patch

import diagnose
from diagnose import probes
from diagnose import probes, sensor
from diagnose.instruments import ProbeTestInstrument
from diagnose.test_fixtures import (
a_func,
Expand All @@ -41,7 +41,7 @@ def test_return_event_result(self):
assert result == "<ok>"

# The probe MUST have logged an entry
assert p.instruments.values()[0].results == [([], "<ok>")]
assert p.instruments.values()[0].results == ["<ok>"]

def test_return_event_elapsed(self):
with self.probe(
Expand All @@ -54,7 +54,7 @@ def test_return_event_elapsed(self):
assert result == "<ok>"

# The probe MUST have logged an entry
assert p.instruments.values()[0].results[0][1] < elapsed
assert p.instruments.values()[0].results[0] < elapsed

def test_return_event_locals(self):
with self.probe(
Expand All @@ -66,21 +66,18 @@ def test_return_event_locals(self):

# The probe MUST have logged an entry
assert p.instruments.values()[0].results == [
(
[],
[
"arg",
"args",
"elapsed",
"end",
"frame",
"kwargs",
"now",
"result",
"self",
"start",
],
)
[
"arg",
"args",
"elapsed",
"end",
"frame",
"kwargs",
"now",
"result",
"self",
"start",
]
]

def test_return_event_locals_frame(self):
Expand All @@ -94,7 +91,9 @@ def test_return_event_locals_frame(self):
custom=None,
)
a_func(923775)
assert probe.instruments["instrument1"].results, ["test_locals_frame"]
assert probe.instruments["instrument1"].results == [
"test_return_event_locals_frame"
]
finally:
probe.stop()

Expand All @@ -110,7 +109,7 @@ def test_call_event_args(self):
assert result == "<ok>"

# The probe MUST have logged an entry
assert p.instruments.values()[0].results == [([], (t, "ok"))]
assert p.instruments.values()[0].results == [(t, "ok")]

def test_call_event_elapsed(self):
with self.probe(
Expand Down Expand Up @@ -143,7 +142,7 @@ def test_call_event_locals(self):

# The probe MUST have logged an entry
assert p.instruments.values()[0].results == [
([], ["arg", "args", "frame", "kwargs", "now", "self", "start"])
["arg", "args", "frame", "kwargs", "now", "self", "start"]
]

def test_call_event_locals_frame(self):
Expand All @@ -158,7 +157,9 @@ def test_call_event_locals_frame(self):
custom=None,
)
a_func(923775)
assert probe.instruments["instrument1"].results, ["test_locals_frame"]
assert probe.instruments["instrument1"].results == [
"test_call_event_locals_frame"
]
finally:
probe.stop()

Expand All @@ -176,7 +177,7 @@ def test_end_event_success(self):
custom=None,
)
assert a_func(27) == 40
assert i.results == [([], 40)]
assert i.results == [40]
finally:
probe.stop()

Expand All @@ -193,7 +194,7 @@ def test_end_event_exception_in_target(self):
)
with self.assertRaises(TypeError):
a_func(None)
self.assertEqual(i.results, [([], 13)])
self.assertEqual(i.results, [13])
finally:
probe.stop()

Expand Down Expand Up @@ -236,10 +237,10 @@ def test_slowest_line(self):
},
)
assert hard_work(0, 10000) == 1000
assert [tags for tags, value in i.results] == [
assert [tags for tags, value in i.log] == [
["source:34: summary = len([x for x in output if x % 10 == 0])\n"]
]
assert [type(value) for tags, value in i.results] == [float]
assert [type(value) for tags, value in i.log] == [float]
finally:
probe.stop()

Expand Down Expand Up @@ -331,24 +332,24 @@ class Entity(object):
# Invoking x.y is typical and works naturally...
self.assertTrue(func_2 is not old_probes_func_2)
func_2(44)
self.assertEqual(i.results, [([], 44)])
self.assertEqual(i.results, [44])

# ...but invoking M.y (we imported func_2 into test_probes' namespace)
# is harder:
self.assertTrue(func_2 is not old_local_func_2)
func_2(99999)
self.assertEqual(i.results, [([], 44), ([], 99999)])
self.assertEqual(i.results, [44, 99999])

# ...and invoking Entity().y is just as hard:
self.assertTrue(t.add13 is not old_local_func_2)
self.assertTrue(t2.add13 is not old_local_func_2)
t.add13(1001)
self.assertEqual(i.results, [([], 44), ([], 99999), ([], 1001)])
self.assertEqual(i.results, [44, 99999, 1001])

# ...etc:
self.assertTrue(registry["in_a_dict"] is not old_local_func_2)
registry["in_a_dict"](777)
self.assertEqual(i.results, [([], 44), ([], 99999), ([], 1001), ([], 777)])
self.assertEqual(i.results, [44, 99999, 1001, 777])

# The next problem is that, while our patch is live,
# if t2 goes out of its original scope, we've still got
Expand Down Expand Up @@ -387,17 +388,17 @@ class Entity(object):
func_2(456)
t.add13(789)
registry["in_a_dict"](101112)
assert i.results == [([], 44), ([], 99999), ([], 1001), ([], 777)]
assert i.results == [44, 99999, 1001, 777]

def test_function_registries(self):
with self.probe("test", "orig", "diagnose.test_fixtures.orig", "result") as p:
assert funcs["orig"]("ahem") == "aha!"

# The probe MUST have logged an entry
i = p.instruments.values()[0]
assert i.results == [([], "aha!")]
assert i.results == ["aha!"]

i.results = []
i.log = []

assert funcs["orig"]("ahem") == "aha!"

Expand All @@ -416,7 +417,7 @@ def test_patch_staticmethod(self):
"test", "quantile", "diagnose.test_fixtures.Thing.static", "result"
) as p:
assert Thing().static() == 15
assert p.instruments.values()[0].results == [([], 15)]
assert p.instruments.values()[0].results == [15]

def test_patch_wrapped_function_end_event(self):
probe = probes.attach_to("diagnose.test_fixtures.Thing.add5")
Expand All @@ -425,7 +426,7 @@ def test_patch_wrapped_function_end_event(self):
instr = ProbeTestInstrument("deco", "arg1", event="end")
probe.instruments["deco"] = instr
Thing().add5(13)
assert instr.results == [([], 113)]
assert instr.results == [113]
finally:
probe.stop()

Expand All @@ -445,10 +446,10 @@ def only_some_users(probe, instrument, *args, **kwargs):
custom={"valid_ids": [1, 2, 3]},
) as p:
assert Thing().do("ok", user_id=2) == "<ok>"
assert p.instruments.values()[0].results == [([], "<ok>")]
assert p.instruments.values()[0].results == ["<ok>"]

assert Thing().do("not ok", user_id=10004) == "<not ok>"
assert p.instruments.values()[0].results == [([], "<ok>")]
assert p.instruments.values()[0].results == ["<ok>"]


class TestHardcodedProbes(ProbeTestCase):
Expand All @@ -460,7 +461,7 @@ def test_hardcoded_probes(self):
for p in probes.active_probes.values()
for k, i in p.instruments.iteritems()
if k.startswith("hardcode:")
] == [[([], 24)]]
] == [[24]]

diagnose.manager.apply()
assert mult_by_8(3) == 24
Expand All @@ -469,4 +470,22 @@ def test_hardcoded_probes(self):
for p in probes.active_probes.values()
for k, i in p.instruments.iteritems()
if k.startswith("hardcode:")
] == [[([], 24), ([], 24)]]
] == [[24, 24]]


class TestSensors(ProbeTestCase):
def test_basic_sensor(self):
assert probes.active_probes.get("diagnose.test_fixtures.Thing.do") is None

with sensor("diagnose.test_fixtures.Thing.do") as s:
assert s.log == []
Thing().do("ok")
assert s.results == ["<ok>"]
assert probes.active_probes.get("diagnose.test_fixtures.Thing.do") is None

with sensor("diagnose.test_fixtures.Thing.do") as s:
Thing().do("tagless")
s.custom["tags"] = '{"foo": "bar"}'
Thing().do("tagged")
assert s.log == [([], "<tagless>"), (["foo:bar"], "<tagged>")]
assert probes.active_probes.get("diagnose.test_fixtures.Thing.do") is None

0 comments on commit 483c458

Please sign in to comment.