Add isRinging query to check alarm state via API
Features: - New GraphQL query 'isRinging' returns true/false if wecker is active - Checks the wecker process via PID file (PID_FILE in common.py) DRY refactoring: - Extract PID_FILE into shared common.py module - Both wecker.py and api/schema.py import from common - DRY enforcement tests verify identity (is) not just equality Tests: - test_is_ringing_returns_false_when_not_running - test_is_ringing_returns_true_when_running - test_pid_file_defined_once_across_modules (DRY enforcement) - test_pid_file_same_shared_constant_in_api (DRY enforcement) - Cleaned up unused imports in test_single_instance.py Docs: - Updated README.md with isRinging query documentation
This commit is contained in:
@@ -57,6 +57,14 @@ Instead of manually editing your crontab, this project provides a simple GraphQL
|
|||||||
|
|
||||||
### Example API Usage
|
### Example API Usage
|
||||||
|
|
||||||
|
**Check if alarm is currently ringing:**
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
isRinging
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Returns `true` if the wecker alarm clock is currently active (playing music), `false` otherwise.
|
||||||
|
|
||||||
**Get all alarms:**
|
**Get all alarms:**
|
||||||
```graphql
|
```graphql
|
||||||
query {
|
query {
|
||||||
@@ -92,6 +100,15 @@ mutation {
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Example response for `isRinging`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"isRinging": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## How the Puzzle Works
|
## How the Puzzle Works
|
||||||
1. **Ringing:** The music plays in an endless loop.
|
1. **Ringing:** The music plays in an endless loop.
|
||||||
2. **Start:** Press the arcade button once to start the puzzle. Wait 3 seconds.
|
2. **Start:** Press the arcade button once to start the puzzle. Wait 3 seconds.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from api.crontab_manager import CrontabManager
|
from api.crontab_manager import CrontabManager
|
||||||
|
from common import PID_FILE
|
||||||
|
|
||||||
|
|
||||||
def get_manager():
|
def get_manager():
|
||||||
@@ -11,6 +12,20 @@ def get_manager():
|
|||||||
return CrontabManager(tabfile=tabfile)
|
return CrontabManager(tabfile=tabfile)
|
||||||
|
|
||||||
|
|
||||||
|
def is_wecker_ringing() -> bool:
|
||||||
|
"""Check if the wecker process is currently running via the PID file."""
|
||||||
|
if not os.path.exists(PID_FILE):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
pid: int
|
||||||
|
with open(PID_FILE) as f:
|
||||||
|
pid = int(f.read().strip())
|
||||||
|
os.kill(pid, 0)
|
||||||
|
return True
|
||||||
|
except (ValueError, ProcessLookupError, PermissionError, OSError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class Alarm:
|
class Alarm:
|
||||||
id: str
|
id: str
|
||||||
@@ -21,6 +36,10 @@ class Alarm:
|
|||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class Query:
|
class Query:
|
||||||
|
@strawberry.field
|
||||||
|
def is_ringing(self) -> bool:
|
||||||
|
return is_wecker_ringing()
|
||||||
|
|
||||||
@strawberry.field
|
@strawberry.field
|
||||||
def get_alarms(self) -> List[Alarm]:
|
def get_alarms(self) -> List[Alarm]:
|
||||||
manager = get_manager()
|
manager = get_manager()
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# The PID file path — defined once here to avoid duplication.
|
||||||
|
# wecker.py writes this file when it starts; api/schema.py reads it to check state.
|
||||||
|
PID_FILE = str(Path(__file__).parent.absolute() / "wecker.pid")
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from unittest.mock import patch
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
import tempfile
|
import tempfile
|
||||||
import os
|
import os
|
||||||
@@ -14,6 +15,33 @@ from api.main import app # noqa: E402
|
|||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_ringing_returns_false_when_not_running():
|
||||||
|
"""Test that isRinging returns False when no wecker process is running."""
|
||||||
|
headers = {"X-API-Key": "test-secret"}
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
isRinging
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
res = client.post("/graphql", json={"query": query}, headers=headers)
|
||||||
|
assert res.status_code == 200
|
||||||
|
assert res.json()["data"]["isRinging"] is False
|
||||||
|
|
||||||
|
|
||||||
|
@patch("api.schema.is_wecker_ringing", return_value=True)
|
||||||
|
def test_is_ringing_returns_true_when_running(mock_is_ringing):
|
||||||
|
"""Test that isRinging returns True when a wecker process is running."""
|
||||||
|
headers = {"X-API-Key": "test-secret"}
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
isRinging
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
res = client.post("/graphql", json={"query": query}, headers=headers)
|
||||||
|
assert res.status_code == 200
|
||||||
|
assert res.json()["data"]["isRinging"] is True
|
||||||
|
|
||||||
|
|
||||||
def test_auth_missing():
|
def test_auth_missing():
|
||||||
query = """
|
query = """
|
||||||
query {
|
query {
|
||||||
@@ -173,3 +201,13 @@ def test_graphql_workflow():
|
|||||||
# 7. List again is empty
|
# 7. List again is empty
|
||||||
res = client.post("/graphql", json={"query": query_get}, headers=headers)
|
res = client.post("/graphql", json={"query": query_get}, headers=headers)
|
||||||
assert res.json()["data"]["getAlarms"] == []
|
assert res.json()["data"]["getAlarms"] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_pid_file_same_shared_constant_in_api():
|
||||||
|
"""Ensure api.schema uses the same PID_FILE from common, not a redefinition."""
|
||||||
|
from api import schema as api_schema
|
||||||
|
import common
|
||||||
|
|
||||||
|
assert api_schema.PID_FILE is common.PID_FILE, (
|
||||||
|
"api.schema.PID_FILE must reference common.PID_FILE, not redefine it"
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import common
|
||||||
import wecker
|
import wecker
|
||||||
|
|
||||||
# We need to mock the PID file existence and os.kill to test the logic
|
# We need to mock the PID file existence and os.kill to test the logic
|
||||||
@@ -80,3 +80,17 @@ def test_remove_pid_file_wrong_pid(mock_pid_file):
|
|||||||
|
|
||||||
wecker.remove_pid_file()
|
wecker.remove_pid_file()
|
||||||
assert mock_pid_file.exists()
|
assert mock_pid_file.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_pid_file_defined_once_across_modules():
|
||||||
|
"""
|
||||||
|
DRY principle: PID_FILE must be defined in common.py and imported by
|
||||||
|
both wecker.py and api/schema.py — not redefined in each.
|
||||||
|
"""
|
||||||
|
# wecker.py imports PID_FILE from common — verify it's the same object
|
||||||
|
assert wecker.PID_FILE is common.PID_FILE, (
|
||||||
|
"wecker.PID_FILE must reference common.PID_FILE, not redefine it"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the path ends with "wecker.pid"
|
||||||
|
assert common.PID_FILE.endswith("wecker.pid")
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import random
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from common import PID_FILE
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
@@ -13,8 +15,6 @@ logging.basicConfig(
|
|||||||
handlers=[logging.FileHandler("wecker.log"), logging.StreamHandler()],
|
handlers=[logging.FileHandler("wecker.log"), logging.StreamHandler()],
|
||||||
)
|
)
|
||||||
|
|
||||||
PID_FILE = "wecker.pid"
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_single_instance():
|
def ensure_single_instance():
|
||||||
"""Ensures that only one instance of the script is running."""
|
"""Ensures that only one instance of the script is running."""
|
||||||
|
|||||||
Reference in New Issue
Block a user