Eerste versie
This commit is contained in:
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
74
tests/readme.md
Normal file
74
tests/readme.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Tests with pytest
|
||||
|
||||
make sure pytest is installed `pip install -r requirements-dev.txt`
|
||||
run `pytest -s -o log_cli=true -o log_cli_level="DEBUG"` from the repository root
|
||||
|
||||
## test_mqtt_explorer.py:test_update_metrics
|
||||
|
||||
This test loads test mqtt data from a file and feeds it into mqtt_exporter and check if expected results are recorded in the prometheus client.
|
||||
|
||||
### directory structure
|
||||
Test data is loaded from following directory structure:
|
||||
|
||||
```
|
||||
./tests/
|
||||
./test_data/
|
||||
./test1/
|
||||
conf.yaml
|
||||
mqtt_msg.csv
|
||||
./test2/
|
||||
conf.yaml
|
||||
mqtt_msg.csv
|
||||
./test_xyz/
|
||||
conf.yaml
|
||||
mqtt_msg.csv
|
||||
./tmp_data
|
||||
[metric_test...##,txt]
|
||||
```
|
||||
|
||||
In `test-data` each subfolder (e.g. `test1`, `test_bla`) contains a separate set of test data. There is no naming convention for folders. The could be descriptive like `test_for_issue1234`. Avoid any special characters and white spaces.
|
||||
|
||||
Files:
|
||||
- `conf.yaml`: a config like a config for mqtt_exporter itself, but only the `metrics` part and a new optional attribute `timescale` is read from it.
|
||||
- `mqtt_msg.csv`: fake mqtt data, format description see below.
|
||||
|
||||
`tmp-data` contains prometheus scrape output from after each processed mqtt msg data. This folder will be cleaned before each test run.
|
||||
|
||||
### mqtt_msg.csv file format
|
||||
|
||||
`mqtt_msg.csv` is a CSV file with `;` as delimiter and `'` as quotation character.
|
||||
Following Column looks are expected:
|
||||
|
||||
```
|
||||
in_topic;in_payload;out_name;out_labels;out_value;delay;assert
|
||||
```
|
||||
- `in_topic`: topic as from mqtt server
|
||||
- `in_payload`: payload from mqtt server as string (will be converted byte array)
|
||||
- `out_name`: metric name without any suffix like `total`, `sum`, `bucket`, ...
|
||||
- `out_labels`: labels notes as a JSON string including the topic.
|
||||
- `out_value`: expected value for simple metrics like gauge it is a number. For other metrics is is a JSON string with expected values per suffix e.g. `{"_count": 10, "_sum": 85.55, "_bucket": 10}`
|
||||
- `delay`: seconds delay until the next mqtt msg is processed. The `timescale` config attribute speed up/slow down the delay. A time scale of 0 means no default, a timescale of 1 means realtime. Default timescale = 0
|
||||
- `assert`: `True/False`. Specify if the test should pass or not. In most cases this should be `True`
|
||||
|
||||
Metric type `Histogram` special handling here as it will log a `$(metric_name)_bucket` metric for each bucket with a reserved label `le` in the meaning of _less or equal_. Specify `le` for one bucket and set the expected count to the `bucket` attribute in the `out_value` JSON. See examples in `test1`.
|
||||
|
||||
For sample data see existing tests above.
|
||||
|
||||
### Gather test data from live environment
|
||||
|
||||
If logging level is set to `debug` the log will contain some lines that should be already correct formatted to be placed in a `mqtt_msq.csv`.
|
||||
|
||||
they look like this:
|
||||
```
|
||||
2021-08-08 22:24:36,996 DEBUG: TEST_DATA: fhem/Terrasse/TermPearl02/humidity; 21.0; fhem_humidity_percent; {"location": "paz", "topic": "fhem/paz/TermPearl01/humidity"}; 17.0; 0; True
|
||||
2021-08-08 22:24:30,601 DEBUG: TEST_DATA: fhem/Terrasse/TerrasseWeiss/humidity; 20.0; fhem_humidity_percent; {"location": "paz", "topic": "fhem/paz/TermPearl01/humidity"}; 17.0; 0; True
|
||||
2021-08-08 22:24:27,097 DEBUG: TEST_DATA: fhem/Garten/TermFetanten01/temperature; 16.7; fhem_temperature_celsius; {"location": "Terrasse", "topic": "fhem/Terrasse/TerrasseWeiss/temperature"}; 16.0; 0; True
|
||||
2021-08-08 22:23:58,831 DEBUG: TEST_DATA: fhem/paz/TermPearl01/humidity; 17.0; fhem_humidity_percent; {"location": "paz", "topic": "fhem/paz/TermPearl01/humidity"}; 17.0; 0; True
|
||||
```
|
||||
Tips:
|
||||
- remove `.* DEBUG: TEST_DATA: `.
|
||||
- Make sure the `mqtt_msg.csv` contains as first line the headers given above.
|
||||
- The captured data won't fit if the `payload/__value__` has been replaced by a label_config. Please set `in_payload` to the correct value manually. An example for this exception is the `- name: 'mqtt_broker_version'` metric from the example configurations.
|
||||
- put the data in a new subfolder in the `test_data` dir. Copy also the config file from the live environment to this folder (you should remove the `mqtt` part from it. Make also sure the recorded data don't contain sensitive data).
|
||||
- Create a PR and share the test data, as this will allow all developers to verify code changes.
|
||||
|
76
tests/test_data/test1/conf.yaml
Normal file
76
tests/test_data/test1/conf.yaml
Normal file
@@ -0,0 +1,76 @@
|
||||
|
||||
# Logging
|
||||
logging:
|
||||
# logfile: 'conf/mqttexperter.log' # Optional default '' (stdout)
|
||||
level: 'debug' # Optional default 'info'
|
||||
|
||||
timescale: 0
|
||||
|
||||
# Metric definitions
|
||||
metrics:
|
||||
- name: 'ftp_transferred_bytes'
|
||||
help: 'data transferred in bytes pe file'
|
||||
type: 'summary'
|
||||
topic: 'ftp/+/transferred'
|
||||
label_configs:
|
||||
- source_labels: ['__msg_topic__']
|
||||
target_label: '__topic__'
|
||||
- source_labels: ["__msg_topic__"]
|
||||
regex: "ftp/([^/]+).*"
|
||||
target_label: "file"
|
||||
replacement: '\1'
|
||||
action: "replace"
|
||||
- name: 'network_ping_ms'
|
||||
help: 'ping response in ms'
|
||||
type: 'histogram'
|
||||
topic: 'network/+/+/ping'
|
||||
buckets: '0.5,5,10'
|
||||
label_configs:
|
||||
- source_labels: ['__msg_topic__']
|
||||
target_label: '__topic__'
|
||||
- source_labels: ["__msg_topic__"]
|
||||
regex: "network/([^/]+).*"
|
||||
target_label: "network"
|
||||
replacement: '\1'
|
||||
action: "replace"
|
||||
- source_labels: ["__msg_topic__"]
|
||||
regex: "network/[^/]+/([^/]+).*"
|
||||
target_label: "server"
|
||||
replacement: '\1'
|
||||
action: "replace"
|
||||
- name: "fhem_temperature_celsius"
|
||||
help: "443 Mhz Sensors, Temperature in C"
|
||||
type: "gauge"
|
||||
topic: "fhem/+/+/temperature"
|
||||
label_configs:
|
||||
- source_labels: ['__msg_topic__']
|
||||
target_label: '__topic__'
|
||||
- source_labels: ["__msg_topic__"]
|
||||
regex: "fhem/([^/]+).*"
|
||||
target_label: "location"
|
||||
replacement: '\1'
|
||||
action: "replace"
|
||||
- name: "fhem_humidity_percent"
|
||||
help: "443 Mhz Sensors, Humidity in %"
|
||||
type: "gauge"
|
||||
topic: "fhem/+/+/humidity"
|
||||
label_configs:
|
||||
- source_labels: ['__msg_topic__']
|
||||
target_label: '__topic__'
|
||||
- source_labels: ["__msg_topic__"]
|
||||
regex: "fhem/([^/]+).*"
|
||||
target_label: "location"
|
||||
replacement: '\1'
|
||||
action: "replace"
|
||||
- name: "fhem_rain_mm"
|
||||
help: "443 Mhz Sensors, rain in mm/m2"
|
||||
type: "counter"
|
||||
topic: "fhem/+/+/rain_total"
|
||||
label_configs:
|
||||
- source_labels: ['__msg_topic__']
|
||||
target_label: '__topic__'
|
||||
- source_labels: ["__msg_topic__"]
|
||||
regex: "fhem/([^/]+).*"
|
||||
target_label: "location"
|
||||
replacement: '\1'
|
||||
action: "replace"
|
26
tests/test_data/test1/mqtt_msg.csv
Normal file
26
tests/test_data/test1/mqtt_msg.csv
Normal file
@@ -0,0 +1,26 @@
|
||||
in_topic;in_payload;out_name;out_labels;out_value;delay;assert
|
||||
fhem/Terrasse/TermPearl02/temperature;18;fhem_temperature_celsius;{"location": "Terrasse","topic": "fhem/Terrasse/TermPearl02/temperature"};18;1;True
|
||||
fhem/Terrasse/TermPearl02/humidity;21;fhem_humidity_percent;{"location": "Terrasse","topic": "fhem/Terrasse/TermPearl02/humidity"};21;2;True
|
||||
fhem/Garten/TermFetanten01/humidity;79;fhem_humidity_percent;{"location": "Garten","topic": "fhem/Garten/TermFetanten01/humidity"};79;2;True
|
||||
fhem/Garten/rainmeter01/rain_total;134.8;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};134.8;4;True
|
||||
fhem/Terrasse/TermPearl02/temperature;22;fhem_temperature_celsius;{"location": "Terrasse","topic": "fhem/Terrasse/TermPearl02/temperature"};22;5;True
|
||||
fhem/Terrasse/TermPearl02/humidity;24.3;fhem_humidity_percent;{"location": "Terrasse","topic": "fhem/Terrasse/TermPearl02/humidity"};24.3;1;True
|
||||
fhem/Garten/TermFetanten01/humidity;79;fhem_humidity_percent;{"location": "Garten","topic": "fhem/Garten/TermFetanten01/humidity"};79;2;True
|
||||
fhem/Garten/rainmeter01/rain_total;11.1;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};145.9;3;True
|
||||
fhem/Garten/rainmeter01/rain_total;10;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};155.9;5;True
|
||||
network/vlan11/srv01.local/ping;2;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "5.0"};{"_count": 1, "_sum": 2, "_bucket": 1};2;True
|
||||
network/vlan11/srv01.local/ping;4;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "5.0"};{"_count": 2, "_sum": 6, "_bucket": 2};6;True
|
||||
network/vlan11/srv01.local/ping;7;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "10.0"};{"_count": 3, "_sum": 13, "_bucket": 3};1;True
|
||||
network/vlan11/srv01.local/ping;0.4;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "0.5"};{"_count": 4, "_sum": 13.4, "_bucket": 1};4;True
|
||||
network/vlan11/srv01.local/ping;20;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "+Inf"};{"_count": 5, "_sum": 33.4, "_bucket": 5};5;True
|
||||
network/vlan11/srv01.local/ping;11.1;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "+Inf"};{"_count": 6, "_sum": 44.5, "_bucket": 6};2;True
|
||||
network/vlan11/srv01.local/ping;5;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "5.0"};{"_count": 7, "_sum": 49.5, "_bucket": 4};4;True
|
||||
network/vlan11/srv01.local/ping;6;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "10.0"};{"_count": 8, "_sum": 55.5, "_bucket": 6};1;True
|
||||
network/vlan11/srv01.local/ping;0.05;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "0.5"};{"_count": 9, "_sum": 55.55, "_bucket": 2};4;True
|
||||
network/vlan11/srv01.local/ping;30;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "+Inf"};{"_count": 10, "_sum": 85.55, "_bucket": 10};5;True
|
||||
ftp/update.bin/transferred;123;ftp_transferred_bytes;{"file": "update.bin","topic": "ftp/update.bin/transferred"};{"_count": 1, "_sum": 123};1;True
|
||||
ftp/update.bin/transferred;234;ftp_transferred_bytes;{"file": "update.bin","topic": "ftp/update.bin/transferred"};{"_count": 2, "_sum": 357};1;True
|
||||
ftp/update.bin/transferred;34;ftp_transferred_bytes;{"file": "update.bin","topic": "ftp/update.bin/transferred"};{"_count": 3, "_sum": 391};1;True
|
||||
ftp/update.bin/transferred;45;ftp_transferred_bytes;{"file": "update.bin","topic": "ftp/update.bin/transferred"};{"_count": 4, "_sum": 436};1;True
|
||||
ftp/update.bin/transferred;89;ftp_transferred_bytes;{"file": "update.bin","topic": "ftp/update.bin/transferred"};{"_count": 5, "_sum": 525};1;True
|
||||
ftp/update.bin/transferred;11111;ftp_transferred_bytes;{"file": "update.bin","topic": "ftp/update.bin/transferred"};{"_count": 6, "_sum": 11636};1;True
|
Can't render this file because it contains an unexpected character in line 2 and column 68.
|
99
tests/test_data/test2/conf.yaml
Normal file
99
tests/test_data/test2/conf.yaml
Normal file
@@ -0,0 +1,99 @@
|
||||
# Config file for MQTT prometheus exporter
|
||||
|
||||
|
||||
# Metric definitions
|
||||
metrics:
|
||||
# - name: 'mqtt_broker'
|
||||
# help: 'System events from broker'
|
||||
# type: 'gauge'
|
||||
# topic: '$SYS/broker/#'
|
||||
# label_configs:
|
||||
# - source_labels: ['__msg_topic__']
|
||||
# target_label: '__topic__'
|
||||
# - source_labels: ['__value__']
|
||||
# regex: '^(\d+([,.]\d*)?)$|^([,.]\d+)$'
|
||||
# action: 'keep'
|
||||
|
||||
- name: 'mqtt_broker_version'
|
||||
help: 'Mosquitto version (static)'
|
||||
type: 'gauge'
|
||||
topic: '$SYS/broker/version'
|
||||
label_configs:
|
||||
- source_labels: ['__msg_topic__']
|
||||
target_label: '__topic__'
|
||||
- source_labels: ['__value__']
|
||||
regex: '^\D+((?:\d+[\.]?)+)$'
|
||||
target_label: 'version'
|
||||
replacement: '\1'
|
||||
action: 'replace'
|
||||
- source_labels: ['__value__']
|
||||
replacement: '1'
|
||||
target_label: '__value__'
|
||||
action: 'replace'
|
||||
|
||||
- name: 'mqtt_broker_changeset'
|
||||
help: 'Mosquitto build changeset (static)'
|
||||
type: 'gauge'
|
||||
topic: '$SYS/broker/changeset'
|
||||
label_configs:
|
||||
- source_labels: ['__msg_topic__']
|
||||
target_label: '__topic__'
|
||||
- source_labels: ['__value__']
|
||||
target_label: 'changeset'
|
||||
action: 'replace'
|
||||
- source_labels: ['__value__']
|
||||
replacement: '1'
|
||||
target_label: '__value__'
|
||||
action: 'replace'
|
||||
|
||||
- name: 'mqtt_broker_timestamp'
|
||||
help: 'Mosquitto build timestamp (static)'
|
||||
type: 'gauge'
|
||||
topic: '$SYS/broker/timestamp'
|
||||
label_configs:
|
||||
- source_labels: ['__msg_topic__']
|
||||
target_label: '__topic__'
|
||||
- source_labels: ['__value__']
|
||||
target_label: 'timestamp'
|
||||
action: 'replace'
|
||||
- source_labels: ['__value__']
|
||||
replacement: '1'
|
||||
target_label: '__value__'
|
||||
action: 'replace'
|
||||
|
||||
- name: "fhem_temperature_celsius"
|
||||
help: "443 Mhz Sensors, Temperature in C"
|
||||
type: "gauge"
|
||||
topic: "fhem/+/+/temperature"
|
||||
label_configs:
|
||||
- source_labels: ['__msg_topic__']
|
||||
target_label: '__topic__'
|
||||
- source_labels: ["__msg_topic__"]
|
||||
regex: "fhem/([^/]+).*"
|
||||
target_label: "location"
|
||||
replacement: '\1'
|
||||
action: "replace"
|
||||
- name: "fhem_humidity_percent"
|
||||
help: "443 Mhz Sensors, Humidity in %"
|
||||
type: "gauge"
|
||||
topic: "fhem/+/+/humidity"
|
||||
label_configs:
|
||||
- source_labels: ['__msg_topic__']
|
||||
target_label: '__topic__'
|
||||
- source_labels: ["__msg_topic__"]
|
||||
regex: "fhem/([^/]+).*"
|
||||
target_label: "location"
|
||||
replacement: '\1'
|
||||
action: "replace"
|
||||
- name: "fhem_rain_mm"
|
||||
help: "443 Mhz Sensors, rain in mm/m2"
|
||||
type: "counter"
|
||||
topic: "fhem/+/+/rain_total"
|
||||
label_configs:
|
||||
- source_labels: ['__msg_topic__']
|
||||
target_label: '__topic__'
|
||||
- source_labels: ["__msg_topic__"]
|
||||
regex: "fhem/([^/]+).*"
|
||||
target_label: "location"
|
||||
replacement: '\1'
|
||||
action: "replace"
|
15
tests/test_data/test2/mqtt_msg.csv
Normal file
15
tests/test_data/test2/mqtt_msg.csv
Normal file
@@ -0,0 +1,15 @@
|
||||
in_topic;in_payload;out_name;out_labels;out_value;delay;assert
|
||||
$SYS/broker/version;' 2.0.11'; mqtt_broker_version; {"topic": "$SYS/broker/version", "version": "2.0.11"}; 1.0; 0; True
|
||||
fhem/Terrasse/TerrasseWeiss/humidity; 20.0; fhem_humidity_percent; {"location": "Terrasse", "topic": "fhem/Terrasse/TerrasseWeiss/humidity"}; 20.0; 0; True
|
||||
fhem/Terrasse/TermPearl02/temperature; 17.5; fhem_temperature_celsius; {"location": "Terrasse", "topic": "fhem/Terrasse/TermPearl02/temperature"}; 17.5; 0; True
|
||||
fhem/Terrasse/TermPearl02/humidity; 20.0; fhem_humidity_percent; {"location": "Terrasse", "topic": "fhem/Terrasse/TermPearl02/humidity"}; 20.0; 0; True
|
||||
fhem/Terrasse/TermPearl02/humidity; 21.0; fhem_humidity_percent; {"location": "Terrasse", "topic": "fhem/Terrasse/TermPearl02/humidity"}; 21.0; 0; True
|
||||
fhem/Terrasse/TermPearl02/temperature; 17.6; fhem_temperature_celsius; {"location": "Terrasse", "topic": "fhem/Terrasse/TermPearl02/temperature"}; 17.6; 0; True
|
||||
fhem/Garten/TermFetanten01/humidity; 66.0; fhem_humidity_percent; {"location": "Garten", "topic": "fhem/Garten/TermFetanten01/humidity"}; 66.0; 0; True
|
||||
fhem/Terrasse/TermPearl02/temperature; 17.5; fhem_temperature_celsius; {"location": "Terrasse", "topic": "fhem/Terrasse/TermPearl02/temperature"}; 17.5; 0; True
|
||||
$SYS/broker/version;' 2.0.11'; mqtt_broker_version; {"topic": "$SYS/broker/version", "version": "2.0.11"}; 1.0; 0; True
|
||||
fhem/Garten/rainmeter01/rain_total; 106.426; fhem_rain_mm; {"location": "Garten", "topic": "fhem/Garten/rainmeter01/rain_total"}; {"_total": 106.426, "_created": 1628459492.695393}; 0; True
|
||||
fhem/Garten/TermFetanten01/humidity; 65.0; fhem_humidity_percent; {"location": "Garten", "topic": "fhem/Garten/TermFetanten01/humidity"}; 65.0; 0; True
|
||||
fhem/paz/TermPearl01/temperature; 24.3; fhem_temperature_celsius; {"location": "paz", "topic": "fhem/paz/TermPearl01/temperature"}; 24.3; 0; True
|
||||
fhem/paz/TermPearl01/humidity; 17.0; fhem_humidity_percent; {"location": "paz", "topic": "fhem/paz/TermPearl01/humidity"}; 17.0; 0; True
|
||||
fhem/Terrasse/TerrasseWeiss/humidity; 20.0; fhem_humidity_percent; {"location": "paz", "topic": "fhem/paz/TermPearl01/humidity"}; 17.0; 0; True
|
Can't render this file because it contains an unexpected character in line 2 and column 54.
|
22
tests/test_data/test_counter_absolute/conf.yaml
Normal file
22
tests/test_data/test_counter_absolute/conf.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
# Logging
|
||||
logging:
|
||||
# logfile: 'conf/mqttexperter.log' # Optional default '' (stdout)
|
||||
level: 'debug' # Optional default 'info'
|
||||
|
||||
timescale: 0
|
||||
|
||||
# Metric definitions
|
||||
metrics:
|
||||
- name: "fhem_rain_mm"
|
||||
help: "443 Mhz Sensors, rain in mm/m2"
|
||||
type: "counter_absolute"
|
||||
topic: "fhem/+/+/rain_total"
|
||||
label_configs:
|
||||
- source_labels: ['__msg_topic__']
|
||||
target_label: '__topic__'
|
||||
- source_labels: ["__msg_topic__"]
|
||||
regex: "fhem/([^/]+).*"
|
||||
target_label: "location"
|
||||
replacement: '\1'
|
||||
action: "replace"
|
13
tests/test_data/test_counter_absolute/mqtt_msg.csv
Normal file
13
tests/test_data/test_counter_absolute/mqtt_msg.csv
Normal file
@@ -0,0 +1,13 @@
|
||||
in_topic;in_payload;out_name;out_labels;out_value;delay;assert
|
||||
fhem/Garten/rainmeter01/rain_total;4.8;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};4.8;4;True
|
||||
fhem/Garten/rainmeter01/rain_total;11.1;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};11.1;3;True
|
||||
fhem/Garten/rainmeter01/rain_total;110;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};110;5;True
|
||||
fhem/Garten/rainmeter01/rain_total;134.8;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};134.8;4;True
|
||||
fhem/Garten/rainmeter01/rain_total;211.1;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};211.1;3;True
|
||||
fhem/Garten/rainmeter01/rain_total;155.9;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};155.9;5;True
|
||||
fhem/Garten/rainmeter01/rain_total;2134.8;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};2134.8;4;True
|
||||
fhem/Garten/rainmeter01/rain_total;11.1;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};11.1;3;True
|
||||
fhem/Garten/rainmeter01/rain_total;155.9;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};155.9;5;True
|
||||
fhem/Garten/rainmeter01/rain_total;23134.8;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};23134.8;4;True
|
||||
fhem/Garten/rainmeter01/rain_total;1233123.123123123123;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};1233123.123123123123;3;True
|
||||
fhem/Garten/rainmeter01/rain_total;1233123.123123123124;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};1233123.123123123124;5;True
|
Can't render this file because it contains an unexpected character in line 2 and column 54.
|
58
tests/test_data/test_enum/conf.yaml
Normal file
58
tests/test_data/test_enum/conf.yaml
Normal file
@@ -0,0 +1,58 @@
|
||||
|
||||
# Logging
|
||||
logging:
|
||||
# logfile: 'conf/mqttexperter.log' # Optional default '' (stdout)
|
||||
level: 'debug' # Optional default 'info'
|
||||
|
||||
timescale: 0
|
||||
|
||||
# Metric definitions
|
||||
metrics:
|
||||
- name: "fhem_light_state"
|
||||
help: "Light state on/off"
|
||||
type: "enum"
|
||||
topic: "fhem/+/+/light"
|
||||
parameters:
|
||||
states:
|
||||
- 'on'
|
||||
- 'off'
|
||||
label_configs:
|
||||
- source_labels: ['__value__']
|
||||
regex: "(ON|0)"
|
||||
target_label: '__value__'
|
||||
replacement: 'on'
|
||||
action: "replace"
|
||||
- source_labels: ['__value__']
|
||||
regex: "(OFF|1)"
|
||||
target_label: '__value__'
|
||||
replacement: 'off'
|
||||
action: "replace"
|
||||
- source_labels: ['__msg_topic__']
|
||||
target_label: '__topic__'
|
||||
- source_labels: ["__msg_topic__"]
|
||||
regex: "fhem/([^/]+).*"
|
||||
target_label: "location"
|
||||
replacement: '\1'
|
||||
action: "replace"
|
||||
- name: 'network_ping_ms'
|
||||
help: 'ping response in ms'
|
||||
type: 'histogram'
|
||||
topic: 'network/+/+/ping'
|
||||
parameters:
|
||||
buckets:
|
||||
- 0.5
|
||||
- 5
|
||||
- 10
|
||||
label_configs:
|
||||
- source_labels: ['__msg_topic__']
|
||||
target_label: '__topic__'
|
||||
- source_labels: ["__msg_topic__"]
|
||||
regex: "network/([^/]+).*"
|
||||
target_label: "network"
|
||||
replacement: '\1'
|
||||
action: "replace"
|
||||
- source_labels: ["__msg_topic__"]
|
||||
regex: "network/[^/]+/([^/]+).*"
|
||||
target_label: "server"
|
||||
replacement: '\1'
|
||||
action: "replace"
|
23
tests/test_data/test_enum/mqtt_msg.csv
Normal file
23
tests/test_data/test_enum/mqtt_msg.csv
Normal file
@@ -0,0 +1,23 @@
|
||||
in_topic;in_payload;out_name;out_labels;out_value;delay;assert
|
||||
fhem/room01/desk/light01;on;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};0;4;True
|
||||
fhem/room01/desk/light01;on;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};0;3;True
|
||||
fhem/room01/desk/light01;off;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};1;5;True
|
||||
fhem/room01/desk/light01;on;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};0;4;True
|
||||
fhem/room01/desk/light01;off;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};1;3;True
|
||||
fhem/room01/desk/light01;ON;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};0;5;True
|
||||
fhem/room01/desk/light01;OFF;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};1;4;True
|
||||
fhem/room01/desk/light01;off;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};1;3;True
|
||||
fhem/room01/desk/light01;1;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};1;5;True
|
||||
fhem/room01/desk/light01;0;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};0;4;True
|
||||
fhem/room01/desk/light01;on;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};0;3;True
|
||||
fhem/room01/desk/light01;off;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};1;5;True
|
||||
network/vlan11/srv01.local/ping;2;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "5.0"};{"_count": 1, "_sum": 2, "_bucket": 1};2;True
|
||||
network/vlan11/srv01.local/ping;4;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "5.0"};{"_count": 2, "_sum": 6, "_bucket": 2};6;True
|
||||
network/vlan11/srv01.local/ping;7;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "10.0"};{"_count": 3, "_sum": 13, "_bucket": 3};1;True
|
||||
network/vlan11/srv01.local/ping;0.4;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "0.5"};{"_count": 4, "_sum": 13.4, "_bucket": 1};4;True
|
||||
network/vlan11/srv01.local/ping;20;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "+Inf"};{"_count": 5, "_sum": 33.4, "_bucket": 5};5;True
|
||||
network/vlan11/srv01.local/ping;11.1;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "+Inf"};{"_count": 6, "_sum": 44.5, "_bucket": 6};2;True
|
||||
network/vlan11/srv01.local/ping;5;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "5.0"};{"_count": 7, "_sum": 49.5, "_bucket": 4};4;True
|
||||
network/vlan11/srv01.local/ping;6;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "10.0"};{"_count": 8, "_sum": 55.5, "_bucket": 6};1;True
|
||||
network/vlan11/srv01.local/ping;0.05;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "0.5"};{"_count": 9, "_sum": 55.55, "_bucket": 2};4;True
|
||||
network/vlan11/srv01.local/ping;30;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "+Inf"};{"_count": 10, "_sum": 85.55, "_bucket": 10};5;True
|
Can't render this file because it contains an unexpected character in line 2 and column 47.
|
178
tests/test_mqtt_exporter.py
Normal file
178
tests/test_mqtt_exporter.py
Normal file
@@ -0,0 +1,178 @@
|
||||
"""
|
||||
py test testing for mqtt_exporter
|
||||
|
||||
"""
|
||||
|
||||
import csv
|
||||
import distutils.util
|
||||
import json
|
||||
from json.decoder import JSONDecodeError
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
|
||||
import prometheus_client as prometheus
|
||||
import prometheus_client.registry
|
||||
import mqtt_exporter
|
||||
import pytest
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
TMP_DIR=os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
'tmp_data'
|
||||
)
|
||||
DATA_DIR=os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
'test_data'
|
||||
)
|
||||
|
||||
def setup_module(module): #pylint: disable=unused-argument
|
||||
""" setup any state specific to the execution of the given module."""
|
||||
delete_temp_test_files()
|
||||
|
||||
|
||||
def delete_temp_test_files():
|
||||
# delete TEMP files
|
||||
for file in os.listdir(TMP_DIR):
|
||||
if file == '.gitkeep':
|
||||
continue
|
||||
os.remove(os.path.join(TMP_DIR, file))
|
||||
|
||||
|
||||
class MqttCVS:
|
||||
in_topic = "in_topic"
|
||||
in_payload = "in_payload"
|
||||
out_name = "out_name"
|
||||
out_labels = "out_labels"
|
||||
out_value = "out_value"
|
||||
delay = "delay"
|
||||
expected_assert = "assert"
|
||||
|
||||
def _get_mqtt_data(file_name):
|
||||
"""
|
||||
Reads mqtt fake data and expected results from file
|
||||
"""
|
||||
mqtt_data = []
|
||||
with open(file_name, newline='') as mqtt_data_csv:
|
||||
csv_reader = csv.DictReader(mqtt_data_csv, quotechar="'", delimiter=';')
|
||||
for row in csv_reader:
|
||||
row[MqttCVS.in_topic] = row[MqttCVS.in_topic].strip()
|
||||
row[MqttCVS.out_name] = row[MqttCVS.out_name].strip()
|
||||
# covert payloud to bytes, as in a MQTT Message
|
||||
row[MqttCVS.in_payload] = row[MqttCVS.in_payload].encode('UTF-8')
|
||||
# parse labels, to a python object.
|
||||
try:
|
||||
row[MqttCVS.out_labels] = json.loads(row.get(MqttCVS.out_labels, '{}'))
|
||||
except json.decoder.JSONDecodeError as jde:
|
||||
logging.error(f"json.decoder.JSONDecodeError while decoding {row.get(MqttCVS.out_labels, '{}')}")
|
||||
raise jde
|
||||
# Value could be a JSON, a float or anthing else.
|
||||
try:
|
||||
row[MqttCVS.out_value] = float(row.get(MqttCVS.out_value))
|
||||
except ValueError:
|
||||
try:
|
||||
row[MqttCVS.out_value] = json.loads(row.get(MqttCVS.out_value))
|
||||
except (JSONDecodeError, TypeError):
|
||||
pass # leave as it is
|
||||
# set delay to 0 if not a number
|
||||
try:
|
||||
row[MqttCVS.delay] = float(row.get(MqttCVS.delay, 0))
|
||||
except ValueError:
|
||||
row[MqttCVS.delay] = 0
|
||||
# convert string to bool for expected assertion.
|
||||
row[MqttCVS.expected_assert] = bool(
|
||||
distutils.util.strtobool(row.get(MqttCVS.expected_assert, "True").strip()))
|
||||
mqtt_data.append(row)
|
||||
return mqtt_data
|
||||
|
||||
|
||||
def _get_test_data():
|
||||
"""
|
||||
Reads test data from DATA_DIR sub directories.
|
||||
Each subdirectory is expected to contain a `conf.yaml` file with a metrics config (like in the config file)
|
||||
and a CSV file `mqtt_msg.csv` with fake mqtt data ";" delimited:
|
||||
`in_topic;in_payload;out_name;out_labels;out_value;delay;assert`
|
||||
where
|
||||
lables_out: json string with all expected lables
|
||||
delay: delay until the next line is processed
|
||||
assert: expected assert result, True if out_value matches prometheus metric
|
||||
"""
|
||||
test_data_sets = []
|
||||
test_data_dirs = [f.path for f in os.scandir(DATA_DIR) if f.is_dir()]
|
||||
test_names = [ os.path.basename(os.path.normpath(name)) for name in test_data_dirs]
|
||||
for test_data_dir in test_data_dirs:
|
||||
conf_file = os.path.join(test_data_dir, 'conf.yaml')
|
||||
mqtt_data_file = os.path.join(test_data_dir, 'mqtt_msg.csv')
|
||||
if not os.path.isfile(conf_file) or not os.path.isfile(mqtt_data_file):
|
||||
logging.error(f"Test data dir {test_data_dir} doesn't contain required files, skipping")
|
||||
continue
|
||||
config_yaml = mqtt_exporter._read_config(conf_file)
|
||||
config_yaml = mqtt_exporter._parse_config_and_add_defaults(config_yaml)
|
||||
test_data_sets.append((
|
||||
config_yaml['metrics'],
|
||||
_get_mqtt_data(mqtt_data_file),
|
||||
config_yaml.get('timescale', 0),
|
||||
))
|
||||
return test_names, test_data_sets
|
||||
|
||||
def _get_suffixes_by_metric_name(metrics, metric_name):
|
||||
metric_type = None
|
||||
for _, outer_metric in metrics.items():
|
||||
for metric in outer_metric:
|
||||
if metric['name'] == metric_name:
|
||||
metric_type = metric['type']
|
||||
break
|
||||
|
||||
for suffix in mqtt_exporter.SUFFIXES_PER_TYPE[metric_type]:
|
||||
if len(suffix) == 0:
|
||||
yield suffix
|
||||
else:
|
||||
yield f"_{suffix}"
|
||||
|
||||
|
||||
class FakeMSG():
|
||||
""""Simulate MQTT Msg"""
|
||||
def __init__(self, topic, payload) -> None:
|
||||
self.topic = topic
|
||||
self.payload = payload
|
||||
|
||||
|
||||
param_test_data_dirs, param_test_data_sets = _get_test_data()
|
||||
|
||||
@pytest.mark.parametrize("metrics,mqtt_data_set,timescale", param_test_data_sets, ids=param_test_data_dirs)
|
||||
def test_update_metrics(caplog, request, metrics, mqtt_data_set, timescale):
|
||||
"""
|
||||
reads a label_config and some mqtt data and asserts if they are in the metrics
|
||||
"""
|
||||
logging.info(f"Start test_update_metrics with ID {request.node.callspec.id}")
|
||||
|
||||
# reset prometheus registry between tests
|
||||
collectors = list(prometheus.REGISTRY._collector_to_names.keys())
|
||||
for collector in collectors:
|
||||
prometheus.REGISTRY.unregister(collector)
|
||||
|
||||
i = 1
|
||||
for mqtt_data in mqtt_data_set:
|
||||
msg = FakeMSG(mqtt_data[MqttCVS.in_topic], mqtt_data[MqttCVS.in_payload])
|
||||
mqtt_exporter._on_message(None, metrics, msg)
|
||||
prometheus.REGISTRY.collect()
|
||||
prometheus.write_to_textfile(os.path.join(TMP_DIR, f"metric_{request.node.callspec.id}_{i:02}.txt"), prometheus.REGISTRY)
|
||||
# depending on metric type one or more metrics with different suffixes are added.
|
||||
for suffix in _get_suffixes_by_metric_name(metrics, mqtt_data[MqttCVS.out_name]):
|
||||
# historgram with buckets need special handling, remove bucket labe label 'le'
|
||||
labels = mqtt_data[MqttCVS.out_labels].copy()
|
||||
if not suffix == "_bucket" and labels.get('le'):
|
||||
labels.pop('le')
|
||||
|
||||
expected_result = mqtt_data[MqttCVS.out_value]
|
||||
expected_result = expected_result if not isinstance(expected_result, dict) else expected_result[suffix]
|
||||
logging.info(f"Assert {mqtt_data[MqttCVS.out_name]}{suffix} from testdata record {i}")
|
||||
assert ( prometheus.REGISTRY.get_sample_value(
|
||||
f"{mqtt_data[MqttCVS.out_name]}{suffix}",
|
||||
labels
|
||||
) == expected_result ) == mqtt_data[MqttCVS.expected_assert]
|
||||
time.sleep(mqtt_data[MqttCVS.delay] * timescale)
|
||||
i += 1
|
||||
for record in caplog.records:
|
||||
assert record.levelno < logging.ERROR
|
72
tests/test_prometheus_additions.py
Normal file
72
tests/test_prometheus_additions.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
Pytest for prometheus client enhancements
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
import time
|
||||
import pytest
|
||||
from utils.prometheus_additions import CounterAbsolute
|
||||
import prometheus_client as prometheus
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
TMP_DIR=os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
'tmp_data'
|
||||
)
|
||||
DATA_DIR=os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
'test_data'
|
||||
)
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def get_registry():
|
||||
yield prometheus.REGISTRY
|
||||
# reset prometheus registry between tests
|
||||
collectors = list(prometheus.REGISTRY._collector_to_names.keys())
|
||||
for collector in collectors:
|
||||
prometheus.REGISTRY.unregister(collector)
|
||||
|
||||
old_creation_time = 0.0
|
||||
|
||||
class TestCounterWithReset:
|
||||
a_counter_absolute = CounterAbsolute('Absolute_Counter', 'Test metric' )
|
||||
old_creation_time = 0.0
|
||||
param_test_data_sets = [
|
||||
(10, False),
|
||||
(10, True),
|
||||
(11, True),
|
||||
(110, True),
|
||||
(110, True),
|
||||
(210, True),
|
||||
(310.7, True),
|
||||
(110, False),
|
||||
(210, True),
|
||||
(310.7, True),
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize("value, same_creation_time", param_test_data_sets)
|
||||
def test_counter_absolute(self, request, get_registry, value, same_creation_time):
|
||||
global old_creation_time
|
||||
self.a_counter_absolute.set(value)
|
||||
creation_time = self.a_counter_absolute._created
|
||||
logging.info(f"Creation time: {creation_time:e}")
|
||||
registry = get_registry
|
||||
registry.collect()
|
||||
prometheus.write_to_textfile(os.path.join(TMP_DIR, f"absolute_counter_{request.node.callspec.id}_{value:05}.txt"), prometheus.REGISTRY)
|
||||
|
||||
assert self.a_counter_absolute._value.get() == value
|
||||
assert (creation_time == old_creation_time ) == same_creation_time
|
||||
old_creation_time = creation_time
|
||||
time.sleep(0.005)
|
||||
|
||||
|
||||
class TestCounterRestForbidden:
|
||||
a_counter_absolute = CounterAbsolute('Strict_Absolute_Counter', "This Counters doesn't allow reset")
|
||||
|
||||
def test_counter_reset(self):
|
||||
val_first = 0.3324234
|
||||
val_second = 0.3324233
|
||||
self.a_counter_absolute.set(val_first, fail_on_decrease=True)
|
||||
with pytest.raises(ValueError, match=rf"Counter must increase {val_second} lower {val_first}"):
|
||||
self.a_counter_absolute.set(val_second, fail_on_decrease=True)
|
0
tests/tmp_data/.gitkeep
Normal file
0
tests/tmp_data/.gitkeep
Normal file
Reference in New Issue
Block a user