Repository: celery/celery

Issue:
GroupResult.as_tuple forget serialize group parent
```
celery -A ct report

software -> celery:4.0.2 (latentcall) kombu:4.0.2 py:2.7.9
            billiard:3.5.0.2 py-amqp:2.1.4
platform -> system:Linux arch:64bit, ELF imp:CPython
loader   -> celery.loaders.app.AppLoader
settings -> transport:amqp results:cache+memcached://127.0.0.1:11211/

broker_url: u'amqp://guest:********@localhost:5672//'
result_backend: u'cache+memcached://127.0.0.1:11211/'

```

**Steps to reproduce**

```
# ct.py

# ct.py
from __future__ import absolute_import, unicode_literals
from celery import Celery

app = Celery(
    'ct',
    broker='amqp://guest@localhost//',
    backend='cache+memcached://127.0.0.1:11211/'
)

@app.task
def nothing(*args):
   return args

if __name__ == '__main__':
    app.start()
```

Start worker

`python ct.py worker -l INFO`

In `python` console

```
>>> from celery import chain, group
>>> from celery.result import result_from_tuple
>>> from ct import nothing
>>> c = nothing.s() | group(nothing.s(), nothing.s())
>>> r = c.delay()
>>> r
<GroupResult: f46d7bcc-3140-40e8-9e21-d2f74eef6f28 [4854af6e-8e4a-48d8-98e9-d5f0eefcce20, 5c8d4ec1-84ff-4845-a9b2-9f07fd9bed19]>
>>> r.parent
<AsyncResult: a359ae60-ed18-4876-a67b-fc8cd83759a2>
>>> t = r.as_tuple()
>>> r2 = result_from_tuple(t)
>>> r2
<GroupResult: f46d7bcc-3140-40e8-9e21-d2f74eef6f28 [4854af6e-8e4a-48d8-98e9-d5f0eefcce20, 5c8d4ec1-84ff-4845-a9b2-9f07fd9bed19]>
>>> r2.parent

>>> r2.parent is None
True
```

**Expected behavior**

```
>>> r2 = result_from_tuple(t)
>>> r2.parent
<AsyncResult: a359ae60-ed18-4876-a67b-fc8cd83759a2>
>>>
```



Discussion:
does this produce same against master branch
> does this produce same against master branch

yes, it is

Resolving patch:
diff --git a/celery/result.py b/celery/result.py
--- a/celery/result.py
+++ b/celery/result.py
@@ -815,6 +815,7 @@ class GroupResult(ResultSet):
     Arguments:
         id (str): The id of the group.
         results (Sequence[AsyncResult]): List of result instances.
+        parent (ResultBase): Parent result of this group.
     """
 
     #: The UUID of the group.
@@ -823,8 +824,9 @@ class GroupResult(ResultSet):
     #: List/iterator of results in the group
     results = None
 
-    def __init__(self, id=None, results=None, **kwargs):
+    def __init__(self, id=None, results=None, parent=None, **kwargs):
         self.id = id
+        self.parent = parent
         ResultSet.__init__(self, results, **kwargs)
 
     def save(self, backend=None):
@@ -853,7 +855,11 @@ def __bool__(self):
 
     def __eq__(self, other):
         if isinstance(other, GroupResult):
-            return other.id == self.id and other.results == self.results
+            return (
+                other.id == self.id and
+                other.results == self.results and
+                other.parent == self.parent
+            )
         return NotImplemented
 
     def __ne__(self, other):
@@ -865,7 +871,7 @@ def __repr__(self):
                                          ', '.join(r.id for r in self.results))
 
     def as_tuple(self):
-        return self.id, [r.as_tuple() for r in self.results]
+        return (self.id, self.parent), [r.as_tuple() for r in self.results]
 
     @property
     def children(self):
@@ -969,13 +975,15 @@ def result_from_tuple(r, app=None):
     Result = app.AsyncResult
     if not isinstance(r, ResultBase):
         res, nodes = r
-        if nodes:
-            return app.GroupResult(
-                res, [result_from_tuple(child, app) for child in nodes],
-            )
-        # previously didn't include parent
         id, parent = res if isinstance(res, (list, tuple)) else (res, None)
         if parent:
             parent = result_from_tuple(parent, app)
+
+        if nodes:
+            return app.GroupResult(
+                id, [result_from_tuple(child, app) for child in nodes],
+                parent=parent,
+            )
+
         return Result(id, parent=parent)
     return r


Test patch:
diff --git a/t/unit/tasks/test_result.py b/t/unit/tasks/test_result.py
--- a/t/unit/tasks/test_result.py
+++ b/t/unit/tasks/test_result.py
@@ -595,6 +595,21 @@ def test_len(self):
     def test_eq_other(self):
         assert self.ts != 1
 
+    def test_eq_with_parent(self):
+        # GroupResult instances with different .parent are not equal
+        grp_res = self.app.GroupResult(
+            uuid(), [self.app.AsyncResult(uuid()) for _ in range(10)],
+            parent=self.app.AsyncResult(uuid())
+        )
+        grp_res_2 = self.app.GroupResult(grp_res.id, grp_res.results)
+        assert grp_res != grp_res_2
+
+        grp_res_2.parent = self.app.AsyncResult(uuid())
+        assert grp_res != grp_res_2
+
+        grp_res_2.parent = grp_res.parent
+        assert grp_res == grp_res_2
+
     @pytest.mark.usefixtures('depends_on_current_app')
     def test_pickleable(self):
         assert pickle.loads(pickle.dumps(self.ts))
@@ -892,3 +907,29 @@ def test_GroupResult(self):
         )
         assert x, result_from_tuple(x.as_tuple() == self.app)
         assert x, result_from_tuple(x == self.app)
+
+    def test_GroupResult_with_parent(self):
+        parent = self.app.AsyncResult(uuid())
+        result = self.app.GroupResult(
+            uuid(), [self.app.AsyncResult(uuid()) for _ in range(10)],
+            parent
+        )
+        second_result = result_from_tuple(result.as_tuple(), self.app)
+        assert second_result == result
+        assert second_result.parent == parent
+
+    def test_GroupResult_as_tuple(self):
+        parent = self.app.AsyncResult(uuid())
+        result = self.app.GroupResult(
+            'group-result-1',
+            [self.app.AsyncResult('async-result-{}'.format(i))
+             for i in range(2)],
+            parent
+        )
+        (result_id, parent_id), group_results = result.as_tuple()
+        assert result_id == result.id
+        assert parent_id == parent.id
+        assert isinstance(group_results, list)
+        expected_grp_res = [(('async-result-{}'.format(i), None), None)
+                            for i in range(2)]
+        assert group_results == expected_grp_res