BuildbotSyncer should be able to fetch JSON from buildbot
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 30 Mar 2016 03:16:29 +0000 (03:16 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 30 Mar 2016 03:16:29 +0000 (03:16 +0000)
https://bugs.webkit.org/show_bug.cgi?id=155921

Reviewed by Joseph Pecoraro.

Added BuildbotSyncer.pullBuildbot which fetches pending, in-progress, and finished builds from buildbot
with lots of unit tests as this has historically been a source of subtle bugs in the old script.

New implementation fixes a subtle bug in the old pythons script which overlooked the possibility that
the state of some builds may change between each HTTP request. In the old script, we fetched the list
of the pending builds, and requested -1, -2, etc... builds for N times. But between each request,
a pending build may start running or an in-progress build finish and shift the offset by one. The new
script avoids this problem by first requesting all pending builds, then all in-progress and finished
builds in a single HTTP request. The results are then merged so that entries for in-progress and
finished builds would override the entries for pending builds if they overlap.

Also renamed RemoteAPI.fetchJSON to RemoteAPI.getJSON to match v3 UI's RemoteAPI. This change makes
the class interchangeable between frontend (public/v3/remote.js) and backend (tools/js/remote.js).

* server-tests/api-build-requests-tests.js:
* server-tests/api-manifest.js:
* tools/js/buildbot-syncer.js:
(BuildbotBuildEntry): Removed the unused argument "type". Store the syncer as an instance variable as
we'd need to query for the buildbot URL. Also fixed a bug that _isInProgress was true for finished
builds as 'currentStep' is always defined but null in those builds.
(BuildbotBuildEntry.prototype.buildNumber): Added.
(BuildbotBuildEntry.prototype.isPending): Added.
(BuildbotBuildEntry.prototype.hasFinished): Added.
(BuildbotSyncer.prototype.pullBuildbot): Added. Fetches pending builds first and then finished builds.
(BuildbotSyncer.prototype._pullRecentBuilds): Added. Fetches in-progress and finished builds.
(BuildbotSyncer.prototype.urlForPendingBuildsJSON): Added.
(BuildbotSyncer.prototype.urlForBuildJSON): Added.
(BuildbotSyncer.prototype.url): Added.
(BuildbotSyncer.prototype.urlForBuildNumber): Added.
* tools/js/remote.js:
(RemoteAPI.prototype.getJSON): Renamed from fetchJSON.
(RemoteAPI.prototype.getJSONWithStatus): Renamed from fetchJSONWithStatus.
* tools/js/v3-models.js: Load tools/js/remote.js instead of public/v3/remote.js inside node.
* unit-tests/buildbot-syncer-tests.js: Added a lot of unit tests for BuildbotSyncer.pullBuildbot
(samplePendingBuild):
(sampleInProgressBuild): Added.
(sampleFinishedBuild): Added.
* unit-tests/resources/mock-remote-api.js:
(global.RemoteAPI.getJSON): Use the same mock as getJSONWithStatus.

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

Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/server-tests/api-build-requests-tests.js
Websites/perf.webkit.org/server-tests/api-manifest.js
Websites/perf.webkit.org/tools/js/buildbot-syncer.js
Websites/perf.webkit.org/tools/js/remote.js
Websites/perf.webkit.org/tools/js/v3-models.js
Websites/perf.webkit.org/unit-tests/buildbot-syncer-tests.js
Websites/perf.webkit.org/unit-tests/resources/mock-remote-api.js

index a0c4b25..d3672b0 100644 (file)
@@ -1,3 +1,50 @@
+2016-03-29  Ryosuke Niwa  <rniwa@webkit.org>
+
+        BuildbotSyncer should be able to fetch JSON from buildbot
+        https://bugs.webkit.org/show_bug.cgi?id=155921
+
+        Reviewed by Joseph Pecoraro.
+
+        Added BuildbotSyncer.pullBuildbot which fetches pending, in-progress, and finished builds from buildbot
+        with lots of unit tests as this has historically been a source of subtle bugs in the old script.
+
+        New implementation fixes a subtle bug in the old pythons script which overlooked the possibility that
+        the state of some builds may change between each HTTP request. In the old script, we fetched the list
+        of the pending builds, and requested -1, -2, etc... builds for N times. But between each request,
+        a pending build may start running or an in-progress build finish and shift the offset by one. The new
+        script avoids this problem by first requesting all pending builds, then all in-progress and finished
+        builds in a single HTTP request. The results are then merged so that entries for in-progress and
+        finished builds would override the entries for pending builds if they overlap.
+
+        Also renamed RemoteAPI.fetchJSON to RemoteAPI.getJSON to match v3 UI's RemoteAPI. This change makes
+        the class interchangeable between frontend (public/v3/remote.js) and backend (tools/js/remote.js).
+
+        * server-tests/api-build-requests-tests.js:
+        * server-tests/api-manifest.js:
+        * tools/js/buildbot-syncer.js:
+        (BuildbotBuildEntry): Removed the unused argument "type". Store the syncer as an instance variable as
+        we'd need to query for the buildbot URL. Also fixed a bug that _isInProgress was true for finished
+        builds as 'currentStep' is always defined but null in those builds.
+        (BuildbotBuildEntry.prototype.buildNumber): Added.
+        (BuildbotBuildEntry.prototype.isPending): Added.
+        (BuildbotBuildEntry.prototype.hasFinished): Added.
+        (BuildbotSyncer.prototype.pullBuildbot): Added. Fetches pending builds first and then finished builds.
+        (BuildbotSyncer.prototype._pullRecentBuilds): Added. Fetches in-progress and finished builds.
+        (BuildbotSyncer.prototype.urlForPendingBuildsJSON): Added.
+        (BuildbotSyncer.prototype.urlForBuildJSON): Added.
+        (BuildbotSyncer.prototype.url): Added.
+        (BuildbotSyncer.prototype.urlForBuildNumber): Added.
+        * tools/js/remote.js:
+        (RemoteAPI.prototype.getJSON): Renamed from fetchJSON.
+        (RemoteAPI.prototype.getJSONWithStatus): Renamed from fetchJSONWithStatus.
+        * tools/js/v3-models.js: Load tools/js/remote.js instead of public/v3/remote.js inside node.
+        * unit-tests/buildbot-syncer-tests.js: Added a lot of unit tests for BuildbotSyncer.pullBuildbot
+        (samplePendingBuild):
+        (sampleInProgressBuild): Added.
+        (sampleFinishedBuild): Added.
+        * unit-tests/resources/mock-remote-api.js:
+        (global.RemoteAPI.getJSON): Use the same mock as getJSONWithStatus.
+
 2016-03-24  Ryosuke Niwa  <rniwa@webkit.org>
 
         Migrate admin-regenerate-manifest.js to mocha.js and test v3 UI code
index 09965f2..0576628 100644 (file)
@@ -8,7 +8,7 @@ describe('/api/build-requests', function () {
     TestServer.inject();
 
     it('should return "TriggerableNotFound" when the database is empty', function (done) {
-        TestServer.remoteAPI().fetchJSON('/api/build-requests/build-webkit').then(function (content) {
+        TestServer.remoteAPI().getJSON('/api/build-requests/build-webkit').then(function (content) {
             assert.equal(content['status'], 'TriggerableNotFound');
             done();
         }).catch(done);
@@ -18,7 +18,7 @@ describe('/api/build-requests', function () {
         TestServer.database().connect().then(function () {
             return TestServer.database().insert('build_triggerables', {name: 'build-webkit'});
         }).then(function () {
-            return TestServer.remoteAPI().fetchJSON('/api/build-requests/build-webkit');
+            return TestServer.remoteAPI().getJSON('/api/build-requests/build-webkit');
         }).then(function (content) {
             assert.equal(content['status'], 'OK');
             assert.deepEqual(content['buildRequests'], []);
@@ -61,7 +61,7 @@ describe('/api/build-requests', function () {
         db.connect().then(function () {
             return addMockData(db);
         }).then(function () {
-            return TestServer.remoteAPI().fetchJSONWithStatus('/api/build-requests/build-webkit');
+            return TestServer.remoteAPI().getJSONWithStatus('/api/build-requests/build-webkit');
         }).then(function (content) {
             assert.deepEqual(Object.keys(content).sort(), ['buildRequests', 'rootSets', 'roots', 'status']);
 
@@ -119,7 +119,7 @@ describe('/api/build-requests', function () {
         db.connect().then(function () {
             return addMockData(db);
         }).then(function () {
-            return TestServer.remoteAPI().fetchJSONWithStatus('/api/build-requests/build-webkit?useLegacyIdResolution=true');
+            return TestServer.remoteAPI().getJSONWithStatus('/api/build-requests/build-webkit?useLegacyIdResolution=true');
         }).then(function (content) {
             assert.deepEqual(Object.keys(content).sort(), ['buildRequests', 'rootSets', 'roots', 'status']);
 
index 6bd06d5..caae155 100644 (file)
@@ -20,7 +20,7 @@ describe('/api/build-requests', function () {
     });
 
     it("should generate an empty manifest when database is empty", function (done) {
-        TestServer.remoteAPI().fetchJSON('/api/manifest').then(function (manifest) {
+        TestServer.remoteAPI().getJSON('/api/manifest').then(function (manifest) {
             assert.deepEqual(Object.keys(manifest).sort(), ['all', 'bugTrackers', 'builders', 'dashboard', 'dashboards',
                 'elapsedTime', 'metrics', 'repositories', 'siteTitle', 'status', 'tests']);
 
@@ -49,7 +49,7 @@ describe('/api/build-requests', function () {
     it("should generate manifest with bug trackers without repositories", function (done) {
         TestServer.database().connect();
         TestServer.database().insert('bug_trackers', bugzillaData).then(function () {
-            return TestServer.remoteAPI().fetchJSON('/api/manifest');
+            return TestServer.remoteAPI().getJSON('/api/manifest');
         }).then(function (content) {
             assert.deepEqual(content.bugTrackers, {1: {name: 'Bugzilla', bugUrl: 'https://webkit.org/b/$number',
                 newBugUrl: 'https://bugs.webkit.org/', repositories: null}});
@@ -78,7 +78,7 @@ describe('/api/build-requests', function () {
             db.insert('tracker_repositories', {tracker: radarData.id, repository: 9}),
             db.insert('tracker_repositories', {tracker: radarData.id, repository: 22}),
         ]).then(function () {
-            return TestServer.remoteAPI().fetchJSON('/api/manifest');
+            return TestServer.remoteAPI().getJSON('/api/manifest');
         }).then(function (content) {
             let manifest = Manifest._didFetchManifest(content);
 
@@ -119,7 +119,7 @@ describe('/api/build-requests', function () {
                 build_url: 'https://build.webkit.org/builders/$builderName/build/$buildNumber'}),
             db.insert('builders', {id: 2, name: 'SomeOtherBuilder', password_hash: 'b'})
         ]).then(function () {
-            return TestServer.remoteAPI().fetchJSON('/api/manifest');
+            return TestServer.remoteAPI().getJSON('/api/manifest');
         }).then(function (content) {
             assert.deepEqual(content.builders, {
                 '1': {name: 'SomeBuilder', buildUrl: 'https://build.webkit.org/builders/$builderName/build/$buildNumber'},
@@ -166,7 +166,7 @@ describe('/api/build-requests', function () {
             db.insert('test_configurations', {id: 106, metric: 5, platform: 23, type: 'current'}),
             db.insert('test_configurations', {id: 107, metric: 5, platform: 23, type: 'baseline'}),
         ]).then(function () {
-            return TestServer.remoteAPI().fetchJSON('/api/manifest');
+            return TestServer.remoteAPI().getJSON('/api/manifest');
         }).then(function (content) {
             assert.deepEqual(content.tests, {
                 "1": {"name": "SomeTest", "parentId": null, "url": null},
index 7b7b62d..8b2cc1f 100644 (file)
@@ -5,19 +5,20 @@ let assert = require('assert');
 require('./v3-models.js');
 
 class BuildbotBuildEntry {
-    constructor(syncer, type, rawData)
+    constructor(syncer, rawData)
     {
         assert.equal(syncer.builderName(), rawData['builderName']);
 
+        this._syncer = syncer;
         this._slaveName = null;
         this._buildRequestId = null;
-        this._isInProgress = 'currentStep' in rawData;
+        this._isInProgress = rawData['currentStep'] || (rawData['times'] && !rawData['times'][1]);
         this._buildNumber = rawData['number'];
 
-        for (var propertyTuple of (rawData['properties'] || [])) {
+        for (let propertyTuple of (rawData['properties'] || [])) {
             // e.g. ['build_request_id', '16733', 'Force Build Form']
-            var name = propertyTuple[0];
-            var value = propertyTuple[1];
+            let name = propertyTuple[0];
+            let value = propertyTuple[1];
             if (name == syncer._slavePropertyName)
                 this._slaveName = value;
             else if (name == syncer._buildRequestPropertyName)
@@ -25,9 +26,13 @@ class BuildbotBuildEntry {
         }
     }
 
+    buildNumber() { return this._buildNumber; }
     slaveName() { return this._slaveName; }
     buildRequestId() { return this._buildRequestId; }
+    isPending() { return !this._buildNumber; }
     isInProgress() { return this._isInProgress; }
+    hasFinished() { return !this.isPending() && !this.isInProgress(); }
+    url() { return this.isPending() ? this._syncer.url() : this._syncer.urlForBuildNumber(this._buildNumber); }
 }
 
 class BuildbotSyncer {
@@ -47,24 +52,57 @@ class BuildbotSyncer {
     builderName() { return this._builderName; }
     platformName() { return this._platformName; }
 
-    fetchPendingRequests()
+    pullBuildbot(count)
     {
-        return RemoteAPI.fetchJSON(`${this._url}/json/builders/${this._name}/pendingBuilds`).then(function (content) {
-            var requests = [];
-            for (var entry of content) {
-                var properties = entry['properties'];
-                if (!properties)
-                    continue;
-                for (var propertyTuple of properties) {
-                    // e.g. ['build_request_id', '16733', 'Force Build Form']
-                    if (propertyTuple[0] == this._buildRequestPropertyName)
-                        requests.push(propertyTuple[1]);
-                }
+        let self = this;
+        return RemoteAPI.getJSON(this.urlForPendingBuildsJSON()).then(function (content) {
+            let pendingEntries = content.map(function (entry) { return new BuildbotBuildEntry(self, entry); });
+
+            return self._pullRecentBuilds(count).then(function (entries) {
+                let entryByRequest = {};
+
+                for (let entry of pendingEntries)
+                    entryByRequest[entry.buildRequestId()] = entry;
+
+                for (let entry of entries)
+                    entryByRequest[entry.buildRequestId()] = entry;
+
+                return entryByRequest;
+            });
+        });
+    }
+
+    _pullRecentBuilds(count)
+    {
+        if (!count)
+            return Promise.resolve([]);
+
+        let selectedBuilds = new Array(count);
+        for (let i = 0; i < count; i++)
+            selectedBuilds[i] = -i - 1;
+
+        let self = this;
+        return RemoteAPI.getJSON(this.urlForBuildJSON(selectedBuilds)).then(function (content) {
+            let entries = [];
+            for (let index of selectedBuilds) {
+                let entry = content[index];
+                if (entry && !entry['error'])
+                    entries.push(new BuildbotBuildEntry(self, entry));
             }
-            return requests;
+            return entries;
         });
     }
 
+    urlForPendingBuildsJSON() { return `${this._url}/json/builders/${this._builderName}/pendingBuilds`; }
+    urlForBuildJSON(selectedBuilds)
+    {
+        return `${this._url}/json/builders/${this._builderName}/builds/?`
+            + selectedBuilds.map(function (number) { return 'select=' + number; }).join('&');
+    }
+
+    url() { return `${this._url}/builders/${this._builderName}/`; }
+    urlForBuildNumber(number) { return `${this._url}/builders/${this._builderName}/builds/${number}`; }
+
     _propertiesForBuildRequest(buildRequest)
     {
         console.assert(buildRequest instanceof BuildRequest);
index 64e381d..3454bed 100644 (file)
@@ -22,7 +22,7 @@ let RemoteAPI = new (class RemoteAPI {
         this._server = server;
     }
 
-    fetchJSON(path, data)
+    getJSON(path, data)
     {
         let contentType = null;
         if (data) {
@@ -34,9 +34,9 @@ let RemoteAPI = new (class RemoteAPI {
         });
     }
 
-    fetchJSONWithStatus(path, data)
+    getJSONWithStatus(path, data)
     {
-        return this.fetchJSON(path, data).then(function (result) {
+        return this.getJSON(path, data).then(function (result) {
             if (result['status'] != 'OK')
                 return Promise.reject(result);
             return result;
index a438414..c820a5d 100644 (file)
@@ -29,4 +29,7 @@ importFromV3('models/test-group.js', 'TestGroup');
 
 importFromV3('instrumentation.js', 'Instrumentation');
 
+// RemoteAPI has a different implementation in node since XHR isn't available.
+global.RemoteAPI = require('./remote.js').RemoteAPI;
+
 global.Statistics = require('../../public/shared/statistics.js');
index 1b9bb12..72fa09f 100644 (file)
@@ -2,8 +2,8 @@
 
 let assert = require('assert');
 
-require('./resources/mock-remote-api.js');
 require('../tools/js/v3-models.js');
+require('./resources/mock-remote-api.js');
 require('./resources/mock-v3-models.js');
 
 let BuildbotBuildEntry = require('../tools/js/buildbot-syncer.js').BuildbotBuildEntry;
@@ -83,12 +83,13 @@ function createSampleBuildRequest()
     return request;
 }
 
-let samplePendingBuilds = [
-    {
+function samplePendingBuild(buildRequestId)
+{
+    return {
         'builderName': 'ABTest-iPad-RunBenchmark-Tests',
         'builds': [],
         'properties': [
-            ['build_request_id', '16733', 'Force Build Form'],
+            ['build_request_id', buildRequestId || '16733', 'Force Build Form'],
             ['desired_image', '13A452', 'Force Build Form'],
             ['owner', '<unknown>', 'Force Build Form'],
             ['test_name', 'speedometer', 'Force Build Form'],
@@ -98,7 +99,7 @@ let samplePendingBuilds = [
                 JSON.stringify(sampleRootSetData),
                 'Force Build Form'
             ],
-            ['scheduler', 'ABTest-iPad-Performance-Tests-ForceScheduler', 'Scheduler']
+            ['scheduler', 'ABTest-iPad-RunBenchmark-Tests-ForceScheduler', 'Scheduler']
         ],
         'source': {
             'branch': '',
@@ -110,14 +111,176 @@ let samplePendingBuilds = [
             'revision': ''
         },
         'submittedAt': 1458704983
-    }
-];
+    };
+}
 
-describe('BuildbotSyncer', function () {
-    describe('fetchPendingBuilds', function () {
-        BuildbotSyncer.fetchPendingBuilds
-    });
+function sampleInProgressBuild()
+{
+    return {
+        'blame': [],
+        'builderName': 'ABTest-iPad-RunBenchmark-Tests',
+        'currentStep': {
+            'eta': 0.26548067698460565,
+            'expectations': [['output', 845, 1315.0]],
+            'hidden': false,
+            'isFinished': false,
+            'isStarted': true,
+            'logs': [['stdio', 'https://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/614/steps/Some%20step/logs/stdio']],
+            'name': 'Some step',
+            'results': [null,[]],
+            'statistics': {},
+            'step_number': 1,
+            'text': [''],
+            'times': [1458718657.581628, null],
+            'urls': {}
+        },
+        'eta': 6497.991612434387,
+        'logs': [['stdio','https://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/614/steps/shell/logs/stdio']],
+        'number': 614,
+        'properties': [
+            ['build_request_id', '16733', 'Force Build Form'],
+            ['buildername', 'ABTest-iPad-RunBenchmark-Tests', 'Builder'],
+            ['buildnumber', 614, 'Build'],
+            ['desired_image', '13A452', 'Force Build Form'],
+            ['owner', '<unknown>', 'Force Build Form'],
+            ['reason', 'force build', 'Force Build Form'],
+            ['roots_dict', JSON.stringify(sampleRootSetData), 'Force Build Form'],
+            ['scheduler', 'ABTest-iPad-RunBenchmark-Tests-ForceScheduler', 'Scheduler'],
+            ['slavename', 'ABTest-iPad-0', 'BuildSlave'],
+        ],
+        'reason': 'A build was forced by \'<unknown>\': force build',
+        'results': null,
+        'slave': 'ABTest-iPad-0',
+        'sourceStamps': [{'branch': '', 'changes': [], 'codebase': 'compiler-rt', 'hasPatch': false, 'project': '', 'repository': '', 'revision': ''}],
+        'steps': [
+            {
+                'eta': null,
+                'expectations': [['output',2309,2309.0]],
+                'hidden': false,
+                'isFinished': true,
+                'isStarted': true,
+                'logs': [['stdio', 'https://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/614/steps/shell/logs/stdio']],
+                'name': 'Finished step',
+                'results': [0, []],
+                'statistics': {},
+                'step_number': 0,
+                'text': [''],
+                'times': [1458718655.419865, 1458718655.453633],
+                'urls': {}
+            },
+            {
+                'eta': 0.26548067698460565,
+                'expectations': [['output', 845, 1315.0]],
+                'hidden': false,
+                'isFinished': false,
+                'isStarted': true,
+                'logs': [['stdio', 'https://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/614/steps/Some%20step/logs/stdio']],
+                'name': 'Some step',
+                'results': [null,[]],
+                'statistics': {},
+                'step_number': 1,
+                'text': [''],
+                'times': [1458718657.581628, null],
+                'urls': {}
+            },
+            {
+                'eta': null,
+                'expectations': [['output', null, null]],
+                'hidden': false,
+                'isFinished': false,
+                'isStarted': false,
+                'logs': [],
+                'name': 'Some other step',
+                'results': [null, []],
+                'statistics': {},
+                'step_number': 2,
+                'text': [],
+                'times': [null, null],
+                'urls': {}
+            },
+        ],
+        'text': [],
+        'times': [1458718655.415821, null]
+    };
+}
 
+function sampleFinishedBuild(buildRequestId)
+{
+    return {
+        'blame': [],
+        'builderName': 'ABTest-iPad-RunBenchmark-Tests',
+        'currentStep': null,
+        'eta': null,
+        'logs': [['stdio','https://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/1755/steps/shell/logs/stdio']],
+        'number': 1755,
+        'properties': [
+            ['build_request_id', buildRequestId || '18935', 'Force Build Form'],
+            ['buildername', 'ABTest-iPad-RunBenchmark-Tests', 'Builder'],
+            ['buildnumber', 1755, 'Build'],
+            ['desired_image', '13A452', 'Force Build Form'],
+            ['owner', '<unknown>', 'Force Build Form'],
+            ['reason', 'force build', 'Force Build Form'],
+            ['roots_dict', JSON.stringify(sampleRootSetData), 'Force Build Form'],
+            ['scheduler', 'ABTest-iPad-RunBenchmark-Tests-ForceScheduler', 'Scheduler'],
+            ['slavename', 'ABTest-iPad-0', 'BuildSlave'],
+        ],
+        'reason': 'A build was forced by \'<unknown>\': force build',
+        'results': 2,
+        'slave': 'ABTest-iPad-0',
+        'sourceStamps': [{'branch': '', 'changes': [], 'codebase': 'compiler-rt', 'hasPatch': false, 'project': '', 'repository': '', 'revision': ''}],
+        'steps': [
+            {
+                'eta': null,
+                'expectations': [['output',2309,2309.0]],
+                'hidden': false,
+                'isFinished': true,
+                'isStarted': true,
+                'logs': [['stdio', 'https://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/614/steps/shell/logs/stdio']],
+                'name': 'Finished step',
+                'results': [0, []],
+                'statistics': {},
+                'step_number': 0,
+                'text': [''],
+                'times': [1458718655.419865, 1458718655.453633],
+                'urls': {}
+            },
+            {
+                'eta': null,
+                'expectations': [['output', 845, 1315.0]],
+                'hidden': false,
+                'isFinished': true,
+                'isStarted': true,
+                'logs': [['stdio', 'https://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/614/steps/Some%20step/logs/stdio']],
+                'name': 'Some step',
+                'results': [null,[]],
+                'statistics': {},
+                'step_number': 1,
+                'text': [''],
+                'times': [1458718657.581628, null],
+                'urls': {}
+            },
+            {
+                'eta': null,
+                'expectations': [['output', null, null]],
+                'hidden': false,
+                'isFinished': true,
+                'isStarted': true,
+                'logs': [],
+                'name': 'Some other step',
+                'results': [null, []],
+                'statistics': {},
+                'step_number': 2,
+                'text': [],
+                'times': [null, null],
+                'urls': {}
+            },
+        ],
+        'text': [],
+        'times': [1458937478.25837, 1458946147.173785]
+    };
+}
+
+describe('BuildbotSyncer', function () {
     describe('_loadConfig', function () {
 
         function smallConfiguration()
@@ -292,4 +455,226 @@ describe('BuildbotSyncer', function () {
         });
     });
 
-});
\ No newline at end of file
+    describe('pullBuildbot', function () {
+        it('should fetch pending builds from the right URL', function () {
+            let syncer = BuildbotSyncer._loadConfig('http://build.webkit.org', sampleiOSConfig())[3];
+            assert.equal(syncer.builderName(), 'ABTest-iPad-RunBenchmark-Tests');
+            let expectedURL = 'http://build.webkit.org/json/builders/ABTest-iPad-RunBenchmark-Tests/pendingBuilds';
+            assert.equal(syncer.urlForPendingBuildsJSON(), expectedURL);
+            syncer.pullBuildbot();
+            assert.equal(requests.length, 1);
+            assert.equal(requests[0].url, expectedURL);
+        });
+
+        it('should fetch recent builds once pending builds have been fetched', function (done) {
+            let syncer = BuildbotSyncer._loadConfig('http://build.webkit.org', sampleiOSConfig())[3];
+            assert.equal(syncer.builderName(), 'ABTest-iPad-RunBenchmark-Tests');
+
+            syncer.pullBuildbot(1);
+            assert.equal(requests.length, 1);
+            assert.equal(requests[0].url, 'http://build.webkit.org/json/builders/ABTest-iPad-RunBenchmark-Tests/pendingBuilds');
+            requests[0].resolve([]);
+            Promise.resolve().then(function () {
+                assert.equal(requests.length, 2);
+                assert.equal(requests[1].url, 'http://build.webkit.org/json/builders/ABTest-iPad-RunBenchmark-Tests/builds/?select=-1');
+                done();
+            }).catch(done);
+        });
+
+        it('should fetch the right number of recent builds', function (done) {
+            let syncer = BuildbotSyncer._loadConfig('http://build.webkit.org', sampleiOSConfig())[3];
+
+            syncer.pullBuildbot(3);
+            assert.equal(requests.length, 1);
+            assert.equal(requests[0].url, 'http://build.webkit.org/json/builders/ABTest-iPad-RunBenchmark-Tests/pendingBuilds');
+            requests[0].resolve([]);
+            Promise.resolve().then(function () {
+                assert.equal(requests.length, 2);
+                assert.equal(requests[1].url, 'http://build.webkit.org/json/builders/ABTest-iPad-RunBenchmark-Tests/builds/?select=-1&select=-2&select=-3');
+                done();
+            }).catch(done);
+        });
+
+        it('should create BuildbotBuildEntry for pending builds', function (done) {
+            let syncer = BuildbotSyncer._loadConfig('http://build.webkit.org', sampleiOSConfig())[3];
+            let promise = syncer.pullBuildbot();
+            requests[0].resolve([samplePendingBuild()]);
+            promise.then(function (entries) {
+                assert.deepEqual(Object.keys(entries), ['16733']);
+                let entry = entries['16733'];
+                assert.ok(entry instanceof BuildbotBuildEntry);
+                assert.ok(!entry.buildNumber());
+                assert.ok(!entry.slaveName());
+                assert.equal(entry.buildRequestId(), 16733);
+                assert.ok(entry.isPending());
+                assert.ok(!entry.isInProgress());
+                assert.ok(!entry.hasFinished());
+                assert.equal(entry.url(), 'http://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/');
+                done();
+            }).catch(done);
+        });
+
+        it('should create BuildbotBuildEntry for in-progress builds', function (done) {
+            let syncer = BuildbotSyncer._loadConfig('http://build.webkit.org', sampleiOSConfig())[3];
+
+            let promise = syncer.pullBuildbot(1);
+            assert.equal(requests.length, 1);
+            requests[0].resolve([]);
+            Promise.resolve().then(function () {
+                assert.equal(requests.length, 2);
+                requests[1].resolve({[-1]: sampleInProgressBuild()});
+            }).catch(done);
+
+            promise.then(function (entries) {
+                assert.deepEqual(Object.keys(entries), ['16733']);
+                let entry = entries['16733'];
+                assert.ok(entry instanceof BuildbotBuildEntry);
+                assert.equal(entry.buildNumber(), 614);
+                assert.equal(entry.slaveName(), 'ABTest-iPad-0');
+                assert.equal(entry.buildRequestId(), 16733);
+                assert.ok(!entry.isPending());
+                assert.ok(entry.isInProgress());
+                assert.ok(!entry.hasFinished());
+                assert.equal(entry.url(), 'http://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/614');
+                done();
+            }).catch(done);
+        });
+
+        it('should create BuildbotBuildEntry for finished builds', function (done) {
+            let syncer = BuildbotSyncer._loadConfig('http://build.webkit.org', sampleiOSConfig())[3];
+
+            let promise = syncer.pullBuildbot(1);
+            assert.equal(requests.length, 1);
+            requests[0].resolve([]);
+            Promise.resolve().then(function () {
+                assert.equal(requests.length, 2);
+                requests[1].resolve({[-1]: sampleFinishedBuild()});
+            }).catch(done);
+
+            promise.then(function (entries) {
+                assert.deepEqual(Object.keys(entries), ['18935']);
+                let entry = entries['18935'];
+                assert.ok(entry instanceof BuildbotBuildEntry);
+                assert.equal(entry.buildNumber(), 1755);
+                assert.equal(entry.slaveName(), 'ABTest-iPad-0');
+                assert.equal(entry.buildRequestId(), 18935);
+                assert.ok(!entry.isPending());
+                assert.ok(!entry.isInProgress());
+                assert.ok(entry.hasFinished());
+                assert.equal(entry.url(), 'http://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/1755');
+                done();
+            }).catch(done);
+        });
+
+        it('should create BuildbotBuildEntry for mixed pending, in-progress, finished, and missing builds', function (done) {
+            let syncer = BuildbotSyncer._loadConfig('http://build.webkit.org', sampleiOSConfig())[3];
+
+            let promise = syncer.pullBuildbot(5);
+            assert.equal(requests.length, 1);
+
+            requests[0].resolve([samplePendingBuild(123, 456)]);
+
+            Promise.resolve().then(function () {
+                assert.equal(requests.length, 2);
+                requests[1].resolve({[-1]: sampleFinishedBuild(), [-2]: {'error': 'Not available'}, [-4]: sampleInProgressBuild()});
+            }).catch(done);
+
+            promise.then(function (entries) {
+                assert.deepEqual(Object.keys(entries), ['123', '16733', '18935']);
+
+                let entry = entries['123'];
+                assert.ok(entry instanceof BuildbotBuildEntry);
+                assert.equal(entry.buildNumber(), null);
+                assert.equal(entry.slaveName(), null);
+                assert.equal(entry.buildRequestId(), 123);
+                assert.ok(entry.isPending());
+                assert.ok(!entry.isInProgress());
+                assert.ok(!entry.hasFinished());
+                assert.equal(entry.url(), 'http://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/');
+
+                entry = entries['16733'];
+                assert.ok(entry instanceof BuildbotBuildEntry);
+                assert.equal(entry.buildNumber(), 614);
+                assert.equal(entry.slaveName(), 'ABTest-iPad-0');
+                assert.equal(entry.buildRequestId(), 16733);
+                assert.ok(!entry.isPending());
+                assert.ok(entry.isInProgress());
+                assert.ok(!entry.hasFinished());
+                assert.equal(entry.url(), 'http://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/614');
+
+                entry = entries['18935'];
+                assert.ok(entry instanceof BuildbotBuildEntry);
+                assert.equal(entry.buildNumber(), 1755);
+                assert.equal(entry.slaveName(), 'ABTest-iPad-0');
+                assert.equal(entry.buildRequestId(), 18935);
+                assert.ok(!entry.isPending());
+                assert.ok(!entry.isInProgress());
+                assert.ok(entry.hasFinished());
+                assert.equal(entry.url(), 'http://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/1755');
+
+                done();
+            }).catch(done);
+        });
+
+        it('should override BuildbotBuildEntry for pending builds by in-progress builds', function (done) {
+            let syncer = BuildbotSyncer._loadConfig('http://build.webkit.org', sampleiOSConfig())[3];
+
+            let promise = syncer.pullBuildbot(5);
+            assert.equal(requests.length, 1);
+
+            requests[0].resolve([samplePendingBuild()]);
+
+            Promise.resolve().then(function () {
+                assert.equal(requests.length, 2);
+                requests[1].resolve({[-1]: sampleInProgressBuild()});
+            }).catch(done);
+
+            promise.then(function (entries) {
+                assert.deepEqual(Object.keys(entries), ['16733']);
+
+                let entry = entries['16733'];
+                assert.ok(entry instanceof BuildbotBuildEntry);
+                assert.equal(entry.buildNumber(), 614);
+                assert.equal(entry.slaveName(), 'ABTest-iPad-0');
+                assert.equal(entry.buildRequestId(), 16733);
+                assert.ok(!entry.isPending());
+                assert.ok(entry.isInProgress());
+                assert.ok(!entry.hasFinished());
+                assert.equal(entry.url(), 'http://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/614');
+
+                done();
+            }).catch(done);
+        });
+
+        it('should override BuildbotBuildEntry for pending builds by finished builds', function (done) {
+            let syncer = BuildbotSyncer._loadConfig('http://build.webkit.org', sampleiOSConfig())[3];
+
+            let promise = syncer.pullBuildbot(5);
+            assert.equal(requests.length, 1);
+
+            requests[0].resolve([samplePendingBuild()]);
+
+            Promise.resolve().then(function () {
+                assert.equal(requests.length, 2);
+                requests[1].resolve({[-1]: sampleFinishedBuild(16733)});
+            }).catch(done);
+
+            promise.then(function (entries) {
+                assert.deepEqual(Object.keys(entries), ['16733']);
+
+                let entry = entries['16733'];
+                assert.ok(entry instanceof BuildbotBuildEntry);
+                assert.equal(entry.buildNumber(), 1755);
+                assert.equal(entry.slaveName(), 'ABTest-iPad-0');
+                assert.equal(entry.buildRequestId(), 16733);
+                assert.ok(!entry.isPending());
+                assert.ok(!entry.isInProgress());
+                assert.ok(entry.hasFinished());
+                assert.equal(entry.url(), 'http://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/1755');
+
+                done();
+            }).catch(done);
+        });
+
+    });
+});
index 534a46f..5e54b6f 100644 (file)
@@ -5,9 +5,9 @@ if (!assert.notReached)
 
 global.requests = [];
 global.RemoteAPI = {
-    getJSON: function ()
+    getJSON: function (url)
     {
-        assert.notReached();
+        return this.getJSONWithStatus(url);
     },
     getJSONWithStatus: function (url)
     {