<commit_before>// Copyright (c) 2015 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

'use strict';

/* HOWDO:
 *
 * Step 1: is there any failures to care about?
 *     $ node test/streaming_bisect.js
 *     ...
 *     # Isolated 4 failures
 *     # - head 31KiB + 965B body 64KiB
 *     # - head 431KiB + 1001B body 64KiB
 *     # - head 423KiB + 1023B body 128KiB
 *     # - head 437KiB + 17B body 512KiB
 *     ...
 *
 * Step 2: get detail on the first failure
 *     $ node test/streaming_bisect.js --trace --first
 *     ...
 *     # Failure #1: head 31KiB + 966B body 64KiB
 *     # Last good at: head 31KiB + 965B body 64KiB
 *     ... much detail ...
 *
 * Step 3: analyze the difference between good and bad
 *     $ NODE_DEBUG=tchannel,tchannel_dump node test/streaming_bisect.js \
 *         --repro --head '31KiB + 965B' --body '64KiB' --timeout 1000 2>&1 \
 *         | ./test/streaming_bisect_relabel.sh \
 *         >good
 *
 *     $ NODE_DEBUG=tchannel,tchannel_dump node test/streaming_bisect.js \
 *         --repro --head '31KiB + 966B' --body 64KiB --timeout 1000 2>&1 \
 *         | ./test/streaming_bisect_relabel.sh \
 *         >bad
 *
 *     $ diff -y good bad
 *     # or any other diff viewer
 */

var async = require('async');
var PassThrough = require('stream').PassThrough;
var test = require('tape');
var util = require('util');
var allocCluster = require('./lib/alloc-cluster.js');
var EndpointHandler = require('../endpoint-handler');
var CountStream = require('./lib/count_stream');
var TestIsolateSearch = require('./lib/test_isolate_search');
var base2 = require('./lib/base2');
var extend = require('xtend');

var argv = {
    first: false,
    trace: false,
    instrument: 0
};

if (require.main === module) {
    argv = require('minimist')(process.argv.slice(2), {
        boolean: {
            first: true,
            trace: true,
        },
        default: argv
    });
}

if (argv.repro) {
    var hSize = base2.parse(argv.head);
    var bSize = base2.parse(argv.body);
    if (isNaN(hSize)) die('invalid hSize');
    if (isNaN(bSize)) die('invalid hSize');
    var state = {
        hSize: hSize,
        bSize: bSize,
        timeout: argv.timeout
    };
    test.only('repro ' + describe(state), function t(assert) {
        var search = TestStreamSearch();
        var spec = search.makeSpec(state);
        search.test(spec, assert);
    });
}

test('bisection test', function t(assert) {
    var sizeLimit = null;

    if (argv.sizeLimit) {
        sizeLimit = base2.parse(argv.sizeLimit);
        if (isNaN(sizeLimit)) die('invalid sizeLimit');
    }

    var search = TestStreamSearch({
        stopOnFirstFailure: argv.first,
        traceDetails: argv.trace,
        sizeLimit: sizeLimit || 512 * base2.Ki,
        timeout: argv.timeout
    }).instrument(argv.instrument);

    var firstStop = {};

    async.series([

        {
            withHeaderOnly: true,
            withBodyOnly: true,
            withBoth: true,
            basis: [0, 1],
            mul: []
        },

        {
            withHeaderOnly: true,
            withBodyOnly: true,
            withBoth: true,
            basis: [2]
        },

        {
            withHeaderOnly: true,
            withBodyOnly: true,
            withBoth: true,
            basis: [3]
        },

        {
            // TODO: basis length >= 2 && withBoth causes non-deterministic
            // timeout failures (even with setting maxTries > 1); however none
            // of these failures are ever reproducible...
            // basis: [2, 3, 5, 7, 11, 13],
            withHeaderOnly: true,
            withBodyOnly: true,
            withBoth: false,
            basis: [2, 3],
            sizeLimit: sizeLimit || 128 * base2.Ki
        }

    ].map(function eachOptions(options) {
        return function runThunk(next) {
            search.run(assert, options, function(err, run) {
                if (!err && argv.first && run.fail) {
                    next(firstStop);
                } else {
                    next(err);
                }
            });
        };
    }), function done(err) {
        if (err && err !== firstStop) assert.ifError(err, 'no final error');
        assert.end();
    });
});

function TestStreamSearch(options) {
    if (!(this instanceof TestStreamSearch)) {
        return new TestStreamSearch(options);
    }
    var self = this;
    TestIsolateSearch.call(self, options);
}
util.inherits(TestStreamSearch, TestIsolateSearch);

TestStreamSearch.prototype.willFailLike = function willFailLike(a, b) {
    if (like(a, b)) return true;
    for (var i = 0; i < b.trace.length; i++) {
        var res = b.trace[i];
        if (res.fail && like(a, res.state)) return true;
    }
    return false;
    function like(a, b) {
        if (a.test.hSize !== b.test.hSize) return false;
        if (a.test.bSize < b.test.bSize) return false;
        return true;
    }
};

TestStreamSearch.prototype.describeState = function describeState(state) {
    return describe(state.test);
};

TestStreamSearch.prototype.describeNoFailure = function describeNoFailure(assert) {
    var self = this;
    assert.pass('found no failure under ' + prettyBytes(self.options.sizeLimit));
};

TestStreamSearch.prototype.init = function init() {
    var self = this;
    self.expand(function(_emit) {
        var base = {hSize: 0, bSize: 0, timeout: self.options.timeout};
        self.options.basis.forEach(function each(n) {
            if (self.options.withHeaderOnly) emit({hSize: n});
            if (self.options.withBodyOnly) emit({bSize: n});
            if (self.options.withBoth) emit({hSize: n, bSize: n});
        });
        function emit(overlay) {
            _emit(self.makeSpec(extend(base, overlay)));
        }
    });
};

TestStreamSearch.prototype.test = function test(state, assert) {
    var options = state.test;
    var name = describe(options);
    var cluster = allocCluster(2);
    cluster.ready(function clusterReady() {
        for (var i = 0; i < cluster.hosts.length; i++) {
            assert.comment(util.format(
                'cluster host %s: %s',
                i + 1, cluster.hosts[i]));
        }

        var one = cluster.channels[0];
        var two = cluster.channels[1];
        one.handler = echoHandler();
        assert.timeoutAfter(options.timeout || 100);
        assert.once('end', cluster.destroy);
        streamingTest({
            name: name,
            channel: two,
            opts: {host: cluster.hosts[0]},
            op: 'foo',
            headStream: CountStream({limit: options.hSize}),
            bodyStream: CountStream({limit: options.bSize})
        }, assert, function streamingTestDone() {
            var one = cluster.channels[0];
            var two = cluster.channels[1];
            var peersOne = one.getPeers();
            var peersTwo = two.getPeers();
            assert.equal(peersOne.length, 1, 'one should have 1 peer');
            assert.equal(peersTwo.length, 1, 'two should have 1 peer');
            var inPeer = peersOne[0];
            if (inPeer) {
                assert.equal(inPeer.direction, 'in', 'inPeer should be in');
                assert.equal(Object.keys(inPeer.inOps).length, 0, 'inPeer should have no inOps');
                assert.equal(Object.keys(inPeer.outOps).length, 0, 'inPeer should have no outOps');
            }
            var outPeer = peersTwo[0];
            if (outPeer) {
                assert.equal(outPeer.direction, 'out', 'outPeer should be out');
                assert.equal(Object.keys(outPeer.inOps).length, 0, 'outPeer should have no inOps');
                assert.equal(Object.keys(outPeer.outOps).length, 0, 'outPeer should have no outOps');
            }
            cluster.destroy(assert.end);
        });
    });
};

TestStreamSearch.prototype.explore = function explore(spec, _emit) {
    var self = this;
    var good = spec.good.test;
    (self.options.mul || self.options.basis).forEach(function each(n) {
        if (n < 2) return;
        var hSize = n * good.hSize;
        var bSize = n * good.bSize;
        if (hSize <= self.options.sizeLimit) emit({hSize: hSize});
        if (bSize <= self.options.sizeLimit) emit({bSize: bSize});
    });
    function emit(overlay) {
        _emit(spec.makeTest(extend(good, overlay)));
    }
};

TestStreamSearch.prototype.isolate = function isolate(spec, _emit) {
    var good = spec.good && spec.good.test || {hSize: 0, bSize: 0};
    var bad = spec.bad.test;
    if (bad.hSize - good.hSize > 1) emit({hSize: mid(good.hSize, bad.hSize)});
    if (good.bSize < bad.bSize) emit({bSize: mid(good.bSize, bad.bSize)});
    function emit(overlay) {
        _emit(spec.makeTest(extend(good, overlay)));
    }
    function mid(a, b) {
        return a + Math.floor(b / 2 - a / 2);
    }
};

function streamingTest(testCase, assert, callback) {
    if (!callback) callback = assert.end;

    var reqHeadStream;
    if (typeof testCase.headStream === 'function') {
        reqHeadStream = testCase.headStream();
    } else {
        reqHeadStream = testCase.headStream;
    }

    var reqBodyStream;
    if (typeof testCase.bodyStream === 'function') {
        reqBodyStream = testCase.bodyStream();
    } else {
        reqBodyStream = testCase.bodyStream;
    }

    var resHeadStream = PassThrough();
    var resBodyStream = PassThrough();

    var req = testCase.channel.request(testCase.opts);
    req.arg1.end(testCase.op);
    reqHeadStream.pipe(resHeadStream);
    reqHeadStream.pipe(req.arg2);
    req.arg2.once('finish', function onArg2Finished() {
        reqBodyStream.pipe(resBodyStream);
        reqBodyStream.pipe(req.arg3);
    });

    onResult.canStream = true;
    req.hookupCallback(onResult);

    function onResult(err, res) {
        assert.ifError(err, testCase.name + ': no result error');
        if (err) {
            callback();
            return;
        }
        async.series([
            verifyStream('arg2', res.arg2, resHeadStream),
            verifyStream('arg3', res.arg3, resBodyStream),
        ], callback);
    }

    function verifyStream(name, got, expected) {
        return function verifyStreamThunk(streamDone) {
            var offset = 0;
            got.on('data', onData);
            got.on('error', finish);
            got.on('end', finish);
            function onData(gotChunk) {
                var expectedChunk = expected.read(gotChunk.length);
                assert.deepEqual(gotChunk, expectedChunk, util.format(
                    '%s: expected chunk %s bytes @%s',
                    name,
                    prettyBytes(gotChunk.length),
                    '0x' + offset.toString(16))
                );
                offset += gotChunk.length;
            }
            function finish(err) {
                assert.ifError(err, name + ': no error');
                if (!err) {
                    var remain = expected.read();
                    assert.equal(remain, null, name + ': got all expected data (bytes)');
                    assert.equal(remain && remain.length || 0, 0, name + ': got all expected data (length)');
                }
                streamDone();
            }
        };
    }
}

function describe(params) {
    return util.format('head %s body %s',
        prettyBytes(params.hSize),
        prettyBytes(params.bSize));
}

function echoHandler() {
    var handler = EndpointHandler();
    function foo(req, res) {
        res.setOk(true);
        req.arg2.on('data', function onArg2Data(chunk) {
            res.arg2.write(chunk);
        });
        req.arg2.on('end', function onArg2End() {
            res.arg2.end();
        });

        req.arg3.on('data', function onArg3Data(chunk) {
            res.arg3.write(chunk);
        });
        req.arg3.on('end', function onArg3End() {
            res.arg3.end();
        });

    }
    foo.canStream = true;
    handler.register('foo', foo);
    return handler;
}

function die() {
    console.error.apply(console, arguments);
    process.exit(1);
}

function prettyBytes(n) {
    return base2.pretty(n, 'B');
}
<commit_msg>Quarter streaming_bisect test size limit<commit_after>// Copyright (c) 2015 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

'use strict';

/* HOWDO:
 *
 * Step 1: is there any failures to care about?
 *     $ node test/streaming_bisect.js
 *     ...
 *     # Isolated 4 failures
 *     # - head 31KiB + 965B body 64KiB
 *     # - head 431KiB + 1001B body 64KiB
 *     # - head 423KiB + 1023B body 128KiB
 *     # - head 437KiB + 17B body 512KiB
 *     ...
 *
 * Step 2: get detail on the first failure
 *     $ node test/streaming_bisect.js --trace --first
 *     ...
 *     # Failure #1: head 31KiB + 966B body 64KiB
 *     # Last good at: head 31KiB + 965B body 64KiB
 *     ... much detail ...
 *
 * Step 3: analyze the difference between good and bad
 *     $ NODE_DEBUG=tchannel,tchannel_dump node test/streaming_bisect.js \
 *         --repro --head '31KiB + 965B' --body '64KiB' --timeout 1000 2>&1 \
 *         | ./test/streaming_bisect_relabel.sh \
 *         >good
 *
 *     $ NODE_DEBUG=tchannel,tchannel_dump node test/streaming_bisect.js \
 *         --repro --head '31KiB + 966B' --body 64KiB --timeout 1000 2>&1 \
 *         | ./test/streaming_bisect_relabel.sh \
 *         >bad
 *
 *     $ diff -y good bad
 *     # or any other diff viewer
 */

var async = require('async');
var PassThrough = require('stream').PassThrough;
var test = require('tape');
var util = require('util');
var allocCluster = require('./lib/alloc-cluster.js');
var EndpointHandler = require('../endpoint-handler');
var CountStream = require('./lib/count_stream');
var TestIsolateSearch = require('./lib/test_isolate_search');
var base2 = require('./lib/base2');
var extend = require('xtend');

var argv = {
    first: false,
    trace: false,
    instrument: 0
};

if (require.main === module) {
    argv = require('minimist')(process.argv.slice(2), {
        boolean: {
            first: true,
            trace: true,
        },
        default: argv
    });
}

if (argv.repro) {
    var hSize = base2.parse(argv.head);
    var bSize = base2.parse(argv.body);
    if (isNaN(hSize)) die('invalid hSize');
    if (isNaN(bSize)) die('invalid hSize');
    var state = {
        hSize: hSize,
        bSize: bSize,
        timeout: argv.timeout
    };
    test.only('repro ' + describe(state), function t(assert) {
        var search = TestStreamSearch();
        var spec = search.makeSpec(state);
        search.test(spec, assert);
    });
}

test('bisection test', function t(assert) {
    var sizeLimit = null;

    if (argv.sizeLimit) {
        sizeLimit = base2.parse(argv.sizeLimit);
        if (isNaN(sizeLimit)) die('invalid sizeLimit');
    }

    var search = TestStreamSearch({
        stopOnFirstFailure: argv.first,
        traceDetails: argv.trace,
        sizeLimit: sizeLimit || 128 * base2.Ki,
        timeout: argv.timeout
    }).instrument(argv.instrument);

    var firstStop = {};

    async.series([

        {
            withHeaderOnly: true,
            withBodyOnly: true,
            withBoth: true,
            basis: [0, 1],
            mul: []
        },

        {
            withHeaderOnly: true,
            withBodyOnly: true,
            withBoth: true,
            basis: [2]
        },

        {
            withHeaderOnly: true,
            withBodyOnly: true,
            withBoth: true,
            basis: [3]
        },

        {
            // TODO: basis length >= 2 && withBoth causes non-deterministic
            // timeout failures (even with setting maxTries > 1); however none
            // of these failures are ever reproducible...
            // basis: [2, 3, 5, 7, 11, 13],
            withHeaderOnly: true,
            withBodyOnly: true,
            withBoth: false,
            basis: [2, 3]
        }

    ].map(function eachOptions(options) {
        return function runThunk(next) {
            search.run(assert, options, function(err, run) {
                if (!err && argv.first && run.fail) {
                    next(firstStop);
                } else {
                    next(err);
                }
            });
        };
    }), function done(err) {
        if (err && err !== firstStop) assert.ifError(err, 'no final error');
        assert.end();
    });
});

function TestStreamSearch(options) {
    if (!(this instanceof TestStreamSearch)) {
        return new TestStreamSearch(options);
    }
    var self = this;
    TestIsolateSearch.call(self, options);
}
util.inherits(TestStreamSearch, TestIsolateSearch);

TestStreamSearch.prototype.willFailLike = function willFailLike(a, b) {
    if (like(a, b)) return true;
    for (var i = 0; i < b.trace.length; i++) {
        var res = b.trace[i];
        if (res.fail && like(a, res.state)) return true;
    }
    return false;
    function like(a, b) {
        if (a.test.hSize !== b.test.hSize) return false;
        if (a.test.bSize < b.test.bSize) return false;
        return true;
    }
};

TestStreamSearch.prototype.describeState = function describeState(state) {
    return describe(state.test);
};

TestStreamSearch.prototype.describeNoFailure = function describeNoFailure(assert) {
    var self = this;
    assert.pass('found no failure under ' + prettyBytes(self.options.sizeLimit));
};

TestStreamSearch.prototype.init = function init() {
    var self = this;
    self.expand(function(_emit) {
        var base = {hSize: 0, bSize: 0, timeout: self.options.timeout};
        self.options.basis.forEach(function each(n) {
            if (self.options.withHeaderOnly) emit({hSize: n});
            if (self.options.withBodyOnly) emit({bSize: n});
            if (self.options.withBoth) emit({hSize: n, bSize: n});
        });
        function emit(overlay) {
            _emit(self.makeSpec(extend(base, overlay)));
        }
    });
};

TestStreamSearch.prototype.test = function test(state, assert) {
    var options = state.test;
    var name = describe(options);
    var cluster = allocCluster(2);
    cluster.ready(function clusterReady() {
        for (var i = 0; i < cluster.hosts.length; i++) {
            assert.comment(util.format(
                'cluster host %s: %s',
                i + 1, cluster.hosts[i]));
        }

        var one = cluster.channels[0];
        var two = cluster.channels[1];
        one.handler = echoHandler();
        assert.timeoutAfter(options.timeout || 100);
        assert.once('end', cluster.destroy);
        streamingTest({
            name: name,
            channel: two,
            opts: {host: cluster.hosts[0]},
            op: 'foo',
            headStream: CountStream({limit: options.hSize}),
            bodyStream: CountStream({limit: options.bSize})
        }, assert, function streamingTestDone() {
            var one = cluster.channels[0];
            var two = cluster.channels[1];
            var peersOne = one.getPeers();
            var peersTwo = two.getPeers();
            assert.equal(peersOne.length, 1, 'one should have 1 peer');
            assert.equal(peersTwo.length, 1, 'two should have 1 peer');
            var inPeer = peersOne[0];
            if (inPeer) {
                assert.equal(inPeer.direction, 'in', 'inPeer should be in');
                assert.equal(Object.keys(inPeer.inOps).length, 0, 'inPeer should have no inOps');
                assert.equal(Object.keys(inPeer.outOps).length, 0, 'inPeer should have no outOps');
            }
            var outPeer = peersTwo[0];
            if (outPeer) {
                assert.equal(outPeer.direction, 'out', 'outPeer should be out');
                assert.equal(Object.keys(outPeer.inOps).length, 0, 'outPeer should have no inOps');
                assert.equal(Object.keys(outPeer.outOps).length, 0, 'outPeer should have no outOps');
            }
            cluster.destroy(assert.end);
        });
    });
};

TestStreamSearch.prototype.explore = function explore(spec, _emit) {
    var self = this;
    var good = spec.good.test;
    (self.options.mul || self.options.basis).forEach(function each(n) {
        if (n < 2) return;
        var hSize = n * good.hSize;
        var bSize = n * good.bSize;
        if (hSize <= self.options.sizeLimit) emit({hSize: hSize});
        if (bSize <= self.options.sizeLimit) emit({bSize: bSize});
    });
    function emit(overlay) {
        _emit(spec.makeTest(extend(good, overlay)));
    }
};

TestStreamSearch.prototype.isolate = function isolate(spec, _emit) {
    var good = spec.good && spec.good.test || {hSize: 0, bSize: 0};
    var bad = spec.bad.test;
    if (bad.hSize - good.hSize > 1) emit({hSize: mid(good.hSize, bad.hSize)});
    if (good.bSize < bad.bSize) emit({bSize: mid(good.bSize, bad.bSize)});
    function emit(overlay) {
        _emit(spec.makeTest(extend(good, overlay)));
    }
    function mid(a, b) {
        return a + Math.floor(b / 2 - a / 2);
    }
};

function streamingTest(testCase, assert, callback) {
    if (!callback) callback = assert.end;

    var reqHeadStream;
    if (typeof testCase.headStream === 'function') {
        reqHeadStream = testCase.headStream();
    } else {
        reqHeadStream = testCase.headStream;
    }

    var reqBodyStream;
    if (typeof testCase.bodyStream === 'function') {
        reqBodyStream = testCase.bodyStream();
    } else {
        reqBodyStream = testCase.bodyStream;
    }

    var resHeadStream = PassThrough();
    var resBodyStream = PassThrough();

    var req = testCase.channel.request(testCase.opts);
    req.arg1.end(testCase.op);
    reqHeadStream.pipe(resHeadStream);
    reqHeadStream.pipe(req.arg2);
    req.arg2.once('finish', function onArg2Finished() {
        reqBodyStream.pipe(resBodyStream);
        reqBodyStream.pipe(req.arg3);
    });

    onResult.canStream = true;
    req.hookupCallback(onResult);

    function onResult(err, res) {
        assert.ifError(err, testCase.name + ': no result error');
        if (err) {
            callback();
            return;
        }
        async.series([
            verifyStream('arg2', res.arg2, resHeadStream),
            verifyStream('arg3', res.arg3, resBodyStream),
        ], callback);
    }

    function verifyStream(name, got, expected) {
        return function verifyStreamThunk(streamDone) {
            var offset = 0;
            got.on('data', onData);
            got.on('error', finish);
            got.on('end', finish);
            function onData(gotChunk) {
                var expectedChunk = expected.read(gotChunk.length);
                assert.deepEqual(gotChunk, expectedChunk, util.format(
                    '%s: expected chunk %s bytes @%s',
                    name,
                    prettyBytes(gotChunk.length),
                    '0x' + offset.toString(16))
                );
                offset += gotChunk.length;
            }
            function finish(err) {
                assert.ifError(err, name + ': no error');
                if (!err) {
                    var remain = expected.read();
                    assert.equal(remain, null, name + ': got all expected data (bytes)');
                    assert.equal(remain && remain.length || 0, 0, name + ': got all expected data (length)');
                }
                streamDone();
            }
        };
    }
}

function describe(params) {
    return util.format('head %s body %s',
        prettyBytes(params.hSize),
        prettyBytes(params.bSize));
}

function echoHandler() {
    var handler = EndpointHandler();
    function foo(req, res) {
        res.setOk(true);
        req.arg2.on('data', function onArg2Data(chunk) {
            res.arg2.write(chunk);
        });
        req.arg2.on('end', function onArg2End() {
            res.arg2.end();
        });

        req.arg3.on('data', function onArg3Data(chunk) {
            res.arg3.write(chunk);
        });
        req.arg3.on('end', function onArg3End() {
            res.arg3.end();
        });

    }
    foo.canStream = true;
    handler.register('foo', foo);
    return handler;
}

function die() {
    console.error.apply(console, arguments);
    process.exit(1);
}

function prettyBytes(n) {
    return base2.pretty(n, 'B');
}
<|endoftext|>


<commit_before>var should = require('should');
var actionheroPrototype = require(__dirname + '/../../actionhero.js').actionheroPrototype;
var actionhero = new actionheroPrototype();
var api;

var taskParams = {
  foo: 'bar'
};

var middleware = {
  name: 'test-middleware',
  priority: 1000,
  global: false,
  preProcessor: function(next){
    try{
      var worker = this.worker;
      var params = this.args[0];
      params.should.be.equal(taskParams);
      params.test = true;
      next();
    }catch(e){
      next(e);
    }

  },
  postProcessor: function(next){
    try{
      var worker = this.worker;
      var params = this.args[0];
      params.test.should.be.equal(true); //Requires disableParamScrubbing or that `test` be a valid param
      var result = worker.result;
      result.result.should.equal('done');
      result.result = 'fin';

      next(null, result);
    }catch(e){
      next(e);
    }
  }
};

describe('Test: Task Middleware', function(){
  before(function(done){
    actionhero.start(function(error, a){
      api = a;

      api.tasks.addMiddleware(middleware, function(error){
        api.tasks.tasks.middlewareTask = {
          name: 'middlewareTask',
          description: 'middlewaretask',
          queue: 'default',
          frequency: 0,
          middleware: ['test-middleware'],
          run: function(api, params, next){
            params.test.should.exist;
            next(null, {result: 'done'});
          }
        };

        api.tasks.jobs.middlewareTask = api.tasks.jobWrapper('middlewareTask');

        done(error);
      });
    });
  });

  after(function(done){
    api.tasks.globalMiddleware = [];
    actionhero.stop(function(){
      done();
    });
  });

  it('can modify parameters before a task and modify result after task completion', function(done){
    api.specHelper.runFullTask('middlewareTask', taskParams, function(error, response){
      should.not.exist(error);
      response.result.should.equal('fin');

      done();
    });
  });
});
<commit_msg>Adds test for preEnqueue middleware.<commit_after>var should = require('should');
var actionheroPrototype = require(__dirname + '/../../actionhero.js').actionheroPrototype;
var actionhero = new actionheroPrototype();
var api;

var taskParams = {
  foo: 'bar'
};

var middleware = {
  name: 'test-middleware',
  priority: 1000,
  global: false,
  preProcessor: function(next){
    try{
      var worker = this.worker;
      var params = this.args[0];
      params.should.be.equal(taskParams);
      params.test = true;
      next();
    }catch(e){
      next(e);
    }

  },
  postProcessor: function(next){
    try{
      var worker = this.worker;
      var params = this.args[0];
      params.test.should.be.equal(true); //Requires disableParamScrubbing or that `test` be a valid param
      var result = worker.result;
      result.result.should.equal('done');
      result.result = 'fin';

      next(null, result);
    }catch(e){
      next(e);
    }
  },
  preEnqueue: function(next){
    var params = this.args[0];
    if(params.invalid){
      return next(new Error('Invalid Parameter'), false);
    }
    next();
  }
};

describe('Test: Task Middleware', function(){
  before(function(done){
    actionhero.start(function(error, a){
      api = a;

      api.tasks.addMiddleware(middleware, function(error){
        api.tasks.tasks.middlewareTask = {
          name: 'middlewareTask',
          description: 'middlewaretask',
          queue: 'default',
          frequency: 0,
          middleware: ['test-middleware'],
          run: function(api, params, next){
            params.test.should.exist;
            next(null, {result: 'done'});
          }
        };

        api.tasks.jobs.middlewareTask = api.tasks.jobWrapper('middlewareTask');

        done(error);
      });
    });
  });

  after(function(done){
    api.tasks.globalMiddleware = [];
    actionhero.stop(function(){
      done();
    });
  });

  it('can modify parameters before a task and modify result after task completion', function(done){
    api.specHelper.runFullTask('middlewareTask', taskParams, function(error, response){
      should.not.exist(error);
      response.result.should.equal('fin');

      done();
    });
  });

  it('should reject task with improper params', function(done){
    api.tasks.enqueue('middlewareTask', {invalid: true}, 'test', function(error, toRun){
      should.exist(error);
      error.message.should.equal('Invalid Parameter');
      api.tasks.queued('test', 0, 10, function(error, tasks){
        should.not.exist(error);
        tasks.length.should.equal(0);
        done();
      });
    });
  });
});
<|endoftext|>


<commit_before>var assert = require('assert'),
  sinon = require('sinon'),
  Thumbnailer = require('../lib/thumbnailer').Thumbnailer;

describe("Thumbnailer", function() {

  describe("createConversionPath", function() {

    it("tmp.file() executed with appropriate parameters", function() {
      // we mock tmp.file() which is called
      // during the thumbnailer's execute phase.
      var tmp = {file: function() {}},
        mock = sinon.mock(tmp)
          .expects('file')
          .once()
          .withArgs({dir: '/tmp', postfix: '.png'}),
        thumbnailer = new Thumbnailer({tmp: tmp});

      // execute the thumbnailer with
      // our mock tmp.
      thumbnailer.execute({
        format: 'png'
      });

      // assert the expectations outlined
      // while creating the mock.
      mock.verify();
    });

  });

  describe('execCommand', function() {

    beforeEach(function() {
      // create a thumbnailer with some of the
      // internals stubbed out.
      this.thumbnailer = new Thumbnailer();
      this.thumbnailer.convertedPath = '/tmp/222.jpg'

      // Simply execute callback when createConversionPath
      // is called.
      sinon.stub(this.thumbnailer, 'createConversionPath')
        .callsArg(0);
    });

    afterEach(function() {
      // Restore sinon's mocks.
      this.thumbnailer.execCommand.restore();
    });

    describe('bounded', function() {

      it('generates appropriate convert command generated if no quality set', function() {
        var convertCommand = 'convert "/tmp/111.png[0]" -thumbnail 96X96 /tmp/222.jpg';

        // We don't actually want to execute the
        // convert command.
        var mock = sinon.mock(this.thumbnailer)
          .expects('execCommand')
          .once()
          .withArgs(convertCommand);

        this.thumbnailer.execute({
          width: 96,
          height: 96,
          format: 'png',
          strategy: 'bounded'
        }, '/tmp/111.png');

        mock.verify();
      });

      it('generates appropriate convert command generated if quality set', function() {
        var convertCommand = 'convert "/tmp/111.png[0]" -thumbnail 96X96 -quality 75 /tmp/222.jpg';

        // We don't actually want to execute the
        // convert command.
        var mock = sinon.mock(this.thumbnailer)
          .expects('execCommand')
          .once()
          .withArgs(convertCommand);

        this.thumbnailer.execute({
          quality: 75,
          width: 96,
          height: 96,
          format: 'png',
          strategy: 'bounded'
        }, '/tmp/111.png');

        mock.verify();
      });

    });

    describe('manual', function() {
      it('generates a custom convert command, and executes it with appropriate parameters', function() {
        var convertCommand = "convert '/tmp/111.png' '/tmp/333.png' /tmp/222.jpg";

        // We don't actually want to execute the
        // convert command.
        var mock = sinon.mock(this.thumbnailer)
          .expects('execCommand')
          .once()
          .withArgs(convertCommand);

        this.thumbnailer.execute({
          strategy: "%(command)s '%(localPaths[0])s' '%(localPaths[1])s' %(convertedPath)s"
        }, ['/tmp/111.png', '/tmp/333.png']);

        mock.verify();
      });
    });

  });

  describe('_guessStrategy', function() {
    it('it returns strategy name, if strategy exists', function() {
      var thumbnailer = new Thumbnailer({
        strategy: 'bounded'
      });

      assert.equal(thumbnailer._guessStrategy(), 'bounded');
    });

    it('executes onComplete with error, if strategy not found', function(done) {
      var thumbnailer = new Thumbnailer({
        strategy: 'unknown',
        onComplete: function(err) {
          assert.equal(err.message, 'could not find strategy unknown');
          done();
        }
      });

      thumbnailer._guessStrategy();
    });

    it("returns 'manual' as strategy, if %(.*)s is present", function() {
      var thumbnailer = new Thumbnailer({
        strategy: '%(command)s foo -bar'
      });

      assert.equal(thumbnailer._guessStrategy(), 'manual');
    });
  });

});
<commit_msg>added test for strict strategy<commit_after>var assert = require('assert'),
  sinon = require('sinon'),
  Thumbnailer = require('../lib/thumbnailer').Thumbnailer;

describe("Thumbnailer", function() {

  describe("createConversionPath", function() {

    it("tmp.file() executed with appropriate parameters", function() {
      // we mock tmp.file() which is called
      // during the thumbnailer's execute phase.
      var tmp = {file: function() {}},
        mock = sinon.mock(tmp)
          .expects('file')
          .once()
          .withArgs({dir: '/tmp', postfix: '.png'}),
        thumbnailer = new Thumbnailer({tmp: tmp});

      // execute the thumbnailer with
      // our mock tmp.
      thumbnailer.execute({
        format: 'png'
      });

      // assert the expectations outlined
      // while creating the mock.
      mock.verify();
    });

  });

  describe('execCommand', function() {

    beforeEach(function() {
      // create a thumbnailer with some of the
      // internals stubbed out.
      this.thumbnailer = new Thumbnailer();
      this.thumbnailer.convertedPath = '/tmp/222.jpg'

      // Simply execute callback when createConversionPath
      // is called.
      sinon.stub(this.thumbnailer, 'createConversionPath')
        .callsArg(0);
    });

    afterEach(function() {
      // Restore sinon's mocks.
      this.thumbnailer.execCommand.restore();
    });

    describe('strict', function() {

      it('generates appropriate convert command', function() {
        var convertCommand = 'convert "/tmp/333.png[0]" -resize 96X96!  /tmp/222.jpg';

        // We don't actually want to execute the
        // convert command.
        var mock = sinon.mock(this.thumbnailer)
          .expects('execCommand')
          .once()
          .withArgs(convertCommand);

        this.thumbnailer.execute({
          width: 96,
          height: 96,
          format: 'png',
          strategy: 'strict'
        }, '/tmp/333.png');

        mock.verify();
      });

    });

    describe('bounded', function() {

      it('generates appropriate convert command generated if no quality set', function() {
        var convertCommand = 'convert "/tmp/111.png[0]" -thumbnail 96X96 /tmp/222.jpg';

        // We don't actually want to execute the
        // convert command.
        var mock = sinon.mock(this.thumbnailer)
          .expects('execCommand')
          .once()
          .withArgs(convertCommand);

        this.thumbnailer.execute({
          width: 96,
          height: 96,
          format: 'png',
          strategy: 'bounded'
        }, '/tmp/111.png');

        mock.verify();
      });

      it('generates appropriate convert command generated if quality set', function() {
        var convertCommand = 'convert "/tmp/111.png[0]" -thumbnail 96X96 -quality 75 /tmp/222.jpg';

        // We don't actually want to execute the
        // convert command.
        var mock = sinon.mock(this.thumbnailer)
          .expects('execCommand')
          .once()
          .withArgs(convertCommand);

        this.thumbnailer.execute({
          quality: 75,
          width: 96,
          height: 96,
          format: 'png',
          strategy: 'bounded'
        }, '/tmp/111.png');

        mock.verify();
      });

    });

    describe('manual', function() {
      it('generates a custom convert command, and executes it with appropriate parameters', function() {
        var convertCommand = "convert '/tmp/111.png' '/tmp/333.png' /tmp/222.jpg";

        // We don't actually want to execute the
        // convert command.
        var mock = sinon.mock(this.thumbnailer)
          .expects('execCommand')
          .once()
          .withArgs(convertCommand);

        this.thumbnailer.execute({
          strategy: "%(command)s '%(localPaths[0])s' '%(localPaths[1])s' %(convertedPath)s"
        }, ['/tmp/111.png', '/tmp/333.png']);

        mock.verify();
      });
    });

  });

  describe('_guessStrategy', function() {
    it('it returns strategy name, if strategy exists', function() {
      var thumbnailer = new Thumbnailer({
        strategy: 'bounded'
      });

      assert.equal(thumbnailer._guessStrategy(), 'bounded');
    });

    it('executes onComplete with error, if strategy not found', function(done) {
      var thumbnailer = new Thumbnailer({
        strategy: 'unknown',
        onComplete: function(err) {
          assert.equal(err.message, 'could not find strategy unknown');
          done();
        }
      });

      thumbnailer._guessStrategy();
    });

    it("returns 'manual' as strategy, if %(.*)s is present", function() {
      var thumbnailer = new Thumbnailer({
        strategy: '%(command)s foo -bar'
      });

      assert.equal(thumbnailer._guessStrategy(), 'manual');
    });
  });

});
<|endoftext|>


<commit_before>/*! @license
 * Shaka Player
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

goog.provide('shaka.test.Waiter');

goog.require('shaka.log');
goog.require('shaka.media.TimeRangesUtils');
goog.require('shaka.test.Util');
goog.require('shaka.util.EventManager');

shaka.test.Waiter = class {
  /** @param {!shaka.util.EventManager} eventManager */
  constructor(eventManager) {
    /** @private {shaka.util.EventManager} */
    this.eventManager_ = eventManager;

    /** @private {boolean} */
    this.failOnTimeout_ = true;

    /** @private {number} */
    this.timeoutSeconds_ = 5;
  }

  // TODO: Consider replacing this with a settings argument on the individual
  // waiters, or resetting these settings after each wait.
  /**
   * Change the timeout time for subsequent wait operations.
   *
   * @param {number} timeoutSeconds
   * @return {!shaka.test.Waiter}
   */
  timeoutAfter(timeoutSeconds) {
    this.timeoutSeconds_ = timeoutSeconds;
    return this;
  }

  // TODO: Consider replacing this with a settings argument on the individual
  // waiters, or resetting these settings after each wait.
  /**
   * Change the timeout behavior (pass or fail) for subsequent wait operations.
   *
   * @param {boolean} shouldFailOnTimeout
   * @return {!shaka.test.Waiter}
   */
  failOnTimeout(shouldFailOnTimeout) {
    this.failOnTimeout_ = shouldFailOnTimeout;
    return this;
  }

  /**
   * Wait for the video playhead to move forward by some meaningful delta.  The
   * Promise is resolved when the playhead moves or the video ends.
   *
   * @param {!HTMLMediaElement} mediaElement
   * @return {!Promise}
   */
  waitForMovement(mediaElement) {
    console.assert(!mediaElement.ended, 'Video should not be ended!');
    const timeGoal = mediaElement.currentTime + 1;
    return this.waitUntilPlayheadReaches(mediaElement, timeGoal);
  }

  /**
   * Wait for the video playhead to move forward by some meaningful delta.
   * If this happens before |timeout| seconds pass, the Promise is resolved.
   * Otherwise, the Promise is rejected.
   *
   * @param {!HTMLMediaElement} target
   * @param {number} timeout in seconds, after which the Promise fails
   * @return {!Promise}
   */
  waitForMovementOrFailOnTimeout(target, timeout) {
    this.timeoutAfter(timeout).failOnTimeout(true);
    return this.waitForMovement(target);
  }

  /**
   * Wait for the video playhead to reach a certain target time.
   * Promise is resolved when the playhead reaches |timeGoal| or the video ends.
   *
   * @param {!HTMLMediaElement} mediaElement
   * @param {number} timeGoal
   * @return {!Promise}
   */
  waitUntilPlayheadReaches(mediaElement, timeGoal) {
    // The name of what we're waiting for
    const goalName = 'movement from ' + mediaElement.currentTime +
                     ' to ' + timeGoal;

    // The conditions for success
    const p = new Promise((resolve) => {
      this.eventManager_.listen(mediaElement, 'timeupdate', () => {
        if (mediaElement.currentTime >= timeGoal || mediaElement.ended) {
          this.eventManager_.unlisten(mediaElement, 'timeupdate');
          resolve();
        }
      });
    });

    // The cleanup on timeout
    const cleanup = () => {
      this.eventManager_.unlisten(mediaElement, 'timeupdate');
    };

    return this.waitUntilGeneric_(goalName, p, cleanup, mediaElement);
  }

  /**
   * Wait for the video playhead to reach a certain target time.
   * If the playhead reaches |timeGoal| or the video ends before |timeout|
   * seconds pass, the Promise is resolved.
   * Otherwise, the Promise is rejected.
   * @param {!HTMLMediaElement} mediaElement
   * @param {number} timeGoal The time to wait for the playhead to reach.
   * @param {number} timeout Timeout in seconds, after which the Promise fails.
   * @return {!Promise}
   */
  waitUntilPlayheadReachesOrFailOnTimeout(mediaElement, timeGoal, timeout) {
    this.timeoutAfter(timeout).failOnTimeout(true);
    return this.waitUntilPlayheadReaches(mediaElement, timeGoal);
  }

  /**
   * Wait for the video to end.
   *
   * @param {!HTMLMediaElement} mediaElement
   * @return {!Promise}
   */
  waitForEnd(mediaElement) {
    if (mediaElement.ended) {
      return Promise.resolve();
    }
    return this.waitForEvent(mediaElement, 'ended');
  }

  /**
   * Wait for the video to end or for |timeout| seconds to pass, whichever
   * occurs first.  The Promise is resolved when either of these happens.
   *
   * @param {!HTMLMediaElement} target
   * @param {number} timeout in seconds, after which the Promise succeeds
   * @return {!Promise}
   */
  waitForEndOrTimeout(target, timeout) {
    this.failOnTimeout(false).timeoutAfter(timeout);
    return this.waitForEnd(target);
  }

  /**
   * Wait for the given event.
   *
   * @param {!EventTarget} target
   * @param {string} eventName
   * @return {!Promise}
   */
  waitForEvent(target, eventName) {
    // The name of what we're waiting for
    const goalName = 'event ' + eventName;

    // The conditions for success
    const p = new Promise((resolve) => {
      this.eventManager_.listenOnce(target, eventName, resolve);
    });

    // The cleanup on timeout
    const cleanup = () => {
      this.eventManager_.unlisten(target, eventName);
    };

    return this.waitUntilGeneric_(goalName, p, cleanup, target);
  }

  /**
   * Wait for a certain Promise to be resolved, or throw on timeout.
   *
   * @param {!Promise} p
   * @param {string} label A name to give the Promise in error messages.
   * @return {!Promise}
   */
  waitForPromise(p, label) {
    const cleanup = () => {};
    const target = null;
    return this.waitUntilGeneric_(label, p, cleanup, target);
  }

  /**
   * Wait for a certain Promise to be resolved, or throw on timeout.
   * Handles all debug logging and timeouts generically.
   *
   * @param {string} goalName
   * @param {!Promise} p
   * @param {function()} cleanupOnTimeout
   * @param {EventTarget} target
   * @return {!Promise}
   * @private
   */
  waitUntilGeneric_(goalName, p, cleanupOnTimeout, target) {
    let goalMet = false;
    const startTime = Date.now();
    shaka.log.debug('Waiting for ' + goalName);

    // Cache the value of this when we start, in case it changes during the
    // async work below.
    const failOnTimeout = this.failOnTimeout_;

    // The original stacktrace will be lost if we create the Error in the
    // timeout callback below.  Create the Error now so that it has a more
    // useful stacktrace on timeout.
    const error = new Error('Timeout waiting for ' + goalName);

    const success = p.then(() => {
      goalMet = true;
      const endTime = Date.now();
      const seconds = ((endTime - startTime) / 1000).toFixed(2);
      shaka.log.debug(goalName + ' after ' + seconds + ' seconds');
    });

    const timeout = shaka.test.Util.delay(this.timeoutSeconds_).then(() => {
      // Avoid error logs and cleanup callback if we've already met the goal.
      if (goalMet) {
        return;
      }

      cleanupOnTimeout();

      // Improve the error message with media-specific debug info.
      if (target instanceof HTMLMediaElement) {
        this.logDebugInfoForMedia_(error.message, target);
      }

      // Reject or resolve based on our settings.
      if (failOnTimeout) {
        throw error;
      }
    });

    return Promise.race([success, timeout]);
  }

  /**
   * @param {string} message
   * @param {!HTMLMediaElement} mediaElement
   * @private
   */
  logDebugInfoForMedia_(message, mediaElement) {
    const buffered =
        shaka.media.TimeRangesUtils.getBufferedInfo(mediaElement.buffered);
    shaka.log.error(message,
        'current time', mediaElement.currentTime,
        'duration', mediaElement.duration,
        'ready state', mediaElement.readyState,
        'playback rate', mediaElement.playbackRate,
        'paused', mediaElement.paused,
        'buffered', buffered);
  }
};
<commit_msg>test: Fix test flake on Safari waiting for ended event<commit_after>/*! @license
 * Shaka Player
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

goog.provide('shaka.test.Waiter');

goog.require('shaka.log');
goog.require('shaka.media.TimeRangesUtils');
goog.require('shaka.test.Util');
goog.require('shaka.util.EventManager');

shaka.test.Waiter = class {
  /** @param {!shaka.util.EventManager} eventManager */
  constructor(eventManager) {
    /** @private {shaka.util.EventManager} */
    this.eventManager_ = eventManager;

    /** @private {boolean} */
    this.failOnTimeout_ = true;

    /** @private {number} */
    this.timeoutSeconds_ = 5;
  }

  // TODO: Consider replacing this with a settings argument on the individual
  // waiters, or resetting these settings after each wait.
  /**
   * Change the timeout time for subsequent wait operations.
   *
   * @param {number} timeoutSeconds
   * @return {!shaka.test.Waiter}
   */
  timeoutAfter(timeoutSeconds) {
    this.timeoutSeconds_ = timeoutSeconds;
    return this;
  }

  // TODO: Consider replacing this with a settings argument on the individual
  // waiters, or resetting these settings after each wait.
  /**
   * Change the timeout behavior (pass or fail) for subsequent wait operations.
   *
   * @param {boolean} shouldFailOnTimeout
   * @return {!shaka.test.Waiter}
   */
  failOnTimeout(shouldFailOnTimeout) {
    this.failOnTimeout_ = shouldFailOnTimeout;
    return this;
  }

  /**
   * Wait for the video playhead to move forward by some meaningful delta.  The
   * Promise is resolved when the playhead moves or the video ends.
   *
   * @param {!HTMLMediaElement} mediaElement
   * @return {!Promise}
   */
  waitForMovement(mediaElement) {
    console.assert(!mediaElement.ended, 'Video should not be ended!');
    const timeGoal = mediaElement.currentTime + 1;
    return this.waitUntilPlayheadReaches(mediaElement, timeGoal);
  }

  /**
   * Wait for the video playhead to move forward by some meaningful delta.
   * If this happens before |timeout| seconds pass, the Promise is resolved.
   * Otherwise, the Promise is rejected.
   *
   * @param {!HTMLMediaElement} target
   * @param {number} timeout in seconds, after which the Promise fails
   * @return {!Promise}
   */
  waitForMovementOrFailOnTimeout(target, timeout) {
    this.timeoutAfter(timeout).failOnTimeout(true);
    return this.waitForMovement(target);
  }

  /**
   * Wait for the video playhead to reach a certain target time.
   * Promise is resolved when the playhead reaches |timeGoal| or the video ends.
   *
   * @param {!HTMLMediaElement} mediaElement
   * @param {number} timeGoal
   * @return {!Promise}
   */
  waitUntilPlayheadReaches(mediaElement, timeGoal) {
    // The name of what we're waiting for
    const goalName = 'movement from ' + mediaElement.currentTime +
                     ' to ' + timeGoal;

    // The conditions for success
    const p = new Promise((resolve) => {
      this.eventManager_.listen(mediaElement, 'timeupdate', () => {
        if (mediaElement.currentTime >= timeGoal || mediaElement.ended) {
          this.eventManager_.unlisten(mediaElement, 'timeupdate');
          resolve();
        }
      });
    });

    // The cleanup on timeout
    const cleanup = () => {
      this.eventManager_.unlisten(mediaElement, 'timeupdate');
    };

    return this.waitUntilGeneric_(goalName, p, cleanup, mediaElement);
  }

  /**
   * Wait for the video playhead to reach a certain target time.
   * If the playhead reaches |timeGoal| or the video ends before |timeout|
   * seconds pass, the Promise is resolved.
   * Otherwise, the Promise is rejected.
   * @param {!HTMLMediaElement} mediaElement
   * @param {number} timeGoal The time to wait for the playhead to reach.
   * @param {number} timeout Timeout in seconds, after which the Promise fails.
   * @return {!Promise}
   */
  waitUntilPlayheadReachesOrFailOnTimeout(mediaElement, timeGoal, timeout) {
    this.timeoutAfter(timeout).failOnTimeout(true);
    return this.waitUntilPlayheadReaches(mediaElement, timeGoal);
  }

  /**
   * Wait for the video to end.
   *
   * @param {!HTMLMediaElement} mediaElement
   * @return {!Promise}
   */
  waitForEnd(mediaElement) {
    // Sometimes, the ended flag is not set, or the event does not fire,
    // (I'm looking at **you**, Safari), so also check if we've reached the
    // duration.
    if (mediaElement.ended ||
        mediaElement.currentTime >= mediaElement.duration) {
      return Promise.resolve();
    }

    // The name of what we're waiting for.
    const goalName = 'end of media';

    // Cleanup on timeout.
    const cleanup = () => {
      this.eventManager_.unlisten(mediaElement, 'timeupdate');
      this.eventManager_.unlisten(mediaElement, 'ended');
    };

    // The conditions for success.  Don't rely on either time, or the ended
    // flag, or the ended event, specifically.  Any of these is sufficient.
    // This flexibility cuts down on test flake on Safari (currently 14) in
    // particular, where the flag might be set, but the ended event did not
    // fire.
    const p = new Promise((resolve) => {
      this.eventManager_.listen(mediaElement, 'timeupdate', () => {
        if (mediaElement.currentTime >= mediaElement.duration ||
            mediaElement.ended) {
          cleanup();
          resolve();
        }
      });
      this.eventManager_.listen(mediaElement, 'ended', () => {
        cleanup();
        resolve();
      });
    });

    return this.waitUntilGeneric_(goalName, p, cleanup, mediaElement);
  }

  /**
   * Wait for the video to end or for |timeout| seconds to pass, whichever
   * occurs first.  The Promise is resolved when either of these happens.
   *
   * @param {!HTMLMediaElement} target
   * @param {number} timeout in seconds, after which the Promise succeeds
   * @return {!Promise}
   */
  waitForEndOrTimeout(target, timeout) {
    this.failOnTimeout(false).timeoutAfter(timeout);
    return this.waitForEnd(target);
  }

  /**
   * Wait for the given event.
   *
   * @param {!EventTarget} target
   * @param {string} eventName
   * @return {!Promise}
   */
  waitForEvent(target, eventName) {
    // The name of what we're waiting for
    const goalName = 'event ' + eventName;

    // The conditions for success
    const p = new Promise((resolve) => {
      this.eventManager_.listenOnce(target, eventName, resolve);
    });

    // The cleanup on timeout
    const cleanup = () => {
      this.eventManager_.unlisten(target, eventName);
    };

    return this.waitUntilGeneric_(goalName, p, cleanup, target);
  }

  /**
   * Wait for a certain Promise to be resolved, or throw on timeout.
   *
   * @param {!Promise} p
   * @param {string} label A name to give the Promise in error messages.
   * @return {!Promise}
   */
  waitForPromise(p, label) {
    const cleanup = () => {};
    const target = null;
    return this.waitUntilGeneric_(label, p, cleanup, target);
  }

  /**
   * Wait for a certain Promise to be resolved, or throw on timeout.
   * Handles all debug logging and timeouts generically.
   *
   * @param {string} goalName
   * @param {!Promise} p
   * @param {function()} cleanupOnTimeout
   * @param {EventTarget} target
   * @return {!Promise}
   * @private
   */
  waitUntilGeneric_(goalName, p, cleanupOnTimeout, target) {
    let goalMet = false;
    const startTime = Date.now();
    shaka.log.debug('Waiting for ' + goalName);

    // Cache the value of this when we start, in case it changes during the
    // async work below.
    const failOnTimeout = this.failOnTimeout_;

    // The original stacktrace will be lost if we create the Error in the
    // timeout callback below.  Create the Error now so that it has a more
    // useful stacktrace on timeout.
    const error = new Error('Timeout waiting for ' + goalName);

    const success = p.then(() => {
      goalMet = true;
      const endTime = Date.now();
      const seconds = ((endTime - startTime) / 1000).toFixed(2);
      shaka.log.debug(goalName + ' after ' + seconds + ' seconds');
    });

    const timeout = shaka.test.Util.delay(this.timeoutSeconds_).then(() => {
      // Avoid error logs and cleanup callback if we've already met the goal.
      if (goalMet) {
        return;
      }

      cleanupOnTimeout();

      // Improve the error message with media-specific debug info.
      if (target instanceof HTMLMediaElement) {
        this.logDebugInfoForMedia_(error.message, target);
      }

      // Reject or resolve based on our settings.
      if (failOnTimeout) {
        throw error;
      }
    });

    return Promise.race([success, timeout]);
  }

  /**
   * @param {string} message
   * @param {!HTMLMediaElement} mediaElement
   * @private
   */
  logDebugInfoForMedia_(message, mediaElement) {
    const buffered =
        shaka.media.TimeRangesUtils.getBufferedInfo(mediaElement.buffered);
    shaka.log.error(message,
        'current time', mediaElement.currentTime,
        'duration', mediaElement.duration,
        'ready state', mediaElement.readyState,
        'playback rate', mediaElement.playbackRate,
        'paused', mediaElement.paused,
        'ended', mediaElement.ended,
        'buffered', buffered);
  }
};
<|endoftext|>


<commit_before>/* jshint -W097 */// jshint strict:false
/*jslint node: true */
var expect = require('chai').expect;
var fs        = require('fs');

describe('Test package.json and io-package.json', function() {
    it('Test package files', function (done) {
        var fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json');
        var ioPackage = JSON.parse(fileContentIOPackage);

        var fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json');
        var npmPackage = JSON.parse(fileContentNPMPackage);

        expect(ioPackage).to.be.an('object');
        expect(npmPackage).to.be.an('object');

        expect(ioPackage.common.version).to.exist;
        expect(npmPackage.version).to.exist;

        if (!expect(ioPackage.common.version).to.be.equal(npmPackage.version)) {
            console.log('ERROR: Version numbers in package.json and io-package.json differ!!');
        }

        if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) {
            console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!');
        }

        done();
    });
});
<commit_msg>update basic package-file testing<commit_after>/* jshint -W097 */
/* jshint strict:false */
/* jslint node: true */
/* jshint expr: true */
var expect = require('chai').expect;
var fs        = require('fs');

describe('Test package.json and io-package.json', function() {
    it('Test package files', function (done) {
        var fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json');
        var ioPackage = JSON.parse(fileContentIOPackage);

        var fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json');
        var npmPackage = JSON.parse(fileContentNPMPackage);

        expect(ioPackage).to.be.an('object');
        expect(npmPackage).to.be.an('object');

        expect(ioPackage.common.version).to.exist;
        expect(npmPackage.version).to.exist;

        if (!expect(ioPackage.common.version).to.be.equal(npmPackage.version)) {
            console.log('ERROR: Version numbers in package.json and io-package.json differ!!');
        }

        if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) {
            console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!');
        }

        expect(ioPackage.common.authors).to.exist;
        if (ioPackage.common.name.indexOf('template') !== 0) {
            if (Array.isArray(ioPackage.common.authors)) {
                expect(ioPackage.common.authors.length).to.not.be.equal(0);
                if (ioPackage.common.authors.length === 1) {
                    expect(ioPackage.common.authors[0]).to.not.be.equal('my Name <my@email.com>');
                }
            }
            else {
                expect(ioPackage.common.authors).to.not.be.equal('my Name <my@email.com>');
            }
        }
        else {
            console.log('Testing for set authors field in io-package skipped because template adapter');
        }
        done();
    });
});
<|endoftext|>


<commit_before>/* jshint -W097 */
/* jshint strict:false */
/* jslint node: true */
/* jshint expr: true */
var expect = require('chai').expect;
var fs        = require('fs');

describe('Test package.json and io-package.json', function() {
    it('Test package files', function (done) {
        console.log();

        var fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json', 'utf8');
        var ioPackage = JSON.parse(fileContentIOPackage);

        var fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json', 'utf8');
        var npmPackage = JSON.parse(fileContentNPMPackage);

        expect(ioPackage).to.be.an('object');
        expect(npmPackage).to.be.an('object');

        expect(ioPackage.common.version, 'ERROR: Version number in io-package.json needs to exist').to.exist;
        expect(npmPackage.version, 'ERROR: Version number in package.json needs to exist').to.exist;

        expect(ioPackage.common.version, 'ERROR: Version numbers in package.json and io-package.json needs to match').to.be.equal(npmPackage.version);

        if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) {
            console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!');
            console.log();
        }

        expect(npmPackage.author, 'ERROR: Author in package.json needs to exist').to.exist;
        expect(ioPackage.common.authors, 'ERROR: Authors in io-package.json needs to exist').to.exist;

        if (ioPackage.common.name.indexOf('template') !== 0) {
            if (Array.isArray(ioPackage.common.authors)) {
                expect(ioPackage.common.authors.length, 'ERROR: Author in io-package.json needs to be set').to.not.be.equal(0);
                if (ioPackage.common.authors.length === 1) {
                    expect(ioPackage.common.authors[0], 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
                }
            }
            else {
                expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
            }
        }
        else {
            console.log('WARNING: Testing for set authors field in io-package skipped because template adapter');
            console.log();
        }
        expect(fs.existsSync(__dirname + '/../README.md'), 'ERROR: README.md needs to exist! Please create one with description, detail information and changelog. English is mandatory.').to.be.true;
        expect(fs.existsSync(__dirname + '/../LICENSE'), 'ERROR: LICENSE needs to exist! Please create one.').to.be.true;
        if (!ioPackage.common.titleLang || typeof ioPackage.common.titleLang !== 'object') {
            console.log('WARNING: titleLang is not existing in io-package.json. Please add');
            console.log();
        }
        if (
            ioPackage.common.title.indexOf('iobroker') !== -1 ||
            ioPackage.common.title.indexOf('ioBroker') !== -1 ||
            ioPackage.common.title.indexOf('adapter') !== -1 ||
            ioPackage.common.title.indexOf('Adapter') !== -1
        ) {
            console.log('WARNING: title contains Adapter or ioBroker. It is clear anyway, that it is adapter for ioBroker.');
            console.log();
        }

        if (ioPackage.common.name.indexOf('vis-') !== 0) {
            if (!ioPackage.common.materialize || !fs.existsSync(__dirname + '/../admin/index_m.html') || !fs.existsSync(__dirname + '/../gulpfile.js')) {
                console.log('WARNING: Admin3 support is missing! Please add it');
                console.log();
            }
            if (ioPackage.common.materialize) {
                expect(fs.existsSync(__dirname + '/../admin/index_m.html'), 'Admin3 support is enabled in io-package.json, but index_m.html is missing!').to.be.true;
            }
        }

        var licenseFileExists = fs.existsSync(__dirname + '/../LICENSE');
        var fileContentReadme = fs.readFileSync(__dirname + '/../README.md', 'utf8');
        if (fileContentReadme.indexOf('## Changelog') === -1) {
            console.log('Warning: The README.md should have a section ## Changelog');
            console.log();
        }
        expect((licenseFileExists || fileContentReadme.indexOf('## License') !== -1), 'A LICENSE must exist as LICENSE file or as part of the README.md').to.be.true;
        if (!licenseFileExists) {
            console.log('Warning: The License should also exist as LICENSE file');
            console.log();
        }
        if (fileContentReadme.indexOf('## License') === -1) {
            console.log('Warning: The README.md should also have a section ## License to be shown in Admin3');
            console.log();
        }
        done();
    });
});
<commit_msg>update basic package-file testing<commit_after>/* jshint -W097 */
/* jshint strict:false */
/* jslint node: true */
/* jshint expr: true */
var expect = require('chai').expect;
var fs        = require('fs');

describe('Test package.json and io-package.json', function() {
    it('Test package files', function (done) {
        console.log();

        var fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json', 'utf8');
        var ioPackage = JSON.parse(fileContentIOPackage);

        var fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json', 'utf8');
        var npmPackage = JSON.parse(fileContentNPMPackage);

        expect(ioPackage).to.be.an('object');
        expect(npmPackage).to.be.an('object');

        expect(ioPackage.common.version, 'ERROR: Version number in io-package.json needs to exist').to.exist;
        expect(npmPackage.version, 'ERROR: Version number in package.json needs to exist').to.exist;

        expect(ioPackage.common.version, 'ERROR: Version numbers in package.json and io-package.json needs to match').to.be.equal(npmPackage.version);

        if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) {
            console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!');
            console.log();
        }

        expect(npmPackage.author, 'ERROR: Author in package.json needs to exist').to.exist;
        expect(ioPackage.common.authors, 'ERROR: Authors in io-package.json needs to exist').to.exist;

        if (ioPackage.common.name.indexOf('template') !== 0) {
            if (Array.isArray(ioPackage.common.authors)) {
                expect(ioPackage.common.authors.length, 'ERROR: Author in io-package.json needs to be set').to.not.be.equal(0);
                if (ioPackage.common.authors.length === 1) {
                    expect(ioPackage.common.authors[0], 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
                }
            }
            else {
                expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
            }
        }
        else {
            console.log('WARNING: Testing for set authors field in io-package skipped because template adapter');
            console.log();
        }
        expect(fs.existsSync(__dirname + '/../README.md'), 'ERROR: README.md needs to exist! Please create one with description, detail information and changelog. English is mandatory.').to.be.true;
        if (!ioPackage.common.titleLang || typeof ioPackage.common.titleLang !== 'object') {
            console.log('WARNING: titleLang is not existing in io-package.json. Please add');
            console.log();
        }
        if (
            ioPackage.common.title.indexOf('iobroker') !== -1 ||
            ioPackage.common.title.indexOf('ioBroker') !== -1 ||
            ioPackage.common.title.indexOf('adapter') !== -1 ||
            ioPackage.common.title.indexOf('Adapter') !== -1
        ) {
            console.log('WARNING: title contains Adapter or ioBroker. It is clear anyway, that it is adapter for ioBroker.');
            console.log();
        }

        if (ioPackage.common.name.indexOf('vis-') !== 0) {
            if (!ioPackage.common.materialize || !fs.existsSync(__dirname + '/../admin/index_m.html') || !fs.existsSync(__dirname + '/../gulpfile.js')) {
                console.log('WARNING: Admin3 support is missing! Please add it');
                console.log();
            }
            if (ioPackage.common.materialize) {
                expect(fs.existsSync(__dirname + '/../admin/index_m.html'), 'Admin3 support is enabled in io-package.json, but index_m.html is missing!').to.be.true;
            }
        }

        var licenseFileExists = fs.existsSync(__dirname + '/../LICENSE');
        var fileContentReadme = fs.readFileSync(__dirname + '/../README.md', 'utf8');
        if (fileContentReadme.indexOf('## Changelog') === -1) {
            console.log('Warning: The README.md should have a section ## Changelog');
            console.log();
        }
        expect((licenseFileExists || fileContentReadme.indexOf('## License') !== -1), 'A LICENSE must exist as LICENSE file or as part of the README.md').to.be.true;
        if (!licenseFileExists) {
            console.log('Warning: The License should also exist as LICENSE file');
            console.log();
        }
        if (fileContentReadme.indexOf('## License') === -1) {
            console.log('Warning: The README.md should also have a section ## License to be shown in Admin3');
            console.log();
        }
        done();
    });
});
<|endoftext|>


<commit_before>// ===========================================
//                 Imports
// ===========================================
var formulas = require("./formulas");

// ==============================================
//                     Note
// ==============================================
/**
* Represents a musical note.
*
* @constructor
* @param name   The name of the note.
* @param freq   Frequency of the note (optional).
* @param octave Index/octave of the note (optional).
* @param name2  Secondary name of the note (optional).
* @returns A Note object.
*/
function Note(name, freq, octave, name2){
    this.name   = name   || "";
    this.octave = octave || 0;
    this.freq   = freq   || 0;
    this.name2  = name2  || "";
}

// ------------------------
//         Mutators
// ------------------------

/** Sets name. */
Note.prototype.setName = function(newName){
    this.name = newName;
};

/** Sets octave. */
Note.prototype.setOctave = function(newOctave){
    this.octave = newOctave;
};

/** Sets freq. */
Note.prototype.setFreq = function(newFreq){
    this.freq = newFreq;
};

/** Sets the secondary name (name2). */
Note.prototype.setName2 = function(newName2){
    this.name2 = newName2;
};

// ------------------------
//        Accessors
// ------------------------

/** Returns the primitive value of the object, used by the + operator. */
Note.prototype.valueOf = function(){
    return this.freq;
};

/** Returns name. */
Note.prototype.getName = function(){
    return this.name;
};

/** Returns octave. */
Note.prototype.getOctave = function(){
    return this.octave;
};

/** Returns freq. */
Note.prototype.getFreq = function(){
    return this.freq;
};

/** Returns secondary name (name2). */
Note.prototype.getName2 = function(){
    return this.name2;
};

/** Returns the object state as a string. */
Note.prototype.toString = function(){
    return "name=" + this.name + " name2=" + this.name2 + " freq=" + this.freq +
           " octave=" + this.octave;
};

/**
* Represents a group of notes. Parent constructor for Chord and Scale.
* @constructor
* @param notes An array of Note objects.
* @param name  Name for the NoteCollection (optional).
* @param name2 Secondary name for the NoteCollection (optional).
*/
function NoteCollection(notes, name, name2){
    // if (!(notes instanceof Array)) throw new Error("notes must be an array of Note"); needs to handle prototype inheritance
    this.notes  = notes;                           // array of notes
    this.notes2 = notes ? notes.slice(0) : notes;  // original notes (clone)
    this.name   = name  || "";
    this.name2  = name2 || "";
    this.size   = notes ? notes.length : 0;

    // ==============================================
    //                   Mutators
    // ==============================================

    /** Change the notes. */ // !!! Should this implement variable arguments?
    this.setNotes = function(notes){
        if (!(notes instanceof Array)) throw new Error("notes must be an array of Note");
        this.notes = notes;
        this.size  = this.notes.length;
    };

    /** Adds one note. */
    this.addNote = function(note){
        if (!(note instanceof Note)) throw new Error("note must be a Note object");
        this.notes.push(note);
        this.size++;
    };

    /** Changes the name of the NoteCollection object. */
    this.setName = function(newName){
        this.name = newName;
    };

    /** Changes the secondary name of the NoteCollection object. */
    this.setName2 = function(newName){
        this.name2 = newName;
    };

    /** Resets notes to its original state (notes2). */
    this.reset = function(){
        this.notes = this.notes2.slice(0);
        this.size  = this.notes.length;
    };

    /** Sends the first note to the last index. */  // !!! Can this be more efficient?
    this.rotate = function(){
        this.notes.push(this.notes.shift());
    };

    /** Sends the last note to the first index. */  // !!! Can this be more efficient?
    this.rotateBack = function(){
        this.notes.unshift(this.notes.pop());
    };

    /** Reverses the order of the notes. */
    this.reverse = function(){
        this.notes.reverse();
    };

    /** Removes the note at given index. */
    this.removeNoteAt = function(index){
        if (index < 0) throw new Error("index must be > 0");
        this.notes.splice(index, 1);
        this.size--;
    };

    /** Removes all the notes with given name. */
    this.removeNotesWithName = function(name){
        this.notes = this.notes.filter(
            function(element){
                return element.getName() !== name;
            });
        this.size = this.notes.length;
    };

    /** Removes all the notes with given secondary name (name2). */
    this.removeNotesWithName2 = function(name){
        this.notes = this.notes.filter(
            function(element){
                return element.getName2() !== name;
            });
        this.size = this.notes.length;
    };

    /** Removes all the notes with given frequency. */
    this.removeNotesWithFreq = function(freq){
        this.notes = this.notes.filter(
            function(element){
                return element.getFreq() !== freq;
            });
        this.size = this.notes.length;
    };

    /** Removes all the notes within a frequency range, inclusive. */
    this.removeNotesWithFreqRange = function(fromFreq, toFreq){
        this.notes = this.notes.filter(
            function(element){
                var theFreq = element.getFreq();
                return theFreq < fromFreq || theFreq > toFreq;
            });
        this.size = this.notes.length;
    };

    // ==============================================
    //                  Accessors
    // ==============================================

    /** Returns the object state as a string */
    this.toString = function(){
        var returnString = "name=" + this.name + "\nname2=" + this.name2 + "\nsize=" + this.size;
        returnString += "\nnotes=\n";
        for (var i in this.notes){
            returnString += "<";
            returnString += this.notes[i].toString();
            returnString += ">";
        }
        returnString += "\nnotes2=\n";
        for (var j in this.notes2){
            returnString += "<";
            returnString += this.notes2[j].toString();
            returnString += ">";
        }
        return returnString;
    };

    /** Retunrs the size of the string NoteCollection. */
    this.getSize = function(){
        return this.size;
    };

    /** Returns the notes. */
    this.getNotes = function(){
        return this.notes;
    };

    /** Returns the original notes. */
    this.getOriginalNotes = function(){
        return this.notes2;
    };

    /** Returns name. */
    this.getName = function(){
        return this.name;
    };

    /** Returns secondary name (name2) */
    this.getName2 = function(){
        return this.name2;
    };

    /** Returns the name of the notes as a string. */
    this.getNotesAsString = function(){
        var returnString = "";
        for (var i in this.notes)
            returnString += this.notes[i].getName() + " ";
        return returnString;
    };

    /** Returns the name of the original notes as a string. */
    this.getOriginalNotesAsString = function(){
        var returnString = "";
        for (var i in this.notes2)
            returnString += this.notes2[i].getName() + " ";
        return returnString;
    };

    /** Returns the frequencies of the notes as a string. */
    this.getFreqsAsString = function(){
        var returnString = "";
        for (var i in this.notes)
            returnString += this.notes[i].getFreq() + " ";
        return returnString;
    };

    /** Builds an array of the indexes the notes have in a pool. */
    this.toIndexes = function(pool){
        var thePool = pool || formulas.ET12POOL;
        var returnArray = [];
        for (var i in this.notes)
            returnArray.push(thePool.indexOf(this.notes[i].getName()));
        return returnArray;
    };

    /** Builds a formula based on the indexes the notes have in a pool. */ // !!! this can be more efficient
    this.toFormula = function(pool){
        var thePool       = pool || formulas.ET12POOL;
        var indexArray    = this.toIndexes(pool);
        var returnArray   = [];
        var formulaLength = indexArray.length;
        var poolLength    = thePool.length;
        for (var i = 0; i < formulaLength; i++){
            var sum = indexArray[i] - indexArray[(i + 1) % formulaLength];
            if (sum > 0) sum -= poolLength;
            returnArray.push(Math.abs(sum));
        }
        return returnArray;
    };

}

/**
* Represents a group of notes as a Chord.
* Inherits fron NoteCollection.
* @constructor
* @param notes An array of Note objects.
* @param name  Name for the NoteCollection (optional).
* @param name2 Secondary name for the NoteCollection (optional).
*/
function Chord(notes, name, name2){
    NoteCollection.apply(this, arguments);  // inherits from NoteCollection

    // ==============================================
    //                   Mutators
    // ==============================================

    /** Sets notes to nth inversion. Sets to previous inversions if n is negative. */ // !!! can this be done more efficiently?
    this.invert = function(n){
        if (typeof(n) !== "number") throw new Error("n must be of type number");
        if (n > 0)
            for (var i = 0; i < n; i++)
                this.notes.push(this.notes.shift());
        if (n < 0)
            for (var j = 0; j > n; j--)
                this.notes.unshift(this.notes.pop());
    };

    /**
    * Sets notes to nth inversion, using the original notes (notes2) as reference.
    * Sets to previous inversions if n is negative.
    */
    this.invertOriginal = function(n){
        if (typeof(n) !== "number") throw new Error("n must be of type number");
        this.reset();
        this.invert(n);
    };
}
Chord.prototype = Object.create(NoteCollection.prototype);
Chord.prototype.constructor = Chord;

/**
* Group of Note objects with melodic dynamics (like a succession of notes).
*
* @constructor
*/
function Scale(notes, name, name2){
    NoteCollection.apply(this, arguments);  // inherits from NoteCollection
}
Scale.prototype = Object.create(NoteCollection.prototype);
Scale.prototype.constructor = Scale;

/**
* Represents a group of chords
* @constructor
*/
function ChordCollection(chords, name, name2){
    // if (!(chords instanceof Array)) throw new Error("chords must be an array of Chord"); !!! this needs to work with prototype inheritance!!!!
    this.name   = name  || "";
    this.name2  = name2 || "";
    this.chords = chords;
    this.size   = chords ? chords.length : 0;

    // ==============================================
    //                   Mutators
    // ==============================================

    this.setChords = function(newChords){
        if (!(chords instanceof Array)) throw new Error("chords must be an array of Chord");
        this.chords = newChords;
        this.size   = chords.length;
    };

    // ==============================================
    //                  Accessors
    // ==============================================

    this.getChords = function(){
        return this.chords;
    };

    this.getChordsNames = function(){
        var returnString = "";
        for (var i in this.chords)
            returnString += this.chords[i].name + " ";
        return returnString;
    };

    this.getChordsNotesAsString = function(){
        var returnString = "";
        for (var i in this.chords)
            returnString += "< " + this.chords[i].getNotesAsString() + "> ";
        return returnString;
    };

    this.toString = function(){
        var returnString = "name=" + this.name + "\nname2=" + this.name2 + "\nsize=" + this.size;
        returnString += "\n\nchords= ";
        for (var i in this.chords){
            returnString += "\n<<<\n";
            returnString += this.chords[i].toString();
            returnString += "\n>>>";
        }
        return returnString;
    };
}

/**
* Represents a group of chords.
* @constructor
*/
function Harmony(chords, name, name2){
    ChordCollection.apply(this, arguments);
}
Harmony.prototype = Object.create(ChordCollection.prototype);
Harmony.prototype.constructor = Harmony;

// node exports
exports.Note            = Note;
exports.NoteCollection  = NoteCollection;
exports.Chord           = Chord;
exports.Scale           = Scale;
exports.ChordCollection = ChordCollection;
exports.Harmony         = Harmony;
<commit_msg>NoteSounds construction now follows the new pattern<commit_after>// ==============================================
//                 Imports
// ==============================================
var formulas = require("./formulas");

// ==============================================
//                     Note
// ==============================================
/**
* Represents a musical note.
*
* @constructor
* @param name   The name of the note.
* @param freq   Frequency of the note (optional).
* @param octave Index/octave of the note (optional).
* @param name2  Secondary name of the note (optional).
* @returns A Note object.
*/
function Note(name, freq, octave, name2){
    this.name   = name   || "";
    this.octave = octave || 0;
    this.freq   = freq   || 0;
    this.name2  = name2  || "";
}

// ------------------------
//         Mutators
// ------------------------

/** Sets name. */
Note.prototype.setName = function(newName){
    this.name = newName;
};

/** Sets octave. */
Note.prototype.setOctave = function(newOctave){
    this.octave = newOctave;
};

/** Sets freq. */
Note.prototype.setFreq = function(newFreq){
    this.freq = newFreq;
};

/** Sets the secondary name (name2). */
Note.prototype.setName2 = function(newName2){
    this.name2 = newName2;
};

// ------------------------
//        Accessors
// ------------------------

/** Returns the primitive value of the object, used by the + operator. */
Note.prototype.valueOf = function(){
    return this.freq;
};

/** Returns name. */
Note.prototype.getName = function(){
    return this.name;
};

/** Returns octave. */
Note.prototype.getOctave = function(){
    return this.octave;
};

/** Returns freq. */
Note.prototype.getFreq = function(){
    return this.freq;
};

/** Returns secondary name (name2). */
Note.prototype.getName2 = function(){
    return this.name2;
};

/** Returns the object state as a string. */
Note.prototype.toString = function(){
    return "name=" + this.name + " name2=" + this.name2 + " freq=" + this.freq +
           " octave=" + this.octave;
};

// ==============================================
//                 NoteCollection
// ==============================================
/**
* Represents a group of notes. Parent constructor for Chord and Scale.
* @constructor
* @param notes An array of Note objects.
* @param name  Name for the NoteCollection (optional).
* @param name2 Secondary name for the NoteCollection (optional).
*/
function NoteCollection(notes, name, name2){
    // if (!(notes instanceof Array)) throw new Error("notes must be an array of Note"); needs to handle prototype inheritance
    this.notes  = notes;                           // array of notes
    this.notes2 = notes ? notes.slice(0) : notes;  // original notes (clone)
    this.name   = name  || "";
    this.name2  = name2 || "";
    this.size   = notes ? notes.length : 0;
}

// ------------------------
//         Mutators
// ------------------------

/** Change the notes. */
NoteCollection.prototype.setNotes = function(notes){
    if (!(notes instanceof Array)) throw new Error("notes must be an array of Note");
    this.notes = notes;
    this.size  = this.notes.length;
};

/** Adds one note. */
NoteCollection.prototype.addNote = function(note){
    if (!(note instanceof Note)) throw new Error("note must be a Note object");
    this.notes.push(note);
    this.size++;
};

/** Changes the name of the NoteCollection object. */
NoteCollection.prototype.setName = function(newName){
    this.name = newName;
};

/** Changes the secondary name of the NoteCollection object. */
NoteCollection.prototype.setName2 = function(newName){
    this.name2 = newName;
};

/** Resets notes to its original state (notes2). */
NoteCollection.prototype.reset = function(){
    this.notes = this.notes2.slice(0);
    this.size  = this.notes.length;
};

/** Sends the first note to the last index. */  // !!! Can this be more efficient?
NoteCollection.prototype.rotate = function(){
    this.notes.push(this.notes.shift());
};

/** Sends the last note to the first index. */  // !!! Can this be more efficient?
NoteCollection.prototype.rotateBack = function(){
    this.notes.unshift(this.notes.pop());
};

/** Reverses the order of the notes. */
NoteCollection.prototype.reverse = function(){
    this.notes.reverse();
};

/** Removes the note at given index. */
NoteCollection.prototype.removeNoteAt = function(index){
    if (index < 0) throw new Error("index must be greater than 0");
    this.notes.splice(index, 1);
    this.size--;
};

/** Removes all the notes with given name. */
NoteCollection.prototype.removeNotesWithName = function(name){
    this.notes = this.notes.filter(
        function(element){
            return element.getName() !== name;
        });
    this.size = this.notes.length;
};

/** Removes all the notes with given secondary name (name2). */
NoteCollection.prototype.removeNotesWithName2 = function(name){
    this.notes = this.notes.filter(
        function(element){
            return element.getName2() !== name;
        });
    this.size = this.notes.length;
};

/** Removes all the notes with given frequency. */
NoteCollection.prototype.removeNotesWithFreq = function(freq){
    this.notes = this.notes.filter(
        function(element){
            return element.getFreq() !== freq;
        });
    this.size = this.notes.length;
};

/** Removes all the notes within a frequency range, inclusive. */
NoteCollection.prototype.removeNotesWithFreqRange = function(fromFreq, toFreq){
    this.notes = this.notes.filter(
        function(element){
            var theFreq = element.getFreq();
            return theFreq < fromFreq || theFreq > toFreq;
        });
    this.size = this.notes.length;
};

// ------------------------
//        Accessors
// ------------------------

/** Returns the object state as a string. */
NoteCollection.prototype.toString = function(){
    var returnString = "name=" + this.name + "\nname2=" + this.name2 + "\nsize=" + this.size;
    returnString += "\nnotes=\n";
    for (var i in this.notes){
        returnString += "<";
        returnString += this.notes[i].toString();
        returnString += ">";
    }
    returnString += "\nnotes2=\n";
    for (var j in this.notes2){
        returnString += "<";
        returnString += this.notes2[j].toString();
        returnString += ">";
    }
    return returnString;
};

/** Retunrs the size of the string NoteCollection. */
NoteCollection.prototype.getSize = function(){
    return this.size;
};

/** Returns the notes. */
NoteCollection.prototype.getNotes = function(){
    return this.notes;
};

/** Returns the original notes. */
NoteCollection.prototype.getOriginalNotes = function(){
    return this.notes2;
};

/** Returns name. */
NoteCollection.prototype.getName = function(){
    return this.name;
};

/** Returns secondary name (name2) */
NoteCollection.prototype.getName2 = function(){
    return this.name2;
};

/** Returns the name of the notes as a string. */
NoteCollection.prototype.getNotesAsString = function(){
    var returnString = "";
    for (var i in this.notes)
        returnString += this.notes[i].getName() + " ";
    return returnString;
};

/** Returns the name of the original notes as a string. */
NoteCollection.prototype.getOriginalNotesAsString = function(){
    var returnString = "";
    for (var i in this.notes2)
        returnString += this.notes2[i].getName() + " ";
    return returnString;
};

/** Returns the frequencies of the notes as a string. */
NoteCollection.prototype.getFreqsAsString = function(){
    var returnString = "";
    for (var i in this.notes)
        returnString += this.notes[i].getFreq() + " ";
    return returnString;
};

/** Builds an array of the indexes the notes have in a pool. */
NoteCollection.prototype.toIndexes = function(pool){
    var thePool = pool || formulas.ET12POOL;
    var returnArray = [];
    for (var i in this.notes)
        returnArray.push(thePool.indexOf(this.notes[i].getName()));
    return returnArray;
};

/** Builds a formula based on the indexes the notes have in a pool. */ // !!! this can be more efficient
NoteCollection.prototype.toFormula = function(pool){
    var thePool       = pool || formulas.ET12POOL;
    var indexArray    = this.toIndexes(pool);
    var returnArray   = [];
    var formulaLength = indexArray.length;
    var poolLength    = thePool.length;
    for (var i = 0; i < formulaLength; i++){
        var sum = indexArray[i] - indexArray[(i + 1) % formulaLength];
        if (sum > 0) sum -= poolLength;
        returnArray.push(Math.abs(sum));
    }
    return returnArray;
};

/**
* Represents a group of notes as a Chord.
* Inherits fron NoteCollection.
* @constructor
* @param notes An array of Note objects.
* @param name  Name for the NoteCollection (optional).
* @param name2 Secondary name for the NoteCollection (optional).
*/
function Chord(notes, name, name2){
    NoteCollection.apply(this, arguments);  // inherits from NoteCollection

    // ==============================================
    //                   Mutators
    // ==============================================

    /** Sets notes to nth inversion. Sets to previous inversions if n is negative. */ // !!! can this be done more efficiently?
    this.invert = function(n){
        if (typeof(n) !== "number") throw new Error("n must be of type number");
        if (n > 0)
            for (var i = 0; i < n; i++)
                this.notes.push(this.notes.shift());
        if (n < 0)
            for (var j = 0; j > n; j--)
                this.notes.unshift(this.notes.pop());
    };

    /**
    * Sets notes to nth inversion, using the original notes (notes2) as reference.
    * Sets to previous inversions if n is negative.
    */
    this.invertOriginal = function(n){
        if (typeof(n) !== "number") throw new Error("n must be of type number");
        this.reset();
        this.invert(n);
    };
}
Chord.prototype = Object.create(NoteCollection.prototype);
Chord.prototype.constructor = Chord;

/**
* Group of Note objects with melodic dynamics (like a succession of notes).
*
* @constructor
*/
function Scale(notes, name, name2){
    NoteCollection.apply(this, arguments);  // inherits from NoteCollection
}
Scale.prototype = Object.create(NoteCollection.prototype);
Scale.prototype.constructor = Scale;

/**
* Represents a group of chords
* @constructor
*/
function ChordCollection(chords, name, name2){
    // if (!(chords instanceof Array)) throw new Error("chords must be an array of Chord"); !!! this needs to work with prototype inheritance!!!!
    this.name   = name  || "";
    this.name2  = name2 || "";
    this.chords = chords;
    this.size   = chords ? chords.length : 0;

    // ==============================================
    //                   Mutators
    // ==============================================

    this.setChords = function(newChords){
        if (!(chords instanceof Array)) throw new Error("chords must be an array of Chord");
        this.chords = newChords;
        this.size   = chords.length;
    };

    // ==============================================
    //                  Accessors
    // ==============================================

    this.getChords = function(){
        return this.chords;
    };

    this.getChordsNames = function(){
        var returnString = "";
        for (var i in this.chords)
            returnString += this.chords[i].name + " ";
        return returnString;
    };

    this.getChordsNotesAsString = function(){
        var returnString = "";
        for (var i in this.chords)
            returnString += "< " + this.chords[i].getNotesAsString() + "> ";
        return returnString;
    };

    this.toString = function(){
        var returnString = "name=" + this.name + "\nname2=" + this.name2 + "\nsize=" + this.size;
        returnString += "\n\nchords= ";
        for (var i in this.chords){
            returnString += "\n<<<\n";
            returnString += this.chords[i].toString();
            returnString += "\n>>>";
        }
        return returnString;
    };
}

/**
* Represents a group of chords.
* @constructor
*/
function Harmony(chords, name, name2){
    ChordCollection.apply(this, arguments);
}
Harmony.prototype = Object.create(ChordCollection.prototype);
Harmony.prototype.constructor = Harmony;

// node exports
exports.Note            = Note;
exports.NoteCollection  = NoteCollection;
exports.Chord           = Chord;
exports.Scale           = Scale;
exports.ChordCollection = ChordCollection;
exports.Harmony         = Harmony;
<|endoftext|>