Title: [269871] trunk/Websites/perf.webkit.org
Revision
269871
Author
dewei_...@apple.com
Date
2020-11-16 12:32:08 -0800 (Mon, 16 Nov 2020)

Log Message

Performance dashboard should avoid building same configuration under one analysis task.
https://bugs.webkit.org/show_bug.cgi?id=218413

Reviewed by Ryosuke Niwa.

Add logic in syncing script to reuse already built roots from same build request under current analysis task.
If there is another same build request scheduled/running, will defer scheduling current build request.

* public/admin/platforms.php: Fixed newer version of PHP warns on accessing invalid key in an array.
* public/admin/tests.php: Added a null check against $selected_parent.
* public/api/build-requests.php: Extended this API to allow reusing roots from existing build request.
* public/v3/models/build-request.js:
(BuildRequest.prototype.async findBuildRequestWithSameRoots): Find build type build request with same commit set
which can/will be reused under current analysis task.
* public/v3/models/commit-set.js:
(CommitSet.prototype.equalsIgnoringRoot): Added a helper function which checks commit set equality ignoring the root.
(CommitSet.prototype.equals): Use '_equalsOptionallyIgnoringRoot' as underlying implementation.
(CommitSet.prototype._equalsOptionallyIgnoringRoot): Implementation for both equals and equalsIngnoringRoot.
* server-tests/api-build-requests-tests.js: Added unit tests.
* server-tests/resources/mock-data.js: Added new mock data for new unit tests.
(MockData.addMockConfiguration):
(MockData.addMockData):
(MockData.set addMockBuildRequestsWithRoots):
(MockData.set addTwoMockTestGroupWithOwnedCommits):
(MockData.mockTestSyncConfigWithPatchAcceptingBuilder):
* server-tests/tools-buildbot-triggerable-tests.js: Added unit tests.
* tools/js/buildbot-triggerable.js:
(BuildbotTriggerable.prototype.async syncOnce):
(BuildbotTriggerable.prototype.async _scheduleRequest): A helper function to reuse the roots if there are built
roots available for same build type build request. If there is another build scheduled for same build request,
will defer scheduling the build request.
* unit-tests/build-request-tests.js: Add unit tests.

Modified Paths

Diff

Modified: trunk/Websites/perf.webkit.org/ChangeLog (269870 => 269871)


--- trunk/Websites/perf.webkit.org/ChangeLog	2020-11-16 20:25:21 UTC (rev 269870)
+++ trunk/Websites/perf.webkit.org/ChangeLog	2020-11-16 20:32:08 UTC (rev 269871)
@@ -1,3 +1,38 @@
+2020-10-30  Dewei Zhu  <dewei_...@apple.com>
+
+        Performance dashboard should avoid building same configuration under one analysis task.
+        https://bugs.webkit.org/show_bug.cgi?id=218413
+
+        Reviewed by Ryosuke Niwa.
+
+        Add logic in syncing script to reuse already built roots from same build request under current analysis task.
+        If there is another same build request scheduled/running, will defer scheduling current build request.
+
+        * public/admin/platforms.php: Fixed newer version of PHP warns on accessing invalid key in an array.
+        * public/admin/tests.php: Added a null check against $selected_parent.
+        * public/api/build-requests.php: Extended this API to allow reusing roots from existing build request.
+        * public/v3/models/build-request.js:
+        (BuildRequest.prototype.async findBuildRequestWithSameRoots): Find build type build request with same commit set
+        which can/will be reused under current analysis task.
+        * public/v3/models/commit-set.js:
+        (CommitSet.prototype.equalsIgnoringRoot): Added a helper function which checks commit set equality ignoring the root.
+        (CommitSet.prototype.equals): Use '_equalsOptionallyIgnoringRoot' as underlying implementation.
+        (CommitSet.prototype._equalsOptionallyIgnoringRoot): Implementation for both equals and equalsIngnoringRoot.
+        * server-tests/api-build-requests-tests.js: Added unit tests.
+        * server-tests/resources/mock-data.js: Added new mock data for new unit tests.
+        (MockData.addMockConfiguration):
+        (MockData.addMockData):
+        (MockData.set addMockBuildRequestsWithRoots):
+        (MockData.set addTwoMockTestGroupWithOwnedCommits):
+        (MockData.mockTestSyncConfigWithPatchAcceptingBuilder):
+        * server-tests/tools-buildbot-triggerable-tests.js: Added unit tests.
+        * tools/js/buildbot-triggerable.js:
+        (BuildbotTriggerable.prototype.async syncOnce):
+        (BuildbotTriggerable.prototype.async _scheduleRequest): A helper function to reuse the roots if there are built
+        roots available for same build type build request. If there is another build scheduled for same build request,
+        will defer scheduling the build request.
+        * unit-tests/build-request-tests.js: Add unit tests.
+
 2020-10-27  Dewei Zhu  <dewei_...@apple.com>
 
         Refactor 'platforms' table to contain group information.

Modified: trunk/Websites/perf.webkit.org/public/admin/platforms.php (269870 => 269871)


--- trunk/Websites/perf.webkit.org/public/admin/platforms.php	2020-11-16 20:25:21 UTC (rev 269870)
+++ trunk/Websites/perf.webkit.org/public/admin/platforms.php	2020-11-16 20:32:08 UTC (rev 269871)
@@ -102,7 +102,7 @@
     {
         global $platform_group_options;
         $id = intval($platform_row['platform_id']);
-        $platform_group_id = $platform_row['platform_group'];
+        $platform_group_id = array_get($platform_row, 'platform_group');
         $content = <<< END
 <form method="POST"><input type="hidden" name="id" value="$id">
 <select name="group">

Modified: trunk/Websites/perf.webkit.org/public/admin/tests.php (269870 => 269871)


--- trunk/Websites/perf.webkit.org/public/admin/tests.php	2020-11-16 20:25:21 UTC (rev 269870)
+++ trunk/Websites/perf.webkit.org/public/admin/tests.php	2020-11-16 20:32:08 UTC (rev 269871)
@@ -64,7 +64,7 @@
 <?php
 
         foreach ($test_name_resolver->tests() as $test) {
-            if ($test['test_parent'] != $selected_parent['test_id'])
+            if ($selected_parent && $test['test_parent'] != $selected_parent['test_id'])
                 continue;
 
             $test_id = $test['test_id'];

Modified: trunk/Websites/perf.webkit.org/public/api/build-requests.php (269870 => 269871)


--- trunk/Websites/perf.webkit.org/public/api/build-requests.php	2020-11-16 20:25:21 UTC (rev 269870)
+++ trunk/Websites/perf.webkit.org/public/api/build-requests.php	2020-11-16 20:32:08 UTC (rev 269871)
@@ -47,6 +47,7 @@
         $status = $info['status'];
         $url = "" 'url');
         $status_description = array_get($info, 'statusDescription');
+        $build_request_for_root_reuse_id = array_get($info, 'buildRequestForRootReuse');
         $request_row = $db->select_first_row('build_requests', 'request', array('id' => $id));
         if ($status == 'failedIfNotCompleted') {
             if (!$request_row) {
@@ -68,6 +69,23 @@
                 exit_with_error('UnknownBuildRequestStatus', array('buildRequest' => $id, 'status' => $status));
             }
             $db->update_row('build_requests', 'request', array('id' => $id), array('status' => $status, 'url' => $url, 'status_description' => $status_description));
+            if ($build_request_for_root_reuse_id) {
+                $build_request_for_root_reuse_id = intval($build_request_for_root_reuse_id);
+                $build_request_for_root_reuse = $db->select_first_row('build_requests', 'request', array('id' => $build_request_for_root_reuse_id));
+                if (!$build_request_for_root_reuse) {
+                    $db->rollback_transaction();
+                    exit_with_error('FailedToFindbuildRequestForRootReuse', array('buildRequest' => $build_request_for_root_reuse_id));
+                }
+                if ($build_request_for_root_reuse['request_status'] != 'completed') {
+                    $db->rollback_transaction();
+                    exit_with_error('CanOnlyReuseCompletedBuildRequest', array('buildRequest' => $build_request_for_root_reuse_id, 'status' => $build_request_for_root_reuse['request_status']));
+                }
+                $error = reuse_roots_in_commit_set($db, $build_request_for_root_reuse['request_commit_set'], $request_row['request_commit_set']);
+                if ($error) {
+                    $db->rollback_transaction();
+                    exit_with_error($error['status'], $error['details']);
+                }
+            }
             if ($status != 'failed')
                 continue;
         }
@@ -82,8 +100,60 @@
     $db->commit_transaction();
 }
 
+function reuse_roots_in_commit_set($db, $source_id, $destination_id) {
+    $commit_set_items_source = $db->query_and_fetch_all('SELECT * FROM commit_set_items WHERE commitset_set = $1 AND commitset_commit IS NOT NULL', array($source_id));
+    $commit_set_items_destination = $db->query_and_fetch_all('SELECT * FROM commit_set_items WHERE commitset_set = $1 AND commitset_commit IS NOT NULL', array($destination_id));
+    if (count($commit_set_items_source) != count($commit_set_items_destination)) {
+        return array('status' => 'CannotReuseRootWithNonMatchingCommitSets', 'details' => array('sourceCommitSet' => $source_id,
+            'destinationCommitSet' => $destination_id));
+    }
+
+    $root_file_ids = array();
+    foreach ($commit_set_items_destination as &$destination_item) {
+        $source_item = NULL;
+        foreach ($commit_set_items_source as &$item) {
+            if ($destination_item['commitset_commit'] == $item['commitset_commit']
+                && $destination_item['commitset_patch_file'] == $item['commitset_patch_file']
+                && $destination_item['commitset_commit_owner'] == $item['commitset_commit_owner']
+                && $destination_item['commitset_requires_build'] == $item['commitset_requires_build']) {
+                $source_item = $item;
+                break;
+            }
+        }
+        if (!$source_item)
+            return array('status' => 'NoMatchingCommitSetItem', 'details' => array('commitSet' => $source_id));
+
+        $root_file_id = $source_item['commitset_root_file'];
+        if (!$root_file_id) {
+            if (!$db->is_true($source_item['commitset_requires_build']))
+                continue;
+            return array('status' => 'MissingRootFileFromSourceCommitSet', 'details' => array('commitSet' => $destination_id, 'rootFile' => $root_file_id));
+        }
+
+        $root_file_row = $db->select_first_row('uploaded_files', 'file', array('id' => $root_file_id));
+        if ($root_file_row['file_deleted_at'])
+            return array('status' => 'CannotReuseDeletedRoot', 'details' => array('commitSet' => $destination_id, 'rootFile' => $root_file_id));
+
+        $db->update_row('commit_set_items', 'commitset', array('set' => $destination_id, 'commit' => $destination_item['commitset_commit']),
+            array('root_file' => $root_file_id), 'set');
+
+        array_push($root_file_ids, $root_file_id);
+    }
+
+    // There could be a race that those root files get purged after updating commit_set_items.
+    // Add another round of check to ensure they are not deleted by the end of this function to
+    // mitigate the race condition.
+    foreach ($root_file_ids as &$root_file_id) {
+        $root_file_row = $db->select_first_row('uploaded_files', 'file', array('id' => $root_file_id));
+        if ($root_file_row['file_deleted_at'])
+            return array('status' => 'CannotReuseDeletedRoot', 'details' => array('commitSet' => $destination_id, 'rootFile' => $root_file_id));
+    }
+
+    return NULL;
+}
+
 main(array_get($_GET, 'id'),
     array_key_exists('PATH_INFO', $_SERVER) ? explode('/', trim($_SERVER['PATH_INFO'], '/')) : array(),
     file_get_contents("php://input"));
 
-?>
+?>
\ No newline at end of file

Modified: trunk/Websites/perf.webkit.org/public/v3/models/build-request.js (269870 => 269871)


--- trunk/Websites/perf.webkit.org/public/v3/models/build-request.js	2020-11-16 20:25:21 UTC (rev 269870)
+++ trunk/Websites/perf.webkit.org/public/v3/models/build-request.js	2020-11-16 20:32:08 UTC (rev 269871)
@@ -87,8 +87,45 @@
 
     buildId() { return this._buildId; }
     createdAt() { return this._createdAt; }
+    async findBuildRequestWithSameRoots()
+    {
+        if (!this.isBuild())
+            return null;
+        let scheduledBuildRequest = null;
+        let runningBuildRequest = null;
+        // Set ignoreCache = true as latest status of test groups is expected.
+        const allTestGroupsInTask = await TestGroup.fetchForTask(this.analysisTaskId(), true);
+        for (const group of allTestGroupsInTask) {
+            if (group.id() == this.testGroupId())
+                continue;
+            if (group.isHidden())
+                continue;
+            for (const buildRequest of group.buildRequests()) {
+                if (!buildRequest.isBuild())
+                    continue;
+                if (!this.platform().isInSameGroupAs(buildRequest.platform()))
+                    continue;
+                if (!buildRequest.commitSet().equalsIgnoringRoot(this.commitSet()))
+                    continue;
+                if (!buildRequest.commitSet().areAllRootsAvailable())
+                    continue;
+                if (buildRequest.hasCompleted())
+                    return buildRequest;
+                if (buildRequest.isScheduled()
+                    && (!scheduledBuildRequest || buildRequest.createdAt() < scheduledBuildRequest.createdAt())) {
+                    scheduledBuildRequest = buildRequest;
+                }
+                if (buildRequest.status() == 'running'
+                    && (!runningBuildRequest || buildRequest.createdAt() < runningBuildRequest.createdAt())) {
+                    runningBuildRequest = buildRequest;
+                }
+            }
+        }
+        return runningBuildRequest || scheduledBuildRequest;
+    }
 
-    static formatTimeInterval(intervalInMillionSeconds) {
+    static formatTimeInterval(intervalInMillionSeconds)
+    {
         let intervalInSeconds = intervalInMillionSeconds / 1000;
         const units = [
             {unit: 'week', length: 7 * 24 * 3600},

Modified: trunk/Websites/perf.webkit.org/public/v3/models/commit-set.js (269870 => 269871)


--- trunk/Websites/perf.webkit.org/public/v3/models/commit-set.js	2020-11-16 20:25:21 UTC (rev 269870)
+++ trunk/Websites/perf.webkit.org/public/v3/models/commit-set.js	2020-11-16 20:32:08 UTC (rev 269871)
@@ -74,6 +74,11 @@
     commitsWithTestability() { return this.commits().filter((commit) => !!commit.testability()); }
     commits() { return  Array.from(this._repositoryToCommitMap.values()); }
 
+    areAllRootsAvailable()
+    {
+        return this.allRootFiles().every(rootFile => !rootFile.deletedAt() || this.customRoots().find(rootFile));
+    }
+
     revisionForRepository(repository)
     {
         var commit = this._repositoryToCommitMap.get(repository);
@@ -102,8 +107,18 @@
         return this._latestCommitTime;
     }
 
+    equalsIgnoringRoot(other)
+    {
+        return this._equalsOptionallyIgnoringRoot(other, true);
+    }
+
     equals(other)
     {
+        return this._equalsOptionallyIgnoringRoot(other, false);
+    }
+
+    _equalsOptionallyIgnoringRoot(other, ignoringRoot)
+    {
         if (this._repositories.length != other._repositories.length)
             return false;
         for (const [repository, commit] of this._repositoryToCommitMap) {
@@ -111,7 +126,7 @@
                 return false;
             if (this.patchForRepository(repository) != other.patchForRepository(repository))
                 return false;
-            if (this.rootForRepository(repository) != other.rootForRepository(repository))
+            if (this.rootForRepository(repository) != other.rootForRepository(repository) && !ignoringRoot)
                 return false;
             if (this.ownerCommitForRepository(repository) != other.ownerCommitForRepository(repository))
                 return false;

Modified: trunk/Websites/perf.webkit.org/public/v3/models/platform.js (269870 => 269871)


--- trunk/Websites/perf.webkit.org/public/v3/models/platform.js	2020-11-16 20:25:21 UTC (rev 269870)
+++ trunk/Websites/perf.webkit.org/public/v3/models/platform.js	2020-11-16 20:32:08 UTC (rev 269871)
@@ -24,6 +24,13 @@
         return map ? map[name] : null;
     }
 
+    isInSameGroupAs(other)
+    {
+        if (!this.group() && !other.group())
+            return this == other;
+        return this.group() == other.group();
+    }
+
     hasTest(test)
     {
         if (!this._containingTests) {

Modified: trunk/Websites/perf.webkit.org/server-tests/api-build-requests-tests.js (269870 => 269871)


--- trunk/Websites/perf.webkit.org/server-tests/api-build-requests-tests.js	2020-11-16 20:25:21 UTC (rev 269870)
+++ trunk/Websites/perf.webkit.org/server-tests/api-build-requests-tests.js	2020-11-16 20:32:08 UTC (rev 269871)
@@ -5,6 +5,8 @@
 let MockData = require('./resources/mock-data.js');
 let TestServer = require('./resources/test-server.js');
 const prepareServerTest = require('./resources/common-operations.js').prepareServerTest;
+const assertThrows = require('../server-tests/resources/common-operations').assertThrows;
+const crypto = require('crypto');
 
 describe('/api/build-requests', function () {
     prepareServerTest(this);
@@ -64,6 +66,416 @@
        });
     });
 
+    it('reuse roots from existing build requests if the commits sets are equal except the existence of roots', async () => {
+        await MockData.addMockBuildRequestsWithRoots(TestServer.database());
+        let content = await TestServer.remoteAPI().getJSONWithStatus('/api/build-requests/build-webkit');
+
+        assert.deepEqual(Object.keys(content).sort(), ['buildRequests', 'commitSets', 'commits', 'status', 'uploadedFiles']);
+        assert.equal(content['commitSets'].length, 4);
+        assert.equal(content['commitSets'][0].id, 500);
+        assert.equal(content['commitSets'][2].id, 600);
+
+        assert.deepEqual(content['commitSets'][0].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: 100, requiresBuild: true, rootFile: 101}]);
+        assert.deepEqual(content['commitSets'][2].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: 100, requiresBuild: true, rootFile: null}]);
+
+        assert.equal(content['buildRequests'].length, 8);
+        assert.equal(content['buildRequests'][0].id, 800);
+        assert.equal(content['buildRequests'][0].commitSet, 500);
+        assert.equal(content['buildRequests'][0].status, 'completed');
+        assert.equal(content['buildRequests'][0].url, 'http://build.webkit.org/buids/1');
+        assert.equal(content['buildRequests'][4].id, 900);
+        assert.equal(content['buildRequests'][4].commitSet, 600);
+        assert.equal(content['buildRequests'][4].status, 'pending');
+        assert.equal(content['buildRequests'][4].url, null);
+
+        const updates = {900: {
+            status: content['buildRequests'][0].status, url: content['buildRequests'][0].url, buildRequestForRootReuse: content['buildRequests'][0].id}};
+        const response = await TestServer.remoteAPI().postJSONWithStatus('/api/build-requests/build-webkit', {
+            'slaveName': 'sync-slave',
+            'slavePassword': 'password',
+            'buildRequestUpdates': updates
+        });
+
+        content = await TestServer.remoteAPI().getJSONWithStatus('/api/build-requests/build-webkit');
+        assert.equal(content['commitSets'].length, 4);
+        assert.equal(content['commitSets'][0].id, 500);
+        assert.equal(content['commitSets'][2].id, 600);
+        assert.deepEqual(content['commitSets'][0].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: 100, requiresBuild: true, rootFile: 101}]);
+        assert.deepEqual(content['commitSets'][2].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: 100, requiresBuild: true, rootFile: 101}]);
+
+        assert.equal(content['buildRequests'].length, 8);
+        assert.equal(content['buildRequests'][0].id, 800);
+        assert.equal(content['buildRequests'][0].commitSet, 500);
+        assert.equal(content['buildRequests'][0].status, 'completed');
+        assert.equal(content['buildRequests'][0].url, 'http://build.webkit.org/buids/1');
+        assert.equal(content['buildRequests'][4].id, 900);
+        assert.equal(content['buildRequests'][4].commitSet, 600);
+        assert.equal(content['buildRequests'][4].status, 'completed');
+        assert.equal(content['buildRequests'][4].url, 'http://build.webkit.org/buids/1');
+    });
+
+    it('should reuse root built for owned commit if same completed build request exists', async () => {
+        await MockData.addTwoMockTestGroupWithOwnedCommits(TestServer.database());
+        let content = await TestServer.remoteAPI().getJSONWithStatus('/api/build-requests/build-webkit');
+        assert.deepEqual(Object.keys(content).sort(), ['buildRequests', 'commitSets', 'commits', 'status', 'uploadedFiles']);
+        assert.equal(content['commitSets'].length, 4);
+        assert.equal(content['commitSets'][0].id, 403);
+        assert.equal(content['commitSets'][2].id, 405);
+
+        assert.deepEqual(content['commitSets'][0].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '1797', commitOwner: '93116', patch: null, requiresBuild: true, rootFile: 101}]);
+        assert.deepEqual(content['commitSets'][2].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '1797', commitOwner: '93116', patch: null, requiresBuild: true, rootFile: null}]);
+
+        assert.equal(content['buildRequests'].length, 8);
+        assert.equal(content['buildRequests'][0].id, 704);
+        assert.equal(content['buildRequests'][0].commitSet, 403);
+        assert.equal(content['buildRequests'][0].status, 'completed');
+        assert.equal(content['buildRequests'][0].url, 'http://build.webkit.org/buids/1');
+        assert.equal(content['buildRequests'][4].id, 708);
+        assert.equal(content['buildRequests'][4].commitSet, 405);
+        assert.equal(content['buildRequests'][4].status, 'pending');
+        assert.equal(content['buildRequests'][4].url, null);
+
+        const updates = {708: {
+            status: content['buildRequests'][0].status, url: content['buildRequests'][0].url, buildRequestForRootReuse: 704}};
+
+        const response = await TestServer.remoteAPI().postJSONWithStatus('/api/build-requests/build-webkit', {
+            'slaveName': 'sync-slave',
+            'slavePassword': 'password',
+            'buildRequestUpdates': updates
+        });
+
+        content = await TestServer.remoteAPI().getJSONWithStatus('/api/build-requests/build-webkit');
+        assert.deepEqual(Object.keys(content).sort(), ['buildRequests', 'commitSets', 'commits', 'status', 'uploadedFiles']);
+        assert.equal(content['commitSets'].length, 4);
+        assert.equal(content['commitSets'][0].id, 403);
+        assert.equal(content['commitSets'][2].id, 405);
+
+        assert.deepEqual(content['commitSets'][0].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '1797', commitOwner: '93116', patch: null, requiresBuild: true, rootFile: 101}]);
+        assert.deepEqual(content['commitSets'][2].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '1797', commitOwner: '93116', patch: null, requiresBuild: true, rootFile: 101}]);
+
+        assert.equal(content['buildRequests'].length, 8);
+        assert.equal(content['buildRequests'][0].id, 704);
+        assert.equal(content['buildRequests'][0].commitSet, 403);
+        assert.equal(content['buildRequests'][0].status, 'completed');
+        assert.equal(content['buildRequests'][0].url, 'http://build.webkit.org/buids/1');
+        assert.equal(content['buildRequests'][4].id, 708);
+        assert.equal(content['buildRequests'][4].commitSet, 405);
+        assert.equal(content['buildRequests'][4].status, 'completed');
+        assert.equal(content['buildRequests'][0].url, 'http://build.webkit.org/buids/1');
+    });
+
+    it('reuse roots from existing build requests if the commits sets are equal except the existence of custom roots', async () => {
+        const db = TestServer.database();
+        await MockData.addMockBuildRequestsWithRoots(db);
+        await Promise.all([
+            db.insert('uploaded_files', {id: 103, filename: 'custom-root-103', extension: '.tgz', size: 1, sha256: crypto.createHash('sha256').update('custom-root-103').digest('hex')}),
+            db.insert('commit_set_items', {set: 500, commit: null, patch_file: null, requires_build: false, root_file: 103}),
+            db.insert('uploaded_files', {id: 104, filename: 'custom-root-104', extension: '.tgz', size: 1, sha256: crypto.createHash('sha256').update('custom-root-104').digest('hex')}),
+            db.insert('commit_set_items', {set: 600, commit: null, patch_file: null, requires_build: false, root_file: 104}),
+        ]);
+        let content = await TestServer.remoteAPI().getJSONWithStatus('/api/build-requests/build-webkit');
+
+        assert.deepEqual(Object.keys(content).sort(), ['buildRequests', 'commitSets', 'commits', 'status', 'uploadedFiles']);
+        assert.equal(content['commitSets'].length, 4);
+        assert.equal(content['commitSets'][0].id, 500);
+        assert.equal(content['commitSets'][2].id, 600);
+
+        assert.deepEqual(content['commitSets'][0].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: 100, requiresBuild: true, rootFile: 101}]);
+        assert.deepEqual(content['commitSets'][2].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: 100, requiresBuild: true, rootFile: null}]);
+        assert.deepEqual(content['commitSets'][0].customRoots, [103]);
+        assert.deepEqual(content['commitSets'][2].customRoots, [104]);
+
+        assert.equal(content['buildRequests'].length, 8);
+        assert.equal(content['buildRequests'][0].id, 800);
+        assert.equal(content['buildRequests'][0].commitSet, 500);
+        assert.equal(content['buildRequests'][0].status, 'completed');
+        assert.equal(content['buildRequests'][0].url, 'http://build.webkit.org/buids/1');
+        assert.equal(content['buildRequests'][4].id, 900);
+        assert.equal(content['buildRequests'][4].commitSet, 600);
+        assert.equal(content['buildRequests'][4].status, 'pending');
+        assert.equal(content['buildRequests'][4].url, null);
+
+        const updates = {900: {
+                status: content['buildRequests'][0].status, url: content['buildRequests'][0].url, buildRequestForRootReuse: content['buildRequests'][0].id}};
+        const response = await TestServer.remoteAPI().postJSONWithStatus('/api/build-requests/build-webkit', {
+            'slaveName': 'sync-slave',
+            'slavePassword': 'password',
+            'buildRequestUpdates': updates
+        });
+
+        content = await TestServer.remoteAPI().getJSONWithStatus('/api/build-requests/build-webkit');
+        assert.equal(content['commitSets'].length, 4);
+        assert.equal(content['commitSets'][0].id, 500);
+        assert.equal(content['commitSets'][2].id, 600);
+        assert.deepEqual(content['commitSets'][0].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: 100, requiresBuild: true, rootFile: 101}]);
+        assert.deepEqual(content['commitSets'][2].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: 100, requiresBuild: true, rootFile: 101}]);
+        assert.deepEqual(content['commitSets'][0].customRoots, [103]);
+        assert.deepEqual(content['commitSets'][2].customRoots, [104]);
+
+        assert.equal(content['buildRequests'].length, 8);
+        assert.equal(content['buildRequests'][0].id, 800);
+        assert.equal(content['buildRequests'][0].commitSet, 500);
+        assert.equal(content['buildRequests'][0].status, 'completed');
+        assert.equal(content['buildRequests'][0].url, 'http://build.webkit.org/buids/1');
+        assert.equal(content['buildRequests'][4].id, 900);
+        assert.equal(content['buildRequests'][4].commitSet, 600);
+        assert.equal(content['buildRequests'][4].status, 'completed');
+        assert.equal(content['buildRequests'][4].url, 'http://build.webkit.org/buids/1');
+    });
+
+    it('should fail request with "CannotReuseDeletedRoot" if any root to reuse is deleted', async () => {
+        await MockData.addMockBuildRequestsWithRoots(TestServer.database());
+        await TestServer.database().query("UPDATE uploaded_files SET file_deleted_at = now() at time zone 'utc' WHERE file_id=101");
+        const content = await TestServer.remoteAPI().getJSONWithStatus('/api/build-requests/build-webkit');
+
+        assert.deepEqual(Object.keys(content).sort(), ['buildRequests', 'commitSets', 'commits', 'status', 'uploadedFiles']);
+        assert.equal(content['commitSets'].length, 4);
+        assert.equal(content['commitSets'][0].id, 500);
+        assert.equal(content['commitSets'][2].id, 600);
+
+        assert.deepEqual(content['commitSets'][0].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: 100, requiresBuild: true, rootFile: 101}]);
+        assert.deepEqual(content['commitSets'][2].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: 100, requiresBuild: true, rootFile: null}]);
+
+        assert.equal(content['buildRequests'].length, 8);
+        assert.equal(content['buildRequests'][0].id, 800);
+        assert.equal(content['buildRequests'][0].commitSet, 500);
+        assert.equal(content['buildRequests'][0].status, 'completed');
+        assert.equal(content['buildRequests'][0].url, 'http://build.webkit.org/buids/1');
+        assert.equal(content['buildRequests'][4].id, 900);
+        assert.equal(content['buildRequests'][4].commitSet, 600);
+        assert.equal(content['buildRequests'][4].status, 'pending');
+        assert.equal(content['buildRequests'][4].url, null);
+
+        const updates = {900: {
+            status: content['buildRequests'][0].status, url: content['buildRequests'][0].url, buildRequestForRootReuse: content['buildRequests'][0].id}};
+        await assertThrows('CannotReuseDeletedRoot', () => TestServer.remoteAPI().postJSONWithStatus('/api/build-requests/build-webkit', {
+            'slaveName': 'sync-slave',
+            'slavePassword': 'password',
+            'buildRequestUpdates': updates
+        }));
+    });
+
+    it('should fail request with "CannotReuseDeletedRoot" if any root to reuse is deleted while updating commit set items ', async () => {
+        await MockData.addMockBuildRequestsWithRoots(TestServer.database());
+        await TestServer.database().query(`CREATE OR REPLACE FUNCTION emunlate_file_purge() RETURNS TRIGGER AS $emunlate_file_purge$
+            BEGIN
+                UPDATE uploaded_files SET file_deleted_at = (CURRENT_TIMESTAMP AT TIME ZONE 'UTC') WHERE file_id = NEW.commitset_root_file;
+                RETURN NULL;
+            END;
+            $emunlate_file_purge$ LANGUAGE plpgsql;`);
+        await TestServer.database().query(`CREATE TRIGGER emunlate_file_purge AFTER UPDATE OF commitset_root_file ON commit_set_items
+            FOR EACH ROW EXECUTE PROCEDURE emunlate_file_purge();`);
+        const content = await TestServer.remoteAPI().getJSONWithStatus('/api/build-requests/build-webkit');
+
+        assert.deepEqual(Object.keys(content).sort(), ['buildRequests', 'commitSets', 'commits', 'status', 'uploadedFiles']);
+        assert.equal(content['commitSets'].length, 4);
+        assert.equal(content['commitSets'][0].id, 500);
+        assert.equal(content['commitSets'][2].id, 600);
+
+        assert.deepEqual(content['commitSets'][0].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: 100, requiresBuild: true, rootFile: 101}]);
+        assert.deepEqual(content['commitSets'][2].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: 100, requiresBuild: true, rootFile: null}]);
+
+        assert.equal(content['buildRequests'].length, 8);
+        assert.equal(content['buildRequests'][0].id, 800);
+        assert.equal(content['buildRequests'][0].commitSet, 500);
+        assert.equal(content['buildRequests'][0].status, 'completed');
+        assert.equal(content['buildRequests'][0].url, 'http://build.webkit.org/buids/1');
+        assert.equal(content['buildRequests'][4].id, 900);
+        assert.equal(content['buildRequests'][4].commitSet, 600);
+        assert.equal(content['buildRequests'][4].status, 'pending');
+        assert.equal(content['buildRequests'][4].url, null);
+
+        const updates = {900: {
+            status: content['buildRequests'][0].status, url: content['buildRequests'][0].url, buildRequestForRootReuse: content['buildRequests'][0].id}};
+
+        await assertThrows('CannotReuseDeletedRoot', () => TestServer.remoteAPI().postJSONWithStatus('/api/build-requests/build-webkit', {
+            'slaveName': 'sync-slave',
+            'slavePassword': 'password',
+            'buildRequestUpdates': updates
+        }));
+    });
+
+    it('should fail request with "NoMatchingCommitSetItem" if build request to reuse does not have patch', async () => {
+        await MockData.addMockData(TestServer.database(), ['completed', 'pending', 'pending', 'pending'], true, ['http://build.webkit.org/buids/2', null, null, null]);
+        await MockData.addMockBuildRequestsWithRoots(TestServer.database(), ['running', 'pending', 'pending', 'pending', 'pending', 'pending', 'pending', 'pending'], true, false);
+        const content = await TestServer.remoteAPI().getJSONWithStatus('/api/build-requests/build-webkit');
+
+        assert.equal(content['commitSets'].length, 6);
+        assert.equal(content['commitSets'][0].id, 401);
+        assert.equal(content['commitSets'][4].id, 600);
+
+        assert.deepEqual(content['commitSets'][0].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: null, requiresBuild: false, rootFile: null}]);
+        assert.deepEqual(content['commitSets'][4].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: 100, requiresBuild: true, rootFile: null}]);
+
+        assert.equal(content['buildRequests'].length, 12);
+        assert.equal(content['buildRequests'][0].id, 700);
+        assert.equal(content['buildRequests'][0].commitSet, 401);
+        assert.equal(content['buildRequests'][0].status, 'completed');
+        assert.equal(content['buildRequests'][0].url, 'http://build.webkit.org/buids/2');
+        assert.equal(content['buildRequests'][8].id, 900);
+        assert.equal(content['buildRequests'][8].commitSet, 600);
+        assert.equal(content['buildRequests'][8].status, 'pending');
+        assert.equal(content['buildRequests'][8].url, null);
+
+        const updates = {900: {
+                status: content['buildRequests'][0].status, url: content['buildRequests'][0].url, buildRequestForRootReuse: content['buildRequests'][0].id}};
+
+        await assertThrows('NoMatchingCommitSetItem', () => TestServer.remoteAPI().postJSONWithStatus('/api/build-requests/build-webkit', {
+            'slaveName': 'sync-slave',
+            'slavePassword': 'password',
+            'buildRequestUpdates': updates
+        }));
+    });
+
+    it('should fail request with "CannotReuseRootWithNonMatchingCommitSets" if commit sets have different number of entries', async () => {
+        await MockData.addMockData(TestServer.database(), ['completed', 'pending', 'pending', 'pending'], true, ['http://build.webkit.org/buids/2', null, null, null]);
+        await MockData.addMockBuildRequestsWithRoots(TestServer.database(), ['running', 'pending', 'pending', 'pending', 'pending', 'pending', 'pending', 'pending'], true, false);
+        await TestServer.database().insert('commit_set_items', {set: 401, commit: 111168})
+        const content = await TestServer.remoteAPI().getJSONWithStatus('/api/build-requests/build-webkit');
+
+        assert.equal(content['commitSets'].length, 6);
+        assert.equal(content['commitSets'][0].id, 401);
+        assert.equal(content['commitSets'][4].id, 600);
+
+        assert.deepEqual(content['commitSets'][0].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '111168', commitOwner: null, patch: null, requiresBuild: false, rootFile: null}]);
+        assert.deepEqual(content['commitSets'][4].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: 100, requiresBuild: true, rootFile: null}]);
+
+        assert.equal(content['buildRequests'].length, 12);
+        assert.equal(content['buildRequests'][0].id, 700);
+        assert.equal(content['buildRequests'][0].commitSet, 401);
+        assert.equal(content['buildRequests'][0].status, 'completed');
+        assert.equal(content['buildRequests'][0].url, 'http://build.webkit.org/buids/2');
+        assert.equal(content['buildRequests'][8].id, 900);
+        assert.equal(content['buildRequests'][8].commitSet, 600);
+        assert.equal(content['buildRequests'][8].status, 'pending');
+        assert.equal(content['buildRequests'][8].url, null);
+
+        const updates = {900: {
+                status: content['buildRequests'][0].status, url: content['buildRequests'][0].url, buildRequestForRootReuse: content['buildRequests'][0].id}};
+
+        await assertThrows('CannotReuseRootWithNonMatchingCommitSets', () => TestServer.remoteAPI().postJSONWithStatus('/api/build-requests/build-webkit', {
+            'slaveName': 'sync-slave',
+            'slavePassword': 'password',
+            'buildRequestUpdates': updates
+        }));
+    });
+
+    it('should fail request with "CanOnlyReuseCompletedBuildRequest" if build request to reuse is not completed', async () => {
+        await MockData.addMockBuildRequestsWithRoots(TestServer.database(), ['running', 'pending', 'pending', 'pending', 'pending', 'pending', 'pending', 'pending']);
+        const content = await TestServer.remoteAPI().getJSONWithStatus('/api/build-requests/build-webkit');
+
+        assert.deepEqual(Object.keys(content).sort(), ['buildRequests', 'commitSets', 'commits', 'status', 'uploadedFiles']);
+        assert.equal(content['commitSets'].length, 4);
+        assert.equal(content['commitSets'][0].id, 500);
+        assert.equal(content['commitSets'][2].id, 600);
+
+        assert.deepEqual(content['commitSets'][0].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: 100, requiresBuild: true, rootFile: 101}]);
+        assert.deepEqual(content['commitSets'][2].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: 100, requiresBuild: true, rootFile: null}]);
+
+        assert.equal(content['buildRequests'].length, 8);
+        assert.equal(content['buildRequests'][0].id, 800);
+        assert.equal(content['buildRequests'][0].commitSet, 500);
+        assert.equal(content['buildRequests'][0].status, 'running');
+        assert.equal(content['buildRequests'][0].url, 'http://build.webkit.org/buids/1');
+        assert.equal(content['buildRequests'][4].id, 900);
+        assert.equal(content['buildRequests'][4].commitSet, 600);
+        assert.equal(content['buildRequests'][4].status, 'pending');
+        assert.equal(content['buildRequests'][4].url, null);
+
+        const updates = {900: {
+            status: content['buildRequests'][0].status, url: content['buildRequests'][0].url, buildRequestForRootReuse: content['buildRequests'][0].id}};
+        await assertThrows('CanOnlyReuseCompletedBuildRequest', () => TestServer.remoteAPI().postJSONWithStatus('/api/build-requests/build-webkit', {
+            'slaveName': 'sync-slave',
+            'slavePassword': 'password',
+            'buildRequestUpdates': updates
+        }));
+    });
+
+    it('should fail request with "FailedToFindReuseBuildRequest" if the build request to reuse does not exist', async () => {
+        await MockData.addMockBuildRequestsWithRoots(TestServer.database());
+        const content = await TestServer.remoteAPI().getJSONWithStatus('/api/build-requests/build-webkit');
+
+        assert.deepEqual(Object.keys(content).sort(), ['buildRequests', 'commitSets', 'commits', 'status', 'uploadedFiles']);
+        assert.equal(content['commitSets'].length, 4);
+        assert.equal(content['commitSets'][0].id, 500);
+        assert.equal(content['commitSets'][2].id, 600);
+
+        assert.deepEqual(content['commitSets'][0].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: 100, requiresBuild: true, rootFile: 101}]);
+        assert.deepEqual(content['commitSets'][2].revisionItems, [
+            {commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+            {commit: '93116', commitOwner: null, patch: 100, requiresBuild: true, rootFile: null}]);
+
+        assert.equal(content['buildRequests'].length, 8);
+        assert.equal(content['buildRequests'][0].id, 800);
+        assert.equal(content['buildRequests'][0].commitSet, 500);
+        assert.equal(content['buildRequests'][0].status, 'completed');
+        assert.equal(content['buildRequests'][0].url, 'http://build.webkit.org/buids/1');
+        assert.equal(content['buildRequests'][4].id, 900);
+        assert.equal(content['buildRequests'][4].commitSet, 600);
+        assert.equal(content['buildRequests'][4].status, 'pending');
+        assert.equal(content['buildRequests'][4].url, null);
+
+        const updates = {900: {
+            status: content['buildRequests'][0].status, url: content['buildRequests'][0].url, buildRequestForRootReuse: 999}};
+
+        await assertThrows('FailedToFindbuildRequestForRootReuse', () => TestServer.remoteAPI().postJSONWithStatus('/api/build-requests/build-webkit', {
+            'slaveName': 'sync-slave',
+            'slavePassword': 'password',
+            'buildRequestUpdates': updates
+        }));
+    });
+
     it('should return build requests associated with a given triggerable with appropriate commits and commitSets', () => {
         return MockData.addMockData(TestServer.database()).then(() => {
             return TestServer.remoteAPI().getJSONWithStatus('/api/build-requests/build-webkit');

Modified: trunk/Websites/perf.webkit.org/server-tests/resources/mock-data.js (269870 => 269871)


--- trunk/Websites/perf.webkit.org/server-tests/resources/mock-data.js	2020-11-16 20:25:21 UTC (rev 269870)
+++ trunk/Websites/perf.webkit.org/server-tests/resources/mock-data.js	2020-11-16 20:32:08 UTC (rev 269871)
@@ -40,9 +40,9 @@
             db.insert('repositories', {id: this.sharedRepositoryId(), name: 'Shared'}),
             db.insert('repositories', {id: this.ownedJSCRepositoryId(), owner: this.webkitRepositoryId(), name: '_javascript_Core'}),
             db.insert('repositories', {id: this.jscRepositoryId(), name: '_javascript_Core'}),
-            db.insert('triggerable_repository_groups', {id: 2001, name: 'webkit-svn', triggerable: 1000}),
+            db.insert('triggerable_repository_groups', {id: 2001, name: 'webkit-svn', triggerable: 1000, accepts_roots: true}),
             db.insert('triggerable_repositories', {repository: this.macosRepositoryId(), group: 2001}),
-            db.insert('triggerable_repositories', {repository: this.webkitRepositoryId(), group: 2001}),
+            db.insert('triggerable_repositories', {repository: this.webkitRepositoryId(), group: 2001, accepts_patch: true}),
             db.insert('commits', {id: 87832, repository: this.macosRepositoryId(), revision: '10.11 15A284'}),
             db.insert('commits', {id: 93116, repository: this.webkitRepositoryId(), revision: '191622', time: (new Date(1445945816878)).toISOString()}),
             db.insert('commits', {id: 96336, repository: this.webkitRepositoryId(), revision: '192736', time: (new Date(1448225325650)).toISOString()}),
@@ -64,10 +64,12 @@
             db.insert('test_runs', {id: 801, config: 301, build: 901, mean_cache: 100}),
         ]);
     },
-    addMockData: function (db, statusList, needsNotification=true)
+    addMockData: function (db, statusList, needsNotification=true, urlList=[])
     {
         if (!statusList)
             statusList = ['pending', 'pending', 'pending', 'pending'];
+        if (!urlList)
+            urlList = [null, null, null, null];
         return Promise.all([
             this.addMockConfiguration(db),
             db.insert('commit_sets', {id: 401}),
@@ -80,12 +82,52 @@
                 start_run: 801, start_run_time: '2015-10-27T12:05:27.1Z',
                 end_run: 801, end_run_time: '2015-10-27T12:05:27.1Z'}),
             db.insert('analysis_test_groups', {id: 600, task: 500, name: 'some test group', initial_repetition_count: 4, needs_notification: needsNotification}),
-            db.insert('build_requests', {id: 700, status: statusList[0], triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 600, order: 0, commit_set: 401}),
-            db.insert('build_requests', {id: 701, status: statusList[1], triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 600, order: 1, commit_set: 402}),
-            db.insert('build_requests', {id: 702, status: statusList[2], triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 600, order: 2, commit_set: 401}),
-            db.insert('build_requests', {id: 703, status: statusList[3], triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 600, order: 3, commit_set: 402}),
+            db.insert('build_requests', {id: 700, status: statusList[0], url: urlList[0], triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 600, order: 0, commit_set: 401}),
+            db.insert('build_requests', {id: 701, status: statusList[1], url: urlList[1], triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 600, order: 1, commit_set: 402}),
+            db.insert('build_requests', {id: 702, status: statusList[2], url: urlList[2], triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 600, order: 2, commit_set: 401}),
+            db.insert('build_requests', {id: 703, status: statusList[3], url: urlList[3], triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 600, order: 3, commit_set: 402}),
         ]);
     },
+    addMockBuildRequestsWithRoots(db, statusList, needsNotification=true, addMockConfiguration=true)
+    {
+        const setupSteps = addMockConfiguration ? [this.addMockConfiguration(db)] : [];
+        if (!statusList)
+            statusList = ['completed', 'running', 'pending', 'pending', 'pending', 'pending', 'pending', 'pending'];
+        return Promise.all([
+            ...setupSteps,
+            db.insert('uploaded_files', {id: 100, filename: 'patch-100', extension: '.txt', size: 1, sha256: crypto.createHash('sha256').update('patch-100').digest('hex')}),
+            db.insert('uploaded_files', {id: 101, filename: 'root-101', extension: '.tgz', size: 1, sha256: crypto.createHash('sha256').update('root-101').digest('hex')}),
+            db.insert('uploaded_files', {id: 102, filename: 'patch-102', extension: '.txt', size: 1, sha256: crypto.createHash('sha256').update('patch-102').digest('hex')}),
+
+            db.insert('commit_sets', {id: 500}),
+            db.insert('commit_set_items', {set: 500, commit: 87832}),
+            db.insert('commit_set_items', {set: 500, commit: 93116, patch_file: 100, requires_build: true, root_file: 101}),
+            db.insert('commit_sets', {id: 501}),
+            db.insert('commit_set_items', {set: 501, commit: 87832}),
+            db.insert('commit_set_items', {set: 501, commit: 96336, patch_file: 102, requires_build: true}),
+
+            db.insert('commit_sets', {id: 600}),
+            db.insert('commit_set_items', {set: 600, commit: 87832}),
+            db.insert('commit_set_items', {set: 600, commit: 93116, patch_file: 100, requires_build: true}),
+            db.insert('commit_sets', {id: 601}),
+            db.insert('commit_set_items', {set: 601, commit: 87832}),
+            db.insert('commit_set_items', {set: 601, commit: 96336, patch_file: 102, requires_build: true}),
+
+            db.insert('analysis_tasks', {id: 600, name: 'another task'}),
+
+            db.insert('analysis_test_groups', {id: 700, task: 600, name: 'test with root built', initial_repetition_count: 1, needs_notification: needsNotification}),
+            db.insert('build_requests', {id: 800, status: statusList[0], triggerable: 1000, repository_group: 2001, platform: 65, group: 700, order: -2, commit_set: 500, url: 'http://build.webkit.org/buids/1'}),
+            db.insert('build_requests', {id: 801, status: statusList[1], triggerable: 1000, repository_group: 2001, platform: 65, group: 700, order: -1, commit_set: 501}),
+            db.insert('build_requests', {id: 802, status: statusList[2], triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 700, order: 0, commit_set: 500}),
+            db.insert('build_requests', {id: 803, status: statusList[3], triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 700, order: 1, commit_set: 501}),
+
+            db.insert('analysis_test_groups', {id: 701, task: 600, name: 'test will reuse root', initial_repetition_count: 1, needs_notification: needsNotification}),
+            db.insert('build_requests', {id: 900, status: statusList[4], triggerable: 1000, repository_group: 2001, platform: 65, group: 701, order: -2, commit_set: 600}),
+            db.insert('build_requests', {id: 901, status: statusList[5], triggerable: 1000, repository_group: 2001, platform: 65, group: 701, order: -1, commit_set: 601}),
+            db.insert('build_requests', {id: 902, status: statusList[6], triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 701, order: 0, commit_set: 600}),
+            db.insert('build_requests', {id: 903, status: statusList[7], triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 701, order: 1, commit_set: 601}),
+        ]);
+    },
     addAnotherTriggerable(db) {
         return Promise.all([
             db.insert('build_triggerables', {id: 2345, name: 'build-webkit-jsc'}),
@@ -172,6 +214,45 @@
             db.insert('build_requests', {id: 707, status: statusList[3], triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 900, order: 3, commit_set: 404}),
         ]);
     },
+    addTwoMockTestGroupWithOwnedCommits(db)
+    {
+        return Promise.all([
+            this.addMockConfiguration(db),
+            this.addAnotherTriggerable(db),
+            db.insert('analysis_tasks', {id: 1080, platform: 65, metric: 300, name: 'some task with component test',
+                start_run: 801, start_run_time: '2015-10-27T12:05:27.1Z',
+                end_run: 801, end_run_time: '2015-10-27T12:05:27.1Z'}),
+
+            db.insert('uploaded_files', {id: 101, filename: 'root-101', size: 1, sha256: crypto.createHash('sha256').update('root-101').digest('hex'), }),
+            db.insert('analysis_test_groups', {id: 900, task: 1080, name: 'some test group with component test(root built)', initial_repetition_count: 1}),
+            db.insert('commit_sets', {id: 403}),
+            db.insert('commit_set_items', {set: 403, commit: 87832}),
+            db.insert('commit_set_items', {set: 403, commit: 93116}),
+            db.insert('commit_set_items', {set: 403, commit: 1797, commit_owner: 93116, requires_build: true, root_file: 101}),
+            db.insert('commit_sets', {id: 404}),
+            db.insert('commit_set_items', {set: 404, commit: 87832}),
+            db.insert('commit_set_items', {set: 404, commit: 96336}),
+            db.insert('commit_set_items', {set: 404, commit: 2017, commit_owner: 96336, requires_build: true}),
+            db.insert('build_requests', {id: 704, status: 'completed', triggerable: 1000, repository_group: 2001, platform: 65, group: 900, order: -2, commit_set: 403, url: 'http://build.webkit.org/buids/1'}),
+            db.insert('build_requests', {id: 705, status: 'pending', triggerable: 1000, repository_group: 2001, platform: 65, group: 900, order: -1, commit_set: 404}),
+            db.insert('build_requests', {id: 706, status: 'pending', triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 900, order: 0, commit_set: 403}),
+            db.insert('build_requests', {id: 707, status: 'pending', triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 900, order: 1, commit_set: 404}),
+
+            db.insert('analysis_test_groups', {id: 901, task: 1080, name: 'some test group with component test', initial_repetition_count: 1}),
+            db.insert('commit_sets', {id: 405}),
+            db.insert('commit_set_items', {set: 405, commit: 87832}),
+            db.insert('commit_set_items', {set: 405, commit: 93116}),
+            db.insert('commit_set_items', {set: 405, commit: 1797, commit_owner: 93116, requires_build: true}),
+            db.insert('commit_sets', {id: 406}),
+            db.insert('commit_set_items', {set: 406, commit: 87832}),
+            db.insert('commit_set_items', {set: 406, commit: 96336}),
+            db.insert('commit_set_items', {set: 406, commit: 2017, commit_owner: 96336, requires_build: true}),
+            db.insert('build_requests', {id: 708, status: 'pending', triggerable: 1000, repository_group: 2001, platform: 65, group: 901, order: -2, commit_set: 405}),
+            db.insert('build_requests', {id: 709, status: 'pending', triggerable: 1000, repository_group: 2001, platform: 65, group: 901, order: -1, commit_set: 406}),
+            db.insert('build_requests', {id: 710, status: 'pending', triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 901, order: 0, commit_set: 405}),
+            db.insert('build_requests', {id: 711, status: 'pending', triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 901, order: 1, commit_set: 406}),
+        ]);
+    },
     addTestGroupWithOwnerCommitNotInCommitSet(db)
     {
         return Promise.all([
@@ -219,6 +300,51 @@
             ]
         }
     },
+    mockTestSyncConfigWithPatchAcceptingBuilder: function ()
+    {
+        return {
+            'triggerableName': 'build-webkit',
+            'lookbackCount': 2,
+            'buildRequestArgument': 'build-request-id',
+            'repositoryGroups': {
+                'webkit-svn': {
+                    'repositories': {'WebKit': {'acceptsPatch': true}, 'macOS': {}},
+                    'acceptsRoots': true,
+                    'testProperties': {
+                        'os': {'revision': 'macOS'},
+                        'wk': {'revision': 'WebKit'},
+                        'roots': {"roots": {}},
+                    },
+                    'buildProperties': {
+                        'os': {'revision': 'macOS'},
+                        'wk': {'revision': 'WebKit'},
+                        'wk-patch': {'patch': 'WebKit'},
+                    }
+                }
+            },
+            'types': {
+                'some-test': {'test': ['some test']}
+            },
+            'builders': {
+                'tester': {'builder': 'some tester', properties: {forcescheduler: 'force-some-tester'}},
+                'builder-1': {'builder': 'some-builder-1', properties: {forcescheduler: 'force-some-builder-1'}},
+                'builder-2': {'builder': 'some builder 2', properties: {forcescheduler: 'force-some-builder-2'}},
+            },
+            'testConfigurations': [
+                {
+                    'platforms': ['some platform'],
+                    'types': ['some-test'],
+                    'builders': ['tester'],
+                }
+            ],
+            'buildConfigurations': [
+                {
+                    'platforms': ['some platform'],
+                    'builders': ['builder-1', 'builder-2'],
+                }
+            ]
+        }
+    },
     mockTestSyncConfigWithTwoBuilders: function ()
     {
         return {

Modified: trunk/Websites/perf.webkit.org/server-tests/tools-buildbot-triggerable-tests.js (269870 => 269871)


--- trunk/Websites/perf.webkit.org/server-tests/tools-buildbot-triggerable-tests.js	2020-11-16 20:25:21 UTC (rev 269870)
+++ trunk/Websites/perf.webkit.org/server-tests/tools-buildbot-triggerable-tests.js	2020-11-16 20:32:08 UTC (rev 269871)
@@ -946,6 +946,302 @@
                 MockRemoteAPI.requests[2].resolve('OK');
             });
         });
+
+        it('should reuse the roots from a completed build request with the same commit set', async () => {
+            await MockData.addMockBuildRequestsWithRoots(TestServer.database());
+            await Manifest.fetch();
+            const config = MockData.mockTestSyncConfigWithPatchAcceptingBuilder();
+            const logger = new MockLogger;
+            const slaveInfo = {name: 'sync-slave', password: 'password'};
+            const triggerable = new BuildbotTriggerable(config, TestServer.remoteAPI(), MockRemoteAPI, slaveInfo, logger);
+            const syncPromise = triggerable.initSyncers().then(() => triggerable.syncOnce());
+            assertRequestAndResolve(MockRemoteAPI.requests[0], 'GET', MockData.buildbotBuildersURL(), MockData.mockBuildbotBuilders());
+            MockRemoteAPI.reset();
+            await MockRemoteAPI.waitForRequest();
+
+            assert.equal(BuildRequest.all().length, 8);
+            let buildRequest = BuildRequest.findById(800);
+            let anotherBuildRequest = BuildRequest.findById(900);
+            assert.equal(buildRequest.status(), 'completed');
+            assert.equal(anotherBuildRequest.status(), 'pending');
+            assert.equal(buildRequest.statusUrl(), 'http://build.webkit.org/buids/1');
+            assert.equal(anotherBuildRequest.statusUrl(), null);
+            let commitSet = buildRequest.commitSet();
+            let anotherCommitSet = anotherBuildRequest.commitSet();
+            assert.ok(commitSet.equalsIgnoringRoot(anotherCommitSet));
+            assert.ok(!commitSet.equals(anotherCommitSet));
+
+            assert.equal(MockRemoteAPI.requests.length, 3);
+            assert.equal(MockRemoteAPI.requests[0].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[0].url, MockData.pendingBuildsUrl('some tester'));
+            MockRemoteAPI.requests[0].resolve({});
+            assert.equal(MockRemoteAPI.requests[1].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[1].url, MockData.pendingBuildsUrl('some-builder-1'));
+            MockRemoteAPI.requests[1].resolve({});
+            assert.equal(MockRemoteAPI.requests[2].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[2].url, MockData.pendingBuildsUrl('some builder 2'));
+            MockRemoteAPI.requests[2].resolve({});
+            MockRemoteAPI.reset();
+            await MockRemoteAPI.waitForRequest();
+
+            assert.equal(MockRemoteAPI.requests.length, 3);
+            assert.equal(MockRemoteAPI.requests[0].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[0].url, MockData.recentBuildsUrl('some tester', 2));
+            MockRemoteAPI.requests[0].resolve({});
+            assert.equal(MockRemoteAPI.requests[1].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[1].url, MockData.recentBuildsUrl('some-builder-1', 2));
+            MockRemoteAPI.requests[1].resolve({'builds': [MockData.runningBuildData({buildRequestId: 801}), MockData.finishedBuildData({buildRequestId: 800})]});
+            assert.equal(MockRemoteAPI.requests[2].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[2].url, MockData.recentBuildsUrl('some builder 2', 2));
+            MockRemoteAPI.requests[2].resolve({});
+            MockRemoteAPI.reset();
+            await MockRemoteAPI.waitForRequest();
+
+            assert.equal(MockRemoteAPI.requests.length, 3);
+            assert.equal(MockRemoteAPI.requests[0].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[0].url, MockData.pendingBuildsUrl('some tester'));
+            MockRemoteAPI.requests[0].resolve({});
+            assert.equal(MockRemoteAPI.requests[1].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[1].url, MockData.pendingBuildsUrl('some-builder-1'));
+            MockRemoteAPI.requests[1].resolve({});
+            assert.equal(MockRemoteAPI.requests[2].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[2].url, MockData.pendingBuildsUrl('some builder 2'));
+            MockRemoteAPI.requests[2].resolve({});
+            MockRemoteAPI.reset();
+            await MockRemoteAPI.waitForRequest();
+
+            assert.equal(MockRemoteAPI.requests.length, 3);
+            assert.equal(MockRemoteAPI.requests[0].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[0].url, MockData.recentBuildsUrl('some tester', 2));
+            MockRemoteAPI.requests[0].resolve({});
+            assert.equal(MockRemoteAPI.requests[1].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[1].url, MockData.recentBuildsUrl('some-builder-1', 2));
+            MockRemoteAPI.requests[1].resolve({'builds': [MockData.runningBuildData({buildRequestId: 801}), MockData.finishedBuildData({buildRequestId: 800})]});
+            assert.equal(MockRemoteAPI.requests[2].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[2].url, MockData.recentBuildsUrl('some builder 2', 2));
+            MockRemoteAPI.requests[2].resolve({});
+            MockRemoteAPI.reset();
+
+            await syncPromise;
+            await BuildRequest.fetchForTriggerable(MockData.mockTestSyncConfigWithPatchAcceptingBuilder().triggerableName);
+            assert.equal(BuildRequest.all().length, 8);
+            buildRequest = BuildRequest.findById(800);
+            anotherBuildRequest = BuildRequest.findById(900);
+            assert.equal(buildRequest.status(), 'completed');
+            assert.equal(anotherBuildRequest.status(), 'completed');
+            assert.equal(buildRequest.statusUrl(), 'http://build.webkit.org/buids/1');
+            assert.equal(anotherBuildRequest.statusUrl(), 'http://build.webkit.org/buids/1');
+        });
+
+        it('should defer scheduling a build request if there is a "running" build request with same commit set, but should schedule the build request if "running" build request with same commit set fails later on', async () => {
+            await MockData.addMockBuildRequestsWithRoots(TestServer.database(),  ['running', 'scheduled', 'pending', 'pending', 'pending', 'pending', 'pending', 'pending']);
+            await Manifest.fetch();
+            const config = MockData.mockTestSyncConfigWithPatchAcceptingBuilder();
+            const logger = new MockLogger;
+            const slaveInfo = {name: 'sync-slave', password: 'password'};
+            const triggerable = new BuildbotTriggerable(config, TestServer.remoteAPI(), MockRemoteAPI, slaveInfo, logger);
+            const syncPromise = triggerable.initSyncers().then(() => triggerable.syncOnce());
+            assertRequestAndResolve(MockRemoteAPI.requests[0], 'GET', MockData.buildbotBuildersURL(), MockData.mockBuildbotBuilders());
+            MockRemoteAPI.reset();
+            await MockRemoteAPI.waitForRequest();
+
+            assert.equal(BuildRequest.all().length, 8);
+            let buildRequest = BuildRequest.findById(800);
+            let anotherBuildRequest = BuildRequest.findById(900);
+            assert.equal(buildRequest.status(), 'running');
+            assert.equal(anotherBuildRequest.status(), 'pending');
+            assert.equal(buildRequest.statusUrl(), 'http://build.webkit.org/buids/1');
+            assert.equal(anotherBuildRequest.statusUrl(), null);
+            let commitSet = buildRequest.commitSet();
+            let anotherCommitSet = anotherBuildRequest.commitSet();
+            assert.ok(commitSet.equalsIgnoringRoot(anotherCommitSet));
+            assert.ok(!commitSet.equals(anotherCommitSet));
+
+            assert.equal(MockRemoteAPI.requests.length, 3);
+            assert.equal(MockRemoteAPI.requests[0].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[0].url, MockData.pendingBuildsUrl('some tester'));
+            MockRemoteAPI.requests[0].resolve({});
+            assert.equal(MockRemoteAPI.requests[1].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[1].url, MockData.pendingBuildsUrl('some-builder-1'));
+            MockRemoteAPI.requests[1].resolve({'builds': [MockData.pendingBuild({buildRequestId: 801})]});
+            assert.equal(MockRemoteAPI.requests[2].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[2].url, MockData.pendingBuildsUrl('some builder 2'));
+            MockRemoteAPI.requests[2].resolve({});
+            MockRemoteAPI.reset();
+            await MockRemoteAPI.waitForRequest();
+
+            assert.equal(MockRemoteAPI.requests.length, 3);
+            assert.equal(MockRemoteAPI.requests[0].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[0].url, MockData.recentBuildsUrl('some tester', 2));
+            MockRemoteAPI.requests[0].resolve({});
+            assert.equal(MockRemoteAPI.requests[1].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[1].url, MockData.recentBuildsUrl('some-builder-1', 2));
+            MockRemoteAPI.requests[1].resolve({'builds': [MockData.runningBuildData({buildRequestId: 800})]});
+            assert.equal(MockRemoteAPI.requests[2].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[2].url, MockData.recentBuildsUrl('some builder 2', 2));
+            MockRemoteAPI.requests[2].resolve({});
+            MockRemoteAPI.reset();
+            await MockRemoteAPI.waitForRequest();
+
+            assert.equal(MockRemoteAPI.requests.length, 3);
+            assert.equal(MockRemoteAPI.requests[0].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[0].url, MockData.pendingBuildsUrl('some tester'));
+            MockRemoteAPI.requests[0].resolve({});
+            assert.equal(MockRemoteAPI.requests[1].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[1].url, MockData.pendingBuildsUrl('some-builder-1'));
+            MockRemoteAPI.requests[1].resolve({});
+            assert.equal(MockRemoteAPI.requests[2].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[2].url, MockData.pendingBuildsUrl('some builder 2'));
+            MockRemoteAPI.requests[2].resolve({});
+            MockRemoteAPI.reset();
+            await MockRemoteAPI.waitForRequest();
+
+            assert.equal(MockRemoteAPI.requests.length, 3);
+            assert.equal(MockRemoteAPI.requests[0].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[0].url, MockData.recentBuildsUrl('some tester', 2));
+            MockRemoteAPI.requests[0].resolve({});
+            assert.equal(MockRemoteAPI.requests[1].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[1].url, MockData.recentBuildsUrl('some-builder-1', 2));
+            MockRemoteAPI.requests[1].resolve({'builds': [MockData.runningBuildData({buildRequestId: 801}), MockData.finishedBuildData({buildRequestId: 800})]});
+            assert.equal(MockRemoteAPI.requests[2].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[2].url, MockData.recentBuildsUrl('some builder 2', 2));
+            MockRemoteAPI.requests[2].resolve({});
+            MockRemoteAPI.reset();
+
+            await syncPromise;
+            await BuildRequest.fetchForTriggerable(MockData.mockTestSyncConfigWithPatchAcceptingBuilder().triggerableName);
+            assert.equal(BuildRequest.all().length, 8);
+            buildRequest = BuildRequest.findById(800);
+            anotherBuildRequest = BuildRequest.findById(900);
+            assert.equal(buildRequest.status(), 'failed');
+            assert.equal(anotherBuildRequest.status(), 'pending');
+            assert.equal(buildRequest.statusUrl(), MockData.statusUrl('some-builder-1', 123));
+            assert.equal(anotherBuildRequest.statusUrl(), null);
+
+            const secondSyncPromise = triggerable.initSyncers().then(() => triggerable.syncOnce());
+            assertRequestAndResolve(MockRemoteAPI.requests[0], 'GET', MockData.buildbotBuildersURL(), MockData.mockBuildbotBuilders());
+            MockRemoteAPI.reset();
+            await MockRemoteAPI.waitForRequest();
+
+            assert.equal(MockRemoteAPI.requests.length, 3);
+            assert.equal(MockRemoteAPI.requests[0].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[0].url, MockData.pendingBuildsUrl('some tester'));
+            MockRemoteAPI.requests[0].resolve({});
+            assert.equal(MockRemoteAPI.requests[1].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[1].url, MockData.pendingBuildsUrl('some-builder-1'));
+            MockRemoteAPI.requests[1].resolve({});
+            assert.equal(MockRemoteAPI.requests[2].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[2].url, MockData.pendingBuildsUrl('some builder 2'));
+            MockRemoteAPI.requests[2].resolve({});
+            MockRemoteAPI.reset();
+            await MockRemoteAPI.waitForRequest();
+
+            assert.equal(MockRemoteAPI.requests.length, 3);
+            assert.equal(MockRemoteAPI.requests[0].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[0].url, MockData.recentBuildsUrl('some tester', 2));
+            MockRemoteAPI.requests[0].resolve({});
+            assert.equal(MockRemoteAPI.requests[1].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[1].url, MockData.recentBuildsUrl('some-builder-1', 2));
+            MockRemoteAPI.requests[1].resolve({'builds': [MockData.runningBuildData({buildRequestId: 801})]});
+            assert.equal(MockRemoteAPI.requests[2].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[2].url, MockData.recentBuildsUrl('some builder 2', 2));
+            MockRemoteAPI.requests[2].resolve({});
+            MockRemoteAPI.reset();
+            await MockRemoteAPI.waitForRequest();
+
+            assert.equal(MockRemoteAPI.requests.length, 1);
+            assert.equal(MockRemoteAPI.requests[0].method, 'POST');
+            assert.equal(MockRemoteAPI.requests[0].url, '/api/v2/forceschedulers/force-some-builder-2');
+            assert.deepEqual(MockRemoteAPI.requests[0].data, {'id': 900, 'jsonrpc': '2.0', 'method': 'force', 'params':
+                {'wk': '191622', 'os': '10.11 15A284', 'wk-patch': 'http://localhost:8180/api/uploaded-file/100.txt',
+                'build-request-id': '900', 'forcescheduler': 'force-some-builder-2'}});
+        });
+
+        it('should not reuse the root when a build request with same commit set is avaialble but the build request has been scheduled', async () => {
+            await MockData.addMockBuildRequestsWithRoots(TestServer.database());
+            await Manifest.fetch();
+            const config = MockData.mockTestSyncConfigWithPatchAcceptingBuilder();
+            const logger = new MockLogger;
+            const slaveInfo = {name: 'sync-slave', password: 'password'};
+            const triggerable = new BuildbotTriggerable(config, TestServer.remoteAPI(), MockRemoteAPI, slaveInfo, logger);
+            const syncPromise = triggerable.initSyncers().then(() => triggerable.syncOnce());
+            assertRequestAndResolve(MockRemoteAPI.requests[0], 'GET', MockData.buildbotBuildersURL(), MockData.mockBuildbotBuilders());
+            MockRemoteAPI.reset();
+            await MockRemoteAPI.waitForRequest();
+
+            assert.equal(BuildRequest.all().length, 8);
+            let buildRequest = BuildRequest.findById(800);
+            let anotherBuildRequest = BuildRequest.findById(900);
+            assert.equal(buildRequest.status(), 'completed');
+            assert.equal(anotherBuildRequest.status(), 'pending');
+            assert.equal(buildRequest.statusUrl(), 'http://build.webkit.org/buids/1');
+            assert.equal(anotherBuildRequest.statusUrl(), null);
+            let commitSet = buildRequest.commitSet();
+            let anotherCommitSet = anotherBuildRequest.commitSet();
+            assert.ok(commitSet.equalsIgnoringRoot(anotherCommitSet));
+            assert.ok(!commitSet.equals(anotherCommitSet));
+
+            assert.equal(MockRemoteAPI.requests.length, 3);
+            assert.equal(MockRemoteAPI.requests[0].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[0].url, MockData.pendingBuildsUrl('some tester'));
+            MockRemoteAPI.requests[0].resolve({});
+            assert.equal(MockRemoteAPI.requests[1].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[1].url, MockData.pendingBuildsUrl('some-builder-1'));
+            MockRemoteAPI.requests[1].resolve({});
+            assert.equal(MockRemoteAPI.requests[2].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[2].url, MockData.pendingBuildsUrl('some builder 2'));
+            MockRemoteAPI.requests[2].resolve({});
+            MockRemoteAPI.reset();
+            await MockRemoteAPI.waitForRequest();
+
+            assert.equal(MockRemoteAPI.requests.length, 3);
+            assert.equal(MockRemoteAPI.requests[0].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[0].url, MockData.recentBuildsUrl('some tester', 2));
+            MockRemoteAPI.requests[0].resolve({});
+            assert.equal(MockRemoteAPI.requests[1].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[1].url, MockData.recentBuildsUrl('some-builder-1', 2));
+            MockRemoteAPI.requests[1].resolve({'builds': [MockData.runningBuildData({buildRequestId: 801}), MockData.finishedBuildData({buildRequestId: 800})]});
+            assert.equal(MockRemoteAPI.requests[2].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[2].url, MockData.recentBuildsUrl('some builder 2', 2));
+            MockRemoteAPI.requests[2].resolve({});
+            MockRemoteAPI.reset();
+            await MockRemoteAPI.waitForRequest();
+
+            assert.equal(MockRemoteAPI.requests.length, 3);
+            assert.equal(MockRemoteAPI.requests[0].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[0].url, MockData.pendingBuildsUrl('some tester'));
+            MockRemoteAPI.requests[0].resolve({});
+            assert.equal(MockRemoteAPI.requests[1].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[1].url, MockData.pendingBuildsUrl('some-builder-1'));
+            MockRemoteAPI.requests[1].resolve({});
+            assert.equal(MockRemoteAPI.requests[2].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[2].url, MockData.pendingBuildsUrl('some builder 2'));
+            MockRemoteAPI.requests[2].resolve({});
+            MockRemoteAPI.reset();
+            await MockRemoteAPI.waitForRequest();
+
+            assert.equal(MockRemoteAPI.requests.length, 3);
+            assert.equal(MockRemoteAPI.requests[0].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[0].url, MockData.recentBuildsUrl('some tester', 2));
+            MockRemoteAPI.requests[0].resolve({});
+            assert.equal(MockRemoteAPI.requests[1].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[1].url, MockData.recentBuildsUrl('some-builder-1', 2));
+            MockRemoteAPI.requests[1].resolve({'builds': [MockData.runningBuildData({buildRequestId: 801}), MockData.finishedBuildData({buildRequestId: 800})]});
+            assert.equal(MockRemoteAPI.requests[2].method, 'GET');
+            assert.equal(MockRemoteAPI.requests[2].url, MockData.recentBuildsUrl('some builder 2', 2));
+            MockRemoteAPI.requests[2].resolve({'builds': [MockData.runningBuildData({buildRequestId: 900, builderId: 3})]});
+            MockRemoteAPI.reset();
+
+            await syncPromise;
+            await BuildRequest.fetchForTriggerable(MockData.mockTestSyncConfigWithPatchAcceptingBuilder().triggerableName);
+            assert.equal(BuildRequest.all().length, 8);
+            buildRequest = BuildRequest.findById(800);
+            anotherBuildRequest = BuildRequest.findById(900);
+            assert.equal(buildRequest.status(), 'completed');
+            assert.equal(anotherBuildRequest.status(), 'running');
+            assert.equal(buildRequest.statusUrl(), 'http://build.webkit.org/buids/1');
+            assert.equal(anotherBuildRequest.statusUrl(), 'http://build.webkit.org/#/builders/3/builds/124');
+        });
     });
 
     describe('updateTriggerables', () => {

Modified: trunk/Websites/perf.webkit.org/tools/js/buildbot-triggerable.js (269870 => 269871)


--- trunk/Websites/perf.webkit.org/tools/js/buildbot-triggerable.js	2020-11-16 20:25:21 UTC (rev 269870)
+++ trunk/Websites/perf.webkit.org/tools/js/buildbot-triggerable.js	2020-11-16 20:32:08 UTC (rev 269871)
@@ -77,42 +77,56 @@
             })});
     }
 
-    syncOnce()
+    async syncOnce()
     {
-        let syncerList = this._syncers;
-        let buildReqeustsByGroup = new Map;
+        this._logger.log(`Fetching build requests for ${this._name}...`);
 
-        this._logger.log(`Fetching build requests for ${this._name}...`);
-        let validRequests;
-        return BuildRequest.fetchForTriggerable(this._name).then((buildRequests) => {
-            validRequests = this._validateRequests(buildRequests);
-            buildReqeustsByGroup = BuildbotTriggerable._testGroupMapForBuildRequests(buildRequests);
-            return this._pullBuildbotOnAllSyncers(buildReqeustsByGroup);
-        }).then((updates) => {
-            this._logger.log('Scheduling builds');
-            const promistList = [];
-            const testGroupList = Array.from(buildReqeustsByGroup.values()).sort(function (a, b) { return a.groupOrder - b.groupOrder; });
-            for (const group of testGroupList) {
-                const nextRequest = this._nextRequestInGroup(group, updates);
-                if (!validRequests.has(nextRequest))
-                    continue;
-                const promise = this._scheduleRequestIfSlaveIsAvailable(nextRequest, group.requests,
-                    nextRequest.isBuild() ? group.buildSyncer : group.testSyncer,
-                    nextRequest.isBuild() ? group.buildSlaveName : group.testSlaveName);
-                if (promise)
-                    promistList.push(promise);
+        const buildRequests = await BuildRequest.fetchForTriggerable(this._name);
+        const validRequests = this._validateRequests(buildRequests);
+        const buildReqeustsByGroup = BuildbotTriggerable._testGroupMapForBuildRequests(buildRequests);
+        let updates = await this._pullBuildbotOnAllSyncers(buildReqeustsByGroup);
+        let rootReuseUpdates = {}
+        this._logger.log('Scheduling builds');
+        const promiseList = [];
+        const testGroupList = Array.from(buildReqeustsByGroup.values()).sort(function (a, b) { return a.groupOrder - b.groupOrder; });
+
+        await Promise.all(testGroupList.map((group) => [group, this._nextRequestInGroup(group, updates)])
+            .filter(([group, request]) => validRequests.has(request))
+            .map(([group, request]) => this._scheduleRequest(group, request, rootReuseUpdates)));
+
+        // Pull all buildbots for the second time since the previous step may have scheduled more builds
+        updates = await this._pullBuildbotOnAllSyncers(buildReqeustsByGroup);
+
+        // rootReuseUpdates will be overridden by status fetched from buildbot.
+        updates = {
+            ...rootReuseUpdates,
+            ...updates
+        };
+        return await this._remote.postJSONWithStatus(`/api/build-requests/${this._name}`, {
+            'slaveName': this._slaveInfo.name,
+            'slavePassword': this._slaveInfo.password,
+            'buildRequestUpdates': updates});
+    }
+
+    async _scheduleRequest(testGroup, buildRequest, updates)
+    {
+        const buildRequestForRootReuse = await buildRequest.findBuildRequestWithSameRoots();
+        if (buildRequestForRootReuse) {
+            if (!buildRequestForRootReuse.hasCompleted()) {
+                this._logger.log(`Found build request ${buildRequestForRootReuse.id()} is building the same root, will wait until it finishes.`);
+                return;
             }
-            return Promise.all(promistList);
-        }).then(() => {
-            // Pull all buildbots for the second time since the previous step may have scheduled more builds.
-            return this._pullBuildbotOnAllSyncers(buildReqeustsByGroup);
-        }).then((updates) => {
-            // FIXME: Add a new API that just updates the requests.
-            return this._remote.postJSONWithStatus(`/api/build-requests/${this._name}`, {
-                'slaveName': this._slaveInfo.name,
-                'slavePassword': this._slaveInfo.password,
-                'buildRequestUpdates': updates});
-        });
+
+            this._logger.log(`Will reuse existing root built from ${buildRequestForRootReuse.id()} for ${buildRequest.id()}`);
+            updates[buildRequest.id()] = {status: 'completed', url: buildRequestForRootReuse.statusUrl(),
+                statusDescription: buildRequestForRootReuse.statusDescription(),
+                buildRequestForRootReuse: buildRequestForRootReuse.id()};
+            return;
+        }
+
+        return await this._scheduleRequestIfSlaveIsAvailable(buildRequest, testGroup.requests,
+            buildRequest.isBuild() ? testGroup.buildSyncer : testGroup.testSyncer,
+            buildRequest.isBuild() ? testGroup.buildSlaveName : testGroup.testSlaveName);
     }
 
     _validateRequests(buildRequests)

Modified: trunk/Websites/perf.webkit.org/unit-tests/build-request-tests.js (269870 => 269871)


--- trunk/Websites/perf.webkit.org/unit-tests/build-request-tests.js	2020-11-16 20:25:21 UTC (rev 269870)
+++ trunk/Websites/perf.webkit.org/unit-tests/build-request-tests.js	2020-11-16 20:32:08 UTC (rev 269871)
@@ -3,7 +3,9 @@
 const assert = require('assert');
 
 require('../tools/js/v3-models.js');
-let MockModels = require('./resources/mock-v3-models.js').MockModels;
+const MockModels = require('./resources/mock-v3-models.js').MockModels;
+const NodePrivilegedAPI = require('../tools/js/privileged-api.js').PrivilegedAPI;
+const MockRemoteAPI = require('./resources/mock-remote-api.js').MockRemoteAPI;
 
 function sampleBuildRequestData()
 {
@@ -56,9 +58,531 @@
     };
 }
 
+function oneTestGroup()
+{
+    return {
+        "testGroups": [{
+            "id": "2128",
+            "task": "1376",
+            "platform": "31",
+            "name": "Confirm",
+            "author": "rniwa",
+            "createdAt": 1458688514000,
+            "hidden": false,
+            "needsNotification": true,
+            "buildRequests": ["16985", "16986", "16987", "16988"],
+            "commitSets": ["4255", "4256"],
+            "notificationSentAt": null,
+            'initialRepetitionCount': 1
+        }],
+        "buildRequests": [{
+            "id": "16985",
+            "triggerable": "3",
+            "task": "1376",
+            "test": "844",
+            "platform": "31",
+            "testGroup": "2128",
+            "order": "-2",
+            "commitSet": "4255",
+            "status": "pending",
+            "url": null,
+            "build": null,
+            "createdAt": 1458688514000
+        }, {
+            "id": "16986",
+            "triggerable": "3",
+            "task": "1376",
+            "test": "844",
+            "platform": "31",
+            "testGroup": "2128",
+            "order": "-1",
+            "commitSet": "4256",
+            "status": "pending",
+            "url": null,
+            "build": null,
+            "createdAt": 1458688514000
+        }, {
+            "id": "16987",
+            "triggerable": "3",
+            "task": "1376",
+            "test": "844",
+            "platform": "31",
+            "testGroup": "2128",
+            "order": "0",
+            "commitSet": "4255",
+            "status": "pending",
+            "url": null,
+            "build": null,
+            "createdAt": 1458688514000
+        }, {
+            "id": "16988",
+            "triggerable": "3",
+            "task": "1376",
+            "test": "844",
+            "platform": "31",
+            "testGroup": "2128",
+            "order": "3",
+            "commitSet": "4256",
+            "status": "pending",
+            "url": null,
+            "build": null,
+            "createdAt": 1458688514000
+        }],
+        "commitSets": [{
+            "id": "4255",
+            "revisionItems": [{"commit": "87832"}, {"commit": "93116"}],
+            "customRoots": [],
+        }, {
+            "id": "4256",
+            "revisionItems": [{"commit": "87832"}, {"commit": "96336"}],
+            "customRoots": [],
+        }],
+        "commits": [{
+            "id": "87832",
+            "repository": "9",
+            "revision": "10.11 15A284",
+            "time": 0
+        }, {
+            "id": "93116",
+            "repository": "11",
+            "revision": "191622",
+            "time": 1445945816878
+        }, {
+            "id": "87832",
+            "repository": "9",
+            "revision": "10.11 15A284",
+            "time": 0
+        }, {
+            "id": "96336",
+            "repository": "11",
+            "revision": "192736",
+            "time": 1448225325650
+        }],
+        "uploadedFiles": [],
+        "status": "OK"
+    };
+}
+
+function threeTestGroups(secondTestGroupOverrides, thirdTestGroupOverrides)
+{
+    if (!secondTestGroupOverrides)
+        secondTestGroupOverrides = {};
+    if (!thirdTestGroupOverrides)
+        thirdTestGroupOverrides = {};
+    return {
+        "testGroups": [{
+            "id": "2128",
+            "task": "1376",
+            "platform": "32",
+            "name": "Confirm",
+            "author": "rniwa",
+            "createdAt": 1458688514000,
+            "hidden": false,
+            "needsNotification": true,
+            "buildRequests": ["16985", "16986", "16987", "16988"],
+            "commitSets": ["4255", "4256"],
+            "notificationSentAt": null,
+            'initialRepetitionCount': 1
+        }, {
+            "id": "2129",
+            "task": secondTestGroupOverrides.task || '1376',
+            "platform": secondTestGroupOverrides.platform || '32',
+            "name": "Confirm",
+            "author": "rniwa",
+            "createdAt": 1458688514000,
+            "hidden": false,
+            "needsNotification": true,
+            "buildRequests": ["16989", "16990", "16991", "16992"],
+            "commitSets": ["4255", "4256"],
+            "notificationSentAt": null,
+            'initialRepetitionCount': 1
+        }, {
+            "id": "2130",
+            "task": thirdTestGroupOverrides.task || '1376',
+            "platform": thirdTestGroupOverrides.platform || '32',
+            "name": "Confirm",
+            "author": "rniwa",
+            "createdAt": 1458688514000,
+            "hidden": false,
+            "needsNotification": true,
+            "buildRequests": ["16993", "16994", "16995", "16996"],
+            "commitSets": ["4255", "4256"],
+            "notificationSentAt": null,
+            'initialRepetitionCount': 1
+        }],
+        "buildRequests": [{
+            "id": "16985",
+            "triggerable": "3",
+            "task": "1376",
+            "test": "844",
+            "platform": "32",
+            "testGroup": "2128",
+            "order": "-2",
+            "commitSet": "4255",
+            "status": "pending",
+            "url": null,
+            "build": null,
+            "createdAt": 1458688514000
+        }, {
+            "id": "16986",
+            "triggerable": "3",
+            "task": "1376",
+            "test": "844",
+            "platform": "32",
+            "testGroup": "2128",
+            "order": "-1",
+            "commitSet": "4256",
+            "status": "pending",
+            "url": null,
+            "build": null,
+            "createdAt": 1458688514000
+        }, {
+            "id": "16987",
+            "triggerable": "3",
+            "task": "1376",
+            "test": "844",
+            "platform": "32",
+            "testGroup": "2128",
+            "order": "0",
+            "commitSet": "4255",
+            "status": "pending",
+            "url": null,
+            "build": null,
+            "createdAt": 1458688514000
+        }, {
+            "id": "16988",
+            "triggerable": "3",
+            "task": "1376",
+            "test": "844",
+            "platform": "32",
+            "testGroup": "2128",
+            "order": "1",
+            "commitSet": "4256",
+            "status": "pending",
+            "url": null,
+            "build": null,
+            "createdAt": 1458688514000
+        }, {
+            "id": "16989",
+            "triggerable": "3",
+            "task": secondTestGroupOverrides.task || '1376',
+            "test": "844",
+            "platform": secondTestGroupOverrides.platform || '32',
+            "testGroup": "2129",
+            "order": "-2",
+            "commitSet": "4255",
+            "status": secondTestGroupOverrides.status && secondTestGroupOverrides.status[0] || 'pending',
+            "url": null,
+            "build": null,
+            "createdAt": 1458688514000
+        }, {
+            "id": "16990",
+            "triggerable": "3",
+            "task": secondTestGroupOverrides.task || '1376',
+            "test": "844",
+            "platform": secondTestGroupOverrides.platform || '32',
+            "testGroup": "2129",
+            "order": "-1",
+            "commitSet": "4256",
+            "status": secondTestGroupOverrides.status && secondTestGroupOverrides.status[1] || 'pending',
+            "url": null,
+            "build": null,
+            "createdAt": 1458688514000
+        }, {
+            "id": "16991",
+            "triggerable": "3",
+            "task": secondTestGroupOverrides.task || '1376',
+            "test": "844",
+            "platform": secondTestGroupOverrides.platform || '32',
+            "testGroup": "2129",
+            "order": "0",
+            "commitSet": "4255",
+            "status": secondTestGroupOverrides.status && secondTestGroupOverrides.status[2] || 'pending',
+            "url": null,
+            "build": null,
+            "createdAt": 1458688514000
+        }, {
+            "id": "16992",
+            "triggerable": "3",
+            "task": secondTestGroupOverrides.task || '1376',
+            "test": "844",
+            "platform": secondTestGroupOverrides.platform || '32',
+            "testGroup": "2129",
+            "order": "3",
+            "commitSet": "4256",
+            "status": secondTestGroupOverrides.status && secondTestGroupOverrides.status[3] || 'pending',
+            "url": null,
+            "build": null,
+            "createdAt": 1458688514000
+        }, {
+            "id": "16993",
+            "triggerable": "3",
+            "task": thirdTestGroupOverrides.task || '1376',
+            "test": "844",
+            "platform": thirdTestGroupOverrides.platform || '32',
+            "testGroup": "2130",
+            "order": "-2",
+            "commitSet": "4255",
+            "status": thirdTestGroupOverrides.status && thirdTestGroupOverrides.status[0] || 'pending',
+            "url": null,
+            "build": null,
+            "createdAt": 1458688513000
+        }, {
+            "id": "16994",
+            "triggerable": "3",
+            "task": thirdTestGroupOverrides.task || '1376',
+            "test": "844",
+            "platform": thirdTestGroupOverrides.platform || '32',
+            "testGroup": "2130",
+            "order": "-1",
+            "commitSet": "4256",
+            "status": thirdTestGroupOverrides.status && thirdTestGroupOverrides.status[1] || 'pending',
+            "url": null,
+            "build": null,
+            "createdAt": 1458688514000
+        }, {
+            "id": "16995",
+            "triggerable": "3",
+            "task": thirdTestGroupOverrides.task || '1376',
+            "test": "844",
+            "platform": thirdTestGroupOverrides.platform || '32',
+            "testGroup": "2130",
+            "order": "0",
+            "commitSet": "4255",
+            "status": thirdTestGroupOverrides.status && thirdTestGroupOverrides.status[2] || 'pending',
+            "url": null,
+            "build": null,
+            "createdAt": 1458688514000
+        }, {
+            "id": "16996",
+            "triggerable": "3",
+            "task": secondTestGroupOverrides.task || '1376',
+            "test": "844",
+            "platform": secondTestGroupOverrides.platform || '32',
+            "testGroup": "2130",
+            "order": "1",
+            "commitSet": "4256",
+            "status": thirdTestGroupOverrides.status && thirdTestGroupOverrides.status[3] || 'pending',
+            "url": null,
+            "build": null,
+            "createdAt": 1458688514000
+        }],
+        "commitSets": [{
+            "id": "4255",
+            "revisionItems": [{"commit": "87832"}, {"commit": "93116"}],
+            "customRoots": [],
+        }, {
+            "id": "4256",
+            "revisionItems": [{"commit": "87832"}, {"commit": "96336"}],
+            "customRoots": [],
+        }],
+        "commits": [{
+            "id": "87832",
+            "repository": "9",
+            "revision": "10.11 15A284",
+            "time": 0
+        }, {
+            "id": "93116",
+            "repository": "11",
+            "revision": "191622",
+            "time": 1445945816878
+        }, {
+            "id": "87832",
+            "repository": "9",
+            "revision": "10.11 15A284",
+            "time": 0
+        }, {
+            "id": "96336",
+            "repository": "11",
+            "revision": "192736",
+            "time": 1448225325650
+        }],
+        "uploadedFiles": [],
+        "status": "OK"
+    };
+}
+
 describe('BuildRequest', function () {
     MockModels.inject();
 
+    describe('findBuildRequestWithSameRoots', () => {
+        const requests = MockRemoteAPI.inject('https://perf.webkit.org');
+
+        it('should return null when the build request is not build type build request', async () => {
+            const data = ""
+            const request = BuildRequest.constructBuildRequestsFromData(data)[0];
+            const result = await request.findBuildRequestWithSameRoots();
+            assert.equal(result, null);
+        });
+
+        it('should return null if this there is no other test group under current analysis task', async () => {
+            const data = ""
+            const platformId = data.buildRequests[0].platform;
+            Platform.ensureSingleton(platformId, {id: platformId, metrics: [], name: 'some platform'});
+            const request = BuildRequest.constructBuildRequestsFromData(data)[0];
+            const promise = request.findBuildRequestWithSameRoots();
+            assert.equal(requests.length, 1);
+
+            assert.equal(requests[0].url, '/api/test-groups?task=1376');
+            assert.equal(requests[0].method, 'GET');
+            requests[0].resolve(oneTestGroup());
+
+            const result = await promise;
+            assert.equal(result, null);
+        });
+
+        it('should return completed build request if a completed reusable build request found', async () => {
+            const overrides = {
+                task: '1376',
+                platform: '32',
+                status: ['completed', 'pending', 'pending', 'pending']
+            }
+            const data = ""
+            const platformId = data.buildRequests[0].platform;
+            Platform.ensureSingleton(platformId, {id: platformId, metrics: [], name: 'some platform'});
+            const request = BuildRequest.constructBuildRequestsFromData(data)[0];
+            const promise = request.findBuildRequestWithSameRoots();
+            assert.equal(requests.length, 1);
+
+            assert.equal(requests[0].url, '/api/test-groups?task=1376');
+            assert.equal(requests[0].method, 'GET');
+            requests[0].resolve(threeTestGroups(overrides));
+
+            const result = await promise;
+            assert.equal(result, BuildRequest.findById(16989))
+        });
+
+        it('should not use cache while fetching test groups under analysis task', async () => {
+            const overrides = {
+                task: '1376',
+                platform: '32',
+                status: ['completed', 'pending', 'pending', 'pending']
+            };
+            const data = ""
+            const platformId = data.buildRequests[0].platform;
+            Platform.ensureSingleton(platformId, {id: platformId, metrics: [], name: 'some platform'});
+            const request = BuildRequest.constructBuildRequestsFromData(data)[0];
+            let promise = request.findBuildRequestWithSameRoots();
+            assert.equal(requests.length, 1);
+
+            assert.equal(requests[0].url, '/api/test-groups?task=1376');
+            assert.equal(requests[0].method, 'GET');
+            requests[0].resolve(threeTestGroups(overrides));
+
+            let result = await promise;
+            assert.equal(result, BuildRequest.findById(16989))
+
+            MockRemoteAPI.reset();
+            promise = request.findBuildRequestWithSameRoots();
+            assert.equal(requests.length, 1);
+
+            assert.equal(requests[0].url, '/api/test-groups?task=1376');
+            assert.equal(requests[0].method, 'GET');
+            requests[0].resolve(threeTestGroups(overrides));
+
+            result = await promise;
+            assert.equal(result, BuildRequest.findById(16989))
+        });
+
+        it('should only allow identical platform if the platform is not under a platform group', async () => {
+            const overrides = {
+                task: '1376',
+                platform: '33',
+                status: ['completed', 'pending', 'pending', 'pending']
+            };
+            const data = ""
+            const platformId = data.buildRequests[0].platform;
+            Platform.ensureSingleton(platformId, {id: platformId, metrics: [], name: 'some platform'});
+            Platform.ensureSingleton('33', {id: '33', metrics: [], name: 'another platform'});
+            const request = BuildRequest.constructBuildRequestsFromData(data)[0];
+            const promise = request.findBuildRequestWithSameRoots();
+            assert.equal(requests.length, 1);
+
+            assert.equal(requests[0].url, '/api/test-groups?task=1376');
+            assert.equal(requests[0].method, 'GET');
+            requests[0].resolve(threeTestGroups(overrides));
+
+            const result = await promise;
+            assert.equal(result, null);
+        });
+
+        it('should allow different platform if the platforms are under the same platform group', async () => {
+            const overrides = {
+                task: '1376',
+                platform: '33',
+                status: ['completed', 'pending', 'pending', 'pending']
+            };
+            const data = ""
+            const platformId = data.buildRequests[0].platform;
+            const platformGroup = PlatformGroup.ensureSingleton('1', {id: 1, name: 'some platform group'});
+            Platform.ensureSingleton(platformId, {id: platformId, metrics: [], name: 'some platform', group: platformGroup});
+            Platform.ensureSingleton('33', {id: '33', metrics: [], name: 'another platform', group: platformGroup});
+            const request = BuildRequest.constructBuildRequestsFromData(data)[0];
+            const promise = request.findBuildRequestWithSameRoots();
+            assert.equal(requests.length, 1);
+
+            assert.equal(requests[0].url, '/api/test-groups?task=1376');
+            assert.equal(requests[0].method, 'GET');
+            requests[0].resolve(threeTestGroups(overrides));
+
+            const result = await promise;
+            assert.equal(result, BuildRequest.findById(16989))
+        });
+
+        it('should in favor of running build request over scheduled build request', async () => {
+            const secondOverrides = {
+                task: '1376',
+                platform: '32',
+                status: ['scheduled', 'pending', 'pending', 'pending']
+            };
+            const thirdOverrides = {
+                task: '1376',
+                platform: '32',
+                status: ['running', 'pending', 'pending', 'pending']
+            }
+            const data = "" thirdOverrides);
+            const platformId = data.buildRequests[0].platform;
+            Platform.ensureSingleton(platformId, {id: platformId, metrics: [], name: 'some platform'});
+            const request = BuildRequest.constructBuildRequestsFromData(data)[0];
+            const promise = request.findBuildRequestWithSameRoots();
+            assert.equal(requests.length, 1);
+
+            assert.equal(requests[0].url, '/api/test-groups?task=1376');
+            assert.equal(requests[0].method, 'GET');
+            requests[0].resolve(threeTestGroups(secondOverrides, thirdOverrides));
+
+            const result = await promise;
+            assert.equal(result, BuildRequest.findById(16993))
+        });
+
+        it('should in favor of running build request which has earlier creation time', async () => {
+            const secondOverrides = {
+                task: '1376',
+                platform: '32',
+                status: ['running', 'pending', 'pending', 'pending']
+            };
+            const thirdOverrides = {
+                task: '1376',
+                platform: '32',
+                status: ['running', 'pending', 'pending', 'pending']
+            }
+            const data = "" thirdOverrides);
+            const platformId = data.buildRequests[0].platform;
+            Platform.ensureSingleton(platformId, {id: platformId, metrics: [], name: 'some platform'});
+            const request = BuildRequest.constructBuildRequestsFromData(data)[0];
+            const promise = request.findBuildRequestWithSameRoots();
+            assert.equal(requests.length, 1);
+
+            assert.equal(requests[0].url, '/api/test-groups?task=1376');
+            assert.equal(requests[0].method, 'GET');
+            requests[0].resolve(threeTestGroups(secondOverrides, thirdOverrides));
+
+            const result = await promise;
+            assert.equal(result, BuildRequest.findById(16993));
+            assert.ok(BuildRequest.findById(16993).createdAt() < BuildRequest.findById(16989).createdAt());
+        });
+    });
+
     describe('waitingTime', function () {
         it('should return "0 minutes" when the reference time is identically equal to createdAt', function () {
             const data = ""
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to