7d89277717
Similarity requirements now scale with word length (typo.floor_ok: single edit or ratio >= max(0.6, 1 - 3/max(len,3)), first char kept; zsh spdist / nushell did_you_mean derivation in the source). The fixed-cutoff gates are swapped: history_resolver._similar -> floor_ok with the divergence cap deleted (every diverged token is gated per-token, not counted), learned.guess_from_path and the help resolver candidate gates -> floor_ok, so _TOKEN_CUTOFF, _MAX_DIVERGED, GUESS_CUTOFF, _CUTOFF and their difflib plumbing are gone; unique-survivor, no-op and which/'/'/'.'/flag guards kept. New thefuck/danger.py is_dangerous(script) parses via bashlex directly and fail-safes to True when bashlex is unavailable or the script refuses to parse (the flat fallback is head-only); with a tree it matches rm/rmdir recursive flags, dd/mkfs*/shred/wipefs/ mkswap heads, git push --force/-f (not --force-with-lease), chmod/chown -R with a 777-style mode, kill -9, fork-bomb shapes, pipe-to-shell tails and file redirects outside /tmp and /dev/null. fix_command checks it before ANY auto-run, learned-db exact hits included: dangerous candidates fall through to rules+ask. Test-migration inventory (authorized semantic inversions): - tests/resolvers/test_history_resolver.py: declines-3-diverged -> corrects (cap deleted); 0.8-cutoff boundary arithmetic re-based to floor boundaries (len-3 0.6 / len-10 0.7 / len-30 0.9); _TOKEN_CUTOFF import removed with the constant; declines-just- below-cutoff re-based to the len-20 floor 0.85; added a 17-char below-floor decline. - tests/test_learned.py: returns-none-below-cutoff re-based to a same-first-char below-floor pair (0.6 < len-10 floor 0.7); added a ratio-0.7 acceptance pin; single-edit-under-cutoff renamed. - tests/resolvers/test_help_resolver.py: transposition comments re-based to the floor; added a below-floor subcommand decline. - tests/entrypoints/test_fix_command_learned.py: exact-learned-wins fixture's 'git push --force' correction (now correctly refused) replaced by benign scripts; mock_learned stubs danger benign for platform-neutral auto-apply tests; added TestDangerOverride (real module, all four sources: reaches select_command, nothing auto-runs).
184 lines
6.2 KiB
Python
184 lines
6.2 KiB
Python
"""Tests for the generic binary-help subcommand resolver.
|
|
|
|
The harness runs REAL fake binaries (shell scripts on a tmp PATH) that
|
|
append their argv to a log file, so every claim about spawning (or not
|
|
spawning) `binary --help` is grounded in the argv log, not in the
|
|
resolver's return value alone.
|
|
"""
|
|
import os
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from thefuck import shell_ast, typo
|
|
from thefuck.resolvers.help_resolver import get_help_correction
|
|
|
|
|
|
pytestmark = [
|
|
pytest.mark.skipif(
|
|
not shell_ast.AST_AVAILABLE, reason='bashlex is not available'),
|
|
pytest.mark.skipif(
|
|
os.name == 'nt', reason='fake binaries are POSIX shell scripts'),
|
|
]
|
|
|
|
|
|
def _make_fake(path, argv_log, commands, exec_sleep=0):
|
|
"""Writes an executable fake binary.
|
|
|
|
It appends its argv to `argv_log`, records its pid, optionally
|
|
`exec`s a `sleep` (replacing itself, so a kill leaves no orphan)
|
|
before printing a `Commands:` section built from `commands`.
|
|
"""
|
|
body = [
|
|
'#!/bin/sh',
|
|
"printf '%s\\n' \"$*\" >> {}".format(argv_log),
|
|
'echo $$ > {}'.format(path + '.pid'),
|
|
]
|
|
if exec_sleep:
|
|
body.append('exec sleep {}'.format(exec_sleep))
|
|
if commands is not None:
|
|
body.append("cat <<'EOF'")
|
|
body.append('Usage: {} COMMAND [arg...]'.format(
|
|
os.path.basename(path)))
|
|
body.append('')
|
|
body.append('Commands:')
|
|
for command in commands:
|
|
body.append(' {}'.format(command))
|
|
body.append('')
|
|
body.append("Run '{} COMMAND --help' for more information.".format(
|
|
os.path.basename(path)))
|
|
body.append('EOF')
|
|
with open(path, 'w') as script:
|
|
script.write('\n'.join(body) + '\n')
|
|
os.chmod(path, 0o755)
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_bin(tmpdir, os_environ, no_memoize):
|
|
"""Puts a tmp bin dir first on PATH; returns a binary factory."""
|
|
bindir = tmpdir.mkdir('bin')
|
|
os_environ['PATH'] = str(bindir) + os.pathsep + os_environ['PATH']
|
|
|
|
def _factory(name, commands=('build', 'clean', 'pull'), exec_sleep=0):
|
|
path = str(bindir.join(name))
|
|
_make_fake(path, str(tmpdir.join('argv.log')), commands, exec_sleep)
|
|
return path
|
|
|
|
return _factory
|
|
|
|
|
|
@pytest.fixture
|
|
def argv_log(tmpdir):
|
|
return str(tmpdir.join('argv.log'))
|
|
|
|
|
|
@pytest.fixture
|
|
def enable_cache(no_cache, monkeypatch, tmpdir, os_environ):
|
|
"""Re-enables the mtime cache that the autouse no_cache disables."""
|
|
os_environ['XDG_CACHE_HOME'] = str(tmpdir.mkdir('cache'))
|
|
monkeypatch.setattr('thefuck.utils.cache.disabled', False)
|
|
|
|
|
|
def _spawned(argv_log):
|
|
if not os.path.exists(argv_log):
|
|
return []
|
|
with open(argv_log) as log:
|
|
return log.read().splitlines()
|
|
|
|
|
|
def test_returns_correction_when_subcommand_misspelled(
|
|
fake_bin, argv_log, enable_cache):
|
|
fake_bin('fake')
|
|
assert get_help_correction('fake bilud .') == 'fake build .'
|
|
# The mtime-keyed cache suppresses the second `--help` spawn.
|
|
assert get_help_correction('fake bilud .') == 'fake build .'
|
|
assert _spawned(argv_log) == ['--help']
|
|
|
|
|
|
def test_corrects_transposed_subcommand(fake_bin, argv_log, enable_cache):
|
|
# bulid -> build is a single adjacent transposition (li <-> il),
|
|
# so the length-scaled floor admits it on the single-edit path.
|
|
fake_bin('fake')
|
|
assert get_help_correction('fake bulid .') == 'fake build .'
|
|
assert typo.single_edit('bulid', 'build')
|
|
|
|
|
|
def test_corrects_transposition_via_single_edit(
|
|
fake_bin, argv_log, enable_cache):
|
|
# psuh -> push scores 2 * 3 / 8 = 0.75, so only the single-edit
|
|
# path of the floor admits it.
|
|
fake_bin('fake', commands=('push', 'build', 'clean'))
|
|
assert get_help_correction('fake psuh x') == 'fake push x'
|
|
|
|
|
|
def test_declines_subcommand_below_scaled_floor(
|
|
fake_bin, argv_log, enable_cache):
|
|
# 10-char subcommand vs the listed 'burnno1234' scores
|
|
# 2 * 6 / 20 = 0.6, under the len-10 floor 0.7, and is not a
|
|
# single edit; no listed subcommand is close enough.
|
|
fake_bin('fake', commands=('burnno1234', 'build', 'clean'))
|
|
assert get_help_correction('fake burnnoabcd') is None
|
|
assert _spawned(argv_log) == ['--help']
|
|
|
|
|
|
def test_respawns_when_binary_mtime_changes(
|
|
fake_bin, argv_log, enable_cache):
|
|
path = fake_bin('fake')
|
|
assert get_help_correction('fake bilud .') == 'fake build .'
|
|
stats = os.stat(path)
|
|
os.utime(path, (stats.st_atime, stats.st_mtime + 5))
|
|
assert get_help_correction('fake bilud .') == 'fake build .'
|
|
assert _spawned(argv_log) == ['--help', '--help']
|
|
|
|
|
|
def test_declines_when_help_times_out(fake_bin, argv_log, enable_cache):
|
|
path = fake_bin('sleeper', commands=None, exec_sleep=5)
|
|
started = time.time()
|
|
assert get_help_correction('sleeper whatever') is None
|
|
elapsed = time.time() - started
|
|
# The spawn really happened and the process was killed, not waited out.
|
|
assert _spawned(argv_log) == ['--help']
|
|
with open(path + '.pid') as pid_file:
|
|
pid = int(pid_file.read())
|
|
with pytest.raises(OSError):
|
|
os.kill(pid, 0)
|
|
assert elapsed < 3
|
|
|
|
|
|
def test_declines_when_token_is_flag(fake_bin, argv_log):
|
|
fake_bin('fake')
|
|
assert get_help_correction('fake --bilud') is None
|
|
assert _spawned(argv_log) == []
|
|
|
|
|
|
def test_declines_when_subcommand_is_listed(
|
|
fake_bin, argv_log, enable_cache):
|
|
fake_bin('fake')
|
|
assert get_help_correction('fake build .') is None
|
|
assert get_help_correction('fake build .') is None
|
|
assert _spawned(argv_log) == ['--help']
|
|
|
|
|
|
def test_declines_when_head_not_on_path(fake_bin, argv_log):
|
|
assert get_help_correction('no-such-binary-anywhere xyz') is None
|
|
assert _spawned(argv_log) == []
|
|
|
|
|
|
def test_declines_when_match_is_ambiguous(fake_bin, argv_log):
|
|
fake_bin('fake', commands=('clean', 'cleat', 'build', 'pull'))
|
|
assert get_help_correction('fake clea') is None
|
|
|
|
|
|
def test_declines_when_head_is_sudo(fake_bin, argv_log):
|
|
fake_bin('fake')
|
|
assert get_help_correction('sudo fake bilud') is None
|
|
assert _spawned(argv_log) == []
|
|
|
|
|
|
def test_declines_when_script_unparseable(fake_bin, argv_log):
|
|
fake_bin('fake')
|
|
# bashlex cannot parse `case`; the flat fallback's head token then
|
|
# fails the resolver's gates and nothing is spawned.
|
|
assert get_help_correction('case $x in a) fake bilud;; esac') is None
|
|
assert _spawned(argv_log) == []
|