Files
thefuck/tests/test_typo.py
T
Alexander 7d89277717 Scale typo gates with length and refuse destructive auto-runs
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).
2026-09-14 20:42:26 +02:00

143 lines
4.9 KiB
Python

from difflib import SequenceMatcher
from thefuck.typo import floor_ok, single_edit
_LEN30 = 'abcdefghijklmnopqrstuvwxyz0123'
class TestSubstitution(object):
def test_single_substitution(self):
assert single_edit('greo', 'grep')
def test_single_substitution_one_char_words(self):
assert single_edit('a', 'b')
class TestTransposition(object):
def test_adjacent_transposition_gti(self):
assert single_edit('gti', 'git')
def test_adjacent_transposition_psuh(self):
assert single_edit('psuh', 'push')
def test_adjacent_transposition_bulid(self):
assert single_edit('bulid', 'build')
def test_transposition_is_symmetric(self):
assert single_edit('git', 'gti')
def test_non_adjacent_swap_is_two_edits(self):
# Swapping two NON-adjacent characters takes two moves:
# abcd -> badc diverges at every position.
assert not single_edit('abcd', 'badc')
def test_two_differing_positions_not_crossed(self):
# Two substitutions shaped like a swap but with distinct
# characters are two edits, not one transposition.
assert not single_edit('ab', 'cd')
class TestInsertionAndDeletion(object):
def test_single_insertion(self):
assert single_edit('clea', 'clear')
def test_single_deletion(self):
assert single_edit('clear', 'clea')
def test_insertion_in_the_middle(self):
assert single_edit('gt', 'git')
def test_two_insertions(self):
assert not single_edit('cl', 'clear')
class TestBoundaries(object):
def test_equal_strings(self):
assert not single_edit('git', 'git')
def test_empty_strings(self):
assert not single_edit('', '')
def test_empty_to_single_char(self):
assert single_edit('', 'a')
def test_single_char_to_empty(self):
assert single_edit('a', '')
def test_empty_to_two_chars(self):
assert not single_edit('', 'ab')
def test_length_difference_over_one(self):
assert not single_edit('g', 'git')
def test_shifted_words_are_two_edits(self):
# abc -> bcd keeps no common alignment: a deletion plus an
# insertion, i.e. two edits.
assert not single_edit('abc', 'bcd')
def test_two_substitutions(self):
# Adjacent but not crossed: two substitutions, not one
# transposition.
assert not single_edit('abcd', 'abxy')
def test_help_flag_boundary_pair(self):
# The pair the history-resolver boundary test pins: under the
# 0.8 ratio cutoff (0.769) AND not a single edit, so the
# amended gate still declines it.
assert not single_edit('--ignore-whitespaces',
'--ignore-all-spaces')
class TestFloorOk(object):
def test_len3_floor_passes(self):
# max length 3 -> floor max(0.6, 1 - 3/3) = 0.6; the plain
# substitution scores 2 * 2 / 6 = 0.667.
assert SequenceMatcher(None, 'abc', 'abz').ratio() >= 0.6
assert floor_ok('abc', 'abz')
def test_len3_below_floor_declines(self):
# Two substitutions score 0.333, under the 0.6 floor.
assert SequenceMatcher(None, 'axx', 'ayy').ratio() < 0.6
assert not floor_ok('axx', 'ayy')
def test_len10_floor_passes(self):
# max length 10 -> floor 1 - 3/10 = 0.7; three trailing
# substitutions keep 7 matches: exactly 2 * 7 / 20 = 0.7.
assert SequenceMatcher(
None, 'abcdefghij', 'abcdefgxyz').ratio() >= 0.7
assert floor_ok('abcdefghij', 'abcdefgxyz')
def test_len10_below_floor_declines(self):
# Four trailing substitutions score 0.6, under 0.7.
assert SequenceMatcher(
None, 'abcdefghij', 'abcdefwxyz').ratio() < 0.7
assert not floor_ok('abcdefghij', 'abcdefwxyz')
def test_len30_floor_passes(self):
# max length 30 -> floor 1 - 3/30 = 0.9; three trailing
# substitutions keep 27 matches: exactly 2 * 27 / 60 = 0.9.
assert SequenceMatcher(
None, _LEN30, _LEN30[:27] + '456').ratio() >= 0.9
assert floor_ok(_LEN30, _LEN30[:27] + '456')
def test_len30_below_floor_declines(self):
# Four trailing substitutions score ~0.867, under 0.9.
assert SequenceMatcher(
None, _LEN30, _LEN30[:26] + '4567').ratio() < 0.9
assert not floor_ok(_LEN30, _LEN30[:26] + '4567')
def test_single_edit_always_passes(self):
assert floor_ok('gti', 'git')
assert floor_ok('psuh', 'push')
assert floor_ok('clea', 'clear')
assert floor_ok('clear', 'clea')
def test_first_char_mismatch_always_fails(self):
# The first-letter guard holds for the ratio path and the
# single-edit path alike.
assert SequenceMatcher(None, 'abcd', 'zbcd').ratio() >= 0.6
assert not floor_ok('abcd', 'zbcd')
assert single_edit('abz', 'bbz')
assert not floor_ok('abz', 'bbz')
assert not floor_ok('ls', 'sl')