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: http://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 7ae44f6e0ca7e4c9dbcf6678af0431de66f4a2eb..4294f8be7920a35f30f4c3bf48eee8515986b40a 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
index 427344b4a695103f7877aac39465af9f2b782810..c14369209b0d4e7a600abeeb47be0cb8c6fd5237 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,
+    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 (
@@ -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_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,
@@ -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'),
-    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);
index a8a26b10a5eac94285ac2005c01b53ca21c7dd78..234bf3d58768cc5f83c3e7771552f564fa7089a9 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();
-    } 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']);
 
@@ -66,6 +39,26 @@ if ($db) {
             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'];
@@ -90,7 +83,7 @@ if ($db) {
         '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);
             }),
@@ -149,7 +142,21 @@ 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);
@@ -167,32 +174,26 @@ END;
     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">
-        <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;
 }
 
-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'];
+        if (!array_key_exists($id, $selected_repositories))
+            continue;
         $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;
index e823966de4ba04f135283ea4bd7b9a8c558b99c1..f5b22b5bafc249325c49d5a7038c4151bf01d58d 100644 (file)
@@ -26,7 +26,7 @@ function main($post_data)
 
     $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) {
@@ -44,16 +44,19 @@ function main($post_data)
 
     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) {
-            $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',
-                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));
@@ -63,8 +66,11 @@ function main($post_data)
             $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));
             }
@@ -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));
@@ -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));
+
+        $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();
-        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 e0396aa5fe6eff3fce7b7dd3d45c7a715ba9bd62..971cbfd8575a442dcc2d4450b16264e8717466bc 100644 (file)
@@ -94,32 +94,31 @@ class BuildRequestsFetcher {
         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));
 
-        $commit_ids = 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'];
-            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($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;
 
@@ -134,7 +133,15 @@ class BuildRequestsFetcher {
 
         $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 0daa254696bee31c66cf28eb18a30349143cd3f1..80a622e45a7d31760d13db6ecfce1e7f4df9f511 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);
-                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'];
@@ -229,7 +231,8 @@ class ManifestGenerator {
                     '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;
index 7264153d44c1f446cd541c3d5c885f1f366589da..15ddc78d6d0f62e672e0fc4093b008090baf59d2 100644 (file)
@@ -7,6 +7,7 @@ class RepositoryGroupFinder
         $this->db = $db;
         $this->triggerable_id = $triggerable_id;
         $this->repositories_by_group = NULL;
+        $this->accepts_patch_by_group = NULL;
     }
 
     function find_by_repositories($repositories)
@@ -21,6 +22,13 @@ class RepositoryGroupFinder
         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
@@ -30,13 +38,18 @@ class RepositoryGroupFinder
             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'];
+            $repository_id = $row['trigrepo_repository'];
             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->accepts_patch_by_group = &$accepts_patch_by_group;
     }
 }
 
index e0e58430108ffb9db44181189dcf6c0cc837dede..f92d28816c6f68647134d392dfa2e4c7b2eff5e6 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');
-    $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');
@@ -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));
+
+        $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'];
@@ -81,10 +87,12 @@ function main()
         $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;
+            $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']));
@@ -93,6 +101,22 @@ function main()
     $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) {
@@ -103,7 +127,7 @@ function main()
                 'test' => $test_id,
                 'group' => $group_id,
                 'order' => $order,
-                'commit_set' => $config['commit_set'],));
+                'commit_set' => $config['commit_set']));
             $order++;
         }
     }
@@ -126,23 +150,39 @@ function commit_sets_from_revision_sets($db, $triggerable_id, $revision_set_list
 
         $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') {
-                $file_id_list = $revision;
+                $file_id_list = $data;
                 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));
-                    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));
+
+            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));
-            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);
         }
 
@@ -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));
 
+        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));
     }
 
@@ -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()));
-            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 d328769d92e87be65715a196990f6f9b7ffbb692..cab68ad7b04f65333fec3924364c0234885b26cf 100644 (file)
@@ -11,6 +11,7 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
         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));
@@ -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._fileUploaders = {};
+        this._customRootUploaders = {};
     }
 
     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;
-            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._setUploadedFilesIfEmpty(this._fileUploaders['Comparison'], comparisonCommitSet);
+            this._setUploadedFilesIfEmpty(this._customRootUploaders['Comparison'], 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());
 
-        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('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('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()
@@ -268,7 +292,7 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
         if (!repositoryGroup)
             return null;
 
-        const fileUploader = this._fileUploaders[configurationName];
+        const fileUploader = this._customRootUploaders[configurationName];
         if (!fileUploader || fileUploader.hasFileToUpload())
             return null;
 
@@ -279,7 +303,18 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
                 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())
@@ -351,7 +386,7 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
         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) => {
+                    let uploader = currentGroup.acceptsPatchForRepository(repository)
+                        ? this._ensurePatchUploader(configurationName, repository) : null;
+
                     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;
             }
 
-            .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;
-            }
-
-            .revision-table tbody tr:last-child td,
-            .revision-table tbody tr:last-child th {
                 padding-bottom: 0.5rem;
             }
 
index 64688dcd8287a4f40c0472a84191ad157a43d5fd..daef4a597c5b1bebaa48afd33a2a104203f3bc75 100644 (file)
@@ -2,6 +2,8 @@ class InstantFileUploader extends ComponentBase {
     constructor()
     {
         super('instant-file-uploader');
+        this._fileInput = null;
+        this._allowMultipleFiles = false;
         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; }
 
+    allowMultipleFiles()
+    {
+        this._allowMultipleFiles = true;
+        this.enqueueToRender();
+    }
+
     addUploadedFile(uploadedFile)
     {
         console.assert(uploadedFile instanceof UploadedFile);
@@ -25,8 +33,13 @@ class InstantFileUploader extends ComponentBase {
 
     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()
@@ -34,6 +47,8 @@ class InstantFileUploader extends ComponentBase {
         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)
@@ -167,7 +182,7 @@ class InstantFileUploader extends ComponentBase {
     {
         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()
index 31b8c655af66f8e3b82bb95ccf378c221499ba6f..3c7c9cab7fc7be196966f98a33936c3f6bfdc8c6 100644 (file)
@@ -16,7 +16,7 @@ class BuildRequest extends DataModelObject {
         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);
@@ -45,6 +45,8 @@ class BuildRequest extends DataModelObject {
     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; }
 
@@ -121,18 +123,21 @@ class BuildRequest extends DataModelObject {
 
     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);
 
-        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) {
index af79fff04e7c1027d0c18c2bc9aa0c90268d7e38..2cd5832b3bfadad79027197fa284d7d27658fd5d 100644 (file)
@@ -6,42 +6,45 @@ class CommitSet extends DataModelObject {
     {
         super(id);
         this._repositories = [];
-        this._repositoryToCommitMap = {};
+        this._repositoryToCommitMap = new Map;
+        this._repositoryToPatchMap = new Map;
         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; }
-    commitForRepository(repository) { return this._repositoryToCommitMap[repository.id()]; }
+    commitForRepository(repository) { return this._repositoryToCommitMap.get(repository); }
 
     revisionForRepository(repository)
     {
-        var commit = this._repositoryToCommitMap[repository.id()];
+        var commit = this._repositoryToCommitMap.get(repository);
         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;
-            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;
@@ -51,8 +54,10 @@ class CommitSet extends DataModelObject {
     {
         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);
@@ -100,7 +105,9 @@ class MeasurementCommitSet extends CommitSet {
             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);
         }
     }
@@ -127,10 +134,11 @@ class CustomCommitSet {
         this._customRoots = [];
     }
 
-    setRevisionForRepository(repository, revision)
+    setRevisionForRepository(repository, revision, patch = null)
     {
         console.assert(repository instanceof Repository);
-        this._revisionListByRepository.set(repository, revision);
+        console.assert(!patch || patch instanceof UploadedFile);
+        this._revisionListByRepository.set(repository, {revision, patch});
     }
 
     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);
-            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()); }
-    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)
index 973f7219e53e9d7915f772e8f7c914bbfa5065d6..1651de04f73334e6e0f751ce9a2ca8f722809cbb 100644 (file)
@@ -48,7 +48,9 @@ class Manifest {
                 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) => {
index cae9a4ef6295bd3eeec541cbcac3daad03000d19..089f62e2417eeb1e50448d6bc05776db86657add 100644 (file)
@@ -38,7 +38,7 @@ class Repository extends LabeledObject {
         return (this._blameUrl || '').replace(/\$1/g, from).replace(/\$2/g, to);
     }
 
-    owner()
+    ownerId()
     {
         return this._ownerId;
     }
index 32be0a635227df020c45c3acf9868a904fc1a465..c622619ab42fba77ebd837a5a7ab63cf9a2a6bcd 100644 (file)
@@ -10,8 +10,11 @@ class TestGroup extends LabeledObject {
         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._computeRequestedCommitSetsLazily = new LazilyEvaluatedFunction(this._computeRequestedCommitSets.bind(this));
         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);
-        this._requestsAreInOrder = false;
         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()
     {
-        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;
-        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;
@@ -69,43 +80,33 @@ class TestGroup extends LabeledObject {
 
     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)
     {
-        this._orderBuildRequests();
-        return this._buildRequests.filter(function (request) { return request.commitSet() == commitSet; });
+        this._orderedBuildRequests().filter((request) => request.commitSet() == 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()
@@ -229,8 +230,13 @@ class TestGroup extends LabeledObject {
         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());
index c6f81e282c16d09c2e421075b1f45682ea0354c3..3ea8e2145fca0d1b6fbf40a6447ed0b67254e217 100644 (file)
@@ -59,21 +59,33 @@ class TriggerableRepositoryGroup extends LabeledObject {
         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)
     {
-        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[i] != commitSetRepositories[i])
+            const currentRepository = this._repositories[i];
+            if (currentRepository != commitSetRepositories[i])
+                return false;
+            if (commitSet.patchForRepository(currentRepository) && !this._patchAcceptingSet.has(currentRepository))
                 return false;
         }
+        if (commitSet.customRoots().length && !this._acceptsCustomRoots)
+            return false;
         return true;
     }
 
+    acceptsPatchForRepository(repository)
+    {
+        return this._patchAcceptingSet.has(repository);
+    }
+
     description() { return this._description || this.name(); }
     acceptsCustomRoots() { return this._acceptsCustomRoots; }
     repositories() { return this._repositories; }
index 92a1717085d259525d98283ecfffe85876fa78c9..a0cbce3a6e1eec39d9e3a5cfaeb49d22cb785aaa 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.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.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);
@@ -89,9 +89,11 @@ describe('/api/build-requests', function () {
 
             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.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);
index ffe47bcc81f8f07674e138246dfbc4d655359a09..f0687e392a7e33acc4790b1a1f561b79afdd0011 100644 (file)
@@ -341,7 +341,7 @@ describe('/api/manifest', function () {
 
             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);
index af9a9646cd74ec3da7c58df46f25625a90ab6cd3..2b46f3d9c8343238ac56527c28e33acf9a0a35fe 100644 (file)
@@ -67,8 +67,8 @@ describe('/api/update-triggerable/', function () {
         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);
@@ -122,7 +122,9 @@ describe('/api/update-triggerable/', function () {
                 {test: MockData.someTestId(), platform: MockData.somePlatformId()}
             ],
             'repositoryGroups': [
-                {name: 'system-only', repositories: [MockData.macosRepositoryId()]},
+                {name: 'system-only', repositories: [
+                    {repository: MockData.macosRepositoryId(), acceptsPatch: false},
+                ]},
             ]
         };
     }
@@ -175,9 +177,21 @@ 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(() => {
+            return addSlaveForReport(update);
+        }).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(() => {
@@ -196,7 +210,7 @@ describe('/api/update-triggerable/', function () {
         }).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': [
-                {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;
     }
 
+    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;
-        secondUpdate.repositoryGroups[1].repositories[0] = MockData.gitWebkitRepositoryId();
+        secondUpdate.repositoryGroups[1].repositories[0] = {repository: MockData.gitWebkitRepositoryId()}
         return MockData.addEmptyTriggerable(db).then(() => {
             return addSlaveForReport(initialUpdate);
         }).then(() => {
index 667175d350998493c79a7fb54b368e5cca685518..c82a83a7e6b2a1467b80cf5eb4f5580ca5599657 100644 (file)
@@ -103,8 +103,13 @@ function addTriggerableAndCreateTask(name)
             {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(() => {
@@ -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');
-            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');
@@ -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');
-            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');
@@ -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');
-            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');
@@ -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');
-            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) => {
@@ -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) => {
-            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');
@@ -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;
@@ -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());
-            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'];
@@ -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());
-            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'];
@@ -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'];
-                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);
@@ -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];
-            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) => {
@@ -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];
-            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;
-            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) => {
@@ -581,5 +698,4 @@ describe('/privileged-api/create-test-group', function () {
             assert.equal(set1.revisionForRepository(webkit), '192736');
         });
     });
-
 });
index e4b9e59a3dd0ee70b109ffffc208aef5b83eea71..0d04064a89b7da50572896784b347741405915a0 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('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)
@@ -121,7 +123,7 @@ MockData = {
             'buildRequestArgument': 'build-request-id',
             'repositoryGroups': {
                 'webkit-svn': {
-                    'repositories': ['WebKit', 'macOS'],
+                    'repositories': {'WebKit': {}, 'macOS': {}},
                     'properties': {
                         'os': '<macOS>',
                         'wk': '<WebKit>',
@@ -145,7 +147,7 @@ MockData = {
             'buildRequestArgument': 'build-request-id',
             'repositoryGroups': {
                 'webkit-svn': {
-                    'repositories': ['WebKit', 'macOS'],
+                    'repositories': {'WebKit': {}, 'macOS': {}},
                     'properties': {
                         'os': '<macOS>',
                         'wk': '<WebKit>',
index 5d76f8bc7038f2c64c7edf10cfdd8b601900bbc4..a83d652dd4b10c41dc4b330e0dd8a7bbe23b21b4 100644 (file)
@@ -935,10 +935,6 @@ describe('BuildbotTriggerable', function () {
                 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];
@@ -955,8 +951,8 @@ describe('BuildbotTriggerable', function () {
 
                 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;
@@ -964,12 +960,6 @@ describe('BuildbotTriggerable', function () {
                 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());
@@ -979,10 +969,13 @@ describe('BuildbotTriggerable', function () {
 
                 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 = [ ];
@@ -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(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 bf9ee0f4325256b489954c48e02d8b7e665d405d..31062de598ec07e892e84860d90e80cb5e4abd23 100644 (file)
@@ -291,18 +291,28 @@ class BuildbotSyncer {
 
     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 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;
-            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) {
@@ -316,13 +326,16 @@ class BuildbotSyncer {
             }
             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,
+            acceptsRoots: group.acceptsRoots,
             propertiesTemplate,
             arguments: group.arguments,
-            repositories: repositories.map((repository) => repository.id()),
+            repositoryList: parsedRepositoryList,
         };
     }
 
index 429a41e8075a3cdc5f1b3ea91474abab841d1e87..2415f702cc0ae01cfc5bfc851c56f5e71d681ae0 100644 (file)
@@ -45,7 +45,13 @@ class BuildbotTriggerable {
             '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 b0f409d8f403a3f12e2923c776f994ae06a3b52b..56cd807b65f8a5ee90513423760f49d82791007a 100644 (file)
@@ -23,11 +23,11 @@ function sampleBuildRequestData()
         }],
         "commitSets": [{
             "id": "4255",
-            "commits": ["87832", "93116"],
+            "revisionItems": [{"commit": "87832"}, {"commit": "93116"}],
             "customRoots": [],
         }, {
             "id": "4256",
-            "commits": ["87832", "96336"],
+            "revisionItems": [{"commit": "87832"}, {"commit": "96336"}],
             "customRoots": [],
         }],
         "commits": [{
index c47fbdaef6283317f6a4ad2e7ff8b6be762f625c..7e3b0c732e066756176c73cffecc56ddf5968c7f 100644 (file)
@@ -16,7 +16,7 @@ function sampleiOSConfig()
         'buildRequestArgument': 'build_request_id',
         'repositoryGroups': {
             'ios-svn-webkit': {
-                'repositories': ['WebKit', 'iOS'],
+                'repositories': {'WebKit': {}, 'iOS': {}},
                 'properties': {
                     'desired_image': '<iOS>',
                     'opensource': '<WebKit>',
@@ -111,7 +111,7 @@ function smallConfiguration()
         'buildRequestArgument': 'id',
         'repositoryGroups': {
             'ios-svn-webkit': {
-                'repositories': ['iOS', 'WebKit'],
+                'repositories': {'iOS': {}, 'WebKit': {}},
                 'properties': {
                     'os': '<iOS>',
                     'wk': '<WebKit>'
@@ -192,11 +192,11 @@ function createSampleBuildRequest(platform, 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,
@@ -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();
-                config.repositoryGroups = {'some-group': {}};
+                config.repositoryGroups = {'some-group': {'properties': {}}};
                 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);
             });
         });
 
-        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();
-                config.repositoryGroups = {'some-group': {'repositories': []}};
+                config.repositoryGroups = {'some-group': {'repositories': {}, 'properties': {}}};
                 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();
-                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);
             });
         });
@@ -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();
-                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();
-                config.repositoryGroups = {'some-group': {'repositories': ['WebKit'], 'description': [1, 2]}};
+                config.repositoryGroups = {'some-group': {'repositories': [{'WebKit': {}}], 'description': [1, 2]}};
                 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();
-                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();
-                config.repositoryGroups = {'some-group': {'repositories': ['WebKit'], properties: 'hello'}};
+                config.repositoryGroups = {'some-group': {'repositories': {'WebKit': {}}, properties: 'hello'}};
                 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();
-                config.repositoryGroups = {'some-group': {'repositories': ['WebKit'], properties: {'wk': '<InvalidRepository>'}}};
+                config.repositoryGroups = {'some-group': {'repositories': {'WebKit': {}}, properties: {'wk': '<InvalidRepository>'}}};
                 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();
-                config.repositoryGroups = {'some-group': {'repositories': ['WebKit'], properties: {'os': '<iOS>'}}};
+                config.repositoryGroups = {'some-group': {'repositories': {'WebKit': {}}, properties: {'os': '<iOS>'}}};
                 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();
-                config.repositoryGroups = {'some-group': {'repositories': ['WebKit'], properties: {}}};
+                config.repositoryGroups = {'some-group': {'repositories': {'WebKit': {}}, 'properties': {'webkit': '<WebKit>'}, acceptsRoots: []}};
                 BuildbotSyncer._loadConfig(MockRemoteAPI, config);
             });
         });
index 1053f78437a9d4788e1f8d526b2fb338f22579ca..0f6ca12ffb90947ddc14be416a7752b03ba2566b 100644 (file)
@@ -45,15 +45,15 @@ var MockModels = {
 
             MockModels.osRepositoryGroup = new TriggerableRepositoryGroup(31, {
                 name: 'ios',
-                repositories: [MockModels.ios]
+                repositories: [{repository: MockModels.ios}]
             });
             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',
-                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],
index 4b6fd066dc7757639306514e9348f61c81647a1e..8941b64b50d85b49702673982adfc0ded3baea6c 100644 (file)
@@ -69,11 +69,11 @@ function sampleTestGroup() {
         }],
         "commitSets": [{
             "id": "4255",
-            "commits": ["87832", "93116"],
+            "revisionItems": [{"commit": "87832"}, {"commit": "93116"}],
             "customRoots": [],
         }, {
             "id": "4256",
-            "commits": ["87832", "96336"],
+            "revisionItems": [{"commit": "87832"}, {"commit": "96336"}],
             "customRoots": [],
         }],
         "commits": [{