-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
TrevorOctober
committed
May 25, 2016
1 parent
22897dd
commit c98074d
Showing
5 changed files
with
231 additions
and
373 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
# -*- test-case-name: go.apps.jsbox.tests.test_metrics -*- | ||
# -*- coding: utf-8 -*- | ||
|
||
"""Metrics for JS Box sandboxes""" | ||
|
||
import re | ||
|
||
from vxsandbox import SandboxResource | ||
|
||
from vumi.blinkenlights.metrics import SUM, AVG, MIN, MAX, LAST | ||
|
||
|
||
class MetricEventError(Exception): | ||
"""Raised when a command cannot be converted to a metric event.""" | ||
|
||
|
||
class MetricEvent(object): | ||
|
||
AGGREGATORS = { | ||
'sum': SUM, | ||
'avg': AVG, | ||
'min': MIN, | ||
'max': MAX, | ||
'last': LAST | ||
} | ||
|
||
NAME_REGEX = re.compile(r"^[a-zA-Z][a-zA-Z0-9._-]{,100}$") | ||
|
||
def __init__(self, store, metric, value, agg): | ||
self.store = store | ||
self.metric = metric | ||
self.value = value | ||
self.agg = agg | ||
|
||
def __eq__(self, other): | ||
if not isinstance(other, self.__class__): | ||
return False | ||
return all((self.store == other.store, self.metric == other.metric, | ||
self.value == other.value, self.agg is other.agg)) | ||
|
||
@classmethod | ||
def _parse_name(cls, name, kind): | ||
if name is None: | ||
raise MetricEventError("Missing %s name." % (kind,)) | ||
if not isinstance(name, basestring): | ||
raise MetricEventError("Invalid type for %s name: %r" | ||
% (kind, name)) | ||
if not cls.NAME_REGEX.match(name): | ||
raise MetricEventError("Invalid %s name: %r." % (kind, name)) | ||
return name | ||
|
||
@classmethod | ||
def _parse_value(cls, value): | ||
try: | ||
value = float(value) | ||
except (ValueError, TypeError): | ||
raise MetricEventError("Invalid metric value %r." % (value,)) | ||
return value | ||
|
||
@classmethod | ||
def _parse_agg(cls, agg): | ||
if not isinstance(agg, basestring): | ||
raise MetricEventError("Invalid metric aggregator %r" % (agg,)) | ||
if agg not in cls.AGGREGATORS: | ||
raise MetricEventError("Invalid metric aggregator %r." % (agg,)) | ||
return cls.AGGREGATORS[agg] | ||
|
||
@classmethod | ||
def from_command(cls, command): | ||
store = cls._parse_name(command.get('store', 'default'), 'store') | ||
metric = cls._parse_name(command.get('metric'), 'metric') | ||
value = cls._parse_value(command.get('value')) | ||
agg = cls._parse_agg(command.get('agg')) | ||
return cls(store, metric, value, agg) | ||
|
||
|
||
class MetricsResource(SandboxResource): | ||
"""Resource that provides metric storing.""" | ||
|
||
def _publish_event(self, api, ev): | ||
conversation = self.app_worker.conversation_for_api(api) | ||
self.app_worker.publish_account_metric(conversation.user_account.key, | ||
ev.store, ev.metric, ev.value, | ||
ev.agg) | ||
|
||
def handle_fire(self, api, command): | ||
"""Fire a metric value.""" | ||
try: | ||
ev = MetricEvent.from_command(command) | ||
except MetricEventError, e: | ||
return self.reply(command, success=False, reason=unicode(e)) | ||
self._publish_event(api, ev) | ||
return self.reply(command, success=True) |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
"""Tests for go.apps.jsbox.metrics.""" | ||
|
||
import mock | ||
|
||
from vxsandbox.resources import SandboxCommand | ||
|
||
from vumi.tests.helpers import VumiTestCase | ||
|
||
from vxsandbox.resources.metrics import ( | ||
MetricEvent, MetricEventError, MetricsResource) | ||
|
||
|
||
class TestMetricEvent(VumiTestCase): | ||
|
||
SUM = MetricEvent.AGGREGATORS['sum'] | ||
|
||
def test_create(self): | ||
ev = MetricEvent('mystore', 'foo', 2.0, self.SUM) | ||
self.assertEqual(ev.store, 'mystore') | ||
self.assertEqual(ev.metric, 'foo') | ||
self.assertEqual(ev.value, 2.0) | ||
self.assertEqual(ev.agg, self.SUM) | ||
|
||
def test_eq(self): | ||
ev1 = MetricEvent('mystore', 'foo', 1.5, self.SUM) | ||
ev2 = MetricEvent('mystore', 'foo', 1.5, self.SUM) | ||
self.assertEqual(ev1, ev2) | ||
|
||
def test_neq(self): | ||
ev1 = MetricEvent('mystore', 'foo', 1.5, self.SUM) | ||
ev2 = MetricEvent('mystore', 'bar', 1.5, self.SUM) | ||
self.assertNotEqual(ev1, ev2) | ||
|
||
def test_from_command(self): | ||
ev = MetricEvent.from_command({'store': 'mystore', 'metric': 'foo', | ||
'value': 1.5, 'agg': 'sum'}) | ||
self.assertEqual(ev, MetricEvent('mystore', 'foo', 1.5, self.SUM)) | ||
|
||
def test_bad_store(self): | ||
self.assertRaises(MetricEventError, MetricEvent.from_command, { | ||
'store': 'foo bar', 'metric': 'foo', 'value': 1.5, | ||
'agg': 'sum'}) | ||
|
||
def test_bad_type_store(self): | ||
self.assertRaises(MetricEventError, MetricEvent.from_command, { | ||
'store': {}, 'metric': 'foo', 'value': 1.5, | ||
'agg': 'sum'}) | ||
|
||
def test_bad_metric(self): | ||
self.assertRaises(MetricEventError, MetricEvent.from_command, { | ||
'store': 'mystore', 'metric': 'foo bar', 'value': 1.5, | ||
'agg': 'sum'}) | ||
|
||
def test_bad_type_metric(self): | ||
self.assertRaises(MetricEventError, MetricEvent.from_command, { | ||
'store': 'mystore', 'metric': {}, 'value': 1.5, | ||
'agg': 'sum'}) | ||
|
||
def test_missing_metric(self): | ||
self.assertRaises(MetricEventError, MetricEvent.from_command, { | ||
'store': 'mystore', 'value': 1.5, 'agg': 'sum'}) | ||
|
||
def test_bad_value(self): | ||
self.assertRaises(MetricEventError, MetricEvent.from_command, { | ||
'store': 'mystore', 'metric': 'foo', 'value': 'abc', | ||
'agg': 'sum'}) | ||
|
||
def test_bad_type_value(self): | ||
self.assertRaises(MetricEventError, MetricEvent.from_command, { | ||
'store': 'mystore', 'metric': 'foo', 'value': {}, | ||
'agg': 'sum'}) | ||
|
||
def test_missing_value(self): | ||
self.assertRaises(MetricEventError, MetricEvent.from_command, { | ||
'store': 'mystore', 'metric': 'foo', 'agg': 'sum'}) | ||
|
||
def test_bad_agg(self): | ||
self.assertRaises(MetricEventError, MetricEvent.from_command, { | ||
'store': 'mystore', 'metric': 'foo', 'value': 1.5, | ||
'agg': 'foo'}) | ||
|
||
def test_bad_type_agg(self): | ||
self.assertRaises(MetricEventError, MetricEvent.from_command, { | ||
'store': 'mystore', 'metric': 'foo', 'value': 1.5, | ||
'agg': {}}) | ||
|
||
def test_missing_agg(self): | ||
self.assertRaises(MetricEventError, MetricEvent.from_command, { | ||
'store': 'mystore', 'metric': 'foo', 'value': 1.5}) | ||
|
||
|
||
class TestMetricsResource(VumiTestCase): | ||
|
||
SUM = MetricEvent.AGGREGATORS['sum'] | ||
|
||
def setUp(self): | ||
self.conversation = mock.Mock() | ||
self.app_worker = mock.Mock() | ||
self.dummy_api = object() | ||
self.resource = MetricsResource("test", self.app_worker, {}) | ||
self.app_worker.conversation_for_api = mock.Mock( | ||
return_value=self.conversation) | ||
|
||
def check_reply(self, reply, cmd, success): | ||
self.assertEqual(reply['reply'], True) | ||
self.assertEqual(reply['cmd_id'], cmd['cmd_id']) | ||
self.assertEqual(reply['success'], success) | ||
|
||
def check_publish(self, store, metric, value, agg): | ||
self.app_worker.publish_account_metric.assert_called_once_with( | ||
self.conversation.user_account.key, store, metric, value, agg) | ||
|
||
def check_not_published(self): | ||
self.assertFalse(self.app_worker.publish_account_metric.called) | ||
|
||
def test_handle_fire(self): | ||
cmd = SandboxCommand(metric="foo", value=1.5, agg='sum') | ||
reply = self.resource.handle_fire(self.dummy_api, cmd) | ||
self.check_reply(reply, cmd, True) | ||
self.check_publish('default', 'foo', 1.5, self.SUM) | ||
|
||
def _test_error(self, cmd, expected_error): | ||
reply = self.resource.handle_fire(self.dummy_api, cmd) | ||
self.check_reply(reply, cmd, False) | ||
self.assertEqual(reply['reason'], expected_error) | ||
self.check_not_published() | ||
|
||
def test_handle_fire_error(self): | ||
cmd = SandboxCommand(metric="foo bar", value=1.5, agg='sum') | ||
expected_error = "Invalid metric name: 'foo bar'." | ||
self._test_error(cmd, expected_error) | ||
|
||
def test_non_ascii_metric_name_error(self): | ||
cmd = SandboxCommand(metric=u"b\xe6r", value=1.5, agg='sum') | ||
expected_error = "Invalid metric name: u'b\\xe6r'." | ||
self._test_error(cmd, expected_error) |
Oops, something went wrong.