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

add integration test for LP: #1900837 #679

Merged
merged 1 commit into from Nov 20, 2020
Merged

Conversation

OddBloke
Copy link
Collaborator

Proposed Commit Message

add integration test for LP: #1900837

As the first test of this SRU cycle, this also introduces the
sru_2020_11 mark to allow us to easily identify the set of tests
generated for this SRU.

Test Steps

I ran this test against every current Ubuntu release:

for release in xenial bionic focal groovy hirsute; do CLOUD_INIT_OS_IMAGE=$release pytest --log-cli-level=INFO tests/integration_tests/bugs/test_lp1900837.py; done

and it failed with:

FAILED tests/integration_tests/bugs/test_lp1900837.py::TestLp1900836::test_lp1900836 - AssertionError: assert '600' == '644'

in each case, as expected.

I then ran this test against every current Ubuntu release with cloud-init installed from the daily PPA (which includes the fix this is testing) and they all passed.

Checklist:

  • My code follows the process laid out in the documentation
  • I have updated or added any unit tests accordingly
  • I have updated or added any documentation accordingly

Copy link
Member

@TheRealFalcon TheRealFalcon left a comment

Choose a reason for hiding this comment

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

I like the idea of the mark. It gives us an easy way to run (or exclude) tests for an SRU...though over time we could have an unruly number of SRU marks.

What if we had a generic sru mark that took (kw)args of the SRU release, an optional bug reference, and/or anything else we think is relevant? How much work would it be to make it easy to filter on those values?



@pytest.mark.sru_2020_11
class TestLp1900836:
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we should name files/classes/tests after bugs. The docstring at the top has the bug number, and we could perhaps add a comment with the bug number somewhere else, but I'd like to be able to quickly glance a file/class/test name to know what the test is doing.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm wondering if we are abusing marks here as a generic tagging feature (which I guess it actually is :) ). If we adopt a class or test naming convention that includes SRU or SRU_20_4 (cloud-init Major_minor version or date-based SRU start 2020_11) wouldn't we be able to provide pytest keyword search expressions any avoid the bloat of marker definitions in tox? they we could run things like pytest -k "SRU" to run any SRU-related tests ever defined or pytest -k "SRU_2020_11" to match only specific tests related to SRU_2020_11?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think we should name files/classes/tests after bugs. The docstring at the top has the bug number, and we could perhaps add a comment with the bug number somewhere else, but I'd like to be able to quickly glance a file/class/test name to know what the test is doing.

I agree I'd like to glance at test name and know what it's doing, but I also want a way to search and run specific known tests based on my intent:

  • am I SRU testing and can I easily select the subset of tests related to that capacity with a -k or -m param?
  • am I trying to validate just one bug and want to provide a bug Id.

This is probably just representing something might be able to be solved per @TheRealFalcon's generic kwargs handling suggestion above or maybe we create a decorator that could optionally skip test or not include a test that didn't match a set of kwargs or environment variable criteria.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think we should name files/classes/tests after bugs. The docstring at the top has the bug number, and we could perhaps add a comment with the bug number somewhere else, but I'd like to be able to quickly glance a file/class/test name to know what the test is doing.

That's a good point; I think we probably do want the bug number in at least one part of the test ID (so -k 1900837 works, for example, but also so you can easily find the bug based on test output). I'll play around with some naming combinations and see what I like the look of.

I'm wondering if we are abusing marks here as a generic tagging feature (which I guess it actually is :) ).

We are using it that way, and I don't think it's an abuse. :p

If we adopt a class or test naming convention that includes SRU or SRU_20_4 (cloud-init Major_minor version or date-based SRU start 2020_11)

Using cloud-init versions will fall over as soon as we have a non-upstream-release SRU (e.g. 20.2-45-g5f7825e2-0ubuntu1 was our last SRU); that's hard to fit into a mark name, it's even harder to fit into a test name. That's why I went with date-based. (If we're ever unfortunate enough to start two SRUs in a month, we can do sru_2020_11a or similar.)

wouldn't we be able to provide pytest keyword search expressions any avoid the bloat of marker definitions in tox? they we could run things like pytest -k "SRU" to run any SRU-related tests ever defined or pytest -k "SRU_2020_11" to match only specific tests related to SRU_2020_11?

The advantage marks bring is that we gain validation of them: if we misname a test method/class "SRU_2200_11" and it makes it through code review, then we may never fix that (and so may not run the test as part of SRU validation). Using @pytest.mark.sru_2200_11 errors out without running any tests:

========================================== ERRORS ==========================================
_____________ ERROR collecting tests/integration_tests/bugs/test_lp1900837.py ______________
'sru_2200_11' not found in `markers` configuration option
================================= short test summary info ==================================
ERROR tests/integration_tests/bugs/test_lp1900837.py
!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!!!!!!!
===================================== 1 error in 0.24s =====================================

I don't think we should name files/classes/tests after bugs. The docstring at the top has the bug number, and we could perhaps add a comment with the bug number somewhere else, but I'd like to be able to quickly glance a file/class/test name to know what the test is doing.

I agree I'd like to glance at test name and know what it's doing, but I also want a way to search and run specific known tests based on my intent:

* am I SRU testing and can I easily select the subset of tests related to that capacity with a -k or -m param?

* am I trying to validate just one bug and want to provide a bug Id.

This is probably just representing something might be able to be solved per @TheRealFalcon's generic kwargs handling suggestion above or maybe we create a decorator that could optionally skip test or not include a test that didn't match a set of kwargs or environment variable criteria.

We shouldn't need to write any custom code for this: some combination of marking, test naming and -k/-m should be sufficient. I think, basically, we'll want to declare that bug number must be included in some part of the pytest nodeid, so that -k selection can work, and use marks for tagging tests as SRU-relevant. For your first case, -m sru_2020_11 will select this SRU's tests, and for your second case -k 1900837 will select the bug's tests.

(I can't think of a particular reason we would want to run "all SRU tests" instead of just "all tests", but -k does select based on substrings of mark names, so -k sru would get you that.)


@pytest.mark.sru_2020_11
class TestLp1900836:
def test_lp1900836(self, client):
Copy link
Collaborator

Choose a reason for hiding this comment

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

I kindof like that bugid is encoded in test name here as I think that makes those tests seachable via -k bugid I think. But I don't know if we need that at class level and test level or how we want to organize naming conventions vs marks

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I kindof like that bugid is encoded in test name here as I think that makes those tests seachable via -k bugid I think. But I don't know if we need that at class level and test level or how we want to organize naming conventions vs marks

+1 on this, I'm going to do some rejigging to only capture bug number in one part of the test's "full" name, so we can still select on bug number but don't just have "lp1900837" repeated ad nauseum.

@OddBloke
Copy link
Collaborator Author

I like the idea of the mark. It gives us an easy way to run (or exclude) tests for an SRU...though over time we could have an unruly number of SRU marks.

Potentially. The only real maintenance "burden" is the list in tox.ini, and I think we'd have to do a lot of SRUs for that to qualify as unruly rather than merely unkempt. I expect individual tests will only have a single SRU mark applied: the one which they were written for. (Though one advantage of using marks is that we can apply them after-the-fact if we want, so we may find cases where multiple SRU marks make sense; either way, not a huge cost IMO.) If we find that we are overwhelmed by SRU marks, we can always remove them later; once an SRU is complete, they become less and less useful over time (e.g. I don't think we're ever really going to need to think about the 19.2 SRU again at this point).

What if we had a generic sru mark that took (kw)args of the SRU release, an optional bug reference, and/or anything else we think is relevant?

I like this idea, in terms of capturing extra metadata, but

How much work would it be to make it easy to filter on those values?

I've applied this patch locally:

--- a/tests/integration_tests/bugs/test_lp1900837.py
+++ b/tests/integration_tests/bugs/test_lp1900837.py
@@ -11,7 +11,7 @@ def _get_log_perms(client):
     return client.execute("stat -c %a /var/log/cloud-init.log")
 
 
-@pytest.mark.sru_2020_11
+@pytest.mark.sru("2020_11")
 class TestLp1900836:
     def test_lp1900836(self, client):
         # Confirm that the current permissions aren't 600
diff --git a/tox.ini b/tox.ini
index 066f923a..4c3575da 100644
--- a/tox.ini
+++ b/tox.ini
@@ -169,4 +169,4 @@ markers =
     lxd_container: test will only run in LXD container
     user_data: the user data to be passed to the test instance
     instance_name: the name to be used for the test instance
-    sru_2020_11: test is part of the 2020/11 SRU verification
+    sru: test is part of SRU verification

and it certainly doesn't get selected in pytest's default -k/-m behaviours:

$ pytest --collect-only tests/integration_tests -k 2020_11
=========================================================== test session starts ============================================================
platform linux -- Python 3.8.6, pytest-6.1.0, py-1.9.0, pluggy-0.13.1
rootdir: /home/daniel/dev/cloud-init, configfile: tox.ini
plugins: xdist-2.1.0, forked-1.3.0, parallel-0.1.0, cov-2.8.1
collected 57 items / 57 deselected                                                                                                         

========================================================== 57 deselected in 0.08s ==========================================================
$ pytest --collect-only tests/integration_tests -m 2020_11
=========================================================== test session starts ============================================================
platform linux -- Python 3.8.6, pytest-6.1.0, py-1.9.0, pluggy-0.13.1
rootdir: /home/daniel/dev/cloud-init, configfile: tox.ini
plugins: xdist-2.1.0, forked-1.3.0, parallel-0.1.0, cov-2.8.1
collected 57 items / 57 deselected                                                                                                         

========================================================== 57 deselected in 0.06s ==========================================================

Digging in further, I think this is a fundamental limitation. Using this addition to conftest.py:

@pytest.hookimpl(hookwrapper=True)
def pytest_collection_modifyitems(items, config):
    yield
    assert False, list(items[0].iter_markers())

we can see what the pytest Mark object for this looks like:

AssertionError: [Mark(name='sru', args=('2020_11',), kwargs={})]

and looking at the code which performs the mark selection in pytest, it's only the name which is matched on. So I think we need to encode all the information we need in mark or test names.

(In more detail: deselect_by_mark calls MarkMatcher.from_item(item) for each test "item": the line linked above pulls out the mark names that apply to this item and returns a MarkMatcher instance. This instance is passed to expression.evaluate, where expression has been created with Expression.compile(matchexpr), matchexpr being what we passed to -m.)

So we could implement this ourselves somehow, I'm sure; pytest_collection_modifyitems is how keyword/mark selection is done internal to pytest. However, I suspect that's a timesink which doesn't buy us much concrete benefit (other than fewer tox.ini lines 😁).

@TheRealFalcon
Copy link
Member

Hmmmm, I suppose I should have looked at the docs before asking the question. I remembered seeing some sort of cli and mark custom code you could do, but didn't remember the specifics.

I found it here and it doesn't seem like it would be that difficult.
https://docs.pytest.org/en/stable/example/markers.html#custom-marker-and-command-line-option-to-control-test-runs

I agree that only doing it to avoid having extra SRU marks in the tox.ini isn't worth it, but I'm also wondering if this could be leveraged for SRU test metadata as well, instead of having to put bug names as test names for example.

@OddBloke
Copy link
Collaborator Author

Hmmmm, I suppose I should have looked at the docs before asking the question. I remembered seeing some sort of cli and mark custom code you could do, but didn't remember the specifics.

I found it here and it doesn't seem like it would be that difficult.
https://docs.pytest.org/en/stable/example/markers.html#custom-marker-and-command-line-option-to-control-test-runs

Aha, yeah, I've seen this before but didn't connect the dots; good find.

I agree that only doing it to avoid having extra SRU marks in the tox.ini isn't worth it, but I'm also wondering if this could be leveraged for SRU test metadata as well, instead of having to put bug names as test names for example.

I wouldn't be opposed to implementing something along these lines to make a particular use case easier, but I'm hesitant to implement this in a way that means people can't use pytest's regular lookup mechanisms as well. The more specialised our test codebase is, the harder it is for people to pick up on what's happening.

Let me reshuffle this test so it's not repetitively named and we can see how we feel about it.

As the first test of this SRU cycle, this also introduces the
sru_2020_11 mark to allow us to easily identify the set of tests
generated for this SRU.
@OddBloke
Copy link
Collaborator Author

OddBloke commented Nov 20, 2020

So thinking about this: testing for a single bug could have multiple test functions and even multiple test classes, so I think the unit which makes most sense to name after the bug is the test file. I've just pushed up a very simple change:

--- a/tests/integration_tests/bugs/test_lp1900837.py
+++ b/tests/integration_tests/bugs/test_lp1900837.py
@@ -12,8 +12,8 @@ def _get_log_perms(client):
 
 
 @pytest.mark.sru_2020_11
-class TestLp1900836:
-    def test_lp1900836(self, client):
+class TestLogPermissionsNotResetOnReboot:
+    def test_permissions_unchanged(self, client):
         # Confirm that the current permissions aren't 600
         assert "644" == _get_log_perms(client)

This gives output that looks like this (just including the output lines which exhibit the test name):

$ pytest --log-cli-level=INFO tests/integration_tests -k 1900837
...
tests/integration_tests/bugs/test_lp1900837.py::TestLogPermissionsNotResetOnReboot::test_permissions_unchanged 
...
========================================= FAILURES =========================================
______________ TestLogPermissionsNotResetOnReboot.test_permissions_unchanged _______________

self = <test_lp1900837.TestLogPermissionsNotResetOnReboot object at 0x7fec3f571190>
client = <tests.integration_tests.instances.IntegrationLxdContainerInstance object at 0x7fec3f63c1f0>

    def test_permissions_unchanged(self, client):
        # Confirm that the current permissions aren't 600
        assert "644" == _get_log_perms(client)
    
        # Set permissions to 600 and confirm our assertion passes pre-reboot
        client.execute("chmod 600 /var/log/cloud-init.log")
        assert "600" == _get_log_perms(client)
    
        # Reboot
        client.instance.restart()
    
        # Check that permissions are not reset on reboot
>       assert "600" == _get_log_perms(client)
E       AssertionError: assert '600' == '644'
E         - 644
E         + 600

tests/integration_tests/bugs/test_lp1900837.py:28: AssertionError
...
================================= short test summary info ==================================
FAILED tests/integration_tests/bugs/test_lp1900837.py::TestLogPermissionsNotResetOnReboot::test_permissions_unchanged
====================== 1 failed, 56 deselected, 2 warnings in 42.38s =======================

I think this looks pretty reasonable: it's clear what the specific failure is (test_permissions_unchanged failed in TestLogPermissionsNotResetOnReboot so I can infer that log permissions changed on a reboot) and, as demonstrated, I can still select the test by specifying the bug number with -k.

What this doesn't really do a good job of is making it easy to find tests which (e.g.) already exercise log permissions based on examining filenames (which, of course, is a common way of looking things up from an editor). This is a gap, though passing --collect-only to pytest will emit all parts of a test's "name", so there is still a way of seeing a summary of all the tests we have.

It also doesn't give us a way of adding SRU tests which can be selected in this way to existing test files (consider if we're fixing a bug in the write_files module: that test would sit most naturally in tests/integration_tests/modules/test_write_files.py) without including the bug number in the class or method name. I don't think this is too painful for such tests: both TestLp1900837.test_log_permissions_not_reset_on_reboot and TestLp1900837LogPermissions.test_permissions_unchanged_on_reboot would be reasonable names for this test, and don't lose much information about what the test is doing.

It certainly doesn't give us a way of indicating that an existing test (presumably with modifications) tests a particular bug: we would have to rename such a test to include the bug number. It also doesn't give us a way of specifying that a particular test instance of a parameterised test is related to a particular bug: using a mark doesn't help us in this case, though, because our pytest support matrix means we aren't able to apply marks to test instances anyway (search for "pytest.param" in the HACKING doc).

I propose that we move forward with this pattern for this SRU: the hardcoded SRU mark name and using filenames to indicate the relevant bug with the expectation that test classes/methods are to be descriptive of the test being performed. When we retrospect on this SRU, we should spend some time thinking about what interfaces could have made it easier: once we have multiple tests, I think it'll be easier to determine what the right pattern to apply is (and the cost of rearranging marks is negligible, so it doesn't matter if we don't Get This Right up-front).

What do folks think? (I'm happy to be convinced otherwise!)

@TheRealFalcon
Copy link
Member

I think you did a good job explaining all the pros and cons of this proposal. In my head I was thinking a few "but what about..." and you mentioned them in the next paragraph.

I agree that it's a workable solution for now, and that we should also come back to this and see if we can think of a better solution.

@OddBloke OddBloke merged commit 5d4a9a4 into canonical:master Nov 20, 2020
@OddBloke OddBloke deleted the tests branch November 20, 2020 19:12
@blackboxsw
Copy link
Collaborator

@blackboxsw learns to use cut-n-paste and make review comments.

--- a/tests/integration_tests/bugs/test_lp1900837.py
+++ b/tests/integration_tests/bugs/test_lp1900837.py

...

tests/integration_tests/bugs/test_lp1900837.py::TestLogPermissionsNotResetOnReboot::test_permissions_unchanged
FAILED tests/integration_tests/bugs/test_lp1900837.py::TestLogPermissionsNotResetOnReboot::test_permissions_unchanged

Given that we are categorizing test modules in a bugs subdirectory anyway, I feel organizing those modules by bug_id in the filename makes a lot of sense to me (and makes that module searchable via keywords) +2

... though passing --collect-only to pytest will emit all parts of a test's "name", so there is still a way of seeing a summary of all the tests we have.

TIL: Thank you for that --collect-only reference. I kept fumbling around looking for a dry-run equivalent and not seeing that jump out in docs. This meets my need for bugid searchable and human-readable context of a test at runtime.

It also doesn't give us a way of adding SRU tests which can be selected in this way to existing test files (consider if we're fixing a bug in the write_files module: that test would sit most naturally in tests/integration_tests/modules/test_write_files.py) without including the bug number in the class or method name. I don't think this is too painful for such tests: both TestLp1900837.test_log_permissions_not_reset_on_reboot and TestLp1900837LogPermissions.test_permissions_unchanged_on_reboot would be reasonable names for this test, and don't lose much information about what the test is doing.

I recognize that encoding the bugid in classnames/testnames diminishes the character real-estate we have available for naming tests in a consructive/readable manor and adds "cost" to the development or such tests, but I think that may be worth the cost for tests written outside of the "bugs" subdirectory if we want to get searchable coverage in the future for either SRUing specific cherry-picks or pytest.mark(ing) the set of SRU bugs.

I propose that we move forward with this pattern for this SRU: the hardcoded SRU mark name and using filenames to indicate the relevant bug with the expectation that test classes/methods are to be descriptive of the test being performed. When we retrospect on this SRU, we should spend some time thinking about what interfaces could have made it easier: once we have multiple tests, I think it'll be easier to determine what the right pattern to apply is (and the cost of rearranging marks is negligible, so it doesn't matter if we don't Get This Right up-front).

This makes perfect sense. go for it. well need more use cases which we will get this SRU to properly define needs here

This was referenced May 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants