R1 cleanup: collAnon and dead code gone, docs current, every peer's scenario in one sweep
Build / Build (push) Successful in 1m7s
Build / Build (push) Successful in 1m7s
Starts the completion program (docs/ROADMAP.md, "Owner decisions on finishing everything", 2026-10-17). - Dead code: the commented-out collAnon blocks (discussions, seeding, admin user lists, HTTP/3, the old signature middleware), the empty PrivaPubWorkersConfiguration, PublicAvatarUsersService and UserPolicyType, unused helpers, the second IPasswordHasher registration, the PasswordGenerator package, SignUpAsync/LoginAsync's unused invitation parameters. "Starting PrivaPub"; a signed-in login is sent to "/", not to "/discussions". - Stored fields nothing read: RootUser.Contacts and ResetPasswordToken*, the local Avatar.AvatarServer, EmailRecovery.RecoveryCode/RequestIP/CreationDate, MongoSettings.LogsDatabase, and AppConfiguration's upload/discussion keys, RequiresFirstTimeSetup and Version (old documents still load: MongoDB.Entities ignores extra elements). DmPost's indexes are no longer made. 49 collAnon keys leave FieldsNameResource in every language. - /clientapi/data/current-version answers the build's ref. UsersIds is a list that can't throw, and the admin routes leave the administrator out without mutating the request. - deploy.yml checks SecureMode on a name no persona can have, and the browser redirect on the directory's first local persona: @thepra is the administrator's own now. - Town: decePub's e2e remnants (expect, client.jsonl, the decePub column) and town/tests go; the dead gaps/load commands too. Mobilizon no longer keeps an event for a deleted test. - tools/pasture/sweep.sh runs every peer's scenario, batch by batch, as CLAUDE.md asks before a change to what PrivaPub sends. - Docs: CLAUDE.md's repo map, route table (every frozen route) and stale lines; ROADMAP's status (setup, v1.25.0, v1.26.0, the program C), the 2026-10-17 decisions, and dated notes where earlier text no longer holds; FEDERATION.md's FEP list (7888, f228, 7628, ae0c and 8967 supported) and the deploy's persona. Verified: dotnet build PrivaPub.sln -c Release (0 errors, no new warnings); bash -n on the touched scripts; py_compile on the town; the pasture's gts, mastodon and lemmy scenarios on this build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
This commit is contained in:
1 parent
6727e1817a
commit
ff5e635dda
71 files changed
+238
-4767
No files matched your search
@@ -1,13 +1,9 @@
|
||||
{
|
||||
"MongoSettings": {
|
||||
"Database": "PrivaPub",
|
||||
"LogsDatabase": "logs",
|
||||
"ConnectionString": "mongodb://mongo:27017/?directConnection=true"
|
||||
},
|
||||
"AppConfiguration": {
|
||||
"Version": "0.0.0",
|
||||
"MaxAllowedUploadFiles": 3,
|
||||
"MaxAllowedFileSize": 2097152,
|
||||
"SupportedLanguages": [ "en" ],
|
||||
"BackendBaseAddress": "https://privapub.test",
|
||||
"FrontendBaseAddress": "https://privapub.test",
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# Drives PrivaPub and each peer (started by run.sh) through each other's federation, as their users would, then checks
|
||||
# that PrivaPub's statistics saw it all without naming anyone.
|
||||
# usage: tools/pasture/interop.sh [peer...] peers: gts (default), mastodon, misskey, sharkey, akkoma, lemmy,
|
||||
# peertube, crawler
|
||||
# usage: tools/pasture/interop.sh [scenario...] gts (default), and every other file in scenarios/: a peer's (with
|
||||
# that peer up) or one across peers (threads, relay, restore, ...)
|
||||
# tools/pasture/sweep.sh [A|B|C|D ...] every peer's scenario, a batch of peers at a time
|
||||
set -uo pipefail
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
. "$here/lib/interop.sh"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
# usage: tools/pasture/run.sh up [peer...] | add <peer...> | down [--purge] | logs <name> | ps | stats
|
||||
# up starts everything afresh (PrivaPub republished, Mongo empty); add starts more peers next to a running pasture.
|
||||
# peers: gts (default), mastodon, misskey, sharkey, akkoma, lemmy, decepub, and every other file in peers/
|
||||
# tools/pasture/sweep.sh runs every peer's scenario, a batch of peers at a time (up, interop.sh, down).
|
||||
set -euo pipefail
|
||||
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/pasture.sh"
|
||||
|
||||
|
||||
@@ -192,7 +192,8 @@ curl -s -o /dev/null -X POST -H "$LH" "$P/api/v1/follow_requests/$requester/auth
|
||||
until_true 20 '[ "$(gcurl -s -H "$GH" "$G/api/v1/accounts/relationships?id[]=$locked_on_gts" | j "print(d[0][\"following\"])")" = "True" ]' && ok "an authorized request makes gtsuser a follower" || ko "authorization not applied on GoToSocial"
|
||||
|
||||
echo "smoke"
|
||||
"$here/../smoke/mastodon-api.sh" "$P" "$PT" >/dev/null 2>&1 && ok "the deploy's Mastodon smoke check passes, signed in" || ko "the Mastodon smoke check fails"
|
||||
# tools/smoke/mastodon-api.sh: the deploy runs it anonymously; here a persona's token adds its signed-in checks
|
||||
"$here/../smoke/mastodon-api.sh" "$P" "$PT" >/dev/null 2>&1 && ok "the Mastodon smoke check passes, anonymously and with a persona's token" || ko "the Mastodon smoke check fails"
|
||||
|
||||
echo "interaction policies"
|
||||
# gtsuser's post asks before anyone but its author replies or likes, and lets nobody else boost it: PrivaPub shows the
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Mobilizon 5.2.4: a group and its events. alice_mobilizon follows the group, which announces the events its members
|
||||
# organise; an event arrives as an Event with its dates and place; she joins it and leaves; comments both ways; an edit
|
||||
# and comments closed; deletes; a group post; an event kept for decePubClient's e2e; the unfollow. An event made through its API without options has its comments closed
|
||||
# (`commentsEnabled: false`), which PrivaPub honours, so this one opens them. Mobilizon's API is GraphQL (/api), signed in by its login mutation; what it holds is read from
|
||||
# its PostGIS (database mobilizon).
|
||||
# and comments closed; deletes; a group post; the unfollow. An event made through its API without options has its
|
||||
# comments closed (`commentsEnabled: false`), which PrivaPub honours, so this one opens them. Mobilizon's API is GraphQL
|
||||
# (/api), signed in by its login mutation; what it holds is read from its PostGIS (database mobilizon).
|
||||
MZ=https://mobilizon.test:6443
|
||||
mz_sql() { podman exec pasture-postgis psql -U pasture -d mobilizon -tAc "$1" 2>/dev/null; }
|
||||
# mz <query> [variables as JSON]: a GraphQL request as mzuser, its JSON answer
|
||||
@@ -91,16 +91,6 @@ mz 'mutation($g:ID!,$t:String!){createPost(attributedToId:$g,title:$t,body:"<p>t
|
||||
"{\"g\":\"$group\",\"t\":\"$post_title\"}" >/dev/null
|
||||
until_true 60 '[ -n "$(p_home_has "$post_title")" ]' && ok "the group's post reaches alice_mobilizon's home, with its title" || ko "the group's post never arrived"
|
||||
|
||||
echo " an event kept for decePub's tests"
|
||||
# decePubClient's e2e joins it from its card (tests/e2e/Actions/EventTests.cs); the one a run before this one kept goes
|
||||
kept="$here/.state/mobilizon-kept-event.json"
|
||||
[ -s "$kept" ] && mz 'mutation($e:ID!){deleteEvent(eventId:$e){id}}' "{\"e\":\"$(j "print(d['id'])" < "$kept")\"}" >/dev/null
|
||||
kept_title="A concert kept for decePub $(date +%s)"
|
||||
kept_begins=$(date -u -d '+10 days' +%Y-%m-%dT19:00:00Z); kept_ends=$(date -u -d '+10 days' +%Y-%m-%dT22:00:00Z)
|
||||
mz 'mutation($t:String!,$b:DateTime!,$e:DateTime!,$o:ID!,$g:ID){createEvent(title:$t,description:"<p>come and listen</p>",beginsOn:$b,endsOn:$e,organizerActorId:$o,attributedToId:$g,visibility:PUBLIC,joinOptions:FREE,options:{commentModeration:ALLOW_ALL},physicalAddress:{description:"Piazza del Popolo",locality:"Roma",country:"Italia",geom:"12.4763;41.9109"}){id url}}' \
|
||||
"{\"t\":\"$kept_title\",\"b\":\"$kept_begins\",\"e\":\"$kept_ends\",\"o\":\"$me\",\"g\":\"$group\"}" | j "print(json.dumps(d['data']['createEvent']))" > "$kept"
|
||||
until_true 60 '[ -n "$(p_home_has "$kept_title")" ]' && ok "an event kept for decePub's tests reaches PrivaPub" || ko "the kept event never arrived"
|
||||
|
||||
echo " unfollow"
|
||||
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$group_on_p/unfollow"
|
||||
until_true 45 '[ "$(mz_sql "select count(*) from followers f join actors a on a.id = f.actor_id where a.url like '"'%alice_mobilizon%'"'")" = "0" ]' \
|
||||
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
# Every peer's scenario, a batch of peers at a time: a change to what PrivaPub sends (the @context, an actor's
|
||||
# properties, an activity's shape, a delivery's headers) runs against all of them before it is committed, since one
|
||||
# reader stricter than the rest fails on it alone. Each batch starts its own pasture afresh (run.sh up), runs its
|
||||
# scenarios (interop.sh) and takes the pasture down (run.sh down), and the next batch runs whatever the one before did.
|
||||
# PrivaPub runs with SecureMode on, as in production, unless PRIVAPUB_ENV says otherwise; DOTNET and PASTURE_PORT reach
|
||||
# run.sh as they are. Each batch's output is kept in out/sweep-<batch>.log.
|
||||
# usage: tools/pasture/sweep.sh [A|B|C|D ...] (default: every batch, in that order)
|
||||
# A gts mastodon misskey sharkey akkoma, with relay and aoderelay; scenarios: those five, threads followsync pins
|
||||
# moves relay (relay's covers both relays)
|
||||
# B lemmy lemmy19 piefed mbin nodebb
|
||||
# C peertube pixelfed vernissage wordpress ghost writefreely owncast castopod funkwhale
|
||||
# D friendica mobilizon gancio bookwyrm smithereen mitra snac ktistec forte hubzilla
|
||||
# Not swept: restore (run it alone: it restores the whole server), crawler (it needs the crawler's own settings) and
|
||||
# persona-archive; hollo, iceshrimp, pleroma, flood and decepub have no scenario of their own.
|
||||
set -uo pipefail
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
export PRIVAPUB_ENV=${PRIVAPUB_ENV:-Federation__SecureMode=true}
|
||||
|
||||
batch_peers() {
|
||||
case "$1" in
|
||||
A) echo "gts mastodon misskey sharkey akkoma relay aoderelay" ;;
|
||||
B) echo "lemmy lemmy19 piefed mbin nodebb" ;;
|
||||
C) echo "peertube pixelfed vernissage wordpress ghost writefreely owncast castopod funkwhale" ;;
|
||||
D) echo "friendica mobilizon gancio bookwyrm smithereen mitra snac ktistec forte hubzilla" ;;
|
||||
esac
|
||||
}
|
||||
batch_scenarios() {
|
||||
case "$1" in
|
||||
A) echo "gts mastodon misskey sharkey akkoma threads followsync pins moves relay" ;;
|
||||
*) batch_peers "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
[ $# -gt 0 ] || set -- A B C D
|
||||
for batch in "$@"; do
|
||||
[ -n "$(batch_peers "$batch")" ] || { echo "no such batch: $batch (A, B, C or D)" >&2; exit 2; }
|
||||
for peer in $(batch_peers "$batch"); do
|
||||
[ -f "$here/peers/$peer.sh" ] || { echo "batch $batch: no such peer: $peer" >&2; exit 2; }
|
||||
done
|
||||
for scenario in $(batch_scenarios "$batch"); do
|
||||
[ -f "$here/scenarios/$scenario.sh" ] || { echo "batch $batch: no scenario for $scenario" >&2; exit 2; }
|
||||
done
|
||||
done
|
||||
|
||||
mkdir -p "$here/out"
|
||||
summary=(); bad=0
|
||||
for batch in "$@"; do
|
||||
log="$here/out/sweep-$batch.log"
|
||||
echo "== batch $batch: $(batch_peers "$batch")"
|
||||
# shellcheck disable=SC2046
|
||||
if ! "$here/run.sh" up $(batch_peers "$batch") 2>&1 | tee "$log"; then
|
||||
summary+=("$batch the pasture did not start (out/sweep-$batch.log)"); bad=1
|
||||
"$here/run.sh" down || true
|
||||
continue
|
||||
fi
|
||||
# shellcheck disable=SC2046
|
||||
"$here/interop.sh" $(batch_scenarios "$batch") 2>&1 | tee -a "$log"
|
||||
status=${PIPESTATUS[0]}
|
||||
"$here/run.sh" down || true
|
||||
passed=$(grep -c '^ ok ' "$log"); failed=$(grep -c '^ FAIL ' "$log"); xfailed=$(grep -c '^ xf ' "$log")
|
||||
line="$batch ok $passed ko $failed xf $xfailed ($(batch_scenarios "$batch"))"
|
||||
if ! grep -qE '^[0-9]+ passed, [0-9]+ failed, [0-9]+ expected failures$' "$log"; then
|
||||
line="$line stopped before its end (exit $status)"; bad=1
|
||||
fi
|
||||
[ "$failed" = 0 ] || bad=1
|
||||
summary+=("$line")
|
||||
done
|
||||
|
||||
echo; echo "sweep:"
|
||||
printf ' %s\n' "${summary[@]}"
|
||||
[ "$bad" = 0 ]
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# The town: a fake community seeded across every pasture peer, then checked for coherence (see town/town.py).
|
||||
# usage: tools/pasture/town.sh <command> [args] commands: selftest, seed, check, report, backlog, gaps, drive, stored, seen,
|
||||
# usage: tools/pasture/town.sh <command> [args] commands: selftest, seed, check, report, backlog, drive, stored, seen,
|
||||
# renew <peer> (its accounts signed in again)
|
||||
exec python3 "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/town/town.py" "$@"
|
||||
@@ -1,5 +1,5 @@
|
||||
"""docs/INTEROP-BACKLOG.md from the latest town results, the bash scenarios and decePub's e2e cells: new failures (to fix
|
||||
or to record in gaps.json), known gaps by phase, known gaps now passing (close them), and the scenarios' failures.
|
||||
"""docs/INTEROP-BACKLOG.md from the latest town results and the bash scenarios: new failures (to fix or to record in
|
||||
gaps.json), known gaps by phase, known gaps now passing (close them), and the scenarios' failures.
|
||||
|
||||
town.sh backlog [run] [--write]"""
|
||||
import json
|
||||
@@ -27,9 +27,8 @@ def main(args):
|
||||
with open(os.path.join(run_dir, "results.json")) as f:
|
||||
result = json.load(f)
|
||||
gaps = {g["id"]: g for g in check.load_gaps()}
|
||||
client = check.classify(check.latest_client([dict(c) for c in load_jsonl(os.path.join(seed.OUT, "client.jsonl"))]), list(gaps.values()))
|
||||
scenarios = load_jsonl(os.path.join(seed.OUT, "scenarios.jsonl"))
|
||||
checks = result["checks"] + client
|
||||
checks = result["checks"]
|
||||
|
||||
new = defaultdict(list)
|
||||
known = defaultdict(list)
|
||||
|
||||
@@ -478,21 +478,6 @@ def canary_hits(platform, canaries):
|
||||
return sorted(c for c in canaries if c in dump)
|
||||
|
||||
|
||||
def latest_client(lines):
|
||||
"""decePub's e2e cells (out/client.jsonl, appended by every run) as each cell's latest run left them: the rows from
|
||||
within an hour of the cell's newest, the newest per test. A run that picked other accounts leaves no stale failure."""
|
||||
from datetime import datetime, timedelta
|
||||
at = lambda c: datetime.fromisoformat(c["t"].replace("Z", "+00:00"))
|
||||
newest = {}
|
||||
for c in lines:
|
||||
newest[c["cell"]] = max(newest.get(c["cell"], at(c)), at(c))
|
||||
latest = {}
|
||||
for c in sorted(lines, key=at):
|
||||
if at(c) >= newest[c["cell"]] - timedelta(hours=1):
|
||||
latest[(c["cell"], c.get("ref"))] = c
|
||||
return list(latest.values())
|
||||
|
||||
|
||||
def load_gaps():
|
||||
try:
|
||||
with open(os.path.join(HERE, "gaps.json")) as f:
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
"""What the town's objects should look like to a reader, for decePub's e2e tests: for every object PrivaPub holds and some
|
||||
town persona may see, its ref, URI, PrivaPub id, origin, kind, visibility, the text marker, content warning, media alt
|
||||
texts, poll options, quoted and parent URIs, and the personas that may see it.
|
||||
|
||||
town.sh expect [run] [--persona <username>]"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
import check
|
||||
import seed
|
||||
from dialects import driver
|
||||
|
||||
|
||||
def edited_cw(world, ref, args):
|
||||
"""The warning an object shows now: an edit made before plans kept warnings replaced the post without one."""
|
||||
edit = next((s for s in world.planner.steps if s["verb"] == "edit" and s["args"]["target"] == ref), None)
|
||||
if edit is None or ref not in world.edited:
|
||||
return args.get("cw")
|
||||
return edit["args"].get("cw")
|
||||
|
||||
|
||||
def objects(world):
|
||||
steps = {s["ref"]: s for s in world.planner.steps if s.get("ref") and s["verb"] == "post"}
|
||||
held = driver("privapub").stored([o["uri"] for o in world.objects.values()])
|
||||
personas = [k for k in world.planner.accounts if k.startswith("privapub/")]
|
||||
out = []
|
||||
for ref, o in sorted(world.objects.items(), key=lambda kv: int(kv[0][1:])):
|
||||
row = held.get(o["uri"])
|
||||
if not row or not row.exists or row.deleted or ref in world.deleted:
|
||||
continue
|
||||
recipients = world.recipients(o)
|
||||
readers = [p.split("/")[1] for p in personas
|
||||
if p == o["author"] or p in recipients or o["visibility"] in ("public", "unlisted", "community")]
|
||||
if not readers:
|
||||
continue
|
||||
args = steps[ref]["args"]
|
||||
out.append({
|
||||
"ref": ref, "uri": o["uri"], "id": row.local_id, "origin": o["origin"], "author": o["author"],
|
||||
"kind": o["kind"], "visibility": o["visibility"], "marker": f"({ref})", "cw": edited_cw(world, ref, args),
|
||||
"alts": [m["alt"] for m in args.get("media", [])], "poll": (args.get("poll") or {}).get("options"),
|
||||
"quote": world.objects.get(o.get("quote") or "", {}).get("uri"),
|
||||
"parent": world.objects.get(o.get("parent") or "", {}).get("uri"),
|
||||
"edited": ref in world.edited, "title": args.get("title"), "readers": readers,
|
||||
"followers_of_author": sorted(p.split("/")[1] for p in world.follows.get(o["author"], set()) if p.startswith("privapub/")),
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def main(args):
|
||||
name = next((a for a in args if not a.startswith("--") and not a.startswith("privapub/")), "village")
|
||||
persona = None
|
||||
if "--persona" in args:
|
||||
persona = args[args.index("--persona") + 1].split("/")[-1]
|
||||
world = check.World(seed.run_dir(name))
|
||||
items = objects(world)
|
||||
if persona:
|
||||
items = [i for i in items if persona in i["readers"]]
|
||||
print(json.dumps(items, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
@@ -75,7 +75,7 @@
|
||||
"code": "misskey-dev/misskey packages/backend/src/core/NoteDeleteService.ts: only repliesCount is decremented on delete",
|
||||
"opened": "2026-10-05",
|
||||
"status": "open",
|
||||
"note": "the renote row is gone after our Undo (checked in its database); renoteCount stays up. Sharkey decrements it."
|
||||
"note": "the renote row is gone after our Undo (checked in its database); renoteCount stays up. Sharkey decrements it. Its match named decePub's e2e cells (client.action.boost), deleted 2026-10-17, so it matches nothing until a town cell checks Misskey's renote count."
|
||||
},
|
||||
{
|
||||
"id": "G-0006",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The plan: a fake community drawn from a spec and its seed, the same on every run (tests/test_gen.py pins its hash).
|
||||
"""The plan: a fake community drawn from a spec and its seed, the same on every run.
|
||||
|
||||
A plan is a list of steps, each {n, phase, round, actor, verb, args, ref}. Accounts are named "<platform>/<username>",
|
||||
objects by a ref ("p12"); the seeder turns refs into URIs as it goes. Phases run in order:
|
||||
|
||||
@@ -12,7 +12,7 @@ import gen
|
||||
DIRS = ["in", "out", "via", "control", "local"]
|
||||
DIR_LABEL = {"in": "into PrivaPub", "out": "from PrivaPub", "via": "through PrivaPub", "control": "peer to peer",
|
||||
"local": "on its own server"}
|
||||
GROUPS = [("client", "decePub"), ("deliver", "Delivery"), ("relay", "Replies passed on"), ("confine", "Confinement"),
|
||||
GROUPS = [("deliver", "Delivery"), ("relay", "Replies passed on"), ("confine", "Confinement"),
|
||||
("see", "Seen by the audience"), ("hide", "Hidden from outsiders"), ("count", "Counts"), ("thread", "Threads"),
|
||||
("edit", "Edits"), ("delete", "Deletes"), ("graph", "Follows"), ("privacy", "Privacy")]
|
||||
STATUS_ORDER = ["fail", "xpass", "xfail", "pass"]
|
||||
@@ -84,7 +84,7 @@ def render(result, scenarios, title):
|
||||
cells = result["cells"]
|
||||
totals = Counter(result["totals"])
|
||||
observers = [p for p in gen.HOSTS if any(k.split("|")[2] == p for k in cells)] + \
|
||||
[o for o in ("*", "decepub") if any(k.split("|")[2] == o for k in cells)]
|
||||
[o for o in ("*",) if any(k.split("|")[2] == o for k in cells)]
|
||||
features = sorted({k.split("|")[0] for k in cells})
|
||||
out = [f"<title>{html.escape(title)}</title>",
|
||||
'<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=IBM+Plex+Sans:wght@400;600&display=swap">',
|
||||
@@ -169,19 +169,6 @@ def main(args):
|
||||
if os.path.exists(path):
|
||||
with open(path) as f:
|
||||
scenarios = [json.loads(line) for line in f if line.strip()]
|
||||
# a client's cells, when any, join the servers' in the same matrix, under the observer "decepub"
|
||||
client = os.path.join(seed.OUT, "client.jsonl")
|
||||
if os.path.exists(client):
|
||||
import check
|
||||
with open(client) as f:
|
||||
lines = [json.loads(line) for line in f if line.strip()]
|
||||
checks = check.classify(check.latest_client(lines), check.load_gaps())
|
||||
for cell, v in check.summarise(checks).items():
|
||||
result["cells"][cell] = v
|
||||
from collections import Counter
|
||||
totals = Counter(result["totals"])
|
||||
totals.update(Counter(c["status"] for c in checks))
|
||||
result["totals"] = totals
|
||||
page = render(result, scenarios, "PrivaPub Town Matrix")
|
||||
target = os.path.join(run_dir, "report.html")
|
||||
with open(target, "w") as f:
|
||||
|
||||
Whitespace-only changes.
@@ -1,79 +0,0 @@
|
||||
"""The town's own logic, with no pasture: python3 -m unittest discover -s tools/pasture/town/tests"""
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import check # noqa: E402
|
||||
import gen # noqa: E402
|
||||
from core.rng import Rng # noqa: E402
|
||||
|
||||
|
||||
class RngTests(unittest.TestCase):
|
||||
def test_the_same_seed_and_name_give_the_same_stream(self):
|
||||
a, b = Rng(7, "x"), Rng(7, "x")
|
||||
self.assertEqual([a.next64() for _ in range(5)], [b.next64() for _ in range(5)])
|
||||
|
||||
def test_substreams_do_not_disturb_each_other(self):
|
||||
root = Rng(7)
|
||||
before = Rng(7).sub("graph").next64()
|
||||
root.sub("content").next64()
|
||||
self.assertEqual(before, root.sub("graph").next64())
|
||||
|
||||
def test_below_stays_in_range(self):
|
||||
r = Rng(1)
|
||||
self.assertTrue(all(0 <= r.below(3) < 3 for _ in range(1000)))
|
||||
|
||||
|
||||
class PlanTests(unittest.TestCase):
|
||||
def test_a_spec_always_plans_the_same(self):
|
||||
spec = gen.load("village")
|
||||
self.assertEqual(gen.digest(gen.plan(spec)), gen.digest(gen.plan(spec)))
|
||||
|
||||
def test_every_post_names_its_marker_and_every_reply_a_parent_planned_before_it(self):
|
||||
planner = gen.Planner(gen.load("village"))
|
||||
steps = planner.plan()
|
||||
made = set()
|
||||
for s in steps:
|
||||
if s["verb"] != "post":
|
||||
continue
|
||||
self.assertIn(f"({s['ref']})", s["args"]["text"])
|
||||
if s["args"].get("reply_to"):
|
||||
self.assertIn(s["args"]["reply_to"], made)
|
||||
made.add(s["ref"])
|
||||
|
||||
def test_lemmy_accounts_never_follow_people_and_platforms_get_only_what_they_can_do(self):
|
||||
steps = gen.plan(gen.load("village"))
|
||||
for s in steps:
|
||||
platform = s["actor"].split("/")[0]
|
||||
if s["verb"] == "follow":
|
||||
self.assertNotEqual("lemmy", platform)
|
||||
if s["verb"] in ("react", "edit", "vote", "boost"):
|
||||
self.assertIn({"react": "react", "edit": "edit", "vote": "vote", "boost": "boost"}[s["verb"]], gen.CAPS[platform])
|
||||
|
||||
def test_a_stored_plan_rebuilds_the_same_world(self):
|
||||
planner = gen.Planner(gen.load("village"))
|
||||
planner.plan()
|
||||
stored = gen.Stored(gen.snapshot(planner))
|
||||
self.assertEqual(planner.objects.keys(), stored.objects.keys())
|
||||
self.assertEqual(planner.objects["p1"]["mentions"], stored.objects["p1"]["mentions"])
|
||||
|
||||
|
||||
class GapTests(unittest.TestCase):
|
||||
def test_a_known_gap_turns_failures_into_xfail_and_passes_into_xpass(self):
|
||||
gaps = [{"id": "G-9", "match": {"feature": r"count\.like\..*", "observer": "privapub"}, "status": "open"}]
|
||||
checks = check.classify([
|
||||
{"feature": "count.like.followers", "observer": "privapub", "ok": False},
|
||||
{"feature": "count.like.public", "observer": "privapub", "ok": True},
|
||||
{"feature": "count.like.public", "observer": "gts", "ok": False},
|
||||
], gaps)
|
||||
self.assertEqual(["xfail", "xpass", "fail"], [c["status"] for c in checks])
|
||||
|
||||
def test_a_closed_gap_matches_nothing(self):
|
||||
gaps = [{"id": "G-9", "match": {"feature": ".*"}, "status": "closed"}]
|
||||
self.assertEqual("fail", check.classify([{"feature": "x", "ok": False}], gaps)[0]["status"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -59,7 +59,7 @@ def main(argv):
|
||||
s = state.session(args[0], args[1])
|
||||
print(json.dumps(driver(args[0]).seen(s, args[2:])))
|
||||
return 0
|
||||
if cmd in ("plan", "seed", "check", "report", "backlog", "gaps", "expect", "load"):
|
||||
if cmd in ("plan", "seed", "check", "report", "backlog"):
|
||||
module = __import__(cmd if cmd not in ("plan",) else "gen")
|
||||
return module.main(args)
|
||||
print(f"unknown command {cmd}", file=sys.stderr)
|
||||
|
||||
Reference in new issue
Block a user