Add unit tests for config.json and statistics.js
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 18 Mar 2016 07:15:54 +0000 (07:15 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 18 Mar 2016 07:15:54 +0000 (07:15 +0000)
https://bugs.webkit.org/show_bug.cgi?id=155626

Reviewed by Darin Adler.

Added mocha unit tests for statistics.js and validating config.json. For segmentations, I've extracted
real data from our internal perf dashboard.

Also fixed some bugs covered by these new tests.

* public/shared/statistics.js:
(Statistics.movingAverage): Fixed a bug that forwardWindowSize was never used.
(Statistics.exponentialMovingAverage): Fixed the bug that the moving average starts at 0. It should
start at the first value instead.
(.splitIntoSegmentsUntilGoodEnough): Fixed the bug that we may try to segment a time series into
more parts than there are data points. Clearly, that doesn't make any sense.
(.findOptimalSegmentation): Renamed local variables so that they're more descriptive, and rewrote
the debugging code was the old code was emitting some useless data. Also fixed the bug that the length
of "segmentation" was off by one (we need segmentCount + 1 elements in the array sine we always
include the start of the first segment = 0 and the end of the last segment = values.length).
(.SampleVarianceUpperTriangularMatrix):
(Statistics): Modernized the export code.
* tools/js: Added.
* tools/js/config.js: Added.
(Config): Added.
(Config.prototype.configFilePath): Added.
(Config.prototype.value): Added.
(Config.prototype.path): Added.
* tools/js/database.js: Added.
(Database): Added.
(Database.prototype.connect): Added.
(Database.prototype.disconnect): Added.
* unit-tests: Added.
* unit-tests/checkconfig.js: Added. Validates config.json. This is useful while setting up
a local instance of the perf dashboard.
* unit-tests/statistics-tests.js: Added.
(assert.almostEqual): Added. Asserts that two floating values are within a given significant digits.
(.stdev):
(.delta):
(.computeWelchsT):

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

Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/public/shared/statistics.js
Websites/perf.webkit.org/tools/js/config.js [new file with mode: 0644]
Websites/perf.webkit.org/tools/js/database.js [new file with mode: 0644]
Websites/perf.webkit.org/unit-tests/checkconfig.js [new file with mode: 0644]
Websites/perf.webkit.org/unit-tests/statistics-tests.js [new file with mode: 0644]

index d7c6a39..4eca5de 100644 (file)
@@ -1,5 +1,48 @@
 2016-03-17  Ryosuke Niwa  <rniwa@webkit.org>
 
+        Add unit tests for config.json and statistics.js
+        https://bugs.webkit.org/show_bug.cgi?id=155626
+
+        Reviewed by Darin Adler.
+
+        Added mocha unit tests for statistics.js and validating config.json. For segmentations, I've extracted
+        real data from our internal perf dashboard.
+
+        Also fixed some bugs covered by these new tests.
+
+        * public/shared/statistics.js:
+        (Statistics.movingAverage): Fixed a bug that forwardWindowSize was never used.
+        (Statistics.exponentialMovingAverage): Fixed the bug that the moving average starts at 0. It should
+        start at the first value instead.
+        (.splitIntoSegmentsUntilGoodEnough): Fixed the bug that we may try to segment a time series into
+        more parts than there are data points. Clearly, that doesn't make any sense.
+        (.findOptimalSegmentation): Renamed local variables so that they're more descriptive, and rewrote
+        the debugging code was the old code was emitting some useless data. Also fixed the bug that the length
+        of "segmentation" was off by one (we need segmentCount + 1 elements in the array sine we always
+        include the start of the first segment = 0 and the end of the last segment = values.length).
+        (.SampleVarianceUpperTriangularMatrix):
+        (Statistics): Modernized the export code.
+        * tools/js: Added.
+        * tools/js/config.js: Added.
+        (Config): Added.
+        (Config.prototype.configFilePath): Added.
+        (Config.prototype.value): Added.
+        (Config.prototype.path): Added.
+        * tools/js/database.js: Added.
+        (Database): Added.
+        (Database.prototype.connect): Added.
+        (Database.prototype.disconnect): Added.
+        * unit-tests: Added.
+        * unit-tests/checkconfig.js: Added. Validates config.json. This is useful while setting up
+        a local instance of the perf dashboard.
+        * unit-tests/statistics-tests.js: Added.
+        (assert.almostEqual): Added. Asserts that two floating values are within a given significant digits.
+        (.stdev):
+        (.delta):
+        (.computeWelchsT):
+
+2016-03-17  Ryosuke Niwa  <rniwa@webkit.org>
+
         Fix a typo which was supposed to be fixed in r198351.
 
         * public/v3/pages/analysis-task-page.js:
index 9ef1e74..36b188f 100644 (file)
@@ -122,7 +122,7 @@ var Statistics = new (function () {
         for (var i = 0; i < values.length; i++) {
             var sum = 0;
             var count = 0;
-            for (var j = i - backwardWindowSize; j < i + backwardWindowSize; j++) {
+            for (var j = i - backwardWindowSize; j <= i + forwardWindowSize; j++) {
                 if (j >= 0 && j < values.length) {
                     sum += values[j];
                     count++;
@@ -145,8 +145,9 @@ var Statistics = new (function () {
 
     this.exponentialMovingAverage = function (values, smoothingFactor) {
         var averages = new Array(values.length);
-        var movingAverage = 0;
-        for (var i = 0; i < values.length; i++) {
+        var movingAverage = values[0];
+        averages[0] = movingAverage;
+        for (var i = 1; i < values.length; i++) {
             movingAverage = smoothingFactor * values[i] + (1 - smoothingFactor) * movingAverage;
             averages[i] = movingAverage;
         }
@@ -280,7 +281,7 @@ var Statistics = new (function () {
 
         var segmentation;
         var minTotalCost = Infinity;
-        var maxK = 50;
+        var maxK = Math.min(50, values.length);
 
         for (var k = 1; k < maxK; k++) {
             var start = Date.now();
@@ -302,10 +303,10 @@ var Statistics = new (function () {
     function findOptimalSegmentation(values, costMatrix, segmentCount) {
         // Dynamic programming. cost[i][k] = The cost to segmenting values up to i into k segments.
         var cost = new Array(values.length);
-        for (var i = 0; i < values.length; i++) {
-            cost[i] = new Float32Array(segmentCount + 1);
-        }
+        for (var segmentEnd = 0; segmentEnd < values.length; segmentEnd++)
+            cost[segmentEnd] = new Float32Array(segmentCount + 1);
 
+        // previousNode[i][k] = The start of the last segment in an optimal segmentation that ends at i with k segments.
         var previousNode = new Array(values.length);
         for (var i = 0; i < values.length; i++)
             previousNode[i] = new Array(segmentCount + 1);
@@ -313,45 +314,48 @@ var Statistics = new (function () {
         cost[0] = [0]; // The cost of segmenting single value is always 0.
         previousNode[0] = [-1];
         for (var segmentStart = 0; segmentStart < values.length; segmentStart++) {
-            var costBySegment = cost[segmentStart];
-            for (var count = 0; count < segmentCount; count++) {
-                if (previousNode[segmentStart][count] === undefined)
+            var costOfOptimalSegmentationThatEndAtCurrentStart = cost[segmentStart];
+            for (var k = 0; k < segmentCount; k++) {
+                var noSegmentationOfLenghtKEndsAtCurrentStart = previousNode[segmentStart][k] === undefined;
+                if (noSegmentationOfLenghtKEndsAtCurrentStart)
                     continue;
                 for (var segmentEnd = segmentStart + 1; segmentEnd < values.length; segmentEnd++) {
-                    var newCost = costBySegment[count] + costMatrix.costBetween(segmentStart, segmentEnd);
-                    if (previousNode[segmentEnd][count + 1] === undefined || newCost < cost[segmentEnd][count + 1]) {
-                        cost[segmentEnd][count + 1] = newCost;
-                        previousNode[segmentEnd][count + 1] = segmentStart;
+                    var costOfOptimalSegmentationOfLengthK = costOfOptimalSegmentationThatEndAtCurrentStart[k];
+                    var costOfCurrentSegment = costMatrix.costBetween(segmentStart, segmentEnd);
+                    var totalCost = costOfOptimalSegmentationOfLengthK + costOfCurrentSegment;
+                    if (previousNode[segmentEnd][k + 1] === undefined || totalCost < cost[segmentEnd][k + 1]) {
+                        cost[segmentEnd][k + 1] = totalCost;
+                        previousNode[segmentEnd][k + 1] = segmentStart;
                     }
                 }
             }
         }
 
         if (Statistics.debuggingSegmentation) {
-            console.log('findOptimalSegmentation with k=', segmentCount);
-            for (var i = 0; i < cost.length; i++) {
-                var t = cost[i];
-                var s = '';
-                for (var j = 0; j < t.length; j++) {
-                    var p = previousNode[i][j];
-                    s += '(k=' + j;
-                    if (p !== undefined)
-                        s += ' c=' + t[j] + ' p=' + p
-                    s += ')';
+            console.log('findOptimalSegmentation with', segmentCount, 'segments');
+            for (var end = 0; end < values.length; end++) {
+                for (var k = 0; k <= segmentCount; k++) {
+                    var start = previousNode[end][k];
+                    if (start === undefined)
+                        continue;
+                    console.log(`C(segment=[${start}, ${end + 1}], segmentCount=${k})=${cost[end][k]}`);
                 }
-                console.log(i, values[i], s);
             }
         }
 
-        var currentIndex = values.length - 1;
-        var segmentation = new Array(segmentCount);
-        segmentation[0] = values.length;
-        for (var i = 0; i < segmentCount; i++) {
-            currentIndex = previousNode[currentIndex][segmentCount - i];
-            segmentation[i + 1] = currentIndex;
+        var segmentEnd = values.length - 1;
+        var segmentation = new Array(segmentCount + 1);
+        segmentation[segmentCount] = values.length;
+        for (var k = segmentCount; k > 0; k--) {
+            segmentEnd = previousNode[segmentEnd][k];
+            segmentation[k - 1] = segmentEnd;
         }
+        var costOfOptimalSegmentation = cost[values.length - 1][segmentCount];
+
+        if (Statistics.debuggingSegmentation)
+            console.log('Optimal segmentation:', segmentation, 'with cost =', costOfOptimalSegmentation);
 
-        return {segmentation: segmentation.reverse(), cost: cost[values.length - 1][segmentCount]};
+        return {segmentation: segmentation, cost: costOfOptimalSegmentation};
     }
 
     function SampleVarianceUpperTriangularMatrix(values) {
@@ -385,7 +389,5 @@ var Statistics = new (function () {
 
 })();
 
-if (typeof module != 'undefined') {
-    for (var key in Statistics)
-        module.exports[key] = Statistics[key];
-}
+if (typeof module != 'undefined')
+    module.exports = Statistics;
diff --git a/Websites/perf.webkit.org/tools/js/config.js b/Websites/perf.webkit.org/tools/js/config.js
new file mode 100644 (file)
index 0000000..d5f0188
--- /dev/null
@@ -0,0 +1,38 @@
+"use strict";
+
+var fs = require('fs');
+var path = require('path');
+
+var Config = new (class Config {
+    constructor()
+    {
+        this._rootDirectory =  path.resolve(__dirname, '../../'); 
+        this._configPath = path.resolve(this._rootDirectory, 'config.json');
+        this._content = null;
+    }
+
+    configFilePath() { return this._configPath; }
+
+    value(key)
+    {
+        if (!this._content)
+            this._content = JSON.parse(fs.readFileSync(this._configPath));
+
+        let content = this._content;
+        for (var key of key.split('.')) {
+            if (!(key in content))
+                return null;
+            content = content[key];
+        }
+
+        return content;
+    }
+
+    path(key)
+    {
+        return path.resolve(this._rootDirectory, this.value(key));
+    }
+});
+
+if (typeof module != 'undefined')
+    module.exports = Config;
diff --git a/Websites/perf.webkit.org/tools/js/database.js b/Websites/perf.webkit.org/tools/js/database.js
new file mode 100644 (file)
index 0000000..606c4ac
--- /dev/null
@@ -0,0 +1,53 @@
+"use strict";
+
+var pg = require('pg');
+var config = require('./config.js');
+
+class Database {
+    constructor()
+    {
+        this._client = null;
+    }
+
+    connect(options)
+    {
+        console.assert(this._client === null);
+
+        let username = config.value('database.username');
+        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 client = new pg.Client(connectionString);
+        if (!options || !options.keepAlive) {
+            client.on('drain', function () {
+                client.end();
+            });
+        }
+
+        this._client = client;
+
+        return new Promise(function (resolve, reject) {
+            client.connect(function (error) {
+                if (error)
+                    reject(error);
+                resolve();
+            });
+        });
+    }
+
+    disconnect()
+    {
+        if (this._client) {
+            this._client.end();
+            this._client = null;
+        }
+    }
+}
+
+if (typeof module != 'undefined')
+    module.exports = Database;
diff --git a/Websites/perf.webkit.org/unit-tests/checkconfig.js b/Websites/perf.webkit.org/unit-tests/checkconfig.js
new file mode 100644 (file)
index 0000000..e3a3362
--- /dev/null
@@ -0,0 +1,169 @@
+"use strict";
+
+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');
+
+describe('config.json', function () {
+    it('should be a valid file', function () {
+        assert.doesNotThrow(function () {
+            fs.readFileSync(Config.configFilePath())
+        });
+    });
+
+    it('should be a valid JSON', function () {
+        assert.doesNotThrow(function () {
+            JSON.parse(fs.readFileSync(Config.configFilePath()));
+        });
+    });
+
+    it('should define `siteTitle`', function () {
+        assert.equal(typeof Config.value('siteTitle'), 'string');
+    });
+
+    it('should define `dataDirectory`', function () {
+        assert.ok(Config.value('dataDirectory'));
+        assert.ok(fs.existsSync(Config.path('dataDirectory')));
+        assert.ok(fs.statSync(Config.path('dataDirectory')).isDirectory());
+    });
+
+    it('should define `jsonCacheMaxAge`', function () {
+        assert.equal(typeof Config.value('jsonCacheMaxAge'), 'number');
+    });
+
+    it('should define `jsonCacheMaxAge`', function () {
+        assert.equal(typeof Config.value('jsonCacheMaxAge'), 'number');
+    });
+
+    it('should define `clusterStart`', function () {
+        var clusterStart = Config.value('clusterStart');
+        assert.ok(clusterStart instanceof Array);
+        assert.equal(clusterStart.length, [2000, 1, 1, 0, 0].length,
+            'Must specify year, month, date, hour, and minute');
+        var maxYear = (new Date).getFullYear() + 1;
+        assert.ok(clusterStart[0] >= 1970 && clusterStart[0] <= maxYear, `year must be between 1970 and ${maxYear}`);
+        assert.ok(clusterStart[1] >= 1 && clusterStart[1] <= 12, 'month must be between 1 and 12');
+        assert.ok(clusterStart[2] >= 1 && clusterStart[2] <= 31, 'date must be between 1 and 31');
+        assert.ok(clusterStart[3] >= 0 && clusterStart[3] <= 60, 'minute must be between 0 and 60');
+        assert.ok(clusterStart[4] >= 0 && clusterStart[4] <= 60, 'minute must be between 0 and 60');
+    });
+
+    it('should define `clusterSize`', function () {
+        var clusterSize = Config.value('clusterSize');
+        assert.ok(clusterSize instanceof Array);
+        assert.equal(clusterSize.length, [0, 2, 0].length,
+            'Must specify the number of years, months, and days');
+        assert.equal(typeof clusterSize[0], 'number', 'the number of year must be a number');
+        assert.equal(typeof clusterSize[1], 'number', 'the number of month must be a number');
+        assert.equal(typeof clusterSize[2], 'number', 'the number of days must be a number');
+    });
+
+    describe('`dashboards`', function () {
+        var dashboards = Config.value('dashboards');
+
+        it('should exist for v2 and v3 UI', function () {
+            assert.equal(typeof dashboards, 'object');
+        });
+
+        it('dashboard names that do not contain /', function () {
+            for (var name in dashboards)
+                assert.ok(name.indexOf('/') < 0, 'Dashboard name "${name}" should not contain "/"');
+        });
+
+        it('each dashboard must be an array', function () {
+            for (var name in dashboards)
+                assert.ok(dashboards[name] instanceof Array);
+        });
+
+        it('each row in a dashboard must be an array', function () {
+            for (var name in dashboards) {
+                for (var row of dashboards[name]) {
+                    console.assert(row instanceof Array);
+                }
+            }
+        });
+
+        it('each cell in a dashboard must be an array or a string', function () {
+            for (var name in dashboards) {
+                for (var row of dashboards[name]) {
+                    for (var cell of row) {
+                        if (cell instanceof Array)
+                            assert.ok(cell.length == 0 || cell.length == 2,
+                                'Each cell must be empty or specify [platform, metric] pair');
+                        else
+                            assert.equal(typeof cell, 'string');
+                    }
+                }
+            }
+        });
+
+    });
+
+    describe('`database`', function () {
+        it('should exist', function () {
+            assert.ok(Config.value('database'));
+        });
+
+        it('should define `database.host`', function () {
+            assert.equal(typeof Config.value('database.host'), 'string');
+        });
+
+        it('should define `database.port`', function () {
+            assert.equal(typeof Config.value('database.port'), 'string');
+        });
+
+        it('should define `database.username`', function () {
+            assert.equal(typeof Config.value('database.username'), 'string');
+        });
+
+        it('should define `database.password`', function () {
+            assert.equal(typeof Config.value('database.password'), 'string');
+        });
+
+        it('should define `database.name`', function () {
+            assert.equal(typeof Config.value('database.name'), 'string');
+        });
+
+        it('should be able to connect to the database', function (done) {
+            let database = new Database;
+            return database.connect().then(function () {
+                database.disconnect();
+                done();
+            }, function (error) {
+                database.disconnect();
+                done(error);
+            });
+        });
+    });
+
+    describe('optional configurations', function () {
+        function assertNullOrType(value, type) {
+            if (value !== null)
+                assert.equal(typeof value, type);
+        }
+
+        it('`debug` should be `null` or a boolean', function () {
+            assertNullOrType(Config.value('debug'), 'boolean');
+        });
+
+        it('`maintenanceMode` should be `null` or a boolean', function () {
+            assertNullOrType(Config.value('maintenanceMode'), 'boolean');
+        });
+
+        it('`maintenanceDirectory` should be `null` or a string', function () {
+            assertNullOrType(Config.value('maintenanceDirectory'), 'string');
+        });
+
+        it('`maintenanceDirectory` should be a string if `maintenanceMode` is true', function () {
+            if (Config.value('maintenanceMode'))
+                assert.equal(Config.value('maintenanceDirectory'), 'string');
+        });
+
+        it('`universalSlavePassword` should be `null` or a string', function () {
+            assertNullOrType(Config.value('universalSlavePassword'), 'string');
+        });
+    });
+});
diff --git a/Websites/perf.webkit.org/unit-tests/statistics-tests.js b/Websites/perf.webkit.org/unit-tests/statistics-tests.js
new file mode 100644 (file)
index 0000000..dece88e
--- /dev/null
@@ -0,0 +1,399 @@
+"use strict";
+
+var assert = require('assert');
+var Statistics = require('../public/shared/statistics.js');
+
+if (!assert.almostEqual) {
+    assert.almostEqual = function (actual, expected, precision, message) {
+        var suffiedMessage = (message ? message + ' ' : '');
+        if (isNaN(expected)) {
+            assert(isNaN(actual), `${suffiedMessage}expected NaN but got ${actual}`);
+            return;
+        }
+
+        if (expected == 0) {
+            assert.equal(actual, expected, message);
+            return;
+        }
+
+        if (!precision)
+            precision = 6;
+        var tolerance = 1 / Math.pow(10, precision);
+        var relativeDifference = Math.abs((actual - expected) / expected);
+        var percentDifference = (relativeDifference * 100).toFixed(2);
+        assert(relativeDifference < tolerance,
+            `${suffiedMessage}expected ${expected} but got ${actual} (${percentDifference}% difference)`);
+    }
+}
+
+describe('assert.almostEqual', function () {
+    it('should not throw when values are identical', function () {
+        assert.doesNotThrow(function () { assert.almostEqual(1, 1); });
+    });
+
+    it('should not throw when values are close', function () {
+        assert.doesNotThrow(function () { assert.almostEqual(1.10, 1.107, 2); });
+        assert.doesNotThrow(function () { assert.almostEqual(1256.7, 1256.72, 4); });
+    });
+
+    it('should throw when values are not close', function () {
+        assert.throws(function () { assert.almostEqual(1.10, 1.27, 2); });
+        assert.throws(function () { assert.almostEqual(735.4, 735.6, 4); });
+    });
+});
+
+describe('Statistics', function () {
+    describe('min', function () {
+        it('should find the mininum value', function () {
+            assert.equal(Statistics.min([1, 2, 3, 4]), 1);
+            assert.equal(Statistics.min([4, 3, 2, 1]), 1);
+            assert.equal(Statistics.min([2000, 20, 200]), 20);
+            assert.equal(Statistics.min([0.3, 0.06, 0.5]), 0.06);
+            assert.equal(Statistics.min([-0.3, 0.06, 0.5]), -0.3);
+            assert.equal(Statistics.min([-0.3, 0.06, 0.5, Infinity]), -0.3);
+            assert.equal(Statistics.min([-0.3, 0.06, 0.5, -Infinity]), -Infinity);
+            assert.equal(Statistics.min([]), Infinity);
+        });
+    });
+
+    describe('max', function () {
+        it('should find the mininum value', function () {
+            assert.equal(Statistics.max([1, 2, 3, 4]), 4);
+            assert.equal(Statistics.max([4, 3, 2, 1]), 4);
+            assert.equal(Statistics.max([2000, 20, 200]), 2000);
+            assert.equal(Statistics.max([0.3, 0.06, 0.5]), 0.5);
+            assert.equal(Statistics.max([-0.3, 0.06, 0.5]), 0.5);
+            assert.equal(Statistics.max([-0.3, 0.06, 0.5, Infinity]), Infinity);
+            assert.equal(Statistics.max([-0.3, 0.06, 0.5, -Infinity]), 0.5);
+            assert.equal(Statistics.max([]), -Infinity);
+        });
+    });
+
+    describe('sum', function () {
+        it('should find the sum of values', function () {
+            assert.equal(Statistics.sum([1, 2, 3, 4]), 10);
+            assert.equal(Statistics.sum([4, 3, 2, 1]), 10);
+            assert.equal(Statistics.sum([2000, 20, 200]), 2220);
+            assert.equal(Statistics.sum([0.3, 0.06, 0.5]), 0.86);
+            assert.equal(Statistics.sum([-0.3, 0.06, 0.5]), 0.26);
+            assert.equal(Statistics.sum([-0.3, 0.06, 0.5, Infinity]), Infinity);
+            assert.equal(Statistics.sum([-0.3, 0.06, 0.5, -Infinity]), -Infinity);
+            assert.equal(Statistics.sum([]), 0);
+        });
+    });
+
+    describe('squareSum', function () {
+        it('should find the square sum of values', function () {
+            assert.equal(Statistics.squareSum([1, 2, 3, 4]), 30);
+            assert.equal(Statistics.squareSum([4, 3, 2, 1]), 30);
+            assert.equal(Statistics.squareSum([2000, 20, 200]), 2000 * 2000 + 20 * 20 + 200* 200);
+            assert.equal(Statistics.squareSum([0.3, 0.06, 0.5]), 0.09 + 0.0036 + 0.25);
+            assert.equal(Statistics.squareSum([-0.3, 0.06, 0.5]), 0.09 + 0.0036 + 0.25);
+            assert.equal(Statistics.squareSum([-0.3, 0.06, 0.5, Infinity]), Infinity);
+            assert.equal(Statistics.squareSum([-0.3, 0.06, 0.5, -Infinity]), Infinity);
+            assert.equal(Statistics.squareSum([]), 0);
+        });
+    });
+
+    describe('sampleStandardDeviation', function () {
+        function stdev(values) {
+            return Statistics.sampleStandardDeviation(values.length,
+                Statistics.sum(values), Statistics.squareSum(values));
+        }
+
+        it('should find the standard deviation of values', function () {
+            assert.almostEqual(stdev([1, 2, 3, 4]), 1.2909944);
+            assert.almostEqual(stdev([4, 3, 2, 1]), 1.2909944);
+            assert.almostEqual(stdev([2000, 20, 200]), 1094.89726);
+            assert.almostEqual(stdev([0.3, 0.06, 0.5]), 0.220302822);
+            assert.almostEqual(stdev([-0.3, 0.06, 0.5]), 0.40066611203);
+            assert.almostEqual(stdev([-0.3, 0.06, 0.5, Infinity]), NaN);
+            assert.almostEqual(stdev([-0.3, 0.06, 0.5, -Infinity]), NaN);
+            assert.almostEqual(stdev([]), 0);
+        });
+    });
+
+    describe('confidenceIntervalDelta', function () {
+        it('should find the p-value of values using Student\'s t distribution', function () {
+            function delta(values, probabilty) {
+                return Statistics.confidenceIntervalDelta(probabilty, values.length,
+                    Statistics.sum(values), Statistics.squareSum(values));
+            }
+
+            // https://onlinecourses.science.psu.edu/stat414/node/199
+            var values = [118, 115, 125, 110, 112, 130, 117, 112, 115, 120, 113, 118, 119, 122, 123, 126];
+            assert.almostEqual(delta(values, 0.95), 3.015, 3);
+
+            // Following values are computed using Excel Online's STDEV and CONFIDENCE.T
+            assert.almostEqual(delta([1, 2, 3, 4], 0.8), 1.057159);
+            assert.almostEqual(delta([1, 2, 3, 4], 0.9), 1.519090);
+            assert.almostEqual(delta([1, 2, 3, 4], 0.95), 2.054260);
+
+            assert.almostEqual(delta([0.3, 0.06, 0.5], 0.8), 0.2398353);
+            assert.almostEqual(delta([0.3, 0.06, 0.5], 0.9), 0.3713985);
+            assert.almostEqual(delta([0.3, 0.06, 0.5], 0.95), 0.5472625);
+
+            assert.almostEqual(delta([-0.3, 0.06, 0.5], 0.8), 0.4361900);
+            assert.almostEqual(delta([-0.3, 0.06, 0.5], 0.9), 0.6754647);
+            assert.almostEqual(delta([-0.3, 0.06, 0.5], 0.95), 0.9953098);
+
+            assert.almostEqual(delta([123, 107, 109, 104, 111], 0.8), 5.001167);
+            assert.almostEqual(delta([123, 107, 109, 104, 111], 0.9), 6.953874);
+            assert.almostEqual(delta([123, 107, 109, 104, 111], 0.95), 9.056490);
+
+            assert.almostEqual(delta([6785, 7812, 6904, 7503, 6943, 7207, 6812], 0.8), 212.6155);
+            assert.almostEqual(delta([6785, 7812, 6904, 7503, 6943, 7207, 6812], 0.9), 286.9585);
+            assert.almostEqual(delta([6785, 7812, 6904, 7503, 6943, 7207, 6812], 0.95), 361.3469);
+
+        });
+    });
+
+    // https://en.wikipedia.org/wiki/Welch%27s_t_test
+
+    var example1 = {
+        A1: [27.5, 21.0, 19.0, 23.6, 17.0, 17.9, 16.9, 20.1, 21.9, 22.6, 23.1, 19.6, 19.0, 21.7, 21.4],
+        A2: [27.1, 22.0, 20.8, 23.4, 23.4, 23.5, 25.8, 22.0, 24.8, 20.2, 21.9, 22.1, 22.9, 20.5, 24.4],
+        expectedT: 2.46,
+        expectedDegreesOfFreedom: 25.0,
+        expectedRange: [0.95, 0.98] // P = 0.021 so 1 - P = 0.979 is between 0.95 and 0.98
+    };
+
+    var example2 = {
+        A1: [17.2, 20.9, 22.6, 18.1, 21.7, 21.4, 23.5, 24.2, 14.7, 21.8],
+        A2: [21.5, 22.8, 21.0, 23.0, 21.6, 23.6, 22.5, 20.7, 23.4, 21.8, 20.7, 21.7, 21.5, 22.5, 23.6, 21.5, 22.5, 23.5, 21.5, 21.8],
+        expectedT: 1.57,
+        expectedDegreesOfFreedom: 9.9,
+        expectedRange: [0.8, 0.9] // P = 0.149 so 1 - P = 0.851 is between 0.8 and 0.9
+    };
+
+    var example3 = {
+        A1: [19.8, 20.4, 19.6, 17.8, 18.5, 18.9, 18.3, 18.9, 19.5, 22.0],
+        A2: [28.2, 26.6, 20.1, 23.3, 25.2, 22.1, 17.7, 27.6, 20.6, 13.7, 23.2, 17.5, 20.6, 18.0, 23.9, 21.6, 24.3, 20.4, 24.0, 13.2],
+        expectedT: 2.22,
+        expectedDegreesOfFreedom: 24.5,
+        expectedRange: [0.95, 0.98] // P = 0.036 so 1 - P = 0.964 is beteween 0.95 and 0.98
+    };
+
+    describe('computeWelchsT', function () {
+        function computeWelchsT(values1, values2, probability) {
+            return Statistics.computeWelchsT(values1, 0, values1.length, values2, 0, values2.length, probability);
+        }
+
+        it('should detect the statistically significant difference using Welch\'s t-test', function () {
+            assert.equal(computeWelchsT(example1.A1, example1.A2, 0.8).significantlyDifferent, true);
+            assert.equal(computeWelchsT(example1.A1, example1.A2, 0.9).significantlyDifferent, true);
+            assert.equal(computeWelchsT(example1.A1, example1.A2, 0.95).significantlyDifferent, true);
+            assert.equal(computeWelchsT(example1.A1, example1.A2, 0.98).significantlyDifferent, false);
+
+            assert.equal(computeWelchsT(example2.A1, example2.A2, 0.8).significantlyDifferent, true);
+            assert.equal(computeWelchsT(example2.A1, example2.A2, 0.9).significantlyDifferent, false);
+            assert.equal(computeWelchsT(example2.A1, example2.A2, 0.95).significantlyDifferent, false);
+            assert.equal(computeWelchsT(example2.A1, example2.A2, 0.98).significantlyDifferent, false);
+
+            assert.equal(computeWelchsT(example3.A1, example3.A2, 0.8).significantlyDifferent, true);
+            assert.equal(computeWelchsT(example3.A1, example3.A2, 0.9).significantlyDifferent, true);
+            assert.equal(computeWelchsT(example3.A1, example3.A2, 0.95).significantlyDifferent, true);
+            assert.equal(computeWelchsT(example3.A1, example3.A2, 0.98).significantlyDifferent, false);
+        });
+
+        it('should find the t-value of values using Welch\'s t-test', function () {
+            assert.almostEqual(computeWelchsT(example1.A1, example1.A2).t, example1.expectedT, 2);
+            assert.almostEqual(computeWelchsT(example2.A1, example2.A2).t, example2.expectedT, 2);
+            assert.almostEqual(computeWelchsT(example3.A1, example3.A2).t, example3.expectedT, 2);
+        });
+
+        it('should find the degreees of freedom using Welch–Satterthwaite equation', function () {
+            assert.almostEqual(computeWelchsT(example1.A1, example1.A2).degreesOfFreedom, example1.expectedDegreesOfFreedom, 2);
+            assert.almostEqual(computeWelchsT(example2.A1, example2.A2).degreesOfFreedom, example2.expectedDegreesOfFreedom, 2);
+            assert.almostEqual(computeWelchsT(example3.A1, example3.A2).degreesOfFreedom, example3.expectedDegreesOfFreedom, 2);
+        });
+
+        it('should respect the start and the end indices', function () {
+            var A1 = example2.A1.slice();
+            var A2 = example2.A2.slice();
+
+            var expectedT = Statistics.computeWelchsT(A1, 0, A1.length, A2, 0, A2.length).t;
+
+            A1.unshift(21);
+            A1.push(15);
+            A1.push(24);
+            assert.almostEqual(Statistics.computeWelchsT(A1, 1, A1.length - 3, A2, 0, A2.length).t, expectedT);
+
+            A2.unshift(24.3);
+            A2.unshift(25.8);
+            A2.push(23);
+            A2.push(24);
+            A2 = A2.reverse();
+            assert.almostEqual(Statistics.computeWelchsT(A1, 1, A1.length - 3, A2, 2, A2.length - 4).t, expectedT);
+        });
+    });
+
+    describe('probabilityRangeForWelchsT', function () {
+        it('should find the t-value of values using Welch\'s t-test', function () {
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example1.A1, example1.A2).t, example1.expectedT, 2);
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example2.A1, example2.A2).t, example2.expectedT, 2);
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example3.A1, example3.A2).t, example3.expectedT, 2);
+        });
+
+        it('should find the degreees of freedom using Welch–Satterthwaite equation', function () {
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example1.A1, example1.A2).degreesOfFreedom,
+                example1.expectedDegreesOfFreedom, 2);
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example2.A1, example2.A2).degreesOfFreedom,
+                example2.expectedDegreesOfFreedom, 2);
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example3.A1, example3.A2).degreesOfFreedom,
+                example3.expectedDegreesOfFreedom, 2);
+        });
+
+        it('should compute the range of probabilites using the p-value of Welch\'s t-test', function () {
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example1.A1, example1.A2).range[0], example1.expectedRange[0]);
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example1.A1, example1.A2).range[1], example1.expectedRange[1]);
+
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example2.A1, example2.A2).range[0], example2.expectedRange[0]);
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example2.A1, example2.A2).range[1], example2.expectedRange[1]);
+
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example3.A1, example3.A2).range[0], example3.expectedRange[0]);
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example3.A1, example3.A2).range[1], example3.expectedRange[1]);
+        });
+    });
+
+    describe('movingAverage', function () {
+        it('should return the origian values when both forward and backward window size is 0', function () {
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 0, 0), [1, 2, 3, 4, 5]);
+        });
+
+        it('should find the moving average with a positive backward window', function () {
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 1, 0),
+                [1, (1 + 2) / 2, (2 + 3) / 2, (3 + 4) / 2, (4 + 5) / 2]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 2, 0),
+                [1, (1 + 2) / 2, (1 + 2 + 3) / 3, (2 + 3 + 4) / 3, (3 + 4 + 5) / 3]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 3, 0),
+                [1, (1 + 2) / 2, (1 + 2 + 3) / 3, (1 + 2 + 3 + 4) / 4, (2 + 3 + 4 + 5) / 4]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 4, 0),
+                [1, (1 + 2) / 2, (1 + 2 + 3) / 3, (1 + 2 + 3 + 4) / 4, (1 + 2 + 3 + 4 + 5) / 5]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 5, 0),
+                [1, (1 + 2) / 2, (1 + 2 + 3) / 3, (1 + 2 + 3 + 4) / 4, (1 + 2 + 3 + 4 + 5) / 5]);
+        });
+
+        it('should find the moving average with a positive forward window', function () {
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 0, 1),
+                [(1 + 2) / 2, (2 + 3) / 2, (3 + 4) / 2, (4 + 5) / 2, 5]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 0, 2),
+                [(1 + 2 + 3) / 3, (2 + 3 + 4) / 3, (3 + 4 + 5) / 3, (4 + 5) / 2, 5]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 0, 3),
+                [(1 + 2 + 3 + 4) / 4, (2 + 3 + 4 + 5) / 4, (3 + 4 + 5) / 3, (4 + 5) / 2, 5]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 0, 4),
+                [(1 + 2 + 3 + 4 + 5) / 5, (2 + 3 + 4 + 5) / 4, (3 + 4 + 5) / 3, (4 + 5) / 2, 5]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 0, 5),
+                [(1 + 2 + 3 + 4 + 5) / 5, (2 + 3 + 4 + 5) / 4, (3 + 4 + 5) / 3, (4 + 5) / 2, 5]);
+        });
+
+        it('should find the moving average when both backward and forward window sizes are specified', function () {
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 1, 1),
+                [(1 + 2) / 2, (1 + 2 + 3) / 3, (2 + 3 + 4) / 3, (3 + 4 + 5) / 3, (4 + 5) / 2]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 1, 2),
+                [(1 + 2 + 3) / 3, (1 + 2 + 3 + 4) / 4, (2 + 3 + 4 + 5) / 4, (3 + 4 + 5) / 3, (4 + 5) / 2]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 2, 1),
+                [(1 + 2) / 2, (1 + 2 + 3) / 3, (1 + 2 + 3 + 4) / 4, (2 + 3 + 4 + 5) / 4, (3 + 4 + 5) / 3]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 2, 2),
+                [(1 + 2 + 3) / 3, (1 + 2 + 3 + 4) / 4, (1 + 2 + 3 + 4 + 5) / 5, (2 + 3 + 4 + 5) / 4, (3 + 4 + 5) / 3]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 2, 3),
+                [(1 + 2 + 3 + 4) / 4, (1 + 2 + 3 + 4 + 5) / 5, (1 + 2 + 3 + 4 + 5) / 5, (2 + 3 + 4 + 5) / 4, (3 + 4 + 5) / 3]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 3, 2),
+                [(1 + 2 + 3) / 3, (1 + 2 + 3 + 4) / 4, (1 + 2 + 3 + 4 + 5) / 5, (1 + 2 + 3 + 4 + 5) / 5, (2 + 3 + 4 + 5) / 4]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 3, 3),
+                [(1 + 2 + 3 + 4) / 4, (1 + 2 + 3 + 4 + 5) / 5, (1 + 2 + 3 + 4 + 5) / 5, (1 + 2 + 3 + 4 + 5) / 5, (2 + 3 + 4 + 5) / 4]);
+        });
+    });
+
+    describe('cumulativeMovingAverage', function () {
+        it('should find the cumulative moving average', function () {
+            assert.deepEqual(Statistics.cumulativeMovingAverage([1, 2, 3, 4, 5]),
+                [1, (1 + 2) / 2, (1 + 2 + 3) / 3, (1 + 2 + 3 + 4) / 4, (1 + 2 + 3 + 4 + 5) / 5]);
+
+            assert.deepEqual(Statistics.cumulativeMovingAverage([-1, 7, 0, 8.5, 2]),
+                [-1, (-1 + 7) / 2, (-1 + 7 + 0) / 3, (-1 + 7 + 0 + 8.5) / 4, (-1 + 7 + 0 + 8.5 + 2) / 5]);
+        });
+    });
+
+    describe('exponentialMovingAverage', function () {
+        it('should find the exponential moving average', function () {
+            var averages = Statistics.exponentialMovingAverage([1, 2, 3, 4, 5], 0.2);
+            assert.equal(averages[0], 1);
+            assert.almostEqual(averages[1], 0.2 * 2 + 0.8 * averages[0]);
+            assert.almostEqual(averages[2], 0.2 * 3 + 0.8 * averages[1]);
+            assert.almostEqual(averages[3], 0.2 * 4 + 0.8 * averages[2]);
+            assert.almostEqual(averages[4], 0.2 * 5 + 0.8 * averages[3]);
+
+            averages = Statistics.exponentialMovingAverage([0.8, -0.2, 0.4, -0.3, 0.5], 0.1);
+            assert.almostEqual(averages[0], 0.8);
+            assert.almostEqual(averages[1], 0.1 * -0.2 + 0.9 * averages[0]);
+            assert.almostEqual(averages[2], 0.1 * 0.4 + 0.9 * averages[1]);
+            assert.almostEqual(averages[3], 0.1 * -0.3 + 0.9 * averages[2]);
+            assert.almostEqual(averages[4], 0.1 * 0.5 + 0.9 * averages[3]);
+        });
+    });
+
+    describe('segmentTimeSeriesGreedyWithStudentsTTest', function () {
+        it('should segment time series', function () {
+            assert.deepEqual(Statistics.segmentTimeSeriesGreedyWithStudentsTTest([1, 1, 1, 3, 3, 3], 1), [0, 2, 6]);
+            assert.deepEqual(Statistics.segmentTimeSeriesGreedyWithStudentsTTest([1, 1.2, 0.9, 1.1, 1.5, 1.7, 1.8], 1), [0, 4, 7]);
+        });
+    });
+
+    describe('segmentTimeSeriesByMaximizingSchwarzCriterion', function () {
+        it('should not segment time series of length two into two pieces', function () {
+            var values = [1, 2];
+            assert.deepEqual(Statistics.segmentTimeSeriesByMaximizingSchwarzCriterion(values), [0, 2]);
+        });
+
+        it('should segment time series [1, 2, 3] into three pieces', function () {
+            var values = [1, 2, 3];
+            assert.deepEqual(Statistics.segmentTimeSeriesByMaximizingSchwarzCriterion(values), [0, 1, 3]);
+        });
+
+        it('should segment time series for platform=47 metric=4875 between 1453938553772 and 1454630903100 into two parts', function () {
+            var values = [
+                1546.5603, 1548.1536, 1563.5452, 1539.7823, 1546.4184, 1548.9299, 1532.5444, 1546.2800, 1547.1760, 1551.3507,
+                1548.3277, 1544.7673, 1542.7157, 1538.1700, 1538.0948, 1543.0364, 1537.9737, 1542.2611, 1543.9685, 1546.4901,
+                1544.4080, 1540.8671, 1537.3353, 1549.4331, 1541.4436, 1544.1299, 1550.1770, 1553.1872, 1549.3417, 1542.3788,
+                1543.5094, 1541.7905, 1537.6625, 1547.3840, 1538.5185, 1549.6764, 1556.6138, 1552.0476, 1541.7629, 1544.7006,
+                /* segments changes here */
+                1587.1390, 1594.5451, 1586.2430, 1596.7310, 1548.1423];
+            assert.deepEqual(Statistics.segmentTimeSeriesByMaximizingSchwarzCriterion(values), [0, 39, values.length]);
+        });
+
+        it('should segment time series for platform=51 metric=4565 betweeen 1452191332230 and 1454628206453 into two parts', function () {
+            var values = [
+                147243216, 147736350, 146670090, 146629723, 142749220, 148234161, 147303822, 145112097, 145852468, 147094741,
+                147568897, 145160531, 148028242, 141272279, 144323236, 147492567, 146219156, 144895726, 144418925, 145455873,
+                141924694, 141025833, 142082139, 144154698, 145312939, 148282554, 151852126, 149303740, 149431703, 150300257,
+                148752468, 150449779, 150030118, 150553542, 151775421, 146666762, 149492535, 147143284, 150356837, 147799616,
+                149889520,
+                258634751, 147397840, 256106147, 261100534, 255903392, 259658019, 259501433, 257685682, 258460322, 255563633,
+                259050663, 255567490, 253274911];
+            assert.deepEqual(Statistics.segmentTimeSeriesByMaximizingSchwarzCriterion(values), [0, 40, values.length]);
+        });
+
+        it('should not segment time series for platform=51 metric=4817 betweeen 1453926047749 and 1454635479052 into multiple parts', function () {
+            var values = [
+                5761.3, 5729.4, 5733.49, 5727.4, 5726.56, 5727.48, 5716.79, 5721.23, 5682.5, 5735.71,
+                5750.99, 5755.51, 5756.02, 5725.76, 5710.14, 5776.17, 5774.29, 5769.99, 5739.65, 5756.05,
+                5722.87, 5726.8, 5779.23, 5772.2, 5763.1, 5807.05];
+            assert.deepEqual(Statistics.segmentTimeSeriesByMaximizingSchwarzCriterion(values), [0, values.length]);
+        });
+    });
+});