Files
upstream-aw-server/tests/test_client.py
upstream-mirror 07fb811788
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
snapshot of https://github.com/ActivityWatch/aw-server.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 00:20:00 +05:00

298 lines
9.6 KiB
Python
Executable File

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}