Add the support for creating a custom test group in the analysis task page
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 21 Apr 2017 21:08:36 +0000 (21:08 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 21 Apr 2017 21:08:36 +0000 (21:08 +0000)
Make it possible to create more custom test groups in the analysis task page
https://bugs.webkit.org/show_bug.cgi?id=171138

Rubber-stamped by Chris Dumez.

Extracted CustomConfigurationTestGroupForm out of CreateAnalysisTaskPage and added it to AnalysisTaskPage inside
AnalysisTaskConfiguratorPane. This allows configuration of a new test group within a custom analysis task.

* public/privileged-api/create-test-group.php:
(main): Fixed the bug that the triggerable wasn't resolved when creating a test group in a custom analysis task.

* public/v3/components/custom-analysis-task-configurator.js:
(CustomAnalysisTaskConfigurator.prototype.selectTests): Added. Used by CustomConfigurationTestGroupForm's
setConfigurations.
(CustomAnalysisTaskConfigurator.prototype.selectPlatform): Ditto.
(CustomAnalysisTaskConfigurator.prototype.setCommitSets): Ditto.
(CustomAnalysisTaskConfigurator.prototype._setUploadedFilesIfEmpty): Added.
(CustomAnalysisTaskConfigurator.prototype._revisionMapFromCommitSet): Added.
(CustomAnalysisTaskConfigurator.prototype.render): Update the currently selected platforms and tests now that
they can be set externally via selectTests and selectPlatform.
(CustomAnalysisTaskConfigurator.prototype._renderTriggerableTests): Return the result of _renderRadioButtonList
so that the caller can update the currently selected tests without having to reconstruct the list.
(CustomAnalysisTaskConfigurator.prototype._renderTriggerablePlatforms): Ditto.
(CustomAnalysisTaskConfigurator.prototype._renderRadioButtonList): Renamed from _buildCheckboxList. Now returns
a function which updates the currently selected items. We still pretend that multiple items can be selected to
make it future-proof.

* public/v3/components/custom-configuration-test-group-form.js: Added.
(CustomConfigurationTestGroupForm): Added. Inherits from TestGroupForm. Extracted from CreateAnalysisTaskPage.
(CustomConfigurationTestGroupForm.prototype.setHasTask): Added.
(CustomConfigurationTestGroupForm.prototype.hasCommitSets): Added.
(CustomConfigurationTestGroupForm.prototype.setConfigurations): Added. Used by AnalysisTaskConfiguratorPane to
set the default configuration to what the latest test group used.
(CustomConfigurationTestGroupForm.prototype.startTesting): Added. Dispatches "startTesting" action with
platform, test, taskName in addition to what CustomizedTestGroupForm emits.
(CustomConfigurationTestGroupForm.prototype.didConstructShadowTree): Added.
(CustomConfigurationTestGroupForm.prototype.render): Added.
(CustomConfigurationTestGroupForm.prototype._updateTestGroupName): Added.
(CustomConfigurationTestGroupForm.cssTemplate): Added.
(CustomConfigurationTestGroupForm.htmlTemplate): Added.

* public/v3/components/test-group-form.js:
(TestGroupForm.cssTemplate): Make the form display: block.

* public/v3/index.html:

* public/v3/models/test-group.js:
(TestGroup.prototype.test): Added.
(TestGroup.prototype.platform): Added.
(TestGroup.createWithCustomConfiguration): Added. Creates a custom test group with an existing analysis task.

* public/v3/models/uploaded-file.js:
(UploadedFile): Fixed a bug that _deletedAt was set to a Date object even when object.deletedAt is null.

* public/v3/pages/analysis-task-page.js:
(AnalysisTaskConfiguratorPane): Added.
(AnalysisTaskConfiguratorPane.prototype.didConstructShadowTree): Added. Dispatch createCustomTestGroup action
in turn when receiving startTesting from CustomConfigurationTestGroupForm.
(AnalysisTaskConfiguratorPane.prototype.setTestGroups): Added.
(AnalysisTaskConfiguratorPane.prototype.render): Added.
(AnalysisTaskConfiguratorPane.htmlTemplate): Added. We override this instead of formContent to display the
"Start" button at the end instead of at the beginnning.
(AnalysisTaskConfiguratorPane.cssTemplate): Added.
(AnalysisTaskPage.prototype.didConstructShadowTree): Listen to createCustomTestGroup.
(AnalysisTaskPage.prototype.render): Hide AnalysisTaskConfiguratorPane when the analysis task is not custom.
(AnalysisTaskPage.prototype._showTestGroup): Let AnalysisTaskConfiguratorPane know of the current test group
so that it can update the default configuration if the user hasn't modified yet.
(AnalysisTaskPage.prototype._createCustomTestGroup): Added.

* public/v3/pages/create-analysis-task-page.js:
(CreateAnalysisTaskPage.prototype.didConstructShadowTree):
(CreateAnalysisTaskPage.prototype._createAnalysisTaskWithGroup):
(CreateAnalysisTaskPage.prototype.render):
(CreateAnalysisTaskPage.prototype._renderMessage):
(CreateAnalysisTaskPage.htmlTemplate):
(CreateAnalysisTaskPage.cssTemplate):

* server-tests/privileged-api-create-test-group-tests.js: Added a test case for creating a custom test group for
an existing analysis task.

* server-tests/resources/mock-data.js:
(MockData.otherPlatformId): Added.
(MockData.addMockData): Added a test configuration for otherPlatformId.

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

12 files changed:
Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/public/privileged-api/create-test-group.php
Websites/perf.webkit.org/public/v3/components/custom-analysis-task-configurator.js
Websites/perf.webkit.org/public/v3/components/custom-configuration-test-group-form.js [new file with mode: 0644]
Websites/perf.webkit.org/public/v3/components/test-group-form.js
Websites/perf.webkit.org/public/v3/index.html
Websites/perf.webkit.org/public/v3/models/test-group.js
Websites/perf.webkit.org/public/v3/models/uploaded-file.js
Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js
Websites/perf.webkit.org/public/v3/pages/create-analysis-task-page.js
Websites/perf.webkit.org/server-tests/privileged-api-create-test-group-tests.js
Websites/perf.webkit.org/server-tests/resources/mock-data.js

index a628357..45050d2 100644 (file)
@@ -1,5 +1,93 @@
 2017-04-21  Ryosuke Niwa  <rniwa@webkit.org>
 
+        Add the support for creating a custom test group in the analysis task page
+
+        Make it possible to create more custom test groups in the analysis task page
+        https://bugs.webkit.org/show_bug.cgi?id=171138
+
+        Rubber-stamped by Chris Dumez.
+
+        Extracted CustomConfigurationTestGroupForm out of CreateAnalysisTaskPage and added it to AnalysisTaskPage inside
+        AnalysisTaskConfiguratorPane. This allows configuration of a new test group within a custom analysis task.
+
+        * public/privileged-api/create-test-group.php:
+        (main): Fixed the bug that the triggerable wasn't resolved when creating a test group in a custom analysis task.
+
+        * public/v3/components/custom-analysis-task-configurator.js:
+        (CustomAnalysisTaskConfigurator.prototype.selectTests): Added. Used by CustomConfigurationTestGroupForm's
+        setConfigurations.
+        (CustomAnalysisTaskConfigurator.prototype.selectPlatform): Ditto.
+        (CustomAnalysisTaskConfigurator.prototype.setCommitSets): Ditto. 
+        (CustomAnalysisTaskConfigurator.prototype._setUploadedFilesIfEmpty): Added.
+        (CustomAnalysisTaskConfigurator.prototype._revisionMapFromCommitSet): Added.
+        (CustomAnalysisTaskConfigurator.prototype.render): Update the currently selected platforms and tests now that
+        they can be set externally via selectTests and selectPlatform.
+        (CustomAnalysisTaskConfigurator.prototype._renderTriggerableTests): Return the result of _renderRadioButtonList
+        so that the caller can update the currently selected tests without having to reconstruct the list.
+        (CustomAnalysisTaskConfigurator.prototype._renderTriggerablePlatforms): Ditto.
+        (CustomAnalysisTaskConfigurator.prototype._renderRadioButtonList): Renamed from _buildCheckboxList. Now returns
+        a function which updates the currently selected items. We still pretend that multiple items can be selected to
+        make it future-proof.
+
+        * public/v3/components/custom-configuration-test-group-form.js: Added.
+        (CustomConfigurationTestGroupForm): Added. Inherits from TestGroupForm. Extracted from CreateAnalysisTaskPage.
+        (CustomConfigurationTestGroupForm.prototype.setHasTask): Added.
+        (CustomConfigurationTestGroupForm.prototype.hasCommitSets): Added.
+        (CustomConfigurationTestGroupForm.prototype.setConfigurations): Added. Used by AnalysisTaskConfiguratorPane to
+        set the default configuration to what the latest test group used.
+        (CustomConfigurationTestGroupForm.prototype.startTesting): Added. Dispatches "startTesting" action with
+        platform, test, taskName in addition to what CustomizedTestGroupForm emits.
+        (CustomConfigurationTestGroupForm.prototype.didConstructShadowTree): Added.
+        (CustomConfigurationTestGroupForm.prototype.render): Added.
+        (CustomConfigurationTestGroupForm.prototype._updateTestGroupName): Added.
+        (CustomConfigurationTestGroupForm.cssTemplate): Added.
+        (CustomConfigurationTestGroupForm.htmlTemplate): Added.
+
+        * public/v3/components/test-group-form.js:
+        (TestGroupForm.cssTemplate): Make the form display: block.
+
+        * public/v3/index.html:
+
+        * public/v3/models/test-group.js:
+        (TestGroup.prototype.test): Added.
+        (TestGroup.prototype.platform): Added.
+        (TestGroup.createWithCustomConfiguration): Added. Creates a custom test group with an existing analysis task.
+
+        * public/v3/models/uploaded-file.js:
+        (UploadedFile): Fixed a bug that _deletedAt was set to a Date object even when object.deletedAt is null.
+
+        * public/v3/pages/analysis-task-page.js:
+        (AnalysisTaskConfiguratorPane): Added.
+        (AnalysisTaskConfiguratorPane.prototype.didConstructShadowTree): Added. Dispatch createCustomTestGroup action
+        in turn when receiving startTesting from CustomConfigurationTestGroupForm.
+        (AnalysisTaskConfiguratorPane.prototype.setTestGroups): Added.
+        (AnalysisTaskConfiguratorPane.prototype.render): Added.
+        (AnalysisTaskConfiguratorPane.htmlTemplate): Added. We override this instead of formContent to display the
+        "Start" button at the end instead of at the beginnning.
+        (AnalysisTaskConfiguratorPane.cssTemplate): Added.
+        (AnalysisTaskPage.prototype.didConstructShadowTree): Listen to createCustomTestGroup.
+        (AnalysisTaskPage.prototype.render): Hide AnalysisTaskConfiguratorPane when the analysis task is not custom.
+        (AnalysisTaskPage.prototype._showTestGroup): Let AnalysisTaskConfiguratorPane know of the current test group
+        so that it can update the default configuration if the user hasn't modified yet.
+        (AnalysisTaskPage.prototype._createCustomTestGroup): Added. 
+
+        * public/v3/pages/create-analysis-task-page.js:
+        (CreateAnalysisTaskPage.prototype.didConstructShadowTree):
+        (CreateAnalysisTaskPage.prototype._createAnalysisTaskWithGroup):
+        (CreateAnalysisTaskPage.prototype.render):
+        (CreateAnalysisTaskPage.prototype._renderMessage):
+        (CreateAnalysisTaskPage.htmlTemplate):
+        (CreateAnalysisTaskPage.cssTemplate):
+
+        * server-tests/privileged-api-create-test-group-tests.js: Added a test case for creating a custom test group for
+        an existing analysis task.
+
+        * server-tests/resources/mock-data.js:
+        (MockData.otherPlatformId): Added.
+        (MockData.addMockData): Added a test configuration for otherPlatformId.
+
+2017-04-21  Ryosuke Niwa  <rniwa@webkit.org>
+
         Make it possible to view results for sub tests and metrics in A/B testing
         https://bugs.webkit.org/show_bug.cgi?id=170975
 
index a8c3705..ba0abd1 100644 (file)
@@ -59,7 +59,8 @@ function main()
                     exit_with_error('InconsistentTest', array('groupTest' => $test_id, 'taskTest' => $triggerable['test']));
             }
         }
-    } else if ($platform_id && $test_id) {
+    }
+    if (!$triggerable_id && $platform_id && $test_id) {
         $triggerable_configuration = $db->select_first_row('triggerable_configurations', 'trigconfig',
             array('test' => $test_id, 'platform' => $platform_id));
         if ($triggerable_configuration)
index e5a3d01..d328769 100644 (file)
@@ -32,6 +32,69 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
         return [map['Baseline'], map['Comparison']];
     }
 
+    selectTests(selectedTests)
+    {
+        this._selectedTests = selectedTests;
+
+        this._triggerablePlatforms = Triggerable.triggerablePlatformsForTests(this._selectedTests);
+        if (this._selectedTests.length && !this._triggerablePlatforms.includes(this._selectedPlatform))
+            this._selectedPlatform = null;
+
+        this.enqueueToRender();
+    }
+
+    selectPlatform(selectedPlatform)
+    {
+        this._selectedPlatform = selectedPlatform;
+
+        const [triggerable, error] = this._updateTriggerableLazily.evaluate(this._selectedTests, this._selectedPlatform);
+        this._updateRepositoryGroups(triggerable);
+        this._updateCommitSetMap();
+
+        this.enqueueToRender();
+    }
+
+    setCommitSets(baselineCommitSet, comparisonCommitSet)
+    {
+        const [triggerable, error] = this._updateTriggerableLazily.evaluate(this._selectedTests, this._selectedPlatform);
+
+        if (!triggerable)
+            return;
+
+        const baselineRepositoryGroup = triggerable.repositoryGroups().find((repositoryGroup) => repositoryGroup.accepts(baselineCommitSet));
+        if (baselineRepositoryGroup) {
+            this._repositoryGroupByConfiguration['Baseline'] = baselineRepositoryGroup;
+            this._setUploadedFilesIfEmpty(this._fileUploaders['Baseline'], baselineCommitSet);
+            this._specifiedRevisions['Baseline'] = this._revisionMapFromCommitSet(baselineCommitSet);
+        }
+
+        const comparisonRepositoryGroup = triggerable.repositoryGroups().find((repositoryGroup) => repositoryGroup.accepts(baselineCommitSet));
+        if (comparisonRepositoryGroup) {
+            this._repositoryGroupByConfiguration['Comparison'] = comparisonRepositoryGroup;
+            this._setUploadedFilesIfEmpty(this._fileUploaders['Comparison'], comparisonCommitSet);
+            this._specifiedRevisions['Comparison'] = this._revisionMapFromCommitSet(comparisonCommitSet);
+        }
+
+        this._showComparison = true;
+        this._updateCommitSetMap();
+    }
+
+    _setUploadedFilesIfEmpty(uploader, commitSet)
+    {
+        if (uploader.hasFileToUpload() || uploader.uploadedFiles().length)
+            return;
+        for (const uploadedFile of commitSet.customRoots())
+            uploader.addUploadedFile(uploadedFile);
+    }
+
+    _revisionMapFromCommitSet(commitSet)
+    {
+        const revisionMap = new Map;
+        for (const repository of commitSet.repositories())
+            revisionMap.set(repository, commitSet.revisionForRepository(repository));
+        return revisionMap;
+    }
+
     didConstructShadowTree()
     {
         this.content('specify-comparison-button').onclick = this.createEventHandler(() => this._configureComparison());
@@ -68,8 +131,11 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
     {
         super.render();
 
-        this._renderTriggerableTestsLazily.evaluate();
-        this._renderTriggerablePlatformsLazily.evaluate(this._selectedTests, this._triggerablePlatforms);
+        const updateSelectedTestsLazily = this._renderTriggerableTestsLazily.evaluate();
+        updateSelectedTestsLazily.evaluate(...this._selectedTests);
+        const updateSelectedPlatformsLazily = this._renderTriggerablePlatformsLazily.evaluate(this._selectedTests, this._triggerablePlatforms);
+        if (updateSelectedPlatformsLazily)
+            updateSelectedPlatformsLazily.evaluate(this._selectedPlatform);
 
         const [triggerable, error] = this._updateTriggerableLazily.evaluate(this._selectedTests, this._selectedPlatform);
 
@@ -81,52 +147,54 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
         const enabledTriggerables = Triggerable.all().filter((triggerable) => !triggerable.isDisabled());
 
         let tests = Test.topLevelTests().filter((test) => test.metrics().length && enabledTriggerables.some((triggerable) => triggerable.acceptsTest(test)));
-        this.renderReplace(this.content('test-list'), this._buildCheckboxList('test', tests, (selectedTests) => {
-            this._selectedTests = selectedTests;
-
-            this._triggerablePlatforms = Triggerable.triggerablePlatformsForTests(this._selectedTests);
-            if (this._selectedTests.length && !this._triggerablePlatforms.includes(this._selectedPlatform))
-                this._selectedPlatform = null;
-        }));
+        return this._renderRadioButtonList(this.content('test-list'), 'test', tests, this.selectTests.bind(this));
     }
 
     _renderTriggerablePlatforms(selectedTests, triggerablePlatforms)
     {
         if (!selectedTests.length) {
             this.content('platform-pane').style.display = 'none';
-            return;
+            return null;
         }
         this.content('platform-pane').style.display = null;
 
-        this.renderReplace(this.content('platform-list'), this._buildCheckboxList('platform', triggerablePlatforms, (selectedPlatforms) => {
-            this._selectedPlatform = selectedPlatforms.length ? selectedPlatforms[0] : null;
-
-            const [triggerable, error] = this._updateTriggerableLazily.evaluate(this._selectedTests, this._selectedPlatform);
-            this._updateRepositoryGroups(triggerable);
-            this._updateCommitSetMap();
-
-            this.enqueueToRender();
-        }));
+        return this._renderRadioButtonList(this.content('platform-list'), 'platform', triggerablePlatforms, (selectedPlatforms) => {
+            this.selectPlatform(selectedPlatforms.length ? selectedPlatforms[0] : null);
+        });
     }
 
-    _buildCheckboxList(name, objects, callback)
+    _renderRadioButtonList(listContainer, name, objects, callback)
     {
         const listItems = [];
         let selectedListItems = [];
-        const element = ComponentBase.createElement;
-        return objects.map((object) => {
-            const checkbox = element('input', {type: 'radio', name: name, onchange: () => {
-                selectedListItems.forEach((item) => item.label.classList.remove('selected'));
-                selectedListItems = listItems.filter((item) => item.checkbox.checked);
-                selectedListItems.forEach((item) => item.label.classList.add('selected'));
+        const checkSelectedRadioButtons = (newSelectedListItems) => {
+            selectedListItems.forEach((item) => {
+                item.label.classList.remove('selected');
+                item.radioButton.checked = false;
+            });
+            selectedListItems = newSelectedListItems;
+            selectedListItems.forEach((item) => {
+                item.label.classList.add('selected');
+                item.radioButton.checked = true;
+            });
+        }
 
+        const element = ComponentBase.createElement;
+        this.renderReplace(listContainer, objects.map((object) => {
+            const radioButton = element('input', {type: 'radio', name: name, onchange: () => {
+                checkSelectedRadioButtons(listItems.filter((item) => item.radioButton.checked));
                 callback(selectedListItems.map((item) => item.object));
                 this.enqueueToRender();
             }});
-            const label = element('label', [checkbox, object.label()]);
-            listItems.push({checkbox, label, object});
+            const label = element('label', [radioButton, object.label()]);
+            listItems.push({radioButton, label, object});
             return element('li', label);
-        })
+        }));
+
+        return new LazilyEvaluatedFunction((...selectedObjects) => {
+            const objects = new Set(selectedObjects);
+            checkSelectedRadioButtons(listItems.filter((item) => objects.has(item.object)));
+        });
     }
 
     _updateTriggerable(tests, platform)
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
new file mode 100644 (file)
index 0000000..58ebbaa
--- /dev/null
@@ -0,0 +1,110 @@
+
+class CustomConfigurationTestGroupForm extends TestGroupForm {
+
+    constructor()
+    {
+        super('custom-configuration-test-group-form');
+        this._hasTask = false;
+        this._updateTestGroupNameLazily = new LazilyEvaluatedFunction(this._updateTestGroupName.bind(this));
+    }
+
+    setHasTask(hasTask)
+    {
+        this._hasTask = hasTask;
+        this.enqueueToRender();
+    }
+
+    hasCommitSets()
+    {
+        return !!this.part('configurator').commitSets();
+    }
+
+    setConfigurations(test, platform, repetitionCount, commitSets)
+    {
+        const configurator = this.part('configurator');
+        configurator.selectTests([test]);
+        configurator.selectPlatform(platform);
+        if (commitSets.length == 2)
+            configurator.setCommitSets(commitSets[0], commitSets[1]);
+        this.setRepetitionCount(repetitionCount);
+        this.enqueueToRender();
+    }
+
+    startTesting()
+    {
+        const taskName = this.content('task-name').value;
+        const testGroupName = this.content('group-name').value;
+        const configurator = this.part('configurator');
+        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);
+    }
+
+    didConstructShadowTree()
+    {
+        super.didConstructShadowTree();
+
+        this.part('configurator').listenToAction('commitSetChange', () => this.enqueueToRender());
+
+        this.content('task-name').oninput = () => this.enqueueToRender();
+        this.content('group-name').oninput = () => this.enqueueToRender();
+    }
+
+    render()
+    {
+        super.render();
+        const configurator = this.part('configurator');
+        this._updateTestGroupNameLazily.evaluate(configurator.tests(), configurator.platform());
+
+        const needsTaskName = !this._hasTask && !this.content('task-name').value;
+        this.content('iteration-start-pane').style.display = !!configurator.commitSets() ? null : 'none';
+        this.content('task-name').style.display = this._hasTask ? 'none' : null;
+        this.content('start-button').disabled = needsTaskName || !this.content('group-name').value;
+    }
+
+    _updateTestGroupName(tests, platform)
+    {
+        if (!platform || !tests.length)
+            return;
+        this.content('group-name').value = `${tests.map((test) => test.name()).join(', ')} on ${platform.name()}`;
+    }
+
+    static cssTemplate()
+    {
+        return super.cssTemplate() + `
+            input {
+                width: 30%;
+                min-width: 10rem;
+                font-size: 1rem;
+                font-weight: inherit;
+            }
+
+            #form > * {
+                margin-bottom: 1rem;
+            }
+
+            #start-button {
+                display: block;
+                margin-top: 0.5rem;
+                font-size: 1.2rem;
+                font-weight: inherit;
+            }
+            `;
+    }
+
+    static htmlTemplate()
+    {
+        return `<form id="form">
+            <input id="task-name" type="text" placeholder="Name this task">
+            <custom-analysis-task-configurator id="configurator"></custom-analysis-task-configurator>
+            <div id="iteration-start-pane">
+                <input id="group-name" type="text" placeholder="Test group name">
+                ${super.formContent()}
+                <button id="start-button">Start</button>
+            </div>
+        </form>`;
+    }
+}
+
+ComponentBase.defineElement('custom-configuration-test-group-form', CustomConfigurationTestGroupForm);
index 7e0ac0a..c44b66f 100644 (file)
@@ -32,6 +32,15 @@ class TestGroupForm extends ComponentBase {
         return `<form id="form"><button id="start-button" type="submit"><slot>Start A/B testing</slot></button>${this.formContent()}</form>`;
     }
 
+    static cssTemplate()
+    {
+        return `
+            :host {
+                display: block;
+            }
+        `;
+    }
+
     static formContent()
     {
         return `
index 7cd5113..d70ca2d 100644 (file)
@@ -96,6 +96,7 @@ Run tools/bundle-v3-scripts to speed up the load time for production.`);
         <script src="components/analysis-task-bug-list.js"></script>
         <script src="components/ratio-bar-graph.js"></script>
         <script src="components/custom-analysis-task-configurator.js"></script>
+        <script src="components/custom-configuration-test-group-form.js"></script>
         <script src="components/instant-file-uploader.js"></script>
 
         <script src="pages/page.js"></script>
index 1e8140b..32be0a6 100644 (file)
@@ -40,6 +40,20 @@ class TestGroup extends LabeledObject {
         this._commitSetToLabel.clear();
     }
 
+    test()
+    {
+        if (!this._buildRequests.length)
+            return null;
+        return this._buildRequests[0].test();
+    }
+
+    platform()
+    {
+        if (!this._buildRequests.length)
+            return null;
+        return this._buildRequests[0].platform();
+    }
+
     repetitionCount()
     {
         if (!this._buildRequests.length)
@@ -188,6 +202,16 @@ class TestGroup extends LabeledObject {
         });
     }
 
+    static createWithCustomConfiguration(task, platform, test, groupName, repetitionCount, commitSets)
+    {
+        console.assert(commitSets.length == 2);
+        const revisionSets = this._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._fetchTestGroupsForTask(task.id());
+        });
+    }
+
     static createAndRefetchTestGroups(task, name, repetitionCount, commitSets)
     {
         console.assert(commitSets.length == 2);
index 45b6537..d7ab4ac 100644 (file)
@@ -5,7 +5,7 @@ class UploadedFile extends DataModelObject {
     {
         super(id, object);
         this._createdAt = new Date(object.createdAt);
-        this._deletedAt = new Date(object.deletedAt);
+        this._deletedAt = object.deletedAt ? new Date(object.deletedAt) : null;
         this._filename = object.filename;
         this._author = object.author;
         this._size = object.size;
index 30221a5..ee7fc8e 100644 (file)
@@ -133,6 +133,53 @@ class AnalysisTaskResultsPane extends ComponentBase {
 
 ComponentBase.defineElement('analysis-task-results-pane', AnalysisTaskResultsPane);
 
+class AnalysisTaskConfiguratorPane extends ComponentBase {
+    constructor()
+    {
+        super('analysis-task-configurator-pane');
+        this._currentGroup = null;
+    }
+
+    didConstructShadowTree()
+    {
+        const form = this.part('form');
+        form.setHasTask(true);
+        form.listenToAction('startTesting', (...args) => {
+            this.dispatchAction('createCustomTestGroup', ...args);
+        });
+    }
+
+    setTestGroups(testGroups, currentGroup)
+    {
+        this._currentGroup = currentGroup;
+        const form = this.part('form');
+        if (!form.hasCommitSets())
+            form.setConfigurations(currentGroup.test(), currentGroup.platform(), currentGroup.repetitionCount(), currentGroup.requestedCommitSets());
+        this.enqueueToRender();
+    }
+
+    render()
+    {
+        super.render();
+    }
+    
+    static htmlTemplate()
+    {
+        return `<custom-configuration-test-group-form id="form"></custom-configuration-test-group-form>`;
+    }
+
+    static cssTemplate()
+    {
+        return `
+            #form {
+                margin: 1rem;
+            }
+        `;
+    }
+};
+
+ComponentBase.defineElement('analysis-task-configurator-pane', AnalysisTaskConfiguratorPane);
+
 class AnalysisTaskTestGroupPane extends ComponentBase {
 
     constructor()
@@ -389,6 +436,8 @@ class AnalysisTaskPage extends PageWithHeading {
         resultsPane.listenToAction('showTestGroup', (testGroup) => this._showTestGroup(testGroup));
         resultsPane.listenToAction('newTestGroup', this._createTestGroupAfterVerifyingCommitSetList.bind(this));
 
+        this.part('configurator-pane').listenToAction('createCustomTestGroup', this._createCustomTestGroup.bind(this));
+
         const groupPane = this.part('group-pane');
         groupPane.listenToAction('showTestGroup', (testGroup) => this._showTestGroup(testGroup));
         groupPane.listenToAction('showHiddenTestGroups', () => this._showAllTestGroups());
@@ -524,6 +573,8 @@ class AnalysisTaskPage extends PageWithHeading {
         this.content('results-pane').style.display = this._task && !this._task.isCustom() ? null : 'none';
         this.part('results-pane').setShowForm(!!this._triggerable);
 
+        this.content('configurator-pane').style.display = this._task && this._task.isCustom() ? null : 'none';
+
         Instrumentation.endMeasuringTime('AnalysisTaskPage', 'render');
     }
 
@@ -583,6 +634,7 @@ class AnalysisTaskPage extends PageWithHeading {
     _showTestGroup(testGroup)
     {
         this._currentTestGroup = testGroup;
+        this.part('configurator-pane').setTestGroups(this._filteredTestGroups, this._currentTestGroup);
         this.part('results-pane').setTestGroups(this._filteredTestGroups, this._currentTestGroup);
         const groupsInReverseChronology = this._filteredTestGroups.slice(0).reverse();
         const showHiddenGroups = !this._testGroups.some((group) => group.isHidden()) || this._showHiddenTestGroups;
@@ -711,6 +763,20 @@ class AnalysisTaskPage extends PageWithHeading {
         });
     }
 
+    _createCustomTestGroup(repetitionCount, testGroupName, commitSets, platform, test)
+    {
+        console.assert(this._task.isCustom());
+        if (this._hasDuplicateTestGroupName(testGroupName)) {
+            alert(`There is already a test group named "${testGroupName}"`);
+            return;
+        }
+
+        TestGroup.createWithCustomConfiguration(this._task, platform, test, testGroupName, repetitionCount, commitSets)
+            .then(this._didFetchTestGroups.bind(this), function (error) {
+            alert('Failed to create a new test group: ' + error);
+        });
+    }
+
     _createRetryNameForTestGroup(name)
     {
         var nameWithNumberMatch = name.match(/(.+?)\s*\(\s*(\d+)\s*\)\s*$/);
@@ -777,6 +843,7 @@ class AnalysisTaskPage extends PageWithHeading {
                 </div>
                 <analysis-task-chart-pane id="chart-pane"></analysis-task-chart-pane>
                 <analysis-task-results-pane id="results-pane"></analysis-task-results-pane>
+                <analysis-task-configurator-pane id="configurator-pane"></analysis-task-configurator-pane>
                 <analysis-task-test-group-pane id="group-pane"></analysis-task-test-group-pane>
             </div>
 `;
index 9ecff03..a9f0890 100644 (file)
@@ -19,32 +19,12 @@ class CreateAnalysisTaskPage extends PageWithHeading {
 
     didConstructShadowTree()
     {
-        this.part('configurator').listenToAction('commitSetChange', () => this.enqueueToRender());
-        this.content('start-button').onclick = this.createEventHandler(() => this._createAnalysisTaskWithGroup());
+        this.part('form').listenToAction('startTesting', this._createAnalysisTaskWithGroup.bind(this));
     }
 
-    _createAnalysisTaskWithGroup()
+    _createAnalysisTaskWithGroup(repetitionCount, testGroupName, commitSets, platform, test, taskName)
     {
-        const taskNameInput = this.content('task-name');
-        if (!taskNameInput.reportValidity())
-            return;
-
-        const testGroupInput = this.content('test-group-name');
-        if (!testGroupInput.reportValidity())
-            return;
-
-        const configurator = this.part('configurator');
-        const tests = configurator.tests();
-        if (tests.length != 1)
-            return alert('Exactly one test must be selected');
-
-        const taskName = taskNameInput.value;
-        const testGroupName = testGroupInput.value;
-        const iterationCount = this.content('iteration-count').value;
-        const platform = configurator.platform();
-        const commitSets = configurator.commitSets();
-
-        TestGroup.createWithTask(taskName, platform, tests[0], testGroupName, iterationCount, commitSets).then((task) => {
+        TestGroup.createWithTask(taskName, platform, test, testGroupName, repetitionCount, commitSets).then((task) => {
             const url = this.router().url(`analysis/task/${task.id()}`);
             location.href = this.router().url(`analysis/task/${task.id()}`);
         }, (error) => {
@@ -55,41 +35,23 @@ class CreateAnalysisTaskPage extends PageWithHeading {
     render()
     {
         super.render();
-        const configurator = this.part('configurator');
-        this._renderMessageLazily.evaluate(this._message, !!configurator.commitSets(), configurator.tests(), configurator.platform());
+        this._renderMessageLazily.evaluate(this._message);
     }
 
-    _renderMessage(message, hasValidCommitSets, tests, platform)
+    _renderMessage(message)
     {
         const messageContainer = this.content('message');
         messageContainer.textContent = this._message;
         messageContainer.style.display = this._message ? null : 'none';
-        this.content('new-task').style.display = this._message ? 'none' : null;
-        if (platform)
-            this.content('test-group-name').value = `${tests.map((test) => test.name()).join(', ')} on ${platform.name()}`;
-        this.content('iteration-start-pane').style.display = !this._message && hasValidCommitSets ? null : 'none';
+        this.content('form').style.display = this._message ? 'none' : null;
+
       }
 
     static htmlTemplate()
     {
         return `
             <p id="message"></p>
-            <div id="new-task">
-                <input id="task-name" type="text" placeholder="Name this task" required>
-                <custom-analysis-task-configurator id="configurator"></custom-analysis-task-configurator>
-                <div id="iteration-start-pane">
-                    <input id="test-group-name" placeholder="Name this test group" required>
-                    <label><select id="iteration-count">
-                        <option>1</option>
-                        <option>2</option>
-                        <option>3</option>
-                        <option selected>4</option>
-                        <option>5</option>
-                        <option>6</option>
-                    </select> iterations per configuration</label>
-                    <button id="start-button">Start</button>
-                </div>
-            </div>`;
+            <custom-configuration-test-group-form id="form"></custom-configuration-test-group-form>`;
     }
 
     static cssTemplate()
@@ -98,25 +60,9 @@ class CreateAnalysisTaskPage extends PageWithHeading {
             #message {
                 text-align: center;
             }
-
-            #new-task > * {
-                display: block;
+            #form {
                 margin: 1rem;
             }
-
-            #new-task input {
-                width: 30%;
-                min-width: 10rem;
-                font-size: 1rem;
-                font-weight: inherit;
-            }
-
-            #start-button {
-                display: block;
-                margin-top: 0.5rem;
-                font-size: 1.2rem;
-                font-weight: inherit;
-            }
 `;
     }
 }
index afb82c1..667175d 100644 (file)
@@ -99,7 +99,8 @@ function addTriggerableAndCreateTask(name)
         'slavePassword': 'anotherPassword',
         'triggerable': 'build-webkit',
         'configurations': [
-            {test: MockData.someTestId(), platform: MockData.somePlatformId()}
+            {test: MockData.someTestId(), platform: MockData.somePlatformId()},
+            {test: MockData.someTestId(), platform: MockData.otherPlatformId()},
         ],
         'repositoryGroups': [
             {name: 'webkit-only', repositories: [MockData.webkitRepositoryId()]},
@@ -524,4 +525,61 @@ describe('/privileged-api/create-test-group', function () {
         });
     });
 
+    it('should create a custom test group for an existing custom analysis task', () => {
+        let firstResult;
+        let secondResult;
+        let webkit;
+        let test = MockData.someTestId();
+        return addTriggerableAndCreateTask('some task').then(() => {
+            webkit = Repository.all().filter((repository) => repository.name() == 'WebKit')[0];
+            const revisionSets = [{[webkit.id()]: '191622'}, {[webkit.id()]: '191623'}];
+            return PrivilegedAPI.sendRequest('create-test-group',
+                {name: 'test1', taskName: 'other task', platform: MockData.somePlatformId(), test, revisionSets});
+        }).then((result) => {
+            firstResult = result;
+            const revisionSets = [{[webkit.id()]: '191622'}, {[webkit.id()]: '192736'}];
+            return PrivilegedAPI.sendRequest('create-test-group',
+                {name: 'test2', task: result['taskId'], platform: MockData.otherPlatformId(), test, revisionSets, repetitionCount: 2});
+        }).then((result) => {
+            secondResult = result;
+            assert.equal(firstResult['taskId'], secondResult['taskId']);
+            return Promise.all([AnalysisTask.fetchById(result['taskId']), TestGroup.fetchByTask(result['taskId'])]);
+        }).then((result) => {
+            const [analysisTask, testGroups] = result;
+
+            assert.equal(analysisTask.name(), 'other task');
+
+            assert.equal(testGroups.length, 2);
+            TestGroup.sortByName(testGroups);
+
+            assert.equal(testGroups[0].name(), 'test1');
+            assert.equal(testGroups[0].repetitionCount(), 1);
+            let requests = testGroups[0].buildRequests();
+            assert.equal(requests.length, 2);
+            let set0 = requests[0].commitSet();
+            let 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');
+
+            assert.equal(testGroups[1].name(), 'test2');
+            assert.equal(testGroups[1].repetitionCount(), 2);
+            requests = testGroups[1].buildRequests();
+            assert.equal(requests.length, 4);
+            set0 = requests[0].commitSet();
+            set1 = requests[1].commitSet();
+            assert.deepEqual(requests[2].commitSet(), set0);
+            assert.deepEqual(requests[3].commitSet(), set1);
+            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), '192736');
+        });
+    });
+
 });
index 6e067d5..1cb3262 100644 (file)
@@ -21,6 +21,7 @@ MockData = {
     emptyTriggeragbleId() { return 1001; },
     someTestId() { return 200; },
     somePlatformId() { return 65; },
+    otherPlatformId() { return 101; },
     macosRepositoryId() { return 9; },
     webkitRepositoryId() { return 11; },
     gitWebkitRepositoryId() { return 111; },
@@ -41,9 +42,11 @@ MockData = {
             db.insert('commits', {id: 96336, repository: this.webkitRepositoryId(), revision: '192736', time: (new Date(1448225325650)).toISOString()}),
             db.insert('builds', {id: 901, number: '901', time: '2015-10-27T12:05:27.1Z'}),
             db.insert('platforms', {id: MockData.somePlatformId(), name: 'some platform'}),
+            db.insert('platforms', {id: MockData.otherPlatformId(), name: 'other platform'}),
             db.insert('tests', {id: MockData.someTestId(), name: 'some test'}),
             db.insert('test_metrics', {id: 300, test: 200, name: 'some metric'}),
-            db.insert('test_configurations', {id: 301, metric: 300, platform: 65, type: 'current'}),
+            db.insert('test_configurations', {id: 301, metric: 300, platform: MockData.somePlatformId(), type: 'current'}),
+            db.insert('test_configurations', {id: 302, metric: 300, platform: MockData.otherPlatformId(), type: 'current'}),
             db.insert('test_runs', {id: 801, config: 301, build: 901, mean_cache: 100}),
             db.insert('commit_sets', {id: 401}),
             db.insert('commit_set_relationships', {set: 401, commit: 87832}),