WebKit Bugzilla
Attachment 341254 Details for
Bug 184340
: Perf dashboard should send a notification when a test group finishes
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Requests
|
Help
|
New Account
|
Log In
Remember
[x]
|
Forgot Password
Login:
[x]
[patch]
Patch
bug-184340-20180524234542.patch (text/plain), 124.91 KB, created by
dewei_zhu
on 2018-05-24 23:45:43 PDT
(
hide
)
Description:
Patch
Filename:
MIME Type:
Creator:
dewei_zhu
Created:
2018-05-24 23:45:43 PDT
Size:
124.91 KB
patch
obsolete
>Subversion Revision: 232144 >diff --git a/Websites/perf.webkit.org/ChangeLog b/Websites/perf.webkit.org/ChangeLog >index c9543ff08dcf9e3c85a170f95e2b4ee3e8863c06..423af2d405161ada16bee5ff01e1c61be8dc4776 100644 >--- a/Websites/perf.webkit.org/ChangeLog >+++ b/Websites/perf.webkit.org/ChangeLog >@@ -1,3 +1,110 @@ >+2018-05-24 Dewei Zhu <dewei_zhu@apple.com> >+ >+ Added sending notification feature when test group finishes. >+ https://bugs.webkit.org/show_bug.cgi?id=184340 >+ >+ Reviewed by NOBODY (OOPS!). >+ >+ Added 'testgroup_has_pending_notification' filed to 'analysis_test_group' table to indicate whether a test group >+ has a pending notification.SQL query to update existing database is: >+ 'ALTER TABLE analysis_test_groups ADD COLUMN testgroup_has_pending_notification boolean NOT NULL DEFAULT FALSE;' >+ Updated 'run-analysis' script to be able to send notification when test group finishes. >+ Added 'Notify on completion' checkbox while creating/retrying/bisecting a test group. >+ >+ * browser-tests/test-group-form-tests.js: Updated existing tests and added a new test. >+ * init-database.sql: Added 'testgroup_has_pending_notification' filed to 'analysis_test_group' table. >+ * public/api/test-groups.php: Added 'all-with-pending-notification' API to 'test-group' to show all >+ test groups that need to send notification. Only the ones with 'completed', 'failed' or 'cancelled' status and its >+ 'testgroup_has_pending_notification' is true will be returned by this API. >+ * public/include/build-requests-fetcher.php: Added 'fetch_requests_for_groups' to return test groups with given ids. >+ * public/include/commit-sets-helpers.php: Updated the logic to support setting 'testgroup_has_pending_notification' >+ while create an analysis_test_groups. >+ * public/privileged-api/create-analysis-task.php: Updated the logic to support setting 'testgroup_has_pending_notification'. >+ * public/privileged-api/create-test-group.php: Updated the logic to support setting 'testgroup_has_pending_notification'. >+ * public/privileged-api/update-test-group.php: Updated the logic to support updating 'testgroup_has_pending_notification'. >+ Extended this API to allow authentication both from CSRF token and slave. >+ * public/v3/components/custom-configuration-test-group-form.js: >+ (CustomConfigurationTestGroupForm.prototype.startTesting): Pass 'notifyOnCompletion' information which represents >+ 'testgroup_has_pending_notification' from API perspective. >+ * public/v3/components/customizable-test-group-form.js: >+ (CustomizableTestGroupForm.prototype.startTesting): Pass 'notifyOnCompletion' information which represents >+ 'testgroup_has_pending_notification' from API perspective. >+ (CustomizableTestGroupForm.cssTemplate): Added space between 'Notify on completion' checkbox and test group iteration selection list. >+ * public/v3/components/test-group-form.js: >+ (TestGroupForm): Added '_notifyOnCompletion' instance variable. >+ (TestGroupForm.prototype.didConstructShadowTree): Added 'onchange' event for notify on completion checkbox. >+ (TestGroupForm.prototype.startTesting): Pass 'notifyOnCompletion' information which represents >+ 'testgroup_has_pending_notification' from API perspective. >+ (TestGroupForm.cssTemplate): Added space between 'Notify on completion' checkbox and test group iteration selection list. >+ * public/v3/models/analysis-results.js: Export 'AnalysisResults'. >+ (AnalysisResults.fetch): Update API path to use absolute url. >+ (AnalysisResults): >+ * public/v3/models/analysis-task.js: >+ (AnalysisTask.async.create): Extend this function to take notifyOnCompletion as argument which will be used as >+ 'hasPendingNotification' to send to server. >+ (AnalysisTask): >+ * public/v3/models/test-group.js: >+ (TestGroup): Added '_hasPendingNotification' field. >+ (TestGroup.prototype.updateSingleton): Added logic to update '_hasPendingNotification' field. >+ (TestGroup.prototype.hasPendingNotification): Returns '_hasPendingNotification' field. >+ (TestGroup.prototype.author): Returns author information. >+ (TestGroup.prototype.async.didSendNotification): API that updates 'testgroup_has_pending_notification' to true. >+ (TestGroup.createWithTask): Updated this function to accept 'notifyOnCompletion' which will be used as >+ 'hasPendingNotification' to send to server. >+ (TestGroup.createWithCustomConfiguration): Updated this function to accept 'notifyOnCompletion' which will be used as >+ 'hasPendingNotification' to send to server. >+ (TestGroup.createAndRefetchTestGroups): Updated this function to accept 'notifyOnCompletion' which will be used as >+ 'hasPendingNotification' to send to server. >+ (TestGroup.fetchAllWithPendingNotification): New function that invokes '/api/test-groups.php?all-with-pending-notification'. >+ * public/v3/pages/analysis-task-page.js: Update logic to 'notifyOnCompletion' around >+ (AnalysisTaskChartPane.prototype.didConstructShadowTree): >+ (AnalysisTaskResultsPane.prototype.didConstructShadowTree): >+ (AnalysisTaskTestGroupPane.prototype.didConstructShadowTree): >+ (AnalysisTaskPage.prototype.didConstructShadowTree): >+ (AnalysisTaskPage.prototype._retryCurrentTestGroup): >+ (AnalysisTaskPage.prototype.async._bisectCurrentTestGroup): >+ (AnalysisTaskPage.prototype._createTestGroupAfterVerifyingCommitSetList.set const): >+ (AnalysisTaskPage.prototype._createTestGroupAfterVerifyingCommitSetList): >+ (AnalysisTaskPage.prototype._createCustomTestGroup): >+ * public/v3/pages/chart-pane.js: Added 'Notify on completion' checkbox. >+ (ChartPane.prototype.didConstructShadowTree): >+ (ChartPane.prototype.async._analyzeRange): >+ * public/v3/pages/create-analysis-task-page.js: Adapted API change. >+ (CreateAnalysisTaskPage.prototype._createAnalysisTaskWithGroup): >+ * server-tests/api-test-groups.js: Added tests for '/api/test-groups/all-with-pending-notification'. >+ * server-tests/api-upload-root-tests.js: Updated tests to adapt this change. >+ * server-tests/privileged-api-create-analysis-task-tests.js: Updated tests to adapt this change. >+ Added new tests. >+ * server-tests/privileged-api-create-test-group-tests.js: Updated tests to adapt this change. >+ Added new tests. >+ * server-tests/privileged-api-update-test-group-tests.js: Added unit test for 'update-test-group' API. >+ * server-tests/resources/mock-data.js: addMockData should set 'testgroup_has_pending_notification' to be true. >+ * server-tests/tools-sync-buildbot-integration-tests.js: Updated tests to adapt this change. >+ (async.createTestGroupWihPatch): >+ (createTestGroupWihOwnedCommit): >+ * tools/js/email-notifier.js: Added email notifier to send email notification for completed test groups. >+ (EmailNotifier): >+ (EmailNotifier.prototype.async.sendNotificationsForTestGroups): Builds email body, determines recipients and >+ sends email for each test group. >+ (EmailNotifier.prototype.async._determineRecipientsForTestGroup): Base on configuration, returns a list of recipients. >+ (EmailNotifier._buildMatchingFunctionByEmailGroup): Build email group matching functions. >+ (EmailNotifier._characterReference): Use character reference for '<', '>' and '"'. >+ (EmailNotifier.prototype.async._messageForTestGroup): Build email body for a test group. >+ (EmailNotifier._URLForAnalysisTask): Returns URL for an analysis task. >+ (EmailNotifier.prototype.async._sendEmail): Invoke email API to send notification. >+ (EmailNotifier._instantiateNotificationTemplate): >+ * tools/js/measurement-set-analyzer.js: Adapted 'AnalysisTask.create' change. >+ (MeasurementSetAnalyzer.prototype.async._analyzeMeasurementSet): >+ (MeasurementSetAnalyzer): >+ * tools/js/v3-models.js: >+ * tools/run-analysis.js: Added the logic that sends notification for completed test groups. >+ (main): >+ (async.analysisLoop): >+ * unit-tests/analysis-task-tests.js: >+ * unit-tests/email-notifier-tests.js: Added a unit test for 'Email notifier'. >+ * unit-tests/measurement-set-analyzer-tests.js: Updated unit tests per this change. >+ * unit-tests/test-groups-tests.js: Added unit tests for 'TestGroup.hasPendingNotification'. >+ > 2018-05-23 Dewei Zhu <dewei_zhu@apple.com> > > OSBuildFetcher should respect maxRevision while finding OS builds to report. >diff --git a/Websites/perf.webkit.org/browser-tests/test-group-form-tests.js b/Websites/perf.webkit.org/browser-tests/test-group-form-tests.js >index 26535672849a19ddfdff92b45987953b0de80d72..116404041b49d41f5fa0a0da198fe69b151f9e2e 100644 >--- a/Websites/perf.webkit.org/browser-tests/test-group-form-tests.js >+++ b/Websites/perf.webkit.org/browser-tests/test-group-form-tests.js >@@ -18,7 +18,7 @@ describe('TestGroupFormTests', () => { > testGroupForm.listenToAction('startTesting', (...args) => calls.push(args)); > expect(calls).to.eql({}); > testGroupForm.content('start-button').click(); >- expect(calls).to.eql([[4]]); >+ expect(calls).to.eql([[4, true]]); > }); > }); > >@@ -29,12 +29,31 @@ describe('TestGroupFormTests', () => { > testGroupForm.listenToAction('startTesting', (...args) => calls.push(args)); > expect(calls).to.eql({}); > testGroupForm.content('start-button').click(); >- expect(calls).to.eql([[4]]); >+ expect(calls).to.eql([[4, true]]); > const countForm = testGroupForm.content('repetition-count'); > countForm.value = '6'; >- countForm.dispatchEvent(new Event('change')); >+ countForm.dispatchEvent(new Event('change')); > testGroupForm.content('start-button').click(); >- expect(calls).to.eql([[4], [6]]); >+ expect(calls).to.eql([[4, true], [6, true]]); >+ }); >+ }); >+ >+ it('must update "notify on completion" when it is unchecked', () => { >+ const context = new BrowsingContext(); >+ return createTestGroupFormWithContext(context).then((testGroupForm) => { >+ const calls = []; >+ testGroupForm.listenToAction('startTesting', (...args) => calls.push(args)); >+ expect(calls).to.eql({}); >+ testGroupForm.content('start-button').click(); >+ expect(calls).to.eql([[4, true]]); >+ const countForm = testGroupForm.content('repetition-count'); >+ countForm.value = '6'; >+ countForm.dispatchEvent(new Event('change')); >+ const notifyOnCompletionCheckbox = testGroupForm.content('notify-on-completion-checkbox'); >+ notifyOnCompletionCheckbox.checked = false; >+ notifyOnCompletionCheckbox.dispatchEvent(new Event('change')); >+ testGroupForm.content('start-button').click(); >+ expect(calls).to.eql([[4, true], [6, false]]); > }); > }); > >@@ -57,10 +76,10 @@ describe('TestGroupFormTests', () => { > testGroupForm.listenToAction('startTesting', (...args) => calls.push(args)); > expect(calls).to.eql({}); > testGroupForm.content().querySelector('button').click(); >- expect(calls).to.eql([[4]]); >+ expect(calls).to.eql([[4, true]]); > testGroupForm.setRepetitionCount(8); > testGroupForm.content().querySelector('button').click(); >- expect(calls).to.eql([[4], [8]]); >+ expect(calls).to.eql([[4, true], [8, true]]); > }); > }); > }); >diff --git a/Websites/perf.webkit.org/init-database.sql b/Websites/perf.webkit.org/init-database.sql >index c4742e7b6dc3ea805b039e0d18775c52f2f7d067..ce440149d943331e58d63b4e29ba32ac626f2663 100644 >--- a/Websites/perf.webkit.org/init-database.sql >+++ b/Websites/perf.webkit.org/init-database.sql >@@ -279,6 +279,7 @@ CREATE TABLE analysis_test_groups ( > testgroup_author varchar(256), > testgroup_created_at timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'), > testgroup_hidden boolean NOT NULL DEFAULT FALSE, >+ testgroup_has_pending_notification boolean NOT NULL DEFAULT FALSE, > CONSTRAINT testgroup_name_must_be_unique_for_each_task UNIQUE(testgroup_task, testgroup_name)); > CREATE INDEX testgroup_task_index ON analysis_test_groups(testgroup_task); > >diff --git a/Websites/perf.webkit.org/public/api/test-groups.php b/Websites/perf.webkit.org/public/api/test-groups.php >index 42ddd3093509dd2f4ad630e84e4d3a05e7b0961c..a4fe432e1634f529c1d723c17ebcd516b6b68c9e 100644 >--- a/Websites/perf.webkit.org/public/api/test-groups.php >+++ b/Websites/perf.webkit.org/public/api/test-groups.php >@@ -13,7 +13,25 @@ function main($path) { > > $build_requests_fetcher = new BuildRequestsFetcher($db); > >- if (count($path) > 0 && $path[0]) { >+ if (count($path) > 0 && $path[0] == 'all-with-pending-notification') { >+ $test_groups = $db->query_and_fetch_all('SELECT * FROM analysis_test_groups >+ WHERE EXISTS(SELECT 1 FROM build_requests >+ WHERE request_group = testgroup_id >+ AND request_status IN (\'pending\', \'scheduled\', \'running\')) IS FALSE >+ AND testgroup_has_pending_notification IS TRUE and testgroup_hidden IS FALSE'); >+ $test_group_id_list = array(); >+ foreach($test_groups as $group) >+ array_push($test_group_id_list, $group['testgroup_id']); >+ >+ if (!count($test_group_id_list)) >+ exit_with_success(array('testGroups' => array(), >+ 'buildRequests' => array(), >+ 'commitSets' => array(), >+ 'commits' => array(), >+ 'uploadedFiles' => array())); >+ >+ $build_requests_fetcher->fetch_requests_for_groups($test_group_id_list); >+ } elseif (count($path) > 0 && $path[0]) { > $group_id = intval($path[0]); > $group = $db->select_first_row('analysis_test_groups', 'testgroup', array('id' => $group_id)); > if (!$group) >@@ -65,6 +83,7 @@ function format_test_group($group_row) { > 'author' => $group_row['testgroup_author'], > 'createdAt' => strtotime($group_row['testgroup_created_at']) * 1000, > 'hidden' => Database::is_true($group_row['testgroup_hidden']), >+ 'hasPendingNotification' => Database::is_true($group_row['testgroup_has_pending_notification']), > 'buildRequests' => array(), > 'commitSets' => array(), > ); >diff --git a/Websites/perf.webkit.org/public/include/build-requests-fetcher.php b/Websites/perf.webkit.org/public/include/build-requests-fetcher.php >index 27f02407da7491758dedae083f4c340c1b36da31..802e0ac33aa9da34ea6171b7406698e2cda8f6f8 100644 >--- a/Websites/perf.webkit.org/public/include/build-requests-fetcher.php >+++ b/Websites/perf.webkit.org/public/include/build-requests-fetcher.php >@@ -30,6 +30,13 @@ class BuildRequestsFetcher { > $row['task_id'] = $task_id; > } > >+ function fetch_requests_for_groups($test_group_id_list) { >+ $this->rows = $this->db->query_and_fetch_all('SELECT *, testgroup_task as task_id >+ FROM build_requests, analysis_test_groups >+ WHERE request_group = testgroup_id AND testgroup_id = ANY($1) >+ ORDER BY request_group, request_order', array('{' . implode(', ', $test_group_id_list) . '}')); >+ } >+ > function fetch_incomplete_requests_for_triggerable($triggerable_id) { > $this->rows = $this->db->query_and_fetch_all('SELECT *, test_groups.testgroup_task as task_id FROM build_requests, > (SELECT testgroup_id, testgroup_task, (case when testgroup_author is not null then 0 else 1 end) as author_order, testgroup_created_at >diff --git a/Websites/perf.webkit.org/public/include/commit-sets-helpers.php b/Websites/perf.webkit.org/public/include/commit-sets-helpers.php >index a7adc4f8a32d76f5be457bb737eddf14edfa8b22..3369423ee0691f93adb4c9b1e6aa770bc5c1fba4 100644 >--- a/Websites/perf.webkit.org/public/include/commit-sets-helpers.php >+++ b/Websites/perf.webkit.org/public/include/commit-sets-helpers.php >@@ -4,12 +4,12 @@ require_once('repository-group-finder.php'); > require_once('commit-log-fetcher.php'); > > # FIXME: Should create a helper class for below 3 helper functions to avoid passing long argument list. >-function create_test_group_and_build_requests($db, $commit_sets, $task_id, $name, $author, $triggerable_id, $platform_id, $test_id, $repetition_count) { >+function create_test_group_and_build_requests($db, $commit_sets, $task_id, $name, $author, $triggerable_id, $platform_id, $test_id, $repetition_count, $has_pending_notification) { > > list ($build_configuration_list, $test_configuration_list) = insert_commit_sets_and_construct_configuration_list($db, $commit_sets); > > $group_id = $db->insert_row('analysis_test_groups', 'testgroup', >- array('task' => $task_id, 'name' => $name, 'author' => $author)); >+ array('task' => $task_id, 'name' => $name, 'author' => $author, 'has_pending_notification' => $has_pending_notification)); > > $build_count = count($build_configuration_list); > $order = -$build_count; >diff --git a/Websites/perf.webkit.org/public/privileged-api/create-analysis-task.php b/Websites/perf.webkit.org/public/privileged-api/create-analysis-task.php >index 940f80b97a05cbafa0457d39d13730d266f8fcf9..5c9c08872ce2deb143d2cddc8b3313cbdd461525 100644 >--- a/Websites/perf.webkit.org/public/privileged-api/create-analysis-task.php >+++ b/Websites/perf.webkit.org/public/privileged-api/create-analysis-task.php >@@ -10,6 +10,7 @@ function main() { > $author = remote_user_name($data); > $name = array_get($data, 'name'); > $repetition_count = array_get($data, 'repetitionCount'); >+ $has_pending_notification = array_get($data, 'hasPendingNotification', False); > $test_group_name = array_get($data, 'testGroupName'); > $revision_set_list = array_get($data, 'revisionSets'); > >@@ -84,7 +85,7 @@ function main() { > $triggerable_id = $triggerable['id']; > $test_id = $triggerable['test']; > $commit_sets = commit_sets_from_revision_sets($db, $triggerable_id, $revision_set_list); >- create_test_group_and_build_requests($db, $commit_sets, $task_id, $test_group_name, $author, $triggerable_id, $config['config_platform'], $test_id, $repetition_count); >+ create_test_group_and_build_requests($db, $commit_sets, $task_id, $test_group_name, $author, $triggerable_id, $config['config_platform'], $test_id, $repetition_count, $has_pending_notification); > } > > $db->commit_transaction(); >diff --git a/Websites/perf.webkit.org/public/privileged-api/create-test-group.php b/Websites/perf.webkit.org/public/privileged-api/create-test-group.php >index 56b47558f80d3fed8da29c932aad86d7965d8958..35c7af7c72d4eacb8ac59e3fc7016e435045b716 100644 >--- a/Websites/perf.webkit.org/public/privileged-api/create-test-group.php >+++ b/Websites/perf.webkit.org/public/privileged-api/create-test-group.php >@@ -18,6 +18,7 @@ function main() > $task_id = array_get($arguments, 'task'); > $task_name = array_get($data, 'taskName'); > $repetition_count = $arguments['repetitionCount']; >+ $has_pending_notification = array_get($data, 'hasPendingNotification', False); > $platform_id = array_get($data, 'platform'); > $test_id = array_get($data, 'test'); > $revision_set_list = array_get($data, 'revisionSets'); >@@ -85,7 +86,7 @@ function main() > if ($task_name) > $task_id = $db->insert_row('analysis_tasks', 'task', array('name' => $task_name, 'author' => $author)); > >- $group_id = create_test_group_and_build_requests($db, $commit_sets, $task_id, $name, $author, $triggerable_id, $platform_id, $test_id, $repetition_count); >+ $group_id = create_test_group_and_build_requests($db, $commit_sets, $task_id, $name, $author, $triggerable_id, $platform_id, $test_id, $repetition_count, $has_pending_notification); > > $db->commit_transaction(); > >diff --git a/Websites/perf.webkit.org/public/privileged-api/update-test-group.php b/Websites/perf.webkit.org/public/privileged-api/update-test-group.php >index db1ed38392a57201cc076528e48a4968d20e9ee8..bea8c5a6ae8db152f798ed53a4be248969c88945 100644 >--- a/Websites/perf.webkit.org/public/privileged-api/update-test-group.php >+++ b/Websites/perf.webkit.org/public/privileged-api/update-test-group.php >@@ -3,7 +3,8 @@ > require_once('../include/json-header.php'); > > function main() { >- $data = ensure_privileged_api_data_and_token(); >+ $db = connect(); >+ $data = ensure_privileged_api_data_and_token_or_slave($db); > > $test_group_id = array_get($data, 'group'); > if (!$test_group_id) >@@ -17,10 +18,11 @@ function main() { > if (array_key_exists('hidden', $data)) > $values['hidden'] = Database::to_database_boolean($data['hidden']); > >+ if (array_key_exists('hasPendingNotification', $data)) >+ $values['has_pending_notification'] = Database::to_database_boolean($data['hasPendingNotification']); >+ > if (!$values) > exit_with_error('NothingToUpdate'); >- >- $db = connect(); > $db->begin_transaction(); > > if (!$db->update_row('analysis_test_groups', 'testgroup', array('id' => $test_group_id), $values)) { >diff --git a/Websites/perf.webkit.org/public/v3/components/custom-configuration-test-group-form.js b/Websites/perf.webkit.org/public/v3/components/custom-configuration-test-group-form.js >index 58ebbaadc8ce5627df0626de803099394e237b80..2a2b867f5fb36228f5d8ba6050168870289b34f6 100644 >--- a/Websites/perf.webkit.org/public/v3/components/custom-configuration-test-group-form.js >+++ b/Websites/perf.webkit.org/public/v3/components/custom-configuration-test-group-form.js >@@ -38,7 +38,7 @@ class CustomConfigurationTestGroupForm extends TestGroupForm { > const commitSets = configurator.commitSets(); > const platform = configurator.platform(); > const test = configurator.tests()[0]; // FIXME: Add the support for specifying multiple tests. >- this.dispatchAction('startTesting', this._repetitionCount, testGroupName, commitSets, platform, test, taskName); >+ this.dispatchAction('startTesting', this._repetitionCount, testGroupName, commitSets, platform, test, taskName, this._notifyOnCompletion); > } > > didConstructShadowTree() >diff --git a/Websites/perf.webkit.org/public/v3/components/customizable-test-group-form.js b/Websites/perf.webkit.org/public/v3/components/customizable-test-group-form.js >index 53ef7f7f577383adcb2e6c23ac80615c7f84cb1d..20441c118b3d9ed159aa9d7d7cdfd979d5215951 100644 >--- a/Websites/perf.webkit.org/public/v3/components/customizable-test-group-form.js >+++ b/Websites/perf.webkit.org/public/v3/components/customizable-test-group-form.js >@@ -25,7 +25,7 @@ class CustomizableTestGroupForm extends TestGroupForm { > > startTesting() > { >- this.dispatchAction('startTesting', this._repetitionCount, this._name, this._computeCommitSetMap()); >+ this.dispatchAction('startTesting', this._repetitionCount, this._name, this._computeCommitSetMap(), this._notifyOnCompletion); > } > > didConstructShadowTree() >@@ -286,6 +286,10 @@ class CustomizableTestGroupForm extends TestGroupForm { > color: #333; > } > >+ #customize-link-container { >+ margin-left: 0.4rem; >+ } >+ > #custom-table:not(:empty) { > margin: 1rem 0; > } >@@ -320,6 +324,10 @@ class CustomizableTestGroupForm extends TestGroupForm { > #custom-table th { > text-align: center; > } >+ >+ #notify-on-completion-checkbox { >+ margin-left: 0.4rem; >+ } > `; > } > >diff --git a/Websites/perf.webkit.org/public/v3/components/test-group-form.js b/Websites/perf.webkit.org/public/v3/components/test-group-form.js >index c44b66fa05d0a624227f6bac53901ec74840b4d5..a43edf3508bcf074625db9964f604de18082964f 100644 >--- a/Websites/perf.webkit.org/public/v3/components/test-group-form.js >+++ b/Websites/perf.webkit.org/public/v3/components/test-group-form.js >@@ -5,6 +5,7 @@ class TestGroupForm extends ComponentBase { > { > super(name || 'test-group-form'); > this._repetitionCount = 4; >+ this._notifyOnCompletion = true; > } > > setRepetitionCount(count) >@@ -18,13 +19,15 @@ class TestGroupForm extends ComponentBase { > const repetitionCountSelect = this.content('repetition-count'); > repetitionCountSelect.onchange = () => { > this._repetitionCount = repetitionCountSelect.value; >- } >+ }; >+ const notifyOnCompletionCheckBox = this.content('notify-on-completion-checkbox'); >+ notifyOnCompletionCheckBox.onchange = () => this._notifyOnCompletion = notifyOnCompletionCheckBox.checked; > this.content('form').onsubmit = this.createEventHandler(() => this.startTesting()); > } > > startTesting() > { >- this.dispatchAction('startTesting', this._repetitionCount); >+ this.dispatchAction('startTesting', this._repetitionCount, this._notifyOnCompletion); > } > > static htmlTemplate() >@@ -38,6 +41,11 @@ class TestGroupForm extends ComponentBase { > :host { > display: block; > } >+ >+ #notify-on-completion-checkbox { >+ margin-left: 0.5rem; >+ width: 1rem; >+ } > `; > } > >@@ -58,6 +66,7 @@ class TestGroupForm extends ComponentBase { > <option>10</option> > </select> > iterations per set >+ <input id="notify-on-completion-checkbox" type="checkbox" checked/>Notify on completion > `; > } > >diff --git a/Websites/perf.webkit.org/public/v3/models/analysis-results.js b/Websites/perf.webkit.org/public/v3/models/analysis-results.js >index d048beed6d2a0cffd9116f92da922d6a0af8a952..259553ccbb41e1e1d5e88f3f5ae00a3b0ea94aba 100644 >--- a/Websites/perf.webkit.org/public/v3/models/analysis-results.js >+++ b/Websites/perf.webkit.org/public/v3/models/analysis-results.js >@@ -84,7 +84,7 @@ class AnalysisResults { > static fetch(taskId) > { > taskId = parseInt(taskId); >- return RemoteAPI.getJSONWithStatus(`../api/measurement-set?analysisTask=${taskId}`).then(function (response) { >+ return RemoteAPI.getJSONWithStatus(`/api/measurement-set?analysisTask=${taskId}`).then(function (response) { > > Instrumentation.startMeasuringTime('AnalysisResults', 'fetch'); > >@@ -116,3 +116,8 @@ class AnalysisResultsView { > return this._results.findResult(buildRequest.buildId(), this._metric.id()); > } > } >+ >+ >+if (typeof module !== 'undefined') { >+ module.exports.AnalysisResults = AnalysisResults; >+} >\ No newline at end of file >diff --git a/Websites/perf.webkit.org/public/v3/models/analysis-task.js b/Websites/perf.webkit.org/public/v3/models/analysis-task.js >index 5db45242895e2c4a1e144b9fb34153b4a83319e9..f505c85adedb28402574bbc455b6c63a7a17bbdf 100644 >--- a/Websites/perf.webkit.org/public/v3/models/analysis-task.js >+++ b/Websites/perf.webkit.org/public/v3/models/analysis-task.js >@@ -303,7 +303,7 @@ class AnalysisTask extends LabeledObject { > return results; > } > >- static async create(name, startPoint, endPoint, testGroupName=null, repetitionCount=0) >+ static async create(name, startPoint, endPoint, testGroupName=null, repetitionCount=0, notifyOnCompletion=false) > { > const parameters = {name, startRun: startPoint.id, endRun: endPoint.id}; > if (testGroupName) { >@@ -311,6 +311,7 @@ class AnalysisTask extends LabeledObject { > parameters['revisionSets'] = CommitSet.revisionSetsFromCommitSets([startPoint.commitSet(), endPoint.commitSet()]); > parameters['repetitionCount'] = repetitionCount; > parameters['testGroupName'] = testGroupName; >+ parameters['hasPendingNotification'] = notifyOnCompletion; > } > const response = await PrivilegedAPI.sendRequest('create-analysis-task', parameters); > return AnalysisTask.fetchById(response.taskId, true); >diff --git a/Websites/perf.webkit.org/public/v3/models/test-group.js b/Websites/perf.webkit.org/public/v3/models/test-group.js >index 0317e05a979ea72cec06a0d820fd7e81ab4226c4..3c779a0bbc305b1c05ba526fad295cb63e57ffa4 100644 >--- a/Websites/perf.webkit.org/public/v3/models/test-group.js >+++ b/Websites/perf.webkit.org/public/v3/models/test-group.js >@@ -9,6 +9,7 @@ class TestGroup extends LabeledObject { > this._authorName = object.author; > this._createdAt = new Date(object.createdAt); > this._isHidden = object.hidden; >+ this._hasPendingNotification = object.hasPendingNotification; > this._buildRequests = []; > this._orderBuildRequestsLazily = new LazilyEvaluatedFunction((...buildRequests) => { > return buildRequests.sort((a, b) => a.order() - b.order()); >@@ -30,12 +31,15 @@ class TestGroup extends LabeledObject { > console.assert(this._platform == object.platform); > > this._isHidden = object.hidden; >+ this._hasPendingNotification = object.hasPendingNotification; > } > > task() { return AnalysisTask.findById(this._taskId); } > createdAt() { return this._createdAt; } > isHidden() { return this._isHidden; } > buildRequests() { return this._buildRequests; } >+ hasPendingNotification() { return this._hasPendingNotification; } >+ author() { return this._authorName; } > addBuildRequest(request) > { > this._buildRequests.push(request); >@@ -185,11 +189,22 @@ class TestGroup extends LabeledObject { > }); > } > >- static createWithTask(taskName, platform, test, groupName, repetitionCount, commitSets) >+ async didSendNotification() >+ { >+ const id = this.id(); >+ await PrivilegedAPI.sendRequest('update-test-group', { >+ group: id, >+ hasPendingNotification: false >+ }); >+ const data = await TestGroup.cachedFetch(`/api/test-groups/${id}`, {}, true); >+ return TestGroup._createModelsFromFetchedTestGroups(data); >+ } >+ >+ static createWithTask(taskName, platform, test, groupName, repetitionCount, commitSets, notifyOnCompletion) > { > console.assert(commitSets.length == 2); > const revisionSets = CommitSet.revisionSetsFromCommitSets(commitSets); >- const params = {taskName, name: groupName, platform: platform.id(), test: test.id(), repetitionCount, revisionSets}; >+ const params = {taskName, name: groupName, platform: platform.id(), test: test.id(), repetitionCount, revisionSets, hasPendingNotification: !!notifyOnCompletion}; > return PrivilegedAPI.sendRequest('create-test-group', params).then((data) => { > return AnalysisTask.fetchById(data['taskId'], true); > }).then((task) => { >@@ -197,17 +212,17 @@ class TestGroup extends LabeledObject { > }); > } > >- static createWithCustomConfiguration(task, platform, test, groupName, repetitionCount, commitSets) >+ static createWithCustomConfiguration(task, platform, test, groupName, repetitionCount, commitSets, notifyOnCompletion) > { > console.assert(commitSets.length == 2); > const revisionSets = CommitSet.revisionSetsFromCommitSets(commitSets); >- const params = {task: task.id(), name: groupName, platform: platform.id(), test: test.id(), repetitionCount, revisionSets}; >+ const params = {task: task.id(), name: groupName, platform: platform.id(), test: test.id(), repetitionCount, revisionSets, hasPendingNotification: !!notifyOnCompletion}; > return PrivilegedAPI.sendRequest('create-test-group', params).then((data) => { > return this.fetchForTask(data['taskId'], true); > }); > } > >- static createAndRefetchTestGroups(task, name, repetitionCount, commitSets) >+ static createAndRefetchTestGroups(task, name, repetitionCount, commitSets, notifyOnCompletion) > { > console.assert(commitSets.length == 2); > const revisionSets = CommitSet.revisionSetsFromCommitSets(commitSets); >@@ -216,6 +231,7 @@ class TestGroup extends LabeledObject { > name: name, > repetitionCount: repetitionCount, > revisionSets: revisionSets, >+ hasPendingNotification: !!notifyOnCompletion, > }).then((data) => this.fetchForTask(data['taskId'], true)); > } > >@@ -229,6 +245,11 @@ class TestGroup extends LabeledObject { > return this.cachedFetch('/api/test-groups', {task: taskId}, ignoreCache).then(this._createModelsFromFetchedTestGroups.bind(this)); > } > >+ static fetchAllWithPendingNotification() >+ { >+ return this.cachedFetch('/api/test-groups/all-with-pending-notification', null, true).then(this._createModelsFromFetchedTestGroups.bind(this)); >+ } >+ > static _createModelsFromFetchedTestGroups(data) > { > var testGroups = data['testGroups'].map(function (row) { >diff --git a/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js b/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js >index 5aaf068f54b52da39ebe0a3273cd921213d6c69d..14e3a729ac01aef4f0898b629e5886ff53b7a318 100644 >--- a/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js >+++ b/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js >@@ -24,8 +24,8 @@ class AnalysisTaskChartPane extends ChartPaneBase { > > didConstructShadowTree() > { >- this.part('form').listenToAction('startTesting', (repetitionCount, name, commitSetMap) => { >- this.dispatchAction('newTestGroup', name, repetitionCount, commitSetMap); >+ this.part('form').listenToAction('startTesting', (repetitionCount, name, commitSetMap, notifyOnCompletion) => { >+ this.dispatchAction('newTestGroup', name, repetitionCount, commitSetMap, notifyOnCompletion); > }); > } > >@@ -106,8 +106,8 @@ class AnalysisTaskResultsPane extends ComponentBase { > repositoryPicker.addEventListener('change', () => this.enqueueToRender()); > this.part('commit-viewer').setShowRepositoryName(false); > >- this.part('form').listenToAction('startTesting', (repetitionCount, name, commitSetMap) => { >- this.dispatchAction('newTestGroup', name, repetitionCount, commitSetMap); >+ this.part('form').listenToAction('startTesting', (repetitionCount, name, commitSetMap, notifyOnCompletion) => { >+ this.dispatchAction('newTestGroup', name, repetitionCount, commitSetMap, notifyOnCompletion); > }); > } > >@@ -267,14 +267,14 @@ class AnalysisTaskTestGroupPane extends ComponentBase { > didConstructShadowTree() > { > this.content('hide-button').onclick = () => this.dispatchAction('toggleTestGroupVisibility', this._currentTestGroup); >- this.part('retry-form').listenToAction('startTesting', (repetitionCount) => { >- this.dispatchAction('retryTestGroup', this._currentTestGroup, repetitionCount); >+ this.part('retry-form').listenToAction('startTesting', (repetitionCount, notifyOnCompletion) => { >+ this.dispatchAction('retryTestGroup', this._currentTestGroup, repetitionCount, notifyOnCompletion); > }); >- this.part('bisect-form').listenToAction('startTesting', (repetitionCount) => { >+ this.part('bisect-form').listenToAction('startTesting', (repetitionCount, notifyOnCompletion) => { > const bisectingCommitSet = this._bisectingCommitSetByTestGroup.get(this._currentTestGroup); > const [oneCommitSet, anotherCommitSet] = this._currentTestGroup.requestedCommitSets(); > const commitSets = [oneCommitSet, bisectingCommitSet, anotherCommitSet]; >- this.dispatchAction('bisectTestGroup', this._currentTestGroup, commitSets, repetitionCount); >+ this.dispatchAction('bisectTestGroup', this._currentTestGroup, commitSets, repetitionCount, notifyOnCompletion); > }); > } > >@@ -543,8 +543,8 @@ class AnalysisTaskPage extends PageWithHeading { > groupPane.listenToAction('showHiddenTestGroups', () => this._showAllTestGroups()); > groupPane.listenToAction('renameTestGroup', (testGroup, newName) => this._updateTestGroupName(testGroup, newName)); > groupPane.listenToAction('toggleTestGroupVisibility', (testGroup) => this._hideCurrentTestGroup(testGroup)); >- groupPane.listenToAction('retryTestGroup', (testGroup, repetitionCount) => this._retryCurrentTestGroup(testGroup, repetitionCount)); >- groupPane.listenToAction('bisectTestGroup', (testGroup, commitSets, repetitionCount) => this._bisectCurrentTestGroup(testGroup, commitSets, repetitionCount)); >+ groupPane.listenToAction('retryTestGroup', (testGroup, repetitionCount, notifyOnCompletion) => this._retryCurrentTestGroup(testGroup, repetitionCount, notifyOnCompletion)); >+ groupPane.listenToAction('bisectTestGroup', (testGroup, commitSets, repetitionCount, notifyOnCompletion) => this._bisectCurrentTestGroup(testGroup, commitSets, repetitionCount, notifyOnCompletion)); > > this.part('cause-list').listenToAction('addItem', (repository, revision) => { > this._associateCommit('cause', repository, revision); >@@ -819,19 +819,19 @@ class AnalysisTaskPage extends PageWithHeading { > }); > } > >- _retryCurrentTestGroup(testGroup, repetitionCount) >+ _retryCurrentTestGroup(testGroup, repetitionCount, notifyOnCompletion) > { > const existingNames = (this._testGroups || []).map((group) => group.name()); > const newName = CommitSet.createNameWithoutCollision(testGroup.name(), new Set(existingNames)); > const commitSetList = testGroup.requestedCommitSets(); > const platform = this._task.platform() || testGroup.platform(); >- return TestGroup.createWithCustomConfiguration(this._task, platform, testGroup.test(), newName, repetitionCount, commitSetList) >+ return TestGroup.createWithCustomConfiguration(this._task, platform, testGroup.test(), newName, repetitionCount, commitSetList, notifyOnCompletion) > .then(this._didFetchTestGroups.bind(this), function (error) { > alert('Failed to create a new test group: ' + error); > }); > } > >- async _bisectCurrentTestGroup(testGroup, commitSets, repetitionCount) >+ async _bisectCurrentTestGroup(testGroup, commitSets, repetitionCount, notifyOnCompletion) > { > console.assert(testGroup.task()); > const existingTestGroupNames = new Set((this._testGroups || []).map((testGroup) => testGroup.name())); >@@ -841,7 +841,7 @@ class AnalysisTaskPage extends PageWithHeading { > const currentCommitSet = commitSets[i]; > const testGroupName = CommitSet.createNameWithoutCollision(CommitSet.diff(previousCommitSet, currentCommitSet), existingTestGroupNames); > try { >- const testGroups = await TestGroup.createAndRefetchTestGroups(testGroup.task(), testGroupName, repetitionCount, [previousCommitSet, currentCommitSet]); >+ const testGroups = await TestGroup.createAndRefetchTestGroups(testGroup.task(), testGroupName, repetitionCount, [previousCommitSet, currentCommitSet], notifyOnCompletion); > await this._didFetchTestGroups(testGroups); > } catch(error) { > alert('Failed to create a new test group: ' + error); >@@ -850,7 +850,7 @@ class AnalysisTaskPage extends PageWithHeading { > } > } > >- _createTestGroupAfterVerifyingCommitSetList(testGroupName, repetitionCount, commitSetMap) >+ _createTestGroupAfterVerifyingCommitSetList(testGroupName, repetitionCount, commitSetMap, notifyOnCompletion) > { > if (this._hasDuplicateTestGroupName(testGroupName)) { > alert(`There is already a test group named "${testGroupName}"`); >@@ -876,13 +876,13 @@ class AnalysisTaskPage extends PageWithHeading { > for (let label in commitSetMap) > commitSets.push(commitSetMap[label]); > >- return TestGroup.createAndRefetchTestGroups(this._task, testGroupName, repetitionCount, commitSets) >+ return TestGroup.createAndRefetchTestGroups(this._task, testGroupName, repetitionCount, commitSets, notifyOnCompletion) > .then(this._didFetchTestGroups.bind(this), function (error) { > alert('Failed to create a new test group: ' + error); > }); > } > >- _createCustomTestGroup(repetitionCount, testGroupName, commitSets, platform, test) >+ _createCustomTestGroup(repetitionCount, testGroupName, commitSets, platform, test, notifyOnCompletion) > { > console.assert(this._task.isCustom()); > if (this._hasDuplicateTestGroupName(testGroupName)) { >@@ -890,7 +890,7 @@ class AnalysisTaskPage extends PageWithHeading { > return; > } > >- TestGroup.createWithCustomConfiguration(this._task, platform, test, testGroupName, repetitionCount, commitSets) >+ TestGroup.createWithCustomConfiguration(this._task, platform, test, testGroupName, repetitionCount, commitSets, notifyOnCompletion) > .then(this._didFetchTestGroups.bind(this), function (error) { > alert('Failed to create a new test group: ' + error); > }); >diff --git a/Websites/perf.webkit.org/public/v3/pages/chart-pane.js b/Websites/perf.webkit.org/public/v3/pages/chart-pane.js >index 8cd7aa0047f7adfccd6bff404f874ce137fe3070..cd748deb49cee098b3f496cac88d3523f73fef30 100644 >--- a/Websites/perf.webkit.org/public/v3/pages/chart-pane.js >+++ b/Websites/perf.webkit.org/public/v3/pages/chart-pane.js >@@ -119,7 +119,8 @@ class ChartPane extends ChartPaneBase { > }); > const createWithTestGroupCheckbox = this.content('create-with-test-group'); > const repetitionCount = this.content('confirm-repetition'); >- createWithTestGroupCheckbox.onchange = () => repetitionCount.disabled = !createWithTestGroupCheckbox.checked; >+ const notifyOnCompletion = this.content('notify-on-completion'); >+ createWithTestGroupCheckbox.onchange = () => repetitionCount.disabled = notifyOnCompletion.disabled = !createWithTestGroupCheckbox.checked; > } > > serializeState() >@@ -237,10 +238,11 @@ class ChartPane extends ChartPaneBase { > const name = this.content('task-name').value; > const createWithTestGroup = this.content('create-with-test-group').checked; > const repetitionCount = this.content('confirm-repetition').value; >+ const notifyOnCompletion = this.content('notify-on-completion').checked; > > try { > const analysisTask = await (createWithTestGroup ? >- AnalysisTask.create(name, startPoint, endPoint, 'Confirm', repetitionCount) : AnalysisTask.create(name, startPoint, endPoint)); >+ AnalysisTask.create(name, startPoint, endPoint, 'Confirm', repetitionCount, notifyOnCompletion) : AnalysisTask.create(name, startPoint, endPoint)); > newWindow.location.href = router.url('analysis/task/' + analysisTask.id()); > this.fetchAnalysisTasks(true); > } catch(error) { >@@ -583,6 +585,7 @@ class ChartPane extends ChartPaneBase { > <option>10</option> > </select> > <label>iterations</label> >+ <label><input type="checkbox" id="notify-on-completion" checked> Notify on completion</label> > </li> > </form> > <ul class="chart-pane-filtering-options popover" style="display:none"> >diff --git a/Websites/perf.webkit.org/public/v3/pages/create-analysis-task-page.js b/Websites/perf.webkit.org/public/v3/pages/create-analysis-task-page.js >index a9f0890c19befb630bf07dbbb5e5ce3ee2b365ac..301fd2a290651d5128c3523af49820f203bc66a3 100644 >--- a/Websites/perf.webkit.org/public/v3/pages/create-analysis-task-page.js >+++ b/Websites/perf.webkit.org/public/v3/pages/create-analysis-task-page.js >@@ -22,9 +22,9 @@ class CreateAnalysisTaskPage extends PageWithHeading { > this.part('form').listenToAction('startTesting', this._createAnalysisTaskWithGroup.bind(this)); > } > >- _createAnalysisTaskWithGroup(repetitionCount, testGroupName, commitSets, platform, test, taskName) >+ _createAnalysisTaskWithGroup(repetitionCount, testGroupName, commitSets, platform, test, taskName, notifyOnCompletion) > { >- TestGroup.createWithTask(taskName, platform, test, testGroupName, repetitionCount, commitSets).then((task) => { >+ TestGroup.createWithTask(taskName, platform, test, testGroupName, repetitionCount, commitSets, notifyOnCompletion).then((task) => { > const url = this.router().url(`analysis/task/${task.id()}`); > location.href = this.router().url(`analysis/task/${task.id()}`); > }, (error) => { >diff --git a/Websites/perf.webkit.org/server-tests/api-test-groups.js b/Websites/perf.webkit.org/server-tests/api-test-groups.js >new file mode 100644 >index 0000000000000000000000000000000000000000..eebe127c0b2f4267220b3be82dc3fc35a3852cf6 >--- /dev/null >+++ b/Websites/perf.webkit.org/server-tests/api-test-groups.js >@@ -0,0 +1,76 @@ >+'use strict'; >+ >+const assert = require('assert'); >+const MockData = require('./resources/mock-data.js'); >+const TestServer = require('./resources/test-server.js'); >+const prepareServerTest = require('./resources/common-operations.js').prepareServerTest; >+ >+describe('/api/test-groups', function () { >+ prepareServerTest(this); >+ >+ describe('/api/test-groups/all-with-pending-notification', () => { >+ it('should give an empty list if there is not existing test group at all', async () => { >+ const content = await TestServer.remoteAPI().getJSON('/api/test-groups/all-with-pending-notification'); >+ assert.equal(content.status, 'OK'); >+ assert.deepEqual(content.testGroups, []); >+ assert.deepEqual(content.buildRequests, []); >+ assert.deepEqual(content.commitSets, []); >+ assert.deepEqual(content.commits, []); >+ assert.deepEqual(content.uploadedFiles, []); >+ }); >+ >+ it('should list all test groups with pending notification', async () => { >+ await MockData.addMockData(TestServer.database(), ['completed', 'completed', 'completed', 'completed']); >+ const content = await TestServer.remoteAPI().getJSON('/api/test-groups/all-with-pending-notification'); >+ assert.equal(content.testGroups.length, 1); >+ const testGroup = content.testGroups[0]; >+ assert.equal(testGroup.id, 600); >+ assert.equal(testGroup.task, 500); >+ assert.equal(testGroup.name, 'some test group'); >+ assert.equal(testGroup.author, null); >+ assert.equal(testGroup.hidden, false); >+ assert.equal(testGroup.hasPendingNotification, true); >+ assert.equal(testGroup.platform, 65); >+ assert.deepEqual(testGroup.buildRequests, ['700','701', '702', '703']); >+ assert.deepEqual(testGroup.commitSets, ['401', '402', '401', '402']); >+ }); >+ >+ it('should not list hidden test group', async () => { >+ const database = TestServer.database(); >+ await MockData.addMockData(database, ['completed', 'completed', 'completed', 'completed']); >+ await database.query('UPDATE analysis_test_groups SET testgroup_hidden = TRUE WHERE testgroup_id = 600'); >+ const content = await TestServer.remoteAPI().getJSON('/api/test-groups/all-with-pending-notification'); >+ assert.equal(content.status, 'OK'); >+ assert.deepEqual(content.testGroups, []); >+ assert.deepEqual(content.buildRequests, []); >+ assert.deepEqual(content.commitSets, []); >+ assert.deepEqual(content.commits, []); >+ assert.deepEqual(content.uploadedFiles, []); >+ }); >+ >+ it('should not list test groups that have author notified before', async () => { >+ const database = TestServer.database(); >+ await MockData.addMockData(database, ['completed', 'completed', 'completed', 'completed']); >+ await database.query('UPDATE analysis_test_groups SET testgroup_has_pending_notification = FALSE WHERE testgroup_id = 600'); >+ const content = await TestServer.remoteAPI().getJSON('/api/test-groups/all-with-pending-notification'); >+ assert.equal(content.status, 'OK'); >+ assert.deepEqual(content.testGroups, []); >+ assert.deepEqual(content.buildRequests, []); >+ assert.deepEqual(content.commitSets, []); >+ assert.deepEqual(content.commits, []); >+ assert.deepEqual(content.uploadedFiles, []); >+ }); >+ >+ it('should not list a test group that has some incompleted build requests', async () => { >+ const database = TestServer.database(); >+ await MockData.addMockData(database, ['completed', 'completed', 'completed', 'running']); >+ const content = await TestServer.remoteAPI().getJSON('/api/test-groups/all-with-pending-notification'); >+ assert.equal(content.status, 'OK'); >+ assert.deepEqual(content.testGroups, []); >+ assert.deepEqual(content.buildRequests, []); >+ assert.deepEqual(content.commitSets, []); >+ assert.deepEqual(content.commits, []); >+ assert.deepEqual(content.uploadedFiles, []); >+ }); >+ }); >+}); >\ No newline at end of file >diff --git a/Websites/perf.webkit.org/server-tests/api-upload-root-tests.js b/Websites/perf.webkit.org/server-tests/api-upload-root-tests.js >index ab8340ebcc1a9ef3dc146fee5222712d6a0de86a..6f68b0f916c2ce4634ec75ac6c33bd49903bc3e0 100644 >--- a/Websites/perf.webkit.org/server-tests/api-upload-root-tests.js >+++ b/Websites/perf.webkit.org/server-tests/api-upload-root-tests.js >@@ -65,7 +65,7 @@ function createTestGroupWihPatch() > const set2 = new CustomCommitSet; > set2.setRevisionForRepository(webkit, '191622'); > set2.setRevisionForRepository(shared, '80229'); >- return TestGroup.createWithTask('custom task', Platform.findById(MockData.somePlatformId()), someTest, 'some group', 2, [set1, set2]); >+ return TestGroup.createWithTask('custom task', Platform.findById(MockData.somePlatformId()), someTest, 'some group', 2, [set1, set2], true); > }).then((task) => { > return TestGroup.findAllByTask(task.id())[0]; > }).then((group) => { >@@ -107,7 +107,7 @@ function createTestGroupWithPatchAndOwnedCommits() > set2.setRevisionForRepository(webkit, '192736'); > set2.setRevisionForRepository(ownedSJC, 'owned-jsc-9191', null, '192736'); > >- return TestGroup.createWithTask('custom task', Platform.findById(MockData.somePlatformId()), someTest, 'some group', 2, [set1, set2]); >+ return TestGroup.createWithTask('custom task', Platform.findById(MockData.somePlatformId()), someTest, 'some group', 2, [set1, set2], true); > }).then((task) => { > return TestGroup.findAllByTask(task.id())[0]; > }).then((group) => { >diff --git a/Websites/perf.webkit.org/server-tests/privileged-api-create-analysis-task-tests.js b/Websites/perf.webkit.org/server-tests/privileged-api-create-analysis-task-tests.js >index c7a6d1771f9b42c4a2fd07f672a23da24dc7cd89..5630f794ac43301393e20057900068178e35c39c 100644 >--- a/Websites/perf.webkit.org/server-tests/privileged-api-create-analysis-task-tests.js >+++ b/Websites/perf.webkit.org/server-tests/privileged-api-create-analysis-task-tests.js >@@ -6,6 +6,7 @@ let MockData = require('./resources/mock-data.js'); > let TestServer = require('./resources/test-server.js'); > const addBuilderForReport = require('./resources/common-operations.js').addBuilderForReport; > const prepareServerTest = require('./resources/common-operations.js').prepareServerTest; >+const assertThrows = require('./resources/common-operations.js').assertThrows; > > const reportWithRevision = [{ > "buildNumber": "124", >@@ -353,17 +354,10 @@ describe('/privileged-api/create-analysis-task with browser privileged api', fun > const oneRevisionSet = {[webkitId]: {revision: '191622'}}; > const anotherRevisionSet = {[webkitId]: {revision: '191623'}}; > >- let raiseException = false; >- >- try { >- await PrivilegedAPI.sendRequest('create-analysis-task', {name: 'confirm', repetitionCount: 1, >- revisionSets: [oneRevisionSet, anotherRevisionSet], >- startRun: testRuns[0]['id'], endRun: testRuns[1]['id']}); >- } catch (error) { >- assert.equal(error, 'TriggerableNotFoundForTask'); >- raiseException = true; >- } >- assert.ok(raiseException); >+ await assertThrows('TriggerableNotFoundForTask', () => >+ PrivilegedAPI.sendRequest('create-analysis-task', {name: 'confirm', repetitionCount: 1, >+ revisionSets: [oneRevisionSet, anotherRevisionSet], hasPendingNotification: true, >+ startRun: testRuns[0]['id'], endRun: testRuns[1]['id']})); > }); > > it('should create an analysis task with no test group when repetition count is 0', async () => { >@@ -439,7 +433,7 @@ describe('/privileged-api/create-analysis-task with browser privileged api', fun > > const content = await PrivilegedAPI.sendRequest('create-analysis-task', {name: 'confirm', repetitionCount: 1, > testGroupName: 'Confirm', revisionSets: [oneRevisionSet, anotherRevisionSet], >- startRun: testRuns[0]['id'], endRun: testRuns[1]['id']}); >+ startRun: testRuns[0]['id'], endRun: testRuns[1]['id'], hasPendingNotification: true}); > > const task = await AnalysisTask.fetchById(content['taskId']); > assert.equal(task.name(), 'confirm'); >@@ -456,6 +450,7 @@ describe('/privileged-api/create-analysis-task with browser privileged api', fun > assert.equal(testGroups.length, 1); > const testGroup = testGroups[0]; > assert.equal(testGroup.name(), 'Confirm'); >+ assert.equal(testGroup.hasPendingNotification(), true); > const buildRequests = testGroup.buildRequests(); > assert.equal(buildRequests.length, 2); > >@@ -513,17 +508,10 @@ describe('/privileged-api/create-analysis-task with node privileged api', functi > const oneRevisionSet = {[webkitId]: {revision: '191622'}}; > const anotherRevisionSet = {[webkitId]: {revision: '191623'}}; > >- let raiseException = false; >- >- try { >- await PrivilegedAPI.sendRequest('create-analysis-task', {name: 'confirm', repetitionCount: 1, >+ await assertThrows('SlaveNotFound', () => >+ PrivilegedAPI.sendRequest('create-analysis-task', {name: 'confirm', repetitionCount: 1, > revisionSets: [oneRevisionSet, anotherRevisionSet], >- startRun: testRuns[0]['id'], endRun: testRuns[1]['id']}); >- } catch (error) { >- assert.equal(error, 'SlaveNotFound'); >- raiseException = true; >- } >- assert.ok(raiseException); >+ startRun: testRuns[0]['id'], endRun: testRuns[1]['id']})); > > const allAnalysisTasks = await db.selectRows('analysis_tasks'); > assert.ok(!allAnalysisTasks.length); >@@ -567,7 +555,87 @@ describe('/privileged-api/create-analysis-task with node privileged api', functi > > const content = await PrivilegedAPI.sendRequest('create-analysis-task', {name: 'confirm', repetitionCount: 1, > testGroupName: 'Confirm', revisionSets: [oneRevisionSet, anotherRevisionSet], >- startRun: testRuns[0]['id'], endRun: testRuns[1]['id']}); >+ startRun: testRuns[0]['id'], endRun: testRuns[1]['id'], hasPendingNotification: true}); >+ >+ const task = await AnalysisTask.fetchById(content['taskId']); >+ assert.equal(task.name(), 'confirm'); >+ assert(!task.hasResults()); >+ assert(task.hasPendingRequests()); >+ assert.deepEqual(task.bugs(), []); >+ assert.deepEqual(task.causes(), []); >+ assert.deepEqual(task.fixes(), []); >+ assert.equal(task.changeType(), null); >+ assert.equal(task.platform().label(), 'some platform'); >+ assert.equal(task.metric().test().label(), 'test1'); >+ >+ const testGroups = await TestGroup.fetchForTask(task.id()); >+ assert.equal(testGroups.length, 1); >+ const testGroup = testGroups[0]; >+ assert.equal(testGroup.name(), 'Confirm'); >+ assert.equal(testGroup.hasPendingNotification(), true); >+ const buildRequests = testGroup.buildRequests(); >+ assert.equal(buildRequests.length, 2); >+ >+ assert.equal(buildRequests[0].triggerable().id(), triggerableId); >+ assert.equal(buildRequests[0].triggerable().id(), triggerableId); >+ >+ assert.equal(buildRequests[0].testGroup(), testGroup); >+ assert.equal(buildRequests[1].testGroup(), testGroup); >+ >+ assert.equal(buildRequests[0].platform(), task.platform()); >+ assert.equal(buildRequests[1].platform(), task.platform()); >+ >+ assert.equal(buildRequests[0].analysisTaskId(), task.id()); >+ assert.equal(buildRequests[1].analysisTaskId(), task.id()); >+ >+ assert.equal(buildRequests[0].test(), test1); >+ assert.equal(buildRequests[1].test(), test1); >+ >+ assert.ok(!buildRequests[0].isBuild()); >+ assert.ok(!buildRequests[1].isBuild()); >+ assert.ok(buildRequests[0].isTest()); >+ assert.ok(buildRequests[1].isTest()); >+ >+ const firstCommitSet = buildRequests[0].commitSet(); >+ const secondCommitSet = buildRequests[1].commitSet(); >+ const webkitRepository = Repository.findById(webkitId); >+ assert.equal(firstCommitSet.commitForRepository(webkitRepository).revision(), '191622'); >+ assert.equal(secondCommitSet.commitForRepository(webkitRepository).revision(), '191623'); >+ }); >+ >+ it('should create an analysis task with test group and respect the "hasPendingNotification" flag in the http request', async () => { >+ const webkitId = 1; >+ const platformId = 1; >+ const test1Id = 2; >+ const triggerableId = 1234; >+ >+ const db = TestServer.database(); >+ await db.insert('tests', {id: 1, name: 'Suite'}); >+ await db.insert('tests', {id: test1Id, name: 'test1', parent: 1}); >+ await db.insert('repositories', {id: webkitId, name: 'WebKit'}); >+ await db.insert('platforms', {id: platformId, name: 'some platform'}); >+ await db.insert('build_triggerables', {id: 1234, name: 'test-triggerable'}); >+ await db.insert('triggerable_repository_groups', {id: 2345, name: 'webkit-only', triggerable: triggerableId}); >+ await db.insert('triggerable_repositories', {repository: webkitId, group: 2345}); >+ await db.insert('triggerable_configurations', {test: test1Id, platform: platformId, triggerable: triggerableId}); >+ await addBuilderForReport(reportWithRevision[0]); >+ >+ await TestServer.remoteAPI().postJSON('/api/report/', reportWithRevision); >+ await TestServer.remoteAPI().postJSON('/api/report/', anotherReportWithRevision); >+ await Manifest.fetch(); >+ >+ let test1 = Test.findById(test1Id); >+ let somePlatform = Platform.findById(platformId); >+ const configRow = await db.selectFirstRow('test_configurations', {metric: test1.metrics()[0].id(), platform: somePlatform.id()}); >+ const testRuns = await db.selectRows('test_runs', {config: configRow['id']}); >+ assert.equal(testRuns.length, 2); >+ >+ const oneRevisionSet = {[webkitId]: {revision: '191622'}}; >+ const anotherRevisionSet = {[webkitId]: {revision: '191623'}}; >+ >+ const content = await PrivilegedAPI.sendRequest('create-analysis-task', {name: 'confirm', repetitionCount: 1, >+ testGroupName: 'Confirm', revisionSets: [oneRevisionSet, anotherRevisionSet], >+ startRun: testRuns[0]['id'], endRun: testRuns[1]['id'], hasPendingNotification: false}); > > const task = await AnalysisTask.fetchById(content['taskId']); > assert.equal(task.name(), 'confirm'); >@@ -584,6 +652,7 @@ describe('/privileged-api/create-analysis-task with node privileged api', functi > assert.equal(testGroups.length, 1); > const testGroup = testGroups[0]; > assert.equal(testGroup.name(), 'Confirm'); >+ assert.equal(testGroup.hasPendingNotification(), false); > const buildRequests = testGroup.buildRequests(); > assert.equal(buildRequests.length, 2); > >diff --git a/Websites/perf.webkit.org/server-tests/privileged-api-create-test-group-tests.js b/Websites/perf.webkit.org/server-tests/privileged-api-create-test-group-tests.js >index 2f3b29768a626d4e16afe933ed82195ffcc7809a..309367421f7be52696dc76fd74fcb2b021bab266 100644 >--- a/Websites/perf.webkit.org/server-tests/privileged-api-create-test-group-tests.js >+++ b/Websites/perf.webkit.org/server-tests/privileged-api-create-test-group-tests.js >@@ -339,9 +339,9 @@ describe('/privileged-api/create-test-group', function () { > it('should return "DuplicateTestGroupName" when there is already a test group of the same name', () => { > return addTriggerableAndCreateTask('some task').then((taskId) => { > const commitSets = {'WebKit': ['191622', '191623']}; >- return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, commitSets}).then((content) => { >+ return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, commitSets, hasPendingNotification: true}).then((content) => { > assert(content['testGroupId']); >- return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, commitSets}); >+ return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, commitSets, hasPendingNotification: true}); > }).then(() => { > assert(false, 'should never be reached'); > }, (error) => { >@@ -391,7 +391,7 @@ describe('/privileged-api/create-test-group', function () { > const ownedJSC = Repository.all().filter((repository) => repository.name() == 'JavaScriptCore' && repository.ownerId())[0]; > const revisionSets = [{[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}, [ownedJSC.id()]: {revision: 'owned-jsc-6161', ownerRevision: '191622'}}, > {[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}, [ownedJSC.id()]: {revision: 'owned-jsc-9191', ownerRevision: '192736'}}]; >- return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 1, revisionSets}); >+ return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 1, hasPendingNotification: true, revisionSets}); > }).then((content) => { > assert.equal(content['status'], 'OK'); > groupId = content['testGroupId']; >@@ -401,6 +401,7 @@ describe('/privileged-api/create-test-group', function () { > const group = testGroups[0]; > assert.equal(group.id(), groupId); > assert.equal(group.repetitionCount(), 1); >+ assert.ok(group.hasPendingNotification()); > const requests = group.buildRequests(); > assert.equal(requests.length, 4); > assert(requests[0].isBuild()); >@@ -437,7 +438,7 @@ describe('/privileged-api/create-test-group', function () { > const jsc = Repository.all().filter((repository) => repository.name() == 'JavaScriptCore' && repository.ownerId())[0]; > const revisionSets = [{[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}, [jsc.id()]: {revision: 'owned-jsc-6161', ownerRevision: '191622'}}, > {[macos.id()]: {revision: '15A284'}, [jsc.id()]: {revision: 'owned-jsc-9191', ownerRevision: '192736'}}]; >- return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 1, revisionSets}); >+ return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 1, hasPendingNotification: true, revisionSets}); > }).then((content) => { > assert.equal(content['status'], 'OK'); > groupId = content['testGroupId']; >@@ -447,6 +448,7 @@ describe('/privileged-api/create-test-group', function () { > const group = testGroups[0]; > assert.equal(group.id(), groupId); > assert.equal(group.repetitionCount(), 1); >+ assert.ok(group.hasPendingNotification()); > const requests = group.buildRequests(); > assert.equal(requests.length, 4); > assert(requests[0].isBuild()); >@@ -476,7 +478,7 @@ describe('/privileged-api/create-test-group', function () { > 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; >- return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, commitSets: {'macOS': ['15A284', '15A284'], 'WebKit': ['191622', '191623']}}).then((content) => { >+ return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, hasPendingNotification: true, commitSets: {'macOS': ['15A284', '15A284'], 'WebKit': ['191622', '191623']}}).then((content) => { > insertedGroupId = content['testGroupId']; > return TestGroup.fetchForTask(taskId, true); > }).then((testGroups) => { >@@ -484,6 +486,7 @@ describe('/privileged-api/create-test-group', function () { > const group = testGroups[0]; > assert.equal(group.id(), insertedGroupId); > assert.equal(group.repetitionCount(), 1); >+ assert.ok(group.hasPendingNotification()); > const requests = group.buildRequests(); > assert.equal(requests.length, 2); > >@@ -512,7 +515,7 @@ describe('/privileged-api/create-test-group', function () { > return addTriggerableAndCreateTask('some task').then((taskId) => { > const webkit = Repository.findById(MockData.webkitRepositoryId()); > const revisionSets = [{[webkit.id()]: {revision: '191622'}}, {[webkit.id()]: {revision: '191623'}}]; >- const params = {name: 'test', task: taskId, revisionSets}; >+ const params = {name: 'test', task: taskId, hasPendingNotification: true, revisionSets}; > let insertedGroupId; > return PrivilegedAPI.sendRequest('create-test-group', params).then((content) => { > insertedGroupId = content['testGroupId']; >@@ -522,6 +525,7 @@ describe('/privileged-api/create-test-group', function () { > const group = testGroups[0]; > assert.equal(group.id(), insertedGroupId); > assert.equal(group.repetitionCount(), 1); >+ assert.ok(group.hasPendingNotification()); > const requests = group.buildRequests(); > assert.equal(requests.length, 2); > >@@ -545,7 +549,7 @@ describe('/privileged-api/create-test-group', function () { > return addTriggerableAndCreateTask('some task').then((taskId) => { > let insertedGroupId; > return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, >- commitSets: {'WebKit': ['191622', '191623'], 'macOS': ['15A284', '15A284']}}).then((content) => { >+ commitSets: {'WebKit': ['191622', '191623'], 'macOS': ['15A284', '15A284']}, hasPendingNotification: true}).then((content) => { > insertedGroupId = content['testGroupId']; > return TestGroup.fetchForTask(taskId, true); > }).then((testGroups) => { >@@ -553,6 +557,7 @@ describe('/privileged-api/create-test-group', function () { > const group = testGroups[0]; > assert.equal(group.id(), insertedGroupId); > assert.equal(group.repetitionCount(), 2); >+ assert.ok(group.hasPendingNotification()); > const requests = group.buildRequests(); > assert.equal(requests.length, 4); > const webkit = Repository.all().filter((repository) => repository.name() == 'WebKit')[0]; >@@ -589,7 +594,7 @@ describe('/privileged-api/create-test-group', function () { > macos = Repository.findById(MockData.macosRepositoryId()); > const revisionSets = [{[macos.id()]: {revision: '15A284'}, [webkit.id()]: {revision: '2ceda'}}, > {[macos.id()]: {revision: '15A284'}, [webkit.id()]: {revision: '5471a'}}]; >- const params = {name: 'test', task: taskId, repetitionCount: 2, revisionSets}; >+ const params = {name: 'test', task: taskId, repetitionCount: 2, hasPendingNotification: true, revisionSets}; > let insertedGroupId; > return PrivilegedAPI.sendRequest('create-test-group', params).then((content) => { > insertedGroupId = content['testGroupId']; >@@ -599,6 +604,7 @@ describe('/privileged-api/create-test-group', function () { > const group = testGroups[0]; > assert.equal(group.id(), insertedGroupId); > assert.equal(group.repetitionCount(), 2); >+ assert.ok(group.hasPendingNotification()); > const requests = group.buildRequests(); > assert.equal(requests.length, 4); > >@@ -630,7 +636,7 @@ describe('/privileged-api/create-test-group', function () { > macos = Repository.findById(MockData.macosRepositoryId()); > const revisionSets = [{[macos.id()]: {revision: '15A284'}, [webkit.id()]: {revision: '191622'}}, > {[webkit.id()]: {revision: '191623'}}]; >- const params = {name: 'test', task: taskId, repetitionCount: 2, revisionSets}; >+ const params = {name: 'test', task: taskId, repetitionCount: 2, hasPendingNotification: true, revisionSets}; > let insertedGroupId; > return PrivilegedAPI.sendRequest('create-test-group', params).then((content) => { > insertedGroupId = content['testGroupId']; >@@ -640,6 +646,7 @@ describe('/privileged-api/create-test-group', function () { > const group = testGroups[0]; > assert.equal(group.id(), insertedGroupId); > assert.equal(group.repetitionCount(), 2); >+ assert.ok(group.hasPendingNotification()); > const requests = group.buildRequests(); > assert.equal(requests.length, 4); > >@@ -679,7 +686,7 @@ describe('/privileged-api/create-test-group', function () { > uploadedFile = response['uploadedFile']; > 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) => { >+ return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, hasPendingNotification: true, revisionSets}).then((content) => { > insertedGroupId = content['testGroupId']; > return TestGroup.fetchForTask(taskId, true); > }); >@@ -688,6 +695,7 @@ describe('/privileged-api/create-test-group', function () { > const group = testGroups[0]; > assert.equal(group.id(), insertedGroupId); > assert.equal(group.repetitionCount(), 2); >+ assert.ok(group.hasPendingNotification()); > const requests = group.buildRequests(); > assert.equal(requests.length, 4); > >@@ -727,7 +735,7 @@ describe('/privileged-api/create-test-group', function () { > 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}); >+ return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, hasPendingNotification: true, revisionSets}); > }).then((content) => { > insertedGroupId = content['testGroupId']; > return TestGroup.fetchForTask(taskId, true); >@@ -736,6 +744,7 @@ describe('/privileged-api/create-test-group', function () { > const group = testGroups[0]; > assert.equal(group.id(), insertedGroupId); > assert.equal(group.repetitionCount(), 2); >+ assert.ok(group.hasPendingNotification()); > assert.equal(group.test(), Test.findById(MockData.someTestId())); > assert.equal(group.platform(), Platform.findById(MockData.somePlatformId())); > const requests = group.buildRequests(); >@@ -791,7 +800,7 @@ describe('/privileged-api/create-test-group', function () { > jsc = Repository.all().filter((repository) => repository.name() == 'JavaScriptCore')[0]; > const revisionSets = [{[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}}, > {[webkit.id()]: {revision: '192736'}, [macos.id()]: {revision: '15A284'}, [jsc.id()]: {revision: 'owned-jsc-9191', ownerRevision: '192736'}}]; >- return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, revisionSets}); >+ return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, hasPendingNotification: true, revisionSets}); > }).then((content) => { > insertedGroupId = content['testGroupId']; > return TestGroup.fetchForTask(taskId, true); >@@ -800,6 +809,7 @@ describe('/privileged-api/create-test-group', function () { > const group = testGroups[0]; > assert.equal(group.id(), insertedGroupId); > assert.equal(group.repetitionCount(), 2); >+ assert.ok(group.hasPendingNotification()); > assert.equal(group.test(), Test.findById(MockData.someTestId())); > assert.equal(group.platform(), Platform.findById(MockData.somePlatformId())); > const requests = group.buildRequests(); >@@ -865,7 +875,7 @@ describe('/privileged-api/create-test-group', function () { > jsc = Repository.all().filter((repository) => repository.name() == 'JavaScriptCore')[0]; > const revisionSets = [{[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}, [jsc.id()]: {revision: 'owned-jsc-6161', ownerRevision: '191622'}}, > {[webkit.id()]: {revision: '192736'}, [macos.id()]: {revision: '15A284'}, [jsc.id()]: {revision: 'owned-jsc-9191', ownerRevision: '192736'}}]; >- return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, revisionSets}); >+ return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, hasPendingNotification: true, revisionSets}); > }).then((content) => { > insertedGroupId = content['testGroupId']; > return TestGroup.fetchForTask(taskId, true); >@@ -874,6 +884,7 @@ describe('/privileged-api/create-test-group', function () { > const group = testGroups[0]; > assert.equal(group.id(), insertedGroupId); > assert.equal(group.repetitionCount(), 2); >+ assert.ok(group.hasPendingNotification()); > assert.equal(group.test(), Test.findById(MockData.someTestId())); > assert.equal(group.platform(), Platform.findById(MockData.somePlatformId())); > const requests = group.buildRequests(); >@@ -947,7 +958,7 @@ describe('/privileged-api/create-test-group', function () { > uploadedFile = UploadedFile.ensureSingleton(rawFile.id, rawFile); > const revisionSets = [{[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}, [jsc.id()]: {revision: 'owned-jsc-6161', ownerRevision: '191622'}}, > {[webkit.id()]: {revision: '192736', patch: uploadedFile.id()}, [macos.id()]: {revision: '15A284'}, [jsc.id()]: {revision: 'owned-jsc-9191', ownerRevision: '192736'}}]; >- return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, revisionSets}); >+ return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, hasPendingNotification: true, revisionSets}); > }).then((content) => { > insertedGroupId = content['testGroupId']; > return TestGroup.fetchForTask(taskId, true); >@@ -956,6 +967,7 @@ describe('/privileged-api/create-test-group', function () { > const group = testGroups[0]; > assert.equal(group.id(), insertedGroupId); > assert.equal(group.repetitionCount(), 2); >+ assert.ok(group.hasPendingNotification()); > assert.equal(group.test(), Test.findById(MockData.someTestId())); > assert.equal(group.platform(), Platform.findById(MockData.somePlatformId())); > const requests = group.buildRequests(); >@@ -1040,7 +1052,7 @@ describe('/privileged-api/create-test-group', function () { > uploadedFile = UploadedFile.ensureSingleton(rawFile.id, rawFile); > const revisionSets = [{[jsc.id()]: {revision: 'jsc-6161'}, [webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284'}, [ownedJSC.id()]: {revision: 'owned-jsc-6161', ownerRevision: '191622'}}, > {[jsc.id()]: {revision: 'jsc-9191'}, [webkit.id()]: {revision: '192736', patch: uploadedFile.id()}, [macos.id()]: {revision: '15A284'}, [ownedJSC.id()]: {revision: 'owned-jsc-9191', ownerRevision: '192736'}}]; >- return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, revisionSets}); >+ return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, hasPendingNotification: true, revisionSets}); > }).then((content) => { > insertedGroupId = content['testGroupId']; > return TestGroup.fetchForTask(taskId, true); >@@ -1050,6 +1062,7 @@ describe('/privileged-api/create-test-group', function () { > assert.equal(group.id(), insertedGroupId); > assert.equal(group.repetitionCount(), 2); > assert.equal(group.test(), Test.findById(MockData.someTestId())); >+ assert.ok(group.hasPendingNotification()); > assert.equal(group.platform(), Platform.findById(MockData.somePlatformId())); > const requests = group.buildRequests(); > assert.equal(requests.length, 6); >@@ -1136,7 +1149,7 @@ describe('/privileged-api/create-test-group', function () { > uploadedFile = UploadedFile.ensureSingleton(rawFile.id, rawFile); > const revisionSets = [{[macos.id()]: {revision: '15A284'}}, > {[webkit.id()]: {revision: '191622', patch: uploadedFile.id()}, [macos.id()]: {revision: '15A284'}}]; >- return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, revisionSets}); >+ return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, hasPendingNotification: true, revisionSets}); > }).then((content) => { > insertedGroupId = content['testGroupId']; > return TestGroup.fetchForTask(taskId, true); >@@ -1145,6 +1158,7 @@ describe('/privileged-api/create-test-group', function () { > const group = testGroups[0]; > assert.equal(group.id(), insertedGroupId); > assert.equal(group.repetitionCount(), 2); >+ assert.ok(group.hasPendingNotification()); > assert.equal(group.test(), Test.findById(MockData.someTestId())); > assert.equal(group.platform(), Platform.findById(MockData.somePlatformId())); > const requests = group.buildRequests(); >@@ -1199,7 +1213,7 @@ describe('/privileged-api/create-test-group', function () { > uploadedFile = UploadedFile.ensureSingleton(rawFile.id, rawFile); > const revisionSets = [{[webkit.id()]: {revision: '191622'}, [macos.id()]: {revision: '15A284', patch: uploadedFile.id()}}, > {[webkit.id()]: {revision: '192736'}, [macos.id()]: {revision: '15A284'}}]; >- return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, revisionSets}); >+ return PrivilegedAPI.sendRequest('create-test-group', {name: 'test', task: taskId, repetitionCount: 2, hasPendingNotification: true, revisionSets}); > }).then(() => { > assert(false, 'should never be reached'); > }, (error) => { >@@ -1214,7 +1228,42 @@ describe('/privileged-api/create-test-group', function () { > webkit = Repository.all().filter((repository) => repository.name() == 'WebKit')[0]; > 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}); >+ {name: 'test', taskName: 'other task', platform: MockData.somePlatformId(), test: MockData.someTestId(), hasPendingNotification: true, revisionSets}); >+ }).then((result) => { >+ insertedGroupId = result['testGroupId']; >+ return Promise.all([AnalysisTask.fetchById(result['taskId']), TestGroup.fetchForTask(result['taskId'], true)]); >+ }).then((result) => { >+ const [analysisTask, testGroups] = result; >+ >+ assert.equal(analysisTask.name(), 'other task'); >+ >+ assert.equal(testGroups.length, 1); >+ const group = testGroups[0]; >+ assert.equal(group.id(), insertedGroupId); >+ assert.equal(group.repetitionCount(), 1); >+ assert.ok(group.hasPendingNotification()); >+ const requests = group.buildRequests(); >+ assert.equal(requests.length, 2); >+ >+ const set0 = requests[0].commitSet(); >+ const set1 = requests[1].commitSet(); >+ assert.deepEqual(set0.repositories(), [webkit]); >+ assert.deepEqual(set0.customRoots(), []); >+ assert.deepEqual(set1.repositories(), [webkit]); >+ assert.deepEqual(set1.customRoots(), []); >+ assert.equal(set0.revisionForRepository(webkit), '191622'); >+ assert.equal(set1.revisionForRepository(webkit), '191623'); >+ }); >+ }); >+ >+ it('should be able to create a test group with no pending notification', () => { >+ let insertedGroupId; >+ let webkit; >+ return addTriggerableAndCreateTask('some task').then(() => { >+ webkit = Repository.all().filter((repository) => repository.name() == 'WebKit')[0]; >+ 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(), hasPendingNotification: false, revisionSets}); > }).then((result) => { > insertedGroupId = result['testGroupId']; > return Promise.all([AnalysisTask.fetchById(result['taskId']), TestGroup.fetchForTask(result['taskId'], true)]); >@@ -1227,6 +1276,7 @@ describe('/privileged-api/create-test-group', function () { > const group = testGroups[0]; > assert.equal(group.id(), insertedGroupId); > assert.equal(group.repetitionCount(), 1); >+ assert.ok(!group.hasPendingNotification()); > const requests = group.buildRequests(); > assert.equal(requests.length, 2); > >@@ -1250,12 +1300,12 @@ describe('/privileged-api/create-test-group', function () { > webkit = Repository.all().filter((repository) => repository.name() == 'WebKit')[0]; > 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}); >+ {name: 'test1', taskName: 'other task', platform: MockData.somePlatformId(), hasPendingNotification: true, test, revisionSets}); > }).then((result) => { > firstResult = result; > 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}); >+ {name: 'test2', task: result['taskId'], platform: MockData.otherPlatformId(), hasPendingNotification: true, test, revisionSets, repetitionCount: 2}); > }).then((result) => { > secondResult = result; > assert.equal(firstResult['taskId'], secondResult['taskId']); >diff --git a/Websites/perf.webkit.org/server-tests/privileged-api-update-test-group-tests.js b/Websites/perf.webkit.org/server-tests/privileged-api-update-test-group-tests.js >new file mode 100644 >index 0000000000000000000000000000000000000000..e8d9e17aa8360264a2ed3179d3135a078c30f2e7 >--- /dev/null >+++ b/Websites/perf.webkit.org/server-tests/privileged-api-update-test-group-tests.js >@@ -0,0 +1,166 @@ >+'use strict'; >+ >+const assert = require('assert'); >+ >+const MockData = require('./resources/mock-data.js'); >+const TestServer = require('./resources/test-server.js'); >+const addSlaveForReport = require('./resources/common-operations.js').addSlaveForReport; >+const prepareServerTest = require('./resources/common-operations.js').prepareServerTest; >+const assertThrows = require('./resources/common-operations.js').assertThrows; >+ >+async function createAnalysisTask(name, webkitRevisions = ["191622", "191623"]) >+{ >+ const reportWithRevision = [{ >+ "buildNumber": "124", >+ "buildTime": "2015-10-27T15:34:51", >+ "revisions": { >+ "WebKit": { >+ "revision": webkitRevisions[0], >+ "timestamp": '2015-10-27T11:36:56.878473Z', >+ }, >+ "macOS": { >+ "revision": "15A284", >+ } >+ }, >+ "builderName": "someBuilder", >+ "slaveName": "someSlave", >+ "slavePassword": "somePassword", >+ "platform": "some platform", >+ "tests": { >+ "some test": { >+ "metrics": { >+ "Time": ["Arithmetic"], >+ }, >+ "tests": { >+ "test1": { >+ "metrics": {"Time": { "current": [11] }}, >+ } >+ } >+ }, >+ }}]; >+ >+ const anotherReportWithRevision = [{ >+ "buildNumber": "125", >+ "buildTime": "2015-10-27T17:27:41", >+ "revisions": { >+ "WebKit": { >+ "revision": webkitRevisions[1], >+ "timestamp": '2015-10-27T16:38:10.768995Z', >+ }, >+ "macOS": { >+ "revision": "15A284", >+ } >+ }, >+ "builderName": "someBuilder", >+ "slaveName": "someSlave", >+ "slavePassword": "somePassword", >+ "platform": "some platform", >+ "tests": { >+ "some test": { >+ "metrics": { >+ "Time": ["Arithmetic"], >+ }, >+ "tests": { >+ "test1": { >+ "metrics": {"Time": { "current": [12] }}, >+ } >+ } >+ }, >+ }}]; >+ >+ const db = TestServer.database(); >+ const remote = TestServer.remoteAPI(); >+ await addSlaveForReport(reportWithRevision[0]); >+ await remote.postJSON('/api/report/', reportWithRevision); >+ await remote.postJSON('/api/report/', anotherReportWithRevision); >+ await Manifest.fetch(); >+ const test = Test.findByPath(['some test', 'test1']); >+ const platform = Platform.findByName('some platform'); >+ const configRow = await db.selectFirstRow('test_configurations', {metric: test.metrics()[0].id(), platform: platform.id()}); >+ const testRuns = await db.selectRows('test_runs', {config: configRow['id']}); >+ >+ assert.equal(testRuns.length, 2); >+ const content = await PrivilegedAPI.sendRequest('create-analysis-task', { >+ name: name, >+ startRun: testRuns[0]['id'], >+ endRun: testRuns[1]['id'], >+ hasPendingNotification: true, >+ }); >+ return content['taskId']; >+} >+ >+async function addTriggerableAndCreateTask(name, webkitRevisions) >+{ >+ const report = { >+ 'slaveName': 'anotherSlave', >+ 'slavePassword': 'anotherPassword', >+ 'triggerable': 'build-webkit', >+ 'configurations': [ >+ {test: MockData.someTestId(), platform: MockData.somePlatformId()}, >+ {test: MockData.someTestId(), platform: MockData.otherPlatformId()}, >+ ], >+ 'repositoryGroups': [ >+ {name: 'os-only', acceptsRoot: true, repositories: [ >+ {repository: MockData.macosRepositoryId(), acceptsPatch: false}, >+ ]}, >+ {name: 'webkit-only', acceptsRoot: true, repositories: [ >+ {repository: MockData.webkitRepositoryId(), acceptsPatch: true}, >+ ]}, >+ {name: 'system-and-webkit', acceptsRoot: true, repositories: [ >+ {repository: MockData.macosRepositoryId(), acceptsPatch: false}, >+ {repository: MockData.webkitRepositoryId(), acceptsPatch: true} >+ ]}, >+ {name: 'system-webkit-sjc', acceptsRoot: true, repositories: [ >+ {repository: MockData.macosRepositoryId(), acceptsPatch: false}, >+ {repository: MockData.jscRepositoryId(), acceptsPatch: false}, >+ {repository: MockData.webkitRepositoryId(), acceptsPatch: true} >+ ]}, >+ ] >+ }; >+ await MockData.addMockData(TestServer.database()); >+ await addSlaveForReport(report); >+ await TestServer.remoteAPI().postJSON('/api/update-triggerable/', report); >+ await createAnalysisTask(name, webkitRevisions); >+} >+ >+describe('/privileged-api/update-test-group', function(){ >+ prepareServerTest(this, 'node'); >+ beforeEach(() => { >+ PrivilegedAPI.configure('test', 'password'); >+ }); >+ >+ it('should throw "SlaveNotFound" if invalid slave name and password combination is provided', async () => { >+ await addTriggerableAndCreateTask('some task'); >+ >+ PrivilegedAPI.configure('test', 'wrongpassword'); >+ const webkit = Repository.all().filter((repository) => repository.name() == 'WebKit')[0]; >+ const revisionSets = [{[webkit.id()]: {revision: '191622'}}, {[webkit.id()]: {revision: '191623'}}]; >+ >+ await assertThrows('SlaveNotFound', () => >+ PrivilegedAPI.sendRequest('create-test-group', {name: 'test', taskName: 'other task', >+ platform: MockData.somePlatformId(), test: MockData.someTestId(), revisionSets})); >+ }); >+ >+ it('should be able to update has pending notification flag', async () => { >+ await addTriggerableAndCreateTask('some task'); >+ const webkit = Repository.all().filter((repository) => repository.name() == 'WebKit')[0]; >+ const revisionSets = [{[webkit.id()]: {revision: '191622'}}, {[webkit.id()]: {revision: '191623'}}]; >+ let result = await PrivilegedAPI.sendRequest('create-test-group', >+ {name: 'test', taskName: 'other task', platform: MockData.somePlatformId(), test: MockData.someTestId(), hasPendingNotification: true, revisionSets}); >+ const insertedGroupId = result['testGroupId']; >+ result = await Promise.all([AnalysisTask.fetchById(result['taskId']), TestGroup.fetchForTask(result['taskId'], true)]); >+ >+ const [analysisTask, testGroups] = result; >+ >+ assert.equal(analysisTask.name(), 'other task'); >+ >+ assert.equal(testGroups.length, 1); >+ const group = testGroups[0]; >+ assert.equal(group.id(), insertedGroupId); >+ assert.equal(group.repetitionCount(), 1); >+ assert.equal(group.hasPendingNotification(), true); >+ await group.didSendNotification(); >+ const updatedGroup = TestGroup.findById(group.id()); >+ assert.equal(updatedGroup.hasPendingNotification(), false); >+ }); >+}); >\ No newline at end of file >diff --git a/Websites/perf.webkit.org/server-tests/resources/mock-data.js b/Websites/perf.webkit.org/server-tests/resources/mock-data.js >index 94a393f1e96956e59d8520db9047b61e834bc7a5..03c4628a0a11ee2669bfe5197b87381d54392615 100644 >--- a/Websites/perf.webkit.org/server-tests/resources/mock-data.js >+++ b/Websites/perf.webkit.org/server-tests/resources/mock-data.js >@@ -79,7 +79,7 @@ MockData = { > db.insert('analysis_tasks', {id: 500, platform: 65, metric: 300, name: 'some task', > start_run: 801, start_run_time: '2015-10-27T12:05:27.1Z', > end_run: 801, end_run_time: '2015-10-27T12:05:27.1Z'}), >- db.insert('analysis_test_groups', {id: 600, task: 500, name: 'some test group'}), >+ db.insert('analysis_test_groups', {id: 600, task: 500, name: 'some test group', has_pending_notification: true}), > db.insert('build_requests', {id: 700, status: statusList[0], triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 600, order: 0, commit_set: 401}), > db.insert('build_requests', {id: 701, status: statusList[1], triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 600, order: 1, commit_set: 402}), > db.insert('build_requests', {id: 702, status: statusList[2], triggerable: 1000, repository_group: 2001, platform: 65, test: 200, group: 600, order: 2, commit_set: 401}), >diff --git a/Websites/perf.webkit.org/server-tests/tools-sync-buildbot-integration-tests.js b/Websites/perf.webkit.org/server-tests/tools-sync-buildbot-integration-tests.js >index e61258266cd8e0c54d4c036e54d9b7d10f86e15e..7e04e5ee55e77934c22fd433ea3c91807317d58d 100644 >--- a/Websites/perf.webkit.org/server-tests/tools-sync-buildbot-integration-tests.js >+++ b/Websites/perf.webkit.org/server-tests/tools-sync-buildbot-integration-tests.js >@@ -173,7 +173,7 @@ function createTestGroup(task_name='custom task') { > set1.setRevisionForRepository(webkit, '191622'); > const set2 = new CustomCommitSet; > set2.setRevisionForRepository(webkit, '192736'); >- return TestGroup.createWithTask('custom task', Platform.findById(MockData.somePlatformId()), someTest, 'some group', 2, [set1, set2]).then((task) => { >+ return TestGroup.createWithTask('custom task', Platform.findById(MockData.somePlatformId()), someTest, 'some group', 2, [set1, set2], true).then((task) => { > return TestGroup.findAllByTask(task.id())[0]; > }); > } >@@ -193,7 +193,7 @@ async function createTestGroupWihPatch() > set1.setRevisionForRepository(webkit, '191622', uploadedPatchFile); > const set2 = new CustomCommitSet; > set2.setRevisionForRepository(webkit, '191622'); >- const task = await TestGroup.createWithTask('custom task', Platform.findById(MockData.somePlatformId()), someTest, 'some group', 2, [set1, set2]); >+ const task = await TestGroup.createWithTask('custom task', Platform.findById(MockData.somePlatformId()), someTest, 'some group', 2, [set1, set2], true); > > return TestGroup.findAllByTask(task.id())[0]; > } >@@ -209,7 +209,7 @@ function createTestGroupWihOwnedCommit() > const set2 = new CustomCommitSet; > set2.setRevisionForRepository(webkit, '192736'); > set2.setRevisionForRepository(ownedSJC, 'owned-jsc-9191', null, '192736'); >- return TestGroup.createWithTask('custom task', Platform.findById(MockData.somePlatformId()), someTest, 'some group', 2, [set1, set2]).then((task) => { >+ return TestGroup.createWithTask('custom task', Platform.findById(MockData.somePlatformId()), someTest, 'some group', 2, [set1, set2], true).then((task) => { > return TestGroup.findAllByTask(task.id())[0]; > }); > } >diff --git a/Websites/perf.webkit.org/tools/js/email-notifier.js b/Websites/perf.webkit.org/tools/js/email-notifier.js >new file mode 100644 >index 0000000000000000000000000000000000000000..6d875e486df17de1783bb8d65c726e1fcf1ae111 >--- /dev/null >+++ b/Websites/perf.webkit.org/tools/js/email-notifier.js >@@ -0,0 +1,188 @@ >+const RemoteAPI = require('./remote.js').RemoteAPI; >+const LazilyEvaluatedFunction = require('../../public/v3/lazily-evaluated-function').LazilyEvaluatedFunction; >+ >+class EmailNotifier { >+ constructor(findEmailCommand, emailGroupConfiguration, emailServerConfig, emailTemplate, Subprocess) >+ { >+ this._findEmailCommand = findEmailCommand; >+ this._emailServerRemoteAPI = new RemoteAPI(emailServerConfig); >+ this._emailServicePath = emailServerConfig.path; >+ this._emailTemplate = emailTemplate; >+ this._subprocess = Subprocess; >+ this._matchingFunctionByEmailGroup = EmailNotifier._buildMatchingFunctionByEmailGroup(emailGroupConfiguration); >+ } >+ >+ async sendNotificationsForTestGroups(testGroups) >+ { >+ for (const testGroup of testGroups) { >+ const message = await this._messageForTestGroup(testGroup); >+ const title = `Test group: "${testGroup.name()}" from Analysis task "${testGroup.task().name()}" has finished all build requests`; >+ const recipients = await this._determineRecipientsForTestGroup(testGroup); >+ console.log(title, recipients); >+ const content = EmailNotifier._instantiateNotificationTemplate(this._emailTemplate, title, recipients.join(', '), message); >+ await this._sendEmail(content); >+ await testGroup.didSendNotification(); >+ } >+ } >+ >+ async _determineRecipientsForTestGroup(testGroup) >+ { >+ const author = testGroup.author(); >+ const recipients = new Set; >+ if (author && author.length) { >+ try { >+ const authorEmail = (await this._subprocess.execute([...this._findEmailCommand, author])).trim(); >+ if (authorEmail) >+ recipients.add(authorEmail); >+ } catch (error) { >+ console.error(`Failed to find email address for ${author} due to ${error}`); >+ } >+ } >+ >+ const platformName = testGroup.platform().name(); >+ const topLevelTestName = testGroup.test().path()[0].name(); >+ for (const [emailGroup, matchingFunction] of this._matchingFunctionByEmailGroup) { >+ if (matchingFunction(platformName, topLevelTestName)) >+ recipients.add(emailGroup); >+ } >+ return Array.from(recipients); >+ } >+ >+ static _buildMatchingFunctionByEmailGroup(emailGroupConfiguration) >+ { >+ const matchingFunctionByEmailGroup = new Map; >+ for (const [emailGroup, matchingCriteria] of Object.entries(emailGroupConfiguration)) { >+ const matchFunction = (platform, test) => { >+ let hasMatch = false; >+ if (matchingCriteria.tests) { >+ if (!matchingCriteria.tests.includes(test)) >+ return false; >+ hasMatch = true; >+ } >+ if (matchingCriteria.platforms) { >+ if (!matchingCriteria.platforms.includes(platform)) >+ return false; >+ hasMatch = true; >+ } >+ return hasMatch; >+ }; >+ matchingFunctionByEmailGroup.set(emailGroup, matchFunction); >+ } >+ return matchingFunctionByEmailGroup; >+ } >+ >+ static _characterReference(input) >+ { >+ if (!input) >+ return input; >+ return input.replace('<', '<').replace('>', '>').replace('"', '"'); >+ } >+ >+ async _messageForTestGroup(testGroup) >+ { >+ const analysisTask = testGroup.task() || await AnalysisTask.fetchById(testGroup._taskId); >+ const analysisResults = await AnalysisResults.fetch(analysisTask.id()); >+ const requestedCommitSets = testGroup.requestedCommitSets(); >+ const analysisTaskURL = EmailNotifier._URLForAnalysisTask(analysisTask); >+ console.assert(requestedCommitSets.length, 2); >+ const completedCount = testGroup.buildRequests().filter((buildRequest) => buildRequest.hasCompleted()).length; >+ const buildRequestsForStartCommitSet = testGroup.requestsForCommitSet(requestedCommitSets[0]); >+ const buildRequestsForEndCommitSet = testGroup.requestsForCommitSet(requestedCommitSets[1]); >+ const totalRows = buildRequestsForStartCommitSet.length + buildRequestsForEndCommitSet.length; >+ >+ let message = `<b>"${EmailNotifier._characterReference(testGroup.name())}"</b> from <a href="${analysisTaskURL}"><b>${EmailNotifier._characterReference(analysisTask.name())}</b></a> has finished. Success rate: ${completedCount} / ${totalRows}.<br>`; >+ let table = `<table border="1"><thead><tr><th>Test</th><th>Label</th><th>Result</th><th>Average</th><th>Comparison</th></tr></thead><tbody>`; >+ >+ console.log(message); >+ const test = testGroup.test(); >+ const metrics = analysisTask.metric() ? [analysisTask.metric()] : test.metrics(); >+ for (const metric of metrics) { >+ const formatter = metric.makeFormatter(4); >+ const deltaFormatter = metric.makeFormatter(2, false); >+ const formatValue = (value, interval) => { >+ const delta = interval ? (interval[1] - interval[0]) / 2 : null; >+ let result = value == null || isNaN(value) ? '-' : formatter(value); >+ if (delta != null && !isNaN(delta)) >+ result += ` \u00b1 ${deltaFormatter(delta)}`; >+ return result; >+ }; >+ >+ const analysisResultsView = analysisResults.viewForMetric(metric); >+ const measurementsForBuildRequests = (requests) => requests.map((request) => analysisResultsView.resultForRequest(request)); >+ const startValues = measurementsForBuildRequests(buildRequestsForStartCommitSet).filter((result) => !!result).map((result) => result.value); >+ const endValues = measurementsForBuildRequests(buildRequestsForEndCommitSet).filter((result) => !!result).map((result) => result.value); >+ const comparison = testGroup.compareTestResults(analysisTask.metric(), startValues, endValues); >+ >+ if (comparison.status !== 'failed') { >+ // console.log(metric); >+ message += `${EmailNotifier._characterReference(testGroup.test().name())} - ${EmailNotifier._characterReference(metric.aggregatorLabel())}: ${EmailNotifier._characterReference(comparison.fullLabel)}<br>`; >+ } >+ >+ let firstRow = true; >+ for(const measurement of measurementsForBuildRequests(buildRequestsForStartCommitSet)) { >+ if (firstRow) { >+ firstRow = false; >+ table += `<tr> >+ <th rowspan=${totalRows}>${EmailNotifier._characterReference(testGroup.platform().name())} : ${EmailNotifier._characterReference(testGroup.test().name())} : ${EmailNotifier._characterReference(metric.aggregatorLabel())}</th> >+ <th rowspan=${buildRequestsForStartCommitSet.length}>${testGroup.labelForCommitSet(requestedCommitSets[0])}</th> >+ <td>${measurement ? formatValue(measurement.value, measurement.interval) : 'Failed'}</td> >+ <td rowspan=${buildRequestsForStartCommitSet.length}>${formatValue(Statistics.mean(startValues), Statistics.confidenceInterval(startValues))}</td> >+ <td rowspan=${totalRows}>${EmailNotifier._characterReference(comparison.fullLabel)}</td> >+ </tr>`; >+ } >+ else >+ table += `<tr> >+ <td>${measurement ? formatValue(measurement.value, measurement.interval) : 'Failed'}</td> >+ </tr>`; >+ } >+ >+ firstRow = true; >+ for(const measurement of measurementsForBuildRequests(buildRequestsForEndCommitSet)) { >+ if (firstRow) { >+ firstRow = false; >+ table += `<tr> >+ <th rowspan=${buildRequestsForEndCommitSet.length}>${testGroup.labelForCommitSet(requestedCommitSets[1])}</th> >+ <td>${measurement ? formatValue(measurement.value, measurement.interval) : 'Failed'}</td> >+ <td rowspan=${buildRequestsForEndCommitSet.length}>${formatValue(Statistics.mean(endValues), Statistics.confidenceInterval(endValues))}</td> >+ </tr>`; >+ } >+ else >+ table += `<tr> >+ <td>${measurement ? formatValue(measurement.value, measurement.interval) : 'Failed'}</td> >+ </tr>`; >+ } >+ } >+ table += '</tbody></table>'; >+ >+ return message + table; >+ } >+ >+ static _URLForAnalysisTask(analysisTask) >+ { >+ return global.RemoteAPI.url(`/v3/#/analysis/task/${analysisTask.id()}`); >+ } >+ >+ async _sendEmail(content) >+ { >+ this._emailServerRemoteAPI.postJSON(this._emailServicePath, content); >+ } >+ >+ static _instantiateNotificationTemplate(template, title, recipients, message) >+ { >+ const instance = {}; >+ for (const name in template) { >+ const value = template[name]; >+ if (typeof(value) === 'string') >+ instance[name] = value.replace(/\$title/g, title).replace(/\$message/g, message).replace(/\$recipients/g, recipients); >+ else if (typeof(template[name]) === 'object') >+ instance[name] = this._instantiateNotificationTemplate(value, title, recipients, message); >+ else >+ instance[name] = value; >+ } >+ return instance; >+ } >+} >+ >+ >+if (typeof module !== 'undefined') >+ module.exports.EmailNotifier = EmailNotifier; >\ No newline at end of file >diff --git a/Websites/perf.webkit.org/tools/js/measurement-set-analyzer.js b/Websites/perf.webkit.org/tools/js/measurement-set-analyzer.js >index 2db047a4bd148a13884dd4dffa6a4d78074de469..c93b90becf8b9d670f453f9bc3e4739ff9ccc856 100644 >--- a/Websites/perf.webkit.org/tools/js/measurement-set-analyzer.js >+++ b/Websites/perf.webkit.org/tools/js/measurement-set-analyzer.js >@@ -104,7 +104,7 @@ class MeasurementSetAnalyzer { > > // FIXME: The iteration count should be smarter than hard-coding. > const analysisTask = await AnalysisTask.create(summary, rangeWithMostSignificantChange.startPoint, >- rangeWithMostSignificantChange.endPoint, 'Confirm', 4); >+ rangeWithMostSignificantChange.endPoint, 'Confirm', 4, true); > > this._logger.info(`Created analysis task with id "${analysisTask.id()}" to confirm: "${summary}".`); > } >diff --git a/Websites/perf.webkit.org/tools/js/v3-models.js b/Websites/perf.webkit.org/tools/js/v3-models.js >index e1b249c4f174549ad0689be60e9f79b79f7ebe84..f208e848f0563350c3acb551ff8982a3c8ff4280 100644 >--- a/Websites/perf.webkit.org/tools/js/v3-models.js >+++ b/Websites/perf.webkit.org/tools/js/v3-models.js >@@ -11,6 +11,7 @@ importFromV3('models/data-model.js', 'DataModelObject'); > importFromV3('models/data-model.js', 'LabeledObject'); > > importFromV3('models/analysis-task.js', 'AnalysisTask'); >+importFromV3('models/analysis-results.js', 'AnalysisResults'); > importFromV3('models/bug.js', 'Bug'); > importFromV3('models/bug-tracker.js', 'BugTracker'); > importFromV3('models/build-request.js', 'BuildRequest'); >diff --git a/Websites/perf.webkit.org/tools/run-analysis.js b/Websites/perf.webkit.org/tools/run-analysis.js >index 081a0d104cbd4215012b8b6a3dedba899b893d9e..75bede552fd2857d0f7194ba37d10e1d2094eefd 100644 >--- a/Websites/perf.webkit.org/tools/run-analysis.js >+++ b/Websites/perf.webkit.org/tools/run-analysis.js >@@ -3,7 +3,9 @@ > const fs = require('fs'); > const parseArguments = require('./js/parse-arguments.js').parseArguments; > const RemoteAPI = require('./js/remote.js').RemoteAPI; >-const MeasurementSetAnalyzer = require('./js/measurement-set-analyzer').MeasurementSetAnalyzer; >+const MeasurementSetAnalyzer = require('./js/measurement-set-analyzer.js').MeasurementSetAnalyzer; >+const EmailNotifier = require('./js/email-notifier.js').EmailNotifier; >+const Subprocess = require('./js/subprocess.js').Subprocess; > require('./js/v3-models.js'); > global.PrivilegedAPI = require('./js/privileged-api.js').PrivilegedAPI; > >@@ -11,6 +13,7 @@ function main(argv) > { > const options = parseArguments(argv, [ > {name: '--server-config-json', required: true}, >+ {name: '--notification-config-json', required: true}, > {name: '--analysis-range-in-days', type: parseFloat, default: 10}, > {name: '--seconds-to-sleep', type: parseFloat, default: 1200}, > ]); >@@ -26,6 +29,7 @@ async function analysisLoop(options) > let secondsToSleep; > try { > const serverConfig = JSON.parse(fs.readFileSync(options['--server-config-json'], 'utf-8')); >+ const notificationConfig = JSON.parse(fs.readFileSync(options['--notification-config-json'], 'utf-8')); > const analysisRangeInDays = options['--analysis-range-in-days']; > secondsToSleep = options['--seconds-to-sleep']; > global.RemoteAPI = new RemoteAPI(serverConfig.server); >@@ -40,6 +44,12 @@ async function analysisLoop(options) > > console.log(`Start analyzing last ${analysisRangeInDays} days measurement sets.`); > await analyzer.analyzeOnce(); >+ >+ const testGroups = await TestGroup.fetchAllWithPendingNotification(); >+ >+ const notifier = new EmailNotifier(notificationConfig.findEmailCommand, notificationConfig.emailGroupConfiguration, >+ notificationConfig.emailServerConfig, notificationConfig.emailTemplate, new Subprocess); >+ await notifier.sendNotificationsForTestGroups(testGroups); > } catch(error) { > console.error(`Failed analyze measurement sets due to ${error}`); > } >diff --git a/Websites/perf.webkit.org/unit-tests/analysis-task-tests.js b/Websites/perf.webkit.org/unit-tests/analysis-task-tests.js >index 008df3f8a5b0676db0d1924a3a55b453e0934788..60a7caca67262bda7d27aa75265572d979517c24 100644 >--- a/Websites/perf.webkit.org/unit-tests/analysis-task-tests.js >+++ b/Websites/perf.webkit.org/unit-tests/analysis-task-tests.js >@@ -304,7 +304,7 @@ describe('AnalysisTask', () => { > > it('should create analysis task with confirming repetition count specified', async () => { > const [startPoint, endPoint] = mockStartAndEndPoints(); >- AnalysisTask.create('confirm', startPoint, endPoint, 'Confirm', 4); >+ AnalysisTask.create('confirm', startPoint, endPoint, 'Confirm', 4, true); > assert.equal(requests.length, 1); > assert.equal(requests[0].url, '/privileged-api/generate-csrf-token'); > requests[0].resolve({ >@@ -315,7 +315,29 @@ describe('AnalysisTask', () => { > await MockRemoteAPI.waitForRequest(); > assert.equal(requests[1].url, '/privileged-api/create-analysis-task'); > assert.equal(requests.length, 2); >- assert.deepEqual(requests[1].data, {name: 'confirm', repetitionCount: 4, >+ assert.deepEqual(requests[1].data, {name: 'confirm', repetitionCount: 4, hasPendingNotification: true, >+ startRun: 1, endRun: 2, testGroupName: 'Confirm', token: 'abc', revisionSets: [ >+ {'11': {revision: 'webkit-revision-1', ownerRevision: null, patch: null}, >+ '22': {revision: 'ios-revision-1', ownerRevision: null, patch: null}}, >+ {'11': {revision: 'webkit-revision-2', ownerRevision: null, patch: null}, >+ '22': { revision: 'ios-revision-2', ownerRevision: null, patch: null}}]} >+ ); >+ }); >+ >+ it('should create analysis task and test groups with "hasPendingNotification" set to false if specified in creation', async () => { >+ const [startPoint, endPoint] = mockStartAndEndPoints(); >+ AnalysisTask.create('confirm', startPoint, endPoint, 'Confirm', 4, false); >+ assert.equal(requests.length, 1); >+ assert.equal(requests[0].url, '/privileged-api/generate-csrf-token'); >+ requests[0].resolve({ >+ token: 'abc', >+ expiration: Date.now() + 3600 * 1000, >+ }); >+ >+ await MockRemoteAPI.waitForRequest(); >+ assert.equal(requests[1].url, '/privileged-api/create-analysis-task'); >+ assert.equal(requests.length, 2); >+ assert.deepEqual(requests[1].data, {name: 'confirm', repetitionCount: 4, hasPendingNotification: false, > startRun: 1, endRun: 2, testGroupName: 'Confirm', token: 'abc', revisionSets: [ > {'11': {revision: 'webkit-revision-1', ownerRevision: null, patch: null}, > '22': {revision: 'ios-revision-1', ownerRevision: null, patch: null}}, >@@ -326,7 +348,7 @@ describe('AnalysisTask', () => { > > it('should sync the new analysis task status once it is created', async () => { > const [startPoint, endPoint] = mockStartAndEndPoints(); >- const creatingPromise = AnalysisTask.create('confirm', startPoint, endPoint, 'Confirm', 4); >+ const creatingPromise = AnalysisTask.create('confirm', startPoint, endPoint, 'Confirm', 4, true); > assert.equal(requests.length, 1); > assert.equal(requests[0].url, '/privileged-api/generate-csrf-token'); > requests[0].resolve({ >@@ -337,7 +359,7 @@ describe('AnalysisTask', () => { > await MockRemoteAPI.waitForRequest(); > assert.equal(requests[1].url, '/privileged-api/create-analysis-task'); > assert.equal(requests.length, 2); >- assert.deepEqual(requests[1].data, {name: 'confirm', repetitionCount: 4, >+ assert.deepEqual(requests[1].data, {name: 'confirm', repetitionCount: 4, hasPendingNotification: true, > startRun: 1, endRun: 2, testGroupName: 'Confirm', token: 'abc', revisionSets: [ > {'11': {revision: 'webkit-revision-1', ownerRevision: null, patch: null}, > '22': {revision: 'ios-revision-1', ownerRevision: null, patch: null}}, >@@ -387,7 +409,7 @@ describe('AnalysisTask', () => { > > it('should return an rejected promise when analysis task creation failed', async () => { > const [startPoint, endPoint] = mockStartAndEndPoints(); >- const creatingPromise = AnalysisTask.create('confirm', startPoint, endPoint, 'Confirm', 4); >+ const creatingPromise = AnalysisTask.create('confirm', startPoint, endPoint, 'Confirm', 4, true); > assert.equal(requests.length, 1); > assert.equal(requests[0].url, '/privileged-api/generate-csrf-token'); > requests[0].resolve({ >@@ -398,7 +420,7 @@ describe('AnalysisTask', () => { > await MockRemoteAPI.waitForRequest(); > assert.equal(requests[1].url, '/privileged-api/create-analysis-task'); > assert.equal(requests.length, 2); >- assert.deepEqual(requests[1].data, {name: 'confirm', repetitionCount: 4, >+ assert.deepEqual(requests[1].data, {name: 'confirm', repetitionCount: 4, hasPendingNotification: true, > startRun: 1, endRun: 2, testGroupName: 'Confirm', token: 'abc', revisionSets: [ > {'11': {revision: 'webkit-revision-1', ownerRevision: null, patch: null}, > '22': {revision: 'ios-revision-1', ownerRevision: null, patch: null}}, >@@ -432,10 +454,10 @@ describe('AnalysisTask', () => { > > it('should create analysis task with confirming repetition count specified', () => { > const [startPoint, endPoint] = mockStartAndEndPoints(); >- AnalysisTask.create('confirm', startPoint, endPoint, 'Confirm', 4); >+ AnalysisTask.create('confirm', startPoint, endPoint, 'Confirm', 4, true); > assert.equal(requests[0].url, '/privileged-api/create-analysis-task'); > assert.equal(requests.length, 1); >- assert.deepEqual(requests[0].data, {name: 'confirm', repetitionCount: 4, >+ assert.deepEqual(requests[0].data, {name: 'confirm', repetitionCount: 4, hasPendingNotification: true, > startRun: 1, endRun: 2, slaveName: 'worker', slavePassword: 'password', > testGroupName: 'Confirm', revisionSets: [ > {'11': {revision: 'webkit-revision-1', ownerRevision: null, patch: null}, >diff --git a/Websites/perf.webkit.org/unit-tests/email-notifier-tests.js b/Websites/perf.webkit.org/unit-tests/email-notifier-tests.js >new file mode 100644 >index 0000000000000000000000000000000000000000..51619bb5c94aa991af26d2db6315db80ab57da24 >--- /dev/null >+++ b/Websites/perf.webkit.org/unit-tests/email-notifier-tests.js >@@ -0,0 +1,40 @@ >+'use strict'; >+ >+const assert = require('assert'); >+const EmailNotifier = require('../tools/js/email-notifier.js').EmailNotifier; >+ >+describe('EmailNotifier', () => { >+ describe('_buildMatchingCriteriaByEmailGroup', () => { >+ >+ const trunkMacBook = 'Trunk MacBook'; >+ const trunkMacBookPro = 'Trunk MacBook Pro'; >+ const trunkMacBookAir = 'Trunk MacBook Air'; >+ const speedometer = 'speedometer'; >+ const speedometer2 = 'speedometer-2'; >+ const jetStream = 'JetStream'; >+ >+ it('should return a group of matching function based on configuration', () => { >+ const matchingCriteriaByEmailGroup = EmailNotifier._buildMatchingFunctionByEmailGroup({ >+ 'speedometer-perf@webkit.org': {tests: [speedometer, speedometer2], platforms: [trunkMacBook, trunkMacBookPro]}, >+ 'jetstream-perf@webkit.org': {tests: [jetStream]}, >+ }); >+ let matchingFunction = matchingCriteriaByEmailGroup.get('speedometer-perf@webkit.org'); >+ assert.ok(matchingFunction(trunkMacBook, speedometer)); >+ assert.ok(matchingFunction(trunkMacBook, speedometer2)); >+ assert.ok(matchingFunction(trunkMacBookPro, speedometer)); >+ assert.ok(matchingFunction(trunkMacBookPro, speedometer2)); >+ assert.ok(!matchingFunction(trunkMacBook, jetStream)); >+ assert.ok(!matchingFunction(trunkMacBookAir, speedometer)); >+ assert.ok(!matchingFunction(trunkMacBookAir, jetStream)); >+ >+ matchingFunction = matchingCriteriaByEmailGroup.get('jetstream-perf@webkit.org'); >+ assert.ok(!matchingFunction(trunkMacBook, speedometer)); >+ assert.ok(!matchingFunction(trunkMacBook, speedometer2)); >+ assert.ok(!matchingFunction(trunkMacBookPro, speedometer)); >+ assert.ok(!matchingFunction(trunkMacBookPro, speedometer2)); >+ assert.ok(matchingFunction(trunkMacBook, jetStream)); >+ assert.ok(matchingFunction(trunkMacBookPro, jetStream)); >+ assert.ok(matchingFunction(trunkMacBookAir, jetStream)); >+ }); >+ }); >+}); >\ No newline at end of file >diff --git a/Websites/perf.webkit.org/unit-tests/measurement-set-analyzer-tests.js b/Websites/perf.webkit.org/unit-tests/measurement-set-analyzer-tests.js >index 6a89f3532440d3269378a27b80a0c3cc4c5df13c..f81e6d3755967df3f0ab895a6a50b3fe3a19567e 100644 >--- a/Websites/perf.webkit.org/unit-tests/measurement-set-analyzer-tests.js >+++ b/Websites/perf.webkit.org/unit-tests/measurement-set-analyzer-tests.js >@@ -189,6 +189,7 @@ describe('MeasurementSetAnalyzer', () => { > endRun: 6443, > repetitionCount: 4, > testGroupName: 'Confirm', >+ hasPendingNotification: true, > revisionSets: [{'11': {revision: 35, ownerRevision: null, patch: null}}, > {'11': {revision: 44, ownerRevision: null, patch: null}}] > }); >@@ -242,6 +243,7 @@ describe('MeasurementSetAnalyzer', () => { > endRun: 6443, > repetitionCount: 4, > testGroupName: 'Confirm', >+ hasPendingNotification: true, > revisionSets: [{'11': {revision: 35, ownerRevision: null, patch: null}}, > {'11': {revision: 44, ownerRevision: null, patch: null}}] > }); >@@ -386,6 +388,7 @@ describe('MeasurementSetAnalyzer', () => { > endRun: 6448, > repetitionCount: 4, > testGroupName: 'Confirm', >+ hasPendingNotification: true, > revisionSets: [{'11': {revision: 40, ownerRevision: null, patch: null}}, > {'11': {revision: 49, ownerRevision: null, patch: null}}] > }); >@@ -472,6 +475,7 @@ describe('MeasurementSetAnalyzer', () => { > endRun: 6407, > repetitionCount: 4, > testGroupName: 'Confirm', >+ hasPendingNotification: true, > revisionSets: [{'11': {revision: 3, ownerRevision: null, patch: null}}, > {'11': {revision: 8, ownerRevision: null, patch: null}}] > }); >diff --git a/Websites/perf.webkit.org/unit-tests/test-groups-tests.js b/Websites/perf.webkit.org/unit-tests/test-groups-tests.js >index de59652512c38b89923b95febfe2c5f07ead0ceb..5f4297efc9246915ea934f658461df1724ef8557 100644 >--- a/Websites/perf.webkit.org/unit-tests/test-groups-tests.js >+++ b/Websites/perf.webkit.org/unit-tests/test-groups-tests.js >@@ -3,6 +3,7 @@ > const assert = require('assert'); > require('../tools/js/v3-models.js'); > const BrowserPrivilegedAPI = require('../public/v3/privileged-api.js').PrivilegedAPI; >+const NodePrivilegedAPI = require('../tools/js/privileged-api.js').PrivilegedAPI; > const MockModels = require('./resources/mock-v3-models.js').MockModels; > const MockRemoteAPI = require('./resources/mock-remote-api.js').MockRemoteAPI; > >@@ -16,6 +17,7 @@ function sampleTestGroup() { > "author": "rniwa", > "createdAt": 1458688514000, > "hidden": false, >+ "hasPendingNotification": true, > "buildRequests": ["16985", "16986", "16987", "16988", "16989", "16990", "16991", "16992"], > "commitSets": ["4255", "4256"], > }], >@@ -137,6 +139,41 @@ describe('TestGroup', function () { > }); > }); > >+ describe('hasPendingNotification', () => { >+ const requests = MockRemoteAPI.inject('https://perf.webkit.org', NodePrivilegedAPI); >+ beforeEach(() => { >+ PrivilegedAPI.configure('test', 'password'); >+ }); >+ >+ it('should update notified author flag', async () => { >+ const fetchPromise = TestGroup.fetchForTask(1376); >+ requests[0].resolve(sampleTestGroup()); >+ let testGroups = await fetchPromise; >+ assert(testGroups.length, 1); >+ let testGroup = testGroups[0]; >+ assert.equal(testGroup.hasPendingNotification(), true); >+ >+ const updatePromise = testGroup.didSendNotification(); >+ assert.equal(requests.length, 2); >+ assert.equal(requests[1].method, 'POST'); >+ assert.equal(requests[1].url, '/privileged-api/update-test-group'); >+ assert.deepEqual(requests[1].data, {group: '2128', hasPendingNotification: false, slaveName: 'test', slavePassword: 'password'}); >+ requests[1].resolve(); >+ >+ await MockRemoteAPI.waitForRequest(); >+ assert.equal(requests.length, 3); >+ assert.equal(requests[2].method, 'GET'); >+ assert.equal(requests[2].url, '/api/test-groups/2128'); >+ const updatedTestGroup = sampleTestGroup(); >+ updatedTestGroup.testGroups[0].hasPendingNotification = false; >+ requests[2].resolve(updatedTestGroup); >+ >+ testGroups = await updatePromise; >+ testGroup = testGroups[0]; >+ assert.equal(testGroup.hasPendingNotification(), false); >+ }); >+ }); >+ > describe('_createModelsFromFetchedTestGroups', function () { > it('should create test groups', function () { > var groups = TestGroup._createModelsFromFetchedTestGroups(sampleTestGroup()); >@@ -147,6 +184,7 @@ describe('TestGroup', function () { > assert.equal(group.id(), 2128); > assert.ok(group.createdAt() instanceof Date); > assert.equal(group.isHidden(), false); >+ assert.equal(group.hasPendingNotification(), true); > assert.equal(+group.createdAt(), 1458688514000); > assert.equal(group.repetitionCount(), sampleTestGroup()['buildRequests'].length / 2); > assert.ok(group.hasPending()); >@@ -361,7 +399,7 @@ describe('TestGroup', function () { > set2.setRevisionForRepository(MockModels.webkit, '191623'); > set2.setRevisionForRepository(MockModels.sharedRepository, '80229'); > >- const promise = TestGroup.createWithTask('some-task', MockModels.somePlatform, MockModels.someTest, 'some-group', 4, [set1, set2]); >+ const promise = TestGroup.createWithTask('some-task', MockModels.somePlatform, MockModels.someTest, 'some-group', 4, [set1, set2], true); > assert.equal(requests.length, 2); > assert.equal(requests[1].url, '/privileged-api/generate-csrf-token'); > requests[1].resolve({ >@@ -371,6 +409,10 @@ describe('TestGroup', function () { > await MockRemoteAPI.waitForRequest(); > assert.equal(requests.length, 3); > assert.equal(requests[2].method, 'POST'); >+ assert.deepEqual(requests[2].data, {taskName: 'some-task', name: 'some-group', platform: 65, test: 1, >+ repetitionCount: 4, revisionSets: [{'11': {ownerRevision: null, patch: null, revision: "191622"}, >+ '16': {ownerRevision: null, patch: null, revision: "80229"}}, {'11': {ownerRevision: null, patch: null, revision: "191623"}, >+ '16': {ownerRevision: null, patch: null, revision: "80229"}}], hasPendingNotification: true, token: 'abc'}); > assert.equal(requests[2].url, '/privileged-api/create-test-group'); > requests[2].resolve({ > taskId: 123,
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Formatted Diff
|
Diff
Attachments on
bug 184340
:
337299
|
339006
|
341254
|
341502
|
341829
|
342008
|
342101