Merge lp:~dpb/latch-test/dont-be-so-verbose into lp:latch-test
- dont-be-so-verbose
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | David Britton |
Approved revision: | 14 |
Merged at revision: | 10 |
Proposed branch: | lp:~dpb/latch-test/dont-be-so-verbose |
Merge into: | lp:latch-test |
Diff against target: |
360 lines (+173/-45) 2 files modified
latch.py (+54/-24) test_latch.py (+119/-21) |
To merge this branch: | bzr merge lp:~dpb/latch-test/dont-be-so-verbose |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Alberto Donato (community) | Approve | ||
Free Ekanayaka (community) | Approve | ||
🤖 Landscape Builder | test results | Approve | |
Review via email:
|
Commit message
- Don't treat failed LP reads the same as an empty set read
- Don't post such big latch messages, people can just click the results link that latch sticks in there.
Description of the change
- Refactored code to make the failed read from launchpad actionable
- Stop posting full text of failure to MP
- Added test cases for refactored code (mostly overlap with existing ones)
Please feel free to flame me for testing what could be done better, mocking, etc. I didn't want to spend too much time here, but wanted to make a good change at the same time.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 Landscape Builder (landscape-builder) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Free Ekanayaka (free.ekanayaka) wrote : | # |
Thanks for pushing an MP for this David, looks like a good change to me.
Re testing, yes, my recommendation is to avoid mocks, but since these tests already make use of them, it seems out of scope to fix that here, so +1
Unrelated comment, but it seems to me that it's not possible (or it would be very awkward) to save artifacts generated by latch-test, because of this code in test_cli:
with temporary_
result = environment.
which essentially purge everything after tests have run. Would it be possible to add support for specifying an artifacts directory path inside the temporary directory that will moved or copied to the working directory before purging everything? This way we can attach more state information about failed tests using Jenkins artifacts (something that is now pressing because of some flaky integration tests).
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Alberto Donato (ack) wrote : | # |
Looks good, +1
A few minor comments inline.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
David Britton (dpb) wrote : | # |
Added bug for free's comment: https:/
Addressed all of alberto's in this branch.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 Landscape Builder (landscape-builder) wrote : | # |
There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 Landscape Builder (landscape-builder) wrote : | # |
No approved revision specified.
Preview Diff
1 | === modified file 'latch.py' | |||
2 | --- latch.py 2015-04-23 18:43:19 +0000 | |||
3 | +++ latch.py 2015-09-08 14:29:55 +0000 | |||
4 | @@ -12,11 +12,20 @@ | |||
5 | 12 | import sys | 12 | import sys |
6 | 13 | import tempfile | 13 | import tempfile |
7 | 14 | 14 | ||
8 | 15 | |||
9 | 15 | log = logging.getLogger(__name__) | 16 | log = logging.getLogger(__name__) |
10 | 16 | 17 | ||
11 | 17 | 18 | ||
12 | 19 | class LaunchpadFetchError(Exception): | ||
13 | 20 | """Error fetching collection from Launchpad.""" | ||
14 | 21 | |||
15 | 22 | |||
16 | 18 | def fetch_collection(lp, collection): | 23 | def fetch_collection(lp, collection): |
18 | 19 | """Fetch a launchpad collection with proper batch awareness.""" | 24 | """Fetch a launchpad collection with proper batch awareness. |
19 | 25 | |||
20 | 26 | @param collection: Collection to read. | ||
21 | 27 | raises LaunchpadFetchError if any errors come from lp. | ||
22 | 28 | """ | ||
23 | 20 | try: | 29 | try: |
24 | 21 | if collection.total_size > 0: | 30 | if collection.total_size > 0: |
25 | 22 | for entry in collection.entries: | 31 | for entry in collection.entries: |
26 | @@ -25,16 +34,16 @@ | |||
27 | 25 | collection = collection.next_collection | 34 | collection = collection.next_collection |
28 | 26 | for entry in collection.entries: | 35 | for entry in collection.entries: |
29 | 27 | yield lp.load(entry["self_link"]) | 36 | yield lp.load(entry["self_link"]) |
31 | 28 | except Exception: | 37 | except Exception as e: |
32 | 29 | log.exception("Error retrieving collection") | 38 | log.exception("Error retrieving collection") |
36 | 30 | 39 | raise LaunchpadFetchError(str(e)) | |
37 | 31 | 40 | ||
38 | 32 | def format_test_result(result, always_output=False): | 41 | |
39 | 42 | def format_test_result(result, show_output=False): | ||
40 | 33 | """ | 43 | """ |
41 | 34 | Format test results for printing/appending to an MP. | 44 | Format test results for printing/appending to an MP. |
42 | 35 | 45 | ||
45 | 36 | @param always_output True=always append, False=append | 46 | @param show_output True=show, False=hide (default) |
44 | 37 | only on error | ||
46 | 38 | """ | 47 | """ |
47 | 39 | string = u"Command: %(command)s\n" \ | 48 | string = u"Command: %(command)s\n" \ |
48 | 40 | u"Result: %(status)s\n" \ | 49 | u"Result: %(status)s\n" \ |
49 | @@ -42,7 +51,7 @@ | |||
50 | 42 | u"Branch: %(branch)s" % result | 51 | u"Branch: %(branch)s" % result |
51 | 43 | if 'BUILD_URL' in os.environ: | 52 | if 'BUILD_URL' in os.environ: |
52 | 44 | string += u"\nJenkins: %s" % os.environ["BUILD_URL"] | 53 | string += u"\nJenkins: %s" % os.environ["BUILD_URL"] |
54 | 45 | if always_output or result["status"] == u"Fail": | 54 | if show_output: |
55 | 46 | string += u"\n-------------------------------------------\n" | 55 | string += u"\n-------------------------------------------\n" |
56 | 47 | string += u"%s" % result["output"] | 56 | string += u"%s" % result["output"] |
57 | 48 | return string | 57 | return string |
58 | @@ -239,32 +248,53 @@ | |||
59 | 239 | log.debug("-> Would be adding result: %s to %s" % ( | 248 | log.debug("-> Would be adding result: %s to %s" % ( |
60 | 240 | result['status'], self.lp_candidate.web_link)) | 249 | result['status'], self.lp_candidate.web_link)) |
61 | 241 | 250 | ||
62 | 251 | def _get_revision_from_comment(self, comment): | ||
63 | 252 | """Given a comment on an MP, return the revision it tested. | ||
64 | 253 | 0 will be returned if the comment does not apply.""" | ||
65 | 254 | revision_tested = 0 | ||
66 | 255 | if self.lp.me != comment.author: | ||
67 | 256 | return 0 | ||
68 | 257 | m = re.search("Revno: (.*)", comment.message_body[:1024]) | ||
69 | 258 | if m is None: | ||
70 | 259 | log.warning("comment (%s) from %s missing revno, skipping" % ( | ||
71 | 260 | comment.self_link, self.lp.me)) | ||
72 | 261 | return 0 | ||
73 | 262 | stored_revision = m.group(1).strip() | ||
74 | 263 | try: | ||
75 | 264 | revision_tested = int(stored_revision) | ||
76 | 265 | except ValueError: | ||
77 | 266 | log.warning("comment (%s) revno parse error, skipping" % ( | ||
78 | 267 | comment.self_link)) | ||
79 | 268 | return 0 | ||
80 | 269 | return revision_tested | ||
81 | 270 | |||
82 | 242 | def are_test_results_stale(self): | 271 | def are_test_results_stale(self): |
83 | 243 | """ | 272 | """ |
84 | 244 | Evaluate candidate to determine if latest results are stale. Return | 273 | Evaluate candidate to determine if latest results are stale. Return |
85 | 245 | boolean evaluation. Each result is checked for the stored revision | 274 | boolean evaluation. Each result is checked for the stored revision |
86 | 246 | number and compared to the source_branch revno. | 275 | number and compared to the source_branch revno. |
87 | 276 | Error conditions are counted as not stale so as not to trigger | ||
88 | 277 | retests over and over. | ||
89 | 247 | """ | 278 | """ |
90 | 248 | revision_tested = 0 | 279 | revision_tested = 0 |
91 | 249 | log.debug( | 280 | log.debug( |
92 | 250 | "-> current revno: r%s" % ( | 281 | "-> current revno: r%s" % ( |
93 | 251 | self.lp_candidate.source_branch.revision_count)) | 282 | self.lp_candidate.source_branch.revision_count)) |
110 | 252 | for comment in fetch_collection(self.lp, | 283 | |
111 | 253 | self.lp_candidate.all_comments): | 284 | revision_tested = 0 |
112 | 254 | if self.lp.me != comment.author: | 285 | try: |
113 | 255 | continue | 286 | # Since comments are always in chronological order, we can |
114 | 256 | m = re.search("Revno: (.*)", comment.message_body[:1024]) | 287 | # stop after we hit a match going backward |
115 | 257 | if m is None: | 288 | for comment in reversed(fetch_collection( |
116 | 258 | log.warning("comment (%s) from %s missing revno, skipping" % ( | 289 | self.lp, self.lp_candidate.all_comments)): |
117 | 259 | comment.self_link, self.lp.me)) | 290 | revision_tested = self._get_revision_from_comment(comment) |
118 | 260 | continue | 291 | if revision_tested > 0: |
119 | 261 | stored_revision = m.group(1).strip() | 292 | break |
120 | 262 | try: | 293 | except LaunchpadFetchError: |
121 | 263 | revision_tested = max(revision_tested, int(stored_revision)) | 294 | log.exception("Error fetching comments from Launchpad on %s" % ( |
122 | 264 | except ValueError: | 295 | self.lp_candidate.web_link)) |
123 | 265 | log.warning("comment (%s) revno parse error, skipping" % ( | 296 | log.warning("Skipping candidate: %s") |
124 | 266 | comment.self_link)) | 297 | return False |
109 | 267 | continue | ||
125 | 268 | 298 | ||
126 | 269 | if revision_tested == 0: | 299 | if revision_tested == 0: |
127 | 270 | log.debug("-> results are stale, tested_revision == 0") | 300 | log.debug("-> results are stale, tested_revision == 0") |
128 | 271 | 301 | ||
129 | === modified file 'test_latch.py' | |||
130 | --- test_latch.py 2015-04-23 16:01:38 +0000 | |||
131 | +++ test_latch.py 2015-09-08 14:29:55 +0000 | |||
132 | @@ -3,19 +3,45 @@ | |||
133 | 3 | import latch | 3 | import latch |
134 | 4 | import unittest | 4 | import unittest |
135 | 5 | from mock import MagicMock, patch | 5 | from mock import MagicMock, patch |
136 | 6 | import logging | ||
137 | 7 | from logging.handlers import BufferingHandler | ||
138 | 6 | import os | 8 | import os |
139 | 7 | import random | ||
140 | 8 | from subprocess import PIPE, STDOUT, check_call, CalledProcessError | 9 | from subprocess import PIPE, STDOUT, check_call, CalledProcessError |
141 | 9 | 10 | ||
142 | 10 | OUTPUT = u"output ✈" | 11 | OUTPUT = u"output ✈" |
143 | 11 | MULTILINE_OUTPUT = u"multiline\noutput ✈" | 12 | MULTILINE_OUTPUT = u"multiline\noutput ✈" |
144 | 12 | 13 | ||
145 | 14 | FAKE_ME = MagicMock() | ||
146 | 15 | FAKE_ME.name = "fake-user" | ||
147 | 16 | |||
148 | 13 | 17 | ||
149 | 14 | def fetch_collection(self, collection): | 18 | def fetch_collection(self, collection): |
150 | 15 | """Return collection directly.""" | 19 | """Return collection directly.""" |
151 | 16 | return collection | 20 | return collection |
152 | 17 | 21 | ||
153 | 18 | 22 | ||
154 | 23 | def get_fake_comment(body=None, revno=1, author_name=FAKE_ME.name): | ||
155 | 24 | """Return a fake comment. | ||
156 | 25 | @param body: If set, put this in the body, otherwise, formulate the body | ||
157 | 26 | as a latch comment with a Revno: | ||
158 | 27 | @param revno: revision number | ||
159 | 28 | @param author_name: author name | ||
160 | 29 | """ | ||
161 | 30 | fake_comment = MagicMock() | ||
162 | 31 | if author_name == FAKE_ME.name: | ||
163 | 32 | fake_author = FAKE_ME | ||
164 | 33 | else: | ||
165 | 34 | fake_author = MagicMock() | ||
166 | 35 | fake_author.name = author_name | ||
167 | 36 | fake_comment.author = fake_author | ||
168 | 37 | |||
169 | 38 | if body is None: | ||
170 | 39 | fake_comment.message_body = "Revno: %s" % revno | ||
171 | 40 | else: | ||
172 | 41 | fake_comment.message_body = body | ||
173 | 42 | return fake_comment | ||
174 | 43 | |||
175 | 44 | |||
176 | 19 | def add_results(candidate, revision, author): | 45 | def add_results(candidate, revision, author): |
177 | 20 | """ | 46 | """ |
178 | 21 | Add fake test results to a candidate, one for each revision in | 47 | Add fake test results to a candidate, one for each revision in |
179 | @@ -23,7 +49,6 @@ | |||
180 | 23 | """ | 49 | """ |
181 | 24 | candidate.all_comments = [] | 50 | candidate.all_comments = [] |
182 | 25 | revisions = range(1, revision + 1) | 51 | revisions = range(1, revision + 1) |
183 | 26 | random.shuffle(revisions) | ||
184 | 27 | for revno in revisions: | 52 | for revno in revisions: |
185 | 28 | comment = MagicMock() | 53 | comment = MagicMock() |
186 | 29 | comment.message_body = "Revno: %s" % revno | 54 | comment.message_body = "Revno: %s" % revno |
187 | @@ -89,17 +114,61 @@ | |||
188 | 89 | return (source, target) | 114 | return (source, target) |
189 | 90 | 115 | ||
190 | 91 | 116 | ||
191 | 117 | def raises_launchpad_fetch_error(*args): | ||
192 | 118 | raise latch.LaunchpadFetchError("This is an Error.") | ||
193 | 119 | |||
194 | 120 | |||
195 | 92 | # For all tests, bypass fetch_collection and just return raw data | 121 | # For all tests, bypass fetch_collection and just return raw data |
196 | 93 | latch.fetch_collection = fetch_collection | 122 | latch.fetch_collection = fetch_collection |
197 | 94 | 123 | ||
198 | 95 | 124 | ||
199 | 125 | class TestLogHandler(BufferingHandler): | ||
200 | 126 | """LogHandler so unit tests can inspect logs written.""" | ||
201 | 127 | |||
202 | 128 | def __init__(self): | ||
203 | 129 | super(TestLogHandler, self).__init__(0) | ||
204 | 130 | self.buffer = () | ||
205 | 131 | |||
206 | 132 | def shouldFlush(self): | ||
207 | 133 | return False | ||
208 | 134 | |||
209 | 135 | def emit(self, record): | ||
210 | 136 | self.buffer += (record.levelname, record.msg), | ||
211 | 137 | self.buffer += ("ALL", record.msg), | ||
212 | 138 | |||
213 | 139 | |||
214 | 140 | class LoggedTest(unittest.TestCase): | ||
215 | 141 | """Testing class that can track emitted logs""" | ||
216 | 142 | |||
217 | 143 | def setUp(self): | ||
218 | 144 | super(LoggedTest, self).setUp() | ||
219 | 145 | self.handler = TestLogHandler() | ||
220 | 146 | self.logger = logging.getLogger(latch.__name__) | ||
221 | 147 | self.logger.addHandler(self.handler) | ||
222 | 148 | |||
223 | 149 | def tearDown(self): | ||
224 | 150 | self.logger.removeHandler(self.handler) | ||
225 | 151 | self.handler.close() | ||
226 | 152 | |||
227 | 153 | def assertInLogs(self, message, level="ALL"): | ||
228 | 154 | """Assert that a message is logged. Substrings are OK. | ||
229 | 155 | |||
230 | 156 | Optionally restict search to a log level of INFO, WARNING and ERROR. | ||
231 | 157 | """ | ||
232 | 158 | for log_level, log_message in self.handler.buffer: | ||
233 | 159 | if level == log_level: | ||
234 | 160 | if message in log_message: | ||
235 | 161 | return | ||
236 | 162 | raise AssertionError("String '%s' not found in %s" % ( | ||
237 | 163 | message, self.handler.buffer)) | ||
238 | 164 | |||
239 | 165 | |||
240 | 96 | class LatchTest(unittest.TestCase): | 166 | class LatchTest(unittest.TestCase): |
241 | 97 | 167 | ||
242 | 98 | def setUp(self): | 168 | def setUp(self): |
243 | 99 | self.latch = latch.Latch() | 169 | self.latch = latch.Latch() |
244 | 100 | self.latch.lp = MagicMock() | 170 | self.latch.lp = MagicMock() |
247 | 101 | self.latch.lp.me = MagicMock() | 171 | self.latch.lp.me = FAKE_ME |
246 | 102 | self.latch.lp.me.name = "fake-user" | ||
248 | 103 | 172 | ||
249 | 104 | def test_scan_branch_nothing(self): | 173 | def test_scan_branch_nothing(self): |
250 | 105 | """Scan list of candidates, nothing should match.""" | 174 | """Scan list of candidates, nothing should match.""" |
251 | @@ -125,7 +194,7 @@ | |||
252 | 125 | def setUp(self): | 194 | def setUp(self): |
253 | 126 | self.latch = latch.Latch() | 195 | self.latch = latch.Latch() |
254 | 127 | self.latch.lp = MagicMock() | 196 | self.latch.lp = MagicMock() |
256 | 128 | self.latch.lp.me.name = "fake-user" | 197 | self.latch.lp.me = FAKE_ME |
257 | 129 | 198 | ||
258 | 130 | def test__lookup_config(self): | 199 | def test__lookup_config(self): |
259 | 131 | """section and key/value exist in config.""" | 200 | """section and key/value exist in config.""" |
260 | @@ -157,14 +226,14 @@ | |||
261 | 157 | self.latch._lookup_config(config, candidate, "missing_key") | 226 | self.latch._lookup_config(config, candidate, "missing_key") |
262 | 158 | 227 | ||
263 | 159 | 228 | ||
265 | 160 | class CandidateTest(unittest.TestCase): | 229 | class CandidateTest(LoggedTest): |
266 | 161 | 230 | ||
267 | 162 | def setUp(self): | 231 | def setUp(self): |
268 | 232 | super(CandidateTest, self).setUp() | ||
269 | 163 | # Create a candidate object for testing, individual tests | 233 | # Create a candidate object for testing, individual tests |
270 | 164 | # will need to fill out the lp_candidate appropriately | 234 | # will need to fill out the lp_candidate appropriately |
271 | 165 | self.lp = MagicMock() | 235 | self.lp = MagicMock() |
274 | 166 | self.lp.me = MagicMock() | 236 | self.lp.me = FAKE_ME |
273 | 167 | self.lp.me.name = "fake-user" | ||
275 | 168 | self.candidate = latch.Candidate(self.lp, MagicMock()) | 237 | self.candidate = latch.Candidate(self.lp, MagicMock()) |
276 | 169 | 238 | ||
277 | 170 | def test_add_reviewer_readonly(self): | 239 | def test_add_reviewer_readonly(self): |
278 | @@ -240,6 +309,43 @@ | |||
279 | 240 | "This is a test.") | 309 | "This is a test.") |
280 | 241 | self.assertTrue(self.candidate.are_test_results_stale()) | 310 | self.assertTrue(self.candidate.are_test_results_stale()) |
281 | 242 | 311 | ||
282 | 312 | def test_are_test_results_stale_lp_error(self): | ||
283 | 313 | """Test results revno == source branch.""" | ||
284 | 314 | self.candidate.lp_candidate.source_branch.revision_count = 1 | ||
285 | 315 | add_results(self.candidate.lp_candidate, 1, self.lp.me) | ||
286 | 316 | old_fetch_collection = latch.fetch_collection | ||
287 | 317 | try: | ||
288 | 318 | latch.fetch_collection = raises_launchpad_fetch_error | ||
289 | 319 | self.assertFalse( | ||
290 | 320 | self.candidate.are_test_results_stale()) | ||
291 | 321 | self.assertInLogs("Error fetching comments from Launchpad") | ||
292 | 322 | self.assertInLogs("Skipping candidate") | ||
293 | 323 | finally: | ||
294 | 324 | latch.fetch_collection = old_fetch_collection | ||
295 | 325 | |||
296 | 326 | def test_get_revision_from_comment(self): | ||
297 | 327 | """Test comment happy path, it's one of ours, and it has a revno.""" | ||
298 | 328 | comment = get_fake_comment(revno=100) | ||
299 | 329 | self.assertEqual( | ||
300 | 330 | self.candidate._get_revision_from_comment(comment), 100) | ||
301 | 331 | |||
302 | 332 | def test_get_revision_from_comment_different_author(self): | ||
303 | 333 | """Test comment filter: comment not from self.lp.me returns 0.""" | ||
304 | 334 | comment = get_fake_comment(author_name="someone-else") | ||
305 | 335 | self.assertEqual(self.candidate._get_revision_from_comment(comment), 0) | ||
306 | 336 | |||
307 | 337 | def test_get_revision_from_comment_no_revno(self): | ||
308 | 338 | """Test comment filter: revno missing from comment.""" | ||
309 | 339 | comment = get_fake_comment(body="foobar") | ||
310 | 340 | self.assertEqual(self.candidate._get_revision_from_comment(comment), 0) | ||
311 | 341 | self.assertInLogs("missing revno, skipping") | ||
312 | 342 | |||
313 | 343 | def test_get_revision_from_comment_revno_parse_error(self): | ||
314 | 344 | """Test comment filter: revno not an int.""" | ||
315 | 345 | comment = get_fake_comment(revno="foobar") | ||
316 | 346 | self.assertEqual(self.candidate._get_revision_from_comment(comment), 0) | ||
317 | 347 | self.assertInLogs("revno parse error, skipping") | ||
318 | 348 | |||
319 | 243 | def test_add_result_readonly(self): | 349 | def test_add_result_readonly(self): |
320 | 244 | """if readonly, should NOT try to create a comment.""" | 350 | """if readonly, should NOT try to create a comment.""" |
321 | 245 | result = { | 351 | result = { |
322 | @@ -293,25 +399,17 @@ | |||
323 | 293 | 399 | ||
324 | 294 | def test_format_test_result_force_include_output(self): | 400 | def test_format_test_result_force_include_output(self): |
325 | 295 | """Passing result, output requested to appear.""" | 401 | """Passing result, output requested to appear.""" |
327 | 296 | string = latch.format_test_result(self.result, always_output=True) | 402 | string = latch.format_test_result(self.result, show_output=True) |
328 | 297 | self.assertIn(u"Result: Success", string) | 403 | self.assertIn(u"Result: Success", string) |
329 | 298 | self.assertIn(MULTILINE_OUTPUT, string) | 404 | self.assertIn(MULTILINE_OUTPUT, string) |
330 | 299 | self.assertNotIn(u"Jenkins:", string) | 405 | self.assertNotIn(u"Jenkins:", string) |
331 | 300 | 406 | ||
334 | 301 | def test_format_test_result_include_output_with_failure(self): | 407 | def test_format_test_result_with_failure_no_output(self): |
335 | 302 | """Failing result, output should appear.""" | 408 | """Failing result, output should not appear since not requested.""" |
336 | 303 | self.result["status"] = "Fail" | 409 | self.result["status"] = "Fail" |
337 | 304 | string = latch.format_test_result(self.result) | 410 | string = latch.format_test_result(self.result) |
338 | 305 | self.assertIn(u"Revno: 1234", string) | 411 | self.assertIn(u"Revno: 1234", string) |
348 | 306 | self.assertIn(MULTILINE_OUTPUT, string) | 412 | self.assertNotIn(MULTILINE_OUTPUT, string) |
340 | 307 | self.assertNotIn(u"Jenkins:", string) | ||
341 | 308 | |||
342 | 309 | def test_format_test_result_include_output_with_failure_and_request(self): | ||
343 | 310 | """Failing result, output should appear, even if requested.""" | ||
344 | 311 | self.result["status"] = "Fail" | ||
345 | 312 | string = latch.format_test_result(self.result, always_output=True) | ||
346 | 313 | self.assertIn(u"Revno: 1234", string) | ||
347 | 314 | self.assertIn(MULTILINE_OUTPUT, string) | ||
349 | 315 | self.assertNotIn(u"Jenkins:", string) | 413 | self.assertNotIn(u"Jenkins:", string) |
350 | 316 | 414 | ||
351 | 317 | def test_format_test_result_jenkins_build_url(self): | 415 | def test_format_test_result_jenkins_build_url(self): |
352 | @@ -333,7 +431,7 @@ | |||
353 | 333 | class TestEnvironmentTest(unittest.TestCase): | 431 | class TestEnvironmentTest(unittest.TestCase): |
354 | 334 | def setUp(self): | 432 | def setUp(self): |
355 | 335 | self.lp = MagicMock() | 433 | self.lp = MagicMock() |
357 | 336 | self.lp.me.name = "fake-user" | 434 | self.lp.me = FAKE_ME |
358 | 337 | self.environment = latch.TestEnvironment( | 435 | self.environment = latch.TestEnvironment( |
359 | 338 | latch.Candidate(self.lp, get_candidate(self.lp.me.name)), | 436 | latch.Candidate(self.lp, get_candidate(self.lp.me.name)), |
360 | 339 | self.lp, | 437 | self.lp, |
Command: make test /ci.lscape. net/job/ latch-test/ 2272/
Result: Success
Revno: 13
Branch: lp:~davidpbritton/latch-test/dont-be-so-verbose
Jenkins: https:/