Merge ~ubuntu-release/britney/+git/britney2-ubuntu:sil2100/cloud-only-run-once into ~ubuntu-release/britney/+git/britney2-ubuntu:sil2100/security-britney
- Git
- lp:~ubuntu-release/britney/+git/britney2-ubuntu
- sil2100/cloud-only-run-once
- Merge into 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) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
John Chittum (community) | Approve | ||
Ubuntu Release Team | Pending | ||
Review via email:
|
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
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Ł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
1 | diff --git a/britney.conf b/britney.conf | |||
2 | index 40fd523..d27e968 100644 | |||
3 | --- a/britney.conf | |||
4 | +++ b/britney.conf | |||
5 | @@ -143,6 +143,9 @@ CLOUD_ENABLE = no | |||
6 | 143 | # A directory to store Cloud test results and logs. Is created at the start of | 143 | # A directory to store Cloud test results and logs. Is created at the start of |
7 | 144 | # each policy run and deleted after test results are parsed. | 144 | # each policy run and deleted after test results are parsed. |
8 | 145 | CLOUD_WORK_DIR = cloud_tests | 145 | CLOUD_WORK_DIR = cloud_tests |
9 | 146 | # Path to a persistent state file, not to re-run cloud tests for packages that | ||
10 | 147 | # have already been tested. | ||
11 | 148 | CLOUD_STATE_FILE = cloud_state | ||
12 | 146 | # Who to notify regarding test failures | 149 | # Who to notify regarding test failures |
13 | 147 | CLOUD_FAILURE_EMAILS = cpc@canonical.com | 150 | CLOUD_FAILURE_EMAILS = cpc@canonical.com |
14 | 148 | # Who to notify regarding test errors | 151 | # Who to notify regarding test errors |
15 | diff --git a/britney2/policies/cloud.py b/britney2/policies/cloud.py | |||
16 | index b19fbae..41da821 100644 | |||
17 | --- a/britney2/policies/cloud.py | |||
18 | +++ b/britney2/policies/cloud.py | |||
19 | @@ -50,6 +50,7 @@ Regards, Ubuntu Release Team. | |||
20 | 50 | """ | 50 | """ |
21 | 51 | class CloudPolicy(BasePolicy): | 51 | class CloudPolicy(BasePolicy): |
22 | 52 | PACKAGE_SET_FILE = "cloud_package_set" | 52 | PACKAGE_SET_FILE = "cloud_package_set" |
23 | 53 | STATE_FILE = "cloud_state" | ||
24 | 53 | DEFAULT_EMAILS = ["cpc@canonical.com"] | 54 | DEFAULT_EMAILS = ["cpc@canonical.com"] |
25 | 54 | TEST_LOG_FILE = "CTF.log" | 55 | TEST_LOG_FILE = "CTF.log" |
26 | 55 | 56 | ||
27 | @@ -68,6 +69,9 @@ class CloudPolicy(BasePolicy): | |||
28 | 68 | self.work_dir = getattr(self.options, "cloud_work_dir", "cloud_tests") | 69 | self.work_dir = getattr(self.options, "cloud_work_dir", "cloud_tests") |
29 | 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) |
30 | 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) |
31 | 72 | self.state_filename = getattr(self.options, "cloud_state_file", self.STATE_FILE) | ||
32 | 73 | |||
33 | 74 | self.state = {} | ||
34 | 71 | 75 | ||
35 | 72 | adt_ppas = getattr(self.options, "adt_ppas", "") | 76 | adt_ppas = getattr(self.options, "adt_ppas", "") |
36 | 73 | if not isinstance(adt_ppas, list): | 77 | if not isinstance(adt_ppas, list): |
37 | @@ -83,11 +87,13 @@ class CloudPolicy(BasePolicy): | |||
38 | 83 | 87 | ||
39 | 84 | self.failures = {} | 88 | self.failures = {} |
40 | 85 | self.errors = {} | 89 | self.errors = {} |
41 | 90 | self.email_needed = False | ||
42 | 86 | 91 | ||
43 | 87 | def initialise(self, britney): | 92 | def initialise(self, britney): |
44 | 88 | super().initialise(britney) | 93 | super().initialise(britney) |
45 | 89 | 94 | ||
46 | 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) |
47 | 96 | self._load_state() | ||
48 | 91 | 97 | ||
49 | 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): |
50 | 93 | if item.package not in self.package_set: | 99 | if item.package not in self.package_set: |
51 | @@ -103,10 +109,12 @@ class CloudPolicy(BasePolicy): | |||
52 | 103 | self.failures = {} | 109 | self.failures = {} |
53 | 104 | self.errors = {} | 110 | self.errors = {} |
54 | 105 | 111 | ||
56 | 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, |
57 | 113 | self.sources, self.source_type) | ||
58 | 107 | 114 | ||
59 | 108 | if len(self.failures) > 0 or len(self.errors) > 0: | 115 | if len(self.failures) > 0 or len(self.errors) > 0: |
61 | 109 | self._send_emails_if_needed(item.package, source_data_srcdist.version, self.options.series) | 116 | if self.email_needed: |
62 | 117 | self._send_emails_if_needed(item.package, source_data_srcdist.version, self.options.series) | ||
63 | 110 | 118 | ||
64 | 111 | self._cleanup_work_directory() | 119 | self._cleanup_work_directory() |
65 | 112 | verdict = PolicyVerdict.REJECTED_PERMANENTLY | 120 | verdict = PolicyVerdict.REJECTED_PERMANENTLY |
66 | @@ -117,6 +125,76 @@ class CloudPolicy(BasePolicy): | |||
67 | 117 | self._cleanup_work_directory() | 125 | self._cleanup_work_directory() |
68 | 118 | return PolicyVerdict.PASS | 126 | return PolicyVerdict.PASS |
69 | 119 | 127 | ||
70 | 128 | def _mark_tests_run(self, package, version, series, source_type, cloud): | ||
71 | 129 | """Mark the selected package version as already tested. | ||
72 | 130 | This takes which cloud we're testing into consideration. | ||
73 | 131 | |||
74 | 132 | :param package The name of the package to test | ||
75 | 133 | :param version Version of the package | ||
76 | 134 | :param series The Ubuntu codename for the series (e.g. jammy) | ||
77 | 135 | :param source_type Either 'archive' or 'ppa' | ||
78 | 136 | :param cloud The name of the cloud being tested (e.g. azure) | ||
79 | 137 | """ | ||
80 | 138 | if cloud not in self.state: | ||
81 | 139 | self.state[cloud] = {} | ||
82 | 140 | if source_type not in self.state[cloud]: | ||
83 | 141 | self.state[cloud][source_type] = {} | ||
84 | 142 | if series not in self.state[cloud][source_type]: | ||
85 | 143 | self.state[cloud][source_type][series] = {} | ||
86 | 144 | self.state[cloud][source_type][series][package] = { | ||
87 | 145 | "version": version, | ||
88 | 146 | "failures": len(self.failures), | ||
89 | 147 | "errors": len(self.errors) | ||
90 | 148 | } | ||
91 | 149 | |||
92 | 150 | self.email_needed = True | ||
93 | 151 | |||
94 | 152 | self._save_state() | ||
95 | 153 | |||
96 | 154 | def _check_if_tests_run(self, package, version, series, source_type, cloud): | ||
97 | 155 | """Check if tests were already run for the given package version. | ||
98 | 156 | This takes which cloud we're testing into consideration. | ||
99 | 157 | |||
100 | 158 | :param package The name of the package to test | ||
101 | 159 | :param version Version of the package | ||
102 | 160 | :param series The Ubuntu codename for the series (e.g. jammy) | ||
103 | 161 | :param source_type Either 'archive' or 'ppa' | ||
104 | 162 | :param cloud The name of the cloud being tested (e.g. azure) | ||
105 | 163 | """ | ||
106 | 164 | try: | ||
107 | 165 | return self.state[cloud][source_type][series][package]["version"] == version | ||
108 | 166 | except KeyError: | ||
109 | 167 | return False | ||
110 | 168 | |||
111 | 169 | def _set_previous_failure_and_error(self, package, version, series, source_type, cloud): | ||
112 | 170 | """Sets the failures and errors from the previous run. | ||
113 | 171 | This takes which cloud we're testing into consideration. | ||
114 | 172 | |||
115 | 173 | :param package The name of the package to test | ||
116 | 174 | :param version Version of the package | ||
117 | 175 | :param series The Ubuntu codename for the series (e.g. jammy) | ||
118 | 176 | :param source_type Either 'archive' or 'ppa' | ||
119 | 177 | :param cloud The name of the cloud being tested (e.g. azure) | ||
120 | 178 | """ | ||
121 | 179 | if self.state[cloud][source_type][series][package]["failures"] > 0: | ||
122 | 180 | self.failures[cloud] = {} | ||
123 | 181 | |||
124 | 182 | if self.state[cloud][source_type][series][package]["errors"] > 0: | ||
125 | 183 | self.errors[cloud] = {} | ||
126 | 184 | |||
127 | 185 | def _load_state(self): | ||
128 | 186 | """Load the save state of which packages have already been tested.""" | ||
129 | 187 | if os.path.exists(self.state_filename): | ||
130 | 188 | with open(self.state_filename, encoding="utf-8") as data: | ||
131 | 189 | self.state = json.load(data) | ||
132 | 190 | self.logger.info("Loaded cloud policy state file %s" % self.state_filename) | ||
133 | 191 | |||
134 | 192 | def _save_state(self): | ||
135 | 193 | """Save which packages have already been tested.""" | ||
136 | 194 | with open(self.state_filename, "w", encoding="utf-8") as data: | ||
137 | 195 | json.dump(self.state, data) | ||
138 | 196 | self.logger.info("Saved cloud policy state file %s" % self.state_filename) | ||
139 | 197 | |||
140 | 120 | def _retrieve_cloud_package_set_for_series(self, series): | 198 | def _retrieve_cloud_package_set_for_series(self, series): |
141 | 121 | """Retrieves a set of packages for the given series in which cloud | 199 | """Retrieves a set of packages for the given series in which cloud |
142 | 122 | tests should be run. | 200 | tests should be run. |
143 | @@ -134,16 +212,17 @@ class CloudPolicy(BasePolicy): | |||
144 | 134 | 212 | ||
145 | 135 | return package_set | 213 | return package_set |
146 | 136 | 214 | ||
148 | 137 | def _run_cloud_tests(self, package, series, sources, source_type): | 215 | def _run_cloud_tests(self, package, version, series, sources, source_type): |
149 | 138 | """Runs any cloud tests for the given package. | 216 | """Runs any cloud tests for the given package. |
150 | 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. |
151 | 140 | 218 | ||
152 | 141 | :param package The name of the package to test | 219 | :param package The name of the package to test |
153 | 220 | :param version Version of the package | ||
154 | 142 | :param series The Ubuntu codename for the series (e.g. jammy) | 221 | :param series The Ubuntu codename for the series (e.g. jammy) |
155 | 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) |
156 | 144 | :param source_type Either 'archive' or 'ppa' | 223 | :param source_type Either 'archive' or 'ppa' |
157 | 145 | """ | 224 | """ |
159 | 146 | self._run_azure_tests(package, series, sources, source_type) | 225 | self._run_azure_tests(package, version, series, sources, source_type) |
160 | 147 | 226 | ||
161 | 148 | def _send_emails_if_needed(self, package, version, series): | 227 | def _send_emails_if_needed(self, package, version, series): |
162 | 149 | """Sends email(s) if there are test failures and/or errors | 228 | """Sends email(s) if there are test failures and/or errors |
163 | @@ -168,14 +247,19 @@ class CloudPolicy(BasePolicy): | |||
164 | 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)) |
165 | 169 | self._send_email(emails, message) | 248 | self._send_email(emails, message) |
166 | 170 | 249 | ||
168 | 171 | def _run_azure_tests(self, package, series, sources, source_type): | 250 | def _run_azure_tests(self, package, version, series, sources, source_type): |
169 | 172 | """Runs Azure's required package tests. | 251 | """Runs Azure's required package tests. |
170 | 173 | 252 | ||
171 | 174 | :param package The name of the package to test | 253 | :param package The name of the package to test |
172 | 254 | :param version Version of the package | ||
173 | 175 | :param series The Ubuntu codename for the series (e.g. jammy) | 255 | :param series The Ubuntu codename for the series (e.g. jammy) |
174 | 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) |
175 | 177 | :param source_type Either 'archive' or 'ppa' | 257 | :param source_type Either 'archive' or 'ppa' |
176 | 178 | """ | 258 | """ |
177 | 259 | if self._check_if_tests_run(package, version, series, source_type, "azure"): | ||
178 | 260 | self._set_previous_failure_and_error(package, version, series, source_type, "azure") | ||
179 | 261 | return | ||
180 | 262 | |||
181 | 179 | urn = self._retrieve_urn(series) | 263 | urn = self._retrieve_urn(series) |
182 | 180 | 264 | ||
183 | 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)) |
184 | @@ -204,6 +288,7 @@ class CloudPolicy(BasePolicy): | |||
185 | 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") |
186 | 205 | self._parse_xunit_test_results("Azure", results_file_paths) | 289 | self._parse_xunit_test_results("Azure", results_file_paths) |
187 | 206 | self._store_extra_test_result_info(self, package) | 290 | self._store_extra_test_result_info(self, package) |
188 | 291 | self._mark_tests_run(package, version, series, source_type, "azure") | ||
189 | 207 | 292 | ||
190 | 208 | def _retrieve_urn(self, series): | 293 | def _retrieve_urn(self, series): |
191 | 209 | """Retrieves an URN from the configuration options based on series. | 294 | """Retrieves an URN from the configuration options based on series. |
192 | diff --git a/tests/test_cloud.py b/tests/test_cloud.py | |||
193 | index bbd8595..2e859e7 100644 | |||
194 | --- a/tests/test_cloud.py | |||
195 | +++ b/tests/test_cloud.py | |||
196 | @@ -7,6 +7,7 @@ | |||
197 | 7 | # (at your option) any later version. | 7 | # (at your option) any later version. |
198 | 8 | 8 | ||
199 | 9 | import os | 9 | import os |
200 | 10 | import json | ||
201 | 10 | import pathlib | 11 | import pathlib |
202 | 11 | import sys | 12 | import sys |
203 | 12 | from types import SimpleNamespace | 13 | from types import SimpleNamespace |
204 | @@ -35,7 +36,8 @@ class T(unittest.TestCase): | |||
205 | 35 | verbose = False, | 36 | verbose = False, |
206 | 36 | cloud_source = "zazzy-proposed", | 37 | cloud_source = "zazzy-proposed", |
207 | 37 | cloud_source_type = "archive", | 38 | cloud_source_type = "archive", |
209 | 38 | cloud_azure_zazzy_urn = "fake-urn-value" | 39 | cloud_azure_zazzy_urn = "fake-urn-value", |
210 | 40 | cloud_state_file = "/tmp/test_state.json" | ||
211 | 39 | ) | 41 | ) |
212 | 40 | self.policy = CloudPolicy(self.fake_options, {}) | 42 | self.policy = CloudPolicy(self.fake_options, {}) |
213 | 41 | self.policy._setup_work_directory() | 43 | self.policy._setup_work_directory() |
214 | @@ -43,6 +45,54 @@ class T(unittest.TestCase): | |||
215 | 43 | def tearDown(self): | 45 | def tearDown(self): |
216 | 44 | self.policy._cleanup_work_directory() | 46 | self.policy._cleanup_work_directory() |
217 | 45 | 47 | ||
218 | 48 | @patch("britney2.policies.cloud.CloudPolicy._store_extra_test_result_info") | ||
219 | 49 | @patch("britney2.policies.cloud.CloudPolicy._parse_xunit_test_results") | ||
220 | 50 | @patch("subprocess.run") | ||
221 | 51 | def test_run_cloud_tests_state_handling(self, mock_run, mock_xunit, mock_extra): | ||
222 | 52 | """Cloud tests should save state and not re-run tests for packages | ||
223 | 53 | already tested.""" | ||
224 | 54 | expected_state = { | ||
225 | 55 | "azure": { | ||
226 | 56 | "archive": { | ||
227 | 57 | "zazzy": { | ||
228 | 58 | "chromium-browser": { | ||
229 | 59 | "version": "55.0", | ||
230 | 60 | "failures": 0, | ||
231 | 61 | "errors": 0, | ||
232 | 62 | } | ||
233 | 63 | } | ||
234 | 64 | } | ||
235 | 65 | } | ||
236 | 66 | } | ||
237 | 67 | with open(self.policy.options.cloud_state_file, "w") as file: | ||
238 | 68 | json.dump(expected_state, file) | ||
239 | 69 | self.policy._load_state() | ||
240 | 70 | |||
241 | 71 | # Package already tested, no tests should run | ||
242 | 72 | self.policy._run_cloud_tests("chromium-browser", "55.0", "zazzy", ["proposed"], "archive") | ||
243 | 73 | self.assertDictEqual(expected_state, self.policy.state) | ||
244 | 74 | mock_run.assert_not_called() | ||
245 | 75 | |||
246 | 76 | # A new package appears, tests should run | ||
247 | 77 | expected_state["azure"]["archive"]["zazzy"]["hello"] = { | ||
248 | 78 | "version": "2.10", | ||
249 | 79 | "failures": 0, | ||
250 | 80 | "errors": 0, | ||
251 | 81 | } | ||
252 | 82 | self.policy._run_cloud_tests("hello", "2.10", "zazzy", ["proposed"], "archive") | ||
253 | 83 | self.assertDictEqual(expected_state, self.policy.state) | ||
254 | 84 | mock_run.assert_called() | ||
255 | 85 | |||
256 | 86 | # A new version of existing package, tests should run | ||
257 | 87 | expected_state["azure"]["archive"]["zazzy"]["chromium-browser"]["version"] = "55.1" | ||
258 | 88 | self.policy._run_cloud_tests("chromium-browser", "55.1", "zazzy", ["proposed"], "archive") | ||
259 | 89 | self.assertDictEqual(expected_state, self.policy.state) | ||
260 | 90 | self.assertEqual(mock_run.call_count, 2) | ||
261 | 91 | |||
262 | 92 | # Make sure the state was saved properly | ||
263 | 93 | with open(self.policy.options.cloud_state_file, "r") as file: | ||
264 | 94 | self.assertDictEqual(expected_state, json.load(file)) | ||
265 | 95 | |||
266 | 46 | @patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests") | 96 | @patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests") |
267 | 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): |
268 | 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. |
269 | @@ -55,7 +105,7 @@ class T(unittest.TestCase): | |||
270 | 55 | ) | 105 | ) |
271 | 56 | 106 | ||
272 | 57 | mock_run.assert_called_once_with( | 107 | mock_run.assert_called_once_with( |
274 | 58 | "chromium-browser", "jammy", ["proposed"], "archive" | 108 | "chromium-browser", "55.0", "jammy", ["proposed"], "archive" |
275 | 59 | ) | 109 | ) |
276 | 60 | 110 | ||
277 | 61 | @patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests") | 111 | @patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests") |
from a general code level, I don't see any issues.