Add API to upload a patched build for a custom A/B testing
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 11 May 2017 02:32:00 +0000 (02:32 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 11 May 2017 02:32:00 +0000 (02:32 +0000)
https://bugs.webkit.org/show_bug.cgi?id=171956

Reviewed by Chris Dumez.

Added /api/upload-root to upload a root file, the build product of a patch associated with a commit set.

Extracted more functions out of privileged-api/upload-file.php to uploaded-file-helpers.php to share code
with /api/upload-root.php.

* public/api/upload-root.php: Added.
(main):
(compute_commit_set_items_to_update): Find the list of commit set items to associate this root with.
A root can be associated with multiple repositories and there fore commit set items; e.g. if a software
is built from multiple repositories and there is a patch associated with one of them, the built product
must be associated with all those repositories.

* public/include/build-requests-fetcher.php:
(BuildRequestsFetcher::fetch_commits_for_set_if_needed): Include the root file is there is one.

* public/include/json-header.php:
(validate_arguments): Added the support for validating json string.
(verify_slave): Return the slave ID found.

* public/include/uploaded-file-helpers.php:
(validate_uploaded_file): Extracted from /privileged-api/upload-file to be shared with /api/upload-root.
(query_total_file_size): Ditto.
(create_uploaded_file_from_form_data): Ditto.
(upload_file_in_transaction): Ditto. Takes a lambda to do the extra work inside the transaction.

* public/privileged-api/upload-file.php:
(main):

* public/v3/models/build-request.js:
(BuildRequest.constructBuildRequestsFromData): Resolve the rootFIle of each commit set item.

* public/v3/models/commit-set.js:
(CommitSet): Added _repositoryToRootMap and _allRootFiles as instance variables.
(CommitSet.prototype.updateSingleton): Added. Previously, each commit set's states never changed after
its creation. After this patch, each item can be newly associated with a root so we must update its
_repositoryToRootMap and _allRootFiles. For simplicity, we update all states.
(CommitSet.prototype._updateFromObject): Extracted from the constructor.
(CommitSet.prototype.allRootFiles): Added. Includes custom roots and roots created for patches.
(CommitSet.prototype.rootForRepository): Added.
(CommitSet.prototype.equals): Fixed the bug that we were comparing _repositoryToPatchMap to
_repositoryToCommitMap, and added a check for _repositoryToRootMap.

* public/v3/models/test-group.js:
(TestGroup.prototype.task): Added.
(TestGroup.createWithTask):
(TestGroup.createWithCustomConfiguration):
(TestGroup.createAndRefetchTestGroups):
(TestGroup._fetchTestGroupsForTask): Deleted. Now fetchForTask takes a boolean argument: ignoreCache.
(TestGroup.findAllByTask): Added.
(TestGroup.fetchForTask): Renamed from fetchByTask.

* public/v3/pages/analysis-task-page.js:
(AnalysisTaskPage.prototype._fetchRelatedInfoForTaskId):

* server-tests/api-build-requests-tests.js:

* server-tests/api-upload-root-tests.js: Added. Added tests for /api/upload-root.
(makeReport): Added.
(addSlaveAndCreateRootFile): Added.
(createTestGroupWihPatch): Added.

* server-tests/privileged-api-create-test-group-tests.js:

* server-tests/resources/mock-data.js:
(MockData.sharedRepositoryId): Added.
(MockData.addMockData): Added "Shared" repository along with commits.

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

14 files changed:
Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/public/api/upload-root.php [new file with mode: 0644]
Websites/perf.webkit.org/public/include/build-requests-fetcher.php
Websites/perf.webkit.org/public/include/json-header.php
Websites/perf.webkit.org/public/include/uploaded-file-helpers.php
Websites/perf.webkit.org/public/privileged-api/upload-file.php
Websites/perf.webkit.org/public/v3/models/build-request.js
Websites/perf.webkit.org/public/v3/models/commit-set.js
Websites/perf.webkit.org/public/v3/models/test-group.js
Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js
Websites/perf.webkit.org/server-tests/api-build-requests-tests.js
Websites/perf.webkit.org/server-tests/api-upload-root-tests.js [new file with mode: 0644]
Websites/perf.webkit.org/server-tests/privileged-api-create-test-group-tests.js
Websites/perf.webkit.org/server-tests/resources/mock-data.js

index 8744131..7e68ae8 100644 (file)
@@ -1,5 +1,79 @@
 2017-05-10  Ryosuke Niwa  <rniwa@webkit.org>
 
+        Add API to upload a patched build for a custom A/B testing
+        https://bugs.webkit.org/show_bug.cgi?id=171956
+
+        Reviewed by Chris Dumez.
+
+        Added /api/upload-root to upload a root file, the build product of a patch associated with a commit set.
+
+        Extracted more functions out of privileged-api/upload-file.php to uploaded-file-helpers.php to share code
+        with /api/upload-root.php.
+
+        * public/api/upload-root.php: Added.
+        (main):
+        (compute_commit_set_items_to_update): Find the list of commit set items to associate this root with.
+        A root can be associated with multiple repositories and there fore commit set items; e.g. if a software
+        is built from multiple repositories and there is a patch associated with one of them, the built product
+        must be associated with all those repositories.
+
+        * public/include/build-requests-fetcher.php:
+        (BuildRequestsFetcher::fetch_commits_for_set_if_needed): Include the root file is there is one.
+
+        * public/include/json-header.php:
+        (validate_arguments): Added the support for validating json string.
+        (verify_slave): Return the slave ID found.
+
+        * public/include/uploaded-file-helpers.php:
+        (validate_uploaded_file): Extracted from /privileged-api/upload-file to be shared with /api/upload-root.
+        (query_total_file_size): Ditto.
+        (create_uploaded_file_from_form_data): Ditto.
+        (upload_file_in_transaction): Ditto. Takes a lambda to do the extra work inside the transaction.
+
+        * public/privileged-api/upload-file.php:
+        (main):
+
+        * public/v3/models/build-request.js:
+        (BuildRequest.constructBuildRequestsFromData): Resolve the rootFIle of each commit set item.
+
+        * public/v3/models/commit-set.js:
+        (CommitSet): Added _repositoryToRootMap and _allRootFiles as instance variables.
+        (CommitSet.prototype.updateSingleton): Added. Previously, each commit set's states never changed after
+        its creation. After this patch, each item can be newly associated with a root so we must update its
+        _repositoryToRootMap and _allRootFiles. For simplicity, we update all states.
+        (CommitSet.prototype._updateFromObject): Extracted from the constructor.
+        (CommitSet.prototype.allRootFiles): Added. Includes custom roots and roots created for patches.
+        (CommitSet.prototype.rootForRepository): Added.
+        (CommitSet.prototype.equals): Fixed the bug that we were comparing _repositoryToPatchMap to
+        _repositoryToCommitMap, and added a check for _repositoryToRootMap.
+
+        * public/v3/models/test-group.js:
+        (TestGroup.prototype.task): Added.
+        (TestGroup.createWithTask):
+        (TestGroup.createWithCustomConfiguration):
+        (TestGroup.createAndRefetchTestGroups):
+        (TestGroup._fetchTestGroupsForTask): Deleted. Now fetchForTask takes a boolean argument: ignoreCache.
+        (TestGroup.findAllByTask): Added.
+        (TestGroup.fetchForTask): Renamed from fetchByTask.
+
+        * public/v3/pages/analysis-task-page.js:
+        (AnalysisTaskPage.prototype._fetchRelatedInfoForTaskId):
+
+        * server-tests/api-build-requests-tests.js:
+
+        * server-tests/api-upload-root-tests.js: Added. Added tests for /api/upload-root.
+        (makeReport): Added.
+        (addSlaveAndCreateRootFile): Added.
+        (createTestGroupWihPatch): Added.
+
+        * server-tests/privileged-api-create-test-group-tests.js:
+
+        * server-tests/resources/mock-data.js:
+        (MockData.sharedRepositoryId): Added.
+        (MockData.addMockData): Added "Shared" repository along with commits.
+
+2017-05-10  Ryosuke Niwa  <rniwa@webkit.org>
+
         Rename server-tests/api-update-triggerable.js to server-tests/api-update-triggerable-tests.js
         https://bugs.webkit.org/show_bug.cgi?id=171905
 
diff --git a/Websites/perf.webkit.org/public/api/upload-root.php b/Websites/perf.webkit.org/public/api/upload-root.php
new file mode 100644 (file)
index 0000000..973e956
--- /dev/null
@@ -0,0 +1,87 @@
+<?php
+
+require_once('../include/json-header.php');
+require_once('../include/uploaded-file-helpers.php');
+
+function main()
+{
+    $input_file = validate_uploaded_file('rootFile');
+
+    $db = connect();
+    $slave_id = verify_slave($db, $_POST);
+
+    $arguments = validate_arguments($_POST, array(
+        'builderName' => '/.+/',
+        'buildNumber' => 'int',
+        'buildTime' => '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d*Z?$/',
+        'buildRequest' => 'int',
+        'repositoryList' => 'json',
+    ));
+    $build_request_id = $arguments['buildRequest'];
+
+    $request_row = $db->select_first_row('build_requests', 'request', array('id' => $build_request_id));
+    if ($request_row['request_test'] || $request_row['request_order'] >= 0)
+        exit_with_error('InvalidBuildRequestType', array('buildRequest' => $build_request_id));
+
+    $test_group = $db->select_first_row('analysis_test_groups', 'testgroup', array('id' => $request_row['request_group']));
+    assert($test_group);
+
+    $builder_id = $db->select_or_insert_row('builders', 'builder', array('name' => $arguments['builderName']));
+    $build_info = array('builder' => $builder_id, 'slave' => $slave_id, 'number' => $arguments['buildNumber'], 'time' => $arguments['buildTime']);
+
+    $commit_set_id = $request_row['request_commit_set'];
+    $commit_set_items_to_update = compute_commit_set_items_to_update($db, $commit_set_id, $arguments['repositoryList']);
+
+    $uploaded_file = upload_file_in_transaction($db, $input_file, $test_group['testgroup_author'],
+        function ($db, $file_row) use ($build_request_id, $build_info, $commit_set_id, $commit_set_items_to_update) {
+            $root_file_id = $file_row['file_id'];
+            // FIXME: Insert a build row and associated with the request.
+            $build_id = $db->insert_row('builds', 'build', $build_info);
+            if (!$build_id)
+                return array('status' => 'FailedToCreateBuild', 'build' => $build_info);
+            if (!$db->update_row('build_requests', 'request', array('id' => $build_request_id), array('status' => 'completed', 'build' => $build_id)))
+                return array('status' => 'FailedToUpdateBuildRequest', 'buildRequest' => $build_request_id);
+
+            foreach ($commit_set_items_to_update as $commit_id) {
+                if (!$db->update_row('commit_set_items', 'commitset',
+                    array('set' => $commit_set_id, 'commit' => $commit_id),
+                    array('commit' => $commit_id, 'root_file' => $root_file_id), '*'))
+                    return array('status' => 'FailedToUpdateCommitSet', 'commitSet' => $commit_set_id, 'commit' => $commit_id);
+            }
+            return NULL;
+        });
+
+    exit_with_success(array('uploadedFile' => $uploaded_file));
+}
+
+function compute_commit_set_items_to_update($db, $commit_set_id, $repository_name_list)
+{
+    if (!is_array($repository_name_list))
+        exit_with_error('InvalidRepositoryList', array('repositoryList' => $repository_name_list));
+
+    $commit_repository_rows_in_set = $db->query_and_fetch_all('SELECT * FROM repositories, commits, commit_set_items
+        WHERE repository_id = commit_repository AND commit_id = commitset_commit
+            AND commitset_set = $1', array($commit_set_id));
+
+    $commit_by_repository_name = array();
+    if ($commit_repository_rows_in_set) {
+        foreach ($commit_repository_rows_in_set as $row)
+            $commit_by_repository_name[$row['repository_name']] = $row['commitset_commit'];
+    }
+
+    $commit_set_items_to_update = array();
+    foreach ($repository_name_list as $repository_name) {
+        $commit_id = array_get($commit_by_repository_name, $repository_name);
+        if (!$commit_id)
+            exit_with_error('InvalidRepository', array('repositoryName' => $repository_name, 'commitSet' => $commit_set_id));
+        array_push($commit_set_items_to_update, $commit_id);
+    }
+    if (!$commit_set_items_to_update)
+        exit_with_error('InvalidRepositoryList', array('repositoryList' => $repository_list));
+
+    return $commit_set_items_to_update;
+}
+
+main();
+
+?>
index 971cbfd..76a96d4 100644 (file)
@@ -117,7 +117,12 @@ class BuildRequestsFetcher {
             $patch_file_id = $row['commitset_patch_file'];
             if ($patch_file_id)
                 $this->add_uploaded_file($patch_file_id);
-            array_push($revision_items, array('commit' => $row['commit_id'], 'patch' => $patch_file_id));
+
+            $root_file_id = $row['commitset_root_file'];
+            if ($root_file_id)
+                $this->add_uploaded_file($root_file_id);
+
+            array_push($revision_items, array('commit' => $row['commit_id'], 'patch' => $patch_file_id, 'rootFile' => $root_file_id));
 
             if (array_key_exists($commit_id, $this->commits_by_id))
                 continue;
index c1c8dca..9795c65 100644 (file)
@@ -72,6 +72,10 @@ function validate_arguments($array, $list_of_arguments) {
         } else if ($pattern == 'int?') {
             require_format($name, $value, '/^\d*$/');
             $value = $value ? intval($value) : null;
+        } else if ($pattern == 'json') {
+            $value = json_decode($value, true);
+            if ($value === NULL)
+                exit_with_error('Invalid' . camel_case_words_separated_by_underscore($name));
         } else
             require_format($name, $value, $pattern);
         $result[$name] = $value;
@@ -160,6 +164,8 @@ function verify_slave($db, $params) {
     $matched_slave = $db->select_first_row('build_slaves', 'slave', $slave_info);
     if (!$matched_slave)
         exit_with_error('SlaveNotFound', array('name' => $slave_info['name']));
+
+    return $matched_slave['slave_id'];
 }
 
 function find_triggerable_for_task($db, $task_id) {
index 24f4ca5..47a7bac 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 
+define('MEGABYTES', 1024 * 1024);
+
 function format_uploaded_file($file_row)
 {
     return array(
@@ -17,4 +19,102 @@ function uploaded_file_path_for_row($file_row)
     return config_path('uploadDirectory', $file_row['file_id'] . $file_row['file_extension']);
 }
 
+function validate_uploaded_file($field_name)
+{
+    if (array_get($_SERVER, 'CONTENT_LENGTH') && empty($_POST) && empty($_FILES))
+        exit_with_error('FileSizeLimitExceeded');
+
+    if (!is_dir(config_path('uploadDirectory', '')))
+        exit_with_error('NotSupported');
+
+    $input_file = array_get($_FILES, $field_name);
+    if (!$input_file)
+        exit_with_error('NoFileSpecified');
+
+    if ($input_file['error'] == UPLOAD_ERR_INI_SIZE || $input_file['error'] == UPLOAD_ERR_FORM_SIZE)
+        exit_with_error('FileSizeLimitExceeded');
+
+    if ($input_file['error'] != UPLOAD_ERR_OK)
+        exit_with_error('FailedToUploadFile', array('name' => $input_file['name'], 'error' => $input_file['error']));
+
+    if (config('uploadFileLimitInMB') * MEGABYTES < $input_file['size'])
+        exit_with_error('FileSizeLimitExceeded');
+
+    return $input_file;
+}
+
+function query_total_file_size($db, $user)
+{
+    if ($user)
+        $count_result = $db->query_and_fetch_all('SELECT sum(file_size) as "sum" FROM uploaded_files WHERE file_deleted_at IS NULL AND file_author = $1', array($user));
+    else
+        $count_result = $db->query_and_fetch_all('SELECT sum(file_size) as "sum" FROM uploaded_files WHERE file_deleted_at IS NULL AND file_author IS NULL');
+    if (!$count_result)
+        return FALSE;
+    return intval($count_result[0]["sum"]);
+}
+
+function create_uploaded_file_from_form_data($input_file)
+{
+    $file_sha256 = hash_file('sha256', $input_file['tmp_name']);
+    if (!$file_sha256)
+        exit_with_error('FailedToComputeSHA256');
+
+    $matches = array();
+    $file_extension = null;
+    if (preg_match('/(\.[a-zA-Z0-9]{1,5}){1,2}$/', $input_file['name'], $matches)) {
+        $file_extension = $matches[0];
+        assert(strlen($file_extension) <= 16);
+    }
+
+    return array(
+        'author' => remote_user_name(),
+        'filename' => $input_file['name'],
+        'extension' => $file_extension,
+        'mime' => $input_file['type'], // Sanitize MIME types.
+        'size' => $input_file['size'],
+        'sha256' => $file_sha256
+    );
+}
+
+function upload_file_in_transaction($db, $input_file, $remote_user, $additional_work = NULL)
+{
+    // FIXME: Cleanup old files.
+
+    if (config('uploadUserQuotaInMB') * MEGABYTES - query_total_file_size($db, $remote_user) < $input_file['size'])
+        exit_with_error('FileSizeQuotaExceeded');
+
+    $uploaded_file = create_uploaded_file_from_form_data($input_file);
+
+    $db->begin_transaction();
+    $file_row = $db->select_or_insert_row('uploaded_files', 'file',
+        array('sha256' => $uploaded_file['sha256'], 'deleted_at' => null), $uploaded_file, '*');
+    if (!$file_row)
+        exit_with_error('FailedToInsertFileData');
+
+    // A concurrent session may have inserted another file.
+    if (config('uploadUserQuotaInMB') * MEGABYTES < query_total_file_size($db, $remote_user)) {
+        $db->rollback_transaction();
+        exit_with_error('FileSizeQuotaExceeded');
+    }
+
+    if ($additional_work) {
+        $error = $additional_work($db, $file_row);
+        if ($error) {
+            $db->rollback_transaction();
+            exit_with_error($error['status'], $error);
+        }
+    }
+
+    $new_path = uploaded_file_path_for_row($file_row);
+    if (!move_uploaded_file($input_file['tmp_name'], $new_path)) {
+        $db->rollback_transaction();
+        exit_with_error('FailedToMoveUploadedFile');
+    }
+
+    $db->commit_transaction();
+
+    return format_uploaded_file($file_row);
+}
+
 ?>
index ae1b607..5562de2 100644 (file)
 <?php
 
-ini_set('upload_max_filesize', '1025M');
-ini_set('post_max_size', '1025M');
 require_once('../include/json-header.php');
 require_once('../include/uploaded-file-helpers.php');
 
-define('MEGABYTES', 1024 * 1024);
-
 function main()
 {
-    if (array_get($_SERVER, 'CONTENT_LENGTH') && empty($_POST) && empty($_FILES))
-        exit_with_error('FileSizeLimitExceeded');
+    $input_file = validate_uploaded_file('newFile');
 
     if (!verify_token(array_get($_POST, 'token')))
         exit_with_error('InvalidToken');
 
-    if (!is_dir(config_path('uploadDirectory', '')))
-        exit_with_error('NotSupported');
-
-    $input_file = array_get($_FILES, 'newFile');
-    if (!$input_file)
-        exit_with_error('NoFileSpecified');
-
-    if ($input_file['error'] == UPLOAD_ERR_INI_SIZE || $input_file['error'] == UPLOAD_ERR_FORM_SIZE)
-        exit_with_error('FileSizeLimitExceeded');
-
-    if ($input_file['error'] != UPLOAD_ERR_OK)
-        exit_with_error('FailedToUploadFile', array('name' => $input_file['name'], 'error' => $input_file['error']));
-
-    if (config('uploadFileLimitInMB') * MEGABYTES < $input_file['size'])
-        exit_with_error('FileSizeLimitExceeded');
-
-    $uploaded_file = create_uploaded_file_from_form_data($input_file);
-
     $current_user = remote_user_name();
     $db = connect();
 
-    // FIXME: Cleanup old files.
-
-    if (config('uploadUserQuotaInMB') * MEGABYTES - query_total_file_size($db, $current_user) < $input_file['size'])
-        exit_with_error('FileSizeQuotaExceeded');
-
-    $db->begin_transaction();
-    $file_row = $db->select_or_insert_row('uploaded_files', 'file',
-        array('sha256' => $uploaded_file['sha256'], 'deleted_at' => null), $uploaded_file, '*');
-    if (!$file_row)
-        exit_with_error('FailedToInsertFileData');
-
-    // A concurrent session may have inserted another file.
-    if (config('uploadUserQuotaInMB') * MEGABYTES < query_total_file_size($db, $current_user)) {
-        $db->rollback_transaction();
-        exit_with_error('FileSizeQuotaExceeded');
-    }
-
-    $new_path = uploaded_file_path_for_row($file_row);
-    if (!move_uploaded_file($input_file['tmp_name'], $new_path)) {
-        $db->rollback_transaction();
-        exit_with_error('FailedToMoveUploadedFile');
-    }
-    $db->commit_transaction();
-
-    exit_with_success(array('uploadedFile' => format_uploaded_file($file_row)));
-}
-
-function query_total_file_size($db, $user)
-{
-    if ($user)
-        $count_result = $db->query_and_fetch_all('SELECT sum(file_size) as "sum" FROM uploaded_files WHERE file_deleted_at IS NULL AND file_author = $1', array($user));
-    else
-        $count_result = $db->query_and_fetch_all('SELECT sum(file_size) as "sum" FROM uploaded_files WHERE file_deleted_at IS NULL AND file_author IS NULL');
-    if (!$count_result)
-        return FALSE;
-    return intval($count_result[0]["sum"]);
-}
-
-function create_uploaded_file_from_form_data($input_file)
-{
-    $file_sha256 = hash_file('sha256', $input_file['tmp_name']);
-    if (!$file_sha256)
-        exit_with_error('FailedToComputeSHA256');
-
-    $matches = array();
-    $file_extension = null;
-    if (preg_match('/(\.[a-zA-Z0-9]{1,5}){1,2}$/', $input_file['name'], $matches)) {
-        $file_extension = $matches[0];
-        assert(strlen($file_extension) <= 16);
-    }
+    $uploaded_file = upload_file_in_transaction($db, $input_file, $current_user);
 
-    return array(
-        'author' => remote_user_name(),
-        'filename' => $input_file['name'],
-        'extension' => $file_extension,
-        'mime' => $input_file['type'], // Sanitize MIME types.
-        'size' => $input_file['size'],
-        'sha256' => $file_sha256
-    );
+    exit_with_success(array('uploadedFile' => $uploaded_file));
 }
 
 main();
index 3c7c9ca..97daac0 100644 (file)
@@ -135,6 +135,7 @@ class BuildRequest extends DataModelObject {
             for (const item of rawData.revisionItems) {
                 item.commit = CommitLog.findById(item.commit);
                 item.patch = item.patch ? UploadedFile.findById(item.patch) : null;
+                item.rootFile = item.rootFile ? UploadedFile.findById(item.rootFile) : null;
             }
             rawData.customRoots = rawData.customRoots.map((fileId) => UploadedFile.findById(fileId));
             return CommitSet.ensureSingleton(rawData.id, rawData);
index 2cd5832..a125f10 100644 (file)
@@ -8,26 +8,49 @@ class CommitSet extends DataModelObject {
         this._repositories = [];
         this._repositoryToCommitMap = new Map;
         this._repositoryToPatchMap = new Map;
+        this._repositoryToRootMap = new Map;
         this._latestCommitTime = null;
         this._customRoots = [];
+        this._allRootFiles = [];
 
         if (!object)
             return;
 
+        this._updateFromObject(object);
+    }
+
+    updateSingleton(object)
+    {
+        this._repositoryToCommitMap.clear();
+        this._repositoryToPatchMap.clear();
+        this._repositoryToRootMap.clear();
+        this._repositories = [];
+        this._updateFromObject(object);
+    }
+
+    _updateFromObject(object)
+    {
+        const rootFiles = new Set;
         for (const item of object.revisionItems) {
             const commit = item.commit;
             console.assert(commit instanceof CommitLog);
             console.assert(!item.patch || item.patch instanceof UploadedFile);
+            console.assert(!item.rootFile || item.rootFile instanceof UploadedFile);
             const repository = commit.repository();
             this._repositoryToCommitMap.set(repository, commit);
             this._repositoryToPatchMap.set(repository, item.patch);
+            this._repositoryToRootMap.set(repository, item.rootFile);
+            if (item.rootFile)
+                rootFiles.add(item.rootFile);
             this._repositories.push(commit.repository());
         }
         this._customRoots = object.customRoots;
+        this._allRootFiles = Array.from(rootFiles).concat(object.customRoots);
     }
 
     repositories() { return this._repositories; }
     customRoots() { return this._customRoots; }
+    allRootFiles() { return this._allRootFiles; }
     commitForRepository(repository) { return this._repositoryToCommitMap.get(repository); }
 
     revisionForRepository(repository)
@@ -37,6 +60,7 @@ class CommitSet extends DataModelObject {
     }
 
     patchForRepository(repository) { return this._repositoryToPatchMap.get(repository); }
+    rootForRepository(repository) { return this._repositoryToRootMap.get(repository); }
 
     // FIXME: This should return a Date object.
     latestCommitTime()
@@ -57,7 +81,9 @@ class CommitSet extends DataModelObject {
         for (const [repository, commit] of this._repositoryToCommitMap) {
             if (commit != other._repositoryToCommitMap.get(repository))
                 return false;
-            if (this._repositoryToPatchMap.get(repository) != other._repositoryToCommitMap.get(repository))
+            if (this._repositoryToPatchMap.get(repository) != other._repositoryToPatchMap.get(repository))
+                return false;
+            if (this._repositoryToRootMap.get(repository) != other._repositoryToRootMap.get(repository))
                 return false;
         }
         return CommitSet.areCustomRootsEqual(this._customRoots, other._customRoots);
index c622619..7452a85 100644 (file)
@@ -32,6 +32,7 @@ class TestGroup extends LabeledObject {
         this._isHidden = object.hidden;
     }
 
+    task() { return AnalysisTask.findById(this._taskId); }
     createdAt() { return this._createdAt; }
     isHidden() { return this._isHidden; }
     buildRequests() { return this._buildRequests; }
@@ -199,7 +200,7 @@ class TestGroup extends LabeledObject {
         return PrivilegedAPI.sendRequest('create-test-group', params).then((data) => {
             return AnalysisTask.fetchById(data['taskId']);
         }).then((task) => {
-            return this._fetchTestGroupsForTask(task.id()).then(() => task);
+            return this.fetchForTask(task.id()).then(() => task);
         });
     }
 
@@ -209,7 +210,7 @@ class TestGroup extends LabeledObject {
         const revisionSets = this._revisionSetsFromCommitSets(commitSets);
         const params = {task: task.id(), name: groupName, platform: platform.id(), test: test.id(), repetitionCount, revisionSets};
         return PrivilegedAPI.sendRequest('create-test-group', params).then((data) => {
-            return this._fetchTestGroupsForTask(task.id());
+            return this.fetchForTask(task.id(), true);
         });
     }
 
@@ -222,7 +223,7 @@ class TestGroup extends LabeledObject {
             name: name,
             repetitionCount: repetitionCount,
             revisionSets: revisionSets,
-        }).then((data) => this._fetchTestGroupsForTask(task.id()));
+        }).then((data) => this.fetchForTask(task.id(), true));
     }
 
     static _revisionSetsFromCommitSets(commitSets)
@@ -244,14 +245,14 @@ class TestGroup extends LabeledObject {
         });
     }
 
-    static _fetchTestGroupsForTask(taskId)
+    static findAllByTask(taskId)
     {
-        return this.cachedFetch('/api/test-groups', {task: taskId}, true).then((data) => this._createModelsFromFetchedTestGroups(data));
+        return TestGroup.all().filter((testGroup) => testGroup._taskId == taskId);
     }
 
-    static fetchByTask(taskId)
+    static fetchForTask(taskId, ignoreCache = false)
     {
-        return this.cachedFetch('/api/test-groups', {task: taskId}).then(this._createModelsFromFetchedTestGroups.bind(this));
+        return this.cachedFetch('/api/test-groups', {task: taskId}, ignoreCache).then(this._createModelsFromFetchedTestGroups.bind(this));
     }
 
     static _createModelsFromFetchedTestGroups(data)
index ee7fc8e..8129b28 100644 (file)
@@ -455,7 +455,7 @@ class AnalysisTaskPage extends PageWithHeading {
 
     _fetchRelatedInfoForTaskId(taskId)
     {
-        TestGroup.fetchByTask(taskId).then(this._didFetchTestGroups.bind(this));
+        TestGroup.fetchForTask(taskId).then(this._didFetchTestGroups.bind(this));
         AnalysisResults.fetch(taskId).then(this._didFetchAnalysisResults.bind(this));
         AnalysisTask.fetchRelatedTasks(taskId).then((relatedTasks) => {
             this._relatedTasks = relatedTasks;
index a0cbce3..ffdae84 100644 (file)
@@ -35,9 +35,11 @@ describe('/api/build-requests', function () {
 
             assert.equal(content['commitSets'].length, 2);
             assert.equal(content['commitSets'][0].id, 401);
-            assert.deepEqual(content['commitSets'][0].revisionItems, [{commit: '87832', patch: null}, {commit: '93116', patch: null}]);
+            assert.deepEqual(content['commitSets'][0].revisionItems,
+                [{commit: '87832', patch: null, rootFile: null}, {commit: '93116', patch: null, rootFile: null}]);
             assert.equal(content['commitSets'][1].id, 402);
-            assert.deepEqual(content['commitSets'][1].revisionItems, [{commit: '87832', patch: null}, {commit: '96336', patch: null}]);
+            assert.deepEqual(content['commitSets'][1].revisionItems,
+                [{commit: '87832', patch: null, rootFile: null}, {commit: '96336', patch: null, rootFile: null}]);
 
             assert.equal(content['commits'].length, 3);
             assert.equal(content['commits'][0].id, 87832);
@@ -90,10 +92,10 @@ describe('/api/build-requests', function () {
             assert.equal(content['commitSets'].length, 2);
             assert.equal(content['commitSets'][0].id, 401);
             assert.deepEqual(content['commitSets'][0].revisionItems,
-                [{commit: '87832', patch: null}, {commit: '93116', patch: null}]);
+                [{commit: '87832', patch: null, rootFile: null}, {commit: '93116', patch: null, rootFile: null}]);
             assert.equal(content['commitSets'][1].id, 402);
             assert.deepEqual(content['commitSets'][1].revisionItems,
-                [{commit: '87832', patch: null}, {commit: '96336', patch: null}]);
+                [{commit: '87832', patch: null, rootFile: null}, {commit: '96336', patch: null, rootFile: null}]);
 
             assert.equal(content['commits'].length, 3);
             assert.equal(content['commits'][0].id, 87832);
diff --git a/Websites/perf.webkit.org/server-tests/api-upload-root-tests.js b/Websites/perf.webkit.org/server-tests/api-upload-root-tests.js
new file mode 100644 (file)
index 0000000..9096afc
--- /dev/null
@@ -0,0 +1,495 @@
+'use strict';
+
+const assert = require('assert');
+
+require('../tools/js/v3-models.js');
+
+const MockData = require('./resources/mock-data.js');
+const TestServer = require('./resources/test-server.js');
+const TemporaryFile = require('./resources/temporary-file.js').TemporaryFile;
+const addSlaveForReport = require('./resources/common-operations.js').addSlaveForReport;
+const prepareServerTest = require('./resources/common-operations.js').prepareServerTest;
+
+function makeReport(rootFile, buildRequestId = 1)
+{
+    return {
+        slaveName: 'someSlave',
+        slavePassword: 'somePassword',
+        builderName: 'someBuilder',
+        buildNumber: 123,
+        buildTime: '2017-05-10T02:54:08.666',
+        buildRequest: buildRequestId,
+        rootFile: rootFile,
+        repositoryList: '["WebKit"]',
+    };
+}
+
+function addSlaveAndCreateRootFile(slaveInfo = makeReport())
+{
+    return addSlaveForReport(slaveInfo).then(() => {
+        return TemporaryFile.makeTemporaryFile('some.dat', 'some content');
+    });
+}
+
+function createTestGroupWihPatch()
+{
+    const triggerableConfiguration = {
+        'slaveName': 'sync-slave',
+        'slavePassword': 'password',
+        'triggerable': 'build-webkit',
+        'configurations': [
+            {test: MockData.someTestId(), platform: MockData.somePlatformId()},
+        ],
+        'repositoryGroups': [
+            {name: 'webkit', repositories: [
+                {repository: MockData.webkitRepositoryId(), acceptsPatch: true},
+                {repository: MockData.sharedRepositoryId()},
+            ]}
+        ]
+    };
+
+    return MockData.addMockData(TestServer.database()).then(() => {
+        return Promise.all([TemporaryFile.makeTemporaryFile('patch.dat', 'patch file'), Manifest.fetch()]);
+    }).then((result) => {
+        const patchFile = result[0];
+        return Promise.all([UploadedFile.uploadFile(patchFile), TestServer.remoteAPI().postJSON('/api/update-triggerable/', triggerableConfiguration)]);
+    }).then((result) => {
+        const patchFile = result[0];
+        const someTest = Test.findById(MockData.someTestId());
+        const webkit = Repository.findById(MockData.webkitRepositoryId());
+        const shared = Repository.findById(MockData.sharedRepositoryId());
+        const set1 = new CustomCommitSet;
+        set1.setRevisionForRepository(webkit, '191622', patchFile);
+        set1.setRevisionForRepository(shared, '80229');
+        const set2 = new CustomCommitSet;
+        set2.setRevisionForRepository(webkit, '191622');
+        set2.setRevisionForRepository(shared, '80229');
+        return TestGroup.createWithTask('custom task', Platform.findById(MockData.somePlatformId()), someTest, 'some group', 2, [set1, set2]);
+    }).then((task) => {
+        return TestGroup.findAllByTask(task.id())[0];
+    })
+}
+
+describe('/api/upload-root/', function () {
+    prepareServerTest(this);
+    TemporaryFile.inject();
+
+    it('should reject when root file is missing', () => {
+        return TestServer.remoteAPI().postFormData('/api/upload-root/', {}).then((response) => {
+            assert.equal(response['status'], 'NoFileSpecified');
+        });
+    });
+
+    it('should reject when there are no slaves', () => {
+        return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((rootFile) => {
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', makeReport(rootFile));
+        }).then((response) => {
+            assert.equal(response['status'], 'SlaveNotFound');
+        });
+    });
+
+    it('should reject when slave name is missing', () => {
+        return addSlaveAndCreateRootFile().then((rootFile) => {
+            const report = makeReport(rootFile);
+            delete report.slaveName;
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', report);
+        }).then((response) => {
+            assert.equal(response['status'], 'MissingSlaveName');
+        });
+    });
+
+    it('should reject when builder name is missing', () => {
+        return addSlaveAndCreateRootFile().then((rootFile) => {
+            const report = makeReport(rootFile);
+            delete report.builderName;
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', report);
+        }).then((response) => {
+            assert.equal(response['status'], 'InvalidBuilderName');
+        });
+    });
+
+    it('should reject when build number is missing', () => {
+        return addSlaveAndCreateRootFile().then((rootFile) => {
+            const report = makeReport(rootFile);
+            delete report.buildNumber;
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', report);
+        }).then((response) => {
+            assert.equal(response['status'], 'InvalidBuildNumber');
+        });
+    });
+
+    it('should reject when build number is not a number', () => {
+        return addSlaveAndCreateRootFile().then((rootFile) => {
+            const report = makeReport(rootFile);
+            report.buildNumber = '1abc';
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', report);
+        }).then((response) => {
+            assert.equal(response['status'], 'InvalidBuildNumber');
+        });
+    });
+
+    it('should reject when build time is missing', () => {
+        return addSlaveAndCreateRootFile().then((rootFile) => {
+            const report = makeReport(rootFile);
+            delete report.buildTime;
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', report);
+        }).then((response) => {
+            assert.equal(response['status'], 'InvalidBuildTime');
+        });
+    });
+
+    it('should reject when build time is malformed', () => {
+        return addSlaveAndCreateRootFile().then((rootFile) => {
+            const report = makeReport(rootFile);
+            report.buildTime = 'Wed, 10 May 2017 03:02:30 GMT';
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', report);
+        }).then((response) => {
+            assert.equal(response['status'], 'InvalidBuildTime');
+        });
+    });
+
+    it('should reject when build request ID is missing', () => {
+        return addSlaveAndCreateRootFile().then((rootFile) => {
+            const report = makeReport(rootFile);
+            delete report.buildRequest;
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', report);
+        }).then((response) => {
+            assert.equal(response['status'], 'InvalidBuildRequest');
+        });
+    });
+
+    it('should reject when build request ID is not a number', () => {
+        return addSlaveAndCreateRootFile().then((rootFile) => {
+            const report = makeReport(rootFile);
+            report.buildRequest = 'abc';
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', report);
+        }).then((response) => {
+            assert.equal(response['status'], 'InvalidBuildRequest');
+        });
+    });
+
+    it('should reject when build request does not exist', () => {
+        return addSlaveAndCreateRootFile().then((rootFile) => {
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', makeReport(rootFile));
+        }).then((response) => {
+            assert.equal(response['status'], 'InvalidBuildRequestType');
+        });
+    });
+
+    it('should reject when repository list is missing', () => {
+        return addSlaveAndCreateRootFile().then((rootFile) => {
+            const report = makeReport(rootFile);
+            delete report.repositoryList;
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', report);
+        }).then((response) => {
+            assert.equal(response['status'], 'InvalidRepositoryList');
+        });
+    });
+
+    it('should reject when repository list is not a valid JSON string', () => {
+        return addSlaveAndCreateRootFile().then((rootFile) => {
+            const report = makeReport(rootFile);
+            report.repositoryList = 'a+b';
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', report);
+        }).then((response) => {
+            assert.equal(response['status'], 'InvalidRepositoryList');
+        });
+    });
+
+    it('should accept when build request exists', () => {
+        let webkit;
+        let webkitPatch;
+        let shared;
+        let testGroup;
+        let buildRequest;
+        let otherBuildRequest;
+        let uploadedRoot;
+        return createTestGroupWihPatch().then((group) => {
+            webkit = Repository.findById(MockData.webkitRepositoryId());
+            shared = Repository.findById(MockData.sharedRepositoryId());
+
+            testGroup = group;
+            buildRequest = testGroup.buildRequests()[0];
+            assert.equal(testGroup.buildRequests().length, 6);
+
+            assert(buildRequest.isBuild());
+            assert(!buildRequest.isTest());
+            assert(!buildRequest.hasFinished());
+            assert(buildRequest.isPending()); 
+            assert.equal(buildRequest.buildId(), null);
+
+            const commitSet = buildRequest.commitSet();
+            assert.equal(commitSet.revisionForRepository(webkit), '191622');
+            webkitPatch = commitSet.patchForRepository(webkit);
+            assert(webkitPatch instanceof UploadedFile);
+            assert.equal(webkitPatch.filename(), 'patch.dat');
+            assert.equal(commitSet.rootForRepository(webkit), null);
+            assert.equal(commitSet.revisionForRepository(shared), '80229');
+            assert.equal(commitSet.patchForRepository(shared), null);
+            assert.equal(commitSet.rootForRepository(shared), null);
+            assert.deepEqual(commitSet.allRootFiles(), []);
+
+            otherBuildRequest = testGroup.buildRequests()[1];
+            const otherCommitSet = otherBuildRequest.commitSet();
+            assert.equal(otherCommitSet.revisionForRepository(webkit), '191622');
+            assert.equal(otherCommitSet.patchForRepository(webkit), null);
+            assert.equal(otherCommitSet.rootForRepository(webkit), null);
+            assert.equal(otherCommitSet.revisionForRepository(shared), '80229');
+            assert.equal(otherCommitSet.patchForRepository(shared), null);
+            assert.equal(otherCommitSet.rootForRepository(shared), null);
+            assert.deepEqual(otherCommitSet.allRootFiles(), []);
+
+            return addSlaveAndCreateRootFile();
+        }).then((rootFile) => {
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', makeReport(rootFile, buildRequest.id()));
+        }).then((response) => {
+            assert.equal(response['status'], 'OK');
+            const uploadedRootRawData = response['uploadedFile'];
+            uploadedRoot = UploadedFile.ensureSingleton(uploadedRootRawData.id, uploadedRootRawData);
+            assert.equal(uploadedRoot.filename(), 'some.dat');
+            return TestGroup.fetchForTask(buildRequest.testGroup().task().id(), true);
+        }).then((testGroups) => {
+
+            assert.equal(testGroups.length, 1);
+            const group = testGroups[0];
+            assert.equal(group, testGroup);
+            assert.equal(testGroup.buildRequests().length, 6);
+
+            const updatedBuildRequest = testGroup.buildRequests()[0];
+            assert.equal(updatedBuildRequest, buildRequest);
+
+            assert(buildRequest.isBuild());
+            assert(!buildRequest.isTest());
+            assert(buildRequest.hasFinished());
+            assert(!buildRequest.isPending()); 
+            assert.notEqual(buildRequest.buildId(), null);
+
+            const commitSet = buildRequest.commitSet();
+            assert.equal(commitSet.revisionForRepository(webkit), '191622');
+            assert.equal(commitSet.patchForRepository(webkit), webkitPatch);
+            const webkitRoot = commitSet.rootForRepository(webkit);
+            assert.equal(webkitRoot, uploadedRoot);
+            assert.equal(commitSet.revisionForRepository(shared), '80229');
+            assert.equal(commitSet.patchForRepository(shared), null);
+            assert.equal(commitSet.rootForRepository(shared), null);
+            assert.deepEqual(commitSet.allRootFiles(), [uploadedRoot]);
+
+            const otherCommitSet = otherBuildRequest.commitSet();
+            assert.equal(otherCommitSet.revisionForRepository(webkit), '191622');
+            assert.equal(otherCommitSet.patchForRepository(webkit), null);
+            assert.equal(otherCommitSet.rootForRepository(webkit), null);
+            assert.equal(otherCommitSet.revisionForRepository(shared), '80229');
+            assert.equal(otherCommitSet.patchForRepository(shared), null);
+            assert.equal(otherCommitSet.rootForRepository(shared), null);
+            assert.deepEqual(otherCommitSet.allRootFiles(), []);
+        });
+    });
+
+    it('should reject when the repository list is not an array', () => {
+        let buildRequest;
+        return createTestGroupWihPatch().then((group) => {
+            buildRequest = group.buildRequests()[0];
+            return addSlaveAndCreateRootFile();
+        }).then((rootFile) => {
+            const report = makeReport(rootFile, buildRequest.id());
+            report.repositoryList = '"a"';
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', report);
+        }).then((response) => {
+            assert.equal(response['status'], 'InvalidRepositoryList');
+        });
+    });
+
+    it('should reject when the repository list refers to a non-existent repository', () => {
+        let buildRequest;
+        return createTestGroupWihPatch().then((group) => {
+            buildRequest = group.buildRequests()[0];
+            return addSlaveAndCreateRootFile();
+        }).then((rootFile) => {
+            const report = makeReport(rootFile, buildRequest.id());
+            report.repositoryList = '["WebKit", "BadRepositoryName"]';
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', report);
+        }).then((response) => {
+            assert.equal(response['status'], 'InvalidRepository');
+            assert.equal(response['repositoryName'], 'BadRepositoryName');
+        });
+    });
+
+    it('should reject when the repository list refers to a repository not present in the commit set', () => {
+        let buildRequest;
+        return createTestGroupWihPatch().then((group) => {
+            buildRequest = group.buildRequests()[0];
+            return addSlaveAndCreateRootFile();
+        }).then((rootFile) => {
+            const report = makeReport(rootFile, buildRequest.id());
+            report.repositoryList = '["WebKit", "macOS"]';
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', report);
+        }).then((response) => {
+            assert.equal(response['status'], 'InvalidRepository');
+            assert.equal(response['repositoryName'], 'macOS');
+        });
+    });
+
+    it('should update all commit set items in the repository listed', () => {
+        let webkit;
+        let webkitPatch;
+        let shared;
+        let testGroup;
+        let buildRequest;
+        let otherBuildRequest;
+        let uploadedRoot;
+        return createTestGroupWihPatch().then((group) => {
+            webkit = Repository.findById(MockData.webkitRepositoryId());
+            shared = Repository.findById(MockData.sharedRepositoryId());
+
+            testGroup = group;
+            buildRequest = testGroup.buildRequests()[0];
+            assert.equal(testGroup.buildRequests().length, 6);
+
+            assert(buildRequest.isBuild());
+            assert(!buildRequest.isTest());
+            assert(!buildRequest.hasFinished());
+            assert(buildRequest.isPending()); 
+            assert.equal(buildRequest.buildId(), null);
+
+            const commitSet = buildRequest.commitSet();
+            assert.equal(commitSet.revisionForRepository(webkit), '191622');
+            webkitPatch = commitSet.patchForRepository(webkit);
+            assert(webkitPatch instanceof UploadedFile);
+            assert.equal(webkitPatch.filename(), 'patch.dat');
+            assert.equal(commitSet.rootForRepository(webkit), null);
+            assert.equal(commitSet.revisionForRepository(shared), '80229');
+            assert.equal(commitSet.patchForRepository(shared), null);
+            assert.equal(commitSet.rootForRepository(shared), null);
+            assert.deepEqual(commitSet.allRootFiles(), []);
+
+            otherBuildRequest = testGroup.buildRequests()[1];
+            const otherCommitSet = otherBuildRequest.commitSet();
+            assert.equal(otherCommitSet.revisionForRepository(webkit), '191622');
+            assert.equal(otherCommitSet.patchForRepository(webkit), null);
+            assert.equal(otherCommitSet.rootForRepository(webkit), null);
+            assert.equal(otherCommitSet.revisionForRepository(shared), '80229');
+            assert.equal(otherCommitSet.patchForRepository(shared), null);
+            assert.equal(otherCommitSet.rootForRepository(shared), null);
+            assert.deepEqual(otherCommitSet.allRootFiles(), []);
+
+            return addSlaveAndCreateRootFile();
+        }).then((rootFile) => {
+            const report = makeReport(rootFile, buildRequest.id());
+            report.repositoryList = '["WebKit", "Shared"]';
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', report);
+        }).then((response) => {
+            assert.equal(response['status'], 'OK');
+            const uploadedRootRawData = response['uploadedFile'];
+            uploadedRoot = UploadedFile.ensureSingleton(uploadedRootRawData.id, uploadedRootRawData);
+            assert.equal(uploadedRoot.filename(), 'some.dat');
+            return TestGroup.fetchForTask(buildRequest.testGroup().task().id(), true);
+        }).then((testGroups) => {
+
+            assert.equal(testGroups.length, 1);
+            const group = testGroups[0];
+            assert.equal(group, testGroup);
+            assert.equal(testGroup.buildRequests().length, 6);
+
+            const updatedBuildRequest = testGroup.buildRequests()[0];
+            assert.equal(updatedBuildRequest, buildRequest);
+
+            assert(buildRequest.isBuild());
+            assert(!buildRequest.isTest());
+            assert(buildRequest.hasFinished());
+            assert(!buildRequest.isPending()); 
+            assert.notEqual(buildRequest.buildId(), null);
+
+            const commitSet = buildRequest.commitSet();
+            assert.equal(commitSet.revisionForRepository(webkit), '191622');
+            assert.equal(commitSet.patchForRepository(webkit), webkitPatch);
+            assert.equal(commitSet.rootForRepository(webkit), uploadedRoot);
+            assert.equal(commitSet.revisionForRepository(shared), '80229');
+            assert.equal(commitSet.patchForRepository(shared), null);
+            assert.equal(commitSet.rootForRepository(shared), uploadedRoot);
+            assert.deepEqual(commitSet.allRootFiles(), [uploadedRoot]);
+
+            const otherCommitSet = otherBuildRequest.commitSet();
+            assert.equal(otherCommitSet.revisionForRepository(webkit), '191622');
+            assert.equal(otherCommitSet.patchForRepository(webkit), null);
+            assert.equal(otherCommitSet.rootForRepository(webkit), null);
+            assert.equal(otherCommitSet.revisionForRepository(shared), '80229');
+            assert.equal(otherCommitSet.patchForRepository(shared), null);
+            assert.equal(otherCommitSet.rootForRepository(shared), null);
+            assert.deepEqual(otherCommitSet.allRootFiles(), []);
+        });
+    });
+
+    it('should update all commit set items in the repository listed', () => {
+        let webkit;
+        let webkitPatch;
+        let shared;
+        let testGroup;
+        let buildRequest;
+        let otherBuildRequest;
+        let uploadedRoot;
+        return createTestGroupWihPatch().then((group) => {
+            webkit = Repository.findById(MockData.webkitRepositoryId());
+            shared = Repository.findById(MockData.sharedRepositoryId());
+
+            testGroup = group;
+            buildRequest = testGroup.buildRequests()[0];
+            assert.equal(testGroup.buildRequests().length, 6);
+            assert(buildRequest.isBuild());
+            assert(!buildRequest.isTest());
+            assert(!buildRequest.hasFinished());
+            assert(buildRequest.isPending()); 
+            assert.equal(buildRequest.buildId(), null);
+
+            const commitSet = buildRequest.commitSet();
+            assert.equal(commitSet.revisionForRepository(webkit), '191622');
+            webkitPatch = commitSet.patchForRepository(webkit);
+            assert(webkitPatch instanceof UploadedFile);
+            assert.equal(webkitPatch.filename(), 'patch.dat');
+            assert.equal(commitSet.rootForRepository(webkit), null);
+            assert.equal(commitSet.revisionForRepository(shared), '80229');
+            assert.equal(commitSet.patchForRepository(shared), null);
+            assert.equal(commitSet.rootForRepository(shared), null);
+            assert.deepEqual(commitSet.allRootFiles(), []);
+
+            otherBuildRequest = testGroup.buildRequests()[1];
+            const otherCommitSet = otherBuildRequest.commitSet();
+            assert.equal(otherCommitSet.revisionForRepository(webkit), '191622');
+            assert.equal(otherCommitSet.patchForRepository(webkit), null);
+            assert.equal(otherCommitSet.rootForRepository(webkit), null);
+            assert.equal(otherCommitSet.revisionForRepository(shared), '80229');
+            assert.equal(otherCommitSet.patchForRepository(shared), null);
+            assert.equal(otherCommitSet.rootForRepository(shared), null);
+            assert.deepEqual(otherCommitSet.allRootFiles(), []);
+
+            return addSlaveAndCreateRootFile();
+        }).then((rootFile) => {
+            const report = makeReport(rootFile, buildRequest.id());
+            report.repositoryList = '["WebKit", "Shared"]';
+            return TestServer.remoteAPI().postFormData('/api/upload-root/', report);
+        }).then((response) => {
+            assert.equal(response['status'], 'OK');
+            const uploadedRootRawData = response['uploadedFile'];
+            uploadedRoot = UploadedFile.ensureSingleton(uploadedRootRawData.id, uploadedRootRawData);
+            assert.equal(uploadedRoot.filename(), 'some.dat');
+            return TestGroup.fetchForTask(buildRequest.testGroup().task().id(), true);
+        }).then((testGroups) => {
+            assert.equal(testGroups.length, 1);
+            const group = testGroups[0];
+            assert.equal(group, testGroup);
+            assert.equal(testGroup.buildRequests().length, 6);
+
+            const updatedBuildRequest = testGroup.buildRequests()[0];
+            assert.equal(updatedBuildRequest, buildRequest);
+
+            assert(buildRequest.isBuild());
+            assert(!buildRequest.isTest());
+            assert(buildRequest.hasFinished());
+            assert(!buildRequest.isPending()); 
+            assert.notEqual(buildRequest.buildId(), null);
+
+            assert.deepEqual(buildRequest.commitSet().allRootFiles(), [uploadedRoot]);
+            assert.deepEqual(otherBuildRequest.commitSet().allRootFiles(), []);
+        });
+    });
+
+});
index c82a83a..9833579 100644 (file)
@@ -323,7 +323,7 @@ describe('/privileged-api/create-test-group', function () {
             let insertedGroupId;
             return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, commitSets: {'macOS': ['15A284', '15A284'], 'WebKit': ['191622', '191623']}}).then((content) => {
                 insertedGroupId = content['testGroupId'];
-                return TestGroup.fetchByTask(taskId);
+                return TestGroup.fetchForTask(taskId, true);
             }).then((testGroups) => {
                 assert.equal(testGroups.length, 1);
                 const group = testGroups[0];
@@ -361,7 +361,7 @@ describe('/privileged-api/create-test-group', function () {
             let insertedGroupId;
             return PrivilegedAPI.sendRequest('create-test-group', params).then((content) => {
                 insertedGroupId = content['testGroupId'];
-                return TestGroup.fetchByTask(taskId);
+                return TestGroup.fetchForTask(taskId, true);
             }).then((testGroups) => {
                 assert.equal(testGroups.length, 1);
                 const group = testGroups[0];
@@ -392,7 +392,7 @@ describe('/privileged-api/create-test-group', function () {
             return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2,
                 commitSets: {'WebKit': ['191622', '191623'], 'macOS': ['15A284', '15A284']}}).then((content) => {
                 insertedGroupId = content['testGroupId'];
-                return TestGroup.fetchByTask(taskId);
+                return TestGroup.fetchForTask(taskId, true);
             }).then((testGroups) => {
                 assert.equal(testGroups.length, 1);
                 const group = testGroups[0];
@@ -438,7 +438,7 @@ describe('/privileged-api/create-test-group', function () {
             let insertedGroupId;
             return PrivilegedAPI.sendRequest('create-test-group', params).then((content) => {
                 insertedGroupId = content['testGroupId'];
-                return TestGroup.fetchByTask(taskId);
+                return TestGroup.fetchForTask(taskId, true);
             }).then((testGroups) => {
                 assert.equal(testGroups.length, 1);
                 const group = testGroups[0];
@@ -485,7 +485,7 @@ describe('/privileged-api/create-test-group', function () {
                     {[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}, 'customRoots': [uploadedFile['id']]}];
                 return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, revisionSets}).then((content) => {
                     insertedGroupId = content['testGroupId'];
-                    return TestGroup.fetchByTask(taskId);
+                    return TestGroup.fetchForTask(taskId, true);
                 });
             }).then((testGroups) => {
                 assert.equal(testGroups.length, 1);
@@ -534,7 +534,7 @@ describe('/privileged-api/create-test-group', function () {
             return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, revisionSets});
         }).then((content) => {
             insertedGroupId = content['testGroupId'];
-            return TestGroup.fetchByTask(taskId);
+            return TestGroup.fetchForTask(taskId, true);
         }).then((testGroups) => {
             assert.equal(testGroups.length, 1);
             const group = testGroups[0];
@@ -618,7 +618,7 @@ describe('/privileged-api/create-test-group', function () {
                 {name: 'test', taskName: 'other task', platform: MockData.somePlatformId(), test: MockData.someTestId(), revisionSets});
         }).then((result) => {
             insertedGroupId = result['testGroupId'];
-            return Promise.all([AnalysisTask.fetchById(result['taskId']), TestGroup.fetchByTask(result['taskId'])]);
+            return Promise.all([AnalysisTask.fetchById(result['taskId']), TestGroup.fetchForTask(result['taskId'], true)]);
         }).then((result) => {
             const [analysisTask, testGroups] = result;
 
@@ -660,7 +660,7 @@ describe('/privileged-api/create-test-group', function () {
         }).then((result) => {
             secondResult = result;
             assert.equal(firstResult['taskId'], secondResult['taskId']);
-            return Promise.all([AnalysisTask.fetchById(result['taskId']), TestGroup.fetchByTask(result['taskId'])]);
+            return Promise.all([AnalysisTask.fetchById(result['taskId']), TestGroup.fetchForTask(result['taskId'], true)]);
         }).then((result) => {
             const [analysisTask, testGroups] = result;
 
index 0d04064..307c8f8 100644 (file)
@@ -25,6 +25,7 @@ MockData = {
     macosRepositoryId() { return 9; },
     webkitRepositoryId() { return 11; },
     gitWebkitRepositoryId() { return 111; },
+    sharedRepositoryId() { return 14; },
     addMockData: function (db, statusList)
     {
         if (!statusList)
@@ -34,12 +35,15 @@ MockData = {
             db.insert('build_slaves', {id: 20, name: 'sync-slave', password_hash: crypto.createHash('sha256').update('password').digest('hex')}),
             db.insert('repositories', {id: this.macosRepositoryId(), name: 'macOS'}),
             db.insert('repositories', {id: this.webkitRepositoryId(), name: 'WebKit'}),
+            db.insert('repositories', {id: this.sharedRepositoryId(), name: 'Shared'}),
             db.insert('triggerable_repository_groups', {id: 2001, name: 'webkit-svn', triggerable: 1000}),
             db.insert('triggerable_repositories', {repository: this.macosRepositoryId(), group: 2001}),
             db.insert('triggerable_repositories', {repository: this.webkitRepositoryId(), group: 2001}),
             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()}),
+            db.insert('commits', {id: 111168, repository: this.sharedRepositoryId(), revision: '80229', time: '2016-03-02T23:17:54.3Z'}),
+            db.insert('commits', {id: 111169, repository: this.sharedRepositoryId(), revision: '80230', time: '2016-03-02T23:37:18.0Z'}),
             db.insert('builds', {id: 901, number: '901', time: '2015-10-27T12:05:27.1Z'}),
             db.insert('platforms', {id: MockData.somePlatformId(), name: 'some platform'}),
             db.insert('platforms', {id: MockData.otherPlatformId(), name: 'other platform'}),