Extend create-analysis-test API to be able to create with confirming test group.
https://bugs.webkit.org/show_bug.cgi?id=184958

Reviewed by Ryosuke Niwa.

Extend create-analysis-test API to be able to create an analysis task with confirming test group.
Update create analysis task UI in chart pane to adapt this new API.
Refactored '/privileged-api/create-test-group' API to share some creating test group logic with '/privileged-api/create-analysis-task' API.
Moved the shared logic to commit-sets-helpers.php.

* public/api/analysis-tasks.php: Use 'require_once' instead of 'require'.
* public/include/commit-sets-helpers.php: Added.
(create_test_group_and_build_requests): A helper function that creates test group and build requests for a analysis
task. In long term, this should be a class to avoid passing long argument list around.
(insert_commit_sets_and_construct_configuration_list): Based on commit sets returns build and test configurations.
(insert_build_request_for_configuration): Insert build requests based on configuration.
(commit_sets_from_revision_sets): Returns commit sets from given revision set list.
* public/privileged-api/create-analysis-task.php: Added the ability to create analysis task with confirming test
groups when repetition count is specified.
* public/privileged-api/create-test-group.php: Moved shared function to commit-sets-helpers.php.
* public/v3/models/analysis-task.js:
(AnalysisTask.create): Instead of accepting run ids, it now accepts points and test group name and confirming iterations.
It will conditionally add test group information into parameter when confirming iterations is a positive number.
(AnalysisTask):
* public/v3/models/commit-set.js:
(CommitSet.revisionSetsFromCommitSets): Move 'TestGroup._revisionSetsFromCommitSets' since CommitSet class is more
appropriate place and it will be shared by both TestGroup and AnalysisTask
(CommitSet):
* public/v3/models/test-group.js:
(TestGroup.createWithTask): Adapt 'CommitSet.revisionSetsFromCommitSets'.
(TestGroup.createWithCustomConfiguration): Adapt 'CommitSet.revisionSetsFromCommitSets'.
(TestGroup.createAndRefetchTestGroups): Adapt 'CommitSet.revisionSetsFromCommitSets'.
(TestGroup._revisionSetsFromCommitSets): Deleted and moved to 'CommitSet.revisionSetsFromCommitSets'.
* public/v3/pages/chart-pane.js:
(ChartPane.prototype.didConstructShadowTree): Added the logic to disable options when checkbox for creating confirming
test group is unchecked.
(ChartPane.prototype._analyzeRange): Conditionally create confirming test group from UI.
(ChartPane.cssTemplate):
* server-tests/privileged-api-create-analysis-task-tests.js: Added unit tests. Added a unit test for 'NodePrivilegedAPI'.
* unit-tests/analysis-task-tests.js: Added unit tests.
* unit-tests/commit-set-tests.js: Added unit test for 'CommitSet.revisionSetsFromCommitSets'.
* unit-tests/resources/mock-remote-api.js: Reset csrf token when BrowserPrivilegedAPI is used.
(MockRemoteAPI.inject):

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@231087 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/Websites/perf.webkit.org/ChangeLog b/Websites/perf.webkit.org/ChangeLog
index 82e2b48..b399bcb 100644
--- a/Websites/perf.webkit.org/ChangeLog
+++ b/Websites/perf.webkit.org/ChangeLog
@@ -1,3 +1,49 @@
+2018-04-26  Dewei Zhu  <dewei_zhu@apple.com>
+
+        Extend create-analysis-test API to be able to create with confirming test group.
+        https://bugs.webkit.org/show_bug.cgi?id=184958
+
+        Reviewed by Ryosuke Niwa.
+
+        Extend create-analysis-test API to be able to create an analysis task with confirming test group.
+        Update create analysis task UI in chart pane to adapt this new API.
+        Refactored '/privileged-api/create-test-group' API to share some creating test group logic with '/privileged-api/create-analysis-task' API.
+        Moved the shared logic to commit-sets-helpers.php.
+
+        * public/api/analysis-tasks.php: Use 'require_once' instead of 'require'.
+        * public/include/commit-sets-helpers.php: Added.
+        (create_test_group_and_build_requests): A helper function that creates test group and build requests for a analysis
+        task. In long term, this should be a class to avoid passing long argument list around.
+        (insert_commit_sets_and_construct_configuration_list): Based on commit sets returns build and test configurations.
+        (insert_build_request_for_configuration): Insert build requests based on configuration.
+        (commit_sets_from_revision_sets): Returns commit sets from given revision set list.
+        * public/privileged-api/create-analysis-task.php: Added the ability to create analysis task with confirming test
+        groups when repetition count is specified.
+        * public/privileged-api/create-test-group.php: Moved shared function to commit-sets-helpers.php.
+        * public/v3/models/analysis-task.js:
+        (AnalysisTask.create): Instead of accepting run ids, it now accepts points and test group name and confirming iterations.
+        It will conditionally add test group information into parameter when confirming iterations is a positive number.
+        (AnalysisTask):
+        * public/v3/models/commit-set.js:
+        (CommitSet.revisionSetsFromCommitSets): Move 'TestGroup._revisionSetsFromCommitSets' since CommitSet class is more
+        appropriate place and it will be shared by both TestGroup and AnalysisTask
+        (CommitSet):
+        * public/v3/models/test-group.js:
+        (TestGroup.createWithTask): Adapt 'CommitSet.revisionSetsFromCommitSets'.
+        (TestGroup.createWithCustomConfiguration): Adapt 'CommitSet.revisionSetsFromCommitSets'.
+        (TestGroup.createAndRefetchTestGroups): Adapt 'CommitSet.revisionSetsFromCommitSets'.
+        (TestGroup._revisionSetsFromCommitSets): Deleted and moved to 'CommitSet.revisionSetsFromCommitSets'.
+        * public/v3/pages/chart-pane.js:
+        (ChartPane.prototype.didConstructShadowTree): Added the logic to disable options when checkbox for creating confirming
+        test group is unchecked.
+        (ChartPane.prototype._analyzeRange): Conditionally create confirming test group from UI.
+        (ChartPane.cssTemplate):
+        * server-tests/privileged-api-create-analysis-task-tests.js: Added unit tests. Added a unit test for 'NodePrivilegedAPI'.
+        * unit-tests/analysis-task-tests.js: Added unit tests.
+        * unit-tests/commit-set-tests.js: Added unit test for 'CommitSet.revisionSetsFromCommitSets'.
+        * unit-tests/resources/mock-remote-api.js: Reset csrf token when BrowserPrivilegedAPI is used.
+        (MockRemoteAPI.inject):
+
 2018-04-23  Dewei Zhu  <dewei_zhu@apple.com>
 
         Tool scripts should not use PrivilegedAPI from 'public/v3/privileged-api.js'.
diff --git a/Websites/perf.webkit.org/public/api/analysis-tasks.php b/Websites/perf.webkit.org/public/api/analysis-tasks.php
index 352f723..160cff4 100644
--- a/Websites/perf.webkit.org/public/api/analysis-tasks.php
+++ b/Websites/perf.webkit.org/public/api/analysis-tasks.php
@@ -1,7 +1,7 @@
 <?php
 
-require('../include/json-header.php');
-require('../include/commit-log-fetcher.php');
+require_once('../include/json-header.php');
+require_once('../include/commit-log-fetcher.php');
 
 function main($path) {
     $db = new Database;
diff --git a/Websites/perf.webkit.org/public/include/commit-sets-helpers.php b/Websites/perf.webkit.org/public/include/commit-sets-helpers.php
new file mode 100644
index 0000000..a7adc4f
--- /dev/null
+++ b/Websites/perf.webkit.org/public/include/commit-sets-helpers.php
@@ -0,0 +1,160 @@
+<?php
+
+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) {
+
+    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));
+
+    $build_count = count($build_configuration_list);
+    $order = -$build_count;
+    foreach($build_configuration_list as $build_configuration)
+        insert_build_request_for_configuration($db, $build_configuration, $order++, $triggerable_id, $platform_id, NULL, $group_id);
+
+    for ($i = 0; $i < $repetition_count; $i++) {
+        foreach($test_configuration_list as $test_configuration)
+            insert_build_request_for_configuration($db, $test_configuration, $order++, $triggerable_id, $platform_id, $test_id, $group_id);
+    }
+    return $group_id;
+}
+
+function insert_commit_sets_and_construct_configuration_list($db, $commit_sets)
+{
+    $repository_group_with_builds = array();
+    $test_configuration_list = array();
+    $build_configuration_list = array();
+
+    foreach ($commit_sets as $commit_list) {
+        $commit_set_id = $db->insert_row('commit_sets', 'commitset', array());
+        $need_to_build = FALSE;
+        foreach ($commit_list['set'] as $commit_row) {
+            $commit_row['set'] = $commit_set_id;
+            $requires_build =  $commit_row['requires_build'];
+            assert(is_bool($requires_build));
+            $need_to_build = $need_to_build || $requires_build;
+            $db->insert_row('commit_set_items', 'commitset', $commit_row, 'commit');
+        }
+        $repository_group = $commit_list['repository_group'];
+        if ($need_to_build)
+            $repository_group_with_builds[$repository_group] = TRUE;
+        array_push($test_configuration_list, array('commit_set' => $commit_set_id, 'repository_group' => $repository_group));
+    }
+
+    foreach ($test_configuration_list as &$config) {
+        if (array_get($repository_group_with_builds, $config['repository_group']))
+            array_push($build_configuration_list, $config);
+    }
+    return array($build_configuration_list, $test_configuration_list);
+}
+
+function insert_build_request_for_configuration($db, $configuration, $order, $triggerable_id, $platform_id, $test_id, $group_id)
+{
+    $db->insert_row('build_requests', 'request', array(
+        'triggerable' => $triggerable_id,
+        'repository_group' => $configuration['repository_group'],
+        'platform' => $platform_id,
+        'test' => $test_id,
+        'group' => $group_id,
+        'order' => $order,
+        'commit_set' => $configuration['commit_set']));
+}
+
+function commit_sets_from_revision_sets($db, $triggerable_id, $revision_set_list)
+{
+    if (count($revision_set_list) < 2)
+        exit_with_error('InvalidRevisionSets', array('revisionSets' => $revision_set_list));
+
+    $finder = new RepositoryGroupFinder($db, $triggerable_id);
+    $commit_set_list = array();
+    $repository_owner_list = array();
+    $repositories_require_build = array();
+    $commit_set_items_by_repository = array();
+    foreach ($revision_set_list as $revision_set) {
+        if (!count($revision_set))
+            exit_with_error('InvalidRevisionSets', array('revisionSets' => $revision_set_list));
+
+        $commit_set = array();
+        $repository_list = array();
+        $repository_with_patch = array();
+        foreach ($revision_set as $repository_id => $data) {
+            if ($repository_id == 'customRoots') {
+                $file_id_list = $data;
+                foreach ($file_id_list as $file_id) {
+                    if (!is_numeric($file_id) || !$db->select_first_row('uploaded_files', 'file', array('id' => $file_id)))
+                        exit_with_error('InvalidUploadedFile', array('file' => $file_id));
+                    array_push($commit_set, array('root_file' => $file_id, 'patch_file' => NULL, 'requires_build' => FALSE, 'commit_owner' => NULL));
+                }
+                continue;
+            }
+            if (!is_numeric($repository_id))
+                exit_with_error('InvalidRepository', array('repository' => $repository_id));
+
+            if (!is_array($data))
+                exit_with_error('InvalidRepositoryData', array('repository' => $repository_id, 'data' => $data));
+
+            $revision = array_get($data, 'revision');
+            if (!$revision)
+                exit_with_error('InvalidRevision', array('repository' => $repository_id, 'data' => $data));
+            $commit_id = CommitLogFetcher::find_commit_id_by_revision($db, $repository_id, $revision);
+            if ($commit_id < 0)
+                exit_with_error('AmbiguousRevision', array('repository' => $repository_id, 'revision' => $revision));
+            if (!$commit_id)
+                exit_with_error('RevisionNotFound', array('repository' => $repository_id, 'revision' => $revision));
+
+            $owner_revision = array_get($data, 'ownerRevision');
+            $patch_file_id = array_get($data, 'patch');
+            if ($patch_file_id) {
+                if (!is_numeric($patch_file_id) || !$db->select_first_row('uploaded_files', 'file', array('id' => $patch_file_id)))
+                    exit_with_error('InvalidPatchFile', array('patch' => $patch_file_id));
+                array_push($repository_with_patch, $repository_id);
+                $repositories_require_build[$repository_id] =  TRUE;
+            }
+
+            $repository = NULL;
+            $owner_commit_id = NULL;
+            if ($owner_revision) {
+                $repository = $db->select_first_row('repositories', 'repository', array('id' => intval($repository_id)));
+                if (!$repository)
+                    exit_with_error('RepositoryNotFound', array('repository' => $repository_id));
+                $owner_commit = $db->select_first_row('commits', 'commit', array('repository' => $repository['repository_owner'], 'revision' => $owner_revision));
+                if (!$owner_commit)
+                    exit_with_error('InvalidOwnerRevision', array('repository' => $repository['repository_owner'], 'revision' => $owner_revision));
+                if (!$db->select_first_row('commit_ownerships', 'commit', array('owned' => $commit_id, 'owner' => $owner_commit['commit_id'])))
+                    exit_with_error('InvalidCommitOwnership', array('commitOwner' => $owner_commit['commit_id'], 'commitOwned' => $commit_id));
+                $repositories_require_build[$repository_id] =  TRUE;
+                $owner_commit_id = $owner_commit['commit_id'];
+            }
+
+            array_push($commit_set, array('commit' => $commit_id, 'patch_file' => $patch_file_id, 'requires_build' => FALSE, 'commit_owner' => $owner_commit_id));
+
+            array_ensure_item_has_array($commit_set_items_by_repository, $repository_id);
+            $commit_set_items_by_repository[$repository_id][] = &$commit_set[count($commit_set) - 1];
+
+            if ($owner_commit_id)
+                continue;
+            array_push($repository_list, $repository_id);
+        }
+        $repository_group_id = $finder->find_by_repositories($repository_list);
+        if (!$repository_group_id)
+            exit_with_error('NoMatchingRepositoryGroup', array('repositories' => $repository_list));
+
+        foreach ($repository_with_patch as $repository_id) {
+            if (!$finder->accepts_patch($repository_group_id, $repository_id))
+                exit_with_error('PatchNotAccepted', array('repository' => $repository_id, 'repositoryGroup' => $repository_group_id));
+        }
+
+        array_push($commit_set_list, array('repository_group' => $repository_group_id, 'set' => $commit_set));
+    }
+
+    foreach (array_keys($repositories_require_build) as $repository_id) {
+        foreach($commit_set_items_by_repository[$repository_id] as &$commit_set_item)
+            $commit_set_item['requires_build'] = TRUE;
+    }
+    return $commit_set_list;
+}
+?>
\ No newline at end of file
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 408507a..940f80b 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
@@ -1,6 +1,7 @@
 <?php
 
 require_once('../include/json-header.php');
+require_once('../include/commit-sets-helpers.php');
 
 function main() {
     $db = connect();
@@ -8,6 +9,9 @@
 
     $author = remote_user_name($data);
     $name = array_get($data, 'name');
+    $repetition_count = array_get($data, 'repetitionCount');
+    $test_group_name = array_get($data, 'testGroupName');
+    $revision_set_list = array_get($data, 'revisionSets');
 
     $segmentation_name = array_get($data, 'segmentationStrategy');
     $test_range_name = array_get($data, 'testRangeStrategy');
@@ -66,6 +70,23 @@
         'end_run_time' => $end_run_time,
         'segmentation' => $segmentation_id,
         'test_range' => $test_range_id));
+
+    if ($repetition_count) {
+        $triggerable = find_triggerable_for_task($db, $task_id);
+        if (!$triggerable || !$triggerable['id']) {
+            $db->rollback_transaction();
+            exit_with_error('TriggerableNotFoundForTask', array('task' => $task_id, 'platform' => $config['config_platform']));
+        }
+        if ($triggerable['platform'] != $config['config_platform']) {
+            $db->rollback_transaction();
+            exit_with_error('InconsistentPlatform', array('configPlatform' => $config['config_platform'], 'taskPlatform' => $triggerable['platform']));
+        }
+        $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);
+    }
+
     $db->commit_transaction();
 
     exit_with_success(array('taskId' => $task_id));
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 fa55039..56b4755 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
@@ -1,8 +1,7 @@
 <?php
 
 require_once('../include/json-header.php');
-require_once('../include/commit-log-fetcher.php');
-require_once('../include/repository-group-finder.php');
+require_once('../include/commit-sets-helpers.php');
 
 function main()
 {
@@ -86,167 +85,13 @@
     if ($task_name)
         $task_id = $db->insert_row('analysis_tasks', 'task', array('name' => $task_name, 'author' => $author));
 
-    $configuration_list = array();
-    $repository_group_with_builds = array();
-    foreach ($commit_sets as $commit_list) {
-        $commit_set_id = $db->insert_row('commit_sets', 'commitset', array());
-        $need_to_build = FALSE;
-        foreach ($commit_list['set'] as $commit_row) {
-            $commit_row['set'] = $commit_set_id;
-            $requires_build =  $commit_row['requires_build'];
-            assert(is_bool($requires_build));
-            $need_to_build = $need_to_build || $requires_build;
-            $db->insert_row('commit_set_items', 'commitset', $commit_row, 'commit');
-        }
-        $repository_group = $commit_list['repository_group'];
-        if ($need_to_build)
-            $repository_group_with_builds[$repository_group] = TRUE;
-        array_push($configuration_list, array('commit_set' => $commit_set_id, 'repository_group' => $repository_group));
-    }
-
-    $build_count = 0;
-    foreach ($configuration_list as &$config_item) {
-        if (array_get($repository_group_with_builds, $config_item['repository_group'])) {
-            $config_item['need_to_build'] = TRUE;
-            $build_count++;
-        }
-    }
-
-    $group_id = $db->insert_row('analysis_test_groups', 'testgroup',
-        array('task' => $task_id, 'name' => $name, 'author' => $author));
-
-    if ($build_count) {
-        $order = -$build_count;
-        foreach ($configuration_list as $config) {
-            if (!array_get($config, 'need_to_build'))
-                continue;
-            assert($order < 0);
-            $db->insert_row('build_requests', 'request', array(
-                'triggerable' => $triggerable_id,
-                'repository_group' => $config['repository_group'],
-                'platform' => $platform_id,
-                'test' => NULL,
-                'group' => $group_id,
-                'order' => $order,
-                'commit_set' => $config['commit_set']));
-            $order++;
-        }
-    }
-
-    $order = 0;
-    for ($i = 0; $i < $repetition_count; $i++) {
-        foreach ($configuration_list as $config) {
-            $db->insert_row('build_requests', 'request', array(
-                'triggerable' => $triggerable_id,
-                'repository_group' => $config['repository_group'],
-                'platform' => $platform_id,
-                'test' => $test_id,
-                'group' => $group_id,
-                'order' => $order,
-                'commit_set' => $config['commit_set']));
-            $order++;
-        }
-    }
+    $group_id = create_test_group_and_build_requests($db, $commit_sets, $task_id, $name, $author, $triggerable_id, $platform_id, $test_id, $repetition_count);
 
     $db->commit_transaction();
 
     exit_with_success(array('taskId' => $task_id, 'testGroupId' => $group_id));
 }
 
-function commit_sets_from_revision_sets($db, $triggerable_id, $revision_set_list)
-{
-    if (count($revision_set_list) < 2)
-        exit_with_error('InvalidRevisionSets', array('revisionSets' => $revision_set_list));
-
-    $finder = new RepositoryGroupFinder($db, $triggerable_id);
-    $commit_set_list = array();
-    $repository_owner_list = array();
-    $repositories_require_build = array();
-    $commit_set_items_by_repository = array();
-    foreach ($revision_set_list as $revision_set) {
-        if (!count($revision_set))
-            exit_with_error('InvalidRevisionSets', array('revisionSets' => $revision_set_list));
-
-        $commit_set = array();
-        $repository_list = array();
-        $repository_with_patch = array();
-        foreach ($revision_set as $repository_id => $data) {
-            if ($repository_id == 'customRoots') {
-                $file_id_list = $data;
-                foreach ($file_id_list as $file_id) {
-                    if (!is_numeric($file_id) || !$db->select_first_row('uploaded_files', 'file', array('id' => $file_id)))
-                        exit_with_error('InvalidUploadedFile', array('file' => $file_id));
-                    array_push($commit_set, array('root_file' => $file_id, 'patch_file' => NULL, 'requires_build' => FALSE, 'commit_owner' => NULL));
-                }
-                continue;
-            }
-            if (!is_numeric($repository_id))
-                exit_with_error('InvalidRepository', array('repository' => $repository_id));
-
-            if (!is_array($data))
-                exit_with_error('InvalidRepositoryData', array('repository' => $repository_id, 'data' => $data));
-
-            $revision = array_get($data, 'revision');
-            if (!$revision)
-                exit_with_error('InvalidRevision', array('repository' => $repository_id, 'data' => $data));
-            $commit_id = CommitLogFetcher::find_commit_id_by_revision($db, $repository_id, $revision);
-            if ($commit_id < 0)
-                exit_with_error('AmbiguousRevision', array('repository' => $repository_id, 'revision' => $revision));
-            if (!$commit_id)
-                exit_with_error('RevisionNotFound', array('repository' => $repository_id, 'revision' => $revision));
-
-            $owner_revision = array_get($data, 'ownerRevision');
-            $patch_file_id = array_get($data, 'patch');
-            if ($patch_file_id) {
-                if (!is_numeric($patch_file_id) || !$db->select_first_row('uploaded_files', 'file', array('id' => $patch_file_id)))
-                    exit_with_error('InvalidPatchFile', array('patch' => $patch_file_id));
-                array_push($repository_with_patch, $repository_id);
-                $repositories_require_build[$repository_id] =  TRUE;
-            }
-
-            $repository = NULL;
-            $owner_commit_id = NULL;
-            if ($owner_revision) {
-                $repository = $db->select_first_row('repositories', 'repository', array('id' => intval($repository_id)));
-                if (!$repository)
-                    exit_with_error('RepositoryNotFound', array('repository' => $repository_id));
-                $owner_commit = $db->select_first_row('commits', 'commit', array('repository' => $repository['repository_owner'], 'revision' => $owner_revision));
-                if (!$owner_commit)
-                    exit_with_error('InvalidOwnerRevision', array('repository' => $repository['repository_owner'], 'revision' => $owner_revision));
-                if (!$db->select_first_row('commit_ownerships', 'commit', array('owned' => $commit_id, 'owner' => $owner_commit['commit_id'])))
-                    exit_with_error('InvalidCommitOwnership', array('commitOwner' => $owner_commit['commit_id'], 'commitOwned' => $commit_id));
-                $repositories_require_build[$repository_id] =  TRUE;
-                $owner_commit_id = $owner_commit['commit_id'];
-            }
-
-            array_push($commit_set, array('commit' => $commit_id, 'patch_file' => $patch_file_id, 'requires_build' => FALSE, 'commit_owner' => $owner_commit_id));
-
-            array_ensure_item_has_array($commit_set_items_by_repository, $repository_id);
-            $commit_set_items_by_repository[$repository_id][] = &$commit_set[count($commit_set) - 1];
-
-            if ($owner_commit_id)
-                continue;
-            array_push($repository_list, $repository_id);
-        }
-        $repository_group_id = $finder->find_by_repositories($repository_list);
-        if (!$repository_group_id)
-            exit_with_error('NoMatchingRepositoryGroup', array('repositories' => $repository_list));
-
-        foreach ($repository_with_patch as $repository_id) {
-            if (!$finder->accepts_patch($repository_group_id, $repository_id))
-                exit_with_error('PatchNotAccepted', array('repository' => $repository_id, 'repositoryGroup' => $repository_group_id));
-        }
-
-        array_push($commit_set_list, array('repository_group' => $repository_group_id, 'set' => $commit_set));
-    }
-
-    foreach (array_keys($repositories_require_build) as $repository_id) {
-        foreach($commit_set_items_by_repository[$repository_id] as &$commit_set_item)
-            $commit_set_item['requires_build'] = TRUE;
-    }
-    return $commit_set_list;
-}
-
 function ensure_commit_sets($db, $triggerable_id, $commit_sets_info) {
     $repository_name_to_id = array();
     foreach ($db->select_rows('repositories', 'repository', array('owner' => NULL)) as $row)
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 0179be1..83cc3cd 100644
--- a/Websites/perf.webkit.org/public/v3/models/analysis-task.js
+++ b/Websites/perf.webkit.org/public/v3/models/analysis-task.js
@@ -303,13 +303,16 @@
         return results;
     }
 
-    static create(name, startRunId, endRunId)
+    static create(name, startPoint, endPoint, testGroupName=null, repetitionCount=0)
     {
-        return PrivilegedAPI.sendRequest('create-analysis-task', {
-            name: name,
-            startRun: startRunId,
-            endRun: endRunId,
-        });
+        const parameters = {name, startRun: startPoint.id, endRun: endPoint.id};
+        if (testGroupName) {
+            console.assert(repetitionCount);
+            parameters['revisionSets'] = CommitSet.revisionSetsFromCommitSets([startPoint.commitSet(), endPoint.commitSet()]);
+            parameters['repetitionCount'] = repetitionCount;
+            parameters['testGroupName'] = testGroupName;
+        }
+        return PrivilegedAPI.sendRequest('create-analysis-task', parameters);
     }
 }
 
diff --git a/Websites/perf.webkit.org/public/v3/models/commit-set.js b/Websites/perf.webkit.org/public/v3/models/commit-set.js
index 8112d56..29a9825 100644
--- a/Websites/perf.webkit.org/public/v3/models/commit-set.js
+++ b/Websites/perf.webkit.org/public/v3/models/commit-set.js
@@ -242,6 +242,26 @@
 
         return nameParts.join(' ');
     }
+
+    static revisionSetsFromCommitSets(commitSets)
+    {
+        return commitSets.map((commitSet) => {
+            console.assert(commitSet instanceof CustomCommitSet || commitSet instanceof CommitSet);
+            const revisionSet = {};
+            for (let repository of commitSet.repositories()) {
+                const patchFile = commitSet.patchForRepository(repository);
+                revisionSet[repository.id()] = {
+                    revision: commitSet.revisionForRepository(repository),
+                    ownerRevision: commitSet.ownerRevisionForRepository(repository),
+                    patch: patchFile ? patchFile.id() : null,
+                };
+            }
+            const customRoots = commitSet.customRoots();
+            if (customRoots && customRoots.length)
+                revisionSet['customRoots'] = customRoots.map((uploadedFile) => uploadedFile.id());
+            return revisionSet;
+        });
+    }
 }
 
 class MeasurementCommitSet extends CommitSet {
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 e821670..e09f142 100644
--- a/Websites/perf.webkit.org/public/v3/models/test-group.js
+++ b/Websites/perf.webkit.org/public/v3/models/test-group.js
@@ -188,7 +188,7 @@
     static createWithTask(taskName, platform, test, groupName, repetitionCount, commitSets)
     {
         console.assert(commitSets.length == 2);
-        const revisionSets = this._revisionSetsFromCommitSets(commitSets);
+        const revisionSets = CommitSet.revisionSetsFromCommitSets(commitSets);
         const params = {taskName, name: groupName, platform: platform.id(), test: test.id(), repetitionCount, revisionSets};
         return PrivilegedAPI.sendRequest('create-test-group', params).then((data) => {
             return AnalysisTask.fetchById(data['taskId']);
@@ -200,7 +200,7 @@
     static createWithCustomConfiguration(task, platform, test, groupName, repetitionCount, commitSets)
     {
         console.assert(commitSets.length == 2);
-        const revisionSets = this._revisionSetsFromCommitSets(commitSets);
+        const revisionSets = CommitSet.revisionSetsFromCommitSets(commitSets);
         const params = {task: task.id(), name: groupName, platform: platform.id(), test: test.id(), repetitionCount, revisionSets};
         return PrivilegedAPI.sendRequest('create-test-group', params).then((data) => {
             return this.fetchForTask(data['taskId'], true);
@@ -210,7 +210,7 @@
     static createAndRefetchTestGroups(task, name, repetitionCount, commitSets)
     {
         console.assert(commitSets.length == 2);
-        const revisionSets = this._revisionSetsFromCommitSets(commitSets);
+        const revisionSets = CommitSet.revisionSetsFromCommitSets(commitSets);
         return PrivilegedAPI.sendRequest('create-test-group', {
             task: task.id(),
             name: name,
@@ -219,26 +219,6 @@
         }).then((data) => this.fetchForTask(data['taskId'], true));
     }
 
-    static _revisionSetsFromCommitSets(commitSets)
-    {
-        return commitSets.map((commitSet) => {
-            console.assert(commitSet instanceof CustomCommitSet || commitSet instanceof CommitSet);
-            const revisionSet = {};
-            for (let repository of commitSet.repositories()) {
-                const patchFile = commitSet.patchForRepository(repository);
-                revisionSet[repository.id()] = {
-                    revision: commitSet.revisionForRepository(repository),
-                    ownerRevision: commitSet.ownerRevisionForRepository(repository),
-                    patch: patchFile ? patchFile.id() : null,
-                };
-            }
-            const customRoots = commitSet.customRoots();
-            if (customRoots && customRoots.length)
-                revisionSet['customRoots'] = customRoots.map((uploadedFile) => uploadedFile.id());
-            return revisionSet;
-        });
-    }
-
     static findAllByTask(taskId)
     {
         return TestGroup.all().filter((testGroup) => testGroup._taskId == taskId);
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 f32bdec..5fc172f 100644
--- a/Websites/perf.webkit.org/public/v3/pages/chart-pane.js
+++ b/Websites/perf.webkit.org/public/v3/pages/chart-pane.js
@@ -116,7 +116,10 @@
     {
         this.part('close').listenToAction('activate', () => {
             this._chartsPage.closePane(this);
-        })
+        });
+        const createWithTestGroupCheckbox = this.content('create-with-test-group');
+        const repetitionCount = this.content('confirm-repetition');
+        createWithTestGroupCheckbox.onchange = () => repetitionCount.disabled = !createWithTestGroupCheckbox.checked;
     }
 
     serializeState()
@@ -226,19 +229,23 @@
         super._indicatorDidChange(indicatorID, isLocked);
     }
 
-    _analyzeRange(startPoint, endPoint)
+    async _analyzeRange(startPoint, endPoint)
     {
         const router = this._chartsPage.router();
         const newWindow = window.open(router.url('analysis/task/create', {inProgress: true}), '_blank');
 
-        const analyzePopover = this.content().querySelector('.chart-pane-analyze-popover');
-        const name = analyzePopover.querySelector('input').value;
-        AnalysisTask.create(name, startPoint.id, endPoint.id).then((data) => {
+        const name = this.content('task-name').value;
+        const createWithTestGroup = this.content('create-with-test-group').checked;
+        const repetitionCount = this.content('confirm-repetition').value;
+
+        try {
+            const data = await (createWithTestGroup ?
+                AnalysisTask.create(name, startPoint, endPoint, 'Confirm', repetitionCount) : AnalysisTask.create(name, startPoint, endPoint));
             newWindow.location.href = router.url('analysis/task/' + data['taskId']);
             this.fetchAnalysisTasks(true);
-        }, (error) => {
+        } catch(error) {
             newWindow.location.href = router.url('analysis/task/create', {error: error});
-        });
+        }
     }
 
     _markAsOutlier(markAsOutlier, points)
@@ -558,8 +565,25 @@
                     <ul class="chart-pane-action-buttons buttoned-toolbar"></ul>
                     <ul class="chart-pane-alternative-platforms popover" style="display:none"></ul>
                     <form class="chart-pane-analyze-popover popover" style="display:none">
-                        <input type="text" required>
+                        <input type="text" id="task-name" required>
                         <button>Create</button>
+                        <li>
+                            <label><input type="checkbox" id="create-with-test-group" checked></label>
+                            <label>Confirm with</label>
+                                <select id="confirm-repetition">
+                                    <option>1</option>
+                                    <option>2</option>
+                                    <option>3</option>
+                                    <option selected>4</option>
+                                    <option>5</option>
+                                    <option>6</option>
+                                    <option>7</option>
+                                    <option>8</option>
+                                    <option>9</option>
+                                    <option>10</option>
+                                </select>
+                            <label>iterations</label>
+                        </li>
                     </form>
                     <ul class="chart-pane-filtering-options popover" style="display:none">
                         <li><label><input type="checkbox" class="enable-sampling">Sampling</label></li>
@@ -619,7 +643,7 @@
                 padding: 0 0;
             }
 
-            .chart-pane-actions ul {
+            .chart-pane-actions ul, form {
                 display: block;
                 padding: 0;
                 margin: 0 0.5rem;
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 eb60d7b..c7a6d17 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
@@ -105,7 +105,7 @@
         },
     }}];
 
-describe('/privileged-api/create-analysis-task', function () {
+describe('/privileged-api/create-analysis-task with browser privileged api', function () {
     prepareServerTest(this);
 
     it('should return "MissingName" on an empty request', () => {
@@ -334,4 +334,283 @@
         });
     });
 
+    it('should failed with "TriggerableNotFoundForTask" when there is no matching triggerable', async () => {
+        const db = TestServer.database();
+        await addBuilderForReport(reportWithRevision[0]);
+        await TestServer.remoteAPI().postJSON('/api/report/', reportWithRevision);
+        await TestServer.remoteAPI().postJSON('/api/report/', anotherReportWithRevision);
+        await Manifest.fetch();
+
+        const test1 = Test.findByPath(['Suite', 'test1']);
+        const somePlatform = Platform.findByName('some platform');
+        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 webkitRepositoryRow = await db.selectFirstRow('repositories', {name: 'WebKit'});
+        const webkitId = webkitRepositoryRow.id;
+
+        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);
+    });
+
+    it('should create an analysis task with no test group when repetition count is 0', async () => {
+        const db = TestServer.database();
+        await addBuilderForReport(reportWithRevision[0]);
+        await TestServer.remoteAPI().postJSON('/api/report/', reportWithRevision);
+        await TestServer.remoteAPI().postJSON('/api/report/', anotherReportWithRevision);
+        await Manifest.fetch();
+
+        const test1 = Test.findByPath(['Suite', 'test1']);
+        const platform = Platform.findByName('some platform');
+        const configRow = await db.selectFirstRow('test_configurations', {metric: test1.metrics()[0].id(), platform: platform.id()});
+        const testRuns = await db.selectRows('test_runs', {config: configRow['id']});
+        assert.equal(testRuns.length, 2);
+
+        const webkitRepositoryRow = await db.selectFirstRow('repositories', {name: 'WebKit'});
+        const webkitId = webkitRepositoryRow.id;
+
+        const oneRevisionSet = {[webkitId]: {revision: '191622'}};
+        const anotherRevisionSet = {[webkitId]: {revision: '191623'}};
+
+        const content = await PrivilegedAPI.sendRequest('create-analysis-task', {name: 'confirm', repetitionCount: 0,
+            revisionSets: [oneRevisionSet, anotherRevisionSet],
+            startRun: testRuns[0]['id'], endRun: testRuns[1]['id']});
+
+        TestServer.cleanDataDirectory();
+        await Manifest.fetch();
+
+        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, 0);
+    });
+
+    it('should create an analysis task with test group when commit set list and a positive repetition count is specified', 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']});
+
+        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');
+        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');
+    });
+});
+
+describe('/privileged-api/create-analysis-task with node privileged api', function () {
+    prepareServerTest(this, 'node');
+    beforeEach(() => {
+        PrivilegedAPI.configure('test', 'password');
+    });
+
+    it('should return "SlaveNotFound" when incorrect slave user and password combination is provided and no analysis task, test group or build request should be created', async () => {
+        PrivilegedAPI.configure('test', 'wrongpassword');
+        const db = TestServer.database();
+        await addBuilderForReport(reportWithRevision[0]);
+        await TestServer.remoteAPI().postJSON('/api/report/', reportWithRevision);
+        await TestServer.remoteAPI().postJSON('/api/report/', anotherReportWithRevision);
+        await Manifest.fetch();
+
+        const test1 = Test.findByPath(['Suite', 'test1']);
+        const somePlatform = Platform.findByName('some platform');
+        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 webkitRepositoryRow = await db.selectFirstRow('repositories', {name: 'WebKit'});
+        const webkitId = webkitRepositoryRow.id;
+
+        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, 'SlaveNotFound');
+            raiseException = true;
+        }
+        assert.ok(raiseException);
+
+        const allAnalysisTasks = await db.selectRows('analysis_tasks');
+        assert.ok(!allAnalysisTasks.length);
+
+        const allTestGroups = await db.selectRows('analysis_test_groups');
+        assert.ok(!allTestGroups.length);
+
+        const allBuildRequests = await db.selectRows('build_requests');
+        assert.ok(!allBuildRequests.length);
+    });
+
+    it('should create an analysis task with test group when commit set list and a positive repetition count is specified', 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']});
+
+        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');
+        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');
+    });
 });
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 d696f68..5d499ab 100644
--- a/Websites/perf.webkit.org/unit-tests/analysis-task-tests.js
+++ b/Websites/perf.webkit.org/unit-tests/analysis-task-tests.js
@@ -121,9 +121,15 @@
 
 describe('AnalysisTask', () => {
     MockModels.inject();
-    let requests = MockRemoteAPI.inject();
+    function makeMockPoints(id, commitSet) {
+        return {
+            id,
+            commitSet: () => commitSet
+        }
+    }
 
     describe('fetchAll', () => {
+        const requests = MockRemoteAPI.inject();
         it('should request all analysis tasks', () => {
             let callCount = 0;
             AnalysisTask.fetchAll().then(() => { callCount++; });
@@ -224,4 +230,89 @@
             });
         });
     });
+
+
+    function mockStartAndEndPoints() {
+        const startPoint = makeMockPoints(1, new MeasurementCommitSet(1, [
+            [1, MockModels.ios.id(), 'ios-revision-1', null, 0],
+            [3, MockModels.webkit.id(), 'webkit-revision-1', null, 0]
+        ]));
+        const endPoint = makeMockPoints(2, new MeasurementCommitSet(2, [
+            [2, MockModels.ios.id(), 'ios-revision-2', null, 0],
+            [4, MockModels.webkit.id(), 'webkit-revision-2', null, 0]
+        ]));
+        return [startPoint, endPoint];
+    }
+
+    describe('create with browser privilege api', () => {
+        const requests = MockRemoteAPI.inject();
+
+        it('should create analysis task with confirming repetition count zero as default with browser privilege api', async () => {
+            const [startPoint, endPoint] = mockStartAndEndPoints();
+            AnalysisTask.create('confirm', startPoint, endPoint);
+            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', startRun: 1, endRun: 2, token: 'abc'});
+        });
+
+        it('should create analysis task with confirming repetition count specified', async () => {
+            const [startPoint, endPoint] = mockStartAndEndPoints();
+            AnalysisTask.create('confirm', startPoint, endPoint, 'Confirm', 4);
+            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,
+                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}}]}
+            );
+        });
+    });
+
+    describe('create with node privilege api', () => {
+        const requests = MockRemoteAPI.inject(null, 'node');
+        beforeEach(() => {
+            PrivilegedAPI.configure('worker', 'password');
+        });
+
+        it('should create analysis task with confirming repetition count zero as default with browser privilege api', () => {
+            const [startPoint, endPoint] = mockStartAndEndPoints();
+            AnalysisTask.create('confirm', startPoint, endPoint);
+            assert.equal(requests[0].url, '/privileged-api/create-analysis-task');
+            assert.equal(requests.length, 1);
+            assert.deepEqual(requests[0].data, {name: 'confirm', startRun: 1, endRun: 2, slaveName: 'worker', slavePassword: 'password'});
+        });
+
+        it('should create analysis task with confirming repetition count specified', () => {
+            const [startPoint, endPoint] = mockStartAndEndPoints();
+            AnalysisTask.create('confirm', startPoint, endPoint, 'Confirm', 4);
+            assert.equal(requests[0].url, '/privileged-api/create-analysis-task');
+            assert.equal(requests.length, 1);
+            assert.deepEqual(requests[0].data, {name: 'confirm', repetitionCount: 4,
+                startRun: 1, endRun: 2, slaveName: 'worker', slavePassword: 'password',
+                testGroupName: 'Confirm', 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}}]}
+            );
+        });
+    });
 });
diff --git a/Websites/perf.webkit.org/unit-tests/commit-set-tests.js b/Websites/perf.webkit.org/unit-tests/commit-set-tests.js
index 646deed..a8f345f 100644
--- a/Websites/perf.webkit.org/unit-tests/commit-set-tests.js
+++ b/Websites/perf.webkit.org/unit-tests/commit-set-tests.js
@@ -387,6 +387,15 @@
             assert.equal(CommitSet.diff(oneCommitSet(), commitSetWithAnotherCommitPatchAndRoot()), 'WebKit: webkit-commit-0 with none - webkit-commit-1 with patch.dat Roots: none - root.dat, root.dat (2)');
         });
     });
+
+    describe('revisionSetsFromCommitSets', () => {
+        it('should create revision sets from commit sets', () => {
+            assert.deepEqual(CommitSet.revisionSetsFromCommitSets([oneCommitSet(), commitSetWithRoot(), commitSetWithTwoRoots()]),
+                [{'11': { revision: 'webkit-commit-0', ownerRevision: null, patch: null}},
+                    {'11': { revision: 'webkit-commit-0', ownerRevision: null, patch: null}, customRoots: [456]},
+                    {'11': { revision: 'webkit-commit-0', ownerRevision: null, patch: null}, customRoots: [456, 458]}]);
+        });
+    });
 });
 
 describe('IntermediateCommitSet', () => {
diff --git a/Websites/perf.webkit.org/unit-tests/resources/mock-remote-api.js b/Websites/perf.webkit.org/unit-tests/resources/mock-remote-api.js
index dc12782..6bc10fb 100644
--- a/Websites/perf.webkit.org/unit-tests/resources/mock-remote-api.js
+++ b/Websites/perf.webkit.org/unit-tests/resources/mock-remote-api.js
@@ -65,7 +65,8 @@
         console.assert(privilegedAPIType === 'browser' || privilegedAPIType === 'node');
         let originalRemoteAPI = global.RemoteAPI;
         let originalPrivilegedAPI = global.PrivilegedAPI;
-        const PrivilegedAPI = privilegedAPIType === 'node' ? NodePrivilegedAPI: BrowserPrivilegedAPI;
+        const useNodePrivilegedAPI = privilegedAPIType === 'node';
+        const PrivilegedAPI = useNodePrivilegedAPI ? NodePrivilegedAPI: BrowserPrivilegedAPI;
 
         beforeEach(() => {
             MockRemoteAPI.reset(urlPrefix);
@@ -73,6 +74,8 @@
             global.RemoteAPI = MockRemoteAPI;
             originalPrivilegedAPI = global.PrivilegedAPI;
             global.PrivilegedAPI = PrivilegedAPI;
+            if (!useNodePrivilegedAPI)
+                PrivilegedAPI._token = null;
         });
 
         afterEach(() => {