Repository: buildbot/buildbot

Issue:
prepareSourcestampListForTrigger fails when sourcestamp contains a patch_body
Our Buildbot configuration has a builder that will trigger another builder but this fails when we use the Buildbot try command to submit a build with a patch. The patch file is binary but the `prepareSourcestampListForTrigger()` function in `master/buildbot/steps/trigger.py` calls `asDict()` on the buildrequest instance and that fails an assertion that all of the fields in the buildrequest have values that are of type string, int, or None. The assertion fails because the patch_body field is binary (as I think it probably should be).

Here is a stack trace in case that helps:
```
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/twisted/internet/defer.py", line 1442, in gotResult
    _inlineCallbacks(r, g, deferred)
  File "/usr/lib/python3/dist-packages/twisted/internet/defer.py", line 1386, in _inlineCallbacks
    result = g.send(result)
  File "/home/cit/.local/lib/python3.6/site-packages/buildbot/process/buildstep.py", line 567, in startStep
    self.results = yield self.run()
  File "/usr/lib/python3/dist-packages/twisted/internet/defer.py", line 1532, in unwindGenerator
    return _inlineCallbacks(None, gen, Deferred())
--- <exception caught here> ---
  File "/usr/lib/python3/dist-packages/twisted/internet/defer.py", line 1386, in _inlineCallbacks
    result = g.send(result)
  File "/home/cit/.local/lib/python3.6/site-packages/buildbot/steps/trigger.py", line 257, in run
    ss_for_trigger = self.prepareSourcestampListForTrigger()
  File "/home/cit/.local/lib/python3.6/site-packages/buildbot/steps/trigger.py", line 167, in prepareSourcestampListForTrigger
    ss_for_trigger[ss.codebase] = ss.asDict()
  File "/home/cit/.local/lib/python3.6/site-packages/buildbot/process/buildrequest.py", line 137, in asDict
    ), result
builtins.AssertionError: {'branch': None, 'revision': '8a3fad6b5f844704f678af71d10fdc822ff11641', 'repository': 'ssh://some-private-git-host/infra/bb-test.git', 'project': '', 'codebase': '', 'patch_level': 1, 'patch_body': b':100755 100755 1465b4e... 0000000... M\ttest\n\ndiff --git a/test b/test\nindex 1465b4e..c8548bb 100755\n--- a/test\n+++ b/test\n@@ -4,5 +4,5 @@ echo "This is a fake test to be run by a test buildbot"\n \n echo "Still not actually running any tests"\n \n-echo "NO TESTS PASSED!!!"\n+echo "NO TESTS PASSED, BUT ALL TESTS PASSED!!!"\n \n', 'patch_subdir': '', 'patch_author': 'ehohenstein', 'patch_comment': 'ehohenstein'}
```

Also, here is a patch (based on v2.6.0) with the fix that I am suggesting, though I'm not familiar enough with Buildbot code to know if this is really the right fix for this problem:

```
diff --git a/master/buildbot/process/buildrequest.py b/master/buildbot/process/buildrequest.py
index be37254..45201e0 100644
--- a/master/buildbot/process/buildrequest.py
+++ b/master/buildbot/process/buildrequest.py
@@ -132,7 +132,7 @@ class TempSourceStamp:
             result['patch_%s' % attr] = patch.get(attr)

         assert all(
-            isinstance(val, (str, int, type(None)))
+            isinstance(val, (str, int, type(None), bytes))
             for attr, val in result.items()
         ), result
         return result
```


Discussion:
Thanks for the bug report, indeed it looks like the behavior should be as you propose. I need to dig a little bit to see if there are any gotchas.
For what it's worth, I've tested this fix and found it to work as expected with our buildbot configuration, though what I actually tested was an *equivalent* change using python3 and buildbot v1.2.0.

Resolving patch:
diff --git a/master/buildbot/process/buildrequest.py b/master/buildbot/process/buildrequest.py
--- a/master/buildbot/process/buildrequest.py
+++ b/master/buildbot/process/buildrequest.py
@@ -98,6 +98,14 @@ class TempSourceStamp:
 
     ATTRS = ('branch', 'revision', 'repository', 'project', 'codebase')
 
+    PATCH_ATTRS = (
+        ('patch_level', 'level'),
+        ('patch_body', 'body'),
+        ('patch_subdir', 'subdir'),
+        ('patch_author', 'author'),
+        ('patch_comment', 'comment')
+    )
+
     def __init__(self, ssdict):
         self._ssdict = ssdict
 
@@ -118,8 +126,6 @@ def __getattr__(self, attr):
     def asSSDict(self):
         return self._ssdict
 
-    PATCH_ATTRS = ('level', 'body', 'subdir', 'author', 'comment')
-
     def asDict(self):
         # This return value should match the kwargs to
         # SourceStampsConnectorComponent.findSourceStampId
@@ -128,11 +134,11 @@ def asDict(self):
             result[attr] = self._ssdict.get(attr)
 
         patch = self._ssdict.get('patch') or {}
-        for attr in self.PATCH_ATTRS:
-            result['patch_{}'.format(attr)] = patch.get(attr)
+        for patch_attr, attr in self.PATCH_ATTRS:
+            result[patch_attr] = patch.get(attr)
 
         assert all(
-            isinstance(val, (str, int, type(None)))
+            isinstance(val, (str, int, bytes, type(None)))
             for attr, val in result.items()
         ), result
         return result


Test patch:
diff --git a/master/buildbot/test/unit/process/test_buildrequest.py b/master/buildbot/test/unit/process/test_buildrequest.py
--- a/master/buildbot/test/unit/process/test_buildrequest.py
+++ b/master/buildbot/test/unit/process/test_buildrequest.py
@@ -13,6 +13,8 @@
 #
 # Copyright Buildbot Team Members
 
+import datetime
+
 import mock
 
 from twisted.internet import defer
@@ -287,6 +289,93 @@ def collapseRequests_fn(master, builder, brdict1, brdict2):
         yield self.do_request_collapse(rows, [21], [20])
 
 
+class TestSourceStamp(unittest.TestCase):
+    def test_asdict_minimal(self):
+        ssdatadict = {
+            'ssid': '123',
+            'branch': None,
+            'revision': None,
+            'patch': None,
+            'repository': 'testrepo',
+            'codebase': 'testcodebase',
+            'project': 'testproject',
+            'created_at': datetime.datetime(2019, 4, 1, 23, 38, 33, 154354),
+        }
+        ss = buildrequest.TempSourceStamp(ssdatadict)
+
+        self.assertEqual(ss.asDict(), {
+            'branch': None,
+            'codebase': 'testcodebase',
+            'patch_author': None,
+            'patch_body': None,
+            'patch_comment': None,
+            'patch_level': None,
+            'patch_subdir': None,
+            'project': 'testproject',
+            'repository': 'testrepo',
+            'revision': None
+        })
+
+    def test_asdict_no_patch(self):
+        ssdatadict = {
+            'ssid': '123',
+            'branch': 'testbranch',
+            'revision': 'testrev',
+            'patch': None,
+            'repository': 'testrepo',
+            'codebase': 'testcodebase',
+            'project': 'testproject',
+            'created_at': datetime.datetime(2019, 4, 1, 23, 38, 33, 154354),
+        }
+        ss = buildrequest.TempSourceStamp(ssdatadict)
+
+        self.assertEqual(ss.asDict(), {
+            'branch': 'testbranch',
+            'codebase': 'testcodebase',
+            'patch_author': None,
+            'patch_body': None,
+            'patch_comment': None,
+            'patch_level': None,
+            'patch_subdir': None,
+            'project': 'testproject',
+            'repository': 'testrepo',
+            'revision': 'testrev',
+        })
+
+    def test_asdict_with_patch(self):
+        ssdatadict = {
+            'ssid': '123',
+            'branch': 'testbranch',
+            'revision': 'testrev',
+            'patch': {
+                'patchid': 1234,
+                'body': b'testbody',
+                'level': 2,
+                'author': 'testauthor',
+                'comment': 'testcomment',
+                'subdir': 'testsubdir',
+            },
+            'repository': 'testrepo',
+            'codebase': 'testcodebase',
+            'project': 'testproject',
+            'created_at': datetime.datetime(2019, 4, 1, 23, 38, 33, 154354),
+        }
+        ss = buildrequest.TempSourceStamp(ssdatadict)
+
+        self.assertEqual(ss.asDict(), {
+            'branch': 'testbranch',
+            'codebase': 'testcodebase',
+            'patch_author': 'testauthor',
+            'patch_body': b'testbody',
+            'patch_comment': 'testcomment',
+            'patch_level': 2,
+            'patch_subdir': 'testsubdir',
+            'project': 'testproject',
+            'repository': 'testrepo',
+            'revision': 'testrev'
+        })
+
+
 class TestBuildRequest(TestReactorMixin, unittest.TestCase):
 
     def setUp(self):