Added: trunk/Websites/perf.webkit.org/tools/js/buildbot-syncer.js (0 => 198614)
--- trunk/Websites/perf.webkit.org/tools/js/buildbot-syncer.js (rev 0)
+++ trunk/Websites/perf.webkit.org/tools/js/buildbot-syncer.js 2016-03-24 03:25:10 UTC (rev 198614)
@@ -0,0 +1,216 @@
+'use strict';
+
+let assert = require('assert');
+
+require('./v3-models.js');
+
+class BuildbotBuildEntry {
+ constructor(syncer, type, rawData)
+ {
+ assert.equal(syncer.builderName(), rawData['builderName']);
+
+ this._slaveName = null;
+ this._buildRequestId = null;
+ this._isInProgress = 'currentStep' in rawData;
+ this._buildNumber = rawData['number'];
+
+ for (var propertyTuple of (rawData['properties'] || [])) {
+ // e.g. ['build_request_id', '16733', 'Force Build Form']
+ var name = propertyTuple[0];
+ var value = propertyTuple[1];
+ if (name == syncer._slavePropertyName)
+ this._slaveName = value;
+ else if (name == syncer._buildRequestPropertyName)
+ this._buildRequestId = value;
+ }
+ }
+
+ slaveName() { return this._slaveName; }
+ buildRequestId() { return this._buildRequestId; }
+ isInProgress() { return this._isInProgress; }
+}
+
+class BuildbotSyncer {
+
+ constructor(url, object)
+ {
+ this._url = url;
+ this._builderName = object.builder;
+ this._platformName = object.platform;
+ this._testPath = object.test;
+ this._propertiesTemplate = object.properties;
+ this._slavePropertyName = object.slaveArgument;
+ this._buildRequestPropertyName = object.buildRequestArgument;
+ }
+
+ testPath() { return this._testPath }
+ builderName() { return this._builderName; }
+ platformName() { return this._platformName; }
+
+ fetchPendingRequests()
+ {
+ return RemoteAPI.fetchJSON(`${this._url}/json/builders/${this._name}/pendingBuilds`).then(function (content) {
+ var requests = [];
+ for (var entry of content) {
+ var properties = entry['properties'];
+ if (!properties)
+ continue;
+ for (var propertyTuple of properties) {
+ // e.g. ['build_request_id', '16733', 'Force Build Form']
+ if (propertyTuple[0] == this._buildRequestPropertyName)
+ requests.push(propertyTuple[1]);
+ }
+ }
+ return requests;
+ });
+ }
+
+ _propertiesForBuildRequest(buildRequest)
+ {
+ console.assert(buildRequest instanceof BuildRequest);
+
+ let rootSet = buildRequest.rootSet();
+ console.assert(rootSet instanceof RootSet);
+
+ let repositoryByName = {};
+ for (let repository of rootSet.repositories())
+ repositoryByName[repository.name()] = repository;
+
+ let properties = {};
+ for (let key in this._propertiesTemplate) {
+ let value = this._propertiesTemplate[key];
+ if (typeof(value) != 'object')
+ properties[key] = value;
+ else if ('root' in value) {
+ let repositoryName = value['root'];
+ let repository = repositoryByName[repositoryName];
+ assert(repository, '"${repositoryName}" must be specified');
+ properties[key] = rootSet.revisionForRepository(repository);
+ } else if ('rootsExcluding' in value) {
+ let revisionSet = this._revisionSetFromRootSetWithExclusionList(rootSet, value['rootsExcluding']);
+ properties[key] = JSON.stringify(revisionSet);
+ }
+ }
+
+ properties[this._buildRequestPropertyName] = buildRequest.id();
+
+ return properties;
+ }
+
+ _revisionSetFromRootSetWithExclusionList(rootSet, exclusionList)
+ {
+ let revisionSet = {};
+ for (let repository of rootSet.repositories()) {
+ if (exclusionList.indexOf(repository.name()) >= 0)
+ continue;
+ let commit = rootSet.commitForRepository(repository);
+ revisionSet[repository.name()] = {
+ id: commit.id(),
+ time: +commit.time(),
+ repository: repository.name(),
+ revision: commit.revision(),
+ };
+ }
+ return revisionSet;
+ }
+
+ static _loadConfig(url, config)
+ {
+ let shared = config['shared'] || {};
+ let types = config['types'] || {};
+ let builders = config['builders'] || {};
+
+ let syncers = [];
+ for (let entry of config['configurations']) {
+ let newConfig = {};
+ this._validateAndMergeConfig(newConfig, shared);
+
+ this._validateAndMergeConfig(newConfig, entry);
+
+ let type = entry['type'];
+ if (type) {
+ assert(types[type]);
+ this._validateAndMergeConfig(newConfig, types[type]);
+ }
+
+ let builder = entry['builder'];
+ if (builders[builder])
+ this._validateAndMergeConfig(newConfig, builders[builder]);
+
+ assert('platform' in newConfig, 'configuration must specify a platform');
+ assert('test' in newConfig, 'configuration must specify a test');
+ 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));
+ }
+
+ return syncers;
+ }
+
+ static _validateAndMergeConfig(config, valuesToMerge)
+ {
+ for (let name in valuesToMerge) {
+ let value = valuesToMerge[name];
+ switch (name) {
+ case 'arguments':
+ assert.equal(typeof(value), 'object', 'arguments should be a dictionary');
+ if (!config['properties'])
+ config['properties'] = {};
+ this._validateAndMergeProperties(config['properties'], value);
+ break;
+ case 'test':
+ assert(value instanceof Array, 'test should be an array');
+ assert(value.every(function (part) { return typeof part == 'string'; }), 'test should be an array of strings');
+ config[name] = value.slice();
+ break;
+ case 'type': // fallthrough
+ case 'builder': // fallthrough
+ case 'platform': // fallthrough
+ case 'slaveArgument': // fallthrough
+ case 'buildRequestArgument':
+ assert.equal(typeof(value), 'string', `${name} should be of string type`);
+ config[name] = value;
+ break;
+ default:
+ assert(false, `Unrecognized parameter ${name}`);
+ }
+ }
+ }
+
+ static _validateAndMergeProperties(properties, configArguments)
+ {
+ for (let name in configArguments) {
+ let value = configArguments[name];
+ if (typeof(value) == 'string') {
+ properties[name] = value;
+ continue;
+ }
+ assert.equal(typeof(value), 'object', 'A argument value must be either a string or a dictionary');
+
+ let keys = Object.keys(value);
+ assert.equal(keys.length, 1, 'arguments value cannot contain more than one key');
+ let namedValue = value[keys[0]];
+ switch (keys[0]) {
+ case 'root':
+ assert.equal(typeof(namedValue), 'string', 'root name must be a string');
+ break;
+ case 'rootsExcluding':
+ assert(namedValue instanceof Array, 'rootsExcluding must specify an array');
+ for (let excludedRootName of namedValue)
+ assert.equal(typeof(excludedRootName), 'string', 'rootsExcluding must specify an array of strings');
+ namedValue = namedValue.slice();
+ break;
+ default:
+ assert(false, `Unrecognized named argument ${keys[0]}`);
+ }
+ properties[name] = {[keys[0]]: namedValue};
+ }
+ }
+
+}
+
+if (typeof module != 'undefined') {
+ module.exports.BuildbotSyncer = BuildbotSyncer;
+ module.exports.BuildbotBuildEntry = BuildbotBuildEntry;
+}
Added: trunk/Websites/perf.webkit.org/unit-tests/buildbot-syncer-tests.js (0 => 198614)
--- trunk/Websites/perf.webkit.org/unit-tests/buildbot-syncer-tests.js (rev 0)
+++ trunk/Websites/perf.webkit.org/unit-tests/buildbot-syncer-tests.js 2016-03-24 03:25:10 UTC (rev 198614)
@@ -0,0 +1,295 @@
+'use strict';
+
+let assert = require('assert');
+
+require('./resources/mock-remote-api.js');
+require('../tools/js/v3-models.js');
+require('./resources/mock-v3-models.js');
+
+let BuildbotBuildEntry = require('../tools/js/buildbot-syncer.js').BuildbotBuildEntry;
+let BuildbotSyncer = require('../tools/js/buildbot-syncer.js').BuildbotSyncer;
+
+function sampleiOSConfig()
+{
+ return {
+ 'shared':
+ {
+ 'arguments': {
+ 'desired_image': {'root': 'iOS'},
+ 'roots_dict': {'rootsExcluding': ['iOS']}
+ },
+ 'slaveArgument': 'slavename',
+ 'buildRequestArgument': 'build_request_id'
+ },
+ 'types': {
+ 'speedometer': {
+ 'test': ['Speedometer'],
+ 'arguments': {'test_name': 'speedometer'}
+ },
+ 'jetstream': {
+ 'test': ['JetStream'],
+ 'arguments': {'test_name': 'jetstream'}
+ },
+ "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' }
+ },
+ 'iPad-bench': {
+ 'builder': 'ABTest-iPad-RunBenchmark-Tests',
+ 'arguments': { 'forcescheduler': 'ABTest-iPad-RunBenchmark-Tests-ForceScheduler' }
+ }
+ },
+ 'configurations': [
+ {'type': 'speedometer', 'builder': 'iPhone-bench', 'platform': 'iPhone'},
+ {'type': 'jetstream', 'builder': 'iPhone-bench', 'platform': 'iPhone'},
+ {'type': 'dromaeo-dom', 'builder': 'iPhone-bench', 'platform': 'iPhone'},
+
+ {'type': 'speedometer', 'builder': 'iPad-bench', 'platform': 'iPad'},
+ {'type': 'jetstream', 'builder': 'iPad-bench', 'platform': 'iPad'},
+ ]
+ };
+}
+
+let sampleRootSetData = {
+ 'WebKit': {
+ 'id': '111127',
+ 'time': 1456955807334,
+ 'repository': 'WebKit',
+ 'revision': '197463',
+ },
+ 'Shared': {
+ 'id': '111237',
+ 'time': 1456931874000,
+ 'repository': 'Shared',
+ 'revision': '80229',
+ }
+};
+
+function createSampleBuildRequest()
+{
+ let rootSet = RootSet.ensureSingleton('4197', {roots: [
+ {'id': '111127', 'time': 1456955807334, 'repository': webkit, 'revision': '197463'},
+ {'id': '111237', 'time': 1456931874000, 'repository': sharedRepository, 'revision': '80229'},
+ {'id': '88930', 'time': 0, 'repository': ios, 'revision': '13A452'},
+ ]});
+
+ let request = BuildRequest.ensureSingleton('16733', {'rootSet': rootSet, 'status': 'pending'});
+ return request;
+}
+
+let samplePendingBuilds = [
+ {
+ 'builderName': 'ABTest-iPad-RunBenchmark-Tests',
+ 'builds': [],
+ 'properties': [
+ ['build_request_id', '16733', 'Force Build Form'],
+ ['desired_image', '13A452', 'Force Build Form'],
+ ['owner', '<unknown>', 'Force Build Form'],
+ ['test_name', 'speedometer', 'Force Build Form'],
+ ['reason', 'force build','Force Build Form'],
+ [
+ 'roots_dict',
+ JSON.stringify(sampleRootSetData),
+ 'Force Build Form'
+ ],
+ ['scheduler', 'ABTest-iPad-Performance-Tests-ForceScheduler', 'Scheduler']
+ ],
+ 'source': {
+ 'branch': '',
+ 'changes': [],
+ 'codebase': 'compiler-rt',
+ 'hasPatch': false,
+ 'project': '',
+ 'repository': '',
+ 'revision': ''
+ },
+ 'submittedAt': 1458704983
+ }
+];
+
+describe('BuildbotSyncer', function () {
+ describe('fetchPendingBuilds', function () {
+ BuildbotSyncer.fetchPendingBuilds
+ });
+
+ 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()]});
+ assert.equal(syncers.length, 1);
+ });
+
+ it('should throw when some required options are missing', function () {
+ assert.throws(function () {
+ let config = smallConfiguration();
+ delete config['builder'];
+ BuildbotSyncer._loadConfig('http://build.webkit.org/', {'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]});
+ }, 'platform should be a required option');
+ assert.throws(function () {
+ let config = smallConfiguration();
+ delete config['test'];
+ BuildbotSyncer._loadConfig('http://build.webkit.org/', {'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]});
+ });
+ assert.throws(function () {
+ let config = smallConfiguration();
+ delete config['buildRequestArgument'];
+ BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ });
+ });
+
+ it('should throw when a test name is not an array of strings', function () {
+ assert.throws(function () {
+ let config = smallConfiguration();
+ config.test = 'some test';
+ BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ });
+ assert.throws(function () {
+ let config = smallConfiguration();
+ config.test = [1];
+ BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ });
+ });
+
+ it('should throw when arguments is not an object', function () {
+ assert.throws(function () {
+ let config = smallConfiguration();
+ config.arguments = 'hello';
+ BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ });
+ });
+
+ it('should throw when arguments\'s values are malformed', function () {
+ assert.throws(function () {
+ let config = smallConfiguration();
+ config.arguments = {'some': {'root': 'some root', 'rootsExcluding': ['other root']}};
+ BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ });
+ assert.throws(function () {
+ let config = smallConfiguration();
+ config.arguments = {'some': {'otherKey': 'some root'}};
+ BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ });
+ assert.throws(function () {
+ let config = smallConfiguration();
+ config.arguments = {'some': {'root': ['a', 'b']}};
+ BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ });
+ assert.throws(function () {
+ let config = smallConfiguration();
+ config.arguments = {'some': {'root': 1}};
+ BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ });
+ assert.throws(function () {
+ let config = smallConfiguration();
+ config.arguments = {'some': {'rootsExcluding': 'a'}};
+ BuildbotSyncer._loadConfig('http://build.webkit.org/', {'configurations': [config]});
+ });
+ assert.throws(function () {
+ let config = smallConfiguration();
+ config.arguments = {'some': {'rootsExcluding': [1]}};
+ BuildbotSyncer._loadConfig('http://build.webkit.org/', {'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);
+ 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());
+ 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');
+ });
+
+ 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']);
+ });
+ });
+
+ describe('_propertiesForBuildRequest', function () {
+ it('should include all properties specified in a given configuration', function () {
+ let syncers = BuildbotSyncer._loadConfig('http://build.webkit.org/', sampleiOSConfig());
+ let properties = syncers[0]._propertiesForBuildRequest(createSampleBuildRequest());
+ 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 properties = syncers[0]._propertiesForBuildRequest(createSampleBuildRequest());
+ assert.equal(properties['test_name'], 'speedometer');
+ assert.equal(properties['forcescheduler'], 'ABTest-iPhone-RunBenchmark-Tests-ForceScheduler');
+
+ properties = syncers[1]._propertiesForBuildRequest(createSampleBuildRequest());
+ assert.equal(properties['test_name'], 'jetstream');
+ assert.equal(properties['forcescheduler'], 'ABTest-iPhone-RunBenchmark-Tests-ForceScheduler');
+ });
+
+ it('should resolve "root"', function () {
+ let syncers = BuildbotSyncer._loadConfig('http://build.webkit.org/', sampleiOSConfig());
+ let properties = syncers[0]._propertiesForBuildRequest(createSampleBuildRequest());
+ assert.equal(properties['desired_image'], '13A452');
+ });
+
+ it('should resolve "rootsExcluding"', function () {
+ let syncers = BuildbotSyncer._loadConfig('http://build.webkit.org/', sampleiOSConfig());
+ let properties = syncers[0]._propertiesForBuildRequest(createSampleBuildRequest());
+ 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 properties = syncers[0]._propertiesForBuildRequest(createSampleBuildRequest());
+ assert.equal(properties['build_request_id'], createSampleBuildRequest().id());
+ });
+ });
+
+});
\ No newline at end of file