Merge ~cjwatson/launchpad-buildd:ci-individual-results into launchpad-buildd:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 46066418c3663ee037cb5d84946680ba9d8c9157
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad-buildd:ci-individual-results
Merge into: launchpad-buildd:master
Diff against target: 201 lines (+110/-12)
3 files modified
debian/changelog (+6/-0)
lpbuildd/ci.py (+16/-9)
lpbuildd/tests/test_ci.py (+88/-3)
Reviewer Review Type Date Requested Status
Ioana Lasc (community) Approve
Review via email: mp+414164@code.launchpad.net

Commit message

Return results from individual CI jobs

Description of the change

It's otherwise difficult for Launchpad to know how to record the results.

To post a comment you must log in.
Revision history for this message
Ioana Lasc (ilasc) wrote :

LGTM!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/debian/changelog b/debian/changelog
2index c35ca7e..b2bcf77 100644
3--- a/debian/changelog
4+++ b/debian/changelog
5@@ -1,3 +1,9 @@
6+launchpad-buildd (207) UNRELEASED; urgency=medium
7+
8+ * Return results from individual CI jobs.
9+
10+ -- Colin Watson <cjwatson@ubuntu.com> Thu, 13 Jan 2022 14:51:09 +0000
11+
12 launchpad-buildd (206) bionic; urgency=medium
13
14 * Fix flake8 violations.
15diff --git a/lpbuildd/ci.py b/lpbuildd/ci.py
16index 9c70451..14ac7c6 100644
17--- a/lpbuildd/ci.py
18+++ b/lpbuildd/ci.py
19@@ -24,6 +24,11 @@ RETCODE_SUCCESS = 0
20 RETCODE_FAILURE_INSTALL = 200
21 RETCODE_FAILURE_BUILD = 201
22
23+# These must match the names of `RevisionStatusResult` enumeration items in
24+# Launchpad.
25+RESULT_SUCCEEDED = "SUCCEEDED"
26+RESULT_FAILED = "FAILED"
27+
28
29 class CIBuildState(DebianBuildState):
30 PREPARE = "PREPARE"
31@@ -125,18 +130,20 @@ class CIBuildManager(BuildManagerProxyMixin, DebianBuildManager):
32 This state is repeated for each CI job in the pipeline.
33 """
34 if retcode == RETCODE_SUCCESS:
35- pass
36- elif (retcode >= RETCODE_FAILURE_INSTALL and
37- retcode <= RETCODE_FAILURE_BUILD):
38- if not self.alreadyfailed:
39- self._builder.log("Job %s failed." % self.current_job_id)
40- self._builder.buildFail()
41- self.alreadyfailed = True
42+ result = RESULT_SUCCEEDED
43 else:
44- if not self.alreadyfailed:
45- self._builder.builderFail()
46+ result = RESULT_FAILED
47+ if (retcode >= RETCODE_FAILURE_INSTALL and
48+ retcode <= RETCODE_FAILURE_BUILD):
49+ self._builder.log("Job %s failed." % self.current_job_id)
50+ if not self.alreadyfailed:
51+ self._builder.buildFail()
52+ else:
53+ if not self.alreadyfailed:
54+ self._builder.builderFail()
55 self.alreadyfailed = True
56 yield self.deferGatherResults(reap=False)
57+ self.job_status[self.current_job_id]["result"] = result
58 if self.remaining_jobs and not self.alreadyfailed:
59 self.runNextJob()
60 else:
61diff --git a/lpbuildd/tests/test_ci.py b/lpbuildd/tests/test_ci.py
62index 1009a36..2cc1387 100644
63--- a/lpbuildd/tests/test_ci.py
64+++ b/lpbuildd/tests/test_ci.py
65@@ -18,6 +18,10 @@ from lpbuildd.builder import get_build_path
66 from lpbuildd.ci import (
67 CIBuildManager,
68 CIBuildState,
69+ RESULT_SUCCEEDED,
70+ RESULT_FAILED,
71+ RETCODE_FAILURE_BUILD,
72+ RETCODE_SUCCESS,
73 )
74 from lpbuildd.tests.fakebuilder import FakeBuilder
75 from lpbuildd.tests.matchers import HasWaitingFiles
76@@ -94,8 +98,9 @@ class TestCIBuildManagerIteration(TestCase):
77 self.assertFalse(self.builder.wasCalled("chrootFail"))
78
79 @defer.inlineCallbacks
80- def expectRunJob(self, job_name, job_index, options=None):
81- yield self.buildmanager.iterate(0)
82+ def expectRunJob(self, job_name, job_index, options=None,
83+ retcode=RETCODE_SUCCESS):
84+ yield self.buildmanager.iterate(retcode)
85 self.assertEqual(CIBuildState.RUN_JOB, self.getState())
86 expected_command = [
87 "sharepath/bin/in-target", "in-target", "run-ci",
88@@ -110,7 +115,7 @@ class TestCIBuildManagerIteration(TestCase):
89 self.assertFalse(self.builder.wasCalled("chrootFail"))
90
91 @defer.inlineCallbacks
92- def test_iterate(self):
93+ def test_iterate_success(self):
94 # The build manager iterates multiple CI jobs from start to finish.
95 args = {
96 "git_repository": "https://git.launchpad.test/~example/+git/ci",
97@@ -148,6 +153,7 @@ class TestCIBuildManagerIteration(TestCase):
98 "output": {
99 "ci.whl": self.builder.waitingfiles["build:0/ci.whl"],
100 },
101+ "result": RESULT_SUCCEEDED,
102 },
103 },
104 extra_status["jobs"])
105@@ -179,6 +185,7 @@ class TestCIBuildManagerIteration(TestCase):
106 "output": {
107 "ci.whl": self.builder.waitingfiles["build:0/ci.whl"],
108 },
109+ "result": RESULT_SUCCEEDED,
110 },
111 "test:0": {
112 "log": self.builder.waitingfiles["test:0.log"],
113@@ -186,6 +193,7 @@ class TestCIBuildManagerIteration(TestCase):
114 "ci.tar.gz":
115 self.builder.waitingfiles["test:0/ci.tar.gz"],
116 },
117+ "result": RESULT_SUCCEEDED,
118 },
119 },
120 extra_status["jobs"])
121@@ -221,3 +229,80 @@ class TestCIBuildManagerIteration(TestCase):
122 shutil.rmtree(get_build_path(
123 self.buildmanager.home, self.buildmanager._buildid))
124 self.assertIn("jobs", self.buildmanager.status())
125+
126+ @defer.inlineCallbacks
127+ def test_iterate_failure(self):
128+ # The build manager records CI jobs that fail.
129+ args = {
130+ "git_repository": "https://git.launchpad.test/~example/+git/ci",
131+ "git_path": "main",
132+ "jobs": [("build", "0"), ("test", "0")],
133+ }
134+ expected_options = [
135+ "--git-repository", "https://git.launchpad.test/~example/+git/ci",
136+ "--git-path", "main",
137+ ]
138+ yield self.startBuild(args, expected_options)
139+
140+ # After preparation, start running the first job.
141+ yield self.expectRunJob("build", "0")
142+ self.buildmanager.backend.add_file(
143+ "/build/output/build:0.log", b"I am a failing CI build job log.")
144+
145+ # If the first job fails, then the build fails here.
146+ yield self.buildmanager.iterate(RETCODE_FAILURE_BUILD)
147+ expected_command = [
148+ "sharepath/bin/in-target", "in-target", "scan-for-processes",
149+ "--backend=lxd", "--series=focal", "--arch=amd64", self.buildid,
150+ ]
151+ self.assertEqual(CIBuildState.RUN_JOB, self.getState())
152+ self.assertEqual(expected_command, self.buildmanager.commands[-1])
153+ self.assertNotEqual(
154+ self.buildmanager.iterate, self.buildmanager.iterators[-1])
155+ self.assertTrue(self.builder.wasCalled("buildFail"))
156+ self.assertThat(self.builder, HasWaitingFiles.byEquality({
157+ "build:0.log": b"I am a failing CI build job log.",
158+ }))
159+
160+ # Output from the first job is visible in the status response.
161+ extra_status = self.buildmanager.status()
162+ self.assertEqual(
163+ {
164+ "build:0": {
165+ "log": self.builder.waitingfiles["build:0.log"],
166+ "result": RESULT_FAILED,
167+ },
168+ },
169+ extra_status["jobs"])
170+
171+ # Control returns to the DebianBuildManager in the UMOUNT state.
172+ self.buildmanager.iterateReap(self.getState(), 0)
173+ expected_command = [
174+ "sharepath/bin/in-target", "in-target", "umount-chroot",
175+ "--backend=lxd", "--series=focal", "--arch=amd64", self.buildid,
176+ ]
177+ self.assertEqual(CIBuildState.UMOUNT, self.getState())
178+ self.assertEqual(expected_command, self.buildmanager.commands[-1])
179+ self.assertEqual(
180+ self.buildmanager.iterate, self.buildmanager.iterators[-1])
181+ self.assertTrue(self.builder.wasCalled("buildFail"))
182+
183+ # If we iterate to the end of the build, then the extra status
184+ # information is still present.
185+ self.buildmanager.iterate(0)
186+ expected_command = [
187+ 'sharepath/bin/in-target', 'in-target', 'remove-build',
188+ '--backend=lxd', '--series=focal', '--arch=amd64', self.buildid,
189+ ]
190+ self.assertEqual(CIBuildState.CLEANUP, self.getState())
191+ self.assertEqual(expected_command, self.buildmanager.commands[-1])
192+ self.assertEqual(
193+ self.buildmanager.iterate, self.buildmanager.iterators[-1])
194+
195+ self.buildmanager.iterate(0)
196+ self.assertFalse(self.builder.wasCalled('buildOK'))
197+ self.assertTrue(self.builder.wasCalled('buildComplete'))
198+ # remove-build would remove this in a non-test environment.
199+ shutil.rmtree(get_build_path(
200+ self.buildmanager.home, self.buildmanager._buildid))
201+ self.assertIn("jobs", self.buildmanager.status())

Subscribers

People subscribed via source and target branches