Add project infrastructure and core application files
Add .gitignore, GitLab CI pipeline with test/lint/build/release stages, project documentation (README, LICENSE, CHANGELOG, CONTRIBUTING, SECURITY), PyInstaller packaging configuration with icon and version info, Python project configuration with dependencies, and complete application source including battery provider, device discovery, settings management, UI components, startup control, and comprehensive test suite
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
from contextlib import nullcontext
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from logitech_battery_widget.windows import autostart
|
||||
|
||||
|
||||
def registry_mock():
|
||||
registry = MagicMock()
|
||||
registry.HKEY_CURRENT_USER = object()
|
||||
registry.KEY_READ = 1
|
||||
registry.KEY_SET_VALUE = 2
|
||||
registry.REG_SZ = 1
|
||||
key = MagicMock()
|
||||
registry.OpenKey.return_value = nullcontext(key)
|
||||
registry.CreateKeyEx.return_value = nullcontext(key)
|
||||
return registry, key
|
||||
|
||||
|
||||
def test_enable_writes_current_user_run_value():
|
||||
registry, key = registry_mock()
|
||||
|
||||
autostart.set_enabled(True, registry)
|
||||
|
||||
registry.SetValueEx.assert_called_once_with(
|
||||
key, autostart.VALUE_NAME, 0, registry.REG_SZ, autostart.launch_command()
|
||||
)
|
||||
|
||||
|
||||
def test_disable_removes_value_without_real_registry():
|
||||
registry, key = registry_mock()
|
||||
|
||||
autostart.set_enabled(False, registry)
|
||||
|
||||
registry.DeleteValue.assert_called_once_with(key, autostart.VALUE_NAME)
|
||||
|
||||
|
||||
def test_is_enabled_compares_exact_command():
|
||||
registry, _ = registry_mock()
|
||||
registry.QueryValueEx.return_value = (autostart.launch_command(), registry.REG_SZ)
|
||||
assert autostart.is_enabled(registry)
|
||||
|
||||
|
||||
def test_missing_value_is_disabled():
|
||||
registry, _ = registry_mock()
|
||||
registry.QueryValueEx.side_effect = FileNotFoundError
|
||||
assert not autostart.is_enabled(registry)
|
||||
@@ -0,0 +1,30 @@
|
||||
import json
|
||||
|
||||
from logitech_battery_widget.config import AppConfig, ConfigStore
|
||||
|
||||
|
||||
def test_config_round_trip(tmp_path):
|
||||
path = tmp_path / "config.json"
|
||||
store = ConfigStore(path)
|
||||
expected = AppConfig(theme="dark", always_on_top=True, window_x=12, window_y=34)
|
||||
|
||||
store.save(expected)
|
||||
|
||||
assert store.load() == expected
|
||||
assert json.loads(path.read_text(encoding="utf-8"))["theme"] == "dark"
|
||||
|
||||
|
||||
def test_invalid_config_uses_safe_values(tmp_path):
|
||||
path = tmp_path / "config.json"
|
||||
path.write_text('{"theme": "purple", "refresh_interval_seconds": 1}', encoding="utf-8")
|
||||
|
||||
loaded = ConfigStore(path).load()
|
||||
|
||||
assert loaded.theme == "system"
|
||||
assert loaded.refresh_interval_seconds == 15
|
||||
|
||||
|
||||
def test_broken_json_returns_defaults(tmp_path):
|
||||
path = tmp_path / "config.json"
|
||||
path.write_text("not json", encoding="utf-8")
|
||||
assert ConfigStore(path).load() == AppConfig()
|
||||
@@ -0,0 +1,25 @@
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from logitech_battery_widget.battery.mock_provider import MockBatteryProvider
|
||||
from logitech_battery_widget.battery.provider import BatteryDevice, ChargeStatus
|
||||
|
||||
|
||||
def test_mock_provider_returns_multiple_independent_devices():
|
||||
first = MockBatteryProvider().get_devices()
|
||||
second = MockBatteryProvider().get_devices()
|
||||
|
||||
assert len(first) == 2
|
||||
assert all(device.updated_at is not None for device in first)
|
||||
assert first is not second
|
||||
|
||||
|
||||
def test_custom_mock_devices_are_preserved():
|
||||
device = BatteryDevice("id", "Test", 42, ChargeStatus.CHARGING, datetime.now())
|
||||
assert MockBatteryProvider([device]).get_devices() == [device]
|
||||
|
||||
|
||||
def test_invalid_percentage_is_rejected():
|
||||
with pytest.raises(ValueError):
|
||||
BatteryDevice("id", "Test", 101)
|
||||
@@ -0,0 +1,19 @@
|
||||
from logitech_battery_widget.battery.provider import ChargeStatus
|
||||
from logitech_battery_widget.battery.windows_hid import WindowsHidBatteryProvider
|
||||
|
||||
|
||||
def test_hidpp_charge_status_mapping():
|
||||
provider = WindowsHidBatteryProvider()
|
||||
|
||||
assert provider._status(0) == ChargeStatus.DISCHARGING
|
||||
assert provider._status(1) == ChargeStatus.CHARGING
|
||||
assert provider._status(4) == ChargeStatus.CHARGING
|
||||
assert provider._status(99) == ChargeStatus.UNKNOWN
|
||||
|
||||
|
||||
def test_device_name_is_clear_and_vendor_prefixed():
|
||||
provider = WindowsHidBatteryProvider()
|
||||
|
||||
assert provider._name({"product_string": "MX Keys"}) == "Logitech MX Keys"
|
||||
assert provider._name({"product_string": "Logitech G Pro"}) == "Logitech G Pro"
|
||||
assert provider._name({}) == "Logitech HID Device"
|
||||
Reference in New Issue
Block a user