2025-08-09 12:05:18 -05:00
|
|
|
# -*- coding: utf-8; -*-
|
|
|
|
|
2025-08-10 15:13:20 -05:00
|
|
|
from unittest.mock import patch, MagicMock
|
2025-08-09 12:05:18 -05:00
|
|
|
|
|
|
|
from wuttjamaican.testing import ConfigTestCase
|
|
|
|
|
|
|
|
from wuttatell import telemetry as mod
|
|
|
|
|
|
|
|
|
|
|
|
class TestTelemetryHandler(ConfigTestCase):
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
super().setUp()
|
|
|
|
self.handler = self.make_handler()
|
|
|
|
|
|
|
|
def make_handler(self):
|
|
|
|
return mod.TelemetryHandler(self.config)
|
|
|
|
|
|
|
|
def test_get_profile(self):
|
|
|
|
|
|
|
|
# default
|
2025-08-31 12:47:46 -05:00
|
|
|
default = self.handler.get_profile("default")
|
2025-08-09 12:05:18 -05:00
|
|
|
self.assertIsInstance(default, mod.TelemetryProfile)
|
2025-08-31 12:47:46 -05:00
|
|
|
self.assertEqual(default.key, "default")
|
2025-08-09 12:05:18 -05:00
|
|
|
|
|
|
|
# same profile is returned
|
|
|
|
profile = self.handler.get_profile(default)
|
|
|
|
self.assertIs(profile, default)
|
|
|
|
|
|
|
|
def test_collect_data_os(self):
|
2025-08-31 12:47:46 -05:00
|
|
|
profile = self.handler.get_profile("default")
|
2025-08-09 12:05:18 -05:00
|
|
|
|
|
|
|
# typical / working scenario
|
|
|
|
data = self.handler.collect_data_os(profile)
|
|
|
|
self.assertIsInstance(data, dict)
|
2025-08-31 12:47:46 -05:00
|
|
|
self.assertIn("release_id", data)
|
|
|
|
self.assertIn("release_version", data)
|
|
|
|
self.assertIn("release_full", data)
|
|
|
|
self.assertIn("timezone", data)
|
|
|
|
self.assertNotIn("errors", data)
|
2025-08-09 12:05:18 -05:00
|
|
|
|
|
|
|
# unreadable release path
|
2025-08-31 12:47:46 -05:00
|
|
|
data = self.handler.collect_data_os(
|
|
|
|
profile, release_path="/a/path/which/does/not/exist"
|
|
|
|
)
|
2025-08-09 12:05:18 -05:00
|
|
|
self.assertIsInstance(data, dict)
|
2025-08-31 12:47:46 -05:00
|
|
|
self.assertNotIn("release_id", data)
|
|
|
|
self.assertNotIn("release_version", data)
|
|
|
|
self.assertNotIn("release_full", data)
|
|
|
|
self.assertIn("timezone", data)
|
|
|
|
self.assertIn("errors", data)
|
|
|
|
self.assertEqual(
|
|
|
|
data["errors"], ["Failed to read /a/path/which/does/not/exist"]
|
|
|
|
)
|
2025-08-09 12:05:18 -05:00
|
|
|
|
|
|
|
# unparsable release path
|
2025-08-31 12:47:46 -05:00
|
|
|
path = self.write_file("release", "bad-content")
|
2025-08-09 12:05:18 -05:00
|
|
|
data = self.handler.collect_data_os(profile, release_path=path)
|
|
|
|
self.assertIsInstance(data, dict)
|
2025-08-31 12:47:46 -05:00
|
|
|
self.assertNotIn("release_id", data)
|
|
|
|
self.assertNotIn("release_version", data)
|
|
|
|
self.assertNotIn("release_full", data)
|
|
|
|
self.assertIn("timezone", data)
|
|
|
|
self.assertIn("errors", data)
|
|
|
|
self.assertEqual(data["errors"], [f"Failed to parse {path}"])
|
2025-08-09 12:05:18 -05:00
|
|
|
|
|
|
|
# unreadable timezone path
|
2025-08-31 12:47:46 -05:00
|
|
|
data = self.handler.collect_data_os(
|
|
|
|
profile, timezone_path="/a/path/which/does/not/exist"
|
|
|
|
)
|
2025-08-09 12:05:18 -05:00
|
|
|
self.assertIsInstance(data, dict)
|
2025-08-31 12:47:46 -05:00
|
|
|
self.assertIn("release_id", data)
|
|
|
|
self.assertIn("release_version", data)
|
|
|
|
self.assertIn("release_full", data)
|
|
|
|
self.assertNotIn("timezone", data)
|
|
|
|
self.assertIn("errors", data)
|
|
|
|
self.assertEqual(
|
|
|
|
data["errors"], ["Failed to read /a/path/which/does/not/exist"]
|
|
|
|
)
|
2025-08-09 12:05:18 -05:00
|
|
|
|
|
|
|
def test_collect_data_python(self):
|
2025-08-31 12:47:46 -05:00
|
|
|
profile = self.handler.get_profile("default")
|
2025-08-09 12:05:18 -05:00
|
|
|
|
|
|
|
# typical / working (system-wide) scenario
|
|
|
|
data = self.handler.collect_data_python(profile)
|
|
|
|
self.assertIsInstance(data, dict)
|
2025-08-31 12:47:46 -05:00
|
|
|
self.assertNotIn("envroot", data)
|
|
|
|
self.assertIn("executable", data)
|
|
|
|
self.assertIn("release_full", data)
|
|
|
|
self.assertIn("release_version", data)
|
|
|
|
self.assertNotIn("errors", data)
|
2025-08-09 12:05:18 -05:00
|
|
|
|
|
|
|
# missing executable
|
2025-08-31 12:47:46 -05:00
|
|
|
with patch.dict(
|
|
|
|
self.config.defaults,
|
|
|
|
{"wutta.telemetry.default.collect.python.executable": "/bad/path"},
|
|
|
|
):
|
2025-08-09 12:05:18 -05:00
|
|
|
data = self.handler.collect_data_python(profile)
|
|
|
|
self.assertIsInstance(data, dict)
|
2025-08-31 12:47:46 -05:00
|
|
|
self.assertNotIn("envroot", data)
|
|
|
|
self.assertIn("executable", data)
|
|
|
|
self.assertNotIn("release_full", data)
|
|
|
|
self.assertNotIn("release_version", data)
|
|
|
|
self.assertIn("errors", data)
|
|
|
|
self.assertEqual(data["errors"][0], "Failed to execute `python --version`")
|
2025-08-09 12:05:18 -05:00
|
|
|
|
|
|
|
# unparsable executable output
|
2025-08-31 12:47:46 -05:00
|
|
|
with patch.object(mod, "subprocess") as subprocess:
|
|
|
|
subprocess.check_output.return_value = "bad output".encode("utf_8")
|
2025-08-09 12:05:18 -05:00
|
|
|
|
|
|
|
data = self.handler.collect_data_python(profile)
|
|
|
|
self.assertIsInstance(data, dict)
|
2025-08-31 12:47:46 -05:00
|
|
|
self.assertNotIn("envroot", data)
|
|
|
|
self.assertIn("executable", data)
|
|
|
|
self.assertIn("release_full", data)
|
|
|
|
self.assertNotIn("release_version", data)
|
|
|
|
self.assertIn("errors", data)
|
|
|
|
self.assertEqual(
|
|
|
|
data["errors"],
|
|
|
|
[
|
|
|
|
"Failed to parse Python version",
|
|
|
|
],
|
|
|
|
)
|
2025-08-09 12:05:18 -05:00
|
|
|
|
|
|
|
# typical / working (virtual environment) scenario
|
2025-08-31 12:47:46 -05:00
|
|
|
self.config.setdefault(
|
|
|
|
"wutta.telemetry.default.collect.python.envroot", "/srv/envs/poser"
|
|
|
|
)
|
2025-08-09 12:05:18 -05:00
|
|
|
data = self.handler.collect_data_python(profile)
|
|
|
|
self.assertIsInstance(data, dict)
|
2025-08-31 12:47:46 -05:00
|
|
|
self.assertIn("executable", data)
|
|
|
|
self.assertEqual(data["executable"], "/srv/envs/poser/bin/python")
|
|
|
|
self.assertNotIn("release_full", data)
|
|
|
|
self.assertNotIn("release_version", data)
|
|
|
|
self.assertIn("errors", data)
|
|
|
|
self.assertEqual(data["errors"][0], "Failed to execute `python --version`")
|
2025-08-09 12:05:18 -05:00
|
|
|
|
|
|
|
def test_normalize_errors(self):
|
|
|
|
data = {
|
2025-08-31 12:47:46 -05:00
|
|
|
"os": {
|
|
|
|
"timezone": "America/Chicago",
|
|
|
|
"errors": [
|
2025-08-09 12:05:18 -05:00
|
|
|
"Failed to read /etc/os-release",
|
|
|
|
],
|
|
|
|
},
|
2025-08-31 12:47:46 -05:00
|
|
|
"python": {
|
|
|
|
"executable": "/usr/bin/python3",
|
|
|
|
"errors": [
|
2025-08-09 12:05:18 -05:00
|
|
|
"Failed to run `python --version`",
|
|
|
|
],
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
self.handler.normalize_errors(data)
|
2025-08-31 12:47:46 -05:00
|
|
|
self.assertIn("os", data)
|
|
|
|
self.assertIn("python", data)
|
|
|
|
self.assertIn("errors", data)
|
|
|
|
self.assertEqual(
|
|
|
|
data["errors"],
|
|
|
|
[
|
|
|
|
"Failed to read /etc/os-release",
|
|
|
|
"Failed to run `python --version`",
|
|
|
|
],
|
|
|
|
)
|
2025-08-09 12:05:18 -05:00
|
|
|
|
|
|
|
def test_collect_all_data(self):
|
|
|
|
|
|
|
|
# typical / working scenario
|
|
|
|
data = self.handler.collect_all_data()
|
|
|
|
self.assertIsInstance(data, dict)
|
2025-08-31 12:47:46 -05:00
|
|
|
self.assertIn("os", data)
|
|
|
|
self.assertIn("python", data)
|
|
|
|
self.assertNotIn("errors", data)
|
2025-08-09 12:05:18 -05:00
|
|
|
|
|
|
|
def test_submit_all_data(self):
|
2025-08-31 12:47:46 -05:00
|
|
|
profile = self.handler.get_profile("default")
|
|
|
|
profile.submit_url = "/testing"
|
2025-08-10 15:13:20 -05:00
|
|
|
|
2025-08-31 12:47:46 -05:00
|
|
|
with patch.object(mod, "SimpleAPIClient") as SimpleAPIClient:
|
2025-08-10 15:13:20 -05:00
|
|
|
client = MagicMock()
|
|
|
|
SimpleAPIClient.return_value = client
|
|
|
|
|
|
|
|
# collecting all data
|
2025-08-31 12:47:46 -05:00
|
|
|
with patch.object(self.handler, "collect_all_data") as collect_all_data:
|
2025-08-10 15:13:20 -05:00
|
|
|
collect_all_data.return_value = []
|
|
|
|
self.handler.submit_all_data(profile)
|
|
|
|
collect_all_data.assert_called_once_with(profile)
|
2025-08-31 12:47:46 -05:00
|
|
|
client.post.assert_called_once_with("/testing", data=[])
|
2025-08-10 15:13:20 -05:00
|
|
|
|
|
|
|
# use data from caller
|
|
|
|
client.post.reset_mock()
|
2025-08-31 12:47:46 -05:00
|
|
|
self.handler.submit_all_data(profile, data=["foo"])
|
|
|
|
client.post.assert_called_once_with("/testing", data=["foo"])
|
2025-08-09 12:05:18 -05:00
|
|
|
|
|
|
|
|
|
|
|
class TestTelemetryProfile(ConfigTestCase):
|
|
|
|
|
2025-08-31 12:47:46 -05:00
|
|
|
def make_profile(self, key="default"):
|
2025-08-09 12:05:18 -05:00
|
|
|
return mod.TelemetryProfile(self.config, key)
|
|
|
|
|
|
|
|
def test_section(self):
|
|
|
|
|
|
|
|
# default
|
|
|
|
profile = self.make_profile()
|
2025-08-31 12:47:46 -05:00
|
|
|
self.assertEqual(profile.section, "wutta.telemetry")
|
2025-08-09 12:05:18 -05:00
|
|
|
|
|
|
|
# custom appname
|
2025-08-31 12:47:46 -05:00
|
|
|
with patch.object(self.config, "appname", new="wuttatest"):
|
2025-08-09 12:05:18 -05:00
|
|
|
profile = self.make_profile()
|
2025-08-31 12:47:46 -05:00
|
|
|
self.assertEqual(profile.section, "wuttatest.telemetry")
|
2025-08-09 12:05:18 -05:00
|
|
|
|
|
|
|
def test_load(self):
|
|
|
|
|
|
|
|
# defaults
|
|
|
|
profile = self.make_profile()
|
2025-08-31 12:47:46 -05:00
|
|
|
self.assertEqual(profile.collect_keys, ["os", "python"])
|
2025-08-09 12:05:18 -05:00
|
|
|
self.assertIsNone(profile.submit_url)
|
|
|
|
|
|
|
|
# configured
|
2025-08-31 12:47:46 -05:00
|
|
|
self.config.setdefault(
|
|
|
|
"wutta.telemetry.default.collect.keys", "os,network,python"
|
|
|
|
)
|
|
|
|
self.config.setdefault("wutta.telemetry.default.submit.url", "/nodes/telemetry")
|
2025-08-09 12:05:18 -05:00
|
|
|
profile = self.make_profile()
|
2025-08-31 12:47:46 -05:00
|
|
|
self.assertEqual(profile.collect_keys, ["os", "network", "python"])
|
|
|
|
self.assertEqual(profile.submit_url, "/nodes/telemetry")
|