From 7d256f56c5cfaaba43a36c4e69e2b2379f3558ec Mon Sep 17 00:00:00 2001 From: alim4r <7687869+alim4r@users.noreply.github.com> Date: Wed, 8 Dec 2021 20:18:21 +0100 Subject: [PATCH] Refactor Prometheus tests (#60451) * Removed prometheus from .coveragerc * Update prometheus tests with handler categories * Updated prometheus metrics to use the current registry - don't use the registry created on import (needed for tests) * Reset the prometheus CollectorRegistry before every test * Update prometheus metrics generation - Use latest registry when generating a response * Add default collectors when resetting the registry * Move entities to the specific prometheus test case * Refactor body generation for prometheus tests * Add test case for sensors without unit after rebase * Fix prometheus tests - Wait for events in prometheus tests - Add workaround for demo platform dependecy conversation (aiohttp frozen router) * Added prometheus tests for attribute metrics * Added prometheus tests for binary_sensor * Add prometheus test for input_boolean * Add prometheus test for lights * Add prometheus test for lock * Add prometheus test for sensor fahrenheit conversion * Fix prometheus test for input_number --- .coveragerc | 1 - .../components/prometheus/__init__.py | 9 +- tests/components/prometheus/test_init.py | 553 ++++++++++++++---- 3 files changed, 447 insertions(+), 116 deletions(-) diff --git a/.coveragerc b/.coveragerc index 2afc0300d8e..7ea4fa34a13 100644 --- a/.coveragerc +++ b/.coveragerc @@ -844,7 +844,6 @@ omit = homeassistant/components/progettihwsw/__init__.py homeassistant/components/progettihwsw/binary_sensor.py homeassistant/components/progettihwsw/switch.py - homeassistant/components/prometheus/* homeassistant/components/prowl/notify.py homeassistant/components/proxmoxve/* homeassistant/components/proxy/camera.py diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 2eb4193377d..5b72a109d44 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -210,7 +210,12 @@ class PrometheusMetrics: full_metric_name = self._sanitize_metric_name( f"{self.metrics_prefix}{metric}" ) - self._metrics[metric] = factory(full_metric_name, documentation, labels) + self._metrics[metric] = factory( + full_metric_name, + documentation, + labels, + registry=self.prometheus_cli.REGISTRY, + ) return self._metrics[metric] @staticmethod @@ -524,6 +529,6 @@ class PrometheusView(HomeAssistantView): _LOGGER.debug("Received Prometheus metrics request") return web.Response( - body=self.prometheus_cli.generate_latest(), + body=self.prometheus_cli.generate_latest(self.prometheus_cli.REGISTRY), content_type=CONTENT_TYPE_TEXT_PLAIN, ) diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index df8c07bfc73..b625642d12e 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -4,9 +4,12 @@ import datetime from http import HTTPStatus import unittest.mock as mock +import prometheus_client import pytest -from homeassistant.components import climate, humidifier, sensor +from homeassistant.components import climate, humidifier, lock, sensor +from homeassistant.components.demo.binary_sensor import DemoBinarySensor +from homeassistant.components.demo.light import DemoLight from homeassistant.components.demo.number import DemoNumber from homeassistant.components.demo.sensor import DemoSensor import homeassistant.components.prometheus as prometheus @@ -15,8 +18,10 @@ from homeassistant.const import ( CONTENT_TYPE_TEXT_PLAIN, DEGREE, DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, EVENT_STATE_CHANGED, + TEMP_FAHRENHEIT, ) from homeassistant.core import split_entity_id from homeassistant.setup import async_setup_component @@ -33,24 +38,109 @@ class FilterTest: should_pass: bool -async def prometheus_client(hass, hass_client, namespace): +async def setup_prometheus_client(hass, hass_client, namespace): """Initialize an hass_client with Prometheus component.""" + # Reset registry + prometheus_client.REGISTRY = prometheus_client.CollectorRegistry(auto_describe=True) + prometheus_client.ProcessCollector(registry=prometheus_client.REGISTRY) + prometheus_client.PlatformCollector(registry=prometheus_client.REGISTRY) + prometheus_client.GCCollector(registry=prometheus_client.REGISTRY) + config = {} if namespace is not None: config[prometheus.CONF_PROM_NAMESPACE] = namespace - await async_setup_component(hass, prometheus.DOMAIN, {prometheus.DOMAIN: config}) - - await async_setup_component(hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]}) - - await async_setup_component( - hass, climate.DOMAIN, {"climate": [{"platform": "demo"}]} - ) - - await async_setup_component( - hass, humidifier.DOMAIN, {"humidifier": [{"platform": "demo"}]} + assert await async_setup_component( + hass, prometheus.DOMAIN, {prometheus.DOMAIN: config} ) await hass.async_block_till_done() + return await hass_client() + + +async def generate_latest_metrics(client): + """Generate the latest metrics and transform the body.""" + resp = await client.get(prometheus.API_ENDPOINT) + assert resp.status == HTTPStatus.OK + assert resp.headers["content-type"] == CONTENT_TYPE_TEXT_PLAIN + body = await resp.text() + body = body.split("\n") + + assert len(body) > 3 + + return body + + +async def test_view_empty_namespace(hass, hass_client): + """Test prometheus metrics view.""" + client = await setup_prometheus_client(hass, hass_client, "") + + sensor2 = DemoSensor( + None, "Radio Energy", 14, DEVICE_CLASS_POWER, None, ENERGY_KILO_WATT_HOUR, None + ) + sensor2.hass = hass + sensor2.entity_id = "sensor.radio_energy" + with mock.patch( + "homeassistant.util.dt.utcnow", + return_value=datetime.datetime(1970, 1, 2, tzinfo=dt_util.UTC), + ): + await sensor2.async_update_ha_state() + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + + assert "# HELP python_info Python platform information" in body + assert ( + "# HELP python_gc_objects_collected_total " + "Objects collected during gc" in body + ) + + assert ( + 'entity_available{domain="sensor",' + 'entity="sensor.radio_energy",' + 'friendly_name="Radio Energy"} 1.0' in body + ) + + assert ( + 'last_updated_time_seconds{domain="sensor",' + 'entity="sensor.radio_energy",' + 'friendly_name="Radio Energy"} 86400.0' in body + ) + + +async def test_view_default_namespace(hass, hass_client): + """Test prometheus metrics view.""" + assert await async_setup_component( + hass, + "conversation", + {}, + ) + + client = await setup_prometheus_client(hass, hass_client, None) + + assert await async_setup_component( + hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]} + ) + await hass.async_block_till_done() + + body = await generate_latest_metrics(client) + + assert "# HELP python_info Python platform information" in body + assert ( + "# HELP python_gc_objects_collected_total " + "Objects collected during gc" in body + ) + + assert ( + 'homeassistant_sensor_temperature_celsius{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 15.6' in body + ) + + +async def test_sensor_unit(hass, hass_client): + """Test prometheus metrics for sensors with a unit.""" + client = await setup_prometheus_client(hass, hass_client, "") + sensor1 = DemoSensor( None, "Television Energy", 74, None, None, ENERGY_KILO_WATT_HOUR, None ) @@ -100,6 +190,38 @@ async def prometheus_client(hass, hass_client, namespace): sensor5.entity_id = "sensor.sps30_pm_1um_weight_concentration" await sensor5.async_update_ha_state() + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + + assert ( + 'sensor_unit_kwh{domain="sensor",' + 'entity="sensor.television_energy",' + 'friendly_name="Television Energy"} 74.0' in body + ) + + assert ( + 'sensor_unit_sek_per_kwh{domain="sensor",' + 'entity="sensor.electricity_price",' + 'friendly_name="Electricity price"} 0.123' in body + ) + + assert ( + 'sensor_unit_u0xb0{domain="sensor",' + 'entity="sensor.wind_direction",' + 'friendly_name="Wind Direction"} 25.0' in body + ) + + assert ( + 'sensor_unit_u0xb5g_per_mu0xb3{domain="sensor",' + 'entity="sensor.sps30_pm_1um_weight_concentration",' + 'friendly_name="SPS30 PM <1µm Weight concentration"} 3.7069' in body + ) + + +async def test_sensor_without_unit(hass, hass_client): + """Test prometheus metrics for sensors without a unit.""" + client = await setup_prometheus_client(hass, hass_client, "") + sensor6 = DemoSensor(None, "Trend Gradient", 0.002, None, None, None, None) sensor6.hass = hass sensor6.entity_id = "sensor.trend_gradient" @@ -115,6 +237,90 @@ async def prometheus_client(hass, hass_client, namespace): sensor8.entity_id = "sensor.text_unit" await sensor8.async_update_ha_state() + body = await generate_latest_metrics(client) + + assert ( + 'sensor_state{domain="sensor",' + 'entity="sensor.trend_gradient",' + 'friendly_name="Trend Gradient"} 0.002' in body + ) + + assert ( + 'sensor_state{domain="sensor",' + 'entity="sensor.text",' + 'friendly_name="Text"} 0' not in body + ) + + assert ( + 'sensor_unit_text{domain="sensor",' + 'entity="sensor.text_unit",' + 'friendly_name="Text Unit"} 0' not in body + ) + + +async def test_sensor_device_class(hass, hass_client): + """Test prometheus metrics for sensor with a device_class.""" + assert await async_setup_component( + hass, + "conversation", + {}, + ) + + client = await setup_prometheus_client(hass, hass_client, "") + + await async_setup_component(hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]}) + await hass.async_block_till_done() + + sensor1 = DemoSensor( + None, "Fahrenheit", 50, DEVICE_CLASS_TEMPERATURE, None, TEMP_FAHRENHEIT, None + ) + sensor1.hass = hass + sensor1.entity_id = "sensor.fahrenheit" + await sensor1.async_update_ha_state() + + sensor2 = DemoSensor( + None, "Radio Energy", 14, DEVICE_CLASS_POWER, None, ENERGY_KILO_WATT_HOUR, None + ) + sensor2.hass = hass + sensor2.entity_id = "sensor.radio_energy" + with mock.patch( + "homeassistant.util.dt.utcnow", + return_value=datetime.datetime(1970, 1, 2, tzinfo=dt_util.UTC), + ): + await sensor2.async_update_ha_state() + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + + assert ( + 'sensor_temperature_celsius{domain="sensor",' + 'entity="sensor.fahrenheit",' + 'friendly_name="Fahrenheit"} 10.0' in body + ) + + assert ( + 'sensor_temperature_celsius{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 15.6' in body + ) + + assert ( + 'sensor_humidity_percent{domain="sensor",' + 'entity="sensor.outside_humidity",' + 'friendly_name="Outside Humidity"} 54.0' in body + ) + + assert ( + 'sensor_power_kwh{domain="sensor",' + 'entity="sensor.radio_energy",' + 'friendly_name="Radio Energy"} 14.0' in body + ) + + +async def test_input_number(hass, hass_client): + """Test prometheus metrics for input_number.""" + client = await setup_prometheus_client(hass, hass_client, "") + number1 = DemoNumber(None, "Threshold", 5.2, None, False, 0, 10, 0.1) number1.hass = hass number1.entity_id = "input_number.threshold" @@ -126,39 +332,61 @@ async def prometheus_client(hass, hass_client, namespace): number2._attr_name = None await number2.async_update_ha_state() - return await hass_client() + await hass.async_block_till_done() + body = await generate_latest_metrics(client) - -async def test_view_empty_namespace(hass, hass_client): - """Test prometheus metrics view.""" - client = await prometheus_client(hass, hass_client, "") - resp = await client.get(prometheus.API_ENDPOINT) - - assert resp.status == HTTPStatus.OK - assert resp.headers["content-type"] == CONTENT_TYPE_TEXT_PLAIN - body = await resp.text() - body = body.split("\n") - - assert len(body) > 3 - - assert "# HELP python_info Python platform information" in body assert ( - "# HELP python_gc_objects_collected_total " - "Objects collected during gc" in body + 'input_number_state{domain="input_number",' + 'entity="input_number.threshold",' + 'friendly_name="Threshold"} 5.2' in body ) assert ( - 'sensor_temperature_celsius{domain="sensor",' - 'entity="sensor.outside_temperature",' - 'friendly_name="Outside Temperature"} 15.6' in body + 'input_number_state{domain="input_number",' + 'entity="input_number.brightness",' + 'friendly_name="None"} 60.0' in body ) + +async def test_battery(hass, hass_client): + """Test prometheus metrics for battery.""" + assert await async_setup_component( + hass, + "conversation", + {}, + ) + + client = await setup_prometheus_client(hass, hass_client, "") + + await async_setup_component(hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]}) + await hass.async_block_till_done() + + body = await generate_latest_metrics(client) + assert ( 'battery_level_percent{domain="sensor",' 'entity="sensor.outside_temperature",' 'friendly_name="Outside Temperature"} 12.0' in body ) + +async def test_climate(hass, hass_client): + """Test prometheus metrics for battery.""" + assert await async_setup_component( + hass, + "conversation", + {}, + ) + + client = await setup_prometheus_client(hass, hass_client, "") + + await async_setup_component( + hass, climate.DOMAIN, {"climate": [{"platform": "demo"}]} + ) + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + assert ( 'climate_current_temperature_celsius{domain="climate",' 'entity="climate.heatpump",' @@ -183,6 +411,24 @@ async def test_view_empty_namespace(hass, hass_client): 'friendly_name="Ecobee"} 24.0' in body ) + +async def test_humidifier(hass, hass_client): + """Test prometheus metrics for battery.""" + assert await async_setup_component( + hass, + "conversation", + {}, + ) + + client = await setup_prometheus_client(hass, hass_client, "") + + await async_setup_component( + hass, humidifier.DOMAIN, {"humidifier": [{"platform": "demo"}]} + ) + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + assert ( 'humidifier_target_humidity_percent{domain="humidifier",' 'entity="humidifier.humidifier",' @@ -208,107 +454,188 @@ async def test_view_empty_namespace(hass, hass_client): 'mode="eco"} 0.0' in body ) + +async def test_attributes(hass, hass_client): + """Test prometheus metrics for entity attributes.""" + client = await setup_prometheus_client(hass, hass_client, "") + + switch1 = DemoSensor(None, "Boolean", 74, None, None, None, None) + switch1.hass = hass + switch1.entity_id = "switch.boolean" + switch1._attr_extra_state_attributes = {"boolean": True} + await switch1.async_update_ha_state() + + switch2 = DemoSensor(None, "Number", 42, None, None, None, None) + switch2.hass = hass + switch2.entity_id = "switch.number" + switch2._attr_extra_state_attributes = {"Number": 10.2} + await switch2.async_update_ha_state() + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + assert ( - 'sensor_humidity_percent{domain="sensor",' - 'entity="sensor.outside_humidity",' - 'friendly_name="Outside Humidity"} 54.0' in body + 'switch_state{domain="switch",' + 'entity="switch.boolean",' + 'friendly_name="Boolean"} 74.0' in body ) assert ( - 'sensor_unit_kwh{domain="sensor",' - 'entity="sensor.television_energy",' - 'friendly_name="Television Energy"} 74.0' in body + 'switch_attr_boolean{domain="switch",' + 'entity="switch.boolean",' + 'friendly_name="Boolean"} 1.0' in body ) assert ( - 'sensor_power_kwh{domain="sensor",' - 'entity="sensor.radio_energy",' - 'friendly_name="Radio Energy"} 14.0' in body + 'switch_state{domain="switch",' + 'entity="switch.number",' + 'friendly_name="Number"} 42.0' in body ) assert ( - 'entity_available{domain="sensor",' - 'entity="sensor.radio_energy",' - 'friendly_name="Radio Energy"} 1.0' in body - ) - - assert ( - 'last_updated_time_seconds{domain="sensor",' - 'entity="sensor.radio_energy",' - 'friendly_name="Radio Energy"} 86400.0' in body - ) - - assert ( - 'sensor_unit_sek_per_kwh{domain="sensor",' - 'entity="sensor.electricity_price",' - 'friendly_name="Electricity price"} 0.123' in body - ) - - assert ( - 'sensor_unit_u0xb0{domain="sensor",' - 'entity="sensor.wind_direction",' - 'friendly_name="Wind Direction"} 25.0' in body - ) - - assert ( - 'sensor_unit_u0xb5g_per_mu0xb3{domain="sensor",' - 'entity="sensor.sps30_pm_1um_weight_concentration",' - 'friendly_name="SPS30 PM <1µm Weight concentration"} 3.7069' in body - ) - - assert ( - 'sensor_state{domain="sensor",' - 'entity="sensor.trend_gradient",' - 'friendly_name="Trend Gradient"} 0.002' in body - ) - - assert ( - 'sensor_state{domain="sensor",' - 'entity="sensor.text",' - 'friendly_name="Text"} 0' not in body - ) - - assert ( - 'sensor_unit_text{domain="sensor",' - 'entity="sensor.text_unit",' - 'friendly_name="Text Unit"} 0' not in body - ) - - assert ( - 'input_number_state{domain="input_number",' - 'entity="input_number.threshold",' - 'friendly_name="Threshold"} 5.2' in body - ) - - assert ( - 'input_number_state{domain="input_number",' - 'entity="input_number.brightness",' - 'friendly_name="None"} 60.0' in body + 'switch_attr_number{domain="switch",' + 'entity="switch.number",' + 'friendly_name="Number"} 10.2' in body ) -async def test_view_default_namespace(hass, hass_client): - """Test prometheus metrics view.""" - client = await prometheus_client(hass, hass_client, None) - resp = await client.get(prometheus.API_ENDPOINT) +async def test_binary_sensor(hass, hass_client): + """Test prometheus metrics for binary_sensor.""" + client = await setup_prometheus_client(hass, hass_client, "") - assert resp.status == HTTPStatus.OK - assert resp.headers["content-type"] == CONTENT_TYPE_TEXT_PLAIN - body = await resp.text() - body = body.split("\n") + binary_sensor1 = DemoBinarySensor(None, "Door", True, None) + binary_sensor1.hass = hass + binary_sensor1.entity_id = "binary_sensor.door" + await binary_sensor1.async_update_ha_state() - assert len(body) > 3 + binary_sensor1 = DemoBinarySensor(None, "Window", False, None) + binary_sensor1.hass = hass + binary_sensor1.entity_id = "binary_sensor.window" + await binary_sensor1.async_update_ha_state() + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) - assert "# HELP python_info Python platform information" in body assert ( - "# HELP python_gc_objects_collected_total " - "Objects collected during gc" in body + 'binary_sensor_state{domain="binary_sensor",' + 'entity="binary_sensor.door",' + 'friendly_name="Door"} 1.0' in body ) assert ( - 'homeassistant_sensor_temperature_celsius{domain="sensor",' - 'entity="sensor.outside_temperature",' - 'friendly_name="Outside Temperature"} 15.6' in body + 'binary_sensor_state{domain="binary_sensor",' + 'entity="binary_sensor.window",' + 'friendly_name="Window"} 0.0' in body + ) + + +async def test_input_boolean(hass, hass_client): + """Test prometheus metrics for input_boolean.""" + client = await setup_prometheus_client(hass, hass_client, "") + + input_boolean1 = DemoSensor(None, "Test", 1, None, None, None, None) + input_boolean1.hass = hass + input_boolean1.entity_id = "input_boolean.test" + await input_boolean1.async_update_ha_state() + + input_boolean2 = DemoSensor(None, "Helper", 0, None, None, None, None) + input_boolean2.hass = hass + input_boolean2.entity_id = "input_boolean.helper" + await input_boolean2.async_update_ha_state() + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + + assert ( + 'input_boolean_state{domain="input_boolean",' + 'entity="input_boolean.test",' + 'friendly_name="Test"} 1.0' in body + ) + + assert ( + 'input_boolean_state{domain="input_boolean",' + 'entity="input_boolean.helper",' + 'friendly_name="Helper"} 0.0' in body + ) + + +async def test_light(hass, hass_client): + """Test prometheus metrics for lights.""" + client = await setup_prometheus_client(hass, hass_client, "") + + light1 = DemoSensor(None, "Desk", 1, None, None, None, None) + light1.hass = hass + light1.entity_id = "light.desk" + await light1.async_update_ha_state() + + light2 = DemoSensor(None, "Wall", 0, None, None, None, None) + light2.hass = hass + light2.entity_id = "light.wall" + await light2.async_update_ha_state() + + light3 = DemoLight(None, "TV", True, True, 255, None, None) + light3.hass = hass + light3.entity_id = "light.tv" + await light3.async_update_ha_state() + + light4 = DemoLight(None, "PC", True, True, 180, None, None) + light4.hass = hass + light4.entity_id = "light.pc" + await light4.async_update_ha_state() + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + + assert ( + 'light_brightness_percent{domain="light",' + 'entity="light.desk",' + 'friendly_name="Desk"} 100.0' in body + ) + + assert ( + 'light_brightness_percent{domain="light",' + 'entity="light.wall",' + 'friendly_name="Wall"} 0.0' in body + ) + + assert ( + 'light_brightness_percent{domain="light",' + 'entity="light.tv",' + 'friendly_name="TV"} 100.0' in body + ) + + assert ( + 'light_brightness_percent{domain="light",' + 'entity="light.pc",' + 'friendly_name="PC"} 70.58823529411765' in body + ) + + +async def test_lock(hass, hass_client): + """Test prometheus metrics for lock.""" + assert await async_setup_component( + hass, + "conversation", + {}, + ) + + client = await setup_prometheus_client(hass, hass_client, "") + + await async_setup_component(hass, lock.DOMAIN, {"lock": [{"platform": "demo"}]}) + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + + assert ( + 'lock_state{domain="lock",' + 'entity="lock.front_door",' + 'friendly_name="Front Door"} 1.0' in body + ) + + assert ( + 'lock_state{domain="lock",' + 'entity="lock.kitchen_door",' + 'friendly_name="Kitchen Door"} 0.0' in body )