diff --git a/mypy.ini b/mypy.ini --- a/mypy.ini +++ b/mypy.ini @@ -26,6 +26,9 @@ [mypy-iso8601.*] ignore_missing_imports = True +[mypy-magic.*] +ignore_missing_imports = True + [mypy-msgpack.*] ignore_missing_imports = True diff --git a/swh/core/pytest_plugin.py b/swh/core/pytest_plugin.py --- a/swh/core/pytest_plugin.py +++ b/swh/core/pytest_plugin.py @@ -1,8 +1,9 @@ -# Copyright (C) 2019 The Software Heritage developers +# Copyright (C) 2019-2021 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information +from collections import deque from functools import partial import logging from os import path @@ -327,3 +328,41 @@ ctx.pop() request.addfinalizer(teardown) + + +class FakeSocket(object): + """ A fake socket for testing. """ + + def __init__(self): + self.payloads = deque() + + def send(self, payload): + assert type(payload) == bytes + self.payloads.append(payload) + + def recv(self): + try: + return self.payloads.popleft().decode("utf-8") + except IndexError: + return None + + def close(self): + pass + + def __repr__(self): + return str(self.payloads) + + +@pytest.fixture +def statsd(): + """Simple fixture giving a Statsd instance suitable for tests + + The Statsd instance uses a FakeSocket as `.socket` attribute in which one + can get the accumulated statsd messages in a deque in `.socket.payloads`. + """ + + from swh.core.statsd import Statsd + + statsd = Statsd() + statsd._socket = FakeSocket() + yield statsd diff --git a/swh/core/tests/test_statsd.py b/swh/core/tests/test_statsd.py --- a/swh/core/tests/test_statsd.py +++ b/swh/core/tests/test_statsd.py @@ -1,4 +1,4 @@ -# Copyright (C) 2018-2019 The Software Heritage developers +# Copyright (C) 2018-2021 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information @@ -33,528 +33,504 @@ # -from collections import deque -from contextlib import contextmanager -import os import socket import time -import unittest import pytest +from swh.core.pytest_plugin import FakeSocket from swh.core.statsd import Statsd, TimedContextManagerDecorator -@contextmanager -def preserve_envvars(*envvars): - """Context manager preserving the value of environment variables""" - preserved = {} - to_delete = object() +class BrokenSocket(FakeSocket): + def send(self, payload): + raise socket.error("Socket error") - for var in envvars: - preserved[var] = os.environ.get(var, to_delete) - yield +class SlowSocket(FakeSocket): + def send(self, payload): + raise socket.timeout("Socket timeout") - for var in envvars: - old = preserved[var] - if old is not to_delete: - os.environ[var] = old - else: - del os.environ[var] +def assert_almost_equal(a, b, delta): + assert 0 <= abs(a - b) <= delta, f"|{a} - {b}| not within {delta}" -class FakeSocket(object): - """ A fake socket for testing. """ - def __init__(self): - self.payloads = deque() +def test_set(statsd): + statsd.set("set", 123) + assert statsd.socket.recv() == "set:123|s" - def send(self, payload): - assert type(payload) == bytes - self.payloads.append(payload) - def recv(self): - try: - return self.payloads.popleft().decode("utf-8") - except IndexError: - return None +def test_gauge(statsd): + statsd.gauge("gauge", 123.4) + assert statsd.socket.recv() == "gauge:123.4|g" - def close(self): - pass - def __repr__(self): - return str(self.payloads) +def test_counter(statsd): + statsd.increment("page.views") + assert statsd.socket.recv() == "page.views:1|c" + statsd.increment("page.views", 11) + assert statsd.socket.recv() == "page.views:11|c" -class BrokenSocket(FakeSocket): - def send(self, payload): - raise socket.error("Socket error") + statsd.decrement("page.views") + assert statsd.socket.recv() == "page.views:-1|c" + statsd.decrement("page.views", 12) + assert statsd.socket.recv() == "page.views:-12|c" -class SlowSocket(FakeSocket): - def send(self, payload): - raise socket.timeout("Socket timeout") +def test_histogram(statsd): + statsd.histogram("histo", 123.4) + assert statsd.socket.recv() == "histo:123.4|h" -class TestStatsd(unittest.TestCase): - def setUp(self): - """ - Set up a default Statsd instance and mock the socket. - """ - # - self.statsd = Statsd() - self.statsd._socket = FakeSocket() - - def recv(self): - return self.statsd.socket.recv() - - def test_set(self): - self.statsd.set("set", 123) - assert self.recv() == "set:123|s" - - def test_gauge(self): - self.statsd.gauge("gauge", 123.4) - assert self.recv() == "gauge:123.4|g" - - def test_counter(self): - self.statsd.increment("page.views") - self.assertEqual("page.views:1|c", self.recv()) - - self.statsd.increment("page.views", 11) - self.assertEqual("page.views:11|c", self.recv()) - - self.statsd.decrement("page.views") - self.assertEqual("page.views:-1|c", self.recv()) - - self.statsd.decrement("page.views", 12) - self.assertEqual("page.views:-12|c", self.recv()) - - def test_histogram(self): - self.statsd.histogram("histo", 123.4) - self.assertEqual("histo:123.4|h", self.recv()) - - def test_tagged_gauge(self): - self.statsd.gauge("gt", 123.4, tags={"country": "china", "age": 45}) - self.assertEqual("gt:123.4|g|#age:45,country:china", self.recv()) - - def test_tagged_counter(self): - self.statsd.increment("ct", tags={"country": "españa"}) - self.assertEqual("ct:1|c|#country:españa", self.recv()) - - def test_tagged_histogram(self): - self.statsd.histogram("h", 1, tags={"test_tag": "tag_value"}) - self.assertEqual("h:1|h|#test_tag:tag_value", self.recv()) - - def test_sample_rate(self): - self.statsd.increment("c", sample_rate=0) - assert not self.recv() - for i in range(10000): - self.statsd.increment("sampled_counter", sample_rate=0.3) - self.assert_almost_equal(3000, len(self.statsd.socket.payloads), 150) - self.assertEqual("sampled_counter:1|c|@0.3", self.recv()) - - def test_tags_and_samples(self): - for i in range(100): - self.statsd.gauge("gst", 23, tags={"sampled": True}, sample_rate=0.9) - - self.assert_almost_equal(90, len(self.statsd.socket.payloads), 10) - self.assertEqual("gst:23|g|@0.9|#sampled:True", self.recv()) - - def test_timing(self): - self.statsd.timing("t", 123) - self.assertEqual("t:123|ms", self.recv()) - - def test_metric_namespace(self): - """ - Namespace prefixes all metric names. - """ - self.statsd.namespace = "foo" - self.statsd.gauge("gauge", 123.4) - self.assertEqual("foo.gauge:123.4|g", self.recv()) - - # Test Client level constant tags - def test_gauge_constant_tags(self): - self.statsd.constant_tags = { - "bar": "baz", - } - self.statsd.gauge("gauge", 123.4) - assert self.recv() == "gauge:123.4|g|#bar:baz" - - def test_counter_constant_tag_with_metric_level_tags(self): - self.statsd.constant_tags = { - "bar": "baz", - "foo": True, - } - self.statsd.increment("page.views", tags={"extra": "extra"}) - self.assertEqual( - "page.views:1|c|#bar:baz,extra:extra,foo:True", self.recv(), - ) - - def test_gauge_constant_tags_with_metric_level_tags_twice(self): - metric_level_tag = {"foo": "bar"} - self.statsd.constant_tags = {"bar": "baz"} - self.statsd.gauge("gauge", 123.4, tags=metric_level_tag) - assert self.recv() == "gauge:123.4|g|#bar:baz,foo:bar" - - # sending metrics multiple times with same metric-level tags - # should not duplicate the tags being sent - self.statsd.gauge("gauge", 123.4, tags=metric_level_tag) - assert self.recv() == "gauge:123.4|g|#bar:baz,foo:bar" - - def assert_almost_equal(self, a, b, delta): - self.assertTrue( - 0 <= abs(a - b) <= delta, "%s - %s not within %s" % (a, b, delta) - ) - - def test_socket_error(self): - self.statsd._socket = BrokenSocket() - self.statsd.gauge("no error", 1) - assert True, "success" - def test_socket_timeout(self): - self.statsd._socket = SlowSocket() - self.statsd.gauge("no error", 1) - assert True, "success" +def test_tagged_gauge(statsd): + statsd.gauge("gt", 123.4, tags={"country": "china", "age": 45}) + assert statsd.socket.recv() == "gt:123.4|g|#age:45,country:china" - def test_timed(self): - """ - Measure the distribution of a function's run time. - """ - @self.statsd.timed("timed.test") - def func(a, b, c=1, d=1): - """docstring""" - time.sleep(0.5) - return (a, b, c, d) +def test_tagged_counter(statsd): + statsd.increment("ct", tags={"country": "españa"}) + assert statsd.socket.recv() == "ct:1|c|#country:españa" - self.assertEqual("func", func.__name__) - self.assertEqual("docstring", func.__doc__) - result = func(1, 2, d=3) - # Assert it handles args and kwargs correctly. - self.assertEqual(result, (1, 2, 1, 3)) +def test_tagged_histogram(statsd): + statsd.histogram("h", 1, tags={"test_tag": "tag_value"}) + assert statsd.socket.recv() == "h:1|h|#test_tag:tag_value" - packet = self.recv() - name_value, type_ = packet.split("|") - name, value = name_value.split(":") - self.assertEqual("ms", type_) - self.assertEqual("timed.test", name) - self.assert_almost_equal(500, float(value), 100) +def test_sample_rate(statsd): + statsd.increment("c", sample_rate=0) + assert not statsd.socket.recv() + for i in range(10000): + statsd.increment("sampled_counter", sample_rate=0.3) + assert_almost_equal(3000, len(statsd.socket.payloads), 150) + assert statsd.socket.recv() == "sampled_counter:1|c|@0.3" - def test_timed_exception(self): - """ - Exception bubble out of the decorator and is reported - to statsd as a dedicated counter. - """ - @self.statsd.timed("timed.test") - def func(a, b, c=1, d=1): - """docstring""" - time.sleep(0.5) - return (a / b, c, d) +def test_tags_and_samples(statsd): + for i in range(100): + statsd.gauge("gst", 23, tags={"sampled": True}, sample_rate=0.9) - self.assertEqual("func", func.__name__) - self.assertEqual("docstring", func.__doc__) + assert_almost_equal(90, len(statsd.socket.payloads), 10) + assert statsd.socket.recv() == "gst:23|g|@0.9|#sampled:True" - with self.assertRaises(ZeroDivisionError): - func(1, 0) - packet = self.recv() - name_value, type_ = packet.split("|") - name, value = name_value.split(":") +def test_timing(statsd): + statsd.timing("t", 123) + assert statsd.socket.recv() == "t:123|ms" - self.assertEqual("c", type_) - self.assertEqual("timed.test_error_count", name) - self.assertEqual(int(value), 1) - def test_timed_no_metric(self,): - """ - Test using a decorator without providing a metric. - """ +def test_metric_namespace(statsd): + """ + Namespace prefixes all metric names. + """ + statsd.namespace = "foo" + statsd.gauge("gauge", 123.4) + assert statsd.socket.recv() == "foo.gauge:123.4|g" - @self.statsd.timed() - def func(a, b, c=1, d=1): - """docstring""" - time.sleep(0.5) - return (a, b, c, d) - self.assertEqual("func", func.__name__) - self.assertEqual("docstring", func.__doc__) +# Test Client level constant tags +def test_gauge_constant_tags(statsd): + statsd.constant_tags = { + "bar": "baz", + } + statsd.gauge("gauge", 123.4) + assert statsd.socket.recv() == "gauge:123.4|g|#bar:baz" + + +def test_counter_constant_tag_with_metric_level_tags(statsd): + statsd.constant_tags = { + "bar": "baz", + "foo": True, + } + statsd.increment("page.views", tags={"extra": "extra"}) + assert statsd.socket.recv() == "page.views:1|c|#bar:baz,extra:extra,foo:True" + + +def test_gauge_constant_tags_with_metric_level_tags_twice(statsd): + metric_level_tag = {"foo": "bar"} + statsd.constant_tags = {"bar": "baz"} + statsd.gauge("gauge", 123.4, tags=metric_level_tag) + assert statsd.socket.recv() == "gauge:123.4|g|#bar:baz,foo:bar" + + # sending metrics multiple times with same metric-level tags + # should not duplicate the tags being sent + statsd.gauge("gauge", 123.4, tags=metric_level_tag) + assert statsd.socket.recv() == "gauge:123.4|g|#bar:baz,foo:bar" + + +def test_socket_error(statsd): + statsd._socket = BrokenSocket() + statsd.gauge("no error", 1) + assert True, "success" + + +def test_socket_timeout(statsd): + statsd._socket = SlowSocket() + statsd.gauge("no error", 1) + assert True, "success" + + +def test_timed(statsd): + """ + Measure the distribution of a function's run time. + """ + + @statsd.timed("timed.test") + def func(a, b, c=1, d=1): + """docstring""" + time.sleep(0.5) + return (a, b, c, d) + + assert func.__name__ == "func" + assert func.__doc__ == "docstring" + + result = func(1, 2, d=3) + # Assert it handles args and kwargs correctly. + assert result, (1, 2, 1 == 3) + + packet = statsd.socket.recv() + name_value, type_ = packet.split("|") + name, value = name_value.split(":") + + assert type_ == "ms" + assert name == "timed.test" + assert_almost_equal(500, float(value), 100) + + +def test_timed_exception(statsd): + """ + Exception bubble out of the decorator and is reported + to statsd as a dedicated counter. + """ + + @statsd.timed("timed.test") + def func(a, b, c=1, d=1): + """docstring""" + time.sleep(0.5) + return (a / b, c, d) + + assert func.__name__ == "func" + assert func.__doc__ == "docstring" + + with pytest.raises(ZeroDivisionError): + func(1, 0) + + packet = statsd.socket.recv() + name_value, type_ = packet.split("|") + name, value = name_value.split(":") + + assert type_ == "c" + assert name == "timed.test_error_count" + assert int(value) == 1 + + +def test_timed_no_metric(statsd): + """ + Test using a decorator without providing a metric. + """ + + @statsd.timed() + def func(a, b, c=1, d=1): + """docstring""" + time.sleep(0.5) + return (a, b, c, d) + + assert func.__name__ == "func" + assert func.__doc__ == "docstring" + + result = func(1, 2, d=3) + # Assert it handles args and kwargs correctly. + assert result, (1, 2, 1 == 3) + + packet = statsd.socket.recv() + name_value, type_ = packet.split("|") + name, value = name_value.split(":") + + assert type_ == "ms" + assert name == "swh.core.tests.test_statsd.func" + assert_almost_equal(500, float(value), 100) + + +def test_timed_coroutine(statsd): + """ + Measure the distribution of a coroutine function's run time. + + Warning: Python >= 3.5 only. + """ + import asyncio + + @statsd.timed("timed.test") + @asyncio.coroutine + def print_foo(): + """docstring""" + time.sleep(0.5) + print("foo") + + loop = asyncio.new_event_loop() + loop.run_until_complete(print_foo()) + loop.close() + + # Assert + packet = statsd.socket.recv() + name_value, type_ = packet.split("|") + name, value = name_value.split(":") + + assert type_ == "ms" + assert name == "timed.test" + assert_almost_equal(500, float(value), 100) + - result = func(1, 2, d=3) - # Assert it handles args and kwargs correctly. - self.assertEqual(result, (1, 2, 1, 3)) +def test_timed_context(statsd): + """ + Measure the distribution of a context's run time. + """ + # In milliseconds + with statsd.timed("timed_context.test") as timer: + assert isinstance(timer, TimedContextManagerDecorator) + time.sleep(0.5) + + packet = statsd.socket.recv() + name_value, type_ = packet.split("|") + name, value = name_value.split(":") - packet = self.recv() - name_value, type_ = packet.split("|") - name, value = name_value.split(":") + assert type_ == "ms" + assert name == "timed_context.test" + assert_almost_equal(500, float(value), 100) + assert_almost_equal(500, timer.elapsed, 100) - self.assertEqual("ms", type_) - self.assertEqual("swh.core.tests.test_statsd.func", name) - self.assert_almost_equal(500, float(value), 100) - def test_timed_coroutine(self): - """ - Measure the distribution of a coroutine function's run time. +def test_timed_context_exception(statsd): + """ + Exception bubbles out of the `timed` context manager and is + reported to statsd as a dedicated counter. + """ - Warning: Python >= 3.5 only. - """ - import asyncio + class ContextException(Exception): + pass - @self.statsd.timed("timed.test") - @asyncio.coroutine - def print_foo(): - """docstring""" + def func(statsd): + with statsd.timed("timed_context.test"): time.sleep(0.5) - print("foo") - - loop = asyncio.new_event_loop() - loop.run_until_complete(print_foo()) - loop.close() - - # Assert - packet = self.recv() - name_value, type_ = packet.split("|") - name, value = name_value.split(":") - - self.assertEqual("ms", type_) - self.assertEqual("timed.test", name) - self.assert_almost_equal(500, float(value), 100) - - def test_timed_context(self): - """ - Measure the distribution of a context's run time. - """ - # In milliseconds - with self.statsd.timed("timed_context.test") as timer: - self.assertIsInstance(timer, TimedContextManagerDecorator) + raise ContextException() + + # Ensure the exception was raised. + with pytest.raises(ContextException): + func(statsd) + + # Ensure the timing was recorded. + packet = statsd.socket.recv() + name_value, type_ = packet.split("|") + name, value = name_value.split(":") + + assert type_ == "c" + assert name == "timed_context.test_error_count" + assert int(value) == 1 + + +def test_timed_context_no_metric_name_exception(statsd): + """Test that an exception occurs if using a context manager without a + metric name. + """ + + def func(statsd): + with statsd.timed(): time.sleep(0.5) - packet = self.recv() - name_value, type_ = packet.split("|") - name, value = name_value.split(":") + # Ensure the exception was raised. + with pytest.raises(TypeError): + func(statsd) - self.assertEqual("ms", type_) - self.assertEqual("timed_context.test", name) - self.assert_almost_equal(500, float(value), 100) - self.assert_almost_equal(500, timer.elapsed, 100) + # Ensure the timing was recorded. + packet = statsd.socket.recv() + assert packet is None - def test_timed_context_exception(self): - """ - Exception bubbles out of the `timed` context manager and is - reported to statsd as a dedicated counter. - """ - class ContextException(Exception): - pass +def test_timed_start_stop_calls(statsd): + timer = statsd.timed("timed_context.test") + timer.start() + time.sleep(0.5) + timer.stop() - def func(self): - with self.statsd.timed("timed_context.test"): - time.sleep(0.5) - raise ContextException() + packet = statsd.socket.recv() + name_value, type_ = packet.split("|") + name, value = name_value.split(":") - # Ensure the exception was raised. - self.assertRaises(ContextException, func, self) + assert type_ == "ms" + assert name == "timed_context.test" + assert_almost_equal(500, float(value), 100) - # Ensure the timing was recorded. - packet = self.recv() - name_value, type_ = packet.split("|") - name, value = name_value.split(":") - self.assertEqual("c", type_) - self.assertEqual("timed_context.test_error_count", name) - self.assertEqual(int(value), 1) +def test_batched(statsd): + statsd.open_buffer() + statsd.gauge("page.views", 123) + statsd.timing("timer", 123) + statsd.close_buffer() - def test_timed_context_no_metric_name_exception(self): - """Test that an exception occurs if using a context manager without a - metric name. - """ + assert statsd.socket.recv() == "page.views:123|g\ntimer:123|ms" - def func(self): - with self.statsd.timed(): - time.sleep(0.5) - # Ensure the exception was raised. - self.assertRaises(TypeError, func, self) +def test_context_manager(): + fake_socket = FakeSocket() + with Statsd() as statsd: + statsd._socket = fake_socket + statsd.gauge("page.views", 123) + statsd.timing("timer", 123) - # Ensure the timing was recorded. - packet = self.recv() - self.assertEqual(packet, None) + assert fake_socket.recv() == "page.views:123|g\ntimer:123|ms" - def test_timed_start_stop_calls(self): - timer = self.statsd.timed("timed_context.test") - timer.start() - time.sleep(0.5) - timer.stop() - - packet = self.recv() - name_value, type_ = packet.split("|") - name, value = name_value.split(":") - - self.assertEqual("ms", type_) - self.assertEqual("timed_context.test", name) - self.assert_almost_equal(500, float(value), 100) - - def test_batched(self): - self.statsd.open_buffer() - self.statsd.gauge("page.views", 123) - self.statsd.timing("timer", 123) - self.statsd.close_buffer() - - self.assertEqual("page.views:123|g\ntimer:123|ms", self.recv()) - - def test_context_manager(self): - fake_socket = FakeSocket() - with Statsd() as statsd: - statsd._socket = fake_socket - statsd.gauge("page.views", 123) - statsd.timing("timer", 123) - - self.assertEqual("page.views:123|g\ntimer:123|ms", fake_socket.recv()) - - def test_batched_buffer_autoflush(self): - fake_socket = FakeSocket() - with Statsd() as statsd: - statsd._socket = fake_socket - for i in range(51): - statsd.increment("mycounter") - self.assertEqual( - "\n".join(["mycounter:1|c" for i in range(50)]), fake_socket.recv(), - ) - - self.assertEqual("mycounter:1|c", fake_socket.recv()) - - def test_module_level_instance(self): - from swh.core.statsd import statsd - - self.assertTrue(isinstance(statsd, Statsd)) - - def test_instantiating_does_not_connect(self): - local_statsd = Statsd() - self.assertEqual(None, local_statsd._socket) - - def test_accessing_socket_opens_socket(self): - local_statsd = Statsd() - try: - self.assertIsNotNone(local_statsd.socket) - finally: - local_statsd.close_socket() - - def test_accessing_socket_multiple_times_returns_same_socket(self): - local_statsd = Statsd() - fresh_socket = FakeSocket() - local_statsd._socket = fresh_socket - self.assertEqual(fresh_socket, local_statsd.socket) - self.assertNotEqual(FakeSocket(), local_statsd.socket) - - def test_tags_from_environment(self): - with preserve_envvars("STATSD_TAGS"): - os.environ["STATSD_TAGS"] = "country:china,age:45" - statsd = Statsd() - - statsd._socket = FakeSocket() - statsd.gauge("gt", 123.4) - self.assertEqual("gt:123.4|g|#age:45,country:china", statsd.socket.recv()) - - def test_tags_from_environment_and_constant(self): - with preserve_envvars("STATSD_TAGS"): - os.environ["STATSD_TAGS"] = "country:china,age:45" - statsd = Statsd(constant_tags={"country": "canada"}) - statsd._socket = FakeSocket() - statsd.gauge("gt", 123.4) - self.assertEqual("gt:123.4|g|#age:45,country:canada", statsd.socket.recv()) - - def test_tags_from_environment_warning(self): - with preserve_envvars("STATSD_TAGS"): - os.environ["STATSD_TAGS"] = "valid:tag,invalid_tag" - with pytest.warns(UserWarning) as record: - statsd = Statsd() - - assert len(record) == 1 - assert "invalid_tag" in record[0].message.args[0] - assert "valid:tag" not in record[0].message.args[0] - assert statsd.constant_tags == {"valid": "tag"} - - def test_gauge_doesnt_send_none(self): - self.statsd.gauge("metric", None) - assert self.recv() is None - - def test_increment_doesnt_send_none(self): - self.statsd.increment("metric", None) - assert self.recv() is None - - def test_decrement_doesnt_send_none(self): - self.statsd.decrement("metric", None) - assert self.recv() is None - - def test_timing_doesnt_send_none(self): - self.statsd.timing("metric", None) - assert self.recv() is None - - def test_histogram_doesnt_send_none(self): - self.statsd.histogram("metric", None) - assert self.recv() is None - - def test_param_host(self): - with preserve_envvars("STATSD_HOST", "STATSD_PORT"): - os.environ["STATSD_HOST"] = "test-value" - os.environ["STATSD_PORT"] = "" - local_statsd = Statsd(host="actual-test-value") - - self.assertEqual(local_statsd.host, "actual-test-value") - self.assertEqual(local_statsd.port, 8125) - - def test_param_port(self): - with preserve_envvars("STATSD_HOST", "STATSD_PORT"): - os.environ["STATSD_HOST"] = "" - os.environ["STATSD_PORT"] = "12345" - local_statsd = Statsd(port=4321) - - self.assertEqual(local_statsd.host, "localhost") - self.assertEqual(local_statsd.port, 4321) - - def test_envvar_host(self): - with preserve_envvars("STATSD_HOST", "STATSD_PORT"): - os.environ["STATSD_HOST"] = "test-value" - os.environ["STATSD_PORT"] = "" - local_statsd = Statsd() - - self.assertEqual(local_statsd.host, "test-value") - self.assertEqual(local_statsd.port, 8125) - - def test_envvar_port(self): - with preserve_envvars("STATSD_HOST", "STATSD_PORT"): - os.environ["STATSD_HOST"] = "" - os.environ["STATSD_PORT"] = "12345" - local_statsd = Statsd() - - self.assertEqual(local_statsd.host, "localhost") - self.assertEqual(local_statsd.port, 12345) - - def test_namespace_added(self): - local_statsd = Statsd(namespace="test-namespace") - local_statsd._socket = FakeSocket() - - local_statsd.gauge("gauge", 123.4) - assert local_statsd.socket.recv() == "test-namespace.gauge:123.4|g" - - def test_contextmanager_empty(self): - with self.statsd: - assert True, "success" - - def test_contextmanager_buffering(self): - with self.statsd as s: - s.gauge("gauge", 123.4) - s.gauge("gauge_other", 456.78) - self.assertIsNone(s.socket.recv()) - - self.assertEqual(self.recv(), "gauge:123.4|g\ngauge_other:456.78|g") - - def test_timed_elapsed(self): - with self.statsd.timed("test_timer") as t: - pass - - self.assertGreaterEqual(t.elapsed, 0) - self.assertEqual(self.recv(), "test_timer:%s|ms" % t.elapsed) + +def test_batched_buffer_autoflush(): + fake_socket = FakeSocket() + with Statsd() as statsd: + statsd._socket = fake_socket + for i in range(51): + statsd.increment("mycounter") + assert "\n".join(["mycounter:1|c" for i in range(50)]) == fake_socket.recv() + + assert fake_socket.recv() == "mycounter:1|c" + + +def test_module_level_instance(statsd): + from swh.core.statsd import statsd + + assert isinstance(statsd, Statsd) + + +def test_instantiating_does_not_connect(): + local_statsd = Statsd() + assert local_statsd._socket is None + + +def test_accessing_socket_opens_socket(): + local_statsd = Statsd() + try: + assert local_statsd.socket is not None + finally: + local_statsd.close_socket() + + +def test_accessing_socket_multiple_times_returns_same_socket(): + local_statsd = Statsd() + fresh_socket = FakeSocket() + local_statsd._socket = fresh_socket + assert fresh_socket == local_statsd.socket + assert FakeSocket() != local_statsd.socket + + +def test_tags_from_environment(monkeypatch): + monkeypatch.setenv("STATSD_TAGS", "country:china,age:45") + statsd = Statsd() + statsd._socket = FakeSocket() + statsd.gauge("gt", 123.4) + assert statsd.socket.recv() == "gt:123.4|g|#age:45,country:china" + + +def test_tags_from_environment_and_constant(monkeypatch): + monkeypatch.setenv("STATSD_TAGS", "country:china,age:45") + statsd = Statsd(constant_tags={"country": "canada"}) + statsd._socket = FakeSocket() + statsd.gauge("gt", 123.4) + assert statsd.socket.recv() == "gt:123.4|g|#age:45,country:canada" + + +def test_tags_from_environment_warning(monkeypatch): + monkeypatch.setenv("STATSD_TAGS", "valid:tag,invalid_tag") + with pytest.warns(UserWarning) as record: + statsd = Statsd() + + assert len(record) == 1 + assert "invalid_tag" in record[0].message.args[0] + assert "valid:tag" not in record[0].message.args[0] + assert statsd.constant_tags == {"valid": "tag"} + + +def test_gauge_doesnt_send_none(statsd): + statsd.gauge("metric", None) + assert statsd.socket.recv() is None + + +def test_increment_doesnt_send_none(statsd): + statsd.increment("metric", None) + assert statsd.socket.recv() is None + + +def test_decrement_doesnt_send_none(statsd): + statsd.decrement("metric", None) + assert statsd.socket.recv() is None + + +def test_timing_doesnt_send_none(statsd): + statsd.timing("metric", None) + assert statsd.socket.recv() is None + + +def test_histogram_doesnt_send_none(statsd): + statsd.histogram("metric", None) + assert statsd.socket.recv() is None + + +def test_param_host(monkeypatch): + monkeypatch.setenv("STATSD_HOST", "test-value") + monkeypatch.setenv("STATSD_PORT", "") + local_statsd = Statsd(host="actual-test-value") + + assert local_statsd.host == "actual-test-value" + assert local_statsd.port == 8125 + + +def test_param_port(monkeypatch): + monkeypatch.setenv("STATSD_HOST", "") + monkeypatch.setenv("STATSD_PORT", "12345") + local_statsd = Statsd(port=4321) + assert local_statsd.host == "localhost" + assert local_statsd.port == 4321 + + +def test_envvar_host(monkeypatch): + monkeypatch.setenv("STATSD_HOST", "test-value") + monkeypatch.setenv("STATSD_PORT", "") + local_statsd = Statsd() + assert local_statsd.host == "test-value" + assert local_statsd.port == 8125 + + +def test_envvar_port(monkeypatch): + monkeypatch.setenv("STATSD_HOST", "") + monkeypatch.setenv("STATSD_PORT", "12345") + local_statsd = Statsd() + + assert local_statsd.host == "localhost" + assert local_statsd.port == 12345 + + +def test_namespace_added(): + local_statsd = Statsd(namespace="test-namespace") + local_statsd._socket = FakeSocket() + + local_statsd.gauge("gauge", 123.4) + assert local_statsd.socket.recv() == "test-namespace.gauge:123.4|g" + + +def test_contextmanager_empty(statsd): + with statsd: + assert True, "success" + + +def test_contextmanager_buffering(statsd): + with statsd as s: + s.gauge("gauge", 123.4) + s.gauge("gauge_other", 456.78) + assert s.socket.recv() is None + + assert statsd.socket.recv() == "gauge:123.4|g\ngauge_other:456.78|g" + + +def test_timed_elapsed(statsd): + with statsd.timed("test_timer") as t: + pass + + assert t.elapsed >= 0 + assert statsd.socket.recv() == "test_timer:%s|ms" % t.elapsed