Repository: DataBiosphere/toil

Issue:
`job.addChild().encapsulate` doesn't work
A.addChild(B).encapsulate creates the relationship between jobs A and B before encapsulating B, thereby causing the encapsulated version of B to have 2 parents with unique roots (the original parent, and the synthetic root for the encapsulation subgraph) Adding a relationship between the synthetic encapsulating parent and the original parent fixes this (the if block in the snippet).
```
    def __init__(self, job):
        """
        :param toil.job.Job job: the job to encapsulate.
        """
        # Giving the root of the subgraph the same resources as the first job in the subgraph.
        Job.__init__(self, **job._requirements)
        if job._directPredecessors:
            for job_ in job._directPredecessors:
                job_.addChild(self)
        self.encapsulatedJob = job
        Job.addChild(self, job)
        # Use small resource requirements for dummy Job instance.
        self.encapsulatedFollowOn = Job(disk='1M', memory='32M', cores=0.1)
        Job.addFollowOn(self, self.encapsulatedFollowOn)
```


Discussion:
Would it be clearer and cleaner to allow only graphs to be encapsulated? That is, only jobs with no parents. In the above example, A.addChild(B), would give an error, and would need to be done in two steps: 
```python
B'=B.encapsultate()
A.addChild(B')
```
Well the alternative is to do
```
B = job.wrapJobFn().encapsulate()
A.addChild(B)
```

I would be fine if `A.addChild(B).encapsulate()` explicitly raised an error saying that syntax is incorrect.

Resolving patch:
diff --git a/src/toil/job.py b/src/toil/job.py
--- a/src/toil/job.py
+++ b/src/toil/job.py
@@ -1473,6 +1473,12 @@ class EncapsulatedJob(Job):
         #  B will run after A and all its successors have completed, A and its subgraph of
         # successors in effect appear to be just one job.
 
+    If the job being encapsulated has predecessors (e.g. is not the root job), then the encapsulated
+    job will inherit these predecessors. If predecessors are added to the job being encapsulated
+    after the encapsulated job is created then the encapsulating job will NOT inherit these
+    predecessors automatically. Care should be exercised to ensure the encapsulated job has the
+    proper set of predecessors.
+
     The return value of an encapsulatd job (as accessed by the :func:`toil.job.Job.rv` function)
     is the return value of the root job, e.g. A().encapsulate().rv() and A().rv() will resolve to
     the same value after A or A.encapsulate() has been run.
@@ -1483,6 +1489,11 @@ def __init__(self, job):
         """
         # Giving the root of the subgraph the same resources as the first job in the subgraph.
         Job.__init__(self, **job._requirements)
+        # Ensure that the encapsulated job has the same direct predecessors as the job
+        # being encapsulated.
+        if job._directPredecessors:
+            for job_ in job._directPredecessors:
+                job_.addChild(self)
         self.encapsulatedJob = job
         Job.addChild(self, job)
         # Use small resource requirements for dummy Job instance.


Test patch:
diff --git a/src/toil/test/src/jobEncapsulationTest.py b/src/toil/test/src/jobEncapsulationTest.py
--- a/src/toil/test/src/jobEncapsulationTest.py
+++ b/src/toil/test/src/jobEncapsulationTest.py
@@ -50,7 +50,22 @@ def testEncapsulation(self):
             self.assertEquals(open(outFile, 'r').readline(), "ABCDE")
         finally:
             os.remove(outFile)
-            
+
+    def testAddChildEncapsulate(self):
+        """
+        Make sure that the encapsulate child does not have two pareents
+        with unique roots.
+        """
+        # Temporary file
+        a = T.wrapFn(noOp)
+        b = T.wrapFn(noOp)
+        a.addChild(b).encapsulate()
+        self.assertEquals(len(a.getRootJobs()), 1)
+
+
+def noOp():
+    pass
+
 def encapsulatedJobFn(job, string, outFile):
     a = job.addChildFn(f, string, outFile)
     b = a.addFollowOnFn(f, a.rv(), outFile)