Repository: buildbot/buildbot

Issue:
lazylogfiles broken on 3.0.2
I'm upgrading from 0.8.x to 3.0.2, and have this:
```python
test_factory = util.BuildFactory()

test_factory.addStep(ShellCommand(
    env={'PATH' : bin_path},
    command=["runurl", bb_url + "bb-dependencies.sh"],
    decodeRC={0 : SUCCESS, 1 : FAILURE, 2 : WARNINGS, 3 : SKIPPED },
    haltOnFailure=True, logEnviron=False,
    lazylogfiles=True,
    description=["installing dependencies"],
    descriptionDone=["installed dependencies"]))
```
Which gives me:
```
2021-03-26 18:38:03+0000 [-]   Invalid argument(s) passed to ShellCommand: lazylogfiles
```
According to the 3.0.2 documentation, `lazylogfiles` is a valid parameter for `ShellCommand`
http://docs.buildbot.net/3.0.2/manual/configuration/steps/shell_command.html

I originally opened a question for this (https://github.com/buildbot/buildbot/discussions/5954) but was told to open a bug instead.


Discussion:

Resolving patch:
diff --git a/master/buildbot/steps/shell.py b/master/buildbot/steps/shell.py
--- a/master/buildbot/steps/shell.py
+++ b/master/buildbot/steps/shell.py
@@ -182,6 +182,7 @@ def __init__(self, **kwargs):
                 'maxTime',
                 'sigtermTime',
                 'logfiles',
+                'lazylogfiles',
                 'usePTY',
                 'logEnviron',
                 'collectStdout',


Test patch:
diff --git a/master/buildbot/test/fake/remotecommand.py b/master/buildbot/test/fake/remotecommand.py
--- a/master/buildbot/test/fake/remotecommand.py
+++ b/master/buildbot/test/fake/remotecommand.py
@@ -21,7 +21,6 @@
 from buildbot.process.results import CANCELLED
 from buildbot.process.results import FAILURE
 from buildbot.process.results import SUCCESS
-from buildbot.test.fake import logfile
 
 
 class FakeRemoteCommand:
@@ -75,12 +74,47 @@ def run(self, step, conn, builder_name):
     def useLog(self, log_, closeWhenFinished=False, logfileName=None):
         if not logfileName:
             logfileName = log_.getName()
+        assert logfileName not in self.logs
+        assert logfileName not in self.delayedLogs
         self.logs[logfileName] = log_
         self._log_close_when_finished[logfileName] = closeWhenFinished
 
     def useLogDelayed(self, logfileName, activateCallBack, closeWhenFinished=False):
+        assert logfileName not in self.logs
+        assert logfileName not in self.delayedLogs
         self.delayedLogs[logfileName] = (activateCallBack, closeWhenFinished)
 
+    def addStdout(self, data):
+        if self.collectStdout:
+            self.stdout += data
+        if self.stdioLogName is not None and self.stdioLogName in self.logs:
+            self.logs[self.stdioLogName].addStdout(data)
+
+    def addStderr(self, data):
+        if self.collectStderr:
+            self.stderr += data
+        if self.stdioLogName is not None and self.stdioLogName in self.logs:
+            self.logs[self.stdioLogName].addStderr(data)
+
+    def addHeader(self, data):
+        if self.stdioLogName is not None and self.stdioLogName in self.logs:
+            self.logs[self.stdioLogName].addHeader(data)
+
+    @defer.inlineCallbacks
+    def addToLog(self, logname, data):
+        # Activate delayed logs on first data.
+        if logname in self.delayedLogs:
+            (activate_callback, close_when_finished) = self.delayedLogs[logname]
+            del self.delayedLogs[logname]
+            loog = yield activate_callback(self)
+            self.logs[logname] = loog
+            self._log_close_when_finished[logname] = close_when_finished
+
+        if logname in self.logs:
+            self.logs[logname].addStdout(data)
+        else:
+            raise Exception("{}.addToLog: no such log {}".format(self, logname))
+
     def interrupt(self, why):
         if not self._waiting_for_interrupt:
             raise RuntimeError("Got interrupt, but FakeRemoteCommand was not expecting it")
@@ -97,12 +131,6 @@ def results(self):
     def didFail(self):
         return self.results() == FAILURE
 
-    def fakeLogData(self, step, log, header='', stdout='', stderr=''):
-        # note that this should not be used in the same test as useLog(Delayed)
-        self.logs[log] = fakelog = logfile.FakeLogFile(log)
-        self._log_close_when_finished[log] = False
-        fakelog.fakeData(header=header, stdout=stdout, stderr=stderr)
-
     def set_run_interrupt(self):
         self._waiting_for_interrupt = True
 
@@ -128,6 +156,9 @@ def __init__(self, workdir, command, env=None,
                     initial_stdin=initialStdin,
                     timeout=timeout, maxTime=maxTime, logfiles=logfiles,
                     usePTY=usePTY, logEnviron=logEnviron)
+
+        if interruptSignal is not None and interruptSignal != 'KILL':
+            args['interruptSignal'] = interruptSignal
         super().__init__("shell", args,
                          collectStdout=collectStdout,
                          collectStderr=collectStderr,
@@ -235,16 +266,21 @@ def runBehavior(self, behavior, args, command):
             command.updates.setdefault(args[0], []).append(args[1])
         elif behavior == 'log':
             name, streams = args
-            if 'header' in streams:
-                command.logs[name].addHeader(streams['header'])
-            if 'stdout' in streams:
-                command.logs[name].addStdout(streams['stdout'])
-                if command.collectStdout:
-                    command.stdout += streams['stdout']
-            if 'stderr' in streams:
-                command.logs[name].addStderr(streams['stderr'])
-                if command.collectStderr:
-                    command.stderr += streams['stderr']
+            for stream in streams:
+                if stream not in ['header', 'stdout', 'stderr']:
+                    raise Exception('Log stream {} is not recognized'.format(stream))
+
+            if name == command.stdioLogName:
+                if 'header' in streams:
+                    command.addHeader(streams['header'])
+                if 'stdout' in streams:
+                    command.addStdout(streams['stdout'])
+                if 'stderr' in streams:
+                    command.addStderr(streams['stderr'])
+            else:
+                if 'header' in streams or 'stderr' in streams:
+                    raise Exception('Non stdio streams only support stdout')
+                return command.addToLog(name, streams['stdout'])
         elif behavior == 'callable':
             return defer.maybeDeferred(lambda: args[0](command))
         else:
@@ -325,7 +361,7 @@ class ExpectShell(Expect):
     def __init__(self, workdir, command, env=None,
                  want_stdout=1, want_stderr=1, initialStdin=None,
                  timeout=20 * 60, maxTime=None, logfiles=None,
-                 usePTY=None, logEnviron=True):
+                 usePTY=None, logEnviron=True, interruptSignal=None):
         if env is None:
             env = {}
         if logfiles is None:
@@ -335,6 +371,8 @@ def __init__(self, workdir, command, env=None,
                     initial_stdin=initialStdin,
                     timeout=timeout, maxTime=maxTime, logfiles=logfiles,
                     usePTY=usePTY, logEnviron=logEnviron)
+        if interruptSignal is not None:
+            args['interruptSignal'] = interruptSignal
         super().__init__("shell", args)
 
     def __repr__(self):
diff --git a/master/buildbot/test/unit/process/test_buildstep.py b/master/buildbot/test/unit/process/test_buildstep.py
--- a/master/buildbot/test/unit/process/test_buildstep.py
+++ b/master/buildbot/test/unit/process/test_buildstep.py
@@ -998,38 +998,17 @@ def test_glob_fail(self):
         return self.runStep()
 
 
-class ShellMixinExample(buildstep.ShellMixin, buildstep.BuildStep):
-    # note that this is straight out of cls-buildsteps.rst
-
-    def __init__(self, cleanupScript='./cleanup.sh', **kwargs):
-        self.cleanupScript = cleanupScript
-        kwargs = self.setupShellMixin(kwargs, prohibitArgs=['command'])
-        super().__init__(**kwargs)
-
-    @defer.inlineCallbacks
-    def run(self):
-        cmd = yield self.makeRemoteShellCommand(
-            command=[self.cleanupScript])
-        yield self.runCommand(cmd)
-        if cmd.didFail():
-            cmd = yield self.makeRemoteShellCommand(
-                command=[self.cleanupScript, '--force'],
-                logEnviron=False)
-            yield self.runCommand(cmd)
-        return cmd.results()
-
-
 class SimpleShellCommand(buildstep.ShellMixin, buildstep.BuildStep):
 
-    def __init__(self, makeRemoteShellCommandKwargs=None, **kwargs):
-        self.makeRemoteShellCommandKwargs = makeRemoteShellCommandKwargs or {}
+    def __init__(self, make_cmd_kwargs=None, prohibit_args=None, **kwargs):
+        self.make_cmd_kwargs = make_cmd_kwargs or {}
 
-        kwargs = self.setupShellMixin(kwargs)
+        kwargs = self.setupShellMixin(kwargs, prohibitArgs=prohibit_args)
         super().__init__(**kwargs)
 
     @defer.inlineCallbacks
     def run(self):
-        cmd = yield self.makeRemoteShellCommand(**self.makeRemoteShellCommandKwargs)
+        cmd = yield self.makeRemoteShellCommand(**self.make_cmd_kwargs)
         yield self.runCommand(cmd)
         return cmd.results()
 
@@ -1048,20 +1027,18 @@ def tearDown(self):
         return self.tearDownBuildStep()
 
     def test_setupShellMixin_bad_arg(self):
-        mixin = ShellMixinExample()
-        with self.assertRaisesConfigError(
-                "invalid ShellMixinExample argument invarg"):
+        mixin = SimpleShellCommand()
+        with self.assertRaisesConfigError("invalid SimpleShellCommand argument invarg"):
             mixin.setupShellMixin({'invarg': 13})
 
     def test_setupShellMixin_prohibited_arg(self):
-        mixin = ShellMixinExample()
-        with self.assertRaisesConfigError(
-                "invalid ShellMixinExample argument logfiles"):
+        mixin = SimpleShellCommand()
+        with self.assertRaisesConfigError("invalid SimpleShellCommand argument logfiles"):
             mixin.setupShellMixin({'logfiles': None},
                                   prohibitArgs=['logfiles'])
 
     def test_constructor_defaults(self):
-        class MySubclass(ShellMixinExample):
+        class MySubclass(SimpleShellCommand):
             timeout = 9999
         # ShellMixin arg
         self.assertEqual(MySubclass().timeout, 9999)
@@ -1075,121 +1052,180 @@ class MySubclass(ShellMixinExample):
                          ['charming'])
 
     @defer.inlineCallbacks
-    def test_example(self):
-        self.setupStep(ShellMixinExample(), wantDefaultWorkdir=False)
+    def test_prohibit_args(self):
+        self.setupStep(SimpleShellCommand(prohibit_args=['command'],
+                                          make_cmd_kwargs={'command': ['cmd', 'arg']}))
         self.expectCommands(
-            ExpectShell(workdir='build', command=['./cleanup.sh']) +
-            Expect.log('stdio', stderr="didn't go so well\n") +
-            1,
-            ExpectShell(workdir='build', command=['./cleanup.sh', '--force'],
-                        logEnviron=False) +
+            ExpectShell(workdir='wkdir', command=['cmd', 'arg']) +
             0,
         )
         self.expectOutcome(result=SUCCESS)
         yield self.runStep()
 
     @defer.inlineCallbacks
-    def test_example_extra_logfile(self):
-        self.setupStep(ShellMixinExample(
-            logfiles={'cleanup': 'cleanup.log'}), wantDefaultWorkdir=False)
+    def test_no_default_workdir(self):
+        self.setupStep(SimpleShellCommand(command=['cmd', 'arg']), wantDefaultWorkdir=False)
         self.expectCommands(
-            ExpectShell(workdir='build', command=['./cleanup.sh'],
-                        logfiles={'cleanup': 'cleanup.log'}) +
-            Expect.log('cleanup', stdout='cleaning\ncleaned\n') +
+            ExpectShell(workdir='build', command=['cmd', 'arg']) +
             0,
         )
         self.expectOutcome(result=SUCCESS)
         yield self.runStep()
-        self.assertEqual(self.step.getLog('cleanup').stdout,
-                         'cleaning\ncleaned\n')
 
     @defer.inlineCallbacks
-    def test_example_build_workdir(self):
-        self.setupStep(ShellMixinExample(), wantDefaultWorkdir=False)
+    def test_build_workdir(self):
+        self.setupStep(SimpleShellCommand(command=['cmd', 'arg']), wantDefaultWorkdir=False)
         self.build.workdir = '/alternate'
         self.expectCommands(
-            ExpectShell(workdir='/alternate', command=['./cleanup.sh']) +
+            ExpectShell(workdir='/alternate', command=['cmd', 'arg']) +
             0,
         )
         self.expectOutcome(result=SUCCESS)
         yield self.runStep()
 
     @defer.inlineCallbacks
-    def test_example_build_workdir_callable(self):
-        self.setupStep(ShellMixinExample(), wantDefaultWorkdir=False)
+    def test_build_workdir_callable(self):
+        self.setupStep(SimpleShellCommand(command=['cmd', 'arg']), wantDefaultWorkdir=False)
         self.build.workdir = lambda x: '/alternate'
         self.expectCommands(
-            ExpectShell(workdir='/alternate', command=['./cleanup.sh']) +
+            ExpectShell(workdir='/alternate', command=['cmd', 'arg']) +
             0,
         )
         self.expectOutcome(result=SUCCESS)
         yield self.runStep()
 
     @defer.inlineCallbacks
-    def test_example_build_workdir_rendereable(self):
-        self.setupStep(ShellMixinExample(), wantDefaultWorkdir=False)
+    def test_build_workdir_callable_error(self):
+        self.setupStep(SimpleShellCommand(command=['cmd', 'arg']), wantDefaultWorkdir=False)
+        self.build.workdir = lambda x: x.nosuchattribute  # will raise AttributeError
+        self.expectException(buildstep.CallableAttributeError)
+        yield self.runStep()
+
+    @defer.inlineCallbacks
+    def test_build_workdir_renderable(self):
+        self.setupStep(SimpleShellCommand(command=['cmd', 'arg']), wantDefaultWorkdir=False)
         self.build.workdir = properties.Property("myproperty")
         self.properties.setProperty("myproperty", "/myproperty", "test")
         self.expectCommands(
-            ExpectShell(workdir='/myproperty', command=['./cleanup.sh']) +
+            ExpectShell(workdir='/myproperty', command=['cmd', 'arg']) +
             0,
         )
         self.expectOutcome(result=SUCCESS)
         yield self.runStep()
 
     @defer.inlineCallbacks
-    def test_example_build_workdir_callable_attribute_error(self):
-        self.setupStep(ShellMixinExample(), wantDefaultWorkdir=False)
-        self.build.workdir = lambda x: x.p  # will raise AttributeError
-        self.expectException(buildstep.CallableAttributeError)
+    def test_step_workdir(self):
+        self.setupStep(SimpleShellCommand(command=['cmd', 'arg'], workdir='/stepdir'))
+        self.build.workdir = '/builddir'
+        self.expectCommands(
+            ExpectShell(workdir='/stepdir', command=['cmd', 'arg']) +
+            0,
+        )
+        self.expectOutcome(result=SUCCESS)
         yield self.runStep()
 
     @defer.inlineCallbacks
-    def test_example_step_workdir(self):
-        self.setupStep(ShellMixinExample(workdir='/alternate'))
-        self.build.workdir = '/overridden'
+    def test_step_renderable_workdir(self):
+        @renderer
+        def rendered_workdir(_):
+            return '/stepdir'
+
+        self.setupStep(SimpleShellCommand(command=['cmd', 'arg'], workdir=rendered_workdir))
+        self.build.workdir = '/builddir'
         self.expectCommands(
-            ExpectShell(workdir='/alternate', command=['./cleanup.sh']) +
+            ExpectShell(workdir='/stepdir', command=['cmd', 'arg']) +
             0,
         )
         self.expectOutcome(result=SUCCESS)
         yield self.runStep()
 
     @defer.inlineCallbacks
-    def test_example_step_renderable_workdir(self):
-        @renderer
-        def rendered_workdir(_):
-            return '/alternate'
+    def test_step_workdir_overridden(self):
+        self.setupStep(SimpleShellCommand(command=['cmd', 'arg'], workdir='/stepdir',
+                                          make_cmd_kwargs={'workdir': '/overridden'}))
+        self.build.workdir = '/builddir'
+        self.expectCommands(
+            ExpectShell(workdir='/overridden', command=['cmd', 'arg']) +
+            0,
+        )
+        self.expectOutcome(result=SUCCESS)
+        yield self.runStep()
 
-        self.setupStep(ShellMixinExample(workdir=rendered_workdir))
-        self.build.workdir = '/overridden'
+    @defer.inlineCallbacks
+    def test_extra_logfile(self):
+        self.setupStep(SimpleShellCommand(command=['cmd', 'arg'],
+                                          logfiles={'logname': 'logpath.log'}))
         self.expectCommands(
-            ExpectShell(workdir='/alternate', command=['./cleanup.sh']) +
+            ExpectShell(workdir='wkdir', command=['cmd', 'arg'],
+                        logfiles={'logname': 'logpath.log'}) +
+            Expect.log('logname', stdout='logline\nlogline2\n') +
+            Expect.log('stdio', stdout="some log\n") +
             0,
         )
         self.expectOutcome(result=SUCCESS)
         yield self.runStep()
+        self.assertEqual(self.step.getLog('logname').stdout,
+                         'logline\nlogline2\n')
 
     @defer.inlineCallbacks
-    def test_example_override_workdir(self):
-        # Test that makeRemoteShellCommand(workdir=X) works.
-        self.setupStep(SimpleShellCommand(
-            makeRemoteShellCommandKwargs={'workdir': '/alternate'},
-            command=['foo', properties.Property('bar', 'BAR')]))
+    def test_lazy_logfiles_stdout_has_stdout(self):
+        self.setupStep(SimpleShellCommand(command=['cmd', 'arg'], lazylogfiles=True))
         self.expectCommands(
-            ExpectShell(workdir='/alternate', command=['foo', 'BAR']) +
+            ExpectShell(workdir='wkdir', command=['cmd', 'arg']) +
+            Expect.log('stdio', stdout="some log\n") +
             0,
         )
         self.expectOutcome(result=SUCCESS)
         yield self.runStep()
+        self.assertEqual(self.step.getLog('stdio').stdout, 'some log\n')
 
     @defer.inlineCallbacks
-    def test_example_env(self):
-        self.setupStep(
-            ShellMixinExample(env={'BAR': 'BAR'}), wantDefaultWorkdir=False)
+    def test_lazy_logfiles_stdout_no_stdout(self):
+        # lazy log files do not apply to stdout
+        self.setupStep(SimpleShellCommand(command=['cmd', 'arg'], lazylogfiles=True))
+        self.expectCommands(
+            ExpectShell(workdir='wkdir', command=['cmd', 'arg']) +
+            0,
+        )
+        self.expectOutcome(result=SUCCESS)
+        yield self.runStep()
+        self.assertEqual(self.step.getLog('stdio').stdout, '')
+
+    @defer.inlineCallbacks
+    def test_lazy_logfiles_logfile(self):
+        self.setupStep(SimpleShellCommand(command=['cmd', 'arg'], lazylogfiles=True,
+                                          logfiles={'logname': 'logpath.log'}))
+        self.expectCommands(
+            ExpectShell(workdir='wkdir', command=['cmd', 'arg'],
+                        logfiles={'logname': 'logpath.log'}) +
+            Expect.log('logname', stdout='logline\nlogline2\n') +
+            0,
+        )
+        self.expectOutcome(result=SUCCESS)
+        yield self.runStep()
+        self.assertEqual(self.step.getLog('logname').stdout,
+                         'logline\nlogline2\n')
+
+    @defer.inlineCallbacks
+    def test_lazy_logfiles_no_logfile(self):
+        self.setupStep(SimpleShellCommand(command=['cmd', 'arg'], lazylogfiles=True,
+                                          logfiles={'logname': 'logpath.log'}))
+        self.expectCommands(
+            ExpectShell(workdir='wkdir', command=['cmd', 'arg'],
+                        logfiles={'logname': 'logpath.log'}) +
+            0,
+        )
+        self.expectOutcome(result=SUCCESS)
+        yield self.runStep()
+        with self.assertRaises(KeyError):
+            self.step.getLog('logname')
+
+    @defer.inlineCallbacks
+    def test_env(self):
+        self.setupStep(SimpleShellCommand(command=['cmd', 'arg'], env={'BAR': 'BAR'}))
         self.build.builder.config.env = {'FOO': 'FOO'}
         self.expectCommands(
-            ExpectShell(workdir='build', command=['./cleanup.sh'],
+            ExpectShell(workdir='wkdir', command=['cmd', 'arg'],
                         env={'FOO': 'FOO', 'BAR': 'BAR'})
             + 0,
         )
@@ -1197,11 +1233,12 @@ def test_example_env(self):
         yield self.runStep()
 
     @defer.inlineCallbacks
-    def test_example_old_worker(self):
-        self.setupStep(ShellMixinExample(usePTY=False, interruptSignal='DIE'),
-                       worker_version={'*': "1.1"}, wantDefaultWorkdir=False)
+    def test_old_worker_args(self):
+        self.setupStep(SimpleShellCommand(command=['cmd', 'arg'], usePTY=False,
+                                          interruptSignal='DIE'),
+                       worker_version={'*': "1.1"})
         self.expectCommands(
-            ExpectShell(workdir='build', command=['./cleanup.sh']) +
+            ExpectShell(workdir='wkdir', command=['cmd', 'arg']) +
             # note missing parameters
             0,
         )
@@ -1212,25 +1249,24 @@ def test_example_old_worker(self):
                          'NOTE: worker does not allow master to specify interruptSignal\n')
 
     @defer.inlineCallbacks
-    def test_example_new_worker(self):
-        self.setupStep(ShellMixinExample(usePTY=False, interruptSignal='DIE'),
-                       worker_version={'*': "3.0"}, wantDefaultWorkdir=False)
+    def test_new_worker_args(self):
+        self.setupStep(SimpleShellCommand(command=['cmd', 'arg'], usePTY=False,
+                                          interruptSignal='DIE'),
+                       worker_version={'*': "3.0"})
         self.expectCommands(
-            ExpectShell(workdir='build', usePTY=False, command=['./cleanup.sh']) +
-            # note missing parameters
+            ExpectShell(workdir='wkdir', usePTY=False, interruptSignal='DIE',
+                        command=['cmd', 'arg']) +
             0,
         )
         self.expectOutcome(result=SUCCESS)
         yield self.runStep()
-        self.assertEqual(self.step.getLog('stdio').header,
-                         '')
+        self.assertEqual(self.step.getLog('stdio').header, '')
 
     @defer.inlineCallbacks
     def test_description(self):
-        self.setupStep(SimpleShellCommand(
-            command=['foo', properties.Property('bar', 'BAR')]), wantDefaultWorkdir=False)
+        self.setupStep(SimpleShellCommand(command=['foo', properties.Property('bar', 'BAR')]))
         self.expectCommands(
-            ExpectShell(workdir='build', command=['foo', 'BAR']) +
+            ExpectShell(workdir='wkdir', command=['foo', 'BAR']) +
             0,
         )
         self.expectOutcome(result=SUCCESS, state_string="'foo BAR'")
diff --git a/master/buildbot/test/unit/steps/test_shell.py b/master/buildbot/test/unit/steps/test_shell.py
--- a/master/buildbot/test/unit/steps/test_shell.py
+++ b/master/buildbot/test/unit/steps/test_shell.py
@@ -228,7 +228,7 @@ def test_run_misparsed(self):
         self.expectCommands(
             ExpectShell(workdir='wkdir',
                         command=['du', '-s', '-k', '.'])
-            + ExpectShell.log('stdio', stdio='abcdef\n')
+            + ExpectShell.log('stdio', stdout='abcdef\n')
             + 0
         )
         self.expectOutcome(result=WARNINGS,
diff --git a/master/buildbot/test/unit/steps/test_shellsequence.py b/master/buildbot/test/unit/steps/test_shellsequence.py
--- a/master/buildbot/test/unit/steps/test_shellsequence.py
+++ b/master/buildbot/test/unit/steps/test_shellsequence.py
@@ -21,7 +21,6 @@
 from buildbot.process.results import SUCCESS
 from buildbot.process.results import WARNINGS
 from buildbot.steps import shellsequence
-from buildbot.test.fake.remotecommand import Expect
 from buildbot.test.fake.remotecommand import ExpectShell
 from buildbot.test.util import config as configmixin
 from buildbot.test.util import steps
@@ -76,14 +75,12 @@ def testShellArgInput(self):
             arg.validateAttributes()
 
     def testShellArgsAreRendered(self):
-        arg1 = shellsequence.ShellArg(command=WithProperties('make %s', 'project'),
-                                      logname=WithProperties('make %s', 'project'))
+        arg1 = shellsequence.ShellArg(command=WithProperties('make %s', 'project'))
         self.setupStep(
             shellsequence.ShellSequence(commands=[arg1],
                                         workdir='build'))
         self.properties.setProperty("project", "BUILDBOT-TEST", "TEST")
-        self.expectCommands(ExpectShell(workdir='build', command='make BUILDBOT-TEST')
-                            + 0 + Expect.log('stdio make BUILDBOT-TEST'))
+        self.expectCommands(ExpectShell(workdir='build', command='make BUILDBOT-TEST') + 0)
         # TODO: need to factor command-summary stuff into a utility method and
         # use it here
         self.expectOutcome(result=SUCCESS, state_string="'make BUILDBOT-TEST'")
@@ -114,13 +111,12 @@ def testSanityChecksAreDoneInRuntimeWhenDynamicCmdIsInvalidShellArg(self):
 
     def testMultipleCommandsAreRun(self):
         arg1 = shellsequence.ShellArg(command='make p1')
-        arg2 = shellsequence.ShellArg(command='deploy p1', logname='deploy')
+        arg2 = shellsequence.ShellArg(command='deploy p1')
         self.setupStep(
             shellsequence.ShellSequence(commands=[arg1, arg2],
                                         workdir='build'))
         self.expectCommands(ExpectShell(workdir='build', command='make p1') + 0,
-                            ExpectShell(workdir='build', command='deploy p1') + 0 +
-                            Expect.log('stdio deploy p1'))
+                            ExpectShell(workdir='build', command='deploy p1') + 0)
         self.expectOutcome(result=SUCCESS, state_string="'deploy p1'")
         return self.runStep()
 
@@ -166,16 +162,13 @@ def testShellArgsAreRenderedAnewAtEachBuild(self):
         This unit test makes sure that ShellArg instances are rendered anew at
         each new build.
         """
-        arg = shellsequence.ShellArg(command=WithProperties('make %s', 'project'),
-                                     logname=WithProperties('make %s', 'project'))
+        arg = shellsequence.ShellArg(command=WithProperties('make %s', 'project'))
         step = shellsequence.ShellSequence(commands=[arg], workdir='build')
 
         # First "build"
         self.setupStep(step)
         self.properties.setProperty("project", "BUILDBOT-TEST-1", "TEST")
-        self.expectCommands(ExpectShell(workdir='build',
-                            command='make BUILDBOT-TEST-1') + 0 +
-                            Expect.log('stdio make BUILDBOT-TEST-1'))
+        self.expectCommands(ExpectShell(workdir='build', command='make BUILDBOT-TEST-1') + 0)
         self.expectOutcome(result=SUCCESS,
                            state_string="'make BUILDBOT-TEST-1'")
         self.runStep()
@@ -183,9 +176,7 @@ def testShellArgsAreRenderedAnewAtEachBuild(self):
         # Second "build"
         self.setupStep(step)
         self.properties.setProperty("project", "BUILDBOT-TEST-2", "TEST")
-        self.expectCommands(ExpectShell(workdir='build',
-                            command='make BUILDBOT-TEST-2') + 0 +
-                            Expect.log('stdio make BUILDBOT-TEST-2'))
+        self.expectCommands(ExpectShell(workdir='build', command='make BUILDBOT-TEST-2') + 0)
         self.expectOutcome(result=SUCCESS,
                            state_string="'make BUILDBOT-TEST-2'")
 
diff --git a/master/buildbot/test/unit/steps/test_source_git.py b/master/buildbot/test/unit/steps/test_source_git.py
--- a/master/buildbot/test/unit/steps/test_source_git.py
+++ b/master/buildbot/test/unit/steps/test_source_git.py
@@ -1674,7 +1674,7 @@ def test_mode_incremental_oldworker(self):
                            mode='incremental', progress=True))
         self.step.build.getWorkerCommandVersion = lambda cmd, oldversion: "2.15"
         self.expectCommands(
-            ExpectShell(workdir='wkdir',
+            ExpectShell(workdir='wkdir', interruptSignal='TERM',
                         command=['git', '--version'])
             + ExpectShell.log('stdio',
                               stdout='git version 1.7.5')
@@ -1685,15 +1685,15 @@ def test_mode_incremental_oldworker(self):
             Expect('stat', dict(file='wkdir/.git',
                                 logEnviron=True))
             + 0,
-            ExpectShell(workdir='wkdir',
+            ExpectShell(workdir='wkdir', interruptSignal='TERM',
                         command=['git', 'fetch', '-f', '-t',
                                  'http://github.com/buildbot/buildbot.git',
                                  'HEAD', '--progress'])
             + 0,
-            ExpectShell(workdir='wkdir',
+            ExpectShell(workdir='wkdir', interruptSignal='TERM',
                         command=['git', 'checkout', '-f', 'FETCH_HEAD'])
             + 0,
-            ExpectShell(workdir='wkdir',
+            ExpectShell(workdir='wkdir', interruptSignal='TERM',
                         command=['git', 'rev-parse', 'HEAD'])
             + ExpectShell.log('stdio',
                               stdout='f6ad368298bd941e934a41f3babc827b2aa95a1d')
@@ -2675,7 +2675,7 @@ def test_mode_incremental_no_existing_repo_oldworker(self):
                            mode='incremental'))
         self.step.build.getWorkerCommandVersion = lambda cmd, oldversion: "2.15"
         self.expectCommands(
-            ExpectShell(workdir='wkdir',
+            ExpectShell(workdir='wkdir', interruptSignal='TERM',
                         command=['git', '--version'])
             + ExpectShell.log('stdio',
                               stdout='git version 1.7.5')
@@ -2686,12 +2686,12 @@ def test_mode_incremental_no_existing_repo_oldworker(self):
             Expect('stat', dict(file='wkdir/.git',
                                 logEnviron=True))
             + 1,
-            ExpectShell(workdir='wkdir',
+            ExpectShell(workdir='wkdir', interruptSignal='TERM',
                         command=['git', 'clone',
                                  'http://github.com/buildbot/buildbot.git',
                                  '.', '--progress'])
             + 0,
-            ExpectShell(workdir='wkdir',
+            ExpectShell(workdir='wkdir', interruptSignal='TERM',
                         command=['git', 'rev-parse', 'HEAD'])
             + ExpectShell.log('stdio',
                               stdout='f6ad368298bd941e934a41f3babc827b2aa95a1d')