+2016-04-05 Ryosuke Niwa <rniwa@webkit.org>
+
+ New buildbot syncing scripts that supports multiple builders and slaves
+ https://bugs.webkit.org/show_bug.cgi?id=156269
+
+ Reviewed by Chris Dumez.
+
+ Add sync-buildbot.js that supports scheduling A/B testing jobs on multiple builders and slaves.
+ The old python script (sync-with-buildbot.py) could only support a single builder and slave
+ for each platform, test pair.
+
+ The main logic is implemented in BuildbotTriggerable.syncOnce. Various helper methods are added
+ throughout the codebase and tests have been refactored.
+
+ BuildbotSyncer has been updated to support multiple platform, test pairs. It's now responsible
+ for syncing everything on each builder (on a buildbot).
+
+ Added more unit tests for BuildbotSyncer and server tests for BuildbotTriggerable, and refactored
+ test helpers and mocks as needed.
+
+ * public/v3/models/build-request.js:
+ (BuildRequest.prototype.status): Added.
+ (BuildRequest.prototype.isScheduled): Added.
+ * public/v3/models/metric.js:
+ (Metric.prototype.fullName): Added.
+ * public/v3/models/platform.js:
+ (Platform): Added the map based on platform name.
+ (Platform.findByName): Added.
+ * public/v3/models/test.js:
+ (Test.topLevelTests):
+ (Test.findByPath): Added. Finds a test based on an array of test names; e.g. ['A', 'B'] would
+ find the test whose name is "B" which has a parent test named "A".
+ (Test.prototype.fullName): Added.
+ * server-tests/api-build-requests-tests.js:
+ (addMockData): Moved to resources/mock-data.js.
+ (addAnotherMockTestGroup): Ditto.
+ * server-tests/resources/mock-data.js: Added.
+ (MockData.resetV3Models): Added.
+ (MockData.addMockData): Moved from api-build-requests-tests.js.
+ (MockData.addAnotherMockTestGroup): Ditto.
+ (MockData.mockTestSyncConfigWithSingleBuilder): Added.
+ (MockData.mockTestSyncConfigWithTwoBuilders): Added.
+ (MockData.pendingBuild): Added.
+ (MockData.runningBuild): Added.
+ (MockData.finishedBuild): Added.
+ * server-tests/resources/test-server.js:
+ (TestServer):
+ (TestServer.prototype.remoteAPI):
+ (TestServer.prototype._ensureTestDatabase): Don't fail even if the test database doesn't exit.
+ (TestServer.prototype._startApache): Create a RemoteAPI instance to access the test sever.
+ (TestServer.prototype._waitForPid): Increase the timeout.
+ (TestServer.prototype.inject): Replace global.RemoteAPI during the test and restore it afterwards.
+ * server-tests/tools-buildbot-triggerable-tests.js: Added. Tests BuildbotTriggerable.syncOnce.
+ (MockLogger): Added.
+ (MockLogger.prototype.log): Added.
+ (MockLogger.prototype.error): Added.
+ * tools/detect-changes.js:
+ (parseArgument): Moved to js/parse-arguments.js.
+ * tools/js/buildbot-syncer.js:
+ (BuildbotBuildEntry):
+ (BuildbotBuildEntry.prototype.syncer): Added.
+ (BuildbotBuildEntry.prototype.buildRequestStatusIfUpdateIsNeeded): Added. Returns a new status
+ for a build request (of the matching build request ID) if it needs to be updated in the server.
+ (BuildbotSyncer): This class
+ (BuildbotSyncer.prototype.addTestConfiguration): Added.
+ (BuildbotSyncer.prototype.testConfigurations): Returns the list of test configurations.
+ (BuildbotSyncer.prototype.matchesConfiguration): Returns true iff the request can be scheduled on
+ this builder.
+ (BuildbotSyncer.prototype.scheduleRequest): Added. Schedules a new job on buildbot for a request.
+ (BuildbotSyncer.prototype.scheduleFirstRequestInGroupIfAvailable): Added. Schedules a new job for
+ the specified build request on the first slave that's available.
+ (BuildbotSyncer.prototype.pullBuildbot): Return a list of BuildbotBuildEntry instead of an object.
+ Also store it on an instance variable so that scheduleFirstRequestInGroupIfAvailable could use it.
+ (BuildbotSyncer.prototype._pullRecentBuilds):
+ (BuildbotSyncer.prototype.pathForPendingBuildsJSON): Renamed from urlForPendingBuildsJSON and now
+ only returns the path instead of the full URL since RemoteAPI takes a path, not full URL.
+ (BuildbotSyncer.prototype.pathForBuildJSON): Ditto from pathForBuildJSON.
+ (BuildbotSyncer.prototype.pathForForceBuild): Added.
+ (BuildbotSyncer.prototype.url): Use RemoteAPI's url method instead of manually constructing URL.
+ (BuildbotSyncer.prototype.urlForBuildNumber): Ditto.
+ (BuildbotSyncer.prototype._propertiesForBuildRequest): Now that each syncer can have multiple test
+ configurations associated with it, find the one matching for this request.
+ (BuildbotSyncer._loadConfig): Create a syncer per builder and add all test configurations to it.
+ (BuildbotSyncer._validateAndMergeConfig): Added the support for 'SlaveList', which is a list of
+ slave names present on this builder.
+ * tools/js/buildbot-triggerable.js: Added.
+ (BuildbotTriggerable): Added.
+ (BuildbotTriggerable.prototype.name): Added.
+ (BuildbotTriggerable.prototype.syncOnce): Added. The main logic for the syncing script. It pulls
+ existing build requests from the perf dashboard, pulls buildbot for pending and running/completed
+ builds on each builder (represented by each syncer), schedules build requests on buildbot if there
+ is any builder/slave available, and updates the status of build requests in the database.
+ (BuildbotTriggerable.prototype._validateRequests): Added.
+ (BuildbotTriggerable.prototype._pullBuildbotOnAllSyncers): Added.
+ (BuildbotTriggerable.prototype._scheduleNextRequestInGroupIfSlaveIsAvailable): Added.
+ (BuildbotTriggerable._testGroupMapForBuildRequests): Added.
+ * tools/js/database.js:
+ * tools/js/parse-arguments.js: Added. Extracted out of tools/detect-changes.js.
+ (parseArguments):
+ * tools/js/remote.js:
+ (RemoteAPI): Now optionally takes the server configuration.
+ (RemoteAPI.prototype.url): Added.
+ (RemoteAPI.prototype.getJSON): Removed the code for specifying request content.
+ (RemoteAPI.prototype.getJSONWithStatus): Ditto.
+ (RemoteAPI.prototype.postJSON): Added.
+ (RemoteAPI.prototype.postFormUrlencodedData): Added.
+ (RemoteAPI.prototype.sendHttpRequest): Fixed the code to specify auth.
+ * tools/js/v3-models.js: Don't include RemoteAPI here as they require a configuration for each host.
+ * tools/sync-buildbot.js: Added.
+ (main): Added. Parse the arguments and start the loop.
+ (syncLoop): Added.
+ * unit-tests/buildbot-syncer-tests.js: Added tests for pullBuildbot, scheduleRequest, as well as
+ scheduleFirstRequestInGroupIfAvailable. Refactored helper functions as needed.
+ (sampleiOSConfig):
+ (smallConfiguration): Added.
+ (smallPendingBuild): Added.
+ (smallInProgressBuild): Added.
+ (smallFinishedBuild): Added.
+ (createSampleBuildRequest): Create a unique build request for each platform.
+ (samplePendingBuild): Optionally specify build time and slave name.
+ (sampleInProgressBuild): Optionally specify slave name.
+ (sampleFinishedBuild): Ditto.
+ * unit-tests/resources/mock-remote-api.js:
+ (assert.notReached.assert.notReached):
+ (MockRemoteAPI.url): Added.
+ (MockRemoteAPI.postFormUrlencodedData): Added.
+ (MockRemoteAPI._addRequest): Extracted from getJSONWithStatus.
+ (MockRemoteAPI.waitForRequest): Extracted from inject. For tools-buildbot-triggerable-tests.js, we
+ need to instantiate a RemoteAPI for buildbot without replacing global.RemoteAPI.
+ (MockRemoteAPI.inject):
+ (MockRemoteAPI.reset): Added.
+
2016-03-30 Ryosuke Niwa <rniwa@webkit.org>
Simplify API of Test model by removing Test.setParentTest
order() { return this._order; }
rootSet() { return this._rootSet; }
+ status() { return this._status; }
hasFinished() { return this._status == 'failed' || this._status == 'completed' || this._status == 'canceled'; }
hasStarted() { return this._status != 'pending'; }
+ isScheduled() { return this._status == 'scheduled'; }
isPending() { return this._status == 'pending'; }
statusLabel()
{
path() { return this._test.path().concat([this]); }
- fullName()
- {
- return this._test.path().map(function (test) { return test.label(); }).join(' \u220B ') + ' : ' + this.label();
- }
+ fullName() { return this._test.fullName() + ' : ' + this.label(); }
label()
{
this._lastModifiedByMetric = object.lastModifiedByMetric;
this._containingTests = null;
+ this.ensureNamedStaticMap('name')[object.name] = this;
+
for (var metric of this._metrics)
metric.addPlatform(this);
}
+ static findByName(name)
+ {
+ var map = this.namedStaticMap('name');
+ return map ? map[name] : null;
+ }
+
hasTest(test)
{
if (!this._containingTests) {
static topLevelTests() { return this.sortByName(this.listForStaticMap('topLevelTests')); }
+ static findByPath(path)
+ {
+ var matchingTest = null;
+ var testList = this.topLevelTests();
+ for (var part of path) {
+ matchingTest = null;
+ for (var test of testList) {
+ if (part == test.name()) {
+ matchingTest = test;
+ break;
+ }
+ }
+ if (!matchingTest)
+ return null;
+ testList = matchingTest.childTests();
+ }
+ return matchingTest;
+ }
+
parentTest() { return Test.findById(this._parentId); }
path()
return path;
}
+ fullName() { return this.path().map(function (test) { return test.label(); }).join(' \u220B '); }
+
onlyContainsSingleMetric() { return !this.childTests().length && this._metrics.length == 1; }
childTests()
let assert = require('assert');
-require('../tools/js/v3-models.js');
-
+let MockData = require('./resources/mock-data.js');
let TestServer = require('./resources/test-server.js');
describe('/api/build-requests', function () {
TestServer.inject();
beforeEach(function () {
- AnalysisTask._fetchAllPromise = null;
- AnalysisTask.clearStaticMap();
- BuildRequest.clearStaticMap();
- CommitLog.clearStaticMap();
- Metric.clearStaticMap();
- Platform.clearStaticMap();
- Repository.clearStaticMap();
- RootSet.clearStaticMap();
- Test.clearStaticMap();
- TestGroup.clearStaticMap();
- })
+ MockData.resetV3Models();
+ });
it('should return "TriggerableNotFound" when the database is empty', function (done) {
TestServer.remoteAPI().getJSON('/api/build-requests/build-webkit').then(function (content) {
}).catch(done);
});
- function addMockData(db, statusList)
- {
- if (!statusList)
- statusList = ['pending', 'pending', 'pending', 'pending'];
- return Promise.all([
- db.insert('build_triggerables', {id: 1, name: 'build-webkit'}),
- db.insert('repositories', {id: 9, name: 'OS X'}),
- db.insert('repositories', {id: 11, name: 'WebKit'}),
- db.insert('commits', {id: 87832, repository: 9, revision: '10.11 15A284'}),
- db.insert('commits', {id: 93116, repository: 11, revision: '191622', time: (new Date(1445945816878)).toISOString()}),
- db.insert('commits', {id: 96336, repository: 11, revision: '192736', time: (new Date(1448225325650)).toISOString()}),
- db.insert('platforms', {id: 65, name: 'some platform'}),
- db.insert('tests', {id: 200, 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('root_sets', {id: 401}),
- db.insert('roots', {set: 401, commit: 87832}),
- db.insert('roots', {set: 401, commit: 93116}),
- db.insert('root_sets', {id: 402}),
- db.insert('roots', {set: 402, commit: 87832}),
- db.insert('roots', {set: 402, commit: 96336}),
- db.insert('analysis_tasks', {id: 500, platform: 65, metric: 300, name: 'some task'}),
- db.insert('analysis_test_groups', {id: 600, task: 500, name: 'some test group'}),
- db.insert('build_requests', {id: 700, status: statusList[0], triggerable: 1, platform: 65, test: 200, group: 600, order: 0, root_set: 401}),
- db.insert('build_requests', {id: 701, status: statusList[1], triggerable: 1, platform: 65, test: 200, group: 600, order: 1, root_set: 402}),
- db.insert('build_requests', {id: 702, status: statusList[2], triggerable: 1, platform: 65, test: 200, group: 600, order: 2, root_set: 401}),
- db.insert('build_requests', {id: 703, status: statusList[3], triggerable: 1, platform: 65, test: 200, group: 600, order: 3, root_set: 402}),
- ]);
- }
-
- function addAnotherMockTestGroup(db, statusList)
- {
- if (!statusList)
- statusList = ['pending', 'pending', 'pending', 'pending'];
- return Promise.all([
- db.insert('analysis_test_groups', {id: 599, task: 500, name: 'another test group'}),
- db.insert('build_requests', {id: 713, status: statusList[3], triggerable: 1, platform: 65, test: 200, group: 599, order: 3, root_set: 402}),
- db.insert('build_requests', {id: 710, status: statusList[0], triggerable: 1, platform: 65, test: 200, group: 599, order: 0, root_set: 401}),
- db.insert('build_requests', {id: 712, status: statusList[2], triggerable: 1, platform: 65, test: 200, group: 599, order: 2, root_set: 401}),
- db.insert('build_requests', {id: 711, status: statusList[1], triggerable: 1, platform: 65, test: 200, group: 599, order: 1, root_set: 402}),
- ]);
- }
-
it('should return build requets associated with a given triggerable with appropriate roots and rootSets', function (done) {
let db = TestServer.database();
db.connect().then(function () {
- return addMockData(db);
+ return MockData.addMockData(db);
}).then(function () {
return TestServer.remoteAPI().getJSONWithStatus('/api/build-requests/build-webkit');
}).then(function (content) {
it('should support useLegacyIdResolution option', function (done) {
let db = TestServer.database();
db.connect().then(function () {
- return addMockData(db);
+ return MockData.addMockData(db);
}).then(function () {
return TestServer.remoteAPI().getJSONWithStatus('/api/build-requests/build-webkit?useLegacyIdResolution=true');
}).then(function (content) {
it('should be fetchable by BuildRequest.fetchForTriggerable', function (done) {
let db = TestServer.database();
db.connect().then(function () {
- return addMockData(db);
+ return MockData.addMockData(db);
}).then(function () {
return Manifest.fetch();
}).then(function () {
it('should not include a build request if all requests in the same group had been completed', function (done) {
let db = TestServer.database();
db.connect().then(function () {
- return addMockData(db, ['completed', 'completed', 'completed', 'completed']);
+ return MockData.addMockData(db, ['completed', 'completed', 'completed', 'completed']);
}).then(function () {
return Manifest.fetch();
}).then(function () {
it('should not include a build request if all requests in the same group had been failed or cancled', function (done) {
let db = TestServer.database();
db.connect().then(function () {
- return addMockData(db, ['failed', 'failed', 'canceled', 'canceled']);
+ return MockData.addMockData(db, ['failed', 'failed', 'canceled', 'canceled']);
}).then(function () {
return Manifest.fetch();
}).then(function () {
it('should include all build requests of a test group if one of the reqeusts in the group had not been finished', function (done) {
let db = TestServer.database();
db.connect().then(function () {
- return addMockData(db, ['completed', 'completed', 'scheduled', 'pending']);
+ return MockData.addMockData(db, ['completed', 'completed', 'scheduled', 'pending']);
}).then(function () {
return Manifest.fetch();
}).then(function () {
it('should include all build requests of a test group if one of the reqeusts in the group is still running', function (done) {
let db = TestServer.database();
db.connect().then(function () {
- return addMockData(db, ['completed', 'completed', 'completed', 'running']);
+ return MockData.addMockData(db, ['completed', 'completed', 'completed', 'running']);
}).then(function () {
return Manifest.fetch();
}).then(function () {
it('should order build requests based on test group and order', function (done) {
let db = TestServer.database();
db.connect().then(function () {
- return Promise.all([addMockData(db), addAnotherMockTestGroup(db)])
+ return Promise.all([MockData.addMockData(db), MockData.addAnotherMockTestGroup(db)])
}).then(function () {
return Manifest.fetch();
}).then(function () {
--- /dev/null
+require('../../tools/js/v3-models.js');
+
+var crypto = require('crypto');
+
+MockData = {
+ resetV3Models: function ()
+ {
+ AnalysisTask._fetchAllPromise = null;
+ AnalysisTask.clearStaticMap();
+ BuildRequest.clearStaticMap();
+ CommitLog.clearStaticMap();
+ Metric.clearStaticMap();
+ Platform.clearStaticMap();
+ Repository.clearStaticMap();
+ RootSet.clearStaticMap();
+ Test.clearStaticMap();
+ TestGroup.clearStaticMap();
+ },
+ addMockData: function (db, statusList)
+ {
+ if (!statusList)
+ statusList = ['pending', 'pending', 'pending', 'pending'];
+ return Promise.all([
+ db.insert('build_triggerables', {id: 1, name: 'build-webkit'}),
+ db.insert('build_slaves', {id: 2, name: 'sync-slave', password_hash: crypto.createHash('sha256').update('password').digest('hex')}),
+ db.insert('repositories', {id: 9, name: 'OS X'}),
+ db.insert('repositories', {id: 11, name: 'WebKit'}),
+ db.insert('commits', {id: 87832, repository: 9, revision: '10.11 15A284'}),
+ db.insert('commits', {id: 93116, repository: 11, revision: '191622', time: (new Date(1445945816878)).toISOString()}),
+ db.insert('commits', {id: 96336, repository: 11, revision: '192736', time: (new Date(1448225325650)).toISOString()}),
+ db.insert('platforms', {id: 65, name: 'some platform'}),
+ db.insert('tests', {id: 200, 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('root_sets', {id: 401}),
+ db.insert('roots', {set: 401, commit: 87832}),
+ db.insert('roots', {set: 401, commit: 93116}),
+ db.insert('root_sets', {id: 402}),
+ db.insert('roots', {set: 402, commit: 87832}),
+ db.insert('roots', {set: 402, commit: 96336}),
+ db.insert('analysis_tasks', {id: 500, platform: 65, metric: 300, name: 'some task'}),
+ db.insert('analysis_test_groups', {id: 600, task: 500, name: 'some test group'}),
+ db.insert('build_requests', {id: 700, status: statusList[0], triggerable: 1, platform: 65, test: 200, group: 600, order: 0, root_set: 401}),
+ db.insert('build_requests', {id: 701, status: statusList[1], triggerable: 1, platform: 65, test: 200, group: 600, order: 1, root_set: 402}),
+ db.insert('build_requests', {id: 702, status: statusList[2], triggerable: 1, platform: 65, test: 200, group: 600, order: 2, root_set: 401}),
+ db.insert('build_requests', {id: 703, status: statusList[3], triggerable: 1, platform: 65, test: 200, group: 600, order: 3, root_set: 402}),
+ ]);
+ },
+ addAnotherMockTestGroup: function (db, statusList)
+ {
+ if (!statusList)
+ statusList = ['pending', 'pending', 'pending', 'pending'];
+ return Promise.all([
+ db.insert('analysis_test_groups', {id: 599, task: 500, name: 'another test group'}),
+ db.insert('build_requests', {id: 713, status: statusList[3], triggerable: 1, platform: 65, test: 200, group: 599, order: 3, root_set: 402}),
+ db.insert('build_requests', {id: 710, status: statusList[0], triggerable: 1, platform: 65, test: 200, group: 599, order: 0, root_set: 401}),
+ db.insert('build_requests', {id: 712, status: statusList[2], triggerable: 1, platform: 65, test: 200, group: 599, order: 2, root_set: 401}),
+ db.insert('build_requests', {id: 711, status: statusList[1], triggerable: 1, platform: 65, test: 200, group: 599, order: 1, root_set: 402}),
+ ]);
+ },
+ mockTestSyncConfigWithSingleBuilder: function ()
+ {
+ return {
+ 'triggerableName': 'build-webkit',
+ 'lookbackCount': 2,
+ 'configurations': [
+ {
+ 'platform': 'some platform',
+ 'test': ['some test'],
+ 'builder': 'some-builder-1',
+ 'arguments': {
+ 'wk': {'root': 'WebKit'},
+ 'os': {'root': 'OS X'},
+ },
+ 'buildRequestArgument': 'build-request-id',
+ }
+ ]
+ }
+ },
+ mockTestSyncConfigWithTwoBuilders: function ()
+ {
+ return {
+ 'triggerableName': 'build-webkit',
+ 'lookbackCount': 2,
+ 'configurations': [
+ {
+ 'platform': 'some platform',
+ 'test': ['some test'],
+ 'builder': 'some-builder-1',
+ 'arguments': {
+ 'wk': {'root': 'WebKit'},
+ 'os': {'root': 'OS X'},
+ },
+ 'buildRequestArgument': 'build-request-id',
+ },
+ {
+ 'platform': 'some platform',
+ 'test': ['some test'],
+ 'builder': 'some-builder-2',
+ 'arguments': {
+ 'wk': {'root': 'WebKit'},
+ 'os': {'root': 'OS X'},
+ },
+ 'buildRequestArgument': 'build-request-id',
+ }
+ ]
+ }
+ },
+ pendingBuild(options)
+ {
+ options = options || {};
+ return {
+ 'builderName': options.builder || 'some-builder-1',
+ 'builds': [],
+ 'properties': [
+ ['wk', options.webkitRevision || '191622'],
+ ['os', options.osxRevision || '10.11 15A284'],
+ ['build-request-id', (options.buildRequestId || 702).toString(), ]
+ ],
+ 'source': {
+ 'branch': '',
+ 'changes': [],
+ 'codebase': 'WebKit',
+ 'hasPatch': false,
+ 'project': '',
+ 'repository': '',
+ 'revision': ''
+ },
+ };
+ },
+ runningBuild(options)
+ {
+ options = options || {};
+ return {
+ 'builderName': options.builder || 'some-builder-1',
+ 'builds': [],
+ 'properties': [
+ ['wk', options.webkitRevision || '192736'],
+ ['os', options.osxRevision || '10.11 15A284'],
+ ['build-request-id', (options.buildRequestId || 701).toString(), ]
+ ],
+ 'currentStep': {},
+ 'eta': 721,
+ 'number': 124,
+ 'source': {
+ 'branch': '',
+ 'changes': [],
+ 'codebase': 'WebKit',
+ 'hasPatch': false,
+ 'project': '',
+ 'repository': '',
+ 'revision': ''
+ },
+ };
+ },
+ finishedBuild(options)
+ {
+ options = options || {};
+ return {
+ 'builderName': options.builder || 'some-builder-1',
+ 'builds': [],
+ 'properties': [
+ ['wk', options.webkitRevision || '191622'],
+ ['os', options.osxRevision || '10.11 15A284'],
+ ['build-request-id', (options.buildRequestId || 700).toString(), ]
+ ],
+ 'currentStep': null,
+ 'eta': null,
+ 'number': 123,
+ 'source': {
+ 'branch': '',
+ 'changes': [],
+ 'codebase': 'WebKit',
+ 'hasPatch': false,
+ 'project': '',
+ 'repository': '',
+ 'revision': ''
+ },
+ 'times': [0, 1],
+ };
+ }
+}
+
+if (typeof module != 'undefined')
+ module.exports = MockData;
this._databaseHost = Config.value('database.host');
this._databasePort = Config.value('database.port');
this._database = null;
+
+ this._remote = null
}
start()
remoteAPI()
{
- assert(this._server);
- RemoteAPI.configure(this._server);
- return RemoteAPI;
+ assert(this._remote);
+ return this._remote;
}
database()
_ensureTestDatabase()
{
- this._executePgsqlCommand('dropdb');
+ try {
+ this._executePgsqlCommand('dropdb');
+ } catch (error) { }
this._executePgsqlCommand('createdb');
this._executePgsqlCommand('psql', ['--command', `grant all privileges on database "${this._databaseName}" to "${this._databaseUser}";`]);
this.initDatabase();
}
this._pidWaitStart = Date.now();
this._pidFile = pidFile;
+
+ this._remote = new RemoteAPI(this._server);
+
return new Promise(this._waitForPid.bind(this, true));
}
_waitForPid(shouldExist, resolve, reject)
{
if (fs.existsSync(this._pidFile) != shouldExist) {
- if (Date.now() - this._pidWaitStart > 5000)
+ if (Date.now() - this._pidWaitStart > 8000)
reject();
else
setTimeout(this._waitForPid.bind(this, shouldExist, resolve, reject), 100);
{
let self = this;
before(function () {
- this.timeout(5000);
+ this.timeout(10000);
return self.start();
});
+ let originalRemote;
+
beforeEach(function () {
this.timeout(10000);
self.initDatabase();
self.cleanDataDirectory();
+ originalRemote = global.RemoteAPI;
+ global.RemoteAPI = self._remote;
});
after(function () {
- this.timeout(5000);
+ this.timeout(10000);
+ global.RemoteAPI = originalRemote;
return self.stop();
});
}
--- /dev/null
+'use strict';
+
+let assert = require('assert');
+
+let BuildbotTriggerable = require('../tools/js/buildbot-triggerable.js').BuildbotTriggerable;
+let MockData = require('./resources/mock-data.js');
+let MockRemoteAPI = require('../unit-tests/resources/mock-remote-api.js').MockRemoteAPI;
+let TestServer = require('./resources/test-server.js');
+
+class MockLogger {
+ constructor()
+ {
+ this._logs = [];
+ }
+
+ log(text) { this._logs.push(text); }
+ error(text) { this._logs.push(text); }
+}
+
+describe('BuildbotTriggerable', function () {
+ this.timeout(10000);
+ TestServer.inject();
+
+ beforeEach(function () {
+ MockData.resetV3Models();
+ MockRemoteAPI.reset('http://build.webkit.org');
+ });
+
+ describe('syncOnce', function () {
+ it('should schedule the next build request when there are no pending builds', function (done) {
+ let db = TestServer.database();
+ let syncPromise;
+ db.connect().then(function () {
+ return MockData.addMockData(db, ['completed', 'running', 'pending', 'pending']);
+ }).then(function () {
+ return Manifest.fetch();
+ }).then(function () {
+ 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);
+ syncPromise = triggerable.syncOnce();
+ syncPromise.catch(done);
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(BuildRequest.all().length, 4);
+ assert.equal(BuildRequest.findById(700).status(), 'completed');
+ assert.equal(BuildRequest.findById(701).status(), 'running');
+ assert.equal(BuildRequest.findById(702).status(), 'pending');
+ assert.equal(BuildRequest.findById(703).status(), 'pending');
+ assert.equal(MockRemoteAPI.requests[0].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[0].url, '/json/builders/some-builder-1/pendingBuilds');
+ MockRemoteAPI.requests[0].resolve([]);
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests[1].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[1].url, '/json/builders/some-builder-1/builds/?select=-1&select=-2');
+ MockRemoteAPI.requests[1].resolve({[-1]: MockData.runningBuild(), [-2]: MockData.finishedBuild()});
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests[2].method, 'POST');
+ assert.equal(MockRemoteAPI.requests[2].url, '/builders/some-builder-1/force');
+ assert.deepEqual(MockRemoteAPI.requests[2].data, {'wk': '191622', 'os': '10.11 15A284', 'build-request-id': '702'});
+ MockRemoteAPI.requests[2].resolve('OK');
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests[3].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[3].url, '/json/builders/some-builder-1/pendingBuilds');
+ MockRemoteAPI.requests[3].resolve([MockData.pendingBuild()])
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests[4].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[4].url, '/json/builders/some-builder-1/builds/?select=-1&select=-2');
+ MockRemoteAPI.requests[4].resolve({[-1]: MockData.runningBuild(), [-2]: MockData.finishedBuild()});
+ return syncPromise;
+ }).then(function () {
+ return BuildRequest.fetchForTriggerable(MockData.mockTestSyncConfigWithSingleBuilder().triggerableName);
+ }).then(function () {
+ assert.equal(BuildRequest.all().length, 4);
+ assert.equal(BuildRequest.findById(700).status(), 'completed');
+ assert.equal(BuildRequest.findById(701).status(), 'running');
+ assert.equal(BuildRequest.findById(702).status(), 'scheduled');
+ assert.equal(BuildRequest.findById(703).status(), 'pending');
+ done();
+ }).catch(done);
+ });
+
+ it('should not schedule the next build request when there is a pending build', function (done) {
+ let db = TestServer.database();
+ let syncPromise;
+ db.connect().then(function () {
+ return MockData.addMockData(db, ['completed', 'running', 'pending', 'pending']);
+ }).then(function () {
+ return Manifest.fetch();
+ }).then(function () {
+ 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);
+ syncPromise = triggerable.syncOnce();
+ syncPromise.catch(done);
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests[0].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[0].url, '/json/builders/some-builder-1/pendingBuilds');
+ MockRemoteAPI.requests[0].resolve([MockData.pendingBuild()]);
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests[1].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[1].url, '/json/builders/some-builder-1/builds/?select=-1&select=-2');
+ MockRemoteAPI.requests[1].resolve({[-1]: MockData.runningBuild(), [-2]: MockData.finishedBuild()});
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests[2].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[2].url, '/json/builders/some-builder-1/pendingBuilds');
+ MockRemoteAPI.requests[2].resolve([MockData.pendingBuild()])
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests[3].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[3].url, '/json/builders/some-builder-1/builds/?select=-1&select=-2');
+ MockRemoteAPI.requests[3].resolve({[-1]: MockData.runningBuild(), [-2]: MockData.finishedBuild()});
+ return syncPromise;
+ }).then(function () {
+ assert.equal(BuildRequest.all().length, 4);
+ assert.equal(BuildRequest.findById(700).status(), 'completed');
+ assert.equal(BuildRequest.findById(701).status(), 'running');
+ assert.equal(BuildRequest.findById(702).status(), 'pending');
+ assert.equal(BuildRequest.findById(703).status(), 'pending');
+ return BuildRequest.fetchForTriggerable(MockData.mockTestSyncConfigWithSingleBuilder().triggerableName);
+ }).then(function () {
+ assert.equal(BuildRequest.all().length, 4);
+ assert.equal(BuildRequest.findById(700).status(), 'completed');
+ assert.equal(BuildRequest.findById(701).status(), 'running');
+ assert.equal(BuildRequest.findById(702).status(), 'scheduled');
+ assert.equal(BuildRequest.findById(703).status(), 'pending');
+ done();
+ }).catch(done);
+ });
+
+ it('should schedule the build request on a builder without a pending build if it\'s the first request in the group', function (done) {
+ let db = TestServer.database();
+ let syncPromise;
+ db.connect().then(function () {
+ return MockData.addMockData(db, ['pending', 'pending', 'pending', 'pending']);
+ }).then(function () {
+ return Manifest.fetch();
+ }).then(function () {
+ let config = MockData.mockTestSyncConfigWithTwoBuilders();
+ let logger = new MockLogger;
+ let slaveInfo = {name: 'sync-slave', password: 'password'};
+ let triggerable = new BuildbotTriggerable(config, TestServer.remoteAPI(), MockRemoteAPI, slaveInfo, logger);
+ syncPromise = triggerable.syncOnce();
+ syncPromise.catch(done);
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests.length, 2);
+ assert.equal(MockRemoteAPI.requests[0].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[0].url, '/json/builders/some-builder-1/pendingBuilds');
+ MockRemoteAPI.requests[0].resolve([MockData.pendingBuild({buildRequestId: 999})]);
+ assert.equal(MockRemoteAPI.requests[1].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[1].url, '/json/builders/some-builder-2/pendingBuilds');
+ MockRemoteAPI.requests[1].resolve([]);
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests.length, 4);
+ assert.equal(MockRemoteAPI.requests[2].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[2].url, '/json/builders/some-builder-1/builds/?select=-1&select=-2');
+ MockRemoteAPI.requests[2].resolve({});
+ assert.equal(MockRemoteAPI.requests[3].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[3].url, '/json/builders/some-builder-2/builds/?select=-1&select=-2');
+ MockRemoteAPI.requests[3].resolve({});
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests.length, 5);
+ assert.equal(MockRemoteAPI.requests[4].method, 'POST');
+ assert.equal(MockRemoteAPI.requests[4].url, '/builders/some-builder-2/force');
+ assert.deepEqual(MockRemoteAPI.requests[4].data, {'wk': '191622', 'os': '10.11 15A284', 'build-request-id': '700'});
+ MockRemoteAPI.requests[4].resolve('OK');
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests.length, 7);
+ assert.equal(MockRemoteAPI.requests[5].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[5].url, '/json/builders/some-builder-1/pendingBuilds');
+ MockRemoteAPI.requests[5].resolve([MockData.pendingBuild({buildRequestId: 999})]);
+ assert.equal(MockRemoteAPI.requests[6].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[6].url, '/json/builders/some-builder-2/pendingBuilds');
+ MockRemoteAPI.requests[6].resolve([MockData.pendingBuild({builder: 'some-builder-2', buildRequestId: 700})]);
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests.length, 9);
+ assert.equal(MockRemoteAPI.requests[7].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[7].url, '/json/builders/some-builder-1/builds/?select=-1&select=-2');
+ MockRemoteAPI.requests[7].resolve({});
+ assert.equal(MockRemoteAPI.requests[8].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[8].url, '/json/builders/some-builder-2/builds/?select=-1&select=-2');
+ MockRemoteAPI.requests[8].resolve({});
+ return syncPromise;
+ }).then(function () {
+ assert.equal(BuildRequest.all().length, 4);
+ assert.equal(BuildRequest.findById(700).status(), 'pending');
+ assert.equal(BuildRequest.findById(701).status(), 'pending');
+ assert.equal(BuildRequest.findById(702).status(), 'pending');
+ assert.equal(BuildRequest.findById(703).status(), 'pending');
+ return BuildRequest.fetchForTriggerable(MockData.mockTestSyncConfigWithTwoBuilders().triggerableName);
+ }).then(function () {
+ assert.equal(BuildRequest.all().length, 4);
+ assert.equal(BuildRequest.findById(700).status(), 'scheduled');
+ assert.equal(BuildRequest.findById(701).status(), 'pending');
+ assert.equal(BuildRequest.findById(702).status(), 'pending');
+ assert.equal(BuildRequest.findById(703).status(), 'pending');
+ done();
+ }).catch(done);
+ });
+
+ it('should not schedule a build request on a different builder than the one the first build request is pending', function (done) {
+ let db = TestServer.database();
+ let syncPromise;
+ db.connect().then(function () {
+ return MockData.addMockData(db, ['pending', 'pending', 'pending', 'pending']);
+ }).then(function () {
+ return Manifest.fetch();
+ }).then(function () {
+ let config = MockData.mockTestSyncConfigWithTwoBuilders();
+ let logger = new MockLogger;
+ let slaveInfo = {name: 'sync-slave', password: 'password'};
+ let triggerable = new BuildbotTriggerable(config, TestServer.remoteAPI(), MockRemoteAPI, slaveInfo, logger);
+ syncPromise = triggerable.syncOnce();
+ syncPromise.catch(done);
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests.length, 2);
+ assert.equal(MockRemoteAPI.requests[0].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[0].url, '/json/builders/some-builder-1/pendingBuilds');
+ MockRemoteAPI.requests[0].resolve([MockData.pendingBuild({buildRequestId: 700})]);
+ assert.equal(MockRemoteAPI.requests[1].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[1].url, '/json/builders/some-builder-2/pendingBuilds');
+ MockRemoteAPI.requests[1].resolve([]);
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests.length, 4);
+ assert.equal(MockRemoteAPI.requests[2].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[2].url, '/json/builders/some-builder-1/builds/?select=-1&select=-2');
+ MockRemoteAPI.requests[2].resolve({});
+ assert.equal(MockRemoteAPI.requests[3].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[3].url, '/json/builders/some-builder-2/builds/?select=-1&select=-2');
+ MockRemoteAPI.requests[3].resolve({});
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests.length, 6);
+ assert.equal(MockRemoteAPI.requests[4].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[4].url, '/json/builders/some-builder-1/pendingBuilds');
+ MockRemoteAPI.requests[4].resolve([MockData.pendingBuild({buildRequestId: 700})]);
+ assert.equal(MockRemoteAPI.requests[5].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[5].url, '/json/builders/some-builder-2/pendingBuilds');
+ MockRemoteAPI.requests[5].resolve([]);
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests.length, 8);
+ assert.equal(MockRemoteAPI.requests[6].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[6].url, '/json/builders/some-builder-1/builds/?select=-1&select=-2');
+ MockRemoteAPI.requests[6].resolve({});
+ assert.equal(MockRemoteAPI.requests[7].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[7].url, '/json/builders/some-builder-2/builds/?select=-1&select=-2');
+ MockRemoteAPI.requests[7].resolve({});
+ return syncPromise;
+ }).then(function () {
+ assert.equal(BuildRequest.all().length, 4);
+ assert.equal(BuildRequest.findById(700).status(), 'pending');
+ assert.equal(BuildRequest.findById(701).status(), 'pending');
+ assert.equal(BuildRequest.findById(702).status(), 'pending');
+ assert.equal(BuildRequest.findById(703).status(), 'pending');
+ return BuildRequest.fetchForTriggerable(MockData.mockTestSyncConfigWithTwoBuilders().triggerableName);
+ }).then(function () {
+ assert.equal(BuildRequest.all().length, 4);
+ assert.equal(BuildRequest.findById(700).status(), 'scheduled');
+ assert.equal(BuildRequest.findById(701).status(), 'pending');
+ assert.equal(BuildRequest.findById(702).status(), 'pending');
+ assert.equal(BuildRequest.findById(703).status(), 'pending');
+ done();
+ }).catch(done);
+ });
+
+ it('should update the status of a pending build and schedule a new build if the pending build had started running', function (done) {
+ let db = TestServer.database();
+ let syncPromise;
+ db.connect().then(function () {
+ return MockData.addMockData(db, ['pending', 'pending', 'pending', 'pending']);
+ }).then(function () {
+ return Manifest.fetch();
+ }).then(function () {
+ let config = MockData.mockTestSyncConfigWithTwoBuilders();
+ let logger = new MockLogger;
+ let slaveInfo = {name: 'sync-slave', password: 'password'};
+ let triggerable = new BuildbotTriggerable(config, TestServer.remoteAPI(), MockRemoteAPI, slaveInfo, logger);
+ syncPromise = triggerable.syncOnce();
+ syncPromise.catch(done);
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests.length, 2);
+ assert.equal(MockRemoteAPI.requests[0].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[0].url, '/json/builders/some-builder-1/pendingBuilds');
+ MockRemoteAPI.requests[0].resolve([]);
+ assert.equal(MockRemoteAPI.requests[1].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[1].url, '/json/builders/some-builder-2/pendingBuilds');
+ MockRemoteAPI.requests[1].resolve([]);
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests.length, 4);
+ assert.equal(MockRemoteAPI.requests[2].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[2].url, '/json/builders/some-builder-1/builds/?select=-1&select=-2');
+ MockRemoteAPI.requests[2].resolve({[-1]: MockData.runningBuild({buildRequestId: 701}), [-2]: MockData.finishedBuild({buildRequestId: 700})});
+ assert.equal(MockRemoteAPI.requests[3].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[3].url, '/json/builders/some-builder-2/builds/?select=-1&select=-2');
+ MockRemoteAPI.requests[3].resolve({});
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests.length, 5);
+ assert.equal(MockRemoteAPI.requests[4].method, 'POST');
+ assert.equal(MockRemoteAPI.requests[4].url, '/builders/some-builder-1/force');
+ assert.deepEqual(MockRemoteAPI.requests[4].data, {'wk': '191622', 'os': '10.11 15A284', 'build-request-id': '702'});
+ MockRemoteAPI.requests[4].resolve('OK');
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests.length, 7);
+ assert.equal(MockRemoteAPI.requests[5].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[5].url, '/json/builders/some-builder-1/pendingBuilds');
+ MockRemoteAPI.requests[5].resolve([MockData.pendingBuild({buildRequestId: 702})]);
+ assert.equal(MockRemoteAPI.requests[6].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[6].url, '/json/builders/some-builder-2/pendingBuilds');
+ MockRemoteAPI.requests[6].resolve([]);
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests.length, 9);
+ assert.equal(MockRemoteAPI.requests[7].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[7].url, '/json/builders/some-builder-1/builds/?select=-1&select=-2');
+ MockRemoteAPI.requests[7].resolve({[-1]: MockData.runningBuild({buildRequestId: 701}), [-2]: MockData.finishedBuild({buildRequestId: 700})});
+ assert.equal(MockRemoteAPI.requests[8].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[8].url, '/json/builders/some-builder-2/builds/?select=-1&select=-2');
+ MockRemoteAPI.requests[8].resolve({});
+ return syncPromise;
+ }).then(function () {
+ assert.equal(BuildRequest.all().length, 4);
+ assert.equal(BuildRequest.findById(700).status(), 'pending');
+ assert.equal(BuildRequest.findById(701).status(), 'pending');
+ assert.equal(BuildRequest.findById(702).status(), 'pending');
+ assert.equal(BuildRequest.findById(703).status(), 'pending');
+ return BuildRequest.fetchForTriggerable(MockData.mockTestSyncConfigWithTwoBuilders().triggerableName);
+ }).then(function () {
+ assert.equal(BuildRequest.all().length, 4);
+ assert.equal(BuildRequest.findById(700).status(), 'failed');
+ assert.equal(BuildRequest.findById(701).status(), 'running');
+ assert.equal(BuildRequest.findById(702).status(), 'scheduled');
+ assert.equal(BuildRequest.findById(703).status(), 'pending');
+ done();
+ }).catch(done);
+ });
+
+ it('should schedule a build request on a builder without pending builds if the request belongs to a new test group', function (done) {
+ let db = TestServer.database();
+ let syncPromise;
+ db.connect().then(function () {
+ return Promise.all([
+ MockData.addMockData(db, ['completed', 'pending', 'pending', 'pending']),
+ MockData.addAnotherMockTestGroup(db, ['pending', 'pending', 'pending', 'pending'])
+ ]);
+ }).then(function () {
+ return Manifest.fetch();
+ }).then(function () {
+ let config = MockData.mockTestSyncConfigWithTwoBuilders();
+ let logger = new MockLogger;
+ let slaveInfo = {name: 'sync-slave', password: 'password'};
+ let triggerable = new BuildbotTriggerable(config, TestServer.remoteAPI(), MockRemoteAPI, slaveInfo, logger);
+ syncPromise = triggerable.syncOnce();
+ syncPromise.catch(done);
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests.length, 2);
+ assert.equal(MockRemoteAPI.requests[0].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[0].url, '/json/builders/some-builder-1/pendingBuilds');
+ MockRemoteAPI.requests[0].resolve([MockData.pendingBuild({buildRequestId: 702})]);
+ assert.equal(MockRemoteAPI.requests[1].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[1].url, '/json/builders/some-builder-2/pendingBuilds');
+ MockRemoteAPI.requests[1].resolve([]);
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests.length, 4);
+ assert.equal(MockRemoteAPI.requests[2].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[2].url, '/json/builders/some-builder-1/builds/?select=-1&select=-2');
+ MockRemoteAPI.requests[2].resolve({[-1]: MockData.runningBuild({buildRequestId: 701}), [-2]: MockData.finishedBuild({buildRequestId: 700})});
+ assert.equal(MockRemoteAPI.requests[3].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[3].url, '/json/builders/some-builder-2/builds/?select=-1&select=-2');
+ MockRemoteAPI.requests[3].resolve({});
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests.length, 5);
+ assert.equal(MockRemoteAPI.requests[4].method, 'POST');
+ assert.equal(MockRemoteAPI.requests[4].url, '/builders/some-builder-2/force');
+ assert.deepEqual(MockRemoteAPI.requests[4].data, {'wk': '191622', 'os': '10.11 15A284', 'build-request-id': '710'});
+ MockRemoteAPI.requests[4].resolve('OK');
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests.length, 7);
+ assert.equal(MockRemoteAPI.requests[5].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[5].url, '/json/builders/some-builder-1/pendingBuilds');
+ MockRemoteAPI.requests[5].resolve([MockData.pendingBuild({buildRequestId: 702})]);
+ assert.equal(MockRemoteAPI.requests[6].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[6].url, '/json/builders/some-builder-2/pendingBuilds');
+ MockRemoteAPI.requests[6].resolve([MockData.pendingBuild({builder: 'some-builder-2', buildRequestId: 710})]);
+ return MockRemoteAPI.waitForRequest();
+ }).then(function () {
+ assert.equal(MockRemoteAPI.requests.length, 9);
+ assert.equal(MockRemoteAPI.requests[7].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[7].url, '/json/builders/some-builder-1/builds/?select=-1&select=-2');
+ MockRemoteAPI.requests[7].resolve({[-1]: MockData.runningBuild({buildRequestId: 701}), [-2]: MockData.finishedBuild({buildRequestId: 700})});
+ assert.equal(MockRemoteAPI.requests[8].method, 'GET');
+ assert.equal(MockRemoteAPI.requests[8].url, '/json/builders/some-builder-2/builds/?select=-1&select=-2');
+ MockRemoteAPI.requests[8].resolve({});
+ return syncPromise;
+ }).then(function () {
+ assert.equal(BuildRequest.all().length, 8);
+ assert.equal(BuildRequest.findById(700).status(), 'completed');
+ assert.equal(BuildRequest.findById(701).status(), 'pending');
+ assert.equal(BuildRequest.findById(702).status(), 'pending');
+ assert.equal(BuildRequest.findById(703).status(), 'pending');
+ assert.equal(BuildRequest.findById(710).status(), 'pending');
+ assert.equal(BuildRequest.findById(711).status(), 'pending');
+ assert.equal(BuildRequest.findById(712).status(), 'pending');
+ assert.equal(BuildRequest.findById(713).status(), 'pending');
+ return BuildRequest.fetchForTriggerable(MockData.mockTestSyncConfigWithTwoBuilders().triggerableName);
+ }).then(function () {
+ assert.equal(BuildRequest.all().length, 8);
+ assert.equal(BuildRequest.findById(700).status(), 'completed');
+ assert.equal(BuildRequest.findById(701).status(), 'running');
+ assert.equal(BuildRequest.findById(702).status(), 'scheduled');
+ assert.equal(BuildRequest.findById(703).status(), 'pending');
+ assert.equal(BuildRequest.findById(710).status(), 'scheduled');
+ assert.equal(BuildRequest.findById(711).status(), 'pending');
+ assert.equal(BuildRequest.findById(712).status(), 'pending');
+ assert.equal(BuildRequest.findById(713).status(), 'pending');
+ done();
+ }).catch(done);
+ });
+ });
+});
var RunsData = data.RunsData;
var Statistics = require('../public/shared/statistics.js');
var StatisticsStrategies = require('../public/v2/statistics-strategies.js');
+var parseArguments = require('./js/parse-arguments.js').parseArguments;
// FIXME: We shouldn't use a global variable like this.
var settings;
function main(argv)
{
- var options = parseArgument(argv, [
+ var options = parseArguments(argv, [
{name: '--server-config-json', required: true},
{name: '--change-detection-config-json', required: true},
{name: '--seconds-to-sleep', type: parseFloat, default: 1200},
fetchManifestAndAnalyzeData(options['--server-config-json']);
}
-function parseArgument(argv, acceptedOptions) {
- var args = argv.slice(2);
- var options = {}
- for (var i = 0; i < args.length; i += 2) {
- var current = args[i];
- var next = args[i + 1];
- for (var option of acceptedOptions) {
- if (current == option['name']) {
- options[option['name']] = next;
- next = null;
- break;
- }
- }
- if (next) {
- console.error('Invalid argument:', current);
- return null;
- }
- }
- for (var option of acceptedOptions) {
- var name = option['name'];
- if (option['required'] && !(name in options)) {
- console.log('Required argument', name, 'is missing');
- return null;
- }
- var value = options[name] || option['default'];
- var converter = option['type'];
- options[name] = converter ? converter(value) : value;
- }
- return options;
-}
-
function fetchManifestAndAnalyzeData(serverConfigJSON)
{
loadServerConfig(serverConfigJSON);
}
}
+ syncer() { return this._syncer; }
buildNumber() { return this._buildNumber; }
slaveName() { return this._slaveName; }
buildRequestId() { return this._buildRequestId; }
isInProgress() { return this._isInProgress; }
hasFinished() { return !this.isPending() && !this.isInProgress(); }
url() { return this.isPending() ? this._syncer.url() : this._syncer.urlForBuildNumber(this._buildNumber); }
+
+ buildRequestStatusIfUpdateIsNeeded(request)
+ {
+ assert.equal(request.id(), this._buildRequestId);
+ if (!request)
+ return null;
+ if (this.isPending()) {
+ if (request.isPending())
+ return 'scheduled';
+ } else if (this.isInProgress()) {
+ if (!request.hasStarted())
+ return 'running';
+ } else if (this.hasFinished()) {
+ if (!request.hasFinished())
+ return 'failedIfNotCompleted';
+ }
+ return null;
+ }
}
+
class BuildbotSyncer {
- constructor(url, object)
+ constructor(remote, object)
{
- this._url = url;
+ this._remote = remote;
+ this._testConfigurations = [];
this._builderName = object.builder;
- this._platformName = object.platform;
- this._testPath = object.test;
- this._propertiesTemplate = object.properties;
this._slavePropertyName = object.slaveArgument;
+ this._slaveList = object.slaveList;
this._buildRequestPropertyName = object.buildRequestArgument;
+ this._entryList = null;
}
- testPath() { return this._testPath }
builderName() { return this._builderName; }
- platformName() { return this._platformName; }
+
+ addTestConfiguration(test, platform, propertiesTemplate)
+ {
+ assert(test instanceof Test);
+ assert(platform instanceof Platform);
+ this._testConfigurations.push({test: test, platform: platform, propertiesTemplate: propertiesTemplate});
+ }
+ testConfigurations() { return this._testConfigurations; }
+
+ matchesConfiguration(request)
+ {
+ for (let config of this._testConfigurations) {
+ if (config.platform == request.platform() && config.test == request.test())
+ return true;
+ }
+ return false;
+ }
+
+ scheduleRequest(newRequest, slaveName)
+ {
+ let properties = this._propertiesForBuildRequest(newRequest);
+
+ assert.equal(!this._slavePropertyName, !slaveName);
+ if (this._slavePropertyName)
+ properties[this._slavePropertyName] = slaveName;
+
+ return this._remote.postFormUrlencodedData(this.pathForForceBuild(), properties);
+ }
+
+ scheduleFirstRequestInGroupIfAvailable(newRequest)
+ {
+ assert(newRequest instanceof BuildRequest);
+
+ if (!this.matchesConfiguration(newRequest))
+ return null;
+
+ let hasPendingBuildsWithoutSlaveNameSpecified = false;
+ let usedSlaves = new Set;
+ for (let entry of this._entryList) {
+ if (entry.isPending()) {
+ if (!entry.slaveName())
+ hasPendingBuildsWithoutSlaveNameSpecified = true;
+ usedSlaves.add(entry.slaveName());
+ }
+ }
+
+ if (!this._slaveList || hasPendingBuildsWithoutSlaveNameSpecified) {
+ if (usedSlaves.size)
+ return null;
+ return this.scheduleRequest(newRequest, null);
+ }
+
+ for (let slaveName of this._slaveList) {
+ if (!usedSlaves.has(slaveName))
+ return this.scheduleRequest(newRequest, slaveName);
+ }
+
+ return null;
+ }
pullBuildbot(count)
{
let self = this;
- return RemoteAPI.getJSON(this.urlForPendingBuildsJSON()).then(function (content) {
+ return this._remote.getJSON(this.pathForPendingBuildsJSON()).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 entries)
entryByRequest[entry.buildRequestId()] = entry;
- return entryByRequest;
+ let entryList = [];
+ for (let id in entryByRequest)
+ entryList.push(entryByRequest[id]);
+
+ self._entryList = entryList;
+
+ return entryList;
});
});
}
selectedBuilds[i] = -i - 1;
let self = this;
- return RemoteAPI.getJSON(this.urlForBuildJSON(selectedBuilds)).then(function (content) {
- let entries = [];
+ return this._remote.getJSON(this.pathForBuildJSON(selectedBuilds)).then(function (content) {
+ var entries = [];
for (let index of selectedBuilds) {
let entry = content[index];
if (entry && !entry['error'])
});
}
- urlForPendingBuildsJSON() { return `${this._url}/json/builders/${this._builderName}/pendingBuilds`; }
- urlForBuildJSON(selectedBuilds)
+ pathForPendingBuildsJSON() { return `/json/builders/${this._builderName}/pendingBuilds`; }
+ pathForBuildJSON(selectedBuilds)
{
- return `${this._url}/json/builders/${this._builderName}/builds/?`
+ return `/json/builders/${this._builderName}/builds/?`
+ selectedBuilds.map(function (number) { return 'select=' + number; }).join('&');
}
+ pathForForceBuild() { return `/builders/${this._builderName}/force`; }
- url() { return `${this._url}/builders/${this._builderName}/`; }
- urlForBuildNumber(number) { return `${this._url}/builders/${this._builderName}/builds/${number}`; }
+ url() { return this._remote.url(`/builders/${this._builderName}/`); }
+ urlForBuildNumber(number) { return this._remote.url(`/builders/${this._builderName}/builds/${number}`); }
_propertiesForBuildRequest(buildRequest)
{
- console.assert(buildRequest instanceof BuildRequest);
+ assert(buildRequest instanceof BuildRequest);
let rootSet = buildRequest.rootSet();
- console.assert(rootSet instanceof RootSet);
+ assert(rootSet instanceof RootSet);
let repositoryByName = {};
for (let repository of rootSet.repositories())
repositoryByName[repository.name()] = repository;
+ let propertiesTemplate = null;
+ for (let config of this._testConfigurations) {
+ if (config.platform == buildRequest.platform() && config.test == buildRequest.test())
+ propertiesTemplate = config.propertiesTemplate;
+ }
+ assert(propertiesTemplate);
+
let properties = {};
- for (let key in this._propertiesTemplate) {
- let value = this._propertiesTemplate[key];
+ for (let key in propertiesTemplate) {
+ let value = propertiesTemplate[key];
if (typeof(value) != 'object')
properties[key] = value;
else if ('root' in value) {
return revisionSet;
}
- static _loadConfig(url, config)
+ static _loadConfig(remote, config)
{
let shared = config['shared'] || {};
let types = config['types'] || {};
let builders = config['builders'] || {};
- let syncers = [];
+ let syncerByBuilder = new Map;
for (let entry of config['configurations']) {
let newConfig = {};
this._validateAndMergeConfig(newConfig, shared);
assert('builder' in newConfig, 'configuration must specify a builder');
assert('properties' in newConfig, 'configuration must specify arguments to post on a builder');
assert('buildRequestArgument' in newConfig, 'configuration must specify buildRequestArgument');
- syncers.push(new BuildbotSyncer(url, newConfig));
+
+ let test = Test.findByPath(newConfig.test);
+ assert(test, `${newConfig.test} is not a valid test path`);
+
+ let platform = Platform.findByName(newConfig.platform);
+ assert(platform, `${newConfig.platform} is not a valid platform name`);
+
+ let syncer = syncerByBuilder.get(newConfig.builder);
+ if (!syncer) {
+ syncer = new BuildbotSyncer(remote, newConfig);
+ syncerByBuilder.set(newConfig.builder, syncer);
+ }
+ syncer.addTestConfiguration(test, platform, newConfig.properties);
}
- return syncers;
+ return Array.from(syncerByBuilder.values());
}
static _validateAndMergeConfig(config, valuesToMerge)
assert(value.every(function (part) { return typeof part == 'string'; }), 'test should be an array of strings');
config[name] = value.slice();
break;
+ case 'slaveList':
+ assert(value instanceof Array, 'slaveList should be an array');
+ assert(value.every(function (part) { return typeof part == 'string'; }), 'slaveList should be an array of strings');
+ config[name] = value;
+ break;
case 'type': // fallthrough
case 'builder': // fallthrough
case 'platform': // fallthrough
--- /dev/null
+'use strict';
+
+let assert = require('assert');
+
+require('./v3-models.js');
+
+let BuildbotSyncer = require('./buildbot-syncer').BuildbotSyncer;
+
+class BuildbotTriggerable {
+ constructor(config, remote, buildbotRemote, slaveInfo, logger)
+ {
+ this._name = config.triggerableName;
+ assert(typeof(this._name) == 'string', 'triggerableName must be specified');
+
+ this._lookbackCount = config.lookbackCount;
+ assert(typeof(this._lookbackCount) == 'number' && this._lookbackCount > 0, 'lookbackCount must be a number greater than 0');
+
+ this._remote = remote;
+
+ this._slaveInfo = slaveInfo;
+ assert(typeof(slaveInfo.name) == 'string', 'slave name must be specified');
+ assert(typeof(slaveInfo.password) == 'string', 'slave password must be specified');
+
+ this._syncers = BuildbotSyncer._loadConfig(buildbotRemote, config);
+ this._logger = logger || {log: function () { }, error: function () { }};
+ }
+
+ name() { return this._name; }
+
+ syncOnce()
+ {
+ let syncerList = this._syncers;
+ let buildReqeustsByGroup = new Map;
+
+ let self = this;
+ this._logger.log(`Fetching build requests for ${this._name}...`);
+ return BuildRequest.fetchForTriggerable(this._name).then(function () {
+ let buildRequests = BuildRequest.all();
+ self._validateRequests(buildRequests);
+ buildReqeustsByGroup = BuildbotTriggerable._testGroupMapForBuildRequests(buildRequests);
+ return self._pullBuildbotOnAllSyncers(buildReqeustsByGroup);
+ }).then(function (updates) {
+ self._logger.log('Scheduling builds');
+ let promistList = [];
+ let testGroupList = Array.from(buildReqeustsByGroup.values()).sort(function (a, b) { return a.id - b.id; });
+ for (let group of testGroupList) {
+ let promise = self._scheduleNextRequestInGroupIfSlaveIsAvailable(group, updates);
+ if (promise)
+ promistList.push(promise);
+ }
+ return Promise.all(promistList);
+ }).then(function () {
+ // Pull all buildbots for the second time since the previous step may have scheduled more builds.
+ return self._pullBuildbotOnAllSyncers(buildReqeustsByGroup);
+ }).then(function (updates) {
+ // FIXME: Add a new API that just updates the requests.
+ return self._remote.postJSON(`/api/build-requests/${self._name}`, {
+ 'slaveName': self._slaveInfo.name,
+ 'slavePassword': self._slaveInfo.password,
+ 'buildRequestUpdates': updates});
+ }).then(function (response) {
+ if (response['status'] != 'OK')
+ self._logger.log('Failed to update the build requests status: ' + response['status']);
+ })
+ }
+
+ _validateRequests(buildRequests)
+ {
+ let testPlatformPairs = {};
+ for (let request of buildRequests) {
+ if (!this._syncers.some(function (syncer) { return syncer.matchesConfiguration(request); })) {
+ let key = request.platform().id + '-' + request.test().id();
+ if (!(key in testPlatformPairs))
+ this._logger.error(`No matching configuration for "${request.test().fullName()}" on "${request.platform().name()}".`);
+ testPlatformPairs[key] = true;
+ }
+ }
+ }
+
+ _pullBuildbotOnAllSyncers(buildReqeustsByGroup)
+ {
+ let updates = {};
+ let self = this;
+ return Promise.all(this._syncers.map(function (syncer) {
+ self._logger.log(`Fetching builds on ${syncer.builderName()}`);
+ return syncer.pullBuildbot(self._lookbackCount).then(function (entryList) {
+ for (let entry of entryList) {
+ let request = BuildRequest.findById(entry.buildRequestId());
+ if (!request)
+ continue;
+
+ let info = buildReqeustsByGroup.get(request.testGroupId());
+ assert(!info.syncer || info.syncer == syncer);
+ info.syncer = syncer;
+ if (entry.slaveName()) {
+ assert(!info.slaveName || info.slaveName == entry.slaveName());
+ info.slaveName = entry.slaveName();
+ }
+
+ let newStatus = entry.buildRequestStatusIfUpdateIsNeeded(request);
+ if (newStatus) {
+ self._logger.log(`Updating the status of build request ${request.id()} from ${request.status()} to ${newStatus}`);
+ updates[entry.buildRequestId()] = {status: newStatus, url: entry.url()};
+ }
+ }
+ });
+ })).then(function () { return updates; });
+ }
+
+ _scheduleNextRequestInGroupIfSlaveIsAvailable(groupInfo, pendingUpdates)
+ {
+ let orderedRequests = groupInfo.requests.sort(function (a, b) { return a.order() - b.order(); });
+ let nextRequest = null;
+ for (let request of orderedRequests) {
+ if (request.isScheduled() || (request.id() in pendingUpdates && pendingUpdates[request.id()]['status'] == 'scheduled'))
+ break;
+ if (request.isPending() && !(request.id() in pendingUpdates)) {
+ nextRequest = request;
+ break;
+ }
+ }
+ if (!nextRequest)
+ return null;
+
+ let firstRequest = !nextRequest.order();
+ if (firstRequest) {
+ this._logger.log(`Syncing build request ${nextRequest.id()} on ${groupInfo.slaveName} in ${groupInfo.syncer.builderName()}`);
+ return groupInfo.syncer.scheduleRequest(request, groupInfo.slaveName);
+ }
+
+ for (let syncer of this._syncers) {
+ let promise = syncer.scheduleFirstRequestInGroupIfAvailable(nextRequest);
+ if (promise) {
+ let slaveName = groupInfo.slaveName ? ` on ${groupInfo.slaveName}` : '';
+ this._logger.log(`Syncing build request ${nextRequest.id()}${slaveName} in ${syncer.builderName()}`);
+ return promise;
+ }
+ }
+ return null;
+ }
+
+ static _testGroupMapForBuildRequests(buildRequests)
+ {
+ let map = new Map;
+ for (let request of buildRequests) {
+ let groupId = request.testGroupId();
+ if (!map.has(groupId)) // Don't use real TestGroup objects to avoid executing postgres query in the server
+ map.set(groupId, {id: groupId, requests: [request], syncer: null, slaveName: null});
+ else
+ map.get(groupId).requests.push(request);
+ }
+ return map;
+ }
+}
+
+if (typeof module != 'undefined')
+ module.exports.BuildbotTriggerable = BuildbotTriggerable;
'bug_trackers': 'tracker',
'build_triggerables': 'triggerable',
'build_requests': 'request',
+ 'build_slaves': 'slave',
'builders': 'builder',
'commits': 'commit',
'test_configurations': 'config',
--- /dev/null
+
+function parseArgument(argv, acceptedOptions) {
+ var args = argv.slice(2);
+ var options = {}
+ for (var i = 0; i < args.length; i += 2) {
+ var current = args[i];
+ var next = args[i + 1];
+ for (var option of acceptedOptions) {
+ if (current == option['name']) {
+ options[option['name']] = next;
+ next = null;
+ break;
+ }
+ }
+ if (next) {
+ console.error('Invalid argument:', current);
+ return null;
+ }
+ }
+ for (var option of acceptedOptions) {
+ var name = option['name'];
+ if (option['required'] && !(name in options)) {
+ console.log('Required argument', name, 'is missing');
+ return null;
+ }
+ var value = options[name] || option['default'];
+ var converter = option['type'];
+ options[name] = converter ? converter(value) : value;
+ }
+ return options;
+}
+
+if (typeof module != 'undefined')
+ module.exports.parseArgument = parseArgument;
let assert = require('assert');
let http = require('http');
let https = require('https');
+let querystring = require('querystring');
-let RemoteAPI = new (class RemoteAPI {
- constructor()
+class RemoteAPI {
+ constructor(server)
{
- this._server = {
- scheme: 'http',
- host: 'localhost',
- }
+ this._server = null;
+ if (server)
+ this.configure(server);
+ }
+
+ url(path)
+ {
+ let scheme = this._server.scheme;
+ let port = this._server.port;
+ let portSuffix = (scheme == 'http' && port == 80) || (scheme == 'https' && port == 443) ? '' : `:${port}`;
+ if (path.charAt(0) != '/')
+ path = '/' + path;
+ return `${scheme}://${this._server.host}portSuffix${path}`;
}
configure(server)
this._server = server;
}
- getJSON(path, data)
+ getJSON(path)
{
- let contentType = null;
- if (data) {
- contentType = 'application/json';
- data = JSON.stringify(data);
- }
- return this.sendHttpRequest(path, 'GET', contentType, data).then(function (result) {
+ return this.sendHttpRequest(path, 'GET', null, null).then(function (result) {
return JSON.parse(result.responseText);
});
}
- getJSONWithStatus(path, data)
+ getJSONWithStatus(path)
{
- return this.getJSON(path, data).then(function (result) {
+ return this.getJSON(path).then(function (result) {
if (result['status'] != 'OK')
return Promise.reject(result);
return result;
});
}
+ postJSON(path, data)
+ {
+ const contentType = 'application/json';
+ const payload = JSON.stringify(data);
+ return this.sendHttpRequest(path, 'POST', 'application/json', payload).then(function (result) {
+ return JSON.parse(result.responseText);
+ });
+ }
+
+ postFormUrlencodedData(path, data)
+ {
+ const contentType = 'application/x-www-form-urlencoded';
+ const payload = querystring.stringify(data);
+ return this.sendHttpRequest(path, 'POST', contentType, payload).then(function (result) {
+ return result.responseText;
+ });
+ }
+
sendHttpRequest(path, method, contentType, content)
{
let server = this._server;
let options = {
hostname: server.host,
port: server.port || 80,
- auth: server.auth,
+ auth: server.auth ? server.auth.username + ':' + server.auth.password : null,
method: method,
path: path,
};
request.end();
});
}
-})
+};
if (typeof module != 'undefined')
module.exports.RemoteAPI = RemoteAPI;
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');
--- /dev/null
+#!/usr/local/bin/node
+'use strict';
+
+let BuildbotTriggerable = require('./js/buildbot-triggerable.js').BuildbotTriggerable;
+let RemoteAPI = require('./js/remote.js').RemoteAPI;
+let fs = require('fs');
+let parseArguments = require('./js/parse-arguments.js').parseArguments;
+
+function main(argv)
+{
+ let options = parseArguments(argv, [
+ {name: '--server-config-json', required: true},
+ {name: '--buildbot-config-json', required: true},
+ {name: '--seconds-to-sleep', type: parseFloat, default: 120},
+ ]);
+ if (!options)
+ return;
+
+ syncLoop(options);
+}
+
+function syncLoop(options)
+{
+ let serverConfig = JSON.parse(fs.readFileSync(options['--server-config-json'], 'utf8'));
+ let buildbotConfig = JSON.parse(fs.readFileSync(options['--buildbot-config-json'], 'utf8'));
+ let buildbotRemote = new RemoteAPI(buildbotConfig.server);
+
+ // v3 models use the global RemoteAPI to access the perf dashboard.
+ global.RemoteAPI = new RemoteAPI(serverConfig.server);
+
+ console.log(`Fetching the manifest...`);
+ Manifest.fetch().then(function () {
+ let triggerable = new BuildbotTriggerable(buildbotConfig, global.RemoteAPI, buildbotRemote, serverConfig.slave, console);
+ return triggerable.syncOnce();
+ }).catch(function (error) {
+ console.error(error);
+ if (typeof(error.stack) == 'string') {
+ for (let line of error.stack.split('\n'))
+ console.error(line);
+ }
+ }).then(function () {
+ setTimeout(syncLoop.bind(global, options), options['--seconds-to-sleep'] * 1000);
+ });
+}
+
+main(process.argv);
'test': ['JetStream'],
'arguments': {'test_name': 'jetstream'}
},
- "dromaeo-dom": {
- "test": ["Dromaeo", "DOM Core Tests"],
- "arguments": {"tests": "dromaeo-dom"}
+ 'dromaeo-dom': {
+ 'test': ['Dromaeo', 'DOM Core Tests'],
+ 'arguments': {'tests': 'dromaeo-dom'}
},
},
'builders': {
'iPhone-bench': {
'builder': 'ABTest-iPhone-RunBenchmark-Tests',
- 'arguments': { 'forcescheduler': 'ABTest-iPhone-RunBenchmark-Tests-ForceScheduler' }
+ 'arguments': { 'forcescheduler': 'ABTest-iPhone-RunBenchmark-Tests-ForceScheduler' },
+ 'slaveList': ['ABTest-iPhone-0'],
},
'iPad-bench': {
'builder': 'ABTest-iPad-RunBenchmark-Tests',
- 'arguments': { 'forcescheduler': 'ABTest-iPad-RunBenchmark-Tests-ForceScheduler' }
+ 'arguments': { 'forcescheduler': 'ABTest-iPad-RunBenchmark-Tests-ForceScheduler' },
+ 'slaveList': ['ABTest-iPad-0'],
}
},
'configurations': [
}
};
+function smallConfiguration()
+{
+ return {
+ 'builder': 'some builder',
+ 'platform': 'Some platform',
+ 'test': ['Some test'],
+ 'arguments': {},
+ 'buildRequestArgument': 'id'};
+}
+
+function smallPendingBuild()
+{
+ return {
+ 'builderName': 'some builder',
+ 'builds': [],
+ 'properties': [],
+ 'source': {
+ 'branch': '',
+ 'changes': [],
+ 'codebase': 'WebKit',
+ 'hasPatch': false,
+ 'project': '',
+ 'repository': '',
+ 'revision': ''
+ },
+ };
+}
+
+function smallInProgressBuild()
+{
+ return {
+ 'builderName': 'some builder',
+ 'builds': [],
+ 'properties': [],
+ 'currentStep': { },
+ 'eta': 123,
+ 'number': 456,
+ 'source': {
+ 'branch': '',
+ 'changes': [],
+ 'codebase': 'WebKit',
+ 'hasPatch': false,
+ 'project': '',
+ 'repository': '',
+ 'revision': ''
+ },
+ };
+}
+
+function smallFinishedBuild()
+{
+ return {
+ 'builderName': 'some builder',
+ 'builds': [],
+ 'properties': [],
+ 'currentStep': null,
+ 'eta': null,
+ 'number': 789,
+ 'source': {
+ 'branch': '',
+ 'changes': [],
+ 'codebase': 'WebKit',
+ 'hasPatch': false,
+ 'project': '',
+ 'repository': '',
+ 'revision': ''
+ },
+ 'times': [0, 1],
+ };
+}
+
function createSampleBuildRequest(platform, test)
{
assert(platform instanceof Platform);
{'id': '88930', 'time': 0, 'repository': MockModels.ios, 'revision': '13A452'},
]});
- let request = BuildRequest.ensureSingleton('16733', {'rootSet': rootSet, 'status': 'pending', 'platform': platform, 'test': test});
+ let request = BuildRequest.ensureSingleton('16733-' + platform.id(), {'rootSet': rootSet, 'status': 'pending', 'platform': platform, 'test': test});
return request;
}
-function samplePendingBuild(buildRequestId)
+function samplePendingBuild(buildRequestId, buildTime, slaveName)
{
return {
'builderName': 'ABTest-iPad-RunBenchmark-Tests',
JSON.stringify(sampleRootSetData),
'Force Build Form'
],
+ ['slavename', slaveName, ''],
['scheduler', 'ABTest-iPad-RunBenchmark-Tests-ForceScheduler', 'Scheduler']
],
'source': {
'repository': '',
'revision': ''
},
- 'submittedAt': 1458704983
+ 'submittedAt': buildTime || 1458704983
};
}
-function sampleInProgressBuild()
+function sampleInProgressBuild(slaveName)
{
return {
'blame': [],
['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'],
+ ['slavename', slaveName || 'ABTest-iPad-0', 'BuildSlave'],
],
'reason': 'A build was forced by \'<unknown>\': force build',
'results': null,
};
}
-function sampleFinishedBuild(buildRequestId)
+function sampleFinishedBuild(buildRequestId, slaveName)
{
return {
'blame': [],
['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'],
+ ['slavename', slaveName || 'ABTest-iPad-0', 'BuildSlave'],
],
'reason': 'A build was forced by \'<unknown>\': force build',
'results': 2,
describe('BuildbotSyncer', function () {
MockModels.inject();
- let requests = MockRemoteAPI.inject();
+ let requests = MockRemoteAPI.inject('http://build.webkit.org');
describe('_loadConfig', function () {
- function smallConfiguration()
- {
- return {
- 'builder': 'some builder',
- 'platform': 'some platform',
- 'test': ['some test'],
- 'arguments': {},
- 'buildRequestArgument': 'id'};
- }
-
it('should create BuildbotSyncer objects for a configuration that specify all required options', function () {
- let syncers = BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [smallConfiguration()]});
+ let syncers = BuildbotSyncer._loadConfig(MockRemoteAPI, {'configurations': [smallConfiguration()]});
assert.equal(syncers.length, 1);
});
assert.throws(function () {
let config = smallConfiguration();
delete config['builder'];
- BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ BuildbotSyncer._loadConfig(MockRemoteAPI, {'configurations': [config]});
}, 'builder should be a required option');
assert.throws(function () {
let config = smallConfiguration();
delete config['platform'];
- BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ BuildbotSyncer._loadConfig(MockRemoteAPI, {'configurations': [config]});
}, 'platform should be a required option');
assert.throws(function () {
let config = smallConfiguration();
delete config['test'];
- BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ BuildbotSyncer._loadConfig(MockRemoteAPI, {'configurations': [config]});
}, 'test should be a required option');
assert.throws(function () {
let config = smallConfiguration();
delete config['arguments'];
- BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ BuildbotSyncer._loadConfig(MockRemoteAPI, {'configurations': [config]});
});
assert.throws(function () {
let config = smallConfiguration();
delete config['buildRequestArgument'];
- BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ BuildbotSyncer._loadConfig(MockRemoteAPI, {'configurations': [config]});
});
});
assert.throws(function () {
let config = smallConfiguration();
config.test = 'some test';
- BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ BuildbotSyncer._loadConfig(MockRemoteAPI, {'configurations': [config]});
});
assert.throws(function () {
let config = smallConfiguration();
config.test = [1];
- BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ BuildbotSyncer._loadConfig(MockRemoteAPI, {'configurations': [config]});
});
});
assert.throws(function () {
let config = smallConfiguration();
config.arguments = 'hello';
- BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ BuildbotSyncer._loadConfig(MockRemoteAPI, {'configurations': [config]});
});
});
assert.throws(function () {
let config = smallConfiguration();
config.arguments = {'some': {'root': 'some root', 'rootsExcluding': ['other root']}};
- BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ BuildbotSyncer._loadConfig(RemoteAPI, {'configurations': [config]});
});
assert.throws(function () {
let config = smallConfiguration();
config.arguments = {'some': {'otherKey': 'some root'}};
- BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ BuildbotSyncer._loadConfig(RemoteAPI, {'configurations': [config]});
});
assert.throws(function () {
let config = smallConfiguration();
config.arguments = {'some': {'root': ['a', 'b']}};
- BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ BuildbotSyncer._loadConfig(RemoteAPI, {'configurations': [config]});
});
assert.throws(function () {
let config = smallConfiguration();
config.arguments = {'some': {'root': 1}};
- BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ BuildbotSyncer._loadConfig(RemoteAPI, {'configurations': [config]});
});
assert.throws(function () {
let config = smallConfiguration();
config.arguments = {'some': {'rootsExcluding': 'a'}};
- BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ BuildbotSyncer._loadConfig(RemoteAPI, {'configurations': [config]});
});
assert.throws(function () {
let config = smallConfiguration();
config.arguments = {'some': {'rootsExcluding': [1]}};
- BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ BuildbotSyncer._loadConfig(RemoteAPI, {'configurations': [config]});
});
});
it('should create BuildbotSyncer objects for valid configurations', function () {
- let syncers = BuildbotSyncer._loadConfig('http://build.webkit.org/', sampleiOSConfig());
- assert.equal(syncers.length, 5);
+ let syncers = BuildbotSyncer._loadConfig(RemoteAPI, sampleiOSConfig());
+ assert.equal(syncers.length, 2);
assert.ok(syncers[0] instanceof BuildbotSyncer);
assert.ok(syncers[1] instanceof BuildbotSyncer);
- assert.ok(syncers[2] instanceof BuildbotSyncer);
- assert.ok(syncers[3] instanceof BuildbotSyncer);
- assert.ok(syncers[4] instanceof BuildbotSyncer);
});
it('should parse builder names correctly', function () {
- let syncers = BuildbotSyncer._loadConfig('http://build.webkit.org/', sampleiOSConfig());
+ let syncers = BuildbotSyncer._loadConfig(RemoteAPI, sampleiOSConfig());
assert.equal(syncers[0].builderName(), 'ABTest-iPhone-RunBenchmark-Tests');
- assert.equal(syncers[1].builderName(), 'ABTest-iPhone-RunBenchmark-Tests');
- assert.equal(syncers[2].builderName(), 'ABTest-iPhone-RunBenchmark-Tests');
- assert.equal(syncers[3].builderName(), 'ABTest-iPad-RunBenchmark-Tests');
- assert.equal(syncers[4].builderName(), 'ABTest-iPad-RunBenchmark-Tests');
+ assert.equal(syncers[1].builderName(), 'ABTest-iPad-RunBenchmark-Tests');
});
- it('should parse platform names correctly', function () {
- let syncers = BuildbotSyncer._loadConfig('http://build.webkit.org/', sampleiOSConfig());
- assert.equal(syncers[0].platformName(), 'iPhone');
- assert.equal(syncers[1].platformName(), 'iPhone');
- assert.equal(syncers[2].platformName(), 'iPhone');
- assert.equal(syncers[3].platformName(), 'iPad');
- assert.equal(syncers[4].platformName(), 'iPad');
- });
-
- it('should parse test names correctly', function () {
- let syncers = BuildbotSyncer._loadConfig('http://build.webkit.org/', sampleiOSConfig());
- assert.deepEqual(syncers[0].testPath(), ['Speedometer']);
- assert.deepEqual(syncers[1].testPath(), ['JetStream']);
- assert.deepEqual(syncers[2].testPath(), ['Dromaeo', 'DOM Core Tests']);
- assert.deepEqual(syncers[3].testPath(), ['Speedometer']);
- assert.deepEqual(syncers[4].testPath(), ['JetStream']);
+ it('should parse test configurations correctly', function () {
+ let syncers = BuildbotSyncer._loadConfig(RemoteAPI, sampleiOSConfig());
+
+ let configurations = syncers[0].testConfigurations();
+ assert.equal(configurations.length, 3);
+ assert.equal(configurations[0].platform, MockModels.iphone);
+ assert.equal(configurations[0].test, MockModels.speedometer);
+ assert.equal(configurations[1].platform, MockModels.iphone);
+ assert.equal(configurations[1].test, MockModels.jetstream);
+ assert.equal(configurations[2].platform, MockModels.iphone);
+ assert.equal(configurations[2].test, MockModels.domcore);
+
+ configurations = syncers[1].testConfigurations();
+ assert.equal(configurations.length, 2);
+ assert.equal(configurations[0].platform, MockModels.ipad);
+ assert.equal(configurations[0].test, MockModels.speedometer);
+ assert.equal(configurations[1].platform, MockModels.ipad);
+ assert.equal(configurations[1].test, MockModels.jetstream);
});
});
describe('_propertiesForBuildRequest', function () {
it('should include all properties specified in a given configuration', function () {
- let syncers = BuildbotSyncer._loadConfig('http://build.webkit.org/', sampleiOSConfig());
+ let syncers = BuildbotSyncer._loadConfig(RemoteAPI, sampleiOSConfig());
let properties = syncers[0]._propertiesForBuildRequest(createSampleBuildRequest(MockModels.iphone, MockModels.speedometer));
assert.deepEqual(Object.keys(properties), ['desired_image', 'roots_dict', 'test_name', 'forcescheduler', 'build_request_id']);
});
it('should preserve non-parametric property values', function () {
- let syncers = BuildbotSyncer._loadConfig('http://build.webkit.org/', sampleiOSConfig());
+ let syncers = BuildbotSyncer._loadConfig(RemoteAPI, sampleiOSConfig());
let properties = syncers[0]._propertiesForBuildRequest(createSampleBuildRequest(MockModels.iphone, MockModels.speedometer));
assert.equal(properties['test_name'], 'speedometer');
assert.equal(properties['forcescheduler'], 'ABTest-iPhone-RunBenchmark-Tests-ForceScheduler');
- properties = syncers[1]._propertiesForBuildRequest(createSampleBuildRequest(MockModels.iphone, MockModels.speedometer));
+ properties = syncers[1]._propertiesForBuildRequest(createSampleBuildRequest(MockModels.ipad, MockModels.jetstream));
assert.equal(properties['test_name'], 'jetstream');
- assert.equal(properties['forcescheduler'], 'ABTest-iPhone-RunBenchmark-Tests-ForceScheduler');
+ assert.equal(properties['forcescheduler'], 'ABTest-iPad-RunBenchmark-Tests-ForceScheduler');
});
it('should resolve "root"', function () {
- let syncers = BuildbotSyncer._loadConfig('http://build.webkit.org/', sampleiOSConfig());
+ let syncers = BuildbotSyncer._loadConfig(RemoteAPI, sampleiOSConfig());
let properties = syncers[0]._propertiesForBuildRequest(createSampleBuildRequest(MockModels.iphone, MockModels.speedometer));
assert.equal(properties['desired_image'], '13A452');
});
it('should resolve "rootsExcluding"', function () {
- let syncers = BuildbotSyncer._loadConfig('http://build.webkit.org/', sampleiOSConfig());
+ let syncers = BuildbotSyncer._loadConfig(RemoteAPI, sampleiOSConfig());
let properties = syncers[0]._propertiesForBuildRequest(createSampleBuildRequest(MockModels.iphone, MockModels.speedometer));
assert.equal(properties['roots_dict'], JSON.stringify(sampleRootSetData));
});
it('should set the property for the build request id', function () {
- let syncers = BuildbotSyncer._loadConfig('http://build.webkit.org/', sampleiOSConfig());
+ let syncers = BuildbotSyncer._loadConfig(RemoteAPI, sampleiOSConfig());
let request = createSampleBuildRequest(MockModels.iphone, MockModels.speedometer);
let properties = syncers[0]._propertiesForBuildRequest(request);
assert.equal(properties['build_request_id'], request.id());
describe('pullBuildbot', function () {
it('should fetch pending builds from the right URL', function () {
- let syncer = BuildbotSyncer._loadConfig('http://build.webkit.org', sampleiOSConfig())[3];
+ let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig())[1];
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);
+ let expectedURL = '/json/builders/ABTest-iPad-RunBenchmark-Tests/pendingBuilds';
+ assert.equal(syncer.pathForPendingBuildsJSON(), 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];
+ let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig())[1];
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');
+ assert.equal(requests[0].url, '/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');
+ assert.equal(requests[1].url, '/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];
+ let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig())[1];
syncer.pullBuildbot(3);
assert.equal(requests.length, 1);
- assert.equal(requests[0].url, 'http://build.webkit.org/json/builders/ABTest-iPad-RunBenchmark-Tests/pendingBuilds');
+ assert.equal(requests[0].url, '/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');
+ assert.equal(requests[1].url, '/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 syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig())[1];
let promise = syncer.pullBuildbot();
requests[0].resolve([samplePendingBuild()]);
promise.then(function (entries) {
- assert.deepEqual(Object.keys(entries), ['16733']);
- let entry = entries['16733'];
+ assert.equal(entries.length, 1);
+ let entry = entries[0];
assert.ok(entry instanceof BuildbotBuildEntry);
assert.ok(!entry.buildNumber());
assert.ok(!entry.slaveName());
});
it('should create BuildbotBuildEntry for in-progress builds', function (done) {
- let syncer = BuildbotSyncer._loadConfig('http://build.webkit.org', sampleiOSConfig())[3];
+ let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig())[1];
let promise = syncer.pullBuildbot(1);
assert.equal(requests.length, 1);
}).catch(done);
promise.then(function (entries) {
- assert.deepEqual(Object.keys(entries), ['16733']);
- let entry = entries['16733'];
+ assert.equal(entries.length, 1);
+ let entry = entries[0];
assert.ok(entry instanceof BuildbotBuildEntry);
assert.equal(entry.buildNumber(), 614);
assert.equal(entry.slaveName(), 'ABTest-iPad-0');
});
it('should create BuildbotBuildEntry for finished builds', function (done) {
- let syncer = BuildbotSyncer._loadConfig('http://build.webkit.org', sampleiOSConfig())[3];
+ let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig())[1];
let promise = syncer.pullBuildbot(1);
assert.equal(requests.length, 1);
}).catch(done);
promise.then(function (entries) {
- assert.deepEqual(Object.keys(entries), ['18935']);
- let entry = entries['18935'];
+ assert.deepEqual(entries.length, 1);
+ let entry = entries[0];
assert.ok(entry instanceof BuildbotBuildEntry);
assert.equal(entry.buildNumber(), 1755);
assert.equal(entry.slaveName(), 'ABTest-iPad-0');
});
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 syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig())[1];
let promise = syncer.pullBuildbot(5);
assert.equal(requests.length, 1);
- requests[0].resolve([samplePendingBuild(123, 456)]);
+ requests[0].resolve([samplePendingBuild(123)]);
Promise.resolve().then(function () {
assert.equal(requests.length, 2);
}).catch(done);
promise.then(function (entries) {
- assert.deepEqual(Object.keys(entries), ['123', '16733', '18935']);
+ assert.deepEqual(entries.length, 3);
+
+ let entry = entries[0];
+ 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[1];
+ 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[2];
+ 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 sort BuildbotBuildEntry by order', function (done) {
+ let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig())[1];
+
+ let promise = syncer.pullBuildbot(5);
+ assert.equal(requests.length, 1);
+
+ requests[0].resolve([samplePendingBuild(456, 2), samplePendingBuild(123, 1)]);
+
+ Promise.resolve().then(function () {
+ assert.equal(requests.length, 2);
+ requests[1].resolve({[-3]: sampleFinishedBuild(), [-1]: {'error': 'Not available'}, [-2]: sampleInProgressBuild()});
+ }).catch(done);
+
+ promise.then(function (entries) {
+ assert.deepEqual(entries.length, 4);
- let entry = entries['123'];
+ let entry = entries[0];
assert.ok(entry instanceof BuildbotBuildEntry);
assert.equal(entry.buildNumber(), null);
assert.equal(entry.slaveName(), null);
assert.ok(!entry.hasFinished());
assert.equal(entry.url(), 'http://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/');
- entry = entries['16733'];
+ entry = entries[1];
+ assert.ok(entry instanceof BuildbotBuildEntry);
+ assert.equal(entry.buildNumber(), null);
+ assert.equal(entry.slaveName(), null);
+ assert.equal(entry.buildRequestId(), 456);
+ 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[2];
assert.ok(entry instanceof BuildbotBuildEntry);
assert.equal(entry.buildNumber(), 614);
assert.equal(entry.slaveName(), 'ABTest-iPad-0');
assert.ok(!entry.hasFinished());
assert.equal(entry.url(), 'http://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/614');
- entry = entries['18935'];
+ entry = entries[3];
assert.ok(entry instanceof BuildbotBuildEntry);
assert.equal(entry.buildNumber(), 1755);
assert.equal(entry.slaveName(), 'ABTest-iPad-0');
});
it('should override BuildbotBuildEntry for pending builds by in-progress builds', function (done) {
- let syncer = BuildbotSyncer._loadConfig('http://build.webkit.org', sampleiOSConfig())[3];
+ let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig())[1];
let promise = syncer.pullBuildbot(5);
assert.equal(requests.length, 1);
}).catch(done);
promise.then(function (entries) {
- assert.deepEqual(Object.keys(entries), ['16733']);
+ assert.equal(entries.length, 1);
- let entry = entries['16733'];
+ let entry = entries[0];
assert.ok(entry instanceof BuildbotBuildEntry);
assert.equal(entry.buildNumber(), 614);
assert.equal(entry.slaveName(), 'ABTest-iPad-0');
});
it('should override BuildbotBuildEntry for pending builds by finished builds', function (done) {
- let syncer = BuildbotSyncer._loadConfig('http://build.webkit.org', sampleiOSConfig())[3];
+ let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig())[1];
let promise = syncer.pullBuildbot(5);
assert.equal(requests.length, 1);
}).catch(done);
promise.then(function (entries) {
- assert.deepEqual(Object.keys(entries), ['16733']);
+ assert.equal(entries.length, 1);
- let entry = entries['16733'];
+ let entry = entries[0];
assert.ok(entry instanceof BuildbotBuildEntry);
assert.equal(entry.buildNumber(), 1755);
assert.equal(entry.slaveName(), 'ABTest-iPad-0');
done();
}).catch(done);
});
+ });
+
+ describe('scheduleRequest', function () {
+ it('should schedule a build request on a specified slave', function (done) {
+ let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig())[0];
+
+ syncer.scheduleRequest(createSampleBuildRequest(MockModels.iphone, MockModels.speedometer), 'some-slave');
+ Promise.resolve().then(function () {
+ assert.equal(requests.length, 1);
+ assert.equal(requests[0].url, '/builders/ABTest-iPhone-RunBenchmark-Tests/force');
+ assert.equal(requests[0].method, 'POST');
+ assert.deepEqual(requests[0].data, {
+ 'build_request_id': '16733-' + MockModels.iphone.id(),
+ 'desired_image': '13A452',
+ 'forcescheduler': 'ABTest-iPhone-RunBenchmark-Tests-ForceScheduler',
+ 'roots_dict': '{"WebKit":{"id":"111127","time":1456955807334,"repository":"WebKit","revision":"197463"},'
+ + '"Shared":{"id":"111237","time":1456931874000,"repository":"Shared","revision":"80229"}}',
+ 'slavename': 'some-slave',
+ 'test_name': 'speedometer'
+ });
+ done();
+ }).catch(done);
+ });
+ });
+
+ describe('scheduleFirstRequestInGroupIfAvailable', function () {
+
+ function pullBuildbotWithAssertion(syncer, pendingBuilds, inProgressAndFinishedBuilds)
+ {
+ let promise = syncer.pullBuildbot(5);
+ assert.equal(requests.length, 1);
+ requests[0].resolve(pendingBuilds);
+ return Promise.resolve().then(function () {
+ assert.equal(requests.length, 2);
+ requests[1].resolve(inProgressAndFinishedBuilds);
+ requests.length = 0;
+ }).then(function () {
+ return promise;
+ });
+ }
+
+ it('should schedule a build if builder has no builds if slaveList is not specified', function (done) {
+ let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, {'configurations': [smallConfiguration()]})[0];
+
+ pullBuildbotWithAssertion(syncer, [], {}).then(function () {
+ syncer.scheduleFirstRequestInGroupIfAvailable(createSampleBuildRequest(MockModels.somePlatform, MockModels.someTest));
+ }).then(function () {
+ assert.equal(requests.length, 1);
+ assert.equal(requests[0].url, '/builders/some builder/force');
+ assert.equal(requests[0].method, 'POST');
+ assert.deepEqual(requests[0].data, {id: '16733-' + MockModels.somePlatform.id()});
+ done();
+ }).catch(done);
+ });
+
+ it('should schedule a build if builder only has finished builds if slaveList is not specified', function (done) {
+ let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, {'configurations': [smallConfiguration()]})[0];
+
+ pullBuildbotWithAssertion(syncer, [], {[-1]: smallFinishedBuild()}).then(function () {
+ syncer.scheduleFirstRequestInGroupIfAvailable(createSampleBuildRequest(MockModels.somePlatform, MockModels.someTest));
+ }).then(function () {
+ assert.equal(requests.length, 1);
+ assert.equal(requests[0].url, '/builders/some builder/force');
+ assert.equal(requests[0].method, 'POST');
+ assert.deepEqual(requests[0].data, {id: '16733-' + MockModels.somePlatform.id()});
+ done();
+ }).catch(done);
+ });
+
+ it('should not schedule a build if builder has a pending build if slaveList is not specified', function (done) {
+ let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, {'configurations': [smallConfiguration()]})[0];
+
+ pullBuildbotWithAssertion(syncer, [smallPendingBuild()], {}).then(function () {
+ syncer.scheduleFirstRequestInGroupIfAvailable(createSampleBuildRequest(MockModels.somePlatform, MockModels.someTest));
+ }).then(function () {
+ assert.equal(requests.length, 0);
+ done();
+ }).catch(done);
+ });
+
+ it('should schedule a build if builder does not have pending or completed builds on the matching slave', function (done) {
+ let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig())[0];
+
+ pullBuildbotWithAssertion(syncer, [], {}).then(function () {
+ syncer.scheduleFirstRequestInGroupIfAvailable(createSampleBuildRequest(MockModels.iphone, MockModels.speedometer));
+ }).then(function () {
+ assert.equal(requests.length, 1);
+ assert.equal(requests[0].url, '/builders/ABTest-iPhone-RunBenchmark-Tests/force');
+ assert.equal(requests[0].method, 'POST');
+ done();
+ }).catch(done);
+ });
+
+ it('should schedule a build if builder only has finished builds on the matching slave', function (done) {
+ let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig())[1];
+
+ pullBuildbotWithAssertion(syncer, [], {[-1]: sampleFinishedBuild()}).then(function () {
+ syncer.scheduleFirstRequestInGroupIfAvailable(createSampleBuildRequest(MockModels.ipad, MockModels.speedometer));
+ }).then(function () {
+ assert.equal(requests.length, 1);
+ assert.equal(requests[0].url, '/builders/ABTest-iPad-RunBenchmark-Tests/force');
+ assert.equal(requests[0].method, 'POST');
+ done();
+ }).catch(done);
+ });
+
+ it('should not schedule a build if builder has a pending build on the maching slave', function (done) {
+ let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig())[1];
+
+ pullBuildbotWithAssertion(syncer, [samplePendingBuild()], {}).then(function () {
+ syncer.scheduleFirstRequestInGroupIfAvailable(createSampleBuildRequest(MockModels.ipad, MockModels.speedometer));
+ }).then(function () {
+ assert.equal(requests.length, 0);
+ done();
+ }).catch(done);
+ });
+
+ it('should schedule a build if builder only has a pending build on a non-maching slave', function (done) {
+ let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig())[1];
+
+ pullBuildbotWithAssertion(syncer, [samplePendingBuild(1, 1, 'another-slave')], {}).then(function () {
+ syncer.scheduleFirstRequestInGroupIfAvailable(createSampleBuildRequest(MockModels.ipad, MockModels.speedometer));
+ }).then(function () {
+ assert.equal(requests.length, 1);
+ done();
+ }).catch(done);
+ });
+
+ it('should schedule a build if builder only has an in-progress build on the matching slave', function (done) {
+ let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig())[1];
+
+ pullBuildbotWithAssertion(syncer, [], {[-1]: sampleInProgressBuild()}).then(function () {
+ syncer.scheduleFirstRequestInGroupIfAvailable(createSampleBuildRequest(MockModels.ipad, MockModels.speedometer));
+ }).then(function () {
+ assert.equal(requests.length, 1);
+ done();
+ }).catch(done);
+ });
+
+ it('should schedule a build if builder has an in-progress build on another slave', function (done) {
+ let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig())[1];
+
+ pullBuildbotWithAssertion(syncer, [], {[-1]: sampleInProgressBuild('other-slave')}).then(function () {
+ syncer.scheduleFirstRequestInGroupIfAvailable(createSampleBuildRequest(MockModels.ipad, MockModels.speedometer));
+ }).then(function () {
+ assert.equal(requests.length, 1);
+ done();
+ }).catch(done);
+ });
+
+ it('should not schedule a build if the request does not match any configuration', function (done) {
+ let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig())[0];
+
+ pullBuildbotWithAssertion(syncer, [], {}).then(function () {
+ syncer.scheduleFirstRequestInGroupIfAvailable(createSampleBuildRequest(MockModels.ipad, MockModels.speedometer));
+ }).then(function () {
+ assert.equal(requests.length, 0);
+ done();
+ }).catch(done);
+ });
});
});
assert.notReached = function () { assert(false, 'This code path should not be reached'); }
var MockRemoteAPI = {
+ url: function (path)
+ {
+ return `${this.urlPrefix}${path}`;
+ },
getJSON: function (url)
{
return this.getJSONWithStatus(url);
},
getJSONWithStatus: function (url)
+ {
+ return this._addRequest(url, 'GET', null);
+ },
+ postFormUrlencodedData: function (url, data)
+ {
+ return this._addRequest(url, 'POST', data);
+ },
+ _addRequest: function (url, method, data)
{
var request = {
url: url,
+ method: method,
+ data: data,
promise: null,
resolve: null,
reject: null,
request.reject = reject;
});
+ if (this._waitingPromise) {
+ this._waitingPromiseResolver();
+ this._waitingPromise = null;
+ this._waitingPromiseResolver = null;
+ }
+
MockRemoteAPI.requests.push(request);
return request.promise;
},
- inject: function ()
+ waitForRequest()
+ {
+ if (!this._waitingPromise) {
+ this._waitingPromise = new Promise(function (resolve, reject) {
+ MockRemoteAPI._waitingPromiseResolver = resolve;
+ });
+ }
+ return this._waitingPromise;
+ },
+ inject: function (urlPrefix)
{
var originalRemoteAPI = global.RemoteAPI;
beforeEach(function () {
- MockRemoteAPI.requests.length = 0;
+ MockRemoteAPI.reset(urlPrefix);
originalRemoteAPI = global.RemoteAPI;
global.RemoteAPI = MockRemoteAPI;
});
});
return MockRemoteAPI.requests;
- }
+ },
+ reset: function (urlPrefix)
+ {
+ if (urlPrefix)
+ MockRemoteAPI.urlPrefix = urlPrefix;
+ MockRemoteAPI.requests.length = 0;
+ },
+ requests: [],
+ _waitingPromise: null,
+ _waitingPromiseResolver: null,
+ urlPrefix: 'http://mockhost',
};
-MockRemoteAPI.requests = [];
if (typeof module != 'undefined')
module.exports.MockRemoteAPI = MockRemoteAPI;
AnalysisTask.clearStaticMap();
CommitLog.clearStaticMap();
Metric.clearStaticMap();
+ Platform.clearStaticMap();
+ Repository.clearStaticMap();
RootSet.clearStaticMap();
Test.clearStaticMap();
TestGroup.clearStaticMap();