Frontend Features and refactor
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
"""Fuzzes /find with many ways of typing a reference and checks the verses that come back.
|
||||
|
||||
Run from the project root: python3 tools/fuzz_refs.py [sample_size]
|
||||
Expected verse ids come straight from data/kjv.db, not from the app.
|
||||
"""
|
||||
import sys, re, random, sqlite3, itertools, json
|
||||
sys.path.insert(0, ".") # run from the project root
|
||||
import logging
|
||||
import app as a
|
||||
a.app.testing = True
|
||||
client = a.app.test_client()
|
||||
|
||||
db = sqlite3.connect("data/kjv.db")
|
||||
BOOKS = [r[0] for r in db.execute("select n from key_english order by b")]
|
||||
ids = {} # (book, chap) -> [(verse, id)]
|
||||
for vid, b, c, v in db.execute("select id,b,c,v from fts_kjv"):
|
||||
ids.setdefault((int(b), int(c)), []).append((int(v), int(vid)))
|
||||
maxch = {b: max(c for (bb, c) in ids if bb == b) for b in range(1, 67)}
|
||||
|
||||
NAME_VARIANTS = {
|
||||
1: ["Gen", "Ge", "Gn"], 2: ["Exod", "Ex", "Exo"], 3: ["Lev", "Lv"], 4: ["Num", "Nm", "Nu"],
|
||||
5: ["Deut", "Dt", "Deu"], 6: ["Josh", "Jos"], 7: ["Judg", "Jdg", "Jg"], 8: ["Ru"],
|
||||
9: ["1 Sam", "1 Sm", "1Sa", "I Samuel", "1st Samuel", "First Samuel"],
|
||||
10: ["2 Sam", "2 Sm", "2Sa", "II Samuel", "2nd Samuel", "Second Samuel"],
|
||||
11: ["1 Kgs", "1 Ki", "1Kin", "I Kings", "First Kings"], 12: ["2 Kgs", "2 Ki", "II Kings", "Second Kings"],
|
||||
13: ["1 Chron", "1 Chr", "1Ch", "I Chronicles", "First Chronicles"], 14: ["2 Chron", "2 Chr", "II Chronicles"],
|
||||
15: ["Ezr"], 16: ["Neh", "Ne"], 17: ["Est", "Esth"], 18: [], 19: ["Ps", "Psa", "Psalm", "Pslm"],
|
||||
20: ["Prov", "Pr", "Prv"], 21: ["Eccl", "Ecc", "Eccles", "Ecclesiasts"],
|
||||
22: ["Song", "Song of Songs", "Songs of Solomon", "Song of Sol", "SOS", "Canticles", "SongofSolomon"],
|
||||
23: ["Isa", "Is"], 24: ["Jer", "Je"], 25: ["Lam"], 26: ["Ezek", "Eze", "Ezk"], 27: ["Dan", "Dn"],
|
||||
28: ["Hos", "Ho"], 29: ["Joe", "Jl"], 30: ["Am"], 31: ["Obad", "Ob"], 32: ["Jon", "Jnh"], 33: ["Mic"],
|
||||
34: ["Nah", "Na"], 35: ["Hab"], 36: ["Zeph", "Zep"], 37: ["Hag", "Hg"], 38: ["Zech", "Zec"], 39: ["Mal"],
|
||||
40: ["Matt", "Mt", "Mat"], 41: ["Mk", "Mar"], 42: ["Lk", "Luk"], 43: ["Jn", "Joh"], 44: ["Act"],
|
||||
45: ["Rom", "Ro"], 46: ["1 Cor", "1Co", "I Corinthians", "First Corinthians", "1st Corinthians"],
|
||||
47: ["2 Cor", "2Co", "II Corinthians", "Second Corinthians"], 48: ["Gal", "Ga"], 49: ["Eph"],
|
||||
50: ["Phil", "Php", "Philip"], 51: ["Col"], 52: ["1 Thess", "1 Th", "1Thes", "I Thessalonians"],
|
||||
53: ["2 Thess", "2 Th", "II Thessalonians"], 54: ["1 Tim", "1 Ti", "I Timothy", "First Timothy"],
|
||||
55: ["2 Tim", "2 Ti", "II Timothy"], 56: ["Tit", "Ti"], 57: ["Phlm", "Phm", "Philem"], 58: ["Heb"],
|
||||
59: ["Jas", "Jam"], 60: ["1 Pet", "1 Pe", "1Pt", "I Peter", "First Peter"], 61: ["2 Pet", "2 Pe", "II Peter"],
|
||||
62: ["1 Jn", "1 Jo", "1Joh", "I John", "First John", "1st John"], 63: ["2 Jn", "2 John", "II John"],
|
||||
64: ["3 Jn", "3 John", "III John"], 65: ["Jud"], 66: ["Rev", "Re", "Revelations", "Revelation of John"],
|
||||
}
|
||||
|
||||
def name_forms(b):
|
||||
full = BOOKS[b - 1]
|
||||
forms = {full, full.lower(), full.upper(), full.replace(" ", ""), full.lower().replace(" ", "")}
|
||||
forms |= set(NAME_VARIANTS.get(b, []))
|
||||
forms |= {f.lower() for f in NAME_VARIANTS.get(b, [])}
|
||||
return sorted(forms)
|
||||
|
||||
def cases():
|
||||
rnd = random.Random(1611)
|
||||
for b in range(1, 67):
|
||||
for nm in name_forms(b):
|
||||
ch = rnd.randint(1, maxch[b])
|
||||
vs = ids[(b, ch)]
|
||||
v1 = rnd.choice(vs)[0]
|
||||
v2 = min(vs[-1][0], v1 + rnd.randint(1, 3))
|
||||
chap_ids = [i for _, i in vs]
|
||||
single = [i for v, i in vs if v == v1]
|
||||
rng = [i for v, i in vs if v1 <= v <= v2]
|
||||
key = a.normalize_book_text(nm)
|
||||
if key in a.BOOK_CANONICAL or key in a.BARE_BOOK_EXTRA: # abbreviations alone stay keyword searches
|
||||
for sp in ("", " ", " "):
|
||||
yield (f"{nm}{sp}", [i for _, i in ids[(b, 1)]], "bare") # book alone -> chapter 1
|
||||
for fmt, exp, kind in (
|
||||
(f"{nm} {ch}", chap_ids, "chapter"),
|
||||
(f"{nm}{ch}", chap_ids, "chapter-nospace"),
|
||||
(f"{nm} {ch}:{v1}", single, "verse"),
|
||||
(f"{nm}{ch}:{v1}", single, "verse-nospace"),
|
||||
(f" {nm} {ch} : {v1} ", single, "verse-spaced"),
|
||||
(f"{nm} {ch}.{v1}", single, "verse-dot"),
|
||||
(f"{nm} {ch} {v1}", single, "verse-space-sep"),
|
||||
(f"{nm} {ch}:{v1}-{v2}", rng, "range"),
|
||||
(f"{nm} {ch}:{v1} - {v2}", rng, "range-spaced"),
|
||||
(f"{nm} {ch}:{v1}–{v2}", rng, "range-endash"),
|
||||
(f"{nm.upper()} {ch}:{v1}", single, "verse-upper"),
|
||||
(f"{nm} {ch}:{v1}-{v2}\t", rng, "range-trailing-tab"),
|
||||
):
|
||||
if v2 > v1 or kind not in ("range", "range-spaced", "range-endash", "range-trailing-tab"):
|
||||
yield fmt, exp, kind
|
||||
|
||||
def verse_ids_in(html):
|
||||
return [int(x) for x in re.findall(r'data-verse-id="(\d+)"', html)]
|
||||
|
||||
def run(limit=None, show=25):
|
||||
allcases = list(cases())
|
||||
if limit:
|
||||
random.Random(7).shuffle(allcases); allcases = allcases[:limit]
|
||||
fails = {}
|
||||
total = 0
|
||||
for q, exp, kind in allcases:
|
||||
total += 1
|
||||
try:
|
||||
r = client.get("/find", query_string={"kw": q, "view": "rich"})
|
||||
got = verse_ids_in(r.get_data(as_text=True)) if r.status_code == 200 else None
|
||||
except Exception as e:
|
||||
got = None
|
||||
ok = got is not None and got[:len(exp)] == exp and (kind.startswith("range") or kind.startswith("verse") or kind == "chapter" or True) and (len(got) == len(exp) if kind != "bare" else got[:len(exp)] == exp)
|
||||
if not ok:
|
||||
fails.setdefault(kind, []).append((q, len(exp), None if got is None else len(got)))
|
||||
nfail = sum(len(v) for v in fails.values())
|
||||
print(f"{total} cases, {nfail} failed ({100*nfail//max(total,1)}%)")
|
||||
for k, v in sorted(fails.items(), key=lambda kv: -len(kv[1])):
|
||||
print(f" {k}: {len(v)} e.g. {v[:3]}")
|
||||
return fails
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.disable(logging.CRITICAL)
|
||||
run(int(sys.argv[1]) if len(sys.argv) > 1 else None)
|
||||
Reference in New Issue
Block a user