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).
143 lines
4.9 KiB
Python
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')
|