From c4c71e08be66a37dfd05208f9470f0d77b2e2487 Mon Sep 17 00:00:00 2001 From: Stephane Odul Date: Mon, 22 Jan 2024 00:07:25 -0800 Subject: [PATCH 1/2] Cleanup test_runner.py with more modern python style. --- green/test/test_runner.py | 316 +++++++++++++++++--------------------- 1 file changed, 139 insertions(+), 177 deletions(-) diff --git a/green/test/test_runner.py b/green/test/test_runner.py index 1fd5348..837bc5b 100644 --- a/green/test/test_runner.py +++ b/green/test/test_runner.py @@ -1,6 +1,7 @@ import copy from io import StringIO import os +import pathlib import platform import shutil import signal @@ -8,7 +9,7 @@ import tempfile from textwrap import dedent import unittest -from unittest.mock import MagicMock +from unittest import mock import weakref from green.config import default_args @@ -19,7 +20,6 @@ from green.suite import GreenTestSuite -global skip_testtools skip_testtools = False try: import testtools @@ -31,7 +31,6 @@ # --- Helper stuff --- -global importable_function_worked importable_function_worked = False @@ -50,7 +49,7 @@ def _crashy(): """ Used by TestInitializerOrFinalizer.test_crash() """ - raise Exception("Oops! I crashed.") + raise AssertionError("Oops! I crashed.") # --- End of helper stuff @@ -128,7 +127,7 @@ def test_stdout(self): run(GreenTestSuite(), sys.stdout, args=self.args) self.assertIn("No Tests Found", self.stream.getvalue()) - def test_GreenStream(self): + def test_green_stream(self): """ run() can use a GreenStream for output. """ @@ -141,23 +140,21 @@ def test_verbose3(self): verbose=3 causes version output, and an empty test case passes. """ self.args.verbose = 3 - sub_tmpdir = tempfile.mkdtemp(dir=self.tmpdir) - fh = open(os.path.join(sub_tmpdir, "test_verbose3.py"), "w") - fh.write( - dedent( - """ + sub_tmpdir = pathlib.Path(tempfile.mkdtemp(dir=self.tmpdir)) + content = dedent(""" import unittest class Verbose3(unittest.TestCase): def test01(self): pass """ - ) ) - fh.close() + (sub_tmpdir / "test_verbose3.py").write_text(content, encoding="utf-8") os.chdir(sub_tmpdir) - tests = self.loader.loadTargets("test_verbose3") - result = run(tests, self.stream, self.args) - os.chdir(self.startdir) + try: + tests = self.loader.loadTargets("test_verbose3") + result = run(tests, self.stream, self.args) + finally: + os.chdir(self.startdir) self.assertEqual(result.testsRun, 1) self.assertIn("Green", self.stream.getvalue()) self.assertIn("OK", self.stream.getvalue()) @@ -167,27 +164,26 @@ def test_warnings(self): setting warnings='always' doesn't crash """ self.args.warnings = "always" - sub_tmpdir = tempfile.mkdtemp(dir=self.tmpdir) - fh = open(os.path.join(sub_tmpdir, "test_warnings.py"), "w") - fh.write( - dedent( - """ + sub_tmpdir = pathlib.Path(tempfile.mkdtemp(dir=self.tmpdir)) + content = dedent( + """ import unittest class Warnings(unittest.TestCase): def test01(self): pass """ - ) ) - fh.close() + (sub_tmpdir / "test_warnings.py").write_text(content, encoding="utf-8") os.chdir(sub_tmpdir) - tests = self.loader.loadTargets("test_warnings") - result = run(tests, self.stream, self.args) - os.chdir(self.startdir) + try: + tests = self.loader.loadTargets("test_warnings") + result = run(tests, self.stream, self.args) + finally: + os.chdir(self.startdir) self.assertEqual(result.testsRun, 1) self.assertIn("OK", self.stream.getvalue()) - def test_noTestsFound(self): + def test_no_tests_found(self): """ When we don't find any tests, we say so. """ @@ -196,27 +192,26 @@ def test_noTestsFound(self): self.assertEqual(result.testsRun, 0) self.assertEqual(result.wasSuccessful(), False) - def test_failedSaysSo(self): + def test_failed_says_so(self): """ A failing test case causes the whole run to report 'FAILED' """ - sub_tmpdir = tempfile.mkdtemp(dir=self.tmpdir) - fh = open(os.path.join(sub_tmpdir, "test_failed.py"), "w") - fh.write( - dedent( - """ + sub_tmpdir = pathlib.Path(tempfile.mkdtemp(dir=self.tmpdir)) + content = dedent( + """ import unittest class Failed(unittest.TestCase): def test01(self): self.assertTrue(False) """ - ) ) - fh.close() + (sub_tmpdir / "test_failed.py").write_text(content, encoding="utf-8") os.chdir(sub_tmpdir) - tests = self.loader.loadTargets("test_failed") - result = run(tests, self.stream, self.args) - os.chdir(self.startdir) + try: + tests = self.loader.loadTargets("test_failed") + result = run(tests, self.stream, self.args) + finally: + os.chdir(self.startdir) self.assertEqual(result.testsRun, 1) self.assertIn("FAILED", self.stream.getvalue()) @@ -224,11 +219,9 @@ def test_failfast(self): """ failfast causes the testing to stop after the first failure. """ - sub_tmpdir = tempfile.mkdtemp(dir=self.tmpdir) - fh = open(os.path.join(sub_tmpdir, "test_failfast.py"), "w") - fh.write( - dedent( - """ + sub_tmpdir = pathlib.Path(tempfile.mkdtemp(dir=self.tmpdir)) + content = dedent( + """ import unittest class SIGINTCase(unittest.TestCase): def test00(self): @@ -236,25 +229,24 @@ def test00(self): def test01(self): pass """ - ) ) - fh.close() + (sub_tmpdir / "test_failfast.py").write_text(content, encoding="utf-8") os.chdir(sub_tmpdir) - tests = self.loader.loadTargets("test_failfast") - self.args.failfast = True - result = run(tests, self.stream, self.args) - os.chdir(self.startdir) + try: + tests = self.loader.loadTargets("test_failfast") + self.args.failfast = True + result = run(tests, self.stream, self.args) + finally: + os.chdir(self.startdir) self.assertEqual(result.testsRun, 1) - def test_systemExit(self): + def test_system_exit(self): """ Raising a SystemExit gets caught and reported. """ - sub_tmpdir = tempfile.mkdtemp(dir=self.tmpdir) - fh = open(os.path.join(sub_tmpdir, "test_systemexit.py"), "w") - fh.write( - dedent( - """ + sub_tmpdir = pathlib.Path(tempfile.mkdtemp(dir=self.tmpdir)) + content = dedent( + """ import unittest class SystemExitCase(unittest.TestCase): def test00(self): @@ -262,13 +254,14 @@ def test00(self): def test01(self): pass """ - ) ) - fh.close() + (sub_tmpdir / "test_systemexit.py").write_text(content, encoding="utf-8") os.chdir(sub_tmpdir) - tests = self.loader.loadTargets("test_systemexit") - result = run(tests, self.stream, self.args) - os.chdir(self.startdir) + try: + tests = self.loader.loadTargets("test_systemexit") + result = run(tests, self.stream, self.args) + finally: + os.chdir(self.startdir) self.assertEqual(result.testsRun, 2) @@ -300,66 +293,54 @@ def tearDown(self): shutil.rmtree(self.tmpdir, ignore_errors=True) del self.stream - def test_catchProcessSIGINT(self): + @unittest.skipIf(platform.system() == "Windows", "Windows doesn't have SIGINT.") + def test_catch_process_sigint(self): """ run() can catch SIGINT while running a process. """ - if platform.system() == "Windows": - self.skipTest("This test is for posix-specific behavior.") # Mock the list of TestResult instances that should be stopped, # otherwise the actual TestResult that is running this test will be # told to stop when we send SIGINT - sub_tmpdir = tempfile.mkdtemp(dir=self.tmpdir) + sub_tmpdir = pathlib.Path(tempfile.mkdtemp(dir=self.tmpdir)) saved__results = unittest.signals._results unittest.signals._results = weakref.WeakKeyDictionary() self.addCleanup(setattr, unittest.signals, "_results", saved__results) - fh = open(os.path.join(sub_tmpdir, "test_sigint.py"), "w") - fh.write( - dedent( - """ + content = dedent( + """ import os import signal import unittest - class SIGINTCase(unittest.TestCase): - def test00(self): + class TestSigint(unittest.TestCase): + def test_00(self): os.kill({}, signal.SIGINT) - """.format( - os.getpid() - ) - ) - ) - fh.close() + """ + ).format(os.getpid()) + (sub_tmpdir / "test_sigint.py").write_text(content, encoding="utf-8") os.chdir(sub_tmpdir) - tests = self.loader.loadTargets("test_sigint") - self.args.processes = 2 - run(tests, self.stream, self.args) - os.chdir(TestProcesses.startdir) + try: + tests = self.loader.loadTargets("test_sigint") + self.args.processes = 2 + run(tests, self.stream, self.args) + finally: + os.chdir(TestProcesses.startdir) - def test_collisionProtection(self): + def test_collision_protection(self): """ If tempfile.gettempdir() is used for dir, using same testfile name will not collide. """ - sub_tmpdir = tempfile.mkdtemp(dir=self.tmpdir) + sub_tmpdir = pathlib.Path(tempfile.mkdtemp(dir=self.tmpdir)) # Child setup # pkg/__init__.py - fh = open(os.path.join(sub_tmpdir, "__init__.py"), "w") - fh.write("\n") - fh.close() - # pkg/target_module.py - fh = open(os.path.join(sub_tmpdir, "some_module.py"), "w") - fh.write("a = 1\n") - fh.close() + (sub_tmpdir / "__init__.py").write_text("\n", encoding="utf-8") + # pkg/some_module.py + (sub_tmpdir / "some_module.py").write_text("a = 1\n", encoding="utf-8") # pkg/test/__init__.py - os.mkdir(os.path.join(sub_tmpdir, "test")) - fh = open(os.path.join(sub_tmpdir, "test", "__init__.py"), "w") - fh.write("\n") - fh.close() - # pkg/test/test_target_module.py - fh = open(os.path.join(sub_tmpdir, "test", "test_some_module.py"), "w") - fh.write( - dedent( - """ + os.mkdir(sub_tmpdir / "test") + (sub_tmpdir / "test/__init__.py").write_text("\n", encoding="utf-8") + # pkg/test/test_some_module.py + content = dedent( + """ import os import tempfile import time @@ -389,142 +370,123 @@ def testTwo(self): actual = fh.read() fh.close() self.assertEqual(msg, actual) - """.format( - os.path.basename(sub_tmpdir) - ) - ) - ) - fh.close() + """ + ).format(os.path.basename(sub_tmpdir)) + (sub_tmpdir / "test/test_some_module.py").write_text(content, encoding="utf-8") # Load the tests os.chdir(self.tmpdir) - tests = self.loader.loadTargets(".") - self.args.processes = 2 - self.args.termcolor = False try: - run(tests, self.stream, self.args) - except KeyboardInterrupt: - os.kill(os.getpid(), signal.SIGINT) - os.chdir(TestProcesses.startdir) + tests = self.loader.loadTargets(".") + self.args.processes = 2 + self.args.termcolor = False + try: + run(tests, self.stream, self.args) + except KeyboardInterrupt: + os.kill(os.getpid(), signal.SIGINT) + finally: + os.chdir(TestProcesses.startdir) self.assertIn("OK", self.stream.getvalue()) - def test_detectNumProcesses(self): + def test_detect_num_processes(self): """ args.processes = 0 causes auto-detection of number of processes. """ - sub_tmpdir = tempfile.mkdtemp(dir=self.tmpdir) - # pkg/__init__.py - fh = open(os.path.join(sub_tmpdir, "__init__.py"), "w") - fh.write("\n") - fh.close() - fh = open(os.path.join(sub_tmpdir, "test_autoprocesses.py"), "w") - fh.write( - dedent( - """ + sub_tmpdir = pathlib.Path(tempfile.mkdtemp(dir=self.tmpdir)) + (sub_tmpdir / "__init__.py").write_text("\n", encoding="utf-8") + content = dedent( + """ import unittest class A(unittest.TestCase): def testPasses(self): pass""" - ) ) - fh.close() + (sub_tmpdir / "test_detectNumProcesses.py").write_text(content, encoding="utf-8") # Load the tests os.chdir(self.tmpdir) - tests = self.loader.loadTargets(".") - self.args.processes = 0 - run(tests, self.stream, self.args) - os.chdir(TestProcesses.startdir) + try: + tests = self.loader.loadTargets(".") + self.args.processes = 0 + run(tests, self.stream, self.args) + finally: + os.chdir(TestProcesses.startdir) self.assertIn("OK", self.stream.getvalue()) - def test_runCoverage(self): + def test_run_coverage(self): """ Running coverage in process mode doesn't crash """ try: import coverage - - coverage - except: + except ImportError: self.skipTest("Coverage needs to be installed for this test") - sub_tmpdir = tempfile.mkdtemp(dir=self.tmpdir) - # pkg/__init__.py - fh = open(os.path.join(sub_tmpdir, "__init__.py"), "w") - fh.write("\n") - fh.close() - fh = open(os.path.join(sub_tmpdir, "test_coverage.py"), "w") - fh.write( - dedent( - """ + sub_tmpdir = pathlib.Path(tempfile.mkdtemp(dir=self.tmpdir)) + (sub_tmpdir / "__init__.py").write_text("\n", encoding="utf-8") + content = dedent( + """ import unittest class A(unittest.TestCase): def testPasses(self): pass""" - ) ) - fh.close() + (sub_tmpdir / "test_coverage.py").write_text(content, encoding="utf-8") # Load the tests os.chdir(self.tmpdir) - tests = self.loader.loadTargets(".") - self.args.processes = 2 - self.args.run_coverage = True - self.args.cov = MagicMock() - run(tests, self.stream, self.args, testing=True) - os.chdir(TestProcesses.startdir) + try: + tests = self.loader.loadTargets(".") + self.args.processes = 2 + self.args.run_coverage = True + self.args.cov = mock.MagicMock() + run(tests, self.stream, self.args, testing=True) + finally: + os.chdir(TestProcesses.startdir) self.assertIn("OK", self.stream.getvalue()) - def test_badTest(self): + def test_bad_test(self): """ Bad syntax in a testfile is caught as a test error. """ - sub_tmpdir = tempfile.mkdtemp(dir=self.tmpdir) - # pkg/__init__.py - fh = open(os.path.join(sub_tmpdir, "__init__.py"), "w") - fh.write("\n") - fh.close() - # pkg/test/test_target_module.py - fh = open(os.path.join(sub_tmpdir, "test_bad_syntax.py"), "w") - fh.write("aoeu") - fh.close() + sub_tmpdir = pathlib.Path(tempfile.mkdtemp(dir=self.tmpdir)) + (sub_tmpdir / "__init__.py").write_text("\n", encoding="utf-8") + (sub_tmpdir / "test_bad_syntax.py").write_text("aoeu", encoding="utf-8") # Load the tests os.chdir(self.tmpdir) - tests = self.loader.loadTargets(".") - self.args.processes = 2 - os.chdir(TestProcesses.startdir) + try: + tests = self.loader.loadTargets(".") + self.args.processes = 2 + finally: + os.chdir(TestProcesses.startdir) self.assertRaises(ImportError, run, tests, self.stream, self.args) - def test_uncaughtException(self): + def test_uncaught_exception(self): """ Exceptions that escape the test framework get caught by poolRunner and reported as a failure. For example, the testtools implementation of TestCase unwisely (but deliberately) lets SystemExit exceptions through. """ - global skip_testtools if skip_testtools: self.skipTest("testtools must be installed to run this test.") - sub_tmpdir = tempfile.mkdtemp(dir=self.tmpdir) + sub_tmpdir = pathlib.Path(tempfile.mkdtemp(dir=self.tmpdir)) # pkg/__init__.py - fh = open(os.path.join(sub_tmpdir, "__init__.py"), "w") - fh.write("\n") - fh.close() - fh = open(os.path.join(sub_tmpdir, "test_uncaught.py"), "w") - fh.write( - dedent( - """ + (sub_tmpdir / "__init__.py").write_text("\n", encoding="utf-8") + content = dedent( + """ import testtools class Uncaught(testtools.TestCase): def test_uncaught(self): raise SystemExit(0) - """ - ) + """ ) - fh.close() + (sub_tmpdir / "test_uncaught.py").write_text(content, encoding="utf-8") # Load the tests os.chdir(self.tmpdir) - tests = self.loader.loadTargets(".") - self.args.processes = 2 - run(tests, self.stream, self.args) - os.chdir(TestProcesses.startdir) + try: + tests = self.loader.loadTargets(".") + self.args.processes = 2 + run(tests, self.stream, self.args) + finally: + os.chdir(TestProcesses.startdir) self.assertIn("FAILED", self.stream.getvalue()) def test_empty(self): From 9069172cea34ccdd2a6488e2e4d2d8eae1d70613 Mon Sep 17 00:00:00 2001 From: Stephane Odul Date: Mon, 22 Jan 2024 14:37:03 -0800 Subject: [PATCH 2/2] Simplify dev testing setup. - Add dev dependencies to setup.cfg through requirements-dev.txt, to simpl - Add support to test python versions locally with docker. --- .github/workflows/ci.yml | 3 +-- CHANGELOG.md | 3 +++ Makefile | 12 +++++++++--- requirements-dev.txt | 6 ++++++ requirements-optional.txt | 5 ----- requirements.txt | 2 +- setup.cfg | 2 ++ 7 files changed, 22 insertions(+), 11 deletions(-) create mode 100644 requirements-dev.txt delete mode 100644 requirements-optional.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ef1597..f068a4d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,8 +31,7 @@ jobs: - name: Install run: | python -m pip install --upgrade pip - pip install --upgrade -r requirements-optional.txt - pip install --upgrade -e . + pip install --upgrade .[dev] - name: Format run: black --check --diff green example diff --git a/CHANGELOG.md b/CHANGELOG.md index 75bd96e..ddd04ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Unreleased #### Date TBD +- Cleanup test_runner.py to more modern Python style. +- Simplify green's dev testing setup. + # Version 4.0.0 #### 16 Jan 2024 diff --git a/Makefile b/Makefile index 096e4e6..1b8fa5b 100644 --- a/Makefile +++ b/Makefile @@ -22,12 +22,19 @@ test: test-versions test-installed test-coverage @echo "\n(test) completed\n" test-local: - @pip3 install -r requirements-optional.txt + @pip3 install --upgrade -e .[dev] @make test-installed make test-versions make test-coverage @# test-coverage needs to be last in deps, don't clean after it runs! +test-on-containers: clean-silent + @# Run the tests on pristine containers to isolate us from the local environment. + @for version in 3.8 3.9 3.10 3.11 3.12.0; do \ + docker run --rm -it -v `pwd`:/green python:$$version \ + bash -c "python --version; cd /green && pip install -e .[dev] && ./g green" ; \ + done + test-coverage: @# Generate coverage files for travis builds (don't clean after this!) @make clean-silent @@ -40,10 +47,9 @@ test-installed: @rm -rf venv-installed @python3 -m venv venv-installed @make clean-silent - source venv-installed/bin/activate; pip3 install -r requirements-optional.txt source venv-installed/bin/activate; python3 setup.py sdist tar zxvf dist/green-$(VERSION).tar.gz - source venv-installed/bin/activate; cd green-$(VERSION) && pip3 install . + source venv-installed/bin/activate; cd green-$(VERSION) && pip3 install --upgrade .[dev] source venv-installed/bin/activate; green -vvvv green @rm -rf venv-installed @make clean-silent diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..2ccf57d --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,6 @@ +black +# coverage[toml] needs to be listed explictly for python < 3.11. +coverage[toml]; python_full_version<="3.11.0a6" +django +mypy +testtools diff --git a/requirements-optional.txt b/requirements-optional.txt deleted file mode 100644 index 97c2e38..0000000 --- a/requirements-optional.txt +++ /dev/null @@ -1,5 +0,0 @@ -black -django -mypy -testtools --r requirements.txt diff --git a/requirements.txt b/requirements.txt index 012098a..536838f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ colorama -coverage[toml] +coverage lxml setuptools unidecode diff --git a/setup.cfg b/setup.cfg index dccf1a2..3163e0a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -71,6 +71,8 @@ install_requires = file:requirements.txt include_package_data = True packages = find: +[options.extras_require] +dev = file:requirements-dev.txt [options.package_data] green = VERSION, shell_completion.sh