@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,208 @@
|
|
|
1
|
+
"""Shared test fixtures for DevPulse tests."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from devpulse.core.database import Database
|
|
8
|
+
from devpulse.core.config import reset_settings
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture(autouse=True)
|
|
12
|
+
def isolated_environment(tmp_path, monkeypatch):
|
|
13
|
+
"""Ensure each test uses an isolated database and config."""
|
|
14
|
+
db_path = str(tmp_path / "test_devpulse.db")
|
|
15
|
+
reports_dir = str(tmp_path / "reports")
|
|
16
|
+
export_dir = str(tmp_path / "exports")
|
|
17
|
+
|
|
18
|
+
monkeypatch.setenv("DEVPULSE_DATABASE_PATH", db_path)
|
|
19
|
+
monkeypatch.setenv("DEVPULSE_REPORTS_DIR", reports_dir)
|
|
20
|
+
monkeypatch.setenv("DEVPULSE_EXPORT_DIR", export_dir)
|
|
21
|
+
monkeypatch.setenv("DEVPULSE_GITHUB_TOKEN", "test_token_12345")
|
|
22
|
+
monkeypatch.setenv("DEVPULSE_GITHUB_USERNAME", "testuser")
|
|
23
|
+
|
|
24
|
+
reset_settings()
|
|
25
|
+
yield
|
|
26
|
+
reset_settings()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def db(isolated_environment) -> Database:
|
|
31
|
+
"""Provide a fresh database instance."""
|
|
32
|
+
from devpulse.core.config import get_settings
|
|
33
|
+
settings = get_settings()
|
|
34
|
+
return Database(db_path=settings.database_path)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.fixture
|
|
38
|
+
def sample_commits():
|
|
39
|
+
"""Sample commit data for testing."""
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
"sha": "abc1234567890",
|
|
43
|
+
"repo": "testorg/testrepo",
|
|
44
|
+
"author": "Alice",
|
|
45
|
+
"author_date": "2026-04-01T10:00:00Z",
|
|
46
|
+
"message": "Add new feature",
|
|
47
|
+
"additions": 50,
|
|
48
|
+
"deletions": 10,
|
|
49
|
+
"url": "https://github.com/testorg/testrepo/commit/abc1234567890",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"sha": "def1234567890",
|
|
53
|
+
"repo": "testorg/testrepo",
|
|
54
|
+
"author": "Alice",
|
|
55
|
+
"author_date": "2026-04-01T14:00:00Z",
|
|
56
|
+
"message": "Fix bug in feature",
|
|
57
|
+
"additions": 20,
|
|
58
|
+
"deletions": 5,
|
|
59
|
+
"url": "https://github.com/testorg/testrepo/commit/def1234567890",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"sha": "ghi1234567890",
|
|
63
|
+
"repo": "testorg/testrepo",
|
|
64
|
+
"author": "Bob",
|
|
65
|
+
"author_date": "2026-04-02T09:00:00Z",
|
|
66
|
+
"message": "Update docs",
|
|
67
|
+
"additions": 30,
|
|
68
|
+
"deletions": 0,
|
|
69
|
+
"url": "https://github.com/testorg/testrepo/commit/ghi1234567890",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"sha": "jkl1234567890",
|
|
73
|
+
"repo": "testorg/testrepo",
|
|
74
|
+
"author": "Alice",
|
|
75
|
+
"author_date": "2026-04-03T11:00:00Z",
|
|
76
|
+
"message": "Refactor module",
|
|
77
|
+
"additions": 100,
|
|
78
|
+
"deletions": 80,
|
|
79
|
+
"url": "https://github.com/testorg/testrepo/commit/jkl1234567890",
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@pytest.fixture
|
|
85
|
+
def sample_prs():
|
|
86
|
+
"""Sample pull request data for testing."""
|
|
87
|
+
return [
|
|
88
|
+
{
|
|
89
|
+
"number": 1,
|
|
90
|
+
"repo": "testorg/testrepo",
|
|
91
|
+
"title": "Add new feature",
|
|
92
|
+
"author": "Alice",
|
|
93
|
+
"state": "merged",
|
|
94
|
+
"created_at": "2026-04-01T10:00:00Z",
|
|
95
|
+
"merged_at": "2026-04-02T10:00:00Z",
|
|
96
|
+
"closed_at": "2026-04-02T10:00:00Z",
|
|
97
|
+
"additions": 200,
|
|
98
|
+
"deletions": 50,
|
|
99
|
+
"changed_files": 5,
|
|
100
|
+
"review_comments": 3,
|
|
101
|
+
"url": "https://github.com/testorg/testrepo/pull/1",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"number": 2,
|
|
105
|
+
"repo": "testorg/testrepo",
|
|
106
|
+
"title": "Fix critical bug",
|
|
107
|
+
"author": "Bob",
|
|
108
|
+
"state": "merged",
|
|
109
|
+
"created_at": "2026-04-01T08:00:00Z",
|
|
110
|
+
"merged_at": "2026-04-01T20:00:00Z",
|
|
111
|
+
"closed_at": "2026-04-01T20:00:00Z",
|
|
112
|
+
"additions": 30,
|
|
113
|
+
"deletions": 10,
|
|
114
|
+
"changed_files": 2,
|
|
115
|
+
"review_comments": 5,
|
|
116
|
+
"url": "https://github.com/testorg/testrepo/pull/2",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"number": 3,
|
|
120
|
+
"repo": "testorg/testrepo",
|
|
121
|
+
"title": "WIP: big refactor",
|
|
122
|
+
"author": "Alice",
|
|
123
|
+
"state": "open",
|
|
124
|
+
"created_at": "2026-04-03T09:00:00Z",
|
|
125
|
+
"merged_at": None,
|
|
126
|
+
"closed_at": None,
|
|
127
|
+
"additions": 500,
|
|
128
|
+
"deletions": 300,
|
|
129
|
+
"changed_files": 15,
|
|
130
|
+
"review_comments": 0,
|
|
131
|
+
"url": "https://github.com/testorg/testrepo/pull/3",
|
|
132
|
+
},
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@pytest.fixture
|
|
137
|
+
def sample_issues():
|
|
138
|
+
"""Sample issue data for testing."""
|
|
139
|
+
return [
|
|
140
|
+
{
|
|
141
|
+
"number": 10,
|
|
142
|
+
"repo": "testorg/testrepo",
|
|
143
|
+
"title": "Bug: crash on startup",
|
|
144
|
+
"author": "Bob",
|
|
145
|
+
"state": "closed",
|
|
146
|
+
"labels": ["bug"],
|
|
147
|
+
"created_at": "2026-03-25T10:00:00Z",
|
|
148
|
+
"closed_at": "2026-04-01T14:00:00Z",
|
|
149
|
+
"url": "https://github.com/testorg/testrepo/issues/10",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"number": 11,
|
|
153
|
+
"repo": "testorg/testrepo",
|
|
154
|
+
"title": "Feature: add export",
|
|
155
|
+
"author": "Alice",
|
|
156
|
+
"state": "open",
|
|
157
|
+
"labels": ["enhancement"],
|
|
158
|
+
"created_at": "2026-04-02T08:00:00Z",
|
|
159
|
+
"closed_at": None,
|
|
160
|
+
"url": "https://github.com/testorg/testrepo/issues/11",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"number": 12,
|
|
164
|
+
"repo": "testorg/testrepo",
|
|
165
|
+
"title": "Bug: memory leak",
|
|
166
|
+
"author": "Charlie",
|
|
167
|
+
"state": "open",
|
|
168
|
+
"labels": ["bug", "priority:high"],
|
|
169
|
+
"created_at": "2026-04-03T15:00:00Z",
|
|
170
|
+
"closed_at": None,
|
|
171
|
+
"url": "https://github.com/testorg/testrepo/issues/12",
|
|
172
|
+
},
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@pytest.fixture
|
|
177
|
+
def sample_reviews():
|
|
178
|
+
"""Sample review data for testing."""
|
|
179
|
+
return [
|
|
180
|
+
{
|
|
181
|
+
"id": 100,
|
|
182
|
+
"repo": "testorg/testrepo",
|
|
183
|
+
"pr_number": 1,
|
|
184
|
+
"author": "Bob",
|
|
185
|
+
"state": "APPROVED",
|
|
186
|
+
"submitted_at": "2026-04-01T18:00:00Z",
|
|
187
|
+
"body": "Looks good!",
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"id": 101,
|
|
191
|
+
"repo": "testorg/testrepo",
|
|
192
|
+
"pr_number": 2,
|
|
193
|
+
"author": "Alice",
|
|
194
|
+
"state": "CHANGES_REQUESTED",
|
|
195
|
+
"submitted_at": "2026-04-01T12:00:00Z",
|
|
196
|
+
"body": "Please add tests.",
|
|
197
|
+
},
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@pytest.fixture
|
|
202
|
+
def populated_db(db, sample_commits, sample_prs, sample_issues, sample_reviews):
|
|
203
|
+
"""Database pre-populated with sample data."""
|
|
204
|
+
db.upsert_commits(sample_commits)
|
|
205
|
+
db.upsert_pull_requests(sample_prs)
|
|
206
|
+
db.upsert_issues(sample_issues)
|
|
207
|
+
db.upsert_reviews(sample_reviews)
|
|
208
|
+
return db
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Tests for the AnalyticsEngine."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from devpulse.core.analytics import AnalyticsEngine, _parse_date, _days_between
|
|
6
|
+
from devpulse.core.models import DeveloperMetrics, TeamHealth
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestParseDate:
|
|
10
|
+
"""Test date parsing utility."""
|
|
11
|
+
|
|
12
|
+
def test_iso_with_tz(self):
|
|
13
|
+
dt = _parse_date("2026-04-01T10:00:00Z")
|
|
14
|
+
assert dt is not None
|
|
15
|
+
assert dt.year == 2026
|
|
16
|
+
assert dt.month == 4
|
|
17
|
+
assert dt.day == 1
|
|
18
|
+
|
|
19
|
+
def test_iso_without_tz(self):
|
|
20
|
+
dt = _parse_date("2026-04-01T10:00:00")
|
|
21
|
+
assert dt is not None
|
|
22
|
+
|
|
23
|
+
def test_date_only(self):
|
|
24
|
+
dt = _parse_date("2026-04-01")
|
|
25
|
+
assert dt is not None
|
|
26
|
+
|
|
27
|
+
def test_empty_string(self):
|
|
28
|
+
assert _parse_date("") is None
|
|
29
|
+
|
|
30
|
+
def test_none(self):
|
|
31
|
+
assert _parse_date(None) is None
|
|
32
|
+
|
|
33
|
+
def test_invalid_format(self):
|
|
34
|
+
assert _parse_date("not-a-date") is None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TestDaysBetween:
|
|
38
|
+
"""Test time difference computation."""
|
|
39
|
+
|
|
40
|
+
def test_same_day(self):
|
|
41
|
+
hours = _days_between("2026-04-01T10:00:00Z", "2026-04-01T14:00:00Z")
|
|
42
|
+
assert hours == 4.0
|
|
43
|
+
|
|
44
|
+
def test_24_hours(self):
|
|
45
|
+
hours = _days_between("2026-04-01T00:00:00Z", "2026-04-02T00:00:00Z")
|
|
46
|
+
assert hours == 24.0
|
|
47
|
+
|
|
48
|
+
def test_empty_start(self):
|
|
49
|
+
hours = _days_between("", "2026-04-01T10:00:00Z")
|
|
50
|
+
assert hours == 0.0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestDeveloperMetrics:
|
|
54
|
+
"""Test developer metrics computation."""
|
|
55
|
+
|
|
56
|
+
def test_basic_metrics(self, populated_db):
|
|
57
|
+
engine = AnalyticsEngine(db=populated_db)
|
|
58
|
+
metrics = engine.developer_metrics(author="Alice", days=30)
|
|
59
|
+
|
|
60
|
+
assert isinstance(metrics, DeveloperMetrics)
|
|
61
|
+
assert metrics.author == "Alice"
|
|
62
|
+
assert metrics.commits_count == 3
|
|
63
|
+
assert metrics.prs_created == 2
|
|
64
|
+
assert metrics.prs_merged == 1 # Only PR #1 is merged and authored by Alice
|
|
65
|
+
|
|
66
|
+
def test_bob_metrics(self, populated_db):
|
|
67
|
+
engine = AnalyticsEngine(db=populated_db)
|
|
68
|
+
metrics = engine.developer_metrics(author="Bob", days=30)
|
|
69
|
+
|
|
70
|
+
assert metrics.author == "Bob"
|
|
71
|
+
assert metrics.commits_count == 1
|
|
72
|
+
assert metrics.prs_created == 1
|
|
73
|
+
|
|
74
|
+
def test_unknown_developer(self, populated_db):
|
|
75
|
+
engine = AnalyticsEngine(db=populated_db)
|
|
76
|
+
metrics = engine.developer_metrics(author="Nobody", days=30)
|
|
77
|
+
|
|
78
|
+
assert metrics.commits_count == 0
|
|
79
|
+
assert metrics.author == "Nobody"
|
|
80
|
+
|
|
81
|
+
def test_active_days(self, populated_db):
|
|
82
|
+
engine = AnalyticsEngine(db=populated_db)
|
|
83
|
+
metrics = engine.developer_metrics(author="Alice", days=30)
|
|
84
|
+
|
|
85
|
+
assert metrics.active_days == 2 # April 1 and April 3
|
|
86
|
+
|
|
87
|
+
def test_commits_per_day(self, populated_db):
|
|
88
|
+
engine = AnalyticsEngine(db=populated_db)
|
|
89
|
+
metrics = engine.developer_metrics(author="Alice", days=30)
|
|
90
|
+
|
|
91
|
+
assert metrics.commits_per_day > 0
|
|
92
|
+
assert metrics.period_days == 30
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TestTeamMetrics:
|
|
96
|
+
"""Test team-wide metrics."""
|
|
97
|
+
|
|
98
|
+
def test_team_metrics_list(self, populated_db):
|
|
99
|
+
engine = AnalyticsEngine(db=populated_db)
|
|
100
|
+
team = engine.team_metrics(days=30)
|
|
101
|
+
|
|
102
|
+
assert len(team) >= 2 # At least Alice and Bob
|
|
103
|
+
authors = {m.author for m in team}
|
|
104
|
+
assert "Alice" in authors
|
|
105
|
+
assert "Bob" in authors
|
|
106
|
+
|
|
107
|
+
def test_empty_team(self, db):
|
|
108
|
+
engine = AnalyticsEngine(db=db)
|
|
109
|
+
team = engine.team_metrics(days=30)
|
|
110
|
+
assert team == []
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class TestTeamHealth:
|
|
114
|
+
"""Test team health analysis."""
|
|
115
|
+
|
|
116
|
+
def test_team_health_with_data(self, populated_db):
|
|
117
|
+
engine = AnalyticsEngine(db=populated_db)
|
|
118
|
+
health = engine.team_health(days=30)
|
|
119
|
+
|
|
120
|
+
assert isinstance(health, TeamHealth)
|
|
121
|
+
assert 0 <= health.overall_score <= 100
|
|
122
|
+
assert 0 <= health.workload_balance <= 1
|
|
123
|
+
assert 0 <= health.collaboration_score <= 1
|
|
124
|
+
assert health.velocity_trend in ("growing", "stable", "declining")
|
|
125
|
+
assert len(health.recommendations) > 0
|
|
126
|
+
|
|
127
|
+
def test_team_health_empty(self, db):
|
|
128
|
+
engine = AnalyticsEngine(db=db)
|
|
129
|
+
health = engine.team_health(days=30)
|
|
130
|
+
|
|
131
|
+
assert health.overall_score == 0
|
|
132
|
+
assert len(health.recommendations) > 0 # Should have "no data" recommendation
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class TestSprintAnalytics:
|
|
136
|
+
"""Test sprint analytics."""
|
|
137
|
+
|
|
138
|
+
def test_sprint_burndown(self, db):
|
|
139
|
+
# Record multiple snapshots
|
|
140
|
+
for i, completed in enumerate([0, 5, 10, 15, 18]):
|
|
141
|
+
db.save_sprint_snapshot({
|
|
142
|
+
"sprint_name": "Sprint 1",
|
|
143
|
+
"total_points": 20,
|
|
144
|
+
"completed_points": completed,
|
|
145
|
+
"remaining_points": 20 - completed,
|
|
146
|
+
"added_points": 0,
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
engine = AnalyticsEngine(db=db)
|
|
150
|
+
data = engine.sprint_burndown("Sprint 1")
|
|
151
|
+
assert len(data) == 5
|
|
152
|
+
assert data[0]["remaining"] == 20
|
|
153
|
+
assert data[-1]["remaining"] == 2
|
|
154
|
+
|
|
155
|
+
def test_sprint_burndown_nonexistent(self, populated_db):
|
|
156
|
+
engine = AnalyticsEngine(db=populated_db)
|
|
157
|
+
data = engine.sprint_burndown("Nonexistent Sprint")
|
|
158
|
+
assert data == []
|
|
159
|
+
|
|
160
|
+
def test_predict_sprint_completion(self, db):
|
|
161
|
+
for i, completed in enumerate([0, 5, 10, 15]):
|
|
162
|
+
db.save_sprint_snapshot({
|
|
163
|
+
"sprint_name": "Sprint X",
|
|
164
|
+
"total_points": 20,
|
|
165
|
+
"completed_points": completed,
|
|
166
|
+
"remaining_points": 20 - completed,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
engine = AnalyticsEngine(db=db)
|
|
170
|
+
result = engine.predict_sprint_completion("Sprint X", total_points=20, days_elapsed=4, total_days=10)
|
|
171
|
+
assert result["prediction"] in ("on_track", "at_risk", "unknown")
|
|
172
|
+
assert "message" in result
|
|
173
|
+
|
|
174
|
+
def test_predict_nonexistent_sprint(self, populated_db):
|
|
175
|
+
engine = AnalyticsEngine(db=populated_db)
|
|
176
|
+
result = engine.predict_sprint_completion("Ghost Sprint", 20, 5, 10)
|
|
177
|
+
assert result["prediction"] == "unknown"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class TestCodeQuality:
|
|
181
|
+
"""Test code quality analytics."""
|
|
182
|
+
|
|
183
|
+
def test_quality_trend(self, db):
|
|
184
|
+
db.save_quality_snapshot({
|
|
185
|
+
"repo": "testorg/testrepo",
|
|
186
|
+
"test_coverage": 0.75,
|
|
187
|
+
"open_bugs": 5,
|
|
188
|
+
"tech_debt_score": 3.5,
|
|
189
|
+
})
|
|
190
|
+
db.save_quality_snapshot({
|
|
191
|
+
"repo": "testorg/testrepo",
|
|
192
|
+
"test_coverage": 0.80,
|
|
193
|
+
"open_bugs": 3,
|
|
194
|
+
"tech_debt_score": 3.0,
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
engine = AnalyticsEngine(db=db)
|
|
198
|
+
trends = engine.code_quality_trend(repo="testorg/testrepo")
|
|
199
|
+
assert len(trends) == 2
|
|
200
|
+
assert trends[0].test_coverage == 0.75
|
|
201
|
+
assert trends[1].test_coverage == 0.80
|
|
202
|
+
|
|
203
|
+
def test_compute_quality_score(self, populated_db):
|
|
204
|
+
engine = AnalyticsEngine(db=populated_db)
|
|
205
|
+
score = engine.compute_quality_score(repo="testorg/testrepo", days=30)
|
|
206
|
+
|
|
207
|
+
assert score["repo"] == "testorg/testrepo"
|
|
208
|
+
assert score["total_commits"] == 4
|
|
209
|
+
assert score["total_prs"] == 3
|
|
210
|
+
assert isinstance(score["tech_debt_score"], float)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class TestInsights:
|
|
214
|
+
"""Test AI insight generation."""
|
|
215
|
+
|
|
216
|
+
def test_generate_insights_with_data(self, populated_db):
|
|
217
|
+
engine = AnalyticsEngine(db=populated_db)
|
|
218
|
+
insights = engine.generate_insights(days=30)
|
|
219
|
+
|
|
220
|
+
assert isinstance(insights, list)
|
|
221
|
+
assert len(insights) > 0
|
|
222
|
+
for insight in insights:
|
|
223
|
+
assert "type" in insight
|
|
224
|
+
assert "severity" in insight
|
|
225
|
+
assert "message" in insight
|
|
226
|
+
assert "recommendation" in insight
|
|
227
|
+
|
|
228
|
+
def test_generate_insights_empty(self, db):
|
|
229
|
+
engine = AnalyticsEngine(db=db)
|
|
230
|
+
insights = engine.generate_insights(days=30)
|
|
231
|
+
|
|
232
|
+
assert len(insights) > 0
|
|
233
|
+
# Should have a "no data" or default insight
|
|
234
|
+
|
|
235
|
+
def test_insights_include_merge_time(self, populated_db):
|
|
236
|
+
engine = AnalyticsEngine(db=populated_db)
|
|
237
|
+
insights = engine.generate_insights(days=30, repo="testorg/testrepo")
|
|
238
|
+
|
|
239
|
+
messages = " ".join(i["message"] for i in insights)
|
|
240
|
+
assert "merge time" in messages.lower() or "healthy" in messages.lower()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class TestGoalCoaching:
|
|
244
|
+
"""Test goal coaching."""
|
|
245
|
+
|
|
246
|
+
def test_coaching_active_goals(self, db):
|
|
247
|
+
db.upsert_goal({
|
|
248
|
+
"title": "Commits per day",
|
|
249
|
+
"target_value": 10,
|
|
250
|
+
"current_value": 7,
|
|
251
|
+
"metric": "commits_per_day",
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
engine = AnalyticsEngine(db=db)
|
|
255
|
+
coaching = engine.goal_coaching()
|
|
256
|
+
|
|
257
|
+
assert len(coaching) == 1
|
|
258
|
+
assert coaching[0]["progress_pct"] == 70.0
|
|
259
|
+
assert coaching[0]["status"] in ("on_track", "at_risk", "achieved")
|
|
260
|
+
|
|
261
|
+
def test_coaching_achieved_goal(self, db):
|
|
262
|
+
db.upsert_goal({
|
|
263
|
+
"title": "Write code",
|
|
264
|
+
"target_value": 1,
|
|
265
|
+
"current_value": 5,
|
|
266
|
+
"metric": "commits",
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
engine = AnalyticsEngine(db=db)
|
|
270
|
+
coaching = engine.goal_coaching()
|
|
271
|
+
|
|
272
|
+
assert coaching[0]["status"] == "achieved"
|
|
273
|
+
|
|
274
|
+
def test_coaching_specific_goal(self, db):
|
|
275
|
+
gid = db.upsert_goal({
|
|
276
|
+
"title": "Reviews",
|
|
277
|
+
"target_value": 5,
|
|
278
|
+
"current_value": 1,
|
|
279
|
+
"metric": "reviews",
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
engine = AnalyticsEngine(db=db)
|
|
283
|
+
coaching = engine.goal_coaching(goal_id=gid)
|
|
284
|
+
assert len(coaching) == 1
|