mirror of
https://github.com/jorijn/meshcore-stats.git
synced 2026-03-28 17:42:55 +01:00
* tests: cache integration/report fixtures to speed up tests * fix: speed up yearly aggregation and refresh timings report * chore: remove the report * fix: unrecognized named-value: 'runner'. Located at position 1 within expression: runner.temp * fix: ruff linting error * test: strengthen assertions and stabilize tests * test(integration): expand rendered chart metrics
254 lines
9.0 KiB
Python
254 lines
9.0 KiB
Python
"""Tests for meshcore.conf file parsing."""
|
|
|
|
import os
|
|
|
|
from meshmon.env import _parse_config_value
|
|
|
|
|
|
def _load_config_from_content(tmp_path, monkeypatch, content: str | None) -> None:
|
|
import meshmon.env as env
|
|
|
|
config_path = tmp_path / "meshcore.conf"
|
|
if content is not None:
|
|
config_path.write_text(content)
|
|
|
|
fake_env_path = tmp_path / "src" / "meshmon" / "env.py"
|
|
fake_env_path.parent.mkdir(parents=True, exist_ok=True)
|
|
fake_env_path.write_text("")
|
|
|
|
monkeypatch.setattr(env, "__file__", str(fake_env_path))
|
|
env._load_config_file()
|
|
|
|
class TestParseConfigValueDetailed:
|
|
"""Detailed tests for _parse_config_value."""
|
|
|
|
# ==========================================================================
|
|
# Empty/whitespace handling
|
|
# ==========================================================================
|
|
|
|
def test_empty_string(self):
|
|
assert _parse_config_value("") == ""
|
|
|
|
def test_only_spaces(self):
|
|
assert _parse_config_value(" ") == ""
|
|
|
|
def test_only_tabs(self):
|
|
assert _parse_config_value("\t\t") == ""
|
|
|
|
# ==========================================================================
|
|
# Unquoted values
|
|
# ==========================================================================
|
|
|
|
def test_simple_value(self):
|
|
assert _parse_config_value("hello") == "hello"
|
|
|
|
def test_value_with_leading_trailing_space(self):
|
|
assert _parse_config_value(" hello ") == "hello"
|
|
|
|
def test_value_with_internal_spaces(self):
|
|
assert _parse_config_value("hello world") == "hello world"
|
|
|
|
def test_numeric_value(self):
|
|
assert _parse_config_value("12345") == "12345"
|
|
|
|
def test_path_value(self):
|
|
assert _parse_config_value("/dev/ttyUSB0") == "/dev/ttyUSB0"
|
|
|
|
# ==========================================================================
|
|
# Double-quoted strings
|
|
# ==========================================================================
|
|
|
|
def test_double_quoted_simple(self):
|
|
assert _parse_config_value('"hello"') == "hello"
|
|
|
|
def test_double_quoted_with_spaces(self):
|
|
assert _parse_config_value('"hello world"') == "hello world"
|
|
|
|
def test_double_quoted_with_special_chars(self):
|
|
assert _parse_config_value('"hello #world"') == "hello #world"
|
|
|
|
def test_double_quoted_unclosed(self):
|
|
assert _parse_config_value('"hello') == "hello"
|
|
|
|
def test_double_quoted_empty(self):
|
|
assert _parse_config_value('""') == ""
|
|
|
|
def test_double_quoted_with_trailing_content(self):
|
|
# Only extracts content within first pair of quotes
|
|
assert _parse_config_value('"hello" # comment') == "hello"
|
|
|
|
# ==========================================================================
|
|
# Single-quoted strings
|
|
# ==========================================================================
|
|
|
|
def test_single_quoted_simple(self):
|
|
assert _parse_config_value("'hello'") == "hello"
|
|
|
|
def test_single_quoted_with_spaces(self):
|
|
assert _parse_config_value("'hello world'") == "hello world"
|
|
|
|
def test_single_quoted_unclosed(self):
|
|
assert _parse_config_value("'hello") == "hello"
|
|
|
|
def test_single_quoted_empty(self):
|
|
assert _parse_config_value("''") == ""
|
|
|
|
# ==========================================================================
|
|
# Inline comments
|
|
# ==========================================================================
|
|
|
|
def test_inline_comment_with_space(self):
|
|
assert _parse_config_value("hello # comment") == "hello"
|
|
|
|
def test_inline_comment_multiple_spaces(self):
|
|
assert _parse_config_value("hello # comment here") == "hello"
|
|
|
|
def test_hash_without_space_kept(self):
|
|
# Hash without preceding space is kept (not a comment)
|
|
assert _parse_config_value("color#ffffff") == "color#ffffff"
|
|
|
|
def test_hash_at_start_kept(self):
|
|
# Hash at start is kept (though unusual for a value)
|
|
assert _parse_config_value("#ffffff") == "#ffffff"
|
|
|
|
# ==========================================================================
|
|
# Mixed scenarios
|
|
# ==========================================================================
|
|
|
|
def test_quoted_preserves_hash_comment_style(self):
|
|
assert _parse_config_value('"test # not a comment"') == "test # not a comment"
|
|
|
|
def test_value_ending_with_hash(self):
|
|
# "test#" has no space before #, so kept
|
|
assert _parse_config_value("test#") == "test#"
|
|
|
|
|
|
class TestLoadConfigFileBehavior:
|
|
"""Tests for _load_config_file behavior."""
|
|
|
|
def test_nonexistent_file_no_error(self, tmp_path, monkeypatch, isolate_config_loading):
|
|
"""Missing config file doesn't raise error."""
|
|
_load_config_from_content(tmp_path, monkeypatch, content=None)
|
|
|
|
assert "MESH_TRANSPORT" not in os.environ
|
|
|
|
def test_skips_empty_lines(self, tmp_path, monkeypatch, isolate_config_loading):
|
|
"""Empty lines are skipped."""
|
|
config_content = """
|
|
MESH_TRANSPORT=tcp
|
|
|
|
MESH_DEBUG=1
|
|
|
|
"""
|
|
_load_config_from_content(tmp_path, monkeypatch, config_content)
|
|
|
|
assert os.environ["MESH_TRANSPORT"] == "tcp"
|
|
assert os.environ["MESH_DEBUG"] == "1"
|
|
|
|
def test_skips_comment_lines(self, tmp_path, monkeypatch, isolate_config_loading):
|
|
"""Lines starting with # are skipped."""
|
|
config_content = """# This is a comment
|
|
MESH_TRANSPORT=tcp
|
|
# Another comment
|
|
"""
|
|
_load_config_from_content(tmp_path, monkeypatch, config_content)
|
|
|
|
assert os.environ["MESH_TRANSPORT"] == "tcp"
|
|
|
|
def test_handles_export_prefix(self, tmp_path, monkeypatch, isolate_config_loading):
|
|
"""Lines with 'export ' prefix are handled."""
|
|
config_content = "export MESH_TRANSPORT=tcp\n"
|
|
_load_config_from_content(tmp_path, monkeypatch, config_content)
|
|
|
|
assert os.environ["MESH_TRANSPORT"] == "tcp"
|
|
|
|
def test_skips_lines_without_equals(self, tmp_path, monkeypatch, isolate_config_loading):
|
|
"""Lines without = are skipped."""
|
|
config_content = """MESH_TRANSPORT=tcp
|
|
this line has no equals
|
|
MESH_DEBUG=1
|
|
"""
|
|
_load_config_from_content(tmp_path, monkeypatch, config_content)
|
|
|
|
assert os.environ["MESH_TRANSPORT"] == "tcp"
|
|
assert os.environ["MESH_DEBUG"] == "1"
|
|
|
|
def test_env_vars_take_precedence(self, tmp_path, monkeypatch, isolate_config_loading):
|
|
"""Environment variables override config file values."""
|
|
# Set env var first
|
|
monkeypatch.setenv("MESH_TRANSPORT", "ble")
|
|
|
|
# Config file has different value
|
|
config_content = "MESH_TRANSPORT=serial\n"
|
|
_load_config_from_content(tmp_path, monkeypatch, config_content)
|
|
|
|
# After loading, env var should still be "ble"
|
|
assert os.environ.get("MESH_TRANSPORT") == "ble"
|
|
|
|
|
|
class TestConfigFileFormats:
|
|
"""Test various config file format scenarios."""
|
|
|
|
def test_standard_format(self):
|
|
"""Standard KEY=value format."""
|
|
assert _parse_config_value("value") == "value"
|
|
|
|
def test_spaces_around_equals(self):
|
|
"""Key = value with spaces (handled by partition)."""
|
|
# Note: _parse_config_value only handles the value part
|
|
# The key=value split happens in _load_config_file
|
|
assert _parse_config_value(" value ") == "value"
|
|
|
|
def test_quoted_path_with_spaces(self):
|
|
"""Path with spaces must be quoted."""
|
|
assert _parse_config_value('"/path/with spaces/file.txt"') == "/path/with spaces/file.txt"
|
|
|
|
def test_url_value(self):
|
|
"""URL values work correctly."""
|
|
assert _parse_config_value("https://example.com:8080/path") == "https://example.com:8080/path"
|
|
|
|
def test_email_value(self):
|
|
"""Email values work correctly."""
|
|
assert _parse_config_value("user@example.com") == "user@example.com"
|
|
|
|
def test_json_like_value(self):
|
|
"""JSON-like values need quoting if they have spaces."""
|
|
# Without spaces, works fine
|
|
assert _parse_config_value("{key:value}") == "{key:value}"
|
|
# With spaces, needs quotes
|
|
assert _parse_config_value('"{key: value}"') == "{key: value}"
|
|
|
|
|
|
class TestValidKeyPatterns:
|
|
"""Test key validation patterns."""
|
|
|
|
def test_valid_key_patterns(self):
|
|
"""Valid shell identifier patterns."""
|
|
# These would be tested in _load_config_file
|
|
# Valid: starts with letter or underscore, contains letters/numbers/underscores
|
|
valid_keys = [
|
|
"MESH_TRANSPORT",
|
|
"_PRIVATE",
|
|
"var123",
|
|
"MY_VAR_2",
|
|
]
|
|
# All should match: ^[A-Za-z_][A-Za-z0-9_]*$
|
|
import re
|
|
pattern = r"^[A-Za-z_][A-Za-z0-9_]*$"
|
|
for key in valid_keys:
|
|
assert re.match(pattern, key), f"{key} should be valid"
|
|
|
|
def test_invalid_key_patterns(self):
|
|
"""Invalid key patterns are rejected."""
|
|
invalid_keys = [
|
|
"123_starts_with_number",
|
|
"has-dash",
|
|
"has.dot",
|
|
"has space",
|
|
"",
|
|
]
|
|
import re
|
|
pattern = r"^[A-Za-z_][A-Za-z0-9_]*$"
|
|
for key in invalid_keys:
|
|
assert not re.match(pattern, key), f"{key} should be invalid"
|