1
2
3 from zope.interface import implements
4 from twisted.internet import reactor, defer, error
5 from twisted.protocols import basic
6 from twisted.spread import pb
7 from twisted.python import log
8 from twisted.python.failure import Failure
9 from twisted.web.util import formatFailure
10
11 from buildbot import interfaces, locks
12 from buildbot.status import progress
13 from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, SKIPPED, \
14 EXCEPTION
15
16 """
17 BuildStep and RemoteCommand classes for master-side representation of the
18 build process
19 """
20
22 """
23 I represent a single command to be run on the slave. I handle the details
24 of reliably gathering status updates from the slave (acknowledging each),
25 and (eventually, in a future release) recovering from interrupted builds.
26 This is the master-side object that is known to the slave-side
27 L{buildbot.slave.bot.SlaveBuilder}, to which status updates are sent.
28
29 My command should be started by calling .run(), which returns a
30 Deferred that will fire when the command has finished, or will
31 errback if an exception is raised.
32
33 Typically __init__ or run() will set up self.remote_command to be a
34 string which corresponds to one of the SlaveCommands registered in
35 the buildslave, and self.args to a dictionary of arguments that will
36 be passed to the SlaveCommand instance.
37
38 start, remoteUpdate, and remoteComplete are available to be overridden
39
40 @type commandCounter: list of one int
41 @cvar commandCounter: provides a unique value for each
42 RemoteCommand executed across all slaves
43 @type active: boolean
44 @ivar active: whether the command is currently running
45 """
46 commandCounter = [0]
47 active = False
48
49 - def __init__(self, remote_command, args):
50 """
51 @type remote_command: string
52 @param remote_command: remote command to start. This will be
53 passed to
54 L{buildbot.slave.bot.SlaveBuilder.remote_startCommand}
55 and needs to have been registered
56 slave-side by
57 L{buildbot.slave.registry.registerSlaveCommand}
58 @type args: dict
59 @param args: arguments to send to the remote command
60 """
61
62 self.remote_command = remote_command
63 self.args = args
64
66 dict = self.__dict__.copy()
67
68
69 if dict.has_key("remote"):
70 del dict["remote"]
71 return dict
72
73 - def run(self, step, remote):
74 self.active = True
75 self.step = step
76 self.remote = remote
77 c = self.commandCounter[0]
78 self.commandCounter[0] += 1
79
80 self.commandID = "%d" % c
81 log.msg("%s: RemoteCommand.run [%s]" % (self, self.commandID))
82 self.deferred = defer.Deferred()
83
84 d = defer.maybeDeferred(self.start)
85
86
87
88
89
90
91
92 d.addErrback(self._finished)
93
94
95
96 return self.deferred
97
99 """
100 Tell the slave to start executing the remote command.
101
102 @rtype: L{twisted.internet.defer.Deferred}
103 @returns: a deferred that will fire when the remote command is
104 done (with None as the result)
105 """
106
107
108
109
110 d = self.remote.callRemote("startCommand", self, self.commandID,
111 self.remote_command, self.args)
112 return d
113
115
116
117
118
119 log.msg("RemoteCommand.interrupt", self, why)
120 if not self.active:
121 log.msg(" but this RemoteCommand is already inactive")
122 return
123 if not self.remote:
124 log.msg(" but our .remote went away")
125 return
126 if isinstance(why, Failure) and why.check(error.ConnectionLost):
127 log.msg("RemoteCommand.disconnect: lost slave")
128 self.remote = None
129 self._finished(why)
130 return
131
132
133
134
135 d = defer.maybeDeferred(self.remote.callRemote, "interruptCommand",
136 self.commandID, str(why))
137
138 d.addErrback(self._interruptFailed)
139 return d
140
142 log.msg("RemoteCommand._interruptFailed", self)
143
144
145 return None
146
148 """
149 I am called by the slave's L{buildbot.slave.bot.SlaveBuilder} so
150 I can receive updates from the running remote command.
151
152 @type updates: list of [object, int]
153 @param updates: list of updates from the remote command
154 """
155 self.buildslave.messageReceivedFromSlave()
156 max_updatenum = 0
157 for (update, num) in updates:
158
159 try:
160 if self.active:
161 self.remoteUpdate(update)
162 except:
163
164 self._finished(Failure())
165
166
167 if num > max_updatenum:
168 max_updatenum = num
169 return max_updatenum
170
172 raise NotImplementedError("You must implement this in a subclass")
173
175 """
176 Called by the slave's L{buildbot.slave.bot.SlaveBuilder} to
177 notify me the remote command has finished.
178
179 @type failure: L{twisted.python.failure.Failure} or None
180
181 @rtype: None
182 """
183 self.buildslave.messageReceivedFromSlave()
184
185
186 if self.active:
187 reactor.callLater(0, self._finished, failure)
188 return None
189
191 self.active = False
192
193
194
195
196
197 d = defer.maybeDeferred(self.remoteComplete, failure)
198
199
200 d.addCallback(lambda r: self)
201
202
203 d.addBoth(self.deferred.callback)
204
206 """Subclasses can override this.
207
208 This is called when the RemoteCommand has finished. 'maybeFailure'
209 will be None if the command completed normally, or a Failure
210 instance in one of the following situations:
211
212 - the slave was lost before the command was started
213 - the slave didn't respond to the startCommand message
214 - the slave raised an exception while starting the command
215 (bad command name, bad args, OSError from missing executable)
216 - the slave raised an exception while finishing the command
217 (they send back a remote_complete message with a Failure payload)
218
219 and also (for now):
220 - slave disconnected while the command was running
221
222 This method should do cleanup, like closing log files. It should
223 normally return the 'failure' argument, so that any exceptions will
224 be propagated to the Step. If it wants to consume them, return None
225 instead."""
226
227 return maybeFailure
228
230 """
231
232 I am a L{RemoteCommand} which gathers output from the remote command into
233 one or more local log files. My C{self.logs} dictionary contains
234 references to these L{buildbot.status.builder.LogFile} instances. Any
235 stdout/stderr/header updates from the slave will be put into
236 C{self.logs['stdio']}, if it exists. If the remote command uses other log
237 files, they will go into other entries in C{self.logs}.
238
239 If you want to use stdout or stderr, you should create a LogFile named
240 'stdio' and pass it to my useLog() message. Otherwise stdout/stderr will
241 be ignored, which is probably not what you want.
242
243 Unless you tell me otherwise, when my command completes I will close all
244 the LogFiles that I know about.
245
246 @ivar logs: maps logname to a LogFile instance
247 @ivar _closeWhenFinished: maps logname to a boolean. If true, this
248 LogFile will be closed when the RemoteCommand
249 finishes. LogFiles which are shared between
250 multiple RemoteCommands should use False here.
251
252 """
253
254 rc = None
255 debug = False
256
261
263 return "<RemoteCommand '%s' at %d>" % (self.remote_command, id(self))
264
265 - def useLog(self, loog, closeWhenFinished=False, logfileName=None):
266 """Start routing messages from a remote logfile to a local LogFile
267
268 I take a local ILogFile instance in 'loog', and arrange to route
269 remote log messages for the logfile named 'logfileName' into it. By
270 default this logfileName comes from the ILogFile itself (using the
271 name by which the ILogFile will be displayed), but the 'logfileName'
272 argument can be used to override this. For example, if
273 logfileName='stdio', this logfile will collect text from the stdout
274 and stderr of the command.
275
276 @param loog: an instance which implements ILogFile
277 @param closeWhenFinished: a boolean, set to False if the logfile
278 will be shared between multiple
279 RemoteCommands. If True, the logfile will
280 be closed when this ShellCommand is done
281 with it.
282 @param logfileName: a string, which indicates which remote log file
283 should be routed into this ILogFile. This should
284 match one of the keys of the logfiles= argument
285 to ShellCommand.
286
287 """
288
289 assert interfaces.ILogFile.providedBy(loog)
290 if not logfileName:
291 logfileName = loog.getName()
292 assert logfileName not in self.logs
293 self.logs[logfileName] = loog
294 self._closeWhenFinished[logfileName] = closeWhenFinished
295
297 log.msg("LoggedRemoteCommand.start")
298 if 'stdio' not in self.logs:
299 log.msg("LoggedRemoteCommand (%s) is running a command, but "
300 "it isn't being logged to anything. This seems unusual."
301 % self)
302 self.updates = {}
303 return RemoteCommand.start(self)
304
314
316 if logname in self.logs:
317 self.logs[logname].addStdout(data)
318 else:
319 log.msg("%s.addToLog: no such log %s" % (self, logname))
320
348
350 for name,loog in self.logs.items():
351 if self._closeWhenFinished[name]:
352 if maybeFailure:
353 loog.addHeader("\nremoteFailed: %s" % maybeFailure)
354 else:
355 log.msg("closing log %s" % loog)
356 loog.finish()
357 return maybeFailure
358
359
361 implements(interfaces.ILogObserver)
362
365
369
370 - def logChunk(self, build, step, log, channel, text):
375
376
377
379 """This will be called with chunks of stdout data. Override this in
380 your observer."""
381 pass
382
384 """This will be called with chunks of stderr data. Override this in
385 your observer."""
386 pass
387
388
401
403 """
404 Set the maximum line length: lines longer than max_length are
405 dropped. Default is 16384 bytes. Use sys.maxint for effective
406 infinity.
407 """
408 self.stdoutParser.MAX_LENGTH = max_length
409 self.stderrParser.MAX_LENGTH = max_length
410
412 self.stdoutParser.dataReceived(data)
413
415 self.stderrParser.dataReceived(data)
416
418 """This will be called with complete stdout lines (not including the
419 delimiter). Override this in your observer."""
420 pass
421
423 """This will be called with complete lines of stderr (not including
424 the delimiter). Override this in your observer."""
425 pass
426
427
429 """This class helps you run a shell command on the build slave. It will
430 accumulate all the command's output into a Log named 'stdio'. When the
431 command is finished, it will fire a Deferred. You can then check the
432 results of the command and parse the output however you like."""
433
434 - def __init__(self, workdir, command, env=None,
435 want_stdout=1, want_stderr=1,
436 timeout=20*60, logfiles={}, usePTY="slave-config"):
437 """
438 @type workdir: string
439 @param workdir: directory where the command ought to run,
440 relative to the Builder's home directory. Defaults to
441 '.': the same as the Builder's homedir. This should
442 probably be '.' for the initial 'cvs checkout'
443 command (which creates a workdir), and the Build-wide
444 workdir for all subsequent commands (including
445 compiles and 'cvs update').
446
447 @type command: list of strings (or string)
448 @param command: the shell command to run, like 'make all' or
449 'cvs update'. This should be a list or tuple
450 which can be used directly as the argv array.
451 For backwards compatibility, if this is a
452 string, the text will be given to '/bin/sh -c
453 %s'.
454
455 @type env: dict of string->string
456 @param env: environment variables to add or change for the
457 slave. Each command gets a separate
458 environment; all inherit the slave's initial
459 one. TODO: make it possible to delete some or
460 all of the slave's environment.
461
462 @type want_stdout: bool
463 @param want_stdout: defaults to True. Set to False if stdout should
464 be thrown away. Do this to avoid storing or
465 sending large amounts of useless data.
466
467 @type want_stderr: bool
468 @param want_stderr: False if stderr should be thrown away
469
470 @type timeout: int
471 @param timeout: tell the remote that if the command fails to
472 produce any output for this number of seconds,
473 the command is hung and should be killed. Use
474 None to disable the timeout.
475 """
476
477 self.command = command
478 if env is not None:
479
480
481
482 env = env.copy()
483 args = {'workdir': workdir,
484 'env': env,
485 'want_stdout': want_stdout,
486 'want_stderr': want_stderr,
487 'logfiles': logfiles,
488 'timeout': timeout,
489 'usePTY': usePTY,
490 }
491 LoggedRemoteCommand.__init__(self, "shell", args)
492
494 self.args['command'] = self.command
495 if self.remote_command == "shell":
496
497
498 if self.step.slaveVersion("shell", "old") == "old":
499 self.args['dir'] = self.args['workdir']
500 what = "command '%s' in dir '%s'" % (self.args['command'],
501 self.args['workdir'])
502 log.msg(what)
503 return LoggedRemoteCommand.start(self)
504
506 return "<RemoteShellCommand '%s'>" % repr(self.command)
507
509 """
510 I represent a single step of the build process. This step may involve
511 zero or more commands to be run in the build slave, as well as arbitrary
512 processing on the master side. Regardless of how many slave commands are
513 run, the BuildStep will result in a single status value.
514
515 The step is started by calling startStep(), which returns a Deferred that
516 fires when the step finishes. See C{startStep} for a description of the
517 results provided by that Deferred.
518
519 __init__ and start are good methods to override. Don't forget to upcall
520 BuildStep.__init__ or bad things will happen.
521
522 To launch a RemoteCommand, pass it to .runCommand and wait on the
523 Deferred it returns.
524
525 Each BuildStep generates status as it runs. This status data is fed to
526 the L{buildbot.status.builder.BuildStepStatus} listener that sits in
527 C{self.step_status}. It can also feed progress data (like how much text
528 is output by a shell command) to the
529 L{buildbot.status.progress.StepProgress} object that lives in
530 C{self.progress}, by calling C{self.setProgress(metric, value)} as it
531 runs.
532
533 @type build: L{buildbot.process.base.Build}
534 @ivar build: the parent Build which is executing this step
535
536 @type progress: L{buildbot.status.progress.StepProgress}
537 @ivar progress: tracks ETA for the step
538
539 @type step_status: L{buildbot.status.builder.BuildStepStatus}
540 @ivar step_status: collects output status
541 """
542
543
544
545
546
547
548
549
550 haltOnFailure = False
551 flunkOnWarnings = False
552 flunkOnFailure = False
553 warnOnWarnings = False
554 warnOnFailure = False
555 alwaysRun = False
556
557
558
559
560
561
562
563
564 parms = ['name', 'locks',
565 'haltOnFailure',
566 'flunkOnWarnings',
567 'flunkOnFailure',
568 'warnOnWarnings',
569 'warnOnFailure',
570 'alwaysRun',
571 'progressMetrics',
572 'doStepIf',
573 ]
574
575 name = "generic"
576 locks = []
577 progressMetrics = ()
578 useProgress = True
579 build = None
580 step_status = None
581 progress = None
582
583 doStepIf = True
584
586 self.factory = (self.__class__, dict(kwargs))
587 for p in self.__class__.parms:
588 if kwargs.has_key(p):
589 setattr(self, p, kwargs[p])
590 del kwargs[p]
591 if kwargs:
592 why = "%s.__init__ got unexpected keyword argument(s) %s" \
593 % (self, kwargs.keys())
594 raise TypeError(why)
595 self._pendingLogObservers = []
596
604
607
609
610
611
612
613
614 pass
615
618
621
624
632
634 """BuildSteps can call self.setProgress() to announce progress along
635 some metric."""
636 if self.progress:
637 self.progress.setProgress(metric, value)
638
641
642 - def setProperty(self, propname, value, source="Step"):
644
646 """Begin the step. This returns a Deferred that will fire when the
647 step finishes.
648
649 This deferred fires with a tuple of (result, [extra text]), although
650 older steps used to return just the 'result' value, so the receiving
651 L{base.Build} needs to be prepared to handle that too. C{result} is
652 one of the SUCCESS/WARNINGS/FAILURE/SKIPPED constants from
653 L{buildbot.status.builder}, and the extra text is a list of short
654 strings which should be appended to the Build's text results. This
655 text allows a test-case step which fails to append B{17 tests} to the
656 Build's status, in addition to marking the build as failing.
657
658 The deferred will errback if the step encounters an exception,
659 including an exception on the slave side (or if the slave goes away
660 altogether). Failures in shell commands (rc!=0) will B{not} cause an
661 errback, in general the BuildStep will evaluate the results and
662 decide whether to treat it as a WARNING or FAILURE.
663
664 @type remote: L{twisted.spread.pb.RemoteReference}
665 @param remote: a reference to the slave's
666 L{buildbot.slave.bot.SlaveBuilder} instance where any
667 RemoteCommands may be run
668 """
669
670 self.remote = remote
671 self.deferred = defer.Deferred()
672
673 lock_list = []
674 for access in self.locks:
675 if not isinstance(access, locks.LockAccess):
676
677 access = access.defaultAccess()
678 lock = self.build.builder.botmaster.getLockByID(access.lockid)
679 lock_list.append((lock, access))
680 self.locks = lock_list
681
682
683 self.locks = [(l.getLock(self.build.slavebuilder), la) for l, la in self.locks]
684 for l, la in self.locks:
685 if l in self.build.locks:
686 log.msg("Hey, lock %s is claimed by both a Step (%s) and the"
687 " parent Build (%s)" % (l, self, self.build))
688 raise RuntimeError("lock claimed by both Step and Build")
689 d = self.acquireLocks()
690 d.addCallback(self._startStep_2)
691 return self.deferred
692
707
730
732 """Begin the step. Override this method and add code to do local
733 processing, fire off remote commands, etc.
734
735 To spawn a command in the buildslave, create a RemoteCommand instance
736 and run it with self.runCommand::
737
738 c = RemoteCommandFoo(args)
739 d = self.runCommand(c)
740 d.addCallback(self.fooDone).addErrback(self.failed)
741
742 As the step runs, it should send status information to the
743 BuildStepStatus::
744
745 self.step_status.setText(['compile', 'failed'])
746 self.step_status.setText2(['4', 'warnings'])
747
748 To have some code parse stdio (or other log stream) in realtime, add
749 a LogObserver subclass. This observer can use self.step.setProgress()
750 to provide better progress notification to the step.::
751
752 self.addLogObserver('stdio', MyLogObserver())
753
754 To add a LogFile, use self.addLog. Make sure it gets closed when it
755 finishes. When giving a Logfile to a RemoteShellCommand, just ask it
756 to close the log when the command completes::
757
758 log = self.addLog('output')
759 cmd = RemoteShellCommand(args)
760 cmd.useLog(log, closeWhenFinished=True)
761
762 You can also create complete Logfiles with generated text in a single
763 step::
764
765 self.addCompleteLog('warnings', text)
766
767 When the step is done, it should call self.finished(result). 'result'
768 will be provided to the L{buildbot.process.base.Build}, and should be
769 one of the constants defined above: SUCCESS, WARNINGS, FAILURE, or
770 SKIPPED.
771
772 If the step encounters an exception, it should call self.failed(why).
773 'why' should be a Failure object. This automatically fails the whole
774 build with an exception. It is a good idea to add self.failed as an
775 errback to any Deferreds you might obtain.
776
777 If the step decides it does not need to be run, start() can return
778 the constant SKIPPED. This fires the callback immediately: it is not
779 necessary to call .finished yourself. This can also indicate to the
780 status-reporting mechanism that this step should not be displayed.
781
782 A step can be configured to only run under certain conditions. To
783 do this, set the step's doStepIf to a boolean value, or to a function
784 that returns a boolean value. If the value or function result is
785 False, then the step will return SKIPPED without doing anything,
786 otherwise the step will be executed normally. If you set doStepIf
787 to a function, that function should accept one parameter, which will
788 be the Step object itself."""
789
790 raise NotImplementedError("your subclass must implement this method")
791
793 """Halt the command, either because the user has decided to cancel
794 the build ('reason' is a string), or because the slave has
795 disconnected ('reason' is a ConnectionLost Failure). Any further
796 local processing should be skipped, and the Step completed with an
797 error status. The results text should say something useful like
798 ['step', 'interrupted'] or ['remote', 'lost']"""
799 pass
800
805
812
842
843
844
846 """Return the version number of the given slave command. For the
847 commands defined in buildbot.slave.commands, this is the value of
848 'cvs_ver' at the top of that file. Non-existent commands will return
849 a value of None. Buildslaves running buildbot-0.5.0 or earlier did
850 not respond to the version query: commands on those slaves will
851 return a value of OLDVERSION, so you can distinguish between old
852 buildslaves and missing commands.
853
854 If you know that <=0.5.0 buildslaves have the command you want (CVS
855 and SVN existed back then, but none of the other VC systems), then it
856 makes sense to call this with oldversion='old'. If the command you
857 want is newer than that, just leave oldversion= unspecified, and the
858 command will return None for a buildslave that does not implement the
859 command.
860 """
861 return self.build.getSlaveCommandVersion(command, oldversion)
862
864 sv = self.build.getSlaveCommandVersion(command, None)
865 if sv is None:
866 return True
867
868
869
870
871
872 if sv.split(".") < minversion.split("."):
873 return True
874 return False
875
878
883
889
898
903
909
911 if not self._pendingLogObservers:
912 return
913 if not self.step_status:
914 return
915 current_logs = {}
916 for loog in self.step_status.getLogs():
917 current_logs[loog.getName()] = loog
918 for logname, observer in self._pendingLogObservers[:]:
919 if logname in current_logs:
920 observer.setLog(current_logs[logname])
921 self._pendingLogObservers.remove((logname, observer))
922
924 """Add a BuildStep URL to this step.
925
926 An HREF to this URL will be added to any HTML representations of this
927 step. This allows a step to provide links to external web pages,
928 perhaps to provide detailed HTML code coverage results or other forms
929 of build status.
930 """
931 self.step_status.addURL(name, url)
932
937
938
940 length = 0
941
944
945 - def logChunk(self, build, step, log, channel, text):
948
950 """This is an abstract base class, suitable for inheritance by all
951 BuildSteps that invoke RemoteCommands which emit stdout/stderr messages.
952 """
953
954 progressMetrics = ('output',)
955 logfiles = {}
956
957 parms = BuildStep.parms + ['logfiles']
958
959 - def __init__(self, logfiles={}, *args, **kwargs):
967
969 raise NotImplementedError("implement this in a subclass")
970
972 """
973 @param cmd: a suitable RemoteCommand which will be launched, with
974 all output being put into our self.stdio_log LogFile
975 """
976 log.msg("ShellCommand.startCommand(cmd=%s)" % (cmd,))
977 log.msg(" cmd.args = %r" % (cmd.args))
978 self.cmd = cmd
979 self.step_status.setText(self.describe(False))
980
981
982 self.stdio_log = stdio_log = self.addLog("stdio")
983 cmd.useLog(stdio_log, True)
984 for em in errorMessages:
985 stdio_log.addHeader(em)
986
987
988
989
990
991 self.setupLogfiles(cmd, self.logfiles)
992
993 d = self.runCommand(cmd)
994 d.addCallback(lambda res: self.commandComplete(cmd))
995 d.addCallback(lambda res: self.createSummary(cmd.logs['stdio']))
996 d.addCallback(lambda res: self.evaluateCommand(cmd))
997 def _gotResults(results):
998 self.setStatus(cmd, results)
999 return results
1000 d.addCallback(_gotResults)
1001 d.addCallbacks(self.finished, self.checkDisconnect)
1002 d.addErrback(self.failed)
1003
1005 """Set up any additional logfiles= logs.
1006 """
1007 for logname,remotefilename in logfiles.items():
1008
1009 newlog = self.addLog(logname)
1010
1011 cmd.useLog(newlog, True)
1012
1020
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1053 """This is a general-purpose hook method for subclasses. It will be
1054 called after the remote command has finished, but before any of the
1055 other hook functions are called."""
1056 pass
1057
1059 """To create summary logs, do something like this:
1060 warnings = grep('^Warning:', log.getText())
1061 self.addCompleteLog('warnings', warnings)
1062 """
1063 pass
1064
1066 """Decide whether the command was SUCCESS, WARNINGS, or FAILURE.
1067 Override this to, say, declare WARNINGS if there is any stderr
1068 activity, or to say that rc!=0 is not actually an error."""
1069
1070 if cmd.rc != 0:
1071 return FAILURE
1072
1073 return SUCCESS
1074
1075 - def getText(self, cmd, results):
1076 if results == SUCCESS:
1077 return self.describe(True)
1078 elif results == WARNINGS:
1079 return self.describe(True) + ["warnings"]
1080 else:
1081 return self.describe(True) + ["failed"]
1082
1083 - def getText2(self, cmd, results):
1084 """We have decided to add a short note about ourselves to the overall
1085 build description, probably because something went wrong. Return a
1086 short list of short strings. If your subclass counts test failures or
1087 warnings of some sort, this is a good place to announce the count."""
1088
1089
1090 return [self.name]
1091
1092 - def maybeGetText2(self, cmd, results):
1093 if results == SUCCESS:
1094
1095 pass
1096 elif results == WARNINGS:
1097 if (self.flunkOnWarnings or self.warnOnWarnings):
1098
1099 return self.getText2(cmd, results)
1100 else:
1101 if (self.haltOnFailure or self.flunkOnFailure
1102 or self.warnOnFailure):
1103
1104 return self.getText2(cmd, results)
1105 return []
1106
1112
1113
1114 from buildbot.process.properties import WithProperties
1115 _hush_pyflakes = [WithProperties]
1116 del _hush_pyflakes
1117