Add mocha server tests for /api/build-requests
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 24 Mar 2016 20:17:01 +0000 (20:17 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 24 Mar 2016 20:17:01 +0000 (20:17 +0000)
https://bugs.webkit.org/show_bug.cgi?id=155831

Reviewed by Chris Dumez.

Added the new mocha.js based server-tests for /api/build-requests. The new harness automatically:
 - starts a new Apache instance
 - switches the database during testing via setting an environmental variable
 - backups and restores public/data directory during testing

As a result, developer no longer has to manually setup Apache, edit config.json manually to use
a testing database, or run /api/manifest.php to re-generate the manifest file after testing.

This patch also makes ID resolution optional on /api/build-requests so that v3 model based syncing
scripts can re-use the same code as the v3 UI to process the JSON. tools/sync-with-buildbot.py has
been modified to use this option (useLegacyIdResolution).

* config.json: Added configurations for the test httpd server.
* init-database.sql: Don't error when tables and types don't exist (when database is empty).
* public/api/build-requests.php:
(main): Made the ID resolution optional with useLegacyIdResolution. Also removed "updates" from the
results JSON since it's never used.
* public/include/build-requests-fetcher.php:
(BuildRequestsFetcher::__construct):
(BuildRequestsFetcher::fetch_roots_for_set_if_needed): Fixed the bug that we would include the same
commit multiple times for each root set.
* public/include/db.php:
(config): If present, use ORG_WEBKIT_PERF_CONFIG_PATH instead of Websites/perf.webkit.org/config.json.
* server-tests: Added.
* server-tests/api-build-requests-tests.js: Added. Tests for /api/build-requests.
(.addMockData):
* server-tests/resources: Added.
* server-tests/resources/test-server.conf: Added. Apache configuration file for testing.
* server-tests/resources/test-server.js: Added.
(TestSever): Added.
(TestSever.prototype.start): Added.
(TestSever.prototype.stop): Added.
(TestSever.prototype.remoteAPI): Added. Configures RemoteAPI to be used with the test sever.
(TestSever.prototype.database): Added. Returns Database configured to use the test database.
(TestSever.prototype._constructTestConfig): Creates config.json for testing. The file is generated by
_start and db.php's config() reads it from the environmental variable: ORG_WEBKIT_PERF_CONFIG_PATH.
(TestSever.prototype._ensureDataDirectory): Renames public/data to public/original-data if exists,
and creates a new empty public/data.
(TestSever.prototype._restoreDataDirectory): Deletes public/data and renames public/original-data
back to public/data.
(TestSever.prototype._ensureTestDatabase): Drops the test database if exists and creates a new one.
(TestSever.prototype.initDatabase): Run init-database.sql to start each test with a consistent state.
(TestSever.prototype._executePgsqlCommand): Executes a postgres command line tool such as psql.
(TestSever.prototype._determinePgsqlDirectory): Finds the directory that contains psql.
(TestSever.prototype._startApache): Starts an Apache instance for testing.
(TestSever.prototype._stopApache): Stops the Apache instance for testing.
(TestSever.prototype._waitForPid): Waits for the Apache pid file to appear or disappear.
(before): Start the test server at the beginning.
(beforeEach): Re-initialize all tables before each test.
(after): Stop the test server at the end.
* tools/js/config.js:
(Config.prototype.path):
(Config.prototype.serverRoot): Added. The path to Websites/perf.webkit.org/public/.
(Config.prototype.pathFromRoot): Added. Resolves a path from Websites/perf.webkit.org.
* tools/js/database.js:
(Database): Now optionally takes the database name to use a different database during testing.
(Database.prototype.connect):
(Database.prototype.query): Added.
(Database.prototype.insert): Added.
(tableToPrefixMap): Maps table name to its prefix. Used by Database.insert.
* tools/js/remote.js: Added.
(RemoteAPI): Added. This is node.js equivalent of RemoteAPI in public/v3/remote.js.
(RemoteAPI.prototype.configure): Added.
(RemoteAPI.prototype.fetchJSON): Added.
(RemoteAPI.prototype.fetchJSONWithStatus): Added.
(RemoteAPI.prototype.sendHttpRequest): Added.
* tools/sync-with-buildbot.py:
(main): Use useLegacyIdResolution as this script relies on the legacy behavior.
* unit-tests/checkconfig.js: pg was never directly used in this test.

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

14 files changed:
Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/config.json
Websites/perf.webkit.org/init-database.sql
Websites/perf.webkit.org/public/api/build-requests.php
Websites/perf.webkit.org/public/include/build-requests-fetcher.php
Websites/perf.webkit.org/public/include/db.php
Websites/perf.webkit.org/server-tests/api-build-requests-tests.js [new file with mode: 0644]
Websites/perf.webkit.org/server-tests/resources/test-server.conf [new file with mode: 0644]
Websites/perf.webkit.org/server-tests/resources/test-server.js [new file with mode: 0644]
Websites/perf.webkit.org/tools/js/config.js
Websites/perf.webkit.org/tools/js/database.js
Websites/perf.webkit.org/tools/js/remote.js [new file with mode: 0644]
Websites/perf.webkit.org/tools/sync-with-buildbot.py
Websites/perf.webkit.org/unit-tests/checkconfig.js

index 5c2f2d5..50740a0 100644 (file)
@@ -1,5 +1,82 @@
 2016-03-23  Ryosuke Niwa  <rniwa@webkit.org>
 
+        Add mocha server tests for /api/build-requests
+        https://bugs.webkit.org/show_bug.cgi?id=155831
+
+        Reviewed by Chris Dumez.
+
+        Added the new mocha.js based server-tests for /api/build-requests. The new harness automatically:
+         - starts a new Apache instance
+         - switches the database during testing via setting an environmental variable 
+         - backups and restores public/data directory during testing
+
+        As a result, developer no longer has to manually setup Apache, edit config.json manually to use
+        a testing database, or run /api/manifest.php to re-generate the manifest file after testing.
+
+        This patch also makes ID resolution optional on /api/build-requests so that v3 model based syncing
+        scripts can re-use the same code as the v3 UI to process the JSON. tools/sync-with-buildbot.py has
+        been modified to use this option (useLegacyIdResolution).
+
+        * config.json: Added configurations for the test httpd server.
+        * init-database.sql: Don't error when tables and types don't exist (when database is empty).
+        * public/api/build-requests.php:
+        (main): Made the ID resolution optional with useLegacyIdResolution. Also removed "updates" from the
+        results JSON since it's never used.
+        * public/include/build-requests-fetcher.php:
+        (BuildRequestsFetcher::__construct):
+        (BuildRequestsFetcher::fetch_roots_for_set_if_needed): Fixed the bug that we would include the same
+        commit multiple times for each root set.
+        * public/include/db.php:
+        (config): If present, use ORG_WEBKIT_PERF_CONFIG_PATH instead of Websites/perf.webkit.org/config.json.
+        * server-tests: Added.
+        * server-tests/api-build-requests-tests.js: Added. Tests for /api/build-requests.
+        (.addMockData):
+        * server-tests/resources: Added.
+        * server-tests/resources/test-server.conf: Added. Apache configuration file for testing.
+        * server-tests/resources/test-server.js: Added.
+        (TestSever): Added.
+        (TestSever.prototype.start): Added.
+        (TestSever.prototype.stop): Added.
+        (TestSever.prototype.remoteAPI): Added. Configures RemoteAPI to be used with the test sever.
+        (TestSever.prototype.database): Added. Returns Database configured to use the test database.
+        (TestSever.prototype._constructTestConfig): Creates config.json for testing. The file is generated by
+        _start and db.php's config() reads it from the environmental variable: ORG_WEBKIT_PERF_CONFIG_PATH.
+        (TestSever.prototype._ensureDataDirectory): Renames public/data to public/original-data if exists,
+        and creates a new empty public/data.
+        (TestSever.prototype._restoreDataDirectory): Deletes public/data and renames public/original-data
+        back to public/data.
+        (TestSever.prototype._ensureTestDatabase): Drops the test database if exists and creates a new one.
+        (TestSever.prototype.initDatabase): Run init-database.sql to start each test with a consistent state.
+        (TestSever.prototype._executePgsqlCommand): Executes a postgres command line tool such as psql.
+        (TestSever.prototype._determinePgsqlDirectory): Finds the directory that contains psql.
+        (TestSever.prototype._startApache): Starts an Apache instance for testing.
+        (TestSever.prototype._stopApache): Stops the Apache instance for testing.
+        (TestSever.prototype._waitForPid): Waits for the Apache pid file to appear or disappear.
+        (before): Start the test server at the beginning.
+        (beforeEach): Re-initialize all tables before each test.
+        (after): Stop the test server at the end.
+        * tools/js/config.js:
+        (Config.prototype.path):
+        (Config.prototype.serverRoot): Added. The path to Websites/perf.webkit.org/public/.
+        (Config.prototype.pathFromRoot): Added. Resolves a path from Websites/perf.webkit.org.
+        * tools/js/database.js:
+        (Database): Now optionally takes the database name to use a different database during testing.
+        (Database.prototype.connect):
+        (Database.prototype.query): Added.
+        (Database.prototype.insert): Added.
+        (tableToPrefixMap): Maps table name to its prefix. Used by Database.insert.
+        * tools/js/remote.js: Added.
+        (RemoteAPI): Added. This is node.js equivalent of RemoteAPI in public/v3/remote.js.
+        (RemoteAPI.prototype.configure): Added.
+        (RemoteAPI.prototype.fetchJSON): Added.
+        (RemoteAPI.prototype.fetchJSONWithStatus): Added.
+        (RemoteAPI.prototype.sendHttpRequest): Added.
+        * tools/sync-with-buildbot.py:
+        (main): Use useLegacyIdResolution as this script relies on the legacy behavior.
+        * unit-tests/checkconfig.js: pg was never directly used in this test.
+
+2016-03-23  Ryosuke Niwa  <rniwa@webkit.org>
+
         Delete a file that was supposed to be removed in r198614 for real.
 
         * unit-tests/resources/v3-models.js: Removed.
index 811c0c3..bd35457 100644 (file)
     "maintenanceMode": false,
     "maintenanceDirectory": "reported/",
     "testServer": {
-        "hostname": "localhost",
-        "port": 80
+        "port": 8180,
+        "config": "/tmp/org.webkit.perf.test-config.json",
+        "httpdConfig": "server-tests/resources/test-server.conf",
+        "httpdPID": "/tmp/org.webkit.perf.test-httpd.pid",
+        "httpdErrorLog": "server-tests/resources/test-server.log",
+        "httpdMutexDir": "/tmp/org.webkit.perf.tests/"
     },
+    "testDatabaseName": "test-db",
     "clusterStart": [2000, 1, 1, 0, 0],
     "clusterSize": [0, 2, 0],
     "cacheDirectory": "public/data/remote-cache/",
index 28b51c9..e987493 100644 (file)
@@ -1,34 +1,34 @@
-DROP TABLE run_iterations CASCADE;
-DROP TABLE test_runs CASCADE;
-DROP TABLE test_configurations CASCADE;
-DROP TYPE test_configuration_type CASCADE;
-DROP TABLE aggregators CASCADE;
-DROP TABLE builds CASCADE;
-DROP TABLE committers CASCADE;
-DROP TABLE commits CASCADE;
-DROP TABLE build_commits CASCADE;
-DROP TABLE build_slaves CASCADE;
-DROP TABLE builders CASCADE;
-DROP TABLE repositories CASCADE;
-DROP TABLE platforms CASCADE;
-DROP TABLE test_metrics CASCADE;
-DROP TABLE tests CASCADE;
-DROP TABLE reports CASCADE;
-DROP TABLE tracker_repositories CASCADE;
-DROP TABLE bug_trackers CASCADE;
-DROP TABLE task_commits CASCADE;
-DROP TABLE analysis_tasks CASCADE;
-DROP TABLE analysis_strategies CASCADE;
-DROP TYPE analysis_task_result_type CASCADE;
-DROP TABLE build_triggerables CASCADE;
-DROP TABLE triggerable_configurations CASCADE;
-DROP TABLE triggerable_repositories CASCADE;
-DROP TABLE bugs CASCADE;
-DROP TABLE analysis_test_groups CASCADE;
-DROP TABLE root_sets CASCADE;
-DROP TABLE roots CASCADE;
-DROP TABLE build_requests CASCADE;
-DROP TYPE build_request_status_type CASCADE;
+DROP TABLE IF EXISTS run_iterations CASCADE;
+DROP TABLE IF EXISTS test_runs CASCADE;
+DROP TABLE IF EXISTS test_configurations CASCADE;
+DROP TYPE IF EXISTS test_configuration_type CASCADE;
+DROP TABLE IF EXISTS aggregators CASCADE;
+DROP TABLE IF EXISTS builds CASCADE;
+DROP TABLE IF EXISTS committers CASCADE;
+DROP TABLE IF EXISTS commits CASCADE;
+DROP TABLE IF EXISTS build_commits CASCADE;
+DROP TABLE IF EXISTS build_slaves CASCADE;
+DROP TABLE IF EXISTS builders CASCADE;
+DROP TABLE IF EXISTS repositories CASCADE;
+DROP TABLE IF EXISTS platforms CASCADE;
+DROP TABLE IF EXISTS test_metrics CASCADE;
+DROP TABLE IF EXISTS tests CASCADE;
+DROP TABLE IF EXISTS reports CASCADE;
+DROP TABLE IF EXISTS tracker_repositories CASCADE;
+DROP TABLE IF EXISTS bug_trackers CASCADE;
+DROP TABLE IF EXISTS task_commits CASCADE;
+DROP TABLE IF EXISTS analysis_tasks CASCADE;
+DROP TABLE IF EXISTS analysis_strategies CASCADE;
+DROP TYPE IF EXISTS analysis_task_result_type CASCADE;
+DROP TABLE IF EXISTS build_triggerables CASCADE;
+DROP TABLE IF EXISTS triggerable_configurations CASCADE;
+DROP TABLE IF EXISTS triggerable_repositories CASCADE;
+DROP TABLE IF EXISTS bugs CASCADE;
+DROP TABLE IF EXISTS analysis_test_groups CASCADE;
+DROP TABLE IF EXISTS root_sets CASCADE;
+DROP TABLE IF EXISTS roots CASCADE;
+DROP TABLE IF EXISTS build_requests CASCADE;
+DROP TYPE IF EXISTS build_request_status_type CASCADE;
 
 
 CREATE TABLE platforms (
index 4389ef1..2fdda52 100644 (file)
@@ -30,11 +30,11 @@ function main($id, $path, $post_data) {
         $requests_fetcher->fetch_incomplete_requests_for_triggerable($triggerable['triggerable_id']);
     }
 
+    $resolve_id = array_get($_GET, 'useLegacyIdResolution');
     exit_with_success(array(
-        'buildRequests' => $requests_fetcher->results_with_resolved_ids(),
+        'buildRequests' => $resolve_id ? $requests_fetcher->results_with_resolved_ids() : $requests_fetcher->results(),
         'rootSets' => $requests_fetcher->root_sets(),
         'roots' => $requests_fetcher->roots(),
-        'updates' => $updates,
     ));
 }
 
index 52904c1..918bd8f 100644 (file)
@@ -7,6 +7,7 @@ class BuildRequestsFetcher {
         $this->db = $db;
         $this->rows = null;
         $this->root_sets = array();
+        $this->roots_by_id = array();
         $this->roots = array();
         $this->root_sets_by_id = array();
     }
@@ -96,11 +97,18 @@ class BuildRequestsFetcher {
             $revision = $row['commit_revision'];
             $commit_time = $row['commit_time'];
             array_push($root_ids, $row['commit_id']);
+
+            $root_id = $row['commit_id'];
+            if (array_key_exists($root_id, $this->roots_by_id))
+                continue;
+
             array_push($this->roots, array(
-                'id' => $row['commit_id'],
+                'id' => $root_id,
                 'repository' => $repository_id,
                 'revision' => $revision,
                 'time' => Database::to_js_time($commit_time)));
+
+            $this->roots_by_id[$root_id] = TRUE;
         }
 
         $this->root_sets_by_id[$root_set_id] = TRUE;
index 2dac8c4..60fc2b0 100644 (file)
@@ -27,12 +27,16 @@ function array_set_default(&$array, $key, $default) {
 
 $_config = NULL;
 
-define('CONFIG_DIR', dirname(__FILE__) . '/../../');
+define('CONFIG_DIR', realpath(dirname(__FILE__) . '/../../'));
 
 function config($key, $default = NULL) {
     global $_config;
-    if (!$_config)
-        $_config = json_decode(file_get_contents(CONFIG_DIR . 'config.json'), true);
+    if (!$_config) {
+        $file_path = getenv('ORG_WEBKIT_PERF_CONFIG_PATH');
+        if (!$file_path)
+            $file_path = CONFIG_DIR . 'config.json';
+        $_config = json_decode(file_get_contents($file_path), true);
+    }
     return array_get($_config, $key, $default);
 }
 
diff --git a/Websites/perf.webkit.org/server-tests/api-build-requests-tests.js b/Websites/perf.webkit.org/server-tests/api-build-requests-tests.js
new file mode 100644 (file)
index 0000000..9349f27
--- /dev/null
@@ -0,0 +1,174 @@
+'use strict';
+
+let assert = require('assert');
+let TestServer = require('./resources/test-server.js');
+
+describe('/api/build-requests', function () {
+    this.timeout(10000);
+
+    it('should return "TriggerableNotFound" when the database is empty', function (done) {
+        TestServer.remoteAPI().fetchJSON('/api/build-requests/build-webkit').then(function (content) {
+            assert.equal(content['status'], 'TriggerableNotFound');
+            done();
+        }).catch(done);
+    });
+
+    it('should return an empty list when there are no build requests', function (done) {
+        TestServer.database().connect().then(function () {
+            return TestServer.database().insert('build_triggerables', {name: 'build-webkit'});
+        }).then(function () {
+            return TestServer.remoteAPI().fetchJSON('/api/build-requests/build-webkit');
+        }).then(function (content) {
+            assert.equal(content['status'], 'OK');
+            assert.deepEqual(content['buildRequests'], []);
+            assert.deepEqual(content['rootSets'], []);
+            assert.deepEqual(content['roots'], []);
+            assert.deepEqual(Object.keys(content).sort(), ['buildRequests', 'rootSets', 'roots', 'status']);
+            done();
+        }).catch(done);
+    });
+
+    function addMockData(db)
+    {
+        return Promise.all([
+            db.insert('build_triggerables', {id: 1, name: 'build-webkit'}),
+            db.insert('repositories', {id: 9, name: 'OS X'}),
+            db.insert('repositories', {id: 11, name: 'WebKit'}),
+            db.insert('commits', {id: 87832, repository: 9, revision: '10.11 15A284'}),
+            db.insert('commits', {id: 93116, repository: 11, revision: '191622', time: new Date(1445945816878)}),
+            db.insert('commits', {id: 96336, repository: 11, revision: '192736', time: new Date(1448225325650)}),
+            db.insert('platforms', {id: 65, name: 'some platform'}),
+            db.insert('tests', {id: 200, name: 'some test'}),
+            db.insert('test_metrics', {id: 300, test: 200, name: 'some metric'}),
+            db.insert('root_sets', {id: 401}),
+            db.insert('roots', {set: 401, commit: 87832}),
+            db.insert('roots', {set: 401, commit: 93116}),
+            db.insert('root_sets', {id: 402}),
+            db.insert('roots', {set: 402, commit: 87832}),
+            db.insert('roots', {set: 402, commit: 96336}),
+            db.insert('analysis_tasks', {id: 500, platform: 65, metric: 300, name: 'some task'}),
+            db.insert('analysis_test_groups', {id: 600, task: 500, name: 'some test group'}),
+            db.insert('build_requests', {id: 700, triggerable: 1, platform: 65, test: 200, group: 600, order: 0, root_set: 401}),
+            db.insert('build_requests', {id: 701, triggerable: 1, platform: 65, test: 200, group: 600, order: 1, root_set: 402}),
+            db.insert('build_requests', {id: 702, triggerable: 1, platform: 65, test: 200, group: 600, order: 2, root_set: 401}),
+            db.insert('build_requests', {id: 703, triggerable: 1, platform: 65, test: 200, group: 600, order: 3, root_set: 402}),
+        ]);
+    }
+
+    it('should return build requets associated with a given triggerable with appropriate roots and rootSets', function (done) {
+        let db = TestServer.database();
+        db.connect().then(function () {
+            return addMockData(db);
+        }).then(function () {
+            return TestServer.remoteAPI().fetchJSONWithStatus('/api/build-requests/build-webkit');
+        }).then(function (content) {
+            assert.deepEqual(Object.keys(content).sort(), ['buildRequests', 'rootSets', 'roots', 'status']);
+
+            assert.equal(content['rootSets'].length, 2);
+            assert.equal(content['rootSets'][0].id, 401);
+            assert.deepEqual(content['rootSets'][0].roots, ['87832', '93116']);
+            assert.equal(content['rootSets'][1].id, 402);
+            assert.deepEqual(content['rootSets'][1].roots, ['87832', '96336']);
+
+            assert.equal(content['roots'].length, 3);
+            assert.equal(content['roots'][0].id, 87832);
+            assert.equal(content['roots'][0].repository, '9');
+            assert.equal(content['roots'][0].revision, '10.11 15A284');
+            assert.equal(content['roots'][1].id, 93116);
+            assert.equal(content['roots'][1].repository, '11');
+            assert.equal(content['roots'][1].revision, '191622');
+            assert.equal(content['roots'][2].id, 96336);
+            assert.equal(content['roots'][2].repository, '11');
+            assert.equal(content['roots'][2].revision, '192736');
+
+            assert.equal(content['buildRequests'].length, 4);
+            assert.deepEqual(content['buildRequests'][0].id, 700);
+            assert.deepEqual(content['buildRequests'][0].order, 0);
+            assert.deepEqual(content['buildRequests'][0].platform, '65');
+            assert.deepEqual(content['buildRequests'][0].rootSet, 401);
+            assert.deepEqual(content['buildRequests'][0].status, 'pending');
+            assert.deepEqual(content['buildRequests'][0].test, '200');
+
+            assert.deepEqual(content['buildRequests'][1].id, 701);
+            assert.deepEqual(content['buildRequests'][1].order, 1);
+            assert.deepEqual(content['buildRequests'][1].platform, '65');
+            assert.deepEqual(content['buildRequests'][1].rootSet, 402);
+            assert.deepEqual(content['buildRequests'][1].status, 'pending');
+            assert.deepEqual(content['buildRequests'][1].test, '200');
+
+            assert.deepEqual(content['buildRequests'][2].id, 702);
+            assert.deepEqual(content['buildRequests'][2].order, 2);
+            assert.deepEqual(content['buildRequests'][2].platform, '65');
+            assert.deepEqual(content['buildRequests'][2].rootSet, 401);
+            assert.deepEqual(content['buildRequests'][2].status, 'pending');
+            assert.deepEqual(content['buildRequests'][2].test, '200');
+
+            assert.deepEqual(content['buildRequests'][3].id, 703);
+            assert.deepEqual(content['buildRequests'][3].order, 3);
+            assert.deepEqual(content['buildRequests'][3].platform, '65');
+            assert.deepEqual(content['buildRequests'][3].rootSet, 402);
+            assert.deepEqual(content['buildRequests'][3].status, 'pending');
+            assert.deepEqual(content['buildRequests'][3].test, '200');
+            done();
+        }).catch(done);
+    });
+
+    it('should return support useLegacyIdResolution option', function (done) {
+        let db = TestServer.database();
+        db.connect().then(function () {
+            return addMockData(db);
+        }).then(function () {
+            return TestServer.remoteAPI().fetchJSONWithStatus('/api/build-requests/build-webkit?useLegacyIdResolution=true');
+        }).then(function (content) {
+            assert.deepEqual(Object.keys(content).sort(), ['buildRequests', 'rootSets', 'roots', 'status']);
+
+            assert.equal(content['rootSets'].length, 2);
+            assert.equal(content['rootSets'][0].id, 401);
+            assert.deepEqual(content['rootSets'][0].roots, ['87832', '93116']);
+            assert.equal(content['rootSets'][1].id, 402);
+            assert.deepEqual(content['rootSets'][1].roots, ['87832', '96336']);
+
+            assert.equal(content['roots'].length, 3);
+            assert.equal(content['roots'][0].id, 87832);
+            assert.equal(content['roots'][0].repository, 'OS X');
+            assert.equal(content['roots'][0].revision, '10.11 15A284');
+            assert.equal(content['roots'][1].id, 93116);
+            assert.equal(content['roots'][1].repository, 'WebKit');
+            assert.equal(content['roots'][1].revision, '191622');
+            assert.equal(content['roots'][2].id, 96336);
+            assert.equal(content['roots'][2].repository, 'WebKit');
+            assert.equal(content['roots'][2].revision, '192736');
+
+            assert.equal(content['buildRequests'].length, 4);
+            assert.deepEqual(content['buildRequests'][0].id, 700);
+            assert.deepEqual(content['buildRequests'][0].order, 0);
+            assert.deepEqual(content['buildRequests'][0].platform, 'some platform');
+            assert.deepEqual(content['buildRequests'][0].rootSet, 401);
+            assert.deepEqual(content['buildRequests'][0].status, 'pending');
+            assert.deepEqual(content['buildRequests'][0].test, ['some test']);
+
+            assert.deepEqual(content['buildRequests'][1].id, 701);
+            assert.deepEqual(content['buildRequests'][1].order, 1);
+            assert.deepEqual(content['buildRequests'][1].platform, 'some platform');
+            assert.deepEqual(content['buildRequests'][1].rootSet, 402);
+            assert.deepEqual(content['buildRequests'][1].status, 'pending');
+            assert.deepEqual(content['buildRequests'][1].test, ['some test']);
+
+            assert.deepEqual(content['buildRequests'][2].id, 702);
+            assert.deepEqual(content['buildRequests'][2].order, 2);
+            assert.deepEqual(content['buildRequests'][2].platform, 'some platform');
+            assert.deepEqual(content['buildRequests'][2].rootSet, 401);
+            assert.deepEqual(content['buildRequests'][2].status, 'pending');
+            assert.deepEqual(content['buildRequests'][2].test, ['some test']);
+
+            assert.deepEqual(content['buildRequests'][3].id, 703);
+            assert.deepEqual(content['buildRequests'][3].order, 3);
+            assert.deepEqual(content['buildRequests'][3].platform, 'some platform');
+            assert.deepEqual(content['buildRequests'][3].rootSet, 402);
+            assert.deepEqual(content['buildRequests'][3].status, 'pending');
+            assert.deepEqual(content['buildRequests'][3].test, ['some test']);
+            done();
+        }).catch(done);
+    });
+
+});
diff --git a/Websites/perf.webkit.org/server-tests/resources/test-server.conf b/Websites/perf.webkit.org/server-tests/resources/test-server.conf
new file mode 100644 (file)
index 0000000..59fc8f4
--- /dev/null
@@ -0,0 +1,57 @@
+ServerRoot "/usr"
+Listen 8080
+
+LoadModule authn_core_module libexec/apache2/mod_authn_core.so
+LoadModule authz_core_module libexec/apache2/mod_authz_core.so
+LoadModule deflate_module libexec/apache2/mod_deflate.so
+LoadModule log_config_module libexec/apache2/mod_log_config.so
+LoadModule log_forensic_module libexec/apache2/mod_log_forensic.so
+LoadModule env_module libexec/apache2/mod_env.so
+LoadModule headers_module libexec/apache2/mod_headers.so
+LoadModule setenvif_module libexec/apache2/mod_setenvif.so
+LoadModule mime_module libexec/apache2/mod_mime.so
+LoadModule unixd_module libexec/apache2/mod_unixd.so
+LoadModule status_module libexec/apache2/mod_status.so
+LoadModule negotiation_module libexec/apache2/mod_negotiation.so
+LoadModule dir_module libexec/apache2/mod_dir.so
+LoadModule alias_module libexec/apache2/mod_alias.so
+LoadModule rewrite_module libexec/apache2/mod_rewrite.so
+LoadModule php5_module libexec/apache2/libphp5.so
+
+<Directory />
+    Options Indexes FollowSymLinks MultiViews
+    AllowOverride None
+    Require all granted
+</Directory>
+
+<IfModule dir_module>
+    DirectoryIndex index.html index.php
+</IfModule>
+
+LogLevel warn
+
+<IfModule log_config_module>
+    LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
+    LogFormat "%h %l %u %t \"%r\" %>s %b" common
+
+    <IfModule logio_module>
+      LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio
+    </IfModule>
+
+    CustomLog |/usr/bin/tee common
+    ErrorLog |/usr/bin/tee
+</IfModule>
+
+<IfModule mime_module>
+    AddType text/html .html
+    AddType text/html .htm
+    AddType text/css .css
+    AddType text/javascript .js
+</IfModule>
+
+<IfModule php5_module>
+    AddType application/x-httpd-php .php
+    AddType application/x-httpd-php-source .phps
+</IfModule>
+
+Include /private/etc/apache2/extra/httpd-mpm.conf
diff --git a/Websites/perf.webkit.org/server-tests/resources/test-server.js b/Websites/perf.webkit.org/server-tests/resources/test-server.js
new file mode 100644 (file)
index 0000000..787cdfd
--- /dev/null
@@ -0,0 +1,228 @@
+'use strict';
+
+let assert = require('assert');
+let childProcess = require('child_process');
+let fs = require('fs');
+let path = require('path');
+
+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 {
+    constructor()
+    {
+        this._pidFile = null;
+        this._testConfigPath = Config.path('testServer.config');
+        this._dataDirectory = Config.path('dataDirectory');
+        this._backupDataPath = null;
+        this._pidWaitStart = null;
+        this._shouldLog = false;
+        this._pgsqlDirectory = null;
+        this._server = null;
+
+        this._databaseName = Config.value('testDatabaseName');
+        this._databaseUser = Config.value('database.username');
+        this._databaseHost = Config.value('database.host');
+        this._databasePort = Config.value('database.port');
+        this._database = null;
+    }
+
+    start()
+    {        
+        let testConfigContent = this._constructTestConfig(this._dataDirectory);
+        fs.writeFileSync(this._testConfigPath, JSON.stringify(testConfigContent, null, '    '));
+
+        this._ensureTestDatabase();
+        this._ensureDataDirectory();
+
+        return this._startApache();
+    }
+
+    stop()
+    {
+        this._restoreDataDirectory();
+
+        return this._stopApache();
+    }
+
+    remoteAPI()
+    {
+        assert(this._server);
+        RemoteAPI.configure(this._server);
+        return RemoteAPI;
+    }
+
+    database()
+    {
+        assert(this._databaseName);
+        if (!this._database)
+            this._database = new Database(this._databaseName);
+        return this._database;
+    }
+
+    _constructTestConfig(dataDirectory)
+    {
+        return {
+            'siteTitle': 'Test Dashboard',
+            'debug': true,
+            'jsonCacheMaxAge': 600,
+            'dataDirectory': dataDirectory,
+            'database': {
+                'host': Config.value('database.host'),
+                'port': Config.value('database.port'),
+                'username': Config.value('database.username'),
+                'password': Config.value('database.password'),
+                'name': Config.value('testDatabaseName'),
+            },
+            'universalSlavePassword': null,
+            'maintenanceMode': false,
+            'clusterStart': [2000, 1, 1, 0, 0],
+            'clusterSize': [0, 2, 0],
+            'defaultDashboard': [[]],
+            'dashboards': {}
+        }
+    }
+
+    _ensureDataDirectory()
+    {
+
+        let backupPath = path.resolve(this._dataDirectory, '../original-data');
+        if (fs.existsSync(this._dataDirectory)) {
+            assert.ok(!fs.existsSync(backupPath), `Both ${this._dataDirectory} and ${backupPath} exist. Cannot make a backup of data`);
+            fs.rename(this._dataDirectory, backupPath);
+            this._backupDataPath = backupPath;
+        } else {
+            if (fs.existsSync(backupPath)) // Assume this is a backup from the last failed run
+                this._backupDataPath = backupPath;
+            fs.mkdirSync(this._dataDirectory, 0o755);
+        }
+    }
+
+    _restoreDataDirectory()
+    {
+        childProcess.execFileSync('rm', ['-rf', this._dataDirectory]);
+        if (this._backupDataPath)
+            fs.rename(this._backupDataPath, this._dataDirectory);
+    }
+
+    _ensureTestDatabase()
+    {
+        this._executePgsqlCommand('dropdb');
+        this._executePgsqlCommand('createdb');
+        this._executePgsqlCommand('psql', ['--command', `grant all privileges on database "${this._databaseName}" to "${this._databaseUser}";`]);
+        this.initDatabase();
+    }
+
+    initDatabase()
+    {
+        if (this._database)
+            this._database.disconnect();
+        this._database = null;
+
+        let initFilePath = Config.pathFromRoot('init-database.sql');
+        this._executePgsqlCommand('psql', ['--username', this._databaseUser, '--file', initFilePath],
+            {stdio: ['ignore', 'ignore', 'ignore']});
+    }
+
+    _executePgsqlCommand(command, args, options)
+    {
+        if (!this._pgsqlDirectory)
+            this._pgsqlDirectory = this._determinePgsqlDirectory();
+        childProcess.execFileSync(path.resolve(this._pgsqlDirectory, command),
+            [this._databaseName, '--host', this._databaseHost, '--port', this._databasePort].concat(args || []), options);
+    }
+
+    _determinePgsqlDirectory()
+    {
+        try {
+            let initdbLocation = childProcess.execFileSync('which', ['initdb']);
+            return path.dirname(initdbLocation);
+        } catch (error) {
+            let serverPgsqlLocation = '/Applications/Server.app/Contents/ServerRoot/usr/bin/';
+            childProcess.execFileSync(path.resolve(serverPgsqlLocation, 'initdb'), ['--version']);
+            return serverPgsqlLocation;
+        }
+    }
+
+    _startApache()
+    {
+        let pidFile = Config.path('testServer.httpdPID');
+        let httpdConfig = Config.path('testServer.httpdConfig');
+        let port = Config.value('testServer.port');
+        let errorLog = Config.path('testServer.httpdErrorLog');
+        let mutexFile = Config.path('testServer.httpdMutexDir');
+
+        if (!fs.existsSync(mutexFile))
+            fs.mkdirSync(mutexFile, 0o755);
+
+        let args = [
+            '-f', httpdConfig,
+            '-c', `SetEnv ORG_WEBKIT_PERF_CONFIG_PATH ${this._testConfigPath}`,
+            '-c', `Listen ${port}`,
+            '-c', `PidFile ${pidFile}`,
+            '-c', `ErrorLog ${errorLog}`,
+            '-c', `Mutex file:${mutexFile}`,
+            '-c', `DocumentRoot ${Config.serverRoot()}`];
+
+        if (this._shouldLog)
+            console.log(args);
+
+        childProcess.execFileSync('httpd', args);
+
+        this._server = {
+            scheme: 'http',
+            host: 'localhost',
+            port: port,
+        }
+        this._pidWaitStart = Date.now();
+        this._pidFile = pidFile;
+        return new Promise(this._waitForPid.bind(this, true));
+    }
+
+    _stopApache()
+    {
+        if (!this._pidFile)
+            return;
+
+        let pid = fs.readFileSync(this._pidFile, 'utf-8').trim();
+
+        if (this._shouldLog)
+            console.log('Stopping', pid);
+
+        childProcess.execFileSync('kill', ['-TERM', pid]);
+
+        return new Promise(this._waitForPid.bind(this, false));
+    }
+
+    _waitForPid(shouldExist, resolve, reject)
+    {
+        if (fs.existsSync(this._pidFile) != shouldExist) {
+            if (Date.now() - this._pidWaitStart > 5000)
+                reject();
+            else
+                setTimeout(this._waitForPid.bind(this, shouldExist, resolve, reject), 100);
+            return;
+        }
+        resolve();
+    }
+});
+
+
+before(function () {
+    this.timeout(5000);
+    return TestServer.start();
+});
+
+beforeEach(function () {
+    this.timeout(5000);
+    return TestServer.initDatabase();
+});
+
+after(function () {
+    this.timeout(5000);
+    return TestServer.stop();
+});
+
+if (typeof module != 'undefined')
+    module.exports = TestServer;
index d5f0188..f754cfc 100644 (file)
@@ -28,10 +28,9 @@ var Config = new (class Config {
         return content;
     }
 
-    path(key)
-    {
-        return path.resolve(this._rootDirectory, this.value(key));
-    }
+    path(key) { return path.resolve(this._rootDirectory, this.value(key)); }
+    serverRoot() { return path.resolve(this._rootDirectory, 'public'); }
+    pathFromRoot(relativePathFromRoot) { return path.resolve(this._rootDirectory, relativePathFromRoot); }
 });
 
 if (typeof module != 'undefined')
index 606c4ac..67b60f9 100644 (file)
@@ -4,9 +4,10 @@ var pg = require('pg');
 var config = require('./config.js');
 
 class Database {
-    constructor()
+    constructor(databaseName)
     {
         this._client = null;
+        this._databaseName = databaseName || config.value('database.name');
     }
 
     connect(options)
@@ -17,10 +18,9 @@ class Database {
         let password = config.value('database.password');
         let host = config.value('database.host');
         let port = config.value('database.port');
-        let name = config.value('database.name');
 
         // No need to worry about escaping strings since they are only set by someone who can write to config.json.
-        let connectionString = `tcp://${username}:${password}@${host}:${port}/${name}`;
+        let connectionString = `tcp://${username}:${password}@${host}:${port}/${this._databaseName}`;
 
         let client = new pg.Client(connectionString);
         if (!options || !options.keepAlive) {
@@ -47,6 +47,49 @@ class Database {
             this._client = null;
         }
     }
+
+    query(statement, parameters)
+    {
+        console.assert(this._client);
+        var client = this._client;
+        return new Promise(function (resolve, reject) {
+            client.query(statement, parameters || [], function (error, result) {
+                if (error)
+                    reject(error);
+                else
+                    resolve(result);
+            });
+        });
+    }
+
+    insert(table, parameters)
+    {
+        let columnNames = [];
+        let placeholders = [];
+        let values = [];
+        for (let name in parameters) {
+            values.push(parameters[name]);
+            if (table in tableToPrefixMap)
+                name = tableToPrefixMap[table] + '_' + name;
+            columnNames.push(name);
+            placeholders.push(`\$${placeholders.length + 1}`);
+        }
+        return this.query(`INSERT INTO ${table} (${columnNames}) VALUES (${placeholders})`, values);
+    }
+}
+
+let tableToPrefixMap = {
+    'analysis_tasks': 'task',
+    'analysis_test_groups': 'testgroup',
+    'build_triggerables': 'triggerable',
+    'build_requests': 'request',
+    'commits': 'commit',
+    'test_metrics': 'metric',
+    'tests': 'test',
+    'platforms': 'platform',
+    'repositories': 'repository',
+    'root_sets': 'rootset',
+    'roots': 'root',
 }
 
 if (typeof module != 'undefined')
diff --git a/Websites/perf.webkit.org/tools/js/remote.js b/Websites/perf.webkit.org/tools/js/remote.js
new file mode 100644 (file)
index 0000000..64e381d
--- /dev/null
@@ -0,0 +1,79 @@
+'use strict';
+
+let assert = require('assert');
+let http = require('http');
+let https = require('https');
+
+let RemoteAPI = new (class RemoteAPI {
+    constructor()
+    {
+        this._server = {
+            scheme: 'http',
+            host: 'localhost',
+        }
+    }
+
+    configure(server)
+    {
+        assert(server.scheme === 'http' || server.scheme === 'https');
+        assert.equal(typeof(server.host), 'string');
+        assert(!server.port || typeof(server.port) == 'number');
+        assert(!server.auth || typeof(server.auth) == 'object');
+        this._server = server;
+    }
+
+    fetchJSON(path, data)
+    {
+        let contentType = null;
+        if (data) {
+            contentType = 'application/json';
+            data = JSON.stringify(data);
+        }
+        return this.sendHttpRequest(path, 'GET', contentType, data).then(function (result) {
+            return JSON.parse(result.responseText);
+        });
+    }
+
+    fetchJSONWithStatus(path, data)
+    {
+        return this.fetchJSON(path, data).then(function (result) {
+            if (result['status'] != 'OK')
+                return Promise.reject(result);
+            return result;
+        });
+    }
+
+    sendHttpRequest(path, method, contentType, content)
+    {
+        let server = this._server;
+        return new Promise(function (resolve, reject) {
+            let options = {
+                hostname: server.host,
+                port: server.port || 80,
+                auth: server.auth,
+                method: method,
+                path: path,
+            };
+
+            let request = (server.scheme == 'http' ? http : https).request(options, function (response) {
+                let responseText = '';
+                response.setEncoding('utf8');
+                response.on('data', function (chunk) { responseText += chunk; });
+                response.on('end', function () { resolve({statusCode: response.statusCode, responseText: responseText}); });
+            });
+
+            request.on('error', reject);
+
+            if (contentType)
+                request.setHeader('Content-Type', contentType);
+
+            if (content)
+                request.write(content);
+
+            request.end();
+        });
+    }
+})
+
+if (typeof module != 'undefined')
+    module.exports.RemoteAPI = RemoteAPI;
index f44125e..aae6871 100755 (executable)
@@ -41,7 +41,7 @@ def main():
             'slaveName': server_config['slave']['name'],
             'slavePassword': server_config['slave']['password']}
 
-        build_requests_url = server_config['server']['url'] + '/api/build-requests/' + args.triggerable
+        build_requests_url = server_config['server']['url'] + '/api/build-requests/' + args.triggerable + '?useLegacyIdResolution=true'
         response = update_and_fetch_build_requests(build_requests_url, payload)
         open_requests = response.get('buildRequests', [])
 
index 4f0524a..78b10e1 100644 (file)
@@ -2,7 +2,6 @@
 
 var assert = require('assert');
 var fs = require('fs');
-var pg = require('pg');
 
 var Config = require('../tools/js/config.js');
 var Database = require('../tools/js/database.js');