blob: 57678e68d5fa28f1fce6d5508bd3af4835b68c2f [file] [log] [blame]
# Copyright 2024 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Unit tests for the sensor metadata validator"""
from pathlib import Path
import unittest
import tempfile
import yaml
from pw_sensor.validator import Validator
class ValidatorTest(unittest.TestCase):
"""Tests the Validator class."""
maxDiff = None
def test_missing_compatible(self) -> None:
"""Check that missing 'compatible' key throws exception"""
self._check_with_exception(
metadata={},
exception_string="ERROR: Malformed sensor metadata YAML:\n{}",
cause_substrings=["'compatible' is a required property"],
)
def test_invalid_compatible_type(self) -> None:
"""Check that incorrect type of 'compatible' throws exception"""
self._check_with_exception(
metadata={"compatible": {}},
exception_string=(
"ERROR: Malformed sensor metadata YAML:\ncompatible: {}"
),
cause_substrings=[
"'org' is a required property",
],
)
self._check_with_exception(
metadata={"compatible": []},
exception_string=(
"ERROR: Malformed sensor metadata YAML:\ncompatible: []"
),
cause_substrings=["[] is not of type 'object'"],
)
self._check_with_exception(
metadata={"compatible": 1},
exception_string=(
"ERROR: Malformed sensor metadata YAML:\ncompatible: 1"
),
cause_substrings=["1 is not of type 'object'"],
)
self._check_with_exception(
metadata={"compatible": ""},
exception_string=(
"ERROR: Malformed sensor metadata YAML:\ncompatible: ''"
),
cause_substrings=[" is not of type 'object'"],
)
def test_empty_dependency_list(self) -> None:
"""
Check that an empty or missing 'deps' resolves to one with an empty
'deps' list
"""
expected = {
"sensors": {
"google,foo": {
"compatible": {"org": "google", "part": "foo"},
"channels": {},
"attributes": {},
"triggers": {},
},
},
"channels": {},
"attributes": {},
"triggers": {},
}
metadata = {
"compatible": {"org": "google", "part": "foo"},
"deps": [],
}
result = Validator().validate(metadata=metadata)
self.assertEqual(result, expected)
metadata = {"compatible": {"org": "google", "part": "foo"}}
result = Validator().validate(metadata=metadata)
self.assertEqual(result, expected)
def test_invalid_dependency_file(self) -> None:
"""
Check that if an invalid dependency file is listed, we throw an error.
We know this will not be a valid file, because we have no files in the
include path so we have nowhere to look for the file.
"""
self._check_with_exception(
metadata={
"compatible": {"org": "google", "part": "foo"},
"deps": ["test.yaml"],
},
exception_string="Failed to find test.yaml using search paths:",
cause_substrings=[],
exception_type=FileNotFoundError,
)
def test_invalid_channel_name_raises_exception(self) -> None:
"""
Check that if given a channel name that's not defined, we raise an Error
"""
self._check_with_exception(
metadata={
"compatible": {"org": "google", "part": "foo"},
"channels": {"bar": {}},
},
exception_string="Failed to find a definition for 'bar', did"
" you forget a dependency?",
cause_substrings=[],
)
def test_channel_info_from_deps(self) -> None:
"""
End to end test resolving a dependency file and setting the right
default attribute values.
"""
with tempfile.NamedTemporaryFile(
mode="w", suffix=".yaml", encoding="utf-8", delete=False
) as dep:
dep_filename = Path(dep.name)
dep.write(
yaml.safe_dump(
{
"attributes": {
"sample_rate": {
"units": {"symbol": "Hz"},
},
},
"channels": {
"bar": {
"units": {"symbol": "sandwiches"},
},
"soap": {
"name": "The soap",
"description": (
"Measurement of how clean something is"
),
"units": {"symbol": "sqeaks"},
},
"laundry": {
"description": "Clean clothes count",
"units": {"symbol": "items"},
"sub-channels": {
"shirts": {
"description": "Clean shirt count",
},
"pants": {
"description": "Clean pants count",
},
},
},
},
"triggers": {
"data_ready": {
"description": "notify when new data is ready",
},
},
},
)
)
metadata = Validator(include_paths=[dep_filename.parent]).validate(
metadata={
"compatible": {"org": "google", "part": "foo"},
"deps": [dep_filename.name],
"attributes": {
"sample_rate": {},
},
"channels": {
"bar": {},
"soap": {
"name": "soap name override",
},
"laundry_shirts": {},
"laundry_pants": {},
"laundry": {
"indicies": [
{"name": "kids' laundry"},
{"name": "adults' laundry"},
]
},
},
"triggers": {
"data_ready": {},
},
},
)
expected_trigger_data_ready = {
"name": "data_ready",
"description": "notify when new data is ready",
}
expected_attribute_sample_rate = {
"name": "sample_rate",
"description": "",
"units": {"name": "Hz", "symbol": "Hz"},
}
expected_channel_bar = {
"name": "bar",
"description": "",
"units": {
"name": "sandwiches",
"symbol": "sandwiches",
},
}
expected_channel_soap = {
"name": "The soap",
"description": "Measurement of how clean something is",
"units": {
"name": "sqeaks",
"symbol": "sqeaks",
},
}
expected_channel_laundry_shirts = {
"name": "laundry_shirts",
"description": "Clean shirt count",
"units": {
"name": "items",
"symbol": "items",
},
}
expected_channel_laundry_pants = {
"name": "laundry_pants",
"description": "Clean pants count",
"units": {
"name": "items",
"symbol": "items",
},
}
expected_channel_laundry = {
"name": "laundry",
"description": "Clean clothes count",
"units": {
"name": "items",
"symbol": "items",
},
}
expected_sensor_channel_bar = {
"name": "bar",
"description": "",
"units": {
"name": "sandwiches",
"symbol": "sandwiches",
},
"indicies": [
{
"name": "bar",
"description": "",
},
],
}
expected_sensor_channel_soap = {
"name": "soap name override",
"description": "Measurement of how clean something is",
"units": {
"name": "sqeaks",
"symbol": "sqeaks",
},
"indicies": [
{
"name": "soap name override",
"description": "Measurement of how clean something is",
},
],
}
expected_sensor_channel_laundry_shirts = {
"name": "laundry_shirts",
"description": "Clean shirt count",
"units": {
"name": "items",
"symbol": "items",
},
"indicies": [
{
"name": "laundry_shirts",
"description": "Clean shirt count",
},
],
}
expected_sensor_channel_laundry_pants = {
"name": "laundry_pants",
"description": "Clean pants count",
"units": {
"name": "items",
"symbol": "items",
},
"indicies": [
{
"name": "laundry_pants",
"description": "Clean pants count",
},
],
}
expected_sensor_channel_laundry = {
"name": "laundry",
"description": "Clean clothes count",
"units": {
"name": "items",
"symbol": "items",
},
"indicies": [
{
"name": "kids' laundry",
"description": "Clean clothes count",
},
{
"name": "adults' laundry",
"description": "Clean clothes count",
},
],
}
self.assertEqual(
metadata,
{
"attributes": {"sample_rate": expected_attribute_sample_rate},
"channels": {
"bar": expected_channel_bar,
"soap": expected_channel_soap,
"laundry_shirts": expected_channel_laundry_shirts,
"laundry_pants": expected_channel_laundry_pants,
"laundry": expected_channel_laundry,
},
"triggers": {
"data_ready": expected_trigger_data_ready,
},
"sensors": {
"google,foo": {
"compatible": {
"org": "google",
"part": "foo",
},
"attributes": {
"sample_rate": expected_attribute_sample_rate,
},
"triggers": {
"data_ready": expected_trigger_data_ready,
},
"channels": {
"bar": expected_sensor_channel_bar,
"soap": expected_sensor_channel_soap,
"laundry_shirts": (
expected_sensor_channel_laundry_shirts
),
"laundry_pants": (
expected_sensor_channel_laundry_pants
),
"laundry": expected_sensor_channel_laundry,
},
},
},
},
)
def _check_with_exception(
self,
metadata: dict,
exception_string: str,
cause_substrings: list[str],
exception_type: type[BaseException] = RuntimeError,
) -> None:
with self.assertRaises(exception_type) as context:
Validator().validate(metadata=metadata)
self.assertEqual(str(context.exception).rstrip(), exception_string)
for cause_substring in cause_substrings:
self.assertTrue(
cause_substring in str(context.exception.__cause__),
f"Actual cause: {str(context.exception.__cause__)}",
)
if __name__ == "__main__":
unittest.main()