Add the support for scheduling a A/B testing with a patch.
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sun, 30 Apr 2017 18:11:47 +0000 (18:11 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sun, 30 Apr 2017 18:11:47 +0000 (18:11 +0000)
https://bugs.webkit.org/show_bug.cgi?id=171209

Reviewed by Chris Dumez.

Added the support for creating a custom test group with a patch applied.

First, each repository in a repository group has a boolean indicating whether a given repository can have
a patch applied or not. When any configuration in a test group contains a patch, we create build requests
without a test specified in order to "build" those patches. These build requests have negative order numbers
to differentiate them from regular build requests. We can't simply build ones with patches since there could
be differences in SDK, build options, etc... when patches are applied.

The JSON format for commit sets returned by /api/build-requests have been changed from using an array of
commit IDs to an array of dictionaries indicate commit and acceptsPatch boolean. /api/update-triggerable now
uses a dictionary with two keys: repository and acceptsPatch to specify a set of repositories associated with
a repository group, and /privileged-api-create-test-group uses a dictionary with two keys: revision and patch
instead of a revision string to specify commit sets.

Furthermore, the syncing script's configuration have been updated to use a dictionary of repository names to
an options dictionary instead of an array of repositories names. For now, the only supported option is
acceptsPatch but will be extended when we add the support for rolling back system components.
e.g. {"WebKit": {acceptsPatch: true}, "macOS": {}} instead of ["WebKit", "macOS"]

On the UI side, InstantFileUploader has been changed to accept only one file by default, and added a new method
allowMultipleFiles() to allow multiple files to be selected for custom roots. Also replaced the input element
with type=file by a button with a custom label to show labels such as "Apply a patch" or "Add a new root"
instead of the generic label like "choose a file".

* init-database.sql: Added trigrepo_accepts_patch to triggerable_repositories to indicate whether a given
repository can have a patch applied or not. Made request_test optional in build_requests for when a build
request is created to build patches. Such a build request have a negative request_order. Updated the related
constraints accordingly.

* public/admin/triggerables.php: Added the support for updating whether a given repository can have a patch
applied in each repository group. Only show the repositories in the repository group for this purpose since
there is no way to accept a patch on a repository without it being a part of the group.
(generate_repository_form): Now takes the markup for checkboxes instead of generating one itself.
(generate_repository_checkboxes): Now takes an array of repositories to generate checkboxes. The checkbox is
shown when the repository ID exists as a key in this array, and is checked when its value is true. The new
capability to skip repositories not in the array is used to hide repositories not associated with the group
in the list of checkboxes to indicate a repository accepts a patch.

* public/api/update-triggerable.php:
(main): Now updates the description and acceptsRoots states of each repository group, and sets acceptsPatch
boolean for each repository in the group if set in the update.
(validate_repository_groups): Use a reference to $repository_groups in order to set repository_id_list, which
contains an array of repository IDs to find the existing repository group that matches the set via
RepositoryGroupFinder's find_by_repositories. Also added a various validations for acceptsRoots, a dictionary
specifying repository and acceptsPatch.

* public/include/build-requests-fetcher.php:
(BuildRequestsFetcher::fetch_commits_for_set_if_needed): Instead of returning an array of commit IDs as
"commits", it now returns an array of dictionaries with "commit" and "patch" keys specifying the commit ID
and the patch file's ID respectively as "revisionItems".
(BuildRequestsFetcher::add_uploaded_file): Added. Extracted from fetch_commits_for_set_if_needed. Used to
add either a patch file or a custom root file in the list of uploaded files in the result.

* public/include/manifest-generator.php:
(fetch_triggerables): Each element in repository group's "repositories" field is now an array of dictionaries
with "repository" and "acceptsPatch" as keys.

* public/include/repository-group-finder.php:
(RepositoryGroupFinder::__construct): Added a map for boolean indicating whether a given repository group
allows a patch on a repository. Used in /privileged-api/create-test-group.
(RepositoryGroupFinder::accepts_patch): Added.
(RepositoryGroupFinder::populate_map): Build up the map for acceptsPatch boolean per repository per group.

* public/privileged-api/create-test-group.php:
(main): Fixed a bug that we were not explicitly checking for a duplicate test group name (with a test). Create
build requests to "build" patches if there is any patch file specified.
(commit_sets_from_revision_sets): Updated to take a dictionary with "revision" and "patch" as keys to specify
a revision and a patch if any instead of just a revision string for each repository. Also validate that each
repository is allowed to have a patch once the repository group has been found for the set of repositories.
(ensure_commit_sets):

* public/v3/components/custom-analysis-task-configurator.js:
(CustomAnalysisTaskConfigurator): Added _patchUploaders as an instance variable, which is a dictionary of
configuration names to a map of InstantFileUploader's used to upload a patch. Also renamed _fileUploaders to
_customRootUploaders for clarity.
(CustomAnalysisTaskConfigurator.prototype.setCommitSets):
(CustomAnalysisTaskConfigurator.prototype.didConstructShadowTree.createRootUploader): Added.
(CustomAnalysisTaskConfigurator.prototype.didConstructShadowTree):
(CustomAnalysisTaskConfigurator.prototype._ensurePatchUploader): Added. Creates an instant file uploader for
patches. We only allow a single patch per repository.
(CustomAnalysisTaskConfigurator.prototype._computeCommitSet): Include a patch in the commit set as needed.
(CustomAnalysisTaskConfigurator.prototype._buildRevisionTable): Show the patch file uploader for repositories
which can have patches in the current repository group.
(CustomAnalysisTaskConfigurator.cssTemplate): Show borders between every rows instead of just between tbody's
now that each row can have a patch file uploader.

* public/v3/components/instant-file-uploader.js:
(InstantFileUploader): Added _fileInput and _allowMultipleFiles as instance variables. We now show a button
in the UI instead of an input with type=file. _fileInput is a hidden input with type=file used inside a click
event of the button to let the user pick a file.
(InstantFileUploader.prototype.allowMultipleFiles): Added. Allows this instance to accept multiple files.
(InstantFileUploader.prototype.didConstructShadowTree): Synthetically click on the hidden input element when
the newly added button element is clicked to open the browser's file picker.
(InstantFileUploader.prototype.render): Hide the button to add a file if this instance can only select one file
and there is already some file being uploaded in this instance.
(InstantFileUploader.htmlTemplate): Replaced the input element with type=file with a button. Its label comes
from the default slot content.

* public/v3/models/build-request.js:
(BuildRequest): Made the test optional.
(BuildRequest.prototype.isBuild): Returns true if this is a build request for building a patch.
(BuildRequest.prototype.isTest): Returns true if this is a build request for running tests.
(BuildRequest.constructBuildRequestsFromData): Create each commit log here instead of relying on CommitSet's
constructor to construct its commit logs. Also updated per the replacement of an array of commit IDs by
an array of dictionaries with commit and patch properties.

* public/v3/models/commit-set.js:
(CommitSet): Made _repositoryToCommitMap a real Map object. Also added _repositoryToPatchMap. Also got rid of
the code to instantiate commit logs since that's now done in BuildRequest.constructBuildRequestsFromData.
(CommitSet.prototype.commitForRepository):
(CommitSet.prototype.revisionForRepository):
(CommitSet.prototype.patchForRepository): Added.
(CommitSet.prototype.latestCommitTime): Modernized the code.
(CommitSet.prototype.equals): Modernized the code. Also added the check for patches.
(MeasurementCommitSet): Updated per the change to make _repositoryToCommitMap a real Map.
(CustomCommitSet.prototype.setRevisionForRepository):
(CustomCommitSet.prototype.equals): Added the check for patches.
(CustomCommitSet.prototype.revisionForRepository):
(CustomCommitSet.prototype.patchForRepository): Added.

* public/v3/models/manifest.js:
(Manifest._didFetchManifest): Updated per the replacement of an array of commit IDs by an array of dictionaries
with commit and patch properties.

* public/v3/models/repository.js:
(Repository.prototype.ownerId): Renamed from owner for clarity.

* public/v3/models/test-group.js:
(TestGroup): Modernized the code by using LazilyEvaluatedFunction. Removed _requestsAreInOrder since it's not
necessary anymore with LazilyEvaluatedFunction.
(TestGroup.prototype.addBuildRequest):
(TestGroup.prototype.test): Use the last build request's test since the first few requests could be requests to
build patches.
(TestGroup.prototype.platform): Ditto.
(TestGroup.prototype._lastRequest): Added.
(TestGroup.prototype._orderedBuildRequests): Added.
(TestGroup.prototype.repetitionCount): Only count the build requests for testing (skipping any requests to
build patches).
(TestGroup.prototype.requestedCommitSets): Simply call _computeRequestedCommitSetsLazily.
(TestGroup.prototype._computeRequestedCommitSets): Extracted from requestedCommitSets.
(TestGroup.prototype.requestsForCommitSet):
(TestGroup.prototype.labelForCommitSet): Rewritten. Just compute the label here instead of relying on
_commitSetToLabel since requestedSets is always of the length two at the moment.
(TestGroup._revisionSetsFromCommitSets): Specify both the revision and the patch in the revision set.

* public/v3/models/triggerable.js:
(TriggerableRepositoryGroup): Added _patchAcceptingSet as an instance variable. Use
sortByNamePreferringOnesWithURL to sort repositories instead of simple sortByName.
(TriggerableRepositoryGroup.prototype.accepts): Added checks for the custom roots and patches.
(TriggerableRepositoryGroup.prototype.acceptsPatchForRepository): Added.

* server-tests/api-build-requests-tests.js: Updated the test cases per the replacement of an array of commit
IDs by an array of dictionaries with commit and patch properties.

* server-tests/api-manifest-tests.js: Updated the test case per the name of Repository's owner to ownerId.

* server-tests/api-update-triggerable.js: Updated the test case per the name of Repository's owner to ownerId,
and added a test case for updating whether a given repository group allows custom roots as well as patches
on repositories via /api/update-triggerable.
(.updateWithOSXRepositoryGroup): Updated the sample syncing script configuration per the format change.
(.refetchManifest): Added.

* server-tests/privileged-api-create-test-group-tests.js: Updated per the syncing script configuration format
change. Also added a test for creating a test group with a duplicate name, which is expected to fail with
DuplicateTestGroupName, and creating a test group with a patch both when it's allowed and when it's not allowed
in the matching repository group.
(.addTriggerableAndCreateTask): Updated per the format change.

* server-tests/resources/mock-data.js:
(MockData.addEmptyTriggerable): Added a metric and its configuration to make it appear in the manifest file.
The new test case in api-update-triggerable.js requires this.
(MockData.mockTestSyncConfigWithSingleBuilder): Updated per the syncing script configuration format change.
(MockData.mockTestSyncConfigWithTwoBuilders): Ditto.

* server-tests/tools-buildbot-triggerable-tests.js: Removed the useless assertions about test configurations,
and added assertions about custom roots and patches in the test case for updateTriggerables.

* tools/js/buildbot-syncer.js:
(BuildbotSyncer._parseRepositoryGroup): Made each assertion explicitly refer to the specific repository group
to make it more user friendly. Now each repository group uses a dictionary of repository names to its options
in the syncing script configurations. When parsed, we insert it as an array of dictionaries with repository ID
and acceptsPatch boolean specified separately since this is the format /api/update-triggerable expects.

* tools/js/buildbot-triggerable.js:
(BuildbotTriggerable.prototype.updateTriggerable):

* unit-tests/build-request-tests.js:
(sampleBuildRequestData): Updated per the commit sets format change in /api/build-requests.

* unit-tests/buildbot-syncer-tests.js: Updated the existing tests per various format changes and added a couple
of new test cases for the syncing script's configuration validation.
(sampleiOSConfig):
(smallConfiguration):
(createSampleBuildRequest):

* unit-tests/resources/mock-v3-models.js:
(MockModels.inject): Updated per the repository group format change.

* unit-tests/test-groups-tests.js:
(sampleTestGroup): Updated per the commit sets format change in /api/build-requests.

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

28 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/api/update-triggerable.php
Websites/perf.webkit.org/public/include/build-requests-fetcher.php
Websites/perf.webkit.org/public/include/manifest-generator.php
Websites/perf.webkit.org/public/include/repository-group-finder.php
Websites/perf.webkit.org/public/privileged-api/create-test-group.php
Websites/perf.webkit.org/public/v3/components/custom-analysis-task-configurator.js
Websites/perf.webkit.org/public/v3/components/instant-file-uploader.js
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/manifest.js
Websites/perf.webkit.org/public/v3/models/repository.js
Websites/perf.webkit.org/public/v3/models/test-group.js
Websites/perf.webkit.org/public/v3/models/triggerable.js
Websites/perf.webkit.org/server-tests/api-build-requests-tests.js
Websites/perf.webkit.org/server-tests/api-manifest-tests.js
Websites/perf.webkit.org/server-tests/api-update-triggerable.js
Websites/perf.webkit.org/server-tests/privileged-api-create-test-group-tests.js
Websites/perf.webkit.org/server-tests/resources/mock-data.js
Websites/perf.webkit.org/server-tests/tools-buildbot-triggerable-tests.js
Websites/perf.webkit.org/tools/js/buildbot-syncer.js
Websites/perf.webkit.org/tools/js/buildbot-triggerable.js
Websites/perf.webkit.org/unit-tests/build-request-tests.js
Websites/perf.webkit.org/unit-tests/buildbot-syncer-tests.js
Websites/perf.webkit.org/unit-tests/resources/mock-v3-models.js
Websites/perf.webkit.org/unit-tests/test-groups-tests.js

index 7ae44f6..4294f8b 100644 (file)
@@ -1,3 +1,212 @@
+2017-04-30  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Add the support for scheduling a A/B testing with a patch.
+        https://bugs.webkit.org/show_bug.cgi?id=171209
+
+        Reviewed by Chris Dumez.
+
+        Added the support for creating a custom test group with a patch applied.
+
+        First, each repository in a repository group has a boolean indicating whether a given repository can have
+        a patch applied or not. When any configuration in a test group contains a patch, we create build requests
+        without a test specified in order to "build" those patches. These build requests have negative order numbers
+        to differentiate them from regular build requests. We can't simply build ones with patches since there could
+        be differences in SDK, build options, etc... when patches are applied.
+
+        The JSON format for commit sets returned by /api/build-requests have been changed from using an array of
+        commit IDs to an array of dictionaries indicate commit and acceptsPatch boolean. /api/update-triggerable now
+        uses a dictionary with two keys: repository and acceptsPatch to specify a set of repositories associated with
+        a repository group, and /privileged-api-create-test-group uses a dictionary with two keys: revision and patch
+        instead of a revision string to specify commit sets.
+
+        Furthermore, the syncing script's configuration have been updated to use a dictionary of repository names to
+        an options dictionary instead of an array of repositories names. For now, the only supported option is
+        acceptsPatch but will be extended when we add the support for rolling back system components.
+        e.g. {"WebKit": {acceptsPatch: true}, "macOS": {}} instead of ["WebKit", "macOS"]
+
+        On the UI side, InstantFileUploader has been changed to accept only one file by default, and added a new method
+        allowMultipleFiles() to allow multiple files to be selected for custom roots. Also replaced the input element
+        with type=file by a button with a custom label to show labels such as "Apply a patch" or "Add a new root"
+        instead of the generic label like "choose a file".
+
+
+        * init-database.sql: Added trigrepo_accepts_patch to triggerable_repositories to indicate whether a given
+        repository can have a patch applied or not. Made request_test optional in build_requests for when a build
+        request is created to build patches. Such a build request have a negative request_order. Updated the related
+        constraints accordingly.
+
+        * public/admin/triggerables.php: Added the support for updating whether a given repository can have a patch
+        applied in each repository group. Only show the repositories in the repository group for this purpose since
+        there is no way to accept a patch on a repository without it being a part of the group.
+        (generate_repository_form): Now takes the markup for checkboxes instead of generating one itself.
+        (generate_repository_checkboxes): Now takes an array of repositories to generate checkboxes. The checkbox is
+        shown when the repository ID exists as a key in this array, and is checked when its value is true. The new
+        capability to skip repositories not in the array is used to hide repositories not associated with the group
+        in the list of checkboxes to indicate a repository accepts a patch.
+
+        * public/api/update-triggerable.php:
+        (main): Now updates the description and acceptsRoots states of each repository group, and sets acceptsPatch
+        boolean for each repository in the group if set in the update.
+        (validate_repository_groups): Use a reference to $repository_groups in order to set repository_id_list, which
+        contains an array of repository IDs to find the existing repository group that matches the set via
+        RepositoryGroupFinder's find_by_repositories. Also added a various validations for acceptsRoots, a dictionary
+        specifying repository and acceptsPatch.
+
+        * public/include/build-requests-fetcher.php:
+        (BuildRequestsFetcher::fetch_commits_for_set_if_needed): Instead of returning an array of commit IDs as
+        "commits", it now returns an array of dictionaries with "commit" and "patch" keys specifying the commit ID
+        and the patch file's ID respectively as "revisionItems". 
+        (BuildRequestsFetcher::add_uploaded_file): Added. Extracted from fetch_commits_for_set_if_needed. Used to
+        add either a patch file or a custom root file in the list of uploaded files in the result.
+
+        * public/include/manifest-generator.php:
+        (fetch_triggerables): Each element in repository group's "repositories" field is now an array of dictionaries
+        with "repository" and "acceptsPatch" as keys.
+
+        * public/include/repository-group-finder.php:
+        (RepositoryGroupFinder::__construct): Added a map for boolean indicating whether a given repository group
+        allows a patch on a repository. Used in /privileged-api/create-test-group.
+        (RepositoryGroupFinder::accepts_patch): Added.
+        (RepositoryGroupFinder::populate_map): Build up the map for acceptsPatch boolean per repository per group.
+
+        * public/privileged-api/create-test-group.php:
+        (main): Fixed a bug that we were not explicitly checking for a duplicate test group name (with a test). Create
+        build requests to "build" patches if there is any patch file specified.
+        (commit_sets_from_revision_sets): Updated to take a dictionary with "revision" and "patch" as keys to specify
+        a revision and a patch if any instead of just a revision string for each repository. Also validate that each
+        repository is allowed to have a patch once the repository group has been found for the set of repositories.
+        (ensure_commit_sets):
+
+        * public/v3/components/custom-analysis-task-configurator.js:
+        (CustomAnalysisTaskConfigurator): Added _patchUploaders as an instance variable, which is a dictionary of
+        configuration names to a map of InstantFileUploader's used to upload a patch. Also renamed _fileUploaders to
+        _customRootUploaders for clarity.
+        (CustomAnalysisTaskConfigurator.prototype.setCommitSets):
+        (CustomAnalysisTaskConfigurator.prototype.didConstructShadowTree.createRootUploader): Added.
+        (CustomAnalysisTaskConfigurator.prototype.didConstructShadowTree):
+        (CustomAnalysisTaskConfigurator.prototype._ensurePatchUploader): Added. Creates an instant file uploader for
+        patches. We only allow a single patch per repository.
+        (CustomAnalysisTaskConfigurator.prototype._computeCommitSet): Include a patch in the commit set as needed.
+        (CustomAnalysisTaskConfigurator.prototype._buildRevisionTable): Show the patch file uploader for repositories
+        which can have patches in the current repository group.
+        (CustomAnalysisTaskConfigurator.cssTemplate): Show borders between every rows instead of just between tbody's
+        now that each row can have a patch file uploader.
+
+        * public/v3/components/instant-file-uploader.js:
+        (InstantFileUploader): Added _fileInput and _allowMultipleFiles as instance variables. We now show a button
+        in the UI instead of an input with type=file. _fileInput is a hidden input with type=file used inside a click
+        event of the button to let the user pick a file.
+        (InstantFileUploader.prototype.allowMultipleFiles): Added. Allows this instance to accept multiple files.
+        (InstantFileUploader.prototype.didConstructShadowTree): Synthetically click on the hidden input element when
+        the newly added button element is clicked to open the browser's file picker.
+        (InstantFileUploader.prototype.render): Hide the button to add a file if this instance can only select one file
+        and there is already some file being uploaded in this instance.
+        (InstantFileUploader.htmlTemplate): Replaced the input element with type=file with a button. Its label comes
+        from the default slot content.
+
+        * public/v3/models/build-request.js:
+        (BuildRequest): Made the test optional.
+        (BuildRequest.prototype.isBuild): Returns true if this is a build request for building a patch.
+        (BuildRequest.prototype.isTest): Returns true if this is a build request for running tests.
+        (BuildRequest.constructBuildRequestsFromData): Create each commit log here instead of relying on CommitSet's
+        constructor to construct its commit logs. Also updated per the replacement of an array of commit IDs by
+        an array of dictionaries with commit and patch properties.
+
+        * public/v3/models/commit-set.js:
+        (CommitSet): Made _repositoryToCommitMap a real Map object. Also added _repositoryToPatchMap. Also got rid of
+        the code to instantiate commit logs since that's now done in BuildRequest.constructBuildRequestsFromData.
+        (CommitSet.prototype.commitForRepository):
+        (CommitSet.prototype.revisionForRepository):
+        (CommitSet.prototype.patchForRepository): Added.
+        (CommitSet.prototype.latestCommitTime): Modernized the code.
+        (CommitSet.prototype.equals): Modernized the code. Also added the check for patches.
+        (MeasurementCommitSet): Updated per the change to make _repositoryToCommitMap a real Map.
+        (CustomCommitSet.prototype.setRevisionForRepository):
+        (CustomCommitSet.prototype.equals): Added the check for patches.
+        (CustomCommitSet.prototype.revisionForRepository):
+        (CustomCommitSet.prototype.patchForRepository): Added.
+
+        * public/v3/models/manifest.js:
+        (Manifest._didFetchManifest): Updated per the replacement of an array of commit IDs by an array of dictionaries
+        with commit and patch properties.
+
+        * public/v3/models/repository.js:
+        (Repository.prototype.ownerId): Renamed from owner for clarity.
+
+        * public/v3/models/test-group.js:
+        (TestGroup): Modernized the code by using LazilyEvaluatedFunction. Removed _requestsAreInOrder since it's not
+        necessary anymore with LazilyEvaluatedFunction.
+        (TestGroup.prototype.addBuildRequest):
+        (TestGroup.prototype.test): Use the last build request's test since the first few requests could be requests to
+        build patches.
+        (TestGroup.prototype.platform): Ditto.
+        (TestGroup.prototype._lastRequest): Added.
+        (TestGroup.prototype._orderedBuildRequests): Added.
+        (TestGroup.prototype.repetitionCount): Only count the build requests for testing (skipping any requests to
+        build patches).
+        (TestGroup.prototype.requestedCommitSets): Simply call _computeRequestedCommitSetsLazily.
+        (TestGroup.prototype._computeRequestedCommitSets): Extracted from requestedCommitSets.
+        (TestGroup.prototype.requestsForCommitSet):
+        (TestGroup.prototype.labelForCommitSet): Rewritten. Just compute the label here instead of relying on
+        _commitSetToLabel since requestedSets is always of the length two at the moment.
+        (TestGroup._revisionSetsFromCommitSets): Specify both the revision and the patch in the revision set.        
+
+        * public/v3/models/triggerable.js:
+        (TriggerableRepositoryGroup): Added _patchAcceptingSet as an instance variable. Use
+        sortByNamePreferringOnesWithURL to sort repositories instead of simple sortByName.
+        (TriggerableRepositoryGroup.prototype.accepts): Added checks for the custom roots and patches.
+        (TriggerableRepositoryGroup.prototype.acceptsPatchForRepository): Added.
+
+        * server-tests/api-build-requests-tests.js: Updated the test cases per the replacement of an array of commit
+        IDs by an array of dictionaries with commit and patch properties.
+
+        * server-tests/api-manifest-tests.js: Updated the test case per the name of Repository's owner to ownerId.
+
+        * server-tests/api-update-triggerable.js: Updated the test case per the name of Repository's owner to ownerId,
+        and added a test case for updating whether a given repository group allows custom roots as well as patches
+        on repositories via /api/update-triggerable. 
+        (.updateWithOSXRepositoryGroup): Updated the sample syncing script configuration per the format change.
+        (.refetchManifest): Added.
+
+        * server-tests/privileged-api-create-test-group-tests.js: Updated per the syncing script configuration format
+        change. Also added a test for creating a test group with a duplicate name, which is expected to fail with 
+        DuplicateTestGroupName, and creating a test group with a patch both when it's allowed and when it's not allowed
+        in the matching repository group.
+        (.addTriggerableAndCreateTask): Updated per the format change.
+
+        * server-tests/resources/mock-data.js:
+        (MockData.addEmptyTriggerable): Added a metric and its configuration to make it appear in the manifest file.
+        The new test case in api-update-triggerable.js requires this.
+        (MockData.mockTestSyncConfigWithSingleBuilder): Updated per the syncing script configuration format change.
+        (MockData.mockTestSyncConfigWithTwoBuilders): Ditto.
+
+        * server-tests/tools-buildbot-triggerable-tests.js: Removed the useless assertions about test configurations,
+        and added assertions about custom roots and patches in the test case for updateTriggerables.
+
+        * tools/js/buildbot-syncer.js:
+        (BuildbotSyncer._parseRepositoryGroup): Made each assertion explicitly refer to the specific repository group
+        to make it more user friendly. Now each repository group uses a dictionary of repository names to its options
+        in the syncing script configurations. When parsed, we insert it as an array of dictionaries with repository ID
+        and acceptsPatch boolean specified separately since this is the format /api/update-triggerable expects.
+
+        * tools/js/buildbot-triggerable.js:
+        (BuildbotTriggerable.prototype.updateTriggerable):
+
+        * unit-tests/build-request-tests.js:
+        (sampleBuildRequestData): Updated per the commit sets format change in /api/build-requests.
+
+        * unit-tests/buildbot-syncer-tests.js: Updated the existing tests per various format changes and added a couple
+        of new test cases for the syncing script's configuration validation.
+        (sampleiOSConfig):
+        (smallConfiguration):
+        (createSampleBuildRequest):
+
+        * unit-tests/resources/mock-v3-models.js:
+        (MockModels.inject): Updated per the repository group format change.
+
+        * unit-tests/test-groups-tests.js:
+        (sampleTestGroup): Updated per the commit sets format change in /api/build-requests. 
+
 2017-04-21  Ryosuke Niwa  <rniwa@webkit.org>
 
         Rename commit_set_relationships to commit_set_items
 2017-04-21  Ryosuke Niwa  <rniwa@webkit.org>
 
         Rename commit_set_relationships to commit_set_items
index 427344b..c143692 100644 (file)
@@ -249,6 +249,7 @@ CREATE TABLE triggerable_repository_groups (
 CREATE TABLE triggerable_repositories (
     trigrepo_repository integer REFERENCES repositories NOT NULL,
     trigrepo_group integer REFERENCES triggerable_repository_groups NOT NULL,
 CREATE TABLE triggerable_repositories (
     trigrepo_repository integer REFERENCES repositories NOT NULL,
     trigrepo_group integer REFERENCES triggerable_repository_groups NOT NULL,
+    trigrepo_accepts_patch boolean NOT NULL DEFAULT FALSE,
     CONSTRAINT repository_must_be_unique_for_repository_group UNIQUE(trigrepo_repository, trigrepo_group));
 
 CREATE TABLE triggerable_configurations (
     CONSTRAINT repository_must_be_unique_for_repository_group UNIQUE(trigrepo_repository, trigrepo_group));
 
 CREATE TABLE triggerable_configurations (
@@ -297,7 +298,7 @@ CREATE TABLE build_requests (
     request_triggerable integer REFERENCES build_triggerables NOT NULL,
     request_repository_group integer REFERENCES triggerable_repository_groups,
     request_platform integer REFERENCES platforms NOT NULL,
     request_triggerable integer REFERENCES build_triggerables NOT NULL,
     request_repository_group integer REFERENCES triggerable_repository_groups,
     request_platform integer REFERENCES platforms NOT NULL,
-    request_test integer REFERENCES tests NOT NULL,
+    request_test integer REFERENCES tests,
     request_group integer REFERENCES analysis_test_groups NOT NULL,
     request_order integer NOT NULL,
     request_commit_set integer REFERENCES commit_sets NOT NULL,
     request_group integer REFERENCES analysis_test_groups NOT NULL,
     request_order integer NOT NULL,
     request_commit_set integer REFERENCES commit_sets NOT NULL,
@@ -305,6 +306,8 @@ CREATE TABLE build_requests (
     request_url varchar(1024),
     request_build integer REFERENCES builds,
     request_created_at timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
     request_url varchar(1024),
     request_build integer REFERENCES builds,
     request_created_at timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
-    CONSTRAINT build_request_order_must_be_unique_in_group UNIQUE(request_group, request_order));
+    CONSTRAINT build_request_order_must_be_unique_in_group UNIQUE(request_group, request_order),
+    CONSTRAINT build_request_order_must_be_positive_for_testing
+        CHECK ((request_test IS NOT NULL AND request_order >= 0) OR (request_test IS NULL AND request_order < 0)));
 CREATE INDEX build_request_triggerable ON build_requests(request_triggerable);
 CREATE INDEX build_request_build ON build_requests(request_build);
 CREATE INDEX build_request_triggerable ON build_requests(request_triggerable);
 CREATE INDEX build_request_build ON build_requests(request_build);
index a8a26b1..234bf3d 100644 (file)
@@ -27,33 +27,6 @@ if ($db) {
         if (update_field('triggerable_repository_groups', 'repositorygroup', 'accepts_roots',
             Database::to_database_boolean(array_get($_POST, 'accepts'))))
             regenerate_manifest();
         if (update_field('triggerable_repository_groups', 'repositorygroup', 'accepts_roots',
             Database::to_database_boolean(array_get($_POST, 'accepts'))))
             regenerate_manifest();
-    } else if ($action == 'update-repository') {
-        $association = array_get($_POST, 'association');
-        $triggerable_id = array_get($_POST, 'triggerable');
-        $repository_id = array_get($_POST, 'repository');
-
-        $should_delete = FALSE;
-        $accepted = $association == 'accepted';
-        $required = $association == 'required';
-        if ($accepted || $required) {
-            $db->begin_transaction();
-            $select = array('repository' => $repository_id, 'triggerable' => $triggerable_id);
-            $update = array('repository' => $repository_id, 'triggerable' => $triggerable_id, 'required' => Database::to_database_boolean($required));
-            if (!$db->update_row('triggerable_repositories', 'trigrepo', $select, $update, 'repository')) {
-                notice("Failed to update the association of repository $repository_id with triggerable $triggerable_id.");
-                $db->rollback_transaction();
-            } else
-                $db->commit_transaction();
-        } else if ($association == 'not-accepted') {
-            $db->begin_transaction();
-            $result = $db->query_and_get_affected_rows("DELETE FROM triggerable_repositories WHERE trigrepo_triggerable = $1 AND trigrepo_repository = $2",
-                array($triggerable_id, $repository_id));
-            if ($result > 1) {
-                notice("Failed to update the association of repository $repository_id with triggerable $triggerable_id.");
-                $db->rollback_transaction();
-            } else
-                $db->commit_transaction();
-        }
     } else if ($action == 'update-repositories') {
         $group_id = intval($_POST['group']);
 
     } else if ($action == 'update-repositories') {
         $group_id = intval($_POST['group']);
 
@@ -66,6 +39,26 @@ if ($db) {
             regenerate_manifest();
         } else
             $db->rollback_transaction();
             regenerate_manifest();
         } else
             $db->rollback_transaction();
+    }  else if ($action == 'update-accept-patch') {
+        $group_id = intval($_POST['group']);
+        $repositories_that_accepts_patch = array_get($_POST, 'repositories', array());
+
+        $db->begin_transaction();
+        if (!$db->query_and_get_affected_rows("UPDATE triggerable_repositories SET trigrepo_accepts_patch = FALSE WHERE trigrepo_group = $1", array($group_id))) {
+            notice('Failed to update the accept-patch status.');
+            $db->rollback_transaction();
+        } else {
+            foreach ($repositories_that_accepts_patch as $repository_id) {
+                if (!$db->query_and_get_affected_rows("UPDATE triggerable_repositories SET trigrepo_accepts_patch = TRUE
+                    WHERE trigrepo_group = $1 AND trigrepo_repository = $2", array($group_id, $repository_id))) {
+                    notice('Failed to update the accept-patch status.');
+                    $db->rollback_transaction();
+                }
+            }
+            $db->commit_transaction();
+            notice('Updated the accept-patch status.');
+            regenerate_manifest();
+        }
     } else if ($action == 'add-repository-group') {
         $triggerable_id = intval($_POST['triggerable']);
         $name = $_POST['name'];
     } else if ($action == 'add-repository-group') {
         $triggerable_id = intval($_POST['triggerable']);
         $name = $_POST['name'];
@@ -90,7 +83,7 @@ if ($db) {
         'disabled' => array('editing_mode' => 'boolean', 'post_insertion' => TRUE),
         'repositories' => array(
             'label' => 'Repository Groups',
         'disabled' => array('editing_mode' => 'boolean', 'post_insertion' => TRUE),
         'repositories' => array(
             'label' => 'Repository Groups',
-            'subcolumns'=> array('ID', 'Name', 'Description', 'Accepts Roots', 'Repositories'),
+            'subcolumns'=> array('ID', 'Name', 'Description', 'Accepts Roots', 'Repositories', 'Accept patches'),
             'custom' => function ($triggerable_row) use (&$db, &$repository_rows) {
                 return generate_repository_list($db, $triggerable_row['triggerable_id'], $repository_rows);
             }),
             'custom' => function ($triggerable_row) use (&$db, &$repository_rows) {
                 return generate_repository_list($db, $triggerable_row['triggerable_id'], $repository_rows);
             }),
@@ -149,7 +142,21 @@ END;
             </form>
 END;
 
             </form>
 END;
 
-        array_push($group_forms, array($group_id, $group_name_form, $group_description_form, $group_accepts_roots, generate_repository_form($db, $repository_rows, $group_id)));
+        $group_repository_rows = $db->select_rows('triggerable_repositories', 'trigrepo', array('group' => $group_id));
+        $repositories_in_group = array();
+        $repositories_that_accepts_patch = array();
+        foreach ($repository_rows as $row)
+            $repositories_in_group[$row['repository_id']] = FALSE;
+        foreach ($group_repository_rows as $row) {
+            $repository_id = $row['trigrepo_repository'];
+            $repositories_in_group[$repository_id] = TRUE;
+            $repositories_that_accepts_patch[$repository_id] = Database::is_true($row['trigrepo_accepts_patch']);
+        }
+
+        array_push($group_forms, array($group_id, $group_name_form, $group_description_form, $group_accepts_roots,
+            generate_repository_form('update-repositories', $group_id, generate_repository_checkboxes($db, $repository_rows, $repositories_in_group)),
+            generate_repository_form('update-accept-patch', $group_id, generate_repository_checkboxes($db, $repository_rows, $repositories_that_accepts_patch)),
+        ));
     }
 
     $new_group_checkboxes = generate_repository_checkboxes($db, $repository_rows);
     }
 
     $new_group_checkboxes = generate_repository_checkboxes($db, $repository_rows);
@@ -167,32 +174,26 @@ END;
     return $group_forms;
 }
 
     return $group_forms;
 }
 
-function generate_repository_form($db, $repository_rows, $group_id)
+function generate_repository_form($action, $group_id, $checkboxes)
 {
 {
-    $checkboxes = generate_repository_checkboxes($db, $repository_rows, $group_id);
     return <<< END
         <form method="POST">
     return <<< END
         <form method="POST">
-        <input type="hidden" name="action" value="update-repositories">
+        <input type="hidden" name="action" value="$action">
         <input type="hidden" name="group" value="$group_id">
         $checkboxes
         <br><button type="submit">Save</button></form>
 END;
 }
 
         <input type="hidden" name="group" value="$group_id">
         $checkboxes
         <br><button type="submit">Save</button></form>
 END;
 }
 
-function generate_repository_checkboxes($db, $repository_rows, $group_id = NULL)
+function generate_repository_checkboxes($db, $repository_rows, $selected_repositories = array())
 {
 {
-    $repositories_in_group = array();
-    if ($group_id) {
-        $group_repository_rows = $db->select_rows('triggerable_repositories', 'trigrepo', array('group' => $group_id));
-        foreach ($group_repository_rows as $row)
-            $repositories_in_group[$row['trigrepo_repository']] = TRUE;
-    }
-
     $form = '';
     foreach ($repository_rows as $row) {
         $id = $row['repository_id'];
     $form = '';
     foreach ($repository_rows as $row) {
         $id = $row['repository_id'];
+        if (!array_key_exists($id, $selected_repositories))
+            continue;
         $name = $row['repository_name'];
         $name = $row['repository_name'];
-        $checked = array_key_exists($id, $repositories_in_group) ? 'checked' : '';
+        $checked = $selected_repositories[$id] ? 'checked' : '';
         $form .= "<label><input type=\"checkbox\" name=\"repositories[]\" value=\"$id\" $checked>$name</label>";
     }
     return $form;
         $form .= "<label><input type=\"checkbox\" name=\"repositories[]\" value=\"$id\" $checked>$name</label>";
     }
     return $form;
index e823966..f5b22b5 100644 (file)
@@ -26,7 +26,7 @@ function main($post_data)
 
     $finder = new RepositoryGroupFinder($db, $triggerable_id);
     foreach ($repository_groups as &$group)
 
     $finder = new RepositoryGroupFinder($db, $triggerable_id);
     foreach ($repository_groups as &$group)
-        $group['existingGroup'] = $finder->find_by_repositories($group['repositories']);
+        $group['existingGroup'] = $finder->find_by_repositories($group['repository_id_list']);
 
     $db->begin_transaction();
     if ($db->query_and_get_affected_rows('DELETE FROM triggerable_configurations WHERE trigconfig_triggerable = $1', array($triggerable_id)) === false) {
 
     $db->begin_transaction();
     if ($db->query_and_get_affected_rows('DELETE FROM triggerable_configurations WHERE trigconfig_triggerable = $1', array($triggerable_id)) === false) {
@@ -44,16 +44,19 @@ function main($post_data)
 
     foreach ($repository_groups as &$group) {
         $group_id = $group['existingGroup'];
 
     foreach ($repository_groups as &$group) {
         $group_id = $group['existingGroup'];
+        $group_info = array(
+            'triggerable' => $triggerable_id,
+            'name' => $group['name'],
+            'description' => array_get($group, 'description'),
+            'accepts_roots' => Database::to_database_boolean(array_get($group, 'acceptsRoots', FALSE)));
         if ($group_id) {
         if ($group_id) {
-            $group_info = array('name' => $group['name'], 'description' => array_get($group, 'description'));
             if (!$db->update_row('triggerable_repository_groups', 'repositorygroup', array('id' => $group_id), $group_info)) {
                 $db->rollback_transaction();
                 exit_with_error('FailedToInsertRepositoryGroup', array('repositoryGroup' => $group));
             }
         } else {
             $group_id = $db->update_or_insert_row('triggerable_repository_groups', 'repositorygroup',
             if (!$db->update_row('triggerable_repository_groups', 'repositorygroup', array('id' => $group_id), $group_info)) {
                 $db->rollback_transaction();
                 exit_with_error('FailedToInsertRepositoryGroup', array('repositoryGroup' => $group));
             }
         } else {
             $group_id = $db->update_or_insert_row('triggerable_repository_groups', 'repositorygroup',
-                array('triggerable' => $triggerable_id, 'name' => $group['name']),
-                array('triggerable' => $triggerable_id, 'name' => $group['name'], 'description' => array_get($group, 'description')));
+                array('triggerable' => $triggerable_id, 'name' => $group['name']), $group_info);
             if (!$group_id) {
                 $db->rollback_transaction();
                 exit_with_error('FailedToInsertRepositoryGroup', array('repositoryGroup' => $group));
             if (!$group_id) {
                 $db->rollback_transaction();
                 exit_with_error('FailedToInsertRepositoryGroup', array('repositoryGroup' => $group));
@@ -63,8 +66,11 @@ function main($post_data)
             $db->rollback_transaction();
             exit_with_error('FailedToDisassociateRepositories', array('repositoryGroup' => $group));
         }
             $db->rollback_transaction();
             exit_with_error('FailedToDisassociateRepositories', array('repositoryGroup' => $group));
         }
-        foreach ($group['repositories'] as $repository_id) {
-            if (!$db->insert_row('triggerable_repositories', 'trigrepo', array('group' => $group_id, 'repository' => $repository_id), null)) {
+        foreach ($group['repositories'] as $repository_data) {
+            $row = array('group' => $group_id,
+                'repository' => $repository_data['repository'],
+                'accepts_patch' => Database::to_database_boolean(array_get($repository_data, 'acceptsPatch', FALSE)));
+            if (!$db->insert_row('triggerable_repositories', 'trigrepo', $row, null)) {
                 $db->rollback_transaction();
                 exit_with_error('FailedToAssociateRepository', array('repositoryGroup' => $group, 'repository' => $repository_id));
             }
                 $db->rollback_transaction();
                 exit_with_error('FailedToAssociateRepository', array('repositoryGroup' => $group, 'repository' => $repository_id));
             }
@@ -86,7 +92,7 @@ function validate_configurations($db, $configurations)
     }
 }
 
     }
 }
 
-function validate_repository_groups($db, $repository_groups)
+function validate_repository_groups($db, &$repository_groups)
 {
     if (!is_array($repository_groups))
         exit_with_error('InvalidRepositoryGroups', array('repositoryGroups' => $repository_groups));
 {
     if (!is_array($repository_groups))
         exit_with_error('InvalidRepositoryGroups', array('repositoryGroups' => $repository_groups));
@@ -99,13 +105,31 @@ function validate_repository_groups($db, $repository_groups)
     foreach ($repository_groups as &$group) {
         if (!is_array($group) || !array_key_exists('name', $group) || !array_key_exists('repositories', $group) || !is_array($group['repositories']))
             exit_with_error('InvalidRepositoryGroup', array('repositoryGroup' => $group));
     foreach ($repository_groups as &$group) {
         if (!is_array($group) || !array_key_exists('name', $group) || !array_key_exists('repositories', $group) || !is_array($group['repositories']))
             exit_with_error('InvalidRepositoryGroup', array('repositoryGroup' => $group));
+
+        $accepts_roots = array_get($group, 'acceptsRoots', FALSE);
+        if ($accepts_roots !== TRUE && $accepts_roots !== FALSE)
+            exit_with_error('InvalidAcceptsRoots', array('repositoryGroup' => $group, 'acceptsRoots' => accepts_roots));
+
         $repository_list = $group['repositories'];
         $group_repository_list = array();
         $repository_list = $group['repositories'];
         $group_repository_list = array();
-        foreach ($repository_list as $repository_id) {
-            if (!array_key_exists($repository_id, $top_level_repository_ids) || array_key_exists($repository_id, $group_repository_list))
-                exit_with_error('InvalidRepository', array('repositoryGroup' => $group, 'repository' => $repository_id));
-            $group_repository_list[$repository_id] = true;
+        foreach ($repository_list as $repository_data) {
+            if (!$repository_data || !is_array($repository_data))
+                exit_with_error('InvalidRepositoryData', array('repositoryGroup' => $group, 'data' => $repository_data));
+
+            $id = array_get($repository_data, 'repository');
+            if (!$id || !is_numeric($id) || !array_key_exists($id, $top_level_repository_ids))
+                exit_with_error('InvalidRepository', array('repositoryGroup' => $group, 'repository' => $id));
+
+            if (array_key_exists($id, $group_repository_list))
+                exit_with_error('DuplicateRepository', array('repositoryGroup' => $group, 'repository' => $id));
+
+            $accepts_patch = array_get($repository_data, 'acceptsPatch', FALSE);
+            if ($accepts_patch !== TRUE && $accepts_patch !== FALSE)
+                exit_with_error('InvalidRepositoryData', array('repositoryGroup' => $group, 'repository' => $id));
+
+            $group_repository_list[$id] = true;
         }
         }
+        $group['repository_id_list'] = array_keys($group_repository_list);
     }
 }
 
     }
 }
 
index e0396aa..971cbfd 100644 (file)
@@ -94,32 +94,31 @@ class BuildRequestsFetcher {
         if (array_key_exists($commit_set_id, $this->commit_sets_by_id))
             return;
 
         if (array_key_exists($commit_set_id, $this->commit_sets_by_id))
             return;
 
-        $commit_rows = $this->db->query_and_fetch_all('SELECT *
+        $commit_set_items = $this->db->query_and_fetch_all('SELECT *
             FROM commit_set_items LEFT OUTER JOIN  commits ON commitset_commit = commit_id
                 LEFT OUTER JOIN repositories ON repository_id = commit_repository
                 WHERE commitset_set = $1', array($commit_set_id));
 
             FROM commit_set_items LEFT OUTER JOIN  commits ON commitset_commit = commit_id
                 LEFT OUTER JOIN repositories ON repository_id = commit_repository
                 WHERE commitset_set = $1', array($commit_set_id));
 
-        $commit_ids = array();
         $custom_roots = array();
         $custom_roots = array();
-
-        foreach ($commit_rows as $row) {
+        $revision_items = array();
+        foreach ($commit_set_items as $row) {
             $repository_id = $resolve_ids ? $row['repository_name'] : $row['repository_id'];
             $revision = $row['commit_revision'];
             $commit_time = $row['commit_time'];
 
             $root_file_id = $row['commitset_root_file'];
             $repository_id = $resolve_ids ? $row['repository_name'] : $row['repository_id'];
             $revision = $row['commit_revision'];
             $commit_time = $row['commit_time'];
 
             $root_file_id = $row['commitset_root_file'];
-            if ($root_file_id) {
-                if (!array_key_exists($root_file_id, $this->uploaded_files_by_id)) {
-                    $uploaded_file_row = $this->db->select_first_row('uploaded_files', 'file', array('id' => $root_file_id));
-                    array_push($this->uploaded_files, format_uploaded_file($uploaded_file_row));
-                }
+            $commit_id = $row['commitset_commit'];
+            if ($root_file_id && !$commit_id) {
+                $this->add_uploaded_file($root_file_id);
                 array_push($custom_roots, $root_file_id);
                 continue;
             }
 
                 array_push($custom_roots, $root_file_id);
                 continue;
             }
 
-            array_push($commit_ids, $row['commit_id']);
+            $patch_file_id = $row['commitset_patch_file'];
+            if ($patch_file_id)
+                $this->add_uploaded_file($patch_file_id);
+            array_push($revision_items, array('commit' => $row['commit_id'], 'patch' => $patch_file_id));
 
 
-            $commit_id = $row['commit_id'];
             if (array_key_exists($commit_id, $this->commits_by_id))
                 continue;
 
             if (array_key_exists($commit_id, $this->commits_by_id))
                 continue;
 
@@ -134,7 +133,15 @@ class BuildRequestsFetcher {
 
         $this->commit_sets_by_id[$commit_set_id] = TRUE;
 
 
         $this->commit_sets_by_id[$commit_set_id] = TRUE;
 
-        array_push($this->commit_sets, array('id' => $commit_set_id, 'commits' => $commit_ids, 'customRoots' => $custom_roots));
+        array_push($this->commit_sets, array('id' => $commit_set_id, 'revisionItems' => $revision_items, 'customRoots' => $custom_roots));
+    }
+
+    private function add_uploaded_file($root_file_id)
+    {
+        if (!array_key_exists($root_file_id, $this->uploaded_files_by_id)) {
+            $uploaded_file_row = $this->db->select_first_row('uploaded_files', 'file', array('id' => $root_file_id));
+            array_push($this->uploaded_files, format_uploaded_file($uploaded_file_row));
+        }
     }
 }
 
     }
 }
 
index 0daa254..80a622e 100644 (file)
@@ -213,7 +213,9 @@ class ManifestGenerator {
             foreach ($group_repositories as &$repository_row) {
                 $group_id = $repository_row['trigrepo_group'];
                 array_ensure_item_has_array($repository_set_by_group, $group_id);
             foreach ($group_repositories as &$repository_row) {
                 $group_id = $repository_row['trigrepo_group'];
                 array_ensure_item_has_array($repository_set_by_group, $group_id);
-                array_push($repository_set_by_group[$group_id], $repository_row['trigrepo_repository']);
+                array_push($repository_set_by_group[$group_id], array(
+                    'repository' => $repository_row['trigrepo_repository'],
+                    'acceptsPatch' => Database::is_true($repository_row['trigrepo_accepts_patch'])));
             }
             foreach ($repository_groups as &$group_row) {
                 $triggerable_id = $group_row['repositorygroup_triggerable'];
             }
             foreach ($repository_groups as &$group_row) {
                 $triggerable_id = $group_row['repositorygroup_triggerable'];
@@ -229,7 +231,8 @@ class ManifestGenerator {
                     'acceptsCustomRoots' => Database::is_true($group_row['repositorygroup_accepts_roots']),
                     'repositories' => $repository_list));
                 // V2 UI compatibility.
                     'acceptsCustomRoots' => Database::is_true($group_row['repositorygroup_accepts_roots']),
                     'repositories' => $repository_list));
                 // V2 UI compatibility.
-                foreach ($repository_list as $repository_id) {
+                foreach ($repository_list as $repository_data) {
+                    $repository_id = $repository_data['repository'];
                     $set = &$triggerable_id_to_repository_set[$triggerable_id];
                     if (array_key_exists($repository_id, $set))
                         continue;
                     $set = &$triggerable_id_to_repository_set[$triggerable_id];
                     if (array_key_exists($repository_id, $set))
                         continue;
index 7264153..15ddc78 100644 (file)
@@ -7,6 +7,7 @@ class RepositoryGroupFinder
         $this->db = $db;
         $this->triggerable_id = $triggerable_id;
         $this->repositories_by_group = NULL;
         $this->db = $db;
         $this->triggerable_id = $triggerable_id;
         $this->repositories_by_group = NULL;
+        $this->accepts_patch_by_group = NULL;
     }
 
     function find_by_repositories($repositories)
     }
 
     function find_by_repositories($repositories)
@@ -21,6 +22,13 @@ class RepositoryGroupFinder
         return NULL;
     }
 
         return NULL;
     }
 
+    function accepts_patch($group_id, $repository_id)
+    {
+        if ($this->accepts_patch_by_group === NULL)
+            $this->populate_map();
+        return array_get($this->accepts_patch_by_group[$group_id], $repository_id, FALSE);
+    }
+
     private function populate_map()
     {
         $repository_rows = $this->db->query_and_fetch_all('SELECT * FROM triggerable_repositories WHERE trigrepo_group IN
     private function populate_map()
     {
         $repository_rows = $this->db->query_and_fetch_all('SELECT * FROM triggerable_repositories WHERE trigrepo_group IN
@@ -30,13 +38,18 @@ class RepositoryGroupFinder
             exit_with_error('FailedToFetchRepositoryGroups', array('triggerable' => $this->triggerable_id));
 
         $repositories_by_group = array();
             exit_with_error('FailedToFetchRepositoryGroups', array('triggerable' => $this->triggerable_id));
 
         $repositories_by_group = array();
+        $accepts_patch_by_group = array();
         foreach ($repository_rows as &$row) {
             $group_id = $row['trigrepo_group'];
         foreach ($repository_rows as &$row) {
             $group_id = $row['trigrepo_group'];
+            $repository_id = $row['trigrepo_repository'];
             array_ensure_item_has_array($repositories_by_group, $group_id);
             array_ensure_item_has_array($repositories_by_group, $group_id);
-            array_push($repositories_by_group[$group_id], $row['trigrepo_repository']);
+            array_push($repositories_by_group[$group_id], $repository_id);
+            array_ensure_item_has_array($accepts_patch_by_group, $group_id);
+            $accepts_patch_by_group[$group_id][$repository_id] = Database::is_true($row['trigrepo_accepts_patch']);
         }
 
         $this->repositories_by_group = &$repositories_by_group;
         }
 
         $this->repositories_by_group = &$repositories_by_group;
+        $this->accepts_patch_by_group = &$accepts_patch_by_group;
     }
 }
 
     }
 }
 
index e0e5843..f92d288 100644 (file)
@@ -21,7 +21,7 @@ function main()
     $platform_id = array_get($data, 'platform');
     $test_id = array_get($data, 'test');
     $revision_set_list = array_get($data, 'revisionSets');
     $platform_id = array_get($data, 'platform');
     $test_id = array_get($data, 'test');
     $revision_set_list = array_get($data, 'revisionSets');
-    $commit_sets_info = array_get($data, 'commitSets');
+    $commit_sets_info = array_get($data, 'commitSets'); // V2 UI compatibility
 
     if (!$task_id == !$task_name)
         exit_with_error('InvalidTask');
 
     if (!$task_id == !$task_name)
         exit_with_error('InvalidTask');
@@ -46,6 +46,12 @@ function main()
         $task = $db->select_first_row('analysis_tasks', 'task', array('id' => $task_id));
         if (!$task)
             exit_with_error('InvalidTask', array('task' => $task_id));
         $task = $db->select_first_row('analysis_tasks', 'task', array('id' => $task_id));
         if (!$task)
             exit_with_error('InvalidTask', array('task' => $task_id));
+
+        $duplicate_test_group = $db->select_first_row('analysis_test_groups', 'testgroup', array('task' => $task_id, 'name' => $name));
+        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'];
         $triggerable = find_triggerable_for_task($db, $task_id);
         if ($triggerable) {
             $triggerable_id = $triggerable['id'];
@@ -81,10 +87,12 @@ function main()
         $task_id = $db->insert_row('analysis_tasks', 'task', array('name' => $task_name, 'author' => $author));
 
     $configuration_list = array();
         $task_id = $db->insert_row('analysis_tasks', 'task', array('name' => $task_name, 'author' => $author));
 
     $configuration_list = array();
+    $needs_to_build = FALSE;
     foreach ($commit_sets as $commit_list) {
         $commit_set_id = $db->insert_row('commit_sets', 'commitset', array());
         foreach ($commit_list['set'] as $commit_row) {
             $commit_row['set'] = $commit_set_id;
     foreach ($commit_sets as $commit_list) {
         $commit_set_id = $db->insert_row('commit_sets', 'commitset', array());
         foreach ($commit_list['set'] as $commit_row) {
             $commit_row['set'] = $commit_set_id;
+            $needs_to_build = $needs_to_build || $commit_row['patch_file'];
             $db->insert_row('commit_set_items', 'commitset', $commit_row, 'commit');
         }
         array_push($configuration_list, array('commit_set' => $commit_set_id, 'repository_group' => $commit_list['repository_group']));
             $db->insert_row('commit_set_items', 'commitset', $commit_row, 'commit');
         }
         array_push($configuration_list, array('commit_set' => $commit_set_id, 'repository_group' => $commit_list['repository_group']));
@@ -93,6 +101,22 @@ function main()
     $group_id = $db->insert_row('analysis_test_groups', 'testgroup',
         array('task' => $task_id, 'name' => $name, 'author' => $author));
 
     $group_id = $db->insert_row('analysis_test_groups', 'testgroup',
         array('task' => $task_id, 'name' => $name, 'author' => $author));
 
+    if ($needs_to_build) {
+        $order = -count($configuration_list);
+        foreach ($configuration_list as $config) {
+            assert($order < 0);
+            $db->insert_row('build_requests', 'request', array(
+                'triggerable' => $triggerable_id,
+                'repository_group' => $config['repository_group'],
+                'platform' => $platform_id,
+                'test' => NULL,
+                'group' => $group_id,
+                'order' => $order,
+                'commit_set' => $config['commit_set']));
+            $order++;
+        }        
+    }
+
     $order = 0;
     for ($i = 0; $i < $repetition_count; $i++) {
         foreach ($configuration_list as $config) {
     $order = 0;
     for ($i = 0; $i < $repetition_count; $i++) {
         foreach ($configuration_list as $config) {
@@ -103,7 +127,7 @@ function main()
                 'test' => $test_id,
                 'group' => $group_id,
                 'order' => $order,
                 'test' => $test_id,
                 'group' => $group_id,
                 'order' => $order,
-                'commit_set' => $config['commit_set'],));
+                'commit_set' => $config['commit_set']));
             $order++;
         }
     }
             $order++;
         }
     }
@@ -126,23 +150,39 @@ function commit_sets_from_revision_sets($db, $triggerable_id, $revision_set_list
 
         $commit_set = array();
         $repository_list = array();
 
         $commit_set = array();
         $repository_list = array();
-        foreach ($revision_set as $repository_id => $revision) {
+        $repository_with_patch = array();
+        foreach ($revision_set as $repository_id => $data) {
             if ($repository_id == 'customRoots') {
             if ($repository_id == 'customRoots') {
-                $file_id_list = $revision;
+                $file_id_list = $data;
                 foreach ($file_id_list as $file_id) {
                 foreach ($file_id_list as $file_id) {
-                    if (!$db->select_first_row('uploaded_files', 'file', array('id' => $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));
                         exit_with_error('InvalidUploadedFile', array('file' => $file_id));
-                    array_push($commit_set, array('root_file' => $file_id));
+                    array_push($commit_set, array('root_file' => $file_id, 'patch_file' => NULL));
                 }
                 continue;
             }
             if (!is_numeric($repository_id))
                 exit_with_error('InvalidRepository', array('repository' => $repository_id));
                 }
                 continue;
             }
             if (!is_numeric($repository_id))
                 exit_with_error('InvalidRepository', array('repository' => $repository_id));
+
+            if (!is_array($data))
+                exit_with_error('InvalidRepositoryData', array('repository' => $repository_id, 'data' => $data));
+
+            $revision = array_get($data, 'revision');
+            if (!$revision)
+                exit_with_error('InvalidRevision', array('repository' => $repository_id, 'data' => $data));
             $commit = $db->select_first_row('commits', 'commit',
                 array('repository' => intval($repository_id), 'revision' => $revision));
             if (!$commit)
                 exit_with_error('RevisionNotFound', array('repository' => $repository_id, 'revision' => $revision));
             $commit = $db->select_first_row('commits', 'commit',
                 array('repository' => intval($repository_id), 'revision' => $revision));
             if (!$commit)
                 exit_with_error('RevisionNotFound', array('repository' => $repository_id, 'revision' => $revision));
-            array_push($commit_set, array('commit' => $commit['commit_id']));
+
+            $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);
+            }
+
+            array_push($commit_set, array('commit' => $commit['commit_id'], 'patch_file' => $patch_file_id));
             array_push($repository_list, $repository_id);
         }
 
             array_push($repository_list, $repository_id);
         }
 
@@ -150,6 +190,11 @@ function commit_sets_from_revision_sets($db, $triggerable_id, $revision_set_list
         if (!$repository_group_id)
             exit_with_error('NoMatchingRepositoryGroup', array('repositoris' => $repository_list));
 
         if (!$repository_group_id)
             exit_with_error('NoMatchingRepositoryGroup', array('repositoris' => $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));
+        }
+
         array_push($commit_set_list, array('repository_group' => $repository_group_id, 'set' => $commit_set));
     }
 
         array_push($commit_set_list, array('repository_group' => $repository_group_id, 'set' => $commit_set));
     }
 
@@ -174,7 +219,7 @@ 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()));
             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']));
+            array_push($commit_sets[$i]['set'], array('commit' => $commit['commit_id'], 'patch_file' => NULL));
         }
     }
 
         }
     }
 
index d328769..cab68ad 100644 (file)
@@ -11,6 +11,7 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
         this._showComparison = false;
         this._commitSetMap = {};
         this._specifiedRevisions = {'Baseline': new Map, 'Comparison': new Map};
         this._showComparison = false;
         this._commitSetMap = {};
         this._specifiedRevisions = {'Baseline': new Map, 'Comparison': new Map};
+        this._patchUploaders = {'Baseline': new Map, 'Comparison': new Map};
         this._fetchedRevisions = {'Baseline': new Map, 'Comparison': new Map};
         this._repositoryGroupByConfiguration = {'Baseline': null, 'Comparison': null};
         this._updateTriggerableLazily = new LazilyEvaluatedFunction(this._updateTriggerable.bind(this));
         this._fetchedRevisions = {'Baseline': new Map, 'Comparison': new Map};
         this._repositoryGroupByConfiguration = {'Baseline': null, 'Comparison': null};
         this._updateTriggerableLazily = new LazilyEvaluatedFunction(this._updateTriggerable.bind(this));
@@ -19,7 +20,7 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
         this._renderTriggerablePlatformsLazily = new LazilyEvaluatedFunction(this._renderTriggerablePlatforms.bind(this));
         this._renderRepositoryPanesLazily = new LazilyEvaluatedFunction(this._renderRepositoryPanes.bind(this));
 
         this._renderTriggerablePlatformsLazily = new LazilyEvaluatedFunction(this._renderTriggerablePlatforms.bind(this));
         this._renderRepositoryPanesLazily = new LazilyEvaluatedFunction(this._renderRepositoryPanes.bind(this));
 
-        this._fileUploaders = {};
+        this._customRootUploaders = {};
     }
 
     tests() { return this._selectedTests; }
     }
 
     tests() { return this._selectedTests; }
@@ -64,14 +65,14 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
         const baselineRepositoryGroup = triggerable.repositoryGroups().find((repositoryGroup) => repositoryGroup.accepts(baselineCommitSet));
         if (baselineRepositoryGroup) {
             this._repositoryGroupByConfiguration['Baseline'] = baselineRepositoryGroup;
         const baselineRepositoryGroup = triggerable.repositoryGroups().find((repositoryGroup) => repositoryGroup.accepts(baselineCommitSet));
         if (baselineRepositoryGroup) {
             this._repositoryGroupByConfiguration['Baseline'] = baselineRepositoryGroup;
-            this._setUploadedFilesIfEmpty(this._fileUploaders['Baseline'], baselineCommitSet);
+            this._setUploadedFilesIfEmpty(this._customRootUploaders['Baseline'], baselineCommitSet);
             this._specifiedRevisions['Baseline'] = this._revisionMapFromCommitSet(baselineCommitSet);
         }
 
         const comparisonRepositoryGroup = triggerable.repositoryGroups().find((repositoryGroup) => repositoryGroup.accepts(baselineCommitSet));
         if (comparisonRepositoryGroup) {
             this._repositoryGroupByConfiguration['Comparison'] = comparisonRepositoryGroup;
             this._specifiedRevisions['Baseline'] = this._revisionMapFromCommitSet(baselineCommitSet);
         }
 
         const comparisonRepositoryGroup = triggerable.repositoryGroups().find((repositoryGroup) => repositoryGroup.accepts(baselineCommitSet));
         if (comparisonRepositoryGroup) {
             this._repositoryGroupByConfiguration['Comparison'] = comparisonRepositoryGroup;
-            this._setUploadedFilesIfEmpty(this._fileUploaders['Comparison'], comparisonCommitSet);
+            this._setUploadedFilesIfEmpty(this._customRootUploaders['Comparison'], comparisonCommitSet);
             this._specifiedRevisions['Comparison'] = this._revisionMapFromCommitSet(comparisonCommitSet);
         }
 
             this._specifiedRevisions['Comparison'] = this._revisionMapFromCommitSet(comparisonCommitSet);
         }
 
@@ -99,18 +100,41 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
     {
         this.content('specify-comparison-button').onclick = this.createEventHandler(() => this._configureComparison());
 
     {
         this.content('specify-comparison-button').onclick = this.createEventHandler(() => this._configureComparison());
 
-        const baselineRootsUploader = new InstantFileUploader;
+        const createRootUploader = () => {
+            const uploader = new InstantFileUploader;
+            uploader.allowMultipleFiles();
+            uploader.element().textContent = 'Add a new root';
+            uploader.listenToAction('removedFile', () => this._updateCommitSetMap());
+            return uploader;
+        }
+
+        const baselineRootsUploader = createRootUploader();
         baselineRootsUploader.listenToAction('uploadedFile', (uploadedFile) => {
             comparisonRootsUploader.addUploadedFile(uploadedFile);
             this._updateCommitSetMap();
         });
         baselineRootsUploader.listenToAction('uploadedFile', (uploadedFile) => {
             comparisonRootsUploader.addUploadedFile(uploadedFile);
             this._updateCommitSetMap();
         });
-        baselineRootsUploader.listenToAction('removedFile', () => this._updateCommitSetMap());
-        this._fileUploaders['Baseline'] = baselineRootsUploader;
 
 
-        const comparisonRootsUploader = new InstantFileUploader;
+        this._customRootUploaders['Baseline'] = baselineRootsUploader;
+
+        const comparisonRootsUploader = createRootUploader();
         comparisonRootsUploader.listenToAction('uploadedFile', () => this._updateCommitSetMap());
         comparisonRootsUploader.listenToAction('uploadedFile', () => this._updateCommitSetMap());
-        comparisonRootsUploader.listenToAction('removedFile', () => this._updateCommitSetMap());
-        this._fileUploaders['Comparison'] = comparisonRootsUploader;
+        this._customRootUploaders['Comparison'] = comparisonRootsUploader;
+    }
+
+    _ensurePatchUploader(configurationName, repository)
+    {
+        const uploaderMap = this._patchUploaders[configurationName];
+        let uploader = uploaderMap.get(repository);
+        if (uploader)
+            return uploader;
+
+        uploader = new InstantFileUploader;
+        uploader.element().textContent = 'Apply a patch';
+        uploader.listenToAction('uploadedFile', () => this._updateCommitSetMap());
+        uploader.listenToAction('removedFile', () => this._updateCommitSetMap());
+        uploaderMap.set(repository, uploader);
+
+        return uploader;
     }
 
     _configureComparison()
     }
 
     _configureComparison()
@@ -268,7 +292,7 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
         if (!repositoryGroup)
             return null;
 
         if (!repositoryGroup)
             return null;
 
-        const fileUploader = this._fileUploaders[configurationName];
+        const fileUploader = this._customRootUploaders[configurationName];
         if (!fileUploader || fileUploader.hasFileToUpload())
             return null;
 
         if (!fileUploader || fileUploader.hasFileToUpload())
             return null;
 
@@ -279,7 +303,18 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
                 revision = this._fetchedRevisions[configurationName].get(repository);
             if (!revision)
                 return null;
                 revision = this._fetchedRevisions[configurationName].get(repository);
             if (!revision)
                 return null;
-            commitSet.setRevisionForRepository(repository, revision);
+            let patch = null;
+            if (repositoryGroup.acceptsPatchForRepository(repository)) {
+                const uploaderMap = this._patchUploaders[configurationName];
+                const uploader = uploaderMap.get(repository);
+                if (uploader) {
+                    const files = uploader.uploadedFiles();
+                    console.assert(files.length <= 1);
+                    if (files.length)
+                        patch = files[0];
+                }
+            }
+            commitSet.setRevisionForRepository(repository, revision, patch);
         }
 
         for (let uploadedFile of fileUploader.uploadedFiles())
         }
 
         for (let uploadedFile of fileUploader.uploadedFiles())
@@ -351,7 +386,7 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
         const customRootsTBody = element('tbody', [
             element('tr', [
                 element('th', 'Roots'),
         const customRootsTBody = element('tbody', [
             element('tr', [
                 element('th', 'Roots'),
-                element('td', this._fileUploaders[configurationName]),
+                element('td', this._customRootUploaders[configurationName]),
             ]),
         ]);
 
             ]),
         ]);
 
@@ -371,9 +406,15 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
             !alwaysAcceptsCustomRoots && currentGroup && currentGroup.acceptsCustomRoots() ? customRootsTBody : [],
             element('tbody',
                 optionalRepositoryList.map((repository) => {
             !alwaysAcceptsCustomRoots && currentGroup && currentGroup.acceptsCustomRoots() ? customRootsTBody : [],
             element('tbody',
                 optionalRepositoryList.map((repository) => {
+                    let uploader = currentGroup.acceptsPatchForRepository(repository)
+                        ? this._ensurePatchUploader(configurationName, repository) : null;
+
                     return element('tr',[
                         element('th', repository.name()),
                     return element('tr',[
                         element('th', repository.name()),
-                        element('td', this._buildRevisionInput(configurationName, repository, platform))
+                        element('td', [
+                            this._buildRevisionInput(configurationName, repository, platform),
+                            uploader || [],
+                        ])
                     ]);
                 })
             )];
                     ]);
                 })
             )];
@@ -544,14 +585,10 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
                 display: none;
             }
 
                 display: none;
             }
 
-            .revision-table tbody tr:first-child td,
-            .revision-table tbody tr:first-child th {
+            .revision-table tbody td,
+            .revision-table tbody th {
                 border-top: solid 1px #ddd;
                 padding-top: 0.5rem;
                 border-top: solid 1px #ddd;
                 padding-top: 0.5rem;
-            }
-
-            .revision-table tbody tr:last-child td,
-            .revision-table tbody tr:last-child th {
                 padding-bottom: 0.5rem;
             }
 
                 padding-bottom: 0.5rem;
             }
 
index 64688dc..daef4a5 100644 (file)
@@ -2,6 +2,8 @@ class InstantFileUploader extends ComponentBase {
     constructor()
     {
         super('instant-file-uploader');
     constructor()
     {
         super('instant-file-uploader');
+        this._fileInput = null;
+        this._allowMultipleFiles = false;
         this._uploadedFiles = [];
         this._preuploadFiles = [];
         this._uploadProgress = new WeakMap;
         this._uploadedFiles = [];
         this._preuploadFiles = [];
         this._uploadProgress = new WeakMap;
@@ -14,6 +16,12 @@ class InstantFileUploader extends ComponentBase {
     hasFileToUpload() { return !!this._preuploadFiles.length; }
     uploadedFiles() { return this._uploadedFiles; }
 
     hasFileToUpload() { return !!this._preuploadFiles.length; }
     uploadedFiles() { return this._uploadedFiles; }
 
+    allowMultipleFiles()
+    {
+        this._allowMultipleFiles = true;
+        this.enqueueToRender();
+    }
+
     addUploadedFile(uploadedFile)
     {
         console.assert(uploadedFile instanceof UploadedFile);
     addUploadedFile(uploadedFile)
     {
         console.assert(uploadedFile instanceof UploadedFile);
@@ -25,8 +33,13 @@ class InstantFileUploader extends ComponentBase {
 
     didConstructShadowTree()
     {
 
     didConstructShadowTree()
     {
-        const input = this.content('file-input');
-        input.onchange = () => this._didFileInputChange(input);
+        this.content('file-adder').onclick = () => {
+            inputElement.click();
+        }
+        const inputElement = document.createElement('input');
+        inputElement.type = 'file';
+        inputElement.onchange = () => this._didFileInputChange(inputElement);
+        this._fileInput = inputElement;
     }
 
     render()
     }
 
     render()
@@ -34,6 +47,8 @@ class InstantFileUploader extends ComponentBase {
         this._renderUploadedFilesLazily.evaluate(...this._uploadedFiles);
         const uploadStatusElements = this._renderPreuploadFilesLazily.evaluate(...this._preuploadFiles);
         this._updateUploadStatus(uploadStatusElements);
         this._renderUploadedFilesLazily.evaluate(...this._uploadedFiles);
         const uploadStatusElements = this._renderPreuploadFilesLazily.evaluate(...this._preuploadFiles);
         this._updateUploadStatus(uploadStatusElements);
+        const fileCount = this._uploadedFiles.length + this._preuploadFiles.length;
+        this.content('file-adder').style.display = this._allowMultipleFiles || !fileCount ? null : 'none';
     }
 
     _renderUploadedFiles(...uploadedFiles)
     }
 
     _renderUploadedFiles(...uploadedFiles)
@@ -167,7 +182,7 @@ class InstantFileUploader extends ComponentBase {
     {
         return `<ul id="uploaded-files"></ul>
             <ul id="preupload-files"></ul>
     {
         return `<ul id="uploaded-files"></ul>
             <ul id="preupload-files"></ul>
-            <input id="file-input" type="file" multiple="false">`;
+            <button id="file-adder"><slot>Add a new file</slot></button>`;
     }
 
     static cssTemplate()
     }
 
     static cssTemplate()
index 31b8c65..3c7c9ca 100644 (file)
@@ -16,7 +16,7 @@ class BuildRequest extends DataModelObject {
         this._repositoryGroup = object.repositoryGroup;
         console.assert(object.platform instanceof Platform);
         this._platform = object.platform;
         this._repositoryGroup = object.repositoryGroup;
         console.assert(object.platform instanceof Platform);
         this._platform = object.platform;
-        console.assert(object.test instanceof Test);
+        console.assert(!object.test || object.test instanceof Test);
         this._test = object.test;
         this._order = object.order;
         console.assert(object.commitSet instanceof CommitSet);
         this._test = object.test;
         this._order = object.order;
         console.assert(object.commitSet instanceof CommitSet);
@@ -45,6 +45,8 @@ class BuildRequest extends DataModelObject {
     repositoryGroup() { return this._repositoryGroup; }
     platform() { return this._platform; }
     test() { return this._test; }
     repositoryGroup() { return this._repositoryGroup; }
     platform() { return this._platform; }
     test() { return this._test; }
+    isBuild() { return this._order < 0; }
+    isTest() { return this._order >= 0; }
     order() { return +this._order; }
     commitSet() { return this._commitSet; }
 
     order() { return +this._order; }
     commitSet() { return this._commitSet; }
 
@@ -121,18 +123,21 @@ class BuildRequest extends DataModelObject {
 
     static constructBuildRequestsFromData(data)
     {
 
     static constructBuildRequestsFromData(data)
     {
-        const commitIdMap = {};
-        for (let commit of data['commits']) {
-            commitIdMap[commit.id] = commit;
-            commit.repository = Repository.findById(commit.repository);
+        for (let rawData of data['commits']) {
+            rawData.repository = Repository.findById(rawData.repository);
+            CommitLog.ensureSingleton(rawData.id, rawData);
         }
 
         for (let uploadedFile of data['uploadedFiles'])
             UploadedFile.ensureSingleton(uploadedFile.id, uploadedFile);
 
         }
 
         for (let uploadedFile of data['uploadedFiles'])
             UploadedFile.ensureSingleton(uploadedFile.id, uploadedFile);
 
-        const commitSets = data['commitSets'].map((row) => {
-            row.commits = row.commits.map((commitId) => commitIdMap[commitId]);
-            return CommitSet.ensureSingleton(row.id, row);
+        const commitSets = data['commitSets'].map((rawData) => {
+            for (const item of rawData.revisionItems) {
+                item.commit = CommitLog.findById(item.commit);
+                item.patch = item.patch ? UploadedFile.findById(item.patch) : null;
+            }
+            rawData.customRoots = rawData.customRoots.map((fileId) => UploadedFile.findById(fileId));
+            return CommitSet.ensureSingleton(rawData.id, rawData);
         });
 
         return data['buildRequests'].map(function (rawData) {
         });
 
         return data['buildRequests'].map(function (rawData) {
index af79fff..2cd5832 100644 (file)
@@ -6,42 +6,45 @@ class CommitSet extends DataModelObject {
     {
         super(id);
         this._repositories = [];
     {
         super(id);
         this._repositories = [];
-        this._repositoryToCommitMap = {};
+        this._repositoryToCommitMap = new Map;
+        this._repositoryToPatchMap = new Map;
         this._latestCommitTime = null;
         this._customRoots = [];
 
         if (!object)
             return;
 
         this._latestCommitTime = null;
         this._customRoots = [];
 
         if (!object)
             return;
 
-        for (let row of object.commits) {
-            const repositoryId = row.repository.id();
-            console.assert(!this._repositoryToCommitMap[repositoryId]);
-            this._repositoryToCommitMap[repositoryId] = CommitLog.ensureSingleton(row.id, row);
-            this._repositories.push(row.repository);
-        }
-        for (let fileId of object.customRoots) {
-            const uploadedFile = UploadedFile.findById(fileId);
-            this._customRoots.push(uploadedFile);
+        for (const item of object.revisionItems) {
+            const commit = item.commit;
+            console.assert(commit instanceof CommitLog);
+            console.assert(!item.patch || item.patch instanceof UploadedFile);
+            const repository = commit.repository();
+            this._repositoryToCommitMap.set(repository, commit);
+            this._repositoryToPatchMap.set(repository, item.patch);
+            this._repositories.push(commit.repository());
         }
         }
+        this._customRoots = object.customRoots;
     }
 
     repositories() { return this._repositories; }
     customRoots() { return this._customRoots; }
     }
 
     repositories() { return this._repositories; }
     customRoots() { return this._customRoots; }
-    commitForRepository(repository) { return this._repositoryToCommitMap[repository.id()]; }
+    commitForRepository(repository) { return this._repositoryToCommitMap.get(repository); }
 
     revisionForRepository(repository)
     {
 
     revisionForRepository(repository)
     {
-        var commit = this._repositoryToCommitMap[repository.id()];
+        var commit = this._repositoryToCommitMap.get(repository);
         return commit ? commit.revision() : null;
     }
 
         return commit ? commit.revision() : null;
     }
 
+    patchForRepository(repository) { return this._repositoryToPatchMap.get(repository); }
+
     // FIXME: This should return a Date object.
     latestCommitTime()
     {
         if (this._latestCommitTime == null) {
             var maxTime = 0;
     // FIXME: This should return a Date object.
     latestCommitTime()
     {
         if (this._latestCommitTime == null) {
             var maxTime = 0;
-            for (var repositoryId in this._repositoryToCommitMap)
-                maxTime = Math.max(maxTime, +this._repositoryToCommitMap[repositoryId].time());
+            for (const [repository, commit] of this._repositoryToCommitMap)
+                maxTime = Math.max(maxTime, +commit.time());
             this._latestCommitTime = maxTime;
         }
         return this._latestCommitTime;
             this._latestCommitTime = maxTime;
         }
         return this._latestCommitTime;
@@ -51,8 +54,10 @@ class CommitSet extends DataModelObject {
     {
         if (this._repositories.length != other._repositories.length)
             return false;
     {
         if (this._repositories.length != other._repositories.length)
             return false;
-        for (var repositoryId in this._repositoryToCommitMap) {
-            if (this._repositoryToCommitMap[repositoryId] != other._repositoryToCommitMap[repositoryId])
+        for (const [repository, commit] of this._repositoryToCommitMap) {
+            if (commit != other._repositoryToCommitMap.get(repository))
+                return false;
+            if (this._repositoryToPatchMap.get(repository) != other._repositoryToCommitMap.get(repository))
                 return false;
         }
         return CommitSet.areCustomRootsEqual(this._customRoots, other._customRoots);
                 return false;
         }
         return CommitSet.areCustomRootsEqual(this._customRoots, other._customRoots);
@@ -100,7 +105,9 @@ class MeasurementCommitSet extends CommitSet {
             if (!repository)
                 continue;
 
             if (!repository)
                 continue;
 
-            this._repositoryToCommitMap[repositoryId] = CommitLog.ensureSingleton(commitId, {repository: repository, revision: revision, time: time});
+            // FIXME: Add a flag to remember the fact this commit log is incomplete.
+            const commit = CommitLog.ensureSingleton(commitId, {repository: repository, revision: revision, time: time});
+            this._repositoryToCommitMap.set(repository, commit);
             this._repositories.push(repository);
         }
     }
             this._repositories.push(repository);
         }
     }
@@ -127,10 +134,11 @@ class CustomCommitSet {
         this._customRoots = [];
     }
 
         this._customRoots = [];
     }
 
-    setRevisionForRepository(repository, revision)
+    setRevisionForRepository(repository, revision, patch = null)
     {
         console.assert(repository instanceof Repository);
     {
         console.assert(repository instanceof Repository);
-        this._revisionListByRepository.set(repository, revision);
+        console.assert(!patch || patch instanceof UploadedFile);
+        this._revisionListByRepository.set(repository, {revision, patch});
     }
 
     equals(other)
     }
 
     equals(other)
@@ -141,14 +149,30 @@ class CustomCommitSet {
         for (let repository of this._revisionListByRepository.keys()) {
             const thisRevision = this._revisionListByRepository.get(repository);
             const otherRevision = other._revisionListByRepository.get(repository);
         for (let repository of this._revisionListByRepository.keys()) {
             const thisRevision = this._revisionListByRepository.get(repository);
             const otherRevision = other._revisionListByRepository.get(repository);
-            if (thisRevision != otherRevision)
+            if (!thisRevision != !otherRevision)
+                return false;
+            if (thisRevision && (thisRevision.revision != otherRevision.revision
+                || thisRevision.patch != otherRevision.patch))
                 return false;
         }
         return CommitSet.areCustomRootsEqual(this._customRoots, other._customRoots);
     }
 
     repositories() { return Array.from(this._revisionListByRepository.keys()); }
                 return false;
         }
         return CommitSet.areCustomRootsEqual(this._customRoots, other._customRoots);
     }
 
     repositories() { return Array.from(this._revisionListByRepository.keys()); }
-    revisionForRepository(repository) { return this._revisionListByRepository.get(repository); }
+    revisionForRepository(repository)
+    {
+        const entry = this._revisionListByRepository.get(repository);
+        if (!entry)
+            return null;
+        return entry.revision;
+    }
+    patchForRepository(repository)
+    {
+        const entry = this._revisionListByRepository.get(repository);
+        if (!entry)
+            return null;
+        return entry.patch;
+    }
     customRoots() { return this._customRoots; }
 
     addCustomRoot(uploadedFile)
     customRoots() { return this._customRoots; }
 
     addCustomRoot(uploadedFile)
index 973f721..1651de0 100644 (file)
@@ -48,7 +48,9 @@ class Manifest {
                 return Repository.findById(repositoryId);
             });
             raw.repositoryGroups = raw.repositoryGroups.map((group) => {
                 return Repository.findById(repositoryId);
             });
             raw.repositoryGroups = raw.repositoryGroups.map((group) => {
-                group.repositories = group.repositories.map((repositoryId) => Repository.findById(repositoryId));
+                group.repositories = group.repositories.map((entry) => {
+                    return {repository: Repository.findById(entry.repository), acceptsPatch: entry.acceptsPatch};
+                });
                 return TriggerableRepositoryGroup.ensureSingleton(group.id, group);
             });
             raw.configurations = raw.configurations.map((configuration) => {
                 return TriggerableRepositoryGroup.ensureSingleton(group.id, group);
             });
             raw.configurations = raw.configurations.map((configuration) => {
index cae9a4e..089f62e 100644 (file)
@@ -38,7 +38,7 @@ class Repository extends LabeledObject {
         return (this._blameUrl || '').replace(/\$1/g, from).replace(/\$2/g, to);
     }
 
         return (this._blameUrl || '').replace(/\$1/g, from).replace(/\$2/g, to);
     }
 
-    owner()
+    ownerId()
     {
         return this._ownerId;
     }
     {
         return this._ownerId;
     }
index 32be0a6..c622619 100644 (file)
@@ -10,8 +10,11 @@ class TestGroup extends LabeledObject {
         this._createdAt = new Date(object.createdAt);
         this._isHidden = object.hidden;
         this._buildRequests = [];
         this._createdAt = new Date(object.createdAt);
         this._isHidden = object.hidden;
         this._buildRequests = [];
-        this._requestsAreInOrder = false;
+        this._orderBuildRequestsLazily = new LazilyEvaluatedFunction((...buildRequests) => {
+            return buildRequests.sort((a, b) => a.order() - b.order());
+        });
         this._repositories = null;
         this._repositories = null;
+        this._computeRequestedCommitSetsLazily = new LazilyEvaluatedFunction(this._computeRequestedCommitSets.bind(this));
         this._requestedCommitSets = null;
         this._commitSetToLabel = new Map;
         console.assert(!object.platform || object.platform instanceof Platform);
         this._requestedCommitSets = null;
         this._commitSetToLabel = new Map;
         console.assert(!object.platform || object.platform instanceof Platform);
@@ -35,33 +38,41 @@ class TestGroup extends LabeledObject {
     addBuildRequest(request)
     {
         this._buildRequests.push(request);
     addBuildRequest(request)
     {
         this._buildRequests.push(request);
-        this._requestsAreInOrder = false;
         this._requestedCommitSets = null;
         this._commitSetToLabel.clear();
     }
 
     test()
     {
         this._requestedCommitSets = null;
         this._commitSetToLabel.clear();
     }
 
     test()
     {
-        if (!this._buildRequests.length)
-            return null;
-        return this._buildRequests[0].test();
+        const request = this._lastRequest();
+        return request ? request.test() : null;
     }
 
     platform()
     {
     }
 
     platform()
     {
-        if (!this._buildRequests.length)
-            return null;
-        return this._buildRequests[0].platform();
+        const request = this._lastRequest();
+        return request ? request.platform() : null;
+    }
+
+    _lastRequest()
+    {
+        const requests = this._orderedBuildRequests();
+        return requests.length ? requests[requests.length - 1] : null;
+    }
+
+    _orderedBuildRequests()
+    {
+        return this._orderBuildRequestsLazily.evaluate(...this._buildRequests);
     }
 
     repetitionCount()
     {
         if (!this._buildRequests.length)
             return 0;
     }
 
     repetitionCount()
     {
         if (!this._buildRequests.length)
             return 0;
-        var commitSet = this._buildRequests[0].commitSet();
-        var count = 0;
-        for (var request of this._buildRequests) {
-            if (request.commitSet() == commitSet)
+        const commitSet = this._buildRequests[0].commitSet();
+        let count = 0;
+        for (const request of this._buildRequests) {
+            if (request.isTest() && request.commitSet() == commitSet)
                 count++;
         }
         return count;
                 count++;
         }
         return count;
@@ -69,43 +80,33 @@ class TestGroup extends LabeledObject {
 
     requestedCommitSets()
     {
 
     requestedCommitSets()
     {
-        if (!this._requestedCommitSets) {
-            this._orderBuildRequests();
-            this._requestedCommitSets = [];
-            for (var request of this._buildRequests) {
-                var set = request.commitSet();
-                if (!this._requestedCommitSets.includes(set))
-                    this._requestedCommitSets.push(set);
-            }
-            this._requestedCommitSets.sort(function (a, b) { return a.latestCommitTime() - b.latestCommitTime(); });
-            var setIndex = 0;
-            for (var set of this._requestedCommitSets) {
-                this._commitSetToLabel.set(set, String.fromCharCode('A'.charCodeAt(0) + setIndex));
-                setIndex++;
-            }
+        return this._computeRequestedCommitSetsLazily.evaluate(...this._orderedBuildRequests());
+    }
 
 
+    _computeRequestedCommitSets(...orderedBuildRequests)
+    {
+        const requestedCommitSets = [];
+        const commitSetLabelMap = new Map;
+        for (const request of orderedBuildRequests) {
+            const set = request.commitSet();
+            if (!this._requestedCommitSets.includes(set))
+                this._requestedCommitSets.push(set);
         }
         }
-        return this._requestedCommitSets;
+        return requestedCommitSets;
     }
 
     requestsForCommitSet(commitSet)
     {
     }
 
     requestsForCommitSet(commitSet)
     {
-        this._orderBuildRequests();
-        return this._buildRequests.filter(function (request) { return request.commitSet() == commitSet; });
+        this._orderedBuildRequests().filter((request) => request.commitSet() == commitSet);
     }
 
     labelForCommitSet(commitSet)
     {
     }
 
     labelForCommitSet(commitSet)
     {
-        console.assert(this._requestedCommitSets);
-        return this._commitSetToLabel.get(commitSet);
-    }
-
-    _orderBuildRequests()
-    {
-        if (this._requestsAreInOrder)
-            return;
-        this._buildRequests = this._buildRequests.sort(function (a, b) { return a.order() - b.order(); });
-        this._requestsAreInOrder = true;
+        const requestedSets = this.requestedCommitSets();
+        const setIndex = requestedSets.indexOf(commitSet);
+        if (setIndex < 0)
+            return null;
+        return String.fromCharCode('A'.charCodeAt(0) + setIndex);
     }
 
     hasFinished()
     }
 
     hasFinished()
@@ -229,8 +230,13 @@ class TestGroup extends LabeledObject {
         return commitSets.map((commitSet) => {
             console.assert(commitSet instanceof CustomCommitSet || commitSet instanceof CommitSet);
             const revisionSet = {};
         return commitSets.map((commitSet) => {
             console.assert(commitSet instanceof CustomCommitSet || commitSet instanceof CommitSet);
             const revisionSet = {};
-            for (let repository of commitSet.repositories())
-                revisionSet[repository.id()] = commitSet.revisionForRepository(repository);
+            for (let repository of commitSet.repositories()) {
+                const patchFile = commitSet.patchForRepository(repository);
+                revisionSet[repository.id()] = {
+                    revision: commitSet.revisionForRepository(repository),
+                    patch: patchFile ? patchFile.id() : null,
+                };
+            }
             const customRoots = commitSet.customRoots();
             if (customRoots && customRoots.length)
                 revisionSet['customRoots'] = customRoots.map((uploadedFile) => uploadedFile.id());
             const customRoots = commitSet.customRoots();
             if (customRoots && customRoots.length)
                 revisionSet['customRoots'] = customRoots.map((uploadedFile) => uploadedFile.id());
index c6f81e2..3ea8e21 100644 (file)
@@ -59,21 +59,33 @@ class TriggerableRepositoryGroup extends LabeledObject {
         super(id, object);
         this._description = object.description;
         this._acceptsCustomRoots = !!object.acceptsCustomRoots;
         super(id, object);
         this._description = object.description;
         this._acceptsCustomRoots = !!object.acceptsCustomRoots;
-        this._repositories = Repository.sortByName(object.repositories);
+        this._repositories = Repository.sortByNamePreferringOnesWithURL(object.repositories.map((item) => item.repository));
+        this._patchAcceptingSet = new Set(object.repositories.filter((item) => item.acceptsPatch).map((item) => item.repository));
     }
 
     accepts(commitSet)
     {
     }
 
     accepts(commitSet)
     {
-        const commitSetRepositories = Repository.sortByName(commitSet.repositories());
+        // FIXME: Add a check for patch.
+        const commitSetRepositories = Repository.sortByNamePreferringOnesWithURL(commitSet.repositories());
         if (this._repositories.length != commitSetRepositories.length)
             return false;
         for (let i = 0; i < this._repositories.length; i++) {
         if (this._repositories.length != commitSetRepositories.length)
             return false;
         for (let i = 0; i < this._repositories.length; i++) {
-            if (this._repositories[i] != commitSetRepositories[i])
+            const currentRepository = this._repositories[i];
+            if (currentRepository != commitSetRepositories[i])
+                return false;
+            if (commitSet.patchForRepository(currentRepository) && !this._patchAcceptingSet.has(currentRepository))
                 return false;
         }
                 return false;
         }
+        if (commitSet.customRoots().length && !this._acceptsCustomRoots)
+            return false;
         return true;
     }
 
         return true;
     }
 
+    acceptsPatchForRepository(repository)
+    {
+        return this._patchAcceptingSet.has(repository);
+    }
+
     description() { return this._description || this.name(); }
     acceptsCustomRoots() { return this._acceptsCustomRoots; }
     repositories() { return this._repositories; }
     description() { return this._description || this.name(); }
     acceptsCustomRoots() { return this._acceptsCustomRoots; }
     repositories() { return this._repositories; }
index 92a1717..a0cbce3 100644 (file)
@@ -35,9 +35,9 @@ describe('/api/build-requests', function () {
 
             assert.equal(content['commitSets'].length, 2);
             assert.equal(content['commitSets'][0].id, 401);
 
             assert.equal(content['commitSets'].length, 2);
             assert.equal(content['commitSets'][0].id, 401);
-            assert.deepEqual(content['commitSets'][0].commits, ['87832', '93116']);
+            assert.deepEqual(content['commitSets'][0].revisionItems, [{commit: '87832', patch: null}, {commit: '93116', patch: null}]);
             assert.equal(content['commitSets'][1].id, 402);
             assert.equal(content['commitSets'][1].id, 402);
-            assert.deepEqual(content['commitSets'][1].commits, ['87832', '96336']);
+            assert.deepEqual(content['commitSets'][1].revisionItems, [{commit: '87832', patch: null}, {commit: '96336', patch: null}]);
 
             assert.equal(content['commits'].length, 3);
             assert.equal(content['commits'][0].id, 87832);
 
             assert.equal(content['commits'].length, 3);
             assert.equal(content['commits'][0].id, 87832);
@@ -89,9 +89,11 @@ describe('/api/build-requests', function () {
 
             assert.equal(content['commitSets'].length, 2);
             assert.equal(content['commitSets'][0].id, 401);
 
             assert.equal(content['commitSets'].length, 2);
             assert.equal(content['commitSets'][0].id, 401);
-            assert.deepEqual(content['commitSets'][0].commits, ['87832', '93116']);
+            assert.deepEqual(content['commitSets'][0].revisionItems,
+                [{commit: '87832', patch: null}, {commit: '93116', patch: null}]);
             assert.equal(content['commitSets'][1].id, 402);
             assert.equal(content['commitSets'][1].id, 402);
-            assert.deepEqual(content['commitSets'][1].commits, ['87832', '96336']);
+            assert.deepEqual(content['commitSets'][1].revisionItems,
+                [{commit: '87832', patch: null}, {commit: '96336', patch: null}]);
 
             assert.equal(content['commits'].length, 3);
             assert.equal(content['commits'][0].id, 87832);
 
             assert.equal(content['commits'].length, 3);
             assert.equal(content['commits'][0].id, 87832);
index ffe47bc..f0687e3 100644 (file)
@@ -341,7 +341,7 @@ describe('/api/manifest', function () {
 
             const osWebkit1 = Repository.findById(101);
             assert.equal(osWebkit1.name(), 'WebKit');
 
             const osWebkit1 = Repository.findById(101);
             assert.equal(osWebkit1.name(), 'WebKit');
-            assert.equal(osWebkit1.owner(), 9);
+            assert.equal(osWebkit1.ownerId(), 9);
             assert.equal(osWebkit1.urlForRevision(123), 'https://trac.webkit.org/123');
 
             const macos = Repository.findById(9);
             assert.equal(osWebkit1.urlForRevision(123), 'https://trac.webkit.org/123');
 
             const macos = Repository.findById(9);
index af9a964..2b46f3d 100644 (file)
@@ -67,8 +67,8 @@ describe('/api/update-triggerable/', function () {
         return MockData.addMockData(db).then(() => {
             return Promise.all([
                 addSlaveForReport(emptyUpdate),
         return MockData.addMockData(db).then(() => {
             return Promise.all([
                 addSlaveForReport(emptyUpdate),
-                db.insert('triggerable_configurations',
-                    {'triggerable': 1000 /* build-webkit */, 'test': MockData.someTestId(), 'platform': MockData.somePlatformId()})
+                db.insert('triggerable_configurations', {'triggerable': 1000 // build-webkit
+                    , 'test': MockData.someTestId(), 'platform': MockData.somePlatformId()})
             ]);
         }).then(() => {
             return TestServer.remoteAPI().postJSON('/api/update-triggerable/', emptyUpdate);
             ]);
         }).then(() => {
             return TestServer.remoteAPI().postJSON('/api/update-triggerable/', emptyUpdate);
@@ -122,7 +122,9 @@ describe('/api/update-triggerable/', function () {
                 {test: MockData.someTestId(), platform: MockData.somePlatformId()}
             ],
             'repositoryGroups': [
                 {test: MockData.someTestId(), platform: MockData.somePlatformId()}
             ],
             'repositoryGroups': [
-                {name: 'system-only', repositories: [MockData.macosRepositoryId()]},
+                {name: 'system-only', repositories: [
+                    {repository: MockData.macosRepositoryId(), acceptsPatch: false},
+                ]},
             ]
         };
     }
             ]
         };
     }
@@ -175,7 +177,7 @@ describe('/api/update-triggerable/', function () {
         });
     });
 
         });
     });
 
-    it('should reject when a repository group contains an invalid repository id', () => {
+    it('should reject when a repository group contains a repository data that is not an array', () => {
         const update = updateWithOSXRepositoryGroup();
         update.repositoryGroups[0].repositories[0] = 999;
         return MockData.addEmptyTriggerable(TestServer.database()).then(() => {
         const update = updateWithOSXRepositoryGroup();
         update.repositoryGroups[0].repositories[0] = 999;
         return MockData.addEmptyTriggerable(TestServer.database()).then(() => {
@@ -183,6 +185,18 @@ describe('/api/update-triggerable/', function () {
         }).then(() => {
             return TestServer.remoteAPI().postJSON('/api/update-triggerable/', update);
         }).then((response) => {
         }).then(() => {
             return TestServer.remoteAPI().postJSON('/api/update-triggerable/', update);
         }).then((response) => {
+            assert.equal(response['status'], 'InvalidRepositoryData');
+        });
+    });
+
+    it('should reject when a repository group contains an invalid repository id', () => {
+        const update = updateWithOSXRepositoryGroup();
+        update.repositoryGroups[0].repositories[0] = {repository: 999};
+        return MockData.addEmptyTriggerable(TestServer.database()).then(() => {
+            return addSlaveForReport(update);
+        }).then(() => {
+            return TestServer.remoteAPI().postJSON('/api/update-triggerable/', update);
+        }).then((response) => {
             assert.equal(response['status'], 'InvalidRepository');
         });
     });
             assert.equal(response['status'], 'InvalidRepository');
         });
     });
@@ -196,7 +210,7 @@ describe('/api/update-triggerable/', function () {
         }).then(() => {
             return TestServer.remoteAPI().postJSON('/api/update-triggerable/', update);
         }).then((response) => {
         }).then(() => {
             return TestServer.remoteAPI().postJSON('/api/update-triggerable/', update);
         }).then((response) => {
-            assert.equal(response['status'], 'InvalidRepository');
+            assert.equal(response['status'], 'DuplicateRepository');
         });
     });
 
         });
     });
 
@@ -297,8 +311,9 @@ describe('/api/update-triggerable/', function () {
                 {test: MockData.someTestId(), platform: MockData.somePlatformId()}
             ],
             'repositoryGroups': [
                 {test: MockData.someTestId(), platform: MockData.somePlatformId()}
             ],
             'repositoryGroups': [
-                {name: 'system-only', repositories: [MockData.macosRepositoryId()]},
-                {name: 'system-and-webkit', repositories: [MockData.webkitRepositoryId(), MockData.macosRepositoryId()]},
+                {name: 'system-only', repositories: [{repository: MockData.macosRepositoryId()}]},
+                {name: 'system-and-webkit', repositories:
+                    [{repository: MockData.webkitRepositoryId()}, {repository: MockData.macosRepositoryId()}]},
             ]
         };
     }
             ]
         };
     }
@@ -315,12 +330,69 @@ describe('/api/update-triggerable/', function () {
         return map;
     }
 
         return map;
     }
 
+    function refetchManifest()
+    {
+        MockData.resetV3Models();
+        return TestServer.remoteAPI().getJSON('/api/manifest').then((content) => Manifest._didFetchManifest(content));
+    }
+
+    it('should update the acceptable of custom roots and patches', () => {
+        const db = TestServer.database();
+        const initialUpdate = updateWithMacWebKitRepositoryGroups();
+        const secondUpdate = updateWithMacWebKitRepositoryGroups();
+        secondUpdate.repositoryGroups[0].acceptsRoots = true;
+        secondUpdate.repositoryGroups[1].repositories[0].acceptsPatch = true;
+        return MockData.addEmptyTriggerable(db).then(() => {
+            return addSlaveForReport(initialUpdate);
+        }).then(() => {
+            return TestServer.remoteAPI().postJSONWithStatus('/api/update-triggerable/', initialUpdate);
+        }).then(() => refetchManifest()).then(() => {
+            const repositoryGroups = TriggerableRepositoryGroup.sortByName(TriggerableRepositoryGroup.all());
+            const webkit = Repository.findTopLevelByName('WebKit');
+            const macos = Repository.findTopLevelByName('macOS');
+            assert.equal(repositoryGroups.length, 2);
+            assert.equal(repositoryGroups[0].name(), 'system-and-webkit');
+            assert.equal(repositoryGroups[0].description(), 'system-and-webkit');
+            assert.equal(repositoryGroups[0].acceptsCustomRoots(), false);
+            assert.deepEqual(repositoryGroups[0].repositories(), [webkit, macos]);
+            assert.equal(repositoryGroups[0].acceptsPatchForRepository(webkit), false);
+            assert.equal(repositoryGroups[0].acceptsPatchForRepository(macos), false);
+
+            assert.equal(repositoryGroups[1].name(), 'system-only');
+            assert.equal(repositoryGroups[1].description(), 'system-only');
+            assert.equal(repositoryGroups[1].acceptsCustomRoots(), false);
+            assert.deepEqual(repositoryGroups[1].repositories(), [macos]);
+            assert.equal(repositoryGroups[1].acceptsPatchForRepository(webkit), false);
+            assert.equal(repositoryGroups[1].acceptsPatchForRepository(macos), false);
+            return TestServer.remoteAPI().postJSONWithStatus('/api/update-triggerable/', secondUpdate);
+        }).then(() => refetchManifest()).then(() => {
+            const repositoryGroups = TriggerableRepositoryGroup.sortByName(TriggerableRepositoryGroup.all());
+            const webkit = Repository.findTopLevelByName('WebKit');
+            const macos = Repository.findTopLevelByName('macOS');
+            assert.equal(repositoryGroups.length, 2);
+            assert.equal(repositoryGroups[0].name(), 'system-and-webkit');
+            assert.equal(repositoryGroups[0].description(), 'system-and-webkit');
+            assert.equal(repositoryGroups[0].acceptsCustomRoots(), false);
+            assert.deepEqual(repositoryGroups[0].repositories(), [webkit, macos]);
+            assert.equal(repositoryGroups[0].acceptsPatchForRepository(webkit), true);
+            assert.equal(repositoryGroups[0].acceptsPatchForRepository(macos), false);
+
+            assert.equal(repositoryGroups[1].name(), 'system-only');
+            assert.equal(repositoryGroups[1].description(), 'system-only');
+            assert.equal(repositoryGroups[1].acceptsCustomRoots(), true);
+            assert.deepEqual(repositoryGroups[1].repositories(), [macos]);
+            assert.equal(repositoryGroups[1].acceptsPatchForRepository(webkit), false);
+            assert.equal(repositoryGroups[1].acceptsPatchForRepository(macos), false);
+            return TestServer.remoteAPI().postJSONWithStatus('/api/update-triggerable/', initialUpdate);
+        });
+    });
+
     it('should replace a repository when the repository group name matches', () => {
         const db = TestServer.database();
         const initialUpdate = updateWithMacWebKitRepositoryGroups();
         const secondUpdate = updateWithMacWebKitRepositoryGroups();
         let initialGroups;
     it('should replace a repository when the repository group name matches', () => {
         const db = TestServer.database();
         const initialUpdate = updateWithMacWebKitRepositoryGroups();
         const secondUpdate = updateWithMacWebKitRepositoryGroups();
         let initialGroups;
-        secondUpdate.repositoryGroups[1].repositories[0] = MockData.gitWebkitRepositoryId();
+        secondUpdate.repositoryGroups[1].repositories[0] = {repository: MockData.gitWebkitRepositoryId()}
         return MockData.addEmptyTriggerable(db).then(() => {
             return addSlaveForReport(initialUpdate);
         }).then(() => {
         return MockData.addEmptyTriggerable(db).then(() => {
             return addSlaveForReport(initialUpdate);
         }).then(() => {
index 667175d..c82a83a 100644 (file)
@@ -103,8 +103,13 @@ function addTriggerableAndCreateTask(name)
             {test: MockData.someTestId(), platform: MockData.otherPlatformId()},
         ],
         'repositoryGroups': [
             {test: MockData.someTestId(), platform: MockData.otherPlatformId()},
         ],
         'repositoryGroups': [
-            {name: 'webkit-only', repositories: [MockData.webkitRepositoryId()]},
-            {name: 'system-and-webkit', repositories: [MockData.macosRepositoryId(), MockData.webkitRepositoryId()]},
+            {name: 'webkit-only', repositories: [
+                {repository: MockData.webkitRepositoryId(), acceptsPatch: true}
+            ]},
+            {name: 'system-and-webkit', repositories: [
+                {repository: MockData.macosRepositoryId(), acceptsPatch: false},
+                {repository: MockData.webkitRepositoryId(), acceptsPatch: true}
+            ]},
         ]
     };
     return MockData.addMockData(TestServer.database()).then(() => {
         ]
     };
     return MockData.addMockData(TestServer.database()).then(() => {
@@ -215,7 +220,7 @@ describe('/privileged-api/create-test-group', function () {
     it('should return "InvalidRevisionSets" when a revision set is empty', () => {
         return addTriggerableAndCreateTask('some task').then((taskId) => {
             const webkit = Repository.all().find((repository) => repository.name() == 'WebKit');
     it('should return "InvalidRevisionSets" when a revision set is empty', () => {
         return addTriggerableAndCreateTask('some task').then((taskId) => {
             const webkit = Repository.all().find((repository) => repository.name() == 'WebKit');
-            return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, revisionSets: [{[webkit.id()]: '191622'}, {}]}).then((content) => {
+            return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, revisionSets: [{[webkit.id()]: {revision: '191622'}}, {}]}).then((content) => {
                 assert(false, 'should never be reached');
             }, (error) => {
                 assert.equal(error, 'InvalidRevisionSets');
                 assert(false, 'should never be reached');
             }, (error) => {
                 assert.equal(error, 'InvalidRevisionSets');
@@ -226,7 +231,7 @@ describe('/privileged-api/create-test-group', function () {
     it('should return "InvalidRevisionSets" when the number of revision sets is less than two', () => {
         return addTriggerableAndCreateTask('some task').then((taskId) => {
             const webkit = Repository.all().find((repository) => repository.name() == 'WebKit');
     it('should return "InvalidRevisionSets" when the number of revision sets is less than two', () => {
         return addTriggerableAndCreateTask('some task').then((taskId) => {
             const webkit = Repository.all().find((repository) => repository.name() == 'WebKit');
-            return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, revisionSets: [{[webkit.id()]: '191622'}]}).then((content) => {
+            return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, revisionSets: [{[webkit.id()]: {revision: '191622'}}]}).then((content) => {
                 assert(false, 'should never be reached');
             }, (error) => {
                 assert.equal(error, 'InvalidRevisionSets');
                 assert(false, 'should never be reached');
             }, (error) => {
                 assert.equal(error, 'InvalidRevisionSets');
@@ -257,7 +262,8 @@ describe('/privileged-api/create-test-group', function () {
     it('should return "RevisionNotFound" when revision sets contains an invalid revision', () => {
         return addTriggerableAndCreateTask('some task').then((taskId) => {
             const webkit = Repository.all().find((repository) => repository.name() == 'WebKit');
     it('should return "RevisionNotFound" when revision sets contains an invalid revision', () => {
         return addTriggerableAndCreateTask('some task').then((taskId) => {
             const webkit = Repository.all().find((repository) => repository.name() == 'WebKit');
-            return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, revisionSets: [{[webkit.id()]: '191622'}, {[webkit.id()]: '1'}]}).then((content) => {
+            const revisionSets = [{[webkit.id()]: {revision: '191622'}}, {[webkit.id()]: {revision: '1'}}];
+            return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, revisionSets}).then((content) => {
                 assert(false, 'should never be reached');
             }, (error) => {
                 assert.equal(error, 'RevisionNotFound');
                 assert(false, 'should never be reached');
             }, (error) => {
                 assert.equal(error, 'RevisionNotFound');
@@ -268,7 +274,7 @@ describe('/privileged-api/create-test-group', function () {
     it('should return "InvalidUploadedFile" when revision sets contains an invalid file ID', () => {
         return addTriggerableAndCreateTask('some task').then((taskId) => {
             const webkit = Repository.all().find((repository) => repository.name() == 'WebKit');
     it('should return "InvalidUploadedFile" when revision sets contains an invalid file ID', () => {
         return addTriggerableAndCreateTask('some task').then((taskId) => {
             const webkit = Repository.all().find((repository) => repository.name() == 'WebKit');
-            const revisionSets = [{[webkit.id()]: '191622', 'customRoots': ['1']}, {[webkit.id()]: '1'}];
+            const revisionSets = [{[webkit.id()]: {revision: '191622'}, 'customRoots': ['1']}, {[webkit.id()]: {revision: '1'}}];
             return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, revisionSets}).then((content) => {
                 assert(false, 'should never be reached');
             }, (error) => {
             return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, revisionSets}).then((content) => {
                 assert(false, 'should never be reached');
             }, (error) => {
@@ -279,7 +285,8 @@ describe('/privileged-api/create-test-group', function () {
 
     it('should return "InvalidRepository" when a revision set uses a repository name instead of a repository id', () => {
         return addTriggerableAndCreateTask('some task').then((taskId) => {
 
     it('should return "InvalidRepository" when a revision set uses a repository name instead of a repository id', () => {
         return addTriggerableAndCreateTask('some task').then((taskId) => {
-            return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, revisionSets: [{'WebKit': '191622'}, {}]}).then((content) => {
+            const revisionSets = [{'WebKit': {revision: '191622'}}, {}];
+            return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, revisionSets}).then((content) => {
                 assert(false, 'should never be reached');
             }, (error) => {
                 assert.equal(error, 'InvalidRepository');
                 assert(false, 'should never be reached');
             }, (error) => {
                 assert.equal(error, 'InvalidRepository');
@@ -297,6 +304,20 @@ describe('/privileged-api/create-test-group', function () {
         });
     });
 
         });
     });
 
+    it('should return "DuplicateTestGroupName" when there is already a test group of the same name', () => {
+        return addTriggerableAndCreateTask('some task').then((taskId) => {
+            const commitSets = {'WebKit': ['191622', '191623']};
+            return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, commitSets}).then((content) => {
+                assert(content['testGroupId']);
+                return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, commitSets});
+            }).then(() => {
+                assert(false, 'should never be reached');
+            }, (error) => {
+                assert.equal(error, 'DuplicateTestGroupName');
+            });
+        });
+    });
+
     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;
     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;
@@ -335,7 +356,8 @@ describe('/privileged-api/create-test-group', function () {
         let webkit;
         return addTriggerableAndCreateTask('some task').then((taskId) => {
             const webkit = Repository.findById(MockData.webkitRepositoryId());
         let webkit;
         return addTriggerableAndCreateTask('some task').then((taskId) => {
             const webkit = Repository.findById(MockData.webkitRepositoryId());
-            const params = {name: 'test', task: taskId, revisionSets: [{[webkit.id()]: '191622'}, {[webkit.id()]: '191623'}]};
+            const revisionSets = [{[webkit.id()]: {revision: '191622'}}, {[webkit.id()]: {revision: '191623'}}];
+            const params = {name: 'test', task: taskId, revisionSets};
             let insertedGroupId;
             return PrivilegedAPI.sendRequest('create-test-group', params).then((content) => {
                 insertedGroupId = content['testGroupId'];
             let insertedGroupId;
             return PrivilegedAPI.sendRequest('create-test-group', params).then((content) => {
                 insertedGroupId = content['testGroupId'];
@@ -410,8 +432,9 @@ describe('/privileged-api/create-test-group', function () {
         return addTriggerableAndCreateTask('some task').then((taskId) => {
             webkit = Repository.findById(MockData.webkitRepositoryId());
             macos = Repository.findById(MockData.macosRepositoryId());
         return addTriggerableAndCreateTask('some task').then((taskId) => {
             webkit = Repository.findById(MockData.webkitRepositoryId());
             macos = Repository.findById(MockData.macosRepositoryId());
-            const params = {name: 'test', task: taskId, repetitionCount: 2,
-                revisionSets: [{[macos.id()]: '15A284', [webkit.id()]: '191622'}, {[webkit.id()]: '191623'}]};
+            const revisionSets = [{[macos.id()]: {revision: '15A284'}, [webkit.id()]: {revision: '191622'}},
+                {[webkit.id()]: {revision: '191623'}}];
+            const params = {name: 'test', task: taskId, repetitionCount: 2, revisionSets};
             let insertedGroupId;
             return PrivilegedAPI.sendRequest('create-test-group', params).then((content) => {
                 insertedGroupId = content['testGroupId'];
             let insertedGroupId;
             return PrivilegedAPI.sendRequest('create-test-group', params).then((content) => {
                 insertedGroupId = content['testGroupId'];
@@ -458,8 +481,8 @@ describe('/privileged-api/create-test-group', function () {
                 return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true});
             }).then((response) => {
                 uploadedFile = response['uploadedFile'];
                 return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true});
             }).then((response) => {
                 uploadedFile = response['uploadedFile'];
-                const revisionSets = [{[webkit.id()]: '191622', [macos.id()]: '15A284'},
-                    {[webkit.id()]: '191622', [macos.id()]: '15A284', 'customRoots': [uploadedFile['id']]}];
+                const revisionSets = [{[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}},
+                    {[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}, 'customRoots': [uploadedFile['id']]}];
                 return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, revisionSets}).then((content) => {
                     insertedGroupId = content['testGroupId'];
                     return TestGroup.fetchByTask(taskId);
                 return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, revisionSets}).then((content) => {
                     insertedGroupId = content['testGroupId'];
                     return TestGroup.fetchByTask(taskId);
@@ -491,12 +514,106 @@ describe('/privileged-api/create-test-group', function () {
         });
     });
 
         });
     });
 
+    it('should create a test group with a patch', () => {
+        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];
+            macos = Repository.all().filter((repository) => repository.name() == 'macOS')[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', patch: uploadedFile.id()}, [macos.id()]: {revision: '15A284'}},
+                {[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}}];
+            return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, revisionSets});
+        }).then((content) => {
+            insertedGroupId = content['testGroupId'];
+            return TestGroup.fetchByTask(taskId);
+        }).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()), [webkit, macos]);
+            assert.deepEqual(set1.customRoots(), []);
+            assert.equal(set0.revisionForRepository(webkit), '191622');
+            assert.equal(set0.revisionForRepository(webkit), set1.revisionForRepository(webkit));
+            assert.equal(set0.commitForRepository(webkit), set1.commitForRepository(webkit));
+            assert.equal(set0.patchForRepository(webkit), uploadedFile);
+            assert.equal(set1.patchForRepository(webkit), null);
+            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(!set0.equals(set1));
+        });
+    });
+
+    it('should return "PatchNotAccepted" when a patch is specified for a repository that does not accept a patch', () => {
+        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];
+            macos = Repository.all().filter((repository) => repository.name() == 'macOS')[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', patch: uploadedFile.id()}},
+                {[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}}];
+            return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, revisionSets});
+        }).then(() => {
+            assert(false, 'should never be reached');
+        }, (error) => {
+            assert.equal(error, 'PatchNotAccepted');
+        });
+    });
+
     it('should create a test group with an analysis task', () => {
         let insertedGroupId;
         let webkit;
         return addTriggerableAndCreateTask('some task').then(() => {
             webkit = Repository.all().filter((repository) => repository.name() == 'WebKit')[0];
     it('should create a test group with an analysis task', () => {
         let insertedGroupId;
         let webkit;
         return addTriggerableAndCreateTask('some task').then(() => {
             webkit = Repository.all().filter((repository) => repository.name() == 'WebKit')[0];
-            const revisionSets = [{[webkit.id()]: '191622'}, {[webkit.id()]: '191623'}];
+            const revisionSets = [{[webkit.id()]: {revision: '191622'}}, {[webkit.id()]: {revision: '191623'}}];
             return PrivilegedAPI.sendRequest('create-test-group',
                 {name: 'test', taskName: 'other task', platform: MockData.somePlatformId(), test: MockData.someTestId(), revisionSets});
         }).then((result) => {
             return PrivilegedAPI.sendRequest('create-test-group',
                 {name: 'test', taskName: 'other task', platform: MockData.somePlatformId(), test: MockData.someTestId(), revisionSets});
         }).then((result) => {
@@ -532,12 +649,12 @@ describe('/privileged-api/create-test-group', function () {
         let test = MockData.someTestId();
         return addTriggerableAndCreateTask('some task').then(() => {
             webkit = Repository.all().filter((repository) => repository.name() == 'WebKit')[0];
         let test = MockData.someTestId();
         return addTriggerableAndCreateTask('some task').then(() => {
             webkit = Repository.all().filter((repository) => repository.name() == 'WebKit')[0];
-            const revisionSets = [{[webkit.id()]: '191622'}, {[webkit.id()]: '191623'}];
+            const revisionSets = [{[webkit.id()]: {revision: '191622'}}, {[webkit.id()]: {revision: '191623'}}];
             return PrivilegedAPI.sendRequest('create-test-group',
                 {name: 'test1', taskName: 'other task', platform: MockData.somePlatformId(), test, revisionSets});
         }).then((result) => {
             firstResult = result;
             return PrivilegedAPI.sendRequest('create-test-group',
                 {name: 'test1', taskName: 'other task', platform: MockData.somePlatformId(), test, revisionSets});
         }).then((result) => {
             firstResult = result;
-            const revisionSets = [{[webkit.id()]: '191622'}, {[webkit.id()]: '192736'}];
+            const revisionSets = [{[webkit.id()]: {revision: '191622'}}, {[webkit.id()]: {revision: '192736'}}];
             return PrivilegedAPI.sendRequest('create-test-group',
                 {name: 'test2', task: result['taskId'], platform: MockData.otherPlatformId(), test, revisionSets, repetitionCount: 2});
         }).then((result) => {
             return PrivilegedAPI.sendRequest('create-test-group',
                 {name: 'test2', task: result['taskId'], platform: MockData.otherPlatformId(), test, revisionSets, repetitionCount: 2});
         }).then((result) => {
@@ -581,5 +698,4 @@ describe('/privileged-api/create-test-group', function () {
             assert.equal(set1.revisionForRepository(webkit), '192736');
         });
     });
             assert.equal(set1.revisionForRepository(webkit), '192736');
         });
     });
-
 });
 });
index e4b9e59..0d04064 100644 (file)
@@ -73,6 +73,8 @@ MockData = {
             db.insert('repositories', {id: this.gitWebkitRepositoryId(), name: 'Git-WebKit'}),
             db.insert('platforms', {id: MockData.somePlatformId(), name: 'some platform'}),
             db.insert('tests', {id: MockData.someTestId(), name: 'some test'}),
             db.insert('repositories', {id: this.gitWebkitRepositoryId(), name: 'Git-WebKit'}),
             db.insert('platforms', {id: MockData.somePlatformId(), name: 'some platform'}),
             db.insert('tests', {id: MockData.someTestId(), name: 'some test'}),
+            db.insert('test_metrics', {id: 5300, test: MockData.someTestId(), name: 'some metric'}),
+            db.insert('test_configurations', {id: 5400, metric: 5300, platform: MockData.somePlatformId(), type: 'current'}),
         ]);
     },
     addMockTestGroupWithGitWebKit(db)
         ]);
     },
     addMockTestGroupWithGitWebKit(db)
@@ -121,7 +123,7 @@ MockData = {
             'buildRequestArgument': 'build-request-id',
             'repositoryGroups': {
                 'webkit-svn': {
             'buildRequestArgument': 'build-request-id',
             'repositoryGroups': {
                 'webkit-svn': {
-                    'repositories': ['WebKit', 'macOS'],
+                    'repositories': {'WebKit': {}, 'macOS': {}},
                     'properties': {
                         'os': '<macOS>',
                         'wk': '<WebKit>',
                     'properties': {
                         'os': '<macOS>',
                         'wk': '<WebKit>',
@@ -145,7 +147,7 @@ MockData = {
             'buildRequestArgument': 'build-request-id',
             'repositoryGroups': {
                 'webkit-svn': {
             'buildRequestArgument': 'build-request-id',
             'repositoryGroups': {
                 'webkit-svn': {
-                    'repositories': ['WebKit', 'macOS'],
+                    'repositories': {'WebKit': {}, 'macOS': {}},
                     'properties': {
                         'os': '<macOS>',
                         'wk': '<WebKit>',
                     'properties': {
                         'os': '<macOS>',
                         'wk': '<WebKit>',
index 5d76f8b..a83d652 100644 (file)
@@ -935,10 +935,6 @@ describe('BuildbotTriggerable', function () {
                 assert.equal(macos.name(), 'macOS');
                 webkit = Repository.findById(11);
                 assert.equal(webkit.name(), 'WebKit');
                 assert.equal(macos.name(), 'macOS');
                 webkit = Repository.findById(11);
                 assert.equal(webkit.name(), 'WebKit');
-
-                return db.selectAll('triggerable_configurations', 'test');
-            }).then((configurations) => {
-                assert.equal(configurations.length, 0);
                 assert.equal(Triggerable.all().length, 1);
 
                 const triggerable = Triggerable.all()[0];
                 assert.equal(Triggerable.all().length, 1);
 
                 const triggerable = Triggerable.all()[0];
@@ -955,8 +951,8 @@ describe('BuildbotTriggerable', function () {
 
                 const config = MockData.mockTestSyncConfigWithSingleBuilder();
                 config.repositoryGroups = {
 
                 const config = MockData.mockTestSyncConfigWithSingleBuilder();
                 config.repositoryGroups = {
-                    'system-only': {repositories: ['macOS'], properties: {'os': '<macOS>'}},
-                    'system-and-webkit': {repositories: ['WebKit', 'macOS'], properties: {'os': '<macOS>', 'wk': '<WebKit>'}}
+                    'system-and-roots': {description: 'Custom Roots', repositories: {'macOS': {}}, properties: {'os': '<macOS>'}, acceptsRoots: true},
+                    'system-and-webkit': {repositories: {'WebKit': {acceptsPatch: true}, 'macOS': {}}, properties: {'os': '<macOS>', 'wk': '<WebKit>'}}
                 }
 
                 const logger = new MockLogger;
                 }
 
                 const logger = new MockLogger;
@@ -964,12 +960,6 @@ describe('BuildbotTriggerable', function () {
                 const buildbotTriggerable = new BuildbotTriggerable(config, TestServer.remoteAPI(), MockRemoteAPI, slaveInfo, logger);
                 return buildbotTriggerable.updateTriggerable();
             }).then(() => refetchManifest()).then(() => {
                 const buildbotTriggerable = new BuildbotTriggerable(config, TestServer.remoteAPI(), MockRemoteAPI, slaveInfo, logger);
                 return buildbotTriggerable.updateTriggerable();
             }).then(() => refetchManifest()).then(() => {
-                return db.selectAll('triggerable_configurations', 'test');
-            }).then((configurations) => {
-                assert.equal(configurations.length, 1);
-                assert.equal(configurations[0].test, MockData.someTestId());
-                assert.equal(configurations[0].platform, MockData.somePlatformId());
-
                 assert.equal(Triggerable.all().length, 1);
 
                 let test = Test.findById(MockData.someTestId());
                 assert.equal(Triggerable.all().length, 1);
 
                 let test = Test.findById(MockData.someTestId());
@@ -979,10 +969,13 @@ describe('BuildbotTriggerable', function () {
 
                 const groups = TriggerableRepositoryGroup.sortByName(triggerable.repositoryGroups());
                 assert.equal(groups.length, 2);
 
                 const groups = TriggerableRepositoryGroup.sortByName(triggerable.repositoryGroups());
                 assert.equal(groups.length, 2);
-                assert.equal(groups[0].name(), 'system-and-webkit');
-                assert.deepEqual(groups[0].repositories(), [webkit, macos]);
-                assert.equal(groups[1].name(), 'system-only');
-                assert.deepEqual(groups[1].repositories(), [macos]);
+                assert.equal(groups[0].name(), 'system-and-roots');
+                assert.equal(groups[0].description(), 'Custom Roots');
+                assert.deepEqual(groups[0].repositories(), [macos]);
+                assert.equal(groups[0].acceptsCustomRoots(), true);
+                assert.equal(groups[1].name(), 'system-and-webkit');
+                assert.deepEqual(groups[1].repositories(), [webkit, macos]);
+                assert.equal(groups[1].acceptsCustomRoots(), false);
 
                 const config = MockData.mockTestSyncConfigWithSingleBuilder();
                 config.repositoryGroups = [ ];
 
                 const config = MockData.mockTestSyncConfigWithSingleBuilder();
                 config.repositoryGroups = [ ];
@@ -995,10 +988,10 @@ describe('BuildbotTriggerable', function () {
                 assert.equal(Triggerable.all().length, 1);
                 const groups = TriggerableRepositoryGroup.sortByName(Triggerable.all()[0].repositoryGroups());
                 assert.equal(groups.length, 2);
                 assert.equal(Triggerable.all().length, 1);
                 const groups = TriggerableRepositoryGroup.sortByName(Triggerable.all()[0].repositoryGroups());
                 assert.equal(groups.length, 2);
-                assert.equal(groups[0].name(), 'system-and-webkit');
-                assert.deepEqual(groups[0].repositories(), [webkit, macos]);
-                assert.equal(groups[1].name(), 'system-only');
-                assert.deepEqual(groups[1].repositories(), [macos]);
+                assert.equal(groups[0].name(), 'system-and-roots');
+                assert.deepEqual(groups[0].repositories(), [macos]);
+                assert.equal(groups[1].name(), 'system-and-webkit');
+                assert.deepEqual(groups[1].repositories(), [webkit, macos]);
             })
         });
     });
             })
         });
     });
index bf9ee0f..31062de 100644 (file)
@@ -291,18 +291,28 @@ class BuildbotSyncer {
 
     static _parseRepositoryGroup(name, group)
     {
 
     static _parseRepositoryGroup(name, group)
     {
-        assert(Array.isArray(group.repositories), 'Each repository group must specify a list of repositories');
-        assert(group.repositories.length, 'Each repository group must specify a list of repositories');
-        assert(!('description' in group) || typeof(group['description']) == 'string', 'The description of a repository group must be a string');
-        assert.equal(typeof(group.properties), 'object', 'Each repository group must specify a dictionary of properties');
+        assert.equal(typeof(group.repositories), 'object',
+            `Repository group "${name}" does not specify a dictionary of repositories`);
+        assert(!('description' in group) || typeof(group['description']) == 'string',
+            `Repository group "${name}" have an invalid description`);
+        assert.equal(typeof(group.properties), 'object', `Repository group "${name}" specifies an invalid dictionary of properties`);
+        assert([undefined, true, false].includes(group.acceptsRoots),
+            `Repository group "${name}" contains invalid acceptsRoots value: ${group.acceptsRoots}`);
 
         const repositoryByName = {};
 
         const repositoryByName = {};
-        const repositories = group.repositories.map((repositoryName) => {
+        const parsedRepositoryList = [];
+        for (const repositoryName in group.repositories) {
+            const options = group.repositories[repositoryName];
             const repository = Repository.findTopLevelByName(repositoryName);
             assert(repository, `"${repositoryName}" is not a valid repository name`);
             repositoryByName[repositoryName] = repository;
             const repository = Repository.findTopLevelByName(repositoryName);
             assert(repository, `"${repositoryName}" is not a valid repository name`);
             repositoryByName[repositoryName] = repository;
-            return repository;
-        });
+            assert.equal(typeof(options), 'object', `"${repositoryName}" does not specify a valid option`);
+            assert([undefined, true, false].includes(options.acceptsPatch),
+                `"${repositoryName}" contains invalid acceptsPatch value: ${options.acceptsPatch}`);
+            repositoryByName[repositoryName] = repository;
+            parsedRepositoryList.push({repository: repository.id(), acceptsPatch: options.acceptsPatch});
+        }
+
         const propertiesTemplate = {};
         const usedRepositories = [];
         for (const propertyName in group.properties) {
         const propertiesTemplate = {};
         const usedRepositories = [];
         for (const propertyName in group.properties) {
@@ -316,13 +326,16 @@ class BuildbotSyncer {
             }
             propertiesTemplate[propertyName] = value;
         }
             }
             propertiesTemplate[propertyName] = value;
         }
-        assert.equal(repositories.length, usedRepositories.length, `Repository group "${name}" does not use some of the listed repositories`);
+        assert(parsedRepositoryList.length, `Repository group "${name}" does not specify any repository`);
+        assert.equal(parsedRepositoryList.length, usedRepositories.length,
+            `Repository group "${name}" does not use some of the repositories listed`);
         return {
             name: group.name,
             description: group.description,
         return {
             name: group.name,
             description: group.description,
+            acceptsRoots: group.acceptsRoots,
             propertiesTemplate,
             arguments: group.arguments,
             propertiesTemplate,
             arguments: group.arguments,
-            repositories: repositories.map((repository) => repository.id()),
+            repositoryList: parsedRepositoryList,
         };
     }
 
         };
     }
 
index 429a41e..2415f70 100644 (file)
@@ -45,7 +45,13 @@ class BuildbotTriggerable {
             'triggerable': this._name,
             'configurations': Array.from(map.values()),
             'repositoryGroups': Object.keys(repositoryGroups).map((groupName) => {
             'triggerable': this._name,
             'configurations': Array.from(map.values()),
             'repositoryGroups': Object.keys(repositoryGroups).map((groupName) => {
-                return {name: groupName, repositories: repositoryGroups[groupName].repositories};
+                const group = repositoryGroups[groupName];
+                return {
+                    name: groupName,
+                    description: group.description,
+                    acceptsRoots: group.acceptsRoots,
+                    repositories: group.repositoryList,
+                };
             })});
     }
 
             })});
     }
 
index b0f409d..56cd807 100644 (file)
@@ -23,11 +23,11 @@ function sampleBuildRequestData()
         }],
         "commitSets": [{
             "id": "4255",
         }],
         "commitSets": [{
             "id": "4255",
-            "commits": ["87832", "93116"],
+            "revisionItems": [{"commit": "87832"}, {"commit": "93116"}],
             "customRoots": [],
         }, {
             "id": "4256",
             "customRoots": [],
         }, {
             "id": "4256",
-            "commits": ["87832", "96336"],
+            "revisionItems": [{"commit": "87832"}, {"commit": "96336"}],
             "customRoots": [],
         }],
         "commits": [{
             "customRoots": [],
         }],
         "commits": [{
index c47fbda..7e3b0c7 100644 (file)
@@ -16,7 +16,7 @@ function sampleiOSConfig()
         'buildRequestArgument': 'build_request_id',
         'repositoryGroups': {
             'ios-svn-webkit': {
         'buildRequestArgument': 'build_request_id',
         'repositoryGroups': {
             'ios-svn-webkit': {
-                'repositories': ['WebKit', 'iOS'],
+                'repositories': {'WebKit': {}, 'iOS': {}},
                 'properties': {
                     'desired_image': '<iOS>',
                     'opensource': '<WebKit>',
                 'properties': {
                     'desired_image': '<iOS>',
                     'opensource': '<WebKit>',
@@ -111,7 +111,7 @@ function smallConfiguration()
         'buildRequestArgument': 'id',
         'repositoryGroups': {
             'ios-svn-webkit': {
         'buildRequestArgument': 'id',
         'repositoryGroups': {
             'ios-svn-webkit': {
-                'repositories': ['iOS', 'WebKit'],
+                'repositories': {'iOS': {}, 'WebKit': {}},
                 'properties': {
                     'os': '<iOS>',
                     'wk': '<WebKit>'
                 'properties': {
                     'os': '<iOS>',
                     'wk': '<WebKit>'
@@ -192,11 +192,11 @@ function createSampleBuildRequest(platform, test)
     assert(platform instanceof Platform);
     assert(test instanceof Test);
 
     assert(platform instanceof Platform);
     assert(test instanceof Test);
 
-    let commitSet = CommitSet.ensureSingleton('4197', {customRoots: [], commits: [
-        {'id': '111127', 'time': 1456955807334, 'repository': MockModels.webkit, 'revision': '197463'},
-        {'id': '111237', 'time': 1456931874000, 'repository': MockModels.sharedRepository, 'revision': '80229'},
-        {'id': '88930', 'time': 0, 'repository': MockModels.ios, 'revision': '13A452'},
-    ]});
+    const webkit197463 = CommitLog.ensureSingleton('111127', {'id': '111127', 'time': 1456955807334, 'repository': MockModels.webkit, 'revision': '197463'});
+    const shared111237 = CommitLog.ensureSingleton('111237', {'id': '111237', 'time': 1456931874000, 'repository': MockModels.sharedRepository, 'revision': '80229'});
+    const ios13A452 = CommitLog.ensureSingleton('88930', {'id': '88930', 'time': 0, 'repository': MockModels.ios, 'revision': '13A452'});
+
+    const commitSet = CommitSet.ensureSingleton('4197', {customRoots: [], revisionItems: [{commit: webkit197463}, {commit: shared111237}, {commit: ios13A452}]});
 
     return BuildRequest.ensureSingleton('16733-' + platform.id(), {'triggerable': MockModels.triggerable,
         repositoryGroup: MockModels.svnRepositoryGroup,
 
     return BuildRequest.ensureSingleton('16733-' + platform.id(), {'triggerable': MockModels.triggerable,
         repositoryGroup: MockModels.svnRepositoryGroup,
@@ -536,31 +536,39 @@ describe('BuildbotSyncer', () => {
             });
         });
 
             });
         });
 
-        it('should throw when a repository group does not specify a list of repository', () => {
+        it('should throw when a repository group does not specify a dictionary of repositories', () => {
             assert.throws(() => {
                 const config = smallConfiguration();
             assert.throws(() => {
                 const config = smallConfiguration();
-                config.repositoryGroups = {'some-group': {}};
+                config.repositoryGroups = {'some-group': {'properties': {}}};
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
             assert.throws(() => {
                 const config = smallConfiguration();
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
             assert.throws(() => {
                 const config = smallConfiguration();
-                config.repositoryGroups = {'some-group': {'repositories': 1}};
+                config.repositoryGroups = {'some-group': {'repositories': 1}, 'properties': {}};
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
         });
 
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
         });
 
-        it('should throw when a repository group specifies an empty list of repository', () => {
+        it('should throw when a repository group specifies an empty dictionary', () => {
             assert.throws(() => {
                 const config = smallConfiguration();
             assert.throws(() => {
                 const config = smallConfiguration();
-                config.repositoryGroups = {'some-group': {'repositories': []}};
+                config.repositoryGroups = {'some-group': {'repositories': {}, 'properties': {}}};
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
         });
 
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
         });
 
-        it('should throw when a repository group specifies a valid repository', () => {
+        it('should throw when a repository group specifies an invalid repository name', () => {
             assert.throws(() => {
                 const config = smallConfiguration();
             assert.throws(() => {
                 const config = smallConfiguration();
-                config.repositoryGroups = {'some-group': {'repositories': ['InvalidRepositoryName']}};
+                config.repositoryGroups = {'some-group': {'repositories': {'InvalidRepositoryName': {}}}};
+                BuildbotSyncer._loadConfig(MockRemoteAPI, config);
+            });
+        });
+
+        it('should throw when a repository group specifies a repository with a non-dictionary value', () => {
+            assert.throws(() => {
+                const config = smallConfiguration();
+                config.repositoryGroups = {'some-group': {'repositories': {'WebKit': 1}}};
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
         });
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
         });
@@ -568,12 +576,12 @@ describe('BuildbotSyncer', () => {
         it('should throw when the description of a repository group is not a string', () => {
             assert.throws(() => {
                 const config = smallConfiguration();
         it('should throw when the description of a repository group is not a string', () => {
             assert.throws(() => {
                 const config = smallConfiguration();
-                config.repositoryGroups = {'some-group': {'repositories': ['WebKit'], 'description': 1}};
+                config.repositoryGroups = {'some-group': {'repositories': [{'WebKit': {}}], 'description': 1}};
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
             assert.throws(() => {
                 const config = smallConfiguration();
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
             assert.throws(() => {
                 const config = smallConfiguration();
-                config.repositoryGroups = {'some-group': {'repositories': ['WebKit'], 'description': [1, 2]}};
+                config.repositoryGroups = {'some-group': {'repositories': [{'WebKit': {}}], 'description': [1, 2]}};
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
         });
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
         });
@@ -581,12 +589,12 @@ describe('BuildbotSyncer', () => {
         it('should throw when a repository group does not specify a dictionary of properties', () => {
             assert.throws(() => {
                 const config = smallConfiguration();
         it('should throw when a repository group does not specify a dictionary of properties', () => {
             assert.throws(() => {
                 const config = smallConfiguration();
-                config.repositoryGroups = {'some-group': {'repositories': ['WebKit'], properties: 1}};
+                config.repositoryGroups = {'some-group': {'repositories': {'WebKit': {}}, properties: 1}};
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
             assert.throws(() => {
                 const config = smallConfiguration();
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
             assert.throws(() => {
                 const config = smallConfiguration();
-                config.repositoryGroups = {'some-group': {'repositories': ['WebKit'], properties: 'hello'}};
+                config.repositoryGroups = {'some-group': {'repositories': {'WebKit': {}}, properties: 'hello'}};
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
         });
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
         });
@@ -594,7 +602,7 @@ describe('BuildbotSyncer', () => {
         it('should throw when a repository group refers to a non-existent repository in the properties dictionary', () => {
             assert.throws(() => {
                 const config = smallConfiguration();
         it('should throw when a repository group refers to a non-existent repository in the properties dictionary', () => {
             assert.throws(() => {
                 const config = smallConfiguration();
-                config.repositoryGroups = {'some-group': {'repositories': ['WebKit'], properties: {'wk': '<InvalidRepository>'}}};
+                config.repositoryGroups = {'some-group': {'repositories': {'WebKit': {}}, properties: {'wk': '<InvalidRepository>'}}};
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
         });
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
         });
@@ -602,15 +610,28 @@ describe('BuildbotSyncer', () => {
         it('should throw when a repository group refers to a repository in the properties dictionary which is not listed in the list of repositories', () => {
             assert.throws(() => {
                 const config = smallConfiguration();
         it('should throw when a repository group refers to a repository in the properties dictionary which is not listed in the list of repositories', () => {
             assert.throws(() => {
                 const config = smallConfiguration();
-                config.repositoryGroups = {'some-group': {'repositories': ['WebKit'], properties: {'os': '<iOS>'}}};
+                config.repositoryGroups = {'some-group': {'repositories': {'WebKit': {}}, properties: {'os': '<iOS>'}}};
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
         });
 
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
         });
 
-        it('should throw when a repository group does not use a lited repository', () => {
+        it('should throw when a repository group does not use a listed repository', () => {
+            assert.throws(() => {
+                const config = smallConfiguration();
+                config.repositoryGroups = {'some-group': {'repositories': {'WebKit': {}}, properties: {}}};
+                BuildbotSyncer._loadConfig(MockRemoteAPI, config);
+            });
+        });
+
+        it('should throw when a repository group specifies non-boolean value to acceptsRoots', () => {
+            assert.throws(() => {
+                const config = smallConfiguration();
+                config.repositoryGroups = {'some-group': {'repositories': {'WebKit': {}}, 'properties': {'webkit': '<WebKit>'}, acceptsRoots: 1}};
+                BuildbotSyncer._loadConfig(MockRemoteAPI, config);
+            });
             assert.throws(() => {
                 const config = smallConfiguration();
             assert.throws(() => {
                 const config = smallConfiguration();
-                config.repositoryGroups = {'some-group': {'repositories': ['WebKit'], properties: {}}};
+                config.repositoryGroups = {'some-group': {'repositories': {'WebKit': {}}, 'properties': {'webkit': '<WebKit>'}, acceptsRoots: []}};
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
         });
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
         });
index 1053f78..0f6ca12 100644 (file)
@@ -45,15 +45,15 @@ var MockModels = {
 
             MockModels.osRepositoryGroup = new TriggerableRepositoryGroup(31, {
                 name: 'ios',
 
             MockModels.osRepositoryGroup = new TriggerableRepositoryGroup(31, {
                 name: 'ios',
-                repositories: [MockModels.ios]
+                repositories: [{repository: MockModels.ios}]
             });
             MockModels.svnRepositoryGroup = new TriggerableRepositoryGroup(32, {
                 name: 'ios-svn-webkit',
             });
             MockModels.svnRepositoryGroup = new TriggerableRepositoryGroup(32, {
                 name: 'ios-svn-webkit',
-                repositories: [MockModels.ios, MockModels.webkit, MockModels.sharedRepository]
+                repositories: [{repository: MockModels.ios}, {repository: MockModels.webkit}, {repository: MockModels.sharedRepository}]
             });
             MockModels.gitRepositoryGroup = new TriggerableRepositoryGroup(33, {
                 name: 'ios-git-webkit',
             });
             MockModels.gitRepositoryGroup = new TriggerableRepositoryGroup(33, {
                 name: 'ios-git-webkit',
-                repositories: [MockModels.ios, MockModels.webkitGit, MockModels.sharedRepository]
+                repositories: [{repository: MockModels.ios}, {repository: MockModels.webkitGit}, {repository: MockModels.sharedRepository}]
             });
             MockModels.triggerable = new Triggerable(3, {name: 'build-webkit',
                 repositoryGroups: [MockModels.osRepositoryGroup, MockModels.svnRepositoryGroup, MockModels.gitRepositoryGroup],
             });
             MockModels.triggerable = new Triggerable(3, {name: 'build-webkit',
                 repositoryGroups: [MockModels.osRepositoryGroup, MockModels.svnRepositoryGroup, MockModels.gitRepositoryGroup],
index 4b6fd06..8941b64 100644 (file)
@@ -69,11 +69,11 @@ function sampleTestGroup() {
         }],
         "commitSets": [{
             "id": "4255",
         }],
         "commitSets": [{
             "id": "4255",
-            "commits": ["87832", "93116"],
+            "revisionItems": [{"commit": "87832"}, {"commit": "93116"}],
             "customRoots": [],
         }, {
             "id": "4256",
             "customRoots": [],
         }, {
             "id": "4256",
-            "commits": ["87832", "96336"],
+            "revisionItems": [{"commit": "87832"}, {"commit": "96336"}],
             "customRoots": [],
         }],
         "commits": [{
             "customRoots": [],
         }],
         "commits": [{