diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index de21288..34abc14 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -17,10 +17,10 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pylint pycodestyle colorama + pip install pylint pycodestyle colorama pytest pytest-mock - name: Analysing the code with pylint run: | - pylint $(git ls-files '*.py') + pylint $(git ls-files '*.py' | grep -v setup.py) - name: Analysing the code with pycodestyle run: | pycodestyle $(git ls-files '*.py' | grep -v setup.py) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 0000000..260032e --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,39 @@ +name: Unit testing application + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint pycodestyle colorama pytest pytest-mock + - name: Install ctags + run: sudo apt-get update && sudo apt-get install -y exuberant-ctags + - name: Install betty + run: | + sudo ./bettyfixer/install_dependency/.Betty/install.sh && + sudo mv bettyfixer/install_dependency/.Betty/betty.sh /bin/betty + - name: Create test-results directory + run: mkdir -p test-results + - name: Test with pytest + run: | + pytest --junitxml=test-results/results.xml + - uses: actions/upload-artifact@v3 + if: always() + with: + name: pytest-results + path: test-results + retention-days: 30 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..874e549 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +main.c +__pycache__/ +*.pyc +.coverage +htmlcov/ +launch.json \ No newline at end of file diff --git a/bettyfixer/autoprototype.py b/bettyfixer/autoprototype.py index 5671243..61495ff 100644 --- a/bettyfixer/autoprototype.py +++ b/bettyfixer/autoprototype.py @@ -84,7 +84,7 @@ def generate_tags(directory): return False -def filter_tags(directory, tags_file): +def filter_tags(directory, tags_file): # ❗ This function has bugs """ Filter the tags file to get only the function prototypes. Args: diff --git a/bettyfixer/backup.py b/bettyfixer/backup.py index 4d2d734..5d096d0 100644 --- a/bettyfixer/backup.py +++ b/bettyfixer/backup.py @@ -15,7 +15,7 @@ def create_backup(file_path): shutil.copy2(file_path, backup_path) except shutil.SameFileError: print( - f"Err creating backup {file_path}: Src and dest are same file.") + f"Error creating backup {file_path}: Src and dest are same file.") except FileNotFoundError: print(f"Error creating backup for {file_path}: File not found.") except IsADirectoryError: diff --git a/bettyfixer/betty_fixer.py b/bettyfixer/betty_fixer.py index 135fec2..e62e5fc 100644 --- a/bettyfixer/betty_fixer.py +++ b/bettyfixer/betty_fixer.py @@ -141,6 +141,7 @@ def process_errors(file_path): process_error_file(file_path) +# ❗ Needs to be refactored with Dependency Injection in mind def fix_betty_warnings(content, file_path): """ Fix Betty warnings in the specified content. @@ -190,6 +191,7 @@ def remove_blank_lines_inside_comments(file_path): return +# ❗ in serious need of refactoring. Decoupling and Dependency Injection def fix_betty_style(file_paths): """ Fix Betty style for the specified file paths. diff --git a/errors_logs/autoprototype.txt b/errors_logs/linting/autoprototype.txt similarity index 100% rename from errors_logs/autoprototype.txt rename to errors_logs/linting/autoprototype.txt diff --git a/errors_logs/backup.txt b/errors_logs/linting/backup.txt similarity index 100% rename from errors_logs/backup.txt rename to errors_logs/linting/backup.txt diff --git a/errors_logs/betty_fixer.txt b/errors_logs/linting/betty_fixer.txt similarity index 100% rename from errors_logs/betty_fixer.txt rename to errors_logs/linting/betty_fixer.txt diff --git a/errors_logs/betty_handler.txt b/errors_logs/linting/betty_handler.txt similarity index 100% rename from errors_logs/betty_handler.txt rename to errors_logs/linting/betty_handler.txt diff --git a/errors_logs/errors_exractor.txt b/errors_logs/linting/errors_exractor.txt similarity index 100% rename from errors_logs/errors_exractor.txt rename to errors_logs/linting/errors_exractor.txt diff --git a/errors_logs/extract_line.txt b/errors_logs/linting/extract_line.txt similarity index 100% rename from errors_logs/extract_line.txt rename to errors_logs/linting/extract_line.txt diff --git a/linter.sh b/linter.sh new file mode 100755 index 0000000..b5968c6 --- /dev/null +++ b/linter.sh @@ -0,0 +1,42 @@ +#!/bin/env bash + +# Get the current date and time +CURRENT_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + +# Get the name of the person from git config +NAME=$(git config --get user.name) + +# path from the root of project to the directory where the log files will be saved +PATHLOG=$(git rev-parse --show-toplevel)/errors_logs/linting +# Check if pylint is installed +if ! command -v pylint &> /dev/null +then + echo "pylint is not installed" + exit 1 +fi + +# Check if person's name is set in git config +if [ -z "$(git config --get user.name)" ] +then + echo "Please set your name in git config" + exit 1 +fi + + +if [ ! -d "$PATHLOG" ] +then + mkdir -p "$PATHLOG" +fi +# Loop over all command line arguments +for file in "$@" +do + # Change the file suffix to .txt + filename=$(basename "$file") + output_file="${filename%.py}.txt" + + echo -e "\n\nName: $NAME\n" >> "${PATHLOG}/${output_file}" + echo -e "Date: $CURRENT_DATE\n" >> "${PATHLOG}/${output_file}" + pylint "$file" + # Run pylint with the current command line argument + pylint "$file" --reports=y -f parseable >> "${PATHLOG}/${output_file}" +done \ No newline at end of file diff --git a/pylintrc b/pylintrc index 9a06013..f59e26f 100644 --- a/pylintrc +++ b/pylintrc @@ -337,7 +337,7 @@ indent-after-paren=4 indent-string=' ' # Maximum number of characters on a single line. -max-line-length=100 +max-line-length=79 # Maximum number of lines in a module. max-module-lines=1000 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_autoprototype.py b/tests/test_autoprototype.py new file mode 100644 index 0000000..59bb0dc --- /dev/null +++ b/tests/test_autoprototype.py @@ -0,0 +1,259 @@ +""" + Test the autoprototype.py file +""" +import os +from subprocess import CompletedProcess +import subprocess +import pytest +from colorama import Fore +# from pytest_mock.plugin import _mocker +from bettyfixer.autoprototype import (betty_check, filter_tags, generate_tags, + print_check_betty_first, + print_header_name_missing, + print_ctags_header_error, + check_ctags, create_header, + delete_files, check_header_file, + autoproto + ) + + +class TestAutoprototypeSuite: + """ + Test the autoprototype.py file + """ + + @pytest.fixture(autouse=True) + def setup_method(self): + """Set up the test """ + + with open("test.c", "w", encoding="utf-8") as f: + f.write( + 'int main(int argc, char **argv)\ +{\nprintf("Hello World"\nreturn 0; \n}') + yield + + os.remove("test.c") + + @pytest.fixture() + def setup_tear_down_temp_files(self): + """Set up the test """ + generate_tags(".") + filter_tags(".", "tags") + yield + if os.path.exists("tags"): + os.remove("tags") + if os.path.exists("temp_tags"): + os.remove("temp_tags") + if os.path.exists("test.h"): + os.remove("test.h") + + def test_betty_check_installed(self, mocker): + """ Test if betty is installed""" + + mock_run, mock_glob = mocker.patch( + "subprocess.run"), mocker.patch("glob.glob") + + mock_glob.return_value = ["test.c"] + mock_run.return_value = CompletedProcess( + args=['betty'] + mock_glob.return_value, + returncode=0, + stdout='ERROR: ', + stderr='WARNING:' + ) + betty_check_result = betty_check() + assert betty_check_result is not None + assert betty_check_result is False + assert "ERROR:" in mock_run.return_value.stdout or \ + "ERROR:" in mock_run.return_value.stderr + assert "WARNING:" in mock_run.return_value.stderr or \ + "WARNING:" in mock_run.return_value.stdout + + def test_betty_check_errors(self, capsys): + """ Test if there are errors in the files + Create a temporary file with errors""" + betty = betty_check() + assert not betty + captured = capsys.readouterr() + assert "exit status 1" in captured.out + + def test_betty_check_warnings(self): + """ Test if there are warnings in the files + Create a temporary file with warnings""" + assert not betty_check() + + def test_print_check_betty_first(self, capsys): + """Test the print_check_betty_first function from autoprototype.py""" + expected_output = Fore.RED + \ + "You should fix betty Errors first before copy prototype\ + functions into The header file" + Fore.RESET + "\n" + + print_check_betty_first() + captured = capsys.readouterr() + assert captured.out == expected_output + + def test_print_header_name_missing(self, capsys): + """Test the print_header_name_missing function from autoprototype.py""" + expected_output = Fore.RED + \ + "Usage : bettyfixer -H .h" + Fore.RESET + "\n" + print_header_name_missing() + captured = capsys.readouterr() + assert captured.out == expected_output + + def test_print_ctags_header_error(self, capsys): + """Test the print_ctags_header_error function from autoprototype.py""" + expected_output = Fore.RED + "Error" + Fore.RESET + "\n" + print_ctags_header_error("Error") + captured = capsys.readouterr() + assert captured.out == expected_output + + def test_check_ctags_installed(self, mocker): + """ Test if ctags is installed""" + mock_run = mocker.patch("subprocess.run") + mock_run.return_value = CompletedProcess(args=['ctags', '--version'], + returncode=0, + ) + ctag = check_ctags() + assert ctag[0] is True and ctag[1] is None + + def test_check_ctags_not_installed(self, mocker): + """ Test if ctags is not installed""" + mock_run = mocker.patch("subprocess.run") + mock_run.side_effect = subprocess.CalledProcessError( + returncode=1, cmd=['ctags', '--version'], stderr=b'') + ctag = check_ctags() + assert ctag[0] is False and isinstance(ctag[1], str) + + def test_generate_tags_failure(self, mocker): + """ Test the generate_tags function not + working from autoprototype.py""" + mock_run = mocker.patch("subprocess.run") + mock_run.side_effect = subprocess.CalledProcessError( + returncode=1, cmd=['ctags', '-R', '.'], stderr="Error") + assert not generate_tags('.') + + def test_generate_tags_success(self, mocker): + """Test the generate_tags function when the subprocess command + succeeds.""" + mock_run = mocker.patch("subprocess.run") + mock_run.return_value = CompletedProcess(args=[ + 'ctags', '-R', '--c-kinds=+p', + '--fields=+S', '--extra=+q', + '--languages=c', '--langmap=c:.c' + ], + returncode=0, + ) + assert generate_tags('.') + mock_run.assert_called_once_with(['ctags', '-R', '--c-kinds=+p', + '--fields=+S', '--extra=+q', + '--languages=c', '--langmap=c:.c', + '.'], check=True) + mocker.stopall() + result = generate_tags(".") + assert result is not None + assert os.path.exists("tags") is True + with open("tags", "r", encoding='utf-8') as f: + assert 'int main(int argc, char **argv)' in f.read() + + @pytest.mark.usefixtures("setup_tear_down_temp_files") + def test_filter_tags_success(self): + """Test the filter_tags function when the + subprocess command succeeds.""" + generate_tags(".") + result = filter_tags('.', "tags") + + assert result is not None + + @pytest.mark.usefixtures("setup_tear_down_temp_files") + def test_filter_tags_failure(self, mocker, capsys): + """Test the filter_tags function when the subprocess command fails.""" + mock_run = mocker.patch("subprocess.run") + mock_run.return_value = CompletedProcess(args=['ctags', '-R', '.'], + returncode=1, + stderr="Error" + ) + + # Call the function and check the result + assert not filter_tags('.', "tags") + assert os.path.exists("temp_tags") is True + mocker.stopall() + mock_run = mocker.patch("os.path.exists") + mock_run.return_value = False + assert filter_tags('.', "tags") is None + captured = capsys.readouterr() + assert "Error" in captured.out + + @pytest.mark.usefixtures("setup_tear_down_temp_files") + def test_create_header(self): + """Test the create_header function from autoprototype.py""" + generate_tags(".") + filtered_tags = filter_tags(".", "tags") + + create_header("test.h", filtered_tags=filtered_tags) + assert os.path.exists("test.h") is True + with open("test.h", "r", encoding='utf-8') as f: + content = f.read() + assert 'test'.upper() in content + assert 'endif' in content + + @pytest.mark.usefixtures("setup_tear_down_temp_files") + def test_create_header_failure(self, mocker): + """Test the create_header function from autoprototype.py""" + mock_open = mocker.patch("builtins.open") + mock_open.side_effect = AttributeError + with pytest.raises(AttributeError): + create_header(123, filtered_tags="tags") + assert os.path.exists("test.h") is False + + @pytest.mark.usefixtures("setup_tear_down_temp_files") + def test_delete_files(self): + """Test the delete_files function from autoprototype.py""" + delete_files('tags', 'temp_tags') + assert os.path.exists("tags") is False + assert os.path.exists("temp_tags") is False + + @pytest.mark.usefixtures("setup_tear_down_temp_files") + def test_delete_files_failure(self, mocker): + """Test the delete_files function from autoprototype.py""" + mock_remove = mocker.patch("subprocess.run") + mock_remove.side_effect = subprocess.CalledProcessError( + returncode=1, cmd=['rm', 'tags', 'temp_tags'], stderr="Error") + with pytest.raises(subprocess.CalledProcessError): + delete_files('tags', 'temp_tags') + mock_remove.assert_called_once_with( + 'rm tags temp_tags', check=True, shell=True) + assert os.path.exists("tags") is True + + def test_check_header_file(self): + """ Test the check_header_file function from autoprototype.py""" + os.mknod("test.h") + result = check_header_file("test.h") + assert result == (True, None) + os.remove("test.h") + result = check_header_file("test") + assert result == ( + False, + "Error: Invalid header file. It should have a '.h' extension." + ) + + @pytest.mark.usefixtures("setup_tear_down_temp_files") + def test_autoproto(self, mocker): + """ Test the autoproto function from autoprototype.py""" + mock_check_header_file, mock_filter_tags, \ + mock_generate_tags, mock_create_header = mocker.patch( + "bettyfixer.autoprototype.check_header_file"), mocker.patch( + "bettyfixer.autoprototype.filter_tags"), mocker.patch( + "bettyfixer.autoprototype.generate_tags"), mocker.patch( + "bettyfixer.autoprototype.create_header") + + mock_check_header_file.return_value = (True, None) + mock_generate_tags.return_value = True + mock_filter_tags.return_value = True + mock_create_header.return_value = None + + autoproto(".", "test.h") + mock_check_header_file.assert_called_once_with("test.h") + mock_generate_tags.assert_called_once_with(".") + mock_filter_tags.assert_called_once() + mock_create_header.assert_called_once_with( + "test.h", mock_filter_tags.return_value) + mocker.stopall() diff --git a/tests/test_backup.py b/tests/test_backup.py new file mode 100644 index 0000000..41caad5 --- /dev/null +++ b/tests/test_backup.py @@ -0,0 +1,76 @@ +""" +Test the backup module. +""" +import os +import shutil +import pytest +from bettyfixer import backup + + +class TestBackup: + """Test the backup module.""" + + @pytest.fixture(autouse=True) + def setup_method(self): + """Create a test file.""" + # pylint: disable=attribute-defined-outside-init + self.test_file = 'test.c' + with open(self.test_file, 'w', encoding='utf-8') as f: + self.test_file = f.name + f.write( + 'int main(int argc, char **argv){\nprintf(\ +"Hello World"\nreturn 0; \n}' + ) + yield + os.remove(self.test_file) + if os.path.exists(self.test_file + '.bak'): + os.remove(self.test_file + '.bak') + + def test_create_backup(self): + """Test the create_backup function.""" + backup.create_backup(self.test_file) + assert os.path.exists(self.test_file + '.bak') + with open(self.test_file + '.bak', 'r', encoding='utf-8') as f: + assert 'int main(int argc,' in f.read() + os.remove(self.test_file + '.bak') + + def test_create_backup_same_file_error(self, mocker, capsys): + """Test the create_backup function with a same file error.""" + + mocker.patch('shutil.copy2', side_effect=shutil.SameFileError) + backup.create_backup(self.test_file) + captured = capsys.readouterr() + assert 'Src and dest are same file' in captured.out + assert not os.path.exists(self.test_file + '.bak') + + def test_create_backup_file_not_found(self, mocker, capsys): + """Test the create_backup function with a file not found error.""" + mocker.patch('shutil.copy2', side_effect=FileNotFoundError) + backup.create_backup(self.test_file) + captured = capsys.readouterr() + assert 'File not found' in captured.out + assert not os.path.exists(self.test_file + '.bak') + + def test_create_backup_is_a_directory_error(self, mocker, capsys): + """Test the create_backup function with an is a directory error.""" + mocker.patch('shutil.copy2', side_effect=IsADirectoryError) + backup.create_backup(self.test_file) + captured = capsys.readouterr() + assert 'Is a directory error' in captured.out + assert not os.path.exists(self.test_file + '.bak') + + def test_create_backup_permission_error(self, mocker, capsys): + """Test the create_backup function with a permission error.""" + mocker.patch('shutil.copy2', side_effect=PermissionError) + backup.create_backup(self.test_file) + captured = capsys.readouterr() + assert 'Permission error' in captured.out + assert not os.path.exists(self.test_file + '.bak') + + def test_create_backup_os_error(self, mocker, capsys): + """Test the create_backup function with an os error.""" + mocker.patch('shutil.copy2', side_effect=OSError) + backup.create_backup(self.test_file) + captured = capsys.readouterr() + assert 'Unexpected error' in captured.out + assert not os.path.exists(self.test_file + '.bak') diff --git a/tests/test_betty_fixer.py b/tests/test_betty_fixer.py new file mode 100644 index 0000000..751fa4e --- /dev/null +++ b/tests/test_betty_fixer.py @@ -0,0 +1,425 @@ +""" +Test the betty_fixer module. +""" +import os +import re +import sys +import pytest +from bettyfixer.betty_fixer import ( + read_file, write_file, add_line_without_newline, + remove_consecutive_blank_lines, add_parentheses_around_return, + fix_comments, remove_trailing_whitespaces, + fix_betty_warnings, remove_blank_lines_inside_comments, + fix_betty_style, main, + find_available_file_name, copy_remaining_lines, + betty_handler, other_handlers, create_tasks_directory, + copy_files_to_tasks, modify_main_files, record_processed_file, + is_file_processed +) + + +class TestBettyFixer: # pylint: disable=too-many-public-methods + """ + Test the betty_fixer module. + """ + @pytest.fixture(autouse=True) + def setup_teardown(self): + """Set up the test """ + + with open("test.c", "w", encoding="utf-8") as f: + f.write( + 'int main(int argc, char **argv)\ +{\nprintf("Hello World"\nreturn 0; \n}') + yield + + os.remove("test.c") + if os.path.exists("errors.txt"): + os.remove("errors.txt") + + def test_read_file(self): + """Test read_file function.""" + assert read_file( + "test.c") == 'int main(int argc, char **argv)\ +{\nprintf("Hello World"\nreturn 0; \n}' + + def test_write_file(self): + """Test write_file function.""" + write_file("./hello.txt", "Hello World") + assert read_file("./hello.txt") == "Hello World" + os.remove("./hello.txt") + + def test_add_line_without_newline(self): + """Test add_line_without_newline function.""" + add_line_without_newline("test.c", "\n") + assert read_file( + "test.c") == 'int main(int argc, char **argv)\ +{\nprintf("Hello World"\nreturn 0; \n}\n' + + def test_add_line_without_newline_fail(self): + """Test add_line_without_newline function.""" + with pytest.raises(FileNotFoundError): + add_line_without_newline("notFound", "\n") + + def test_remove_consecutive_blank_lines(self): + """Test remove_consecutive_blank_lines function.""" + list_str_blank = ["Hello\n\n\nWorld", "Hello\n\n\nWorld\n\n\n", + "Hello\n\n\nWorld\n\n\n\n\n", + "\n\n\nHello\n\n\nWorld\n\n\n\n\n\n"] + + assert all(remove_consecutive_blank_lines(string) != re.search( + "\n{3,}", string) for string in list_str_blank) + + def test_add_parentheses_around_return(self): + """Test add_parentheses_around_return function.""" + + return_ = { + "return 0;": r"return\s*\((\d+)\);", + "return (0);": r"return\s*\((\d+)\);", + "return ;": r"return\s*;", + "return value;": r"return\s*\(value\);", + "return (value);": r"return\s*\(value\);" + } + + for key, value in return_.items(): + assert re.search( + value, add_parentheses_around_return(key)) + + def test_fix_comments(self): + """Test fix_comments function.""" + comments = { + "something// comment": r"something\s*", + "something // comment": r"something\s*", + "something // comment\n": r"something\s*", + "// comment": r"\s*", + "/* comment */": r"\s*\/\* comment \*/\s*", + } + for key, value in comments.items(): + assert re.search(value, fix_comments(key)) + + def test_remove_trailing_whitespaces(self): + """Test remove_trailing_whitespaces function.""" + lines = ["Hello World ", "Hello World", + "Hello World\t", "Hello World\t\t"] + # ❗ "Hello World\n" and "Hello World\n " is failing + assert all(remove_trailing_whitespaces(line) + == re.search( + r"Hello World\S*", line).group() for line in lines + ) + + # ❗ @pytest.mark.skip(reason= + # "Needs to be refactored with Dependency Injection in mind") + def test_fix_betty_warnings(self, mocker): + """Test fix_betty_warnings function.""" + mock_remove_consecutive_blank_lines = mocker.patch( + "bettyfixer.betty_fixer.remove_consecutive_blank_lines", + return_value="some_content" + ) + mock_fix_comments = mocker.patch( + "bettyfixer.betty_fixer.fix_comments", return_value="some_content") + mock_remove_trailing_whitespaces = mocker.patch( + "bettyfixer.betty_fixer.remove_trailing_whitespaces") + fix_betty_warnings("some_content", "test.c") + mock_remove_consecutive_blank_lines.assert_called_once_with( + "some_content") + mock_fix_comments.assert_called_once_with("some_content") + mock_remove_trailing_whitespaces.assert_called_once_with( + "some_content") + + def test_remove_blank_lines_inside_comments(self, mocker): + """Test remove_blank_lines_inside_comments function.""" + + mock_clean_errors_file = mocker.patch( + "bettyfixer.betty_fixer.clean_errors_file") + mock_open = mocker.patch( + "builtins.open", mocker.mock_open(read_data="/**\n\n*/")) + + remove_blank_lines_inside_comments("some_file") + mock_clean_errors_file.assert_called_once_with('errors.txt') + assert mock_open.call_count == 2 + mock_open.assert_any_call("some_file", 'r', encoding='utf-8') + mock_open.assert_any_call("some_file", 'w', encoding='utf-8') + mock_open().writelines.assert_called_once_with(["/**\n", "*/"]) + +# ❗@pytest.mark.skip(reason= + # "in serious need of refactoring.Decoupling and Dependency Injection") + def test_fix_betty_style(self, mocker): + """Test fix_betty_style function.""" + file_paths = ["file1", "file2", "file3"] + mock_create_backup = mocker.patch( + "bettyfixer.betty_fixer.create_backup", + return_value="file content" + ) + mock_run_vi_script = mocker.patch( + "bettyfixer.betty_fixer.run_vi_script", + return_value="file content" + ) + mock_read_file = mocker.patch( + "bettyfixer.betty_fixer.read_file", + return_value="file content" + ) + mock_write_file = mocker.patch( + "bettyfixer.betty_fixer.write_file", + return_value="file content" + ) + mock_add_line_without_newline = mocker.patch( + "bettyfixer.betty_fixer.add_line_without_newline", + return_value="file content" + ) + mock_process_errors = mocker.patch( + "bettyfixer.betty_fixer.process_errors", + return_value="file content" + ) + mock_open = mocker.patch( + "builtins.open", mocker.mock_open(read_data="file content")) + + fix_betty_style(file_paths) + + mock_process_errors.call_count = len(file_paths) * 2 + # Check that the mocked functions were called with correct args + for file_path in file_paths: + mock_create_backup.assert_any_call(file_path) + mock_run_vi_script.assert_any_call(file_path) + mock_read_file.assert_any_call(file_path) + mock_write_file.assert_any_call(file_path, "file content") + mock_add_line_without_newline.assert_any_call(file_path, "\n") + mock_process_errors.assert_any_call(file_path) + + mock_open.assert_any_call(file_path, 'r', encoding='utf-8') + mock_open.assert_any_call(file_path, 'w', encoding='utf-8') + mock_open().writelines.assert_called_with(["file content"]) + + # ❗ + @pytest.mark.skip( + reason="Needs refactoring [DI, pure functions, decoupling]" + ) + def test_more_than_5_functions_in_the_file(self, mocker): + """Test more_than_5_functions_in_the_file function.""" + pass # pylint: disable=unnecessary-pass + + def test_find_available_file_name_no_existing_files(self, mocker): + """Test find_available_file_name when there are + no existing files with the same base name.""" + mocker.patch('os.path.exists', return_value=False) + assert find_available_file_name("test.txt") == "test1.txt" + + def test_find_available_file_name_with_existing_files(self, mocker): + """Test find_available_file_name when + there are existing files with the same base name.""" + exists_side_effect = [ + True, True, False] + mocker.patch('os.path.exists', side_effect=exists_side_effect) + assert find_available_file_name("test.txt") == "test3.txt" + + def test_find_available_file_name_with_many_existing_files(self, mocker): + """Test find_available_file_name when there are many existing + files with the same base name.""" + exists_side_effect = [ + True] * 100 + [False] + mocker.patch('os.path.exists', side_effect=exists_side_effect) + assert find_available_file_name("test.txt") == "test101.txt" + + def test_copy_remaining_lines(self, mocker): + """Test copy_remaining_lines function.""" + mock_open = mocker.patch("builtins.open", mocker.mock_open()) + lines = ["line1\n", "line2\n", "line3\n"] + start_line = 1 + new_file_path = "new_file.txt" + copy_remaining_lines(lines, start_line, new_file_path) + mock_open.assert_called_once_with(new_file_path, 'w', encoding='utf-8') + mock_open().write.assert_called_once_with("".join(lines[start_line:])) + + def test_betty_handler_no_errors(self, mocker): + """Test betty_handler function when there are no Betty errors.""" + mock_open = mocker.patch( + "builtins.open", mocker.mock_open(read_data="")) + mock_other_handlers = mocker.patch( + "bettyfixer.betty_fixer.other_handlers") + betty_handler("errors.txt") + mock_open.assert_called_once_with("errors.txt", 'r', encoding='utf-8') + mock_other_handlers.assert_not_called() + + def test_betty_handler_with_errors(self, mocker): + """Test betty_handler function when there are Betty errors.""" + error_lines = "More than 40 lines in a function\nline over\ + 80 characters\n" + mock_open = mocker.patch( + "builtins.open", mocker.mock_open(read_data=error_lines)) + mock_other_handlers = mocker.patch( + "bettyfixer.betty_fixer.other_handlers") + mock_extract_and_print_variables = mocker.patch( + "bettyfixer.betty_fixer.extract_and_print_variables", + return_value=("file_path",) + ) + betty_handler("errors.txt") + mock_open.assert_called_once_with("errors.txt", 'r', encoding='utf-8') + mock_extract_and_print_variables.assert_called() + mock_other_handlers.assert_called_with("file_path") + + def test_other_handlers(self, mocker): + """Test other_handlers function.""" + mock_create_tasks_directory = mocker.patch( + "bettyfixer.betty_fixer.create_tasks_directory") + mock_copy_files_to_tasks = mocker.patch( + "bettyfixer.betty_fixer.copy_files_to_tasks") + mock_modify_main_files = mocker.patch( + "bettyfixer.betty_fixer.modify_main_files") + mock_clean_errors_file = mocker.patch( + "bettyfixer.betty_fixer.clean_errors_file") + mock_exctract_errors = mocker.patch( + "bettyfixer.betty_fixer.exctract_errors") + + file_path = "test_file.c" + other_handlers(file_path) + + mock_create_tasks_directory.assert_called_once() + mock_copy_files_to_tasks.assert_called_once_with([file_path]) + mock_modify_main_files.assert_called_once_with([file_path]) + mock_clean_errors_file.assert_called_once_with('errors.txt') + mock_exctract_errors.assert_called_once_with(file_path, 'errors.txt') + + def test_create_tasks_directory(self, mocker): + """Test create_tasks_directory function.""" + mock_makedirs = mocker.patch("os.makedirs") + mock_os_path_exists = mocker.patch( + "os.path.exists", return_value=False) + create_tasks_directory() + mock_os_path_exists.assert_called_once_with("tasks") + mock_makedirs.assert_called_once_with("tasks") + + def test_copy_files_to_tasks(self, mocker): + """Test copy_files_to_tasks function.""" + mock_exists = mocker.patch("os.path.exists", return_value=False) + mock_open = mocker.patch("builtins.open", mocker.mock_open( + read_data="#include \nint main(\ +) {}\n#include \"file.h\"")) + files = ["file1.c", "file2.c"] + copy_files_to_tasks(files) + assert mock_exists.call_count == len(files) + # Each file is opened twice: once for reading and once for writing + assert mock_open.call_count == len(files) * 2 + mock_open.assert_any_call("file1.c", 'r', encoding='utf-8') + + def test_modify_main_files(self, mocker): + """Test modify_main_files function.""" + mock_open = mocker.patch("builtins.open", mocker.mock_open( + read_data="#include \nint main(\ +) {}\n#include \"file.h\"")) + files = ["file1.c", "file2.c"] + modify_main_files(files) + assert mock_open.call_count == len(files) * 2 + mock_open.assert_any_call("file1.c", 'r', encoding='utf-8') + mock_open.assert_any_call("file1.c", 'w', encoding='utf-8') + + def test_record_processed_file(self, mocker): + """Test record_processed_file function.""" + HIDDEN_FILE_NAME = ".processed_files" # pylint: disable=invalid-name + mock_open = mocker.patch("builtins.open", mocker.mock_open()) + filename = "test_file.c" + record_processed_file(filename) + mock_open.assert_called_once_with( + HIDDEN_FILE_NAME, 'a', encoding='utf-8') + mock_open().write.assert_called_once_with(filename + '\n') + + def test_is_file_processed(self, mocker): + """Test is_file_processed function.""" + mocker.patch("os.path.exists", return_value=True) + mocker.patch("builtins.open", mocker.mock_open( + read_data="test_file.c\n")) + + assert is_file_processed("test_file.c") is True + + def test_is_file_processed_not_exists(self, mocker): + """Test is_file_processed function when the file does not exist.""" + mocker.patch("os.path.exists", return_value=False) + + assert is_file_processed("test_file.c") is False + + def test_is_file_processed_not_processed(self, mocker): + """Test is_file_processed function when + the file has not been processed.""" + # Mock os.path.exists to return True + mocker.patch("os.path.exists", return_value=True) + # Mock builtins.open to return a file without the filename in it + mocker.patch("builtins.open", mocker.mock_open( + read_data="other_file.c\n")) + + assert is_file_processed("test_file.c") is False + + def test_main_processed_files(self, mocker, capsys): + """Test main function when .processed_files exists.""" + mocker.patch("bettyfixer.betty_fixer.is_file_processed", + return_value=True) + + with pytest.raises(SystemExit): + main() + captured = capsys.readouterr() + assert "The files have already been processed.\ + Skipping.\n" == captured.out + + def test_main_no_args(self, mocker, capsys): + """Test main function with no arguments.""" + mocker.patch("bettyfixer.betty_fixer.is_file_processed", + return_value=False) + mocker.patch("sys.argv", ["betty_fixer"]) + with pytest.raises(SystemExit): + main() + + captured = capsys.readouterr() + assert "Usage: python -m \ +betty_fixer_package.betty_fixer " in captured.out + + def test_main_header_option(self, mocker): + """Test main function with -H option.""" + mocker.patch("bettyfixer.betty_fixer.is_file_processed", + return_value=False) + mocker.patch("bettyfixer.betty_fixer.betty_check", return_value=True) + mocker.patch("sys.argv", ["betty_fixer", "-H", "header.h"]) + mock_autoproto = mocker.patch("bettyfixer.betty_fixer.autoproto") + + main() + + assert sys.argv[sys.argv.index("-H") + 1] == "header.h" + mock_autoproto.assert_called_once_with( + ".", "header.h") + + def test_main_header_option_no_header(self, mocker, capsys): + """Test main function with -H option but no header. + checks stdout for the error message. + """ + mocker.patch("sys.argv", ["betty_fixer", "-H"]) + main() + capured = capsys.readouterr() + assert "\x1b[31mUsage : bettyfixer -H \ +.h\x1b[39m" in capured.out + + def test_main_header_print_assertion(self, mocker): + """Test main function with -H option but no header. + checks print_header_name_missing being called. + """ + mock_print_header_missing = mocker.patch( + "bettyfixer.betty_fixer.print_header_name_missing") + mocker.patch("sys.argv", ["betty_fixer", "-H"]) + main() + mock_print_header_missing.assert_called_once() + + def test_main_files(self, mocker): + """Test main function with file arguments.""" + mocker.patch("bettyfixer.betty_fixer.is_file_processed", + return_value=False) + mocker.patch("sys.argv", ["betty_fixer", "file1.c", "file2.c"]) + mock_betty_style = mocker.patch( + "bettyfixer.betty_fixer.fix_betty_style") + mock_vi_script = mocker.patch("bettyfixer.betty_fixer.run_vi_script") + mock_record_processed_file = mocker.patch( + "bettyfixer.betty_fixer.record_processed_file") + mocker.patch("os.remove") + + main() + + mock_betty_style.assert_called_once_with( + ["file1.c", "file2.c"]) + mock_vi_script.assert_has_calls( + [mocker.call("file1.c"), mocker.call("file2.c")]) + mock_record_processed_file.assert_has_calls( + [mocker.call("file1.c"), mocker.call("file2.c")]) + os.remove.assert_called_once_with('errors.txt') diff --git a/tests/test_errors_extractor.py b/tests/test_errors_extractor.py new file mode 100644 index 0000000..0c18db3 --- /dev/null +++ b/tests/test_errors_extractor.py @@ -0,0 +1,44 @@ +""" +Test the errors_extractor module. +""" +from subprocess import CompletedProcess, CalledProcessError +from bettyfixer.errors_extractor import exctract_errors + + +def test_exctract_errors_no_errors(mocker): + """ + Test the exctract_errors function when Betty does not return any errors. + """ + mock_subprocess = mocker.patch( + 'subprocess.run', return_value=CompletedProcess( + ['betty', 'file1.c'], 0, 'output', '') + ) + mock_open = mocker.patch('builtins.open', mocker.mock_open()) + + exctract_errors('file1.c', 'errors.txt') + + mock_subprocess.assert_called_once_with( + ['betty', 'file1.c'], + capture_output=True, text=True, check=True + ) + mock_open.assert_called_once_with('errors.txt', 'a', encoding='utf-8') + mock_open().write.assert_called_once_with('output') + + +def test_exctract_errors_with_errors(mocker): + """ + Test the exctract_errors function when Betty returns errors. + """ + mock_subprocess = mocker.patch( + 'subprocess.run', side_effect=CalledProcessError( + ['betty', 'file1.c'], 1, 'output', 'error') + ) + mock_open = mocker.patch('builtins.open', mocker.mock_open()) + + exctract_errors('file1.c', 'errors.txt') + + mock_subprocess.assert_called_once_with( + ['betty', 'file1.c'], + capture_output=True, text=True, check=True + ) + mock_open.assert_called_once_with('errors.txt', 'a', encoding='utf-8')