From d205f4909e0b0a4dc96305fef383a8200401832d Mon Sep 17 00:00:00 2001 From: bronsen Date: Sun, 22 Feb 2026 18:08:39 +0100 Subject: [PATCH 1/7] test: ensure we read expected data from database --- src/teilchensammler_cli/main.py | 6 +-- src/teilchensammler_cli/models.py | 6 +-- tests.py | 85 +++++++++++++++++++++++++++++-- 3 files changed, 86 insertions(+), 11 deletions(-) diff --git a/src/teilchensammler_cli/main.py b/src/teilchensammler_cli/main.py index 42feda2..04c218d 100644 --- a/src/teilchensammler_cli/main.py +++ b/src/teilchensammler_cli/main.py @@ -12,7 +12,7 @@ from textual.screen import Screen from textual.widget import Widget from textual.widgets import Button, DataTable, Footer, Header, Input, Static -from .database import create_db_and_tables +from .database import create_db_and_tables, engine from .models import add_to_database, load_initial_data, make_teilchen_input logging.basicConfig( @@ -68,7 +68,7 @@ class SearchBar(Static): event.input.value = "" - teilchen = await add_to_database(tc) + teilchen = await add_to_database(tc, engine) self.screen.query_one(DataTable).add_row( teilchen.id, teilchen.name, teilchen.description, teilchen.number, teilchen.tags ) @@ -81,7 +81,7 @@ class SearchResults(Widget): async def on_mount(self) -> None: table: DataTable = self.query_one(DataTable) table.add_columns(*TEILCHEN_DATA_HEADER) - table.add_rows(await load_initial_data()) + table.add_rows(await load_initial_data(engine)) class AddInventoryScreen(Screen): diff --git a/src/teilchensammler_cli/models.py b/src/teilchensammler_cli/models.py index bca6c42..4ed6afc 100644 --- a/src/teilchensammler_cli/models.py +++ b/src/teilchensammler_cli/models.py @@ -9,7 +9,6 @@ from sqlmodel import ( select, ) -from .database import engine logger = logging.getLogger(__name__) @@ -82,8 +81,7 @@ async def make_teilchen_input(text: str) -> TeilchenCreate | None: return teilchen -async def load_initial_data() -> Sequence[Teilchen]: - from .database import engine +async def load_initial_data(engine) -> Sequence[Teilchen]: with Session(engine) as session: statement = select( @@ -93,7 +91,7 @@ async def load_initial_data() -> Sequence[Teilchen]: return all_teilchen -async def add_to_database(tc: TeilchenCreate) -> Teilchen: +async def add_to_database(tc: TeilchenCreate, engine) -> Teilchen: with Session(engine) as session: teilchen = Teilchen.model_validate(tc) session.add(teilchen) diff --git a/tests.py b/tests.py index 4eb3280..f035e5d 100644 --- a/tests.py +++ b/tests.py @@ -1,6 +1,15 @@ -import pytest +from typing import Generator, Sequence -from teilchensammler_cli.models import TeilchenCreate, make_teilchen_input +import pytest +from sqlalchemy import Engine +from sqlmodel import Session + +from teilchensammler_cli.models import ( + Teilchen, + TeilchenCreate, + load_initial_data, + make_teilchen_input, +) @pytest.mark.final # don't run while we are fiddling with the app @@ -10,7 +19,7 @@ def test_initial_layout(snap_compare): assert snap_compare(app, terminal_size=(130, 40)) -empty_teilchen = { +empty_teilchen_data = { "name": "", "description": "", "tags": "", @@ -32,7 +41,7 @@ def TC(**kwargs) -> TeilchenCreate | None: - an instance of `TeilchenCreate` """ if kwargs: - arguments = empty_teilchen | kwargs + arguments = empty_teilchen_data | kwargs return TeilchenCreate(**arguments) # ty:ignore[invalid-argument-type] @@ -46,11 +55,15 @@ def TC(**kwargs) -> TeilchenCreate | None: (".a.", TC()), ("a", TC()), ("aa", TC()), + (" .", TC()), # still an empty string # Just enough for "name" ("a.", TC(name="a", text="a.")), ("aa.", TC(name="aa", text="aa.")), ("a..", TC(name="a", text="a..")), ("a.b.", TC(name="a", text="a.b.")), + ("1.", TC(name="1", text="1.")), # numbers can be names + ("_.", TC(name="_", text="_.")), # underscores can be names + ("a b.", TC(name="a b", text="a b.")), # names can contain spaces # Just enough for "name" and "description" ('a."b"', TC(name="a", description="b", text='a."b"')), ('a. "b"', TC(name="a", description="b", text='a. "b"')), @@ -68,6 +81,7 @@ def TC(**kwargs) -> TeilchenCreate | None: ("a. ##b", TC(name="a", description="", tags="#b", text="a. ##b")), ("a. #", TC(name="a", description="", tags="", text="a. #")), ("a. ##", TC(name="a", description="", tags="", text="a. ##")), + ("a. #tag with spaces", TC(name="a", tags="#tag", text="a. #tag with spaces")), # do we care about "number"? ], ) @@ -78,3 +92,66 @@ async def test_maketeilcheninput_can_create_desired_teilchen( actual = await make_teilchen_input(example_input) assert expected == actual + + +@pytest.fixture(name="engine") +def in_memory_db() -> Generator[Engine]: + """Creates an in-memory sqlite database + + Yields: + The engine used to create the database. + """ + from sqlmodel import SQLModel, create_engine + + engine = create_engine("sqlite:///:memory:") + + SQLModel.metadata.create_all(engine) + yield engine + + engine.dispose() + + +@pytest.fixture(name="db_teilchen") +def teilchen_in_db(engine: Engine) -> Generator[Teilchen]: + """Creates a new Teilchen and stores it in the database. + + Args: + engine: an instance of sqlalchemy.Engine which was used to create the database. + + Yields: + The Teilchen refreshed from the database. + """ + import uuid + + teilchen = Teilchen( + id=uuid.uuid7(), name="TT", description="Test Teilchen", number=1, tags="", text="" + ) + with Session(engine) as session: + session.add(teilchen) + session.commit() + session.refresh(teilchen) + + yield teilchen + + with Session(engine) as session: + session.delete(teilchen) + session.commit() + + +async def test_loadinitialdata_returns_expected_data(engine: Engine, db_teilchen: Teilchen): + all_data: Sequence[tuple] = await load_initial_data(engine) + assert len(all_data) == 1 + + fetched_data: tuple = all_data[0] + teilchen_data: dict[str, str | int] = { + "id": fetched_data[0], + "name": fetched_data[1], + "description": fetched_data[2], + "number": 1, + "tags": "", + "text": "", + } + + fetched_teilchen = Teilchen.model_validate(teilchen_data) + + assert fetched_teilchen == db_teilchen From a4df79cda30ef978318bbbbc376caaeca56949cf Mon Sep 17 00:00:00 2001 From: bronsen Date: Sun, 22 Feb 2026 18:15:25 +0100 Subject: [PATCH 2/7] tests: ensure we actually commit given data to the database --- tests.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests.py b/tests.py index f035e5d..844b6b4 100644 --- a/tests.py +++ b/tests.py @@ -1,3 +1,4 @@ +import uuid from typing import Generator, Sequence import pytest @@ -9,6 +10,7 @@ from teilchensammler_cli.models import ( TeilchenCreate, load_initial_data, make_teilchen_input, + add_to_database, ) @@ -155,3 +157,16 @@ async def test_loadinitialdata_returns_expected_data(engine: Engine, db_teilchen fetched_teilchen = Teilchen.model_validate(teilchen_data) assert fetched_teilchen == db_teilchen + + +async def test_data_provided_to_addtodatabase_ends_up_in_database(engine: Engine): + all_data = await load_initial_data(engine) + assert len(all_data) == 0 + + teilchen = Teilchen( + id=uuid.uuid7(), name="test", description="test", tags="#test", number=1, text="test" + ) + + db_teilchen = await add_to_database(teilchen, engine) + + assert teilchen == db_teilchen From aba919adcacfbbdde2ae02665fedb2ef11337627 Mon Sep 17 00:00:00 2001 From: bronsen Date: Sun, 22 Feb 2026 18:57:01 +0100 Subject: [PATCH 3/7] tests: separate "engine" from "db with tables" --- tests.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/tests.py b/tests.py index 844b6b4..0bb0a73 100644 --- a/tests.py +++ b/tests.py @@ -103,18 +103,22 @@ def in_memory_db() -> Generator[Engine]: Yields: The engine used to create the database. """ - from sqlmodel import SQLModel, create_engine engine = create_engine("sqlite:///:memory:") - SQLModel.metadata.create_all(engine) yield engine engine.dispose() +@pytest.fixture(name="app_db") +def database_ready_to_use(engine: Engine) -> Generator[Engine]: + SQLModel.metadata.create_all(engine) + yield engine + + @pytest.fixture(name="db_teilchen") -def teilchen_in_db(engine: Engine) -> Generator[Teilchen]: +def teilchen_in_db(app_db: Engine) -> Generator[Teilchen]: """Creates a new Teilchen and stores it in the database. Args: @@ -128,20 +132,20 @@ def teilchen_in_db(engine: Engine) -> Generator[Teilchen]: teilchen = Teilchen( id=uuid.uuid7(), name="TT", description="Test Teilchen", number=1, tags="", text="" ) - with Session(engine) as session: + with Session(app_db) as session: session.add(teilchen) session.commit() session.refresh(teilchen) yield teilchen - with Session(engine) as session: + with Session(app_db) as session: session.delete(teilchen) session.commit() -async def test_loadinitialdata_returns_expected_data(engine: Engine, db_teilchen: Teilchen): - all_data: Sequence[tuple] = await load_initial_data(engine) +async def test_loadinitialdata_returns_expected_data(app_db: Engine, db_teilchen: Teilchen): + all_data: Sequence[tuple] = await load_initial_data(app_db) assert len(all_data) == 1 fetched_data: tuple = all_data[0] @@ -159,14 +163,14 @@ async def test_loadinitialdata_returns_expected_data(engine: Engine, db_teilchen assert fetched_teilchen == db_teilchen -async def test_data_provided_to_addtodatabase_ends_up_in_database(engine: Engine): - all_data = await load_initial_data(engine) +async def test_data_provided_to_addtodatabase_ends_up_in_database(app_db: Engine): + all_data = await load_initial_data(app_db) assert len(all_data) == 0 teilchen = Teilchen( id=uuid.uuid7(), name="test", description="test", tags="#test", number=1, text="test" ) - db_teilchen = await add_to_database(teilchen, engine) + db_teilchen = await add_to_database(teilchen, app_db) assert teilchen == db_teilchen From aecd10115e9433639af7091537beb4c0b42c44e3 Mon Sep 17 00:00:00 2001 From: bronsen Date: Sun, 22 Feb 2026 18:58:05 +0100 Subject: [PATCH 4/7] tests: ensure helper function actually updates database schema --- src/teilchensammler_cli/database.py | 2 +- src/teilchensammler_cli/main.py | 2 +- tests.py | 18 ++++++++++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/teilchensammler_cli/database.py b/src/teilchensammler_cli/database.py index b58456b..2d9f888 100644 --- a/src/teilchensammler_cli/database.py +++ b/src/teilchensammler_cli/database.py @@ -6,5 +6,5 @@ sqlite_url = os.environ.get("DATABASE_URL", "sqlite:///database.db") engine = create_engine(sqlite_url, echo=False) -def create_db_and_tables(): +def create_db_and_tables(engine): SQLModel.metadata.create_all(engine) diff --git a/src/teilchensammler_cli/main.py b/src/teilchensammler_cli/main.py index 04c218d..9787807 100644 --- a/src/teilchensammler_cli/main.py +++ b/src/teilchensammler_cli/main.py @@ -26,7 +26,7 @@ TEILCHEN_DATA_HEADER = "pk Name Description Number Tags".split() class SammlerApp(App): async def on_mount(self) -> None: - create_db_and_tables() + create_db_and_tables(engine) self.push_screen(AddInventoryScreen()) diff --git a/tests.py b/tests.py index 0bb0a73..2e11fcb 100644 --- a/tests.py +++ b/tests.py @@ -1,16 +1,18 @@ +from teilchensammler_cli.database import create_db_and_tables +from sqlalchemy.sql import text import uuid from typing import Generator, Sequence import pytest from sqlalchemy import Engine -from sqlmodel import Session +from sqlmodel import Session, SQLModel, create_engine from teilchensammler_cli.models import ( Teilchen, TeilchenCreate, + add_to_database, load_initial_data, make_teilchen_input, - add_to_database, ) @@ -174,3 +176,15 @@ async def test_data_provided_to_addtodatabase_ends_up_in_database(app_db: Engine db_teilchen = await add_to_database(teilchen, app_db) assert teilchen == db_teilchen + + +async def test_created_database_contains_expected_tables(engine: Engine): + + with engine.connect() as connection: + statement = text("SELECT name from sqlite_schema WHERE type = 'table'") + results = connection.execute(statement).all() + assert len(results) == 0 + + create_db_and_tables(engine) + results = connection.execute(statement).all() + assert len(results) == 1 From 1fdf5cb1d3197d4942e760069d375d41af6b6e3f Mon Sep 17 00:00:00 2001 From: bronsen Date: Sun, 22 Feb 2026 19:04:46 +0100 Subject: [PATCH 5/7] codestyle: remove unhelpful type hints --- tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests.py b/tests.py index 2e11fcb..1f0e1d9 100644 --- a/tests.py +++ b/tests.py @@ -1,7 +1,7 @@ from teilchensammler_cli.database import create_db_and_tables from sqlalchemy.sql import text import uuid -from typing import Generator, Sequence +from typing import Generator import pytest from sqlalchemy import Engine @@ -147,7 +147,7 @@ def teilchen_in_db(app_db: Engine) -> Generator[Teilchen]: async def test_loadinitialdata_returns_expected_data(app_db: Engine, db_teilchen: Teilchen): - all_data: Sequence[tuple] = await load_initial_data(app_db) + all_data = await load_initial_data(app_db) assert len(all_data) == 1 fetched_data: tuple = all_data[0] From 4832a0d69b91e2e9f084c768386e9b045f3232e3 Mon Sep 17 00:00:00 2001 From: bronsen Date: Sun, 22 Feb 2026 19:05:37 +0100 Subject: [PATCH 6/7] codestyle: move "statement" out of `with` block --- tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests.py b/tests.py index 1f0e1d9..844823e 100644 --- a/tests.py +++ b/tests.py @@ -180,11 +180,12 @@ async def test_data_provided_to_addtodatabase_ends_up_in_database(app_db: Engine async def test_created_database_contains_expected_tables(engine: Engine): + statement = text("SELECT name from sqlite_schema WHERE type = 'table'") with engine.connect() as connection: - statement = text("SELECT name from sqlite_schema WHERE type = 'table'") results = connection.execute(statement).all() assert len(results) == 0 create_db_and_tables(engine) + results = connection.execute(statement).all() assert len(results) == 1 From 278bd4a8ed77b7a3745c03770f609a160a355ebf Mon Sep 17 00:00:00 2001 From: bronsen Date: Sun, 22 Feb 2026 19:06:31 +0100 Subject: [PATCH 7/7] release: bump version since we have more tests now re: #14 --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6356117..f9c5d33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "teilchensammler-cli" -version = "0.5.0" +version = "0.5.1" description = "Build up and maintain an inventory of electronics parts and tools." readme = "README.md" requires-python = ">=3.14,<4.0" diff --git a/uv.lock b/uv.lock index 0c0c8bc..4c6979b 100644 --- a/uv.lock +++ b/uv.lock @@ -1153,7 +1153,7 @@ wheels = [ [[package]] name = "teilchensammler-cli" -version = "0.5.0" +version = "0.5.1" source = { editable = "." } dependencies = [ { name = "ciso8601" },