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.")