feat: sync execution with 3s progress polling (stage 2)
- pulp_client: get_repo / sync_repo / get_task (비동기 task href 반환)
- POST /repos/{uuid}/sync: remote 확인 후 동기화 시작, 진행률 조각 반환
- GET /repos/{uuid}/progress: progress_reports 합산 → 바/퍼센트,
완료/실패 시 hx-trigger 제거로 폴링 중단
- task_store: uuid→task_href 메모리 추적 (단일 인스턴스), 대시보드 '동기화 중' 집계
- views.task_progress: state/done/total/percent + created_resources에서 새 vN
- 동기화는 안전 동작이라 확인 모달 없이 실행 (스펙 §5-1)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
3
PLAN.md
3
PLAN.md
@@ -68,7 +68,7 @@
|
||||
|
||||
---
|
||||
|
||||
## [ ] 단계 2 — 동기화 실행 + 진행률 폴링 (화면 2)
|
||||
## [x] 단계 2 — 동기화 실행 + 진행률 폴링 (화면 2)
|
||||
|
||||
**목표:** [동기화] 클릭 → sync task 시작 → 3초 폴링으로 진행률 갱신 → 완료/실패 표시.
|
||||
|
||||
@@ -148,6 +148,7 @@ distribution PATCH → task 폴링 → 배지 갱신 → 감사 로그 기록.
|
||||
wheel/오프라인 인덱스로 설치 가능하게(litellm 반입 방식 참고, 스펙 §9).
|
||||
3. 실행 문서화: `README` 또는 CLAUDE.md "Conventions"에 기동 명령/환경변수 정리.
|
||||
4. (선택, §5-3) 읽기전용 / 배포권한 계정 분리 여지를 남긴 인증 훅 위치 표시.
|
||||
5. 디자인 가이드는 ref 하위의 markdown 파일들을 참고.
|
||||
|
||||
**검증:** 네트워크 차단 상태에서 컨테이너/venv만으로 앱이 뜨고 모든 정적 자산이 로드된다.
|
||||
|
||||
|
||||
14
app/demo.py
14
app/demo.py
@@ -67,3 +67,17 @@ class DemoPulpClient:
|
||||
|
||||
def get_version(self, version_href: str) -> dict[str, Any]:
|
||||
return _VERSIONS.get(version_href, {})
|
||||
|
||||
def get_repo(self, uuid: str) -> dict[str, Any]:
|
||||
return {"remote": "/pulp/api/v3/remotes/rpm/rpm/demo/"}
|
||||
|
||||
def sync_repo(self, uuid: str, remote_href: str) -> str:
|
||||
return f"/pulp/api/v3/tasks/demo-{uuid}/"
|
||||
|
||||
def get_task(self, task_href: str) -> dict[str, Any]:
|
||||
# 데모: 첫 폴링에서 바로 완료 처리
|
||||
return {
|
||||
"state": "completed",
|
||||
"progress_reports": [{"done": 120, "total": 120}],
|
||||
"created_resources": ["/pulp/api/v3/repositories/rpm/rpm/demo/versions/9/"],
|
||||
}
|
||||
|
||||
@@ -15,13 +15,14 @@ from fastapi.staticfiles import StaticFiles
|
||||
# 테스트 호환을 위해 re-export (tests 가 app.main.get_pulp_client 를 override 함)
|
||||
from .deps import get_pulp_client, templates
|
||||
from .pulp_client import PulpClient
|
||||
from .routes import dashboard
|
||||
from .routes import dashboard, sync
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
|
||||
app = FastAPI(title="Pulp 패치 관리 콘솔")
|
||||
app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")
|
||||
app.include_router(dashboard.router)
|
||||
app.include_router(sync.router)
|
||||
|
||||
__all__ = ["app", "get_pulp_client"]
|
||||
|
||||
|
||||
@@ -48,7 +48,9 @@ class PulpClient:
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
def patch(self, href: str, json: Any | None = None, **kwargs: Any) -> dict[str, Any]:
|
||||
def patch(
|
||||
self, href: str, json: Any | None = None, **kwargs: Any
|
||||
) -> dict[str, Any]:
|
||||
resp = self._client.patch(href, json=json, **kwargs)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
@@ -66,3 +68,23 @@ class PulpClient:
|
||||
def get_version(self, version_href: str) -> dict[str, Any]:
|
||||
"""RepositoryVersion 단건 조회 (href 기준). content_summary 등 포함."""
|
||||
return self.get(version_href)
|
||||
|
||||
def get_repo(self, uuid: str) -> dict[str, Any]:
|
||||
"""저장소 단건 조회 (remote href 등 확인용)."""
|
||||
return self.get(f"{API_PREFIX}/repositories/rpm/rpm/{uuid}/")
|
||||
|
||||
def sync_repo(self, uuid: str, remote_href: str) -> str:
|
||||
"""동기화 시작 → 비동기 task. 반환 task_href 를 폴링으로 추적한다(스펙 §4).
|
||||
|
||||
POST .../repositories/rpm/rpm/{uuid}/sync/ body {"remote": <remote_href>}
|
||||
응답 {"task": "/pulp/api/v3/tasks/<uuid>/"} 에서 task href 추출.
|
||||
"""
|
||||
data = self.post(
|
||||
f"{API_PREFIX}/repositories/rpm/rpm/{uuid}/sync/",
|
||||
json={"remote": remote_href},
|
||||
)
|
||||
return data["task"]
|
||||
|
||||
def get_task(self, task_href: str) -> dict[str, Any]:
|
||||
"""task 단건 조회 (state, progress_reports, created_resources, error)."""
|
||||
return self.get(task_href)
|
||||
|
||||
@@ -12,7 +12,7 @@ import httpx
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
from .. import views
|
||||
from .. import task_store, views
|
||||
from ..deps import get_pulp_client, templates
|
||||
from ..pulp_client import PulpClient
|
||||
|
||||
@@ -21,10 +21,11 @@ router = APIRouter()
|
||||
|
||||
def _load_repos(pulp: PulpClient) -> dict[str, Any]:
|
||||
"""저장소 목록을 읽어 표시 모델 + 요약 + (실패 시) 에러 메시지를 만든다."""
|
||||
syncing = task_store.active_count()
|
||||
try:
|
||||
repos = pulp.list_repos()
|
||||
except httpx.HTTPError as exc:
|
||||
return {"repos": [], "summary": views.summarize([]), "error": str(exc)}
|
||||
return {"repos": [], "summary": views.summarize([], syncing), "error": str(exc)}
|
||||
|
||||
repo_views: list[dict[str, Any]] = []
|
||||
for repo in repos:
|
||||
@@ -37,7 +38,11 @@ def _load_repos(pulp: PulpClient) -> dict[str, Any]:
|
||||
version = None # 버전 상세 실패는 치명적이지 않음 — 행은 표시
|
||||
repo_views.append(views.build_repo_view(repo, version))
|
||||
|
||||
return {"repos": repo_views, "summary": views.summarize(repo_views), "error": None}
|
||||
return {
|
||||
"repos": repo_views,
|
||||
"summary": views.summarize(repo_views, syncing),
|
||||
"error": None,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
|
||||
114
app/routes/sync.py
Normal file
114
app/routes/sync.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""동기화 실행 + 진행률 폴링 (화면 2).
|
||||
|
||||
POST /repos/{uuid}/sync → 동기화 시작, 진행률 바 조각 반환 (이후 3초 폴링)
|
||||
GET /repos/{uuid}/progress → 진행률 바 조각 (폴링 대상)
|
||||
|
||||
동기화는 외부 미러에서 새 스냅샷을 만들 뿐 운영망 배포가 아니므로 비교적 안전 →
|
||||
확인 모달 없이 바로 실행한다(스펙 §5-1). 폴링은 화면(HTMX)이 담당한다(스펙 §7).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
from .. import task_store, views
|
||||
from ..deps import get_pulp_client, templates
|
||||
from ..pulp_client import PulpClient
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _render(request: Request, uuid: str, progress: dict, status_code: int = 200):
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"partials/progress.html",
|
||||
{"uuid": uuid, "progress": progress},
|
||||
status_code=status_code,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/repos/{uuid}/sync", response_class=HTMLResponse)
|
||||
def start_sync(
|
||||
request: Request, uuid: str, pulp: PulpClient = Depends(get_pulp_client)
|
||||
) -> HTMLResponse:
|
||||
try:
|
||||
repo = pulp.get_repo(uuid)
|
||||
remote = repo.get("remote")
|
||||
if not remote:
|
||||
return _render(
|
||||
request,
|
||||
uuid,
|
||||
{
|
||||
"running": False,
|
||||
"completed": False,
|
||||
"failed": True,
|
||||
"error": "이 저장소에 remote(미러 소스)가 설정되어 있지 않습니다.",
|
||||
},
|
||||
status_code=400,
|
||||
)
|
||||
task_href = pulp.sync_repo(uuid, remote)
|
||||
except httpx.HTTPError as exc:
|
||||
return _render(
|
||||
request,
|
||||
uuid,
|
||||
{
|
||||
"running": False,
|
||||
"completed": False,
|
||||
"failed": True,
|
||||
"error": f"동기화 시작 실패: {exc}",
|
||||
},
|
||||
status_code=502,
|
||||
)
|
||||
|
||||
task_store.set_task(uuid, task_href)
|
||||
# 첫 조각: 폴링 시작 (아직 progress_reports 없음 → 준비 중)
|
||||
return _render(
|
||||
request,
|
||||
uuid,
|
||||
{
|
||||
"running": True,
|
||||
"completed": False,
|
||||
"failed": False,
|
||||
"state": "waiting",
|
||||
"done": 0,
|
||||
"total": 0,
|
||||
"percent": 0,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/repos/{uuid}/progress", response_class=HTMLResponse)
|
||||
def progress(
|
||||
request: Request, uuid: str, pulp: PulpClient = Depends(get_pulp_client)
|
||||
) -> HTMLResponse:
|
||||
task_href = task_store.get_task(uuid)
|
||||
if not task_href:
|
||||
# 추적 중인 sync 없음 → 폴링 중단(빈 조각)
|
||||
return _render(
|
||||
request,
|
||||
uuid,
|
||||
{"running": False, "completed": False, "failed": False, "idle": True},
|
||||
)
|
||||
|
||||
try:
|
||||
task = pulp.get_task(task_href)
|
||||
except httpx.HTTPError as exc:
|
||||
task_store.clear_task(uuid)
|
||||
return _render(
|
||||
request,
|
||||
uuid,
|
||||
{
|
||||
"running": False,
|
||||
"completed": False,
|
||||
"failed": True,
|
||||
"error": f"작업 상태 조회 실패: {exc}",
|
||||
},
|
||||
status_code=502,
|
||||
)
|
||||
|
||||
prog = views.task_progress(task)
|
||||
if prog["completed"] or prog["failed"]:
|
||||
task_store.clear_task(uuid) # 폴링 종료 + 동기화중 카운트에서 제외
|
||||
return _render(request, uuid, prog)
|
||||
@@ -220,6 +220,21 @@ button:disabled,
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
/* 동기화 진행률 바 */
|
||||
.sync-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 160px;
|
||||
}
|
||||
.sync-progress progress {
|
||||
margin: 0;
|
||||
height: 6px;
|
||||
}
|
||||
.sync-progress small {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* 연결 상태 표시 (dot + 텍스트) */
|
||||
.status-line {
|
||||
display: inline-flex;
|
||||
|
||||
26
app/task_store.py
Normal file
26
app/task_store.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""진행 중 동기화 task 의 메모리 저장소 (uuid → task_href).
|
||||
|
||||
폐쇄망 단일 인스턴스 가정. 다중 워커/프로세스로 띄우면 워커 간 공유되지 않으므로,
|
||||
규모가 커지면 Redis 등 외부 저장소로 교체해야 한다. 폴링 자체는 화면(HTMX)이 한다(스펙 §7).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
_active: dict[str, str] = {}
|
||||
|
||||
|
||||
def set_task(uuid: str, task_href: str) -> None:
|
||||
_active[uuid] = task_href
|
||||
|
||||
|
||||
def get_task(uuid: str) -> str | None:
|
||||
return _active.get(uuid)
|
||||
|
||||
|
||||
def clear_task(uuid: str) -> None:
|
||||
_active.pop(uuid, None)
|
||||
|
||||
|
||||
def active_count() -> int:
|
||||
"""대시보드 '동기화 중' 카드용 — 아직 완료/실패로 정리되지 않은 sync 수."""
|
||||
return len(_active)
|
||||
21
app/templates/partials/progress.html
Normal file
21
app/templates/partials/progress.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{# 진행률 바 조각. running 일 때만 hx-trigger 를 달아 3초 폴링; 완료/실패 시 폴링 중단. #}
|
||||
<div id="prog-{{ uuid }}"
|
||||
{% if progress.running %}hx-get="/repos/{{ uuid }}/progress" hx-trigger="every 3s" hx-swap="outerHTML"{% endif %}>
|
||||
{% if progress.failed %}
|
||||
<span class="status-line status-bad">동기화 실패: {{ progress.error }}</span>
|
||||
{% elif progress.completed %}
|
||||
<span class="badge badge-pass">동기화 완료{% if progress.new_version %} · v{{ progress.new_version }} 생성됨{% endif %}</span>
|
||||
{% elif progress.idle %}
|
||||
{# 추적 중인 작업 없음 — 빈 조각 #}
|
||||
{% else %}
|
||||
<div class="sync-progress">
|
||||
{% if progress.total %}
|
||||
<progress value="{{ progress.done }}" max="{{ progress.total }}"></progress>
|
||||
<small class="num">{{ progress.done }}/{{ progress.total }} 패키지 · {{ progress.percent }}%</small>
|
||||
{% else %}
|
||||
<progress></progress>
|
||||
<small>동기화 준비 중…</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -31,8 +31,13 @@
|
||||
<td class="num">{% if repo.package_count is not none %}{{ repo.package_count }}{% else %}—{% endif %}</td>
|
||||
<td>{{ repo.last_sync or "—" }}</td>
|
||||
<td>
|
||||
<!-- 동기화 버튼은 단계2에서 동작 연결 -->
|
||||
<button disabled class="secondary">동기화</button>
|
||||
<div id="sync-{{ repo.uuid }}">
|
||||
<button class="secondary"
|
||||
hx-post="/repos/{{ repo.uuid }}/sync"
|
||||
hx-target="#sync-{{ repo.uuid }}"
|
||||
hx-swap="innerHTML"
|
||||
hx-disabled-elt="this">동기화</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
43
app/views.py
43
app/views.py
@@ -69,12 +69,47 @@ def build_repo_view(
|
||||
}
|
||||
|
||||
|
||||
def summarize(repo_views: list[dict[str, Any]]) -> dict[str, int]:
|
||||
"""상단 요약 카드 수치."""
|
||||
def summarize(repo_views: list[dict[str, Any]], syncing: int = 0) -> dict[str, int]:
|
||||
"""상단 요약 카드 수치. syncing 은 task_store.active_count() 에서 주입."""
|
||||
return {
|
||||
"total": len(repo_views),
|
||||
# TODO(단계2): 진행 중 sync task 수를 메모리 task 맵에서 집계
|
||||
"syncing": 0,
|
||||
"syncing": syncing,
|
||||
"gpg_pass": sum(1 for r in repo_views if r["gpg"]["level"] == "pass"),
|
||||
"total_packages": sum(r["package_count"] or 0 for r in repo_views),
|
||||
}
|
||||
|
||||
|
||||
def task_progress(task: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Pulp task → 진행률 표시 모델.
|
||||
|
||||
progress_reports[] 의 done/total 을 합산해 퍼센트를 낸다. 완료 시
|
||||
created_resources 에서 새 RepositoryVersion 번호(vN)를 추출한다.
|
||||
"""
|
||||
state = task.get("state", "waiting")
|
||||
reports = task.get("progress_reports") or []
|
||||
done = sum((r or {}).get("done") or 0 for r in reports)
|
||||
total = sum((r or {}).get("total") or 0 for r in reports)
|
||||
percent = int(done / total * 100) if total else 0
|
||||
|
||||
new_version = None
|
||||
for href in task.get("created_resources") or []:
|
||||
if "/versions/" in href:
|
||||
new_version = version_number(href)
|
||||
break
|
||||
|
||||
error = None
|
||||
if state == "failed":
|
||||
err = task.get("error") or {}
|
||||
error = err.get("description") or "동기화 작업이 실패했습니다."
|
||||
|
||||
return {
|
||||
"state": state,
|
||||
"done": done,
|
||||
"total": total,
|
||||
"percent": percent,
|
||||
"new_version": new_version,
|
||||
"error": error,
|
||||
"completed": state == "completed",
|
||||
"failed": state == "failed",
|
||||
"running": state in ("waiting", "running"),
|
||||
}
|
||||
|
||||
@@ -37,3 +37,27 @@ def test_get_raises_on_http_error():
|
||||
)
|
||||
with PulpClient() as pulp, pytest.raises(httpx.HTTPStatusError):
|
||||
pulp.status()
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_sync_repo_posts_remote_and_returns_task():
|
||||
import json
|
||||
|
||||
route = respx.post(
|
||||
f"{PULP_TEST_URL}/pulp/api/v3/repositories/rpm/rpm/abc/sync/"
|
||||
).mock(return_value=httpx.Response(202, json={"task": "/pulp/api/v3/tasks/t1/"}))
|
||||
with PulpClient() as pulp:
|
||||
href = pulp.sync_repo("abc", "/pulp/api/v3/remotes/rpm/rpm/r1/")
|
||||
assert href == "/pulp/api/v3/tasks/t1/"
|
||||
body = json.loads(route.calls.last.request.content)
|
||||
assert body == {"remote": "/pulp/api/v3/remotes/rpm/rpm/r1/"}
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_get_task_returns_state():
|
||||
respx.get(f"{PULP_TEST_URL}/pulp/api/v3/tasks/t1/").mock(
|
||||
return_value=httpx.Response(200, json={"state": "running"})
|
||||
)
|
||||
with PulpClient() as pulp:
|
||||
task = pulp.get_task("/pulp/api/v3/tasks/t1/")
|
||||
assert task["state"] == "running"
|
||||
|
||||
115
tests/test_sync.py
Normal file
115
tests/test_sync.py
Normal file
@@ -0,0 +1,115 @@
|
||||
import httpx
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app import task_store
|
||||
from app.main import app, get_pulp_client
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
class _FakePulp:
|
||||
def __init__(self, repo=None, task=None, sync_raises=False):
|
||||
self._repo = (
|
||||
{"remote": "/pulp/api/v3/remotes/rpm/rpm/r1/"} if repo is None else repo
|
||||
)
|
||||
self._task = task
|
||||
self._sync_raises = sync_raises
|
||||
self.synced = None
|
||||
|
||||
def get_repo(self, uuid):
|
||||
return self._repo
|
||||
|
||||
def sync_repo(self, uuid, remote_href):
|
||||
if self._sync_raises:
|
||||
raise httpx.ConnectError("connection refused")
|
||||
self.synced = (uuid, remote_href)
|
||||
return f"/pulp/api/v3/tasks/t-{uuid}/"
|
||||
|
||||
def get_task(self, task_href):
|
||||
return self._task
|
||||
|
||||
|
||||
def _use(fake):
|
||||
app.dependency_overrides[get_pulp_client] = lambda: fake
|
||||
|
||||
|
||||
def teardown_function():
|
||||
app.dependency_overrides.clear()
|
||||
task_store._active.clear()
|
||||
|
||||
|
||||
def test_start_sync_begins_polling_and_records_task():
|
||||
fake = _FakePulp()
|
||||
_use(fake)
|
||||
resp = client.post("/repos/abc/sync")
|
||||
assert resp.status_code == 200
|
||||
assert 'hx-get="/repos/abc/progress"' in resp.text
|
||||
assert 'hx-trigger="every 3s"' in resp.text
|
||||
assert task_store.get_task("abc") == "/pulp/api/v3/tasks/t-abc/"
|
||||
assert fake.synced == ("abc", "/pulp/api/v3/remotes/rpm/rpm/r1/")
|
||||
|
||||
|
||||
def test_start_sync_without_remote_is_rejected():
|
||||
_use(_FakePulp(repo={})) # remote 미설정
|
||||
resp = client.post("/repos/abc/sync")
|
||||
assert resp.status_code == 400
|
||||
assert "remote" in resp.text
|
||||
assert task_store.get_task("abc") is None
|
||||
|
||||
|
||||
def test_start_sync_handles_pulp_error():
|
||||
_use(_FakePulp(sync_raises=True))
|
||||
resp = client.post("/repos/abc/sync")
|
||||
assert resp.status_code == 502
|
||||
assert "동기화 시작 실패" in resp.text
|
||||
assert task_store.get_task("abc") is None
|
||||
|
||||
|
||||
def test_progress_running_renders_bar_and_keeps_polling():
|
||||
task_store.set_task("abc", "/pulp/api/v3/tasks/t1/")
|
||||
_use(
|
||||
_FakePulp(
|
||||
task={"state": "running", "progress_reports": [{"done": 4, "total": 8}]}
|
||||
)
|
||||
)
|
||||
resp = client.get("/repos/abc/progress")
|
||||
assert resp.status_code == 200
|
||||
assert "50%" in resp.text
|
||||
assert 'hx-trigger="every 3s"' in resp.text
|
||||
assert task_store.get_task("abc") is not None # 아직 추적 중
|
||||
|
||||
|
||||
def test_progress_completed_stops_polling_and_clears():
|
||||
task_store.set_task("abc", "/pulp/api/v3/tasks/t1/")
|
||||
_use(
|
||||
_FakePulp(
|
||||
task={
|
||||
"state": "completed",
|
||||
"created_resources": [
|
||||
"/pulp/api/v3/repositories/rpm/rpm/abc/versions/7/"
|
||||
],
|
||||
}
|
||||
)
|
||||
)
|
||||
resp = client.get("/repos/abc/progress")
|
||||
assert "동기화 완료" in resp.text
|
||||
assert "v7" in resp.text
|
||||
assert "hx-trigger" not in resp.text # 폴링 중단
|
||||
assert task_store.get_task("abc") is None # 정리됨
|
||||
|
||||
|
||||
def test_progress_failed_shows_error_and_clears():
|
||||
task_store.set_task("abc", "/pulp/api/v3/tasks/t1/")
|
||||
_use(_FakePulp(task={"state": "failed", "error": {"description": "GPG 검증 실패"}}))
|
||||
resp = client.get("/repos/abc/progress")
|
||||
assert "동기화 실패" in resp.text
|
||||
assert "GPG 검증 실패" in resp.text
|
||||
assert "hx-trigger" not in resp.text
|
||||
assert task_store.get_task("abc") is None
|
||||
|
||||
|
||||
def test_progress_idle_when_no_active_task():
|
||||
_use(_FakePulp())
|
||||
resp = client.get("/repos/zzz/progress")
|
||||
assert resp.status_code == 200
|
||||
assert "hx-trigger" not in resp.text # 폴링하지 않음
|
||||
@@ -70,3 +70,34 @@ def test_summarize_counts():
|
||||
"gpg_pass": 2,
|
||||
"total_packages": 150,
|
||||
}
|
||||
|
||||
|
||||
def test_summarize_includes_syncing_count():
|
||||
assert views.summarize([], syncing=2)["syncing"] == 2
|
||||
|
||||
|
||||
def test_task_progress_running_sums_reports():
|
||||
task = {
|
||||
"state": "running",
|
||||
"progress_reports": [{"done": 5, "total": 10}, {"done": 3, "total": 10}],
|
||||
}
|
||||
p = views.task_progress(task)
|
||||
assert p["running"] is True
|
||||
assert p["completed"] is False
|
||||
assert (p["done"], p["total"], p["percent"]) == (8, 20, 40)
|
||||
|
||||
|
||||
def test_task_progress_completed_extracts_version():
|
||||
task = {
|
||||
"state": "completed",
|
||||
"created_resources": ["/pulp/api/v3/repositories/rpm/rpm/x/versions/4/"],
|
||||
}
|
||||
p = views.task_progress(task)
|
||||
assert p["completed"] is True
|
||||
assert p["new_version"] == 4
|
||||
|
||||
|
||||
def test_task_progress_failed_surfaces_error():
|
||||
p = views.task_progress({"state": "failed", "error": {"description": "boom"}})
|
||||
assert p["failed"] is True
|
||||
assert p["error"] == "boom"
|
||||
|
||||
Reference in New Issue
Block a user