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
111 changes: 109 additions & 2 deletions aw_core/dirs.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,105 @@

GetDirFunc = Callable[[Optional[str]], str]

_DEFAULT_APPNAME = "activitywatch"
_TESTING_PROFILE = "testing"
_TESTING_APPNAME = f"{_DEFAULT_APPNAME}-{_TESTING_PROFILE}"

# Filenames that mark a machine as still using the pre-profile shared-root
# testing layout (ActivityWatch/activitywatch#1399). Keep this list specific:
# a false positive would pin a fresh install to the legacy layout forever.
_LEGACY_TESTING_FILENAME_MARKERS = (
"peewee-sqlite-testing",
"sqlite-testing",
"settings-testing",
"config-testing",
"-testing.db",
"-testing.toml",
"-testing.json",
"_testing_",
)


def get_profile() -> str:
"""Return the active ``AW_PROFILE`` value, or ``""`` for the default profile."""
return os.environ.get("AW_PROFILE", "") or ""


def _new_testing_root_exists() -> bool:
"""True if any platform dir for ``activitywatch-testing`` already exists.

Must not create directories: existence is the signal that a previous run
already adopted the isolated testing root.
"""
for getter in (
platformdirs.user_data_dir,
platformdirs.user_config_dir,
platformdirs.user_cache_dir,
):
if os.path.isdir(getter(_TESTING_APPNAME)):
return True
return False


def _is_legacy_testing_filename(name: str) -> bool:
lower = name.lower()
return any(marker in lower for marker in _LEGACY_TESTING_FILENAME_MARKERS)


def _legacy_testing_artifacts_exist() -> bool:
"""True if testing data still lives under the shared ``activitywatch`` root."""
roots = (
platformdirs.user_data_dir(_DEFAULT_APPNAME),
platformdirs.user_config_dir(_DEFAULT_APPNAME),
platformdirs.user_cache_dir(_DEFAULT_APPNAME),
)
for root in roots:
if not os.path.isdir(root):
continue
for dirpath, dirnames, filenames in os.walk(root):
for filename in filenames:
if _is_legacy_testing_filename(filename):
return True
# Descend one level (activitywatch/aw-server/...) not further.
if os.path.relpath(dirpath, root) != ".":
dirnames.clear()
return False


def using_legacy_testing_root() -> bool:
"""Whether ``AW_PROFILE=testing`` should stay on the shared ``activitywatch`` root.

Resolution rule (ActivityWatch/activitywatch#1399), identical on python and rust:

1. If ``activitywatch-testing/`` already exists: use it (new layout).
2. Else if legacy testing artifacts exist in the bare ``activitywatch/``
root: stay in legacy mode (old paths, old filenames).
3. Else (fresh setup): create and use ``activitywatch-testing/``.
"""
if get_profile() != _TESTING_PROFILE:
return False
if _new_testing_root_exists():
return False
return _legacy_testing_artifacts_exist()


def legacy_testing_suffix(testing: bool) -> str:
"""Return ``"-testing"`` only when testing data shares the default root.

Isolated profile roots (including new-style ``activitywatch-testing/``) use
bare filenames: the directory already isolates. Suffixed names remain only
in legacy mode so existing ``peewee-sqlite-testing.v2.db`` files keep working.
"""
if not testing:
return ""
profile = get_profile()
if profile == _TESTING_PROFILE and not using_legacy_testing_root():
return ""
if profile and profile != _TESTING_PROFILE:
# Named isolated profile — directory already isolates.
return ""
return "-testing"


def _get_appname() -> str:
"""Return the platformdirs appname, optionally suffixed by the active profile.
Expand All @@ -17,14 +116,22 @@ def _get_appname() -> str:
default profile. An unset or empty ``AW_PROFILE`` returns the bare
``"activitywatch"`` name, which is identical to the pre-profile behaviour.

The ``testing`` profile is special: see :func:`using_legacy_testing_root`.
Existing testing data in the shared root is not orphaned; fresh setups and
machines that already have ``activitywatch-testing/`` use the isolated root.

This is the single authoritative place where profile isolation is applied.
Every module that uses :func:`get_data_dir`, :func:`get_config_dir`,
:func:`get_cache_dir` or :func:`get_log_dir` automatically inherits the
correct root for the running profile; no ``profile=`` parameter needs to be
threaded through the call chain.
"""
profile = os.environ.get("AW_PROFILE", "")
return f"activitywatch-{profile}" if profile else "activitywatch"
profile = get_profile()
if not profile:
return _DEFAULT_APPNAME
if profile == _TESTING_PROFILE and using_legacy_testing_root():
return _DEFAULT_APPNAME
return f"{_DEFAULT_APPNAME}-{profile}"


def ensure_path_exists(path: str) -> None:
Expand Down
21 changes: 14 additions & 7 deletions aw_core/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,16 @@ def _get_latest_log_files(name, testing=False) -> List[str]: # pragma: no cover
"""
log_dir = dirs.get_log_dir(name)
files = filter(lambda filename: name in filename, os.listdir(log_dir))
files = filter(
lambda filename: "testing" in filename
if testing
else "testing" not in filename,
files,
)
# Isolated testing roots already separate logs by directory; the
# filename filter is only needed in the shared-root legacy layout.
isolated_testing = testing and not dirs.legacy_testing_suffix(testing)
if not isolated_testing:
files = filter(
lambda filename: (
"testing" in filename if testing else "testing" not in filename
),
files,
)
return [os.path.join(log_dir, filename) for filename in sorted(files, reverse=True)]


Expand Down Expand Up @@ -100,7 +104,10 @@ def _create_file_handler(
# $LOG_DIR/aw-server_testing_2017-01-05T00:21:39.log
file_ext = ".log.json" if log_json else ".log"
now_str = str(datetime.now().replace(microsecond=0).isoformat()).replace(":", "-")
log_name = name + "_" + ("testing_" if testing else "") + now_str + file_ext
testing_prefix = (
"testing_" if testing and dirs.legacy_testing_suffix(testing) else ""
)
log_name = name + "_" + testing_prefix + now_str + file_ext
log_file_path = os.path.join(log_dir, log_name)

# Create rotating logfile handler, max 10MB per file, 3 files max
Expand Down
4 changes: 2 additions & 2 deletions aw_datastore/migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os
from typing import List, Optional

from aw_core.dirs import get_data_dir
from aw_core.dirs import get_data_dir, legacy_testing_suffix

from .storages import AbstractStorage

Expand Down Expand Up @@ -31,7 +31,7 @@ def check_for_migration(datastore: AbstractStorage):

if datastore.sid == "sqlite":
peewee_type = "peewee-sqlite"
peewee_name = peewee_type + ("-testing" if datastore.testing else "")
peewee_name = peewee_type + legacy_testing_suffix(datastore.testing)
# Migrate from peewee v2
peewee_db_v2 = detect_db_files(data_dir, peewee_name, 2)
if len(peewee_db_v2) > 0:
Expand Down
4 changes: 2 additions & 2 deletions aw_datastore/storages/peewee.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
)

import iso8601
from aw_core.dirs import get_data_dir
from aw_core.dirs import get_data_dir, legacy_testing_suffix
from aw_core.models import Event
from playhouse.migrate import SqliteMigrator, migrate

Expand Down Expand Up @@ -171,7 +171,7 @@ def __init__(self, testing: bool = True, filepath: Optional[str] = None) -> None
if not filepath:
filename = (
"peewee-sqlite"
+ ("-testing" if testing else "")
+ legacy_testing_suffix(testing)
+ f".v{LATEST_VERSION}"
+ ".db"
)
Expand Down
4 changes: 2 additions & 2 deletions aw_datastore/storages/sqlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from datetime import datetime, timedelta, timezone
from typing import Iterable, List, Optional

from aw_core.dirs import get_data_dir
from aw_core.dirs import get_data_dir, legacy_testing_suffix
from aw_core.models import Event

from .abstract import AbstractStorage
Expand Down Expand Up @@ -77,7 +77,7 @@ def __init__(
# Ignore the migration check if custom filepath is set
ignore_migration_check = filepath is not None

ds_name = self.sid + ("-testing" if testing else "")
ds_name = self.sid + legacy_testing_suffix(testing)
if not filepath:
data_dir = get_data_dir("aw-server")
filename = ds_name + f".v{LATEST_VERSION}" + ".db"
Expand Down
Loading
Loading