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

Finalizer re-registered in parent fixture even when the value of the fixture is cached #12135

Closed
jakkdl opened this issue Mar 18, 2024 · 1 comment · Fixed by #12136
Closed
Labels
topic: fixtures anything involving fixtures directly or indirectly type: bug problem that needs to be addressed

Comments

@jakkdl
Copy link
Member

jakkdl commented Mar 18, 2024

When getting the value of a fixture, it will always add its finalizer to parent fixtures, regardless of if it's cached or not. When we have several subfixtures, this leads to unpredictable ordering between them on when they're torn down (if triggered by the parent fixture being torn down). It is also an obvious inefficiency, where a long-lived parent fixture could rack up tons of irrelevant fixtures.

As found in discussion of #11833, which resolved a similar issue. Also related to #4871

repro

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 add a second copy of 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):
    ...

output

setup 2
setup 3
teardown 2
teardown 3

but if we remove test_3 we get 2-3-3-2.

suggested fix

Here's the relevant method:

pytest/src/_pytest/fixtures.py

Lines 1037 to 1079 in 2607fe8

def execute(self, request: SubRequest) -> FixtureValue:
finalizer = functools.partial(self.finish, request=request)
# Get required arguments and register our own finish()
# with their finalization.
for argname in self.argnames:
fixturedef = request._get_active_fixturedef(argname)
if not isinstance(fixturedef, PseudoFixtureDef):
fixturedef.addfinalizer(finalizer)
my_cache_key = self.cache_key(request)
if self.cached_result is not None:
cache_key = self.cached_result[1]
# note: comparison with `==` can fail (or be expensive) for e.g.
# numpy arrays (#6497).
if my_cache_key is cache_key:
if self.cached_result[2] is not None:
exc = self.cached_result[2]
raise exc
else:
result = self.cached_result[0]
return result
# We have a previous but differently parametrized fixture instance
# so we need to tear it down before creating a new one.
self.finish(request)
assert self.cached_result is None
ihook = request.node.ihook
try:
# Setup the fixture, run the code in it, and cache the value
# in self.cached_result
result = ihook.pytest_fixture_setup(fixturedef=self, request=request)
finally:
# schedule our finalizer, even if the setup failed
request.node.addfinalizer(finalizer)
return result
def cache_key(self, request: SubRequest) -> object:
return request.param_index if not hasattr(request, "param") else request.param
def __repr__(self) -> str:
return f"<FixtureDef argname={self.argname!r} scope={self.scope!r} baseid={self.baseid!r}>"

I thought this would be a trivial fix, merely moving the code that adds finalizers to parent fixtures (L1041 to L1044) to after the check on whether the value is cached (L1062). But this broke tests in very weird ways... and I think that's the same problem as noted in #4871 (comment) - see "Possible solution - make the cache key not match". Current behaviour requires that the call to request._get_active_fixturedef(argname) happens before checking if the value is cached, to let the parent fixture check if it has been differently parametrized -> if so tear itself down -> tear us down -> invalidate our cache.

Once I figured out that the finalize-adding code had dual purposes, it was fairly easy to split it and achieve the wanted behavior without breaking anything:

    def execute(self, request: SubRequest) -> FixtureValue:
        # Ensure arguments (parent fixtures) are loaded. If their cache has been
        # invalidated they will also finalize us and invalidate our cache.
        # If/when parent fixture parametrization is included in our cache key
        # this can be moved after checking our cache key.
        parent_fixtures_to_add_finalizer = []
        for argname in self.argnames:
            fixturedef = request._get_active_fixturedef(argname)
            if not isinstance(fixturedef, PseudoFixtureDef):
                # save fixture as one to add our finalizer to, if we're not cached
                # resolves #12135
                parent_fixtures_to_add_finalizer.append(fixturedef)

        my_cache_key = self.cache_key(request)
        if self.cached_result is not None:
            ...

        finalizer = functools.partial(self.finish, request=request)
        # add finalizer to parent fixtures
        for parent_fixture in parent_fixtures_to_add_finalizer:
            parent_fixture.addfinalizer(finalizer)

possible bad outcomes

... I have a very hard time coming up with any downside of implementing this. Will write a PR once I've rewritten the repro as a proper regression test.

@bluetech
Copy link
Member

... I have a very hard time coming up with any downside of implementing this

If fixture_2 is already cached, then we know that it's already registered in fixture_1's finalizer stack, and we know fixture_1 hasn't switched params otherwise it would have finalized and finished fixture_2 as well (so it wouldn't be cached). So the only effect is to "bump" fixture_2 order in the fixture_1's finalizer stack. In this example, the effect is bad. Is there any case where it's needed for correctness? I don't think so -- if there is a dependency, it would have already been reflected.

So I agree with your conclusion. Good catch!

@Zac-HD Zac-HD added type: bug problem that needs to be addressed topic: fixtures anything involving fixtures directly or indirectly labels Mar 23, 2024
qiluo-msft pushed a commit to sonic-net/sonic-mgmt that referenced this issue May 3, 2024
#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) ===================================================================
```
sdszhang added a commit to sdszhang/sonic-mgmt that referenced this issue May 10, 2024
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) ===================================================================
```
sdszhang added a commit to sdszhang/sonic-mgmt that referenced this issue May 16, 2024
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) ===================================================================
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: fixtures anything involving fixtures directly or indirectly type: bug problem that needs to be addressed
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants