From e0cdaa44adad0b2a30c4d6e55c407c61bb3f8c3c Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Fri, 30 Dec 2022 15:23:27 +0100 Subject: [PATCH 1/4] Bugfix wrong "hint" --- manageprojects/cookiecutter_templates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manageprojects/cookiecutter_templates.py b/manageprojects/cookiecutter_templates.py index 68b3692..9c87002 100644 --- a/manageprojects/cookiecutter_templates.py +++ b/manageprojects/cookiecutter_templates.py @@ -140,7 +140,7 @@ def update_managed_project( print('Seems that the patch was not applied correctly!') print('Hint: run wiggle on the project:') print() - print(f'./mp wiggle {project_path}') + print(f'./cli.py wiggle {project_path}') print() ############################################################################# From d38ec2970f314d07e34fd152e96ff4f8ebb4233f Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Fri, 30 Dec 2022 15:24:11 +0100 Subject: [PATCH 2/4] enhance test run --- .github/workflows/tests.yml | 3 +++ manageprojects/cli/cli_app.py | 15 ++++++++++++++- pyproject.toml | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index adbb631..74f2d2a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,6 +16,9 @@ jobs: fail-fast: false matrix: python-version: ["3.11", "3.10", "3.9"] + env: + PYTHONUNBUFFERED: 1 + PYTHONWARNINGS: always steps: - uses: actions/checkout@v2 with: diff --git a/manageprojects/cli/cli_app.py b/manageprojects/cli/cli_app.py index 6910ba3..86d3b15 100644 --- a/manageprojects/cli/cli_app.py +++ b/manageprojects/cli/cli_app.py @@ -389,8 +389,21 @@ def test(): """ Run unittests """ + args = sys.argv[2:] + if not args: + args = ('--verbose', '--locals', '--buffer') # Use the CLI from unittest module and pass all args to it: - verbose_check_call(sys.executable, '-m', 'unittest', *sys.argv[2:]) + verbose_check_call( + sys.executable, + '-m', + 'unittest', + *args, + timeout=15 * 60, + extra_env=dict( + PYTHONUNBUFFERED='1', + PYTHONWARNINGS='always', + ), + ) def main(): diff --git a/pyproject.toml b/pyproject.toml index 9ed6c13..3d6ba0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ exclude = ''' branch = true source = ['.'] omit = ['.*', '*/tests/*'] -command_line = '-m unittest --locals --verbose' +command_line = '-m unittest --verbose --locals --buffer' [tool.coverage.report] skip_empty = true From 678d088e2de21ba361bdeec922a4a1858175f18e Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Fri, 30 Dec 2022 15:33:12 +0100 Subject: [PATCH 3/4] cleanup editorconfig --- .editorconfig | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.editorconfig b/.editorconfig index 96ded61..8cc1511 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,12 +9,6 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[*.{bat,cmd,ps1}] -end_of_line = crlf - -[*.{html,css,js}] -insert_final_newline = false - [*.py] max_line_length = 100 @@ -23,4 +17,4 @@ indent_style = tab insert_final_newline = false [*.yml] -indent_style = tab \ No newline at end of file +indent_size = 2 From 5fef070331f375f556b0604bb0a876ebc31c2c23 Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Fri, 30 Dec 2022 16:48:31 +0100 Subject: [PATCH 4/4] Capture and check log output in tests --- manageprojects/test_utils/logs.py | 42 +++++++++ manageprojects/tests/__init__.py | 6 +- manageprojects/tests/test_cookiecutter_api.py | 27 +++--- .../tests/test_cookiecutter_templates.py | 88 ++++++++++++------- manageprojects/tests/test_patching.py | 35 ++++---- .../tests/test_utilities_pyproject_toml.py | 30 ++++--- manageprojects/utilities/log_utils.py | 21 +++-- 7 files changed, 174 insertions(+), 75 deletions(-) create mode 100644 manageprojects/test_utils/logs.py diff --git a/manageprojects/test_utils/logs.py b/manageprojects/test_utils/logs.py new file mode 100644 index 0000000..0ec2b9a --- /dev/null +++ b/manageprojects/test_utils/logs.py @@ -0,0 +1,42 @@ +import logging +from pathlib import Path +from unittest import TestCase + + +class AssertLogs: + """ + Capture and assert log output from different loggers. + """ + + def __init__( + self, + test_case: TestCase, + loggers: tuple[str] = ('manageprojects', 'cookiecutter'), + level=logging.DEBUG, + ): + assertLogs = test_case.assertLogs + + self.logs = [] + for logger in loggers: + self.logs.append(assertLogs(logger, level=level)) + + self.context_managers = None + + def __enter__(self): + self.context_managers = [log.__enter__() for log in self.logs] + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + for log in self.logs: + log.__exit__(exc_type, exc_val, exc_tb) + + def assert_in(self, *test_parts): + outputs = [] + for cm in self.context_managers: + outputs.extend(cm.output) + + output = '\n'.join(outputs) + for part in test_parts: + if isinstance(part, Path): + part = str(part) + assert part in output, f'Log part {part!r} not found in:\n{output}' diff --git a/manageprojects/tests/__init__.py b/manageprojects/tests/__init__.py index d64c245..10561ce 100644 --- a/manageprojects/tests/__init__.py +++ b/manageprojects/tests/__init__.py @@ -1,4 +1,8 @@ from manageprojects.utilities.log_utils import log_config -log_config(log_in_file=False) +log_config( + format='%(levelname)s %(name)s.%(funcName)s %(lineno)d | %(message)s', + log_in_file=False, + raise_log_output=True, +) diff --git a/manageprojects/tests/test_cookiecutter_api.py b/manageprojects/tests/test_cookiecutter_api.py index a36e4bd..977dd1b 100644 --- a/manageprojects/tests/test_cookiecutter_api.py +++ b/manageprojects/tests/test_cookiecutter_api.py @@ -4,6 +4,7 @@ from bx_py_utils.path import assert_is_dir from manageprojects.cookiecutter_api import get_repo_path +from manageprojects.test_utils.logs import AssertLogs from manageprojects.tests.base import BaseTestCase @@ -13,11 +14,14 @@ def test_get_repo_path(self): cookiecutter_template = f'https://github.com/jedie/{repro_name}/' directory = 'test_template1' - repo_path = get_repo_path( - template=cookiecutter_template, - directory=directory, - checkout='84d23bf', # Old version - ) + with AssertLogs(self) as logs: + repo_path = get_repo_path( + template=cookiecutter_template, + directory=directory, + checkout='84d23bf', # Old version + ) + logs.assert_in('repo_dir', repo_path) + self.assertIsInstance(repo_path, Path) self.assertEqual(repo_path.name, directory) assert_is_dir(repo_path.parent / '.git') @@ -34,11 +38,14 @@ def test_get_repo_path(self): ), ) - repo_path = get_repo_path( - template=cookiecutter_template, - directory=directory, - checkout=None, # Current main branch - ) + with AssertLogs(self) as logs: + repo_path = get_repo_path( + template=cookiecutter_template, + directory=directory, + checkout=None, # Current main branch + ) + logs.assert_in('repo_dir', repo_path) + self.assertIsInstance(repo_path, Path) self.assertEqual(repo_path.name, directory) assert_is_dir(repo_path.parent / '.git') diff --git a/manageprojects/tests/test_cookiecutter_templates.py b/manageprojects/tests/test_cookiecutter_templates.py index 6bab045..121b9ce 100644 --- a/manageprojects/tests/test_cookiecutter_templates.py +++ b/manageprojects/tests/test_cookiecutter_templates.py @@ -14,6 +14,7 @@ ManageProjectsMeta, ) from manageprojects.test_utils.git_utils import init_git +from manageprojects.test_utils.logs import AssertLogs from manageprojects.tests.base import BaseTestCase from manageprojects.utilities.pyproject_toml import PyProjectToml from manageprojects.utilities.temp_path import TemporaryDirectory @@ -44,17 +45,20 @@ def test_start_managed_project(self): assert cookiecutter_output_dir.exists() is False assert cookiecutters_dir.exists() is False - result: CookiecutterResult = start_managed_project( - template=cookiecutter_template, - output_dir=cookiecutter_output_dir, - no_input=True, - directory=directory, - config_file=config_file_path, - extra_context={ - 'dir_name': 'a_dir_name', - 'file_name': 'a_file_name', - }, - ) + with AssertLogs(self) as logs: + result: CookiecutterResult = start_managed_project( + template=cookiecutter_template, + output_dir=cookiecutter_output_dir, + no_input=True, + directory=directory, + config_file=config_file_path, + extra_context={ + 'dir_name': 'a_dir_name', + 'file_name': 'a_file_name', + }, + ) + logs.assert_in('Cookiecutter generated here', cookiecutter_output_dir) + self.assertIsInstance(result, CookiecutterResult) git_path = cookiecutters_dir / repro_name self.assertEqual(result.git_path, git_path) @@ -80,8 +84,10 @@ def test_start_managed_project(self): project_path = cookiecutter_output_dir / 'a_dir_name' # pyproject.toml created? - toml = PyProjectToml(project_path=project_path) - result: ManageProjectsMeta = toml.get_mp_meta() + with AssertLogs(self, loggers=('manageprojects',)) as logs: + toml = PyProjectToml(project_path=project_path) + result: ManageProjectsMeta = toml.get_mp_meta() + logs.assert_in('Read existing pyproject.toml') self.assertIsInstance(result, ManageProjectsMeta) self.assertEqual( result, @@ -105,8 +111,14 @@ def test_start_managed_project(self): # Test clone a existing project cloned_path = main_temp_path / 'cloned_project' - clone_result: CookiecutterResult = clone_project( - project_path=project_path, destination=cloned_path, no_input=True + with AssertLogs(self) as logs: + clone_result: CookiecutterResult = clone_project( + project_path=project_path, destination=cloned_path, no_input=True + ) + logs.assert_in( + 'Read existing pyproject.toml', + "Call 'cookiecutter'", + 'Create new pyproject.toml', ) self.assertEqual( clone_result.cookiecutter_context, @@ -123,8 +135,10 @@ def test_start_managed_project(self): self.assertEqual(clone_result.destination_path, end_path) # pyproject.toml created? - toml = PyProjectToml(project_path=end_path) - result: ManageProjectsMeta = toml.get_mp_meta() + with AssertLogs(self, loggers=('manageprojects',)) as logs: + toml = PyProjectToml(project_path=end_path) + result: ManageProjectsMeta = toml.get_mp_meta() + logs.assert_in('Read existing pyproject.toml') self.assertIsInstance(result, ManageProjectsMeta) self.assertEqual( result, @@ -204,13 +218,15 @@ def test_update_project(self): git, from_rev = init_git(template_path, comment='Git init template.') from_date = git.get_commit_date(verbose=False) - toml = PyProjectToml(project_path=project_path) - toml.init( - revision=from_rev, - dt=from_date, - template=str(template_path), - directory=template_dir_name, - ) + with AssertLogs(self, loggers=('manageprojects',)) as logs: + toml = PyProjectToml(project_path=project_path) + toml.init( + revision=from_rev, + dt=from_date, + template=str(template_path), + directory=template_dir_name, + ) + logs.assert_in('Create new pyproject.toml') toml.create_or_update_cookiecutter_context(context=context) toml.save() toml_content = toml.path.read_text() @@ -249,12 +265,18 @@ def test_update_project(self): ) self.assertFalse(patch_file_path.exists()) - result = update_managed_project( - project_path=project_path, - password=None, - config_file=config_file_path, - cleanup=False, # Keep temp files if this test fails, for better debugging - no_input=True, # No user input in tests ;) + with AssertLogs(self) as logs: + result = update_managed_project( + project_path=project_path, + password=None, + config_file=config_file_path, + cleanup=False, # Keep temp files if this test fails, for better debugging + no_input=True, # No user input in tests ;) + ) + logs.assert_in( + 'No temp files cleanup', + 'Cookiecutter generated', + 'Read existing pyproject.toml', ) self.assert_file_content( @@ -294,7 +316,9 @@ def test_update_project(self): ) # Check updated toml file: - toml = PyProjectToml(project_path=project_path) - mp_meta: ManageProjectsMeta = toml.get_mp_meta() + with AssertLogs(self, loggers=('manageprojects',)) as logs: + toml = PyProjectToml(project_path=project_path) + mp_meta: ManageProjectsMeta = toml.get_mp_meta() + logs.assert_in('Read existing pyproject.toml') self.assertEqual(mp_meta.initial_revision, from_rev) self.assertEqual(mp_meta.applied_migrations, [to_rev]) diff --git a/manageprojects/tests/test_patching.py b/manageprojects/tests/test_patching.py index 8e90ce2..bbcd716 100644 --- a/manageprojects/tests/test_patching.py +++ b/manageprojects/tests/test_patching.py @@ -8,6 +8,7 @@ from manageprojects.data_classes import GenerateTemplatePatchResult from manageprojects.patching import generate_template_patch, make_git_diff from manageprojects.test_utils.git_utils import init_git +from manageprojects.test_utils.logs import AssertLogs from manageprojects.tests.base import BaseTestCase from manageprojects.utilities.temp_path import TemporaryDirectory @@ -33,12 +34,14 @@ def test_make_git_diff(self): Path(from_path, 'old_name.txt').write_text(renamed_file_content) Path(to_path, 'new_name.txt').write_text(renamed_file_content) - patch = make_git_diff( - temp_path=main_temp_path, - from_path=from_path, - to_path=to_path, - verbose=True, - ) + with AssertLogs(self, loggers=('manageprojects',)) as logs: + patch = make_git_diff( + temp_path=main_temp_path, + from_path=from_path, + to_path=to_path, + verbose=True, + ) + logs.assert_in('old_name.txt', 'new_name.txt') self.assertIn('diff --git a/file1.txt b/file1.txt', patch) self.assertIn('-Rev 1', patch) self.assertIn('+Rev 2', patch) @@ -104,15 +107,17 @@ def test_generate_template_patch(self): ) self.assertFalse(patch_file_path.exists()) - result = generate_template_patch( - project_path=project_path, - template=str(repo_path), - directory=None, - from_rev=from_rev, - replay_context={}, - cleanup=False, # Keep temp files if this test fails, for better debugging - no_input=True, # No user input in tests ;) - ) + with AssertLogs(self) as logs: + result = generate_template_patch( + project_path=project_path, + template=str(repo_path), + directory=None, + from_rev=from_rev, + replay_context={}, + cleanup=False, # Keep temp files if this test fails, for better debugging + no_input=True, # No user input in tests ;) + ) + logs.assert_in("Call 'cookiecutter'", 'Write patch file') self.assertIsInstance(result, GenerateTemplatePatchResult) self.assertEqual(result.patch_file_path, patch_file_path) diff --git a/manageprojects/tests/test_utilities_pyproject_toml.py b/manageprojects/tests/test_utilities_pyproject_toml.py index 3738be1..7c66c32 100644 --- a/manageprojects/tests/test_utilities_pyproject_toml.py +++ b/manageprojects/tests/test_utilities_pyproject_toml.py @@ -3,6 +3,7 @@ from bx_py_utils.test_utils.datetime import parse_dt from manageprojects.data_classes import ManageProjectsMeta +from manageprojects.test_utils.logs import AssertLogs from manageprojects.tests.base import BaseTestCase from manageprojects.utilities.pyproject_toml import PyProjectToml from manageprojects.utilities.temp_path import TemporaryDirectory @@ -12,8 +13,11 @@ class PyProjectTomlTestCase(BaseTestCase): maxDiff = None def test_basic(self): - with TemporaryDirectory(prefix='test_basic') as temp_path: + with TemporaryDirectory(prefix='test_basic') as temp_path, AssertLogs( + self, loggers=('manageprojects',) + ) as logs: toml = PyProjectToml(project_path=temp_path) + logs.assert_in('Create new pyproject.toml') self.assertEqual(toml.path, temp_path / 'pyproject.toml') self.assertFalse(toml.path.exists()) @@ -196,9 +200,12 @@ def test_basic(self): ) self.assertIsInstance(data.cookiecutter_context, dict) self.assertEqual(type(data.cookiecutter_context), dict) + logs.assert_in('Create new pyproject.toml', 'Read existing pyproject.toml') def test_expand_existing_toml(self): - with TemporaryDirectory(prefix='test_basic') as temp_path: + with TemporaryDirectory(prefix='test_basic') as temp_path, AssertLogs( + self, loggers=('manageprojects',) + ) as logs: file_path = temp_path / 'pyproject.toml' file_path.write_text( inspect.cleandoc( @@ -214,14 +221,16 @@ def test_expand_existing_toml(self): ) ) - toml = PyProjectToml(project_path=temp_path) - toml.init( - revision='abc0001', - dt=parse_dt('2000-01-01T00:00:00+0000'), - template='/foo/bar/template', - directory=None, - ) - toml.save() + with AssertLogs(self, loggers=('manageprojects',)) as logs: + toml = PyProjectToml(project_path=temp_path) + toml.init( + revision='abc0001', + dt=parse_dt('2000-01-01T00:00:00+0000'), + template='/foo/bar/template', + directory=None, + ) + toml.save() + logs.assert_in('Read existing pyproject.toml') self.assert_file_content( file_path, @@ -253,3 +262,4 @@ def test_expand_existing_toml(self): cookiecutter_context=None, ), ) + logs.assert_in('Read existing pyproject.toml') diff --git a/manageprojects/utilities/log_utils.py b/manageprojects/utilities/log_utils.py index 4919d7b..fa1efe8 100644 --- a/manageprojects/utilities/log_utils.py +++ b/manageprojects/utilities/log_utils.py @@ -2,21 +2,25 @@ import logging import tempfile +from bx_py_utils.test_utils.log_utils import RaiseLogUsage -def logger_setup(*, logger_name, level, format, log_filename): + +def logger_setup(*, logger_name, level, format, log_filename, raise_log_output): logger = logging.getLogger(logger_name) is_configured = logger.handlers and logger.level if not is_configured: logger.setLevel(level) - if log_filename: - ch = logging.FileHandler(filename=log_filename) + if raise_log_output: + handler = RaiseLogUsage() + elif log_filename: + handler = logging.FileHandler(filename=log_filename) else: - ch = logging.StreamHandler() - ch.setLevel(level) - ch.setFormatter(logging.Formatter(format)) + handler = logging.StreamHandler() + handler.setLevel(level) + handler.setFormatter(logging.Formatter(format)) - logger.addHandler(ch) + logger.addHandler(handler) def print_log_info(filename): @@ -27,6 +31,7 @@ def log_config( level=logging.DEBUG, format='%(asctime)s %(levelname)s %(name)s.%(funcName)s %(lineno)d | %(message)s', log_in_file=True, + raise_log_output=False, ): if log_in_file: log_file = tempfile.NamedTemporaryFile( @@ -41,12 +46,14 @@ def log_config( level=level, format=format, log_filename=log_filename, + raise_log_output=raise_log_output, ) logger_setup( logger_name='cookiecutter', level=level, format=format, log_filename=log_filename, + raise_log_output=raise_log_output, ) if log_filename: