Merge remote-tracking branch 'origin/main' into fix/no-store-for-label-payloads

# Conflicts:
#	skills/sync/scripts/_gitea.py
This commit is contained in:
naudachu
2026-08-10 18:23:39 +05:00
9 changed files with 962 additions and 56 deletions
+5
View File
@@ -255,6 +255,11 @@ class FakeGitea(object):
path, _, query = endpoint.partition("?")
params = dict(urllib.parse.parse_qsl(query))
# Every pull asks for an issue's native links now (dependencies are the
# default). Nothing here has any; the answer just has to exist.
if path.endswith("/dependencies"):
return []
m = re.match(r"^%s/issues/(\d+)$" % re.escape(BASE), path)
if m and method == "GET":
return self.issues.get(int(m.group(1)))
+354
View File
@@ -0,0 +1,354 @@
#!/usr/bin/env python3
"""
A pull returns the unit of work: the issue AND what blocks it.
`--deps` used to be opt-in, so `pull.py 42` wrote a file with an empty
`depends:` and `issue_tree.py` drew it as a root with no blockers. The edge was
not lost — it lives in Gitea's native dependency graph — but it was not asked
for, and the body cannot supply it: `map.from_api` writes slugs into the
`## Depends on` prose and never `#N`. Following the graph is now the default.
What is asserted here:
1. **The default fills the graph.** A bare `pull.py <n>` fills `depends:` and
pulls the blocker too, down to `--depth`.
2. **`--no-deps` is the way out, and it is free.** No `depends:`, no recursion,
and not one request beyond the issue itself.
3. **`--deps` still works and means nothing.** Calls written against the old
default keep running and get what they always got.
4. **The cost is one request per stored issue.** The native links are fetched
once and used twice — for `depends:` and for the walk. Never twice.
5. **Filter mode follows blockers out of the selection, deliberately.** A
blocker no filter selected still lands in the store and does not spend
`--limit`; a closed one is dropped like any other closed issue, and so is
the edge to it. An issue the filter dropped costs no link request at all.
The transport is stubbed at `_gitea.api`, as the other suites do it, and the
stub records every call so "how many requests" is an observation. No network,
and no test touches the developer's store: each builds its own in a
`tempfile.TemporaryDirectory()`.
"""
import contextlib
import io
import os
import sys
import tempfile
import unittest
import urllib.parse
from unittest import mock
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
for _p in (os.path.join(_ROOT, "skills", "sync", "scripts"),
os.path.join(_ROOT, "skills", "issue", "scripts")):
if _p not in sys.path:
sys.path.insert(0, _p)
import _gitea # noqa: E402
import issue # noqa: E402
import pull # noqa: E402
REPO = "claude-skills/tea"
BASE = "repos/%s" % REPO
BODY = """## Summary
Прозаическое описание задачи.
## Spec
skills/issue/references/format.md
## Acceptance criteria
- [ ] что-нибудь работает
"""
def payload(number, title, state="open"):
return {"number": number, "title": title, "body": BODY, "state": state,
"comments": 0, "labels": [{"name": "type/task"}], "assignees": [],
"milestone": None, "ref": "main", "updated_at": "2026-08-10T00:00:00Z",
"html_url": "https://git.example/%s/issues/%d" % (REPO, number),
"repository": {"full_name": REPO}}
class FakeTracker(object):
"""`tea api` answered from memory, with a native dependency graph.
`listed` is what the list endpoint serves — the filter's selection. `extra`
exists and is fetchable by number but is in no selection, which is how a
blocker outside the filter is modelled. `deps` maps a blocked issue's number
to the numbers that block it, the direction `GET …/dependencies` reads.
"""
def __init__(self, listed=(), extra=(), deps=None):
self.listed = list(listed)
self.issues = {p["number"]: p for p in list(listed) + list(extra)}
self.deps = {int(k): list(v) for k, v in (deps or {}).items()}
self.calls = [] # (method, path), in request order
# -- what the tests read off it ----------------------------------------
def paths(self, suffix):
return [p for m, p in self.calls if p.endswith(suffix)]
def issue_gets(self):
"""`GET …/issues/<n>` — one issue fetched by number."""
return [p for m, p in self.calls
if m == "GET" and p.startswith("%s/issues/" % BASE)
and p.rsplit("/", 1)[1].isdigit()]
# -- the seam ----------------------------------------------------------
def api(self, login, endpoint, method="GET", payload=None,
payload_name=None, out_root=None, allow_fail=False):
path, _, qs = endpoint.partition("?")
q = urllib.parse.parse_qs(qs)
self.calls.append((method, path))
if path == "%s/issues" % BASE and method == "GET":
page, per = int(q["page"][0]), int(q["limit"][0])
return self.listed[(page - 1) * per:(page - 1) * per + per]
if path.endswith("/comments"):
return []
if path.endswith("/dependencies") and method == "GET":
n = int(path.split("/issues/")[1].split("/")[0])
return [self.issues[b] for b in self.deps.get(n, []) if b in self.issues]
if path.startswith("%s/issues/" % BASE) and method == "GET":
return self.issues.get(int(path.rsplit("/", 1)[1]))
raise AssertionError("unstubbed call: %s %s" % (method, endpoint))
class PullDepsTestCase(unittest.TestCase):
"""A temp store, a fake tracker, no git and no network."""
def setUp(self):
self.tmp = tempfile.TemporaryDirectory(prefix="tea-deps-")
self.addCleanup(self.tmp.cleanup)
self.root = os.path.join(self.tmp.name, "tmp", "issues")
os.makedirs(self.root)
p = mock.patch.object(_gitea, "require_login", lambda: "test-login")
p.start()
self.addCleanup(p.stop)
def serve(self, listed=(), extra=(), deps=None):
self.fake = FakeTracker(listed, extra, deps)
p = mock.patch.object(_gitea, "api", self.fake.api)
p.start()
self.addCleanup(p.stop)
return self.fake
def blocked_pair(self):
"""#10 "Second thing" is blocked by #7 "First thing"."""
return self.serve(listed=[payload(10, "Second thing"),
payload(7, "First thing")],
deps={10: [7]})
def run_pull(self, *argv):
out, err = io.StringIO(), io.StringIO()
args = ["pull.py", "--repo", REPO, "--out", self.root] + list(argv)
with mock.patch.object(sys, "argv", args), \
contextlib.redirect_stdout(out), \
contextlib.redirect_stderr(err):
pull.main()
return out.getvalue(), err.getvalue()
def stored(self):
return sorted(issue.all_ids(self.root))
def depends_of(self, id):
return issue.load(self.root, id).depends
# --------------------------------------------------------------------------
# 1. the default fills the graph
# --------------------------------------------------------------------------
class DepsAreTheDefaultTest(PullDepsTestCase):
def test_a_bare_pull_fills_depends(self):
"""The acceptance criterion, and the whole point: no flag, and the file
knows what blocks it."""
self.blocked_pair()
self.run_pull("10")
self.assertEqual(self.depends_of("second-thing"), ["first-thing"])
def test_a_bare_pull_stores_the_blocker(self):
"""`depends:` pointing at a file that is not there would be worse than
an empty one — the blocker comes with it."""
self.blocked_pair()
self.run_pull("10")
self.assertIn("first-thing", self.stored())
def test_the_walk_is_recursive(self):
"""A blocker's blocker is context too, down to --depth (default 3)."""
self.serve(listed=[payload(n, "Thing %d" % n) for n in range(1, 6)],
deps={1: [2], 2: [3], 3: [4], 4: [5]})
self.run_pull("1")
self.assertEqual(self.stored(), ["thing-1", "thing-2", "thing-3", "thing-4"],
"the default depth of 3 was not what was walked")
def test_depth_bounds_the_walk(self):
self.serve(listed=[payload(n, "Thing %d" % n) for n in range(1, 6)],
deps={1: [2], 2: [3], 3: [4], 4: [5]})
self.run_pull("1", "--depth", "1")
self.assertEqual(self.stored(), ["thing-1", "thing-2"])
def test_the_graph_hint_is_printed_when_there_is_a_graph(self):
self.blocked_pair()
out, _ = self.run_pull("10")
self.assertIn("issue_tree.py", out)
# --------------------------------------------------------------------------
# 2. --no-deps is the way out, and it is free
# --------------------------------------------------------------------------
class NoDepsOptsOutTest(PullDepsTestCase):
def test_no_deps_leaves_depends_empty(self):
self.blocked_pair()
self.run_pull("10", "--no-deps")
self.assertEqual(self.depends_of("second-thing"), [])
def test_no_deps_does_not_pull_the_blocker(self):
self.blocked_pair()
self.run_pull("10", "--no-deps")
self.assertEqual(self.stored(), ["second-thing"])
def test_no_deps_spends_no_extra_request(self):
"""The other half of the criterion: not the links, not the blocker.
One issue asked for, one request made."""
self.blocked_pair()
self.run_pull("10", "--no-deps")
self.assertEqual(self.fake.paths("/dependencies"), [])
self.assertEqual(self.fake.issue_gets(), ["%s/issues/10" % BASE])
def test_no_deps_prints_no_graph_hint(self):
self.blocked_pair()
out, _ = self.run_pull("10", "--no-deps")
self.assertNotIn("issue_tree.py", out)
# --------------------------------------------------------------------------
# 3. --deps is still accepted, and means nothing
# --------------------------------------------------------------------------
class DepsFlagIsANoOpTest(PullDepsTestCase):
def test_the_flag_is_still_accepted(self):
"""Existing calls and the /tea:sync command tables must not break."""
self.blocked_pair()
self.run_pull("10", "--deps")
self.assertEqual(self.depends_of("second-thing"), ["first-thing"])
def test_it_changes_nothing_about_the_run(self):
self.blocked_pair()
self.run_pull("10", "--deps")
with_flag = (self.stored(), self.depends_of("second-thing"),
list(self.fake.calls))
self.setUp()
self.blocked_pair()
self.run_pull("10")
self.assertEqual((self.stored(), self.depends_of("second-thing"),
list(self.fake.calls)), with_flag)
# --------------------------------------------------------------------------
# 4. one request per stored issue
# --------------------------------------------------------------------------
class TheCostIsOneRequestPerIssueTest(PullDepsTestCase):
def test_the_links_are_fetched_once_per_issue(self):
"""They fill `depends:` AND steer the walk; fetching them twice is
double the price the docstring quotes."""
self.blocked_pair()
self.run_pull("10")
self.assertEqual(self.fake.paths("/dependencies"),
["%s/issues/10/dependencies" % BASE,
"%s/issues/7/dependencies" % BASE])
def test_a_bulk_pull_costs_one_per_issue(self):
"""The number the docstring quotes: one list request, then one link
request per issue that lands in the store."""
self.serve(listed=[payload(n, "Thing %d" % n) for n in range(1, 21)])
self.run_pull("-q", "x")
self.assertEqual(len(self.fake.paths("/dependencies")), 20)
self.assertEqual(len(self.fake.paths("/issues")), 1)
def test_a_cached_issue_costs_its_links_and_nothing_else(self):
"""--cached stops the body and the thread, not the graph: a cached
issue's blockers can be missing from disk even when it is not."""
self.blocked_pair()
self.run_pull("10", "--no-deps") # only #10 on disk
self.fake.calls = []
self.run_pull("10", "--cached")
self.assertEqual(self.fake.paths("/dependencies"),
["%s/issues/10/dependencies" % BASE,
"%s/issues/7/dependencies" % BASE])
self.assertIn("first-thing", self.stored())
# --------------------------------------------------------------------------
# 5. filter mode follows blockers out of the selection
# --------------------------------------------------------------------------
class FilterModeFollowsOutwardTest(PullDepsTestCase):
def test_a_blocker_outside_the_filter_lands_in_the_store(self):
"""Documented as deliberate: a blocker is followed because a stored
issue named it, not because the filter selected it."""
self.serve(listed=[payload(1, "Selected thing")],
extra=[payload(99, "Outside thing")],
deps={1: [99]})
self.run_pull("-q", "x")
self.assertEqual(self.stored(), ["outside-thing", "selected-thing"])
self.assertEqual(self.depends_of("selected-thing"), ["outside-thing"])
def test_a_blocker_does_not_spend_the_limit(self):
"""--limit counts the selection's writes; the graph is not part of the
selection, so the store can legitimately hold more than N."""
self.serve(listed=[payload(n, "Thing %d" % n) for n in range(1, 5)],
extra=[payload(100 + n, "Blocker %d" % n) for n in range(1, 5)],
deps={n: [100 + n] for n in range(1, 5)})
self.run_pull("-q", "x", "--limit", "2")
self.assertEqual(self.stored(),
["blocker-1", "blocker-2", "thing-1", "thing-2"])
def test_a_closed_blocker_is_dropped_with_the_edge_to_it(self):
"""The documented exception. Closed is not a unit of work, so filter
mode drops it like any other closed issue — and `depends:` must not be
left pointing at a file that is not there."""
self.serve(listed=[payload(1, "Selected thing")],
extra=[payload(99, "Closed blocker", state="closed")],
deps={1: [99]})
self.run_pull("-q", "x")
self.assertEqual(self.stored(), ["selected-thing"])
self.assertEqual(self.depends_of("selected-thing"), [])
def test_a_closed_blocker_is_stored_in_key_mode(self):
"""An address is not a bulk read: `pull.py 1` has no closed rule."""
self.serve(listed=[payload(1, "Selected thing")],
extra=[payload(99, "Closed blocker", state="closed")],
deps={1: [99]})
self.run_pull("1")
self.assertEqual(self.stored(), ["closed-blocker", "selected-thing"])
def test_a_dropped_closed_issue_costs_no_link_request(self):
"""Nothing was stored for it, so there is no unit of work to complete
— and its own blockers are not dragged in behind it."""
self.serve(listed=[payload(1, "Closed thing", state="closed"),
payload(2, "Open thing")],
extra=[payload(50, "Blocker of the closed one")],
deps={1: [50]})
self.run_pull("-q", "x", "--state", "all")
self.assertEqual(self.fake.paths("/dependencies"),
["%s/issues/2/dependencies" % BASE])
self.assertEqual(self.stored(), ["open-thing"])
if __name__ == "__main__":
unittest.main()
+349
View File
@@ -0,0 +1,349 @@
#!/usr/bin/env python3
"""
`pull.py --limit N` bounds the WRITE, not the selection.
The bug this file exists to keep dead: the limit used to cut the list of
payloads before pull.py dropped the closed ones, so a milestone whose first
issues are closed spent the budget on issues that never reached disk —
`--limit 20` wrote twelve, and the docstring promised twenty.
What is asserted, in the order the fix has to hold it:
1. **The count is of files.** N issues under the filter that would be stored →
exactly N files, however many closed ones were enumerated on the way.
2. **Pagination serves the budget.** More pages are requested while the budget
is unfilled, and the page after the one that fills it is never requested.
3. **The scan is bounded.** A filter that matches almost only closed issues
stops after `_gitea.PAGE_SLACK` times the ideal page count, says so, and
returns short — it does not walk the tracker.
4. **`remote.py` is unchanged.** Its `--limit` still caps the listing, closed
issues included, because it writes nothing there is a limit for.
The transport is stubbed at `_gitea.api`, the way the other suites do it, and
the stub serves `page=` / `limit=` itself so the request pattern is a real
observation and not an assumption. No network, and no test writes to the
developer's store: each one builds its own in a `tempfile.TemporaryDirectory()`.
"""
import contextlib
import io
import os
import sys
import tempfile
import unittest
import urllib.parse
from unittest import mock
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
for _p in (os.path.join(_ROOT, "skills", "sync", "scripts"),
os.path.join(_ROOT, "skills", "issue", "scripts")):
if _p not in sys.path:
sys.path.insert(0, _p)
import _gitea # noqa: E402
import issue # noqa: E402
import map as gmap # noqa: E402
import pull # noqa: E402
import remote # noqa: E402
REPO = "claude-skills/tea"
BASE = "repos/%s" % REPO
BODY = """## Summary
Прозаическое описание задачи.
## Spec
skills/issue/references/format.md
## Acceptance criteria
- [ ] что-нибудь работает
"""
def payload(number, state="open", title=None, comments=0):
return {"number": number, "title": title or "Issue number %d" % number,
"body": BODY, "state": state, "comments": comments,
"labels": [{"name": "type/task"}], "assignees": [], "milestone": None,
"ref": "main", "updated_at": "2026-08-10T00:00:00Z",
"html_url": "https://git.example/%s/issues/%d" % (REPO, number),
"repository": {"full_name": REPO}}
def alternating(count, first="closed"):
"""`count` issues, every other one closed. The shape of the bug report:
closed issues sitting in front of the open ones, in page order."""
other = "open" if first == "closed" else "closed"
return [payload(n, first if n % 2 else other) for n in range(1, count + 1)]
class FakeTracker(object):
"""`tea api` answered from a list, with real pagination.
It slices on the `page=` and `limit=` it was given rather than ignoring
them, so "which pages were requested" is something the test can read off
`self.list_pages` instead of inferring."""
def __init__(self, payloads):
self.payloads = list(payloads)
self.list_pages = [] # (page, per_page), in request order
def api(self, login, endpoint, method="GET", payload=None,
payload_name=None, out_root=None, allow_fail=False):
path, _, qs = endpoint.partition("?")
q = urllib.parse.parse_qs(qs)
if path == "%s/issues" % BASE and method == "GET":
page, per = int(q["page"][0]), int(q["limit"][0])
self.list_pages.append((page, per))
return self.payloads[(page - 1) * per:(page - 1) * per + per]
if path.endswith("/comments"):
return []
if path.endswith("/dependencies"):
return []
if "/issues/" in path and method == "GET":
n = int(path.rsplit("/", 1)[1])
for p in self.payloads:
if p["number"] == n:
return p
return None
raise AssertionError("unstubbed call: %s %s" % (method, endpoint))
class PullLimitTestCase(unittest.TestCase):
"""A temp store, a fake tracker, no git and no network."""
def setUp(self):
self.tmp = tempfile.TemporaryDirectory(prefix="tea-limit-")
self.addCleanup(self.tmp.cleanup)
self.root = os.path.join(self.tmp.name, "tmp", "issues")
os.makedirs(self.root)
p = mock.patch.object(_gitea, "require_login", lambda: "test-login")
p.start()
self.addCleanup(p.stop)
# -- runners -----------------------------------------------------------
def serve(self, payloads):
self.fake = FakeTracker(payloads)
p = mock.patch.object(_gitea, "api", self.fake.api)
p.start()
self.addCleanup(p.stop)
return self.fake
def run_pull(self, *argv):
return self._run(pull, "pull.py", argv)
def run_remote(self, *argv):
return self._run(remote, "remote.py", argv)
def _run(self, mod, name, argv):
out, err = io.StringIO(), io.StringIO()
args = [name, "--repo", REPO, "--out", self.root] + list(argv)
with mock.patch.object(sys, "argv", args), \
contextlib.redirect_stdout(out), \
contextlib.redirect_stderr(err):
mod.main()
return out.getvalue(), err.getvalue()
# -- assertions --------------------------------------------------------
def stored(self):
return sorted(issue.all_ids(self.root))
def assertStoredCount(self, n, why=""):
got = self.stored()
self.assertEqual(len(got), n, "%d issue(s) in the store, wanted %d%s: %s"
% (len(got), n, why and "" + why, got))
# --------------------------------------------------------------------------
# 1. the count is of files
# --------------------------------------------------------------------------
class LimitCountsWritesTest(PullLimitTestCase):
def test_closed_issues_do_not_spend_the_budget(self):
"""The regression. Half the selection is closed and stands in front of
the open ones; the limit still buys ten files."""
self.serve(alternating(40))
self.run_pull("-q", "x", "--state", "all", "--limit", "10")
self.assertStoredCount(10)
def test_only_open_issues_landed(self):
self.serve(alternating(40))
self.run_pull("-q", "x", "--state", "all", "--limit", "10")
for id in self.stored():
self.assertEqual(issue.load(self.root, id).state, "open")
def test_the_dropped_ones_are_still_reported(self):
"""Enumerated-and-dropped is not silence: the closed ones seen on the
pages that were fetched are counted on stderr."""
self.serve(alternating(40))
_, err = self.run_pull("-q", "x", "--state", "all", "--limit", "10")
self.assertIn("closed issue(s) enumerated, not stored", err)
def test_a_closed_issue_already_in_the_store_spends_it(self):
"""It is refreshed rather than dropped — that is a write, so it counts.
The limit is on what the store holds when the run ends, and this issue
is in it."""
kept = issue.Issue(id="already-here", title="Already here", body=BODY,
labels=["type/task"], origin="gitea",
extra={"gitea": gmap.remote_key(REPO, 1)})
issue.save(self.root, kept)
_gitea.save_map(self.root, {gmap.remote_key(REPO, 1): "already-here"})
self.serve(alternating(40)) # #1 is closed, and is on disk
self.run_pull("-q", "x", "--state", "all", "--limit", "10")
self.assertStoredCount(10)
self.assertEqual(issue.load(self.root, "already-here").state, "closed",
"a stored issue must learn it was closed")
def test_state_closed_writes_closed_ones(self):
"""Nothing above may leak into the mode where closed IS the selection."""
self.serve([payload(n, "closed") for n in range(1, 21)])
self.run_pull("-q", "x", "--state", "closed", "--limit", "6")
self.assertStoredCount(6)
# --------------------------------------------------------------------------
# 2. pagination serves the budget
# --------------------------------------------------------------------------
class PaginationFollowsTheBudgetTest(PullLimitTestCase):
def test_more_pages_are_fetched_until_the_budget_is_full(self):
"""One page of ten holds five open issues, so ten files cost two."""
self.serve(alternating(40))
self.run_pull("-q", "x", "--state", "all", "--limit", "10")
self.assertStoredCount(10)
self.assertEqual([p for p, _ in self.fake.list_pages], [1, 2])
def test_the_page_after_the_last_needed_one_is_never_requested(self):
"""The budget fills inside page 2; page 3 exists and must not be asked
for. Bounding the write must not become fetching the whole repo."""
self.serve(alternating(200))
self.run_pull("-q", "x", "--state", "all", "--limit", "10")
self.assertEqual(len(self.fake.list_pages), 2,
"extra pages requested: %r" % (self.fake.list_pages,))
def test_an_unfiltered_selection_still_costs_one_page(self):
"""Nothing is dropped, so nothing changes: the old arithmetic holds."""
self.serve([payload(n) for n in range(1, 60)])
self.run_pull("-q", "x", "--limit", "10")
self.assertStoredCount(10)
self.assertEqual(len(self.fake.list_pages), 1)
def test_running_out_of_pages_gives_a_short_answer(self):
"""Six issues, three of them open, `--limit 10`: three files, no crash,
and no page beyond the last."""
self.serve(alternating(6))
self.run_pull("-q", "x", "--state", "all", "--limit", "10")
self.assertStoredCount(3)
self.assertEqual(len(self.fake.list_pages), 1)
# --------------------------------------------------------------------------
# 3. the scan is bounded
# --------------------------------------------------------------------------
class ScanIsBoundedTest(PullLimitTestCase):
def test_a_selection_of_only_closed_issues_stops_at_the_page_budget(self):
self.serve([payload(n, "closed") for n in range(1, 501)])
_, err = self.run_pull("-q", "x", "--state", "all", "--limit", "10")
self.assertStoredCount(0)
self.assertEqual(len(self.fake.list_pages), _gitea.PAGE_SLACK,
"the scan walked past its budget: %r" % (self.fake.list_pages,))
self.assertIn("short of --limit", err)
def test_a_full_budget_does_not_warn(self):
"""The warning means "there may be more"; it must not fire on a run
that got everything it asked for."""
self.serve(alternating(40))
_, err = self.run_pull("-q", "x", "--state", "all", "--limit", "10")
self.assertNotIn("short of --limit", err)
def test_a_selection_that_ran_out_does_not_warn(self):
"""Six issues in the repo and the server said so — that is an answer,
not a truncation."""
self.serve(alternating(6))
_, err = self.run_pull("-q", "x", "--state", "all", "--limit", "10")
self.assertNotIn("short of --limit", err)
# --------------------------------------------------------------------------
# 4. remote.py is the deliberate exception
# --------------------------------------------------------------------------
class RemoteListingIsUnchangedTest(PullLimitTestCase):
def test_the_listing_limit_still_counts_lines_not_writes(self):
"""remote.py writes nothing, so there is no write to bound: ten lines
out, closed ones among them, one request."""
self.serve(alternating(40))
out, _ = self.run_remote("-q", "x", "--state", "all", "--limit", "10")
numbered = [l for l in out.splitlines() if l.startswith("#")]
self.assertEqual(len(numbered), 10)
self.assertTrue(any("closed" in l for l in numbered),
"a listing that hides closed issues is not a listing")
self.assertEqual(len(self.fake.list_pages), 1)
def test_it_leaves_the_store_alone(self):
self.serve(alternating(40))
self.run_remote("-q", "x", "--state", "all", "--limit", "10")
self.assertStoredCount(0, "discovery wrote to the store")
# --------------------------------------------------------------------------
# the transport on its own
# --------------------------------------------------------------------------
class ListIssuesKeepTest(PullLimitTestCase):
"""`_gitea.list_issues` without a caller in front of it — the counting rule
is the transport's, and it is testable without a store."""
def list(self, payloads, **kw):
self.serve(payloads)
return _gitea.list_issues("test-login", BASE, state="all", **kw)
def test_without_keep_the_limit_caps_the_selection(self):
got, _ = self.list(alternating(40), limit=10)
self.assertEqual(len(got), 10)
def test_with_keep_the_limit_caps_the_kept(self):
got, _ = self.list(alternating(40), limit=10,
keep=lambda p: p["state"] == "open")
self.assertEqual(len([p for p in got if p["state"] == "open"]), 10)
def test_the_rejected_ones_come_back_too(self):
"""They were enumerated. The caller reports them; the transport does
not get to throw away what it did not count."""
got, _ = self.list(alternating(40), limit=10,
keep=lambda p: p["state"] == "open")
self.assertTrue([p for p in got if p["state"] == "closed"])
def test_a_limit_below_one_is_refused(self):
"""The page arithmetic divides by the page size, and a limit of zero
used to make that a traceback. It is a usage error, so it reads like
one."""
with self.assertRaises(SystemExit):
self.list(alternating(4), limit=0)
def test_pull_requests_never_count(self):
"""`matches` drops them, so they cannot spend the budget either."""
mixed = []
for n in range(1, 41):
p = payload(n)
if n % 2:
p["pull_request"] = {"merged": False}
mixed.append(p)
got, _ = self.list(mixed, limit=10, keep=lambda p: True)
self.assertEqual(len(got), 10)
self.assertFalse([p for p in got if p.get("pull_request")])
if __name__ == "__main__":
unittest.main()