fix: format all code with black

and from now on should not deviate from that...
This commit is contained in:
Lance Edgar 2025-08-31 12:47:46 -05:00
parent 18f7fa6c51
commit 8be1b66c9e
10 changed files with 334 additions and 258 deletions

View file

@ -8,33 +8,33 @@
from importlib.metadata import version as get_version
project = 'WuttaTell'
copyright = '2025, Lance Edgar'
author = 'Lance Edgar'
release = get_version('WuttaTell')
project = "WuttaTell"
copyright = "2025, Lance Edgar"
author = "Lance Edgar"
release = get_version("WuttaTell")
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.viewcode',
'sphinx.ext.todo',
'sphinxcontrib.programoutput',
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx.ext.viewcode",
"sphinx.ext.todo",
"sphinxcontrib.programoutput",
]
templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
intersphinx_mapping = {
'requests': ('https://requests.readthedocs.io/en/latest/', None),
'wuttjamaican': ('https://docs.wuttaproject.org/wuttjamaican/', None),
"requests": ("https://requests.readthedocs.io/en/latest/", None),
"wuttjamaican": ("https://docs.wuttaproject.org/wuttjamaican/", None),
}
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'furo'
html_static_path = ['_static']
html_theme = "furo"
html_static_path = ["_static"]

View file

@ -3,4 +3,4 @@
from importlib.metadata import version
__version__ = version('WuttaTell')
__version__ = version("WuttaTell")

View file

@ -41,9 +41,11 @@ class WuttaTellAppProvider(AppProvider):
:rtype: :class:`~wuttatell.telemetry.TelemetryHandler`
"""
if not hasattr(self, 'telemetry_handler'):
spec = self.config.get(f'{self.appname}.telemetry.handler',
default='wuttatell.telemetry:TelemetryHandler')
if not hasattr(self, "telemetry_handler"):
spec = self.config.get(
f"{self.appname}.telemetry.handler",
default="wuttatell.telemetry:TelemetryHandler",
)
factory = self.app.load_object(spec)
self.telemetry_handler = factory(self.config, **kwargs)
return self.telemetry_handler

View file

@ -37,18 +37,24 @@ log = logging.getLogger(__name__)
@wutta_typer.command()
def tell(
ctx: typer.Context,
profile: Annotated[
str,
typer.Option('--profile', '-p',
help="Profile (type) of telemetry data to collect. "
"This also determines where/how data is submitted. "
"If not specified, default profile is assumed.")] = None,
dry_run: Annotated[
bool,
typer.Option('--dry-run',
help="Go through all the motions but do not submit "
"the data to server.")] = False,
ctx: typer.Context,
profile: Annotated[
str,
typer.Option(
"--profile",
"-p",
help="Profile (type) of telemetry data to collect. "
"This also determines where/how data is submitted. "
"If not specified, default profile is assumed.",
),
] = None,
dry_run: Annotated[
bool,
typer.Option(
"--dry-run",
help="Go through all the motions but do not submit " "the data to server.",
),
] = False,
):
"""
Collect and submit telemetry data
@ -58,7 +64,7 @@ def tell(
telemetry = app.get_telemetry_handler()
data = telemetry.collect_all_data(profile=profile)
log.info("data collected for: %s", ', '.join(sorted(data)))
log.info("data collected for: %s", ", ".join(sorted(data)))
log.debug("%s", data)
if dry_run:

View file

@ -71,23 +71,30 @@ class SimpleAPIClient:
API requests.
"""
def __init__(self, config, base_url=None, token=None, ssl_verify=None, max_retries=None):
def __init__(
self, config, base_url=None, token=None, ssl_verify=None, max_retries=None
):
self.config = config
self.base_url = base_url or self.config.require(f'{self.config.appname}.api.base_url')
self.base_url = self.base_url.rstrip('/')
self.token = token or self.config.require(f'{self.config.appname}.api.token')
self.base_url = base_url or self.config.require(
f"{self.config.appname}.api.base_url"
)
self.base_url = self.base_url.rstrip("/")
self.token = token or self.config.require(f"{self.config.appname}.api.token")
if max_retries is not None:
self.max_retries = max_retries
else:
self.max_retries = self.config.get_int(f'{self.config.appname}.api.max_retries')
self.max_retries = self.config.get_int(
f"{self.config.appname}.api.max_retries"
)
if ssl_verify is not None:
self.ssl_verify = ssl_verify
else:
self.ssl_verify = self.config.get_bool(f'{self.config.appname}.api.ssl_verify',
default=True)
self.ssl_verify = self.config.get_bool(
f"{self.config.appname}.api.ssl_verify", default=True
)
self.session = None
@ -122,14 +129,18 @@ class SimpleAPIClient:
# without it, can get error response:
# 400 Client Error: Bad CSRF Origin for url
parts = urlparse(self.base_url)
self.session.headers.update({
'Origin': f'{parts.scheme}://{parts.netloc}',
})
self.session.headers.update(
{
"Origin": f"{parts.scheme}://{parts.netloc}",
}
)
# authenticate via token only (for now?)
self.session.headers.update({
'Authorization': f'Bearer {self.token}',
})
self.session.headers.update(
{
"Authorization": f"Bearer {self.token}",
}
)
def make_request(self, request_method, api_method, params=None, data=None):
"""
@ -153,13 +164,12 @@ class SimpleAPIClient:
:rtype: :class:`requests:requests.Response` instance.
"""
self.init_session()
api_method = api_method.lstrip('/')
url = f'{self.base_url}/{api_method}'
if request_method == 'GET':
api_method = api_method.lstrip("/")
url = f"{self.base_url}/{api_method}"
if request_method == "GET":
response = self.session.get(url, params=params)
elif request_method == 'POST':
response = self.session.post(url, params=params,
data=json.dumps(data))
elif request_method == "POST":
response = self.session.post(url, params=params, data=json.dumps(data))
else:
raise NotImplementedError(f"unsupported request method: {request_method}")
response.raise_for_status()
@ -180,7 +190,7 @@ class SimpleAPIClient:
:rtype: :class:`requests:requests.Response` instance.
"""
return self.make_request('GET', api_method, params=params)
return self.make_request("GET", api_method, params=params)
def post(self, api_method, **kwargs):
"""
@ -194,4 +204,4 @@ class SimpleAPIClient:
:rtype: :class:`requests:requests.Response` instance.
"""
return self.make_request('POST', api_method, **kwargs)
return self.make_request("POST", api_method, **kwargs)

View file

@ -49,7 +49,7 @@ class TelemetryHandler(GenericHandler):
if isinstance(profile, TelemetryProfile):
return profile
return TelemetryProfile(self.config, profile or 'default')
return TelemetryProfile(self.config, profile or "default")
def collect_all_data(self, profile=None):
"""
@ -76,7 +76,7 @@ class TelemetryHandler(GenericHandler):
profile = self.get_profile(profile)
for key in profile.collect_keys:
collector = getattr(self, f'collect_data_{key}')
collector = getattr(self, f"collect_data_{key}")
data[key] = collector(profile=profile)
self.normalize_errors(data)
@ -87,11 +87,11 @@ class TelemetryHandler(GenericHandler):
all_errors = []
for key, value in data.items():
if value:
errors = value.pop('errors', None)
errors = value.pop("errors", None)
if errors:
all_errors.extend(errors)
if all_errors:
data['errors'] = all_errors
data["errors"] = all_errors
def collect_data_os(self, profile, **kwargs):
"""
@ -119,40 +119,40 @@ class TelemetryHandler(GenericHandler):
errors = []
# release
release_path = kwargs.get('release_path', '/etc/os-release')
release_path = kwargs.get("release_path", "/etc/os-release")
try:
with open(release_path, 'rt') as f:
with open(release_path, "rt") as f:
output = f.read()
except:
errors.append(f"Failed to read {release_path}")
else:
release = {}
pattern = re.compile(r'^([^=]+)=(.*)$')
for line in output.strip().split('\n'):
pattern = re.compile(r"^([^=]+)=(.*)$")
for line in output.strip().split("\n"):
if match := pattern.match(line):
key, val = match.groups()
if val.startswith('"') and val.endswith('"'):
val = val.strip('"')
release[key] = val
try:
data['release_id'] = release['ID']
data['release_version'] = release['VERSION_ID']
data['release_full'] = release['PRETTY_NAME']
data["release_id"] = release["ID"]
data["release_version"] = release["VERSION_ID"]
data["release_full"] = release["PRETTY_NAME"]
except KeyError:
errors.append(f"Failed to parse {release_path}")
# timezone
timezone_path = kwargs.get('timezone_path', '/etc/timezone')
timezone_path = kwargs.get("timezone_path", "/etc/timezone")
try:
with open(timezone_path, 'rt') as f:
with open(timezone_path, "rt") as f:
output = f.read()
except:
errors.append(f"Failed to read {timezone_path}")
else:
data['timezone'] = output.strip()
data["timezone"] = output.strip()
if errors:
data['errors'] = errors
data["errors"] = errors
return data
def collect_data_python(self, profile):
@ -191,31 +191,32 @@ class TelemetryHandler(GenericHandler):
errors = []
# envroot determines python executable
envroot = profile.get_str('collect.python.envroot')
envroot = profile.get_str("collect.python.envroot")
if envroot:
data['envroot'] = envroot
python = os.path.join(envroot, 'bin/python')
data["envroot"] = envroot
python = os.path.join(envroot, "bin/python")
else:
python = profile.get_str('collect.python.executable',
default='/usr/bin/python3')
python = profile.get_str(
"collect.python.executable", default="/usr/bin/python3"
)
# python version
data['executable'] = python
data["executable"] = python
try:
output = subprocess.check_output([python, '--version'])
output = subprocess.check_output([python, "--version"])
except (subprocess.CalledProcessError, FileNotFoundError) as err:
errors.append("Failed to execute `python --version`")
errors.append(str(err))
else:
output = output.decode('utf_8').strip()
data['release_full'] = output
if match := re.match(r'^Python (\d+\.\d+\.\d+)', output):
data['release_version'] = match.group(1)
output = output.decode("utf_8").strip()
data["release_full"] = output
if match := re.match(r"^Python (\d+\.\d+\.\d+)", output):
data["release_version"] = match.group(1)
else:
errors.append("Failed to parse Python version")
if errors:
data['errors'] = errors
data["errors"] = errors
return data
def submit_all_data(self, profile=None, data=None):
@ -269,6 +270,6 @@ class TelemetryProfile(WuttaConfigProfile):
def load(self):
""" """
keys = self.get_str('collect.keys', default='os,python')
keys = self.get_str("collect.keys", default="os,python")
self.collect_keys = self.config.parse_list(keys)
self.submit_url = self.get_str('submit.url')
self.submit_url = self.get_str("submit.url")

View file

@ -15,10 +15,10 @@ def release(c, skip_tests=False):
Release a new version of WuttaTell
"""
if not skip_tests:
c.run('pytest')
c.run("pytest")
if os.path.exists('dist'):
shutil.rmtree('dist')
if os.path.exists("dist"):
shutil.rmtree("dist")
c.run('python -m build --sdist')
c.run('twine upload dist/*')
c.run("python -m build --sdist")
c.run("twine upload dist/*")

View file

@ -14,10 +14,10 @@ class TestTell(ConfigTestCase):
ctx = Mock()
ctx.parent = Mock()
ctx.parent.wutta_config = self.config
with patch.object(TelemetryHandler, 'submit_all_data') as submit_all_data:
with patch.object(TelemetryHandler, "submit_all_data") as submit_all_data:
# dry run
with patch.object(TelemetryHandler, 'collect_all_data') as collect_all_data:
with patch.object(TelemetryHandler, "collect_all_data") as collect_all_data:
mod.tell(ctx, dry_run=True)
collect_all_data.assert_called_once_with(profile=None)
submit_all_data.assert_not_called()

View file

@ -21,33 +21,42 @@ class TestSimpleAPIClient(ConfigTestCase):
def test_constructor(self):
# caller specifies params
client = self.make_client(base_url='https://example.com/api/',
token='XYZPDQ12345',
ssl_verify=False,
max_retries=5)
self.assertEqual(client.base_url, 'https://example.com/api') # no trailing slash
self.assertEqual(client.token, 'XYZPDQ12345')
client = self.make_client(
base_url="https://example.com/api/",
token="XYZPDQ12345",
ssl_verify=False,
max_retries=5,
)
self.assertEqual(
client.base_url, "https://example.com/api"
) # no trailing slash
self.assertEqual(client.token, "XYZPDQ12345")
self.assertFalse(client.ssl_verify)
self.assertEqual(client.max_retries, 5)
self.assertIsNone(client.session)
# now with some defaults
client = self.make_client(base_url='https://example.com/api/',
token='XYZPDQ12345')
self.assertEqual(client.base_url, 'https://example.com/api') # no trailing slash
self.assertEqual(client.token, 'XYZPDQ12345')
client = self.make_client(
base_url="https://example.com/api/", token="XYZPDQ12345"
)
self.assertEqual(
client.base_url, "https://example.com/api"
) # no trailing slash
self.assertEqual(client.token, "XYZPDQ12345")
self.assertTrue(client.ssl_verify)
self.assertIsNone(client.max_retries)
self.assertIsNone(client.session)
# now from config
self.config.setdefault('wutta.api.base_url', 'https://another.com/api/')
self.config.setdefault('wutta.api.token', '9843243q4')
self.config.setdefault('wutta.api.ssl_verify', 'false')
self.config.setdefault('wutta.api.max_retries', '4')
self.config.setdefault("wutta.api.base_url", "https://another.com/api/")
self.config.setdefault("wutta.api.token", "9843243q4")
self.config.setdefault("wutta.api.ssl_verify", "false")
self.config.setdefault("wutta.api.max_retries", "4")
client = self.make_client()
self.assertEqual(client.base_url, 'https://another.com/api') # no trailing slash
self.assertEqual(client.token, '9843243q4')
self.assertEqual(
client.base_url, "https://another.com/api"
) # no trailing slash
self.assertEqual(client.token, "9843243q4")
self.assertFalse(client.ssl_verify)
self.assertEqual(client.max_retries, 4)
self.assertIsNone(client.session)
@ -55,16 +64,18 @@ class TestSimpleAPIClient(ConfigTestCase):
def test_init_session(self):
# client begins with no session
client = self.make_client(base_url='https://example.com/api', token='1234')
client = self.make_client(base_url="https://example.com/api", token="1234")
self.assertIsNone(client.session)
# session is created here
client.init_session()
self.assertIsInstance(client.session, requests.Session)
self.assertTrue(client.session.verify)
self.assertTrue(all([a.max_retries.total == 0 for a in client.session.adapters.values()]))
self.assertIn('Authorization', client.session.headers)
self.assertEqual(client.session.headers['Authorization'], 'Bearer 1234')
self.assertTrue(
all([a.max_retries.total == 0 for a in client.session.adapters.values()])
)
self.assertIn("Authorization", client.session.headers)
self.assertEqual(client.session.headers["Authorization"], "Bearer 1234")
# session is never re-created
orig_session = client.session
@ -72,94 +83,124 @@ class TestSimpleAPIClient(ConfigTestCase):
self.assertIs(client.session, orig_session)
# new client/session with no ssl_verify
client = self.make_client(base_url='https://example.com/api', token='1234', ssl_verify=False)
client = self.make_client(
base_url="https://example.com/api", token="1234", ssl_verify=False
)
client.init_session()
self.assertFalse(client.session.verify)
# new client/session with max_retries
client = self.make_client(base_url='https://example.com/api', token='1234', max_retries=5)
client = self.make_client(
base_url="https://example.com/api", token="1234", max_retries=5
)
client.init_session()
self.assertEqual(client.session.adapters['https://example.com/api'].max_retries.total, 5)
self.assertEqual(
client.session.adapters["https://example.com/api"].max_retries.total, 5
)
def test_make_request_get(self):
# start server
threading.Thread(target=start_server).start()
while not SERVER['running']:
while not SERVER["running"]:
time.sleep(0.02)
# server returns our headers
client = self.make_client(base_url=f'http://127.0.0.1:{SERVER["port"]}', token='1234', ssl_verify=False)
response = client.make_request('GET', '/telemetry')
client = self.make_client(
base_url=f'http://127.0.0.1:{SERVER["port"]}',
token="1234",
ssl_verify=False,
)
response = client.make_request("GET", "/telemetry")
result = response.json()
self.assertIn('headers', result)
self.assertIn('Authorization', result['headers'])
self.assertEqual(result['headers']['Authorization'], 'Bearer 1234')
self.assertNotIn('payload', result)
self.assertIn("headers", result)
self.assertIn("Authorization", result["headers"])
self.assertEqual(result["headers"]["Authorization"], "Bearer 1234")
self.assertNotIn("payload", result)
def test_make_request_post(self):
# start server
threading.Thread(target=start_server).start()
while not SERVER['running']:
while not SERVER["running"]:
time.sleep(0.02)
# server returns our headers + payload
client = self.make_client(base_url=f'http://127.0.0.1:{SERVER["port"]}', token='1234', ssl_verify=False)
response = client.make_request('POST', '/telemetry', data={'os': {'name': 'debian'}})
client = self.make_client(
base_url=f'http://127.0.0.1:{SERVER["port"]}',
token="1234",
ssl_verify=False,
)
response = client.make_request(
"POST", "/telemetry", data={"os": {"name": "debian"}}
)
result = response.json()
self.assertIn('headers', result)
self.assertIn('Authorization', result['headers'])
self.assertEqual(result['headers']['Authorization'], 'Bearer 1234')
self.assertIn('payload', result)
self.assertEqual(json.loads(result['payload']), {'os': {'name': 'debian'}})
self.assertIn("headers", result)
self.assertIn("Authorization", result["headers"])
self.assertEqual(result["headers"]["Authorization"], "Bearer 1234")
self.assertIn("payload", result)
self.assertEqual(json.loads(result["payload"]), {"os": {"name": "debian"}})
def test_make_request_unsupported(self):
# start server
threading.Thread(target=start_server).start()
while not SERVER['running']:
while not SERVER["running"]:
time.sleep(0.02)
# e.g. DELETE is not implemented
client = self.make_client(base_url=f'http://127.0.0.1:{SERVER["port"]}', token='1234', ssl_verify=False)
self.assertRaises(NotImplementedError, client.make_request, 'DELETE', '/telemetry')
client = self.make_client(
base_url=f'http://127.0.0.1:{SERVER["port"]}',
token="1234",
ssl_verify=False,
)
self.assertRaises(
NotImplementedError, client.make_request, "DELETE", "/telemetry"
)
# nb. issue valid request to stop the server
client.make_request('GET', '/telemetry')
client.make_request("GET", "/telemetry")
def test_get(self):
# start server
threading.Thread(target=start_server).start()
while not SERVER['running']:
while not SERVER["running"]:
time.sleep(0.02)
# server returns our headers
client = self.make_client(base_url=f'http://127.0.0.1:{SERVER["port"]}', token='1234', ssl_verify=False)
response = client.get('/telemetry')
client = self.make_client(
base_url=f'http://127.0.0.1:{SERVER["port"]}',
token="1234",
ssl_verify=False,
)
response = client.get("/telemetry")
result = response.json()
self.assertIn('headers', result)
self.assertIn('Authorization', result['headers'])
self.assertEqual(result['headers']['Authorization'], 'Bearer 1234')
self.assertNotIn('payload', result)
self.assertIn("headers", result)
self.assertIn("Authorization", result["headers"])
self.assertEqual(result["headers"]["Authorization"], "Bearer 1234")
self.assertNotIn("payload", result)
def test_post(self):
# start server
threading.Thread(target=start_server).start()
while not SERVER['running']:
while not SERVER["running"]:
time.sleep(0.02)
# server returns our headers + payload
client = self.make_client(base_url=f'http://127.0.0.1:{SERVER["port"]}', token='1234', ssl_verify=False)
response = client.post('/telemetry', data={'os': {'name': 'debian'}})
client = self.make_client(
base_url=f'http://127.0.0.1:{SERVER["port"]}',
token="1234",
ssl_verify=False,
)
response = client.post("/telemetry", data={"os": {"name": "debian"}})
result = response.json()
self.assertIn('headers', result)
self.assertIn('Authorization', result['headers'])
self.assertEqual(result['headers']['Authorization'], 'Bearer 1234')
self.assertIn('payload', result)
self.assertEqual(json.loads(result['payload']), {'os': {'name': 'debian'}})
self.assertIn("headers", result)
self.assertIn("Authorization", result["headers"])
self.assertEqual(result["headers"]["Authorization"], "Bearer 1234")
self.assertIn("payload", result)
self.assertEqual(json.loads(result["payload"]), {"os": {"name": "debian"}})
class FakeRequestHandler(BaseHTTPRequestHandler):
@ -167,37 +208,38 @@ class FakeRequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
headers = dict([(k, v) for k, v in self.headers.items()])
result = {'headers': headers}
result = json.dumps(result).encode('utf_8')
result = {"headers": headers}
result = json.dumps(result).encode("utf_8")
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", 'text/json')
self.send_header("Content-Type", "text/json")
self.send_header("Content-Length", str(len(result)))
self.end_headers()
self.wfile.write(result)
def do_POST(self):
headers = dict([(k, v) for k, v in self.headers.items()])
length = int(self.headers.get('Content-Length'))
payload = self.rfile.read(length).decode('utf_8')
result = {'headers': headers, 'payload': payload}
result = json.dumps(result).encode('utf_8')
length = int(self.headers.get("Content-Length"))
payload = self.rfile.read(length).decode("utf_8")
result = {"headers": headers, "payload": payload}
result = json.dumps(result).encode("utf_8")
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", 'text/json')
self.send_header("Content-Type", "text/json")
self.send_header("Content-Length", str(len(result)))
self.end_headers()
self.wfile.write(result)
SERVER = {'running': False, 'port': 7314}
SERVER = {"running": False, "port": 7314}
def start_server():
if SERVER['running']:
if SERVER["running"]:
raise RuntimeError("http server is already running")
with HTTPServer(('127.0.0.1', SERVER['port']), FakeRequestHandler) as httpd:
SERVER['running'] = True
with HTTPServer(("127.0.0.1", SERVER["port"]), FakeRequestHandler) as httpd:
SERVER["running"] = True
httpd.handle_request()
SERVER['running'] = False
SERVER["running"] = False

View file

@ -19,193 +19,208 @@ class TestTelemetryHandler(ConfigTestCase):
def test_get_profile(self):
# default
default = self.handler.get_profile('default')
default = self.handler.get_profile("default")
self.assertIsInstance(default, mod.TelemetryProfile)
self.assertEqual(default.key, 'default')
self.assertEqual(default.key, "default")
# same profile is returned
profile = self.handler.get_profile(default)
self.assertIs(profile, default)
def test_collect_data_os(self):
profile = self.handler.get_profile('default')
profile = self.handler.get_profile("default")
# typical / working scenario
data = self.handler.collect_data_os(profile)
self.assertIsInstance(data, dict)
self.assertIn('release_id', data)
self.assertIn('release_version', data)
self.assertIn('release_full', data)
self.assertIn('timezone', data)
self.assertNotIn('errors', data)
self.assertIn("release_id", data)
self.assertIn("release_version", data)
self.assertIn("release_full", data)
self.assertIn("timezone", data)
self.assertNotIn("errors", data)
# unreadable release path
data = self.handler.collect_data_os(profile, release_path='/a/path/which/does/not/exist')
data = self.handler.collect_data_os(
profile, release_path="/a/path/which/does/not/exist"
)
self.assertIsInstance(data, dict)
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"
])
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"]
)
# unparsable release path
path = self.write_file('release', "bad-content")
path = self.write_file("release", "bad-content")
data = self.handler.collect_data_os(profile, release_path=path)
self.assertIsInstance(data, dict)
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}"
])
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}"])
# unreadable timezone path
data = self.handler.collect_data_os(profile, timezone_path='/a/path/which/does/not/exist')
data = self.handler.collect_data_os(
profile, timezone_path="/a/path/which/does/not/exist"
)
self.assertIsInstance(data, dict)
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"
])
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"]
)
def test_collect_data_python(self):
profile = self.handler.get_profile('default')
profile = self.handler.get_profile("default")
# typical / working (system-wide) scenario
data = self.handler.collect_data_python(profile)
self.assertIsInstance(data, dict)
self.assertNotIn('envroot', data)
self.assertIn('executable', data)
self.assertIn('release_full', data)
self.assertIn('release_version', data)
self.assertNotIn('errors', data)
self.assertNotIn("envroot", data)
self.assertIn("executable", data)
self.assertIn("release_full", data)
self.assertIn("release_version", data)
self.assertNotIn("errors", data)
# missing executable
with patch.dict(self.config.defaults, {'wutta.telemetry.default.collect.python.executable': '/bad/path'}):
with patch.dict(
self.config.defaults,
{"wutta.telemetry.default.collect.python.executable": "/bad/path"},
):
data = self.handler.collect_data_python(profile)
self.assertIsInstance(data, dict)
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`")
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`")
# unparsable executable output
with patch.object(mod, 'subprocess') as subprocess:
subprocess.check_output.return_value = 'bad output'.encode('utf_8')
with patch.object(mod, "subprocess") as subprocess:
subprocess.check_output.return_value = "bad output".encode("utf_8")
data = self.handler.collect_data_python(profile)
self.assertIsInstance(data, dict)
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",
])
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",
],
)
# typical / working (virtual environment) scenario
self.config.setdefault('wutta.telemetry.default.collect.python.envroot', '/srv/envs/poser')
self.config.setdefault(
"wutta.telemetry.default.collect.python.envroot", "/srv/envs/poser"
)
data = self.handler.collect_data_python(profile)
self.assertIsInstance(data, dict)
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`")
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`")
def test_normalize_errors(self):
data = {
'os': {
'timezone': 'America/Chicago',
'errors': [
"os": {
"timezone": "America/Chicago",
"errors": [
"Failed to read /etc/os-release",
],
},
'python': {
'executable': '/usr/bin/python3',
'errors': [
"python": {
"executable": "/usr/bin/python3",
"errors": [
"Failed to run `python --version`",
],
},
}
self.handler.normalize_errors(data)
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`",
])
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`",
],
)
def test_collect_all_data(self):
# typical / working scenario
data = self.handler.collect_all_data()
self.assertIsInstance(data, dict)
self.assertIn('os', data)
self.assertIn('python', data)
self.assertNotIn('errors', data)
self.assertIn("os", data)
self.assertIn("python", data)
self.assertNotIn("errors", data)
def test_submit_all_data(self):
profile = self.handler.get_profile('default')
profile.submit_url = '/testing'
profile = self.handler.get_profile("default")
profile.submit_url = "/testing"
with patch.object(mod, 'SimpleAPIClient') as SimpleAPIClient:
with patch.object(mod, "SimpleAPIClient") as SimpleAPIClient:
client = MagicMock()
SimpleAPIClient.return_value = client
# collecting all data
with patch.object(self.handler, 'collect_all_data') as collect_all_data:
with patch.object(self.handler, "collect_all_data") as collect_all_data:
collect_all_data.return_value = []
self.handler.submit_all_data(profile)
collect_all_data.assert_called_once_with(profile)
client.post.assert_called_once_with('/testing', data=[])
client.post.assert_called_once_with("/testing", data=[])
# use data from caller
client.post.reset_mock()
self.handler.submit_all_data(profile, data=['foo'])
client.post.assert_called_once_with('/testing', data=['foo'])
self.handler.submit_all_data(profile, data=["foo"])
client.post.assert_called_once_with("/testing", data=["foo"])
class TestTelemetryProfile(ConfigTestCase):
def make_profile(self, key='default'):
def make_profile(self, key="default"):
return mod.TelemetryProfile(self.config, key)
def test_section(self):
# default
profile = self.make_profile()
self.assertEqual(profile.section, 'wutta.telemetry')
self.assertEqual(profile.section, "wutta.telemetry")
# custom appname
with patch.object(self.config, 'appname', new='wuttatest'):
with patch.object(self.config, "appname", new="wuttatest"):
profile = self.make_profile()
self.assertEqual(profile.section, 'wuttatest.telemetry')
self.assertEqual(profile.section, "wuttatest.telemetry")
def test_load(self):
# defaults
profile = self.make_profile()
self.assertEqual(profile.collect_keys, ['os', 'python'])
self.assertEqual(profile.collect_keys, ["os", "python"])
self.assertIsNone(profile.submit_url)
# configured
self.config.setdefault('wutta.telemetry.default.collect.keys', 'os,network,python')
self.config.setdefault('wutta.telemetry.default.submit.url', '/nodes/telemetry')
self.config.setdefault(
"wutta.telemetry.default.collect.keys", "os,network,python"
)
self.config.setdefault("wutta.telemetry.default.submit.url", "/nodes/telemetry")
profile = self.make_profile()
self.assertEqual(profile.collect_keys, ['os', 'network', 'python'])
self.assertEqual(profile.submit_url, '/nodes/telemetry')
self.assertEqual(profile.collect_keys, ["os", "network", "python"])
self.assertEqual(profile.submit_url, "/nodes/telemetry")