Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 46 additions & 5 deletions aw_server/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import os

from aw_core.config import load_config_toml

from .profile import DEFAULT_PROFILE, is_testing
from .profile import DEFAULT_PROFILE, ENV_VAR, TESTING_PROFILE, is_testing

default_config = """
[server]
Expand All @@ -21,18 +23,57 @@
""".strip()


def load_config():
def _using_legacy_testing_root() -> bool:
"""True when testing data still lives on the shared ``activitywatch/`` root."""
try:
from aw_core.dirs import using_legacy_testing_root

return using_legacy_testing_root()
except ImportError:
return os.environ.get(ENV_VAR) == TESTING_PROFILE
Comment thread
TimeToBuildBob marked this conversation as resolved.


def default_config_for(profile: str) -> str:
"""Default TOML for this profile's config file.

Isolated roots (including a fresh ``activitywatch-testing/``) get a
single ``[server]`` section — the directory already isolates, and
``[server-testing]`` in the same file is the pre-profile model.
The two-section default stays on the shared root so legacy testing
still finds ``[server-testing]`` next to prod.
"""
if profile != DEFAULT_PROFILE and not _using_legacy_testing_root():
port = default_port(profile)
return f"""
[server]
host = "localhost"
port = "{port}"
storage = "peewee"
cors_origins = ""

[server.custom_static]
""".strip()
return default_config


def load_config(profile: str = DEFAULT_PROFILE):
"""Load aw-server.toml from the current profile's config dir.

Must be called *after* ``export_profile`` so aw-core dirs see
``AW_PROFILE`` and isolate the file from other instances.
"""
return load_config_toml("aw-server", default_config)
return load_config_toml("aw-server", default_config_for(profile))


def config_section(profile: str) -> str:
"""TOML section for this profile: ``server`` or ``server-<profile>``."""
return "server" if profile == DEFAULT_PROFILE else f"server-{profile}"
"""TOML section for this profile.

Isolated roots use ``[server]``. ``[server-testing]`` remains only
for the legacy shared-root testing layout (same 3-rule as aw-core#152).
"""
if profile != DEFAULT_PROFILE and _using_legacy_testing_root():
return f"server-{profile}"
return "server"


def default_port(profile: str) -> int:
Expand Down
4 changes: 2 additions & 2 deletions aw_server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,10 @@ def parse_settings():
testing = is_testing(profile)

""" Parse config file """
config = load_config()
config = load_config(profile)
section = config_section(profile)
if section not in config:
if profile not in (DEFAULT_PROFILE,):
if profile not in (DEFAULT_PROFILE,) and section != "server":
logger.warning(
"Profile %s has no [%s] section, falling back to [server] "
"(port %s may collide with the default instance)",
Expand Down
19 changes: 15 additions & 4 deletions aw_server/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,23 @@
from .profile import profile_from_env, profile_suffix


def _settings_suffix(testing: bool) -> str:
"""Bare ``settings.json`` in isolated roots; ``-testing`` only in legacy."""
try:
from aw_core.dirs import legacy_testing_suffix

return legacy_testing_suffix(testing)
except ImportError:
return profile_suffix(profile_from_env(testing=testing))


class Settings:
def __init__(self, testing: bool):
# Dir isolation (AW_PROFILE) already separates profiles; the filename
# suffix is the pre-profile workaround and stays so --testing still
# finds settings-testing.json. Named profiles get the same shape.
filename = f"settings{profile_suffix(profile_from_env(testing=testing))}.json"
# Isolated roots (including new-style activitywatch-testing/) use
# bare settings.json — the directory already isolates. The
# settings-testing.json suffix stays so legacy shared-root testing
# still finds its file next to prod.
filename = f"settings{_settings_suffix(testing)}.json"
self.config_file = Path(get_config_dir("aw-server")) / filename
self.load()

Expand Down
104 changes: 83 additions & 21 deletions tests/test_profile_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import os
import sys
from pathlib import Path

import pytest

Expand All @@ -13,17 +14,66 @@

@pytest.fixture
def xdg_tmp(tmp_path, monkeypatch):
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "config"))
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "data"))
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path / "cache"))
"""Isolate dirs on every OS.

``XDG_*_HOME`` only redirects platformdirs on Linux. Windows/macOS keep
using APPDATA / ~/Library, so a marker planted under the XDG data path
is invisible there (CI: Test on windows-latest / macOS-latest). Patch
the same platformdirs getters aw-core tests patch.
"""
data = tmp_path / "data"
config = tmp_path / "config"
cache = tmp_path / "cache"

def _join(root: Path, appname: str) -> str:
return str(root / appname)

monkeypatch.setenv("XDG_CONFIG_HOME", str(config))
monkeypatch.setenv("XDG_DATA_HOME", str(data))
monkeypatch.setenv("XDG_CACHE_HOME", str(cache))
monkeypatch.setattr(
"aw_core.dirs.platformdirs.user_data_dir",
lambda appname, *a, **k: _join(data, appname),
)
monkeypatch.setattr(
"aw_core.dirs.platformdirs.user_config_dir",
lambda appname, *a, **k: _join(config, appname),
)
monkeypatch.setattr(
"aw_core.dirs.platformdirs.user_cache_dir",
lambda appname, *a, **k: _join(cache, appname),
)
monkeypatch.setattr(
"aw_core.dirs.platformdirs.user_cache_path",
lambda appname, *a, **k: cache / appname,
)
monkeypatch.setattr(
"aw_core.dirs.platformdirs.user_log_dir",
lambda appname, *a, **k: str(cache / appname / "log"),
)
monkeypatch.delenv("AW_PROFILE", raising=False)
return tmp_path


def _plant_legacy_testing_db(xdg_tmp: Path) -> None:
data = xdg_tmp / "data" / "activitywatch" / "aw-server"
data.mkdir(parents=True)
(data / "peewee-sqlite-testing.v2.db").write_text("")


class TestConfigHelpers:
def test_sections_are_disjoint(self):
sections = {config_section(p) for p in ("default", "testing", "research")}
assert sections == {"server", "server-testing", "server-research"}
def test_isolated_roots_use_server_section(self, xdg_tmp, monkeypatch):
monkeypatch.delenv("AW_PROFILE", raising=False)
assert config_section("default") == "server"
monkeypatch.setenv("AW_PROFILE", "testing")
assert config_section("testing") == "server"
monkeypatch.setenv("AW_PROFILE", "research")
assert config_section("research") == "server"

def test_legacy_testing_keeps_server_testing_section(self, xdg_tmp, monkeypatch):
_plant_legacy_testing_db(xdg_tmp)
monkeypatch.setenv("AW_PROFILE", "testing")
assert config_section("testing") == "server-testing"

def test_default_ports(self):
assert default_port(DEFAULT_PROFILE) == 5600
Expand All @@ -32,29 +82,26 @@ def test_default_ports(self):


class TestSettingsFilename:
def test_testing_keeps_legacy_name(self, tmp_path, monkeypatch):
monkeypatch.setattr(
"aw_server.settings.get_config_dir", lambda module: str(tmp_path)
)
monkeypatch.delenv("AW_PROFILE", raising=False)
def test_legacy_testing_keeps_suffixed_name(self, xdg_tmp, monkeypatch):
_plant_legacy_testing_db(xdg_tmp)
monkeypatch.setenv("AW_PROFILE", "testing")
settings = Settings(True)
assert settings.config_file.name == "settings-testing.json"

def test_default_unsuffixed(self, tmp_path, monkeypatch):
monkeypatch.setattr(
"aw_server.settings.get_config_dir", lambda module: str(tmp_path)
)
def test_isolated_testing_uses_bare_name(self, xdg_tmp, monkeypatch):
monkeypatch.setenv("AW_PROFILE", "testing")
settings = Settings(True)
assert settings.config_file.name == "settings.json"

def test_default_unsuffixed(self, xdg_tmp, monkeypatch):
monkeypatch.delenv("AW_PROFILE", raising=False)
settings = Settings(False)
assert settings.config_file.name == "settings.json"

def test_named_profile_suffix(self, tmp_path, monkeypatch):
monkeypatch.setattr(
"aw_server.settings.get_config_dir", lambda module: str(tmp_path)
)
def test_named_profile_uses_bare_name(self, xdg_tmp, monkeypatch):
monkeypatch.setenv("AW_PROFILE", "research")
settings = Settings(False)
assert settings.config_file.name == "settings-research.json"
assert settings.config_file.name == "settings.json"


class TestParseSettings:
Expand Down Expand Up @@ -82,7 +129,7 @@ def test_default_keeps_port_5600_and_unsets_env(self, xdg_tmp, monkeypatch):
assert settings.port == 5600
assert "AW_PROFILE" not in os.environ

def test_named_profile_exports_env_and_falls_back_to_server_section(
def test_named_profile_exports_env_and_uses_server_section(
self, xdg_tmp, monkeypatch
):
monkeypatch.setattr(sys, "argv", ["aw-server", "--profile", "research"])
Expand All @@ -92,6 +139,21 @@ def test_named_profile_exports_env_and_falls_back_to_server_section(
assert settings.port == 5600
assert os.environ["AW_PROFILE"] == "research"

def test_isolated_named_profile_reads_server_section_port(
self, xdg_tmp, monkeypatch
):
import aw_core.dirs as aw_dirs

export_profile("research")
cfg = Path(aw_dirs.get_config_dir("aw-server")) / "aw-server.toml"
cfg.write_text(
'[server]\nhost = "localhost"\nport = "5667"\n'
'storage = "peewee"\ncors_origins = ""\n[server.custom_static]\n'
)
monkeypatch.setattr(sys, "argv", ["aw-server", "--profile", "research"])
settings, _storage = parse_settings()
assert settings.port == 5667

def test_cli_port_override(self, xdg_tmp, monkeypatch):
monkeypatch.setattr(
sys, "argv", ["aw-server", "--profile", "research", "--port", "5667"]
Expand Down
Loading