bugcap 0.4.0__tar.gz → 0.5.0__tar.gz
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.
- {bugcap-0.4.0/src/bugcap.egg-info → bugcap-0.5.0}/PKG-INFO +1 -1
- {bugcap-0.4.0 → bugcap-0.5.0}/pyproject.toml +1 -1
- bugcap-0.5.0/src/bugcap/__init__.py +1 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/cli.py +13 -6
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/ingest.py +36 -4
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/service.py +11 -3
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/store.py +8 -0
- {bugcap-0.4.0 → bugcap-0.5.0/src/bugcap.egg-info}/PKG-INFO +1 -1
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_cli_attach.py +74 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_live_session.py +1 -1
- bugcap-0.4.0/src/bugcap/__init__.py +0 -1
- {bugcap-0.4.0 → bugcap-0.5.0}/LICENSE +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/README.md +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/setup.cfg +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/agent_api.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/backends.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/capture.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/config.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/dashboard/__init__.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/dashboard/api.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/dashboard/media.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/dashboard/server.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/dashboard/static/app.css +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/dashboard/static/app.js +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/dashboard/static/index.html +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/drafts.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/errors.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/ghcli.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/live.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/live_session.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/logs.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/mcp_server.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/paths.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/recorder.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/refs.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/repo.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/sync.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/tomlio.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap/validation.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap.egg-info/SOURCES.txt +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap.egg-info/dependency_links.txt +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap.egg-info/entry_points.txt +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap.egg-info/requires.txt +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/src/bugcap.egg-info/top_level.txt +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_agent_api.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_backends.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_capture.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_cli_dashboard.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_cli_init_capture_list.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_cli_live_headless.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_cli_media.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_cli_no_image_delete.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_cli_record.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_cli_setup.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_cli_triage.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_dashboard_api.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_dashboard_media.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_dashboard_mutations.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_dashboard_security.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_dashboard_ui_static.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_drafts.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_ghcli.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_github_media_sync.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_github_pull.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_github_sync.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_image_refs.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_ingest.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_ingest_url.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_mcp.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_mcp_logging.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_paths.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_recorder_argv.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_recorder_devices.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_recorder_media_real.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_recorder_run.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_refs.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_refs_validate_rewrite.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_regression_output.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_repo_config.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_repo_values.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_service_media.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_service_refs.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_store_migration.py +0 -0
- {bugcap-0.4.0 → bugcap-0.5.0}/tests/test_sync_consent.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "bugcap"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "Local-first, agent-readable bug capture: annotated screenshots + notes, stored locally, synced to trackers on your own schedule."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.0"
|
|
@@ -152,12 +152,8 @@ def cmd_list(args: argparse.Namespace) -> int:
|
|
|
152
152
|
return 0
|
|
153
153
|
|
|
154
154
|
|
|
155
|
-
def
|
|
156
|
-
|
|
157
|
-
report = resolve_report(store, args.id)
|
|
158
|
-
if report is None:
|
|
159
|
-
return 1
|
|
160
|
-
|
|
155
|
+
def print_report(report) -> None:
|
|
156
|
+
"""The multi-line details of one report, as `bugcap show` prints them."""
|
|
161
157
|
print(f"#{report.id} {report.title}")
|
|
162
158
|
print(f" created_at: {report.created_at}")
|
|
163
159
|
print(f" status: {report.status}")
|
|
@@ -181,6 +177,15 @@ def cmd_show(args: argparse.Namespace) -> int:
|
|
|
181
177
|
print(f" - {tracker}: {ref}")
|
|
182
178
|
else:
|
|
183
179
|
print(" (not synced to any tracker yet)")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def cmd_show(args: argparse.Namespace) -> int:
|
|
183
|
+
with Store() as store:
|
|
184
|
+
report = resolve_report(store, args.id)
|
|
185
|
+
if report is None:
|
|
186
|
+
return 1
|
|
187
|
+
|
|
188
|
+
print_report(report)
|
|
184
189
|
return 0
|
|
185
190
|
|
|
186
191
|
|
|
@@ -574,6 +579,8 @@ def cmd_attach(args: argparse.Namespace) -> int:
|
|
|
574
579
|
print(f"Attached {shot_path} to report #{args.id}")
|
|
575
580
|
elif note and added:
|
|
576
581
|
print(f"Added note to report #{args.id}")
|
|
582
|
+
print()
|
|
583
|
+
print_report(current)
|
|
577
584
|
return 1 if rejected or (note and not added) else 0
|
|
578
585
|
|
|
579
586
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"""Bring existing images into the store: local paths, globs and http(s) URLs.
|
|
2
2
|
|
|
3
3
|
Every input is validated by its magic bytes (never its extension or Content-Type), copied
|
|
4
|
-
into the images directory under a
|
|
5
|
-
atomically.
|
|
4
|
+
into the images directory under a name derived from its content (the original path is not
|
|
5
|
+
kept) and written atomically. The same image is therefore stored once, however many reports
|
|
6
|
+
use it. Network access is limited to plain http(s) GETs with no credentials."""
|
|
6
7
|
from __future__ import annotations
|
|
7
8
|
|
|
8
9
|
import glob
|
|
10
|
+
import hashlib
|
|
9
11
|
import os
|
|
10
12
|
import socket
|
|
11
13
|
import urllib.error
|
|
@@ -32,6 +34,7 @@ class StoredFile:
|
|
|
32
34
|
mime: str
|
|
33
35
|
size_bytes: int
|
|
34
36
|
source: str
|
|
37
|
+
reused: bool = False # the content was already in the store; no new file was written
|
|
35
38
|
|
|
36
39
|
|
|
37
40
|
@dataclass
|
|
@@ -70,10 +73,16 @@ def sniff_image(data: bytes) -> str:
|
|
|
70
73
|
raise ServiceError("invalid_image", "not an image (PNG, JPEG, GIF or WebP expected)")
|
|
71
74
|
|
|
72
75
|
|
|
76
|
+
def content_name(data: bytes, mime: str) -> str:
|
|
77
|
+
return f"{hashlib.sha256(data).hexdigest()}{_EXT[mime]}"
|
|
78
|
+
|
|
79
|
+
|
|
73
80
|
def _store(data: bytes, source: str) -> StoredFile:
|
|
74
81
|
mime = sniff_image(data)
|
|
75
|
-
dest = images_dir() /
|
|
76
|
-
|
|
82
|
+
dest = images_dir() / content_name(data, mime)
|
|
83
|
+
if dest.is_file() and dest.stat().st_size == len(data):
|
|
84
|
+
return StoredFile(to_data_relative(dest), dest, mime, len(data), source, reused=True)
|
|
85
|
+
tmp = dest.with_name(f"{dest.name}.{uuid.uuid4().hex}.part")
|
|
77
86
|
try:
|
|
78
87
|
tmp.write_bytes(data)
|
|
79
88
|
os.replace(tmp, dest)
|
|
@@ -82,6 +91,29 @@ def _store(data: bytes, source: str) -> StoredFile:
|
|
|
82
91
|
return StoredFile(to_data_relative(dest), dest, mime, len(data), source)
|
|
83
92
|
|
|
84
93
|
|
|
94
|
+
def adopt(path) -> Path:
|
|
95
|
+
"""Give a screenshot the capture tool already saved in the store its content-derived
|
|
96
|
+
name. If the same image is stored already, the new copy is dropped and the existing
|
|
97
|
+
path returned. Anything that is not a recognisable image, or sits outside the store, is
|
|
98
|
+
left where it is."""
|
|
99
|
+
path = Path(path)
|
|
100
|
+
if path.parent != images_dir():
|
|
101
|
+
return path
|
|
102
|
+
try:
|
|
103
|
+
data = path.read_bytes()
|
|
104
|
+
mime = sniff_image(data)
|
|
105
|
+
except (OSError, ServiceError):
|
|
106
|
+
return path
|
|
107
|
+
dest = images_dir() / content_name(data, mime)
|
|
108
|
+
if dest == path:
|
|
109
|
+
return path
|
|
110
|
+
if dest.is_file() and dest.stat().st_size == len(data):
|
|
111
|
+
path.unlink(missing_ok=True)
|
|
112
|
+
return dest
|
|
113
|
+
os.replace(path, dest)
|
|
114
|
+
return dest
|
|
115
|
+
|
|
116
|
+
|
|
85
117
|
def import_local(path: Path) -> StoredFile:
|
|
86
118
|
path = Path(path).expanduser()
|
|
87
119
|
if path.is_dir():
|
|
@@ -81,6 +81,8 @@ class AddResult:
|
|
|
81
81
|
|
|
82
82
|
def _discard(stored_files) -> None:
|
|
83
83
|
for stored in stored_files:
|
|
84
|
+
if stored.reused: # shared with other media; not ours to delete
|
|
85
|
+
continue
|
|
84
86
|
try:
|
|
85
87
|
stored.abs_path.unlink()
|
|
86
88
|
except OSError:
|
|
@@ -158,6 +160,7 @@ def create_report(
|
|
|
158
160
|
with store.transaction():
|
|
159
161
|
report = store.add(title=title, notes="", tags=tags, repo=repo, body=body, status=status)
|
|
160
162
|
if captured is not None:
|
|
163
|
+
captured = str(ingest.adopt(captured))
|
|
161
164
|
result.added.append(
|
|
162
165
|
store.insert_media(report.id, kind="image", path=captured,
|
|
163
166
|
label=captured_label or None, source="captured")
|
|
@@ -194,7 +197,7 @@ def delete_report(store: Store, report_id: int) -> Report:
|
|
|
194
197
|
report = _require(store, report_id)
|
|
195
198
|
store.delete(report_id)
|
|
196
199
|
for item in report.media:
|
|
197
|
-
_delete_files(item)
|
|
200
|
+
_delete_files(store, item)
|
|
198
201
|
return report
|
|
199
202
|
|
|
200
203
|
|
|
@@ -202,6 +205,7 @@ def attach_captured(store: Store, report_id: int, path: str, label: Optional[str
|
|
|
202
205
|
"""Attach a screenshot the capture tool already saved in the store (no copy)."""
|
|
203
206
|
report = _require(store, report_id)
|
|
204
207
|
_check_new_labels(report.media, [label])
|
|
208
|
+
path = ingest.adopt(path)
|
|
205
209
|
return store.insert_media(report_id, kind="image", path=str(path), label=label or None, source="captured")
|
|
206
210
|
|
|
207
211
|
|
|
@@ -343,14 +347,18 @@ def remove_media(store: Store, report_id: int, ref: str, force: bool = False) ->
|
|
|
343
347
|
notes, count = refs.rewrite_references_counted(report.notes, mapping)
|
|
344
348
|
if count:
|
|
345
349
|
store.update(report_id, notes=notes)
|
|
346
|
-
_delete_files(item)
|
|
350
|
+
_delete_files(store, item)
|
|
347
351
|
return count
|
|
348
352
|
|
|
349
353
|
|
|
350
|
-
def _delete_files(item: Media) -> None:
|
|
354
|
+
def _delete_files(store: Store, item: Media) -> None:
|
|
355
|
+
"""Delete an item's files unless another media item still uses them (identical images
|
|
356
|
+
are stored once and shared). Call after the item's rows are gone."""
|
|
351
357
|
paths = [item.path] if item.path else []
|
|
352
358
|
paths += [f.path for f in item.frames]
|
|
353
359
|
for rel in paths:
|
|
360
|
+
if store.path_in_use(rel):
|
|
361
|
+
continue
|
|
354
362
|
try:
|
|
355
363
|
os.unlink(resolve_data_path(rel))
|
|
356
364
|
except (ValueError, OSError):
|
|
@@ -425,6 +425,14 @@ class Store:
|
|
|
425
425
|
).fetchall()
|
|
426
426
|
return [self._load_media(r) for r in rows]
|
|
427
427
|
|
|
428
|
+
def path_in_use(self, path: str) -> bool:
|
|
429
|
+
"""Whether any media item or frame still points at this stored file."""
|
|
430
|
+
row = self._conn.execute(
|
|
431
|
+
"SELECT EXISTS(SELECT 1 FROM media WHERE path = ?) OR EXISTS(SELECT 1 FROM media_frames WHERE path = ?)",
|
|
432
|
+
(path, path),
|
|
433
|
+
).fetchone()
|
|
434
|
+
return bool(row[0])
|
|
435
|
+
|
|
428
436
|
def get_media(self, media_id: int) -> Optional[Media]:
|
|
429
437
|
row = self._conn.execute("SELECT * FROM media WHERE id = ?", (media_id,)).fetchone()
|
|
430
438
|
return self._load_media(row) if row else None
|
|
@@ -169,3 +169,77 @@ def test_pull_ask_flow_appends_note(bugcap_home, monkeypatch, tmp_path):
|
|
|
169
169
|
sync._attach_answer(store, rid, None)
|
|
170
170
|
report = store.get(rid)
|
|
171
171
|
assert len(report.media) == 2 and report.notes.endswith("@i1: seen on pull")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# --- details after attach / shared storage -----------------------------------------------
|
|
175
|
+
|
|
176
|
+
def test_attach_prints_the_bug_details_after_the_usual_output(bugcap_home, tmp_path, capsys):
|
|
177
|
+
with Store() as store:
|
|
178
|
+
rid = store.add("Login fails", tags=["auth"]).id
|
|
179
|
+
img = tmp_path / "new.png"
|
|
180
|
+
img.write_bytes(png_bytes())
|
|
181
|
+
assert cli.main(["attach", str(rid), "--image", str(img), "--note", "see this"]) == 0
|
|
182
|
+
out = capsys.readouterr().out
|
|
183
|
+
assert out.index("added @i1") < out.index(f"#{rid} Login fails")
|
|
184
|
+
assert "tags: auth" in out and "see this" in out and "media:" in out and "i1" in out
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_attach_with_a_rejected_image_still_shows_the_unchanged_bug(bugcap_home, tmp_path, capsys):
|
|
188
|
+
with Store() as store:
|
|
189
|
+
rid = store.add("Bug").id
|
|
190
|
+
assert cli.main(["attach", str(rid), "--image", str(tmp_path / "missing.png")]) == 1
|
|
191
|
+
captured = capsys.readouterr()
|
|
192
|
+
assert "skipped" in captured.err and f"#{rid} Bug" in captured.out and "media:" not in captured.out
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_attach_unknown_id_prints_no_details(bugcap_home, capsys):
|
|
196
|
+
assert cli.main(["attach", "999", "--image", "x.png"]) == 1
|
|
197
|
+
assert capsys.readouterr().out == ""
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_same_image_on_two_bugs_is_stored_once(bugcap_home, tmp_path):
|
|
201
|
+
from bugcap.paths import images_dir
|
|
202
|
+
|
|
203
|
+
with Store() as store:
|
|
204
|
+
a, b = store.add("A").id, store.add("B").id
|
|
205
|
+
img = tmp_path / "same.png"
|
|
206
|
+
img.write_bytes(png_bytes())
|
|
207
|
+
assert cli.main(["attach", str(a), "--image", str(img)]) == 0
|
|
208
|
+
assert cli.main(["attach", str(b), "--image", str(img)]) == 0
|
|
209
|
+
with Store() as store:
|
|
210
|
+
pa, pb = store.get(a).media[0].path, store.get(b).media[0].path
|
|
211
|
+
assert pa == pb and len(list(images_dir().iterdir())) == 1
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_shared_file_survives_until_its_last_user_is_gone(bugcap_home, tmp_path):
|
|
215
|
+
from bugcap import service
|
|
216
|
+
|
|
217
|
+
with Store() as store:
|
|
218
|
+
a, b = store.add("A").id, store.add("B").id
|
|
219
|
+
img = tmp_path / "same.png"
|
|
220
|
+
img.write_bytes(png_bytes())
|
|
221
|
+
for rid in (a, b):
|
|
222
|
+
assert cli.main(["attach", str(rid), "--image", str(img)]) == 0
|
|
223
|
+
with Store() as store:
|
|
224
|
+
path = store.get(a).media[0].abs_path
|
|
225
|
+
service.remove_media(store, a, "i1")
|
|
226
|
+
assert __import__("os").path.isfile(path) # B still uses it
|
|
227
|
+
service.delete_report(store, b)
|
|
228
|
+
assert not __import__("os").path.exists(path)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def test_same_image_twice_in_one_attach_and_failed_attach_keep_shared_file(bugcap_home, tmp_path):
|
|
232
|
+
from bugcap import service
|
|
233
|
+
|
|
234
|
+
with Store() as store:
|
|
235
|
+
a, b = store.add("A").id, store.add("B").id
|
|
236
|
+
img = tmp_path / "same.png"
|
|
237
|
+
img.write_bytes(png_bytes())
|
|
238
|
+
service.add_media(store, a, [str(img)])
|
|
239
|
+
# B asks for the same image with a label that cannot be used: nothing may be deleted
|
|
240
|
+
with pytest.raises(Exception):
|
|
241
|
+
service.add_media(store, b, [str(img)], labels=["i1"])
|
|
242
|
+
path = store.get(a).media[0].abs_path
|
|
243
|
+
assert __import__("os").path.isfile(path)
|
|
244
|
+
res = service.add_media(store, b, [str(img), str(img)])
|
|
245
|
+
assert len(res.added) == 2 and len({m.path for m in res.added}) == 1
|
|
@@ -97,7 +97,7 @@ def test_save_combines_images_and_a_video_in_order(session):
|
|
|
97
97
|
assert report.title == "Login broken" and report.repo == "owner/proj"
|
|
98
98
|
assert report.tags == ["auth", "proj"] and report.status == "in-progress"
|
|
99
99
|
assert [(m.idx, m.kind) for m in report.media] == [(1, "image"), (2, "video"), (3, "image")]
|
|
100
|
-
assert
|
|
100
|
+
assert all(Path(m.abs_path).is_file() for m in report.media) # captured files stay in the store
|
|
101
101
|
|
|
102
102
|
|
|
103
103
|
def test_two_reports_in_a_row(session):
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|