1 # -*- test-case-name: buildbot.test.test_web -*-
3 import os
, time
, shutil
4 from HTMLParser
import HTMLParser
5 from twisted
.python
import components
7 from twisted
.trial
import unittest
8 from buildbot
.test
.runutils
import RunMixin
10 from twisted
.internet
import reactor
, defer
, protocol
11 from twisted
.internet
.interfaces
import IReactorUNIX
12 from twisted
.web
import client
14 from buildbot
import master
, interfaces
, sourcestamp
15 from buildbot
.status
import html
, builder
16 from buildbot
.status
.web
import waterfall
17 from buildbot
.changes
.changes
import Change
18 from buildbot
.process
import base
19 from buildbot
.process
.buildstep
import BuildStep
20 from buildbot
.test
.runutils
import setupBuildStepStatus
22 class ConfiguredMaster(master
.BuildMaster
):
23 """This BuildMaster variant has a static config file, provided as a
24 string when it is created."""
26 def __init__(self
, basedir
, config
):
28 master
.BuildMaster
.__init
__(self
, basedir
)
30 def loadTheConfigFile(self
):
31 self
.loadConfig(self
.config
)
33 components
.registerAdapter(master
.Control
, ConfiguredMaster
,
38 from buildbot.changes.pb import PBChangeSource
39 from buildbot.status import html
40 from buildbot.buildslave import BuildSlave
41 from buildbot.scheduler import Scheduler
42 from buildbot.process.factory import BuildFactory
44 BuildmasterConfig = c = {
45 'change_source': PBChangeSource(),
46 'slaves': [BuildSlave('bot1name', 'bot1passwd')],
47 'schedulers': [Scheduler('name', None, 60, ['builder1'])],
48 'builders': [{'name': 'builder1', 'slavename': 'bot1name',
49 'builddir': 'builder1', 'factory': BuildFactory()}],
57 def __init__(self
, unixpath
):
58 from twisted
.web
import server
, resource
, distrib
59 root
= resource
.Resource()
60 self
.r
= r
= distrib
.ResourceSubscription("unix", unixpath
)
61 root
.putChild('remote', r
)
62 self
.p
= p
= reactor
.listenTCP(0, server
.Site(root
))
63 self
.portnum
= p
.getHost().port
65 d
= defer
.maybeDeferred(self
.p
.stopListening
)
69 def __init__(self
, port
):
70 from twisted
.web
import server
, resource
, distrib
71 root
= resource
.Resource()
72 self
.r
= r
= distrib
.ResourceSubscription("localhost", port
)
73 root
.putChild('remote', r
)
74 self
.p
= p
= reactor
.listenTCP(0, server
.Site(root
))
75 self
.portnum
= p
.getHost().port
77 d
= defer
.maybeDeferred(self
.p
.stopListening
)
78 d
.addCallback(self
._shutdown
_1)
80 def _shutdown_1(self
, res
):
81 return self
.r
.publisher
.broker
.transport
.loseConnection()
83 class SlowReader(protocol
.Protocol
):
87 def __init__(self
, req
):
89 self
.d
= defer
.Deferred()
90 def connectionMade(self
):
91 self
.transport
.write(self
.req
)
92 def dataReceived(self
, data
):
94 self
.count
+= len(data
)
95 if not self
.didPause
and self
.count
> 10*1000:
97 self
.transport
.pauseProducing()
98 reactor
.callLater(2, self
.resume
)
100 self
.transport
.resumeProducing()
101 def connectionLost(self
, why
):
102 self
.d
.callback(None)
104 class CFactory(protocol
.ClientFactory
):
105 def __init__(self
, p
):
107 def buildProtocol(self
, addr
):
108 self
.p
.factory
= self
113 from twisted
.web
import http
114 http
._logDateTimeStop
()
119 def failUnlessIn(self
, substr
, string
, note
=None):
120 self
.failUnless(string
.find(substr
) != -1, note
)
125 d
= self
.master
.stopService()
128 def find_webstatus(self
, master
):
129 for child
in list(master
):
130 if isinstance(child
, html
.WebStatus
):
133 def find_waterfall(self
, master
):
134 for child
in list(master
):
135 if isinstance(child
, html
.Waterfall
):
138 class Ports(BaseWeb
, unittest
.TestCase
):
140 def test_webPortnum(self
):
141 # run a regular web server on a TCP socket
142 config
= base_config
+ "c['status'] = [html.WebStatus(http_port=0)]\n"
143 os
.mkdir("test_web1")
144 self
.master
= m
= ConfiguredMaster("test_web1", config
)
146 # hack to find out what randomly-assigned port it is listening on
147 port
= self
.find_webstatus(m
).getPortnum()
149 d
= client
.getPage("http://localhost:%d/waterfall" % port
)
152 self
.failUnless(page
)
153 d
.addCallback(_check
)
155 test_webPortnum
.timeout
= 10
157 def test_webPathname(self
):
158 # running a t.web.distrib server over a UNIX socket
159 if not IReactorUNIX
.providedBy(reactor
):
160 raise unittest
.SkipTest("UNIX sockets not supported here")
161 config
= (base_config
+
162 "c['status'] = [html.WebStatus(distrib_port='.web-pb')]\n")
163 os
.mkdir("test_web2")
164 self
.master
= m
= ConfiguredMaster("test_web2", config
)
167 p
= DistribUNIX("test_web2/.web-pb")
169 d
= client
.getPage("http://localhost:%d/remote/waterfall" % p
.portnum
)
171 self
.failUnless(page
)
172 d
.addCallback(_check
)
175 d1
.addCallback(lambda x
: res
)
179 test_webPathname
.timeout
= 10
182 def test_webPathname_port(self
):
183 # running a t.web.distrib server over TCP
184 config
= (base_config
+
185 "c['status'] = [html.WebStatus(distrib_port=0)]\n")
186 os
.mkdir("test_web3")
187 self
.master
= m
= ConfiguredMaster("test_web3", config
)
189 dport
= self
.find_webstatus(m
).getPortnum()
191 p
= DistribTCP(dport
)
193 d
= client
.getPage("http://localhost:%d/remote/waterfall" % p
.portnum
)
195 self
.failUnlessIn("BuildBot", page
)
196 d
.addCallback(_check
)
199 d1
.addCallback(lambda x
: res
)
203 test_webPathname_port
.timeout
= 10
206 class Waterfall(BaseWeb
, unittest
.TestCase
):
207 def test_waterfall(self
):
208 os
.mkdir("test_web4")
209 os
.mkdir("my-maildir"); os
.mkdir("my-maildir/new")
210 self
.robots_txt
= os
.path
.abspath(os
.path
.join("test_web4",
212 self
.robots_txt_contents
= "User-agent: *\nDisallow: /\n"
213 f
= open(self
.robots_txt
, "w")
214 f
.write(self
.robots_txt_contents
)
216 # this is the right way to configure the Waterfall status
217 config1
= base_config
+ """
218 from buildbot.changes import mail
219 c['change_source'] = mail.SyncmailMaildirSource('my-maildir')
220 c['status'] = [html.Waterfall(http_port=0, robots_txt=%s)]
221 """ % repr(self
.robots_txt
)
223 self
.master
= m
= ConfiguredMaster("test_web4", config1
)
225 port
= self
.find_waterfall(m
).getPortnum()
228 m
.change_svc
.addChange(Change("user", ["foo.c"], "comments"))
230 d
= client
.getPage("http://localhost:%d/" % port
)
233 self
.failUnless(page
)
234 self
.failUnlessIn("current activity", page
)
235 self
.failUnlessIn("<html", page
)
236 TZ
= time
.tzname
[time
.localtime()[-1]]
237 self
.failUnlessIn("time (%s)" % TZ
, page
)
239 # phase=0 is really for debugging the waterfall layout
240 return client
.getPage("http://localhost:%d/?phase=0" % self
.port
)
241 d
.addCallback(_check1
)
244 self
.failUnless(page
)
245 self
.failUnlessIn("<html", page
)
247 return client
.getPage("http://localhost:%d/changes" % self
.port
)
248 d
.addCallback(_check2
)
250 def _check3(changes
):
251 self
.failUnlessIn("<li>Syncmail mailing list in maildir " +
252 "my-maildir</li>", changes
)
254 return client
.getPage("http://localhost:%d/robots.txt" % self
.port
)
255 d
.addCallback(_check3
)
257 def _check4(robotstxt
):
258 self
.failUnless(robotstxt
== self
.robots_txt_contents
)
259 d
.addCallback(_check4
)
263 test_waterfall
.timeout
= 10
265 class WaterfallSteps(unittest
.TestCase
):
267 # failUnlessSubstring copied from twisted-2.1.0, because this helps us
268 # maintain compatibility with python2.2.
269 def failUnlessSubstring(self
, substring
, astring
, msg
=None):
270 """a python2.2 friendly test to assert that substring is found in
271 astring parameters follow the semantics of failUnlessIn
273 if astring
.find(substring
) == -1:
274 raise self
.failureException(msg
or "%r not found in %r"
275 % (substring
, astring
))
277 assertSubstring
= failUnlessSubstring
280 s
= setupBuildStepStatus("test_web.test_urls")
281 s
.addURL("coverage", "http://coverage.example.org/target")
282 s
.addURL("icon", "http://coverage.example.org/icon.png")
286 def childLink(self
, name
):
289 box
= waterfall
.IBox(s
).getBox(req
)
291 e1
= '[<a href="http://coverage.example.org/target" class="BuildStep external">coverage</a>]'
292 self
.failUnlessSubstring(e1
, td
)
293 e2
= '[<a href="http://coverage.example.org/icon.png" class="BuildStep external">icon</a>]'
294 self
.failUnlessSubstring(e2
, td
)
299 from buildbot.status import html
300 from buildbot.changes import mail
301 from buildbot.process import factory
302 from buildbot.steps import dummy
303 from buildbot.scheduler import Scheduler
304 from buildbot.changes.base import ChangeSource
305 from buildbot.buildslave import BuildSlave
308 class DiscardScheduler(Scheduler):
309 def addChange(self, change):
311 class DummyChangeSource(ChangeSource):
314 BuildmasterConfig = c = {}
315 c['slaves'] = [BuildSlave('bot1', 'sekrit'), BuildSlave('bot2', 'sekrit')]
316 c['change_source'] = DummyChangeSource()
317 c['schedulers'] = [DiscardScheduler('discard', None, 60, ['b1'])]
318 c['slavePortnum'] = 0
319 c['status'] = [html.Waterfall(http_port=0)]
321 f = factory.BuildFactory([s(dummy.RemoteDummy, timeout=1)])
324 {'name': 'b1', 'slavenames': ['bot1','bot2'],
325 'builddir': 'b1', 'factory': f},
327 c['buildbotURL'] = 'http://dummy.example.org:8010/'
331 class GetURL(RunMixin
, unittest
.TestCase
):
335 self
.master
.loadConfig(geturl_config
)
336 self
.master
.startService()
337 d
= self
.connectSlave(["b1"])
342 return RunMixin
.tearDown(self
)
344 def doBuild(self
, buildername
):
345 br
= base
.BuildRequest("forced", sourcestamp
.SourceStamp())
346 d
= br
.waitUntilFinished()
347 self
.control
.getBuilder(buildername
).requestBuild(br
)
350 def assertNoURL(self
, target
):
351 self
.failUnlessIdentical(self
.status
.getURLForThing(target
), None)
353 def assertURLEqual(self
, target
, expected
):
354 got
= self
.status
.getURLForThing(target
)
355 full_expected
= "http://dummy.example.org:8010/" + expected
356 self
.failUnlessEqual(got
, full_expected
)
358 def testMissingBase(self
):
359 noweb_config1
= geturl_config
+ "del c['buildbotURL']\n"
360 d
= self
.master
.loadConfig(noweb_config1
)
361 d
.addCallback(self
._testMissingBase
_1)
363 def _testMissingBase_1(self
, res
):
366 builder_s
= s
.getBuilder("b1")
367 self
.assertNoURL(builder_s
)
371 self
.assertURLEqual(s
, "")
372 builder_s
= s
.getBuilder("b1")
373 self
.assertURLEqual(builder_s
, "builders/b1")
375 def testChange(self
):
377 c
= Change("user", ["foo.c"], "comments")
378 self
.master
.change_svc
.addChange(c
)
379 # TODO: something more like s.getChanges(), requires IChange and
380 # an accessor in IStatus. The HTML page exists already, though
381 self
.assertURLEqual(c
, "changes/1")
384 # first we do some stuff so we'll have things to look at.
386 d
= self
.doBuild("b1")
387 # maybe check IBuildSetStatus here?
388 d
.addCallback(self
._testBuild
_1)
391 def _testBuild_1(self
, res
):
393 builder_s
= s
.getBuilder("b1")
394 build_s
= builder_s
.getLastFinishedBuild()
395 self
.assertURLEqual(build_s
, "builders/b1/builds/0")
396 # no page for builder.getEvent(-1)
397 step
= build_s
.getSteps()[0]
398 self
.assertURLEqual(step
, "builders/b1/builds/0/steps/remote%20dummy")
399 # maybe page for build.getTestResults?
400 self
.assertURLEqual(step
.getLogs()[0],
401 "builders/b1/builds/0/steps/remote%20dummy/logs/0")
405 class Logfile(BaseWeb
, RunMixin
, unittest
.TestCase
):
408 from buildbot.status import html
409 from buildbot.process.factory import BasicBuildFactory
410 from buildbot.buildslave import BuildSlave
411 f1 = BasicBuildFactory('cvsroot', 'cvsmodule')
412 BuildmasterConfig = {
413 'slaves': [BuildSlave('bot1', 'passwd1')],
415 'builders': [{'name': 'builder1', 'slavename': 'bot1',
416 'builddir':'workdir', 'factory':f1}],
418 'status': [html.WebStatus(http_port=0)],
421 if os
.path
.exists("test_logfile"):
422 shutil
.rmtree("test_logfile")
423 os
.mkdir("test_logfile")
424 self
.master
= m
= ConfiguredMaster("test_logfile", config
)
426 # hack to find out what randomly-assigned port it is listening on
427 port
= self
.find_webstatus(m
).getPortnum()
431 req
= base
.BuildRequest("reason", sourcestamp
.SourceStamp())
432 build1
= base
.Build([req
])
433 bs
= m
.status
.getBuilder("builder1").newBuild()
434 bs
.setReason("reason")
435 bs
.buildStarted(build1
)
437 step1
= BuildStep(name
="setup")
438 step1
.setBuild(build1
)
439 bss
= bs
.addStepWithName("setup")
440 step1
.setStepStatus(bss
)
443 log1
= step1
.addLog("output")
444 log1
.addStdout("some stdout\n")
447 log2
= step1
.addHTMLLog("error", "<html>ouch</html>")
449 log3
= step1
.addLog("big")
450 log3
.addStdout("big log\n")
451 for i
in range(1000):
452 log3
.addStdout("a" * 500)
453 log3
.addStderr("b" * 500)
456 log4
= step1
.addCompleteLog("bigcomplete",
457 "big2 log\n" + "a" * 1*1000*1000)
459 log5
= step1
.addLog("mixed")
460 log5
.addHeader("header content")
461 log5
.addStdout("this is stdout content")
462 log5
.addStderr("errors go here")
463 log5
.addEntry(5, "non-standard content on channel 5")
464 log5
.addStderr(" and some trailing stderr")
466 d
= defer
.maybeDeferred(step1
.step_status
.stepFinished
,
471 def getLogPath(self
, stepname
, logname
):
472 return ("/builders/builder1/builds/0/steps/%s/logs/%s" %
475 def getLogURL(self
, stepname
, logname
):
476 return ("http://localhost:%d" % self
.port
477 + self
.getLogPath(stepname
, logname
))
479 def test_logfile1(self
):
480 d
= client
.getPage("http://localhost:%d/" % self
.port
)
482 self
.failUnless(page
)
483 d
.addCallback(_check
)
486 def test_logfile2(self
):
487 logurl
= self
.getLogURL("setup", "output")
488 d
= client
.getPage(logurl
)
490 self
.failUnless(logbody
)
491 d
.addCallback(_check
)
494 def test_logfile3(self
):
495 logurl
= self
.getLogURL("setup", "output")
496 d
= client
.getPage(logurl
+ "/text")
498 self
.failUnlessEqual(logtext
, "some stdout\n")
499 d
.addCallback(_check
)
502 def test_logfile4(self
):
503 logurl
= self
.getLogURL("setup", "error")
504 d
= client
.getPage(logurl
)
506 self
.failUnlessEqual(logbody
, "<html>ouch</html>")
507 d
.addCallback(_check
)
510 def test_logfile5(self
):
511 # this is log3, which is about 1MB in size, made up of alternating
512 # stdout/stderr chunks. buildbot-0.6.6, when run against
513 # twisted-1.3.0, fails to resume sending chunks after the client
514 # stalls for a few seconds, because of a recursive doWrite() call
515 # that was fixed in twisted-2.0.0
516 p
= SlowReader("GET %s HTTP/1.0\r\n\r\n"
517 % self
.getLogPath("setup", "big"))
519 c
= reactor
.connectTCP("localhost", self
.port
, cf
)
522 self
.failUnlessIn("big log", p
.data
)
523 self
.failUnlessIn("a"*100, p
.data
)
524 self
.failUnless(p
.count
> 1*1000*1000)
525 d
.addCallback(_check
)
528 def test_logfile6(self
):
529 # this is log4, which is about 1MB in size, one big chunk.
530 # buildbot-0.6.6 dies as the NetstringReceiver barfs on the
531 # saved logfile, because it was using one big chunk and exceeding
532 # NetstringReceiver.MAX_LENGTH
533 p
= SlowReader("GET %s HTTP/1.0\r\n\r\n"
534 % self
.getLogPath("setup", "bigcomplete"))
536 c
= reactor
.connectTCP("localhost", self
.port
, cf
)
539 self
.failUnlessIn("big2 log", p
.data
)
540 self
.failUnlessIn("a"*100, p
.data
)
541 self
.failUnless(p
.count
> 1*1000*1000)
542 d
.addCallback(_check
)
545 def test_logfile7(self
):
546 # this is log5, with mixed content on the tree standard channels
547 # as well as on channel 5
549 class SpanParser(HTMLParser
):
550 '''Parser subclass to gather all the log spans from the log page'''
551 def __init__(self
, test
):
555 HTMLParser
.__init
__(self
)
557 def handle_starttag(self
, tag
, attrs
):
561 self
.test
.failUnless(cls
[0] == 'class')
562 self
.spans
.append([cls
[1],''])
564 def handle_data(self
, data
):
566 self
.spans
[-1][1] += data
568 def handle_endtag(self
, tag
):
572 logurl
= self
.getLogURL("setup", "mixed")
573 d
= client
.getPage(logurl
, timeout
=2)
581 self
.failUnlessEqual(len(p
.spans
), 4)
582 self
.failUnlessEqual(p
.spans
[0][0], 'header')
583 self
.failUnlessEqual(p
.spans
[0][1], 'header content')
584 self
.failUnlessEqual(p
.spans
[1][0], 'stdout')
585 self
.failUnlessEqual(p
.spans
[1][1], 'this is stdout content')
586 self
.failUnlessEqual(p
.spans
[2][0], 'stderr')
587 self
.failUnlessEqual(p
.spans
[2][1], 'errors go here')
588 self
.failUnlessEqual(p
.spans
[3][0], 'stderr')
589 self
.failUnlessEqual(p
.spans
[3][1], ' and some trailing stderr')
592 d
.addCallback(_check
)