AstarVienna/ScopeSim

Testing logging seems broken in pytest 8

hugobuddel opened this issue · 2 comments

pytest 8 seems to break some of our tests, and perhaps our code as well. The order of the tests suddenly matter, and it is related to logging:

pip install pytest==8.2.1

This works fine:

python -m pytest -x --pdb \
    scopesim/tests/tests_effects/test_Quantization.py::TestApplyTo::test_logs_warning_for_float_dtype \
    scopesim/tests/test_logging.py::test_log_to_file 

This fails:

python -m pytest -x --pdb \
    scopesim/tests/test_logging.py::test_log_to_file \
    scopesim/tests/tests_effects/test_Quantization.py::TestApplyTo::test_logs_warning_for_float_dtype

with

==== test session starts =====
platform linux -- Python 3.11.5, pytest-8.2.1, pluggy-1.5.0
rootdir: .../ScopeSim
configfile: pyproject.toml
plugins: cov-5.0.0, anyio-4.3.0
collected 2 items                                                                                                                          

scopesim/tests/test_logging.py .                                                                                                     [ 50%]
scopesim/tests/tests_effects/test_Quantization.py F
>>>> captured stdout >>>>
astar.scopesim.effects.electronic - WARNING: Cannot access cmds for Quantization effect.
astar.scopesim.effects.electronic - WARNING: Setting quantized data to dtype <class 'float'>, which is not an integer subtype.
>>>> traceback >>>>

self = <scopesim.tests.tests_effects.test_Quantization.TestApplyTo object at 0x7f014d714a10>
det = <scopesim.detector.detector.Detector object at 0x7f014d70f2d0>, caplog = <_pytest.logging.LogCaptureFixture object at 0x7f014d70a5d0>

    def test_logs_warning_for_float_dtype(self, det, caplog):
        assert det.data.sum() > det.data.size * 1.2
        quant = Quantization(dtype=float)
        det = quant.apply_to(det)
        assert det.data.sum() == det.data.size
        # Test sub-dtype because exact dtype for "float" may depend on OS
        assert np.issubdtype(det.data.dtype, np.floating)
>       assert "Setting quantized data to dtype" in caplog.text
E       AssertionError: assert 'Setting quantized data to dtype' in ''
E        +  where '' = <_pytest.logging.LogCaptureFixture object at 0x7f014d70a5d0>.text

scopesim/tests/tests_effects/test_Quantization.py:56: AssertionError
>>>> entering PDB >>>>

>>>> PDB post_mortem (IO-capturing turned off) >>>
> .../ScopeSim/scopesim/tests/tests_effects/test_Quantization.py(56)test_logs_warning_for_float_dtype()
-> assert "Setting quantized data to dtype" in caplog.text

It is already broken in pytest 7 if both tests are in the same file. Kinda ~MVP:

import logging
import pytest
import numpy as np
import scopesim
from scopesim.detector import Detector
from scopesim.effects.electronic import Quantization
from scopesim.tests.mocks.py_objects.header_objects import _implane_header

base_logger = logging.getLogger("astar")
print("HB7", base_logger.handlers)
scopesim.log_to_file(enable=True)
print("HB8", base_logger.handlers)
scopesim.log_to_file(enable=False)
print("HB9", base_logger.handlers)


def test_log_to_file():
    base_logger = logging.getLogger("astar")
    print("HB1", base_logger.handlers)
    scopesim.log_to_file(enable=True)
    print("HB2", base_logger.handlers)
    assert any(handler.name == "file" for handler in base_logger.handlers)
    scopesim.log_to_file(enable=False)
    print("HB3", base_logger.handlers)
    assert not any(handler.name == "file" for handler in base_logger.handlers)


def test_logs_warning_for_float_dtype(caplog):
    base_logger = logging.getLogger("astar")
    print("HB6", base_logger.handlers)

    det = Detector(_implane_header())
    width = det._hdu.data.shape[1]
    det._hdu.data[:] = 1.2
    det._hdu.data[:, width // 2] = 1.99

    assert det.data.sum() > det.data.size * 1.2
    quant = Quantization(dtype=float)
    det = quant.apply_to(det)
    assert det.data.sum() == det.data.size
    # Test sub-dtype because exact dtype for "float" may depend on OS
    assert np.issubdtype(det.data.dtype, np.floating)
    assert "Setting quantized data to dtype" in caplog.text

This will print

HB7 [<StreamHandler <stdout> (INFO)>]
HB8 [<StreamHandler <stdout> (INFO)>, <RotatingFileHandler /mnt/bulk/hugo/repos/scopesim/ScopeSim/.scopesim.log (DEBUG)>]
HB9 [<StreamHandler <stdout> (INFO)>]

HB1 []
HB2 [<StreamHandler <stdout> (INFO)>, <RotatingFileHandler /mnt/bulk/hugo/repos/scopesim/ScopeSim/.scopesim.log (DEBUG)>]
HB3 [<StreamHandler <stdout> (INFO)>]              

HB6 [<StreamHandler <stdout> (INFO)>]

and then raises an AssertionError for the last line.

Commenting out test_log_to_file() prints:

HB7 [<StreamHandler <stdout> (INFO)>]
HB8 [<StreamHandler <stdout> (INFO)>, <RotatingFileHandler /mnt/bulk/hugo/repos/scopesim/ScopeSim/.scopesim.log (DEBUG)>]
HB9 [<StreamHandler <stdout> (INFO)>]

HB6 []

and then succeeds.

So for some reason, the tests are ran without any logging handlers set. And test_logs_warning_for_float_dtype actually requires that no logging handlers are set. However, test_log_to_file sets the handlers, so test_logs_warning_for_float_dtype fails.

The difference between pytest 7 and 8 is that the handlers get reset between files in pytest 7, but not in pytest 8.

I don't really have a proper mental model of what is happening. I would have expected that the tests would have the same logging setup as you'd get upon initialization, but this is apparently not the case.

Ah! configure_logging() in conftest.py is supposed to fix this problem:

@pytest.fixture(scope="package", autouse=True)
def configure_logging():
    base_logger = logging.getLogger("astar")
    handlers = base_logger.handlers
    # Disable handlers
    base_logger.handlers = []
    # Make sure logging can reach pytest's caplog
    base_logger.propagate = True
    yield
    # Restore
    base_logger.handlers = handlers
    base_logger.propagate = False

So configure_logging() disables the handlers, but it is only ran once since its scope is package. Then test_log_to_file() calls log_to_file(), which calls update_logging(), which resets the handles for any test afterwards. But in pytest 7, configure_logging() was ran for every file, or maybe for every sub-package.

Changing the scope of configure_logging() to function seems to fix the problem.