Hide the UI to trigger an A/B testing when there are no triggerables
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 12 Jan 2017 07:18:28 +0000 (07:18 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 12 Jan 2017 07:18:28 +0000 (07:18 +0000)
https://bugs.webkit.org/show_bug.cgi?id=166964

Reviewed by Yusuke Suzuki.

Hide the "Start A/B Testing" button on analysis task pages instead of showing it and failing later
when the user tries to create one it with a TriggerableNotFound error.

Added the list of triggerables to the manifest JSON so that we can determine this condition without
having to fetch /api/triggerable for each analysis task as done in v2 UI.

* public/admin/reprocess-report.php:
* public/api/manifest.php:
* public/api/report.php:
* public/include/admin-header.php:
* public/include/manifest-generator.php: Moved from public/include/manifest.php.
(ManifestGenerator::generate):
(ManifestGenerator::triggerables): Added. Include the list of repositories this triggerable accepts
as well as the list of (test, platform) pairs on which this triggerable is available.
Use [testId, platformId] instead of a dictionary to reduce the file size.
* public/v3/components/customizable-test-group-form.js:
(CustomizableTestGroupForm): Removed this._disabled. This variable was used in TestGroupFrom to
disable the "Start A/B Testing" button when no range is selected but this ended up racy. Compute
the visibility of the button in render() function instead.
(CustomizableTestGroupForm.prototype.setRootSetMap):
(CustomizableTestGroupForm.prototype._submitted):
(CustomizableTestGroupForm.prototype.render): Hide the customize link and the button as needed.
The "Start A/B Testing" button must be hidden when either no range is selected or no title is typed.
"Customize" button must be hidden when no range is selected.
* public/v3/components/test-group-form.js:
(TestGroupForm): Removed _disabled since it's no longer used.
(TestGroupForm.prototype.setDisabled): Ditto.
(TestGroupForm.prototype.render): Ditto.
* public/v3/index.html: Include triggerable.js.
* public/v3/models/manifest.js:
(Manifest._didFetchManifest): Modernized. Create Triggerable objects from the manifest JSON.
* public/v3/models/triggerable.js: Added.
(Triggerable): Add this triggerable object to the static map of (test id, platform id) pair.
(Triggerable.prototype.acceptedRepositories): Added.
(Triggerable.findByTestConfiguration): Added. Finds a triggerable in the aforementioned static map.
* public/v3/pages/analysis-task-page.js:
(AnalysisTaskChartPane.prototype._updateStatus): Added. Re-render the page since time series data
points that were previously not available may have become available. The lack of this update was
causing a race condition in which the "Start A/B Testing" button for the charts is disabled even
after a group name had been specified because setRootSetMap was never called with a valid set.
(AnalysisTaskPage): Added this._triggerable.
(AnalysisTaskPage.prototype._didFetchTask): Find the triggerable now that we've fetched the task.
(AnalysisTaskPage.prototype.render): Hide the group view (the table of A/B testing results) entirely
when there are no groups to show. Also hide the forms to start A/B testing when there are no matching
triggerable, which is the main feature of this patch.
* server-tests/api-manifest.js: Added a test for including a list of triggerables in the manifest JSON.
* server-tests/resources/mock-data.js:
(MockData.resetV3Models): Reset Triggerable's static map.
* server-tests/tools-buildbot-triggerable-tests.js: Assert that Triggerable objects are  constructed
with appropriate list of repositories and (test, platform) associations.
* tools/js/database.js:
(tableToPrefixMap): Added triggerable_repositories's prefix.
* tools/js/remote.js:
(RemoteAPI.prototype.getJSON): Log the entire response to stderr when JSON.parse fails to aid debugging.
* tools/js/v3-models.js: Import triggerable.js.

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

18 files changed:
Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/public/admin/reprocess-report.php
Websites/perf.webkit.org/public/api/manifest.php
Websites/perf.webkit.org/public/api/report.php
Websites/perf.webkit.org/public/include/admin-header.php
Websites/perf.webkit.org/public/include/manifest-generator.php [moved from Websites/perf.webkit.org/public/include/manifest.php with 82% similarity]
Websites/perf.webkit.org/public/v3/components/customizable-test-group-form.js
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/manifest.js
Websites/perf.webkit.org/public/v3/models/triggerable.js [new file with mode: 0644]
Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js
Websites/perf.webkit.org/server-tests/api-manifest.js
Websites/perf.webkit.org/server-tests/resources/mock-data.js
Websites/perf.webkit.org/server-tests/tools-buildbot-triggerable-tests.js
Websites/perf.webkit.org/tools/js/database.js
Websites/perf.webkit.org/tools/js/remote.js
Websites/perf.webkit.org/tools/js/v3-models.js

index ec0ddac..ee09ee4 100644 (file)
@@ -1,3 +1,66 @@
+2017-01-12  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Hide the UI to trigger an A/B testing when there are no triggerables
+        https://bugs.webkit.org/show_bug.cgi?id=166964
+
+        Reviewed by Yusuke Suzuki.
+
+        Hide the "Start A/B Testing" button on analysis task pages instead of showing it and failing later
+        when the user tries to create one it with a TriggerableNotFound error.
+
+        Added the list of triggerables to the manifest JSON so that we can determine this condition without
+        having to fetch /api/triggerable for each analysis task as done in v2 UI.
+
+        * public/admin/reprocess-report.php:
+        * public/api/manifest.php:
+        * public/api/report.php:
+        * public/include/admin-header.php:
+        * public/include/manifest-generator.php: Moved from public/include/manifest.php.
+        (ManifestGenerator::generate):
+        (ManifestGenerator::triggerables): Added. Include the list of repositories this triggerable accepts
+        as well as the list of (test, platform) pairs on which this triggerable is available.
+        Use [testId, platformId] instead of a dictionary to reduce the file size.
+        * public/v3/components/customizable-test-group-form.js:
+        (CustomizableTestGroupForm): Removed this._disabled. This variable was used in TestGroupFrom to
+        disable the "Start A/B Testing" button when no range is selected but this ended up racy. Compute
+        the visibility of the button in render() function instead.
+        (CustomizableTestGroupForm.prototype.setRootSetMap):
+        (CustomizableTestGroupForm.prototype._submitted):
+        (CustomizableTestGroupForm.prototype.render): Hide the customize link and the button as needed.
+        The "Start A/B Testing" button must be hidden when either no range is selected or no title is typed.
+        "Customize" button must be hidden when no range is selected.
+        * public/v3/components/test-group-form.js:
+        (TestGroupForm): Removed _disabled since it's no longer used.
+        (TestGroupForm.prototype.setDisabled): Ditto.
+        (TestGroupForm.prototype.render): Ditto.
+        * public/v3/index.html: Include triggerable.js.
+        * public/v3/models/manifest.js:
+        (Manifest._didFetchManifest): Modernized. Create Triggerable objects from the manifest JSON.
+        * public/v3/models/triggerable.js: Added.
+        (Triggerable): Add this triggerable object to the static map of (test id, platform id) pair.
+        (Triggerable.prototype.acceptedRepositories): Added.
+        (Triggerable.findByTestConfiguration): Added. Finds a triggerable in the aforementioned static map.
+        * public/v3/pages/analysis-task-page.js:
+        (AnalysisTaskChartPane.prototype._updateStatus): Added. Re-render the page since time series data
+        points that were previously not available may have become available. The lack of this update was
+        causing a race condition in which the "Start A/B Testing" button for the charts is disabled even
+        after a group name had been specified because setRootSetMap was never called with a valid set.
+        (AnalysisTaskPage): Added this._triggerable.
+        (AnalysisTaskPage.prototype._didFetchTask): Find the triggerable now that we've fetched the task.
+        (AnalysisTaskPage.prototype.render): Hide the group view (the table of A/B testing results) entirely
+        when there are no groups to show. Also hide the forms to start A/B testing when there are no matching
+        triggerable, which is the main feature of this patch.
+        * server-tests/api-manifest.js: Added a test for including a list of triggerables in the manifest JSON.
+        * server-tests/resources/mock-data.js:
+        (MockData.resetV3Models): Reset Triggerable's static map.
+        * server-tests/tools-buildbot-triggerable-tests.js: Assert that Triggerable objects are  constructed
+        with appropriate list of repositories and (test, platform) associations.
+        * tools/js/database.js:
+        (tableToPrefixMap): Added triggerable_repositories's prefix.
+        * tools/js/remote.js:
+        (RemoteAPI.prototype.getJSON): Log the entire response to stderr when JSON.parse fails to aid debugging.
+        * tools/js/v3-models.js: Import triggerable.js.
+
 2017-01-11  Ryosuke Niwa  <rniwa@webkit.org>
 
         fetch-from-remote doesn’t work with some websites
index ea762ce..be6c1f1 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 require_once('../include/json-header.php');
-require_once('../include/manifest.php');
+require_once('../include/manifest-generator.php');
 require_once('../include/report-processor.php');
 
 $db = new Database;
index 45be7c0..bcf71a9 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 require_once('../include/json-header.php');
-require_once('../include/manifest.php');
+require_once('../include/manifest-generator.php');
 
 function main() {
     $db = new Database;
index b9937e3..76fb6bf 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 require_once('../include/json-header.php');
-require_once('../include/manifest.php');
+require_once('../include/manifest-generator.php');
 require_once('../include/report-processor.php');
 
 function main($post_data) {
index 697ecca..6a5f1ce 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 require_once('db.php');
-require_once('manifest.php');
+require_once('manifest-generator.php');
 
 ?><!DOCTYPE html>
 <html>
@@ -40,6 +40,7 @@ class ManifestGenerator {
             'repositories' => &$repositories,
             'builders' => (object)$this->builders(),
             'bugTrackers' => (object)$this->bug_trackers($repositories_table),
+            'triggerables'=> (object)$this->triggerables(),
             'dashboards' => (object)config('dashboards'),
             'summaryPages' => config('summaryPages'),
         );
@@ -86,7 +87,7 @@ class ManifestGenerator {
 
     private function platforms($platform_table, $is_dashboard) {
         $metrics = $this->db->query_and_fetch_all('SELECT config_metric AS metric_id, config_platform AS platform_id,
-            extract(epoch from max(config_runs_last_modified)) * 1000 AS last_modified, bool_or(config_is_in_dashboard) AS in_dashboard
+            extract(epoch from max(config_runs_last_modified) at time zone \'utc\') * 1000 AS last_modified, bool_or(config_is_in_dashboard) AS in_dashboard
             FROM test_configurations GROUP BY config_metric, config_platform ORDER BY config_platform');
 
         $platform_metrics = array();
@@ -178,6 +179,40 @@ class ManifestGenerator {
 
         return $bug_trackers;
     }
+
+    private function triggerables() {
+
+        $triggerables = $this->db->fetch_table('build_triggerables');
+        if (!$triggerables)
+            return array();
+
+        $id_to_triggerable = array();
+        foreach ($triggerables as $row) {
+            $id = $row['triggerable_id'];
+            $id_to_triggerable[$id] = array(
+                'name' => $row['triggerable_name'],
+                'acceptedRepositories' => array(),
+                'configurations' => array());
+        }
+
+        $repository_map = $this->db->fetch_table('triggerable_repositories');
+        if ($repository_map) {
+            foreach ($repository_map as $row) {
+                $triggerable = &$id_to_triggerable[$row['trigrepo_triggerable']];
+                array_push($triggerable['acceptedRepositories'], $row['trigrepo_repository']);
+            }
+        }
+
+        $configuration_map = $this->db->fetch_table('triggerable_configurations');
+        if ($configuration_map) {
+            foreach ($configuration_map as $row) {
+                $triggerable = &$id_to_triggerable[$row['trigconfig_triggerable']];
+                array_push($triggerable['configurations'], array($row['trigconfig_test'], $row['trigconfig_platform']));
+            }
+        }
+
+        return $id_to_triggerable;
+    }
 }
 
 ?>
index b49e23d..7bb81b5 100644 (file)
@@ -5,9 +5,10 @@ class CustomizableTestGroupForm extends TestGroupForm {
     {
         super('customizable-test-group-form');
         this._rootSetMap = null;
-        this._disabled = true;
         this._renderedRepositorylist = null;
         this._customized = false;
+        this._nameControl = this.content().querySelector('.name');
+        this._nameControl.oninput = this.render.bind(this);
         this.content().querySelector('a').onclick = this._customize.bind(this);
     }
 
@@ -15,13 +16,12 @@ class CustomizableTestGroupForm extends TestGroupForm {
     {
         this._rootSetMap = map;
         this._customized = false;
-        this.setDisabled(!map);
     }
 
     _submitted()
     {
         if (this._startCallback)
-            this._startCallback(this.content().querySelector('.name').value, this._repetitionCount, this._computeRootSetMap());
+            this._startCallback(this._nameControl.value, this._repetitionCount, this._computeRootSetMap());
     }
 
     _customize(event)
@@ -56,13 +56,15 @@ class CustomizableTestGroupForm extends TestGroupForm {
     render()
     {
         super.render();
-        this.content().querySelector('.customize-link').style.display = this._disabled ? 'none' : null;
+        var map = this._rootSetMap;
+
+        this.content().querySelector('button').disabled = !(map && this._nameControl.value);
+        this.content().querySelector('.customize-link').style.display = !map ? 'none' : null;
 
         if (!this._customized) {
             this.renderReplace(this.content().querySelector('.custom-table-container'), []);
             return;
         }
-        var map = this._rootSetMap;
         console.assert(map);
 
         var repositorySet = new Set;
index f158a8a..205f7c1 100644 (file)
@@ -5,7 +5,6 @@ class TestGroupForm extends ComponentBase {
     {
         super(name || 'test-group-form');
         this._startCallback = null;
-        this._disabled = false;
         this._label = undefined;
         this._repetitionCount = 4;
 
@@ -24,7 +23,6 @@ class TestGroupForm extends ComponentBase {
     }
 
     setStartCallback(callback) { this._startCallback = callback; }
-    setDisabled(disabled) { this._disabled = !!disabled; }
     setLabel(label) { this._label = label; }
     setRepetitionCount(count) { this._repetitionCount = count; }
 
@@ -33,7 +31,6 @@ class TestGroupForm extends ComponentBase {
         var button = this.content().querySelector('button');
         if (this._label)
             button.textContent = this._label;
-        button.disabled = this._disabled;
         this._repetitionCountControl.value = this._repetitionCount;
     }
 
index 497ec84..22310ed 100644 (file)
@@ -63,6 +63,7 @@ Run tools/bundle-v3-scripts to speed up the load time for production.`);
         <script src="models/test-group.js"></script>
         <script src="models/build-request.js"></script>
         <script src="models/root-set.js"></script>
+        <script src="models/triggerable.js"></script>
         <script src="models/manifest.js"></script>
 
         <script src="components/base.js"></script>
index 7df022e..8fedc42 100644 (file)
@@ -13,34 +13,40 @@ class Manifest {
     {
         Instrumentation.startMeasuringTime('Manifest', '_didFetchManifest');
 
-        var tests = [];
-        var testParentMap = {};
-        for (var testId in rawResponse.tests)
+        const tests = [];
+        const testParentMap = {};
+        for (let testId in rawResponse.tests)
             tests.push(new Test(testId, rawResponse.tests[testId]));
 
         function buildObjectsFromIdMap(idMap, constructor, resolver) {
-            for (var id in idMap) {
+            for (let id in idMap) {
                 if (resolver)
                     resolver(idMap[id]);
                 new constructor(id, idMap[id]);
             }
         }
-        buildObjectsFromIdMap(rawResponse.metrics, Metric, function (raw) {
+
+        buildObjectsFromIdMap(rawResponse.metrics, Metric, (raw) => {
             raw.test = Test.findById(raw.test);
         });
 
-        buildObjectsFromIdMap(rawResponse.all, Platform, function (raw) {
+        buildObjectsFromIdMap(rawResponse.all, Platform, (raw) => {
             raw.lastModifiedByMetric = {};
-            raw.lastModified.forEach(function (lastModified, index) {
+            raw.lastModified.forEach((lastModified, index) => {
                 raw.lastModifiedByMetric[raw.metrics[index]] = lastModified;
             });
-            raw.metrics = raw.metrics.map(function (id) { return Metric.findById(id); });
+            raw.metrics = raw.metrics.map((id) => { return Metric.findById(id); });
         });
         buildObjectsFromIdMap(rawResponse.builders, Builder);
         buildObjectsFromIdMap(rawResponse.repositories, Repository);
-        buildObjectsFromIdMap(rawResponse.bugTrackers, BugTracker, function (raw) {
+        buildObjectsFromIdMap(rawResponse.bugTrackers, BugTracker, (raw) => {
             if (raw.repositories)
-                raw.repositories = raw.repositories.map(function (id) { return Repository.findById(id); });
+                raw.repositories = raw.repositories.map((id) => { return Repository.findById(id); });
+        });
+        buildObjectsFromIdMap(rawResponse.triggerables, Triggerable, (raw) => {
+            raw.acceptedRepositories = raw.acceptedRepositories.map((repositoryId) => {
+                return Repository.findById(repositoryId);
+            });
         });
 
         Instrumentation.endMeasuringTime('Manifest', '_didFetchManifest');
diff --git a/Websites/perf.webkit.org/public/v3/models/triggerable.js b/Websites/perf.webkit.org/public/v3/models/triggerable.js
new file mode 100644 (file)
index 0000000..422b739
--- /dev/null
@@ -0,0 +1,37 @@
+class Triggerable extends LabeledObject {
+
+    constructor(id, object)
+    {
+        super(id, object);
+        this._name = object.name;
+        this._acceptedRepositories = object.acceptedRepositories;
+        this._configurationList = object.configurations;
+
+        let configurationMap = this.ensureNamedStaticMap('testConfigurations');
+        for (const config of object.configurations) {
+            const [testId, platformId] = config;
+            const key = `${testId}-${platformId}`;
+            console.assert(!(key in configurationMap));
+            configurationMap[key] = this;
+        }
+    }
+
+    acceptedRepositories() { return this._acceptedRepositories; }
+
+    static findByTestConfiguration(test, platform)
+    {
+        let configurationMap = this.ensureNamedStaticMap('testConfigurations');
+        if (!configurationMap)
+            return null;
+        for (; test; test = test.parentTest()) {
+            const key = `${test.id()}-${platform.id()}`;
+            if (key in configurationMap)
+                return configurationMap[key];
+        }
+        return null;
+    }
+
+}
+
+if (typeof module != 'undefined')
+    module.exports.Triggerable = Triggerable;
index bf30b59..76c5cf8 100644 (file)
@@ -16,6 +16,12 @@ class AnalysisTaskChartPane extends ChartPaneBase {
             this._page._chartSelectionDidChange();
     }
 
+    _updateStatus()
+    {
+        super._updateStatus();
+        this._page.render();
+    }
+
     selectedPoints()
     {
         var selection = this._mainChart ? this._mainChart.currentSelection() : null;
@@ -33,6 +39,7 @@ class AnalysisTaskPage extends PageWithHeading {
     {
         super('Analysis Task');
         this._task = null;
+        this._triggerable = null;
         this._relatedTasks = null;
         this._testGroups = null;
         this._renderedTestGroups = null;
@@ -120,20 +127,22 @@ class AnalysisTaskPage extends PageWithHeading {
         console.assert(!this._task);
 
         this._task = task;
-        var platform = task.platform();
-        var metric = task.metric();
-        var lastModified = platform.lastModified(metric);
+        const platform = task.platform();
+        const metric = task.metric();
+        const lastModified = platform.lastModified(metric);
+
+        this._triggerable = Triggerable.findByTestConfiguration(metric.test(), platform);
 
         this._measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), lastModified);
         this._measurementSet.fetchBetween(task.startTime(), task.endTime(), this._didFetchMeasurement.bind(this));
 
-        var formatter = metric.makeFormatter(4);
+        const formatter = metric.makeFormatter(4);
         this._analysisResultsViewer.setValueFormatter(formatter);
         this._testGroupResultsTable.setValueFormatter(formatter);
 
         this._chartPane.configure(platform.id(), metric.id());
 
-        var domain = ChartsPage.createDomainForAnalysisTask(task);
+        const domain = ChartsPage.createDomainForAnalysisTask(task);
         this._chartPane.setOverviewDomain(domain[0], domain[1]);
         this._chartPane.setMainDomain(domain[0], domain[1]);
 
@@ -265,7 +274,7 @@ class AnalysisTaskPage extends PageWithHeading {
 
         this.content().querySelector('.analysis-task-status').style.display = this._task ? null : 'none';
         this.content().querySelector('.overview-chart').style.display = this._task ? null : 'none';
-        this.content().querySelector('.test-group-view').style.display = this._task ? null : 'none';
+        this.content().querySelector('.test-group-view').style.display = this._task && this._testGroups && this._testGroups.length ? null : 'none';
         this._taskNameLabel.render();
 
         if (this._relatedTasks && this._task) {
@@ -288,6 +297,7 @@ class AnalysisTaskPage extends PageWithHeading {
         var b = selectedRange['B'];
         this._newTestGroupFormForViewer.setRootSetMap(a && b ? {'A': a.rootSet(), 'B': b.rootSet()} : null);
         this._newTestGroupFormForViewer.render();
+        this._newTestGroupFormForViewer.element().style.display = this._triggerable ? null : 'none';
 
         this._renderTestGroupList();
         this._renderTestGroupDetails();
@@ -299,6 +309,7 @@ class AnalysisTaskPage extends PageWithHeading {
         this._newTestGroupFormForChart.setRootSetMap(points && points.length >= 2 ?
                 {'A': points[0].rootSet(), 'B': points[points.length - 1].rootSet()} : null);
         this._newTestGroupFormForChart.render();
+        this._newTestGroupFormForChart.element().style.display = this._triggerable ? null : 'none';
 
         this._analysisResultsViewer.setCurrentTestGroup(this._currentTestGroup);
         this._analysisResultsViewer.render();
index 74e9cd6..f5a50be 100644 (file)
@@ -18,7 +18,7 @@ describe('/api/manifest', function () {
     it("should generate an empty manifest when database is empty", function (done) {
         TestServer.remoteAPI().getJSON('/api/manifest').then(function (manifest) {
             assert.deepEqual(Object.keys(manifest).sort(), ['all', 'bugTrackers', 'builders', 'dashboard', 'dashboards',
-                'elapsedTime', 'metrics', 'repositories', 'siteTitle', 'status', 'summaryPages', 'tests']);
+                'elapsedTime', 'metrics', 'repositories', 'siteTitle', 'status', 'summaryPages', 'tests', 'triggerables']);
 
             assert.equal(typeof(manifest.elapsedTime), 'number');
             delete manifest.elapsedTime;
@@ -33,6 +33,7 @@ describe('/api/manifest', function () {
                 metrics: {},
                 repositories: {},
                 tests: {},
+                triggerables: {},
                 summaryPages: [],
                 status: 'OK'
             });
@@ -275,4 +276,78 @@ describe('/api/manifest', function () {
         }).catch(done);
     });
 
+    it("should generate manifest with triggerables", function (done) {
+        let db = TestServer.database();
+        db.connect();
+        Promise.all([
+            db.insert('repositories', {id: 11, name: 'WebKit', url: 'https://trac.webkit.org/$1'}),
+            db.insert('repositories', {id: 9, name: 'OS X'}),
+            db.insert('build_triggerables', {id: 200, name: 'build.webkit.org'}),
+            db.insert('build_triggerables', {id: 201, name: 'ios-build.webkit.org'}),
+            db.insert('tests', {id: 1, name: 'SomeTest'}),
+            db.insert('tests', {id: 2, name: 'SomeOtherTest'}),
+            db.insert('tests', {id: 3, name: 'ChildTest', parent: 1}),
+            db.insert('platforms', {id: 23, name: 'iOS 9 iPhone 5s'}),
+            db.insert('platforms', {id: 46, name: 'Trunk Mavericks'}),
+            db.insert('test_metrics', {id: 5, test: 1, name: 'Time'}),
+            db.insert('test_metrics', {id: 8, test: 2, name: 'FrameRate'}),
+            db.insert('test_metrics', {id: 9, test: 3, name: 'Time'}),
+            db.insert('test_configurations', {id: 101, metric: 5, platform: 46, type: 'current'}),
+            db.insert('test_configurations', {id: 102, metric: 8, platform: 46, type: 'current'}),
+            db.insert('test_configurations', {id: 103, metric: 9, platform: 46, type: 'current'}),
+            db.insert('test_configurations', {id: 104, metric: 5, platform: 23, type: 'current'}),
+            db.insert('test_configurations', {id: 105, metric: 8, platform: 23, type: 'current'}),
+            db.insert('test_configurations', {id: 106, metric: 9, platform: 23, type: 'current'}),
+            db.insert('triggerable_repositories', {triggerable: 200, repository: 11}),
+            db.insert('triggerable_repositories', {triggerable: 201, repository: 11}),
+            db.insert('triggerable_configurations', {triggerable: 200, test: 1, platform: 46}),
+            db.insert('triggerable_configurations', {triggerable: 200, test: 2, platform: 46}),
+            db.insert('triggerable_configurations', {triggerable: 201, test: 1, platform: 23}),
+            db.insert('triggerable_configurations', {triggerable: 201, test: 2, platform: 23}),
+        ]).then(function () {
+            return Manifest.fetch();
+        }).then(function () {
+            let webkit = Repository.findById(11);
+            assert.equal(webkit.name(), 'WebKit');
+            assert.equal(webkit.urlForRevision(123), 'https://trac.webkit.org/123');
+
+            let osx = Repository.findById(9);
+            assert.equal(osx.name(), 'OS X');
+
+            let someTest = Test.findById(1);
+            assert.equal(someTest.name(), 'SomeTest');
+
+            let someOtherTest = Test.findById(2);
+            assert.equal(someOtherTest.name(), 'SomeOtherTest');
+
+            let childTest = Test.findById(3);
+            assert.equal(childTest.name(), 'ChildTest');
+
+            let ios9iphone5s = Platform.findById(23);
+            assert.equal(ios9iphone5s.name(), 'iOS 9 iPhone 5s');
+
+            let mavericks = Platform.findById(46);
+            assert.equal(mavericks.name(), 'Trunk Mavericks');
+
+            assert.equal(Triggerable.all().length, 2);
+
+            let osxTriggerable = Triggerable.findByTestConfiguration(someTest, mavericks);
+            assert.equal(osxTriggerable.name(), 'build.webkit.org');
+            assert.deepEqual(osxTriggerable.acceptedRepositories(), [webkit]);
+
+            assert.equal(Triggerable.findByTestConfiguration(someOtherTest, mavericks), osxTriggerable);
+            assert.equal(Triggerable.findByTestConfiguration(childTest, mavericks), osxTriggerable);
+
+            let iosTriggerable = Triggerable.findByTestConfiguration(someOtherTest, ios9iphone5s);
+            assert.notEqual(iosTriggerable, osxTriggerable);
+            assert.equal(iosTriggerable.name(), 'ios-build.webkit.org');
+            assert.deepEqual(iosTriggerable.acceptedRepositories(), [webkit]);
+
+            assert.equal(Triggerable.findByTestConfiguration(someOtherTest, ios9iphone5s), iosTriggerable);
+            assert.equal(Triggerable.findByTestConfiguration(childTest, ios9iphone5s), iosTriggerable);
+
+            done();
+        }).catch(done);
+    });
+
 });
index 71152b5..c0effcf 100644 (file)
@@ -15,6 +15,7 @@ MockData = {
         RootSet.clearStaticMap();
         Test.clearStaticMap();
         TestGroup.clearStaticMap();
+        Triggerable.clearStaticMap();
     },
     someTestId() { return 200; },
     somePlatformId() { return 65; },
index 5d1c8d5..4e63dc4 100644 (file)
@@ -910,23 +910,46 @@ describe('BuildbotTriggerable', function () {
 
         it('should update available triggerables', function (done) {
             let db = TestServer.database();
-            MockData.addMockData(db).then(function () {
+            MockData.addMockData(db).then(() => {
                 return Manifest.fetch();
-            }).then(function () {
+            }).then(() => {
                 return db.selectAll('triggerable_configurations', 'test');
-            }).then(function (configurations) {
+            }).then((configurations) => {
                 assert.equal(configurations.length, 0);
+                assert.equal(Triggerable.all().length, 1);
+
+                let triggerable = Triggerable.all()[0];
+                assert.equal(triggerable.name(), 'build-webkit');
+                assert.deepEqual(triggerable.acceptedRepositories(), []);
+
+                let test = Test.findById(MockData.someTestId());
+                let platform = Platform.findById(MockData.somePlatformId());
+                assert.equal(Triggerable.findByTestConfiguration(test, platform), null);
+
                 let config = MockData.mockTestSyncConfigWithSingleBuilder();
                 let logger = new MockLogger;
                 let slaveInfo = {name: 'sync-slave', password: 'password'};
-                let triggerable = new BuildbotTriggerable(config, TestServer.remoteAPI(), MockRemoteAPI, slaveInfo, logger);
-                return triggerable.updateTriggerable();
-            }).then(function () {
+                let buildbotTriggerable = new BuildbotTriggerable(config, TestServer.remoteAPI(), MockRemoteAPI, slaveInfo, logger);
+                return buildbotTriggerable.updateTriggerable();
+            }).then(() => {
+                MockData.resetV3Models();
+                assert.equal(Triggerable.all().length, 0);
+                return TestServer.remoteAPI().getJSON('/api/manifest');
+            }).then((manifestContent) => {
+                Manifest._didFetchManifest(manifestContent);
                 return db.selectAll('triggerable_configurations', 'test');
-            }).then(function (configurations) {
+            }).then((configurations) => {
                 assert.equal(configurations.length, 1);
                 assert.equal(configurations[0].test, MockData.someTestId());
                 assert.equal(configurations[0].platform, MockData.somePlatformId());
+
+                assert.equal(Triggerable.all().length, 1);
+
+                let test = Test.findById(MockData.someTestId());
+                let platform = Platform.findById(MockData.somePlatformId());
+                let triggerable = Triggerable.findByTestConfiguration(test, platform);
+                assert.equal(triggerable.name(), 'build-webkit');
+
                 done();
             }).catch(done);
         });
index 864f596..802add6 100644 (file)
@@ -143,6 +143,7 @@ const tableToPrefixMap = {
     'tests': 'test',
     'tracker_repositories': 'tracrepo',
     'triggerable_configurations': 'trigconfig',
+    'triggerable_repositories': 'trigrepo',
     'platforms': 'platform',
     'reports': 'report',
     'repositories': 'repository',
index d588b5f..db26ae6 100644 (file)
@@ -56,7 +56,12 @@ class RemoteAPI {
     getJSON(path)
     {
         return this.sendHttpRequest(path, 'GET', null, null).then(function (result) {
-            return JSON.parse(result.responseText);
+            try {
+                return JSON.parse(result.responseText);
+            } catch (error) {
+                console.error(result.responseText);
+                throw error;
+            }
         });
     }
 
index 7cefe2d..13113ef 100644 (file)
@@ -27,6 +27,7 @@ importFromV3('models/root-set.js', 'RootSet');
 importFromV3('models/test.js', 'Test');
 importFromV3('models/test-group.js', 'TestGroup');
 importFromV3('models/time-series.js', 'TimeSeries');
+importFromV3('models/triggerable.js', 'Triggerable');
 
 importFromV3('privileged-api.js', 'PrivilegedAPI');
 importFromV3('instrumentation.js', 'Instrumentation');