davideisinger.com

My personal website
Log | Files | Refs | README

spelling-hook (5224B)


      1 #!/usr/bin/env python3
      2 """Test real commits offline, substituting a predictable spellchecker."""
      3 
      4 from pathlib import Path
      5 import os
      6 import shutil
      7 import subprocess
      8 import tempfile
      9 
     10 repo = Path(__file__).resolve().parent.parent
     11 env = dict(os.environ, GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=os.devnull)
     12 
     13 with tempfile.TemporaryDirectory(prefix="spelling-hook-") as directory:
     14     root = Path(directory)
     15     work = root / "work"
     16     work.mkdir()
     17     commands = root / "commands"
     18     commands.mkdir()
     19     log = root / "calls"
     20     npx = commands / "npx"
     21     npx.write_text('''#!/usr/bin/env python3
     22 import os, sys
     23 from pathlib import Path
     24 assert sys.argv[1:-1] == ['--yes', 'cspell', '--words-only', '--unique', '--no-progress',
     25     '--no-summary', '--config', '.cspell.json']
     26 assert sys.argv[-1].startswith('stdin://')
     27 with open(os.environ['SPELL_LOG'], 'a') as log:
     28     log.write(sys.argv[-1] + '\\n')
     29 if os.environ.get('SPELL_FAIL'):
     30     print('simulated tool failure', file=sys.stderr)
     31     sys.exit(1)
     32 if os.environ.get('SPELL_NOISE'):
     33     print('Need to install the following packages:')
     34     print('Ok to proceed? (y) jammies')
     35     sys.exit(int(os.environ['SPELL_NOISE']))
     36 known = set(Path('.dictionary').read_text().splitlines())
     37 issues = False
     38 for word in sys.stdin.read().split():
     39     if word.startswith('unknown') and word not in known:
     40         print(word)
     41         issues = True
     42 sys.exit(1 if issues else 0)
     43 ''')
     44     npx.chmod(0o755)
     45     env.update(PATH=f"{commands}{os.pathsep}{env['PATH']}", SPELL_LOG=str(log))
     46 
     47     def git(*args, ok=True):
     48         result = subprocess.run(["git", "-C", str(work), *args], env=env,
     49                                 capture_output=True, text=True)
     50         assert (result.returncode == 0) == ok, result.stderr
     51         return result
     52 
     53     def commit(ok=True):
     54         return git("commit", "-m", "Test change", ok=ok)
     55 
     56     git("init", "-b", "main")
     57     git("config", "user.name", "Hook Test")
     58     git("config", "user.email", "[email protected]")
     59     git("config", "commit.gpgsign", "false")
     60     for name in (".githooks/pre-commit", ".githooks/lib/check-staged-spelling"):
     61         target = work / name
     62         target.parent.mkdir(parents=True, exist_ok=True)
     63         shutil.copy2(repo / name, target)
     64     dictionary = work / ".dictionary"
     65     dictionary.write_text("existing\n")
     66     (work / ".cspell.json").write_text("{}")
     67     git("add", ".")
     68     commit()
     69     git("config", "core.hooksPath", ".githooks")
     70 
     71     # Paths with spaces, duplicate words, and a partially staged file.
     72     post = work / "post with spaces.md"
     73     post.write_text("unknownz unknowna unknownz\n")
     74     git("add", post.name)
     75     post.write_text("unknownunstaged\n")
     76     before = git("rev-parse", "HEAD").stdout
     77     commit(ok=False)
     78     assert git("rev-parse", "HEAD").stdout == before
     79     assert dictionary.read_text() == "existing\nunknowna\nunknownz\n"
     80     assert git("show", ":.dictionary").stdout == "existing\n"
     81     commit(ok=False)  # An immediate retry must not accept unstaged words.
     82     git("add", ".dictionary")
     83     commit()
     84     assert post.read_text() == "unknownunstaged\n"
     85     assert git("show", "HEAD:post with spaces.md").stdout == "unknownz unknowna unknownz\n"
     86 
     87     # Fix a typo instead of accepting it into the dictionary.
     88     post.write_text("unknowntypo\n")
     89     git("add", post.name)
     90     commit(ok=False)
     91     dictionary.write_text(dictionary.read_text().replace("unknowntypo\n", ""))
     92     post.write_text("Correct spelling.\n")
     93     git("add", post.name)
     94     commit()
     95 
     96     # Operational failure must block without modifying the dictionary.
     97     post.write_text("unknownfailure\n")
     98     git("add", post.name)
     99     env['SPELL_FAIL'] = '1'
    100     saved = dictionary.read_bytes()
    101     assert 'Spellcheck failed' in commit(ok=False).stderr
    102     assert dictionary.read_bytes() == saved
    103     del env['SPELL_FAIL']
    104     # Installation prompts or other stdout diagnostics must never become words.
    105     for status in ('0', '1'):
    106         env['SPELL_NOISE'] = status
    107         assert 'unexpected output' in commit(ok=False).stderr
    108         assert dictionary.read_bytes() == saved
    109     del env['SPELL_NOISE']
    110     post.write_text("Correct revised spelling.\n")
    111     git("add", post.name)
    112     commit()
    113 
    114     # Accented words follow system sort under the invoking shell's locale.
    115     env['LC_ALL'] = 'en_US.UTF-8'
    116     dictionary.write_text(dictionary.read_text() + "Ørsted\nrésumé\nresume\nzebra\n")
    117     expected = subprocess.check_output(['sort', '-u', str(dictionary)], env=env)
    118     git('add', '.dictionary')
    119     post.write_text('Another spelling check.\n')
    120     git('add', post.name)
    121     commit(ok=False)
    122     assert dictionary.read_bytes() == expected
    123     git('add', '.dictionary')
    124     commit()
    125 
    126     # Renames to .markdown are checked, deletions and non-Markdown are skipped.
    127     git("mv", post.name, "renamed.markdown")
    128     log.write_text("")
    129     commit()
    130     assert log.read_text() == "stdin://renamed.markdown\n"
    131     git("rm", "renamed.markdown")
    132     log.write_text("")
    133     (work / "script.txt").write_text("unknownignored\n")
    134     git("add", "script.txt")
    135     commit()
    136     assert log.read_text() == ""
    137 
    138 print("Spelling hook checks passed: staged content, dictionary review, retries, failures, renames, and deletions.")