@theihtisham/dev-pulse 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/devpulse/__init__.py +4 -0
- package/devpulse/api/__init__.py +1 -0
- package/devpulse/api/app.py +371 -0
- package/devpulse/cli/__init__.py +1 -0
- package/devpulse/cli/dashboard.py +131 -0
- package/devpulse/cli/main.py +678 -0
- package/devpulse/cli/render.py +175 -0
- package/devpulse/core/__init__.py +34 -0
- package/devpulse/core/analytics.py +487 -0
- package/devpulse/core/config.py +77 -0
- package/devpulse/core/database.py +612 -0
- package/devpulse/core/github_client.py +281 -0
- package/devpulse/core/models.py +142 -0
- package/devpulse/core/report_generator.py +454 -0
- package/devpulse/static/.gitkeep +1 -0
- package/devpulse/templates/report.html +64 -0
- package/jest.config.js +7 -0
- package/package.json +35 -0
- package/pyproject.toml +80 -0
- package/requirements.txt +14 -0
- package/tests/__init__.py +1 -0
- package/tests/conftest.py +208 -0
- package/tests/test_analytics.py +284 -0
- package/tests/test_api.py +313 -0
- package/tests/test_cli.py +204 -0
- package/tests/test_config.py +47 -0
- package/tests/test_database.py +255 -0
- package/tests/test_models.py +107 -0
- package/tests/test_report_generator.py +173 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Tests for the Database layer."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from devpulse.core.database import Database
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestDatabaseInit:
|
|
12
|
+
"""Test database initialization."""
|
|
13
|
+
|
|
14
|
+
def test_creates_tables(self, db):
|
|
15
|
+
"""Database should initialize with all required tables."""
|
|
16
|
+
conn = db._connect()
|
|
17
|
+
tables = conn.execute(
|
|
18
|
+
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
|
|
19
|
+
).fetchall()
|
|
20
|
+
table_names = {t["name"] for t in tables}
|
|
21
|
+
conn.close()
|
|
22
|
+
|
|
23
|
+
expected = {
|
|
24
|
+
"commits", "pull_requests", "issues", "reviews",
|
|
25
|
+
"goals", "reports", "sprint_snapshots", "code_quality",
|
|
26
|
+
}
|
|
27
|
+
assert expected.issubset(table_names), f"Missing tables: {expected - table_names}"
|
|
28
|
+
|
|
29
|
+
def test_idempotent_init(self, db):
|
|
30
|
+
"""Running _init_db twice should not fail."""
|
|
31
|
+
db._init_db()
|
|
32
|
+
db._init_db() # Should not raise
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestCommitOperations:
|
|
36
|
+
"""Test commit CRUD operations."""
|
|
37
|
+
|
|
38
|
+
def test_insert_and_retrieve_commits(self, db, sample_commits):
|
|
39
|
+
count = db.upsert_commits(sample_commits)
|
|
40
|
+
assert count == 4
|
|
41
|
+
|
|
42
|
+
commits = db.get_commits()
|
|
43
|
+
assert len(commits) == 4
|
|
44
|
+
|
|
45
|
+
def test_upsert_commits_idempotent(self, db, sample_commits):
|
|
46
|
+
db.upsert_commits(sample_commits)
|
|
47
|
+
# Re-upserting same data should not duplicate rows
|
|
48
|
+
db.upsert_commits(sample_commits)
|
|
49
|
+
|
|
50
|
+
commits = db.get_commits()
|
|
51
|
+
assert len(commits) == 4 # No duplicates
|
|
52
|
+
|
|
53
|
+
def test_filter_by_repo(self, db, sample_commits):
|
|
54
|
+
db.upsert_commits(sample_commits)
|
|
55
|
+
commits = db.get_commits(repo="testorg/testrepo")
|
|
56
|
+
assert len(commits) == 4
|
|
57
|
+
|
|
58
|
+
commits = db.get_commits(repo="nonexistent/repo")
|
|
59
|
+
assert len(commits) == 0
|
|
60
|
+
|
|
61
|
+
def test_filter_by_author(self, db, sample_commits):
|
|
62
|
+
db.upsert_commits(sample_commits)
|
|
63
|
+
alice = db.get_commits(author="Alice")
|
|
64
|
+
assert len(alice) == 3
|
|
65
|
+
|
|
66
|
+
bob = db.get_commits(author="Bob")
|
|
67
|
+
assert len(bob) == 1
|
|
68
|
+
|
|
69
|
+
def test_filter_by_date_range(self, db, sample_commits):
|
|
70
|
+
db.upsert_commits(sample_commits)
|
|
71
|
+
commits = db.get_commits(since="2026-04-02", until="2026-04-02")
|
|
72
|
+
assert len(commits) == 1
|
|
73
|
+
assert commits[0]["author"] == "Bob"
|
|
74
|
+
|
|
75
|
+
def test_commit_count_by_day(self, db, sample_commits):
|
|
76
|
+
db.upsert_commits(sample_commits)
|
|
77
|
+
counts = db.get_commit_count_by_day(days=30)
|
|
78
|
+
assert len(counts) == 3 # 3 unique dates
|
|
79
|
+
# April 1 should have 2 commits
|
|
80
|
+
apr1 = [c for c in counts if c["day"] == "2026-04-01"]
|
|
81
|
+
assert len(apr1) == 1
|
|
82
|
+
assert apr1[0]["count"] == 2
|
|
83
|
+
|
|
84
|
+
def test_commit_limit(self, db, sample_commits):
|
|
85
|
+
db.upsert_commits(sample_commits)
|
|
86
|
+
commits = db.get_commits(limit=2)
|
|
87
|
+
assert len(commits) == 2
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestPullRequestOperations:
|
|
91
|
+
"""Test pull request CRUD operations."""
|
|
92
|
+
|
|
93
|
+
def test_insert_and_retrieve_prs(self, db, sample_prs):
|
|
94
|
+
count = db.upsert_pull_requests(sample_prs)
|
|
95
|
+
assert count == 3
|
|
96
|
+
|
|
97
|
+
prs = db.get_pull_requests()
|
|
98
|
+
assert len(prs) == 3
|
|
99
|
+
|
|
100
|
+
def test_filter_prs_by_state(self, db, sample_prs):
|
|
101
|
+
db.upsert_pull_requests(sample_prs)
|
|
102
|
+
merged = db.get_pull_requests(state="merged")
|
|
103
|
+
assert len(merged) == 2
|
|
104
|
+
|
|
105
|
+
def test_filter_prs_by_author(self, db, sample_prs):
|
|
106
|
+
db.upsert_pull_requests(sample_prs)
|
|
107
|
+
alice = db.get_pull_requests(author="Alice")
|
|
108
|
+
assert len(alice) == 2
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class TestIssueOperations:
|
|
112
|
+
"""Test issue CRUD operations."""
|
|
113
|
+
|
|
114
|
+
def test_insert_and_retrieve_issues(self, db, sample_issues):
|
|
115
|
+
count = db.upsert_issues(sample_issues)
|
|
116
|
+
assert count == 3
|
|
117
|
+
|
|
118
|
+
issues = db.get_issues()
|
|
119
|
+
assert len(issues) == 3
|
|
120
|
+
|
|
121
|
+
def test_filter_issues_by_state(self, db, sample_issues):
|
|
122
|
+
db.upsert_issues(sample_issues)
|
|
123
|
+
open_issues = db.get_issues(state="open")
|
|
124
|
+
assert len(open_issues) == 2
|
|
125
|
+
|
|
126
|
+
closed_issues = db.get_issues(state="closed")
|
|
127
|
+
assert len(closed_issues) == 1
|
|
128
|
+
|
|
129
|
+
def test_issue_labels_stored_as_json(self, db, sample_issues):
|
|
130
|
+
db.upsert_issues(sample_issues)
|
|
131
|
+
issues = db.get_issues()
|
|
132
|
+
bug_issue = [i for i in issues if i["number"] == 12][0]
|
|
133
|
+
labels = json.loads(bug_issue["labels"])
|
|
134
|
+
assert "bug" in labels
|
|
135
|
+
assert "priority:high" in labels
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class TestReviewOperations:
|
|
139
|
+
"""Test review CRUD operations."""
|
|
140
|
+
|
|
141
|
+
def test_insert_and_retrieve_reviews(self, db, sample_reviews):
|
|
142
|
+
count = db.upsert_reviews(sample_reviews)
|
|
143
|
+
assert count == 2
|
|
144
|
+
|
|
145
|
+
reviews = db.get_reviews()
|
|
146
|
+
assert len(reviews) == 2
|
|
147
|
+
|
|
148
|
+
def test_filter_reviews_by_author(self, db, sample_reviews):
|
|
149
|
+
db.upsert_reviews(sample_reviews)
|
|
150
|
+
bob_reviews = db.get_reviews(author="Bob")
|
|
151
|
+
assert len(bob_reviews) == 1
|
|
152
|
+
assert bob_reviews[0]["state"] == "APPROVED"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class TestGoalOperations:
|
|
156
|
+
"""Test goal CRUD operations."""
|
|
157
|
+
|
|
158
|
+
def test_create_and_list_goals(self, db):
|
|
159
|
+
gid = db.upsert_goal({
|
|
160
|
+
"title": "10 commits/day",
|
|
161
|
+
"target_value": 10,
|
|
162
|
+
"metric": "commits_per_day",
|
|
163
|
+
"deadline": "2026-05-01",
|
|
164
|
+
})
|
|
165
|
+
assert isinstance(gid, int)
|
|
166
|
+
assert gid > 0
|
|
167
|
+
|
|
168
|
+
goals = db.get_goals()
|
|
169
|
+
assert len(goals) == 1
|
|
170
|
+
assert goals[0]["title"] == "10 commits/day"
|
|
171
|
+
|
|
172
|
+
def test_update_goal(self, db):
|
|
173
|
+
gid = db.upsert_goal({
|
|
174
|
+
"title": "10 commits/day",
|
|
175
|
+
"target_value": 10,
|
|
176
|
+
"metric": "commits_per_day",
|
|
177
|
+
})
|
|
178
|
+
db.upsert_goal({
|
|
179
|
+
"id": gid,
|
|
180
|
+
"title": "10 commits/day",
|
|
181
|
+
"target_value": 10,
|
|
182
|
+
"current_value": 5,
|
|
183
|
+
"metric": "commits_per_day",
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
goals = db.get_goals()
|
|
187
|
+
assert goals[0]["current_value"] == 5
|
|
188
|
+
|
|
189
|
+
def test_delete_goal(self, db):
|
|
190
|
+
gid = db.upsert_goal({
|
|
191
|
+
"title": "Test goal",
|
|
192
|
+
"target_value": 5,
|
|
193
|
+
"metric": "test",
|
|
194
|
+
})
|
|
195
|
+
assert db.delete_goal(gid) is True
|
|
196
|
+
assert db.delete_goal(9999) is False # Non-existent
|
|
197
|
+
|
|
198
|
+
def test_filter_goals_by_status(self, db):
|
|
199
|
+
db.upsert_goal({"title": "Active", "target_value": 5, "metric": "test", "status": "active"})
|
|
200
|
+
db.upsert_goal({"title": "Done", "target_value": 5, "metric": "test", "status": "completed"})
|
|
201
|
+
|
|
202
|
+
active = db.get_goals(status="active")
|
|
203
|
+
assert len(active) == 1
|
|
204
|
+
assert active[0]["title"] == "Active"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class TestReportCache:
|
|
208
|
+
"""Test report caching."""
|
|
209
|
+
|
|
210
|
+
def test_save_and_get_report(self, db):
|
|
211
|
+
db.save_report("daily", "2026-04-01", "# Daily Report\nContent here")
|
|
212
|
+
content = db.get_report("daily", "2026-04-01")
|
|
213
|
+
assert content is not None
|
|
214
|
+
assert "Daily Report" in content
|
|
215
|
+
|
|
216
|
+
def test_get_nonexistent_report(self, db):
|
|
217
|
+
content = db.get_report("daily", "1999-01-01")
|
|
218
|
+
assert content is None
|
|
219
|
+
|
|
220
|
+
def test_overwrite_report(self, db):
|
|
221
|
+
db.save_report("daily", "2026-04-01", "Version 1")
|
|
222
|
+
db.save_report("daily", "2026-04-01", "Version 2")
|
|
223
|
+
content = db.get_report("daily", "2026-04-01")
|
|
224
|
+
assert "Version 2" in content
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class TestSprintSnapshots:
|
|
228
|
+
"""Test sprint snapshot operations."""
|
|
229
|
+
|
|
230
|
+
def test_save_and_get_snapshots(self, db):
|
|
231
|
+
db.save_sprint_snapshot({
|
|
232
|
+
"sprint_name": "Sprint 1",
|
|
233
|
+
"total_points": 30,
|
|
234
|
+
"completed_points": 10,
|
|
235
|
+
"remaining_points": 20,
|
|
236
|
+
"added_points": 0,
|
|
237
|
+
})
|
|
238
|
+
snapshots = db.get_sprint_snapshots("Sprint 1")
|
|
239
|
+
assert len(snapshots) == 1
|
|
240
|
+
assert snapshots[0]["total_points"] == 30
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class TestCodeQuality:
|
|
244
|
+
"""Test code quality snapshot operations."""
|
|
245
|
+
|
|
246
|
+
def test_save_and_get_quality(self, db):
|
|
247
|
+
db.save_quality_snapshot({
|
|
248
|
+
"repo": "testorg/testrepo",
|
|
249
|
+
"test_coverage": 0.85,
|
|
250
|
+
"open_bugs": 3,
|
|
251
|
+
"tech_debt_score": 4.2,
|
|
252
|
+
})
|
|
253
|
+
snapshots = db.get_quality_snapshots(repo="testorg/testrepo")
|
|
254
|
+
assert len(snapshots) == 1
|
|
255
|
+
assert snapshots[0]["test_coverage"] == 0.85
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Tests for Pydantic models."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from devpulse.core.models import (
|
|
6
|
+
Commit,
|
|
7
|
+
PullRequest,
|
|
8
|
+
Issue,
|
|
9
|
+
Review,
|
|
10
|
+
DeveloperMetrics,
|
|
11
|
+
SprintData,
|
|
12
|
+
TeamHealth,
|
|
13
|
+
CodeQuality,
|
|
14
|
+
Goal,
|
|
15
|
+
DailyReport,
|
|
16
|
+
WeeklyReport,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestCommit:
|
|
21
|
+
def test_create_commit(self):
|
|
22
|
+
c = Commit(sha="abc123", repo="org/repo", author="Alice", author_date="2026-04-01T10:00:00Z")
|
|
23
|
+
assert c.sha == "abc123"
|
|
24
|
+
assert c.additions == 0
|
|
25
|
+
|
|
26
|
+
def test_commit_defaults(self):
|
|
27
|
+
c = Commit(sha="abc", repo="r", author="a", author_date="d")
|
|
28
|
+
assert c.message == ""
|
|
29
|
+
assert c.url == ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestPullRequest:
|
|
33
|
+
def test_create_pr(self):
|
|
34
|
+
pr = PullRequest(number=1, repo="org/repo")
|
|
35
|
+
assert pr.state == "open"
|
|
36
|
+
assert pr.merged_at is None
|
|
37
|
+
|
|
38
|
+
def test_pr_with_merge(self):
|
|
39
|
+
pr = PullRequest(number=1, repo="org/repo", state="merged", merged_at="2026-04-02")
|
|
40
|
+
assert pr.state == "merged"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TestIssue:
|
|
44
|
+
def test_create_issue(self):
|
|
45
|
+
issue = Issue(number=10, repo="org/repo")
|
|
46
|
+
assert issue.labels == []
|
|
47
|
+
assert issue.state == "open"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TestDeveloperMetrics:
|
|
51
|
+
def test_create_metrics(self):
|
|
52
|
+
m = DeveloperMetrics(author="Alice")
|
|
53
|
+
assert m.commits_count == 0
|
|
54
|
+
assert m.avg_pr_merge_time_hours == 0.0
|
|
55
|
+
|
|
56
|
+
def test_metrics_serialization(self):
|
|
57
|
+
m = DeveloperMetrics(author="Bob", commits_count=5, active_days=3)
|
|
58
|
+
d = m.model_dump()
|
|
59
|
+
assert d["author"] == "Bob"
|
|
60
|
+
assert d["commits_count"] == 5
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TestTeamHealth:
|
|
64
|
+
def test_create_team_health(self):
|
|
65
|
+
th = TeamHealth()
|
|
66
|
+
assert th.overall_score == 0.0
|
|
67
|
+
assert th.recommendations == []
|
|
68
|
+
|
|
69
|
+
def test_team_health_with_burnout(self):
|
|
70
|
+
th = TeamHealth(
|
|
71
|
+
overall_score=75.5,
|
|
72
|
+
burnout_risk={"Alice": 0.3, "Bob": 0.7},
|
|
73
|
+
velocity_trend="stable",
|
|
74
|
+
)
|
|
75
|
+
assert th.burnout_risk["Bob"] == 0.7
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TestGoal:
|
|
79
|
+
def test_create_goal(self):
|
|
80
|
+
g = Goal(title="Test", target_value=10, metric="commits")
|
|
81
|
+
assert g.id is None
|
|
82
|
+
assert g.status == "active"
|
|
83
|
+
|
|
84
|
+
def test_goal_with_id(self):
|
|
85
|
+
g = Goal(id=1, title="Test", target_value=10, metric="commits")
|
|
86
|
+
assert g.id == 1
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class TestSprintData:
|
|
90
|
+
def test_create_sprint(self):
|
|
91
|
+
s = SprintData(name="Sprint 1")
|
|
92
|
+
assert s.total_points == 0
|
|
93
|
+
assert s.scope_creep_pct == 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class TestDailyReport:
|
|
97
|
+
def test_create_daily_report(self):
|
|
98
|
+
r = DailyReport(date="2026-04-01", author="Alice")
|
|
99
|
+
assert r.commits == 0
|
|
100
|
+
assert r.summary == ""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TestWeeklyReport:
|
|
104
|
+
def test_create_weekly_report(self):
|
|
105
|
+
r = WeeklyReport(week_start="2026-04-01", week_end="2026-04-07")
|
|
106
|
+
assert r.total_commits == 0
|
|
107
|
+
assert r.insights == []
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Tests for the ReportGenerator."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from devpulse.core.report_generator import ReportGenerator, _md_to_html
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestDailyReport:
|
|
12
|
+
"""Test daily report generation."""
|
|
13
|
+
|
|
14
|
+
def test_daily_report_basic(self, populated_db):
|
|
15
|
+
gen = ReportGenerator(db=populated_db)
|
|
16
|
+
report = gen.daily_report(target_date="2026-04-01")
|
|
17
|
+
|
|
18
|
+
assert report["date"] == "2026-04-01"
|
|
19
|
+
assert report["commits"] == 2 # Alice's 2 commits on April 1
|
|
20
|
+
assert report["prs_opened"] >= 0
|
|
21
|
+
assert "summary" in report
|
|
22
|
+
|
|
23
|
+
def test_daily_report_with_author(self, populated_db):
|
|
24
|
+
gen = ReportGenerator(db=populated_db)
|
|
25
|
+
report = gen.daily_report(target_date="2026-04-01", author="Alice")
|
|
26
|
+
|
|
27
|
+
assert report["author"] == "Alice"
|
|
28
|
+
assert report["commits"] == 2
|
|
29
|
+
|
|
30
|
+
def test_daily_report_no_activity(self, populated_db):
|
|
31
|
+
gen = ReportGenerator(db=populated_db)
|
|
32
|
+
report = gen.daily_report(target_date="2020-01-01")
|
|
33
|
+
|
|
34
|
+
assert report["commits"] == 0
|
|
35
|
+
assert report["summary"] # Should still have a summary
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestWeeklyReport:
|
|
39
|
+
"""Test weekly report generation."""
|
|
40
|
+
|
|
41
|
+
def test_weekly_report_basic(self, populated_db):
|
|
42
|
+
gen = ReportGenerator(db=populated_db)
|
|
43
|
+
report = gen.weekly_report(week_start="2026-03-30")
|
|
44
|
+
|
|
45
|
+
assert report["week_start"] == "2026-03-30"
|
|
46
|
+
assert report["week_end"] == "2026-04-05"
|
|
47
|
+
assert report["total_commits"] >= 0
|
|
48
|
+
assert isinstance(report["top_contributors"], list)
|
|
49
|
+
|
|
50
|
+
def test_weekly_report_has_insights(self, populated_db):
|
|
51
|
+
gen = ReportGenerator(db=populated_db)
|
|
52
|
+
report = gen.weekly_report(week_start="2026-03-30")
|
|
53
|
+
|
|
54
|
+
assert isinstance(report["insights"], list)
|
|
55
|
+
assert isinstance(report["recommendations"], list)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TestMonthlyReport:
|
|
59
|
+
"""Test monthly report generation."""
|
|
60
|
+
|
|
61
|
+
def test_monthly_report_basic(self, populated_db):
|
|
62
|
+
gen = ReportGenerator(db=populated_db)
|
|
63
|
+
report = gen.monthly_report(year=2026, month=4)
|
|
64
|
+
|
|
65
|
+
assert report["period"] == "2026-04"
|
|
66
|
+
assert report["total_commits"] >= 0
|
|
67
|
+
assert "team_health_score" in report
|
|
68
|
+
assert "velocity_trend" in report
|
|
69
|
+
|
|
70
|
+
def test_monthly_report_default_period(self, populated_db):
|
|
71
|
+
gen = ReportGenerator(db=populated_db)
|
|
72
|
+
report = gen.monthly_report()
|
|
73
|
+
|
|
74
|
+
assert len(report["period"]) == 7 # YYYY-MM format
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class TestOutputFormats:
|
|
78
|
+
"""Test report output format conversions."""
|
|
79
|
+
|
|
80
|
+
def test_to_markdown_daily(self, populated_db):
|
|
81
|
+
gen = ReportGenerator(db=populated_db)
|
|
82
|
+
data = gen.daily_report(target_date="2026-04-01")
|
|
83
|
+
md = gen.to_markdown(data, "daily")
|
|
84
|
+
|
|
85
|
+
assert "# Daily Report" in md
|
|
86
|
+
assert "2026-04-01" in md
|
|
87
|
+
assert "## Metrics" in md
|
|
88
|
+
|
|
89
|
+
def test_to_markdown_weekly(self, populated_db):
|
|
90
|
+
gen = ReportGenerator(db=populated_db)
|
|
91
|
+
data = gen.weekly_report(week_start="2026-03-30")
|
|
92
|
+
md = gen.to_markdown(data, "weekly")
|
|
93
|
+
|
|
94
|
+
assert "# Weekly Report" in md
|
|
95
|
+
assert "Top Contributors" in md
|
|
96
|
+
|
|
97
|
+
def test_to_markdown_monthly(self, populated_db):
|
|
98
|
+
gen = ReportGenerator(db=populated_db)
|
|
99
|
+
data = gen.monthly_report(year=2026, month=4)
|
|
100
|
+
md = gen.to_markdown(data, "monthly")
|
|
101
|
+
|
|
102
|
+
assert "# Monthly Report" in md
|
|
103
|
+
assert "Team Health Score" in md
|
|
104
|
+
|
|
105
|
+
def test_to_json(self, populated_db):
|
|
106
|
+
gen = ReportGenerator(db=populated_db)
|
|
107
|
+
data = gen.daily_report(target_date="2026-04-01")
|
|
108
|
+
j = gen.to_json(data)
|
|
109
|
+
|
|
110
|
+
parsed = json.loads(j)
|
|
111
|
+
assert parsed["date"] == "2026-04-01"
|
|
112
|
+
|
|
113
|
+
def test_to_html(self, populated_db):
|
|
114
|
+
gen = ReportGenerator(db=populated_db)
|
|
115
|
+
data = gen.daily_report(target_date="2026-04-01")
|
|
116
|
+
html = gen.to_html(data, "daily")
|
|
117
|
+
|
|
118
|
+
assert "<!DOCTYPE html>" in html
|
|
119
|
+
assert "DevPulse" in html
|
|
120
|
+
assert "<table>" in html or "<h1>" in html
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TestMdToHtml:
|
|
124
|
+
"""Test the markdown-to-HTML converter."""
|
|
125
|
+
|
|
126
|
+
def test_headers(self):
|
|
127
|
+
html = _md_to_html("# Title\n## Subtitle")
|
|
128
|
+
assert "<h1>Title</h1>" in html
|
|
129
|
+
assert "<h2>Subtitle</h2>" in html
|
|
130
|
+
|
|
131
|
+
def test_list_items(self):
|
|
132
|
+
html = _md_to_html("- Item 1\n- Item 2")
|
|
133
|
+
assert "<ul>" in html
|
|
134
|
+
assert "<li>" in html
|
|
135
|
+
assert "Item 1" in html
|
|
136
|
+
|
|
137
|
+
def test_bold_text(self):
|
|
138
|
+
html = _md_to_html("- **Bold** text")
|
|
139
|
+
assert "<strong>Bold</strong>" in html
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class TestSaveReport:
|
|
143
|
+
"""Test report file saving."""
|
|
144
|
+
|
|
145
|
+
def test_save_markdown_report(self, populated_db):
|
|
146
|
+
gen = ReportGenerator(db=populated_db)
|
|
147
|
+
data = gen.daily_report(target_date="2026-04-01")
|
|
148
|
+
path = gen.save_report(data, "daily", "markdown")
|
|
149
|
+
|
|
150
|
+
assert os.path.exists(path)
|
|
151
|
+
assert path.endswith(".md")
|
|
152
|
+
with open(path) as f:
|
|
153
|
+
content = f.read()
|
|
154
|
+
assert "Daily Report" in content
|
|
155
|
+
|
|
156
|
+
def test_save_json_report(self, populated_db):
|
|
157
|
+
gen = ReportGenerator(db=populated_db)
|
|
158
|
+
data = gen.daily_report(target_date="2026-04-01")
|
|
159
|
+
path = gen.save_report(data, "daily", "json")
|
|
160
|
+
|
|
161
|
+
assert os.path.exists(path)
|
|
162
|
+
assert path.endswith(".json")
|
|
163
|
+
|
|
164
|
+
def test_save_html_report(self, populated_db):
|
|
165
|
+
gen = ReportGenerator(db=populated_db)
|
|
166
|
+
data = gen.daily_report(target_date="2026-04-01")
|
|
167
|
+
path = gen.save_report(data, "daily", "html")
|
|
168
|
+
|
|
169
|
+
assert os.path.exists(path)
|
|
170
|
+
assert path.endswith(".html")
|
|
171
|
+
with open(path) as f:
|
|
172
|
+
content = f.read()
|
|
173
|
+
assert "<!DOCTYPE html>" in content
|