Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Address logging test failures #173

Closed
khaeru opened this issue Apr 17, 2024 · 16 comments · Fixed by #179
Closed

Address logging test failures #173

khaeru opened this issue Apr 17, 2024 · 16 comments · Fixed by #179
Assignees

Comments

@khaeru
Copy link
Member

khaeru commented Apr 17, 2024

@glatterf42 noticed test failures such as these occurring on scheduled runs, starting today.

Symptoms:

  • In general, the Pytest caplog fixture fails to capture certain messages, so the test assertions that check for those messages end up failing.
  • Failures include:
    • Sometimes only util/test_cache.py::test_cached
    • Sometimes a longer list of 13 tests. When this occurs, they seem to all occur on the same pytest-xdist worker such as "gw1".
  • All -latest-py3.12-upstream- jobs, both main and 3.8.0.

Actions to take:

  • @khaeru to open a debugging PR and investigate if recent changes (esp Miscellaneous improvements for 2024-W10 #156) interfere destructively with pytest and/or recent pyam/ixmp4 releases.
  • If necessary, we open a mitigation PR to skip or xfail some or all of these tests so that scheduled and PR CI can pass.
@khaeru khaeru self-assigned this Apr 17, 2024
@glatterf42
Copy link
Member

glatterf42 commented Apr 17, 2024

I was able to reproduce the case with just one test failure locally. However, when I tried running this test again with debug output and alone (via pytest --lf), it passed.

Edit 1: running all util/ tests also passes.

Edit 2: I now started checking when I would see a test failure as above. Running util/test_cache with test_cli and test_import before works just fine, but when I run pytest message_ix_models/tests/test_report.py message_ix_models/tests/util/test_cache.py, I receive two errors as above. And again, if I repeat this command with --lf, these errors disappear. This makes me think that something required for only some tests in test_report changes the logging behaviour.

Edit 3: In line 51 of util/test_cache, I added

    loggers = [logging.getLogger(name) for name in logging.root.manager.loggerDict]
    print(loggers)

and compared the output of the failed test and the successful one (for which I had to include an assert False in the end).
It seems to me that Loggers are added to the loggerDict when they are created, which let the comparison to the following: There are a few loggers for which the level changes.

  • Failing: [<Logger parso.python.diff (WARNING)>, <Logger parso.python (WARNING)>, <Logger parso (WARNING)>, <Logger parso.cache (WARNING)>, <Logger requests (WARNING)>]
  • Successful: [ <Logger parso.python.diff (DEBUG)>, <Logger parso.python (DEBUG)>, <Logger parso (DEBUG)>, <Logger parso.cache (DEBUG)>, <Logger requests (DEBUG)>]

Then, everything stays the same until <Logger message_ix_models.tests.util (DEBUG)>, <Logger message_ix_models.tests (DEBUG)>, <Logger message_ix_models.model.data (DEBUG)>, <Logger message_ix_models.model.bare (DEBUG)>, at which point the successful tests skip to <Logger message_ix_models.report.legacy.iamc_report_hackathon (DEBUG)>, while the failing tests go off on a tangent including ixmp4 and pyam (see below). Once they reach <Logger message_ix_models.report.legacy.iamc_report_hackathon (DEBUG)>, the successful tests end with <Logger message_ix_models.report.legacy (DEBUG)>], whereas the failing tests also include

<Logger message_ix_models.util.click (DEBUG)>, 
<Logger message_ix_models.cli (DEBUG)>, 
<Logger message_ix_models.model.water.utils (DEBUG)>, 
<Logger message_ix_models.model.water (DEBUG)>, 
<Logger message_ix_models.model.water.cli (DEBUG)>, 
<Logger message_ix_models.project.ssp.structure (DEBUG)>, 
<Logger message_ix_models.project.ssp (DEBUG)>, 
<Logger message_ix_models.project (DEBUG)>, 
<Logger message_ix_models.report.cli (DEBUG)>]
The tangent
<Logger fsspec (DEBUG)>, 
<Logger fsspec.mapping (DEBUG)>, 
<Logger fsspec.local (DEBUG)>, 
<Logger dask.dataframe.shuffle (DEBUG)>, 
<Logger dask.dataframe (DEBUG)>, 
<Logger fastapi (DEBUG)>, 
<Logger sqlalchemy (WARNING)>, 
<Logger sqlalchemy.orm.path_registry (WARNING)>, 
<Logger sqlalchemy.orm (WARNING)>, 
<Logger sqlalchemy.orm.relationships.RelationshipProperty (WARNING)>, 
<Logger sqlalchemy.orm.relationships (WARNING)>, 
<Logger sqlalchemy.orm.properties.ColumnProperty (WARNING)>, 
<Logger sqlalchemy.orm.properties (WARNING)>, 
<Logger sqlalchemy.orm.mapper.Mapper (WARNING)>, 
<Logger sqlalchemy.orm.mapper (WARNING)>, 
<Logger sqlalchemy.orm.query.Query (WARNING)>, 
<Logger sqlalchemy.orm.query (WARNING)>, 
<Logger sqlalchemy.orm.strategies.ColumnLoader (WARNING)>, 
<Logger sqlalchemy.orm.strategies (WARNING)>, 
<Logger sqlalchemy.orm.strategies.ExpressionColumnLoader (WARNING)>, 
<Logger sqlalchemy.orm.strategies.DeferredColumnLoader (WARNING)>, 
<Logger sqlalchemy.orm.strategies.DoNothingLoader (WARNING)>, 
<Logger sqlalchemy.orm.strategies.NoLoader (WARNING)>, 
<Logger sqlalchemy.orm.strategies.LazyLoader (WARNING)>, 
<Logger sqlalchemy.orm.strategies.SubqueryLoader (WARNING)>, 
<Logger sqlalchemy.orm.strategies.JoinedLoader (WARNING)>, 
<Logger sqlalchemy.orm.strategies.SelectInLoader (WARNING)>, 
<Logger sqlalchemy.orm.writeonly.WriteOnlyLoader (WARNING)>, 
<Logger sqlalchemy.orm.writeonly (WARNING)>, 
<Logger httpx (DEBUG)>, 
<Logger httpcore.http11 (DEBUG)>, 
<Logger httpcore (DEBUG)>, 
<Logger httpcore.connection (DEBUG)>, 
<Logger httpcore.proxy (DEBUG)>, 
<Logger hpack.table (DEBUG)>, 
<Logger hpack (DEBUG)>, 
<Logger hpack.hpack (DEBUG)>, 
<Logger httpcore.http2 (DEBUG)>, 
<Logger rich (DEBUG)>, 
<Logger dotenv.main (DEBUG)>, 
<Logger dotenv (DEBUG)>, 
<Logger ixmp4.conf.auth (INFO)>,
<Logger ixmp4.conf (INFO)>, 
<Logger ixmp4 (INFO)>, 
<Logger ixmp4.conf.manager (INFO)>, 
<Logger ixmp4.conf.settings (INFO)>, 
<Logger ixmp4.data.api.base (INFO)>, 
<Logger ixmp4.data.api (INFO)>, 
<Logger ixmp4.data (INFO)>, 
<Logger sqlalchemy.dialects.postgresql (WARNING)>, 
<Logger sqlalchemy.dialects (WARNING)>, 
<Logger ixmp4.data.db.base (INFO)>, 
<Logger ixmp4.data.db (INFO)>, 
<Logger ixmp4.data.backend.db (INFO)>, 
<Logger ixmp4.data.backend (INFO)>, 
<Logger ixmp4.server.rest.deps (INFO)>, 
<Logger ixmp4.server.rest (INFO)>, 
<Logger ixmp4.server (INFO)>, 
<Logger ixmp4.server.rest.middleware (INFO)>, 
<Logger ixmp4.data.backend.api (INFO)>, 
<Logger pyam.logging (INFO)>, 
<Logger pyam (INFO)>, 
<Logger pyam.utils (INFO)>, 
<Logger pyam.aggregation (INFO)>, 
<Logger pyam.timeseries (INFO)>, 
<Logger pyam.ixmp4 (INFO)>, 
<Logger PIL.Image (INFO)>, 
<Logger PIL.PngImagePlugin (INFO)>, 
<Logger matplotlib.ticker (WARNING)>, 
<Logger matplotlib.artist (WARNING)>, 
<Logger matplotlib.lines (WARNING)>, 
<Logger matplotlib._afm (WARNING)>, 
<Logger matplotlib.font_manager (WARNING)>, 
<Logger matplotlib.dviread (WARNING)>, 
<Logger matplotlib.mathtext (WARNING)>, 
<Logger matplotlib.texmanager (WARNING)>, 
<Logger matplotlib.textpath (WARNING)>, 
<Logger matplotlib.text (WARNING)>, 
<Logger matplotlib._layoutgrid (WARNING)>, 
<Logger matplotlib._constrained_layout (WARNING)>, 
<Logger matplotlib.backend_bases (WARNING)>, 
<Logger matplotlib.colorbar (WARNING)>, 
<Logger matplotlib.image (WARNING)>, 
<Logger matplotlib.style.core (WARNING)>, 
<Logger matplotlib.style (WARNING)>, 
<Logger matplotlib.axis (WARNING)>, 
<Logger matplotlib.gridspec (WARNING)>, 
<Logger matplotlib.axes._base (WARNING)>, 
<Logger matplotlib.axes (WARNING)>, 
<Logger matplotlib.category (WARNING)>, 
<Logger matplotlib.dates (WARNING)>, 
<Logger matplotlib.axes._axes (WARNING)>, 
<Logger matplotlib.figure (WARNING)>, 
<Logger matplotlib.pyplot (WARNING)>, 
<Logger stack_data.serializing (DEBUG)>, 
<Logger stack_data (DEBUG)>, 
<Logger prompt_toolkit.buffer (DEBUG)>, 
<Logger prompt_toolkit (DEBUG)>, 
<Logger Comm (DEBUG)>, 
<Logger pyam.figures (INFO)>, 
<Logger pyam.plotting (INFO)>, 
<Logger pyam.units (INFO)>, 
<Logger pyam.validation (INFO)>, 
<Logger pyam.core (INFO)>, 
<Logger markdown_it.rules_block.blockquote (DEBUG)>, 
<Logger markdown_it.rules_block (DEBUG)>, 
<Logger markdown_it (DEBUG)>, 
<Logger markdown_it.rules_block.code (DEBUG)>, 
<Logger markdown_it.rules_block.fence (DEBUG)>, 
<Logger markdown_it.rules_block.heading (DEBUG)>, 
<Logger markdown_it.rules_block.hr (DEBUG)>, 
<Logger markdown_it.rules_block.html_block (DEBUG)>, 
<Logger markdown_it.rules_block.lheading (DEBUG)>, 
<Logger markdown_it.rules_block.list (DEBUG)>, 
<Logger markdown_it.rules_block.paragraph (DEBUG)>, 
<Logger markdown_it.rules_block.reference (DEBUG)>, 
<Logger markdown_it.parser_block (DEBUG)>, 
<Logger markdown_it.rules_core.replacements (DEBUG)>, 
<Logger markdown_it.rules_core (DEBUG)>, 
<Logger alembic.util.messaging (DEBUG)>, 
<Logger alembic.util (DEBUG)>, 
<Logger alembic (DEBUG)>, 
<Logger alembic.ddl.impl (DEBUG)>, 
<Logger alembic.ddl (DEBUG)>,
<Logger alembic.autogenerate.compare (DEBUG)>, 
<Logger alembic.autogenerate (DEBUG)>, 
<Logger alembic.ddl.postgresql (DEBUG)>, 
<Logger alembic.runtime.migration (DEBUG)>, 
<Logger alembic.runtime (DEBUG)>, 
<Logger pyam.iiasa (INFO)>, 
<Logger genno.compat.pyam.operator (DEBUG)>, 
<Logger message_ix_models.report.operator (DEBUG)>,

@khaeru
Copy link
Member Author

khaeru commented Apr 18, 2024

Okay, that's great detective work to narrow it down.

To be clear, every Logger object can have 0 or more Handlers attached. Messages from, say, message_ix_models.util.foo are offered first to the Logger of that name, then if it does not choose to handle them they are offered to message_ix_models.util, message_ix_models, and finally the root Logger.

Each logger, if it chooses to handle the message, uses its own respective Handlers.

In each case, if the level of the message (eg DEBUG) is lower than the level set on that Logger (eg WARNING), then handling stops.

The root Logger is where message-ix-models, as a user-facing application, attaches its handlers, filters, etc. So we can next look to see whether these are disturbed in the failing case; this will probably be a side effect of one of the packages listed in the 'tangent'.

@khaeru
Copy link
Member Author

khaeru commented Apr 18, 2024

I've tried the following:

(1) Run watchexec -e py -- "pytest -k 'test_report or test_cache' -xv -rA". This runs 4 tests:

  • PASSED test_report.py::test_register
  • PASSED test_report.py::test_report_bare_res
  • PASSED test_report.py::test_report_deprecated
  • FAILED test_report.py::test_report_legacy

(2) Add a debugging function in test_report.py, roughly like this:

def show_log_handlers(id: int):
    import logging

    from icecream import ic

    root_logger = logging.getLogger("root")

    ic(id, root_logger.level, root_logger.handlers)

(3) Added calls to this function in each of the 4 tests: at least at the top and bottom, or maybe in more places.

This gives the following results (abbreviated):

_____________________________________________ test_register ______________________________________________
------------------------------------------ Captured stderr call ------------------------------------------
ic| id: 'test_register 0'                                                                                 
    root_logger.level: 10                                                                                 
    root_logger.handlers: [<QueueHandler (NOTSET)>,                                                       
                           <_LiveLoggingNullHandler (NOTSET)>,                                            
                           <_FileHandler /dev/null (NOTSET)>,                                             
                           <LogCaptureHandler (NOTSET)>,     
                           <LogCaptureHandler (NOTSET)>]                                                  
ic| id: 'test_register 1'                                                                                 
    root_logger.level: 10                                                                                 
    root_logger.handlers: [<QueueHandler (NOTSET)>,                                                                                  <_LiveLoggingNullHandler (NOTSET)>,                                            
                           <_FileHandler /dev/null (NOTSET)>,                                             
                           <LogCaptureHandler (NOTSET)>,                                                  
                           <LogCaptureHandler (NOTSET)>]                                                  
__________________________________________ test_report_bare_res __________________________________________
------------------------------------------ Captured stderr call ------------------------------------------
ic| id: 'test_report_bare_res 0'
    root_logger.level: 10
    root_logger.handlers: [<QueueHandler (NOTSET)>,
                           <_LiveLoggingNullHandler (NOTSET)>,
                           <_FileHandler /dev/null (NOTSET)>,
                           <LogCaptureHandler (NOTSET)>,
                           <LogCaptureHandler (NOTSET)>]
ic| id: 'test_report_bare_res 1'
    root_logger.level: 0
    root_logger.handlers: []
_________________________________________ test_report_deprecated _________________________________________
------------------------------------------ Captured stderr call ------------------------------------------
ic| id: 'test_report_deprecated 0'
    root_logger.level: 0
    root_logger.handlers: [<LogCaptureHandler (NOTSET)>, <LogCaptureHandler (NOTSET)>]
ic| id: 'test_report_deprecated 1'
    root_logger.level: 0
    root_logger.handlers: [<LogCaptureHandler (NOTSET)>, <LogCaptureHandler (NOTSET)>]
___________________________________________ test_report_legacy ___________________________________________
------------------------------------------ Captured stderr call --------------------------------
ic| id: 0
    root_logger.level: 0
    root_logger.handlers: [<LogCaptureHandler (NOTSET)>, <LogCaptureHandler (NOTSET)>]
ic| id: 1
    root_logger.level: 0
    root_logger.handlers: [<LogCaptureHandler (NOTSET)>, <LogCaptureHandler (NOTSET)>]
ic| id: 2
    root_logger.level: 0
    root_logger.handlers: [<LogCaptureHandler (NOTSET)>, <LogCaptureHandler (NOTSET)>]

So this tells me it is somewhere between the points "test_report_bare_res 0" and "test_report_bare_res 1" where the message-ix-models handlers get wiped out.

I'll continue to debug along these lines.

@khaeru
Copy link
Member Author

khaeru commented Apr 18, 2024

I expand the debugging function:

def show_log_handlers(id: str, expected_n_handlers: int):
    import logging

    from icecream import ic

    root_logger = logging.getLogger("root")

    ic(id)

    if expected_n_handlers != len(root_logger.handlers):
        ic(
            root_logger.level,
            root_logger.handlers,
            root_logger.manager.loggerDict,
        )
        assert False

And put the following at the top of the one test:

def test_report_bare_res(request, tmp_path, test_context):
    ...
    show_log_handlers("test_report_bare_res 0", 5)

    from importlib import import_module

    for name in (
        "dotenv",
        "fastapi",
        "fsspec",
        "hpack",
        "httpcore",
        "httpx",
        "matplotlib",
        "numba",
        "pint",
        "pycountry",
        "requests",
        "rich",
        "sdmx",
        "sqlalchemy",
        "tqdm",
        "urllib3",
        "xarray",
        "ixmp4",
        "pyam",
    ):
        import_module(name)
        show_log_handlers(f"test_report_bare_res after `import {name}`", 5)

Now I run with watchexec -e py -- "pytest -k 'test_report_bare_res' -xv -rA" and see output like:

ic| id: 'test_report_bare_res 0'                                                                                                                                                      
ic| id: 'test_report_bare_res after `import dotenv`'                                                                                                                                  
ic| id: 'test_report_bare_res after `import fastapi`'                                                                                                                                 
ic| id: 'test_report_bare_res after `import fsspec`'                                                                                                                                  
ic| id: 'test_report_bare_res after `import hpack`'                                                                                                                                   
ic| id: 'test_report_bare_res after `import httpcore`'                                                                                                                                
ic| id: 'test_report_bare_res after `import httpx`'                                                                                                                                   
ic| id: 'test_report_bare_res after `import matplotlib`'                                                                                                                              
ic| id: 'test_report_bare_res after `import numba`'                                                                                                                                   
ic| id: 'test_report_bare_res after `import pint`'                                                                                                                                    
ic| id: 'test_report_bare_res after `import pycountry`'                                                                                                                               
ic| id: 'test_report_bare_res after `import requests`'                                                                                                                                
ic| id: 'test_report_bare_res after `import rich`'                                                                                                                                    
ic| id: 'test_report_bare_res after `import sdmx`'                                                                                                                                    
ic| id: 'test_report_bare_res after `import sqlalchemy`'                                                                                                                              
ic| id: 'test_report_bare_res after `import tqdm`'                                                                                                                                    
ic| id: 'test_report_bare_res after `import urllib3`'                                                                                                                                 
ic| id: 'test_report_bare_res after `import xarray`'                                                                                                                                  
ic| id: 'test_report_bare_res after `import ixmp4`'                                                                                                                                   
ic| root_logger.level: 0                                                                                                                                                              
    root_logger.handlers: []                                                                                                                                                          
    root_logger.manager.loggerDict: {'PIL': <Logger PIL (INFO)>,
    ...

This indicates that the root logger's handlers are only wiped out by/as of the import of ixmp4. The prior import of all the other packages appearing in the list of loggers (shared above) does not to seem to trigger the same change.

khaeru added a commit that referenced this issue Apr 18, 2024
- Reduce number of jobs.
- Reduce the set of tests run.
khaeru added a commit that referenced this issue Apr 18, 2024
@glatterf42
Copy link
Member

I don't know if I can find a solution for this today, but I just tried importing pyam before ixmp4, and it fails, too. So whatever it is that ixmp4 is doing, pyam seems to be guilty of the same.

@glatterf42
Copy link
Member

Both of them are configuring their logging via logging.config.FileConfig, which has a Warning at the bottom of this section: if one doesn't set disable_existing_loggers to False, potentially all non-root loggers will be disabled. Oddly enough, both pyam and ixmp4 set this to False, though.

@glatterf42
Copy link
Member

There's also a warning at the bottom of this section which might be relevant: if the root logger is modified during a test, the pytest.caplog handler may be removed, causing no logs to be captured.

@glatterf42
Copy link
Member

Both pyam and ixmp4 do something to the root logger which almost looks like clearing it: https://github.com/IAMconsortium/pyam/blob/main/pyam/logging.conf
I don't know if they actually need to do that.

@khaeru
Copy link
Member Author

khaeru commented Apr 18, 2024

if one doesn't set disable_existing_loggers to False, potentially all non-root loggers will be disabled. Oddly enough, both pyam and ixmp4 set this to False, though.

Keep in mind the issue we are seeing is not with Logger instances, but with Handler instances. The logging docs are, unfortunately, both verbose and also silent on a lot of points, including I think what happens to handlers.

if the root logger is modified during a test, the pytest.caplog handler may be removed, causing no logs to be captured.

This is the reason I wound up with code like this in message-ix-models:

if not any(isinstance(h, logging.handlers.QueueHandler) for h in root.handlers):
# Not yet configured
configure()

and
logging.root.addHandler(h_queue)

To wit: message-ix-models looks for an instance of its own QueueHandler class, and if it is not there, adds it. It ignores other handlers (e.g. the Pytest ones), and does not remove or replace them. I tried but could not get this to work with logging.config.dictConfig.

I suspect by placing something like show_log_handlers() above and below the ixmp4 and/or pyam calls to logging.config.fileConfig(), we could confirm that it is this built-in function, used in this way, that wipes out the existing handlers.

@glatterf42
Copy link
Member

I've added the first version of show_log_handlers() immediately before and after the FileConfig call in ixmp4:

        show_log_handlers(0)
        logging.config.fileConfig(logging_config, disable_existing_loggers=False)
        show_log_handlers(1)

And with that, I see:

ic| id: 'test_report_bare_res after `import xarray`'
ic| id: 0
    root_logger.level: 10
    root_logger.handlers: [<QueueHandler (NOTSET)>,
                           <_LiveLoggingNullHandler (NOTSET)>,
                           <_FileHandler /dev/null (NOTSET)>,
                           <LogCaptureHandler (NOTSET)>,
                           <LogCaptureHandler (NOTSET)>]
ic| id: 1, root_logger.level: 0, root_logger.handlers: []
ic| id: 'test_report_bare_res after `import ixmp4`'
ic| root_logger.level: 0
    root_logger.handlers: []
    root_logger.manager.loggerDict: {'PIL': <Logger PIL (INFO)>,

So it seems this function is indeed responsible for wiping out the handlers.

@glatterf42
Copy link
Member

According to their docs using FileConfig is older than DictConfig and one should prefer the latter. Also according to their docs, the config file must set level and handlers for root_logger, but the DictConfig only requires a version key. So maybe by migrating to DictConfig, we can avoid overwriting the root logger('s handlers).

@glatterf42
Copy link
Member

I found a solution, the test is working fine again locally. We won't need to change anything if we can get the PRs I'm going to push to ixmp4 and pyam merged reasonably fast. Otherwise, we could use their branches. I'll push these tomorrow.

@khaeru khaeru mentioned this issue Apr 19, 2024
4 tasks
khaeru added a commit that referenced this issue Apr 19, 2024
- Reduce number of jobs.
- Reduce the set of tests run.
khaeru added a commit that referenced this issue Apr 19, 2024
@khaeru khaeru mentioned this issue Apr 19, 2024
4 tasks
@glatterf42
Copy link
Member

I released a new version of ixmp4 that should include a fix, I don't yet know when pyam can follow suit. If we use their main-branch version, we should already be able to get a fix from them as well.

@khaeru
Copy link
Member Author

khaeru commented Apr 19, 2024

Great! Could you please trial those adjustments on the #175 branch, when you have a chance?

@glatterf42
Copy link
Member

Not sure what's up with the coverage report, but the fixes work :)

glatterf42 added a commit that referenced this issue Apr 22, 2024
glatterf42 added a commit that referenced this issue Apr 22, 2024
@khaeru
Copy link
Member Author

khaeru commented Apr 22, 2024

Glad to see that new pyam and ixmp4 releases have addressed this!

In the future, on the off chance that message-ix-models users encounter issues with logging behaviour, we should keep in mind to check whether they are using the affected versions and remind them to upgrade.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants