diff options
Diffstat (limited to 'buildbot/buildbot/test/test_web.py')
-rw-r--r-- | buildbot/buildbot/test/test_web.py | 594 |
1 files changed, 594 insertions, 0 deletions
diff --git a/buildbot/buildbot/test/test_web.py b/buildbot/buildbot/test/test_web.py new file mode 100644 index 0000000..0f353d8 --- /dev/null +++ b/buildbot/buildbot/test/test_web.py @@ -0,0 +1,594 @@ +# -*- test-case-name: buildbot.test.test_web -*- + +import os, time, shutil +from HTMLParser import HTMLParser +from twisted.python import components + +from twisted.trial import unittest +from buildbot.test.runutils import RunMixin + +from twisted.internet import reactor, defer, protocol +from twisted.internet.interfaces import IReactorUNIX +from twisted.web import client + +from buildbot import master, interfaces, sourcestamp +from buildbot.status import html, builder +from buildbot.status.web import waterfall +from buildbot.changes.changes import Change +from buildbot.process import base +from buildbot.process.buildstep import BuildStep +from buildbot.test.runutils import setupBuildStepStatus + +class ConfiguredMaster(master.BuildMaster): + """This BuildMaster variant has a static config file, provided as a + string when it is created.""" + + def __init__(self, basedir, config): + self.config = config + master.BuildMaster.__init__(self, basedir) + + def loadTheConfigFile(self): + self.loadConfig(self.config) + +components.registerAdapter(master.Control, ConfiguredMaster, + interfaces.IControl) + + +base_config = """ +from buildbot.changes.pb import PBChangeSource +from buildbot.status import html +from buildbot.buildslave import BuildSlave +from buildbot.scheduler import Scheduler +from buildbot.process.factory import BuildFactory + +BuildmasterConfig = c = { + 'change_source': PBChangeSource(), + 'slaves': [BuildSlave('bot1name', 'bot1passwd')], + 'schedulers': [Scheduler('name', None, 60, ['builder1'])], + 'builders': [{'name': 'builder1', 'slavename': 'bot1name', + 'builddir': 'builder1', 'factory': BuildFactory()}], + 'slavePortnum': 0, + } +""" + + + +class DistribUNIX: + def __init__(self, unixpath): + from twisted.web import server, resource, distrib + root = resource.Resource() + self.r = r = distrib.ResourceSubscription("unix", unixpath) + root.putChild('remote', r) + self.p = p = reactor.listenTCP(0, server.Site(root)) + self.portnum = p.getHost().port + def shutdown(self): + d = defer.maybeDeferred(self.p.stopListening) + return d + +class DistribTCP: + def __init__(self, port): + from twisted.web import server, resource, distrib + root = resource.Resource() + self.r = r = distrib.ResourceSubscription("localhost", port) + root.putChild('remote', r) + self.p = p = reactor.listenTCP(0, server.Site(root)) + self.portnum = p.getHost().port + def shutdown(self): + d = defer.maybeDeferred(self.p.stopListening) + d.addCallback(self._shutdown_1) + return d + def _shutdown_1(self, res): + return self.r.publisher.broker.transport.loseConnection() + +class SlowReader(protocol.Protocol): + didPause = False + count = 0 + data = "" + def __init__(self, req): + self.req = req + self.d = defer.Deferred() + def connectionMade(self): + self.transport.write(self.req) + def dataReceived(self, data): + self.data += data + self.count += len(data) + if not self.didPause and self.count > 10*1000: + self.didPause = True + self.transport.pauseProducing() + reactor.callLater(2, self.resume) + def resume(self): + self.transport.resumeProducing() + def connectionLost(self, why): + self.d.callback(None) + +class CFactory(protocol.ClientFactory): + def __init__(self, p): + self.p = p + def buildProtocol(self, addr): + self.p.factory = self + return self.p + +def stopHTTPLog(): + # grr. + from twisted.web import http + http._logDateTimeStop() + +class BaseWeb: + master = None + + def failUnlessIn(self, substr, string, note=None): + self.failUnless(string.find(substr) != -1, note) + + def tearDown(self): + stopHTTPLog() + if self.master: + d = self.master.stopService() + return d + + def find_webstatus(self, master): + for child in list(master): + if isinstance(child, html.WebStatus): + return child + + def find_waterfall(self, master): + for child in list(master): + if isinstance(child, html.Waterfall): + return child + +class Ports(BaseWeb, unittest.TestCase): + + def test_webPortnum(self): + # run a regular web server on a TCP socket + config = base_config + "c['status'] = [html.WebStatus(http_port=0)]\n" + os.mkdir("test_web1") + self.master = m = ConfiguredMaster("test_web1", config) + m.startService() + # hack to find out what randomly-assigned port it is listening on + port = self.find_webstatus(m).getPortnum() + + d = client.getPage("http://localhost:%d/waterfall" % port) + def _check(page): + #print page + self.failUnless(page) + d.addCallback(_check) + return d + test_webPortnum.timeout = 10 + + def test_webPathname(self): + # running a t.web.distrib server over a UNIX socket + if not IReactorUNIX.providedBy(reactor): + raise unittest.SkipTest("UNIX sockets not supported here") + config = (base_config + + "c['status'] = [html.WebStatus(distrib_port='.web-pb')]\n") + os.mkdir("test_web2") + self.master = m = ConfiguredMaster("test_web2", config) + m.startService() + + p = DistribUNIX("test_web2/.web-pb") + + d = client.getPage("http://localhost:%d/remote/waterfall" % p.portnum) + def _check(page): + self.failUnless(page) + d.addCallback(_check) + def _done(res): + d1 = p.shutdown() + d1.addCallback(lambda x: res) + return d1 + d.addBoth(_done) + return d + test_webPathname.timeout = 10 + + + def test_webPathname_port(self): + # running a t.web.distrib server over TCP + config = (base_config + + "c['status'] = [html.WebStatus(distrib_port=0)]\n") + os.mkdir("test_web3") + self.master = m = ConfiguredMaster("test_web3", config) + m.startService() + dport = self.find_webstatus(m).getPortnum() + + p = DistribTCP(dport) + + d = client.getPage("http://localhost:%d/remote/waterfall" % p.portnum) + def _check(page): + self.failUnlessIn("BuildBot", page) + d.addCallback(_check) + def _done(res): + d1 = p.shutdown() + d1.addCallback(lambda x: res) + return d1 + d.addBoth(_done) + return d + test_webPathname_port.timeout = 10 + + +class Waterfall(BaseWeb, unittest.TestCase): + def test_waterfall(self): + os.mkdir("test_web4") + os.mkdir("my-maildir"); os.mkdir("my-maildir/new") + self.robots_txt = os.path.abspath(os.path.join("test_web4", + "robots.txt")) + self.robots_txt_contents = "User-agent: *\nDisallow: /\n" + f = open(self.robots_txt, "w") + f.write(self.robots_txt_contents) + f.close() + # this is the right way to configure the Waterfall status + config1 = base_config + """ +from buildbot.changes import mail +c['change_source'] = mail.SyncmailMaildirSource('my-maildir') +c['status'] = [html.Waterfall(http_port=0, robots_txt=%s)] +""" % repr(self.robots_txt) + + self.master = m = ConfiguredMaster("test_web4", config1) + m.startService() + port = self.find_waterfall(m).getPortnum() + self.port = port + # insert an event + m.change_svc.addChange(Change("user", ["foo.c"], "comments")) + + d = client.getPage("http://localhost:%d/" % port) + + def _check1(page): + self.failUnless(page) + self.failUnlessIn("current activity", page) + self.failUnlessIn("<html", page) + TZ = time.tzname[time.localtime()[-1]] + self.failUnlessIn("time (%s)" % TZ, page) + + # phase=0 is really for debugging the waterfall layout + return client.getPage("http://localhost:%d/?phase=0" % self.port) + d.addCallback(_check1) + + def _check2(page): + self.failUnless(page) + self.failUnlessIn("<html", page) + + return client.getPage("http://localhost:%d/changes" % self.port) + d.addCallback(_check2) + + def _check3(changes): + self.failUnlessIn("<li>Syncmail mailing list in maildir " + + "my-maildir</li>", changes) + + return client.getPage("http://localhost:%d/robots.txt" % self.port) + d.addCallback(_check3) + + def _check4(robotstxt): + self.failUnless(robotstxt == self.robots_txt_contents) + d.addCallback(_check4) + + return d + + test_waterfall.timeout = 10 + +class WaterfallSteps(unittest.TestCase): + + # failUnlessSubstring copied from twisted-2.1.0, because this helps us + # maintain compatibility with python2.2. + def failUnlessSubstring(self, substring, astring, msg=None): + """a python2.2 friendly test to assert that substring is found in + astring parameters follow the semantics of failUnlessIn + """ + if astring.find(substring) == -1: + raise self.failureException(msg or "%r not found in %r" + % (substring, astring)) + return substring + assertSubstring = failUnlessSubstring + + def test_urls(self): + s = setupBuildStepStatus("test_web.test_urls") + s.addURL("coverage", "http://coverage.example.org/target") + s.addURL("icon", "http://coverage.example.org/icon.png") + class FakeRequest: + prepath = [] + postpath = [] + def childLink(self, name): + return name + req = FakeRequest() + box = waterfall.IBox(s).getBox(req) + td = box.td() + e1 = '[<a href="http://coverage.example.org/target" class="BuildStep external">coverage</a>]' + self.failUnlessSubstring(e1, td) + e2 = '[<a href="http://coverage.example.org/icon.png" class="BuildStep external">icon</a>]' + self.failUnlessSubstring(e2, td) + + + +geturl_config = """ +from buildbot.status import html +from buildbot.changes import mail +from buildbot.process import factory +from buildbot.steps import dummy +from buildbot.scheduler import Scheduler +from buildbot.changes.base import ChangeSource +from buildbot.buildslave import BuildSlave +s = factory.s + +class DiscardScheduler(Scheduler): + def addChange(self, change): + pass +class DummyChangeSource(ChangeSource): + pass + +BuildmasterConfig = c = {} +c['slaves'] = [BuildSlave('bot1', 'sekrit'), BuildSlave('bot2', 'sekrit')] +c['change_source'] = DummyChangeSource() +c['schedulers'] = [DiscardScheduler('discard', None, 60, ['b1'])] +c['slavePortnum'] = 0 +c['status'] = [html.Waterfall(http_port=0)] + +f = factory.BuildFactory([s(dummy.RemoteDummy, timeout=1)]) + +c['builders'] = [ + {'name': 'b1', 'slavenames': ['bot1','bot2'], + 'builddir': 'b1', 'factory': f}, + ] +c['buildbotURL'] = 'http://dummy.example.org:8010/' + +""" + +class GetURL(RunMixin, unittest.TestCase): + + def setUp(self): + RunMixin.setUp(self) + self.master.loadConfig(geturl_config) + self.master.startService() + d = self.connectSlave(["b1"]) + return d + + def tearDown(self): + stopHTTPLog() + return RunMixin.tearDown(self) + + def doBuild(self, buildername): + br = base.BuildRequest("forced", sourcestamp.SourceStamp(), 'test_builder') + d = br.waitUntilFinished() + self.control.getBuilder(buildername).requestBuild(br) + return d + + def assertNoURL(self, target): + self.failUnlessIdentical(self.status.getURLForThing(target), None) + + def assertURLEqual(self, target, expected): + got = self.status.getURLForThing(target) + full_expected = "http://dummy.example.org:8010/" + expected + self.failUnlessEqual(got, full_expected) + + def testMissingBase(self): + noweb_config1 = geturl_config + "del c['buildbotURL']\n" + d = self.master.loadConfig(noweb_config1) + d.addCallback(self._testMissingBase_1) + return d + def _testMissingBase_1(self, res): + s = self.status + self.assertNoURL(s) + builder_s = s.getBuilder("b1") + self.assertNoURL(builder_s) + + def testBase(self): + s = self.status + self.assertURLEqual(s, "") + builder_s = s.getBuilder("b1") + self.assertURLEqual(builder_s, "builders/b1") + + def testChange(self): + s = self.status + c = Change("user", ["foo.c"], "comments") + self.master.change_svc.addChange(c) + # TODO: something more like s.getChanges(), requires IChange and + # an accessor in IStatus. The HTML page exists already, though + self.assertURLEqual(c, "changes/1") + + def testBuild(self): + # first we do some stuff so we'll have things to look at. + s = self.status + d = self.doBuild("b1") + # maybe check IBuildSetStatus here? + d.addCallback(self._testBuild_1) + return d + + def _testBuild_1(self, res): + s = self.status + builder_s = s.getBuilder("b1") + build_s = builder_s.getLastFinishedBuild() + self.assertURLEqual(build_s, "builders/b1/builds/0") + # no page for builder.getEvent(-1) + step = build_s.getSteps()[0] + self.assertURLEqual(step, "builders/b1/builds/0/steps/remote%20dummy") + # maybe page for build.getTestResults? + self.assertURLEqual(step.getLogs()[0], + "builders/b1/builds/0/steps/remote%20dummy/logs/0") + + + +class Logfile(BaseWeb, RunMixin, unittest.TestCase): + def setUp(self): + config = """ +from buildbot.status import html +from buildbot.process.factory import BasicBuildFactory +from buildbot.buildslave import BuildSlave +f1 = BasicBuildFactory('cvsroot', 'cvsmodule') +BuildmasterConfig = { + 'slaves': [BuildSlave('bot1', 'passwd1')], + 'schedulers': [], + 'builders': [{'name': 'builder1', 'slavename': 'bot1', + 'builddir':'workdir', 'factory':f1}], + 'slavePortnum': 0, + 'status': [html.WebStatus(http_port=0)], + } +""" + if os.path.exists("test_logfile"): + shutil.rmtree("test_logfile") + os.mkdir("test_logfile") + self.master = m = ConfiguredMaster("test_logfile", config) + m.startService() + # hack to find out what randomly-assigned port it is listening on + port = self.find_webstatus(m).getPortnum() + self.port = port + # insert an event + + req = base.BuildRequest("reason", sourcestamp.SourceStamp(), 'test_builder') + build1 = base.Build([req]) + bs = m.status.getBuilder("builder1").newBuild() + bs.setReason("reason") + bs.buildStarted(build1) + + step1 = BuildStep(name="setup") + step1.setBuild(build1) + bss = bs.addStepWithName("setup") + step1.setStepStatus(bss) + bss.stepStarted() + + log1 = step1.addLog("output") + log1.addStdout("some stdout\n") + log1.finish() + + log2 = step1.addHTMLLog("error", "<html>ouch</html>") + + log3 = step1.addLog("big") + log3.addStdout("big log\n") + for i in range(1000): + log3.addStdout("a" * 500) + log3.addStderr("b" * 500) + log3.finish() + + log4 = step1.addCompleteLog("bigcomplete", + "big2 log\n" + "a" * 1*1000*1000) + + log5 = step1.addLog("mixed") + log5.addHeader("header content") + log5.addStdout("this is stdout content") + log5.addStderr("errors go here") + log5.addEntry(5, "non-standard content on channel 5") + log5.addStderr(" and some trailing stderr") + + d = defer.maybeDeferred(step1.step_status.stepFinished, + builder.SUCCESS) + bs.buildFinished() + return d + + def getLogPath(self, stepname, logname): + return ("/builders/builder1/builds/0/steps/%s/logs/%s" % + (stepname, logname)) + + def getLogURL(self, stepname, logname): + return ("http://localhost:%d" % self.port + + self.getLogPath(stepname, logname)) + + def test_logfile1(self): + d = client.getPage("http://localhost:%d/" % self.port) + def _check(page): + self.failUnless(page) + d.addCallback(_check) + return d + + def test_logfile2(self): + logurl = self.getLogURL("setup", "output") + d = client.getPage(logurl) + def _check(logbody): + self.failUnless(logbody) + d.addCallback(_check) + return d + + def test_logfile3(self): + logurl = self.getLogURL("setup", "output") + d = client.getPage(logurl + "/text") + def _check(logtext): + self.failUnlessEqual(logtext, "some stdout\n") + d.addCallback(_check) + return d + + def test_logfile4(self): + logurl = self.getLogURL("setup", "error") + d = client.getPage(logurl) + def _check(logbody): + self.failUnlessEqual(logbody, "<html>ouch</html>") + d.addCallback(_check) + return d + + def test_logfile5(self): + # this is log3, which is about 1MB in size, made up of alternating + # stdout/stderr chunks. buildbot-0.6.6, when run against + # twisted-1.3.0, fails to resume sending chunks after the client + # stalls for a few seconds, because of a recursive doWrite() call + # that was fixed in twisted-2.0.0 + p = SlowReader("GET %s HTTP/1.0\r\n\r\n" + % self.getLogPath("setup", "big")) + cf = CFactory(p) + c = reactor.connectTCP("localhost", self.port, cf) + d = p.d + def _check(res): + self.failUnlessIn("big log", p.data) + self.failUnlessIn("a"*100, p.data) + self.failUnless(p.count > 1*1000*1000) + d.addCallback(_check) + return d + + def test_logfile6(self): + # this is log4, which is about 1MB in size, one big chunk. + # buildbot-0.6.6 dies as the NetstringReceiver barfs on the + # saved logfile, because it was using one big chunk and exceeding + # NetstringReceiver.MAX_LENGTH + p = SlowReader("GET %s HTTP/1.0\r\n\r\n" + % self.getLogPath("setup", "bigcomplete")) + cf = CFactory(p) + c = reactor.connectTCP("localhost", self.port, cf) + d = p.d + def _check(res): + self.failUnlessIn("big2 log", p.data) + self.failUnlessIn("a"*100, p.data) + self.failUnless(p.count > 1*1000*1000) + d.addCallback(_check) + return d + + def test_logfile7(self): + # this is log5, with mixed content on the tree standard channels + # as well as on channel 5 + + class SpanParser(HTMLParser): + '''Parser subclass to gather all the log spans from the log page''' + def __init__(self, test): + self.spans = [] + self.test = test + self.inSpan = False + HTMLParser.__init__(self) + + def handle_starttag(self, tag, attrs): + if tag == 'span': + self.inSpan = True + cls = attrs[0] + self.test.failUnless(cls[0] == 'class') + self.spans.append([cls[1],'']) + + def handle_data(self, data): + if self.inSpan: + self.spans[-1][1] += data + + def handle_endtag(self, tag): + if tag == 'span': + self.inSpan = False + + logurl = self.getLogURL("setup", "mixed") + d = client.getPage(logurl, timeout=2) + def _check(logbody): + try: + p = SpanParser(self) + p.feed(logbody) + p.close + except Exception, e: + print e + self.failUnlessEqual(len(p.spans), 4) + self.failUnlessEqual(p.spans[0][0], 'header') + self.failUnlessEqual(p.spans[0][1], 'header content') + self.failUnlessEqual(p.spans[1][0], 'stdout') + self.failUnlessEqual(p.spans[1][1], 'this is stdout content') + self.failUnlessEqual(p.spans[2][0], 'stderr') + self.failUnlessEqual(p.spans[2][1], 'errors go here') + self.failUnlessEqual(p.spans[3][0], 'stderr') + self.failUnlessEqual(p.spans[3][1], ' and some trailing stderr') + def _fail(err): + pass + d.addCallback(_check) + d.addErrback(_fail) + return d |