OLD | NEW |
| (Empty) |
1 # -*- test-case-name: buildbot.test.test_web -*- | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 import os, time, shutil | |
5 import warnings | |
6 from HTMLParser import HTMLParser | |
7 from twisted.python import components | |
8 | |
9 from twisted.trial import unittest | |
10 from buildbot.test.runutils import RunMixin | |
11 | |
12 from twisted.internet import reactor, defer, protocol | |
13 from twisted.internet.interfaces import IReactorUNIX | |
14 from twisted.web import client | |
15 | |
16 from buildbot import master, interfaces, sourcestamp | |
17 from buildbot.status import html, builder | |
18 from buildbot.status.web import waterfall, xmlrpc | |
19 from buildbot.changes.changes import Change | |
20 from buildbot.process import base | |
21 from buildbot.process.buildstep import BuildStep | |
22 from buildbot.test.runutils import setupBuildStepStatus | |
23 | |
24 class ConfiguredMaster(master.BuildMaster): | |
25 """This BuildMaster variant has a static config file, provided as a | |
26 string when it is created.""" | |
27 | |
28 def __init__(self, basedir, config): | |
29 self.config = config | |
30 master.BuildMaster.__init__(self, basedir) | |
31 | |
32 def loadTheConfigFile(self): | |
33 self.loadConfig(self.config) | |
34 | |
35 components.registerAdapter(master.Control, ConfiguredMaster, | |
36 interfaces.IControl) | |
37 | |
38 | |
39 base_config = """ | |
40 from buildbot.changes.pb import PBChangeSource | |
41 from buildbot.status import html | |
42 from buildbot.buildslave import BuildSlave | |
43 from buildbot.scheduler import Scheduler | |
44 from buildbot.process.factory import BuildFactory | |
45 from buildbot.config import BuilderConfig | |
46 | |
47 BuildmasterConfig = c = { | |
48 'change_source': PBChangeSource(), | |
49 'slaves': [BuildSlave('bot1name', 'bot1passwd')], | |
50 'schedulers': [Scheduler('name', None, 60, ['builder1'])], | |
51 'slavePortnum': 0, | |
52 } | |
53 c['builders'] = [ | |
54 BuilderConfig(name='builder1', slavename='bot1name', factory=BuildFactory())
, | |
55 ] | |
56 """ | |
57 | |
58 | |
59 | |
60 class DistribUNIX: | |
61 def __init__(self, unixpath): | |
62 from twisted.web import server, resource, distrib | |
63 root = resource.Resource() | |
64 self.r = r = distrib.ResourceSubscription("unix", unixpath) | |
65 root.putChild('remote', r) | |
66 self.p = p = reactor.listenTCP(0, server.Site(root)) | |
67 self.portnum = p.getHost().port | |
68 def shutdown(self): | |
69 d = defer.maybeDeferred(self.p.stopListening) | |
70 return d | |
71 | |
72 class DistribTCP: | |
73 def __init__(self, port): | |
74 from twisted.web import server, resource, distrib | |
75 root = resource.Resource() | |
76 self.r = r = distrib.ResourceSubscription("localhost", port) | |
77 root.putChild('remote', r) | |
78 self.p = p = reactor.listenTCP(0, server.Site(root)) | |
79 self.portnum = p.getHost().port | |
80 def shutdown(self): | |
81 d = defer.maybeDeferred(self.p.stopListening) | |
82 d.addCallback(self._shutdown_1) | |
83 return d | |
84 def _shutdown_1(self, res): | |
85 return self.r.publisher.broker.transport.loseConnection() | |
86 | |
87 class SlowReader(protocol.Protocol): | |
88 didPause = False | |
89 count = 0 | |
90 data = "" | |
91 def __init__(self, req): | |
92 self.req = req | |
93 self.d = defer.Deferred() | |
94 def connectionMade(self): | |
95 self.transport.write(self.req) | |
96 def dataReceived(self, data): | |
97 self.data += data | |
98 self.count += len(data) | |
99 if not self.didPause and self.count > 10*1000: | |
100 self.didPause = True | |
101 self.transport.pauseProducing() | |
102 reactor.callLater(2, self.resume) | |
103 def resume(self): | |
104 self.transport.resumeProducing() | |
105 def connectionLost(self, why): | |
106 self.d.callback(None) | |
107 | |
108 class CFactory(protocol.ClientFactory): | |
109 def __init__(self, p): | |
110 self.p = p | |
111 def buildProtocol(self, addr): | |
112 self.p.factory = self | |
113 return self.p | |
114 | |
115 def stopHTTPLog(): | |
116 # grr. | |
117 from twisted.web import http | |
118 http._logDateTimeStop() | |
119 | |
120 class BaseWeb: | |
121 master = None | |
122 | |
123 def failUnlessIn(self, substr, string, note=None): | |
124 self.failUnless(string.find(substr) != -1, note) | |
125 | |
126 def tearDown(self): | |
127 stopHTTPLog() | |
128 if self.master: | |
129 d = self.master.stopService() | |
130 return d | |
131 | |
132 def find_webstatus(self, master): | |
133 for child in list(master): | |
134 if isinstance(child, html.WebStatus): | |
135 return child | |
136 | |
137 def find_waterfall(self, master): | |
138 for child in list(master): | |
139 if isinstance(child, html.Waterfall): | |
140 return child | |
141 | |
142 class Ports(BaseWeb, unittest.TestCase): | |
143 | |
144 def test_webPortnum(self): | |
145 # run a regular web server on a TCP socket | |
146 config = base_config + "c['status'] = [html.WebStatus(http_port=0)]\n" | |
147 os.mkdir("test_web1") | |
148 self.master = m = ConfiguredMaster("test_web1", config) | |
149 m.startService() | |
150 # hack to find out what randomly-assigned port it is listening on | |
151 port = self.find_webstatus(m).getPortnum() | |
152 | |
153 d = client.getPage("http://localhost:%d/waterfall" % port) | |
154 def _check(page): | |
155 #print page | |
156 self.failUnless(page) | |
157 d.addCallback(_check) | |
158 return d | |
159 test_webPortnum.timeout = 10 | |
160 | |
161 def test_webPathname(self): | |
162 # running a t.web.distrib server over a UNIX socket | |
163 if not IReactorUNIX.providedBy(reactor): | |
164 raise unittest.SkipTest("UNIX sockets not supported here") | |
165 config = (base_config + | |
166 "c['status'] = [html.WebStatus(distrib_port='.web-pb')]\n") | |
167 os.mkdir("test_web2") | |
168 self.master = m = ConfiguredMaster("test_web2", config) | |
169 m.startService() | |
170 | |
171 p = DistribUNIX("test_web2/.web-pb") | |
172 | |
173 d = client.getPage("http://localhost:%d/remote/waterfall" % p.portnum) | |
174 def _check(page): | |
175 self.failUnless(page) | |
176 d.addCallback(_check) | |
177 def _done(res): | |
178 d1 = p.shutdown() | |
179 d1.addCallback(lambda x: res) | |
180 return d1 | |
181 d.addBoth(_done) | |
182 return d | |
183 test_webPathname.timeout = 10 | |
184 | |
185 | |
186 def test_webPathname_port(self): | |
187 # running a t.web.distrib server over TCP | |
188 config = (base_config + | |
189 "c['status'] = [html.WebStatus(distrib_port=0)]\n") | |
190 os.mkdir("test_web3") | |
191 self.master = m = ConfiguredMaster("test_web3", config) | |
192 m.startService() | |
193 dport = self.find_webstatus(m).getPortnum() | |
194 | |
195 p = DistribTCP(dport) | |
196 | |
197 d = client.getPage("http://localhost:%d/remote/waterfall" % p.portnum) | |
198 def _check(page): | |
199 self.failUnlessIn("BuildBot", page) | |
200 d.addCallback(_check) | |
201 def _done(res): | |
202 d1 = p.shutdown() | |
203 d1.addCallback(lambda x: res) | |
204 return d1 | |
205 d.addBoth(_done) | |
206 return d | |
207 test_webPathname_port.timeout = 10 | |
208 | |
209 | |
210 class Waterfall(BaseWeb, unittest.TestCase): | |
211 def setUp(self): | |
212 warnings.filterwarnings("ignore", category=DeprecationWarning) | |
213 | |
214 def tearDown(self): | |
215 BaseWeb.tearDown(self) | |
216 warnings.resetwarnings() | |
217 | |
218 def test_waterfall(self): | |
219 os.mkdir("test_web4") | |
220 os.mkdir("my-maildir"); os.mkdir("my-maildir/new") | |
221 self.robots_txt = os.path.abspath(os.path.join("test_web4", | |
222 "robots.txt")) | |
223 self.robots_txt_contents = "User-agent: *\nDisallow: /\n" | |
224 f = open(self.robots_txt, "w") | |
225 f.write(self.robots_txt_contents) | |
226 f.close() | |
227 # this is the right way to configure the Waterfall status | |
228 config1 = base_config + """ | |
229 from buildbot.changes import mail | |
230 c['change_source'] = mail.SyncmailMaildirSource('my-maildir') | |
231 c['status'] = [html.Waterfall(http_port=0, robots_txt=%s)] | |
232 """ % repr(self.robots_txt) | |
233 | |
234 self.master = m = ConfiguredMaster("test_web4", config1) | |
235 m.startService() | |
236 port = self.find_waterfall(m).getPortnum() | |
237 self.port = port | |
238 # insert an event | |
239 m.change_svc.addChange(Change("user", ["foo.c"], "comments")) | |
240 | |
241 d = client.getPage("http://localhost:%d/" % port) | |
242 | |
243 def _check1(page): | |
244 self.failUnless(page) | |
245 self.failUnlessIn("current activity", page) | |
246 self.failUnlessIn("<html", page) | |
247 TZ = time.tzname[time.localtime()[-1]] | |
248 self.failUnlessIn("time (%s)" % TZ, page) | |
249 | |
250 # phase=0 is really for debugging the waterfall layout | |
251 return client.getPage("http://localhost:%d/?phase=0" % self.port) | |
252 d.addCallback(_check1) | |
253 | |
254 def _check2(page): | |
255 self.failUnless(page) | |
256 self.failUnlessIn("<html", page) | |
257 | |
258 return client.getPage("http://localhost:%d/changes" % self.port) | |
259 d.addCallback(_check2) | |
260 | |
261 def _check3(changes): | |
262 self.failUnlessIn("<li>Syncmail mailing list in maildir " + | |
263 "my-maildir</li>", changes) | |
264 | |
265 return client.getPage("http://localhost:%d/robots.txt" % self.port) | |
266 d.addCallback(_check3) | |
267 | |
268 def _check4(robotstxt): | |
269 self.failUnless(robotstxt == self.robots_txt_contents) | |
270 d.addCallback(_check4) | |
271 | |
272 return d | |
273 | |
274 test_waterfall.timeout = 10 | |
275 | |
276 class WaterfallSteps(unittest.TestCase): | |
277 | |
278 # failUnlessSubstring copied from twisted-2.1.0, because this helps us | |
279 # maintain compatibility with python2.2. | |
280 def failUnlessSubstring(self, substring, astring, msg=None): | |
281 """a python2.2 friendly test to assert that substring is found in | |
282 astring parameters follow the semantics of failUnlessIn | |
283 """ | |
284 if astring.find(substring) == -1: | |
285 raise self.failureException(msg or "%r not found in %r" | |
286 % (substring, astring)) | |
287 return substring | |
288 assertSubstring = failUnlessSubstring | |
289 | |
290 def test_urls(self): | |
291 s = setupBuildStepStatus("test_web.test_urls") | |
292 s.addURL("coverage", "http://coverage.example.org/target") | |
293 s.addURL("icon", "http://coverage.example.org/icon.png") | |
294 class FakeRequest: | |
295 prepath = [] | |
296 postpath = [] | |
297 def childLink(self, name): | |
298 return name | |
299 req = FakeRequest() | |
300 box = waterfall.IBox(s).getBox(req) | |
301 td = box.td() | |
302 e1 = '[<a href="http://coverage.example.org/target" class="BuildStep ext
ernal">coverage</a>]' | |
303 self.failUnlessSubstring(e1, td) | |
304 e2 = '[<a href="http://coverage.example.org/icon.png" class="BuildStep e
xternal">icon</a>]' | |
305 self.failUnlessSubstring(e2, td) | |
306 | |
307 | |
308 | |
309 geturl_config = """ | |
310 from buildbot.status import html | |
311 from buildbot.changes import mail | |
312 from buildbot.process import factory | |
313 from buildbot.steps import dummy | |
314 from buildbot.scheduler import Scheduler | |
315 from buildbot.changes.base import ChangeSource | |
316 from buildbot.buildslave import BuildSlave | |
317 from buildbot.config import BuilderConfig | |
318 s = factory.s | |
319 | |
320 class DiscardScheduler(Scheduler): | |
321 def addChange(self, change): | |
322 pass | |
323 class DummyChangeSource(ChangeSource): | |
324 pass | |
325 | |
326 BuildmasterConfig = c = {} | |
327 c['slaves'] = [BuildSlave('bot1', 'sekrit'), BuildSlave('bot2', 'sekrit')] | |
328 c['change_source'] = DummyChangeSource() | |
329 c['schedulers'] = [DiscardScheduler('discard', None, 60, ['b1'])] | |
330 c['slavePortnum'] = 0 | |
331 c['status'] = [html.Waterfall(http_port=0)] | |
332 | |
333 f = factory.BuildFactory([s(dummy.RemoteDummy, timeout=1)]) | |
334 | |
335 c['builders'] = [ | |
336 BuilderConfig(name='b1', slavenames=['bot1', 'bot2'], factory=f), | |
337 ] | |
338 c['buildbotURL'] = 'http://dummy.example.org:8010/' | |
339 | |
340 """ | |
341 | |
342 class GetURL(RunMixin, unittest.TestCase): | |
343 def setUp(self): | |
344 warnings.filterwarnings("ignore", category=DeprecationWarning) | |
345 RunMixin.setUp(self) | |
346 self.master.loadConfig(geturl_config) | |
347 self.master.startService() | |
348 d = self.connectSlave(["b1"]) | |
349 return d | |
350 | |
351 def tearDown(self): | |
352 stopHTTPLog() | |
353 warnings.resetwarnings() | |
354 return RunMixin.tearDown(self) | |
355 | |
356 def doBuild(self, buildername): | |
357 br = base.BuildRequest("forced", sourcestamp.SourceStamp(), 'test_builde
r') | |
358 d = br.waitUntilFinished() | |
359 self.control.getBuilder(buildername).requestBuild(br) | |
360 return d | |
361 | |
362 def assertNoURL(self, target): | |
363 self.failUnlessIdentical(self.status.getURLForThing(target), None) | |
364 | |
365 def assertURLEqual(self, target, expected): | |
366 got = self.status.getURLForThing(target) | |
367 full_expected = "http://dummy.example.org:8010/" + expected | |
368 self.failUnlessEqual(got, full_expected) | |
369 | |
370 def testMissingBase(self): | |
371 noweb_config1 = geturl_config + "del c['buildbotURL']\n" | |
372 d = self.master.loadConfig(noweb_config1) | |
373 d.addCallback(self._testMissingBase_1) | |
374 return d | |
375 def _testMissingBase_1(self, res): | |
376 s = self.status | |
377 self.assertNoURL(s) | |
378 builder_s = s.getBuilder("b1") | |
379 self.assertNoURL(builder_s) | |
380 | |
381 def testBase(self): | |
382 s = self.status | |
383 self.assertURLEqual(s, "") | |
384 builder_s = s.getBuilder("b1") | |
385 self.assertURLEqual(builder_s, "builders/b1") | |
386 | |
387 def testChange(self): | |
388 s = self.status | |
389 c = Change("user", ["foo.c"], "comments") | |
390 self.master.change_svc.addChange(c) | |
391 # TODO: something more like s.getChanges(), requires IChange and | |
392 # an accessor in IStatus. The HTML page exists already, though | |
393 self.assertURLEqual(c, "changes/1") | |
394 | |
395 def testBuild(self): | |
396 # first we do some stuff so we'll have things to look at. | |
397 s = self.status | |
398 d = self.doBuild("b1") | |
399 # maybe check IBuildSetStatus here? | |
400 d.addCallback(self._testBuild_1) | |
401 return d | |
402 | |
403 def _testBuild_1(self, res): | |
404 s = self.status | |
405 builder_s = s.getBuilder("b1") | |
406 build_s = builder_s.getLastFinishedBuild() | |
407 self.assertURLEqual(build_s, "builders/b1/builds/0") | |
408 # no page for builder.getEvent(-1) | |
409 step = build_s.getSteps()[0] | |
410 self.assertURLEqual(step, "builders/b1/builds/0/steps/remote%20dummy") | |
411 # maybe page for build.getTestResults? | |
412 self.assertURLEqual(step.getLogs()[0], | |
413 "builders/b1/builds/0/steps/remote%20dummy/logs/stdi
o") | |
414 | |
415 | |
416 | |
417 class Logfile(BaseWeb, RunMixin, unittest.TestCase): | |
418 def setUp(self): | |
419 config = """ | |
420 from buildbot.status import html | |
421 from buildbot.process.factory import BasicBuildFactory | |
422 from buildbot.buildslave import BuildSlave | |
423 from buildbot.config import BuilderConfig | |
424 | |
425 f1 = BasicBuildFactory('cvsroot', 'cvsmodule') | |
426 BuildmasterConfig = c = { | |
427 'slaves': [BuildSlave('bot1', 'passwd1')], | |
428 'schedulers': [], | |
429 'slavePortnum': 0, | |
430 'status': [html.WebStatus(http_port=0)], | |
431 } | |
432 c['builders'] = [ | |
433 BuilderConfig(name='builder1', slavename='bot1', factory=f1), | |
434 ] | |
435 """ | |
436 if os.path.exists("test_logfile"): | |
437 shutil.rmtree("test_logfile") | |
438 os.mkdir("test_logfile") | |
439 self.master = m = ConfiguredMaster("test_logfile", config) | |
440 m.startService() | |
441 # hack to find out what randomly-assigned port it is listening on | |
442 port = self.find_webstatus(m).getPortnum() | |
443 self.port = port | |
444 # insert an event | |
445 | |
446 req = base.BuildRequest("reason", sourcestamp.SourceStamp(), 'test_build
er') | |
447 build1 = base.Build([req]) | |
448 bs = m.status.getBuilder("builder1").newBuild() | |
449 bs.setReason("reason") | |
450 bs.buildStarted(build1) | |
451 | |
452 step1 = BuildStep(name="setup") | |
453 step1.setBuild(build1) | |
454 bss = bs.addStepWithName("setup") | |
455 step1.setStepStatus(bss) | |
456 bss.stepStarted() | |
457 | |
458 log1 = step1.addLog("output") | |
459 log1.addStdout(u"sÒme stdout\n") | |
460 log1.finish() | |
461 | |
462 log2 = step1.addHTMLLog("error", "<html>ouch</html>") | |
463 | |
464 log3 = step1.addLog("big") | |
465 log3.addStdout("big log\n") | |
466 for i in range(1000): | |
467 log3.addStdout("a" * 500) | |
468 log3.addStderr("b" * 500) | |
469 log3.finish() | |
470 | |
471 log4 = step1.addCompleteLog("bigcomplete", | |
472 "big2 log\n" + "a" * 1*1000*1000) | |
473 | |
474 log5 = step1.addLog("mixed") | |
475 log5.addHeader("header content") | |
476 log5.addStdout("this is stdout content") | |
477 log5.addStderr("errors go here") | |
478 log5.addEntry(5, "non-standard content on channel 5") | |
479 log5.addStderr(" and some trailing stderr") | |
480 | |
481 d = defer.maybeDeferred(step1.step_status.stepFinished, | |
482 builder.SUCCESS) | |
483 bs.buildFinished() | |
484 return d | |
485 | |
486 def getLogPath(self, stepname, logname): | |
487 return ("/builders/builder1/builds/0/steps/%s/logs/%s" % | |
488 (stepname, logname)) | |
489 | |
490 def getLogURL(self, stepname, logname): | |
491 return ("http://localhost:%d" % self.port | |
492 + self.getLogPath(stepname, logname)) | |
493 | |
494 def test_logfile1(self): | |
495 d = client.getPage("http://localhost:%d/" % self.port) | |
496 def _check(page): | |
497 self.failUnless(page) | |
498 d.addCallback(_check) | |
499 return d | |
500 | |
501 def test_logfile2(self): | |
502 logurl = self.getLogURL("setup", "output") | |
503 d = client.getPage(logurl) | |
504 def _check(logbody): | |
505 self.failUnless(logbody) | |
506 d.addCallback(_check) | |
507 return d | |
508 | |
509 def test_logfile3(self): | |
510 logurl = self.getLogURL("setup", "output") | |
511 d = client.getPage(logurl + "/text") | |
512 def _check(logtext): | |
513 # verify utf-8 encoding. | |
514 self.failUnlessEqual(logtext, "sÒme stdout\n") | |
515 d.addCallback(_check) | |
516 return d | |
517 | |
518 def test_logfile4(self): | |
519 logurl = self.getLogURL("setup", "error") | |
520 d = client.getPage(logurl) | |
521 def _check(logbody): | |
522 self.failUnlessEqual(logbody, "<html>ouch</html>") | |
523 d.addCallback(_check) | |
524 return d | |
525 | |
526 def test_logfile5(self): | |
527 # this is log3, which is about 1MB in size, made up of alternating | |
528 # stdout/stderr chunks. buildbot-0.6.6, when run against | |
529 # twisted-1.3.0, fails to resume sending chunks after the client | |
530 # stalls for a few seconds, because of a recursive doWrite() call | |
531 # that was fixed in twisted-2.0.0 | |
532 p = SlowReader("GET %s HTTP/1.0\r\n\r\n" | |
533 % self.getLogPath("setup", "big")) | |
534 cf = CFactory(p) | |
535 c = reactor.connectTCP("localhost", self.port, cf) | |
536 d = p.d | |
537 def _check(res): | |
538 self.failUnlessIn("big log", p.data) | |
539 self.failUnlessIn("a"*100, p.data) | |
540 self.failUnless(p.count > 1*1000*1000) | |
541 d.addCallback(_check) | |
542 return d | |
543 | |
544 def test_logfile6(self): | |
545 # this is log4, which is about 1MB in size, one big chunk. | |
546 # buildbot-0.6.6 dies as the NetstringReceiver barfs on the | |
547 # saved logfile, because it was using one big chunk and exceeding | |
548 # NetstringReceiver.MAX_LENGTH | |
549 p = SlowReader("GET %s HTTP/1.0\r\n\r\n" | |
550 % self.getLogPath("setup", "bigcomplete")) | |
551 cf = CFactory(p) | |
552 c = reactor.connectTCP("localhost", self.port, cf) | |
553 d = p.d | |
554 def _check(res): | |
555 self.failUnlessIn("big2 log", p.data) | |
556 self.failUnlessIn("a"*100, p.data) | |
557 self.failUnless(p.count > 1*1000*1000) | |
558 d.addCallback(_check) | |
559 return d | |
560 | |
561 def test_logfile7(self): | |
562 # this is log5, with mixed content on the tree standard channels | |
563 # as well as on channel 5 | |
564 | |
565 class SpanParser(HTMLParser): | |
566 '''Parser subclass to gather all the log spans from the log page''' | |
567 def __init__(self, test): | |
568 self.spans = [] | |
569 self.test = test | |
570 self.inSpan = False | |
571 HTMLParser.__init__(self) | |
572 | |
573 def handle_starttag(self, tag, attrs): | |
574 if tag == 'span': | |
575 self.inSpan = True | |
576 cls = attrs[0] | |
577 self.test.failUnless(cls[0] == 'class') | |
578 self.spans.append([cls[1],'']) | |
579 | |
580 def handle_data(self, data): | |
581 if self.inSpan: | |
582 self.spans[-1][1] += data | |
583 | |
584 def handle_endtag(self, tag): | |
585 if tag == 'span': | |
586 self.inSpan = False | |
587 | |
588 logurl = self.getLogURL("setup", "mixed") | |
589 d = client.getPage(logurl, timeout=2) | |
590 def _check(logbody): | |
591 try: | |
592 p = SpanParser(self) | |
593 p.feed(logbody) | |
594 p.close | |
595 except Exception, e: | |
596 print e | |
597 self.failUnlessEqual(len(p.spans), 4) | |
598 self.failUnlessEqual(p.spans[0][0], 'header') | |
599 self.failUnlessEqual(p.spans[0][1], 'header content') | |
600 self.failUnlessEqual(p.spans[1][0], 'stdout') | |
601 self.failUnlessEqual(p.spans[1][1], 'this is stdout content') | |
602 self.failUnlessEqual(p.spans[2][0], 'stderr') | |
603 self.failUnlessEqual(p.spans[2][1], 'errors go here') | |
604 self.failUnlessEqual(p.spans[3][0], 'stderr') | |
605 self.failUnlessEqual(p.spans[3][1], ' and some trailing stderr') | |
606 def _fail(err): | |
607 pass | |
608 d.addCallback(_check) | |
609 d.addErrback(_fail) | |
610 return d | |
611 | |
612 class XMLRPC(unittest.TestCase): | |
613 def test_init(self): | |
614 server = xmlrpc.XMLRPCServer() | |
615 self.assert_(server) | |
616 | |
617 def test_render(self): | |
618 self.assertRaises(NameError, | |
619 lambda: | |
620 xmlrpc.XMLRPCServer().render(Request())) | |
OLD | NEW |