Skip to content
Closed
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
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
pip install pytest
- name: Run tests
run: pytest
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
__pycache__/
*.py[cod]
*.db
data/
*.jpg
*.png
*.tflite
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Repository Agent Instructions

- Always run `pytest` before committing changes to ensure tests pass.
- The tests are located in the `tests/` directory.
- No external network access is available during test runs, so tests must not rely on downloading resources.

10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,13 @@ http://127.0.0.1:7766 or on http://yourserveraddress:7766

**Docker Image**
The image is on Docker Hub at https://hub.docker.com/r/mmcc73/whosatmyfeeder

## Development

Run the automated test suite with:

```bash
python agents.py
```

Tests use `pytest` and do not require network access.
12 changes: 12 additions & 0 deletions agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import subprocess
import sys


def run_tests():
"""Run the project's test suite using pytest."""
result = subprocess.run([sys.executable, '-m', 'pytest'], check=False)
return result.returncode


if __name__ == "__main__":
raise SystemExit(run_tests())
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ paho_mqtt==1.6.1
PyYAML==6.0
tflite_support==0.4.3
requests==2.30.0
Pillow==9.5.0
Pillow==9.5.0
pytest==8.3.5
63 changes: 63 additions & 0 deletions tests/test_queries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import os
import sys
import sqlite3
import types
import tempfile

sys.path.append(os.path.dirname(os.path.dirname(__file__)))
import queries


def setup_birdnames_db(path):
conn = sqlite3.connect(path)
cur = conn.cursor()
cur.execute("CREATE TABLE birdnames (scientific_name TEXT PRIMARY KEY, common_name TEXT NOT NULL)")
cur.execute("INSERT INTO birdnames VALUES ('passer domesticus', 'House Sparrow')")
conn.commit()
conn.close()


def setup_detections_db(path):
conn = sqlite3.connect(path)
cur = conn.cursor()
cur.execute(
"""
CREATE TABLE detections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
detection_time TIMESTAMP NOT NULL,
detection_index INTEGER,
score REAL,
display_name TEXT,
category_name TEXT,
frigate_event TEXT,
camera_name TEXT
)
"""
)
cur.execute(
"INSERT INTO detections (detection_time, detection_index, score, display_name, category_name, frigate_event, camera_name) "
"VALUES ('2024-01-01 00:00:00', 1, 0.9, 'passer domesticus', 'bird', 'event1', 'cam')"
)
conn.commit()
conn.close()


def test_get_common_name(tmp_path, monkeypatch):
db = tmp_path / "names.db"
setup_birdnames_db(db)
monkeypatch.setattr(queries, "NAMEDBPATH", str(db))
assert queries.get_common_name("passer domesticus") == "House Sparrow"


def test_recent_detections(tmp_path, monkeypatch):
data_db = tmp_path / "detections.db"
names_db = tmp_path / "names.db"
setup_detections_db(data_db)
setup_birdnames_db(names_db)
monkeypatch.setattr(queries, "DBPATH", str(data_db))
monkeypatch.setattr(queries, "NAMEDBPATH", str(names_db))
res = queries.recent_detections(1)
assert len(res) == 1
det = res[0]
assert det["display_name"] == "passer domesticus"
assert det["common_name"] == "House Sparrow"
60 changes: 60 additions & 0 deletions tests/test_speciesid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import sqlite3
import sys
import types
import importlib

def test_setupdb_creates_table(tmp_path, monkeypatch):
db_path = tmp_path / "speciesid.db"

# Create stub modules for missing dependencies
sys.modules.setdefault('numpy', types.ModuleType('numpy'))
sys.modules.setdefault('cv2', types.ModuleType('cv2'))
tflite_module = types.ModuleType('tflite_support')
task_module = types.ModuleType('task')
task_module.core = types.ModuleType('core')
task_module.processor = types.ModuleType('processor')
task_module.vision = types.ModuleType('vision')
tflite_module.task = task_module
sys.modules.setdefault('tflite_support', tflite_module)
sys.modules.setdefault('tflite_support.task', task_module)
sys.modules.setdefault('tflite_support.task.core', task_module.core)
sys.modules.setdefault('tflite_support.task.processor', task_module.processor)
sys.modules.setdefault('tflite_support.task.vision', task_module.vision)
sys.modules.setdefault('paho', types.ModuleType('paho'))
mqtt_pkg = types.ModuleType('paho.mqtt')
client_mod = types.ModuleType('paho.mqtt.client')
client_mod.Client = lambda *args, **kwargs: None
mqtt_pkg.client = client_mod
sys.modules.setdefault('paho.mqtt', mqtt_pkg)
sys.modules.setdefault('paho.mqtt.client', client_mod)
flask_mod = types.ModuleType('flask')
class DummyFlask(types.SimpleNamespace):
def __init__(self, *args, **kwargs):
super().__init__(route=lambda *a, **k: (lambda f: f))
self.jinja_env = types.SimpleNamespace(filters={})

flask_mod.Flask = DummyFlask
flask_mod.render_template = lambda *a, **k: ""
flask_mod.request = None
flask_mod.redirect = lambda *a, **k: ""
flask_mod.url_for = lambda *a, **k: ""
flask_mod.send_file = lambda *a, **k: None
flask_mod.abort = lambda *a, **k: None
flask_mod.send_from_directory = lambda *a, **k: None
sys.modules.setdefault('flask', flask_mod)
yaml_mod = types.ModuleType('yaml')
yaml_mod.safe_load = lambda f: {}
sys.modules.setdefault('yaml', yaml_mod)
sys.modules.setdefault('requests', types.ModuleType('requests'))
sys.modules.setdefault('PIL', types.ModuleType('PIL'))
sys.modules.setdefault('PIL.Image', types.ModuleType('Image'))
sys.modules.setdefault('PIL.ImageOps', types.ModuleType('ImageOps'))

speciesid = importlib.import_module('speciesid')
monkeypatch.setattr(speciesid, 'DBPATH', str(db_path))
speciesid.setupdb()

conn = sqlite3.connect(db_path)
cur = conn.cursor()
cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='detections'")
assert cur.fetchone() is not None
Loading