Merge lp:~abentley/ci-director/update-outcome-artifacts into lp:ci-director

Proposed by Aaron Bentley
Status: Merged
Merged at revision: 176
Proposed branch: lp:~abentley/ci-director/update-outcome-artifacts
Merge into: lp:ci-director
Diff against target: 240 lines (+101/-43)
4 files modified
cidirector/storage.py (+0/-3)
cidirector/tests/test_storage.py (+0/-39)
cidirector/tests/test_update_outcome.py (+69/-0)
cidirector/update_outcome.py (+32/-1)
To merge this branch: bzr merge lp:~abentley/ci-director/update-outcome-artifacts
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+298338@code.launchpad.net

Commit message

Upload artifacts from update-outcome, not ci-director.

Description of the change

This branch moves the artifact uploading from ci-director to update-outcome.

This makes sense, because the script is already producing output for s3 (although results.json is not being uploaded to s3 directly).

It means that artifacts will be uploaded for all completed ci-director builds, not just those for the current revision-build, supporting parallelism and re-testing.

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cidirector/storage.py'
2--- cidirector/storage.py 2016-06-24 14:25:39 +0000
3+++ cidirector/storage.py 2016-06-24 19:33:36 +0000
4@@ -644,9 +644,6 @@
5 return
6 old_status = self.get_status()
7 self.update_from_build_result(result)
8- if old_status == BUILDING and old_status != self.get_status():
9- self.archive_results(
10- jenkins, info, current_version, state_file.s3_storage)
11
12 def update_from_build_result(self, result):
13 """Update a provider test from its build result.
14
15=== modified file 'cidirector/tests/test_storage.py'
16--- cidirector/tests/test_storage.py 2016-06-24 14:07:03 +0000
17+++ cidirector/tests/test_storage.py 2016-06-24 19:33:36 +0000
18@@ -501,45 +501,6 @@
19 sfj.update_from_jenkins(jenkins, 2, sf)
20 self.assertEqual('succeeded', sfj._data['status'])
21
22- def test_update_from_jenkins_triggers_archiving_artifacts(self):
23- build_number = 1234
24- foo_job = 'foo'
25- foo_data = {
26- 'status': PENDING, 'failures': 0, 'failure-threshold': 3,
27- 'build_number': build_number, 'last_build': build_number,
28- 'building': True,
29- }
30- jobs = {foo_job: foo_data}
31- current_version = 4567
32- jenkins = FakeJenkins(jobs, current_version)
33- sf = StateFile()
34- sf._data['versions'][4567] = {}
35- sfj = StateFileJob(foo_job, foo_data, None)
36- with patch.object(sfj, 'archive_results') as mocked_archive_results:
37- self.assertEqual(BUILDING, sfj.get_status())
38- sfj.update_from_jenkins(jenkins, current_version, sf)
39- self.assertEqual(BUILDING, sfj.get_status())
40- self.assertEqual(0, len(mocked_archive_results.mock_calls))
41-
42- artifacts = [
43- {
44- 'fileName': 'some-product.tar.gz',
45- 'relativePath': 'build/some-product.tar.gz',
46- },
47- ]
48- jenkins.set_build_result(
49- foo_job, build_number, 'SUCCESS', artifacts)
50- sfj.update_from_jenkins(jenkins, current_version, sf)
51- self.assertEqual(SUCCEEDED, sfj.get_status())
52- mocked_archive_results.assert_called_once_with(jenkins, {
53- 'artifacts': artifacts,
54- 'building': True,
55- 'number': build_number,
56- 'result': SUCCESS,
57- 'timestamp': 0,
58- 'url': 'fakettp://fake.fake/job/foo/1234',
59- }, current_version, None)
60-
61 def test_update_from_jenkins_missing_build(self):
62 build_number = 1234
63 foo_job = 'foo'
64
65=== modified file 'cidirector/tests/test_update_outcome.py'
66--- cidirector/tests/test_update_outcome.py 2016-06-21 19:42:27 +0000
67+++ cidirector/tests/test_update_outcome.py 2016-06-24 19:33:36 +0000
68@@ -1,5 +1,6 @@
69 from calendar import timegm
70 from contextlib import contextmanager
71+from collections import OrderedDict
72 from copy import deepcopy
73 from datetime import (
74 date,
75@@ -7,9 +8,16 @@
76 timedelta,
77 )
78 import json
79+import logging
80 import os
81 from unittest import TestCase
82
83+from mock import (
84+ call,
85+ Mock,
86+ patch,
87+ )
88+
89 from cidirector.jobs import (
90 BUILD_REVISION,
91 FAILURE,
92@@ -36,6 +44,7 @@
93 OutcomeJobSource,
94 OutcomeState,
95 scan_new_builds,
96+ upload_artifacts,
97 )
98 from cidirector.utility import temp_dir
99 from cidirector.tests.test_cidirector import (
100@@ -825,3 +834,63 @@
101 state.write_mongo_data(32, rb_data, lbs_data, {})
102 mongo_name = state.mongo_filename(32)
103 self.assertFalse(os.path.lexists(mongo_name))
104+
105+
106+class TestUploadArtifacts(TestCase):
107+
108+ @contextmanager
109+ def cxt(self, aj_instance_mock):
110+ with patch('cidirector.update_outcome.ArtifactJob',
111+ spec=['__call__']) as aj_mock:
112+ aj_mock.return_value = aj_instance_mock
113+ yield aj_mock
114+
115+ @staticmethod
116+ def make_by_revision_build():
117+ build_110 = {'result': 'bar', 'number': 110}
118+ build_111 = {'result': 'qux', 'number': 111}
119+ build_120 = {'result': 'baz', 'number': 120}
120+ return OrderedDict([
121+ ('7081', {'foo': {'builds': {120: build_120}}}),
122+ ('7021', {'bar': {'builds': OrderedDict([
123+ (110, build_110), (111, build_111)])}}),
124+ ])
125+
126+ def test_upload_artifacts(self):
127+ dummy_jenkins = object()
128+ dummy_s3_storage = object()
129+ by_revision = self.make_by_revision_build()
130+ aj_instance_mock = Mock(spec=['archive_results'])
131+ with self.cxt(aj_instance_mock) as aj_mock:
132+ upload_artifacts(dummy_jenkins, by_revision, dummy_s3_storage)
133+ self.assertEqual([
134+ call('foo', logging.getLogger()),
135+ call('bar', logging.getLogger()),
136+ ], aj_mock.call_args_list)
137+ self.assertEqual([
138+ call(dummy_jenkins, by_revision['7081']['foo']['builds'][120],
139+ '7081', dummy_s3_storage),
140+ call(dummy_jenkins, by_revision['7021']['bar']['builds'][110],
141+ '7021', dummy_s3_storage),
142+ call(dummy_jenkins, by_revision['7021']['bar']['builds'][111],
143+ '7021', dummy_s3_storage),
144+ ], aj_instance_mock.archive_results.mock_calls)
145+
146+ def test_upload_artifacts_null_result(self):
147+ dummy_jenkins = object()
148+ dummy_s3_storage = object()
149+ by_revision = self.make_by_revision_build()
150+ by_revision['7021']['bar']['builds'][111]['result'] = None
151+ aj_instance_mock = Mock(spec=['archive_results'])
152+ with self.cxt(aj_instance_mock) as aj_mock:
153+ upload_artifacts(dummy_jenkins, by_revision, dummy_s3_storage)
154+ self.assertEqual([
155+ call('foo', logging.getLogger()),
156+ call('bar', logging.getLogger()),
157+ ], aj_mock.call_args_list)
158+ self.assertEqual([
159+ call(dummy_jenkins, by_revision['7081']['foo']['builds'][120],
160+ '7081', dummy_s3_storage),
161+ call(dummy_jenkins, by_revision['7021']['bar']['builds'][110],
162+ '7021', dummy_s3_storage),
163+ ], aj_instance_mock.archive_results.mock_calls)
164
165=== modified file 'cidirector/update_outcome.py'
166--- cidirector/update_outcome.py 2016-06-21 19:42:27 +0000
167+++ cidirector/update_outcome.py 2016-06-24 19:33:36 +0000
168@@ -28,6 +28,7 @@
169 SUCCESS,
170 )
171 from storage import (
172+ ArtifactJob,
173 BUILDING,
174 datetime_from_timestamp,
175 FAILED,
176@@ -41,6 +42,7 @@
177 date_str,
178 ensure_dir,
179 locked_open_rw,
180+ S3Storage,
181 )
182
183
184@@ -56,7 +58,7 @@
185 parser.add_argument('-v', '--verbose', help='Verbose info',
186 action='store_true')
187 parser.add_argument('--log-path', help='Where to write logs.',
188- default='ci-director.log')
189+ default='update-outcome.log')
190 parser.add_argument('--log-count', help='The number of backups to keep.',
191 default=2, type=int)
192 return parser.parse_args()
193@@ -642,6 +644,33 @@
194 self.write_json(mongo_name, mongo_data, make_dir=True)
195
196
197+def upload_artifacts(jenkins, by_revision_build, s3_storage):
198+ """Upload the artifacts for newly-completed builds.
199+
200+ by_revision_build should not include previously-uploaded builds, to avoid
201+ unnecessary re-uploads.
202+
203+ Builds that do not have a result are not uploaded. They lack artifacts,
204+ because that is a post-build action, and they have incomplete console
205+ logs, which could confuse other tools.
206+ """
207+ logger = logging.getLogger()
208+ for revision_build, jobs in by_revision_build.items():
209+ for job, job_data in jobs.items():
210+ artifact_job = ArtifactJob(job, logger)
211+ for build_info in job_data['builds'].values():
212+ if build_info['result'] is None:
213+ logger.info(
214+ 'No result for {} #{}. Skipping.'.format(
215+ job, build_info['number']))
216+ continue
217+ logger.info(
218+ 'Uploading artifacts for {} #{}'.format(
219+ job, build_info['number']))
220+ artifact_job.archive_results(
221+ jenkins, build_info, revision_build, s3_storage)
222+
223+
224 def main():
225 """Entry point for update-outcome script."""
226 args = parse_args()
227@@ -649,11 +678,13 @@
228 config = ConfigReader.read_config()
229 setdefaulttimeout(30)
230 jenkins = make_jenkins(config)
231+ s3_storage = S3Storage.from_config(config)
232 outcome_dir = os.path.join(os.environ['HOME'],
233 '.config/ci-director-outcome')
234 with OutcomeState(outcome_dir) as state:
235 by_revision_build, cloud_health, new_last_completed = scan_new_builds(
236 state.data, jenkins)
237+ upload_artifacts(jenkins, by_revision_build, s3_storage)
238 cloud_health_builds = BuildFiles.for_cloud_health(state)
239 list(cloud_health_builds.update_files(cloud_health))
240 revision_build_files = BuildFiles.for_revision_builds(state)

Subscribers

People subscribed via source and target branches