Migrate legacy perf dashboard tests to mocha.js based tests
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 7 Apr 2016 22:10:50 +0000 (22:10 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 7 Apr 2016 22:10:50 +0000 (22:10 +0000)
https://bugs.webkit.org/show_bug.cgi?id=156335

Reviewed by Chris Dumez.

Migrated all legacy run-tests.js tests to mocha.js based tests. Since the new harness uses Promise
for most of asynchronous operations, refactored the tests to use Promises as well, and added more
assertions where appropriate.

Also consolidated common helper functions into server-tests/resources/common-operations.js.
Unfortunately there were multiple inconsistent implementations of addBuilder/addSlave. Some were
taking an array of reports while others were taking a single report. New shared implementation in
common-operations.js now takes a single report.

Also decreased the timeout in most tests from 10s to 1s so that tests fail early when they timeout.
Most of tests are passing under 100ms on my computer so 1s should be plenty still.

* run-tests.js: Removed.
* server-tests/admin-platforms-tests.js: Moved from tests/admin-platforms.js.
(reportsForDifferentPlatforms):
* server-tests/admin-reprocess-report-tests.js: Moved from tests/admin-reprocess-report.js.
(.addBuilder): Moved to common-operations.js.
* server-tests/api-build-requests-tests.js:
* server-tests/api-manifest.js: Use MockData.resetV3Models() instead of manually clearing maps.
* server-tests/api-measurement-set-tests.js: Moved from tests/api-measurement-set.js.
(.queryPlatformAndMetric):
(.format):
* server-tests/api-report-commits-tests.js: Moved from tests/api-report-commits.js.
* server-tests/api-report-tests.js: Moved from tests/api-report.js.
(.emptyReport):
(.emptySlaveReport):
(.reportWithSameSubtestName):
* server-tests/resources/common-operations.js: Added.
(addBuilderForReport): Extracted from tests.
(addSlaveForReport): Ditto.
(connectToDatabaseInEveryTest): Added.
(submitReport): Extracted from admin-platforms-tests.js.
* server-tests/resources/test-server.js:
(TestServer): Make TestServer a singleton since it doesn't make any sense for each module to start
its own Apache instance (that would certainly will fail).
* server-tests/tools-buildbot-triggerable-tests.js:
* tests: Removed.
* tools/js/database.js:
(Database.prototype.selectAll): Added.
(Database.prototype.selectFirstRow): Added.
(Database.prototype.selectRows): Added. Dynamically construct a query string based on arguments.

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

17 files changed:
Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/run-tests.js
Websites/perf.webkit.org/server-tests/admin-platforms-tests.js [new file with mode: 0644]
Websites/perf.webkit.org/server-tests/admin-reprocess-report-tests.js [new file with mode: 0644]
Websites/perf.webkit.org/server-tests/api-build-requests-tests.js
Websites/perf.webkit.org/server-tests/api-manifest.js
Websites/perf.webkit.org/server-tests/api-measurement-set-tests.js [new file with mode: 0644]
Websites/perf.webkit.org/server-tests/api-report-commits-tests.js [new file with mode: 0644]
Websites/perf.webkit.org/server-tests/api-report-tests.js [new file with mode: 0644]
Websites/perf.webkit.org/server-tests/resources/test-server.js
Websites/perf.webkit.org/server-tests/tools-buildbot-triggerable-tests.js
Websites/perf.webkit.org/tests/admin-platforms.js [deleted file]
Websites/perf.webkit.org/tests/admin-reprocess-report.js [deleted file]
Websites/perf.webkit.org/tests/api-measurement-set.js [deleted file]
Websites/perf.webkit.org/tests/api-report-commits.js [deleted file]
Websites/perf.webkit.org/tests/api-report.js [deleted file]
Websites/perf.webkit.org/tools/js/database.js

index cacfd0b..0a14993 100644 (file)
@@ -1,3 +1,52 @@
+2016-04-07  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Migrate legacy perf dashboard tests to mocha.js based tests
+        https://bugs.webkit.org/show_bug.cgi?id=156335
+
+        Reviewed by Chris Dumez.
+
+        Migrated all legacy run-tests.js tests to mocha.js based tests. Since the new harness uses Promise
+        for most of asynchronous operations, refactored the tests to use Promises as well, and added more
+        assertions where appropriate.
+
+        Also consolidated common helper functions into server-tests/resources/common-operations.js.
+        Unfortunately there were multiple inconsistent implementations of addBuilder/addSlave. Some were
+        taking an array of reports while others were taking a single report. New shared implementation in
+        common-operations.js now takes a single report.
+
+        Also decreased the timeout in most tests from 10s to 1s so that tests fail early when they timeout.
+        Most of tests are passing under 100ms on my computer so 1s should be plenty still.
+
+        * run-tests.js: Removed.
+        * server-tests/admin-platforms-tests.js: Moved from tests/admin-platforms.js.
+        (reportsForDifferentPlatforms):
+        * server-tests/admin-reprocess-report-tests.js: Moved from tests/admin-reprocess-report.js.
+        (.addBuilder): Moved to common-operations.js.
+        * server-tests/api-build-requests-tests.js:
+        * server-tests/api-manifest.js: Use MockData.resetV3Models() instead of manually clearing maps.
+        * server-tests/api-measurement-set-tests.js: Moved from tests/api-measurement-set.js.
+        (.queryPlatformAndMetric):
+        (.format):
+        * server-tests/api-report-commits-tests.js: Moved from tests/api-report-commits.js.
+        * server-tests/api-report-tests.js: Moved from tests/api-report.js.
+        (.emptyReport):
+        (.emptySlaveReport):
+        (.reportWithSameSubtestName):
+        * server-tests/resources/common-operations.js: Added.
+        (addBuilderForReport): Extracted from tests.
+        (addSlaveForReport): Ditto.
+        (connectToDatabaseInEveryTest): Added.
+        (submitReport): Extracted from admin-platforms-tests.js.
+        * server-tests/resources/test-server.js:
+        (TestServer): Make TestServer a singleton since it doesn't make any sense for each module to start
+        its own Apache instance (that would certainly will fail).
+        * server-tests/tools-buildbot-triggerable-tests.js:
+        * tests: Removed.
+        * tools/js/database.js:
+        (Database.prototype.selectAll): Added.
+        (Database.prototype.selectFirstRow): Added.
+        (Database.prototype.selectRows): Added. Dynamically construct a query string based on arguments.
+
 2016-04-05  Ryosuke Niwa  <rniwa@webkit.org>
 
         New buildbot syncing scripts that supports multiple builders and slaves
index 5dbcc5a..e69de29 100644 (file)
@@ -1,340 +0,0 @@
-#!/usr/local/bin/node
-
-var assert = require('assert');
-var crypto = require('crypto');
-var fs = require('fs');
-var http = require('http');
-var path = require('path');
-var vm = require('vm');
-
-function connect(keepAlive) {
-    var pg = require('pg');
-    var database = config('database');
-    var connectionString = 'tcp://' + database.username + ':' + database.password + '@' + database.host + ':' + database.port
-        + '/' + database.name;
-
-    var client = new pg.Client(connectionString);
-    if (!keepAlive) {
-        client.on('drain', function () {
-            client.end();
-            client = undefined;
-        });
-    }
-    client.connect();
-
-    return client;
-}
-
-function pathToDatabseSQL(relativePath) {
-    return path.resolve(__dirname, 'init-database.sql');
-}
-
-function pathToTests(testName) {
-    return testName ? path.resolve(__dirname, 'tests', testName) : path.resolve(__dirname, 'tests');
-}
-
-var configurationJSON = require('./config.json');
-function config(key) {
-    return configurationJSON[key];
-}
-
-function TaskQueue() {
-    var queue = [];
-    var numberOfRemainingTasks = 0;
-    var emptyQueueCallback;
-
-    function startTasksInQueue() {
-        if (!queue.length)
-            return emptyQueueCallback();
-
-        var swappedQueue = queue;
-        queue = [];
-
-        // Increase the counter before the loop in the case taskCallback is called synchronously.
-        numberOfRemainingTasks += swappedQueue.length;
-        for (var i = 0; i < swappedQueue.length; ++i)
-            swappedQueue[i](null, taskCallback);
-    }
-
-    function taskCallback(error) {
-        // FIXME: Handle error.
-        console.assert(numberOfRemainingTasks > 0);
-        numberOfRemainingTasks--;
-        if (!numberOfRemainingTasks)
-            setTimeout(startTasksInQueue, 0);
-    }
-
-    this.addTask = function (task) { queue.push(task); }
-    this.start = function (callback) {
-        emptyQueueCallback = callback;
-        startTasksInQueue();
-    }
-}
-
-function SerializedTaskQueue() {
-    var queue = [];
-
-    function executeNextTask(error) {
-        // FIXME: Handle error.
-        var callback = queue.pop();
-        setTimeout(function () { callback(null, executeNextTask); }, 0);
-    }
-
-    this.addTask = function (task) { queue.push(task); }
-    this.start = function (callback) {
-        queue.push(callback);
-        queue.reverse();
-        executeNextTask();
-    }
-}
-
-function main(argv) {
-    var client = connect(true);
-    var filter = argv[2];
-    confirmUserWantsDatabaseToBeInitializedIfNeeded(client, function (error, shouldContinue) {
-        if (error)
-            console.error(error);
-
-        if (error || !shouldContinue) {
-            client.end();
-            process.exit(1);
-            return;
-        }
-
-        initializeDatabase(client, function (error) {
-            if (error) {
-                console.error('Failed to initialize the database', error);
-                client.end();
-                process.exit(1);
-            }
-
-            var testCaseQueue = new SerializedTaskQueue();
-            var testFileQueue = new SerializedTaskQueue();
-            fs.readdirSync(pathToTests()).map(function (testFile) {
-                if (!testFile.match(/.js$/) || (filter && testFile.indexOf(filter) != 0))
-                    return;
-                testFileQueue.addTask(function (error, callback) {
-                    var testContent = fs.readFileSync(pathToTests(testFile), 'utf-8');
-                    var environment = new TestEnvironment(testCaseQueue);
-                    vm.runInNewContext(testContent, environment, pathToTests(testFile));
-                    callback();
-                });
-            });
-            testFileQueue.start(function () {
-                client.end();
-                testCaseQueue.start(function () {
-                    console.log('DONE');
-                });
-            });
-        });
-    });
-}
-
-function confirmUserWantsDatabaseToBeInitializedIfNeeded(client, callback) {
-    function fetchTableNames(error, callback) {
-        if (error)
-            return callback(error);
-
-        client.query('SELECT table_name FROM information_schema.tables WHERE table_type = \'BASE TABLE\' and table_schema = \'public\'', function (error, result) {
-            if (error)
-                return callback(error);
-            callback(null, result.rows.map(function (row) { return row['table_name']; }));            
-        });
-    }
-
-    function findNonEmptyTable(error, list, callback) {
-        if (error || !list.length)
-            return callback(error);
-
-        var tableName = list.shift();
-        client.query('SELECT COUNT(*) FROM ' + tableName + ' LIMIT 1', function (error, result) {
-            if (error)
-                return callback(error);
-
-            if (result.rows[0]['count'])
-                return callback(null, tableName);
-
-            findNonEmptyTable(null, list, callback);
-        });
-    }
-
-    fetchTableNames(null, function (error, tableNames) {
-        if (error)
-            return callback(error, false);
-
-        findNonEmptyTable(null, tableNames, function (error, nonEmptyTable) {
-            if (error)
-                return callback(error, false);
-
-            if (!nonEmptyTable)
-                return callback(null, true);
-
-            console.warn('Table ' + nonEmptyTable + ' is not empty but running tests will drop all tables.');
-            askYesOrNoQuestion(null, 'Do you really want to continue?', callback);
-        });
-    });
-}
-
-function askYesOrNoQuestion(error, question, callback) {
-    if (error)
-        return callback(error);
-
-    process.stdout.write(question + ' (y/n):');
-    process.stdin.resume();
-    process.stdin.setEncoding('utf-8');
-    process.stdin.on('data', function (line) {
-        line = line.trim();
-        if (line === 'y') {
-            process.stdin.pause();
-            callback(null, true);
-        } else if (line === 'n') {
-            process.stdin.pause();
-            callback(null, false);
-        } else
-            console.warn('Invalid input:', line);
-    });
-}
-
-function initializeDatabase(client, callback) {
-    var commaSeparatedSqlStatements = fs.readFileSync(pathToDatabseSQL(), "utf8");
-
-    var firstError;
-    var queue = new TaskQueue();
-    commaSeparatedSqlStatements.split(/;\s*(?=CREATE|DROP)/).forEach(function (statement) {
-        queue.addTask(function (error, callback) {
-            client.query(statement, function (error) {
-                if (error && !firstError)
-                    firstError = error;
-                callback();
-            });
-        })
-    });
-
-    queue.start(function () { callback(firstError); });
-}
-
-var currentTestContext;
-function TestEnvironment(testCaseQueue) {
-    var currentTestGroup;
-
-    this.assert = assert;
-    this.console = console;
-
-    // describe("~", function () {
-    //     it("~", function () { assert(true); });
-    // });
-    this.describe = function (testGroup, callback) {
-        currentTestGroup = testGroup;
-        callback();
-    }
-
-    this.it = function (testCaseDescription, testCase) {
-        testCaseQueue.addTask(function (error, callback) {
-            currentTestContext = new TestContext(currentTestGroup, testCaseDescription, function () {
-                currentTestContext = null;
-                initializeDatabase(connect(), callback);
-            });
-            testCase();
-        });
-    }
-
-    this.postJSON = function (path, content, callback) {
-        sendHttpRequest(path, 'POST', 'application/json', JSON.stringify(content), function (error, response) {
-            assert.ifError(error);
-            callback(response);
-        });
-    }
-
-    this.httpGet = function (path, callback) {
-        sendHttpRequest(path, 'GET', null, '', function (error, response) {
-            assert.ifError(error);
-            callback(response);
-        });
-    }
-
-    this.httpPost= function (path, content, callback) {
-        var contentType = null;
-        if (typeof(content) != "string") {
-            contentType = 'application/x-www-form-urlencoded';
-            var components = [];
-            for (var key in content)
-                components.push(key + '=' + escape(content[key]));
-            content = components.join('&');
-        }
-        sendHttpRequest(path, 'POST', contentType, content, function (error, response) {
-            assert.ifError(error);
-            callback(response);
-        });
-    }
-
-    this.queryAndFetchAll = function (query, parameters, callback) {
-        var client = connect();
-        client.query(query, parameters, function (error, result) {
-            setTimeout(function () {
-                assert.ifError(error);
-                callback(result.rows);
-            }, 0);
-        });
-    }
-
-    this.sha256 = function (data) {
-        var hash = crypto.createHash('sha256');
-        hash.update(data);
-        return hash.digest('hex');
-    }
-
-    this.config = config;
-
-    this.notifyDone = function () { currentTestContext.done(); }
-}
-
-process.on('uncaughtException', function (error) {
-    if (!currentTestContext)
-        throw error;
-    currentTestContext.logError('Uncaught exception', error);
-    currentTestContext.done();
-});
-
-function sendHttpRequest(path, method, contentType, content, callback) {
-    var options = config('testServer');
-    options.path = path;
-    options.method = method;
-
-    var request = http.request(options, function (response) {
-        var responseText = '';
-        response.setEncoding('utf8');
-        response.on('data', function (chunk) { responseText += chunk; });
-        response.on('end', function () {
-            setTimeout(function () {
-                callback(null, {statusCode: response.statusCode, responseText: responseText});
-            }, 0);
-        });
-    });
-    request.on('error', callback);
-    if (contentType)
-        request.setHeader('Content-Type', contentType);
-    if (content)
-        request.write(content);
-    request.end();
-}
-
-function TestContext(testGroup, testCase, callback) {
-    var failed = false;
-
-    this.description = function () {
-        return testGroup + ' ' + testCase;
-    }
-    this.done = function () {
-        if (!failed)
-            console.log('PASS');
-        callback();
-    }
-    this.logError = function (error, details) {
-        failed = true;
-        console.error(error, details);
-    }
-
-    process.stdout.write(this.description() + ': ');
-}
-
-main(process.argv);
diff --git a/Websites/perf.webkit.org/server-tests/admin-platforms-tests.js b/Websites/perf.webkit.org/server-tests/admin-platforms-tests.js
new file mode 100644 (file)
index 0000000..e75ad9a
--- /dev/null
@@ -0,0 +1,145 @@
+'use strict';
+
+const assert = require('assert');
+
+const TestServer = require('./resources/test-server.js');
+const connectToDatabaseInEveryTest = require('./resources/common-operations.js').connectToDatabaseInEveryTest;
+const submitReport = require('./resources/common-operations.js').submitReport;
+
+describe("/admin/platforms", function () {
+    this.timeout(1000);
+    TestServer.inject();
+    connectToDatabaseInEveryTest();
+
+    function reportsForDifferentPlatforms()
+    {
+        return [
+            {
+                "buildNumber": "3001",
+                "buildTime": "2013-02-28T09:01:47",
+                "builderName": "someBuilder",
+                "builderPassword": "somePassword",
+                "platform": "Mavericks",
+                "tests": {"test": { "metrics": {"FrameRate": { "current": [[1, 1, 1], [1, 1, 1]] } } } },
+            },
+            {
+                "buildNumber": "3001",
+                "buildTime": "2013-02-28T10:12:03",
+                "builderName": "someBuilder",
+                "builderPassword": "somePassword",
+                "platform": "Mountain Lion",
+                "tests": {"test": { "metrics": {"FrameRate": { "current": [[2, 2, 2], [2, 2, 2]] }, "Combined": { "current": [[3, 3, 3], [3, 3, 3]] }} } },
+            },
+            {
+                "buildNumber": "3003",
+                "buildTime": "2013-02-28T12:56:26",
+                "builderName": "someBuilder",
+                "builderPassword": "somePassword",
+                "platform": "Trunk Mountain Lion",
+                "tests": {"test": { "metrics": {"FrameRate": { "current": [[4, 4, 4], [4, 4, 4]] } } } }
+            }];
+    } 
+
+    it("should delete the platform that got merged into another one", function (done) {
+        const db = TestServer.database();
+        let oldPlatforms;        
+        submitReport(reportsForDifferentPlatforms()).then(function () {
+            return db.selectAll('platforms', 'name');
+        }).then(function (platforms) {
+            oldPlatforms = platforms;
+            assert.equal(oldPlatforms.length, 3);
+            assert.equal(oldPlatforms[0]['name'], 'Mavericks');
+            assert.equal(oldPlatforms[1]['name'], 'Mountain Lion');
+            assert.equal(oldPlatforms[2]['name'], 'Trunk Mountain Lion');
+        }).then(function () {
+            return TestServer.remoteAPI().postFormUrlencodedData('/admin/platforms.php',
+                {'action': 'merge', 'id': oldPlatforms[1]['id'], 'destination': oldPlatforms[2]['id']});
+        }).then(function () {
+            return db.selectAll('platforms');
+        }).then(function (newPlatforms) {
+            assert.equal(newPlatforms.length, 2);
+            assert.deepEqual(newPlatforms[0], oldPlatforms[0]);
+            assert.deepEqual(newPlatforms[1], oldPlatforms[2]);
+            done();
+        }).catch(done);
+    });
+
+    it("should move test runs from the merged platform to the destination platform", function (done) {
+        let oldTestRuns;
+        const queryForRuns = 'SELECT * FROM test_runs, test_configurations, platforms WHERE run_config = config_id AND config_platform = platform_id ORDER by run_mean_cache';
+        const db = TestServer.database();
+        submitReport(reportsForDifferentPlatforms()).then(function () {
+            return db.query(queryForRuns);
+        }).then(function (result) {
+            oldTestRuns = result.rows;
+            assert.equal(oldTestRuns.length, 4);
+            assert.equal(oldTestRuns[0]['platform_name'], 'Mavericks');
+            assert.equal(oldTestRuns[0]['run_sum_cache'], 6);
+            assert.equal(oldTestRuns[1]['platform_name'], 'Mountain Lion');
+            assert.equal(oldTestRuns[1]['run_sum_cache'], 12);
+            assert.equal(oldTestRuns[2]['platform_name'], 'Mountain Lion');
+            assert.equal(oldTestRuns[2]['run_sum_cache'], 18);
+            assert.equal(oldTestRuns[3]['platform_name'], 'Trunk Mountain Lion');
+            assert.equal(oldTestRuns[3]['run_sum_cache'], 24);
+        }).then(function () {
+            return TestServer.remoteAPI().postFormUrlencodedData('/admin/platforms.php',
+                {'action': 'merge', 'id': oldTestRuns[1]['platform_id'], 'destination': oldTestRuns[3]['platform_id']});
+        }).then(function () {
+            return db.query(queryForRuns);
+        }).then(function (result) {
+            const newTestRuns = result.rows;
+            assert.equal(newTestRuns.length, 4);
+            assert.equal(newTestRuns[0]['run_id'], oldTestRuns[0]['run_id']);
+            assert.equal(newTestRuns[0]['platform_name'], 'Mavericks');
+            assert.equal(newTestRuns[0]['run_sum_cache'], 6);
+            assert.equal(newTestRuns[1]['run_id'], oldTestRuns[1]['run_id']);
+            assert.equal(newTestRuns[1]['platform_name'], 'Trunk Mountain Lion');
+            assert.equal(newTestRuns[1]['run_sum_cache'], 12);
+            assert.equal(newTestRuns[2]['run_id'], oldTestRuns[2]['run_id']);
+            assert.equal(newTestRuns[2]['platform_name'], 'Trunk Mountain Lion');
+            assert.equal(newTestRuns[2]['run_sum_cache'], 18);
+            assert.equal(newTestRuns[3]['run_id'], oldTestRuns[3]['run_id']);
+            assert.equal(newTestRuns[3]['platform_name'], 'Trunk Mountain Lion');
+            assert.equal(newTestRuns[3]['run_sum_cache'], 24);
+            assert.equal(newTestRuns[1]['run_config'], newTestRuns[3]['run_config']);
+            done();
+        }).catch(done);
+    });
+
+    it("should move test configurations from the merged platform to the destination platform", function (done) {
+        let oldConfigs;
+        const reports = reportsForDifferentPlatforms();
+        reports[0]['tests'] = {"test": { "metrics": {"FrameRate": { "baseline": [[1, 1, 1], [1, 1, 1]] } } } };
+        const queryForConfig = 'SELECT * from test_configurations, platforms, test_metrics'
+            + ' where config_platform = platform_id and config_metric = metric_id and platform_name in ($1, $2) order by config_id';
+        const db = TestServer.database();
+        submitReport(reports).then(function () {
+            return db.query(queryForConfig, [reports[0]['platform'], reports[2]['platform']]);
+        }).then(function (result) {
+            oldConfigs = result.rows;
+            assert.equal(oldConfigs.length, 2);
+            assert.equal(oldConfigs[0]['platform_name'], reports[0]['platform']);
+            assert.equal(oldConfigs[0]['metric_name'], 'FrameRate');
+            assert.equal(oldConfigs[0]['config_type'], 'baseline');
+            assert.equal(oldConfigs[1]['platform_name'], reports[2]['platform']);
+            assert.equal(oldConfigs[1]['metric_name'], 'FrameRate');
+            assert.equal(oldConfigs[1]['config_type'], 'current');
+        }).then(function () {
+            return TestServer.remoteAPI().postFormUrlencodedData('/admin/platforms.php',
+                {'action': 'merge', 'id': oldConfigs[0]['platform_id'], 'destination': oldConfigs[1]['platform_id']});
+        }).then(function () {
+            return db.query(queryForConfig, [reports[0]['platform'], reports[2]['platform']]);
+        }).then(function (result) {
+            const newConfigs = result.rows;
+            assert.equal(newConfigs.length, 2);
+            assert.equal(newConfigs[0]['platform_name'], reports[2]['platform']);
+            assert.equal(newConfigs[0]['metric_name'], 'FrameRate');
+            assert.equal(newConfigs[0]['config_type'], 'baseline');
+            assert.equal(newConfigs[1]['platform_name'], reports[2]['platform']);
+            assert.equal(newConfigs[1]['metric_name'], 'FrameRate');
+            assert.equal(newConfigs[1]['config_type'], 'current');
+            done();
+        }).catch(done);
+    });
+
+});
diff --git a/Websites/perf.webkit.org/server-tests/admin-reprocess-report-tests.js b/Websites/perf.webkit.org/server-tests/admin-reprocess-report-tests.js
new file mode 100644 (file)
index 0000000..def4dcf
--- /dev/null
@@ -0,0 +1,86 @@
+'use strict';
+
+const assert = require('assert');
+
+const TestServer = require('./resources/test-server.js');
+const addBuilderForReport = require('./resources/common-operations.js').addBuilderForReport;
+const connectToDatabaseInEveryTest = require('./resources/common-operations.js').connectToDatabaseInEveryTest;
+
+describe("/admin/reprocess-report", function () {
+    this.timeout(1000);
+    TestServer.inject();
+    connectToDatabaseInEveryTest();
+
+    const simpleReport = [{
+        "buildNumber": "1986",
+        "buildTime": "2013-02-28T10:12:03",
+        "builderName": "someBuilder",
+        "builderPassword": "somePassword",
+        "platform": "Mountain Lion",
+        "tests": {
+                "test": {
+                    "metrics": {"FrameRate": { "current": [[1, 2, 3], [4, 5, 6]] }}
+                },
+            },
+        }];
+
+    it("should add build", function (done) {
+        let db = TestServer.database();
+        let reportId;
+        addBuilderForReport(simpleReport[0]).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', simpleReport);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return Promise.all([db.selectAll('builds'), db.selectAll('reports')]);
+        }).then(function (result) {
+            const builds = result[0];
+            const reports = result[1];
+            assert.equal(builds.length, 1);
+            assert.equal(builds[0]['number'], 1986);
+            assert.equal(reports.length, 1);
+            reportId = reports[0]['id'];
+            assert.equal(reports[0]['build_number'], 1986);
+            return db.query('UPDATE reports SET report_build = NULL; DELETE FROM builds');
+        }).then(function () {
+            return db.selectAll('builds');
+        }).then(function (builds) {
+            assert.equal(builds.length, 0);
+            return TestServer.remoteAPI().getJSONWithStatus(`/admin/reprocess-report?report=${reportId}`);
+        }).then(function () {
+            return db.selectAll('builds');
+        }).then(function (builds) {
+            assert.equal(builds.length, 1);
+            assert.equal(builds[0]['number'], 1986);
+            done();
+        }).catch(done);
+    });
+
+    it("should not duplicate the reprocessed report", function (done) {
+        let db = TestServer.database();
+        let originalReprot;
+        addBuilderForReport(simpleReport[0]).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', simpleReport);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return db.selectAll('reports');
+        }).then(function (reports) {
+            assert.equal(reports.length, 1);
+            originalReprot = reports[0];
+            return db.query('UPDATE reports SET report_build = NULL; DELETE FROM builds');
+        }).then(function () {
+            return TestServer.remoteAPI().getJSONWithStatus(`/admin/reprocess-report?report=${originalReprot['id']}`);
+        }).then(function () {
+            return db.selectAll('reports');
+        }).then(function (reports) {
+            assert.equal(reports.length, 1);
+            const newPort = reports[0];
+            originalReprot['committed_at'] = null;
+            newPort['committed_at'] = null;
+            assert.notEqual(originalReprot['build'], newPort['build']);
+            originalReprot['build'] = null;
+            newPort['build'] = null;
+            assert.deepEqual(originalReprot, newPort);
+            done();
+        }).catch(done);
+    });
+});
index 2e63185..5ca3a7e 100644 (file)
@@ -6,7 +6,7 @@ let MockData = require('./resources/mock-data.js');
 let TestServer = require('./resources/test-server.js');
 
 describe('/api/build-requests', function () {
-    this.timeout(10000);
+    this.timeout(1000);
     TestServer.inject();
 
     beforeEach(function () {
index b5d3452..eacffdf 100644 (file)
@@ -7,16 +7,11 @@ require('../tools/js/v3-models.js');
 let TestServer = require('./resources/test-server.js');
 
 describe('/api/manifest', function () {
-    this.timeout(10000);
+    this.timeout(1000);
     TestServer.inject();
 
     beforeEach(function () {
-        Builder.clearStaticMap();
-        BugTracker.clearStaticMap();
-        Test.clearStaticMap();
-        Metric.clearStaticMap();
-        Platform.clearStaticMap();
-        Repository.clearStaticMap();
+        MockData.resetV3Models();
     });
 
     it("should generate an empty manifest when database is empty", function (done) {
diff --git a/Websites/perf.webkit.org/server-tests/api-measurement-set-tests.js b/Websites/perf.webkit.org/server-tests/api-measurement-set-tests.js
new file mode 100644 (file)
index 0000000..f0b27e8
--- /dev/null
@@ -0,0 +1,413 @@
+'use strict';
+
+const assert = require('assert');
+const crypto = require('crypto');
+
+const TestServer = require('./resources/test-server.js');
+const addBuilderForReport = require('./resources/common-operations.js').addBuilderForReport;
+const connectToDatabaseInEveryTest = require('./resources/common-operations.js').connectToDatabaseInEveryTest;
+
+describe("/api/measurement-set", function () {
+    this.timeout(1000);
+    TestServer.inject();
+    connectToDatabaseInEveryTest();
+
+    function queryPlatformAndMetric(platformName, metricName)
+    {
+        const db = TestServer.database();
+        return Promise.all([
+            db.selectFirstRow('platforms', {name: 'Mountain Lion'}),
+            db.selectFirstRow('test_metrics', {name: 'Time'}),
+        ]).then(function (result) {
+            return {platformId: result[0]['id'], metricId: result[1]['id']};
+        });
+    }
+
+    function format(formatMap, row)
+    {
+        var result = {};
+        for (var i = 0; i < formatMap.length; i++) {
+            var key = formatMap[i];
+            if (key == 'id' || key == 'build' || key == 'builder')
+                continue;
+            result[key] = row[i];
+        }
+        return result;
+    }
+
+    let clusterStart = TestServer.testConfig().clusterStart;
+    clusterStart = +Date.UTC(clusterStart[0], clusterStart[1] - 1, clusterStart[2], clusterStart[3], clusterStart[4]);
+
+    let clusterSize = TestServer.testConfig().clusterSize;
+    const DAY = 24 * 3600 * 1000;
+    const YEAR = 365.24 * DAY;
+    const MONTH = 30 * DAY;
+    clusterSize = clusterSize[0] * YEAR + clusterSize[1] * MONTH + clusterSize[2] * DAY;
+
+    function clusterTime(index) { return new Date(clusterStart + clusterSize * index); }
+
+    const reportWithBuildTime = [{
+        "buildNumber": "123",
+        "buildTime": clusterTime(7.8).toISOString(),
+        "builderName": "someBuilder",
+        "builderPassword": "somePassword",
+        "platform": "Mountain Lion",
+        "tests": {
+            "Suite": {
+                "tests": {
+                    "test1": {
+                        "metrics": {"Time": { "current": [1, 2, 3, 4, 5] }}
+                    },
+                }
+            },
+        }}];
+    reportWithBuildTime.startTime = +clusterTime(7);
+
+    const reportWithRevision = [{
+        "buildNumber": "124",
+        "buildTime": "2013-02-28T15:34:51",
+        "revisions": {
+            "WebKit": {
+                "revision": "144000",
+                "timestamp": clusterTime(10.3).toISOString(),
+            },
+        },
+        "builderName": "someBuilder",
+        "builderPassword": "somePassword",
+        "platform": "Mountain Lion",
+        "tests": {
+            "Suite": {
+                "tests": {
+                    "test1": {
+                        "metrics": {"Time": { "current": [11, 12, 13, 14, 15] }}
+                    }
+                }
+            },
+        }}];
+
+    const reportWithNewRevision = [{
+        "buildNumber": "125",
+        "buildTime": "2013-02-28T21:45:17",
+        "revisions": {
+            "WebKit": {
+                "revision": "160609",
+                "timestamp": clusterTime(12.1).toISOString()
+            },
+        },
+        "builderName": "someBuilder",
+        "builderPassword": "somePassword",
+        "platform": "Mountain Lion",
+        "tests": {
+            "Suite": {
+                "tests": {
+                    "test1": {
+                        "metrics": {"Time": { "current": [16, 17, 18, 19, 20] }}
+                    }
+                }
+            },
+        }}];
+
+    const reportWithAncentRevision = [{
+        "buildNumber": "126",
+        "buildTime": "2013-02-28T23:07:25",
+        "revisions": {
+            "WebKit": {
+                "revision": "137793",
+                "timestamp": clusterTime(1.8).toISOString()
+            },
+        },
+        "builderName": "someBuilder",
+        "builderPassword": "somePassword",
+        "platform": "Mountain Lion",
+        "tests": {
+            "Suite": {
+                "tests": {
+                    "test1": {
+                        "metrics": {"Time": { "current": [21, 22, 23, 24, 25] }}
+                    }
+                }
+            },
+        }}];
+
+    it("should reject when platform ID is missing", function (done) {
+        addBuilderForReport(reportWithBuildTime[0]).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', reportWithBuildTime);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return queryPlatformAndMetric('Mountain Lion', 'Time');
+        }).then(function (result) {
+            return TestServer.remoteAPI().getJSON(`/api/measurement-set/?metric=${result.metricId}`);
+        }).then(function (response) {
+            assert.equal(response['status'], 'AmbiguousRequest');
+            done();
+        }).catch(done);
+    });
+
+    it("should reject when metric ID is missing", function (done) {
+        addBuilderForReport(reportWithBuildTime[0]).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', reportWithBuildTime);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return queryPlatformAndMetric('Mountain Lion', 'Time');
+        }).then(function (result) {
+            return TestServer.remoteAPI().getJSON(`/api/measurement-set/?platform=${result.platformId}`);
+        }).then(function (response) {
+            assert.equal(response['status'], 'AmbiguousRequest');
+            done();
+        }).catch(done);
+    });
+
+    it("should reject an invalid platform name", function (done) {
+        addBuilderForReport(reportWithBuildTime[0]).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', reportWithBuildTime);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return queryPlatformAndMetric('Mountain Lion', 'Time');
+        }).then(function (result) {
+            return TestServer.remoteAPI().getJSON(`/api/measurement-set/?platform=${result.platformId}a&metric=${result.metricId}`);
+        }).then(function (response) {
+            assert.equal(response['status'], 'InvalidPlatform');
+            done();
+        }).catch(done);
+    });
+
+    it("should reject an invalid metric name", function (done) {
+        addBuilderForReport(reportWithBuildTime[0]).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', reportWithBuildTime);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return queryPlatformAndMetric('Mountain Lion', 'Time');
+        }).then(function (result) {
+            return TestServer.remoteAPI().getJSON(`/api/measurement-set/?platform=${result.platformId}&metric=${result.metricId}b`);
+        }).then(function (response) {
+            assert.equal(response['status'], 'InvalidMetric');
+            done();
+        }).catch(done);
+    });
+
+    it("should be able to retrieve a reported value", function (done) {
+        addBuilderForReport(reportWithBuildTime[0]).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', reportWithBuildTime);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return queryPlatformAndMetric('Mountain Lion', 'Time');
+        }).then(function (result) {
+            return TestServer.remoteAPI().getJSONWithStatus(`/api/measurement-set/?platform=${result.platformId}&metric=${result.metricId}`);
+        }).then(function (response) {
+            const buildTime = +(new Date(reportWithBuildTime[0]['buildTime']));
+
+            assert.deepEqual(Object.keys(response).sort(),
+                ['clusterCount', 'clusterSize', 'clusterStart',
+                  'configurations', 'elapsedTime', 'endTime', 'formatMap', 'lastModified', 'startTime', 'status']);
+            assert.equal(response['status'], 'OK');
+            assert.equal(response['clusterCount'], 1);
+            assert.deepEqual(response['formatMap'], [
+                'id', 'mean', 'iterationCount', 'sum', 'squareSum', 'markedOutlier',
+                'revisions', 'commitTime', 'build', 'buildTime', 'buildNumber', 'builder']);
+
+            assert.equal(response['startTime'], reportWithBuildTime.startTime);
+            assert(typeof(response['lastModified']) == 'number', 'lastModified time should be a numeric');
+
+            assert.deepEqual(Object.keys(response['configurations']), ['current']);
+
+            var currentRows = response['configurations']['current'];
+            assert.equal(currentRows.length, 1);
+            assert.equal(currentRows[0].length, response['formatMap'].length);
+            assert.deepEqual(format(response['formatMap'], currentRows[0]), {
+                mean: 3,
+                iterationCount: 5,
+                sum: 15,
+                squareSum: 55,
+                markedOutlier: false,
+                revisions: [],
+                commitTime: buildTime,
+                buildTime: buildTime,
+                buildNumber: '123'});
+            done();
+        }).catch(done);
+    });
+
+    it("should return return the right IDs for measurement, build, and builder", function (done) {
+        addBuilderForReport(reportWithBuildTime[0]).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', reportWithBuildTime);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return queryPlatformAndMetric('Mountain Lion', 'Time');
+        }).then(function (result) {
+            const db = TestServer.database();
+            return Promise.all([
+                db.selectAll('test_runs'),
+                db.selectAll('builds'),
+                db.selectAll('builders'),
+                TestServer.remoteAPI().getJSONWithStatus(`/api/measurement-set/?platform=${result.platformId}&metric=${result.metricId}`),
+            ]);
+        }).then(function (result) {
+            const runs = result[0];
+            const builds = result[1];
+            const builders = result[2];
+            const response = result[3];
+
+            assert.equal(runs.length, 1);
+            assert.equal(builds.length, 1);
+            assert.equal(builders.length, 1);
+            const measurementId = runs[0]['id'];
+            const buildId = builds[0]['id'];
+            const builderId = builders[0]['id'];
+
+            assert.equal(response['configurations']['current'].length, 1);
+            const measurement = response['configurations']['current'][0];
+            assert.equal(response['status'], 'OK');
+
+            assert.equal(measurement[response['formatMap'].indexOf('id')], measurementId);
+            assert.equal(measurement[response['formatMap'].indexOf('build')], buildId);
+            assert.equal(measurement[response['formatMap'].indexOf('builder')], builderId);
+
+            done();
+        }).catch(done);
+    });
+
+    function postReports(reports, callback)
+    {
+        if (!reports.length)
+            return callback();
+
+        postJSON('/api/report/', reports[0], function (response) {
+            assert.equal(response.statusCode, 200);
+            assert.equal(JSON.parse(response.responseText)['status'], 'OK');
+
+            postReports(reports.slice(1), callback);
+        });
+    }
+
+    function queryPlatformAndMetricWithRepository(platformName, metricName, repositoryName)
+    {
+        const db = TestServer.database();
+        return Promise.all([
+            db.selectFirstRow('platforms', {name: platformName}),
+            db.selectFirstRow('test_metrics', {name: metricName}),
+            db.selectFirstRow('repositories', {name: repositoryName}),
+        ]).then(function (result) {
+            return {platformId: result[0]['id'], metricId: result[1]['id'], repositoryId: result[2]['id']};
+        });
+    }
+
+    it("should order results by commit time", function (done) {
+        const remote = TestServer.remoteAPI();
+        let repositoryId;
+        addBuilderForReport(reportWithBuildTime[0]).then(function () {
+            return remote.postJSON('/api/report/', reportWithBuildTime);
+        }).then(function () {
+            return remote.postJSON('/api/report/', reportWithRevision);
+        }).then(function () {
+            return queryPlatformAndMetricWithRepository('Mountain Lion', 'Time', 'WebKit');
+        }).then(function (result) {
+            repositoryId = result.repositoryId;
+            return remote.getJSONWithStatus(`/api/measurement-set/?platform=${result.platformId}&metric=${result.metricId}`);
+        }).then(function (response) {
+            const currentRows = response['configurations']['current'];
+            const buildTime = +(new Date(reportWithBuildTime[0]['buildTime']));
+            const revisionTime = +(new Date(reportWithRevision[0]['revisions']['WebKit']['timestamp']));
+            const revisionBuildTime = +(new Date(reportWithRevision[0]['buildTime']));
+
+            assert.equal(currentRows.length, 2);
+            assert.deepEqual(format(response['formatMap'], currentRows[0]), {
+               mean: 13,
+               iterationCount: 5,
+               sum: 65,
+               squareSum: 855,
+               markedOutlier: false,
+               revisions: [[1, repositoryId, '144000', revisionTime]],
+               commitTime: revisionTime,
+               buildTime: revisionBuildTime,
+               buildNumber: '124' });
+            assert.deepEqual(format(response['formatMap'], currentRows[1]), {
+                mean: 3,
+                iterationCount: 5,
+                sum: 15,
+                squareSum: 55,
+                markedOutlier: false,
+                revisions: [],
+                commitTime: buildTime,
+                buildTime: buildTime,
+                buildNumber: '123' });
+            done();
+        }).catch(done);
+    });
+
+    function buildNumbers(parsedResult, config)
+    {
+        return parsedResult['configurations'][config].map(function (row) {
+            return format(parsedResult['formatMap'], row)['buildNumber'];
+        });
+    }
+
+    it("should include one data point after the current time range", function (done) {
+        const remote = TestServer.remoteAPI();
+        addBuilderForReport(reportWithBuildTime[0]).then(function () {
+            return remote.postJSON('/api/report/', reportWithAncentRevision);
+        }).then(function () {
+            return remote.postJSON('/api/report/', reportWithNewRevision);
+        }).then(function () {
+            return queryPlatformAndMetric('Mountain Lion', 'Time');
+        }).then(function (result) {
+            return remote.getJSONWithStatus(`/api/measurement-set/?platform=${result.platformId}&metric=${result.metricId}`);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            assert.equal(response['clusterCount'], 2, 'should have two clusters');
+            assert.deepEqual(buildNumbers(response, 'current'),
+                [reportWithAncentRevision[0]['buildNumber'], reportWithNewRevision[0]['buildNumber']]);
+            done();
+        }).catch(done);
+    });
+
+    it("should always include one old data point before the current time range", function (done) {
+        const remote = TestServer.remoteAPI();
+        addBuilderForReport(reportWithBuildTime[0]).then(function () {
+            return remote.postJSON('/api/report/', reportWithBuildTime);
+        }).then(function () {
+            return remote.postJSON('/api/report/', reportWithAncentRevision);
+        }).then(function () {
+            return queryPlatformAndMetric('Mountain Lion', 'Time');
+        }).then(function (result) {
+            return remote.getJSONWithStatus(`/api/measurement-set/?platform=${result.platformId}&metric=${result.metricId}`);
+        }).then(function (response) {
+            assert.equal(response['clusterCount'], 2, 'should have two clusters');
+            let currentRows = response['configurations']['current'];
+            assert.equal(currentRows.length, 2, 'should contain two data points');
+            assert.deepEqual(buildNumbers(response, 'current'), [reportWithAncentRevision[0]['buildNumber'], reportWithBuildTime[0]['buildNumber']]);
+            done();
+        }).catch(done);
+    });
+
+
+    it("should create cache results", function (done) {
+        const remote = TestServer.remoteAPI();
+        let cachePrefix;
+        addBuilderForReport(reportWithBuildTime[0]).then(function () {
+            return remote.postJSON('/api/report/', reportWithAncentRevision);
+        }).then(function () {
+            return remote.postJSON('/api/report/', reportWithRevision);
+        }).then(function () {
+            return remote.postJSON('/api/report/', reportWithNewRevision);
+        }).then(function () {
+            return queryPlatformAndMetric('Mountain Lion', 'Time');
+        }).then(function (result) {
+            cachePrefix = '/data/measurement-set-' + result.platformId + '-' + result.metricId;
+            return remote.getJSONWithStatus(`/api/measurement-set/?platform=${result.platformId}&metric=${result.metricId}`);
+        }).then(function (newResult) {
+            return remote.getJSONWithStatus(`${cachePrefix}.json`).then(function (cachedResult) {
+                assert.deepEqual(newResult, cachedResult);
+                return remote.getJSONWithStatus(`${cachePrefix}-${cachedResult['startTime']}.json`);
+            }).then(function (oldResult) {
+                var oldBuildNumbers = buildNumbers(oldResult, 'current');
+                var newBuildNumbers = buildNumbers(newResult, 'current');
+                assert(oldBuildNumbers.length >= 2, 'The old cluster should contain at least two data points');
+                assert(newBuildNumbers.length >= 2, 'The new cluster should contain at least two data points');
+                assert.deepEqual(oldBuildNumbers.slice(oldBuildNumbers.length - 2), newBuildNumbers.slice(0, 2),
+                    'Two conseqcutive clusters should share two data points');
+                done();
+            });
+        }).catch(done);
+    });
+
+});
diff --git a/Websites/perf.webkit.org/server-tests/api-report-commits-tests.js b/Websites/perf.webkit.org/server-tests/api-report-commits-tests.js
new file mode 100644 (file)
index 0000000..cc10c1a
--- /dev/null
@@ -0,0 +1,261 @@
+'use strict';
+
+const assert = require('assert');
+const crypto = require('crypto');
+
+const TestServer = require('./resources/test-server.js');
+const addSlaveForReport = require('./resources/common-operations.js').addSlaveForReport;
+const connectToDatabaseInEveryTest = require('./resources/common-operations.js').connectToDatabaseInEveryTest;
+
+describe("/api/report-commits/", function () {
+    this.timeout(1000);
+    TestServer.inject();
+    connectToDatabaseInEveryTest();
+
+    const emptyReport = {
+        "slaveName": "someSlave",
+        "slavePassword": "somePassword",
+    };
+    const subversionCommit = {
+        "slaveName": "someSlave",
+        "slavePassword": "somePassword",
+        "commits": [
+            {
+                "repository": "WebKit",
+                "revision": "141977",
+                "time": "2013-02-06T08:55:20.9Z",
+                "author": {"name": "Commit Queue", "account": "commit-queue@webkit.org"},
+                "message": "some message",
+            }
+        ],
+    };
+    const subversionInvalidCommit = {
+        "slaveName": "someSlave",
+        "slavePassword": "somePassword",
+        "commits": [
+            {
+                "repository": "WebKit",
+                "revision": "_141977",
+                "time": "2013-02-06T08:55:20.9Z",
+                "author": {"name": "Commit Queue", "account": "commit-queue@webkit.org"},
+                "message": "some message",
+            }
+        ],
+    };
+    const subversionTwoCommits = {
+        "slaveName": "someSlave",
+        "slavePassword": "somePassword",
+        "commits": [
+            {
+                "repository": "WebKit",
+                "revision": "141977",
+                "time": "2013-02-06T08:55:20.9Z",
+                "author": {"name": "Commit Queue", "account": "commit-queue@webkit.org"},
+                "message": "some message",
+            },
+            {
+                "repository": "WebKit",
+                "parent": "141977",
+                "revision": "141978",
+                "time": "2013-02-06T09:54:56.0Z",
+                "author": {"name": "Mikhail Pozdnyakov", "account": "mikhail.pozdnyakov@intel.com"},
+                "message": "another message",
+            }
+        ]
+    }
+
+    it("should reject error when slave name is missing", function (done) {
+        TestServer.remoteAPI().postJSON('/api/report-commits/', {}).then(function (response) {
+            assert.equal(response['status'], 'MissingSlaveName');
+            done();
+        }).catch(done);
+    });
+
+    it("should reject when there are no slaves", function (done) {
+        TestServer.remoteAPI().postJSON('/api/report-commits/', emptyReport).then(function (response) {
+            assert.equal(response['status'], 'SlaveNotFound');
+            return TestServer.database().selectAll('commits');
+        }).then(function (rows) {
+            assert.equal(rows.length, 0);
+            done();
+        }).catch(done);
+    });
+
+    it("should accept an empty report", function (done) {
+        addSlaveForReport(emptyReport).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report-commits/', emptyReport);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            done();
+        }).catch(done);
+    });
+
+    it("should add a missing repository", function (done) {
+        const db = TestServer.database();
+        addSlaveForReport(subversionCommit).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report-commits/', subversionCommit);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return db.selectAll('repositories');
+        }).then(function (rows) {
+            assert.equal(rows.length, 1);
+            assert.equal(rows[0]['name'], subversionCommit.commits[0]['repository']);
+            done();
+        }).catch(done);
+    });
+
+    it("should store a commit from a valid slave", function (done) {
+        const db = TestServer.database();
+        addSlaveForReport(subversionCommit).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report-commits/', subversionCommit);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return Promise.all([db.selectAll('commits'), db.selectAll('committers')]);
+        }).then(function (result) {
+            let commits = result[0];
+            let committers = result[1];
+            let reportedData = subversionCommit.commits[0];
+
+            assert.equal(commits.length, 1);
+            assert.equal(committers.length, 1);
+            assert.equal(commits[0]['revision'], reportedData['revision']);
+            assert.equal(commits[0]['time'].toString(), new Date('2013-02-06 08:55:20.9').toString());
+            assert.equal(commits[0]['message'], reportedData['message']);
+            assert.equal(commits[0]['committer'], committers[0]['id']);
+            assert.equal(committers[0]['name'], reportedData['author']['name']);
+            assert.equal(committers[0]['account'], reportedData['author']['account']);
+
+            done();
+        }).catch(done);
+    });
+
+    it("should reject an invalid revision number", function (done) {
+        addSlaveForReport(subversionCommit).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report-commits/', subversionInvalidCommit);
+        }).then(function (response) {
+            assert.equal(response['status'], 'InvalidRevision');
+            return TestServer.database().selectAll('commits');
+        }).then(function (rows) {
+            assert.equal(rows.length, 0);
+            done();
+        }).catch(done);
+    });
+
+    it("should store two commits from a valid slave", function (done) {
+        const db = TestServer.database();
+        addSlaveForReport(subversionTwoCommits).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report-commits/', subversionTwoCommits);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return Promise.all([db.selectAll('commits'), db.selectAll('committers')]);
+        }).then(function (result) {
+            const commits = result[0];
+            const committers = result[1];
+            assert.equal(commits.length, 2);
+            assert.equal(committers.length, 2);
+
+            let reportedData = subversionTwoCommits.commits[0];
+            assert.equal(commits[0]['revision'], reportedData['revision']);
+            assert.equal(commits[0]['time'].toString(), new Date('2013-02-06 08:55:20.9').toString());
+            assert.equal(commits[0]['message'], reportedData['message']);
+            assert.equal(commits[0]['committer'], committers[0]['id']);
+            assert.equal(committers[0]['name'], reportedData['author']['name']);
+            assert.equal(committers[0]['account'], reportedData['author']['account']);
+
+            reportedData = subversionTwoCommits.commits[1];
+            assert.equal(commits[1]['revision'], reportedData['revision']);
+            assert.equal(commits[1]['time'].toString(), new Date('2013-02-06 09:54:56.0').toString());
+            assert.equal(commits[1]['message'], reportedData['message']);
+            assert.equal(commits[1]['committer'], committers[1]['id']);
+            assert.equal(committers[1]['name'], reportedData['author']['name']);
+            assert.equal(committers[1]['account'], reportedData['author']['account']);
+
+            done();
+        }).catch(done);
+    });
+
+    it("should update an existing commit if there is one", function (done) {
+        const db = TestServer.database();
+        const reportedData = subversionCommit.commits[0];
+        addSlaveForReport(subversionCommit).then(function () {
+            return Promise.all([
+                db.insert('repositories', {'id': 1, 'name': 'WebKit'}),
+                db.insert('commits', {'repository': 1, 'revision': reportedData['revision'], 'time': reportedData['time']})
+            ]);
+        }).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report-commits/', subversionCommit);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return Promise.all([db.selectAll('commits'), db.selectAll('committers')]);
+        }).then(function (result) {
+            const commits = result[0];
+            const committers = result[1];
+
+            assert.equal(commits.length, 1);
+            assert.equal(committers.length, 1);
+            assert.equal(commits[0]['message'], reportedData['message']);
+            assert.equal(commits[0]['committer'], committers[0]['id']);
+            assert.equal(committers[0]['name'], reportedData['author']['name']);
+            assert.equal(committers[0]['account'], reportedData['author']['account']);
+
+            done();
+        }).catch(done);
+    });
+
+    it("should not update an unrelated commit", function (done) {
+        const db = TestServer.database();
+        const firstData = subversionTwoCommits.commits[0];
+        const secondData = subversionTwoCommits.commits[1];
+        addSlaveForReport(subversionCommit).then(function () {
+            return Promise.all([
+                db.insert('repositories', {'id': 1, 'name': 'WebKit'}),
+                db.insert('commits', {'id': 2, 'repository': 1, 'revision': firstData['revision'], 'time': firstData['time']}),
+                db.insert('commits', {'id': 3, 'repository': 1, 'revision': secondData['revision'], 'time': secondData['time']})
+            ]);
+        }).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report-commits/', subversionCommit);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return Promise.all([db.selectAll('commits'), db.selectAll('committers')]);
+        }).then(function (result) {
+            const commits = result[0];
+            const committers = result[1];
+
+            assert.equal(commits.length, 2);
+            assert.equal(committers.length, 1);
+            assert.equal(commits[0]['id'], 2);
+            assert.equal(commits[0]['message'], firstData['message']);
+            assert.equal(commits[0]['committer'], committers[0]['id']);
+            assert.equal(committers[0]['name'], firstData['author']['name']);
+            assert.equal(committers[0]['account'], firstData['author']['account']);
+            
+            assert.equal(commits[1]['id'], 3);
+            assert.equal(commits[1]['message'], null);
+            assert.equal(commits[1]['committer'], null);
+
+            done();
+        }).catch(done);
+    });
+
+    it("should update an existing committer if there is one", function (done) {
+        const db = TestServer.database();
+        const author = subversionCommit.commits[0]['author'];
+        addSlaveForReport(subversionCommit).then(function () {
+            return Promise.all([
+                db.insert('repositories', {'id': 1, 'name': 'WebKit'}),
+                db.insert('committers', {'repository': 1, 'account': author['account']}),
+            ]);
+        }).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report-commits/', subversionCommit);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return db.selectAll('committers');
+        }).then(function (committers) {
+            assert.equal(committers.length, 1);
+            assert.equal(committers[0]['name'], author['name']);
+            assert.equal(committers[0]['account'], author['account']);
+            done();
+        }).catch(done);
+    });
+
+});
diff --git a/Websites/perf.webkit.org/server-tests/api-report-tests.js b/Websites/perf.webkit.org/server-tests/api-report-tests.js
new file mode 100644 (file)
index 0000000..0568c48
--- /dev/null
@@ -0,0 +1,773 @@
+'use strict';
+
+const assert = require('assert');
+
+const TestServer = require('./resources/test-server.js');
+const addBuilderForReport = require('./resources/common-operations.js').addBuilderForReport;
+const addSlaveForReport = require('./resources/common-operations.js').addSlaveForReport;
+const connectToDatabaseInEveryTest = require('./resources/common-operations.js').connectToDatabaseInEveryTest;
+
+describe("/api/report", function () {
+    this.timeout(1000);
+    TestServer.inject();
+    connectToDatabaseInEveryTest();
+
+    function emptyReport()
+    {
+        return {
+            "buildNumber": "123",
+            "buildTime": "2013-02-28T10:12:03.388304",
+            "builderName": "someBuilder",
+            "slaveName": "someSlave",
+            "builderPassword": "somePassword",
+            "platform": "Mountain Lion",
+            "tests": {},
+            "revisions": {
+                "OS X": {
+                    "revision": "10.8.2 12C60"
+                },
+                "WebKit": {
+                    "revision": "141977",
+                    "timestamp": "2013-02-06T08:55:20.9Z"
+                }
+            }
+        };
+    }
+
+    function emptySlaveReport()
+    {
+        return {
+            "buildNumber": "123",
+            "buildTime": "2013-02-28T10:12:03.388304",
+            "builderName": "someBuilder",
+            "slaveName": "someSlave",
+            "slavePassword": "otherPassword",
+            "platform": "Mountain Lion",
+            "tests": {},
+            "revisions": {
+                "OS X": {
+                    "revision": "10.8.2 12C60"
+                },
+                "WebKit": {
+                    "revision": "141977",
+                    "timestamp": "2013-02-06T08:55:20.9Z"
+                }
+            }
+        };
+    }
+
+    it("should reject error when builder name is missing", function (done) {
+        TestServer.remoteAPI().postJSON('/api/report/', [{"buildTime": "2013-02-28T10:12:03.388304"}]).then(function (response) {
+            assert.equal(response['status'], 'MissingBuilderName');
+            done();
+        }).catch(done);
+    });
+
+    it("should reject error when build time is missing", function (done) {
+        addBuilderForReport(emptyReport()).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', [{"builderName": "someBuilder", "builderPassword": "somePassword"}]);
+        }).then(function (response) {
+            assert.equal(response['status'], 'MissingBuildTime');
+            done();
+        });
+    });
+
+    it("should reject when there are no builders", function (done) {
+        TestServer.remoteAPI().postJSON('/api/report/', [emptyReport()]).then(function (response) {
+            assert.equal(response['status'], 'BuilderNotFound');
+            assert.equal(response['failureStored'], false);
+            assert.equal(response['processedRuns'], 0);
+            return TestServer.database().selectAll('reports');
+        }).then(function (reports) {
+            assert.equal(reports.length, 0);
+            done();
+        }).catch(done);
+    });
+
+    it("should reject a report without a builder password", function (done) {
+        addBuilderForReport(emptyReport()).then(function () {
+            var report = [{
+                "buildNumber": "123",
+                "buildTime": "2013-02-28T10:12:03.388304",
+                "builderName": "someBuilder",
+                "tests": {},
+                "revisions": {}}];
+            return TestServer.remoteAPI().postJSON('/api/report/', report);
+        }).then(function (response) {
+            assert.equal(response['status'], 'BuilderNotFound');
+            assert.equal(response['failureStored'], false);
+            assert.equal(response['processedRuns'], 0);
+            return TestServer.database().selectAll('reports');
+        }).then(function (reports) {
+            assert.equal(reports.length, 0);
+            done();
+        }).catch(done);
+    });
+
+    it("should store a report from a valid builder", function (done) {
+        addBuilderForReport(emptyReport()).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', [emptyReport()]);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            assert.equal(response['failureStored'], false);
+            assert.equal(response['processedRuns'], 1);
+            return TestServer.database().selectAll('reports');
+        }).then(function (reports) {
+            assert.equal(reports.length, 1);
+            const submittedContent = emptyReport();
+            const storedContent = JSON.parse(reports[0]['content']);
+
+            delete submittedContent['builderPassword'];
+            delete submittedContent['tests'];
+            delete storedContent['tests'];
+            assert.deepEqual(storedContent, submittedContent);
+
+            done();
+        }).catch(done);
+    });
+
+    it("should treat the slave password as the builder password if there is no matching slave", function (done) {
+        let report = emptyReport();
+        report['slavePassword'] = report['builderPassword'];
+        delete report['builderPassword'];
+
+        addSlaveForReport(report).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', [report]);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            assert.equal(response['failureStored'], false);
+            assert.equal(response['processedRuns'], 1);
+            return TestServer.database().selectAll('reports');
+        }).then(function (reports) {
+            assert.equal(reports.length, 1);
+            const storedContent = JSON.parse(reports[0]['content']);
+
+            delete report['slavePassword'];
+            delete report['tests'];
+            delete storedContent['tests'];
+            assert.deepEqual(storedContent, report);
+
+            done();
+        }).catch(done);
+    });
+
+    it("should store a report from a valid slave", function (done) {
+        addSlaveForReport(emptySlaveReport()).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', [emptySlaveReport()]);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            assert.equal(response['failureStored'], false);
+            assert.equal(response['processedRuns'], 1);
+            return TestServer.database().selectAll('reports');
+        }).then(function (reports) {
+            assert.equal(reports.length, 1);
+            const submittedContent = emptySlaveReport();
+            const storedContent = JSON.parse(reports[0]['content']);
+
+            delete submittedContent['slavePassword'];
+            delete submittedContent['tests'];
+            delete storedContent['tests'];
+            assert.deepEqual(storedContent, submittedContent);
+
+            done();
+        }).catch(done);
+    });
+
+    it("should store the builder name but not the builder password", function (done) {
+        addBuilderForReport(emptyReport()).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', [emptyReport()]);
+        }).then(function (response) {
+            return TestServer.database().selectAll('reports');
+        }).then(function (reports) {
+            assert.equal(reports.length, 1);
+            const storedContent = JSON.parse(reports[0]['content']);
+            assert.equal(storedContent['builderName'], emptyReport()['builderName']);
+            assert(!('builderPassword' in storedContent));
+            done();
+        }).catch(done);
+    });
+
+    it("should add a slave if there isn't one and the report was authenticated by a builder", function (done) {
+        addBuilderForReport(emptyReport()).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', [emptyReport()]);
+        }).then(function (response) {
+            return TestServer.database().selectAll('build_slaves');
+        }).then(function (slaves) {
+            assert.equal(slaves.length, 1);
+            assert.equal(slaves[0]['name'], emptyReport()['slaveName']);
+            done();
+        }).catch(done);
+    });
+
+    it("should add a builder if there isn't one and the report was authenticated by a slave", function (done) {
+        addSlaveForReport(emptySlaveReport()).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', [emptySlaveReport()]);
+        }).then(function (response) {
+            return TestServer.database().selectAll('builders');
+        }).then(function (builders) {
+            assert.equal(builders.length, 1);
+            assert.equal(builders[0]['name'], emptyReport()['builderName']);
+            done();
+        }).catch(done);
+    });
+
+    it("should add a build", function (done) {
+        addBuilderForReport(emptyReport()).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', [emptyReport()]);
+        }).then(function () {
+            return TestServer.database().selectAll('builds');
+        }).then(function (builds) {
+            assert.strictEqual(builds[0]['number'], 123);
+            done();
+        }).catch(done);
+    });
+
+    it("should add the platform", function (done) {
+        addBuilderForReport(emptyReport()).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', [emptyReport()]);
+        }).then(function () {
+            return TestServer.database().selectAll('platforms');
+        }).then(function (platforms) {
+            assert.equal(platforms.length, 1);
+            assert.equal(platforms[0]['name'], 'Mountain Lion');
+            done();
+        }).catch(done);
+    });
+
+    it("should add repositories and build revisions", function (done) {
+        addBuilderForReport(emptyReport()).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', [emptyReport()]);
+        }).then(function (response) {
+            const db = TestServer.database();
+            return Promise.all([
+                db.selectAll('repositories'),
+                db.selectAll('commits'),
+                db.selectAll('build_commits', 'build_commit'),
+            ]);
+        }).then(function (result) {
+            const repositories = result[0];
+            const commits = result[1];
+            const buildCommitsRelations = result[2];
+            assert.equal(repositories.length, 2);
+            assert.deepEqual(repositories.map(function (row) { return row['name']; }).sort(), ['OS X', 'WebKit']);
+
+            assert.equal(commits.length, 2);
+            assert.equal(buildCommitsRelations.length, 2);
+            assert.equal(buildCommitsRelations[0]['build_commit'], commits[0]['id']);
+            assert.equal(buildCommitsRelations[1]['build_commit'], commits[1]['id']);
+            assert.equal(buildCommitsRelations[0]['commit_build'], buildCommitsRelations[1]['commit_build']);
+
+            let repositoryIdToName = {};
+            for (let repository of repositories)
+                repositoryIdToName[repository['id']] = repository['name'];
+
+            let repositoryNameToRevisionRow = {};
+            for (let commit of commits)
+                repositoryNameToRevisionRow[repositoryIdToName[commit['repository']]] = commit;
+
+            assert.equal(repositoryNameToRevisionRow['OS X']['revision'], '10.8.2 12C60');
+            assert.equal(repositoryNameToRevisionRow['WebKit']['revision'], '141977');
+            assert.equal(repositoryNameToRevisionRow['WebKit']['time'].toString(),
+                new Date('2013-02-06 08:55:20.9').toString());
+            done();
+        }).catch(done);
+    });
+
+    it("should not create a duplicate build for the same build number if build times are close", function (done) {
+        let firstReport = emptyReport();
+        firstReport['buildTime'] = '2013-02-28T10:12:04';
+        let secondReport = emptyReport();
+        secondReport['buildTime'] = '2013-02-28T10:22:03';
+
+        addBuilderForReport(emptyReport()).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', [firstReport]);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return TestServer.database().selectAll('builds');
+        }).then(function (builds) {
+            assert.equal(builds.length, 1);
+            return TestServer.remoteAPI().postJSON('/api/report/', [secondReport]);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return TestServer.database().selectAll('builds');
+        }).then(function (builds) {
+            assert.equal(builds.length, 1);
+            done();
+        }).catch(done);
+    });
+
+    it("should create distinct builds for the same build number if build times are far apart", function (done) {
+        let firstReport = emptyReport();
+        firstReport['buildTime'] = '2013-02-28T10:12:03';
+        let secondReport = emptyReport();
+        secondReport['buildTime'] = '2014-01-20T22:23:34';
+
+        addBuilderForReport(emptyReport()).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', [firstReport]);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return TestServer.database().selectAll('builds');
+        }).then(function (builds) {
+            assert.equal(builds.length, 1);
+            return TestServer.remoteAPI().postJSON('/api/report/', [secondReport]);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return TestServer.database().selectAll('builds');
+        }).then(function (builds) {
+            assert.equal(builds.length, 2);
+            done();
+        }).catch(done);
+    });
+
+    it("should reject a report with mismatching revision info", function (done) {
+        let firstReport = emptyReport();
+        firstReport['revisions'] = {
+            "WebKit": {
+                "revision": "141977",
+                "timestamp": "2013-02-06T08:55:20.96Z"
+            }
+        };
+
+        let secondReport = emptyReport();
+        secondReport['revisions'] = {
+            "WebKit": {
+                "revision": "150000",
+                "timestamp": "2013-05-13T10:50:29.6Z"
+            }
+        };
+
+        addBuilderForReport(firstReport).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', [firstReport]);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return TestServer.database().selectAll('builds');
+        }).then(function (builds) {
+            assert.equal(builds.length, 1);
+            return TestServer.remoteAPI().postJSON('/api/report/', [secondReport]);
+        }).then(function (response) {
+            assert.equal(response['status'], 'MismatchingCommitRevision');
+            assert(JSON.stringify(response).indexOf('141977') >= 0);
+            assert(JSON.stringify(response).indexOf('150000') >= 0);
+            assert.equal(response['failureStored'], true);
+            assert.equal(response['processedRuns'], 0);
+            done();
+        }).catch(done);
+    });
+
+    const reportWithTwoLevelsOfAggregations = {
+        "buildNumber": "123",
+        "buildTime": "2013-02-28T10:12:03.388304",
+        "builderName": "someBuilder",
+        "builderPassword": "somePassword",
+        "platform": "Mountain Lion",
+        "tests": {
+            "DummyPageLoading": {
+                "metrics": {"Time": { "aggregators" : ["Arithmetic"], "current": [300, 310, 320, 330] }},
+                "tests": {
+                    "apple.com": {
+                        "metrics": {"Time": { "current": [500, 510, 520, 530] }},
+                        "url": "http://www.apple.com"
+                    },
+                    "webkit.org": {
+                        "metrics": {"Time": { "current": [100, 110, 120, 130] }},
+                        "url": "http://www.webkit.org"
+                    }
+                }
+            },
+            "DummyBenchmark": {
+                "metrics": {"Time": ["Arithmetic"]},
+                "tests": {
+                    "DOM": {
+                        "metrics": {"Time": ["Geometric", "Arithmetic"]},
+                        "tests": {
+                            "ModifyNodes": {"metrics": {"Time": { "current": [[11, 12, 13, 14, 15], [16, 17, 18, 19, 20], [21, 22, 23, 24, 25]] }}},
+                            "TraverseNodes": {"metrics": {"Time": { "current": [[31, 32, 33, 34, 35], [36, 37, 38, 39, 40], [41, 42, 43, 44, 45]] }}}
+                        }
+                    },
+                    "CSS": {"metrics": {"Time": { "current": [[101, 102, 103, 104, 105], [106, 107, 108, 109, 110], [111, 112, 113, 114, 115]] }}}
+                }
+            }
+        },
+        "revisions": {
+            "OS X": {
+                "revision": "10.8.2 12C60"
+            },
+            "WebKit": {
+                "revision": "141977",
+                "timestamp": "2013-02-06T08:55:20.9Z"
+            }
+        }};
+
+    function reportAfterAddingBuilderAndAggregators(report)
+    {
+        return addBuilderForReport(report).then(function () {
+            const db = TestServer.database();
+            return Promise.all([
+                db.insert('aggregators', {name: 'Arithmetic'}),
+                db.insert('aggregators', {name: 'Geometric'}),
+            ]);
+        }).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', [report]);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            assert.equal(response['failureStored'], false);
+            return response;
+        });
+    }
+
+    function fetchRunForMetric(testName, metricName,callback) {
+        queryAndFetchAll('SELECT * FROM test_runs WHERE run_config IN'
+            + '(SELECT config_id FROM test_configurations, test_metrics, tests WHERE config_metric = metric_id AND metric_test = test_id AND'
+            + 'test_name = $1 AND metric_name = $2)',
+            ['Arithmetic', 'values.reduce(function (a, b) { return a + b; }) / values.length'], function () {
+            queryAndFetchAll('INSERT INTO aggregators (aggregator_name, aggregator_definition) values ($1, $2)',
+                ['Geometric', 'Math.pow(values.reduce(function (a, b) { return a * b; }), 1 / values.length)'], callback);
+        });
+    }
+
+    it("should accept a report with aggregators", function (done) {
+        reportAfterAddingBuilderAndAggregators(reportWithTwoLevelsOfAggregations).then(function () {
+            done();
+        }).catch(done);
+    });
+
+    it("should add tests", function (done) {
+        reportAfterAddingBuilderAndAggregators(reportWithTwoLevelsOfAggregations).then(function () {
+            return TestServer.database().selectAll('tests');
+        }).then(function (tests) {
+            assert.deepEqual(tests.map(function (row) { return row['name']; }).sort(),
+                ['CSS', 'DOM', 'DummyBenchmark', 'DummyPageLoading', 'ModifyNodes', 'TraverseNodes', 'apple.com', 'webkit.org']);
+            done();
+        }).catch(done);
+    });
+
+    it("should add metrics", function (done) {
+        reportAfterAddingBuilderAndAggregators(reportWithTwoLevelsOfAggregations).then(function () {
+            return TestServer.database().query('SELECT * FROM tests, test_metrics LEFT JOIN aggregators ON metric_aggregator = aggregator_id WHERE metric_test = test_id');
+        }).then(function (result) {
+            let testNameToMetrics = {};
+            result.rows.forEach(function (row) {
+                if (!(row['test_name'] in testNameToMetrics))
+                    testNameToMetrics[row['test_name']] = new Array;
+                testNameToMetrics[row['test_name']].push([row['metric_name'], row['aggregator_name']]);
+            });
+            assert.deepEqual(testNameToMetrics['CSS'], [['Time', null]]);
+            assert.deepEqual(testNameToMetrics['DOM'].sort(), [['Time', 'Arithmetic'], ['Time', 'Geometric']]);
+            assert.deepEqual(testNameToMetrics['DummyBenchmark'], [['Time', 'Arithmetic']]);
+            assert.deepEqual(testNameToMetrics['DummyPageLoading'], [['Time', 'Arithmetic']]);
+            assert.deepEqual(testNameToMetrics['ModifyNodes'], [['Time', null]]);
+            assert.deepEqual(testNameToMetrics['TraverseNodes'], [['Time', null]]);
+            assert.deepEqual(testNameToMetrics['apple.com'], [['Time', null]]);
+            assert.deepEqual(testNameToMetrics['webkit.org'], [['Time', null]]);
+            done();
+        }).catch(done);
+    });
+
+    function fetchTestConfig(testName, metricName)
+    {
+        return TestServer.database().query(`SELECT * FROM tests, test_metrics, test_configurations
+            WHERE test_id = metric_test AND metric_id = config_metric
+            AND test_name = $1 AND metric_name = $2`, [testName, metricName]).then(function (result) {
+                assert.equal(result.rows.length, 1);
+                return result.rows[0];
+            });
+    }
+
+    function fetchTestRunIterationsForMetric(testName, metricName)
+    {
+        const db = TestServer.database();
+        return fetchTestConfig(testName, metricName).then(function (config) {
+            return db.selectFirstRow('test_runs', {config: config['config_id']});
+        }).then(function (run) {
+            return db.selectRows('run_iterations', {run: run['id']}, {sortBy: 'order'}).then(function (iterations) {
+                return {run: run, iterations: iterations};
+            });
+        });
+    }
+
+    it("should store run values", function (done) {
+        reportAfterAddingBuilderAndAggregators(reportWithTwoLevelsOfAggregations).then(function () {
+            return fetchTestRunIterationsForMetric('apple.com', 'Time');
+        }).then(function (result) {
+            const run = result.run;
+            const runId = run['id'];
+            assert.deepEqual(result.iterations, [
+                {run: runId, order: 0, group: null, value: 500, relative_time: null},
+                {run: runId, order: 1, group: null, value: 510, relative_time: null},
+                {run: runId, order: 2, group: null, value: 520, relative_time: null},
+                {run: runId, order: 3, group: null, value: 530, relative_time: null}]);
+            var sum = 500 + 510 + 520 + 530;
+            assert.equal(run['mean_cache'], sum / result.iterations.length);
+            assert.equal(run['sum_cache'], sum);
+            assert.equal(run['square_sum_cache'], 500 * 500 + 510 * 510 + 520 * 520 + 530 * 530);
+            return fetchTestRunIterationsForMetric('CSS', 'Time');
+        }).then(function (result) {
+            const run = result.run;
+            const runId = run['id'];
+            assert.deepEqual(result.iterations, [
+                {run: runId, order: 0, group: 0, value: 101, relative_time: null},
+                {run: runId, order: 1, group: 0, value: 102, relative_time: null},
+                {run: runId, order: 2, group: 0, value: 103, relative_time: null},
+                {run: runId, order: 3, group: 0, value: 104, relative_time: null},
+                {run: runId, order: 4, group: 0, value: 105, relative_time: null},
+                {run: runId, order: 5, group: 1, value: 106, relative_time: null},
+                {run: runId, order: 6, group: 1, value: 107, relative_time: null},
+                {run: runId, order: 7, group: 1, value: 108, relative_time: null},
+                {run: runId, order: 8, group: 1, value: 109, relative_time: null},
+                {run: runId, order: 9, group: 1, value: 110, relative_time: null},
+                {run: runId, order: 10, group: 2, value: 111, relative_time: null},
+                {run: runId, order: 11, group: 2, value: 112, relative_time: null},
+                {run: runId, order: 12, group: 2, value: 113, relative_time: null},
+                {run: runId, order: 13, group: 2, value: 114, relative_time: null},
+                {run: runId, order: 14, group: 2, value: 115, relative_time: null}]);
+            let sum = 0;
+            let squareSum = 0;
+            for (let value = 101; value <= 115; ++value) {
+                sum += value;
+                squareSum += value * value;
+            }
+            assert.equal(run['mean_cache'], sum / result.iterations.length);
+            assert.equal(run['sum_cache'], sum);
+            assert.equal(run['square_sum_cache'], squareSum);
+            done();
+        }).catch(done);
+    });
+
+    it("should store aggregated run values", function (done) {
+        reportAfterAddingBuilderAndAggregators(reportWithTwoLevelsOfAggregations).then(function () {
+            return fetchTestRunIterationsForMetric('DummyPageLoading', 'Time');
+        }).then(function (result) {
+            const run = result.run;
+            const runId = result.run['id'];
+            const expectedValues = [(500 + 100) / 2, (510 + 110) / 2, (520 + 120) / 2, (530 + 130) / 2];
+            assert.deepEqual(result.iterations, [
+                {run: runId, order: 0, group: null, value: expectedValues[0], relative_time: null},
+                {run: runId, order: 1, group: null, value: expectedValues[1], relative_time: null},
+                {run: runId, order: 2, group: null, value: expectedValues[2], relative_time: null},
+                {run: runId, order: 3, group: null, value: expectedValues[3], relative_time: null}]);
+            const sum = expectedValues.reduce(function (sum, value) { return sum + value; }, 0);
+            assert.equal(run['mean_cache'], sum / result.iterations.length);
+            assert.equal(run['sum_cache'], sum);
+            assert.equal(run['square_sum_cache'], expectedValues.reduce(function (sum, value) { return sum + value * value; }, 0));
+            done();
+        }).catch(done);
+    });
+
+    it("should be able to compute the aggregation of aggregated values", function (done) {
+        reportAfterAddingBuilderAndAggregators(reportWithTwoLevelsOfAggregations).then(function () {
+            return fetchTestRunIterationsForMetric('DummyBenchmark', 'Time');
+        }).then(function (result) {
+            const run = result.run;
+            const runId = run['id'];
+            const expectedIterations = [];
+            let sum = 0;
+            let squareSum = 0;
+            for (let i = 0; i < 15; ++i) {
+                const value = i + 1;
+                const DOMMean = ((10 + value) + (30 + value)) / 2;
+                const expectedValue = (DOMMean + 100 + value) / 2;
+                sum += expectedValue;
+                squareSum += expectedValue * expectedValue;
+                expectedIterations.push({run: runId, order: i, group: Math.floor(i / 5), value: expectedValue, relative_time: null});
+            }
+            assert.deepEqual(result.iterations, expectedIterations);
+            assert.equal(run['mean_cache'], sum / result.iterations.length);
+            assert.equal(run['sum_cache'], sum);
+            assert.equal(run['square_sum_cache'], squareSum);
+            done();
+        }).catch(done);
+    });
+
+    function reportWithSameSubtestName()
+    {
+        return {
+            "buildNumber": "123",
+            "buildTime": "2013-02-28T10:12:03.388304",
+            "builderName": "someBuilder",
+            "builderPassword": "somePassword",
+            "platform": "Mountain Lion",
+            "tests": {
+                "Suite1": {
+                    "tests": {
+                        "test1": {
+                            "metrics": {"Time": { "current": [1, 2, 3, 4, 5] }}
+                        },
+                        "test2": {
+                            "metrics": {"Time": { "current": [6, 7, 8, 9, 10] }}
+                        }
+                    }
+                },
+                "Suite2": {
+                    "tests": {
+                        "test1": {
+                            "metrics": {"Time": { "current": [11, 12, 13, 14, 15] }}
+                        },
+                        "test2": {
+                            "metrics": {"Time": { "current": [16, 17, 18, 19, 20] }}
+                        }
+                    }
+                }
+            }
+        };
+    }
+
+    it("should be able to add a report with same subtest name", function (done) {
+        reportAfterAddingBuilderAndAggregators(reportWithSameSubtestName()).then(function () {
+            done();
+        }).catch(done);
+    });
+
+    it("should be able to reuse the same test rows", function (done) {
+        reportAfterAddingBuilderAndAggregators(reportWithSameSubtestName()).then(function () {
+            return TestServer.database().selectAll('tests');
+        }).then(function (tests) {
+            assert.equal(tests.length, 6);
+            let newReport = reportWithSameSubtestName();
+            newReport.buildNumber = "125";
+            newReport.buildTime = "2013-02-28T12:17:24.1";
+            return TestServer.remoteAPI().postJSON('/api/report/', [newReport]);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return TestServer.database().selectAll('tests');
+        }).then(function (tests) {
+            assert.equal(tests.length, 6);
+            done();
+        }).catch(done);
+    });
+
+    const reportWithSameSingleValue = {
+        "buildNumber": "123",
+        "buildTime": "2013-02-28T10:12:03.388304",
+        "builderName": "someBuilder",
+        "builderPassword": "somePassword",
+        "platform": "Mountain Lion",
+        "tests": {
+            "suite": {
+                "metrics": {"Combined": ["Arithmetic"]},
+                "tests": {
+                    "test1": {
+                        "metrics": {"Combined": { "current": 3 }}
+                    },
+                    "test2": {
+                        "metrics": {"Combined": { "current": 7 }}
+                    }
+                }
+            },
+        }};
+
+    it("should be able to add a report with single value results", function (done) {
+        reportAfterAddingBuilderAndAggregators(reportWithSameSingleValue).then(function () {
+            return fetchTestRunIterationsForMetric('test1', 'Combined');
+        }).then(function (result) {
+            const run = result.run;
+            assert.equal(run['iteration_count_cache'], 1);
+            assert.equal(run['mean_cache'], 3);
+            assert.equal(run['sum_cache'], 3);
+            assert.equal(run['square_sum_cache'], 9);
+            return fetchTestRunIterationsForMetric('suite', 'Combined');
+        }).then(function (result) {
+            const run = result.run;
+            assert.equal(run['iteration_count_cache'], 1);
+            assert.equal(run['mean_cache'], 5);
+            assert.equal(run['sum_cache'], 5);
+            assert.equal(run['square_sum_cache'], 25);
+            done();
+        }).catch(done);
+    });
+
+    const reportWithSameValuePairs = {
+        "buildNumber": "123",
+        "buildTime": "2013-02-28T10:12:03.388304",
+        "builderName": "someBuilder",
+        "builderPassword": "somePassword",
+        "platform": "Mountain Lion",
+        "tests": {
+                "test": {
+                    "metrics": {"FrameRate": { "current": [[[0, 4], [100, 5], [205, 3]]] }}
+                },
+            },
+        };
+
+    it("should be able to add a report with (relative time, value) pairs", function (done) {
+        reportAfterAddingBuilderAndAggregators(reportWithSameValuePairs).then(function () {
+            return fetchTestRunIterationsForMetric('test', 'FrameRate');
+        }).then(function (result) {
+            const run = result.run;
+            assert.equal(run['iteration_count_cache'], 3);
+            assert.equal(run['mean_cache'], 4);
+            assert.equal(run['sum_cache'], 12);
+            assert.equal(run['square_sum_cache'], 16 + 25 + 9);
+
+            const runId = run['id'];
+            assert.deepEqual(result.iterations, [
+                {run: runId, order: 0, group: null, value: 4, relative_time: 0},
+                {run: runId, order: 1, group: null, value: 5, relative_time: 100},
+                {run: runId, order: 2, group: null, value: 3, relative_time: 205}]);
+            done();
+        }).catch(done);
+    });
+
+    const reportsUpdatingDifferentTests = [
+        {
+            "buildNumber": "123",
+            "buildTime": "2013-02-28T10:12:03",
+            "builderName": "someBuilder",
+            "builderPassword": "somePassword",
+            "platform": "Mountain Lion",
+            "tests": {"test1": {"metrics": {"Time": {"current": 3}}}}
+        },
+        {
+            "buildNumber": "124",
+            "buildTime": "2013-02-28T11:31:21",
+            "builderName": "someBuilder",
+            "builderPassword": "somePassword",
+            "platform": "Mountain Lion",
+            "tests": {"test2": {"metrics": {"Time": {"current": 3}}}}
+        },
+        {
+            "buildNumber": "125",
+            "buildTime": "2013-02-28T12:45:34",
+            "builderName": "someBuilder",
+            "builderPassword": "somePassword",
+            "platform": "Mountain Lion",
+            "tests": {"test1": {"metrics": {"Time": {"current": 3}}}}
+        },
+    ];
+
+    it("should update the last modified date of test configurations with new runs", function (done) {
+        addBuilderForReport(reportsUpdatingDifferentTests[0]).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', [reportsUpdatingDifferentTests[0]]);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return fetchTestConfig('test1', 'Time');
+        }).then(function (originalConfig) {
+            return TestServer.remoteAPI().postJSON('/api/report/', [reportsUpdatingDifferentTests[2]]).then(function () {
+                return fetchTestConfig('test1', 'Time');
+            }).then(function (config) {
+                assert(originalConfig['config_runs_last_modified'] instanceof Date);
+                assert(config['config_runs_last_modified'] instanceof Date);
+                assert(+originalConfig['config_runs_last_modified'] < +config['config_runs_last_modified']);
+                done();
+            });
+        }).catch(done);
+    });
+
+    it("should not update the last modified date of unrelated test configurations", function (done) {
+        addBuilderForReport(reportsUpdatingDifferentTests[0]).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', [reportsUpdatingDifferentTests[0]]);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return fetchTestConfig('test1', 'Time');
+        }).then(function (originalConfig) {
+            return TestServer.remoteAPI().postJSON('/api/report/', [reportsUpdatingDifferentTests[1]]).then(function (response) {
+                assert.equal(response['status'], 'OK');
+                return fetchTestConfig('test1', 'Time');
+            }).then(function (config) {
+                assert(originalConfig['config_runs_last_modified'] instanceof Date);
+                assert(config['config_runs_last_modified'] instanceof Date);
+                assert.equal(+originalConfig['config_runs_last_modified'], +config['config_runs_last_modified']);
+                done();
+            });
+        }).catch(done);
+    });
+});
index 6ce6794..4fbbdd0 100644 (file)
@@ -9,7 +9,7 @@ let Config = require('../../tools/js/config.js');
 let Database = require('../../tools/js/database.js');
 let RemoteAPI = require('../../tools/js/remote.js').RemoteAPI;
 
-let TestServer = (new class TestServer {
+class TestServer {
     constructor()
     {
         this._pidFile = null;
@@ -241,8 +241,10 @@ let TestServer = (new class TestServer {
             return self.stop();
         });
     }
-});
+}
 
+if (!global.TestServer)
+    global.TestServer = new TestServer;
 
 if (typeof module != 'undefined')
-    module.exports = TestServer;
+    module.exports = global.TestServer;
index dd7009b..8deb630 100644 (file)
@@ -18,7 +18,7 @@ class MockLogger {
 }
 
 describe('BuildbotTriggerable', function () {
-    this.timeout(10000);
+    this.timeout(1000);
     TestServer.inject();
 
     beforeEach(function () {
diff --git a/Websites/perf.webkit.org/tests/admin-platforms.js b/Websites/perf.webkit.org/tests/admin-platforms.js
deleted file mode 100644 (file)
index 8d0ba2c..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-describe("/admin/platforms", function () {
-    var reportsForDifferentPlatforms = [
-    {
-        "buildNumber": "3001",
-        "buildTime": "2013-02-28T09:01:47",
-        "builderName": "someBuilder",
-        "builderPassword": "somePassword",
-        "platform": "Mavericks",
-        "tests": {"test": { "metrics": {"FrameRate": { "current": [[1, 1, 1], [1, 1, 1]] } } } },
-    },
-    {
-        "buildNumber": "3001",
-        "buildTime": "2013-02-28T10:12:03",
-        "builderName": "someBuilder",
-        "builderPassword": "somePassword",
-        "platform": "Mountain Lion",
-        "tests": {"test": { "metrics": {"FrameRate": { "current": [[2, 2, 2], [2, 2, 2]] }, "Combined": { "current": [[3, 3, 3], [3, 3, 3]] }} } },
-    },
-    {
-        "buildNumber": "3003",
-        "buildTime": "2013-02-28T12:56:26",
-        "builderName": "someBuilder",
-        "builderPassword": "somePassword",
-        "platform": "Trunk Mountain Lion",
-        "tests": {"test": { "metrics": {"FrameRate": { "current": [[4, 4, 4], [4, 4, 4]] } } } }
-    }];
-
-    function submitReport(report, callback) {
-        queryAndFetchAll('INSERT INTO builders (builder_name, builder_password_hash) values ($1, $2)',
-            [report[0].builderName, sha256(report[0].builderPassword)], function () {
-                postJSON('/api/report/', reportsForDifferentPlatforms, function (response) {
-                    callback();
-                });
-            });
-    }
-
-    it("should delete the platform that got merged into another one", function () {
-        submitReport(reportsForDifferentPlatforms, function () {
-            queryAndFetchAll('SELECT * FROM platforms ORDER by platform_name', [], function (oldPlatforms) {
-                assert.equal(oldPlatforms.length, 3);
-                assert.equal(oldPlatforms[0]['platform_name'], 'Mavericks');
-                assert.equal(oldPlatforms[1]['platform_name'], 'Mountain Lion');
-                assert.equal(oldPlatforms[2]['platform_name'], 'Trunk Mountain Lion');
-                httpPost('/admin/platforms.php', {'action': 'merge', 'id': oldPlatforms[1]['platform_id'], 'destination': oldPlatforms[2]['platform_id']}, function (response) {
-                    assert.equal(response.statusCode, 200);
-                    queryAndFetchAll('SELECT * FROM platforms ORDER by platform_name', [], function (newPlatforms) {
-                        assert.deepEqual(newPlatforms, [oldPlatforms[0], oldPlatforms[2]]);
-                        notifyDone();
-                    });
-                });
-            });
-        });
-    });
-
-    it("should move test runs from the merged platform to the destination platform", function () {
-        submitReport(reportsForDifferentPlatforms, function () {
-            var queryForRuns = 'SELECT * FROM test_runs, test_configurations, platforms WHERE run_config = config_id AND config_platform = platform_id ORDER by run_mean_cache';
-            queryAndFetchAll(queryForRuns, [], function (oldTestRuns) {
-                assert.equal(oldTestRuns.length, 4);
-                assert.equal(oldTestRuns[0]['platform_name'], 'Mavericks');
-                assert.equal(oldTestRuns[0]['run_sum_cache'], 6);
-                assert.equal(oldTestRuns[1]['platform_name'], 'Mountain Lion');
-                assert.equal(oldTestRuns[1]['run_sum_cache'], 12);
-                assert.equal(oldTestRuns[2]['platform_name'], 'Mountain Lion');
-                assert.equal(oldTestRuns[2]['run_sum_cache'], 18);
-                assert.equal(oldTestRuns[3]['platform_name'], 'Trunk Mountain Lion');
-                assert.equal(oldTestRuns[3]['run_sum_cache'], 24);
-                httpPost('/admin/platforms.php', {'action': 'merge', 'id': oldTestRuns[1]['platform_id'], 'destination': oldTestRuns[3]['platform_id']}, function (response) {
-                    assert.equal(response.statusCode, 200);
-                    queryAndFetchAll(queryForRuns, [], function (newTestRuns) {
-                        assert.equal(newTestRuns.length, 4);
-                        assert.equal(newTestRuns[0]['run_id'], oldTestRuns[0]['run_id']);
-                        assert.equal(newTestRuns[0]['platform_name'], 'Mavericks');
-                        assert.equal(newTestRuns[0]['run_sum_cache'], 6);
-                        assert.equal(newTestRuns[1]['run_id'], oldTestRuns[1]['run_id']);
-                        assert.equal(newTestRuns[1]['platform_name'], 'Trunk Mountain Lion');
-                        assert.equal(newTestRuns[1]['run_sum_cache'], 12);
-                        assert.equal(newTestRuns[2]['run_id'], oldTestRuns[2]['run_id']);
-                        assert.equal(newTestRuns[2]['platform_name'], 'Trunk Mountain Lion');
-                        assert.equal(newTestRuns[2]['run_sum_cache'], 18);
-                        assert.equal(newTestRuns[3]['run_id'], oldTestRuns[3]['run_id']);
-                        assert.equal(newTestRuns[3]['platform_name'], 'Trunk Mountain Lion');
-                        assert.equal(newTestRuns[3]['run_sum_cache'], 24);
-                        assert.equal(newTestRuns[1]['run_config'], newTestRuns[3]['run_config']);
-                        notifyDone();
-                    });
-                });
-            });
-        });
-    });
-
-    it("should move test configurations from the merged platform to the destination platform", function () {
-        reportsForDifferentPlatforms[0]['tests'] = {"test": { "metrics": {"FrameRate": { "baseline": [[1, 1, 1], [1, 1, 1]] } } } };
-        submitReport(reportsForDifferentPlatforms, function () {
-            var queryForConfig = 'SELECT * from test_configurations, platforms, test_metrics'
-                + ' where config_platform = platform_id and config_metric = metric_id and platform_name in ($1, $2) order by config_id';
-            queryAndFetchAll(queryForConfig, [reportsForDifferentPlatforms[0]['platform'], reportsForDifferentPlatforms[2]['platform']], function (configs) {
-                assert.equal(configs.length, 2);
-                assert.equal(configs[0]['platform_name'], reportsForDifferentPlatforms[0]['platform']);
-                assert.equal(configs[0]['metric_name'], 'FrameRate');
-                assert.equal(configs[0]['config_type'], 'baseline');
-                assert.equal(configs[1]['platform_name'], reportsForDifferentPlatforms[2]['platform']);
-                assert.equal(configs[1]['metric_name'], 'FrameRate');
-                assert.equal(configs[1]['config_type'], 'current');
-                httpPost('/admin/platforms.php', {'action': 'merge', 'id': configs[0]['platform_id'], 'destination': configs[1]['platform_id']}, function (response) {
-                    assert.equal(response.statusCode, 200);
-                    queryAndFetchAll(queryForConfig, [reportsForDifferentPlatforms[0]['platform'], reportsForDifferentPlatforms[2]['platform']], function (newConfigs) {
-                        assert.equal(newConfigs.length, 2);
-                        assert.equal(newConfigs[0]['platform_name'], reportsForDifferentPlatforms[2]['platform']);
-                        assert.equal(newConfigs[0]['metric_name'], 'FrameRate');
-                        assert.equal(newConfigs[0]['config_type'], 'baseline');
-                        assert.equal(newConfigs[1]['platform_name'], reportsForDifferentPlatforms[2]['platform']);
-                        assert.equal(newConfigs[1]['metric_name'], 'FrameRate');
-                        assert.equal(newConfigs[1]['config_type'], 'current');
-                        notifyDone();
-                    });
-                });
-            });
-        });
-    });
-
-});
diff --git a/Websites/perf.webkit.org/tests/admin-reprocess-report.js b/Websites/perf.webkit.org/tests/admin-reprocess-report.js
deleted file mode 100644 (file)
index 61a9cad..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-describe("/admin/reprocess-report", function () {
-    var simpleReport = [{
-        "buildNumber": "1986",
-        "buildTime": "2013-02-28T10:12:03",
-        "builderName": "someBuilder",
-        "builderPassword": "somePassword",
-        "platform": "Mountain Lion",
-        "tests": {
-                "test": {
-                    "metrics": {"FrameRate": { "current": [[1, 2, 3], [4, 5, 6]] }}
-                },
-            },
-        }];
-
-    function addBuilder(report, callback) {
-        queryAndFetchAll('INSERT INTO builders (builder_name, builder_password_hash) values ($1, $2)',
-            [report[0].builderName, sha256(report[0].builderPassword)], callback);
-    }
-
-    it("should add build", function () {
-        addBuilder(simpleReport, function () {
-            postJSON('/api/report/', simpleReport, function (response) {
-                assert.equal(response.statusCode, 200);
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                queryAndFetchAll('SELECT * FROM builds', [], function (buildRows) {
-                    assert.equal(buildRows.length, 1);
-                    assert.equal(buildRows[0]['build_number'], 1986);
-                    queryAndFetchAll('SELECT * FROM reports', [], function (reportRows) {
-                        assert.equal(reportRows.length, 1);
-                        assert.equal(reportRows[0]['report_build_number'], 1986);
-                        queryAndFetchAll('UPDATE reports SET report_build = NULL; DELETE FROM builds; SELECT * FROM builds', [], function (buildRows) {
-                            assert.equal(buildRows.length, 0);
-                            var reportId = reportRows[0]['report_id'];
-                            httpGet('/admin/reprocess-report?report=' + reportId, function (response) {
-                                assert.equal(response.statusCode, 200);
-                                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                                queryAndFetchAll('SELECT * FROM builds', [], function (buildRows) {
-                                    assert.equal(buildRows.length, 1);
-                                    assert.equal(buildRows[0]['build_number'], 1986);
-                                    notifyDone();
-                                });
-                            });
-                        });
-                    });
-                });
-            });
-        });
-    });
-
-    it("should not duplicate the reprocessed report", function () {
-        addBuilder(simpleReport, function () {
-            postJSON('/api/report/', simpleReport, function (response) {
-                assert.equal(response.statusCode, 200);
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                queryAndFetchAll('SELECT * FROM reports', [], function (originalReprotRows) {
-                    assert.equal(originalReprotRows.length, 1);
-                    queryAndFetchAll('UPDATE reports SET report_build = NULL; DELETE FROM builds', [], function () {
-                        httpGet('/admin/reprocess-report?report=' + originalReprotRows[0]['report_id'], function (response) {
-                            assert.equal(response.statusCode, 200);
-                            assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                            queryAndFetchAll('SELECT * FROM reports', [], function (reportRows) {
-                                originalReprotRows[0]['report_committed_at'] = null;
-                                reportRows[0]['report_committed_at'] = null;
-                                assert.notEqual(originalReprotRows[0]['report_build'], reportRows[0]['report_build']);
-                                originalReprotRows[0]['report_build'] = null;
-                                reportRows[0]['report_build'] = null;
-                                assert.deepEqual(reportRows, originalReprotRows);
-                                notifyDone();
-                            });
-                        });
-                    });
-                });
-            });
-        });
-    });
-});
diff --git a/Websites/perf.webkit.org/tests/api-measurement-set.js b/Websites/perf.webkit.org/tests/api-measurement-set.js
deleted file mode 100644 (file)
index de5c048..0000000
+++ /dev/null
@@ -1,404 +0,0 @@
-describe("/api/measurement-set", function () {
-    function addBuilder(report, callback) {
-        queryAndFetchAll('INSERT INTO builders (builder_name, builder_password_hash) values ($1, $2)',
-            [report[0].builderName, sha256(report[0].builderPassword)], callback);
-    }
-
-    function queryPlatformAndMetric(platformName, metricName, callback) {
-        queryAndFetchAll('SELECT * FROM platforms WHERE platform_name = $1', [platformName], function (platformRows) {
-            queryAndFetchAll('SELECT * FROM test_metrics WHERE metric_name = $1', [metricName], function (metricRows) {
-                callback(platformRows[0]['platform_id'], metricRows[0]['metric_id']);
-            });
-        });
-    }
-
-    function format(formatMap, row) {
-        var result = {};
-        for (var i = 0; i < formatMap.length; i++) {
-            var key = formatMap[i];
-            if (key == 'id' || key == 'build' || key == 'builder')
-                continue;
-            result[key] = row[i];
-        }
-        return result;
-    }
-
-    var clusterStart = config('clusterStart');
-    clusterStart = +Date.UTC(clusterStart[0], clusterStart[1] - 1, clusterStart[2], clusterStart[3], clusterStart[4]);
-
-    var clusterSize = config('clusterSize');
-    var DAY = 24 * 3600 * 1000;
-    var YEAR = 365.24 * DAY;
-    var MONTH = 30 * DAY;
-    clusterSize = clusterSize[0] * YEAR + clusterSize[1] * MONTH + clusterSize[2] * DAY;
-
-    function clusterTime(index) { return new Date(clusterStart + clusterSize * index); }
-
-    var reportWithBuildTime = [{
-        "buildNumber": "123",
-        "buildTime": clusterTime(7.8).toISOString(),
-        "builderName": "someBuilder",
-        "builderPassword": "somePassword",
-        "platform": "Mountain Lion",
-        "tests": {
-            "Suite": {
-                "tests": {
-                    "test1": {
-                        "metrics": {"Time": { "current": [1, 2, 3, 4, 5] }}
-                    },
-                }
-            },
-        }}];
-    reportWithBuildTime.startTime = +clusterTime(7);
-
-    var reportWithRevision = [{
-        "buildNumber": "124",
-        "buildTime": "2013-02-28T15:34:51",
-        "revisions": {
-            "WebKit": {
-                "revision": "144000",
-                "timestamp": clusterTime(10.3).toISOString(),
-            },
-        },
-        "builderName": "someBuilder",
-        "builderPassword": "somePassword",
-        "platform": "Mountain Lion",
-        "tests": {
-            "Suite": {
-                "tests": {
-                    "test1": {
-                        "metrics": {"Time": { "current": [11, 12, 13, 14, 15] }}
-                    }
-                }
-            },
-        }}];
-
-    var reportWithNewRevision = [{
-        "buildNumber": "125",
-        "buildTime": "2013-02-28T21:45:17",
-        "revisions": {
-            "WebKit": {
-                "revision": "160609",
-                "timestamp": clusterTime(12.1).toISOString()
-            },
-        },
-        "builderName": "someBuilder",
-        "builderPassword": "somePassword",
-        "platform": "Mountain Lion",
-        "tests": {
-            "Suite": {
-                "tests": {
-                    "test1": {
-                        "metrics": {"Time": { "current": [16, 17, 18, 19, 20] }}
-                    }
-                }
-            },
-        }}];
-
-    var reportWithAncentRevision = [{
-        "buildNumber": "126",
-        "buildTime": "2013-02-28T23:07:25",
-        "revisions": {
-            "WebKit": {
-                "revision": "137793",
-                "timestamp": clusterTime(1.8).toISOString()
-            },
-        },
-        "builderName": "someBuilder",
-        "builderPassword": "somePassword",
-        "platform": "Mountain Lion",
-        "tests": {
-            "Suite": {
-                "tests": {
-                    "test1": {
-                        "metrics": {"Time": { "current": [21, 22, 23, 24, 25] }}
-                    }
-                }
-            },
-        }}];
-
-    it("should reject when platform ID is missing", function () {
-        addBuilder(reportWithBuildTime, function () {
-            postJSON('/api/report/', reportWithBuildTime, function (response) {
-                assert.equal(response.statusCode, 200);
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                queryPlatformAndMetric('Mountain Lion', 'Time', function (platformId, metricId) {
-                    httpGet('/api/measurement-set/?metric=' + metricId, function (response) {
-                        assert.notEqual(JSON.parse(response.responseText)['status'], 'InvalidMetric');
-                        notifyDone();
-                    });
-                });
-
-            });
-        });
-    });
-
-    it("should reject when metric ID is missing", function () {
-        addBuilder(reportWithBuildTime, function () {
-            postJSON('/api/report/', reportWithBuildTime, function (response) {
-                assert.equal(response.statusCode, 200);
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                queryPlatformAndMetric('Mountain Lion', 'Time', function (platformId, metricId) {
-                    httpGet('/api/measurement-set/?platform=' + platformId, function (response) {
-                        assert.notEqual(JSON.parse(response.responseText)['status'], 'InvalidPlatform');
-                        notifyDone();
-                    });
-                });
-
-            });
-        });
-    });
-
-    it("should reject an invalid platform name", function () {
-        addBuilder(reportWithBuildTime, function () {
-            postJSON('/api/report/', reportWithBuildTime, function (response) {
-                assert.equal(response.statusCode, 200);
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                queryPlatformAndMetric('Mountain Lion', 'Time', function (platformId, metricId) {
-                    httpGet('/api/measurement-set/?platform=' + platformId + 'a&metric=' + metricId, function (response) {
-                        assert.equal(JSON.parse(response.responseText)['status'], 'InvalidPlatform');
-                        notifyDone();
-                    });
-                });
-
-            });
-        });
-    });
-
-    it("should reject an invalid metric name", function () {
-        addBuilder(reportWithBuildTime, function () {
-            postJSON('/api/report/', reportWithBuildTime, function (response) {
-                assert.equal(response.statusCode, 200);
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                queryPlatformAndMetric('Mountain Lion', 'Time', function (platformId, metricId) {
-                    httpGet('/api/measurement-set/?platform=' + platformId + '&metric=' + metricId + 'b', function (response) {
-                        assert.equal(JSON.parse(response.responseText)['status'], 'InvalidMetric');
-                        notifyDone();
-                    });
-                });
-
-            });
-        });
-    });
-
-    it("should be able to retrieve a reported value", function () {
-        addBuilder(reportWithBuildTime, function () {
-            postJSON('/api/report/', reportWithBuildTime, function (response) {
-                assert.equal(response.statusCode, 200);
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                queryPlatformAndMetric('Mountain Lion', 'Time', function (platformId, metricId) {
-                    httpGet('/api/measurement-set/?platform=' + platformId + '&metric=' + metricId, function (response) {
-                        try {
-                            var paresdResult = JSON.parse(response.responseText);
-                        } catch (error) {
-                            assert.fail(error, null, response.responseText);
-                        }
-
-                        var buildTime = +(new Date(reportWithBuildTime[0]['buildTime']));
-
-                        assert.deepEqual(Object.keys(paresdResult).sort(),
-                            ['clusterCount', 'clusterSize', 'clusterStart',
-                              'configurations', 'elapsedTime', 'endTime', 'formatMap', 'lastModified', 'startTime', 'status']);
-                        assert.equal(paresdResult['status'], 'OK');
-                        assert.equal(paresdResult['clusterCount'], 1);
-                        assert.deepEqual(paresdResult['formatMap'], [
-                            'id', 'mean', 'iterationCount', 'sum', 'squareSum', 'markedOutlier',
-                            'revisions', 'commitTime', 'build', 'buildTime', 'buildNumber', 'builder']);
-
-                        assert.equal(paresdResult['startTime'], reportWithBuildTime.startTime);
-                        assert(typeof(paresdResult['lastModified']) == 'number', 'lastModified time should be a numeric');
-
-                        assert.deepEqual(Object.keys(paresdResult['configurations']), ['current']);
-
-                        var currentRows = paresdResult['configurations']['current'];
-                        assert.equal(currentRows.length, 1);
-                        assert.equal(currentRows[0].length, paresdResult['formatMap'].length);
-                        assert.deepEqual(format(paresdResult['formatMap'], currentRows[0]), {
-                            mean: 3,
-                            iterationCount: 5,
-                            sum: 15,
-                            squareSum: 55,
-                            markedOutlier: false,
-                            revisions: [],
-                            commitTime: buildTime,
-                            buildTime: buildTime,
-                            buildNumber: '123'});
-                        notifyDone();
-                    });
-                });
-
-            });
-        });
-    });
-
-    it("should return return the right IDs for measurement, build, and builder", function () {
-        addBuilder(reportWithBuildTime, function () {
-            postJSON('/api/report/', reportWithBuildTime, function (response) {
-                assert.equal(response.statusCode, 200);
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                queryPlatformAndMetric('Mountain Lion', 'Time', function (platformId, metricId) {
-                    queryAndFetchAll('SELECT * FROM test_runs', [], function (runs) {
-                        assert.equal(runs.length, 1);
-                        var measurementId = runs[0]['run_id'];
-                        queryAndFetchAll('SELECT * FROM builds', [], function (builds) {
-                            assert.equal(builds.length, 1);
-                            var buildId = builds[0]['build_id'];
-                            queryAndFetchAll('SELECT * FROM builders', [], function (builders) {
-                                assert.equal(builders.length, 1);
-                                var builderId = builders[0]['builder_id'];
-                                httpGet('/api/measurement-set/?platform=' + platformId + '&metric=' + metricId, function (response) {
-                                    var paresdResult = JSON.parse(response.responseText);
-                                    assert.equal(paresdResult['configurations']['current'].length, 1);
-                                    var measurement = paresdResult['configurations']['current'][0];
-                                    assert.equal(paresdResult['status'], 'OK');
-                                    assert.equal(measurement[paresdResult['formatMap'].indexOf('id')], measurementId);
-                                    assert.equal(measurement[paresdResult['formatMap'].indexOf('build')], buildId);
-                                    assert.equal(measurement[paresdResult['formatMap'].indexOf('builder')], builderId);
-                                    notifyDone();
-                                });
-                            });
-                        });
-                    });
-                });
-            });
-        });
-    });
-
-    function postReports(reports, callback) {
-        if (!reports.length)
-            return callback();
-
-        postJSON('/api/report/', reports[0], function (response) {
-            assert.equal(response.statusCode, 200);
-            assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-
-            postReports(reports.slice(1), callback);
-        });
-    }
-
-    function queryPlatformAndMetricWithRepository(platformName, metricName, repositoryName, callback) {
-        queryPlatformAndMetric(platformName, metricName, function (platformId, metricId) {
-            queryAndFetchAll('SELECT * FROM repositories WHERE repository_name = $1', [repositoryName], function (rows) {
-                callback(platformId, metricId, rows[0]['repository_id']);
-            });
-        });
-    }
-
-    it("should order results by commit time", function () {
-        addBuilder(reportWithBuildTime, function () {
-            postReports([reportWithBuildTime, reportWithRevision], function () {
-                queryPlatformAndMetricWithRepository('Mountain Lion', 'Time', 'WebKit', function (platformId, metricId, repositoryId) {
-                    httpGet('/api/measurement-set/?platform=' + platformId + '&metric=' + metricId, function (response) {
-                        var parsedResult = JSON.parse(response.responseText);
-                        assert.equal(parsedResult['status'], 'OK');
-
-                        var buildTime = +(new Date(reportWithBuildTime[0]['buildTime']));
-                        var revisionTime = +(new Date(reportWithRevision[0]['revisions']['WebKit']['timestamp']));
-                        var revisionBuildTime = +(new Date(reportWithRevision[0]['buildTime']));
-
-                        var currentRows = parsedResult['configurations']['current'];
-                        assert.equal(currentRows.length, 2);
-                        assert.deepEqual(format(parsedResult['formatMap'], currentRows[0]), {
-                           mean: 13,
-                           iterationCount: 5,
-                           sum: 65,
-                           squareSum: 855,
-                           markedOutlier: false,
-                           revisions: [[1, repositoryId, '144000', revisionTime]],
-                           commitTime: revisionTime,
-                           buildTime: revisionBuildTime,
-                           buildNumber: '124' });
-                        assert.deepEqual(format(parsedResult['formatMap'], currentRows[1]), {
-                            mean: 3,
-                            iterationCount: 5,
-                            sum: 15,
-                            squareSum: 55,
-                            markedOutlier: false,
-                            revisions: [],
-                            commitTime: buildTime,
-                            buildTime: buildTime,
-                            buildNumber: '123' });
-                        notifyDone();
-                    });
-                });                
-            });
-        });
-    });
-
-    function buildNumbers(parsedResult, config) {
-        return parsedResult['configurations'][config].map(function (row) {
-            return format(parsedResult['formatMap'], row)['buildNumber'];
-        });
-    }
-
-    it("should include one data point after the current time range", function () {
-        addBuilder(reportWithBuildTime, function () {
-            postReports([reportWithAncentRevision, reportWithNewRevision], function () {
-                queryPlatformAndMetricWithRepository('Mountain Lion', 'Time', 'WebKit', function (platformId, metricId, repositoryId) {
-                    httpGet('/api/measurement-set/?platform=' + platformId + '&metric=' + metricId, function (response) {
-                        var parsedResult = JSON.parse(response.responseText);
-                        assert.equal(parsedResult['status'], 'OK');
-                        assert.equal(parsedResult['clusterCount'], 2, 'should have two clusters');
-                        assert.deepEqual(buildNumbers(parsedResult, 'current'),
-                            [reportWithAncentRevision[0]['buildNumber'], reportWithNewRevision[0]['buildNumber']]);
-                        notifyDone();
-                    });
-                });                
-            });
-        });
-    });
-
-    // FIXME: This test assumes a cluster step of 2-3 months
-    it("should always include one old data point before the current time range", function () {
-        addBuilder(reportWithBuildTime, function () {
-            postReports([reportWithBuildTime, reportWithAncentRevision], function () {
-                queryPlatformAndMetricWithRepository('Mountain Lion', 'Time', 'WebKit', function (platformId, metricId, repositoryId) {
-                    httpGet('/api/measurement-set/?platform=' + platformId + '&metric=' + metricId, function (response) {
-                        var parsedResult = JSON.parse(response.responseText);
-                        assert.equal(parsedResult['status'], 'OK');
-                        assert.equal(parsedResult['clusterCount'], 2, 'should have two clusters');
-
-                        var currentRows = parsedResult['configurations']['current'];
-                        assert.equal(currentRows.length, 2, 'should contain at least two data points');
-                        assert.deepEqual(buildNumbers(parsedResult, 'current'),
-                            [reportWithAncentRevision[0]['buildNumber'], reportWithBuildTime[0]['buildNumber']]);
-                        notifyDone();
-                    });
-                });                
-            });
-        });
-    });
-
-    // FIXME: This test assumes a cluster step of 2-3 months
-    it("should create cache results", function () {
-        addBuilder(reportWithBuildTime, function () {
-            postReports([reportWithAncentRevision, reportWithRevision, reportWithNewRevision], function () {
-                queryPlatformAndMetricWithRepository('Mountain Lion', 'Time', 'WebKit', function (platformId, metricId, repositoryId) {
-                    httpGet('/api/measurement-set/?platform=' + platformId + '&metric=' + metricId, function (response) {
-                        var parsedResult = JSON.parse(response.responseText);
-                        assert.equal(parsedResult['status'], 'OK');
-                        var cachePrefix = '/data/measurement-set-' + platformId + '-' + metricId;
-                        httpGet(cachePrefix + '.json', function (response) {
-                            var parsedCachedResult = JSON.parse(response.responseText);
-                            assert.deepEqual(parsedResult, parsedCachedResult);
-
-                            httpGet(cachePrefix + '-' + parsedResult['startTime'] + '.json', function (response) {
-                                var parsedOldResult = JSON.parse(response.responseText);
-
-                                var oldBuildNumbers = buildNumbers(parsedOldResult, 'current');
-                                var newBuildNumbers = buildNumbers(parsedResult, 'current');
-                                assert(oldBuildNumbers.length >= 2, 'The old cluster should contain at least two data points');
-                                assert(newBuildNumbers.length >= 2, 'The new cluster should contain at least two data points');
-                                assert.deepEqual(oldBuildNumbers.slice(oldBuildNumbers.length - 2), newBuildNumbers.slice(0, 2),
-                                    'Two conseqcutive clusters should share two data points');
-
-                                notifyDone();
-                            });
-                        });
-                    });
-                });                
-            });
-        });
-    });
-
-});
diff --git a/Websites/perf.webkit.org/tests/api-report-commits.js b/Websites/perf.webkit.org/tests/api-report-commits.js
deleted file mode 100644 (file)
index 042714f..0000000
+++ /dev/null
@@ -1,237 +0,0 @@
-describe("/api/report-commits/", function () {
-    var emptyReport = {
-        "slaveName": "someSlave",
-        "slavePassword": "somePassword",
-    };
-    var subversionCommit = {
-        "slaveName": "someSlave",
-        "slavePassword": "somePassword",
-        "commits": [
-            {
-                "repository": "WebKit",
-                "revision": "141977",
-                "time": "2013-02-06T08:55:20.9Z",
-                "author": {"name": "Commit Queue", "account": "commit-queue@webkit.org"},
-                "message": "some message",
-            }
-        ],
-    };
-    var subversionInvalidCommit = {
-        "slaveName": "someSlave",
-        "slavePassword": "somePassword",
-        "commits": [
-            {
-                "repository": "WebKit",
-                "revision": "_141977",
-                "time": "2013-02-06T08:55:20.9Z",
-                "author": {"name": "Commit Queue", "account": "commit-queue@webkit.org"},
-                "message": "some message",
-            }
-        ],
-    };
-    var subversionTwoCommits = {
-        "slaveName": "someSlave",
-        "slavePassword": "somePassword",
-        "commits": [
-            {
-                "repository": "WebKit",
-                "revision": "141977",
-                "time": "2013-02-06T08:55:20.9Z",
-                "author": {"name": "Commit Queue", "account": "commit-queue@webkit.org"},
-                "message": "some message",
-            },
-            {
-                "repository": "WebKit",
-                "parent": "141977",
-                "revision": "141978",
-                "time": "2013-02-06T09:54:56.0Z",
-                "author": {"name": "Mikhail Pozdnyakov", "account": "mikhail.pozdnyakov@intel.com"},
-                "message": "another message",
-            }
-        ]
-    }
-
-    function addSlave(report, callback) {
-        queryAndFetchAll('INSERT INTO build_slaves (slave_name, slave_password_hash) values ($1, $2)',
-            [report.slaveName, sha256(report.slavePassword)], callback);
-    }
-
-    it("should reject error when slave name is missing", function () {
-        postJSON('/api/report-commits/', {}, function (response) {
-            assert.equal(response.statusCode, 200);
-            assert.equal(JSON.parse(response.responseText)['status'], 'MissingSlaveName');
-            notifyDone();
-        });
-    });
-
-    it("should reject when there are no slaves", function () {
-        postJSON('/api/report-commits/', emptyReport, function (response) {
-            assert.equal(response.statusCode, 200);
-            assert.notEqual(JSON.parse(response.responseText)['status'], 'OK');
-
-            queryAndFetchAll('SELECT COUNT(*) from commits', [], function (rows) {
-                assert.equal(rows[0].count, 0);
-                notifyDone();
-            });
-        });
-    });
-
-    it("should accept an empty report", function () {
-        addSlave(emptyReport, function () {
-            postJSON('/api/report-commits/', emptyReport, function (response) {
-                assert.equal(response.statusCode, 200);
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                notifyDone();
-            });
-        });
-    });
-
-    it("should add a missing repository", function () {
-        addSlave(subversionCommit, function () {
-            postJSON('/api/report-commits/', subversionCommit, function (response) {
-                assert.equal(response.statusCode, 200);
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                queryAndFetchAll('SELECT * FROM repositories', [], function (rows) {
-                    assert.equal(rows.length, 1);
-                    assert.equal(rows[0]['repository_name'], subversionCommit.commits[0]['repository']);
-                    notifyDone();
-                });
-            });
-        });
-    });
-
-    it("should store a commit from a valid slave", function () {
-        addSlave(subversionCommit, function () {
-            postJSON('/api/report-commits/', subversionCommit, function (response) {
-                assert.equal(response.statusCode, 200);
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                queryAndFetchAll('SELECT * FROM commits JOIN committers ON commit_committer = committer_id', [], function (rows) {
-                    assert.equal(rows.length, 1);
-                    var reportedData = subversionCommit.commits[0];
-                    assert.equal(rows[0]['commit_revision'], reportedData['revision']);
-                    assert.equal(rows[0]['commit_time'].toString(), new Date('2013-02-06 08:55:20.9').toString());
-                    assert.equal(rows[0]['committer_name'], reportedData['author']['name']);
-                    assert.equal(rows[0]['committer_account'], reportedData['author']['account']);
-                    assert.equal(rows[0]['commit_message'], reportedData['message']);
-                    notifyDone();
-                });
-            });
-        });
-    });
-
-    it("should reject an invalid revision number", function () {
-        addSlave(subversionCommit, function () {
-            subversionCommit
-            postJSON('/api/report-commits/', subversionInvalidCommit, function (response) {
-                assert.equal(response.statusCode, 200);
-                assert.notEqual(JSON.parse(response.responseText)['status'], 'OK');
-                queryAndFetchAll('SELECT * FROM commits', [], function (rows) {
-                    assert.equal(rows.length, 0);
-                    notifyDone();
-                });
-            });
-        });
-    });
-
-    it("should store two commits from a valid slave", function () {
-        addSlave(subversionTwoCommits, function () {
-            postJSON('/api/report-commits/', subversionTwoCommits, function (response) {
-                assert.equal(response.statusCode, 200);
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                queryAndFetchAll('SELECT * FROM commits JOIN committers ON commit_committer = committer_id ORDER BY commit_time', [], function (rows) {
-                    assert.equal(rows.length, 2);
-                    var reportedData = subversionTwoCommits.commits[0];
-                    assert.equal(rows[0]['commit_revision'], reportedData['revision']);
-                    assert.equal(rows[0]['commit_time'].toString(), new Date('2013-02-06 08:55:20.9').toString());
-                    assert.equal(rows[0]['committer_name'], reportedData['author']['name']);
-                    assert.equal(rows[0]['committer_account'], reportedData['author']['account']);
-                    assert.equal(rows[0]['commit_message'], reportedData['message']);
-                    var reportedData = subversionTwoCommits.commits[1];
-                    assert.equal(rows[1]['commit_revision'], reportedData['revision']);
-                    assert.equal(rows[1]['commit_time'].toString(), new Date('2013-02-06 09:54:56.0').toString());
-                    assert.equal(rows[1]['committer_name'], reportedData['author']['name']);
-                    assert.equal(rows[1]['committer_account'], reportedData['author']['account']);
-                    assert.equal(rows[1]['commit_message'], reportedData['message']);
-                    notifyDone();
-                });
-            });
-        });
-    });
-
-    it("should update an existing commit if there is one", function () {
-        queryAndFetchAll('INSERT INTO repositories (repository_name) VALUES ($1) RETURNING *', ['WebKit'], function (repositories) {
-            var repositoryId = repositories[0]['repository_id'];
-            var reportedData = subversionCommit.commits[0];
-            queryAndFetchAll('INSERT INTO commits (commit_repository, commit_revision, commit_time) VALUES ($1, $2, $3) RETURNING *',
-                [repositoryId, reportedData['revision'], reportedData['time']], function (existingCommits) {
-                var commitId = existingCommits[0]['commit_id'];
-                assert.equal(existingCommits[0]['commit_message'], null);
-                addSlave(subversionCommit, function () {
-                    postJSON('/api/report-commits/', subversionCommit, function (response) {
-                        assert.equal(response.statusCode, 200);
-                        assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                        queryAndFetchAll('SELECT * FROM commits JOIN committers ON commit_committer = committer_id', [], function (rows) {
-                            assert.equal(rows.length, 1);
-                            var reportedData = subversionCommit.commits[0];
-                            assert.equal(rows[0]['committer_name'], reportedData['author']['name']);
-                            assert.equal(rows[0]['committer_account'], reportedData['author']['account']);
-                            assert.equal(rows[0]['commit_message'], reportedData['message']);
-                            notifyDone();
-                        });
-                    });
-                });
-            });
-        });
-    });
-
-    it("should not update an unrelated commit", function () {
-        queryAndFetchAll('INSERT INTO repositories (repository_name) VALUES ($1) RETURNING *', ['WebKit'], function (repositories) {
-            var repositoryId = repositories[0]['repository_id'];
-            var reportedData = subversionTwoCommits.commits[1];
-            queryAndFetchAll('INSERT INTO commits (commit_repository, commit_revision, commit_time) VALUES ($1, $2, $3) RETURNING *',
-                [repositoryId, reportedData['revision'], reportedData['time']], function (existingCommits) {
-                reportedData = subversionTwoCommits.commits[0];
-                queryAndFetchAll('INSERT INTO commits (commit_repository, commit_revision, commit_time) VALUES ($1, $2, $3) RETURNING *',
-                    [repositoryId, reportedData['revision'], reportedData['time']], function () {
-                        addSlave(subversionCommit, function () {
-                            postJSON('/api/report-commits/', subversionCommit, function (response) {
-                                assert.equal(response.statusCode, 200);
-                                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                                queryAndFetchAll('SELECT * FROM commits LEFT OUTER JOIN committers ON commit_committer = committer_id ORDER BY commit_time', [], function (rows) {
-                                    assert.equal(rows.length, 2);
-                                    assert.equal(rows[0]['committer_name'], reportedData['author']['name']);
-                                    assert.equal(rows[0]['committer_account'], reportedData['author']['account']);
-                                    assert.equal(rows[0]['commit_message'], reportedData['message']);
-                                    assert.equal(rows[1]['committer_name'], null);
-                                    assert.equal(rows[1]['committer_account'], null);
-                                    assert.equal(rows[1]['commit_message'], null);
-                                    notifyDone();
-                                });
-                            });
-                        });
-                });
-            });
-        });
-    });
-
-    it("should update an existing committer if there is one", function () {
-        queryAndFetchAll('INSERT INTO repositories (repository_id, repository_name) VALUES (1, \'WebKit\')', [], function () {
-            var author = subversionCommit.commits[0]['author'];
-            queryAndFetchAll('INSERT INTO committers (committer_repository, committer_account) VALUES (1, $1)', [author['account']], function () {
-                addSlave(subversionCommit, function () {
-                    postJSON('/api/report-commits/', subversionCommit, function (response) {
-                        assert.equal(response.statusCode, 200);
-                        assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                        queryAndFetchAll('SELECT * FROM committers', [], function (rows) {
-                            assert.equal(rows.length, 1);
-                            assert.equal(rows[0]['committer_name'], author['name']);
-                            assert.equal(rows[0]['committer_account'], author['account']);
-                            notifyDone();
-                        });
-                    });
-                });
-            });
-        });
-    });
-
-});
diff --git a/Websites/perf.webkit.org/tests/api-report.js b/Websites/perf.webkit.org/tests/api-report.js
deleted file mode 100644 (file)
index 4d3adb9..0000000
+++ /dev/null
@@ -1,759 +0,0 @@
-describe("/api/report", function () {
-    var emptyReport = [{
-        "buildNumber": "123",
-        "buildTime": "2013-02-28T10:12:03.388304",
-        "builderName": "someBuilder",
-        "slaveName": "someSlave",
-        "builderPassword": "somePassword",
-        "platform": "Mountain Lion",
-        "tests": {},
-        "revisions": {
-            "OS X": {
-                "revision": "10.8.2 12C60"
-            },
-            "WebKit": {
-                "revision": "141977",
-                "timestamp": "2013-02-06T08:55:20.9Z"
-            }
-        }}];
-
-    var emptySlaveReport = [{
-        "buildNumber": "123",
-        "buildTime": "2013-02-28T10:12:03.388304",
-        "builderName": "someBuilder",
-        "builderPassword": "somePassword",
-        "slaveName": "someSlave",
-        "slavePassword": "otherPassword",
-        "platform": "Mountain Lion",
-        "tests": {},
-        "revisions": {
-            "OS X": {
-                "revision": "10.8.2 12C60"
-            },
-            "WebKit": {
-                "revision": "141977",
-                "timestamp": "2013-02-06T08:55:20.9Z"
-            }
-        }}];
-
-    function addBuilder(report, callback) {
-        queryAndFetchAll('INSERT INTO builders (builder_name, builder_password_hash) values ($1, $2)',
-            [report[0].builderName, sha256(report[0].builderPassword)], callback);
-    }
-
-    function addSlave(report, callback) {
-        queryAndFetchAll('INSERT INTO build_slaves (slave_name, slave_password_hash) values ($1, $2)',
-            [report[0].slaveName, sha256(report[0].slavePassword)], callback);
-    }
-
-    it("should reject error when builder name is missing", function () {
-        postJSON('/api/report/', [{"buildTime": "2013-02-28T10:12:03.388304"}], function (response) {
-            assert.equal(response.statusCode, 200);
-            assert.equal(JSON.parse(response.responseText)['status'], 'MissingBuilderName');
-            notifyDone();
-        });
-    });
-
-    it("should reject error when build time is missing", function () {
-        addBuilder(emptyReport, function () {
-            postJSON('/api/report/', [{"builderName": "someBuilder", "builderPassword": "somePassword"}], function (response) {
-                assert.equal(response.statusCode, 200);
-                assert.equal(JSON.parse(response.responseText)['status'], 'MissingBuildTime');
-                notifyDone();
-            });
-        });
-    });
-
-    it("should reject when there are no builders", function () {
-        postJSON('/api/report/', emptyReport, function (response) {
-            assert.equal(response.statusCode, 200);
-            assert.notEqual(JSON.parse(response.responseText)['status'], 'OK');
-            assert.equal(JSON.parse(response.responseText)['failureStored'], false);
-            assert.equal(JSON.parse(response.responseText)['processedRuns'], 0);
-
-            queryAndFetchAll('SELECT COUNT(*) from reports', [], function (rows) {
-                assert.equal(rows[0].count, 0);
-                notifyDone();
-            });
-        });
-    });
-
-    it("should reject a report without a builder password", function () {
-        addBuilder(emptyReport, function () {
-            var report = [{
-                "buildNumber": "123",
-                "buildTime": "2013-02-28T10:12:03.388304",
-                "builderName": "someBuilder",
-                "tests": {},
-                "revisions": {}}];
-            postJSON('/api/report/', report, function (response) {
-                assert.equal(response.statusCode, 200);
-                assert.notEqual(JSON.parse(response.responseText)['status'], 'OK');
-                assert.equal(JSON.parse(response.responseText)['failureStored'], false);
-                assert.equal(JSON.parse(response.responseText)['processedRuns'], 0);
-
-                queryAndFetchAll('SELECT COUNT(*) from reports', [], function (rows) {
-                    assert.equal(rows[0].count, 0);
-                    notifyDone();
-                });
-            });
-        });
-    });
-
-    it("should store a report from a valid builder", function () {
-        addBuilder(emptyReport, function () {
-            postJSON('/api/report/', emptyReport, function (response) {
-                assert.equal(response.statusCode, 200);
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                assert.equal(JSON.parse(response.responseText)['failureStored'], false);
-                assert.equal(JSON.parse(response.responseText)['processedRuns'], 1);
-                queryAndFetchAll('SELECT COUNT(*) from reports', [], function (rows) {
-                    assert.equal(rows[0].count, 1);
-                    notifyDone();
-                });
-            });
-        });
-    });
-
-    it("should treat the slave password as the builder password if there is no matching slave", function () {
-        addBuilder(emptyReport, function () {
-            emptyReport[0]['slavePassword'] = emptyReport[0]['builderPassword'];
-            delete emptyReport[0]['builderPassword'];
-            postJSON('/api/report/', emptyReport, function (response) {
-                emptyReport[0]['builderPassword'] = emptyReport[0]['slavePassword'];
-                delete emptyReport[0]['slavePassword'];
-
-                assert.equal(response.statusCode, 200);
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                assert.equal(JSON.parse(response.responseText)['failureStored'], false);
-                assert.equal(JSON.parse(response.responseText)['processedRuns'], 1);
-                queryAndFetchAll('SELECT COUNT(*) from reports', [], function (rows) {
-                    assert.equal(rows[0].count, 1);
-                    notifyDone();
-                });
-            });
-        });
-    });
-
-    it("should store a report from a valid slave", function () {
-        addSlave(emptySlaveReport, function () {
-            postJSON('/api/report/', emptySlaveReport, function (response) {
-                assert.equal(response.statusCode, 200);
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                assert.equal(JSON.parse(response.responseText)['failureStored'], false);
-                assert.equal(JSON.parse(response.responseText)['processedRuns'], 1);
-                queryAndFetchAll('SELECT COUNT(*) from reports', [], function (rows) {
-                    assert.equal(rows[0].count, 1);
-                    notifyDone();
-                });
-            });
-        });
-    });
-
-    it("should store the builder name but not the builder password", function () {
-        addBuilder(emptyReport, function () {
-            postJSON('/api/report/', emptyReport, function (response) {
-                queryAndFetchAll('SELECT report_content from reports', [], function (rows) {
-                    var storedContent = JSON.parse(rows[0].report_content);
-                    assert.equal(storedContent['builderName'], emptyReport[0]['builderName']);
-                    assert(!('builderPassword' in storedContent));
-                    notifyDone();
-                });
-            });
-        });
-    });
-
-    it("should add a slave if there isn't one and the report was authenticated by a builder", function () {
-        addBuilder(emptyReport, function () {
-            postJSON('/api/report/', emptyReport, function (response) {
-                queryAndFetchAll('SELECT * from build_slaves', [], function (rows) {
-                    assert.strictEqual(rows[0].slave_name, emptyReport[0].slaveName);
-                    notifyDone();
-                });
-            });
-        });
-    });
-
-    it("should add a builder if there isn't one and the report was authenticated by a slave", function () {
-        addSlave(emptySlaveReport, function () {
-            postJSON('/api/report/', emptySlaveReport, function (response) {
-                queryAndFetchAll('SELECT * from builders', [], function (rows) {
-                    assert.strictEqual(rows[0].builder_name, emptyReport[0].builderName);
-                    notifyDone();
-                });
-            });
-        });
-    });
-
-    it("should add a build", function () {
-        addBuilder(emptyReport, function () {
-            postJSON('/api/report/', emptyReport, function (response) {
-                queryAndFetchAll('SELECT * from builds', [], function (rows) {
-                    assert.strictEqual(rows[0].build_number, 123);
-                    notifyDone();
-                });
-            });
-        });
-    });
-
-    it("should add the platform", function () {
-        addBuilder(emptyReport, function () {
-            postJSON('/api/report/', emptyReport, function (response) {
-                queryAndFetchAll('SELECT * from platforms', [], function (rows) {
-                    assert.strictEqual(rows[0].platform_name, 'Mountain Lion');
-                    notifyDone();
-                });
-            });
-        });
-    });
-
-    it("should add repositories and build revisions", function () {
-        addBuilder(emptyReport, function () {
-            postJSON('/api/report/', emptyReport, function (response) {
-                queryAndFetchAll('SELECT * FROM repositories', [], function (rows) {
-                    assert.deepEqual(rows.map(function (row) { return row['repository_name']; }), ['OS X', 'WebKit']);
-
-                    var repositoryIdToName = {};
-                    rows.forEach(function (row) { repositoryIdToName[row['repository_id']] = row['repository_name']; });
-                    queryAndFetchAll('SELECT * FROM build_commits, commits WHERE build_commit = commit_id', [], function (rows) {
-                        var repositoryNameToRevisionRow = {};
-                        rows.forEach(function (row) {
-                            repositoryNameToRevisionRow[repositoryIdToName[row['commit_repository']]] = row;
-                        });
-                        assert.equal(repositoryNameToRevisionRow['OS X']['commit_revision'], '10.8.2 12C60');
-                        assert.equal(repositoryNameToRevisionRow['WebKit']['commit_revision'], '141977');
-                        assert.equal(repositoryNameToRevisionRow['WebKit']['commit_time'].toString(),
-                            new Date('2013-02-06 08:55:20.9').toString());
-                        notifyDone();
-                    });
-                });
-            });
-        });
-    });
-
-    it("should not create a duplicate build for the same build number if build times are close", function () {
-        addBuilder(emptyReport, function () {
-            emptyReport[0]['buildTime'] = '2013-02-28T10:12:04';
-            postJSON('/api/report/', emptyReport, function (response) {
-                assert.equal(response.statusCode, 200);
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-
-                emptyReport[0]['buildTime'] = '2013-02-28T10:22:03';
-                postJSON('/api/report/', emptyReport, function (response) {
-                    assert.equal(response.statusCode, 200);
-                    assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-
-                    queryAndFetchAll('SELECT * FROM builds', [], function (rows) {
-                        assert.equal(rows.length, 1);
-                        notifyDone();
-                    });
-                });
-            });
-        });
-    });
-
-    it("should create distinct builds for the same build number if build times are far apart", function () {
-        addBuilder(emptyReport, function () {
-            emptyReport[0]['buildTime'] = '2013-02-28T10:12:03';
-            postJSON('/api/report/', emptyReport, function (response) {
-                assert.equal(response.statusCode, 200);
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-
-                queryAndFetchAll('SELECT * FROM builds', [], function (rows) {
-                    assert.equal(rows.length, 1);
-
-                    emptyReport[0]['buildTime'] = '2014-01-20T22:23:34';
-                    postJSON('/api/report/', emptyReport, function (response) {
-                        assert.equal(response.statusCode, 200);
-                        assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-
-                        queryAndFetchAll('SELECT * FROM builds', [], function (rows) {
-                            assert.equal(rows.length, 2);
-                            notifyDone();
-                        });
-                    });
-                });
-            });
-        });
-    });
-
-    it("should reject a report with mismatching revision info", function () {
-        addBuilder(emptyReport, function () {
-            emptyReport[0]['revisions'] = {
-                "WebKit": {
-                    "revision": "141977",
-                    "timestamp": "2013-02-06T08:55:20.96Z"
-                }
-            }
-            postJSON('/api/report/', emptyReport, function (response) {
-                assert.equal(response.statusCode, 200);
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-
-                emptyReport[0]['revisions'] = {
-                    "WebKit": {
-                        "revision": "150000",
-                        "timestamp": "2013-05-13T10:50:29.6Z"
-                    }
-                }
-                postJSON('/api/report/', emptyReport, function (response) {
-                    assert.equal(response.statusCode, 200);
-                    assert.notEqual(JSON.parse(response.responseText)['status'], 'OK');
-                    assert(response.responseText.indexOf('141977') >= 0);
-                    assert(response.responseText.indexOf('150000') >= 0);
-                    assert.equal(JSON.parse(response.responseText)['failureStored'], true);
-                    assert.equal(JSON.parse(response.responseText)['processedRuns'], 0);
-                    notifyDone();
-                });
-            });
-        });
-    });
-
-    var reportWithTwoLevelsOfAggregations = [{
-        "buildNumber": "123",
-        "buildTime": "2013-02-28T10:12:03.388304",
-        "builderName": "someBuilder",
-        "builderPassword": "somePassword",
-        "platform": "Mountain Lion",
-        "tests": {
-            "DummyPageLoading": {
-                "metrics": {"Time": { "aggregators" : ["Arithmetic"], "current": [300, 310, 320, 330] }},
-                "tests": {
-                    "apple.com": {
-                        "metrics": {"Time": { "current": [500, 510, 520, 530] }},
-                        "url": "http://www.apple.com"
-                    },
-                    "webkit.org": {
-                        "metrics": {"Time": { "current": [100, 110, 120, 130] }},
-                        "url": "http://www.webkit.org"
-                    }
-                }
-            },
-            "DummyBenchmark": {
-                "metrics": {"Time": ["Arithmetic"]},
-                "tests": {
-                    "DOM": {
-                        "metrics": {"Time": ["Geometric", "Arithmetic"]},
-                        "tests": {
-                            "ModifyNodes": {"metrics": {"Time": { "current": [[11, 12, 13, 14, 15], [16, 17, 18, 19, 20], [21, 22, 23, 24, 25]] }}},
-                            "TraverseNodes": {"metrics": {"Time": { "current": [[31, 32, 33, 34, 35], [36, 37, 38, 39, 40], [41, 42, 43, 44, 45]] }}}
-                        }
-                    },
-                    "CSS": {"metrics": {"Time": { "current": [[101, 102, 103, 104, 105], [106, 107, 108, 109, 110], [111, 112, 113, 114, 115]] }}}
-                }
-            }
-        },
-        "revisions": {
-            "OS X": {
-                "revision": "10.8.2 12C60"
-            },
-            "WebKit": {
-                "revision": "141977",
-                "timestamp": "2013-02-06T08:55:20.9Z"
-            }
-        }}];
-
-    function addBuilderAndMeanAggregators(report, callback) {
-        addBuilder(report, function () {
-            queryAndFetchAll('INSERT INTO aggregators (aggregator_name, aggregator_definition) values ($1, $2)',
-                ['Arithmetic', 'values.reduce(function (a, b) { return a + b; }) / values.length'], function () {
-                queryAndFetchAll('INSERT INTO aggregators (aggregator_name, aggregator_definition) values ($1, $2)',
-                    ['Geometric', 'Math.pow(values.reduce(function (a, b) { return a * b; }), 1 / values.length)'], callback);
-            });
-        });
-    }
-
-    function fetchRunForMetric(testName, metricName,callback) {
-        queryAndFetchAll('SELECT * FROM test_runs WHERE run_config IN'
-            + '(SELECT config_id FROM test_configurations, test_metrics, tests WHERE config_metric = metric_id AND metric_test = test_id AND'
-            + 'test_name = $1 AND metric_name = $2)',
-            ['Arithmetic', 'values.reduce(function (a, b) { return a + b; }) / values.length'], function () {
-            queryAndFetchAll('INSERT INTO aggregators (aggregator_name, aggregator_definition) values ($1, $2)',
-                ['Geometric', 'Math.pow(values.reduce(function (a, b) { return a * b; }), 1 / values.length)'], callback);
-        });
-    }
-
-    it("should not reject when aggregators are missing", function () {
-        addBuilder(reportWithTwoLevelsOfAggregations, function () {
-            postJSON('/api/report/', reportWithTwoLevelsOfAggregations, function (response) {
-                assert.equal(response.statusCode, 200);
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                assert.equal(JSON.parse(response.responseText)['failureStored'], false);
-                notifyDone();
-            });
-        });
-    });
-
-    it("should add tests", function () {
-        addBuilderAndMeanAggregators(reportWithTwoLevelsOfAggregations, function () {
-            postJSON('/api/report/', reportWithTwoLevelsOfAggregations, function (response) {
-                assert.equal(response.statusCode, 200);
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                assert.equal(JSON.parse(response.responseText)['failureStored'], false);
-                queryAndFetchAll('SELECT * FROM tests', [], function (rows) {
-                    assert.deepEqual(rows.map(function (row) { return row['test_name']; }).sort(),
-                        ['CSS', 'DOM', 'DummyBenchmark', 'DummyPageLoading', 'ModifyNodes', 'TraverseNodes', 'apple.com', 'webkit.org']);
-                    emptyReport[0].tests = {};
-                    notifyDone();
-                });
-            });
-        });
-    });
-
-    it("should add metrics", function () {
-        addBuilderAndMeanAggregators(reportWithTwoLevelsOfAggregations, function () {
-            postJSON('/api/report/', reportWithTwoLevelsOfAggregations, function (response) {
-                queryAndFetchAll('SELECT * FROM tests, test_metrics LEFT JOIN aggregators ON metric_aggregator = aggregator_id WHERE metric_test = test_id',
-                    [], function (rows) {
-                    var testNameToMetrics = {};
-                    rows.forEach(function (row) {
-                        if (!(row['test_name'] in testNameToMetrics))
-                            testNameToMetrics[row['test_name']] = new Array;
-                        testNameToMetrics[row['test_name']].push([row['metric_name'], row['aggregator_name']]);
-                    });
-                    assert.deepEqual(testNameToMetrics['CSS'], [['Time', null]]);
-                    assert.deepEqual(testNameToMetrics['DOM'].sort(), [['Time', 'Arithmetic'], ['Time', 'Geometric']]);
-                    assert.deepEqual(testNameToMetrics['DummyBenchmark'], [['Time', 'Arithmetic']]);
-                    assert.deepEqual(testNameToMetrics['DummyPageLoading'], [['Time', 'Arithmetic']]);
-                    assert.deepEqual(testNameToMetrics['ModifyNodes'], [['Time', null]]);
-                    assert.deepEqual(testNameToMetrics['TraverseNodes'], [['Time', null]]);
-                    assert.deepEqual(testNameToMetrics['apple.com'], [['Time', null]]);
-                    assert.deepEqual(testNameToMetrics['webkit.org'], [['Time', null]]);
-                    notifyDone();
-                });
-            });
-        });
-    });
-
-    function fetchTestRunIterationsForMetric(testName, metricName, callback) {
-         queryAndFetchAll('SELECT * FROM tests, test_metrics, test_configurations, test_runs WHERE metric_test = test_id AND config_metric = metric_id'
-            + ' AND run_config = config_id AND test_name = $1 AND metric_name = $2', [testName, metricName], function (runRows) {
-                assert.equal(runRows.length, 1);
-                var run = runRows[0];
-                queryAndFetchAll('SELECT * FROM run_iterations WHERE iteration_run = $1 ORDER BY iteration_order', [run['run_id']], function (iterationRows) {
-                    callback(run, iterationRows);
-                });
-            });
-    }
-
-    it("should store run values", function () {
-        addBuilderAndMeanAggregators(reportWithTwoLevelsOfAggregations, function () {
-            postJSON('/api/report/', reportWithTwoLevelsOfAggregations, function (response) {
-                fetchTestRunIterationsForMetric('apple.com', 'Time', function (run, iterations) {
-                    var runId = run['run_id'];
-                    assert.deepEqual(iterations, [
-                        {iteration_run: runId, iteration_order: 0, iteration_group: null, iteration_value: 500, iteration_relative_time: null},
-                        {iteration_run: runId, iteration_order: 1, iteration_group: null, iteration_value: 510, iteration_relative_time: null},
-                        {iteration_run: runId, iteration_order: 2, iteration_group: null, iteration_value: 520, iteration_relative_time: null},
-                        {iteration_run: runId, iteration_order: 3, iteration_group: null, iteration_value: 530, iteration_relative_time: null}]);
-                    var sum = 500 + 510 + 520 + 530;
-                    assert.equal(run['run_mean_cache'], sum / iterations.length);
-                    assert.equal(run['run_sum_cache'], sum);
-                    assert.equal(run['run_square_sum_cache'], 500 * 500 + 510 * 510 + 520 * 520 + 530 * 530);
-
-                    fetchTestRunIterationsForMetric('CSS', 'Time', function (run, iterations) {
-                        var runId = run['run_id'];
-                        assert.deepEqual(iterations, [
-                            {iteration_run: runId, iteration_order: 0, iteration_group: 0, iteration_value: 101, iteration_relative_time: null},
-                            {iteration_run: runId, iteration_order: 1, iteration_group: 0, iteration_value: 102, iteration_relative_time: null},
-                            {iteration_run: runId, iteration_order: 2, iteration_group: 0, iteration_value: 103, iteration_relative_time: null},
-                            {iteration_run: runId, iteration_order: 3, iteration_group: 0, iteration_value: 104, iteration_relative_time: null},
-                            {iteration_run: runId, iteration_order: 4, iteration_group: 0, iteration_value: 105, iteration_relative_time: null},
-                            {iteration_run: runId, iteration_order: 5, iteration_group: 1, iteration_value: 106, iteration_relative_time: null},
-                            {iteration_run: runId, iteration_order: 6, iteration_group: 1, iteration_value: 107, iteration_relative_time: null},
-                            {iteration_run: runId, iteration_order: 7, iteration_group: 1, iteration_value: 108, iteration_relative_time: null},
-                            {iteration_run: runId, iteration_order: 8, iteration_group: 1, iteration_value: 109, iteration_relative_time: null},
-                            {iteration_run: runId, iteration_order: 9, iteration_group: 1, iteration_value: 110, iteration_relative_time: null},
-                            {iteration_run: runId, iteration_order: 10, iteration_group: 2, iteration_value: 111, iteration_relative_time: null},
-                            {iteration_run: runId, iteration_order: 11, iteration_group: 2, iteration_value: 112, iteration_relative_time: null},
-                            {iteration_run: runId, iteration_order: 12, iteration_group: 2, iteration_value: 113, iteration_relative_time: null},
-                            {iteration_run: runId, iteration_order: 13, iteration_group: 2, iteration_value: 114, iteration_relative_time: null},
-                            {iteration_run: runId, iteration_order: 14, iteration_group: 2, iteration_value: 115, iteration_relative_time: null}]);
-                        var sum = 0;
-                        var squareSum = 0;
-                        for (var value = 101; value <= 115; ++value) {
-                            sum += value;
-                            squareSum += value * value;
-                        }
-                        assert.equal(run['run_mean_cache'], sum / iterations.length);
-                        assert.equal(run['run_sum_cache'], sum);
-                        assert.equal(run['run_square_sum_cache'], squareSum);
-                        notifyDone();
-                    });
-                });
-            });
-        });
-    });
-
-    it("should store aggregated run values", function () {
-        addBuilderAndMeanAggregators(reportWithTwoLevelsOfAggregations, function () {
-            postJSON('/api/report/', reportWithTwoLevelsOfAggregations, function (response) {
-                fetchTestRunIterationsForMetric('DummyPageLoading', 'Time', function (run, iterations) {
-                    var runId = run['run_id'];
-                    var expectedValues = [(500 + 100) / 2, (510 + 110) / 2, (520 + 120) / 2, (530 + 130) / 2];
-                    assert.deepEqual(iterations, [
-                        {iteration_run: runId, iteration_order: 0, iteration_group: null, iteration_value: expectedValues[0], iteration_relative_time: null},
-                        {iteration_run: runId, iteration_order: 1, iteration_group: null, iteration_value: expectedValues[1], iteration_relative_time: null},
-                        {iteration_run: runId, iteration_order: 2, iteration_group: null, iteration_value: expectedValues[2], iteration_relative_time: null},
-                        {iteration_run: runId, iteration_order: 3, iteration_group: null, iteration_value: expectedValues[3], iteration_relative_time: null}]);
-                    var sum = expectedValues.reduce(function (sum, value) { return sum + value; }, 0);
-                    assert.equal(run['run_mean_cache'], sum / iterations.length);
-                    assert.equal(run['run_sum_cache'], sum);
-                    assert.equal(run['run_square_sum_cache'], expectedValues.reduce(function (sum, value) { return sum + value * value; }, 0));
-                    notifyDone();
-                });
-            });
-        });
-    });
-
-    it("should be able to compute the aggregation of aggregated values", function () {
-        addBuilderAndMeanAggregators(reportWithTwoLevelsOfAggregations, function () {
-            postJSON('/api/report/', reportWithTwoLevelsOfAggregations, function (response) {
-                fetchTestRunIterationsForMetric('DummyBenchmark', 'Time', function (run, iterations) {
-                    var expectedIterations = [];
-                    var sum = 0;
-                    var squareSum = 0;
-                    for (var i = 0; i < 15; ++i) {
-                        var value = i + 1;
-                        var DOMMean = ((10 + value) + (30 + value)) / 2;
-                        var expectedValue = (DOMMean + 100 + value) / 2;
-                        sum += expectedValue;
-                        squareSum += expectedValue * expectedValue;
-                        expectedIterations.push({iteration_run: run['run_id'],
-                            iteration_order: i,
-                            iteration_group: Math.floor(i / 5),
-                            iteration_value: expectedValue,
-                            iteration_relative_time: null});
-                    }
-                    assert.deepEqual(iterations, expectedIterations);
-                    assert.equal(run['run_mean_cache'], sum / iterations.length);
-                    assert.equal(run['run_sum_cache'], sum);
-                    assert.equal(run['run_square_sum_cache'], squareSum);
-                    notifyDone();
-                });
-            });
-        });
-    });
-
-    var reportWithSameSubtestName = [{
-        "buildNumber": "123",
-        "buildTime": "2013-02-28T10:12:03.388304",
-        "builderName": "someBuilder",
-        "builderPassword": "somePassword",
-        "platform": "Mountain Lion",
-        "tests": {
-            "Suite1": {
-                "tests": {
-                    "test1": {
-                        "metrics": {"Time": { "current": [1, 2, 3, 4, 5] }}
-                    },
-                    "test2": {
-                        "metrics": {"Time": { "current": [6, 7, 8, 9, 10] }}
-                    }
-                }
-            },
-            "Suite2": {
-                "tests": {
-                    "test1": {
-                        "metrics": {"Time": { "current": [11, 12, 13, 14, 15] }}
-                    },
-                    "test2": {
-                        "metrics": {"Time": { "current": [16, 17, 18, 19, 20] }}
-                    }
-                }
-            }
-        }}];
-
-    it("should be able to add a report with same subtest name", function () {
-        addBuilderAndMeanAggregators(reportWithSameSubtestName, function () {
-            postJSON('/api/report/', reportWithSameSubtestName, function (response) {
-                assert.equal(response.statusCode, 200);
-                try {
-                    JSON.parse(response.responseText);
-                } catch (error) {
-                    assert.fail(error, null, response.responseText);
-                }
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                assert.equal(JSON.parse(response.responseText)['failureStored'], false);
-                notifyDone();
-            });
-        });
-    });
-
-    it("should be able to reuse the same test rows", function () {
-        addBuilderAndMeanAggregators(reportWithSameSubtestName, function () {
-            postJSON('/api/report/', reportWithSameSubtestName, function (response) {
-                assert.equal(response.statusCode, 200);
-                queryAndFetchAll('SELECT * FROM tests', [], function (testRows) {
-                   assert.equal(testRows.length, 6);
-                   reportWithSameSubtestName.buildNumber = "125";
-                   reportWithSameSubtestName.buildTime = "2013-02-28T12:17:24.1";
-
-                   postJSON('/api/report/', reportWithSameSubtestName, function (response) {
-                       assert.equal(response.statusCode, 200);
-                       queryAndFetchAll('SELECT * FROM tests', [], function (testRows) {
-                              assert.equal(testRows.length, 6);
-                              notifyDone();
-                          });
-                      });
-                });
-            });
-        });
-    });
-
-    var reportWithSameSingleValue = [{
-        "buildNumber": "123",
-        "buildTime": "2013-02-28T10:12:03.388304",
-        "builderName": "someBuilder",
-        "builderPassword": "somePassword",
-        "platform": "Mountain Lion",
-        "tests": {
-            "suite": {
-                "metrics": {"Combined": ["Arithmetic"]},
-                "tests": {
-                    "test1": {
-                        "metrics": {"Combined": { "current": 3 }}
-                    },
-                    "test2": {
-                        "metrics": {"Combined": { "current": 7 }}
-                    }
-                }
-            },
-        }}];
-
-    it("should be able to add a report with single value results", function () {
-        addBuilderAndMeanAggregators(reportWithSameSingleValue, function () {
-            postJSON('/api/report/', reportWithSameSingleValue, function (response) {
-                assert.equal(response.statusCode, 200);
-                try {
-                    JSON.parse(response.responseText);
-                } catch (error) {
-                    assert.fail(error, null, response.responseText);
-                }
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                assert.equal(JSON.parse(response.responseText)['failureStored'], false);
-                fetchTestRunIterationsForMetric('test1', 'Combined', function (run, iterations) {
-                    assert.equal(run['run_iteration_count_cache'], 1);
-                    assert.equal(run['run_mean_cache'], 3);
-                    assert.equal(run['run_sum_cache'], 3);
-                    assert.equal(run['run_square_sum_cache'], 9);
-                    fetchTestRunIterationsForMetric('suite', 'Combined', function (run, iterations) {
-                        assert.equal(run['run_iteration_count_cache'], 1);
-                        assert.equal(run['run_mean_cache'], 5);
-                        assert.equal(run['run_sum_cache'], 5);
-                        assert.equal(run['run_square_sum_cache'], 25);
-                        notifyDone();
-                    });
-                });
-            });
-        });
-    });
-
-    var reportWithSameValuePairs = [{
-        "buildNumber": "123",
-        "buildTime": "2013-02-28T10:12:03.388304",
-        "builderName": "someBuilder",
-        "builderPassword": "somePassword",
-        "platform": "Mountain Lion",
-        "tests": {
-                "test": {
-                    "metrics": {"FrameRate": { "current": [[[0, 4], [100, 5], [205, 3]]] }}
-                },
-            },
-        }];
-
-    it("should be able to add a report with (relative time, value) pairs", function () {
-        addBuilderAndMeanAggregators(reportWithSameValuePairs, function () {
-            postJSON('/api/report/', reportWithSameValuePairs, function (response) {
-                assert.equal(response.statusCode, 200);
-                try {
-                    JSON.parse(response.responseText);
-                } catch (error) {
-                    assert.fail(error, null, response.responseText);
-                }
-                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
-                assert.equal(JSON.parse(response.responseText)['failureStored'], false);
-                fetchTestRunIterationsForMetric('test', 'FrameRate', function (run, iterations) {
-                    assert.equal(run['run_iteration_count_cache'], 3);
-                    assert.equal(run['run_mean_cache'], 4);
-                    assert.equal(run['run_sum_cache'], 12);
-                    assert.equal(run['run_square_sum_cache'], 16 + 25 + 9);
-                    var runId = run['run_id'];
-                    assert.deepEqual(iterations, [
-                        {iteration_run: runId, iteration_order: 0, iteration_group: null, iteration_value: 4, iteration_relative_time: 0},
-                        {iteration_run: runId, iteration_order: 1, iteration_group: null, iteration_value: 5, iteration_relative_time: 100},
-                        {iteration_run: runId, iteration_order: 2, iteration_group: null, iteration_value: 3, iteration_relative_time: 205}]);
-                    notifyDone();
-                });
-            });
-        });
-    });
-
-    var reportsUpdatingDifferentTests = [
-        [{
-            "buildNumber": "123",
-            "buildTime": "2013-02-28T10:12:03",
-            "builderName": "someBuilder",
-            "builderPassword": "somePassword",
-            "platform": "Mountain Lion",
-            "tests": {"test1": {"metrics": {"Time": {"current": 3}}}}
-        }],
-        [{
-            "buildNumber": "124",
-            "buildTime": "2013-02-28T11:31:21",
-            "builderName": "someBuilder",
-            "builderPassword": "somePassword",
-            "platform": "Mountain Lion",
-            "tests": {"test2": {"metrics": {"Time": {"current": 3}}}}
-        }],
-        [{
-            "buildNumber": "125",
-            "buildTime": "2013-02-28T12:45:34",
-            "builderName": "someBuilder",
-            "builderPassword": "somePassword",
-            "platform": "Mountain Lion",
-            "tests": {"test1": {"metrics": {"Time": {"current": 3}}}}
-        }],
-    ];
-
-    function fetchTestConfig(testName, metricName, callback) {
-         queryAndFetchAll('SELECT * FROM tests, test_metrics, test_configurations WHERE test_id = metric_test AND metric_id = config_metric'
-            + ' AND test_name = $1 AND metric_name = $2', [testName, metricName], function (runRows) {
-                assert.equal(runRows.length, 1);
-                callback(runRows[0]);
-            });
-    }
-
-    it("should update the last modified date of test configurations with new runs", function () {
-        addBuilder(reportsUpdatingDifferentTests[0], function () {
-            postJSON('/api/report/', reportsUpdatingDifferentTests[0], function (response) {
-                assert.equal(response.statusCode, 200);
-                fetchTestConfig('test1', 'Time', function (originalConfig) {
-                    postJSON('/api/report/', reportsUpdatingDifferentTests[2], function (response) {
-                        assert.equal(response.statusCode, 200);
-                        fetchTestConfig('test1', 'Time', function (config) {
-                            assert.notEqual(+originalConfig['config_runs_last_modified'], +config['config_runs_last_modified']);
-                            notifyDone();
-                        });
-                    });
-                });
-            });
-        });
-    });
-
-    it("should update the last modified date of unrelated test configurations", function () {
-        addBuilder(reportsUpdatingDifferentTests[0], function () {
-            postJSON('/api/report/', reportsUpdatingDifferentTests[0], function (response) {
-                assert.equal(response.statusCode, 200);
-                fetchTestConfig('test1', 'Time', function (originalConfig) {
-                    postJSON('/api/report/', reportsUpdatingDifferentTests[1], function (response) {
-                        assert.equal(response.statusCode, 200);
-                        fetchTestConfig('test1', 'Time', function (config) {
-                            assert.equal(+originalConfig['config_runs_last_modified'], +config['config_runs_last_modified']);
-                            notifyDone();
-                        });
-                    });
-                });
-            });
-        });
-    });
-});
index 6074cc4..5590552 100644 (file)
@@ -1,7 +1,7 @@
 "use strict";
 
-var pg = require('pg');
-var config = require('./config.js');
+const pg = require('pg');
+const config = require('./config.js');
 
 class Database {
     constructor(databaseName)
@@ -23,11 +23,8 @@ class Database {
         let connectionString = `tcp://${username}:${password}@${host}:${port}/${this._databaseName}`;
 
         let client = new pg.Client(connectionString);
-        if (!options || !options.keepAlive) {
-            client.on('drain', function () {
-                client.end();
-            });
-        }
+        if (!options || !options.keepAlive)
+            client.on('drain', this.disconnect.bind(this));
 
         this._client = client;
 
@@ -62,6 +59,56 @@ class Database {
         });
     }
 
+    selectAll(table, columnToSortBy)
+    {
+        return this.selectRows(table, {}, {sortBy: columnToSortBy});
+    }
+
+    selectFirstRow(table, params, columnToSortBy)
+    {
+        return this.selectRows(table, params, {sortBy: columnToSortBy, limit: 1}).then(function (rows) {
+            return rows[0];
+        });
+    }
+
+    selectRows(table, params, options)
+    {
+        let prefix = '';
+        if (table in tableToPrefixMap)
+            prefix = tableToPrefixMap[table] + '_';
+
+        options = options || {};
+
+        let columnNames = [];
+        let placeholders = [];
+        let values = [];
+        for (let name in params) {
+            columnNames.push(prefix + name);
+            placeholders.push('$' + columnNames.length);
+            values.push(params[name]);
+        }
+
+        let qualifier = '';
+        if (columnNames.length)
+            qualifier += ` WHERE (${columnNames.join(',')}) = (${placeholders.join(',')})`;
+        qualifier += ` ORDER BY ${prefix}${options.sortBy || 'id'}`;
+        if (options && options.limit)
+            qualifier += ` LIMIT ${options.limit}`;
+
+        return this.query(`SELECT * FROM ${table}${qualifier}`, values).then(function (result) {
+            return result.rows.map(function (row) {
+                let formattedResult = {};
+                for (let columnName in row) {
+                    let formattedName = columnName;
+                    if (formattedName.startsWith(prefix))
+                        formattedName = formattedName.substring(prefix.length);
+                    formattedResult[formattedName] = row[columnName];
+                }
+                return formattedResult;
+            });
+        });
+    }
+
     insert(table, parameters)
     {
         let columnNames = [];
@@ -78,7 +125,7 @@ class Database {
     }
 }
 
-let tableToPrefixMap = {
+const tableToPrefixMap = {
     'aggregators': 'aggregator',
     'analysis_tasks': 'task',
     'analysis_test_groups': 'testgroup',
@@ -86,16 +133,21 @@ let tableToPrefixMap = {
     'build_triggerables': 'triggerable',
     'build_requests': 'request',
     'build_slaves': 'slave',
+    'builds': 'build',
     'builders': 'builder',
     'commits': 'commit',
+    'committers': 'committer',
     'test_configurations': 'config',
     'test_metrics': 'metric',
+    'test_runs': 'run',
     'tests': 'test',
     'tracker_repositories': 'tracrepo',
     'platforms': 'platform',
+    'reports': 'report',
     'repositories': 'repository',
     'root_sets': 'rootset',
     'roots': 'root',
+    'run_iterations': 'iteration',
 }
 
 if (typeof module != 'undefined')