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

Replace nose with pytest #34778

Merged
merged 38 commits into from
Nov 11, 2024
Merged

Replace nose with pytest #34778

merged 38 commits into from
Nov 11, 2024

Conversation

millerdev
Copy link
Contributor

@millerdev millerdev commented Jun 14, 2024

https://dimagi.atlassian.net/browse/SAAS-14431

🐡 Review by commit.

Safety Assurance

Safety story

Changes tests and documentation with a couple minor exceptions:

  • corehq/util/decorators.py - fix Django settings access on import
  • corehq/util/metrics/const.py - related to previous

Automated test coverage

Absolutely.

Rollback instructions

  • This PR can be reverted after deploy with no further considerations

@millerdev millerdev added the product/invisible Change has no end-user visible impact label Jun 14, 2024
@dimagimon dimagimon added dependencies Pull requests that update a dependency file Risk: High Change affects files that have been flagged as high risk. Risk: Medium Change affects files that have been flagged as medium risk. labels Jun 14, 2024
@millerdev millerdev force-pushed the dm/pytest branch 6 times, most recently from cf75dbf to e474bf5 Compare June 19, 2024 18:11
@millerdev millerdev force-pushed the dm/pytest branch 3 times, most recently from a0fb589 to ed40b81 Compare July 26, 2024 15:09
@millerdev millerdev force-pushed the dm/pytest branch 6 times, most recently from 13ba100 to 73c0be9 Compare September 4, 2024 16:57
@dimagimon dimagimon removed Risk: High Change affects files that have been flagged as high risk. Risk: Medium Change affects files that have been flagged as medium risk. labels Sep 10, 2024
@millerdev millerdev force-pushed the dm/pytest branch 6 times, most recently from 9bc121b to b71a55c Compare September 18, 2024 10:54
Required by --strict-markers
test_user_types tests were not being run because the "yield" tests were masked by @patch_user_data_db_layer, which converted the generator to a normal function, which was then "run" as a test but didn't actually test anything.
A few tests are also now found that nose was missing.
- Disable cacheprovider plugin since it is not possible to re-run failed tests in that context.
- Add option to show local variables in test failure tracebacks.
Tried using capsys, but couldn't make it work.
Also a few other non-test files.
@millerdev millerdev marked this pull request as ready for review October 23, 2024 20:49
@millerdev millerdev requested review from snopoke and gherceg October 24, 2024 10:37
Copy link
Contributor

@kaapstorm kaapstorm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Partial review with Matt. To be continued.)

Comment on lines +12 to +14
# pytest is pinned to a git commit until 8.4 is released.
# `minversion = 8.4` should also be set in .pytest.ini at that time.
pytest @ git+https://github.com/pytest-dev/pytest.git@85760bff2776989b365167c7aeb35c86308ab76b
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What feature are we using that will be in 8.4?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -40,10 +43,9 @@ def main():
run_patches()

from corehq.warnings import configure_warnings
configure_warnings(is_testing(sys.argv))
configure_warnings()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should configure_warnings() take the CCHQ_TESTING env var into account?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair question. Should a management command (that is not running tests) fail because of warnings if CCHQ_TESTING is set? For example, if you are running CCHQ_TESTING=1 ./manage.py migrate to migrate your test database, should it crash if warnings are emitted? I think that might be more annoying than helpful, but I'm happy to hear arguments in favor.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We reckoned that CCHQ_TESTING=1 ./manage.py dbshell would useful for inspecting the test database.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you saying you agree that that command should not crash on a warning? That's my position.


def patch_assertItemsEqual():
import unittest
unittest.TestCase.assertItemsEqual = unittest.TestCase.assertCountEqual
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Why do we currently do this? Do we not test the values of the items?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assertItemsEqual was removed, possibly as long ago as when we upgraded to Python 3, but we still have code that uses it. This was easier than changing all the places that use it. It's not new with this PR, it was moved from here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. Yeah, it looks like Python renamed assertItemsEqual to assertCountEqual in Python 3.2.

@@ -51,7 +52,7 @@ def simulate_reentrant_lock():
@timelimit(0.1)
def test_unreleased_lock():
msg = "unreleased dict_values([ReentrantTestLock(name='unreleased', level=1)])"
with assert_raises(AssertionError, msg=msg):
with assert_raises(AssertionError, msg=re.compile("^" + re.escape(msg))):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why msg can now be longer, or does this fix something that is currently wrong?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This became necessary because pytest changes the assertion error message, I think as part of its assertion introspection logic:

>       assert not locks, f"unreleased {locks.values()}"
E       AssertionError: unreleased dict_values([ReentrantTestLock(name='unreleased', level=1)])
E       assert not {'unreleased': ReentrantTestLock(name='unreleased', level=1)}

corehq/tests/locks.py:58: AssertionError
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
... snip

>               self.gen.throw(typ, value, traceback)
E               AssertionError: not equal
E               "unreleased dict_values([ReentrantTestLock(name='unreleased', level=1)])\nassert not {'unreleased': ReentrantTestLock(name='unreleased', level=1)}"
E               "unreleased dict_values([ReentrantTestLock(name='unreleased', level=1)])"

../../.pyenv/versions/3.9.17/lib/python3.9/contextlib.py:137: AssertionError

from corehq.apps.es.client import manager


class ElasticSnitchPlugin(Plugin):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@millerdev curious...but is something like this still useful/needed? Do we have a backlog to track re-implementing some of these plugins with pytest? I know the commit message says out of scope, but this one in particular seems like it was added fairly recently with the ES migration work (2023) by Joel.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thought was we can reimplement this if/when we need it. I doubt anyone has used it since Joel finished his ES work, but I'd be happy to port it to pytest if someone wants it. Do you use it?

Copy link
Contributor

@gherceg gherceg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Paired with Daniel to review this. Left a couple of non-blocking questions. Great job @millerdev 🎆

@@ -58,40 +58,8 @@
'testapps.test_pillowtop',
) + tuple(INSTALLED_APPS) # noqa: F405

TEST_RUNNER = 'django_nose.BasicNoseRunner'
NOSE_ARGS = [
#'--no-migrations' # trim ~120s from test run with db tests
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should look to see if test run time increases by ~120s after switching to pytest.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the answer is possibly "yes." The average build time does appear to have increased from 14 minutes to 16 minutes soon after Nov 11 when this PR was merged.

Comment on lines +16 to +17
with pytest.raises(AttributeError, match="Mock object has no attribute 'info'"):
CouchModel.get_db().info
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked closely, but I assume there is no way to get this to raise a RuntimeError similar to the other db access attempts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha in hindsight I think this was a silly comment for me to make. I was imagining a way to raise a RuntimeError in how CouchModel was invoked, not necessarily just having the mock translate an AttributeError into a RuntimeError for the sake of it. I'll leave on a comment on the PR as well.

config.reuse_db = reusedb = config.getoption("--reusedb")
config.skip_setup_for_reuse_db = reusedb and reusedb != "reset"
config.reuse_db = reusedb = config.getoption("--reusedb") or config.getvalue("reuse_db")
config.skip_setup_for_reuse_db = reusedb and (reusedb != "reset" or config.getvalue("create_db"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd expect that if create_db were true, we wouldn't skip setup?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs for --create-db say:

Re-create the database, even if it exists. This option can be used to override --reuse-db.

It will not skip setup if create_db is true unless reusedb is not false.

Another way to say that is create_db has no effect unless reusedb is true. Databases are always (re)created unless reusedb is true.

@millerdev millerdev merged commit 90bf033 into master Nov 11, 2024
13 checks passed
@millerdev millerdev deleted the dm/pytest branch November 11, 2024 12:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dependencies Pull requests that update a dependency file product/invisible Change has no end-user visible impact
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants