Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion infra/github-backup/bin/ghb_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def inventory_owner(owner):
"""All repos visible to the gh token under one owner, via GraphQL."""
repos = []
after = None
seen_cursors = set()
while True:
variables = {"login": owner}
if after:
Expand Down Expand Up @@ -124,7 +125,18 @@ def inventory_owner(owner):
}
)
if conn["pageInfo"]["hasNextPage"]:
after = conn["pageInfo"]["endCursor"]
next_after = conn["pageInfo"]["endCursor"]
if (
not isinstance(next_after, str)
or not next_after
or next_after in seen_cursors
):
raise RuntimeError(
f"gh api graphql for {owner}: hasNextPage true with malformed "
f"endCursor {next_after!r}; expected a non-empty advancing cursor"
)
seen_cursors.add(next_after)
after = next_after
else:
return repos

Expand Down
60 changes: 60 additions & 0 deletions infra/github-backup/tests/test_inventory_owner.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
gh CLI failure -> owner-level error (control, pre-existing)
empty-but-visible owner -> zero repos, NO error (legit case)
pagination -> both pages combined, no error
malformed cursor -> bounded failure with owner-level error

Run: python3 infra/github-backup/tests/test_inventory_owner.py
"""
Expand Down Expand Up @@ -89,6 +90,25 @@ def stub(cmd, **kw):
return stub


def malformed_cursor_stub_factory(case):
calls = []
expected_calls = 2 if case == "repeated-cursor" else 1

def stub(cmd, **kw):
calls.append(cmd)
if len(calls) > expected_calls:
return R(1, "", "transport call bound exceeded")
if case == "null-cursor":
return R(0, payload([node("some-owner/repo1")], True, None))
if case == "empty-cursor":
return R(0, payload([node("some-owner/repo1")], True, ""))
if case == "repeated-cursor":
return R(0, payload([node("some-owner/repo1")], True, "cur1"))
raise AssertionError(f"unknown malformed cursor case: {case}")

return stub, calls, expected_calls


PASS = []


Expand Down Expand Up @@ -144,4 +164,44 @@ def check(name, cond):
finally:
ghb_common.run = saved_run

for case, malformed_cursor in [
("null-cursor", "None"),
("empty-cursor", "''"),
("repeated-cursor", "'cur1'"),
]:
stub, calls, expected_calls = malformed_cursor_stub_factory(case)
ghb_common.run = stub
repos, errors = ghb_common.inventory(["some-owner"])
check(f"{case}: no repos returned for failed owner", repos == {})
check(
f"{case}: owner-level cursor error recorded",
"some-owner" in errors
and "some-owner" in errors["some-owner"]
and malformed_cursor in errors["some-owner"],
)
check(
f"{case}: inventory transport calls bounded",
len(calls) == expected_calls,
)

calls.clear()
try:
ghb_common.inventory_owner("some-owner")
raised = False
message = ""
except RuntimeError as e:
raised = True
message = str(e)
check(f"{case}: inventory_owner raises RuntimeError", raised)
check(
f"{case}: exception names owner and malformed cursor",
"some-owner" in message and malformed_cursor in message,
)
check(
f"{case}: direct inventory transport calls bounded",
len(calls) == expected_calls,
)

ghb_common.run = saved_run

print(f"\n{sum(1 for _, ok in PASS if ok)}/{len(PASS)} checks passed")
Loading