# Mutations the read tests must catch. One row each: file | exact text | replacement | why
# `python3 .github/scripts/run_tests.py --mutants --only read` applies each row to a scratch copy
# of read/ and fails when the tests still pass. The text must occur exactly once in the file.
gwt_boe.py | PACE = {'start': 6.0, 'floor': 6.0, 'ceil': 120.0} | PACE = {'start': 6.0, 'floor': 3.0, 'ceil': 120.0} | the BOE gate never goes faster than one request every 6 s, even after quiet hours
gwt_boe.py | ('dato[4]', '"%s"' % phrase.replace('"', '')) | ('dato[4]', phrase) | the BOE phrase is always sent in quotes, so the site reads it as one name
gwt_boe.py | return bool(_TOTAL.search(p) or 'No se han encontrado' in p or 'no ha encontrado' in p) | return True | a BOE page that is not a results page (form, captcha) is a STOP, never zero results
gwt_boe.py | return '%s/gazeta/dias/%s/pdfs/%s' % (HOST, date.replace('-', '/'), href.rsplit('/', 1)[-1]) | return HOST + href | BOE act PDFs come from /gazeta/dias/, never the /datos/pdfs/ robots.txt disallows
gwt_read.py | page=lambda p: p.startswith('%PDF')) | page=lambda p: bool(p)) | an HTML page at a BOE PDF URL is a STOP and is never cached
gwt_read.py | and not c.get('implausible')][:n]: | ][:n]: | --pdf-context skips acts --born already ruled out
gwt_boe.py | return re.compile(r'(?<!\w)%s(?!\w)' % re.escape(_fold(phrase))) | return re.compile(re.escape(_fold(phrase))) | "Ana Zorvalo" is not found inside "Juana Zorvalo"
gwt_boe.py | if True in said: | if said[:1] == [True]: | one occurrence with the tree's parents makes the act consistent
gwt_boe.py | return False if said and all(x is False for x in said) else None | return False if False in said else None | other parents exclude an act only when every occurrence writes other parents
gwt_boe.py | [_fold(x).split()[0] for x in w] == [_fold(x).split()[0] for x in found] | [_fold(x) for x in w] == [_fold(x) for x in found] | a tree's "José María" matches an act's "José"
gwt_boe.py | if c.get('implausible') or c.get('same_parents') is False: | if c.get('implausible'): | other parents mark an act excluded
gwt_boe.py | (r['year'] < born or r['year'] > born + 100) | (r['year'] < born + 14 or r['year'] > born + 100) | --born keeps an act that names a child (orphans in pension lists)
gwt_boe.py | (r['year'] < born or r['year'] > born + 100) | (r['year'] < born) | --born rules out an act over 100 years after the birth
gwt_read.py | str(a.pdf_context), str(a.born), str(a.parents)] | str(a.pdf_context)] | a rerun with other --born or --parents is written to its own file
gwt_read.py | gwt_reader.search_key('\n'.join([first] + words), words) | gwt_reader.search_key(first, words) | a long name rerun with other --parents still gets its own file
gwt_read.py | max(1, min(a.pages, 5)) | max(1, a.pages) | --pages never reads more than 5 pages
gwt_read.py | n = max(0, min(getattr(a, 'pdf_context', 0), 10)) | n = max(0, getattr(a, 'pdf_context', 0)) | --pdf-context never fetches more than 10 PDFs
gwt_insee.py | return y or None, mo > 12 or d > 31 | return y or None, False | an INSEE date with month over 12 or day over 31 is kept and marked malformed, never silent
gwt_insee.py | if not m: | if False: | an INSEE date that is not eight digits stops the run with exit 2, never a crash or a skip
gwt_insee.py | 'malformed': [] if star else ['name']} | 'malformed': []} | an INSEE name with no "*" is kept whole and marked malformed
gwt_insee.py | odd += bool(rec['malformed']) | pass | each file's count of lines with a value out of INSEE's rules reaches the envelope, matched or not
gwt_insee.py | if sex not in SEXES: | if False: | an INSEE sex that is not 1 or 2 stops the run (a shifted line is caught here)
gwt_insee.py | if len(line) < MIN_LENGTH: | if False: | an INSEE line too short to hold the death date stops the run
gwt_insee.py | rec = parse_line(line) | rec = parse_line(line) if surname_matches(line.split('*')[0], wanted) else {'surname': ''} | every INSEE line is checked, not only the lines a --surname matches
gwt_insee.py | return any(have[i:j] in wanted for i in range(len(have)) for j in range(i + 1, len(have) + 1)) | return any(' '.join(w) in ' '.join(have) for w in wanted) | an INSEE --surname matches whole words: ORTEGA never matches ORTEGAS
gwt_insee.py | return cp850, 'cp850' | return latin1, 'latin-1' | a non-UTF-8 INSEE line whose Latin-1 reading has no more accented letters is read as CP850 (PARIS 12°)
gwt_insee.py | if _plausible(latin1) > _plausible(cp850): | if False: | a Latin-1 INSEE line (0xF1 ñ, 0xB0 °) is read as Latin-1, not CP850
gwt_insee.py | return y or None | return y | an INSEE year 0000 is no year, never year 0
gwt_read.py | return cmd_insee_search(a)      # offline: no gate, no cache, no fetcher | pass | gwt-read insee never builds a gate, a cache or a fetcher
gwt_insee.py | if len(line) > MAX_LENGTH: | if False: | two INSEE records glued on one line, or a CR-only file, stop the run instead of losing every record after the first
gwt_insee.py | line = line.removeprefix('\ufeff') | pass | a UTF-8 byte order mark does not shift the first INSEE line
gwt_insee.py | if not lines: | if False: | an empty INSEE file (a failed download) is an error, never a clean "no results"
gwt_read.py | return EXIT_ERROR      # a file that cannot be read is an error, never "no results" | return EXIT_EMPTY | an unreadable INSEE file exits 1, so a caller never logs it as "searched, none found"
gwt_insee.py | h.update(raw) | pass | the envelope lists each INSEE file's real sha256, so a download can be checked against it
gwt_insee.py | wanted = {tuple(fold(s)) for s in surnames if fold(s)} | wanted = {tuple(fold(s))[:1] for s in surnames if fold(s)} | an INSEE --surname of several words keeps only lines that hold all of them
