Performance Dashboard backend should support A/B testing for owned components.
authordewei_zhu@apple.com <dewei_zhu@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 19 Sep 2017 20:14:45 +0000 (20:14 +0000)
committerdewei_zhu@apple.com <dewei_zhu@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 19 Sep 2017 20:14:45 +0000 (20:14 +0000)
https://bugs.webkit.org/show_bug.cgi?id=175978

Reviewed by Ryosuke Niwa.

Add backend change for Performance Dashboard to support A/B testing for owned components.
Added 'commitset_commit_owner' and 'commitset_requires_build' columns to 'commit_set_items' table.
'commitset_commit_owner' referrs to determine a commit with owner.
'commitset_requires_build' indicates whether a root build is required.
This will be set true whenever commit_set_item specifies a patch file,
or commit_set_item is commit with owner commit,
or any other commit from same repository and in same build-request group requires build.
SQL for updating existing database:
    'BEGIN;
        ALTER TABLE commit_set_items ADD COLUMN commitset_commit_owner integer REFERENCES commits DEFAULT NULL, ADD COLUMN commitset_requires_build boolean DEFAULT FALSE;
        UPDATE commit_set_items SET commitset_requires_build = TRUE WHERE commitset_patch_file IS NOT NULL;
        UPDATE commit_set_items SET commitset_requires_build = TRUE WHERE commitset_set IN (SELECT requests1.request_commit_set FROM build_requests as requests1 JOIN build_requests as requests2 ON requests1.request_group = requests2.request_group JOIN commit_set_items as item ON item.commitset_set = requests2.request_commit_set  WHERE item.commitset_patch_file IS NOT NULL);
        ALTER TABLE commit_set_items ADD CONSTRAINT commitset_item_with_patch_must_requires_build CHECK (commitset_patch_file IS NULL OR commitset_requires_build = TRUE),
            ADD CONSTRAINT commitset_item_with_owned_commit_must_requires_build CHECK (commitset_commit_owner IS NULL OR commitset_requires_build = TRUE);
    END;'

* init-database.sql: Updated 'commit_set_items' table.
* public/admin/triggerables.php: Only top level repository should show on triggerables page.
* public/include/build-requests-fetcher.php: Added 'commitOwner' and 'requireBuild' to 'revision_items'. Added 'commitOwner' field to a commit.
* public/include/db.php: Should be able to insert boolean value to database without explicted convert to 't' or 'f'.
* public/privileged-api/create-test-group.php:
    Added logic to process 'commitOwner' and 'requireBuild' in 'commit_set_items'.
    Removed a 'FIXME' that has been addressed before this commit.
* public/v3/models/build-request.js:
(BuildRequest.constructBuildRequestsFromData): Set 'commitOwner' field for a commit set item.
* public/v3/models/commit-set.js:
(CommitSet): Added maps for repository to commit owner and whether a repository requires builds.
(CommitSet.prototype.updateSingleton):
(CommitSet.prototype._updateFromObject):
(CommitSet.prototype.ownerRevisionForRepository): Returns owner revision for a given repository in current commit set.
(CommitSet.prototype.requiresBuildForRepository): Returns whether a repository need to build.
(CommitSet.prototype.equals): Equality check should include 2 new maps.
(CustomCommitSet): CustomCommitSet should be able to store commit with an owner commit.
(CustomCommitSet.prototype.setRevisionForRepository): Added each revision list entry should have 'ownerRevision'(null by default).
(CustomCommitSet.prototype.equals): Equality check should also check the equality of 'ownerRevision'.
(CustomCommitSet.prototype.ownerRevisionForRepository): Returns a owner revision for a given repository.
* public/v3/models/repository.js:
(Repository.prototype.findOwnedRepositoryByName): Return an repository owned by current repository with a given name.
* public/v3/models/test-group.js: Added 'ownerRevision' field in each entry of revisionSet.
* server-tests/api-build-requests-tests.js: Added tests.
* server-tests/privileged-api-create-test-group-tests.js: Added tests.
* server-tests/privileged-api-upload-file-tests.js: Fix unit tests by setting'requires_build' field to be true when updating commit_set_item which has a patch..
* server-tests/resources/mock-data.js: Added mock build requests with commit sets contain owned commits.
(MockData.jscRepositoryId): Returns id for JavaScriptsCore repository.
(MockData.addMockConfiguration): Added mock JavaScriptCore and owned JavaScriptCore repositories and commits associated with them.
(MockData.ownedJSCRepositoryId): Added a JavaScriptCore repository with WebKit as owner.
(MockData.addMockConfiguration): Added mock data for test cases those require a commit with a owner commit.
(MockData.addTestGroupWithOwnedCommits): Added mock data for analysis tasks, the build requires of which contains owned commits.
(MockData.set addAnotherTriggerable): Added another triggerable which has mac, webkit and javascript core repositories as triggerable repository group.
(MockData.set addAnotherMockTestGroup): Added another mock test group.
* tools/js/v3-models.js: Import CustomCommitSet.
* unit-tests/resources/mock-v3-models.js: Added an owned webkit repository.
* unit-tests/commit-set-tests.js: Added unit tests CustomCommitSet.

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

16 files changed:
Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/init-database.sql
Websites/perf.webkit.org/public/admin/triggerables.php
Websites/perf.webkit.org/public/include/build-requests-fetcher.php
Websites/perf.webkit.org/public/include/db.php
Websites/perf.webkit.org/public/privileged-api/create-test-group.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/server-tests/api-build-requests-tests.js
Websites/perf.webkit.org/server-tests/privileged-api-create-test-group-tests.js
Websites/perf.webkit.org/server-tests/privileged-api-upload-file-tests.js
Websites/perf.webkit.org/server-tests/resources/mock-data.js
Websites/perf.webkit.org/tools/js/v3-models.js
Websites/perf.webkit.org/unit-tests/commit-set-tests.js [new file with mode: 0644]
Websites/perf.webkit.org/unit-tests/resources/mock-v3-models.js

index f48ce63..14c2782 100644 (file)
@@ -1,3 +1,64 @@
+2017-09-12  Dewei Zhu  <dewei_zhu@apple.com>
+
+        Performance Dashboard backend should support A/B testing for owned components.
+        https://bugs.webkit.org/show_bug.cgi?id=175978
+
+        Reviewed by Ryosuke Niwa.
+
+        Add backend change for Performance Dashboard to support A/B testing for owned components.
+        Added 'commitset_commit_owner' and 'commitset_requires_build' columns to 'commit_set_items' table.
+        'commitset_commit_owner' referrs to determine a commit with owner.
+        'commitset_requires_build' indicates whether a root build is required.
+        This will be set true whenever commit_set_item specifies a patch file,
+        or commit_set_item is commit with owner commit,
+        or any other commit from same repository and in same build-request group requires build.
+        SQL for updating existing database:
+            'BEGIN;
+                ALTER TABLE commit_set_items ADD COLUMN commitset_commit_owner integer REFERENCES commits DEFAULT NULL, ADD COLUMN commitset_requires_build boolean DEFAULT FALSE;
+                UPDATE commit_set_items SET commitset_requires_build = TRUE WHERE commitset_patch_file IS NOT NULL;
+                UPDATE commit_set_items SET commitset_requires_build = TRUE WHERE commitset_set IN (SELECT requests1.request_commit_set FROM build_requests as requests1 JOIN build_requests as requests2 ON requests1.request_group = requests2.request_group JOIN commit_set_items as item ON item.commitset_set = requests2.request_commit_set  WHERE item.commitset_patch_file IS NOT NULL);
+                ALTER TABLE commit_set_items ADD CONSTRAINT commitset_item_with_patch_must_requires_build CHECK (commitset_patch_file IS NULL OR commitset_requires_build = TRUE),
+                    ADD CONSTRAINT commitset_item_with_owned_commit_must_requires_build CHECK (commitset_commit_owner IS NULL OR commitset_requires_build = TRUE);
+            END;'
+
+        * init-database.sql: Updated 'commit_set_items' table.
+        * public/admin/triggerables.php: Only top level repository should show on triggerables page.
+        * public/include/build-requests-fetcher.php: Added 'commitOwner' and 'requireBuild' to 'revision_items'. Added 'commitOwner' field to a commit.
+        * public/include/db.php: Should be able to insert boolean value to database without explicted convert to 't' or 'f'.
+        * public/privileged-api/create-test-group.php:
+            Added logic to process 'commitOwner' and 'requireBuild' in 'commit_set_items'.
+            Removed a 'FIXME' that has been addressed before this commit.
+        * public/v3/models/build-request.js:
+        (BuildRequest.constructBuildRequestsFromData): Set 'commitOwner' field for a commit set item.
+        * public/v3/models/commit-set.js:
+        (CommitSet): Added maps for repository to commit owner and whether a repository requires builds.
+        (CommitSet.prototype.updateSingleton):
+        (CommitSet.prototype._updateFromObject):
+        (CommitSet.prototype.ownerRevisionForRepository): Returns owner revision for a given repository in current commit set.
+        (CommitSet.prototype.requiresBuildForRepository): Returns whether a repository need to build.
+        (CommitSet.prototype.equals): Equality check should include 2 new maps.
+        (CustomCommitSet): CustomCommitSet should be able to store commit with an owner commit.
+        (CustomCommitSet.prototype.setRevisionForRepository): Added each revision list entry should have 'ownerRevision'(null by default).
+        (CustomCommitSet.prototype.equals): Equality check should also check the equality of 'ownerRevision'.
+        (CustomCommitSet.prototype.ownerRevisionForRepository): Returns a owner revision for a given repository.
+        * public/v3/models/repository.js:
+        (Repository.prototype.findOwnedRepositoryByName): Return an repository owned by current repository with a given name.
+        * public/v3/models/test-group.js: Added 'ownerRevision' field in each entry of revisionSet.
+        * server-tests/api-build-requests-tests.js: Added tests.
+        * server-tests/privileged-api-create-test-group-tests.js: Added tests.
+        * server-tests/privileged-api-upload-file-tests.js: Fix unit tests by setting'requires_build' field to be true when updating commit_set_item which has a patch..
+        * server-tests/resources/mock-data.js: Added mock build requests with commit sets contain owned commits.
+        (MockData.jscRepositoryId): Returns id for JavaScriptsCore repository.
+        (MockData.addMockConfiguration): Added mock JavaScriptCore and owned JavaScriptCore repositories and commits associated with them.
+        (MockData.ownedJSCRepositoryId): Added a JavaScriptCore repository with WebKit as owner.
+        (MockData.addMockConfiguration): Added mock data for test cases those require a commit with a owner commit.
+        (MockData.addTestGroupWithOwnedCommits): Added mock data for analysis tasks, the build requires of which contains owned commits.
+        (MockData.set addAnotherTriggerable): Added another triggerable which has mac, webkit and javascript core repositories as triggerable repository group.
+        (MockData.set addAnotherMockTestGroup): Added another mock test group.
+        * tools/js/v3-models.js: Import CustomCommitSet.
+        * unit-tests/resources/mock-v3-models.js: Added an owned webkit repository.
+        * unit-tests/commit-set-tests.js: Added unit tests CustomCommitSet.
+
 2017-09-15  Dewei Zhu  <dewei_zhu@apple.com>
 
         Should not mark a platform as missing in summary page if all expecting metrics are exlucded.
index c143692..52788f7 100644 (file)
@@ -287,10 +287,14 @@ CREATE TABLE commit_sets (
 CREATE TABLE commit_set_items (
     commitset_set integer REFERENCES commit_sets NOT NULL,
     commitset_commit integer REFERENCES commits,
+    commitset_commit_owner integer REFERENCES commits DEFAULT NULL,
     commitset_patch_file integer REFERENCES uploaded_files,
     commitset_root_file integer REFERENCES uploaded_files,
+    commitset_requires_build boolean DEFAULT FALSE,
     CONSTRAINT commitset_must_have_commit_or_root CHECK (commitset_commit IS NOT NULL OR commitset_root_file IS NOT NULL),
-    CONSTRAINT commitset_with_patch_must_have_commit CHECK (commitset_patch_file IS NULL OR commitset_commit IS NOT NULL));
+    CONSTRAINT commitset_with_patch_must_have_commit CHECK (commitset_patch_file IS NULL OR commitset_commit IS NOT NULL),
+    CONSTRAINT commitset_item_with_patch_must_requires_build CHECK (commitset_patch_file IS NULL OR commitset_requires_build = TRUE),
+    CONSTRAINT commitset_item_with_owned_commit_must_requires_build CHECK (commitset_commit_owner IS NULL OR commitset_requires_build = TRUE));
 
 CREATE TYPE build_request_status_type as ENUM ('pending', 'scheduled', 'running', 'failed', 'completed', 'canceled');
 CREATE TABLE build_requests (
index 234bf3d..a0181f7 100644 (file)
@@ -76,7 +76,7 @@ if ($db) {
         }
     }
 
-    $repository_rows = $db->fetch_table('repositories', 'repository_name');
+    $repository_rows = $db->select_rows('repositories', 'repository', array('owner' => NULL), 'name');
 
     $page = new AdministrativePage($db, 'build_triggerables', 'triggerable', array(
         'name' => array('editing_mode' => 'string'),
index 76a96d4..ed999c4 100644 (file)
@@ -122,7 +122,12 @@ class BuildRequestsFetcher {
             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));
+            array_push($revision_items, array(
+                'commit' => $row['commit_id'],
+                'patch' => $patch_file_id,
+                'rootFile' => $root_file_id,
+                'commitOwner' => $row['commitset_commit_owner'],
+                'requiresBuild' => Database::is_true($row['commitset_requires_build'])));
 
             if (array_key_exists($commit_id, $this->commits_by_id))
                 continue;
@@ -130,6 +135,7 @@ class BuildRequestsFetcher {
             array_push($this->commits, array(
                 'id' => $commit_id,
                 'repository' => $repository_id,
+                'commitOwner' => $row['commitset_commit_owner'],
                 'revision' => $revision,
                 'time' => Database::to_js_time($commit_time)));
 
index 2212f0f..ed0e1d8 100644 (file)
@@ -118,6 +118,8 @@ class Database
             assert(ctype_alnum_underscore($name));
             array_push($column_names, $name);
             array_push($placeholders, '$' . $i);
+            if (is_bool($current_value))
+                $current_value = $this->to_database_boolean($current_value);
             array_push($values, $current_value);
             $i++;
         }
index b151118..7e5ef95 100644 (file)
@@ -52,7 +52,6 @@ function main()
         if ($duplicate_test_group)
             exit_with_error('DuplicateTestGroupName', array('task' => $task_id, 'testGroup' => $duplicate_test_group['testgroup_id']));
 
-        // FIXME: Add a check for duplicate test group name.
         $triggerable = find_triggerable_for_task($db, $task_id);
         if ($triggerable) {
             $triggerable_id = $triggerable['id'];
@@ -94,7 +93,9 @@ function main()
         $need_to_build = FALSE;
         foreach ($commit_list['set'] as $commit_row) {
             $commit_row['set'] = $commit_set_id;
-            $need_to_build = $need_to_build || $commit_row['patch_file'];
+            $requires_build =  $commit_row['requires_build'];
+            assert(is_bool($requires_build));
+            $need_to_build = $need_to_build || $requires_build;
             $db->insert_row('commit_set_items', 'commitset', $commit_row, 'commit');
         }
         $repository_group = $commit_list['repository_group'];
@@ -129,7 +130,7 @@ function main()
                 'order' => $order,
                 'commit_set' => $config['commit_set']));
             $order++;
-        }        
+        }
     }
 
     $order = 0;
@@ -159,6 +160,9 @@ function commit_sets_from_revision_sets($db, $triggerable_id, $revision_set_list
 
     $finder = new RepositoryGroupFinder($db, $triggerable_id);
     $commit_set_list = array();
+    $repository_owner_list = array();
+    $repositories_require_build = array();
+    $commit_set_items_by_repository = array();
     foreach ($revision_set_list as $revision_set) {
         if (!count($revision_set))
             exit_with_error('InvalidRevisionSets', array('revisionSets' => $revision_set_list));
@@ -166,13 +170,15 @@ function commit_sets_from_revision_sets($db, $triggerable_id, $revision_set_list
         $commit_set = array();
         $repository_list = array();
         $repository_with_patch = array();
+        $required_owner_commits = array();
+        $owner_commits_in_set = array();
         foreach ($revision_set as $repository_id => $data) {
             if ($repository_id == 'customRoots') {
                 $file_id_list = $data;
                 foreach ($file_id_list as $file_id) {
                     if (!is_numeric($file_id) || !$db->select_first_row('uploaded_files', 'file', array('id' => $file_id)))
                         exit_with_error('InvalidUploadedFile', array('file' => $file_id));
-                    array_push($commit_set, array('root_file' => $file_id, 'patch_file' => NULL));
+                    array_push($commit_set, array('root_file' => $file_id, 'patch_file' => NULL, 'requires_build' => FALSE, 'commit_owner' => NULL));
                 }
                 continue;
             }
@@ -187,33 +193,65 @@ function commit_sets_from_revision_sets($db, $triggerable_id, $revision_set_list
                 exit_with_error('InvalidRevision', array('repository' => $repository_id, 'data' => $data));
             $commit_id = CommitLogFetcher::find_commit_id_by_revision($db, $repository_id, $revision);
             if ($commit_id < 0)
-                exit_with_error('AmbigiousRevision', array('repository' => $repository_id, 'revision' => $revision));
+                exit_with_error('AmbiguousRevision', array('repository' => $repository_id, 'revision' => $revision));
             if (!$commit_id)
                 exit_with_error('RevisionNotFound', array('repository' => $repository_id, 'revision' => $revision));
 
+            $owner_revision = array_get($data, 'ownerRevision');
             $patch_file_id = array_get($data, 'patch');
             if ($patch_file_id) {
                 if (!is_numeric($patch_file_id) || !$db->select_first_row('uploaded_files', 'file', array('id' => $patch_file_id)))
                     exit_with_error('InvalidPatchFile', array('patch' => $patch_file_id));
                 array_push($repository_with_patch, $repository_id);
+                $repositories_require_build[$repository_id] =  TRUE;
             }
 
-            array_push($commit_set, array('commit' => $commit_id, 'patch_file' => $patch_file_id));
+            $repository = NULL;
+            $owner_commit_id = NULL;
+            if ($owner_revision) {
+                $repository = $db->select_first_row('repositories', 'repository', array('id' => intval($repository_id)));
+                if (!$repository)
+                    exit_with_error('RepositoryNotFound', array('repository' => $repository_id));
+                $owner_commit = $db->select_first_row('commits', 'commit', array('repository' => $repository['repository_owner'], 'revision' => $owner_revision));
+                if (!$owner_commit)
+                    exit_with_error('InvalidOwnerRevision', array('repository' => $repository['repository_owner'], 'revision' => $owner_revision));
+                if (!$db->select_first_row('commit_ownerships', 'commit', array('owned' => $commit_id, 'owner' => $owner_commit['commit_id'])))
+                    exit_with_error('InvalidCommitOwnership', array('commitOwner' => $owner_commit['commit_id'], 'commitOwned' => $commit_id));
+                $repositories_require_build[$repository_id] =  TRUE;
+                $owner_commit_id = $owner_commit['commit_id'];
+                $required_owner_commits[$owner_commit_id] = $commit_id;
+            } else
+                $owner_commits_in_set[$commit_id] = TRUE;
+
+            array_push($commit_set, array('commit' => $commit_id, 'patch_file' => $patch_file_id, 'requires_build' => FALSE, 'commit_owner' => $owner_commit_id));
+
+            array_ensure_item_has_array($commit_set_items_by_repository, $repository_id);
+            $commit_set_items_by_repository[$repository_id][] = &$commit_set[count($commit_set) - 1];
+
+            if ($owner_commit_id)
+                continue;
             array_push($repository_list, $repository_id);
         }
-
         $repository_group_id = $finder->find_by_repositories($repository_list);
         if (!$repository_group_id)
-            exit_with_error('NoMatchingRepositoryGroup', array('repositoris' => $repository_list));
+            exit_with_error('NoMatchingRepositoryGroup', array('repositories' => $repository_list));
 
         foreach ($repository_with_patch as $repository_id) {
             if (!$finder->accepts_patch($repository_group_id, $repository_id))
                 exit_with_error('PatchNotAccepted', array('repository' => $repository_id, 'repositoryGroup' => $repository_group_id));
         }
 
+        foreach($required_owner_commits as $required_owner_commit => $owned_commit) {
+            if (!array_get($owner_commits_in_set, $required_owner_commit, FALSE))
+                exit_with_error('CommitOwnerMustExistInCommitSet', array('owner_commit' => $required_owner_commit, 'owned_commit' => $owned_commit));
+        }
         array_push($commit_set_list, array('repository_group' => $repository_group_id, 'set' => $commit_set));
     }
 
+    foreach (array_keys($repositories_require_build) as $repository_id) {
+        foreach($commit_set_items_by_repository[$repository_id] as &$commit_set_item)
+            $commit_set_item['requires_build'] = TRUE;
+    }
     return $commit_set_list;
 }
 
@@ -235,14 +273,14 @@ function ensure_commit_sets($db, $triggerable_id, $commit_sets_info) {
             if (!$commit)
                 exit_with_error('RevisionNotFound', array('repository' => $repository_name, 'revision' => $revision));
             array_set_default($commit_sets, $i, array('set' => array()));
-            array_push($commit_sets[$i]['set'], array('commit' => $commit['commit_id'], 'patch_file' => NULL));
+            array_push($commit_sets[$i]['set'], array('commit' => $commit['commit_id'], 'patch_file' => NULL, 'requires_build' => FALSE, 'commit_owner' => NULL));
         }
     }
 
     $finder = new RepositoryGroupFinder($db, $triggerable_id);
     $repository_group_id = $finder->find_by_repositories($repository_list);
     if (!$repository_group_id)
-        exit_with_error('NoMatchingRepositoryGroup', array('repositoris' => $repository_list));
+        exit_with_error('NoMatchingRepositoryGroup', array('repositories' => $repository_list));
 
     if (count($commit_sets) < 2)
         exit_with_error('InvalidCommitSets', array('commitSets' => $commit_sets_info));
index 819b63c..06e92d8 100644 (file)
@@ -143,6 +143,7 @@ class BuildRequest extends DataModelObject {
                 item.commit = CommitLog.findById(item.commit);
                 item.patch = item.patch ? UploadedFile.findById(item.patch) : null;
                 item.rootFile = item.rootFile ? UploadedFile.findById(item.rootFile) : null;
+                item.commitOwner = item.commitOwner ? CommitLog.findById(item.commitOwner) : null;
             }
             rawData.customRoots = rawData.customRoots.map((fileId) => UploadedFile.findById(fileId));
             return CommitSet.ensureSingleton(rawData.id, rawData);
index a125f10..cb8e6f2 100644 (file)
@@ -9,6 +9,8 @@ class CommitSet extends DataModelObject {
         this._repositoryToCommitMap = new Map;
         this._repositoryToPatchMap = new Map;
         this._repositoryToRootMap = new Map;
+        this._repositoryToCommitOwnerMap = new Map;
+        this._repositoryRequiresBuildMap = new Map;
         this._latestCommitTime = null;
         this._customRoots = [];
         this._allRootFiles = [];
@@ -24,6 +26,8 @@ class CommitSet extends DataModelObject {
         this._repositoryToCommitMap.clear();
         this._repositoryToPatchMap.clear();
         this._repositoryToRootMap.clear();
+        this._repositoryToCommitOwnerMap.clear();
+        this._repositoryRequiresBuildMap.clear();
         this._repositories = [];
         this._updateFromObject(object);
     }
@@ -36,9 +40,12 @@ class CommitSet extends DataModelObject {
             console.assert(commit instanceof CommitLog);
             console.assert(!item.patch || item.patch instanceof UploadedFile);
             console.assert(!item.rootFile || item.rootFile instanceof UploadedFile);
+            console.assert(!item.commitOwner || item.commitOwner instanceof CommitLog);
             const repository = commit.repository();
             this._repositoryToCommitMap.set(repository, commit);
             this._repositoryToPatchMap.set(repository, item.patch);
+            this._repositoryToCommitOwnerMap.set(repository, item.commitOwner);
+            this._repositoryRequiresBuildMap.set(repository, item.requiresBuild);
             this._repositoryToRootMap.set(repository, item.rootFile);
             if (item.rootFile)
                 rootFiles.add(item.rootFile);
@@ -59,8 +66,15 @@ class CommitSet extends DataModelObject {
         return commit ? commit.revision() : null;
     }
 
+    ownerRevisionForRepository(repository)
+    {
+        const commit = this._repositoryToCommitOwnerMap.get(repository);
+        return commit ? commit.revision() : null;
+    }
+
     patchForRepository(repository) { return this._repositoryToPatchMap.get(repository); }
     rootForRepository(repository) { return this._repositoryToRootMap.get(repository); }
+    requiresBuildForRepository(repository) { return this._repositoryRequiresBuildMap.get(repository); }
 
     // FIXME: This should return a Date object.
     latestCommitTime()
@@ -85,6 +99,10 @@ class CommitSet extends DataModelObject {
                 return false;
             if (this._repositoryToRootMap.get(repository) != other._repositoryToRootMap.get(repository))
                 return false;
+            if (this._repositoryToCommitOwnerMap.get(repository) != other._repositoryToCommitMap.get(repository))
+                return false;
+            if (this._repositoryRequiresBuildMap.get(repository) != other._repositoryRequiresBuildMap.get(repository))
+                return false;
         }
         return CommitSet.areCustomRootsEqual(this._customRoots, other._customRoots);
     }
@@ -139,7 +157,7 @@ class MeasurementCommitSet extends CommitSet {
     }
 
     // Use CommitSet's static maps because MeasurementCommitSet and CommitSet are logically of the same type.
-    // FIXME: Idaelly, DataModel should take care of this but traversing prototype chain is expensive.
+    // FIXME: Ideally, DataModel should take care of this but traversing prototype chain is expensive.
     namedStaticMap(name) { return CommitSet.namedStaticMap(name); }
     ensureNamedStaticMap(name) { return CommitSet.ensureNamedStaticMap(name); }
     static namedStaticMap(name) { return CommitSet.namedStaticMap(name); }
@@ -160,11 +178,11 @@ class CustomCommitSet {
         this._customRoots = [];
     }
 
-    setRevisionForRepository(repository, revision, patch = null)
+    setRevisionForRepository(repository, revision, patch = null, ownerRevision = null)
     {
         console.assert(repository instanceof Repository);
         console.assert(!patch || patch instanceof UploadedFile);
-        this._revisionListByRepository.set(repository, {revision, patch});
+        this._revisionListByRepository.set(repository, {revision, patch, ownerRevision});
     }
 
     equals(other)
@@ -172,13 +190,14 @@ class CustomCommitSet {
         console.assert(other instanceof CustomCommitSet);
         if (this._revisionListByRepository.size != other._revisionListByRepository.size)
             return false;
-        for (let repository of this._revisionListByRepository.keys()) {
-            const thisRevision = this._revisionListByRepository.get(repository);
+
+        for (const [repository, thisRevision] of this._revisionListByRepository) {
             const otherRevision = other._revisionListByRepository.get(repository);
             if (!thisRevision != !otherRevision)
                 return false;
             if (thisRevision && (thisRevision.revision != otherRevision.revision
-                || thisRevision.patch != otherRevision.patch))
+                || thisRevision.patch != otherRevision.patch
+                || thisRevision.ownerRevision != otherRevision.ownerRevision))
                 return false;
         }
         return CommitSet.areCustomRootsEqual(this._customRoots, other._customRoots);
@@ -199,6 +218,13 @@ class CustomCommitSet {
             return null;
         return entry.patch;
     }
+    ownerRevisionForRepository(repository)
+    {
+        const entry = this._revisionListByRepository.get(repository);
+        if (!entry)
+            return null;
+        return entry.ownerRevision;
+    }
     customRoots() { return this._customRoots; }
 
     addCustomRoot(uploadedFile)
index 4b3fb8c..0c0dfad 100644 (file)
@@ -231,6 +231,7 @@ class TestGroup extends LabeledObject {
                 const patchFile = commitSet.patchForRepository(repository);
                 revisionSet[repository.id()] = {
                     revision: commitSet.revisionForRepository(repository),
+                    ownerRevision: commitSet.ownerRevisionForRepository(repository),
                     patch: patchFile ? patchFile.id() : null,
                 };
             }
index ffdae84..1782b30 100644 (file)
@@ -27,7 +27,44 @@ describe('/api/build-requests', function () {
         });
     });
 
-    it('should return build requets associated with a given triggerable with appropriate commits and commitSets', () => {
+    it('should return build requests associated with a given triggerable with appropriate commits and commitSets with owned components', () => {
+        return MockData.addTestGroupWithOwnedCommits(TestServer.database()).then(() => {
+            return TestServer.remoteAPI().getJSONWithStatus('/api/build-requests/build-webkit');
+        }).then((content) => {
+            assert.deepEqual(Object.keys(content).sort(), ['buildRequests', 'commitSets', 'commits', 'status', 'uploadedFiles']);
+
+            assert.equal(content['commitSets'].length, 2);
+            assert.equal(content['commitSets'][0].id, 403);
+            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: null}]);
+            assert.equal(content['commitSets'][1].id, 404);
+            assert.deepEqual(content['commitSets'][1].revisionItems,
+                [{commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+                {commit: '96336', commitOwner: null, patch: null, requiresBuild: false, rootFile: null},
+                {commit: '2017', commitOwner: "96336", patch: null, requiresBuild: true, rootFile: null}]);
+
+            assert.equal(content['commits'].length, 5);
+            assert.deepEqual(content['commits'][0], {commitOwner: null, id: 87832, repository: '9', revision: '10.11 15A284', time: 0});
+            assert.deepEqual(content['commits'][1], {commitOwner: null, id: 93116, repository: '11', revision: '191622', time: 1445945816878})
+            assert.deepEqual(content['commits'][2], {commitOwner: '93116', id: '1797', repository: '213', revision: 'owned-jsc-6161', time: 1456960795300})
+            assert.deepEqual(content['commits'][3], {commitOwner: null, id: 96336, repository: '11', revision: '192736', time: 1448225325650})
+            assert.deepEqual(content['commits'][4], {commitOwner: '96336', id: 2017, repository: '213', revision: 'owned-jsc-9191', time: 1462230837100})
+
+            assert.equal(content['buildRequests'].length, 4);
+            content['buildRequests'][0]['createdAt'] = 0;
+            content['buildRequests'][1]['createdAt'] = 0;
+            content['buildRequests'][2]['createdAt'] = 0;
+            content['buildRequests'][3]['createdAt'] = 0;
+            assert.deepEqual(content['buildRequests'][0], {id: '704', task: '1080', triggerable: '1000', repositoryGroup: '2001', test: '200', platform: '65', testGroup: '900', order: '0', commitSet: '403', status: 'pending', url: null, build: null, createdAt: 0});
+            assert.deepEqual(content['buildRequests'][1], {id: '705', task: '1080', triggerable: '1000', repositoryGroup: '2001', test: '200', platform: '65', testGroup: '900', order: '1', commitSet: '404', status: 'pending', url: null, build: null, createdAt: 0});
+            assert.deepEqual(content['buildRequests'][2], {id: '706', task: '1080', triggerable: '1000', repositoryGroup: '2001', test: '200', platform: '65', testGroup: '900', order: '2', commitSet: '403', status: 'pending', url: null, build: null, createdAt: 0});
+            assert.deepEqual(content['buildRequests'][3], {id: '707', task: '1080', triggerable: '1000', repositoryGroup: '2001', test: '200', platform: '65', testGroup: '900', order: '3', commitSet: '404', status: 'pending', url: null, build: null, createdAt: 0});
+       });
+    });
+
+    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');
         }).then((content) => {
@@ -36,10 +73,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, rootFile: null}, {commit: '93116', patch: null, rootFile: null}]);
+                [{commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null}, {commit: '93116', commitOwner: null, patch: null, requiresBuild: false, rootFile: null}]);
             assert.equal(content['commitSets'][1].id, 402);
             assert.deepEqual(content['commitSets'][1].revisionItems,
-                [{commit: '87832', patch: null, rootFile: null}, {commit: '96336', patch: null, rootFile: null}]);
+                [{commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null}, {commit: '96336', commitOwner: null, patch: null, requiresBuild: false, rootFile: null}]);
 
             assert.equal(content['commits'].length, 3);
             assert.equal(content['commits'][0].id, 87832);
@@ -92,10 +129,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, rootFile: null}, {commit: '93116', patch: null, rootFile: null}]);
+                [{commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null}, {commit: '93116', commitOwner: null, patch: null, requiresBuild: false, rootFile: null}]);
             assert.equal(content['commitSets'][1].id, 402);
             assert.deepEqual(content['commitSets'][1].revisionItems,
-                [{commit: '87832', patch: null, rootFile: null}, {commit: '96336', patch: null, rootFile: null}]);
+                [{commit: '87832', commitOwner: null, patch: null, requiresBuild: false, rootFile: null}, {commit: '96336', commitOwner: null, patch: null, requiresBuild: false, rootFile: null}]);
 
             assert.equal(content['commits'].length, 3);
             assert.equal(content['commits'][0].id, 87832);
@@ -139,6 +176,111 @@ describe('/api/build-requests', function () {
         });
     });
 
+    it('should be fetchable by BuildRequest.fetchForTriggerable for commitSets with owned commits', () => {
+        return MockData.addTestGroupWithOwnedCommits(TestServer.database()).then(() => {
+            return Manifest.fetch();
+        }).then(() => {
+            return BuildRequest.fetchForTriggerable('build-webkit');
+        }).then((buildRequests) => {
+            assert.equal(buildRequests.length, 4);
+
+            let test = Test.findById(200);
+            assert(test);
+
+            let platform = Platform.findById(65);
+            assert(platform);
+
+            assert.equal(buildRequests[0].id(), 704);
+            assert.equal(buildRequests[0].testGroupId(), 900);
+            assert.equal(buildRequests[0].test(), test);
+            assert.equal(buildRequests[0].platform(), platform);
+            assert.equal(buildRequests[0].order(), 0);
+            assert.ok(buildRequests[0].commitSet() instanceof CommitSet);
+            assert.ok(!buildRequests[0].hasFinished());
+            assert.ok(!buildRequests[0].hasStarted());
+            assert.ok(buildRequests[0].isPending());
+            assert.equal(buildRequests[0].statusLabel(), 'Waiting');
+
+            assert.equal(buildRequests[1].id(), 705);
+            assert.equal(buildRequests[1].testGroupId(), 900);
+            assert.equal(buildRequests[1].test(), test);
+            assert.equal(buildRequests[1].platform(), platform);
+            assert.equal(buildRequests[1].order(), 1);
+            assert.ok(buildRequests[1].commitSet() instanceof CommitSet);
+            assert.ok(!buildRequests[1].hasFinished());
+            assert.ok(!buildRequests[1].hasStarted());
+            assert.ok(buildRequests[1].isPending());
+            assert.equal(buildRequests[1].statusLabel(), 'Waiting');
+
+            assert.equal(buildRequests[2].id(), 706);
+            assert.equal(buildRequests[2].testGroupId(), 900);
+            assert.equal(buildRequests[2].test(), test);
+            assert.equal(buildRequests[2].platform(), platform);
+            assert.equal(buildRequests[2].order(), 2);
+            assert.ok(buildRequests[2].commitSet() instanceof CommitSet);
+            assert.ok(!buildRequests[2].hasFinished());
+            assert.ok(!buildRequests[2].hasStarted());
+            assert.ok(buildRequests[2].isPending());
+            assert.equal(buildRequests[2].statusLabel(), 'Waiting');
+
+            assert.equal(buildRequests[3].id(), 707);
+            assert.equal(buildRequests[3].testGroupId(), 900);
+            assert.equal(buildRequests[3].test(), test);
+            assert.equal(buildRequests[3].platform(), platform);
+            assert.equal(buildRequests[3].order(), 3);
+            assert.ok(buildRequests[3].commitSet() instanceof CommitSet);
+            assert.ok(!buildRequests[3].hasFinished());
+            assert.ok(!buildRequests[3].hasStarted());
+            assert.ok(buildRequests[3].isPending());
+            assert.equal(buildRequests[3].statusLabel(), 'Waiting');
+
+            const osx = Repository.findById(9);
+            assert.equal(osx.name(), 'macOS');
+
+            const webkit = Repository.findById(11);
+            assert.equal(webkit.name(), 'WebKit');
+
+            const jsc = Repository.findById(213);
+            assert.equal(jsc.name(), 'JavaScriptCore');
+
+            const firstCommitSet = buildRequests[0].commitSet();
+            assert.equal(buildRequests[2].commitSet(), firstCommitSet);
+
+            const secondCommitSet = buildRequests[1].commitSet();
+            assert.equal(buildRequests[3].commitSet(), secondCommitSet);
+
+            assert.equal(firstCommitSet.revisionForRepository(osx), '10.11 15A284');
+            assert.equal(firstCommitSet.revisionForRepository(webkit), '191622');
+            assert.equal(firstCommitSet.revisionForRepository(jsc), 'owned-jsc-6161');
+            assert.equal(firstCommitSet.ownerRevisionForRepository(jsc), '191622');
+
+            assert.equal(secondCommitSet.revisionForRepository(osx), '10.11 15A284');
+            assert.equal(secondCommitSet.revisionForRepository(webkit), '192736');
+            assert.equal(secondCommitSet.revisionForRepository(jsc), 'owned-jsc-9191');
+            assert.equal(secondCommitSet.ownerRevisionForRepository(jsc), '192736');
+
+            const osxCommit = firstCommitSet.commitForRepository(osx);
+            assert.equal(osxCommit.revision(), '10.11 15A284');
+            assert.equal(osxCommit, secondCommitSet.commitForRepository(osx));
+
+            const firstWebKitCommit = firstCommitSet.commitForRepository(webkit);
+            assert.equal(firstWebKitCommit.revision(), '191622');
+            assert.equal(+firstWebKitCommit.time(), 1445945816878);
+
+            const secondWebKitCommit = secondCommitSet.commitForRepository(webkit);
+            assert.equal(secondWebKitCommit.revision(), '192736');
+            assert.equal(+secondWebKitCommit.time(), 1448225325650);
+
+            const firstSJCCommit = firstCommitSet.commitForRepository(jsc);
+            assert.equal(firstSJCCommit.revision(), 'owned-jsc-6161');
+            assert.equal(+firstSJCCommit.time(), 1456960795300);
+
+            const secondSJCCommit = secondCommitSet.commitForRepository(jsc);
+            assert.equal(secondSJCCommit.revision(), 'owned-jsc-9191');
+            assert.equal(+secondSJCCommit.time(), 1462230837100)
+        });
+    });
+
     it('should be fetchable by BuildRequest.fetchForTriggerable', () => {
         return MockData.addMockData(TestServer.database()).then(() => {
             return Manifest.fetch();
index 98d7296..9504421 100644 (file)
@@ -113,6 +113,11 @@ function addTriggerableAndCreateTask(name, webkitRevisions)
                 {repository: MockData.macosRepositoryId(), acceptsPatch: false},
                 {repository: MockData.webkitRepositoryId(), acceptsPatch: true}
             ]},
+            {name: 'system-webkit-sjc', acceptsRoot: true, repositories: [
+                {repository: MockData.macosRepositoryId(), acceptsPatch: false},
+                {repository: MockData.jscRepositoryId(), acceptsPatch: false},
+                {repository: MockData.webkitRepositoryId(), acceptsPatch: true}
+            ]},
         ]
     };
     return MockData.addMockData(TestServer.database()).then(() => {
@@ -274,14 +279,14 @@ describe('/privileged-api/create-test-group', function () {
         });
     });
 
-    it('should return "AmbigiousRevision" when there are multiple commits that match the specified revision string', () => {
+    it('should return "AmbiguousRevision" when there are multiple commits that match the specified revision string', () => {
         return addTriggerableAndCreateTask('some task', ['2ceda45d3cd63cde58d0dbf5767714e03d902e43', '2c71a8ddc1f661663ccfd1a29c633ba57e879533']).then((taskId) => {
             const webkit = Repository.all().find((repository) => repository.name() == 'WebKit');
             const revisionSets = [{[webkit.id()]: {revision: '2ceda'}}, {[webkit.id()]: {revision: '2c'}}];
             return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, revisionSets}).then((content) => {
                 assert(false, 'should never be reached');
             }, (error) => {
-                assert.equal(error, 'AmbigiousRevision');
+                assert.equal(error, 'AmbiguousRevision');
             });
         });
     });
@@ -345,6 +350,70 @@ describe('/privileged-api/create-test-group', function () {
         });
     });
 
+    it('should return "InvalidOwnerRevision" when commit ownership is not valid', () => {
+        let taskId;
+        return addTriggerableAndCreateTask('some task').then((id) => taskId = id).then(() => {
+            const webkit = Repository.all().filter((repository) => repository.name() == 'WebKit')[0];
+            const macos = Repository.all().filter((repository) => repository.name() == 'macOS')[0];
+            const jsc = Repository.all().filter((repository) => repository.name() == 'JavaScriptCore')[0];
+            const revisionSets = [{[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}, [jsc.id()]: {revision: 'owned-jsc-6161', ownerRevision: '191621'}},
+                {[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}, [jsc.id()]: {revision: 'owned-jsc-9191', ownerRevision: '191622'}}];
+            return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, revisionSets});
+        }).then(() => {
+            assert(false, 'should never be reached');
+        }, (error) => {
+            assert.equal(error, 'InvalidOwnerRevision');
+        });
+    });
+
+    it('should return "InvalidCommitOwnership" when commit ownership is not valid', () => {
+        let taskId;
+        return addTriggerableAndCreateTask('some task').then((id) => taskId = id).then(() => {
+            const webkit = Repository.all().filter((repository) => repository.name() == 'WebKit')[0];
+            const macos = Repository.all().filter((repository) => repository.name() == 'macOS')[0];
+            const jsc = Repository.all().filter((repository) => repository.name() == 'JavaScriptCore')[0];
+            const revisionSets = [{[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}, [jsc.id()]: {revision: 'owned-jsc-6161', ownerRevision: '191622'}},
+                {[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}, [jsc.id()]: {revision: 'owned-jsc-9191', ownerRevision: '191622'}}];
+            return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, revisionSets});
+        }).then(() => {
+            assert(false, 'should never be reached');
+        }, (error) => {
+            assert.equal(error, 'InvalidCommitOwnership');
+        });
+    });
+
+    it('should return "CommitOwnerMustExistInCommitSet" when owner of a commit is missing in the commit set', () => {
+        let taskId;
+        return addTriggerableAndCreateTask('some task').then((id) => taskId = id).then(() => {
+            const webkit = Repository.all().filter((repository) => repository.name() == 'WebKit')[0];
+            const macos = Repository.all().filter((repository) => repository.name() == 'macOS')[0];
+            const jsc = Repository.all().filter((repository) => repository.name() == 'JavaScriptCore')[0];
+            const revisionSets = [{[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}, [jsc.id()]: {revision: 'owned-jsc-6161', ownerRevision: '191622'}},
+                {[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}, [jsc.id()]: {revision: 'owned-jsc-9191', ownerRevision: '192736'}}];
+            return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, revisionSets});
+        }).then(() => {
+            assert(false, 'should never be reached');
+        }, (error) => {
+            assert.equal(error, 'CommitOwnerMustExistInCommitSet');
+        });
+    });
+
+    it('should return "CommitOwnerMustExistInCommitSet" when repository of owner commit is not specified at all', () => {
+        let taskId;
+        return addTriggerableAndCreateTask('some task').then((id) => taskId = id).then(() => {
+            const webkit = Repository.all().filter((repository) => repository.name() == 'WebKit')[0];
+            const macos = Repository.all().filter((repository) => repository.name() == 'macOS')[0];
+            const jsc = Repository.all().filter((repository) => repository.name() == 'JavaScriptCore')[0];
+            const revisionSets = [{[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}, [jsc.id()]: {revision: 'owned-jsc-6161', ownerRevision: '191622'}},
+                {[macos.id()]: {revision: '15A284'}, [jsc.id()]: {revision: 'owned-jsc-9191', ownerRevision: '192736'}}];
+            return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, revisionSets});
+        }).then(() => {
+            assert(false, 'should never be reached');
+        }, (error) => {
+            assert.equal(error, 'CommitOwnerMustExistInCommitSet');
+        });
+    });
+
     it('should create a test group from commitSets with the repetition count of one when repetitionCount is omitted', () => {
         return addTriggerableAndCreateTask('some task').then((taskId) => {
             let insertedGroupId;
@@ -582,7 +651,7 @@ describe('/privileged-api/create-test-group', function () {
         });
     });
 
-    it('should create a test group with a patch', () => {
+    it('should create a build test group with a patch', () => {
         let taskId;
         let webkit;
         let macos;
@@ -651,6 +720,346 @@ describe('/privileged-api/create-test-group', function () {
         });
     });
 
+    it('should create a build test group with a owned commits even when one of group does not contain an owned commit', () => {
+        let taskId;
+        let webkit;
+        let jsc;
+        let macos;
+        let insertedGroupId;
+        return addTriggerableAndCreateTask('some task').then((id) => taskId = id).then(() => {
+            webkit = Repository.all().filter((repository) => repository.name() == 'WebKit')[0];
+            macos = Repository.all().filter((repository) => repository.name() == 'macOS')[0];
+            jsc = Repository.all().filter((repository) => repository.name() == 'JavaScriptCore')[0];
+            const revisionSets = [{[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}},
+                {[webkit.id()]: {revision: '192736'}, [macos.id()]: {revision: '15A284'}, [jsc.id()]: {revision: 'owned-jsc-9191', ownerRevision: '192736'}}];
+            return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, revisionSets});
+        }).then((content) => {
+            insertedGroupId = content['testGroupId'];
+            return TestGroup.fetchForTask(taskId, true);
+        }).then((testGroups) => {
+            assert.equal(testGroups.length, 1);
+            const group = testGroups[0];
+            assert.equal(group.id(), insertedGroupId);
+            assert.equal(group.repetitionCount(), 2);
+            assert.equal(group.test(), Test.findById(MockData.someTestId()));
+            assert.equal(group.platform(), Platform.findById(MockData.somePlatformId()));
+            const requests = group.buildRequests();
+            assert.equal(requests.length, 6);
+
+            assert.equal(requests[0].isBuild(), true);
+            assert.equal(requests[1].isBuild(), true);
+            assert.equal(requests[2].isBuild(), false);
+            assert.equal(requests[3].isBuild(), false);
+            assert.equal(requests[4].isBuild(), false);
+            assert.equal(requests[5].isBuild(), false);
+
+            assert.equal(requests[0].isTest(), false);
+            assert.equal(requests[1].isTest(), false);
+            assert.equal(requests[2].isTest(), true);
+            assert.equal(requests[3].isTest(), true);
+            assert.equal(requests[4].isTest(), true);
+            assert.equal(requests[5].isTest(), true);
+
+            const set0 = requests[0].commitSet();
+            const set1 = requests[1].commitSet();
+            assert.equal(requests[2].commitSet(), set0);
+            assert.equal(requests[3].commitSet(), set1);
+            assert.equal(requests[4].commitSet(), set0);
+            assert.equal(requests[5].commitSet(), set1);
+            assert.deepEqual(Repository.sortByNamePreferringOnesWithURL(set0.repositories()), [webkit, macos]);
+            assert.deepEqual(set0.customRoots(), []);
+            assert.deepEqual(Repository.sortByNamePreferringOnesWithURL(set1.repositories()), [jsc, webkit, macos]);
+            assert.deepEqual(set1.customRoots(), []);
+            assert.equal(set0.revisionForRepository(webkit), '191622');
+            assert.equal(set1.revisionForRepository(webkit), '192736');
+            assert.equal(set0.patchForRepository(webkit), null);
+            assert.equal(set1.patchForRepository(webkit), null);
+            assert.equal(set0.requiresBuildForRepository(webkit), false);
+            assert.equal(set1.requiresBuildForRepository(webkit), false);
+            assert.equal(set0.revisionForRepository(macos), '15A284');
+            assert.equal(set0.revisionForRepository(macos), set1.revisionForRepository(macos));
+            assert.equal(set0.commitForRepository(macos), set1.commitForRepository(macos));
+            assert.equal(set0.patchForRepository(macos), null);
+            assert.equal(set1.patchForRepository(macos), null);
+            assert.equal(set0.requiresBuildForRepository(macos), false);
+            assert.equal(set1.requiresBuildForRepository(macos), false);
+            assert.equal(set0.revisionForRepository(jsc), null);
+            assert.equal(set1.revisionForRepository(jsc), 'owned-jsc-9191');
+            assert.equal(set0.patchForRepository(jsc), null);
+            assert.equal(set1.patchForRepository(jsc), null);
+            assert.equal(set0.ownerRevisionForRepository(jsc), null);
+            assert.equal(set1.ownerRevisionForRepository(jsc), '192736');
+            assert.equal(set1.requiresBuildForRepository(jsc), true);
+            assert(!set0.equals(set1));
+        });
+    });
+
+    it('should create a test group with a owned commits even when no patch is specified', () => {
+        let taskId;
+        let webkit;
+        let jsc;
+        let macos;
+        let insertedGroupId;
+        return addTriggerableAndCreateTask('some task').then((id) => taskId = id).then(() => {
+            webkit = Repository.all().filter((repository) => repository.name() == 'WebKit')[0];
+            macos = Repository.all().filter((repository) => repository.name() == 'macOS')[0];
+            jsc = Repository.all().filter((repository) => repository.name() == 'JavaScriptCore')[0];
+            const revisionSets = [{[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}, [jsc.id()]: {revision: 'owned-jsc-6161', ownerRevision: '191622'}},
+                {[webkit.id()]: {revision: '192736'}, [macos.id()]: {revision: '15A284'}, [jsc.id()]: {revision: 'owned-jsc-9191', ownerRevision: '192736'}}];
+            return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, revisionSets});
+        }).then((content) => {
+            insertedGroupId = content['testGroupId'];
+            return TestGroup.fetchForTask(taskId, true);
+        }).then((testGroups) => {
+            assert.equal(testGroups.length, 1);
+            const group = testGroups[0];
+            assert.equal(group.id(), insertedGroupId);
+            assert.equal(group.repetitionCount(), 2);
+            assert.equal(group.test(), Test.findById(MockData.someTestId()));
+            assert.equal(group.platform(), Platform.findById(MockData.somePlatformId()));
+            const requests = group.buildRequests();
+            assert.equal(requests.length, 6);
+
+            assert.equal(requests[0].isBuild(), true);
+            assert.equal(requests[1].isBuild(), true);
+            assert.equal(requests[2].isBuild(), false);
+            assert.equal(requests[3].isBuild(), false);
+            assert.equal(requests[4].isBuild(), false);
+            assert.equal(requests[5].isBuild(), false);
+
+            assert.equal(requests[0].isTest(), false);
+            assert.equal(requests[1].isTest(), false);
+            assert.equal(requests[2].isTest(), true);
+            assert.equal(requests[3].isTest(), true);
+            assert.equal(requests[4].isTest(), true);
+            assert.equal(requests[5].isTest(), true);
+
+            const set0 = requests[0].commitSet();
+            const set1 = requests[1].commitSet();
+            assert.equal(requests[2].commitSet(), set0);
+            assert.equal(requests[3].commitSet(), set1);
+            assert.equal(requests[4].commitSet(), set0);
+            assert.equal(requests[5].commitSet(), set1);
+            assert.deepEqual(Repository.sortByNamePreferringOnesWithURL(set0.repositories()), [jsc, webkit, macos]);
+            assert.deepEqual(set0.customRoots(), []);
+            assert.deepEqual(Repository.sortByNamePreferringOnesWithURL(set1.repositories()), [jsc, webkit, macos]);
+            assert.deepEqual(set1.customRoots(), []);
+            assert.equal(set0.revisionForRepository(webkit), '191622');
+            assert.equal(set1.revisionForRepository(webkit), '192736');
+            assert.equal(set0.patchForRepository(webkit), null);
+            assert.equal(set1.patchForRepository(webkit), null);
+            assert.equal(set0.requiresBuildForRepository(webkit), false);
+            assert.equal(set1.requiresBuildForRepository(webkit), false);
+            assert.equal(set0.revisionForRepository(macos), '15A284');
+            assert.equal(set0.revisionForRepository(macos), set1.revisionForRepository(macos));
+            assert.equal(set0.commitForRepository(macos), set1.commitForRepository(macos));
+            assert.equal(set0.patchForRepository(macos), null);
+            assert.equal(set1.patchForRepository(macos), null);
+            assert.equal(set0.requiresBuildForRepository(macos), false);
+            assert.equal(set1.requiresBuildForRepository(macos), false);
+            assert.equal(set0.revisionForRepository(jsc), 'owned-jsc-6161');
+            assert.equal(set1.revisionForRepository(jsc), 'owned-jsc-9191');
+            assert.equal(set0.patchForRepository(jsc), null);
+            assert.equal(set1.patchForRepository(jsc), null);
+            assert.equal(set0.ownerRevisionForRepository(jsc), '191622');
+            assert.equal(set1.ownerRevisionForRepository(jsc), '192736');
+            assert.equal(set0.requiresBuildForRepository(jsc), true);
+            assert.equal(set1.requiresBuildForRepository(jsc), true);
+            assert(!set0.equals(set1));
+        });
+    });
+
+    it('should create a test group with a owned commits and a patch', () => {
+        let taskId;
+        let webkit;
+        let macos;
+        let jsc;
+        let insertedGroupId;
+        let uploadedFile;
+        return addTriggerableAndCreateTask('some task').then((id) => taskId = id).then(() => {
+            webkit = Repository.all().filter((repository) => repository.name() == 'WebKit')[0];
+            macos = Repository.all().filter((repository) => repository.name() == 'macOS')[0];
+            jsc = Repository.all().filter((repository) => repository.name() == 'JavaScriptCore')[0];
+            return TemporaryFile.makeTemporaryFile('some.dat', 'some content');
+        }).then((stream) => {
+            return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true});
+        }).then((response) => {
+            const rawFile = response['uploadedFile'];
+            uploadedFile = UploadedFile.ensureSingleton(rawFile.id, rawFile);
+            const revisionSets = [{[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}, [jsc.id()]: {revision: 'owned-jsc-6161', ownerRevision: '191622'}},
+                {[webkit.id()]: {revision: '192736', patch: uploadedFile.id()}, [macos.id()]: {revision: '15A284'}, [jsc.id()]: {revision: 'owned-jsc-9191', ownerRevision: '192736'}}];
+            return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, revisionSets});
+        }).then((content) => {
+            insertedGroupId = content['testGroupId'];
+            return TestGroup.fetchForTask(taskId, true);
+        }).then((testGroups) => {
+            assert.equal(testGroups.length, 1);
+            const group = testGroups[0];
+            assert.equal(group.id(), insertedGroupId);
+            assert.equal(group.repetitionCount(), 2);
+            assert.equal(group.test(), Test.findById(MockData.someTestId()));
+            assert.equal(group.platform(), Platform.findById(MockData.somePlatformId()));
+            const requests = group.buildRequests();
+            assert.equal(requests.length, 6);
+
+            assert.equal(requests[0].isBuild(), true);
+            assert.equal(requests[1].isBuild(), true);
+            assert.equal(requests[2].isBuild(), false);
+            assert.equal(requests[3].isBuild(), false);
+            assert.equal(requests[4].isBuild(), false);
+            assert.equal(requests[5].isBuild(), false);
+
+            assert.equal(requests[0].isTest(), false);
+            assert.equal(requests[1].isTest(), false);
+            assert.equal(requests[2].isTest(), true);
+            assert.equal(requests[3].isTest(), true);
+            assert.equal(requests[4].isTest(), true);
+            assert.equal(requests[5].isTest(), true);
+
+            const set0 = requests[0].commitSet();
+            const set1 = requests[1].commitSet();
+            assert.equal(requests[2].commitSet(), set0);
+            assert.equal(requests[3].commitSet(), set1);
+            assert.equal(requests[4].commitSet(), set0);
+            assert.equal(requests[5].commitSet(), set1);
+            assert.deepEqual(Repository.sortByNamePreferringOnesWithURL(set0.repositories()), [jsc, webkit, macos]);
+            assert.deepEqual(set0.customRoots(), []);
+            assert.deepEqual(Repository.sortByNamePreferringOnesWithURL(set1.repositories()), [jsc, webkit, macos]);
+            assert.deepEqual(set1.customRoots(), []);
+
+            assert.equal(set0.revisionForRepository(webkit), '191622');
+            assert.equal(set1.revisionForRepository(webkit), '192736');
+            assert.equal(set0.patchForRepository(webkit), null);
+            assert.equal(set1.patchForRepository(webkit), uploadedFile);
+            assert.equal(set0.ownerRevisionForRepository(webkit), null);
+            assert.equal(set1.ownerRevisionForRepository(webkit), null);
+            assert.equal(set0.requiresBuildForRepository(webkit), true);
+            assert.equal(set1.requiresBuildForRepository(webkit), true);
+
+            assert.equal(set0.revisionForRepository(macos), '15A284');
+            assert.equal(set0.revisionForRepository(macos), set1.revisionForRepository(macos));
+            assert.equal(set0.commitForRepository(macos), set1.commitForRepository(macos));
+            assert.equal(set0.patchForRepository(macos), null);
+            assert.equal(set1.patchForRepository(macos), null);
+            assert.equal(set0.ownerRevisionForRepository(macos), null);
+            assert.equal(set1.ownerRevisionForRepository(macos), null);
+            assert.equal(set0.requiresBuildForRepository(macos), false);
+            assert.equal(set1.requiresBuildForRepository(macos), false);
+
+            assert.equal(set0.revisionForRepository(jsc), 'owned-jsc-6161');
+            assert.equal(set1.revisionForRepository(jsc), 'owned-jsc-9191');
+            assert.equal(set0.patchForRepository(jsc), null);
+            assert.equal(set1.patchForRepository(jsc), null);
+            assert.equal(set0.ownerRevisionForRepository(jsc), '191622');
+            assert.equal(set1.ownerRevisionForRepository(jsc), '192736');
+            assert.equal(set0.requiresBuildForRepository(jsc), true);
+            assert.equal(set1.requiresBuildForRepository(jsc), true);
+            assert(!set0.equals(set1));
+        });
+    });
+
+    it('should still work even if components with same name but one is owned, one is not', () => {
+        let taskId;
+        let webkit;
+        let macos;
+        let jsc;
+        let ownedJSC;
+        let insertedGroupId;
+        let uploadedFile;
+        return addTriggerableAndCreateTask('some task').then((id) => taskId = id).then(() => {
+            return MockData.addAnotherTriggerable(TestServer.database());
+        }).then(() => {
+            webkit = Repository.all().filter((repository) => repository.name() == 'WebKit')[0];
+            macos = Repository.all().filter((repository) => repository.name() == 'macOS')[0];
+            jsc = Repository.all().filter((repository) => repository.name() == 'JavaScriptCore' && !repository.ownerId())[0];
+            ownedJSC = Repository.all().filter((repository) => repository.name() == 'JavaScriptCore' && repository.ownerId())[0];
+            return TemporaryFile.makeTemporaryFile('some.dat', 'some content');
+        }).then((stream) => {
+            return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true});
+        }).then((response) => {
+            const rawFile = response['uploadedFile'];
+            uploadedFile = UploadedFile.ensureSingleton(rawFile.id, rawFile);
+            const revisionSets = [{[jsc.id()]: {revision: 'jsc-6161'}, [webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}, [ownedJSC.id()]: {revision: 'owned-jsc-6161', ownerRevision: '191622'}},
+                {[jsc.id()]: {revision: 'jsc-9191'}, [webkit.id()]: {revision: '192736', patch: uploadedFile.id()}, [macos.id()]: {revision: '15A284'}, [ownedJSC.id()]: {revision: 'owned-jsc-9191', ownerRevision: '192736'}}];
+            return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, revisionSets});
+        }).then((content) => {
+            insertedGroupId = content['testGroupId'];
+            return TestGroup.fetchForTask(taskId, true);
+        }).then((testGroups) => {
+            assert.equal(testGroups.length, 1);
+            const group = testGroups[0];
+            assert.equal(group.id(), insertedGroupId);
+            assert.equal(group.repetitionCount(), 2);
+            assert.equal(group.test(), Test.findById(MockData.someTestId()));
+            assert.equal(group.platform(), Platform.findById(MockData.somePlatformId()));
+            const requests = group.buildRequests();
+            assert.equal(requests.length, 6);
+
+            assert.equal(requests[0].isBuild(), true);
+            assert.equal(requests[1].isBuild(), true);
+            assert.equal(requests[2].isBuild(), false);
+            assert.equal(requests[3].isBuild(), false);
+            assert.equal(requests[4].isBuild(), false);
+            assert.equal(requests[5].isBuild(), false);
+
+            assert.equal(requests[0].isTest(), false);
+            assert.equal(requests[1].isTest(), false);
+            assert.equal(requests[2].isTest(), true);
+            assert.equal(requests[3].isTest(), true);
+            assert.equal(requests[4].isTest(), true);
+            assert.equal(requests[5].isTest(), true);
+
+            const set0 = requests[0].commitSet();
+            const set1 = requests[1].commitSet();
+            assert.equal(requests[2].commitSet(), set0);
+            assert.equal(requests[3].commitSet(), set1);
+            assert.equal(requests[4].commitSet(), set0);
+            assert.equal(requests[5].commitSet(), set1);
+            assert.deepEqual(Repository.sortByNamePreferringOnesWithURL(set0.repositories()), [jsc, ownedJSC, webkit, macos]);
+            assert.deepEqual(set0.customRoots(), []);
+            assert.deepEqual(Repository.sortByNamePreferringOnesWithURL(set1.repositories()), [jsc, ownedJSC, webkit, macos]);
+            assert.deepEqual(set1.customRoots(), []);
+
+            assert.equal(set0.revisionForRepository(webkit), '191622');
+            assert.equal(set1.revisionForRepository(webkit), '192736');
+            assert.equal(set0.patchForRepository(webkit), null);
+            assert.equal(set1.patchForRepository(webkit), uploadedFile);
+            assert.equal(set0.ownerRevisionForRepository(webkit), null);
+            assert.equal(set1.ownerRevisionForRepository(webkit), null);
+            assert.equal(set0.requiresBuildForRepository(webkit), true);
+            assert.equal(set1.requiresBuildForRepository(webkit), true);
+
+            assert.equal(set0.revisionForRepository(macos), '15A284');
+            assert.equal(set0.revisionForRepository(macos), set1.revisionForRepository(macos));
+            assert.equal(set0.commitForRepository(macos), set1.commitForRepository(macos));
+            assert.equal(set0.patchForRepository(macos), null);
+            assert.equal(set1.patchForRepository(macos), null);
+            assert.equal(set0.ownerRevisionForRepository(macos), null);
+            assert.equal(set1.ownerRevisionForRepository(macos), null);
+            assert.equal(set0.requiresBuildForRepository(macos), false);
+            assert.equal(set1.requiresBuildForRepository(macos), false);
+
+            assert.equal(set0.revisionForRepository(ownedJSC), 'owned-jsc-6161');
+            assert.equal(set1.revisionForRepository(ownedJSC), 'owned-jsc-9191');
+            assert.equal(set0.patchForRepository(ownedJSC), null);
+            assert.equal(set1.patchForRepository(ownedJSC), null);
+            assert.equal(set0.ownerRevisionForRepository(ownedJSC), '191622');
+            assert.equal(set1.ownerRevisionForRepository(ownedJSC), '192736');
+            assert.equal(set0.requiresBuildForRepository(ownedJSC), true);
+            assert.equal(set1.requiresBuildForRepository(ownedJSC), true);
+
+            assert.equal(set0.revisionForRepository(jsc), 'jsc-6161');
+            assert.equal(set1.revisionForRepository(jsc), 'jsc-9191');
+            assert.equal(set0.patchForRepository(jsc), null);
+            assert.equal(set1.patchForRepository(jsc), null);
+            assert.equal(set0.ownerRevisionForRepository(jsc), null);
+            assert.equal(set1.ownerRevisionForRepository(jsc), null);
+            assert.equal(set0.requiresBuildForRepository(jsc), false);
+            assert.equal(set1.requiresBuildForRepository(jsc), false);
+            assert(!set0.equals(set1));
+        });
+    });
+
     it('should not create a build request to build a patch when the commit set does not have a patch', () => {
         let taskId;
         let webkit;
@@ -719,7 +1128,6 @@ describe('/privileged-api/create-test-group', function () {
         let taskId;
         let webkit;
         let macos;
-        let insertedGroupId;
         let uploadedFile;
         return addTriggerableAndCreateTask('some task').then((id) => taskId = id).then(() => {
             webkit = Repository.all().filter((repository) => repository.name() == 'WebKit')[0];
@@ -731,7 +1139,7 @@ describe('/privileged-api/create-test-group', function () {
             const rawFile = response['uploadedFile'];
             uploadedFile = UploadedFile.ensureSingleton(rawFile.id, rawFile);
             const revisionSets = [{[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284', patch: uploadedFile.id()}},
-                {[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}}];
+                {[webkit.id()]: {revision: '192736'}, [macos.id()]: {revision: '15A284'}}];
             return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, revisionSets});
         }).then(() => {
             assert(false, 'should never be reached');
index 2c11218..1b902fa 100644 (file)
@@ -212,8 +212,8 @@ describe('/privileged-api/upload-file', function () {
         }).then((result) => {
             const fileB = result.uploadedFile;
             return Promise.all([
-                db.query('UPDATE commit_set_items SET commitset_patch_file = $1 WHERE commitset_set = 402 AND commitset_commit = 87832', [fileA.id]),
-                db.query('UPDATE commit_set_items SET commitset_patch_file = $1 WHERE commitset_set = 402 AND commitset_commit = 96336', [fileB.id])
+                db.query('UPDATE commit_set_items SET (commitset_patch_file, commitset_requires_build) = ($1, TRUE) WHERE commitset_set = 402 AND commitset_commit = 87832', [fileA.id]),
+                db.query('UPDATE commit_set_items SET (commitset_patch_file, commitset_requires_build) = ($1, TRUE) WHERE commitset_set = 402 AND commitset_commit = 96336', [fileB.id])
             ]);
         }).then(() => {
             return TemporaryFile.makeTemporaryFileOfSizeInMB('other.dat', limitInMB, 'c');
@@ -242,8 +242,8 @@ describe('/privileged-api/upload-file', function () {
         }).then((result) => {
             const fileB = result.uploadedFile;
             return Promise.all([
-                db.query('UPDATE commit_set_items SET commitset_patch_file = $1 WHERE commitset_set = 402 AND commitset_commit = 87832', [fileA.id]),
-                db.query('UPDATE commit_set_items SET commitset_patch_file = $1 WHERE commitset_set = 402 AND commitset_commit = 96336', [fileB.id])
+                db.query('UPDATE commit_set_items SET (commitset_patch_file, commitset_requires_build) = ($1, TRUE) WHERE commitset_set = 402 AND commitset_commit = 87832', [fileA.id]),
+                db.query('UPDATE commit_set_items SET (commitset_patch_file, commitset_requires_build) = ($1, TRUE) WHERE commitset_set = 402 AND commitset_commit = 96336', [fileB.id])
             ]);
         }).then(() => {
             return TemporaryFile.makeTemporaryFileOfSizeInMB('another.dat', limitInMB, 'c');
@@ -277,7 +277,7 @@ describe('/privileged-api/upload-file', function () {
             return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true});
         }).then((result) => {
             const fileB = result.uploadedFile;
-            return db.query(`UPDATE commit_set_items SET (commitset_patch_file, commitset_root_file) = ($1, $2)
+            return db.query(`UPDATE commit_set_items SET (commitset_patch_file, commitset_root_file, commitset_requires_build) = ($1, $2, TRUE)
                 WHERE commitset_set = 402 AND commitset_commit = 87832`, [fileA.id, fileB.id]);
         }).then(() => {
             return TemporaryFile.makeTemporaryFileOfSizeInMB('another.dat', limitInMB, 'c');
index e1a69af..73b7b37 100644 (file)
@@ -13,6 +13,8 @@ MockData = {
     otherPlatformId() { return 101; },
     macosRepositoryId() { return 9; },
     webkitRepositoryId() { return 11; },
+    ownedJSCRepositoryId() { return 213; },
+    jscRepositoryId() { return 222; },
     gitWebkitRepositoryId() { return 111; },
     sharedRepositoryId() { return 14; },
     addMockConfiguration: function (db)
@@ -23,6 +25,8 @@ MockData = {
             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('repositories', {id: this.ownedJSCRepositoryId(), owner: this.webkitRepositoryId(), name: 'JavaScriptCore'}),
+            db.insert('repositories', {id: this.jscRepositoryId(), name: 'JavaScriptCore'}),
             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}),
@@ -31,6 +35,12 @@ MockData = {
             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('commits', {id: 11797, repository: this.jscRepositoryId(), revision: 'jsc-6161', time: '2016-03-02T23:19:55.3Z'}),
+            db.insert('commits', {id: 12017, repository: this.jscRepositoryId(), revision: 'jsc-9191', time: '2016-05-02T23:13:57.1Z'}),
+            db.insert('commits', {id: 1797, repository: this.ownedJSCRepositoryId(), revision: 'owned-jsc-6161', time: '2016-03-02T23:19:55.3Z'}),
+            db.insert('commits', {id: 2017, repository: this.ownedJSCRepositoryId(), revision: 'owned-jsc-9191', time: '2016-05-02T23:13:57.1Z'}),
+            db.insert('commit_ownerships', {owner: 93116, owned: 1797}),
+            db.insert('commit_ownerships', {owner: 96336, owned: 2017}),
             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'}),
@@ -63,6 +73,15 @@ MockData = {
             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}),
         ]);
     },
+    addAnotherTriggerable(db) {
+        return Promise.all([
+            db.insert('build_triggerables', {id: 2345, name: 'build-webkit-jsc'}),
+            db.insert('triggerable_repository_groups', {id: 4002, name: 'mac-svnwebkit-jsc', triggerable: 2345}),
+            db.insert('triggerable_repositories', {repository: this.macosRepositoryId(), group: 4002}),
+            db.insert('triggerable_repositories', {repository: this.webkitRepositoryId(), group: 4002}),
+            db.insert('triggerable_repositories', {repository: this.jscRepositoryId(), group: 4002}),
+        ]);
+    },
     addEmptyTriggerable(db)
     {
         return Promise.all([
@@ -114,6 +133,32 @@ MockData = {
             db.insert('build_requests', {id: 711, status: statusList[1], triggerable, repository_group, platform, test, group: 601, order: 1, commit_set: 402}),
         ]);
     },
+    addTestGroupWithOwnedCommits(db, statusList)
+    {
+
+        if (!statusList)
+            statusList = ['pending', 'pending', 'pending', 'pending'];
+        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('analysis_test_groups', {id: 900, task: 1080, name: 'some test group with component test'}),
+            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}),
+            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: statusList[0], triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 900, order: 0, commit_set: 403}),
+            db.insert('build_requests', {id: 705, status: statusList[1], triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 900, order: 1, commit_set: 404}),
+            db.insert('build_requests', {id: 706, status: statusList[2], triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 900, order: 2, commit_set: 403}),
+            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}),
+        ]);
+    },
     mockTestSyncConfigWithSingleBuilder: function ()
     {
         return {
index 05307e5..aad8e1e 100644 (file)
@@ -16,6 +16,7 @@ importFromV3('models/build-request.js', 'BuildRequest');
 importFromV3('models/builder.js', 'Build');
 importFromV3('models/builder.js', 'Builder');
 importFromV3('models/commit-log.js', 'CommitLog');
+importFromV3('models/commit-set.js', 'CustomCommitSet')
 importFromV3('models/manifest.js', 'Manifest');
 importFromV3('models/measurement-adaptor.js', 'MeasurementAdaptor');
 importFromV3('models/measurement-cluster.js', 'MeasurementCluster');
diff --git a/Websites/perf.webkit.org/unit-tests/commit-set-tests.js b/Websites/perf.webkit.org/unit-tests/commit-set-tests.js
new file mode 100644 (file)
index 0000000..27c2d9e
--- /dev/null
@@ -0,0 +1,190 @@
+"use strict";
+
+const assert = require('assert');
+require('../tools/js/v3-models.js');
+const MockModels = require('./resources/mock-v3-models.js').MockModels;
+
+function createPatch()
+{
+    return new UploadedFile(453, {'createdAt': new Date('2017-05-01T19:16:53Z'), 'filename': 'patch.dat', 'extension': '.dat', 'author': 'some user',
+        size: 534637, sha256: '169463c8125e07c577110fe144ecd63942eb9472d438fc0014f474245e5df8a1'});
+}
+
+function createRoot()
+{
+    return new UploadedFile(456, {'createdAt': new Date('2017-05-01T21:03:27Z'), 'filename': 'root.dat', 'extension': '.dat', 'author': 'some user',
+        size: 16452234, sha256: '03eed7a8494ab8794c44b7d4308e55448fc56f4d6c175809ba968f78f656d58d'});
+}
+
+function customCommitSetWithoutOwnedCommit()
+{
+    const customCommitSet = new CustomCommitSet;
+    customCommitSet.setRevisionForRepository(MockModels.osx, '10.11.4 15E65');
+    customCommitSet.setRevisionForRepository(MockModels.webkit, '200805');
+    return customCommitSet;
+}
+
+function customCommitSetWithOwnedCommit()
+{
+    const customCommitSet = new CustomCommitSet;
+    customCommitSet.setRevisionForRepository(MockModels.osx, '10.11.4 15E65');
+    customCommitSet.setRevisionForRepository(MockModels.ownerRepository, 'OwnerRepository-r0');
+    customCommitSet.setRevisionForRepository(MockModels.ownedRepository, 'OwnedRepository-r0', null, 'OwnerRepository-r0');
+    return customCommitSet;
+}
+
+function customCommitSetWithPatch()
+{
+    const customCommitSet = new CustomCommitSet;
+    const patch = createPatch();
+    customCommitSet.setRevisionForRepository(MockModels.osx, '10.11.4 15E65');
+    customCommitSet.setRevisionForRepository(MockModels.webkit, '200805', patch);
+    return customCommitSet;
+}
+
+function customCommitSetWithOwnedRepositoryHasSameNameAsNotOwnedRepository()
+{
+    const customCommitSet = new CustomCommitSet;
+    customCommitSet.setRevisionForRepository(MockModels.osx, '10.11.4 15E65');
+    customCommitSet.setRevisionForRepository(MockModels.webkit, '200805');
+    customCommitSet.setRevisionForRepository(MockModels.ownedWebkit, 'owned-200805', null, '10.11.4 15E65');
+    return customCommitSet;
+}
+
+describe('CustomCommitSet', () => {
+    MockModels.inject();
+
+    describe('Test custom commit set without owned commit', () => {
+        it('should have right revision for a given repository', () => {
+            const commitSet = customCommitSetWithoutOwnedCommit();
+            assert.equal(commitSet.revisionForRepository(MockModels.osx), '10.11.4 15E65');
+            assert.equal(commitSet.revisionForRepository(MockModels.webkit), '200805');
+        });
+
+        it('should have no patch for any repository', () => {
+            const commitSet = customCommitSetWithoutOwnedCommit();
+            assert.equal(commitSet.patchForRepository(MockModels.osx), null);
+            assert.equal(commitSet.patchForRepository(MockModels.webkit), null);
+        });
+
+        it('should have no owner revision for a given repository', () => {
+            const commitSet = customCommitSetWithoutOwnedCommit();
+            assert.equal(commitSet.ownerRevisionForRepository(MockModels.osx), null);
+            assert.equal(commitSet.ownerRevisionForRepository(MockModels.webkit), null);
+        });
+
+        it('should return all repositories in it', () => {
+            const commitSet = customCommitSetWithoutOwnedCommit();
+            assert.deepEqual(commitSet.repositories(), [MockModels.osx, MockModels.webkit]);
+        });
+    });
+
+    describe('Test custom commit set with owned commit', () => {
+        it('should have right revision for a given repository', () => {
+            const commitSet = customCommitSetWithOwnedCommit();
+            assert.equal(commitSet.revisionForRepository(MockModels.osx), '10.11.4 15E65');
+            assert.equal(commitSet.revisionForRepository(MockModels.ownerRepository), 'OwnerRepository-r0');
+            assert.equal(commitSet.revisionForRepository(MockModels.ownedRepository), 'OwnedRepository-r0');
+        });
+
+        it('should have no patch for any repository', () => {
+            const commitSet = customCommitSetWithOwnedCommit();
+            assert.equal(commitSet.patchForRepository(MockModels.osx), null);
+            assert.equal(commitSet.patchForRepository(MockModels.ownerRepository), null);
+            assert.equal(commitSet.patchForRepository(MockModels.ownedRepository), null);
+        });
+
+        it('should have right owner revision for an owned repository', () => {
+            const commitSet = customCommitSetWithOwnedCommit();
+            assert.equal(commitSet.ownerRevisionForRepository(MockModels.osx), null);
+            assert.equal(commitSet.ownerRevisionForRepository(MockModels.ownerRepository), null);
+            assert.equal(commitSet.ownerRevisionForRepository(MockModels.ownedRepository), 'OwnerRepository-r0');
+        });
+
+        it('should return all repositories in it', () => {
+            const commitSet = customCommitSetWithOwnedCommit();
+            assert.deepEqual(commitSet.repositories(), [MockModels.osx, MockModels.ownerRepository, MockModels.ownedRepository]);
+        });
+    });
+
+    describe('Test custom commit set with patch', () => {
+        it('should have right revision for a given repository', () => {
+            const commitSet = customCommitSetWithPatch();
+            assert.equal(commitSet.revisionForRepository(MockModels.osx), '10.11.4 15E65');
+            assert.equal(commitSet.revisionForRepository(MockModels.webkit), '200805');
+        });
+
+        it('should have a patch for a repository with patch specified', () => {
+            const commitSet = customCommitSetWithPatch();
+            assert.equal(commitSet.patchForRepository(MockModels.osx), null);
+            assert.deepEqual(commitSet.patchForRepository(MockModels.webkit), createPatch());
+        });
+
+        it('should have no owner revision for a given repository', () => {
+            const commitSet = customCommitSetWithPatch();
+            assert.equal(commitSet.ownerRevisionForRepository(MockModels.osx), null);
+            assert.equal(commitSet.ownerRevisionForRepository(MockModels.webkit), null);
+        });
+
+        it('should return all repositories in it', () => {
+            const commitSet = customCommitSetWithPatch();
+            assert.deepEqual(commitSet.repositories(), [MockModels.osx, MockModels.webkit]);
+        });
+    });
+
+    describe('Test custom commit set with owned repository has same name as non-owned repository',  () => {
+        it('should have right revision for a given repository', () => {
+            const commitSet = customCommitSetWithOwnedRepositoryHasSameNameAsNotOwnedRepository();
+            assert.equal(commitSet.revisionForRepository(MockModels.osx), '10.11.4 15E65');
+            assert.equal(commitSet.revisionForRepository(MockModels.webkit), '200805');
+            assert.equal(commitSet.revisionForRepository(MockModels.ownedWebkit), 'owned-200805');
+        });
+
+        it('should have no patch for any repository', () => {
+            const commitSet = customCommitSetWithOwnedRepositoryHasSameNameAsNotOwnedRepository();
+            assert.equal(commitSet.patchForRepository(MockModels.osx), null);
+            assert.equal(commitSet.patchForRepository(MockModels.webkit), null);
+            assert.equal(commitSet.patchForRepository(MockModels.ownedWebkit), null);
+        });
+
+        it('should have right owner revision for an owned repository', () => {
+            const commitSet = customCommitSetWithOwnedRepositoryHasSameNameAsNotOwnedRepository();
+            assert.equal(commitSet.ownerRevisionForRepository(MockModels.osx), null);
+            assert.equal(commitSet.ownerRevisionForRepository(MockModels.webkit), null);
+            assert.equal(commitSet.ownerRevisionForRepository(MockModels.ownedWebkit), '10.11.4 15E65');
+        });
+
+        it('should return all repositories in it', () => {
+            const commitSet = customCommitSetWithOwnedRepositoryHasSameNameAsNotOwnedRepository();
+            assert.deepEqual(commitSet.repositories(), [MockModels.osx, MockModels.webkit, MockModels.ownedWebkit]);
+        });
+    });
+
+    describe('Test custom commit set equality function', () => {
+        it('should be equal to same custom commit set', () => {
+            assert.deepEqual(customCommitSetWithoutOwnedCommit(), customCommitSetWithoutOwnedCommit());
+            assert.deepEqual(customCommitSetWithOwnedCommit(), customCommitSetWithOwnedCommit());
+            assert.deepEqual(customCommitSetWithPatch(), customCommitSetWithPatch());
+            assert.deepEqual(customCommitSetWithOwnedRepositoryHasSameNameAsNotOwnedRepository(),
+                customCommitSetWithOwnedRepositoryHasSameNameAsNotOwnedRepository());
+        });
+
+        it('should not be equal even if non-owned revisions are the same', () => {
+            const commitSet0 = customCommitSetWithoutOwnedCommit();
+            const commitSet1 = customCommitSetWithOwnedRepositoryHasSameNameAsNotOwnedRepository();
+            assert.equal(commitSet0.equals(commitSet1), false);
+        });
+    });
+
+    describe('Test custom commit set custom root operations', () => {
+        it('should return empty custom roots if no custom root specified', () => {
+            assert.deepEqual(customCommitSetWithoutOwnedCommit().customRoots(), []);
+        });
+
+        it('should return root if root is added into commit set', () => {
+            const commitSet = customCommitSetWithoutOwnedCommit();
+            commitSet.addCustomRoot(createRoot());
+            assert.deepEqual(commitSet.customRoots(), [createRoot()]);
+        });
+    });
+});
index ff5d18c..96aec2b 100644 (file)
@@ -18,6 +18,7 @@ var MockModels = {
             MockModels.osx = Repository.ensureSingleton(9, {name: 'OS X'});
             MockModels.ios = Repository.ensureSingleton(22, {name: 'iOS'});
             MockModels.webkit = Repository.ensureSingleton(11, {name: 'WebKit', url: 'http://trac.webkit.org/changeset/$1'});
+            MockModels.ownedWebkit = Repository.ensureSingleton(191, {name: 'WebKit', url: 'http://trac.webkit.org/changeset/$1', owner: 9});
             MockModels.sharedRepository = Repository.ensureSingleton(16, {name: 'Shared'});
             MockModels.ownerRepository = Repository.ensureSingleton(111, {name: 'Owner Repository'});
             MockModels.ownedRepository = Repository.ensureSingleton(112, {name: 'Owned Repository', owner: 111});