-
-
Notifications
You must be signed in to change notification settings - Fork 631
test: add database metadata and YOLO mapping coverage #1343
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| import json | ||
| import sqlite3 | ||
|
|
||
| import pytest | ||
|
|
||
| from app.database import metadata as metadata_db | ||
|
|
||
|
|
||
| @pytest.fixture() | ||
| def metadata_database(tmp_path, monkeypatch): | ||
| db_path = tmp_path / "metadata.sqlite3" | ||
| monkeypatch.setattr(metadata_db, "DATABASE_PATH", str(db_path)) | ||
| return db_path | ||
|
|
||
|
|
||
| # Validates that metadata table creation inserts a default empty metadata row. | ||
| def test_create_metadata_table_initializes_empty_metadata(metadata_database): | ||
| metadata_db.db_create_metadata_table() | ||
|
|
||
| with sqlite3.connect(str(metadata_database)) as conn: | ||
| rows = conn.execute("SELECT metadata FROM metadata").fetchall() | ||
|
|
||
| assert rows == [("{}",)] | ||
| assert metadata_db.db_get_metadata() == {} | ||
|
|
||
|
|
||
| # Validates that creating the metadata table again does not overwrite existing metadata. | ||
| def test_create_metadata_table_preserves_existing_metadata(metadata_database): | ||
| metadata_db.db_create_metadata_table() | ||
| expected_metadata = {"user_preferences": {"YOLO_model_size": "medium"}} | ||
|
|
||
| assert metadata_db.db_update_metadata(expected_metadata) is True | ||
| metadata_db.db_create_metadata_table() | ||
|
|
||
| with sqlite3.connect(str(metadata_database)) as conn: | ||
| row_count = conn.execute("SELECT COUNT(*) FROM metadata").fetchone()[0] | ||
|
|
||
| assert row_count == 1 | ||
| assert metadata_db.db_get_metadata() == expected_metadata | ||
|
|
||
|
|
||
| # Validates that metadata retrieval returns None when the table has no metadata row. | ||
| def test_get_metadata_returns_none_when_no_row_exists(metadata_database): | ||
| metadata_db.db_create_metadata_table() | ||
|
|
||
| with sqlite3.connect(str(metadata_database)) as conn: | ||
| conn.execute("DELETE FROM metadata") | ||
|
|
||
| assert metadata_db.db_get_metadata() is None | ||
|
|
||
|
|
||
| # Validates that blank metadata content is treated as missing metadata. | ||
| def test_get_metadata_returns_none_for_blank_metadata(metadata_database): | ||
| metadata_db.db_create_metadata_table() | ||
|
|
||
| with sqlite3.connect(str(metadata_database)) as conn: | ||
| conn.execute("UPDATE metadata SET metadata = ?", ("",)) | ||
|
|
||
| assert metadata_db.db_get_metadata() is None | ||
|
|
||
|
|
||
| # Validates that invalid JSON in the metadata row is handled as missing metadata. | ||
| def test_get_metadata_returns_none_for_invalid_json(metadata_database): | ||
| metadata_db.db_create_metadata_table() | ||
|
|
||
| with sqlite3.connect(str(metadata_database)) as conn: | ||
| conn.execute("UPDATE metadata SET metadata = ?", ("{invalid-json",)) | ||
|
|
||
| assert metadata_db.db_get_metadata() is None | ||
|
|
||
|
|
||
| # Validates that updating metadata stores nested JSON-compatible values. | ||
| def test_update_metadata_stores_nested_values(metadata_database): | ||
| metadata_db.db_create_metadata_table() | ||
| expected_metadata = { | ||
| "user_preferences": {"YOLO_model_size": "nano", "GPU_Acceleration": False}, | ||
| "recent_folders": ["photos", "archives"], | ||
| "version": 2, | ||
| } | ||
|
|
||
| assert metadata_db.db_update_metadata(expected_metadata) is True | ||
|
|
||
| assert metadata_db.db_get_metadata() == expected_metadata | ||
|
|
||
|
|
||
| # Validates that updating metadata replaces the previous row instead of appending another row. | ||
| def test_update_metadata_replaces_existing_metadata_row(metadata_database): | ||
| metadata_db.db_create_metadata_table() | ||
| old_metadata = {"old": True} | ||
| new_metadata = {"new": True, "count": 3} | ||
|
|
||
| assert metadata_db.db_update_metadata(old_metadata) is True | ||
| assert metadata_db.db_update_metadata(new_metadata) is True | ||
|
|
||
| with sqlite3.connect(str(metadata_database)) as conn: | ||
| rows = conn.execute("SELECT metadata FROM metadata").fetchall() | ||
|
|
||
| assert len(rows) == 1 | ||
| assert json.loads(rows[0][0]) == new_metadata | ||
|
|
||
|
|
||
| # Validates that metadata can be updated through an existing database cursor. | ||
| def test_update_metadata_with_existing_cursor(metadata_database): | ||
| metadata_db.db_create_metadata_table() | ||
| expected_metadata = {"bulk_update": True} | ||
|
|
||
| conn = sqlite3.connect(str(metadata_database)) | ||
| try: | ||
| cursor = conn.cursor() | ||
| assert metadata_db.db_update_metadata(expected_metadata, cursor) is True | ||
| conn.commit() | ||
| finally: | ||
| conn.close() | ||
|
|
||
| assert metadata_db.db_get_metadata() == expected_metadata | ||
|
|
||
|
|
||
| # Validates that update failures roll back without deleting previous metadata. | ||
| def test_update_metadata_rolls_back_when_json_serialization_fails(metadata_database): | ||
| metadata_db.db_create_metadata_table() | ||
| original_metadata = {"safe": True} | ||
|
|
||
| assert metadata_db.db_update_metadata(original_metadata) is True | ||
|
|
||
| with pytest.raises(TypeError): | ||
| metadata_db.db_update_metadata({"bad": object()}) | ||
|
|
||
| assert metadata_db.db_get_metadata() == original_metadata | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,75 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import sqlite3 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import pytest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from app.database import yolo_mapping as yolo_mapping_db | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @pytest.fixture() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def yolo_database(tmp_path, monkeypatch): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db_path = tmp_path / "yolo_mapping.sqlite3" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| monkeypatch.setattr(yolo_mapping_db, "DATABASE_PATH", str(db_path)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return db_path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def fetch_mappings(db_path): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with sqlite3.connect(str(db_path)) as conn: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return conn.execute( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "SELECT class_id, name FROM mappings ORDER BY class_id" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ).fetchall() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Validates that YOLO class table creation stores each class name with its index. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_create_yolo_classes_table_inserts_class_names(yolo_database, monkeypatch): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| monkeypatch.setattr(yolo_mapping_db, "class_names", ["person", "bicycle", "car"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yolo_mapping_db.db_create_YOLO_classes_table() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert fetch_mappings(yolo_database) == [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (0, "person"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (1, "bicycle"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (2, "car"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Validates that rerunning table creation does not duplicate existing class mappings. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_create_yolo_classes_table_is_idempotent(yolo_database, monkeypatch): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| monkeypatch.setattr(yolo_mapping_db, "class_names", ["person", "car"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yolo_mapping_db.db_create_YOLO_classes_table() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yolo_mapping_db.db_create_YOLO_classes_table() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert fetch_mappings(yolo_database) == [(0, "person"), (1, "car")] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Validates that existing class IDs are replaced when class names change. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_create_yolo_classes_table_replaces_existing_class_names( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yolo_database, monkeypatch | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| monkeypatch.setattr(yolo_mapping_db, "class_names", ["old-person", "old-car"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yolo_mapping_db.db_create_YOLO_classes_table() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| monkeypatch.setattr(yolo_mapping_db, "class_names", ["person", "car"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yolo_mapping_db.db_create_YOLO_classes_table() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert fetch_mappings(yolo_database) == [(0, "person"), (1, "car")] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Validates that table creation succeeds when no YOLO classes are configured. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_create_yolo_classes_table_handles_empty_class_list(yolo_database, monkeypatch): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| monkeypatch.setattr(yolo_mapping_db, "class_names", []) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yolo_mapping_db.db_create_YOLO_classes_table() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert fetch_mappings(yolo_database) == [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+45
to
+64
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Add a shrink/clear regression case for stale mappings. These tests only cover replacement when the new Suggested regression test+def test_create_yolo_classes_table_removes_stale_rows_when_class_list_shrinks(
+ yolo_database, monkeypatch
+):
+ monkeypatch.setattr(yolo_mapping_db, "class_names", ["person", "car"])
+ yolo_mapping_db.db_create_YOLO_classes_table()
+
+ monkeypatch.setattr(yolo_mapping_db, "class_names", ["person"])
+ yolo_mapping_db.db_create_YOLO_classes_table()
+
+ assert fetch_mappings(yolo_database) == [(0, "person")]📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Path instructions |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Validates that duplicate class names can be stored under different class IDs. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_create_yolo_classes_table_allows_duplicate_names_with_distinct_ids( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yolo_database, monkeypatch | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| monkeypatch.setattr(yolo_mapping_db, "class_names", ["person", "person"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yolo_mapping_db.db_create_YOLO_classes_table() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert fetch_mappings(yolo_database) == [(0, "person"), (1, "person")] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
This doesn't actually test rollback after a write begins.
backend/app/database/metadata.pyserializes withjson.dumps()before it runsDELETE/INSERT, soobject()fails before any database state changes. That means this test only proves serialization failure leaves the old row untouched; it will not catch a regression in the transactional rollback path the PR summary calls out. Please force an error after the delete/insert sequence starts (for example by injecting a failing execute/commit) and then assert the original metadata is restored. As per path instructions, verify that all critical functionality is covered by tests.🤖 Prompt for AI Agents
Source: Path instructions