1 # -*- test-case-name: buildbot.test.test_web -*-
3 import os
, time
, shutil
4 from twisted
.python
import components
6 from twisted
.trial
import unittest
7 from buildbot
.test
.runutils
import RunMixin
9 from twisted
.internet
import reactor
, defer
, protocol
10 from twisted
.internet
.interfaces
import IReactorUNIX
11 from twisted
.web
import client
13 from buildbot
import master
, interfaces
, sourcestamp
14 from buildbot
.status
import html
, builder
15 from buildbot
.status
.web
import waterfall
16 from buildbot
.changes
.changes
import Change
17 from buildbot
.process
import base
18 from buildbot
.process
.buildstep
import BuildStep
19 from buildbot
.test
.runutils
import setupBuildStepStatus
21 class ConfiguredMaster(master
.BuildMaster
):
22 """This BuildMaster variant has a static config file, provided as a
23 string when it is created."""
25 def __init__(self
, basedir
, config
):
27 master
.BuildMaster
.__init
__(self
, basedir
)
29 def loadTheConfigFile(self
):
30 self
.loadConfig(self
.config
)
32 components
.registerAdapter(master
.Control
, ConfiguredMaster
,
37 from buildbot.status import html
38 BuildmasterConfig = c = {
49 def __init__(self
, unixpath
):
50 from twisted
.web
import server
, resource
, distrib
51 root
= resource
.Resource()
52 self
.r
= r
= distrib
.ResourceSubscription("unix", unixpath
)
53 root
.putChild('remote', r
)
54 self
.p
= p
= reactor
.listenTCP(0, server
.Site(root
))
55 self
.portnum
= p
.getHost().port
57 d
= defer
.maybeDeferred(self
.p
.stopListening
)
61 def __init__(self
, port
):
62 from twisted
.web
import server
, resource
, distrib
63 root
= resource
.Resource()
64 self
.r
= r
= distrib
.ResourceSubscription("localhost", port
)
65 root
.putChild('remote', r
)
66 self
.p
= p
= reactor
.listenTCP(0, server
.Site(root
))
67 self
.portnum
= p
.getHost().port
69 d
= defer
.maybeDeferred(self
.p
.stopListening
)
70 d
.addCallback(self
._shutdown
_1)
72 def _shutdown_1(self
, res
):
73 return self
.r
.publisher
.broker
.transport
.loseConnection()
75 class SlowReader(protocol
.Protocol
):
79 def __init__(self
, req
):
81 self
.d
= defer
.Deferred()
82 def connectionMade(self
):
83 self
.transport
.write(self
.req
)
84 def dataReceived(self
, data
):
86 self
.count
+= len(data
)
87 if not self
.didPause
and self
.count
> 10*1000:
89 self
.transport
.pauseProducing()
90 reactor
.callLater(2, self
.resume
)
92 self
.transport
.resumeProducing()
93 def connectionLost(self
, why
):
96 class CFactory(protocol
.ClientFactory
):
97 def __init__(self
, p
):
99 def buildProtocol(self
, addr
):
100 self
.p
.factory
= self
105 from twisted
.web
import http
106 http
._logDateTimeStop
()
111 def failUnlessIn(self
, substr
, string
):
112 self
.failUnless(string
.find(substr
) != -1)
117 d
= self
.master
.stopService()
120 def find_webstatus(self
, master
):
121 for child
in list(master
):
122 if isinstance(child
, html
.WebStatus
):
125 def find_waterfall(self
, master
):
126 for child
in list(master
):
127 if isinstance(child
, html
.Waterfall
):
130 class Ports(BaseWeb
, unittest
.TestCase
):
132 def test_webPortnum(self
):
133 # run a regular web server on a TCP socket
134 config
= base_config
+ "c['status'] = [html.WebStatus(http_port=0)]\n"
135 os
.mkdir("test_web1")
136 self
.master
= m
= ConfiguredMaster("test_web1", config
)
138 # hack to find out what randomly-assigned port it is listening on
139 port
= self
.find_webstatus(m
).getPortnum()
141 d
= client
.getPage("http://localhost:%d/waterfall" % port
)
144 self
.failUnless(page
)
145 d
.addCallback(_check
)
147 test_webPortnum
.timeout
= 10
149 def test_webPathname(self
):
150 # running a t.web.distrib server over a UNIX socket
151 if not IReactorUNIX
.providedBy(reactor
):
152 raise unittest
.SkipTest("UNIX sockets not supported here")
153 config
= (base_config
+
154 "c['status'] = [html.WebStatus(distrib_port='.web-pb')]\n")
155 os
.mkdir("test_web2")
156 self
.master
= m
= ConfiguredMaster("test_web2", config
)
159 p
= DistribUNIX("test_web2/.web-pb")
161 d
= client
.getPage("http://localhost:%d/remote/waterfall" % p
.portnum
)
163 self
.failUnless(page
)
164 d
.addCallback(_check
)
167 d1
.addCallback(lambda x
: res
)
171 test_webPathname
.timeout
= 10
174 def test_webPathname_port(self
):
175 # running a t.web.distrib server over TCP
176 config
= (base_config
+
177 "c['status'] = [html.WebStatus(distrib_port=0)]\n")
178 os
.mkdir("test_web3")
179 self
.master
= m
= ConfiguredMaster("test_web3", config
)
181 dport
= self
.find_webstatus(m
).getPortnum()
183 p
= DistribTCP(dport
)
185 d
= client
.getPage("http://localhost:%d/remote/waterfall" % p
.portnum
)
187 self
.failUnlessIn("BuildBot", page
)
188 d
.addCallback(_check
)
191 d1
.addCallback(lambda x
: res
)
195 test_webPathname_port
.timeout
= 10
198 class Waterfall(BaseWeb
, unittest
.TestCase
):
199 def test_waterfall(self
):
200 os
.mkdir("test_web4")
201 os
.mkdir("my-maildir"); os
.mkdir("my-maildir/new")
202 self
.robots_txt
= os
.path
.abspath(os
.path
.join("test_web4",
204 self
.robots_txt_contents
= "User-agent: *\nDisallow: /\n"
205 f
= open(self
.robots_txt
, "w")
206 f
.write(self
.robots_txt_contents
)
208 # this is the right way to configure the Waterfall status
209 config1
= base_config
+ """
210 from buildbot.changes import mail
211 c['change_source'] = mail.SyncmailMaildirSource('my-maildir')
212 c['status'] = [html.Waterfall(http_port=0, robots_txt=%s)]
213 """ % repr(self
.robots_txt
)
215 self
.master
= m
= ConfiguredMaster("test_web4", config1
)
217 port
= self
.find_waterfall(m
).getPortnum()
220 m
.change_svc
.addChange(Change("user", ["foo.c"], "comments"))
222 d
= client
.getPage("http://localhost:%d/" % port
)
225 self
.failUnless(page
)
226 self
.failUnlessIn("current activity", page
)
227 self
.failUnlessIn("<html", page
)
228 TZ
= time
.tzname
[time
.daylight
]
229 self
.failUnlessIn("time (%s)" % TZ
, page
)
231 # phase=0 is really for debugging the waterfall layout
232 return client
.getPage("http://localhost:%d/?phase=0" % self
.port
)
233 d
.addCallback(_check1
)
236 self
.failUnless(page
)
237 self
.failUnlessIn("<html", page
)
239 return client
.getPage("http://localhost:%d/changes" % self
.port
)
240 d
.addCallback(_check2
)
242 def _check3(changes
):
243 self
.failUnlessIn("<li>Syncmail mailing list in maildir " +
244 "my-maildir</li>", changes
)
246 return client
.getPage("http://localhost:%d/robots.txt" % self
.port
)
247 d
.addCallback(_check3
)
249 def _check4(robotstxt
):
250 self
.failUnless(robotstxt
== self
.robots_txt_contents
)
251 d
.addCallback(_check4
)
255 test_waterfall
.timeout
= 10
257 class WaterfallSteps(unittest
.TestCase
):
259 # failUnlessSubstring copied from twisted-2.1.0, because this helps us
260 # maintain compatibility with python2.2.
261 def failUnlessSubstring(self
, substring
, astring
, msg
=None):
262 """a python2.2 friendly test to assert that substring is found in
263 astring parameters follow the semantics of failUnlessIn
265 if astring
.find(substring
) == -1:
266 raise self
.failureException(msg
or "%r not found in %r"
267 % (substring
, astring
))
269 assertSubstring
= failUnlessSubstring
272 s
= setupBuildStepStatus("test_web.test_urls")
273 s
.addURL("coverage", "http://coverage.example.org/target")
274 s
.addURL("icon", "http://coverage.example.org/icon.png")
275 box
= waterfall
.IBox(s
).getBox()
277 e1
= '[<a href="http://coverage.example.org/target" class="BuildStep external">coverage</a>]'
278 self
.failUnlessSubstring(e1
, td
)
279 e2
= '[<a href="http://coverage.example.org/icon.png" class="BuildStep external">icon</a>]'
280 self
.failUnlessSubstring(e2
, td
)
285 from buildbot.status import html
286 from buildbot.changes import mail
287 from buildbot.process import factory
288 from buildbot.steps import dummy
289 from buildbot.scheduler import Scheduler
290 from buildbot.changes.base import ChangeSource
291 from buildbot.buildslave import BuildSlave
294 class DiscardScheduler(Scheduler):
295 def addChange(self, change):
297 class DummyChangeSource(ChangeSource):
300 BuildmasterConfig = c = {}
301 c['slaves'] = [BuildSlave('bot1', 'sekrit'), BuildSlave('bot2', 'sekrit')]
302 c['change_source'] = DummyChangeSource()
303 c['schedulers'] = [DiscardScheduler('discard', None, 60, ['b1'])]
304 c['slavePortnum'] = 0
305 c['status'] = [html.Waterfall(http_port=0)]
307 f = factory.BuildFactory([s(dummy.RemoteDummy, timeout=1)])
310 {'name': 'b1', 'slavenames': ['bot1','bot2'],
311 'builddir': 'b1', 'factory': f},
313 c['buildbotURL'] = 'http://dummy.example.org:8010/'
317 class GetURL(RunMixin
, unittest
.TestCase
):
321 self
.master
.loadConfig(geturl_config
)
322 self
.master
.startService()
323 d
= self
.connectSlave(["b1"])
328 return RunMixin
.tearDown(self
)
330 def doBuild(self
, buildername
):
331 br
= base
.BuildRequest("forced", sourcestamp
.SourceStamp())
332 d
= br
.waitUntilFinished()
333 self
.control
.getBuilder(buildername
).requestBuild(br
)
336 def assertNoURL(self
, target
):
337 self
.failUnlessIdentical(self
.status
.getURLForThing(target
), None)
339 def assertURLEqual(self
, target
, expected
):
340 got
= self
.status
.getURLForThing(target
)
341 full_expected
= "http://dummy.example.org:8010/" + expected
342 self
.failUnlessEqual(got
, full_expected
)
344 def testMissingBase(self
):
345 noweb_config1
= geturl_config
+ "del c['buildbotURL']\n"
346 d
= self
.master
.loadConfig(noweb_config1
)
347 d
.addCallback(self
._testMissingBase
_1)
349 def _testMissingBase_1(self
, res
):
352 builder
= s
.getBuilder("b1")
353 self
.assertNoURL(builder
)
357 self
.assertURLEqual(s
, "")
358 builder
= s
.getBuilder("b1")
359 self
.assertURLEqual(builder
, "b1")
361 def testChange(self
):
363 c
= Change("user", ["foo.c"], "comments")
364 self
.master
.change_svc
.addChange(c
)
365 # TODO: something more like s.getChanges(), requires IChange and
366 # an accessor in IStatus. The HTML page exists already, though
367 self
.assertURLEqual(c
, "changes/1")
370 # first we do some stuff so we'll have things to look at.
372 d
= self
.doBuild("b1")
373 # maybe check IBuildSetStatus here?
374 d
.addCallback(self
._testBuild
_1)
377 def _testBuild_1(self
, res
):
379 builder
= s
.getBuilder("b1")
380 build
= builder
.getLastFinishedBuild()
381 self
.assertURLEqual(build
, "b1/builds/0")
382 # no page for builder.getEvent(-1)
383 step
= build
.getSteps()[0]
384 self
.assertURLEqual(step
, "b1/builds/0/step-remote%20dummy")
385 # maybe page for build.getTestResults?
386 self
.assertURLEqual(step
.getLogs()[0],
387 "b1/builds/0/step-remote%20dummy/0")
391 class Logfile(BaseWeb
, RunMixin
, unittest
.TestCase
):
394 from buildbot.status import html
395 from buildbot.process.factory import BasicBuildFactory
396 from buildbot.buildslave import BuildSlave
397 f1 = BasicBuildFactory('cvsroot', 'cvsmodule')
398 BuildmasterConfig = {
399 'slaves': [BuildSlave('bot1', 'passwd1')],
401 'builders': [{'name': 'builder1', 'slavename': 'bot1',
402 'builddir':'workdir', 'factory':f1}],
404 'status': [html.WebStatus(http_port=0)],
407 if os
.path
.exists("test_logfile"):
408 shutil
.rmtree("test_logfile")
409 os
.mkdir("test_logfile")
410 self
.master
= m
= ConfiguredMaster("test_logfile", config
)
412 # hack to find out what randomly-assigned port it is listening on
413 port
= self
.find_webstatus(m
).getPortnum()
417 req
= base
.BuildRequest("reason", sourcestamp
.SourceStamp())
418 build1
= base
.Build([req
])
419 bs
= m
.status
.getBuilder("builder1").newBuild()
420 bs
.setReason("reason")
421 bs
.buildStarted(build1
)
423 step1
= BuildStep(name
="setup")
424 step1
.setBuild(build1
)
425 bss
= bs
.addStepWithName("setup")
426 step1
.setStepStatus(bss
)
429 log1
= step1
.addLog("output")
430 log1
.addStdout("some stdout\n")
433 log2
= step1
.addHTMLLog("error", "<html>ouch</html>")
435 log3
= step1
.addLog("big")
436 log3
.addStdout("big log\n")
437 for i
in range(1000):
438 log3
.addStdout("a" * 500)
439 log3
.addStderr("b" * 500)
442 log4
= step1
.addCompleteLog("bigcomplete",
443 "big2 log\n" + "a" * 1*1000*1000)
445 step1
.step_status
.stepFinished(builder
.SUCCESS
)
448 def getLogPath(self
, stepname
, logname
):
449 return ("/builders/builder1/builds/0/steps/%s/logs/%s" %
452 def getLogURL(self
, stepname
, logname
):
453 return ("http://localhost:%d" % self
.port
454 + self
.getLogPath(stepname
, logname
))
456 def test_logfile1(self
):
457 d
= client
.getPage("http://localhost:%d/" % self
.port
)
459 self
.failUnless(page
)
460 d
.addCallback(_check
)
463 def test_logfile2(self
):
464 logurl
= self
.getLogURL("setup", "output")
465 d
= client
.getPage(logurl
)
467 self
.failUnless(logbody
)
468 d
.addCallback(_check
)
471 def test_logfile3(self
):
472 logurl
= self
.getLogURL("setup", "output")
473 d
= client
.getPage(logurl
+ "/text")
475 self
.failUnlessEqual(logtext
, "some stdout\n")
476 d
.addCallback(_check
)
479 def test_logfile4(self
):
480 logurl
= self
.getLogURL("setup", "error")
481 d
= client
.getPage(logurl
)
483 self
.failUnlessEqual(logbody
, "<html>ouch</html>")
484 d
.addCallback(_check
)
487 def test_logfile5(self
):
488 # this is log3, which is about 1MB in size, made up of alternating
489 # stdout/stderr chunks. buildbot-0.6.6, when run against
490 # twisted-1.3.0, fails to resume sending chunks after the client
491 # stalls for a few seconds, because of a recursive doWrite() call
492 # that was fixed in twisted-2.0.0
493 p
= SlowReader("GET %s HTTP/1.0\r\n\r\n"
494 % self
.getLogPath("setup", "big"))
496 c
= reactor
.connectTCP("localhost", self
.port
, cf
)
499 self
.failUnlessIn("big log", p
.data
)
500 self
.failUnlessIn("a"*100, p
.data
)
501 self
.failUnless(p
.count
> 1*1000*1000)
502 d
.addCallback(_check
)
505 def test_logfile6(self
):
506 # this is log4, which is about 1MB in size, one big chunk.
507 # buildbot-0.6.6 dies as the NetstringReceiver barfs on the
508 # saved logfile, because it was using one big chunk and exceeding
509 # NetstringReceiver.MAX_LENGTH
510 p
= SlowReader("GET %s HTTP/1.0\r\n\r\n"
511 % self
.getLogPath("setup", "bigcomplete"))
513 c
= reactor
.connectTCP("localhost", self
.port
, cf
)
516 self
.failUnlessIn("big2 log", p
.data
)
517 self
.failUnlessIn("a"*100, p
.data
)
518 self
.failUnless(p
.count
> 1*1000*1000)
519 d
.addCallback(_check
)