Update perf dashboard upload logic to support uploading binaries from owned commits.
authordewei_zhu@apple.com <dewei_zhu@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 24 Oct 2017 00:22:57 +0000 (00:22 +0000)
committerdewei_zhu@apple.com <dewei_zhu@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 24 Oct 2017 00:22:57 +0000 (00:22 +0000)
https://bugs.webkit.org/show_bug.cgi?id=178610

Reviewed by Ryosuke Niwa.

Update build requests to 'completed' only when all commit set items are satisfied.
Extend 'repositoryList' parameter to be able to specified own commit information.
Items in 'repositoryList' can either be a string for top level repository,
or a dictionary with two keys: 'ownerRepository' and 'ownedRepository'.

* public/api/upload-root.php: Extend upload logic for support uploading binaries from owned commits.
* server-tests/api-upload-root-tests.js: Added unit tests.
* server-tests/tools-sync-buildbot-integration-tests.js: Added unit tests.

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@223864 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/public/api/upload-root.php
Websites/perf.webkit.org/server-tests/api-upload-root-tests.js
Websites/perf.webkit.org/server-tests/tools-sync-buildbot-integration-tests.js

index 1d757c3..f129ee7 100644 (file)
@@ -1,3 +1,19 @@
+2017-10-20  Dewei Zhu  <dewei_zhu@apple.com>
+
+        Update perf dashboard upload logic to support uploading binaries from owned commits.
+        https://bugs.webkit.org/show_bug.cgi?id=178610
+
+        Reviewed by Ryosuke Niwa.
+
+        Update build requests to 'completed' only when all commit set items are satisfied.
+        Extend 'repositoryList' parameter to be able to specified own commit information.
+        Items in 'repositoryList' can either be a string for top level repository,
+        or a dictionary with two keys: 'ownerRepository' and 'ownedRepository'.
+
+        * public/api/upload-root.php: Extend upload logic for support uploading binaries from owned commits.
+        * server-tests/api-upload-root-tests.js: Added unit tests.
+        * server-tests/tools-sync-buildbot-integration-tests.js: Added unit tests.
+
 2017-10-05  Dewei Zhu  <dewei_zhu@apple.com>
 
         Add try-bot button on perf analysis status page.
index 973e956..b72dac7 100644 (file)
@@ -39,8 +39,6 @@ function main()
             $build_id = $db->insert_row('builds', 'build', $build_info);
             if (!$build_id)
                 return array('status' => 'FailedToCreateBuild', 'build' => $build_info);
-            if (!$db->update_row('build_requests', 'request', array('id' => $build_request_id), array('status' => 'completed', 'build' => $build_id)))
-                return array('status' => 'FailedToUpdateBuildRequest', 'buildRequest' => $build_request_id);
 
             foreach ($commit_set_items_to_update as $commit_id) {
                 if (!$db->update_row('commit_set_items', 'commitset',
@@ -48,36 +46,59 @@ function main()
                     array('commit' => $commit_id, 'root_file' => $root_file_id), '*'))
                     return array('status' => 'FailedToUpdateCommitSet', 'commitSet' => $commit_set_id, 'commit' => $commit_id);
             }
+            if (!all_commit_set_items_are_satisfied($db, $commit_set_id))
+                return NULL;
+            if (!$db->update_row('build_requests', 'request', array('id' => $build_request_id), array('status' => 'completed', 'build' => $build_id)))
+                return array('status' => 'FailedToUpdateBuildRequest', 'buildRequest' => $build_request_id);
             return NULL;
         });
 
     exit_with_success(array('uploadedFile' => $uploaded_file));
 }
 
+function all_commit_set_items_are_satisfied($db, $commit_set_id)
+{
+    return !$db->select_first_row('commit_set_items', 'commitset', array('root_file' => NULL, 'requires_build' => TRUE, 'set' => $commit_set_id));
+}
+
 function compute_commit_set_items_to_update($db, $commit_set_id, $repository_name_list)
 {
     if (!is_array($repository_name_list))
         exit_with_error('InvalidRepositoryList', array('repositoryList' => $repository_name_list));
 
-    $commit_repository_rows_in_set = $db->query_and_fetch_all('SELECT * FROM repositories, commits, commit_set_items
-        WHERE repository_id = commit_repository AND commit_id = commitset_commit
+    $commit_repository_rows_in_set = $db->query_and_fetch_all('SELECT commit_set_items.commitset_commit as commitset_commit,
+        owned.repository_name as repository_name, owner.repository_name as owner_repository_name
+        FROM repositories as owned LEFT OUTER JOIN repositories as owner ON owned.repository_owner = owner.repository_id, commits, commit_set_items
+        WHERE owned.repository_id = commit_repository AND commit_id = commitset_commit
             AND commitset_set = $1', array($commit_set_id));
 
     $commit_by_repository_name = array();
+    $commit_by_owner_and_owned_repository_names = array();
     if ($commit_repository_rows_in_set) {
         foreach ($commit_repository_rows_in_set as $row)
-            $commit_by_repository_name[$row['repository_name']] = $row['commitset_commit'];
+            if ($row['owner_repository_name']) {
+                $owned_repositories = &array_ensure_item_has_array($commit_by_owner_and_owned_repository_names, $row['owner_repository_name']);
+                $owned_repositories[$row['repository_name']] = $row['commitset_commit'];
+            } else
+                $commit_by_repository_name[$row['repository_name']] = $row['commitset_commit'];
     }
 
     $commit_set_items_to_update = array();
     foreach ($repository_name_list as $repository_name) {
-        $commit_id = array_get($commit_by_repository_name, $repository_name);
+        $commit_id = NULL;
+        if (is_string($repository_name))
+            $commit_id = array_get($commit_by_repository_name, $repository_name);
+        else if (is_array($repository_name)) {
+            if (!array_key_exists('ownerRepository', $repository_name) || !array_key_exists('ownedRepository', $repository_name))
+                exit_with_error('InvalidKeyForRepository', array('repositoryName' => $repository_name, 'commitSet' => $commit_set_id));
+            $commit_id = array_get(array_get($commit_by_owner_and_owned_repository_names, $repository_name['ownerRepository'], array()), $repository_name['ownedRepository']);
+        }
         if (!$commit_id)
             exit_with_error('InvalidRepository', array('repositoryName' => $repository_name, 'commitSet' => $commit_set_id));
         array_push($commit_set_items_to_update, $commit_id);
     }
     if (!$commit_set_items_to_update)
-        exit_with_error('InvalidRepositoryList', array('repositoryList' => $repository_list));
+        exit_with_error('InvalidRepositoryList', array('repositoryList' => $repository_name_list, 'commitSet' => $commit_set_id));
 
     return $commit_set_items_to_update;
 }
index bffcf60..ab8340e 100644 (file)
@@ -10,17 +10,17 @@ const TemporaryFile = require('./resources/temporary-file.js').TemporaryFile;
 const addSlaveForReport = require('./resources/common-operations.js').addSlaveForReport;
 const prepareServerTest = require('./resources/common-operations.js').prepareServerTest;
 
-function makeReport(rootFile, buildRequestId = 1)
+function makeReport(rootFile, buildRequestId = 1, repositoryList = ['WebKit'], buildTime = '2017-05-10T02:54:08.666')
 {
     return {
         slaveName: 'someSlave',
         slavePassword: 'somePassword',
         builderName: 'someBuilder',
         buildNumber: 123,
-        buildTime: '2017-05-10T02:54:08.666',
+        buildTime: buildTime,
         buildRequest: buildRequestId,
         rootFile: rootFile,
-        repositoryList: '["WebKit"]',
+        repositoryList: JSON.stringify(repositoryList),
     };
 }
 
@@ -73,6 +73,48 @@ function createTestGroupWihPatch()
     });
 }
 
+function createTestGroupWithPatchAndOwnedCommits()
+{
+    const triggerableConfiguration = {
+        'slaveName': 'sync-slave',
+        'slavePassword': 'password',
+        'triggerable': 'build-webkit',
+        'configurations': [
+            {test: MockData.someTestId(), platform: MockData.somePlatformId()},
+        ],
+        'repositoryGroups': [
+            {name: 'webkit', repositories: [
+                {repository: MockData.webkitRepositoryId(), acceptsPatch: true}
+            ]}
+        ]
+    };
+
+    const db = TestServer.database();
+    return MockData.addMockData(db).then(() => {
+        return Promise.all([TemporaryFile.makeTemporaryFile('patch.dat', 'patch file'), Manifest.fetch()]);
+    }).then((result) => {
+        const patchFile = result[0];
+        return Promise.all([UploadedFile.uploadFile(patchFile), TestServer.remoteAPI().postJSON('/api/update-triggerable/', triggerableConfiguration)]);
+    }).then((result) => {
+        const patchFile = result[0];
+        const someTest = Test.findById(MockData.someTestId());
+        const webkit = Repository.findById(MockData.webkitRepositoryId());
+        const ownedSJC = Repository.findById(MockData.ownedJSCRepositoryId());
+        const set1 = new CustomCommitSet;
+        set1.setRevisionForRepository(webkit, '191622', patchFile);
+        set1.setRevisionForRepository(ownedSJC, 'owned-jsc-6161', null, '191622');
+        const set2 = new CustomCommitSet;
+        set2.setRevisionForRepository(webkit, '192736');
+        set2.setRevisionForRepository(ownedSJC, 'owned-jsc-9191', null, '192736');
+
+        return TestGroup.createWithTask('custom task', Platform.findById(MockData.somePlatformId()), someTest, 'some group', 2, [set1, set2]);
+    }).then((task) => {
+        return TestGroup.findAllByTask(task.id())[0];
+    }).then((group) => {
+        return db.query('UPDATE analysis_test_groups SET testgroup_author = $1', ['someUser']).then(() => group);
+    });
+}
+
 describe('/api/upload-root/', function () {
     prepareServerTest(this);
     TemporaryFile.inject();
@@ -199,6 +241,160 @@ describe('/api/upload-root/', function () {
         });
     });
 
+    it('should reject when using invalid key to specify an owned repository', () => {
+        let webkit;
+        let webkitPatch;
+        let ownedJSC;
+        let testGroup;
+        let buildRequest;
+        let otherBuildRequest;
+        let uploadedRoot;
+        return createTestGroupWithPatchAndOwnedCommits().then((group) => {
+            webkit = Repository.findById(MockData.webkitRepositoryId());
+            ownedJSC = Repository.findById(MockData.ownedJSCRepositoryId());
+
+            testGroup = group;
+            buildRequest = testGroup.buildRequests()[0];
+            assert.equal(testGroup.buildRequests().length, 6);
+            assert(buildRequest.isBuild());
+            assert(!buildRequest.isTest());
+            assert(!buildRequest.hasFinished());
+            assert(buildRequest.isPending());
+            assert.equal(buildRequest.buildId(), null);
+
+            const commitSet = buildRequest.commitSet();
+            assert.equal(commitSet.revisionForRepository(webkit), '191622');
+            webkitPatch = commitSet.patchForRepository(webkit);
+            assert(webkitPatch instanceof UploadedFile);
+            assert.equal(webkitPatch.filename(), 'patch.dat');
+            assert.equal(commitSet.rootForRepository(webkit), null);
+            assert.equal(commitSet.revisionForRepository(ownedJSC), 'owned-jsc-6161');
+            assert.equal(commitSet.patchForRepository(ownedJSC), null);
+            assert.equal(commitSet.rootForRepository(ownedJSC), null);
+            assert.deepEqual(commitSet.allRootFiles(), []);
+
+            otherBuildRequest = testGroup.buildRequests()[1];
+            const otherCommitSet = otherBuildRequest.commitSet();
+            assert.equal(otherCommitSet.revisionForRepository(webkit), '192736');
+            assert.equal(otherCommitSet.patchForRepository(webkit), null);
+            assert.equal(otherCommitSet.rootForRepository(webkit), null);
+            assert.equal(otherCommitSet.revisionForRepository(ownedJSC), 'owned-jsc-9191');
+            assert.equal(otherCommitSet.patchForRepository(ownedJSC), null);
+            assert.equal(otherCommitSet.rootForRepository(ownedJSC), null);
+            assert.deepEqual(otherCommitSet.allRootFiles(), []);
+
+            return addSlaveAndCreateRootFile();
+        }).then((rootFile) => {
+            const report = makeReport(rootFile, buildRequest.id(), ['WebKit']);
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', report);
+        }).then((response) => {
+            assert.equal(response['status'], 'OK');
+            const uploadedRootRawData = response['uploadedFile'];
+            uploadedRoot = UploadedFile.ensureSingleton(uploadedRootRawData.id, uploadedRootRawData);
+            assert.equal(uploadedRoot.filename(), 'some.dat');
+            return TestGroup.fetchForTask(buildRequest.testGroup().task().id(), true);
+        }).then((testGroups) => {
+            assert.equal(testGroups.length, 1);
+            const group = testGroups[0];
+            assert.equal(group, testGroup);
+            assert.equal(testGroup.buildRequests().length, 6);
+
+            const updatedBuildRequest = testGroup.buildRequests()[0];
+            assert.equal(updatedBuildRequest, buildRequest);
+
+            assert(buildRequest.isBuild());
+            assert(!buildRequest.isTest());
+            assert(!buildRequest.hasFinished());
+            assert.equal(buildRequest.buildId(), null);
+
+            assert.deepEqual(buildRequest.commitSet().allRootFiles(), [uploadedRoot]);
+            assert.deepEqual(otherBuildRequest.commitSet().allRootFiles(), []);
+            return TemporaryFile.makeTemporaryFile('JavaScriptCore-Root.dat', 'JavaScript Content 0');
+        }).then((rootFile) => {
+            const report = makeReport(rootFile, buildRequest.id(), [{ownerRepositoryWrongKey: 'WebKit', ownedRepository: 'JavaScriptCore'}], '2017-05-10T02:54:09.666');
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', report);
+        }).then((response) => {
+            assert.equal(response['status'], 'InvalidKeyForRepository');
+        });
+    });
+
+    it('should reject when reporting an invalid owned repository', () => {
+        let webkit;
+        let webkitPatch;
+        let ownedJSC;
+        let testGroup;
+        let buildRequest;
+        let otherBuildRequest;
+        let uploadedRoot;
+        return createTestGroupWithPatchAndOwnedCommits().then((group) => {
+            webkit = Repository.findById(MockData.webkitRepositoryId());
+            ownedJSC = Repository.findById(MockData.ownedJSCRepositoryId());
+
+            testGroup = group;
+            buildRequest = testGroup.buildRequests()[0];
+            assert.equal(testGroup.buildRequests().length, 6);
+            assert(buildRequest.isBuild());
+            assert(!buildRequest.isTest());
+            assert(!buildRequest.hasFinished());
+            assert(buildRequest.isPending());
+            assert.equal(buildRequest.buildId(), null);
+
+            const commitSet = buildRequest.commitSet();
+            assert.equal(commitSet.revisionForRepository(webkit), '191622');
+            webkitPatch = commitSet.patchForRepository(webkit);
+            assert(webkitPatch instanceof UploadedFile);
+            assert.equal(webkitPatch.filename(), 'patch.dat');
+            assert.equal(commitSet.rootForRepository(webkit), null);
+            assert.equal(commitSet.revisionForRepository(ownedJSC), 'owned-jsc-6161');
+            assert.equal(commitSet.patchForRepository(ownedJSC), null);
+            assert.equal(commitSet.rootForRepository(ownedJSC), null);
+            assert.deepEqual(commitSet.allRootFiles(), []);
+
+            otherBuildRequest = testGroup.buildRequests()[1];
+            const otherCommitSet = otherBuildRequest.commitSet();
+            assert.equal(otherCommitSet.revisionForRepository(webkit), '192736');
+            assert.equal(otherCommitSet.patchForRepository(webkit), null);
+            assert.equal(otherCommitSet.rootForRepository(webkit), null);
+            assert.equal(otherCommitSet.revisionForRepository(ownedJSC), 'owned-jsc-9191');
+            assert.equal(otherCommitSet.patchForRepository(ownedJSC), null);
+            assert.equal(otherCommitSet.rootForRepository(ownedJSC), null);
+            assert.deepEqual(otherCommitSet.allRootFiles(), []);
+
+            return addSlaveAndCreateRootFile();
+        }).then((rootFile) => {
+            const report = makeReport(rootFile, buildRequest.id(), ['WebKit']);
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', report);
+        }).then((response) => {
+            assert.equal(response['status'], 'OK');
+            const uploadedRootRawData = response['uploadedFile'];
+            uploadedRoot = UploadedFile.ensureSingleton(uploadedRootRawData.id, uploadedRootRawData);
+            assert.equal(uploadedRoot.filename(), 'some.dat');
+            return TestGroup.fetchForTask(buildRequest.testGroup().task().id(), true);
+        }).then((testGroups) => {
+            assert.equal(testGroups.length, 1);
+            const group = testGroups[0];
+            assert.equal(group, testGroup);
+            assert.equal(testGroup.buildRequests().length, 6);
+
+            const updatedBuildRequest = testGroup.buildRequests()[0];
+            assert.equal(updatedBuildRequest, buildRequest);
+
+            assert(buildRequest.isBuild());
+            assert(!buildRequest.isTest());
+            assert(!buildRequest.hasFinished());
+            assert.equal(buildRequest.buildId(), null);
+
+            assert.deepEqual(buildRequest.commitSet().allRootFiles(), [uploadedRoot]);
+            assert.deepEqual(otherBuildRequest.commitSet().allRootFiles(), []);
+            return TemporaryFile.makeTemporaryFile('JavaScriptCore-Root.dat', 'JavaScript Content 0');
+        }).then((rootFile) => {
+            const report = makeReport(rootFile, buildRequest.id(), [{ownerRepository: 'WebKit2', ownedRepository: 'JavaScriptCore'}], '2017-05-10T02:54:09.666');
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', report);
+        }).then((response) => {
+            assert.equal(response['status'], 'InvalidRepository');
+        });
+    });
+
     it('should accept when build request exists', () => {
         let webkit;
         let webkitPatch;
@@ -424,17 +620,18 @@ describe('/api/upload-root/', function () {
         });
     });
 
-    it('should update all commit set items in the repository listed', () => {
+    it('should only set build requests as complete when all roots are built', () => {
         let webkit;
         let webkitPatch;
-        let shared;
+        let ownedJSC;
         let testGroup;
         let buildRequest;
         let otherBuildRequest;
         let uploadedRoot;
-        return createTestGroupWihPatch().then((group) => {
+        let jscRoot;
+        return createTestGroupWithPatchAndOwnedCommits().then((group) => {
             webkit = Repository.findById(MockData.webkitRepositoryId());
-            shared = Repository.findById(MockData.sharedRepositoryId());
+            ownedJSC = Repository.findById(MockData.ownedJSCRepositoryId());
 
             testGroup = group;
             buildRequest = testGroup.buildRequests()[0];
@@ -442,7 +639,7 @@ describe('/api/upload-root/', function () {
             assert(buildRequest.isBuild());
             assert(!buildRequest.isTest());
             assert(!buildRequest.hasFinished());
-            assert(buildRequest.isPending()); 
+            assert(buildRequest.isPending());
             assert.equal(buildRequest.buildId(), null);
 
             const commitSet = buildRequest.commitSet();
@@ -451,25 +648,24 @@ describe('/api/upload-root/', function () {
             assert(webkitPatch instanceof UploadedFile);
             assert.equal(webkitPatch.filename(), 'patch.dat');
             assert.equal(commitSet.rootForRepository(webkit), null);
-            assert.equal(commitSet.revisionForRepository(shared), '80229');
-            assert.equal(commitSet.patchForRepository(shared), null);
-            assert.equal(commitSet.rootForRepository(shared), null);
+            assert.equal(commitSet.revisionForRepository(ownedJSC), 'owned-jsc-6161');
+            assert.equal(commitSet.patchForRepository(ownedJSC), null);
+            assert.equal(commitSet.rootForRepository(ownedJSC), null);
             assert.deepEqual(commitSet.allRootFiles(), []);
 
             otherBuildRequest = testGroup.buildRequests()[1];
             const otherCommitSet = otherBuildRequest.commitSet();
-            assert.equal(otherCommitSet.revisionForRepository(webkit), '191622');
+            assert.equal(otherCommitSet.revisionForRepository(webkit), '192736');
             assert.equal(otherCommitSet.patchForRepository(webkit), null);
             assert.equal(otherCommitSet.rootForRepository(webkit), null);
-            assert.equal(otherCommitSet.revisionForRepository(shared), '80229');
-            assert.equal(otherCommitSet.patchForRepository(shared), null);
-            assert.equal(otherCommitSet.rootForRepository(shared), null);
+            assert.equal(otherCommitSet.revisionForRepository(ownedJSC), 'owned-jsc-9191');
+            assert.equal(otherCommitSet.patchForRepository(ownedJSC), null);
+            assert.equal(otherCommitSet.rootForRepository(ownedJSC), null);
             assert.deepEqual(otherCommitSet.allRootFiles(), []);
 
             return addSlaveAndCreateRootFile();
         }).then((rootFile) => {
-            const report = makeReport(rootFile, buildRequest.id());
-            report.repositoryList = '["WebKit", "Shared"]';
+            const report = makeReport(rootFile, buildRequest.id(), ['WebKit']);
             return TestServer.remoteAPI().postFormData('/api/upload-root/', report);
         }).then((response) => {
             assert.equal(response['status'], 'OK');
@@ -488,13 +684,38 @@ describe('/api/upload-root/', function () {
 
             assert(buildRequest.isBuild());
             assert(!buildRequest.isTest());
+            assert(!buildRequest.hasFinished());
+            assert.equal(buildRequest.buildId(), null);
+
+            assert.deepEqual(buildRequest.commitSet().allRootFiles(), [uploadedRoot]);
+            assert.deepEqual(otherBuildRequest.commitSet().allRootFiles(), []);
+            return TemporaryFile.makeTemporaryFile('JavaScriptCore-Root.dat', 'JavaScript Content 0');
+        }).then((rootFile) => {
+            const report = makeReport(rootFile, buildRequest.id(), [{ownerRepository: 'WebKit', ownedRepository: 'JavaScriptCore'}], '2017-05-10T02:54:09.666');
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', report);
+        }).then((response) => {
+            assert.equal(response['status'], 'OK');
+            const uploadedRootRawData = response['uploadedFile'];
+            jscRoot = UploadedFile.ensureSingleton(uploadedRootRawData.id, uploadedRootRawData);
+            assert.equal(jscRoot.filename(), 'JavaScriptCore-Root.dat');
+            return TestGroup.fetchForTask(buildRequest.testGroup().task().id(), true);
+        }).then((testGroups) => {
+            assert.equal(testGroups.length, 1);
+            const group = testGroups[0];
+            assert.equal(group, testGroup);
+            assert.equal(testGroup.buildRequests().length, 6);
+
+            const updatedBuildRequest = testGroup.buildRequests()[0];
+            assert.equal(updatedBuildRequest, buildRequest);
+
+            assert(buildRequest.isBuild());
+            assert(!buildRequest.isTest());
             assert(buildRequest.hasFinished());
-            assert(!buildRequest.isPending()); 
+            assert(!buildRequest.isPending());
             assert.notEqual(buildRequest.buildId(), null);
 
-            assert.deepEqual(buildRequest.commitSet().allRootFiles(), [uploadedRoot]);
+            assert.deepEqual(buildRequest.commitSet().allRootFiles(), [uploadedRoot, jscRoot]);
             assert.deepEqual(otherBuildRequest.commitSet().allRootFiles(), []);
         });
     });
-
 });
index b0c084a..b46529a 100644 (file)
@@ -102,18 +102,18 @@ function createTestGroupWihOwnedCommit()
     });
 }
 
-function uploadRoot(buildRequestId, buildNumber)
+function uploadRoot(buildRequestId, buildNumber, repositoryList = ["WebKit"], buildTime = '2017-05-10T02:54:08.666')
 {
-    return TemporaryFile.makeTemporaryFile(`root${buildNumber}.dat`, `root for build ${buildNumber}`).then((rootFile) => {
+    return TemporaryFile.makeTemporaryFile(`root${buildNumber}.dat`, `root for build ${buildNumber} and repository list at ${buildTime}`).then((rootFile) => {
         return TestServer.remoteAPI().postFormData('/api/upload-root/', {
             slaveName: 'sync-slave',
             slavePassword: 'password',
             builderName: 'some builder',
             buildNumber: buildNumber,
-            buildTime: '2017-05-10T02:54:08.666',
+            buildTime: buildTime,
             buildRequest: buildRequestId,
             rootFile,
-            repositoryList: '["WebKit"]',
+            repositoryList: JSON.stringify(repositoryList),
         });
     });
 }
@@ -793,7 +793,7 @@ describe('sync-buildbot', function () {
             assert.equal(otherCommitSet.ownerRevisionForRepository(ownedJSC), otherCommitSet.revisionForRepository(webkit));
             assert.deepEqual(otherCommitSet.allRootFiles(), []);
 
-            return uploadRoot(buildRequest.id(), 123);
+            return uploadRoot(buildRequest.id(), 123).then(() => uploadRoot(buildRequest.id(), 123, [{ownerRepository: 'WebKit', ownedRepository: 'JavaScriptCore'}], '2017-05-10T02:54:09.666'));
         }).then(() => {
             return TestGroup.fetchForTask(taskId, true);
         }).then((testGroups) => {
@@ -818,7 +818,10 @@ describe('sync-buildbot', function () {
             const webkitRoot = commitSet.rootForRepository(webkit);
             assert(webkitRoot instanceof UploadedFile);
             assert.equal(webkitRoot.filename(), 'root123.dat');
-            assert.deepEqual(commitSet.allRootFiles(), [webkitRoot]);
+            const jscRoot = commitSet.rootForRepository(ownedJSC);
+            assert(jscRoot instanceof UploadedFile);
+            assert.equal(jscRoot.filename(), 'root123.dat');
+            assert.deepEqual(commitSet.allRootFiles(), [webkitRoot, jscRoot]);
 
             const otherBuildRequest = testGroup.buildRequests()[1];
             assert(otherBuildRequest.isBuild());
@@ -896,11 +899,350 @@ describe('sync-buildbot', function () {
             const webkitRoot = commitSet.rootForRepository(webkit);
             assert(webkitRoot instanceof UploadedFile);
             assert.equal(webkitRoot.filename(), 'root123.dat');
+            const jscRoot = commitSet.rootForRepository(ownedJSC);
+            assert(jscRoot instanceof UploadedFile);
+            assert.equal(jscRoot.filename(), 'root123.dat');
+            assert.deepEqual(commitSet.allRootFiles(), [webkitRoot, jscRoot]);
+
+            const otherBuildRequest = testGroup.buildRequests()[1];
+            assert(otherBuildRequest.isBuild());
+            assert(!otherBuildRequest.isTest());
+            assert.equal(otherBuildRequest.statusLabel(), 'Running');
+            assert.equal(otherBuildRequest.statusUrl(), 'http://build.webkit.org/builders/some%20builder/builds/124');
+            assert.equal(otherBuildRequest.buildId(), null);
+
+            const otherCommitSet = otherBuildRequest.commitSet();
+            assert.equal(otherCommitSet.revisionForRepository(webkit), '192736');
+            assert.equal(otherCommitSet.patchForRepository(webkit), null);
+            assert.equal(otherCommitSet.rootForRepository(webkit), null);
+            assert.equal(otherCommitSet.ownerRevisionForRepository(webkit), null);
+            assert.equal(otherCommitSet.ownerRevisionForRepository(ownedJSC), otherCommitSet.revisionForRepository(webkit));
+            assert.deepEqual(otherCommitSet.allRootFiles(), []);
+
+            return uploadRoot(otherBuildRequest.id(), 124).then(() => uploadRoot(otherBuildRequest.id(), 124, [{ownerRepository: 'WebKit', ownedRepository: 'JavaScriptCore'}], '2017-05-10T02:54:09.666'));
+        }).then(() => {
+            return TestGroup.fetchForTask(taskId, true);
+        }).then((testGroups) => {
+            assert.equal(testGroups.length, 1);
+            const testGroup = testGroups[0];
+            const webkit = Repository.findById(MockData.webkitRepositoryId());
+            const ownedJSC = Repository.findById(MockData.ownedJSCRepositoryId());
+            assert.equal(testGroup.buildRequests().length, 6);
+
+            const buildRequest = testGroup.buildRequests()[0];
+            assert(buildRequest.isBuild());
+            assert(!buildRequest.isTest());
+            assert.equal(buildRequest.statusLabel(), 'Completed');
+            assert.equal(buildRequest.statusUrl(), 'http://build.webkit.org/builders/some%20builder/builds/124');
+            assert.notEqual(buildRequest.buildId(), null);
+
+            const commitSet = buildRequest.commitSet();
+            assert.equal(commitSet.revisionForRepository(webkit), '191622');
+            assert.equal(commitSet.patchForRepository(webkit), null);
+            assert.equal(commitSet.ownerRevisionForRepository(webkit), null);
+            assert.equal(commitSet.ownerRevisionForRepository(ownedJSC), commitSet.revisionForRepository(webkit));
+            const webkitRoot = commitSet.rootForRepository(webkit);
+            assert(webkitRoot instanceof UploadedFile);
+            assert.equal(webkitRoot.filename(), 'root123.dat');
+            const jscRoot = commitSet.rootForRepository(ownedJSC);
+            assert(jscRoot instanceof UploadedFile);
+            assert.equal(jscRoot.filename(), 'root123.dat');
+            assert.deepEqual(commitSet.allRootFiles(), [webkitRoot, jscRoot]);
+
+            const otherBuildRequest = testGroup.buildRequests()[1];
+            assert(otherBuildRequest.isBuild());
+            assert(!otherBuildRequest.isTest());
+            assert.equal(otherBuildRequest.statusLabel(), 'Completed');
+            assert.equal(otherBuildRequest.statusUrl(), 'http://build.webkit.org/builders/some%20builder/builds/124');
+            assert.notEqual(otherBuildRequest.buildId(), null);
+
+            const otherCommitSet = otherBuildRequest.commitSet();
+            assert.equal(otherCommitSet.revisionForRepository(webkit), '192736');
+            assert.equal(otherCommitSet.patchForRepository(webkit), null);
+            assert.equal(otherCommitSet.ownerRevisionForRepository(webkit), null);
+            assert.equal(otherCommitSet.ownerRevisionForRepository(ownedJSC), otherCommitSet.revisionForRepository(webkit));
+            const otherWebkitRoot = otherCommitSet.rootForRepository(webkit);
+            assert(otherWebkitRoot instanceof UploadedFile);
+            assert.equal(otherWebkitRoot.filename(), 'root124.dat');
+            const otherJSCRoot = otherCommitSet.rootForRepository(ownedJSC);
+            assert(otherJSCRoot instanceof UploadedFile);
+            assert.equal(otherJSCRoot.filename(), 'root124.dat');
+            assert.deepEqual(otherCommitSet.allRootFiles(), [otherWebkitRoot, otherJSCRoot]);
+        });
+    });
+
+    it('should not update a build request to "completed" until all roots needed in the commit set are uploaded', () => {
+        const requests = MockRemoteAPI.requests;
+        let triggerable;
+        let taskId = null;
+        let syncPromise;
+        return createTriggerable().then((newTriggerable) => {
+            triggerable = newTriggerable;
+            return createTestGroupWihOwnedCommit();
+        }).then((testGroup) => {
+            taskId = testGroup.task().id();
+            const webkit = Repository.findById(MockData.webkitRepositoryId());
+            const ownedJSC = Repository.findById(MockData.ownedJSCRepositoryId());
+            assert.equal(testGroup.buildRequests().length, 6);
+
+            const buildRequest = testGroup.buildRequests()[0];
+            assert(buildRequest.isBuild());
+            assert(!buildRequest.isTest());
+            assert.equal(buildRequest.statusLabel(), 'Waiting');
+            assert.equal(buildRequest.statusUrl(), null);
+            assert.equal(buildRequest.buildId(), null);
+
+            const commitSet = buildRequest.commitSet();
+            assert.equal(commitSet.revisionForRepository(webkit), '191622');
+            assert.equal(commitSet.patchForRepository(webkit), null);
+            assert.equal(commitSet.rootForRepository(webkit), null);
+            assert.equal(commitSet.ownerRevisionForRepository(webkit), null);
+            assert.equal(commitSet.ownerRevisionForRepository(ownedJSC), commitSet.revisionForRepository(webkit));
+            assert.deepEqual(commitSet.allRootFiles(), []);
+
+            const otherBuildRequest = testGroup.buildRequests()[1];
+            assert(otherBuildRequest.isBuild());
+            assert(!otherBuildRequest.isTest());
+            assert.equal(otherBuildRequest.statusLabel(), 'Waiting');
+            assert.equal(otherBuildRequest.statusUrl(), null);
+            assert.equal(otherBuildRequest.buildId(), null);
+
+            const otherCommitSet = otherBuildRequest.commitSet();
+            assert.equal(otherCommitSet.revisionForRepository(webkit), '192736');
+            assert.equal(otherCommitSet.patchForRepository(webkit), null);
+            assert.equal(otherCommitSet.rootForRepository(webkit), null);
+            assert.equal(otherCommitSet.ownerRevisionForRepository(webkit), null);
+            assert.equal(otherCommitSet.ownerRevisionForRepository(ownedJSC), otherCommitSet.revisionForRepository(webkit));
+            assert.deepEqual(otherCommitSet.allRootFiles(), []);
+
+            syncPromise = triggerable.initSyncers().then(() => triggerable.syncOnce());
+            return MockRemoteAPI.waitForRequest();
+        }).then(() => {
+            assert.equal(requests.length, 3);
+            assertAndResolveRequest(requests[0], 'GET', '/json/builders/some%20tester/pendingBuilds', []);
+            assertAndResolveRequest(requests[1], 'GET', '/json/builders/some%20builder/pendingBuilds', []);
+            assertAndResolveRequest(requests[2], 'GET', '/json/builders/other%20builder/pendingBuilds', []);
+            return MockRemoteAPI.waitForRequest();
+        }).then(() => {
+            assert.equal(requests.length, 6);
+            assertAndResolveRequest(requests[3], 'GET', '/json/builders/some%20tester/builds/?select=-1&select=-2', {});
+            assertAndResolveRequest(requests[4], 'GET', '/json/builders/some%20builder/builds/?select=-1&select=-2', {});
+            assertAndResolveRequest(requests[5], 'GET', '/json/builders/other%20builder/builds/?select=-1&select=-2', {});
+            return MockRemoteAPI.waitForRequest();
+        }).then(() => {
+            assert.equal(requests.length, 7);
+            assertAndResolveRequest(requests[6], 'POST', '/builders/some%20builder/force', 'OK');
+            assert.deepEqual(requests[6].data, {'wk': '191622', 'build-request-id': '1', 'forcescheduler': 'force-ab-builds', 'owned-commits': `{"WebKit":[{"revision":"owned-jsc-6161","repository":"JavaScriptCore","ownerRevision":"191622"}]}`});
+            return MockRemoteAPI.waitForRequest();
+        }).then(() => {
+            assert.equal(requests.length, 10);
+            assertAndResolveRequest(requests[7], 'GET', '/json/builders/some%20tester/pendingBuilds', []);
+            assertAndResolveRequest(requests[8], 'GET', '/json/builders/some%20builder/pendingBuilds',
+                [MockData.pendingBuild({builder: 'some builder', buildRequestId: 1})]);
+            assertAndResolveRequest(requests[9], 'GET', '/json/builders/other%20builder/pendingBuilds', []);
+            return MockRemoteAPI.waitForRequest();
+        }).then(() => {
+            assert.equal(requests.length, 13);
+            assertAndResolveRequest(requests[10], 'GET', '/json/builders/some%20tester/builds/?select=-1&select=-2', {});
+            assertAndResolveRequest(requests[11], 'GET', '/json/builders/some%20builder/builds/?select=-1&select=-2', {
+                [-1]: MockData.runningBuild({builder: 'some builder', buildRequestId: 1})
+            });
+            assertAndResolveRequest(requests[12], 'GET', '/json/builders/other%20builder/builds/?select=-1&select=-2', {});
+            return syncPromise;
+        }).then(() => {
+            return TestGroup.fetchForTask(taskId, true);
+        }).then((testGroups) => {
+            assert.equal(testGroups.length, 1);
+            const testGroup = testGroups[0];
+            const webkit = Repository.findById(MockData.webkitRepositoryId());
+            const ownedJSC = Repository.findById(MockData.ownedJSCRepositoryId());
+            assert.equal(testGroup.buildRequests().length, 6);
+
+            const buildRequest = testGroup.buildRequests()[0];
+            assert(buildRequest.isBuild());
+            assert(!buildRequest.isTest());
+            assert.equal(buildRequest.statusLabel(), 'Running');
+            assert.equal(buildRequest.statusUrl(), 'http://build.webkit.org/builders/some%20builder/builds/124');
+            assert.equal(buildRequest.buildId(), null);
+
+            const commitSet = buildRequest.commitSet();
+            assert.equal(commitSet.revisionForRepository(webkit), '191622');
+            assert.equal(commitSet.patchForRepository(webkit), null);
+            assert.equal(commitSet.rootForRepository(webkit), null);
+            assert.equal(commitSet.ownerRevisionForRepository(webkit), null);
+            assert.equal(commitSet.ownerRevisionForRepository(ownedJSC), commitSet.revisionForRepository(webkit));
+            assert.deepEqual(commitSet.allRootFiles(), []);
+
+            const otherBuildRequest = testGroup.buildRequests()[1];
+            assert(otherBuildRequest.isBuild());
+            assert(!otherBuildRequest.isTest());
+            assert.equal(otherBuildRequest.statusLabel(), 'Waiting');
+            assert.equal(otherBuildRequest.statusUrl(), null);
+            assert.equal(otherBuildRequest.buildId(), null);
+
+            const otherCommitSet = otherBuildRequest.commitSet();
+            assert.equal(otherCommitSet.revisionForRepository(webkit), '192736');
+            assert.equal(otherCommitSet.patchForRepository(webkit), null);
+            assert.equal(otherCommitSet.rootForRepository(webkit), null);
+            assert.equal(otherCommitSet.ownerRevisionForRepository(webkit), null);
+            assert.equal(otherCommitSet.ownerRevisionForRepository(ownedJSC), otherCommitSet.revisionForRepository(webkit));
+            assert.deepEqual(otherCommitSet.allRootFiles(), []);
+
+            return uploadRoot(buildRequest.id(), 123);
+        }).then(() => {
+            return TestGroup.fetchForTask(taskId, true);
+        }).then((testGroups) => {
+            assert.equal(testGroups.length, 1);
+            const testGroup = testGroups[0];
+            const webkit = Repository.findById(MockData.webkitRepositoryId());
+            const ownedJSC = Repository.findById(MockData.ownedJSCRepositoryId());
+            assert.equal(testGroup.buildRequests().length, 6);
+
+            const buildRequest = testGroup.buildRequests()[0];
+            assert(buildRequest.isBuild());
+            assert(!buildRequest.isTest());
+            assert.equal(buildRequest.statusLabel(), 'Running');
+            assert.equal(buildRequest.statusUrl(), 'http://build.webkit.org/builders/some%20builder/builds/124');
+            assert.equal(buildRequest.buildId(), null);
+
+            const commitSet = buildRequest.commitSet();
+            assert.equal(commitSet.revisionForRepository(webkit), '191622');
+            assert.equal(commitSet.patchForRepository(webkit), null);
+            assert.equal(commitSet.ownerRevisionForRepository(webkit), null);
+            assert.equal(commitSet.ownerRevisionForRepository(ownedJSC), commitSet.revisionForRepository(webkit));
+            const webkitRoot = commitSet.rootForRepository(webkit);
+            assert(webkitRoot instanceof UploadedFile);
+            assert.equal(webkitRoot.filename(), 'root123.dat');
             assert.deepEqual(commitSet.allRootFiles(), [webkitRoot]);
 
             const otherBuildRequest = testGroup.buildRequests()[1];
             assert(otherBuildRequest.isBuild());
             assert(!otherBuildRequest.isTest());
+            assert.equal(otherBuildRequest.statusLabel(), 'Waiting');
+            assert.equal(otherBuildRequest.statusUrl(), null);
+            assert.equal(otherBuildRequest.buildId(), null);
+
+            const otherCommitSet = otherBuildRequest.commitSet();
+            assert.equal(otherCommitSet.revisionForRepository(webkit), '192736');
+            assert.equal(otherCommitSet.patchForRepository(webkit), null);
+            assert.equal(otherCommitSet.rootForRepository(webkit), null);
+            assert.equal(otherCommitSet.ownerRevisionForRepository(webkit), null);
+            assert.equal(otherCommitSet.ownerRevisionForRepository(ownedJSC), otherCommitSet.revisionForRepository(webkit));
+            assert.deepEqual(otherCommitSet.allRootFiles(), []);
+            return uploadRoot(buildRequest.id(), 123, [{ownerRepository: 'WebKit', ownedRepository: 'JavaScriptCore'}], '2017-05-10T02:54:09.666');
+        }).then(() => {
+            return TestGroup.fetchForTask(taskId, true);
+        }).then((testGroups) => {
+            assert.equal(testGroups.length, 1);
+            const testGroup = testGroups[0];
+            const webkit = Repository.findById(MockData.webkitRepositoryId());
+            const ownedJSC = Repository.findById(MockData.ownedJSCRepositoryId());
+            assert.equal(testGroup.buildRequests().length, 6);
+
+            const buildRequest = testGroup.buildRequests()[0];
+            assert(buildRequest.isBuild());
+            assert(!buildRequest.isTest());
+            assert.equal(buildRequest.statusLabel(), 'Completed');
+            assert.equal(buildRequest.statusUrl(), 'http://build.webkit.org/builders/some%20builder/builds/124');
+            assert.notEqual(buildRequest.buildId(), null);
+
+            const commitSet = buildRequest.commitSet();
+            assert.equal(commitSet.revisionForRepository(webkit), '191622');
+            assert.equal(commitSet.patchForRepository(webkit), null);
+            assert.equal(commitSet.ownerRevisionForRepository(webkit), null);
+            assert.equal(commitSet.ownerRevisionForRepository(ownedJSC), commitSet.revisionForRepository(webkit));
+            const webkitRoot = commitSet.rootForRepository(webkit);
+            assert(webkitRoot instanceof UploadedFile);
+            assert.equal(webkitRoot.filename(), 'root123.dat');
+            const jscRoot = commitSet.rootForRepository((ownedJSC));
+            assert(jscRoot instanceof UploadedFile);
+            assert.equal(jscRoot.filename(), 'root123.dat');
+            assert.deepEqual(commitSet.allRootFiles(), [webkitRoot, jscRoot]);
+
+            const otherBuildRequest = testGroup.buildRequests()[1];
+            assert(otherBuildRequest.isBuild());
+            assert(!otherBuildRequest.isTest());
+            assert.equal(otherBuildRequest.statusLabel(), 'Waiting');
+            assert.equal(otherBuildRequest.statusUrl(), null);
+            assert.equal(otherBuildRequest.buildId(), null);
+
+            const otherCommitSet = otherBuildRequest.commitSet();
+            assert.equal(otherCommitSet.revisionForRepository(webkit), '192736');
+            assert.equal(otherCommitSet.patchForRepository(webkit), null);
+            assert.equal(otherCommitSet.rootForRepository(webkit), null);
+            assert.equal(otherCommitSet.ownerRevisionForRepository(webkit), null);
+            assert.equal(otherCommitSet.ownerRevisionForRepository(ownedJSC), otherCommitSet.revisionForRepository(webkit));
+            assert.deepEqual(otherCommitSet.allRootFiles(), []);
+
+            MockRemoteAPI.reset();
+            syncPromise = triggerable.initSyncers().then(() => triggerable.syncOnce());
+            return MockRemoteAPI.waitForRequest();
+        }).then(() => {
+            assert.equal(requests.length, 3);
+            assertAndResolveRequest(requests[0], 'GET', '/json/builders/some%20tester/pendingBuilds', []);
+            assertAndResolveRequest(requests[1], 'GET', '/json/builders/some%20builder/pendingBuilds', []);
+            assertAndResolveRequest(requests[2], 'GET', '/json/builders/other%20builder/pendingBuilds', []);
+            return MockRemoteAPI.waitForRequest();
+        }).then(() => {
+            assert.equal(requests.length, 6);
+            assertAndResolveRequest(requests[3], 'GET', '/json/builders/some%20tester/builds/?select=-1&select=-2', {});
+            assertAndResolveRequest(requests[4], 'GET', '/json/builders/some%20builder/builds/?select=-1&select=-2', {
+                [-1]: MockData.finishedBuild({builder: 'some builder', buildRequestId: 1})
+            });
+            assertAndResolveRequest(requests[5], 'GET', '/json/builders/other%20builder/builds/?select=-1&select=-2', {});
+            return MockRemoteAPI.waitForRequest();
+        }).then(() => {
+            assert.equal(requests.length, 7);
+            assertAndResolveRequest(requests[6], 'POST', '/builders/some%20builder/force', 'OK');
+            assert.deepEqual(requests[6].data, {'wk': '192736', 'build-request-id': '2', 'forcescheduler': 'force-ab-builds', 'owned-commits': `{"WebKit":[{"revision":"owned-jsc-9191","repository":"JavaScriptCore","ownerRevision":"192736"}]}`});
+            return MockRemoteAPI.waitForRequest();
+        }).then(() => {
+            assert.equal(requests.length, 10);
+            assertAndResolveRequest(requests[7], 'GET', '/json/builders/some%20tester/pendingBuilds', []);
+            assertAndResolveRequest(requests[8], 'GET', '/json/builders/some%20builder/pendingBuilds', []);
+            assertAndResolveRequest(requests[9], 'GET', '/json/builders/other%20builder/pendingBuilds', []);
+            return MockRemoteAPI.waitForRequest();
+        }).then(() => {
+            assert.equal(requests.length, 13);
+            assertAndResolveRequest(requests[10], 'GET', '/json/builders/some%20tester/builds/?select=-1&select=-2', {});
+            assertAndResolveRequest(requests[11], 'GET', '/json/builders/some%20builder/builds/?select=-1&select=-2', {
+                [-1]: MockData.runningBuild({builder: 'some builder', buildRequestId: 2}),
+                [-2]: MockData.finishedBuild({builder: 'some builder', buildRequestId: 1}),
+            });
+            assertAndResolveRequest(requests[12], 'GET', '/json/builders/other%20builder/builds/?select=-1&select=-2', {});
+            return syncPromise;
+        }).then(() => {
+            return TestGroup.fetchForTask(taskId, true);
+        }).then((testGroups) => {
+            assert.equal(testGroups.length, 1);
+            const testGroup = testGroups[0];
+            const webkit = Repository.findById(MockData.webkitRepositoryId());
+            const ownedJSC = Repository.findById(MockData.ownedJSCRepositoryId());
+            assert.equal(testGroup.buildRequests().length, 6);
+
+            const buildRequest = testGroup.buildRequests()[0];
+            assert(buildRequest.isBuild());
+            assert(!buildRequest.isTest());
+            assert.equal(buildRequest.statusLabel(), 'Completed');
+            assert.equal(buildRequest.statusUrl(), 'http://build.webkit.org/builders/some%20builder/builds/124');
+            assert.notEqual(buildRequest.buildId(), null);
+
+            const commitSet = buildRequest.commitSet();
+            assert.equal(commitSet.revisionForRepository(webkit), '191622');
+            assert.equal(commitSet.patchForRepository(webkit), null);
+            assert.equal(commitSet.ownerRevisionForRepository(webkit), null);
+            assert.equal(commitSet.ownerRevisionForRepository(ownedJSC), commitSet.revisionForRepository(webkit));
+            const webkitRoot = commitSet.rootForRepository(webkit);
+            assert(webkitRoot instanceof UploadedFile);
+            assert.equal(webkitRoot.filename(), 'root123.dat');
+            const jscRoot = commitSet.rootForRepository(ownedJSC);
+            assert(jscRoot instanceof UploadedFile);
+            assert.equal(jscRoot.filename(), 'root123.dat');
+            assert.deepEqual(commitSet.allRootFiles(), [webkitRoot, jscRoot]);
+
+            const otherBuildRequest = testGroup.buildRequests()[1];
+            assert(otherBuildRequest.isBuild());
+            assert(!otherBuildRequest.isTest());
             assert.equal(otherBuildRequest.statusLabel(), 'Running');
             assert.equal(otherBuildRequest.statusUrl(), 'http://build.webkit.org/builders/some%20builder/builds/124');
             assert.equal(otherBuildRequest.buildId(), null);
@@ -938,7 +1280,56 @@ describe('sync-buildbot', function () {
             const webkitRoot = commitSet.rootForRepository(webkit);
             assert(webkitRoot instanceof UploadedFile);
             assert.equal(webkitRoot.filename(), 'root123.dat');
-            assert.deepEqual(commitSet.allRootFiles(), [webkitRoot]);
+            const jscRoot = commitSet.rootForRepository(ownedJSC);
+            assert(jscRoot instanceof UploadedFile);
+            assert.equal(jscRoot.filename(), 'root123.dat');
+            assert.deepEqual(commitSet.allRootFiles(), [webkitRoot, jscRoot]);
+
+            const otherBuildRequest = testGroup.buildRequests()[1];
+            assert(otherBuildRequest.isBuild());
+            assert(!otherBuildRequest.isTest());
+            assert.equal(otherBuildRequest.statusLabel(), 'Running');
+            assert.equal(otherBuildRequest.statusUrl(), 'http://build.webkit.org/builders/some%20builder/builds/124');
+            assert.equal(otherBuildRequest.buildId(), null);
+
+            const otherCommitSet = otherBuildRequest.commitSet();
+            assert.equal(otherCommitSet.revisionForRepository(webkit), '192736');
+            assert.equal(otherCommitSet.patchForRepository(webkit), null);
+            assert.equal(otherCommitSet.ownerRevisionForRepository(webkit), null);
+            assert.equal(otherCommitSet.ownerRevisionForRepository(ownedJSC), otherCommitSet.revisionForRepository(webkit));
+            const otherWebkitRoot = otherCommitSet.rootForRepository(webkit);
+            assert(otherWebkitRoot instanceof UploadedFile);
+            assert.equal(otherWebkitRoot.filename(), 'root124.dat');
+            assert.deepEqual(otherCommitSet.allRootFiles(), [otherWebkitRoot]);
+            return uploadRoot(otherBuildRequest.id(), 124, [{ownerRepository: 'WebKit', ownedRepository: 'JavaScriptCore'}], '2017-05-10T02:54:09.666');
+        }).then(() => {
+            return TestGroup.fetchForTask(taskId, true);
+        }).then((testGroups) => {
+            assert.equal(testGroups.length, 1);
+            const testGroup = testGroups[0];
+            const webkit = Repository.findById(MockData.webkitRepositoryId());
+            const ownedJSC = Repository.findById(MockData.ownedJSCRepositoryId());
+            assert.equal(testGroup.buildRequests().length, 6);
+
+            const buildRequest = testGroup.buildRequests()[0];
+            assert(buildRequest.isBuild());
+            assert(!buildRequest.isTest());
+            assert.equal(buildRequest.statusLabel(), 'Completed');
+            assert.equal(buildRequest.statusUrl(), 'http://build.webkit.org/builders/some%20builder/builds/124');
+            assert.notEqual(buildRequest.buildId(), null);
+
+            const commitSet = buildRequest.commitSet();
+            assert.equal(commitSet.revisionForRepository(webkit), '191622');
+            assert.equal(commitSet.patchForRepository(webkit), null);
+            assert.equal(commitSet.ownerRevisionForRepository(webkit), null);
+            assert.equal(commitSet.ownerRevisionForRepository(ownedJSC), commitSet.revisionForRepository(webkit));
+            const webkitRoot = commitSet.rootForRepository(webkit);
+            assert(webkitRoot instanceof UploadedFile);
+            assert.equal(webkitRoot.filename(), 'root123.dat');
+            const jscRoot = commitSet.rootForRepository(ownedJSC);
+            assert(jscRoot instanceof UploadedFile);
+            assert.equal(jscRoot.filename(), 'root123.dat');
+            assert.deepEqual(commitSet.allRootFiles(), [webkitRoot, jscRoot]);
 
             const otherBuildRequest = testGroup.buildRequests()[1];
             assert(otherBuildRequest.isBuild());
@@ -955,7 +1346,10 @@ describe('sync-buildbot', function () {
             const otherWebkitRoot = otherCommitSet.rootForRepository(webkit);
             assert(otherWebkitRoot instanceof UploadedFile);
             assert.equal(otherWebkitRoot.filename(), 'root124.dat');
-            assert.deepEqual(otherCommitSet.allRootFiles(), [otherWebkitRoot]);
+            const otherJSCRoot = otherCommitSet.rootForRepository(ownedJSC);
+            assert(otherJSCRoot instanceof UploadedFile);
+            assert.equal(otherJSCRoot.filename(), 'root124.dat');
+            assert.deepEqual(otherCommitSet.allRootFiles(), [otherWebkitRoot, otherJSCRoot]);
         });
     });
 });