feat: set up modularized version of project with testing
This commit is contained in:
commit
7bea08ddac
19 changed files with 1138 additions and 0 deletions
150
tests/test_parser.py
Normal file
150
tests/test_parser.py
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import pytest
|
||||
|
||||
from timesheets.parser import (
|
||||
aggregate_rows,
|
||||
build_description,
|
||||
detect_has_duration_column,
|
||||
parse_table,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures / shared data
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
WITH_DURATION = [
|
||||
"| Start | End | Duration | Project | Story | Note |",
|
||||
"|-------|-------|----------|---------|-------------|---------|",
|
||||
"| 08:00 | 08:30 | 00:30 | bugs | story one | |",
|
||||
"| 08:30 | 09:00 | 00:30 | bugs | story one | |",
|
||||
"| 09:00 | 09:15 | 00:15 | scrum | | dsu |",
|
||||
]
|
||||
|
||||
WITHOUT_DURATION = [
|
||||
"| Start | End | Project | Story | Note |",
|
||||
"|-------|-------|---------|-------------|---------|",
|
||||
"| 08:00 | 08:30 | bugs | story one | |",
|
||||
"| 08:30 | 09:15 | scrum | | dsu |",
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# detect_has_duration_column
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestDetectHasDurationColumn:
|
||||
def test_with_duration(self):
|
||||
assert detect_has_duration_column(WITH_DURATION) is True
|
||||
|
||||
def test_without_duration(self):
|
||||
assert detect_has_duration_column(WITHOUT_DURATION) is False
|
||||
|
||||
def test_no_header_defaults_to_true(self):
|
||||
assert detect_has_duration_column(["no table here"]) is True
|
||||
|
||||
def test_case_insensitive(self):
|
||||
lines = ["| Start | End | DURATION | Project | Story | Note |"]
|
||||
assert detect_has_duration_column(lines) is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# parse_table
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestParseTable:
|
||||
def test_with_duration_column(self):
|
||||
rows = parse_table(WITH_DURATION, has_duration_col=True)
|
||||
assert len(rows) == 3
|
||||
assert rows[0]["project"] == "bugs"
|
||||
assert rows[0]["duration_hours"] == 0.5
|
||||
assert rows[2]["project"] == "scrum"
|
||||
assert rows[2]["note"] == "dsu"
|
||||
|
||||
def test_without_duration_column(self):
|
||||
rows = parse_table(WITHOUT_DURATION, has_duration_col=False)
|
||||
assert len(rows) == 2
|
||||
assert rows[0]["duration_hours"] == 0.5 # 08:00–08:30
|
||||
assert rows[1]["duration_hours"] == 0.75 # 08:30–09:15
|
||||
|
||||
def test_header_row_skipped(self):
|
||||
rows = parse_table(WITH_DURATION)
|
||||
assert all(r["start"] != "Start" for r in rows)
|
||||
|
||||
def test_separator_row_skipped(self):
|
||||
rows = parse_table(WITH_DURATION)
|
||||
assert all(r["start"] != "---" for r in rows)
|
||||
|
||||
def test_markdown_link_stripped_in_story(self):
|
||||
lines = [
|
||||
"| Start | End | Duration | Project | Story | Note |",
|
||||
"|-------|-------|----------|---------|----------------------------|------|",
|
||||
"| 08:00 | 08:30 | 00:30 | bugs | [ticket 1](:/abc123) | |",
|
||||
]
|
||||
rows = parse_table(lines)
|
||||
assert rows[0]["story"] == "ticket 1"
|
||||
|
||||
def test_invalid_duration_row_skipped(self):
|
||||
lines = [
|
||||
"| Start | End | Duration | Project | Story | Note |",
|
||||
"|-------|-------|----------|---------|-------|------|",
|
||||
"| 08:00 | 08:30 | bad | bugs | | |",
|
||||
]
|
||||
assert parse_table(lines) == []
|
||||
|
||||
def test_empty_input(self):
|
||||
assert parse_table([]) == []
|
||||
|
||||
def test_non_table_lines_ignored(self):
|
||||
lines = ["# My Timesheet", "", "Some prose."] + WITH_DURATION
|
||||
rows = parse_table(lines)
|
||||
assert len(rows) == 3
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# build_description
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestBuildDescription:
|
||||
def test_story_and_note(self):
|
||||
assert build_description("story", "note") == "story - note"
|
||||
|
||||
def test_story_only(self):
|
||||
assert build_description("story", "") == "story"
|
||||
|
||||
def test_note_only(self):
|
||||
assert build_description("", "note") == "note"
|
||||
|
||||
def test_both_empty(self):
|
||||
assert build_description("", "") == "/"
|
||||
|
||||
def test_strips_whitespace(self):
|
||||
assert build_description(" story ", " note ") == "story - note"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# aggregate_rows
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestAggregateRows:
|
||||
def test_same_project_story_summed(self):
|
||||
rows = parse_table(WITH_DURATION)
|
||||
aggregated = aggregate_rows(rows)
|
||||
bugs = next(e for e in aggregated if e["project"] == "bugs")
|
||||
assert bugs["quantity"] == 1.0 # 00:30 + 00:30
|
||||
|
||||
def test_distinct_entries_preserved(self):
|
||||
rows = parse_table(WITH_DURATION)
|
||||
aggregated = aggregate_rows(rows)
|
||||
assert len(aggregated) == 2 # bugs/story-one and scrum/dsu
|
||||
|
||||
def test_insertion_order_preserved(self):
|
||||
rows = parse_table(WITH_DURATION)
|
||||
aggregated = aggregate_rows(rows)
|
||||
assert aggregated[0]["project"] == "bugs"
|
||||
assert aggregated[1]["project"] == "scrum"
|
||||
|
||||
def test_empty_input(self):
|
||||
assert aggregate_rows([]) == []
|
||||
Loading…
Add table
Add a link
Reference in a new issue