arthexis 0.1.6__py3-none-any.whl → 0.1.7__py3-none-any.whl
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.
Potentially problematic release.
This version of arthexis might be problematic. Click here for more details.
- {arthexis-0.1.6.dist-info → arthexis-0.1.7.dist-info}/METADATA +8 -8
- {arthexis-0.1.6.dist-info → arthexis-0.1.7.dist-info}/RECORD +32 -28
- config/celery.py +7 -0
- config/horologia_app.py +7 -0
- config/logging.py +8 -3
- config/settings.py +2 -2
- config/workgroup_app.py +7 -0
- core/admin.py +55 -11
- core/apps.py +2 -1
- core/checks.py +29 -0
- core/entity.py +29 -7
- core/models.py +49 -9
- core/release.py +29 -141
- core/system.py +2 -2
- core/test_system_info.py +21 -0
- core/tests.py +200 -1
- core/views.py +153 -134
- nodes/admin.py +211 -0
- nodes/apps.py +1 -1
- nodes/models.py +103 -7
- nodes/tests.py +27 -0
- ocpp/apps.py +1 -1
- ocpp/models.py +1 -1
- ocpp/simulator.py +4 -0
- ocpp/tests.py +5 -1
- pages/admin.py +8 -3
- pages/apps.py +1 -1
- pages/tests.py +23 -4
- pages/views.py +22 -3
- {arthexis-0.1.6.dist-info → arthexis-0.1.7.dist-info}/WHEEL +0 -0
- {arthexis-0.1.6.dist-info → arthexis-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.6.dist-info → arthexis-0.1.7.dist-info}/top_level.txt +0 -0
core/release.py
CHANGED
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import os
|
|
4
4
|
import subprocess
|
|
5
5
|
import sys
|
|
6
|
+
import shutil
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import Optional
|
|
@@ -82,20 +83,12 @@ def _git_clean() -> bool:
|
|
|
82
83
|
return not proc.stdout.strip()
|
|
83
84
|
|
|
84
85
|
|
|
85
|
-
def
|
|
86
|
-
|
|
86
|
+
def _git_has_staged_changes() -> bool:
|
|
87
|
+
"""Return True if there are staged changes ready to commit."""
|
|
88
|
+
proc = subprocess.run(["git", "diff", "--cached", "--quiet"])
|
|
89
|
+
return proc.returncode != 0
|
|
87
90
|
|
|
88
91
|
|
|
89
|
-
def _current_branch() -> str:
|
|
90
|
-
return (
|
|
91
|
-
subprocess.check_output([
|
|
92
|
-
"git",
|
|
93
|
-
"rev-parse",
|
|
94
|
-
"--abbrev-ref",
|
|
95
|
-
"HEAD",
|
|
96
|
-
]).decode().strip()
|
|
97
|
-
)
|
|
98
|
-
|
|
99
92
|
|
|
100
93
|
def _manager_credentials() -> Optional[Credentials]:
|
|
101
94
|
"""Return credentials from the Package's release manager if available."""
|
|
@@ -174,69 +167,6 @@ def _write_pyproject(package: Package, version: str, requirements: list[str]) ->
|
|
|
174
167
|
Path("pyproject.toml").write_text(_dump_toml(content), encoding="utf-8")
|
|
175
168
|
|
|
176
169
|
|
|
177
|
-
def _ensure_changelog() -> str:
|
|
178
|
-
header = "Changelog\n=========\n\n"
|
|
179
|
-
path = Path("CHANGELOG.rst")
|
|
180
|
-
text = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
181
|
-
if not text.startswith("Changelog"):
|
|
182
|
-
text = header + text
|
|
183
|
-
if "Unreleased" not in text:
|
|
184
|
-
text = text[: len(header)] + "Unreleased\n----------\n\n" + text[len(header):]
|
|
185
|
-
return text
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
def _pop_unreleased(text: str) -> tuple[str, str]:
|
|
189
|
-
lines = text.splitlines()
|
|
190
|
-
try:
|
|
191
|
-
idx = lines.index("Unreleased")
|
|
192
|
-
except ValueError:
|
|
193
|
-
return "", text
|
|
194
|
-
body = []
|
|
195
|
-
i = idx + 2
|
|
196
|
-
while i < len(lines) and lines[i].startswith("- "):
|
|
197
|
-
body.append(lines[i])
|
|
198
|
-
i += 1
|
|
199
|
-
if i < len(lines) and lines[i] == "":
|
|
200
|
-
i += 1
|
|
201
|
-
new_lines = lines[:idx] + lines[i:]
|
|
202
|
-
return "\n".join(body), "\n".join(new_lines) + ("\n" if text.endswith("\n") else "")
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
def _last_changelog_revision() -> Optional[str]:
|
|
206
|
-
path = Path("CHANGELOG.rst")
|
|
207
|
-
if not path.exists():
|
|
208
|
-
return None
|
|
209
|
-
for line in path.read_text(encoding="utf-8").splitlines():
|
|
210
|
-
if "[revision" in line:
|
|
211
|
-
try:
|
|
212
|
-
return line.split("[revision", 1)[1].split("]", 1)[0].strip()
|
|
213
|
-
except Exception:
|
|
214
|
-
return None
|
|
215
|
-
return None
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
def update_changelog(version: str, revision: str, prev_revision: Optional[str] = None) -> None:
|
|
219
|
-
text = _ensure_changelog()
|
|
220
|
-
body, text = _pop_unreleased(text)
|
|
221
|
-
if not body:
|
|
222
|
-
prev_revision = prev_revision or _last_changelog_revision()
|
|
223
|
-
log_range = f"{prev_revision}..HEAD" if prev_revision else "HEAD"
|
|
224
|
-
proc = subprocess.run(
|
|
225
|
-
["git", "log", "--pretty=%h %s", "--no-merges", log_range],
|
|
226
|
-
capture_output=True,
|
|
227
|
-
text=True,
|
|
228
|
-
check=False,
|
|
229
|
-
)
|
|
230
|
-
body = "\n".join(
|
|
231
|
-
f"- {l.strip()}" for l in proc.stdout.splitlines() if l.strip()
|
|
232
|
-
)
|
|
233
|
-
header = f"{version} [revision {revision}]"
|
|
234
|
-
underline = "-" * len(header)
|
|
235
|
-
entry = "\n".join([header, underline, "", body, ""]).rstrip() + "\n\n"
|
|
236
|
-
base_header = "Changelog\n=========\n\n"
|
|
237
|
-
remaining = text[len(base_header):]
|
|
238
|
-
new_text = base_header + "Unreleased\n----------\n\n" + entry + remaining
|
|
239
|
-
Path("CHANGELOG.rst").write_text(new_text, encoding="utf-8")
|
|
240
170
|
|
|
241
171
|
|
|
242
172
|
@requires_network
|
|
@@ -268,8 +198,8 @@ def build(
|
|
|
268
198
|
"Git repository is not clean. Commit, stash, or enable auto stash before building."
|
|
269
199
|
)
|
|
270
200
|
|
|
201
|
+
version_path = Path("VERSION")
|
|
271
202
|
if version is None:
|
|
272
|
-
version_path = Path("VERSION")
|
|
273
203
|
if not version_path.exists():
|
|
274
204
|
raise ReleaseError("VERSION file not found")
|
|
275
205
|
version = version_path.read_text().strip()
|
|
@@ -278,6 +208,9 @@ def build(
|
|
|
278
208
|
patch += 1
|
|
279
209
|
version = f"{major}.{minor}.{patch}"
|
|
280
210
|
version_path.write_text(version + "\n")
|
|
211
|
+
else:
|
|
212
|
+
# Ensure the VERSION file reflects the provided release version
|
|
213
|
+
version_path.write_text(version + "\n")
|
|
281
214
|
|
|
282
215
|
requirements = [
|
|
283
216
|
line.strip()
|
|
@@ -291,16 +224,10 @@ def build(
|
|
|
291
224
|
if proc.returncode != 0:
|
|
292
225
|
raise TestsFailed(log_path, proc.stdout + proc.stderr)
|
|
293
226
|
|
|
294
|
-
commit_hash = _current_commit()
|
|
295
|
-
prev_revision = _last_changelog_revision()
|
|
296
|
-
update_changelog(version, commit_hash, prev_revision)
|
|
297
|
-
|
|
298
227
|
_write_pyproject(package, version, requirements)
|
|
299
228
|
if dist:
|
|
300
229
|
if Path("dist").exists():
|
|
301
|
-
|
|
302
|
-
p.unlink()
|
|
303
|
-
Path("dist").rmdir()
|
|
230
|
+
shutil.rmtree("dist")
|
|
304
231
|
try:
|
|
305
232
|
import build # type: ignore
|
|
306
233
|
except Exception:
|
|
@@ -308,10 +235,11 @@ def build(
|
|
|
308
235
|
_run([sys.executable, "-m", "build"])
|
|
309
236
|
|
|
310
237
|
if git:
|
|
311
|
-
files = ["VERSION", "pyproject.toml"
|
|
238
|
+
files = ["VERSION", "pyproject.toml"]
|
|
312
239
|
_run(["git", "add"] + files)
|
|
313
240
|
msg = f"PyPI Release v{version}" if twine else f"Release v{version}"
|
|
314
|
-
|
|
241
|
+
if _git_has_staged_changes():
|
|
242
|
+
_run(["git", "commit", "-m", msg])
|
|
315
243
|
_run(["git", "push"])
|
|
316
244
|
|
|
317
245
|
if tag:
|
|
@@ -359,63 +287,23 @@ def promote(
|
|
|
359
287
|
package: Package = DEFAULT_PACKAGE,
|
|
360
288
|
version: str,
|
|
361
289
|
creds: Optional[Credentials] = None,
|
|
362
|
-
) ->
|
|
363
|
-
"""
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
stashed = True
|
|
379
|
-
build(
|
|
380
|
-
package=package,
|
|
381
|
-
version=version,
|
|
382
|
-
creds=creds,
|
|
383
|
-
tests=False,
|
|
384
|
-
dist=True,
|
|
385
|
-
git=False,
|
|
386
|
-
tag=False,
|
|
387
|
-
stash=True,
|
|
388
|
-
)
|
|
389
|
-
try: # best effort
|
|
390
|
-
_run(
|
|
391
|
-
[
|
|
392
|
-
sys.executable,
|
|
393
|
-
"manage.py",
|
|
394
|
-
"squashmigrations",
|
|
395
|
-
"core",
|
|
396
|
-
"0001",
|
|
397
|
-
"--noinput",
|
|
398
|
-
],
|
|
399
|
-
check=False,
|
|
400
|
-
)
|
|
401
|
-
except Exception:
|
|
402
|
-
# The squashmigrations command may not be available or could fail
|
|
403
|
-
# (e.g. when no migrations exist). Any errors should not interrupt
|
|
404
|
-
# the release promotion flow.
|
|
405
|
-
pass
|
|
406
|
-
_run(["git", "add", "."]) # add all changes
|
|
290
|
+
) -> None:
|
|
291
|
+
"""Build the package and commit the release on the current branch."""
|
|
292
|
+
if not _git_clean():
|
|
293
|
+
raise ReleaseError("Git repository is not clean")
|
|
294
|
+
build(
|
|
295
|
+
package=package,
|
|
296
|
+
version=version,
|
|
297
|
+
creds=creds,
|
|
298
|
+
tests=False,
|
|
299
|
+
dist=True,
|
|
300
|
+
git=False,
|
|
301
|
+
tag=False,
|
|
302
|
+
stash=False,
|
|
303
|
+
)
|
|
304
|
+
_run(["git", "add", "."]) # add all changes
|
|
305
|
+
if _git_has_staged_changes():
|
|
407
306
|
_run(["git", "commit", "-m", f"Release v{version}"])
|
|
408
|
-
commit_hash = _current_commit()
|
|
409
|
-
release_name = f"{package.name}-{version}-{commit_hash[:7]}"
|
|
410
|
-
branch = f"release-{release_name}"
|
|
411
|
-
_run(["git", "branch", "-m", branch])
|
|
412
|
-
except Exception:
|
|
413
|
-
_run(["git", "checkout", current])
|
|
414
|
-
raise
|
|
415
|
-
finally:
|
|
416
|
-
if stashed:
|
|
417
|
-
_run(["git", "stash", "pop"], check=False)
|
|
418
|
-
return commit_hash, branch, current
|
|
419
307
|
|
|
420
308
|
|
|
421
309
|
def publish(
|
core/system.py
CHANGED
|
@@ -31,8 +31,8 @@ def _gather_info() -> dict:
|
|
|
31
31
|
info["mode"] = mode
|
|
32
32
|
info["port"] = 8000 if mode == "public" else 8888
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
info["role"] =
|
|
34
|
+
# Use settings.NODE_ROLE as the single source of truth for the node role.
|
|
35
|
+
info["role"] = getattr(settings, "NODE_ROLE", "Terminal")
|
|
36
36
|
|
|
37
37
|
info["features"] = {
|
|
38
38
|
"celery": (lock_dir / "celery.lck").exists(),
|
core/test_system_info.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
|
4
|
+
|
|
5
|
+
import django
|
|
6
|
+
django.setup()
|
|
7
|
+
|
|
8
|
+
from django.test import SimpleTestCase, override_settings
|
|
9
|
+
from core.system import _gather_info
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SystemInfoRoleTests(SimpleTestCase):
|
|
13
|
+
@override_settings(NODE_ROLE="Terminal")
|
|
14
|
+
def test_defaults_to_terminal(self):
|
|
15
|
+
info = _gather_info()
|
|
16
|
+
self.assertEqual(info["role"], "Terminal")
|
|
17
|
+
|
|
18
|
+
@override_settings(NODE_ROLE="Satellite")
|
|
19
|
+
def test_uses_settings_role(self):
|
|
20
|
+
info = _gather_info()
|
|
21
|
+
self.assertEqual(info["role"], "Satellite")
|
core/tests.py
CHANGED
|
@@ -4,11 +4,13 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
|
|
4
4
|
import django
|
|
5
5
|
django.setup()
|
|
6
6
|
|
|
7
|
-
from django.test import Client, TestCase
|
|
7
|
+
from django.test import Client, TestCase, RequestFactory
|
|
8
8
|
from django.urls import reverse
|
|
9
9
|
from django.http import HttpRequest
|
|
10
10
|
import json
|
|
11
11
|
from unittest import mock
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import subprocess
|
|
12
14
|
|
|
13
15
|
from django.utils import timezone
|
|
14
16
|
from .models import (
|
|
@@ -24,13 +26,18 @@ from .models import (
|
|
|
24
26
|
RFID,
|
|
25
27
|
FediverseProfile,
|
|
26
28
|
SecurityGroup,
|
|
29
|
+
Package,
|
|
30
|
+
PackageRelease,
|
|
27
31
|
)
|
|
32
|
+
from django.contrib.admin.sites import AdminSite
|
|
33
|
+
from core.admin import PackageReleaseAdmin
|
|
28
34
|
from ocpp.models import Transaction, Charger
|
|
29
35
|
|
|
30
36
|
from django.core.exceptions import ValidationError
|
|
31
37
|
from django.core.management import call_command
|
|
32
38
|
from django.db import IntegrityError
|
|
33
39
|
from .backends import LocalhostAdminBackend
|
|
40
|
+
from core.views import _step_check_pypi, _step_promote_build, _step_publish
|
|
34
41
|
|
|
35
42
|
|
|
36
43
|
class DefaultAdminTests(TestCase):
|
|
@@ -481,3 +488,195 @@ class FediverseProfileTests(TestCase):
|
|
|
481
488
|
profile.test_connection()
|
|
482
489
|
self.assertIsNone(profile.verified_on)
|
|
483
490
|
|
|
491
|
+
|
|
492
|
+
class ReleaseProcessTests(TestCase):
|
|
493
|
+
def setUp(self):
|
|
494
|
+
self.package = Package.objects.create(name="pkg")
|
|
495
|
+
self.release = PackageRelease.objects.create(
|
|
496
|
+
package=self.package, version="1.0.0"
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
@mock.patch("core.views.release_utils._git_clean", return_value=False)
|
|
500
|
+
def test_step_check_requires_clean_repo(self, git_clean):
|
|
501
|
+
with self.assertRaises(Exception):
|
|
502
|
+
_step_check_pypi(self.release, {}, Path("rel.log"))
|
|
503
|
+
|
|
504
|
+
@mock.patch("core.views.release_utils._git_clean", return_value=True)
|
|
505
|
+
@mock.patch("core.views.release_utils.network_available", return_value=False)
|
|
506
|
+
def test_step_check_keeps_repo_clean(self, network_available, git_clean):
|
|
507
|
+
version_path = Path("VERSION")
|
|
508
|
+
original = version_path.read_text(encoding="utf-8")
|
|
509
|
+
_step_check_pypi(self.release, {}, Path("rel.log"))
|
|
510
|
+
proc = subprocess.run(
|
|
511
|
+
["git", "status", "--porcelain", str(version_path)],
|
|
512
|
+
capture_output=True,
|
|
513
|
+
text=True,
|
|
514
|
+
)
|
|
515
|
+
self.assertFalse(proc.stdout.strip())
|
|
516
|
+
self.assertEqual(version_path.read_text(encoding="utf-8"), original)
|
|
517
|
+
|
|
518
|
+
@mock.patch("core.models.PackageRelease.dump_fixture")
|
|
519
|
+
def test_save_does_not_dump_fixture(self, dump):
|
|
520
|
+
self.release.pypi_url = "https://example.com"
|
|
521
|
+
self.release.save()
|
|
522
|
+
dump.assert_not_called()
|
|
523
|
+
|
|
524
|
+
@mock.patch("core.views.subprocess.run")
|
|
525
|
+
@mock.patch("core.views.PackageRelease.dump_fixture")
|
|
526
|
+
@mock.patch("core.views.release_utils.promote", side_effect=Exception("boom"))
|
|
527
|
+
def test_promote_cleans_repo_on_failure(
|
|
528
|
+
self, promote, dump_fixture, run
|
|
529
|
+
):
|
|
530
|
+
with self.assertRaises(Exception):
|
|
531
|
+
_step_promote_build(self.release, {}, Path("rel.log"))
|
|
532
|
+
dump_fixture.assert_not_called()
|
|
533
|
+
run.assert_any_call(["git", "reset", "--hard"], check=False)
|
|
534
|
+
run.assert_any_call(["git", "clean", "-fd"], check=False)
|
|
535
|
+
|
|
536
|
+
@mock.patch("core.views.subprocess.run")
|
|
537
|
+
@mock.patch("core.views.PackageRelease.dump_fixture")
|
|
538
|
+
@mock.patch("core.views.release_utils.promote")
|
|
539
|
+
def test_promote_rebases_and_pushes_main(self, promote, dump_fixture, run):
|
|
540
|
+
import subprocess as sp
|
|
541
|
+
|
|
542
|
+
def fake_run(cmd, check=True, capture_output=False, text=False):
|
|
543
|
+
if capture_output:
|
|
544
|
+
return sp.CompletedProcess(cmd, 0, stdout="", stderr="")
|
|
545
|
+
return sp.CompletedProcess(cmd, 0)
|
|
546
|
+
|
|
547
|
+
run.side_effect = fake_run
|
|
548
|
+
_step_promote_build(self.release, {}, Path("rel.log"))
|
|
549
|
+
run.assert_any_call(["git", "fetch", "origin", "main"], check=True)
|
|
550
|
+
run.assert_any_call(["git", "rebase", "origin/main"], check=True)
|
|
551
|
+
run.assert_any_call(["git", "push"], check=True)
|
|
552
|
+
|
|
553
|
+
@mock.patch("core.views.subprocess.run")
|
|
554
|
+
@mock.patch("core.views.PackageRelease.dump_fixture")
|
|
555
|
+
def test_promote_advances_version(self, dump_fixture, run):
|
|
556
|
+
import subprocess as sp
|
|
557
|
+
|
|
558
|
+
def fake_run(cmd, check=True, capture_output=False, text=False):
|
|
559
|
+
if capture_output:
|
|
560
|
+
return sp.CompletedProcess(cmd, 0, stdout="", stderr="")
|
|
561
|
+
return sp.CompletedProcess(cmd, 0)
|
|
562
|
+
|
|
563
|
+
run.side_effect = fake_run
|
|
564
|
+
|
|
565
|
+
version_path = Path("VERSION")
|
|
566
|
+
original = version_path.read_text(encoding="utf-8")
|
|
567
|
+
version_path.write_text("0.0.1\n", encoding="utf-8")
|
|
568
|
+
|
|
569
|
+
def fake_promote(*args, **kwargs):
|
|
570
|
+
version_path.write_text(self.release.version + "\n", encoding="utf-8")
|
|
571
|
+
|
|
572
|
+
with mock.patch("core.views.release_utils.promote", side_effect=fake_promote):
|
|
573
|
+
_step_promote_build(self.release, {}, Path("rel.log"))
|
|
574
|
+
|
|
575
|
+
self.assertEqual(
|
|
576
|
+
version_path.read_text(encoding="utf-8"),
|
|
577
|
+
self.release.version + "\n",
|
|
578
|
+
)
|
|
579
|
+
version_path.write_text(original, encoding="utf-8")
|
|
580
|
+
|
|
581
|
+
@mock.patch("core.views.PackageRelease.dump_fixture")
|
|
582
|
+
@mock.patch("core.views.release_utils.publish")
|
|
583
|
+
def test_publish_sets_pypi_url(self, publish, dump_fixture):
|
|
584
|
+
_step_publish(self.release, {}, Path("rel.log"))
|
|
585
|
+
self.release.refresh_from_db()
|
|
586
|
+
self.assertEqual(
|
|
587
|
+
self.release.pypi_url,
|
|
588
|
+
f"https://pypi.org/project/{self.package.name}/{self.release.version}/",
|
|
589
|
+
)
|
|
590
|
+
dump_fixture.assert_called_once()
|
|
591
|
+
|
|
592
|
+
@mock.patch("core.views.PackageRelease.dump_fixture")
|
|
593
|
+
@mock.patch("core.views.release_utils.publish", side_effect=Exception("boom"))
|
|
594
|
+
def test_publish_failure_keeps_url_blank(self, publish, dump_fixture):
|
|
595
|
+
with self.assertRaises(Exception):
|
|
596
|
+
_step_publish(self.release, {}, Path("rel.log"))
|
|
597
|
+
self.release.refresh_from_db()
|
|
598
|
+
self.assertEqual(self.release.pypi_url, "")
|
|
599
|
+
dump_fixture.assert_not_called()
|
|
600
|
+
|
|
601
|
+
def test_release_progress_uses_lockfile(self):
|
|
602
|
+
run = []
|
|
603
|
+
|
|
604
|
+
def step1(release, ctx, log_path):
|
|
605
|
+
run.append("step1")
|
|
606
|
+
|
|
607
|
+
def step2(release, ctx, log_path):
|
|
608
|
+
run.append("step2")
|
|
609
|
+
|
|
610
|
+
steps = [("One", step1), ("Two", step2)]
|
|
611
|
+
user = User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
612
|
+
url = reverse("release-progress", args=[self.release.pk, "publish"])
|
|
613
|
+
with mock.patch("core.views.PUBLISH_STEPS", steps):
|
|
614
|
+
self.client.force_login(user)
|
|
615
|
+
self.client.get(f"{url}?step=0")
|
|
616
|
+
self.assertEqual(run, ["step1"])
|
|
617
|
+
client2 = Client()
|
|
618
|
+
client2.force_login(user)
|
|
619
|
+
client2.get(f"{url}?step=1")
|
|
620
|
+
self.assertEqual(run, ["step1", "step2"])
|
|
621
|
+
lock_file = Path("locks") / f"release_publish_{self.release.pk}.json"
|
|
622
|
+
self.assertFalse(lock_file.exists())
|
|
623
|
+
|
|
624
|
+
def test_release_progress_restart(self):
|
|
625
|
+
run = []
|
|
626
|
+
|
|
627
|
+
def step_fail(release, ctx, log_path):
|
|
628
|
+
run.append("step")
|
|
629
|
+
raise Exception("boom")
|
|
630
|
+
|
|
631
|
+
steps = [("Fail", step_fail)]
|
|
632
|
+
user = User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
633
|
+
url = reverse("release-progress", args=[self.release.pk, "publish"])
|
|
634
|
+
count_file = Path("locks") / f"release_publish_{self.release.pk}.restarts"
|
|
635
|
+
if count_file.exists():
|
|
636
|
+
count_file.unlink()
|
|
637
|
+
with mock.patch("core.views.PUBLISH_STEPS", steps):
|
|
638
|
+
self.client.force_login(user)
|
|
639
|
+
self.assertFalse(count_file.exists())
|
|
640
|
+
self.client.get(f"{url}?step=0")
|
|
641
|
+
self.client.get(f"{url}?step=0")
|
|
642
|
+
self.assertEqual(run, ["step"])
|
|
643
|
+
self.assertFalse(count_file.exists())
|
|
644
|
+
self.client.get(f"{url}?restart=1")
|
|
645
|
+
self.assertTrue(count_file.exists())
|
|
646
|
+
self.assertEqual(count_file.read_text(), "1")
|
|
647
|
+
self.client.get(f"{url}?step=0")
|
|
648
|
+
self.assertEqual(run, ["step", "step"])
|
|
649
|
+
self.client.get(f"{url}?restart=1")
|
|
650
|
+
self.assertEqual(count_file.read_text(), "2")
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
class PackageReleaseAdminActionTests(TestCase):
|
|
654
|
+
def setUp(self):
|
|
655
|
+
self.factory = RequestFactory()
|
|
656
|
+
self.site = AdminSite()
|
|
657
|
+
self.admin = PackageReleaseAdmin(PackageRelease, self.site)
|
|
658
|
+
self.admin.message_user = lambda *args, **kwargs: None
|
|
659
|
+
self.package = Package.objects.create(name="pkg")
|
|
660
|
+
self.release = PackageRelease.objects.create(
|
|
661
|
+
package=self.package,
|
|
662
|
+
version="1.0.0",
|
|
663
|
+
pypi_url="https://pypi.org/project/pkg/1.0.0/",
|
|
664
|
+
)
|
|
665
|
+
self.request = self.factory.get("/")
|
|
666
|
+
|
|
667
|
+
@mock.patch("core.admin.PackageRelease.dump_fixture")
|
|
668
|
+
@mock.patch("core.admin.requests.get")
|
|
669
|
+
def test_validate_deletes_missing_release(self, mock_get, dump):
|
|
670
|
+
mock_get.return_value.status_code = 404
|
|
671
|
+
self.admin.validate_releases(self.request, PackageRelease.objects.all())
|
|
672
|
+
self.assertEqual(PackageRelease.objects.count(), 0)
|
|
673
|
+
dump.assert_called_once()
|
|
674
|
+
|
|
675
|
+
@mock.patch("core.admin.PackageRelease.dump_fixture")
|
|
676
|
+
@mock.patch("core.admin.requests.get")
|
|
677
|
+
def test_validate_keeps_existing_release(self, mock_get, dump):
|
|
678
|
+
mock_get.return_value.status_code = 200
|
|
679
|
+
self.admin.validate_releases(self.request, PackageRelease.objects.all())
|
|
680
|
+
self.assertEqual(PackageRelease.objects.count(), 1)
|
|
681
|
+
dump.assert_not_called()
|
|
682
|
+
|