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
+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
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 (
request_triggerable integer REFERENCES build_triggerables NOT NULL,
request_repository_group integer REFERENCES triggerable_repository_groups,
request_platform integer REFERENCES platforms NOT NULL,
- request_test integer REFERENCES tests NOT NULL,
+ request_test integer REFERENCES tests,
request_group integer REFERENCES analysis_test_groups NOT NULL,
request_order integer NOT NULL,
request_commit_set integer REFERENCES commit_sets NOT NULL,
request_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);
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']);
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'];
'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);
}),
</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);
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;
$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) {
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));
$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));
}
}
}
-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));
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);
}
}
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;
$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));
+ }
}
}
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'];
'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;
$this->db = $db;
$this->triggerable_id = $triggerable_id;
$this->repositories_by_group = NULL;
+ $this->accepts_patch_by_group = NULL;
}
function find_by_repositories($repositories)
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
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;
}
}
$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');
$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'];
$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']));
$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) {
'test' => $test_id,
'group' => $group_id,
'order' => $order,
- 'commit_set' => $config['commit_set'],));
+ 'commit_set' => $config['commit_set']));
$order++;
}
}
$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);
}
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));
}
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));
}
}
this._showComparison = false;
this._commitSetMap = {};
this._specifiedRevisions = {'Baseline': new Map, 'Comparison': new Map};
+ this._patchUploaders = {'Baseline': new Map, 'Comparison': new Map};
this._fetchedRevisions = {'Baseline': new Map, 'Comparison': new Map};
this._repositoryGroupByConfiguration = {'Baseline': null, 'Comparison': null};
this._updateTriggerableLazily = new LazilyEvaluatedFunction(this._updateTriggerable.bind(this));
this._renderTriggerablePlatformsLazily = new LazilyEvaluatedFunction(this._renderTriggerablePlatforms.bind(this));
this._renderRepositoryPanesLazily = new LazilyEvaluatedFunction(this._renderRepositoryPanes.bind(this));
- this._fileUploaders = {};
+ this._customRootUploaders = {};
}
tests() { return this._selectedTests; }
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);
}
{
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()
if (!repositoryGroup)
return null;
- const fileUploader = this._fileUploaders[configurationName];
+ const fileUploader = this._customRootUploaders[configurationName];
if (!fileUploader || fileUploader.hasFileToUpload())
return null;
revision = this._fetchedRevisions[configurationName].get(repository);
if (!revision)
return null;
- commitSet.setRevisionForRepository(repository, revision);
+ let patch = null;
+ if (repositoryGroup.acceptsPatchForRepository(repository)) {
+ const uploaderMap = this._patchUploaders[configurationName];
+ const uploader = uploaderMap.get(repository);
+ if (uploader) {
+ const files = uploader.uploadedFiles();
+ console.assert(files.length <= 1);
+ if (files.length)
+ patch = files[0];
+ }
+ }
+ commitSet.setRevisionForRepository(repository, revision, patch);
}
for (let uploadedFile of fileUploader.uploadedFiles())
const customRootsTBody = element('tbody', [
element('tr', [
element('th', 'Roots'),
- element('td', this._fileUploaders[configurationName]),
+ element('td', this._customRootUploaders[configurationName]),
]),
]);
!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 || [],
+ ])
]);
})
)];
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;
}
constructor()
{
super('instant-file-uploader');
+ this._fileInput = null;
+ this._allowMultipleFiles = false;
this._uploadedFiles = [];
this._preuploadFiles = [];
this._uploadProgress = new WeakMap;
hasFileToUpload() { return !!this._preuploadFiles.length; }
uploadedFiles() { return this._uploadedFiles; }
+ allowMultipleFiles()
+ {
+ this._allowMultipleFiles = true;
+ this.enqueueToRender();
+ }
+
addUploadedFile(uploadedFile)
{
console.assert(uploadedFile instanceof UploadedFile);
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()
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)
{
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()
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);
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; }
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) {
{
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;
{
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);
if (!repository)
continue;
- this._repositoryToCommitMap[repositoryId] = CommitLog.ensureSingleton(commitId, {repository: repository, revision: revision, time: time});
+ // FIXME: Add a flag to remember the fact this commit log is incomplete.
+ const commit = CommitLog.ensureSingleton(commitId, {repository: repository, revision: revision, time: time});
+ this._repositoryToCommitMap.set(repository, commit);
this._repositories.push(repository);
}
}
this._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)
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)
return Repository.findById(repositoryId);
});
raw.repositoryGroups = raw.repositoryGroups.map((group) => {
- group.repositories = group.repositories.map((repositoryId) => Repository.findById(repositoryId));
+ group.repositories = group.repositories.map((entry) => {
+ return {repository: Repository.findById(entry.repository), acceptsPatch: entry.acceptsPatch};
+ });
return TriggerableRepositoryGroup.ensureSingleton(group.id, group);
});
raw.configurations = raw.configurations.map((configuration) => {
return (this._blameUrl || '').replace(/\$1/g, from).replace(/\$2/g, to);
}
- owner()
+ ownerId()
{
return this._ownerId;
}
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);
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;
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()
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());
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; }
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);
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);
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);
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);
{test: MockData.someTestId(), platform: MockData.somePlatformId()}
],
'repositoryGroups': [
- {name: 'system-only', repositories: [MockData.macosRepositoryId()]},
+ {name: 'system-only', repositories: [
+ {repository: MockData.macosRepositoryId(), acceptsPatch: false},
+ ]},
]
};
}
});
});
- 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(() => {
}).then(() => {
return TestServer.remoteAPI().postJSON('/api/update-triggerable/', update);
}).then((response) => {
- assert.equal(response['status'], 'InvalidRepository');
+ assert.equal(response['status'], 'DuplicateRepository');
});
});
{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()}]},
]
};
}
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(() => {
{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(() => {
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');
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');
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');
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) => {
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');
});
});
+ 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;
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'];
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'];
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);
});
});
+ 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) => {
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) => {
assert.equal(set1.revisionForRepository(webkit), '192736');
});
});
-
});
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)
'buildRequestArgument': 'build-request-id',
'repositoryGroups': {
'webkit-svn': {
- 'repositories': ['WebKit', 'macOS'],
+ 'repositories': {'WebKit': {}, 'macOS': {}},
'properties': {
'os': '<macOS>',
'wk': '<WebKit>',
'buildRequestArgument': 'build-request-id',
'repositoryGroups': {
'webkit-svn': {
- 'repositories': ['WebKit', 'macOS'],
+ 'repositories': {'WebKit': {}, 'macOS': {}},
'properties': {
'os': '<macOS>',
'wk': '<WebKit>',
assert.equal(macos.name(), 'macOS');
webkit = Repository.findById(11);
assert.equal(webkit.name(), 'WebKit');
-
- return db.selectAll('triggerable_configurations', 'test');
- }).then((configurations) => {
- assert.equal(configurations.length, 0);
assert.equal(Triggerable.all().length, 1);
const triggerable = Triggerable.all()[0];
const config = MockData.mockTestSyncConfigWithSingleBuilder();
config.repositoryGroups = {
- 'system-only': {repositories: ['macOS'], properties: {'os': '<macOS>'}},
- 'system-and-webkit': {repositories: ['WebKit', 'macOS'], properties: {'os': '<macOS>', 'wk': '<WebKit>'}}
+ 'system-and-roots': {description: 'Custom Roots', repositories: {'macOS': {}}, properties: {'os': '<macOS>'}, acceptsRoots: true},
+ 'system-and-webkit': {repositories: {'WebKit': {acceptsPatch: true}, 'macOS': {}}, properties: {'os': '<macOS>', 'wk': '<WebKit>'}}
}
const logger = new MockLogger;
const 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());
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 = [ ];
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]);
})
});
});
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) {
}
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,
};
}
'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,
+ };
})});
}
}],
"commitSets": [{
"id": "4255",
- "commits": ["87832", "93116"],
+ "revisionItems": [{"commit": "87832"}, {"commit": "93116"}],
"customRoots": [],
}, {
"id": "4256",
- "commits": ["87832", "96336"],
+ "revisionItems": [{"commit": "87832"}, {"commit": "96336"}],
"customRoots": [],
}],
"commits": [{
'buildRequestArgument': 'build_request_id',
'repositoryGroups': {
'ios-svn-webkit': {
- 'repositories': ['WebKit', 'iOS'],
+ 'repositories': {'WebKit': {}, 'iOS': {}},
'properties': {
'desired_image': '<iOS>',
'opensource': '<WebKit>',
'buildRequestArgument': 'id',
'repositoryGroups': {
'ios-svn-webkit': {
- 'repositories': ['iOS', 'WebKit'],
+ 'repositories': {'iOS': {}, 'WebKit': {}},
'properties': {
'os': '<iOS>',
'wk': '<WebKit>'
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,
});
});
- 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);
});
});
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);
});
});
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);
});
});
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);
});
});
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);
});
});
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],
}],
"commitSets": [{
"id": "4255",
- "commits": ["87832", "93116"],
+ "revisionItems": [{"commit": "87832"}, {"commit": "93116"}],
"customRoots": [],
}, {
"id": "4256",
- "commits": ["87832", "96336"],
+ "revisionItems": [{"commit": "87832"}, {"commit": "96336"}],
"customRoots": [],
}],
"commits": [{