Back to Repositories

Testing Fish Shell Integration in thefuck Command-Line Tool

This test suite validates the Fish shell integration within the ‘thefuck’ command-line tool, focusing on command correction functionality and shell-specific behavior. The tests ensure proper handling of aliases, history management, and shell command transformations.

Test Coverage Overview

The test suite provides comprehensive coverage of Fish shell functionality:
  • Alias management and overrides
  • Command transformation between shell formats
  • History manipulation and retrieval
  • Shell version detection
  • Application alias configuration
Key edge cases include Unicode handling, environment variable parsing, and error conditions in version detection.

Implementation Analysis

The testing approach utilizes pytest’s fixture system extensively for setup and mocking. The implementation follows a class-based structure with TestFish containing parametrized test methods for various shell operations.

Notable patterns include:
  • Mock implementations for subprocess calls
  • Parametrized test cases for different input scenarios
  • Fixture-based environment setup
  • Isolated test cases for each shell operation

Technical Details

Testing tools and configuration:
  • pytest as the primary testing framework
  • pytest-mock for mocking system calls
  • Fixtures for file system operations
  • UTF-8 encoding handling
  • Environment variable manipulation
  • Shell command simulation

Best Practices Demonstrated

The test suite exemplifies several testing best practices:
  • Isolated test cases with proper setup/teardown
  • Comprehensive mock objects for external dependencies
  • Parametrized tests for multiple scenarios
  • Clear test method naming conventions
  • Proper separation of concerns in test organization
  • Thorough error case handling

nvbn/thefuck

tests/shells/test_fish.py

            
# -*- coding: utf-8 -*-

import pytest
from thefuck.const import ARGUMENT_PLACEHOLDER
from thefuck.shells import Fish


@pytest.mark.usefixtures('isfile', 'no_memoize', 'no_cache')
class TestFish(object):
    @pytest.fixture
    def shell(self):
        return Fish()

    @pytest.fixture(autouse=True)
    def Popen(self, mocker):
        mock = mocker.patch('thefuck.shells.fish.Popen')
        mock.return_value.stdout.read.side_effect = [(
            b'cd
fish_config
fuck
funced
funcsave
grep
history
ll
ls
'
            b'man
math
popd
pushd
ruby'),
            (b'alias fish_key_reader /usr/bin/fish_key_reader
alias g git
'
             b'alias alias_with_equal_sign=echo
invalid_alias'), b'func1
func2', b'']
        return mock

    @pytest.mark.parametrize('key, value', [
        ('TF_OVERRIDDEN_ALIASES', 'cut,git,sed'),  # legacy
        ('THEFUCK_OVERRIDDEN_ALIASES', 'cut,git,sed'),
        ('THEFUCK_OVERRIDDEN_ALIASES', 'cut, git, sed'),
        ('THEFUCK_OVERRIDDEN_ALIASES', ' cut,\tgit,sed
'),
        ('THEFUCK_OVERRIDDEN_ALIASES', '
cut,

git,\tsed\r')])
    def test_get_overridden_aliases(self, shell, os_environ, key, value):
        os_environ[key] = value
        overridden = shell._get_overridden_aliases()
        assert set(overridden) == {'cd', 'cut', 'git', 'grep',
                                   'ls', 'man', 'open', 'sed'}

    @pytest.mark.parametrize('before, after', [
        ('cd', 'cd'),
        ('pwd', 'pwd'),
        ('fuck', 'fish -ic "fuck"'),
        ('find', 'find'),
        ('funced', 'fish -ic "funced"'),
        ('grep', 'grep'),
        ('awk', 'awk'),
        ('math "2 + 2"', r'fish -ic "math \"2 + 2\""'),
        ('man', 'man'),
        ('open', 'open'),
        ('vim', 'vim'),
        ('ll', 'fish -ic "ll"'),
        ('ls', 'ls'),
        ('g', 'git')])
    def test_from_shell(self, before, after, shell):
        assert shell.from_shell(before) == after

    def test_to_shell(self, shell):
        assert shell.to_shell('pwd') == 'pwd'

    def test_and_(self, shell):
        assert shell.and_('foo', 'bar') == 'foo; and bar'

    def test_or_(self, shell):
        assert shell.or_('foo', 'bar') == 'foo; or bar'

    def test_get_aliases(self, shell):
        assert shell.get_aliases() == {'fish_config': 'fish_config',
                                       'fuck': 'fuck',
                                       'funced': 'funced',
                                       'funcsave': 'funcsave',
                                       'history': 'history',
                                       'll': 'll',
                                       'math': 'math',
                                       'popd': 'popd',
                                       'pushd': 'pushd',
                                       'ruby': 'ruby',
                                       'g': 'git',
                                       'fish_key_reader': '/usr/bin/fish_key_reader',
                                       'alias_with_equal_sign': 'echo'}
        assert shell.get_aliases() == {'func1': 'func1', 'func2': 'func2'}

    def test_app_alias(self, shell):
        assert 'function fuck' in shell.app_alias('fuck')
        assert 'function FUCK' in shell.app_alias('FUCK')
        assert 'thefuck' in shell.app_alias('fuck')
        assert 'TF_SHELL=fish' in shell.app_alias('fuck')
        assert 'TF_ALIAS=fuck PYTHONIOENCODING' in shell.app_alias('fuck')
        assert 'PYTHONIOENCODING=utf-8 thefuck' in shell.app_alias('fuck')
        assert ARGUMENT_PLACEHOLDER in shell.app_alias('fuck')

    def test_app_alias_alter_history(self, settings, shell):
        settings.alter_history = True
        assert (
            'builtin history delete --exact --case-sensitive -- $fucked_up_command
'
            in shell.app_alias('FUCK')
        )
        assert 'builtin history merge
' in shell.app_alias('FUCK')
        settings.alter_history = False
        assert 'builtin history delete' not in shell.app_alias('FUCK')
        assert 'builtin history merge' not in shell.app_alias('FUCK')

    def test_get_history(self, history_lines, shell):
        history_lines(['- cmd: ls', '  when: 1432613911',
                       '- cmd: rm', '  when: 1432613916'])
        assert list(shell.get_history()) == ['ls', 'rm']

    @pytest.mark.parametrize('entry, entry_utf8', [
        ('ls', '- cmd: ls
   when: 1430707243
'),
        (u'echo café', '- cmd: echo café
   when: 1430707243
')])
    def test_put_to_history(self, entry, entry_utf8, builtins_open, mocker, shell):
        mocker.patch('thefuck.shells.fish.time', return_value=1430707243.3517463)
        shell.put_to_history(entry)
        builtins_open.return_value.__enter__.return_value. \
            write.assert_called_once_with(entry_utf8)

    def test_how_to_configure(self, shell, config_exists):
        config_exists.return_value = True
        assert shell.how_to_configure().can_configure_automatically

    def test_how_to_configure_when_config_not_found(self, shell,
                                                    config_exists):
        config_exists.return_value = False
        assert not shell.how_to_configure().can_configure_automatically

    def test_get_version(self, shell, Popen):
        Popen.return_value.stdout.read.side_effect = [b'fish, version 3.5.9
']
        assert shell._get_version() == '3.5.9'
        assert Popen.call_args[0][0] == ['fish', '--version']

    @pytest.mark.parametrize('side_effect, exception', [
        ([b'
'], IndexError),
        (OSError('file not found'), OSError),
    ])
    def test_get_version_error(self, side_effect, exception, shell, Popen):
        Popen.return_value.stdout.read.side_effect = side_effect
        with pytest.raises(exception):
            shell._get_version()
        assert Popen.call_args[0][0] == ['fish', '--version']