270 lines
9.4 KiB
Python
Executable File
270 lines
9.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
|
||
from __future__ import division
|
||
import argparse
|
||
import re
|
||
import sys
|
||
|
||
# Usage: $ ./are-we-synapse-yet.py [-v] results.tap
|
||
# This script scans a results.tap file from Dendrite's CI process and spits out
|
||
# a rating of how close we are to Synapse parity, based purely on SyTests.
|
||
# The main complexity is grouping tests sensibly into features like 'Registration'
|
||
# and 'Federation'. Then it just checks the ones which are passing and calculates
|
||
# percentages for each group. Produces results like:
|
||
#
|
||
# Client-Server APIs: 29% (196/666 tests)
|
||
# -------------------
|
||
# Registration : 62% (20/32 tests)
|
||
# Login : 7% (1/15 tests)
|
||
# V1 CS APIs : 10% (3/30 tests)
|
||
# ...
|
||
#
|
||
# or in verbose mode:
|
||
#
|
||
# Client-Server APIs: 29% (196/666 tests)
|
||
# -------------------
|
||
# Registration : 62% (20/32 tests)
|
||
# ✓ GET /register yields a set of flows
|
||
# ✓ POST /register can create a user
|
||
# ✓ POST /register downcases capitals in usernames
|
||
# ...
|
||
#
|
||
# You can also tack `-v` on to see exactly which tests each category falls under.
|
||
|
||
test_mappings = {
|
||
"nsp": "Non-Spec API",
|
||
"unk": "Unknown API (no group specified)",
|
||
"app": "Application Services API",
|
||
"msc": "MSCs",
|
||
"f": "Federation", # flag to mark test involves federation
|
||
|
||
"federation_apis": {
|
||
"fky": "Key API",
|
||
"fsj": "send_join API",
|
||
"fmj": "make_join API",
|
||
"fsl": "send_leave API",
|
||
"fiv": "Invite API",
|
||
"fqu": "Query API",
|
||
"frv": "room versions",
|
||
"fau": "Auth",
|
||
"fbk": "Backfill API",
|
||
"fme": "get_missing_events API",
|
||
"fst": "State APIs",
|
||
"fpb": "Public Room API",
|
||
"fdk": "Device Key APIs",
|
||
"fed": "Federation API",
|
||
"fsd": "Send-to-Device APIs",
|
||
},
|
||
|
||
"client_apis": {
|
||
"reg": "Registration",
|
||
"log": "Login",
|
||
"lox": "Logout",
|
||
"v1s": "V1 CS APIs",
|
||
"csa": "Misc CS APIs",
|
||
"pro": "Profile",
|
||
"dev": "Devices",
|
||
"dvk": "Device Keys",
|
||
"dkb": "Device Key Backup",
|
||
"xsk": "Cross-signing Keys",
|
||
"pre": "Presence",
|
||
"crm": "Create Room",
|
||
"syn": "Sync API",
|
||
"rmv": "Room Versions",
|
||
"rst": "Room State APIs",
|
||
"pub": "Public Room APIs",
|
||
"mem": "Room Membership",
|
||
"ali": "Room Aliases",
|
||
"jon": "Joining Rooms",
|
||
"lev": "Leaving Rooms",
|
||
"inv": "Inviting users to Rooms",
|
||
"ban": "Banning users",
|
||
"snd": "Sending events",
|
||
"get": "Getting events for Rooms",
|
||
"rct": "Receipts",
|
||
"red": "Read markers",
|
||
"med": "Media APIs",
|
||
"cap": "Capabilities API",
|
||
"typ": "Typing API",
|
||
"psh": "Push APIs",
|
||
"acc": "Account APIs",
|
||
"eph": "Ephemeral Events",
|
||
"plv": "Power Levels",
|
||
"xxx": "Redaction",
|
||
"3pd": "Third-Party ID APIs",
|
||
"gst": "Guest APIs",
|
||
"ath": "Room Auth",
|
||
"fgt": "Forget APIs",
|
||
"ctx": "Context APIs",
|
||
"upg": "Room Upgrade APIs",
|
||
"tag": "Tagging APIs",
|
||
"sch": "Search APIs",
|
||
"oid": "OpenID API",
|
||
"std": "Send-to-Device APIs",
|
||
"adm": "Server Admin API",
|
||
"ign": "Ignore Users",
|
||
"udr": "User Directory APIs",
|
||
"jso": "Enforced canonical JSON",
|
||
},
|
||
}
|
||
|
||
# optional 'not ' with test number then anything but '#'
|
||
re_testname = re.compile(r"^(not )?ok [0-9]+ ([^#]+)")
|
||
|
||
# Parses lines like the following:
|
||
#
|
||
# SUCCESS: ok 3 POST /register downcases capitals in usernames
|
||
# FAIL: not ok 54 (expected fail) POST /createRoom creates a room with the given version
|
||
# SKIP: ok 821 Multiple calls to /sync should not cause 500 errors # skip lack of can_post_room_receipts
|
||
# EXPECT FAIL: not ok 822 (expected fail) Guest user can call /events on another world_readable room (SYN-606) # TODO expected fail
|
||
#
|
||
# Only SUCCESS lines are treated as success, the rest are not implemented.
|
||
#
|
||
# Returns a dict like:
|
||
# { name: "...", ok: True }
|
||
def parse_test_line(line):
|
||
if not line.startswith("ok ") and not line.startswith("not ok "):
|
||
return
|
||
re_match = re_testname.match(line)
|
||
test_name = re_match.groups()[1].replace("(expected fail) ", "").strip()
|
||
test_pass = False
|
||
if line.startswith("ok ") and not "# skip " in line:
|
||
test_pass = True
|
||
return {
|
||
"name": test_name,
|
||
"ok": test_pass,
|
||
}
|
||
|
||
# Prints the stats for a complete section.
|
||
# header_name => "Client-Server APIs"
|
||
# gid_to_tests => { gid: { <name>: True|False }}
|
||
# gid_to_name => { gid: "Group Name" }
|
||
# verbose => True|False
|
||
# Produces:
|
||
# Client-Server APIs: 29% (196/666 tests)
|
||
# -------------------
|
||
# Registration : 62% (20/32 tests)
|
||
# Login : 7% (1/15 tests)
|
||
# V1 CS APIs : 10% (3/30 tests)
|
||
# ...
|
||
# or in verbose mode:
|
||
# Client-Server APIs: 29% (196/666 tests)
|
||
# -------------------
|
||
# Registration : 62% (20/32 tests)
|
||
# ✓ GET /register yields a set of flows
|
||
# ✓ POST /register can create a user
|
||
# ✓ POST /register downcases capitals in usernames
|
||
# ...
|
||
def print_stats(header_name, gid_to_tests, gid_to_name, verbose):
|
||
subsections = [] # Registration: 100% (13/13 tests)
|
||
subsection_test_names = {} # 'subsection name': ["✓ Test 1", "✓ Test 2", "× Test 3"]
|
||
total_passing = 0
|
||
total_tests = 0
|
||
for gid, tests in gid_to_tests.items():
|
||
group_total = len(tests)
|
||
if group_total == 0:
|
||
continue
|
||
group_passing = 0
|
||
test_names_and_marks = []
|
||
for name, passing in tests.items():
|
||
if passing:
|
||
group_passing += 1
|
||
test_names_and_marks.append(f"{'✓' if passing else '×'} {name}")
|
||
|
||
total_tests += group_total
|
||
total_passing += group_passing
|
||
pct = "{0:.0f}%".format(group_passing/group_total * 100)
|
||
line = "%s: %s (%d/%d tests)" % (gid_to_name[gid].ljust(25, ' '), pct.rjust(4, ' '), group_passing, group_total)
|
||
subsections.append(line)
|
||
subsection_test_names[line] = test_names_and_marks
|
||
|
||
pct = "{0:.0f}%".format(total_passing/total_tests * 100)
|
||
print("%s: %s (%d/%d tests)" % (header_name, pct, total_passing, total_tests))
|
||
print("-" * (len(header_name)+1))
|
||
for line in subsections:
|
||
print(" %s" % (line,))
|
||
if verbose:
|
||
for test_name_and_pass_mark in subsection_test_names[line]:
|
||
print(" %s" % (test_name_and_pass_mark,))
|
||
print("")
|
||
print("")
|
||
|
||
def main(results_tap_path, verbose):
|
||
# Load up test mappings
|
||
test_name_to_group_id = {}
|
||
fed_tests = set()
|
||
client_tests = set()
|
||
with open("./are-we-synapse-yet.list", "r") as f:
|
||
for line in f.readlines():
|
||
test_name = " ".join(line.split(" ")[1:]).strip()
|
||
groups = line.split(" ")[0].split(",")
|
||
for gid in groups:
|
||
if gid == "f" or gid in test_mappings["federation_apis"]:
|
||
fed_tests.add(test_name)
|
||
else:
|
||
client_tests.add(test_name)
|
||
if gid == "f":
|
||
continue # we expect another group ID
|
||
test_name_to_group_id[test_name] = gid
|
||
|
||
# parse results.tap
|
||
summary = {
|
||
"client": {
|
||
# gid: {
|
||
# test_name: OK
|
||
# }
|
||
},
|
||
"federation": {
|
||
# gid: {
|
||
# test_name: OK
|
||
# }
|
||
},
|
||
"appservice": {
|
||
"app": {},
|
||
},
|
||
"nonspec": {
|
||
"nsp": {},
|
||
"msc": {},
|
||
"unk": {}
|
||
},
|
||
}
|
||
with open(results_tap_path, "r") as f:
|
||
for line in f.readlines():
|
||
test_result = parse_test_line(line)
|
||
if not test_result:
|
||
continue
|
||
name = test_result["name"]
|
||
group_id = test_name_to_group_id.get(name)
|
||
if not group_id:
|
||
summary["nonspec"]["unk"][name] = test_result["ok"]
|
||
if group_id == "nsp":
|
||
summary["nonspec"]["nsp"][name] = test_result["ok"]
|
||
elif group_id == "msc":
|
||
summary["nonspec"]["msc"][name] = test_result["ok"]
|
||
elif group_id == "app":
|
||
summary["appservice"]["app"][name] = test_result["ok"]
|
||
elif group_id in test_mappings["federation_apis"]:
|
||
group = summary["federation"].get(group_id, {})
|
||
group[name] = test_result["ok"]
|
||
summary["federation"][group_id] = group
|
||
elif group_id in test_mappings["client_apis"]:
|
||
group = summary["client"].get(group_id, {})
|
||
group[name] = test_result["ok"]
|
||
summary["client"][group_id] = group
|
||
|
||
print("Are We Synapse Yet?")
|
||
print("===================")
|
||
print("")
|
||
print_stats("Non-Spec APIs", summary["nonspec"], test_mappings, verbose)
|
||
print_stats("Client-Server APIs", summary["client"], test_mappings["client_apis"], verbose)
|
||
print_stats("Federation APIs", summary["federation"], test_mappings["federation_apis"], verbose)
|
||
print_stats("Application Services APIs", summary["appservice"], test_mappings, verbose)
|
||
|
||
|
||
|
||
if __name__ == '__main__':
|
||
parser = argparse.ArgumentParser()
|
||
parser.add_argument("tap_file", help="path to results.tap")
|
||
parser.add_argument("-v", action="store_true", help="show individual test names in output")
|
||
args = parser.parse_args()
|
||
main(args.tap_file, args.v) |