snapshot of https://github.com/ActivityWatch/aw-server.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build / Test on ubuntu-latest (push) Successful in 3m6s
Lint / lint (push) Successful in 22s
Build / typecheck (ubuntu-latest, 3.9) (push) Successful in 1m49s
Lint / format (push) Successful in 42s
Build / Test on macOS-latest (push) Has been cancelled
Build / Test on windows-latest (push) Has been cancelled
CodeQL / Analyze (python) (push) Failing after 1m31s

This commit is contained in:
upstream-mirror
2026-09-16 00:20:00 +05:00
commit 07fb811788
40 changed files with 4688 additions and 0 deletions

297
tests/test_client.py Executable file
View File

@@ -0,0 +1,297 @@
import json
import logging
import random
from datetime import datetime, timedelta, timezone
from pprint import pprint
from time import sleep
import pytest
from aw_core.models import Event
logging.basicConfig(level=logging.WARN)
# TODO: Could it be possible to write a sisterclass of ActivityWatchClient
# which calls aw_server.api directly? Would it be of use? Would add another
# layer of integration tests that are actually more like unit tests.
@pytest.fixture(scope="function")
def bucket(aw_client):
bucket_id = "test-" + str(random.randint(0, 10**5))
event_type = "testevents"
aw_client.create_bucket(bucket_id, event_type, queued=False)
print(f"Created bucket {bucket_id}")
sleep(1)
yield bucket_id
aw_client.delete_bucket(bucket_id)
@pytest.fixture
def queued_bucket(aw_client, bucket):
# FIXME: We need a way to clear the failed_requests file in order
# to have tests behave reasonably between runs.
aw_client.connect()
yield bucket
aw_client.disconnect()
def test_get_info(aw_client):
info = aw_client.get_info()
assert info["testing"]
def test_export(aw_client):
export = aw_client._get("export").json()
for bucket_id, bucket in export["buckets"].items():
assert bucket["id"]
assert "events" in bucket
# print(export)
def _create_heartbeat_events(
start=datetime.now(tz=timezone.utc), delta=timedelta(seconds=1)
):
e1_ts = start
e2_ts = e1_ts + delta
# Needed since server (or underlying datastore) drops precision up to milliseconds.
# Update: Even with millisecond precision it sometimes fails. (tried using `round` and `int`)
# Now rounding down to 10ms precision to prevent random failure.
# 10ms precision at least seems to work well.
# TODO: Figure out why it sometimes fails with millisecond precision. Would probably
# be useful to find the microsecond values where it consistently always fails.
e1_ts = e1_ts.replace(microsecond=int(e1_ts.microsecond / 10000) * 100)
e2_ts = e2_ts.replace(microsecond=int(e2_ts.microsecond / 10000) * 100)
e1 = Event(timestamp=e1_ts, data={"label": "test"})
e2 = Event(timestamp=e2_ts, data={"label": "test"})
return e1, e2
def _create_periodic_events(
num_events, start=datetime.now(tz=timezone.utc), delta=timedelta(hours=1)
):
events = num_events * [None]
for i, dt in ((i, start + i * delta) for i in range(len(events))):
events[i] = Event(timestamp=dt, duration=delta, data={"label": "test"})
return events
def test_heartbeat(aw_client, bucket):
bucket_id = bucket
e1, e2 = _create_heartbeat_events()
aw_client.heartbeat(bucket_id, e1, pulsetime=0)
aw_client.heartbeat(bucket_id, e2, pulsetime=10)
event = aw_client.get_events(bucket_id, limit=1)[0]
assert event.timestamp == e1.timestamp
assert event.duration == e2.timestamp - e1.timestamp
def test_heartbeat_random_order(aw_client, bucket):
bucket_id = bucket
# All the events will have the same data
events = _create_periodic_events(100, delta=timedelta(seconds=1))
random.shuffle(events)
for e in events:
aw_client.heartbeat(bucket_id, e, pulsetime=2)
events = aw_client.get_events(bucket_id, limit=-1)
# FIXME: This should pass
# assert len(events) == 1
def test_queued_heartbeat(aw_client, queued_bucket):
bucket_id = queued_bucket
e1, e2 = _create_heartbeat_events()
aw_client.heartbeat(bucket_id, e1, pulsetime=0, queued=True)
aw_client.heartbeat(bucket_id, e2, pulsetime=10, queued=True)
# Needed because of aw_client-side heartbeat merging and delayed dispatch
aw_client.heartbeat(
bucket_id,
Event(timestamp=e2.timestamp, data={"label": "something different"}),
pulsetime=0,
queued=True,
)
# Needed since the dispatcher thread might introduce some delay
max_tries = 20
for i in range(max_tries):
events = aw_client.get_events(bucket_id, limit=1)
if len(events) > 0 and events[0].duration > timedelta(seconds=0):
break
sleep(0.5)
assert i != max_tries - 1
print(f"Done on the {i + 1}th try")
assert len(events) == 1
event = events[0]
assert event.timestamp == e1.timestamp
assert event.duration == e2.timestamp - e1.timestamp
def test_list_buckets(aw_client, bucket):
buckets = aw_client.get_buckets()
assert bucket in buckets.keys()
def test_insert_event(aw_client, bucket):
event = Event(timestamp=datetime.now(tz=timezone.utc), data={"label": "test"})
aw_client.insert_event(bucket, event)
recv_events = aw_client.get_events(bucket, limit=1)
assert recv_events == [event]
def test_insert_events(aw_client, bucket):
events = _create_periodic_events(
2, start=datetime.now(tz=timezone.utc) - timedelta(days=1)
)
aw_client.insert_events(bucket, events)
recv_events = aw_client.get_events(bucket)
# Why isn't reverse=True needed here?
assert events == sorted(recv_events, key=lambda e: e.timestamp)
def test_get_event_single(aw_client, bucket):
start_dt = datetime.now(tz=timezone.utc) - timedelta(days=50)
delta = timedelta(hours=1)
events = _create_periodic_events(10, delta=delta, start=start_dt)
aw_client.insert_events(bucket, events)
events = aw_client.get_events(bucket)
for e in events:
e2 = aw_client.get_event(bucket, e.id)
assert e.id == e2.id
assert e.timestamp == e2.timestamp
assert e.duration == e2.duration
assert e.data == e2.data
def test_get_events_interval(aw_client, bucket):
start_dt = datetime.now(tz=timezone.utc) - timedelta(days=50)
end_dt = start_dt + timedelta(days=1)
delta = timedelta(hours=1)
events = _create_periodic_events(1000, delta=delta, start=start_dt)
aw_client.insert_events(bucket, events)
# start kwarg isn't currently range-inclusive
recv_events = aw_client.get_events(bucket, limit=50, start=start_dt, end=end_dt)
assert len(recv_events) == 25
# drop IDs
for e in recv_events:
e.id = None
print(recv_events)
src_events = sorted(events[:25], reverse=True, key=lambda e: e.timestamp)
# The first event gets cut off
assert recv_events[0].timestamp == src_events[0].timestamp
assert recv_events[0].data == src_events[0].data
# This fails due to microsecond precision issues
assert _round_td(recv_events[0].duration) == _round_td(
end_dt - recv_events[0].timestamp
)
# NOTE: Duration can differ by a few microseconds, so we round durations off to 10ms
recv_events = [_round_durations(e) for e in recv_events]
src_events = [_round_durations(e) for e in src_events]
assert recv_events[1:25] == src_events[1:25]
def _round_td(td: timedelta) -> timedelta:
return timedelta(seconds=round(td.total_seconds(), 2))
def _round_durations(e: Event):
e.duration = _round_td(e.duration)
return e
def test_store_many_events(aw_client, bucket):
events = _create_periodic_events(
1000, start=datetime.now(tz=timezone.utc) - timedelta(days=50)
)
aw_client.insert_events(bucket, events)
recv_events = aw_client.get_events(bucket, limit=-1)
assert len(events) == len(recv_events)
assert recv_events == sorted(events, reverse=True, key=lambda e: e.timestamp)
def test_midnight(aw_client, bucket):
start_dt = datetime.now() - timedelta(days=1)
midnight = start_dt.replace(hour=23, minute=50)
events = _create_periodic_events(100, start=midnight, delta=timedelta(minutes=1))
aw_client.insert_events(bucket, events)
recv_events = aw_client.get_events(bucket, limit=-1)
assert len(recv_events) == len(events)
def test_midnight_heartbeats(aw_client, bucket):
now = datetime.now(tz=timezone.utc) - timedelta(days=1)
# Zero out seconds/microseconds for deterministic midnight boundary.
# Without this, residual microseconds cause non-deterministic boundary
# matching when querying events around midnight (the endtime >= start_query
# condition can include/exclude boundary events depending on microseconds).
midnight = now.replace(hour=23, minute=50, second=0, microsecond=0)
events = _create_periodic_events(20, start=midnight, delta=timedelta(minutes=1))
label_ring = ["1", "1", "2", "3", "4"]
for i, e in enumerate(events):
e.data["label"] = label_ring[i % len(label_ring)]
aw_client.heartbeat(bucket, e, pulsetime=90)
recv_events_merged = aw_client.get_events(bucket, limit=-1)
assert len(recv_events_merged) == 4 / 5 * len(events)
# Query from midnight (00:00), which is exactly midnight + 10 minutes from
# the 23:50 start. Use a small offset to avoid boundary ambiguity where
# events ending exactly at midnight could be included by the >= condition.
query_start = midnight + timedelta(minutes=10, milliseconds=1)
recv_events_after_midnight = aw_client.get_events(bucket, start=query_start)
pprint(recv_events_after_midnight)
assert len(recv_events_after_midnight) == int(len(recv_events_merged) / 2)
def test_settings(aw_client):
aw_client.set_setting("test", "test")
aw_client.set_setting("list", json.dumps([1, 2, 3]))
aw_client.set_setting("dict", json.dumps({"a": 1, "b": 2}))
# check set
assert aw_client.get_setting("test") == "test"
assert json.loads(aw_client.get_setting("list")) == [1, 2, 3]
assert json.loads(aw_client.get_setting("dict")) == {"a": 1, "b": 2}
# check unset
assert aw_client.get_setting("test2") is None
# check get all
settings = aw_client.get_setting()
assert settings["test"] == "test"
assert json.loads(settings["list"]) == [1, 2, 3]
assert json.loads(settings["dict"]) == {"a": 1, "b": 2}