@theihtisham/dev-pulse 1.0.0 → 1.1.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/.editorconfig +12 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +43 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +33 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +18 -0
- package/.github/dependabot.yml +16 -0
- package/.github/workflows/ci.yml +33 -0
- package/CODE_OF_CONDUCT.md +27 -0
- package/Dockerfile +8 -0
- package/LICENSE +21 -21
- package/README.md +135 -39
- package/SECURITY.md +22 -0
- package/devpulse/__init__.py +4 -4
- package/devpulse/api/__init__.py +1 -1
- package/devpulse/api/app.py +371 -371
- package/devpulse/cli/__init__.py +1 -1
- package/devpulse/cli/dashboard.py +131 -131
- package/devpulse/cli/main.py +678 -678
- package/devpulse/cli/render.py +175 -175
- package/devpulse/core/__init__.py +34 -34
- package/devpulse/core/analytics.py +487 -487
- package/devpulse/core/config.py +77 -77
- package/devpulse/core/database.py +612 -612
- package/devpulse/core/github_client.py +281 -281
- package/devpulse/core/models.py +142 -142
- package/devpulse/core/report_generator.py +454 -454
- package/devpulse/static/.gitkeep +1 -1
- package/devpulse/templates/report.html +64 -64
- package/package.json +35 -35
- package/pyproject.toml +80 -80
- package/requirements.txt +14 -14
- package/tests/__init__.py +1 -1
- package/tests/conftest.py +208 -208
- package/tests/test_analytics.py +284 -284
- package/tests/test_api.py +313 -313
- package/tests/test_cli.py +204 -204
- package/tests/test_config.py +47 -47
- package/tests/test_database.py +255 -255
- package/tests/test_models.py +107 -107
- package/tests/test_report_generator.py +173 -173
- package/jest.config.js +0 -7
package/tests/test_models.py
CHANGED
|
@@ -1,107 +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 == []
|
|
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 == []
|
|
@@ -1,173 +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
|
|
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
|