diff --git a/.github/workflows/ci-sage.yml b/.github/workflows/ci-sage.yml index 2be08911..47eb7286 100644 --- a/.github/workflows/ci-sage.yml +++ b/.github/workflows/ci-sage.yml @@ -50,20 +50,6 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -env: - # Ubuntu packages to install so that the project's "setup.py sdist" can succeed - DIST_PREREQ: python3-setuptools autoconf - # Name of this project in the Sage distribution - SPKG: cysignals - # Sage distribution packages to build - TARGETS_PRE: build/make/Makefile - TARGETS: SAGE_CHECK=no SAGE_CHECK_PACKAGES="cysignals,cypari" cysignals cypari - TARGETS_OPTIONAL: build/make/Makefile - # Standard setting: Test the current beta release of Sage - SAGE_REPO: sagemath/sage - SAGE_REF: develop - REMOVE_PATCHES: "*" - jobs: dist: @@ -84,7 +70,8 @@ jobs: && echo "sage-package create ${{ env.SPKG }} --version git --tarball ${{ env.SPKG }}-git.tar.gz --type=standard" > upstream/update-pkgs.sh \ && if [ -n "${{ env.REMOVE_PATCHES }}" ]; then echo "(cd ../build/pkgs/${{ env.SPKG }}/patches && rm -f ${{ env.REMOVE_PATCHES }}; :)" >> upstream/update-pkgs.sh; fi \ && ls -l upstream/ - - uses: actions/upload-artifact@v2 + - name: Upload artifact + uses: actions/upload-artifact@v4 with: path: upstream name: upstream @@ -112,7 +99,7 @@ jobs: choco install make autoconf gcc-core gcc-g++ python3${{ matrix.python-version }}-devel --source cygwin - name: Install dependencies run: | - C:\\tools\\cygwin\\bin\\bash -l -x -c 'export PATH=/usr/local/bin:/usr/bin && cd $(cygpath -u "$GITHUB_WORKSPACE") && python3.${{ matrix.python-version }} -m pip install --upgrade pip' + C:\\tools\\cygwin\\bin\\bash -l -x -c 'export PATH=/usr/local/bin:/usr/bin && cd $(cygpath -u "$GITHUB_WORKSPACE") && python3.${{ matrix.python-version }} -m pip install --upgrade pip && python3.${{ matrix.python-version }} -m pip install --upgrade -r requirements.txt' - name: Build and check run: | C:\\tools\\cygwin\\bin\\bash -l -x -c 'export PATH=/usr/local/bin:/usr/bin && cd $(cygpath -u "$GITHUB_WORKSPACE") && make check PYTHON=python3.${{ matrix.python-version }}' @@ -135,7 +122,8 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip + pip install --upgrade pip + pip install --upgrade -r requirements.txt - name: Build and check run: | make -j4 check @@ -196,7 +184,8 @@ jobs: - name: Install dependencies run: | brew install autoconf - python -m pip install --upgrade pip + pip install --upgrade pip + pip install --upgrade -r requirements.txt - name: Build and check # Work around https://github.com/sagemath/cysignals/issues/179 run: | diff --git a/MANIFEST.in b/MANIFEST.in index c8ea784e..f6b29485 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ global-include README README.rst VERSION LICENSE global-include Makefile configure configure.ac -global-include setup.py rundoctests.py testgdb.py *.pyx +global-include setup.py testgdb.py *.pyx graft src graft docs/source prune build @@ -11,3 +11,5 @@ prune example/.* exclude src/config.h exclude src/cysignals/signals.pxd exclude src/cysignals/cysignals_config.h +exclude src/cysignals/conftest.py +exclude src/scripts/conftest.py diff --git a/Makefile b/Makefile index 5ffb2143..c4b62c1a 100644 --- a/Makefile +++ b/Makefile @@ -6,8 +6,6 @@ PYTHON = python3 PIP = $(PYTHON) -m pip -v LS_R = ls -Ra1 -DOCTEST = $(PYTHON) -B rundoctests.py - ##################### # Build @@ -66,11 +64,11 @@ check-all: check-install: check-doctest check-example check-doctest: install - $(DOCTEST) src/cysignals/*.pyx + $(PYTHON) -m pytest . check-example: install $(PYTHON) -m pip install -U build setuptools wheel Cython - cd example && $(PYTHON) -m build --no-isolation . + $(PYTHON) -m build --no-isolation example check-gdb: install $(PYTHON) testgdb.py diff --git a/pyproject.toml b/pyproject.toml index 162900f5..40026ddb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ [build-system] requires = ['setuptools', 'Cython>=0.28'] build-backend = "setuptools.build_meta" + +[tool.pytest.ini_options] +addopts = "--doctest-modules --import-mode importlib" +norecursedirs = "builddir docs example" diff --git a/requirements.txt b/requirements.txt index 19132679..e5adff56 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ wheel Cython Sphinx flake8 +pytest>=8.0.0 diff --git a/rundoctests.py b/rundoctests.py deleted file mode 100755 index 8db318ae..00000000 --- a/rundoctests.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python3 -# -# Run doctests for cysignals -# -# We add the ELLIPSIS flag by default and we run all tests even if -# one fails. -# -import os -import sys -import doctest -from doctest import DocTestParser, Example, SKIP -from multiprocessing import Process - -flags = doctest.ELLIPSIS -timeout = 600 - -filenames = sys.argv[1:] -if os.name == 'nt': - notinwindows = set(['src/cysignals/pysignals.pyx', - 'src/cysignals/alarm.pyx', - 'src/cysignals/pselect.pyx']) - - filenames = [f for f in filenames if f not in notinwindows] - - -# Add an option to flag doctests which should be skipped depending on -# the platform -SKIP_WINDOWS = doctest.register_optionflag("SKIP_WINDOWS") -SKIP_CYGWIN = doctest.register_optionflag("SKIP_CYGWIN") -SKIP_POSIX = doctest.register_optionflag("SKIP_POSIX") - -skipflags = set() - -if os.name == 'posix': - skipflags.add(SKIP_POSIX) -elif os.name == 'nt': - skipflags.add(SKIP_WINDOWS) -if sys.platform == 'cygwin': - skipflags.add(SKIP_CYGWIN) - - -class CysignalsDocTestParser(DocTestParser): - def parse(self, *args, **kwargs): - examples = DocTestParser.parse(self, *args, **kwargs) - for example in examples: - if not isinstance(example, Example): - continue - if any(flag in example.options for flag in skipflags): - example.options[SKIP] = True - - return examples - - -parser = CysignalsDocTestParser() - - -print(f"Doctesting {len(filenames)} files.") - - -if os.name != 'nt': - import resource - # Limit stack size to avoid errors in stack overflow doctest - stacksize = 1 << 20 - if sys.platform != 'darwin': - # Work around a very strange OS X. - # This was discovered at https://github.com/sagemath/cysignals/issues/71. - # The original solution did not last very long and the issue reappeared. - resource.setrlimit(resource.RLIMIT_STACK, (stacksize, stacksize)) - - # Disable core dumps - resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) - - -def testfile(file): - # Child process - try: - if sys.platform == 'darwin': - from cysignals.signals import _setup_alt_stack - _setup_alt_stack() - failures, _ = doctest.testfile(file, module_relative=False, - optionflags=flags, parser=parser) - if not failures: - os._exit(0) - except BaseException as E: - print(E) - finally: - os._exit(23) - - -if __name__ == "__main__": - success = True - for f in filenames: - print(f) - sys.stdout.flush() - - # Test every file in a separate process (like in SageMath) to avoid - # side effects from doctests. - p = Process(target=testfile, args=(f,)) - p.start() - p.join(timeout) - - status = p.exitcode - - if p.is_alive(): - p.terminate() - print(f"Doctest {f} terminated. Timeout limit exceeded " - f"(>{timeout}s)", file=sys.stderr) - success = False - elif status != 0: - success = False - if status < 0: - print(f"killed by signal: {abs(status)}", file=sys.stderr) - elif status != 23: - print(f"bad exit: {status}", file=sys.stderr) - - sys.exit(0 if success else 1) diff --git a/src/conftest.py b/src/conftest.py new file mode 100755 index 00000000..693071c3 --- /dev/null +++ b/src/conftest.py @@ -0,0 +1,41 @@ +import pathlib + +from _pytest.nodes import Collector +from _pytest.doctest import DoctestModule + + +def pytest_collect_file( + file_path: pathlib.Path, + parent: Collector, +): + """Collect doctests in cython files and run them as test modules.""" + config = parent.config + if file_path.suffix == ".pyx": + if config.option.doctestmodules: + return DoctestModule.from_parent(parent, path=file_path) + return None + + +# Need to import cysignals to initialize it +import cysignals # noqa: E402 + +try: + import cysignals.alarm +except ImportError: + pass +try: + import cysignals.signals +except ImportError: + pass +try: + import cysignals.pselect +except ImportError: + pass +try: + import cysignals.pysignals +except ImportError: + pass +try: + import cysignals.tests # noqa: F401 +except ImportError: + pass diff --git a/src/cysignals/pselect.pyx b/src/cysignals/pselect.pyx index 04fcf31d..07d6b14a 100644 --- a/src/cysignals/pselect.pyx +++ b/src/cysignals/pselect.pyx @@ -88,13 +88,14 @@ def interruptible_sleep(double seconds): >>> import signal, time >>> def alarm_handler(sig, frame): ... pass - >>> _ = signal.signal(signal.SIGALRM, alarm_handler) + >>> old_handler = signal.signal(signal.SIGALRM, alarm_handler) >>> t0 = time.time() >>> _ = signal.alarm(1) >>> interruptible_sleep(2) >>> t = time.time() - t0 >>> (0.9 <= t <= 1.9) or t True + >>> _ = signal.signal(signal.SIGALRM, old_handler) TESTS:: @@ -346,6 +347,7 @@ cdef class PSelecter: The file ``/dev/null`` should always be available for reading and writing:: + >>> import os >>> from cysignals.pselect import PSelecter >>> f = open(os.devnull, "r+") >>> sel = PSelecter() @@ -389,6 +391,7 @@ cdef class PSelecter: Open a file and close it, but save the (invalid) file descriptor:: + >>> import os >>> f = open(os.devnull, "r") >>> n = f.fileno() >>> f.close() diff --git a/src/cysignals/pysignals.pyx b/src/cysignals/pysignals.pyx index b0c3f4fa..4a273264 100644 --- a/src/cysignals/pysignals.pyx +++ b/src/cysignals/pysignals.pyx @@ -168,7 +168,7 @@ def getossignal(int sig): >>> getossignal(signal.SIGUSR1) >>> def handler(*args): pass - >>> _ = signal.signal(signal.SIGUSR1, handler) + >>> old_handler = signal.signal(signal.SIGUSR1, handler) >>> getossignal(signal.SIGUSR1) @@ -182,6 +182,7 @@ def getossignal(int sig): False >>> getossignal(signal.SIGABRT) == python_os_handler False + >>> _ = signal.signal(signal.SIGUSR1, old_handler) TESTS:: @@ -274,11 +275,13 @@ def setsignal(int sig, action, osaction=None): got signal >>> setsignal(signal.SIGILL, signal.SIG_DFL) - >>> _ = setsignal(signal.SIGALRM, signal.SIG_DFL, signal.SIG_IGN) + >>> old_handler = getossignal(signal.SIGALRM) + >>> old_py_handler = setsignal(signal.SIGALRM, signal.SIG_DFL, signal.SIG_IGN) >>> os.kill(os.getpid(), signal.SIGALRM) >>> _ = setsignal(signal.SIGALRM, handler, getossignal(signal.SIGSEGV)) >>> os.kill(os.getpid(), signal.SIGALRM) got signal + >>> _ = setsignal(signal.SIGALRM, old_py_handler, old_handler) TESTS:: diff --git a/src/cysignals/signals.pyx b/src/cysignals/signals.pyx index 5b841364..a2a25c31 100644 --- a/src/cysignals/signals.pyx +++ b/src/cysignals/signals.pyx @@ -105,10 +105,10 @@ class AlarmInterrupt(KeyboardInterrupt): >>> from cysignals import AlarmInterrupt >>> from signal import alarm + >>> from time import sleep >>> try: ... _ = alarm(1) - ... while True: - ... pass + ... sleep(2) ... except AlarmInterrupt: ... print("alarm!") alarm! diff --git a/src/cysignals/tests.pyx b/src/cysignals/tests.pyx index 34629152..e11b3c77 100644 --- a/src/cysignals/tests.pyx +++ b/src/cysignals/tests.pyx @@ -8,15 +8,6 @@ We disable crash debugging for this test run:: >>> import os >>> os.environ["CYSIGNALS_CRASH_NDEBUG"] = "" - -Verify that the doctester was set up correctly:: - - >>> import os - >>> os.name == "posix" # doctest: +SKIP_POSIX - False - >>> os.name == "nt" # doctest: +SKIP_WINDOWS - False - """ #***************************************************************************** @@ -787,8 +778,11 @@ def test_interrupt_bomb(long n=100, long p=10): TESTS:: + >>> import sys, pytest + >>> if sys.platform == 'cygwin': + ... pytest.skip('this doctest does not work on Windows') >>> from cysignals.tests import * - >>> test_interrupt_bomb() # doctest: +SKIP_CYGWIN + >>> test_interrupt_bomb() Received ... interrupts """ diff --git a/src/scripts/conftest.py b/src/scripts/conftest.py new file mode 100755 index 00000000..dc724f44 --- /dev/null +++ b/src/scripts/conftest.py @@ -0,0 +1 @@ +collect_ignore = ["cysignals-CSI-helper.py"]