-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Don't add fixture finalizer if the value is cached #11833
Conversation
seems like the failing tests are unrelated to this PR, they're failing in several other PRs. |
Yes, it's unrelated. If you rebase it should be good. |
IIRC, this is meant to ensure that we teardown the current fixture before its upstream fixtures, because the upstream fixture will call its finalizers and then teardown itself. It is a trick because fixture teardown is not structured/graph based, at least that's how I recall. For the note I also find this extremely confusing, and we have discussed in the past to replace this implicit mechanism in favor of a proper dependency graph. |
Right. I thought that would be handled by the fact that they get torn down in reverse order of setup?
It is at least partly graph-based, see docstring of SetupState: Line 422 in eefc9d4
But yeah the teardowns within any of session/mod/item are just flat stacks. |
This is in part necessary to handle dynamic fixture request |
A big part of todays complexity is the funcarg mechanism which predates fixtures and scopes |
Bump :) |
@jakkdl I will try to review, but right now still mostly dealing with pytest 8 fallout. Fair warning though, making changes to core fixture code is usually very tricky. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The overall implementation looks like an improvement
I'm slightly worried as we currently express the logic in code instead of a data structure
However at first glance the better control looks reasonable
@@ -641,11 +642,8 @@ def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None: | |||
|
|||
# Check if a higher-level scoped fixture accesses a lower level one. | |||
subrequest._check_scope(argname, self._scope, scope) | |||
try: | |||
# Call the fixture function. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like it originally ensured teardown even when setup failed
I suspect that we are missing a edge case tests there
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the fixture need finalizing in case we never run ihook.pytest_fixture_setup
? I thought it was tightly coupled to the user-supplied code.
But I could change it to
try:
fixturedef.execute(...)
except:
self._schedule_finalizers(...)
raise
or specifically schedule a finalizer where I made a comment on line 1076
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The specific schedule is slightly better,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On further consideration, changing that seems... bad to me? It means we'll be running the finalizer multiple times for a fixture with a cached exception, even if its setup was only run once. That is how it worked in the past though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies, what I meant is that a single schedule is better than the current mechanism
Ideally this logic would move to a data structure instead of the code where it is
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right - no yeah I agree that the current implementation is quite messy and would benefit from an overhaul, but that seems out of scope for this PR.
I'm not sure I follow your original comment in that case, the try/except
that previously was on this line is now moved one step deeper into execute()
- and so there should be no change wrt to behaviour of scheduling finalizer if setup fails.
The only real change should ™️ be that the finalizer isn't scheduled if the value is cached (regardless of if it's an exception or not). If the setup code for the fixture fails, it's catched by the new try/finally
.
If some other unrelated code in execute
raises an exception the finalizer will not be scheduled, but in that case the setup has not run either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So is there a problem I should address, or was your comment just a musing on how this should be generally overhauled? Or do you consider that a requirement before modifying any logic?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we need to figure tests that include cached fixtures with failures
Added a test. If I were to add the change discussed, for scheduling teardown when setup failed, the code for the last function would be def test_crash_expected_setup_and_teardown() -> None:
assert executed_crash == ["fix_crash setup", "fix_crash teardown", "fix_crash_teardown"] I should also change these tests so they're robust to reordering before merging. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a real review yet, just some quick comments
…tests to a pytester test in testing/python/fixtures.py
…der to testing/python/fixtures.py
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for the PR @jakkdl, if we can get it merged it would be a very nice simplification.
So here is my understanding:
-
In issue fixture finalizer dependency incorrect when using autouse or getfuncargvalue #1489 we're seeing bad fixture teardown ordering.
-
You've analyzed the situation and concluded that we're adding finalizers too eagerly, particularly what's causing the issue is that we're adding a finalizer even when the fixturedef value is already cached.
-
Your PR makes it so that we don't add a finalizer when the fixture is cached.
-
In the process you've also removed code that was added as a fix for Incorrect finalize/cleanup order of fixtures when using request.getfixturevalue() #1895 (the
SubRequest._schedule_finalizers
code), because it is seemingly no longer needed (indeed, the regression testtest_getfixturevalue_teardown
passes).
So for me as a reviewer there are two questions:
- Is the addfinalizer in the cached case needed for correctness?
- Is the removal of the Incorrect finalize/cleanup order of fixtures when using request.getfixturevalue() #1895 code OK?
Is the removal of the #1895 code OK?
First I'm looking at the 2nd question since it's easier to examine. Here is the regression test for #1895:
import pytest
@pytest.fixture(scope='session')
def resource():
r = ['value']
yield r
r.pop()
@pytest.fixture(scope='session')
def inner(request):
resource = request.getfixturevalue('resource')
assert resource == ['value']
yield
assert resource == ['value']
def test_inner(inner):
pass
def test_func(resource):
pass
I added some prints in pytest to see what's going on. Here is the output before this PR:
x.py::test_inner PASSED
TEARDOWN <Function test_inner>
x.py::test_func PASSED
TEARDOWN <Function test_func>
TEARDOWN <Module x.py>
TEARDOWN <Dir pytest>
TEARDOWN <Session exitstatus=<ExitCode.OK: 0> testsfailed=0 testscollected=2>
FIXTURE FINISH <FixtureDef argname='resource' scope='session' baseid='x.py'> <SubRequest 'resource' for <Function test_func>>
FIXTURE FINISH <FixtureDef argname='inner' scope='session' baseid='x.py'> <SubRequest 'inner' for <Function test_inner>>
FIXTURE FINISH <FixtureDef argname='inner' scope='session' baseid='x.py'> <SubRequest 'inner' for <Function test_inner>>
FIXTURE FINISH <FixtureDef argname='resource' scope='session' baseid='x.py'> <SubRequest 'resource' for <Function test_inner>>
After the PR, the output is (diff):
@@ -19,10 +19,6 @@
TEARDOWN <Session exitstatus=<ExitCode.OK: 0> testsfailed=0 testscollected=2>
-FIXTURE FINISH <FixtureDef argname='resource' scope='session' baseid='x.py'> <SubRequest 'resource' for <Function test_func>>
-
-FIXTURE FINISH <FixtureDef argname='inner' scope='session' baseid='x.py'> <SubRequest 'inner' for <Function test_inner>>
-
FIXTURE FINISH <FixtureDef argname='inner' scope='session' baseid='x.py'> <SubRequest 'inner' for <Function test_inner>>
FIXTURE FINISH <FixtureDef argname='resource' scope='session' baseid='x.py'> <SubRequest 'resource' for <Function test_inner>>
The optimistic interpretation of this is that before #1895 fix, we've been adding a useless finalizer in the cached case, and the #1895 fix fixed this by adding another useless finalizer to beat the previous useless finalizer. And this PR fixes it by not adding the useless finalizer in the first place.
The less optimistic interpretation is that the "useless" finalizer is not useless and is needed for correctness in some odd case, and then this PR causes some regression.
So really the second question reduces down to the first question.
Is the addfinalizer in the cached case needed for correctness?
Unfortunately I'm out of time for today to investigate this question, so this ends with a cliffhanger...
testing/python/fixtures.py
Outdated
""" | ||
pytester.makepyfile( | ||
""" | ||
from typing import Generator |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When inside pytester, probably no need for type annotations, they're not enforced anyway..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Imagine if they were though 🤩
Idk, they could maybe help understanding the code, or if somebody copies the code out from the pytester-string to "real" code. I don't see much/any harm in leaving them anyhow
testing/python/fixtures.py
Outdated
assert result.ret == 0 | ||
|
||
|
||
def test_scope_fixture_caching_1(pytester: Pytester) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we can submit test_scope_fixture_caching_1 and test_scope_fixture_caching_2 in a separate PR, as they're nice to have anyway and will reduce the size of this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems reasonable, see #12121
This will force the FixtureDef object to throw away any previous | ||
results and compute a new fixture value, which will be stored into | ||
the FixtureDef object itself. | ||
If the FixtureDef has cached the result it will do nothing, otherwise it will |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous comment is wrong, however saying that if the FixtureDef has cached the result it does nothing is not right either. It registers finalizers, and recomputes if the cache key no longer matches,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, registering finalizers regardless of if the value is cached seems dumb... if it's cached then we've already registered a finalizer when we computed the value. And this can also cause bad teardown ordering:
import pytest
@pytest.fixture(scope="module")
def fixture_1(request):
...
@pytest.fixture(scope="module")
def fixture_2(fixture_1):
print("setup 2")
yield
print("teardown 2")
@pytest.fixture(scope="module")
def fixture_3(fixture_1):
print("setup 3")
yield
print("teardown 3")
def test_1(fixture_2):
...
def test_2(fixture_3):
...
# this will reschedule fixture_2's finalizer in the parent fixture, causing it to be
# torn down before fixture 3
def test_3(fixture_2):
...
# trigger finalization of fixture_1, otherwise the cleanup would sequence 3&2 before 1 as normal
@pytest.mark.parametrize("fixture_1", [None], indirect=["fixture_1"])
def test_4(fixture_1):
...
this prints
setup 2
setup 3
teardown 2
teardown 3
but if we remove test_3
we get 2-3-3-2.
But this is also a different issue+PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jakkdl would you mind opening a fresh issue for this case and the one you describe in #11833 (comment)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
testing/python/fixtures.py
Outdated
""" | ||
Make sure setup and finalization is only run once when using fixture | ||
multiple times. This might be a duplicate of another test.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
""" | |
Make sure setup and finalization is only run once when using fixture | |
multiple times. This might be a duplicate of another test.""" | |
"""Make sure setup and finalization is only run once when using a fixture | |
multiple times.""" |
If a fixture F1 depends on fixture F2, then F1 must be torn down before F2. F1 guarantees this by registering its own finish as a finalizer in all fixtures it depends on. So when F2.finish() runs, it runs F1.finish() first as a finalizer. That's a pretty brute force way to do it, but it is how it is... |
Oh wait, this can cause funky ordering as well: import pytest
@pytest.fixture(scope="module", params=["a", "b"])
def fixture_1(request):
print("setup 1 ", request.param)
yield
print("teardown 1", request.param)
@pytest.fixture(scope="module")
def fixture_2():
print("setup 2")
yield
print("teardown 2")
@pytest.fixture(scope="module")
def fixture_3(fixture_1):
print("setup 3")
yield
print("\nteardown 3")
def test_1(fixture_1, fixture_2, fixture_3): ...
That's definitely a different issue though, and not sure how to tackle that - if at all realistic. |
for more information, see https://pre-commit.ci
#12393) ## Description of PR Summary: 1. Add `ip/test_mgmt_ipv6_only.py` into PR pipeline testing. 2. Rearrange fixture order for two test cases: `ip/test_mgmt_ipv6_only.py::test_rw_user_ipv6_only` and `ip/test_mgmt_ipv6_only.py::test_ro_user_ipv6_only`. 3. Workaround pytest fixture teardown bug affecting `setup_ntp` when run the `ip/test_mgmt_ipv6_only.py` tests. ### Type of change - [x] Bug fix - [ ] Testbed and Framework(new/improvement) - [x] Test case(new/improvement) ## Approach #### What is the motivation for this PR? ##### 1. Include `ip/test_mgmt_ipv6_only.py` into PR pipeline testing for IPv6 hardening. ##### 2. Fix errors when running individual test cases. ``` $ ./run_tests.sh -n vms-kvm-t0 -d vlab-01 -f vtestbed.yaml -i ../ansible/veos_vtb -u -c ip/test_mgmt_ipv6_only.py::test_ro_user_ipv6_only ...... ip/test_mgmt_ipv6_only.py::test_ro_user_ipv6_only[vlab-01] FAILED [100%] ...... ip/test_mgmt_ipv6_only.py:138: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ output = {'failed': True, 'changed': True, 'stdout': '', 'stderr': "Warning: Permanently added 'fec0::ffff:afa:1' (RSA) to the ...fec0::ffff:afa:1' (RSA) to the list of known hosts.", 'Permission denied, please try again.'], '_ansible_no_log': None} exp_val1 = 'test', exp_val2 = 'remote_user' def check_output(output, exp_val1, exp_val2): > pytest_assert(not output['failed'], output['stderr']) E Failed: Warning: Permanently added 'fec0::ffff:afa:1' (RSA) to the list of known hosts. E Permission denied, please try again. exp_val1 = 'test' exp_val2 = 'remote_user' output = {'failed': True, 'changed': True, 'stdout': '', 'stderr': "Warning: Permanently added 'fec0::ffff:afa:1' (RSA) to the ...fec0::ffff:afa:1' (RSA) to the list of known hosts.", 'Permission denied, please try again.'], '_ansible_no_log': None} tacacs/utils.py:25: Failed ``` The root case is: in current test case definition, the fixture setup sequence is: 1. `tacacs_v6` --> `sudo config tacacs add fec0::ffff:afa:2` 2. `convert_and_restore_config_db_to_ipv6_only` --> `config reload -y` after removing ipv4 mgmt address The `sudo config tacacs add fec0::ffff:afa:2` config is lost after the `config reload -y` in step 2. Therefore, causing tacacs authentication failure. If `convert_and_restore_config_db_to_ipv6_only` is called before `check_tacacs_v6`, there will be no issue. ``` Current definition: def test_ro_user_ipv6_only(localhost, duthosts, enum_rand_one_per_hwsku_hostname, tacacs_creds, check_tacacs_v6, convert_and_restore_config_db_to_ipv6_only): # noqa F811 Correct definition: def test_ro_user_ipv6_only(localhost, duthosts, enum_rand_one_per_hwsku_hostname, tacacs_creds, convert_and_restore_config_db_to_ipv6_only, check_tacacs_v6): # noqa F811 ``` ##### 3. Fix fixture teardown error when running whole ip/test_mgmt_ipv6_only.py. ``` When running the full test cases, we are seeing the following fixture sequence and error. $./run_tests.sh -n vms-kvm-t0 -d vlab-01 -c ip/test_mgmt_ipv6_only.py -f vtestbed.yaml -i ../ansible/veos_vtb -u -e "--setup-show" SETUP M convert_and_restore_config_db_to_ipv6_only (fixtures used: duthosts) SETUP M setup_ntp (fixtures used: duthosts, ptf_use_ipv6, ptfhost, rand_one_dut_hostname) ...... TEARDOWN M convert_and_restore_config_db_to_ipv6_only ---> This is wrong. setup_ntp should be teardown first. TEARDOWN M setup_ntp ...... > raise RunAnsibleModuleFail("run module {} failed".format(self.module_name), res) E tests.common.errors.RunAnsibleModuleFail: run module command failed, Ansible Results => E {"changed": true, "cmd": ["config", "ntp", "del", "fec0::ffff:afa:2"], "delta": "0:00:00.277230", "end": "2024-05-02 11:32:22.404196", "failed": true, "msg": "non-zero return code", "rc": 2, "start": "2024-05-02 11:32:22.126966", "stderr": "Usage: config ntp del [OPTIONS] <ntp_ip_address>\nTry \"config ntp del -h\" for help.\n\nError: NTP server fec0::ffff:afa:2 is not configured.", "stderr_lines": ["Usage: config ntp del [OPTIONS] <ntp_ip_address>", "Try \"config ntp del -h\" for help.", "", "Error: NTP server fec0::ffff:afa:2 is not configured."], "stdout": "", "stdout_lines": []} ...... ``` The teardown should be the reverse of fixture setup. The expected setup/teardown order is: ``` SETUP M convert_and_restore_config_db_to_ipv6_only (fixtures used: duthosts) SETUP M setup_ntp (fixtures used: duthosts, ptf_use_ipv6, ptfhost, rand_one_dut_hostname) ...... TEARDOWN M setup_ntp TEARDOWN M convert_and_restore_config_db_to_ipv6_only ``` This error is linked to a known issue pytest-dev/pytest#12135 in pytest, and it has been fixed pytest 8.2.0 via pytest-dev/pytest#11833. Currently, SONiC is utilizing pytest version 7.4.0, which does not include the fix for this issue. To address this, a workaround will be necessary until sonic-mgmt is upgraded to pytest version 8.2.0. #### How did you do it? 1. Add it into the PR test case list. 2. changed the fixture request sequence, put `convert_and_restore_config_db_to_ipv6_only` to the left of `check_tacacs_v6.` so `convert_and_restore_config_db_to_ipv6_only` fixture will run before `tacacs_v6`. 4. As upgrading pytest version is not trial change, I duplicated the `setup_ntp` fixture at `function` scope. As ntp is only one case in `test_mgmt_ipv6_only.py`, it makes it more suitable to use a `function` scope fixture instead of `module` scope fixture. #### How did you verify/test it? 1. pipeline check included test_mgmt_ipv6_only.py 2. Run individual test against test_rw_user_ipv6_only, test_ro_user_ipv6_only, test_ntp_ipv6_only. All passed: ``` $./run_tests.sh -n vms-kvm-t0 -d vlab-01 -f vtestbed.yaml -i ../ansible/veos_vtb -u -c ip/test_mgmt_ipv6_only.py::test_ro_user_ipv6_only .... ip/test_mgmt_ipv6_only.py::test_ro_user_ipv6_only[vlab-01] PASSED [100%] $ ./run_tests.sh -n vms-kvm-t0 -d vlab-01 -f vtestbed.yaml -i ../ansible/veos_vtb -u -c ip/test_mgmt_ipv6_only.py::test_rw_user_ipv6_only ...... ip/test_mgmt_ipv6_only.py::test_rw_user_ipv6_only[vlab-01] PASSED [100%] $./run_tests.sh -n vms-kvm-t0 -d vlab-01 -f vtestbed.yaml -i ../ansible/veos_vtb -u -c ip/test_mgmt_ipv6_only.py::test_ntp_ipv6_only ...... ip/test_mgmt_ipv6_only.py::test_ntp_ipv6_only[True-vlab-01] PASSED [100%] ``` 3. Full test passed: ``` $./run_tests.sh -n vms-kvm-t0 -d vlab-01 -f vtestbed.yaml -i ../ansible/veos_vtb -u -c ip/test_mgmt_ipv6_only.py ...... ip/test_mgmt_ipv6_only.py::test_bgp_facts_ipv6_only[vlab-01-None] PASSED [ 10%] ip/test_mgmt_ipv6_only.py::test_show_features_ipv6_only[vlab-01] PASSED [ 20%] ip/test_mgmt_ipv6_only.py::test_image_download_ipv6_only[vlab-01] SKIPPED (Cannot get image url) [ 30%] ip/test_mgmt_ipv6_only.py::test_syslog_ipv6_only[vlab-01-fd82:b34f:cc99::100-None] PASSED [ 40%] ip/test_mgmt_ipv6_only.py::test_syslog_ipv6_only[vlab-01-fd82:b34f:cc99::100-fd82:b34f:cc99::200] PASSED [ 50%] ip/test_mgmt_ipv6_only.py::test_ntp_ipv6_only[True-vlab-01] PASSED [ 60%] ip/test_mgmt_ipv6_only.py::test_snmp_ipv6_only[vlab-01] PASSED [ 70%] ip/test_mgmt_ipv6_only.py::test_ro_user_ipv6_only[vlab-01] PASSED [ 80%] ip/test_mgmt_ipv6_only.py::test_rw_user_ipv6_only[vlab-01] PASSED [ 90%] ip/test_mgmt_ipv6_only.py::test_telemetry_output_ipv6_only[vlab-01-True] PASSED [100%] ==================================================================================== warnings summary ==================================================================================== ../../../usr/local/lib/python3.8/dist-packages/paramiko/transport.py:236 /usr/local/lib/python3.8/dist-packages/paramiko/transport.py:236: CryptographyDeprecationWarning: Blowfish has been deprecated "class": algorithms.Blowfish, -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html ----------------------------------------------------------------- generated xml file: /data/sonic-mgmt/tests/logs/tr.xml ----------------------------------------------------------------- ================================================================================ short test summary info ================================================================================= SKIPPED [1] common/helpers/assertions.py:16: Cannot get image url ================================================================== 9 passed, 1 skipped, 1 warning in 745.28s (0:12:25) =================================================================== ```
sonic-net#12393) Summary: 1. Add `ip/test_mgmt_ipv6_only.py` into PR pipeline testing. 2. Rearrange fixture order for two test cases: `ip/test_mgmt_ipv6_only.py::test_rw_user_ipv6_only` and `ip/test_mgmt_ipv6_only.py::test_ro_user_ipv6_only`. 3. Workaround pytest fixture teardown bug affecting `setup_ntp` when run the `ip/test_mgmt_ipv6_only.py` tests. - [x] Bug fix - [ ] Testbed and Framework(new/improvement) - [x] Test case(new/improvement) ``` $ ./run_tests.sh -n vms-kvm-t0 -d vlab-01 -f vtestbed.yaml -i ../ansible/veos_vtb -u -c ip/test_mgmt_ipv6_only.py::test_ro_user_ipv6_only ...... ip/test_mgmt_ipv6_only.py::test_ro_user_ipv6_only[vlab-01] FAILED [100%] ...... ip/test_mgmt_ipv6_only.py:138: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ output = {'failed': True, 'changed': True, 'stdout': '', 'stderr': "Warning: Permanently added 'fec0::ffff:afa:1' (RSA) to the ...fec0::ffff:afa:1' (RSA) to the list of known hosts.", 'Permission denied, please try again.'], '_ansible_no_log': None} exp_val1 = 'test', exp_val2 = 'remote_user' def check_output(output, exp_val1, exp_val2): > pytest_assert(not output['failed'], output['stderr']) E Failed: Warning: Permanently added 'fec0::ffff:afa:1' (RSA) to the list of known hosts. E Permission denied, please try again. exp_val1 = 'test' exp_val2 = 'remote_user' output = {'failed': True, 'changed': True, 'stdout': '', 'stderr': "Warning: Permanently added 'fec0::ffff:afa:1' (RSA) to the ...fec0::ffff:afa:1' (RSA) to the list of known hosts.", 'Permission denied, please try again.'], '_ansible_no_log': None} tacacs/utils.py:25: Failed ``` The root case is: in current test case definition, the fixture setup sequence is: 1. `tacacs_v6` --> `sudo config tacacs add fec0::ffff:afa:2` 2. `convert_and_restore_config_db_to_ipv6_only` --> `config reload -y` after removing ipv4 mgmt address The `sudo config tacacs add fec0::ffff:afa:2` config is lost after the `config reload -y` in step 2. Therefore, causing tacacs authentication failure. If `convert_and_restore_config_db_to_ipv6_only` is called before `check_tacacs_v6`, there will be no issue. ``` Current definition: def test_ro_user_ipv6_only(localhost, duthosts, enum_rand_one_per_hwsku_hostname, tacacs_creds, check_tacacs_v6, convert_and_restore_config_db_to_ipv6_only): # noqa F811 Correct definition: def test_ro_user_ipv6_only(localhost, duthosts, enum_rand_one_per_hwsku_hostname, tacacs_creds, convert_and_restore_config_db_to_ipv6_only, check_tacacs_v6): # noqa F811 ``` ``` When running the full test cases, we are seeing the following fixture sequence and error. $./run_tests.sh -n vms-kvm-t0 -d vlab-01 -c ip/test_mgmt_ipv6_only.py -f vtestbed.yaml -i ../ansible/veos_vtb -u -e "--setup-show" SETUP M convert_and_restore_config_db_to_ipv6_only (fixtures used: duthosts) SETUP M setup_ntp (fixtures used: duthosts, ptf_use_ipv6, ptfhost, rand_one_dut_hostname) ...... TEARDOWN M convert_and_restore_config_db_to_ipv6_only ---> This is wrong. setup_ntp should be teardown first. TEARDOWN M setup_ntp ...... > raise RunAnsibleModuleFail("run module {} failed".format(self.module_name), res) E tests.common.errors.RunAnsibleModuleFail: run module command failed, Ansible Results => E {"changed": true, "cmd": ["config", "ntp", "del", "fec0::ffff:afa:2"], "delta": "0:00:00.277230", "end": "2024-05-02 11:32:22.404196", "failed": true, "msg": "non-zero return code", "rc": 2, "start": "2024-05-02 11:32:22.126966", "stderr": "Usage: config ntp del [OPTIONS] <ntp_ip_address>\nTry \"config ntp del -h\" for help.\n\nError: NTP server fec0::ffff:afa:2 is not configured.", "stderr_lines": ["Usage: config ntp del [OPTIONS] <ntp_ip_address>", "Try \"config ntp del -h\" for help.", "", "Error: NTP server fec0::ffff:afa:2 is not configured."], "stdout": "", "stdout_lines": []} ...... ``` The teardown should be the reverse of fixture setup. The expected setup/teardown order is: ``` SETUP M convert_and_restore_config_db_to_ipv6_only (fixtures used: duthosts) SETUP M setup_ntp (fixtures used: duthosts, ptf_use_ipv6, ptfhost, rand_one_dut_hostname) ...... TEARDOWN M setup_ntp TEARDOWN M convert_and_restore_config_db_to_ipv6_only ``` This error is linked to a known issue pytest-dev/pytest#12135 in pytest, and it has been fixed pytest 8.2.0 via pytest-dev/pytest#11833. Currently, SONiC is utilizing pytest version 7.4.0, which does not include the fix for this issue. To address this, a workaround will be necessary until sonic-mgmt is upgraded to pytest version 8.2.0. 1. Add it into the PR test case list. 2. changed the fixture request sequence, put `convert_and_restore_config_db_to_ipv6_only` to the left of `check_tacacs_v6.` so `convert_and_restore_config_db_to_ipv6_only` fixture will run before `tacacs_v6`. 4. As upgrading pytest version is not trial change, I duplicated the `setup_ntp` fixture at `function` scope. As ntp is only one case in `test_mgmt_ipv6_only.py`, it makes it more suitable to use a `function` scope fixture instead of `module` scope fixture. 1. pipeline check included test_mgmt_ipv6_only.py 2. Run individual test against test_rw_user_ipv6_only, test_ro_user_ipv6_only, test_ntp_ipv6_only. All passed: ``` $./run_tests.sh -n vms-kvm-t0 -d vlab-01 -f vtestbed.yaml -i ../ansible/veos_vtb -u -c ip/test_mgmt_ipv6_only.py::test_ro_user_ipv6_only .... ip/test_mgmt_ipv6_only.py::test_ro_user_ipv6_only[vlab-01] PASSED [100%] $ ./run_tests.sh -n vms-kvm-t0 -d vlab-01 -f vtestbed.yaml -i ../ansible/veos_vtb -u -c ip/test_mgmt_ipv6_only.py::test_rw_user_ipv6_only ...... ip/test_mgmt_ipv6_only.py::test_rw_user_ipv6_only[vlab-01] PASSED [100%] $./run_tests.sh -n vms-kvm-t0 -d vlab-01 -f vtestbed.yaml -i ../ansible/veos_vtb -u -c ip/test_mgmt_ipv6_only.py::test_ntp_ipv6_only ...... ip/test_mgmt_ipv6_only.py::test_ntp_ipv6_only[True-vlab-01] PASSED [100%] ``` 3. Full test passed: ``` $./run_tests.sh -n vms-kvm-t0 -d vlab-01 -f vtestbed.yaml -i ../ansible/veos_vtb -u -c ip/test_mgmt_ipv6_only.py ...... ip/test_mgmt_ipv6_only.py::test_bgp_facts_ipv6_only[vlab-01-None] PASSED [ 10%] ip/test_mgmt_ipv6_only.py::test_show_features_ipv6_only[vlab-01] PASSED [ 20%] ip/test_mgmt_ipv6_only.py::test_image_download_ipv6_only[vlab-01] SKIPPED (Cannot get image url) [ 30%] ip/test_mgmt_ipv6_only.py::test_syslog_ipv6_only[vlab-01-fd82:b34f:cc99::100-None] PASSED [ 40%] ip/test_mgmt_ipv6_only.py::test_syslog_ipv6_only[vlab-01-fd82:b34f:cc99::100-fd82:b34f:cc99::200] PASSED [ 50%] ip/test_mgmt_ipv6_only.py::test_ntp_ipv6_only[True-vlab-01] PASSED [ 60%] ip/test_mgmt_ipv6_only.py::test_snmp_ipv6_only[vlab-01] PASSED [ 70%] ip/test_mgmt_ipv6_only.py::test_ro_user_ipv6_only[vlab-01] PASSED [ 80%] ip/test_mgmt_ipv6_only.py::test_rw_user_ipv6_only[vlab-01] PASSED [ 90%] ip/test_mgmt_ipv6_only.py::test_telemetry_output_ipv6_only[vlab-01-True] PASSED [100%] ==================================================================================== warnings summary ==================================================================================== ../../../usr/local/lib/python3.8/dist-packages/paramiko/transport.py:236 /usr/local/lib/python3.8/dist-packages/paramiko/transport.py:236: CryptographyDeprecationWarning: Blowfish has been deprecated "class": algorithms.Blowfish, -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html ----------------------------------------------------------------- generated xml file: /data/sonic-mgmt/tests/logs/tr.xml ----------------------------------------------------------------- ================================================================================ short test summary info ================================================================================= SKIPPED [1] common/helpers/assertions.py:16: Cannot get image url ================================================================== 9 passed, 1 skipped, 1 warning in 745.28s (0:12:25) =================================================================== ```
sonic-net#12393) ## Description of PR Summary: 1. Add `ip/test_mgmt_ipv6_only.py` into PR pipeline testing. 2. Rearrange fixture order for two test cases: `ip/test_mgmt_ipv6_only.py::test_rw_user_ipv6_only` and `ip/test_mgmt_ipv6_only.py::test_ro_user_ipv6_only`. 3. Workaround pytest fixture teardown bug affecting `setup_ntp` when run the `ip/test_mgmt_ipv6_only.py` tests. ### Type of change - [x] Bug fix - [ ] Testbed and Framework(new/improvement) - [x] Test case(new/improvement) ## Approach #### What is the motivation for this PR? ##### 1. Include `ip/test_mgmt_ipv6_only.py` into PR pipeline testing for IPv6 hardening. ##### 2. Fix errors when running individual test cases. ``` $ ./run_tests.sh -n vms-kvm-t0 -d vlab-01 -f vtestbed.yaml -i ../ansible/veos_vtb -u -c ip/test_mgmt_ipv6_only.py::test_ro_user_ipv6_only ...... ip/test_mgmt_ipv6_only.py::test_ro_user_ipv6_only[vlab-01] FAILED [100%] ...... ip/test_mgmt_ipv6_only.py:138: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ output = {'failed': True, 'changed': True, 'stdout': '', 'stderr': "Warning: Permanently added 'fec0::ffff:afa:1' (RSA) to the ...fec0::ffff:afa:1' (RSA) to the list of known hosts.", 'Permission denied, please try again.'], '_ansible_no_log': None} exp_val1 = 'test', exp_val2 = 'remote_user' def check_output(output, exp_val1, exp_val2): > pytest_assert(not output['failed'], output['stderr']) E Failed: Warning: Permanently added 'fec0::ffff:afa:1' (RSA) to the list of known hosts. E Permission denied, please try again. exp_val1 = 'test' exp_val2 = 'remote_user' output = {'failed': True, 'changed': True, 'stdout': '', 'stderr': "Warning: Permanently added 'fec0::ffff:afa:1' (RSA) to the ...fec0::ffff:afa:1' (RSA) to the list of known hosts.", 'Permission denied, please try again.'], '_ansible_no_log': None} tacacs/utils.py:25: Failed ``` The root case is: in current test case definition, the fixture setup sequence is: 1. `tacacs_v6` --> `sudo config tacacs add fec0::ffff:afa:2` 2. `convert_and_restore_config_db_to_ipv6_only` --> `config reload -y` after removing ipv4 mgmt address The `sudo config tacacs add fec0::ffff:afa:2` config is lost after the `config reload -y` in step 2. Therefore, causing tacacs authentication failure. If `convert_and_restore_config_db_to_ipv6_only` is called before `check_tacacs_v6`, there will be no issue. ``` Current definition: def test_ro_user_ipv6_only(localhost, duthosts, enum_rand_one_per_hwsku_hostname, tacacs_creds, check_tacacs_v6, convert_and_restore_config_db_to_ipv6_only): # noqa F811 Correct definition: def test_ro_user_ipv6_only(localhost, duthosts, enum_rand_one_per_hwsku_hostname, tacacs_creds, convert_and_restore_config_db_to_ipv6_only, check_tacacs_v6): # noqa F811 ``` ##### 3. Fix fixture teardown error when running whole ip/test_mgmt_ipv6_only.py. ``` When running the full test cases, we are seeing the following fixture sequence and error. $./run_tests.sh -n vms-kvm-t0 -d vlab-01 -c ip/test_mgmt_ipv6_only.py -f vtestbed.yaml -i ../ansible/veos_vtb -u -e "--setup-show" SETUP M convert_and_restore_config_db_to_ipv6_only (fixtures used: duthosts) SETUP M setup_ntp (fixtures used: duthosts, ptf_use_ipv6, ptfhost, rand_one_dut_hostname) ...... TEARDOWN M convert_and_restore_config_db_to_ipv6_only ---> This is wrong. setup_ntp should be teardown first. TEARDOWN M setup_ntp ...... > raise RunAnsibleModuleFail("run module {} failed".format(self.module_name), res) E tests.common.errors.RunAnsibleModuleFail: run module command failed, Ansible Results => E {"changed": true, "cmd": ["config", "ntp", "del", "fec0::ffff:afa:2"], "delta": "0:00:00.277230", "end": "2024-05-02 11:32:22.404196", "failed": true, "msg": "non-zero return code", "rc": 2, "start": "2024-05-02 11:32:22.126966", "stderr": "Usage: config ntp del [OPTIONS] <ntp_ip_address>\nTry \"config ntp del -h\" for help.\n\nError: NTP server fec0::ffff:afa:2 is not configured.", "stderr_lines": ["Usage: config ntp del [OPTIONS] <ntp_ip_address>", "Try \"config ntp del -h\" for help.", "", "Error: NTP server fec0::ffff:afa:2 is not configured."], "stdout": "", "stdout_lines": []} ...... ``` The teardown should be the reverse of fixture setup. The expected setup/teardown order is: ``` SETUP M convert_and_restore_config_db_to_ipv6_only (fixtures used: duthosts) SETUP M setup_ntp (fixtures used: duthosts, ptf_use_ipv6, ptfhost, rand_one_dut_hostname) ...... TEARDOWN M setup_ntp TEARDOWN M convert_and_restore_config_db_to_ipv6_only ``` This error is linked to a known issue pytest-dev/pytest#12135 in pytest, and it has been fixed pytest 8.2.0 via pytest-dev/pytest#11833. Currently, SONiC is utilizing pytest version 7.4.0, which does not include the fix for this issue. To address this, a workaround will be necessary until sonic-mgmt is upgraded to pytest version 8.2.0. #### How did you do it? 1. Add it into the PR test case list. 2. changed the fixture request sequence, put `convert_and_restore_config_db_to_ipv6_only` to the left of `check_tacacs_v6.` so `convert_and_restore_config_db_to_ipv6_only` fixture will run before `tacacs_v6`. 4. As upgrading pytest version is not trial change, I duplicated the `setup_ntp` fixture at `function` scope. As ntp is only one case in `test_mgmt_ipv6_only.py`, it makes it more suitable to use a `function` scope fixture instead of `module` scope fixture. #### How did you verify/test it? 1. pipeline check included test_mgmt_ipv6_only.py 2. Run individual test against test_rw_user_ipv6_only, test_ro_user_ipv6_only, test_ntp_ipv6_only. All passed: ``` $./run_tests.sh -n vms-kvm-t0 -d vlab-01 -f vtestbed.yaml -i ../ansible/veos_vtb -u -c ip/test_mgmt_ipv6_only.py::test_ro_user_ipv6_only .... ip/test_mgmt_ipv6_only.py::test_ro_user_ipv6_only[vlab-01] PASSED [100%] $ ./run_tests.sh -n vms-kvm-t0 -d vlab-01 -f vtestbed.yaml -i ../ansible/veos_vtb -u -c ip/test_mgmt_ipv6_only.py::test_rw_user_ipv6_only ...... ip/test_mgmt_ipv6_only.py::test_rw_user_ipv6_only[vlab-01] PASSED [100%] $./run_tests.sh -n vms-kvm-t0 -d vlab-01 -f vtestbed.yaml -i ../ansible/veos_vtb -u -c ip/test_mgmt_ipv6_only.py::test_ntp_ipv6_only ...... ip/test_mgmt_ipv6_only.py::test_ntp_ipv6_only[True-vlab-01] PASSED [100%] ``` 3. Full test passed: ``` $./run_tests.sh -n vms-kvm-t0 -d vlab-01 -f vtestbed.yaml -i ../ansible/veos_vtb -u -c ip/test_mgmt_ipv6_only.py ...... ip/test_mgmt_ipv6_only.py::test_bgp_facts_ipv6_only[vlab-01-None] PASSED [ 10%] ip/test_mgmt_ipv6_only.py::test_show_features_ipv6_only[vlab-01] PASSED [ 20%] ip/test_mgmt_ipv6_only.py::test_image_download_ipv6_only[vlab-01] SKIPPED (Cannot get image url) [ 30%] ip/test_mgmt_ipv6_only.py::test_syslog_ipv6_only[vlab-01-fd82:b34f:cc99::100-None] PASSED [ 40%] ip/test_mgmt_ipv6_only.py::test_syslog_ipv6_only[vlab-01-fd82:b34f:cc99::100-fd82:b34f:cc99::200] PASSED [ 50%] ip/test_mgmt_ipv6_only.py::test_ntp_ipv6_only[True-vlab-01] PASSED [ 60%] ip/test_mgmt_ipv6_only.py::test_snmp_ipv6_only[vlab-01] PASSED [ 70%] ip/test_mgmt_ipv6_only.py::test_ro_user_ipv6_only[vlab-01] PASSED [ 80%] ip/test_mgmt_ipv6_only.py::test_rw_user_ipv6_only[vlab-01] PASSED [ 90%] ip/test_mgmt_ipv6_only.py::test_telemetry_output_ipv6_only[vlab-01-True] PASSED [100%] ==================================================================================== warnings summary ==================================================================================== ../../../usr/local/lib/python3.8/dist-packages/paramiko/transport.py:236 /usr/local/lib/python3.8/dist-packages/paramiko/transport.py:236: CryptographyDeprecationWarning: Blowfish has been deprecated "class": algorithms.Blowfish, -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html ----------------------------------------------------------------- generated xml file: /data/sonic-mgmt/tests/logs/tr.xml ----------------------------------------------------------------- ================================================================================ short test summary info ================================================================================= SKIPPED [1] common/helpers/assertions.py:16: Cannot get image url ================================================================== 9 passed, 1 skipped, 1 warning in 745.28s (0:12:25) =================================================================== ```
haven't figured out what about it yet -- but something about this PR makes sentry's testsuite very flaky annoyingly it's not very reproducible beyond running a whole suite a bunch of times so my "bisect" has taken a few days (I sorta manually bisected against the failure mode looks like https://github.com/getsentry/sentry/actions/runs/10905480202/job/30267377794?pr=76935 -- the vast majority of the exceptions are in a per-test teardown don't suppose anyone has more specific ideas about how I should narrow down what's causing this? I tried looking at the my current hypothesis (with absolutely no evidence because I haven't found a consistent reproduction) is the first test failure (which happens during teardown) somehow interrupts the rest of the teardowns meaning the database connections never get cleaned up (and then subsequent tests which check that error). the test itself is flaky but mostly paved over with retries -- but that shouldn't bring down the rest of the suite with it! first teardown failure, kinda long
|
@asottile-sentry What do you use for retries? |
@asottile-sentry as I said at the start of the PR:
To debug I would suggest logging extensively the order that your fixtures are set up and torn down (before and after this PR) and also watch out for #12134. It's possible that you need to add inter-fixture dependencies to ensure that they're handled correctly. |
are you saying that |
Oh, knowing about This PR really shouldn't change anything other than teardown ordering in some cases, I have no clue why it would impact anything else. But the fixture system is quite messy so ??? |
I suspected this -- rerunfailures is great but it adds another layer of complexity to such issues, especially involving teardown failures, because it does substantial hacking around the pytest runner internals. If you're able to try, do the cascading failures still happen without rerunfailures? Knowing this will tell us where to look.
Indeed... Also, teardown errors should be avoided whenever possible, semantically they're a mess to deal with. |
turning off rerunfailures does seem to fix this -- we're also on an old version of that so I'm going to try upgrading it too |
upgrading it did not fix the problem -- so I believe there's something amiss with rerunfailures and pytest>=8.2 -- potentially with (partial?) missing teardowns |
I think I'm getting closer -- I now have a local reproduction and it seems to need:
hopefully I'll be able to find the problem from that! |
finally narrowed it down to a minimal example -- and reconfirmed that this is the patch which regresses it via bisect from unittest import TestCase
g = True
class TestCaseTest(TestCase):
@classmethod
def tearDownClass(cls):
print('class teardown!')
def test(self):
global g
print('test!')
if g:
print('flaky fail!')
g = False
raise AssertionError('#') this is what it should produce: $ ./venv/bin/pytest --reruns=2 -s t.py
============================ test session starts ============================
platform darwin -- Python 3.12.2, pytest-8.1.2, pluggy-1.5.0
rootdir: /private/tmp/y
plugins: rerunfailures-14.0
collected 1 item
t.py test!
flaky fail!
Rtest!
class teardown!
.
======================== 1 passed, 1 rerun in 0.01s ========================= and this is what it produces after this patch (note that $ ./venvnew/bin/pytest --reruns=2 -s t.py
============================ test session starts ============================
platform darwin -- Python 3.12.2, pytest-8.2.0, pluggy-1.5.0
rootdir: /private/tmp/y
plugins: rerunfailures-14.0
collected 1 item
t.py test!
flaky fail!
Rtest!
.
======================== 1 passed, 1 rerun in 0.01s ========================= |
before this patch redundant copies of teardowns were added (which is what caused erronous ordering), so maybe edit: trying it out locally, replacing |
pytest-rerunfailures does not official support class-scoped fixtures, which unittest I think you may be another instance of pytest-dev/pytest-rerunfailures#267 though, this PR was released as part of 8.2 |
pytest-rerunfailures does not run class teardowns in pytest 8.2+ see pytest-dev/pytest#11833
Fixes #1489 - see #1489 (comment) for explanation of current behavior.
It's possible this might break test setups that (inadvertently) relies on the teardown order being as it is, but I think it's an improvement on the status quo.
The code for setup & teardown of fixtures was very hard to parse, and I added a couple comments and fixed what I think are outdated and now incorrect docstrings. But feels like a more thorough cleanup could be warranted. I have not looked into
git blame
to see if/when/how stuff got changed.Remaining questions:
test_issue_519
fixture scope is ignored when using metafunc.parametrize() #519 (teardown of('test_one[arg1v2-arg2v1]', 'fix2', 'arg2v1'),
got moved one step earlier) and a similar thing intest_parametrization_setup_teardown_ordering
(step1-2
happening beforesetup 2
), but I haven't dug into why or if/how it's bad.functools.partial
call is mostly to make it more visible that it's done in several places in the function, but am down for skipping it.execute
returnNone
?