Merge ~ubuntu-release/britney/+git/britney2-ubuntu:sil2100/cloud-only-run-once into ~ubuntu-release/britney/+git/britney2-ubuntu:sil2100/security-britney

Proposed by Łukasz Zemczak
Status: Merged
Merged at revision: 85c6716af6355f9f034d6dc0e656606f3eada113
Proposed branch: ~ubuntu-release/britney/+git/britney2-ubuntu:sil2100/cloud-only-run-once
Merge into: ~ubuntu-release/britney/+git/britney2-ubuntu:sil2100/security-britney
Diff against target: 277 lines (+145/-7)
3 files modified
britney.conf (+3/-0)
britney2/policies/cloud.py (+90/-5)
tests/test_cloud.py (+52/-2)
Reviewer Review Type Date Requested Status
John Chittum (community) Approve
Ubuntu Release Team Pending
Review via email: mp+437274@code.launchpad.net

Commit message

Save state file in-between runs to mark which packages have already been tested on the given clouds.

Description of the change

Save state file in-between runs to mark which packages have already been tested on the given clouds. Without this, every britney invocation would re-run the same tests over and over again, even though the packages did not change.

We could use some of the ADT databases for this instead of our own state file, but this way we can function regardless of whether autopkgtest policies are enabled.

I'd like to add more unit-testing, but had no time as I should be AFK right now.

To post a comment you must log in.
Revision history for this message
John Chittum (jchittum) wrote :

from a general code level, I don't see any issues.

review: Approve
Revision history for this message
Łukasz Zemczak (sil2100) wrote :

Thanks! I'll merge it + check Aleksa's changes on top.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/britney.conf b/britney.conf
index 40fd523..d27e968 100644
--- a/britney.conf
+++ b/britney.conf
@@ -143,6 +143,9 @@ CLOUD_ENABLE = no
143# A directory to store Cloud test results and logs. Is created at the start of143# A directory to store Cloud test results and logs. Is created at the start of
144# each policy run and deleted after test results are parsed.144# each policy run and deleted after test results are parsed.
145CLOUD_WORK_DIR = cloud_tests145CLOUD_WORK_DIR = cloud_tests
146# Path to a persistent state file, not to re-run cloud tests for packages that
147# have already been tested.
148CLOUD_STATE_FILE = cloud_state
146# Who to notify regarding test failures149# Who to notify regarding test failures
147CLOUD_FAILURE_EMAILS = cpc@canonical.com150CLOUD_FAILURE_EMAILS = cpc@canonical.com
148# Who to notify regarding test errors151# Who to notify regarding test errors
diff --git a/britney2/policies/cloud.py b/britney2/policies/cloud.py
index b19fbae..41da821 100644
--- a/britney2/policies/cloud.py
+++ b/britney2/policies/cloud.py
@@ -50,6 +50,7 @@ Regards, Ubuntu Release Team.
50"""50"""
51class CloudPolicy(BasePolicy):51class CloudPolicy(BasePolicy):
52 PACKAGE_SET_FILE = "cloud_package_set"52 PACKAGE_SET_FILE = "cloud_package_set"
53 STATE_FILE = "cloud_state"
53 DEFAULT_EMAILS = ["cpc@canonical.com"]54 DEFAULT_EMAILS = ["cpc@canonical.com"]
54 TEST_LOG_FILE = "CTF.log"55 TEST_LOG_FILE = "CTF.log"
5556
@@ -68,6 +69,9 @@ class CloudPolicy(BasePolicy):
68 self.work_dir = getattr(self.options, "cloud_work_dir", "cloud_tests")69 self.work_dir = getattr(self.options, "cloud_work_dir", "cloud_tests")
69 self.failure_emails = getattr(self.options, "cloud_failure_emails", self.DEFAULT_EMAILS)70 self.failure_emails = getattr(self.options, "cloud_failure_emails", self.DEFAULT_EMAILS)
70 self.error_emails = getattr(self.options, "cloud_error_emails", self.DEFAULT_EMAILS)71 self.error_emails = getattr(self.options, "cloud_error_emails", self.DEFAULT_EMAILS)
72 self.state_filename = getattr(self.options, "cloud_state_file", self.STATE_FILE)
73
74 self.state = {}
7175
72 adt_ppas = getattr(self.options, "adt_ppas", "")76 adt_ppas = getattr(self.options, "adt_ppas", "")
73 if not isinstance(adt_ppas, list):77 if not isinstance(adt_ppas, list):
@@ -83,11 +87,13 @@ class CloudPolicy(BasePolicy):
8387
84 self.failures = {}88 self.failures = {}
85 self.errors = {}89 self.errors = {}
90 self.email_needed = False
8691
87 def initialise(self, britney):92 def initialise(self, britney):
88 super().initialise(britney)93 super().initialise(britney)
8994
90 self.package_set = self._retrieve_cloud_package_set_for_series(self.options.series)95 self.package_set = self._retrieve_cloud_package_set_for_series(self.options.series)
96 self._load_state()
9197
92 def apply_src_policy_impl(self, policy_info, item, source_data_tdist, source_data_srcdist, excuse):98 def apply_src_policy_impl(self, policy_info, item, source_data_tdist, source_data_srcdist, excuse):
93 if item.package not in self.package_set:99 if item.package not in self.package_set:
@@ -103,10 +109,12 @@ class CloudPolicy(BasePolicy):
103 self.failures = {}109 self.failures = {}
104 self.errors = {}110 self.errors = {}
105111
106 self._run_cloud_tests(item.package, self.options.series, self.sources, self.source_type)112 self._run_cloud_tests(item.package, source_data_srcdist.version, self.options.series,
113 self.sources, self.source_type)
107114
108 if len(self.failures) > 0 or len(self.errors) > 0:115 if len(self.failures) > 0 or len(self.errors) > 0:
109 self._send_emails_if_needed(item.package, source_data_srcdist.version, self.options.series)116 if self.email_needed:
117 self._send_emails_if_needed(item.package, source_data_srcdist.version, self.options.series)
110118
111 self._cleanup_work_directory()119 self._cleanup_work_directory()
112 verdict = PolicyVerdict.REJECTED_PERMANENTLY120 verdict = PolicyVerdict.REJECTED_PERMANENTLY
@@ -117,6 +125,76 @@ class CloudPolicy(BasePolicy):
117 self._cleanup_work_directory()125 self._cleanup_work_directory()
118 return PolicyVerdict.PASS126 return PolicyVerdict.PASS
119127
128 def _mark_tests_run(self, package, version, series, source_type, cloud):
129 """Mark the selected package version as already tested.
130 This takes which cloud we're testing into consideration.
131
132 :param package The name of the package to test
133 :param version Version of the package
134 :param series The Ubuntu codename for the series (e.g. jammy)
135 :param source_type Either 'archive' or 'ppa'
136 :param cloud The name of the cloud being tested (e.g. azure)
137 """
138 if cloud not in self.state:
139 self.state[cloud] = {}
140 if source_type not in self.state[cloud]:
141 self.state[cloud][source_type] = {}
142 if series not in self.state[cloud][source_type]:
143 self.state[cloud][source_type][series] = {}
144 self.state[cloud][source_type][series][package] = {
145 "version": version,
146 "failures": len(self.failures),
147 "errors": len(self.errors)
148 }
149
150 self.email_needed = True
151
152 self._save_state()
153
154 def _check_if_tests_run(self, package, version, series, source_type, cloud):
155 """Check if tests were already run for the given package version.
156 This takes which cloud we're testing into consideration.
157
158 :param package The name of the package to test
159 :param version Version of the package
160 :param series The Ubuntu codename for the series (e.g. jammy)
161 :param source_type Either 'archive' or 'ppa'
162 :param cloud The name of the cloud being tested (e.g. azure)
163 """
164 try:
165 return self.state[cloud][source_type][series][package]["version"] == version
166 except KeyError:
167 return False
168
169 def _set_previous_failure_and_error(self, package, version, series, source_type, cloud):
170 """Sets the failures and errors from the previous run.
171 This takes which cloud we're testing into consideration.
172
173 :param package The name of the package to test
174 :param version Version of the package
175 :param series The Ubuntu codename for the series (e.g. jammy)
176 :param source_type Either 'archive' or 'ppa'
177 :param cloud The name of the cloud being tested (e.g. azure)
178 """
179 if self.state[cloud][source_type][series][package]["failures"] > 0:
180 self.failures[cloud] = {}
181
182 if self.state[cloud][source_type][series][package]["errors"] > 0:
183 self.errors[cloud] = {}
184
185 def _load_state(self):
186 """Load the save state of which packages have already been tested."""
187 if os.path.exists(self.state_filename):
188 with open(self.state_filename, encoding="utf-8") as data:
189 self.state = json.load(data)
190 self.logger.info("Loaded cloud policy state file %s" % self.state_filename)
191
192 def _save_state(self):
193 """Save which packages have already been tested."""
194 with open(self.state_filename, "w", encoding="utf-8") as data:
195 json.dump(self.state, data)
196 self.logger.info("Saved cloud policy state file %s" % self.state_filename)
197
120 def _retrieve_cloud_package_set_for_series(self, series):198 def _retrieve_cloud_package_set_for_series(self, series):
121 """Retrieves a set of packages for the given series in which cloud199 """Retrieves a set of packages for the given series in which cloud
122 tests should be run.200 tests should be run.
@@ -134,16 +212,17 @@ class CloudPolicy(BasePolicy):
134212
135 return package_set213 return package_set
136214
137 def _run_cloud_tests(self, package, series, sources, source_type):215 def _run_cloud_tests(self, package, version, series, sources, source_type):
138 """Runs any cloud tests for the given package.216 """Runs any cloud tests for the given package.
139 Nothing is returned but test failures and errors are stored in instance variables.217 Nothing is returned but test failures and errors are stored in instance variables.
140218
141 :param package The name of the package to test219 :param package The name of the package to test
220 :param version Version of the package
142 :param series The Ubuntu codename for the series (e.g. jammy)221 :param series The Ubuntu codename for the series (e.g. jammy)
143 :param sources List of sources where the package should be installed from (e.g. [proposed] or PPAs)222 :param sources List of sources where the package should be installed from (e.g. [proposed] or PPAs)
144 :param source_type Either 'archive' or 'ppa'223 :param source_type Either 'archive' or 'ppa'
145 """224 """
146 self._run_azure_tests(package, series, sources, source_type)225 self._run_azure_tests(package, version, series, sources, source_type)
147226
148 def _send_emails_if_needed(self, package, version, series):227 def _send_emails_if_needed(self, package, version, series):
149 """Sends email(s) if there are test failures and/or errors228 """Sends email(s) if there are test failures and/or errors
@@ -168,14 +247,19 @@ class CloudPolicy(BasePolicy):
168 self.logger.info("Cloud Policy: Sending error email for {}, to {}".format(package, emails))247 self.logger.info("Cloud Policy: Sending error email for {}, to {}".format(package, emails))
169 self._send_email(emails, message)248 self._send_email(emails, message)
170249
171 def _run_azure_tests(self, package, series, sources, source_type):250 def _run_azure_tests(self, package, version, series, sources, source_type):
172 """Runs Azure's required package tests.251 """Runs Azure's required package tests.
173252
174 :param package The name of the package to test253 :param package The name of the package to test
254 :param version Version of the package
175 :param series The Ubuntu codename for the series (e.g. jammy)255 :param series The Ubuntu codename for the series (e.g. jammy)
176 :param sources List of sources where the package should be installed from (e.g. [proposed] or PPAs)256 :param sources List of sources where the package should be installed from (e.g. [proposed] or PPAs)
177 :param source_type Either 'archive' or 'ppa'257 :param source_type Either 'archive' or 'ppa'
178 """258 """
259 if self._check_if_tests_run(package, version, series, source_type, "azure"):
260 self._set_previous_failure_and_error(package, version, series, source_type, "azure")
261 return
262
179 urn = self._retrieve_urn(series)263 urn = self._retrieve_urn(series)
180264
181 self.logger.info("Cloud Policy: Running Azure tests for: {} in {}".format(package, series))265 self.logger.info("Cloud Policy: Running Azure tests for: {} in {}".format(package, series))
@@ -204,6 +288,7 @@ class CloudPolicy(BasePolicy):
204 results_file_paths = self._find_results_files(r"TEST-NetworkTests-[0-9]*.xml")288 results_file_paths = self._find_results_files(r"TEST-NetworkTests-[0-9]*.xml")
205 self._parse_xunit_test_results("Azure", results_file_paths)289 self._parse_xunit_test_results("Azure", results_file_paths)
206 self._store_extra_test_result_info(self, package)290 self._store_extra_test_result_info(self, package)
291 self._mark_tests_run(package, version, series, source_type, "azure")
207292
208 def _retrieve_urn(self, series):293 def _retrieve_urn(self, series):
209 """Retrieves an URN from the configuration options based on series.294 """Retrieves an URN from the configuration options based on series.
diff --git a/tests/test_cloud.py b/tests/test_cloud.py
index bbd8595..2e859e7 100644
--- a/tests/test_cloud.py
+++ b/tests/test_cloud.py
@@ -7,6 +7,7 @@
7# (at your option) any later version.7# (at your option) any later version.
88
9import os9import os
10import json
10import pathlib11import pathlib
11import sys12import sys
12from types import SimpleNamespace13from types import SimpleNamespace
@@ -35,7 +36,8 @@ class T(unittest.TestCase):
35 verbose = False,36 verbose = False,
36 cloud_source = "zazzy-proposed",37 cloud_source = "zazzy-proposed",
37 cloud_source_type = "archive",38 cloud_source_type = "archive",
38 cloud_azure_zazzy_urn = "fake-urn-value"39 cloud_azure_zazzy_urn = "fake-urn-value",
40 cloud_state_file = "/tmp/test_state.json"
39 )41 )
40 self.policy = CloudPolicy(self.fake_options, {})42 self.policy = CloudPolicy(self.fake_options, {})
41 self.policy._setup_work_directory()43 self.policy._setup_work_directory()
@@ -43,6 +45,54 @@ class T(unittest.TestCase):
43 def tearDown(self):45 def tearDown(self):
44 self.policy._cleanup_work_directory()46 self.policy._cleanup_work_directory()
4547
48 @patch("britney2.policies.cloud.CloudPolicy._store_extra_test_result_info")
49 @patch("britney2.policies.cloud.CloudPolicy._parse_xunit_test_results")
50 @patch("subprocess.run")
51 def test_run_cloud_tests_state_handling(self, mock_run, mock_xunit, mock_extra):
52 """Cloud tests should save state and not re-run tests for packages
53 already tested."""
54 expected_state = {
55 "azure": {
56 "archive": {
57 "zazzy": {
58 "chromium-browser": {
59 "version": "55.0",
60 "failures": 0,
61 "errors": 0,
62 }
63 }
64 }
65 }
66 }
67 with open(self.policy.options.cloud_state_file, "w") as file:
68 json.dump(expected_state, file)
69 self.policy._load_state()
70
71 # Package already tested, no tests should run
72 self.policy._run_cloud_tests("chromium-browser", "55.0", "zazzy", ["proposed"], "archive")
73 self.assertDictEqual(expected_state, self.policy.state)
74 mock_run.assert_not_called()
75
76 # A new package appears, tests should run
77 expected_state["azure"]["archive"]["zazzy"]["hello"] = {
78 "version": "2.10",
79 "failures": 0,
80 "errors": 0,
81 }
82 self.policy._run_cloud_tests("hello", "2.10", "zazzy", ["proposed"], "archive")
83 self.assertDictEqual(expected_state, self.policy.state)
84 mock_run.assert_called()
85
86 # A new version of existing package, tests should run
87 expected_state["azure"]["archive"]["zazzy"]["chromium-browser"]["version"] = "55.1"
88 self.policy._run_cloud_tests("chromium-browser", "55.1", "zazzy", ["proposed"], "archive")
89 self.assertDictEqual(expected_state, self.policy.state)
90 self.assertEqual(mock_run.call_count, 2)
91
92 # Make sure the state was saved properly
93 with open(self.policy.options.cloud_state_file, "r") as file:
94 self.assertDictEqual(expected_state, json.load(file))
95
46 @patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests")96 @patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests")
47 def test_run_cloud_tests_called_for_package_in_manifest(self, mock_run):97 def test_run_cloud_tests_called_for_package_in_manifest(self, mock_run):
48 """Cloud tests should run for a package in the cloud package set.98 """Cloud tests should run for a package in the cloud package set.
@@ -55,7 +105,7 @@ class T(unittest.TestCase):
55 )105 )
56106
57 mock_run.assert_called_once_with(107 mock_run.assert_called_once_with(
58 "chromium-browser", "jammy", ["proposed"], "archive"108 "chromium-browser", "55.0", "jammy", ["proposed"], "archive"
59 )109 )
60110
61 @patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests")111 @patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests")

Subscribers

People subscribed via source and target branches