Skip to content

Commit 670f37f

Browse files
committed
better testing
1 parent b0fc1c7 commit 670f37f

8 files changed

Lines changed: 301 additions & 59 deletions

File tree

Pipfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ python-dateutil = "*"
1414
bump = "*"
1515

1616
[dev-packages]
17+
coverage = "*"
18+
pytest-cov = "*"
1719

1820
[requires]
1921
python_version = "3"

pyproject.toml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ classifiers = [
2121
]
2222

2323
[project.optional-dependencies]
24-
dev = ["pytest"]
24+
dev = ["pytest", "pytest-cov", "coverage"]
2525

2626
[project.urls]
2727
"Homepage" = "https://github.com/bloombar/gitcommitlogger"
@@ -30,3 +30,14 @@ dev = ["pytest"]
3030

3131
[project.scripts]
3232
gitcommitlogger = "gitcommitlogger.__main__:main"
33+
34+
[tool.pytest.ini_options]
35+
testpaths = ["tests"]
36+
addopts = "--cov=gitcommitlogger --cov-report=term-missing"
37+
38+
[tool.coverage.run]
39+
source = ["gitcommitlogger"]
40+
omit = ["tests/*"]
41+
42+
[tool.coverage.report]
43+
show_missing = true

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
build==1.1.1; python_version >= '3.7'
33
bump==1.3.2
44
certifi==2024.2.2; python_version >= '3.6'
5+
coverage==7.13.5; python_version >= '3.9'
6+
pytest-cov==7.1.0; python_version >= '3.9'
57
charset-normalizer==3.3.2; python_full_version >= '3.7.0'
68
click==8.1.7; python_version >= '3.7'
79
docutils==0.20.1; python_version >= '3.7'

src/gitcommitlogger/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ def fix_date(bad_date):
224224
# date from github actions context is in ISO format
225225
iso_date = bad_date
226226
parsed_t = dp.parse(iso_date)
227-
bad_date_int = int(parsed_t.strftime("%s"))
227+
bad_date_int = int(parsed_t.timestamp())
228228

229229
local_tz = pytz.timezone("America/New_York")
230230
utc_time = datetime.utcfromtimestamp(

tests/test.py

Lines changed: 0 additions & 57 deletions
This file was deleted.

tests/test_commit4.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
commit 7a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b
2+
Author: Alice Dev <alice@example.com>
3+
Date: 1700000000
4+
5+
Add user authentication feature
6+
7+
Implement JWT-based authentication with refresh tokens.
8+
Passwords are hashed with bcrypt before storage.
9+
10+
5 files changed, 120 insertions(+), 8 deletions(-)

tests/test_commit5.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
commit 1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b
2+
Author: Bob Dev <bob@example.com>
3+
Date: 1700000100
4+
5+
Fix null pointer exception in UserService
6+
Occurs when user profile is not yet initialized
7+
8+
2 files changed, 15 insertions(+), 3 deletions(-)

tests/test_main.py

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import json
2+
import os
3+
import re
4+
from datetime import datetime, timezone
5+
6+
import pytest
7+
8+
from gitcommitlogger.__main__ import fix_date, get_commit_data, get_commit_ids, get_exclusions
9+
10+
TESTS_DIR = os.path.dirname(__file__)
11+
SRC_DIR = os.path.join(os.path.dirname(TESTS_DIR), "src", "gitcommitlogger")
12+
13+
14+
class TestFixDate:
15+
def test_returns_mm_dd_yyyy_hh_mm_format(self):
16+
result = fix_date("1677887316")
17+
assert re.fullmatch(r"\d{2}/\d{2}/\d{4} \d{2}:\d{2}", result)
18+
19+
def test_unix_timestamp_converts_to_eastern_time(self):
20+
import pytz
21+
ts = 1677887316
22+
expected = (
23+
datetime.utcfromtimestamp(ts)
24+
.replace(tzinfo=pytz.utc)
25+
.astimezone(pytz.timezone("America/New_York"))
26+
.strftime("%m/%d/%Y %H:%M")
27+
)
28+
assert fix_date(str(ts)) == expected
29+
30+
def test_iso_format_accepted(self):
31+
result = fix_date("2023-03-03T21:48:36+00:00")
32+
assert re.fullmatch(r"\d{2}/\d{2}/\d{4} \d{2}:\d{2}", result)
33+
34+
def test_unix_and_iso_yield_same_result_for_same_moment(self):
35+
ts = 1677887316
36+
iso = datetime.fromtimestamp(ts, tz=timezone.utc).isoformat()
37+
assert fix_date(str(ts)) == fix_date(iso)
38+
39+
40+
class TestGetCommitData:
41+
42+
@pytest.fixture
43+
def commit1_data(self):
44+
with open(os.path.join(TESTS_DIR, "test_commit1.txt")) as f:
45+
return get_commit_data(f.read().strip())
46+
47+
@pytest.fixture
48+
def commit2_data(self):
49+
with open(os.path.join(TESTS_DIR, "test_commit2.txt")) as f:
50+
return get_commit_data(f.read().strip())
51+
52+
@pytest.fixture
53+
def commit3_data(self):
54+
with open(os.path.join(TESTS_DIR, "test_commit3.txt")) as f:
55+
return get_commit_data(f.read().strip())
56+
57+
@pytest.fixture
58+
def commit4_data(self):
59+
"""Non-merge commit with paragraph-style multi-line message (Subject + blank + Body)."""
60+
with open(os.path.join(TESTS_DIR, "test_commit4.txt")) as f:
61+
return get_commit_data(f.read().strip())
62+
63+
@pytest.fixture
64+
def commit5_data(self):
65+
"""Non-merge commit with consecutive-line multi-line message (no blank line separator)."""
66+
with open(os.path.join(TESTS_DIR, "test_commit5.txt")) as f:
67+
return get_commit_data(f.read().strip())
68+
69+
# --- simple commit (test_commit1) ---
70+
71+
def test_simple_id(self, commit1_data):
72+
assert commit1_data["id"] == "e32032cb2c89e6a33f0b6ff56667422c5229a518"
73+
74+
def test_simple_author_name(self, commit1_data):
75+
assert commit1_data["author_name"] == "Bishnu Dev"
76+
77+
def test_simple_author_email(self, commit1_data):
78+
assert commit1_data["author_email"] == "61447680+bordernone@users.noreply.github.com"
79+
80+
def test_simple_message(self, commit1_data):
81+
assert commit1_data["message"] == "added login, updated navbar"
82+
83+
def test_simple_files(self, commit1_data):
84+
assert commit1_data["files"] == "2"
85+
86+
def test_simple_additions(self, commit1_data):
87+
assert commit1_data["additions"] == "120"
88+
89+
def test_simple_deletions(self, commit1_data):
90+
assert commit1_data["deletions"] == "22"
91+
92+
def test_simple_date_format(self, commit1_data):
93+
assert re.fullmatch(r"\d{2}/\d{2}/\d{4} \d{2}:\d{2}", commit1_data["date"])
94+
95+
# --- merge commit with multi-line message (test_commit2) ---
96+
97+
def test_merge_multiline_id(self, commit2_data):
98+
assert commit2_data["id"] == "42df000869022d1b784f04a9aa6cacf69a5093c8"
99+
100+
def test_merge_multiline_author_name(self, commit2_data):
101+
assert commit2_data["author_name"] == "Yewon Song"
102+
103+
def test_merge_multiline_author_email(self, commit2_data):
104+
assert commit2_data["author_email"] == "98556497+sywu430@users.noreply.github.com"
105+
106+
def test_merge_multiline_message_is_first_line(self, commit2_data):
107+
assert commit2_data["message"].startswith(
108+
"Merge pull request #24 from agiledev-students-spring-2023"
109+
)
110+
111+
def test_merge_multiline_message_excludes_later_lines(self, commit2_data):
112+
assert "Added login screen" not in commit2_data["message"]
113+
assert "Foo bar baz" not in commit2_data["message"]
114+
115+
def test_merge_multiline_files(self, commit2_data):
116+
assert commit2_data["files"] == "2"
117+
118+
def test_merge_multiline_additions(self, commit2_data):
119+
assert commit2_data["additions"] == "120"
120+
121+
def test_merge_multiline_deletions(self, commit2_data):
122+
assert commit2_data["deletions"] == "22"
123+
124+
# --- merge commit with single-line message (test_commit3) ---
125+
126+
def test_merge_singleline_id(self, commit3_data):
127+
assert commit3_data["id"] == "42df000869022d1b784f04a9aa6cacf69a5093c8"
128+
129+
def test_merge_singleline_message(self, commit3_data):
130+
assert commit3_data["message"].startswith(
131+
"Merge pull request #24 from agiledev-students-spring-2023"
132+
)
133+
134+
def test_merge_singleline_files(self, commit3_data):
135+
assert commit3_data["files"] == "2"
136+
137+
def test_merge_singleline_additions(self, commit3_data):
138+
assert commit3_data["additions"] == "120"
139+
140+
def test_merge_singleline_deletions(self, commit3_data):
141+
assert commit3_data["deletions"] == "22"
142+
143+
# --- non-merge commit with paragraph-style multi-line message (test_commit4) ---
144+
145+
def test_paragraph_multiline_id(self, commit4_data):
146+
assert commit4_data["id"] == "7a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b"
147+
148+
def test_paragraph_multiline_author_name(self, commit4_data):
149+
assert commit4_data["author_name"] == "Alice Dev"
150+
151+
def test_paragraph_multiline_author_email(self, commit4_data):
152+
assert commit4_data["author_email"] == "alice@example.com"
153+
154+
def test_paragraph_multiline_message_is_subject_line(self, commit4_data):
155+
assert commit4_data["message"] == "Add user authentication feature"
156+
157+
def test_paragraph_multiline_message_excludes_body(self, commit4_data):
158+
assert "JWT" not in commit4_data["message"]
159+
assert "bcrypt" not in commit4_data["message"]
160+
161+
def test_paragraph_multiline_files(self, commit4_data):
162+
assert commit4_data["files"] == "5"
163+
164+
def test_paragraph_multiline_additions(self, commit4_data):
165+
assert commit4_data["additions"] == "120"
166+
167+
def test_paragraph_multiline_deletions(self, commit4_data):
168+
assert commit4_data["deletions"] == "8"
169+
170+
def test_paragraph_multiline_date_format(self, commit4_data):
171+
assert re.fullmatch(r"\d{2}/\d{2}/\d{4} \d{2}:\d{2}", commit4_data["date"])
172+
173+
# --- non-merge commit with consecutive-line multi-line message (test_commit5) ---
174+
175+
def test_consecutive_multiline_id(self, commit5_data):
176+
assert commit5_data["id"] == "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b"
177+
178+
def test_consecutive_multiline_author_name(self, commit5_data):
179+
assert commit5_data["author_name"] == "Bob Dev"
180+
181+
def test_consecutive_multiline_author_email(self, commit5_data):
182+
assert commit5_data["author_email"] == "bob@example.com"
183+
184+
def test_consecutive_multiline_message_is_first_line(self, commit5_data):
185+
assert commit5_data["message"] == "Fix null pointer exception in UserService"
186+
187+
def test_consecutive_multiline_message_excludes_second_line(self, commit5_data):
188+
assert "Occurs when" not in commit5_data["message"]
189+
190+
def test_consecutive_multiline_files(self, commit5_data):
191+
assert commit5_data["files"] == "2"
192+
193+
def test_consecutive_multiline_additions(self, commit5_data):
194+
assert commit5_data["additions"] == "15"
195+
196+
def test_consecutive_multiline_deletions(self, commit5_data):
197+
assert commit5_data["deletions"] == "3"
198+
199+
def test_consecutive_multiline_date_format(self, commit5_data):
200+
assert re.fullmatch(r"\d{2}/\d{2}/\d{4} \d{2}:\d{2}", commit5_data["date"])
201+
202+
# --- edge cases ---
203+
204+
def test_empty_input_returns_empty_dict(self):
205+
assert get_commit_data("") == {}
206+
207+
def test_malformed_input_returns_empty_dict(self):
208+
assert get_commit_data("not a git commit") == {}
209+
210+
211+
class TestGetCommitIds:
212+
213+
def test_extracts_ids_from_valid_json(self, tmp_path):
214+
commits = [{"id": "abc123"}, {"id": "def456"}, {"id": "789xyz"}]
215+
f = tmp_path / "commits.json"
216+
f.write_text(json.dumps(commits))
217+
assert get_commit_ids(str(f)) == ["abc123", "def456", "789xyz"]
218+
219+
def test_skips_entries_without_id_field(self, tmp_path):
220+
commits = [{"id": "abc123"}, {"sha": "no_id_here"}, {"id": "def456"}]
221+
f = tmp_path / "commits.json"
222+
f.write_text(json.dumps(commits))
223+
assert get_commit_ids(str(f)) == ["abc123", "def456"]
224+
225+
def test_empty_array_returns_empty_list(self, tmp_path):
226+
f = tmp_path / "commits.json"
227+
f.write_text("[]")
228+
assert get_commit_ids(str(f)) == []
229+
230+
def test_example_commits_file(self):
231+
path = os.path.join(SRC_DIR, "example_commits.json")
232+
ids = get_commit_ids(path)
233+
assert len(ids) == 1
234+
assert ids[0] == "8a126457bbd3a3e390058b6053bf700d404d6501"
235+
236+
237+
class TestGetExclusions:
238+
239+
def test_returns_list_of_strings(self):
240+
path = os.path.join(SRC_DIR, ".logsignore")
241+
result = get_exclusions(path)
242+
assert isinstance(result, list)
243+
assert all(isinstance(item, str) for item in result)
244+
245+
def test_logsignore_contains_known_patterns(self):
246+
path = os.path.join(SRC_DIR, ".logsignore")
247+
result = get_exclusions(path)
248+
assert any("lock" in p.lower() for p in result)
249+
250+
def test_empty_file_returns_empty_list(self, tmp_path):
251+
f = tmp_path / ".logsignore"
252+
f.write_text("")
253+
assert get_exclusions(str(f)) == []
254+
255+
def test_single_pattern_returned(self, tmp_path):
256+
f = tmp_path / ".logsignore"
257+
f.write_text("*.jpg\n")
258+
assert "*.jpg" in get_exclusions(str(f))
259+
260+
def test_multiple_patterns_all_returned(self, tmp_path):
261+
f = tmp_path / ".logsignore"
262+
f.write_text("*.jpg\n*.png\n*.gif\n")
263+
result = get_exclusions(str(f))
264+
assert "*.jpg" in result
265+
assert "*.png" in result
266+
assert "*.gif" in result

0 commit comments

Comments
 (0)