<commit_before>const fs = require('fs');
const path = require('path');

const test = require('tap').test;

const log = require('../../src/util/log');
const makeTestStorage = require('../fixtures/make-test-storage');
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
const VirtualMachine = require('../../src/index');

/**
 * @fileoverview Transform each sb2 in fixtures/execute into a test.
 *
 * Test execution of a group of scratch blocks by SAYing if a test did "pass",
 * or did "fail". Four keywords can be set at the beginning of a SAY messaage
 * to indicate a test primitive.
 *
 * - "pass MESSAGE" will t.pass(MESSAGE).
 * - "fail MESSAGE" will t.fail(MESSAGE).
 * - "plan NUMBER_OF_TESTS" will t.plan(Number(NUMBER_OF_TESTS)).
 * - "end" will t.end().
 *
 * A good strategy to follow is to SAY "plan NUMBER_OF_TESTS" first. Then
 * "pass" and "fail" depending on expected scratch results in conditions, event
 * scripts, or what is best for testing the target block or group of blocks.
 * When its done you must SAY "end" so the test and tap know that the end has
 * been reached.
 */

const whenThreadsComplete = (t, vm, timeLimit = 2000) => (
    // When the number of threads reaches 0 the test is expected to be complete.
    new Promise((resolve, reject) => {
        const intervalId = setInterval(() => {
            let active = 0;
            const threads = vm.runtime.threads;
            for (let i = 0; i < threads.length; i++) {
                if (!threads[i].updateMonitor) {
                    active += 1;
                }
            }
            if (active === 0) {
                resolve();
            }
        }, 50);

        const timeoutId = setTimeout(() => {
            reject(new Error('time limit reached'));
        }, timeLimit);

        // Clear the interval to allow the process to exit
        // naturally.
        t.tearDown(() => {
            clearInterval(intervalId);
            clearTimeout(timeoutId);
        });
    })
);

const executeDir = path.resolve(__dirname, '../fixtures/execute');

fs.readdirSync(executeDir)
    .filter(uri => uri.endsWith('.sb2'))
    .forEach(uri => {
        test(uri, t => {
            // Disable logging during this test.
            log.suggest.deny('vm', 'error');
            t.tearDown(() => log.suggest.clear());

            const vm = new VirtualMachine();

            // Map string messages to tap reporting methods. This will be used
            // with events from scratch's runtime emitted on block instructions.
            let didPlan;
            let didEnd;
            const reporters = {
                comment (message) {
                    t.comment(message);
                },
                pass (reason) {
                    t.pass(reason);
                },
                fail (reason) {
                    t.fail(reason);
                },
                plan (count) {
                    didPlan = true;
                    t.plan(Number(count));
                },
                end () {
                    didEnd = true;
                    vm.quit();
                    t.end();
                }
            };
            const reportVmResult = text => {
                const command = text.split(/\s+/, 1)[0].toLowerCase();
                if (reporters[command]) {
                    return reporters[command](text.substring(command.length).trim());
                }

                // Default to a comment with the full text if we didn't match
                // any command prefix
                return reporters.comment(text);
            };

            vm.attachStorage(makeTestStorage());

            // Start the VM and initialize some vm properties.
            // complete.
            vm.start();
            vm.clear();
            vm.setCompatibilityMode(false);
            vm.setTurboMode(false);

            // Stop the runtime interval once the test is complete so the test
            // process may naturally exit.
            t.tearDown(() => {
                clearInterval(vm.runtime._steppingInterval);
            });

            // Report the text of SAY events as testing instructions.
            vm.runtime.on('SAY', (target, type, text) => reportVmResult(text));

            const project = readFileToBuffer(path.resolve(executeDir, uri));

            // Load the project and once all threads are complete ensure that
            // the scratch project sent us a "end" message.
            return vm.loadProject(project)
                .then(() => vm.greenFlag())
                .then(() => whenThreadsComplete(t, vm))
                .then(() => {
                    // Setting a plan is not required but is a good idea.
                    if (!didPlan) {
                        t.comment('did not say "plan NUMBER_OF_TESTS"');
                    }

                    // End must be called so that tap knows the test is done. If
                    // the test has an SAY "end" block but that block did not
                    // execute, this explicit failure will raise that issue so
                    // it can be resolved.
                    if (!didEnd) {
                        t.fail('did not say "end"');
                        vm.quit();
                        t.end();
                    }
                });
        });
    });
<commit_msg>test(execute): try to make 'execute' test more reliable<commit_after>const fs = require('fs');
const path = require('path');

const test = require('tap').test;

const log = require('../../src/util/log');
const makeTestStorage = require('../fixtures/make-test-storage');
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
const VirtualMachine = require('../../src/index');

/**
 * @fileoverview Transform each sb2 in fixtures/execute into a test.
 *
 * Test execution of a group of scratch blocks by SAYing if a test did "pass",
 * or did "fail". Four keywords can be set at the beginning of a SAY messaage
 * to indicate a test primitive.
 *
 * - "pass MESSAGE" will t.pass(MESSAGE).
 * - "fail MESSAGE" will t.fail(MESSAGE).
 * - "plan NUMBER_OF_TESTS" will t.plan(Number(NUMBER_OF_TESTS)).
 * - "end" will t.end().
 *
 * A good strategy to follow is to SAY "plan NUMBER_OF_TESTS" first. Then
 * "pass" and "fail" depending on expected scratch results in conditions, event
 * scripts, or what is best for testing the target block or group of blocks.
 * When its done you must SAY "end" so the test and tap know that the end has
 * been reached.
 */

const whenThreadsComplete = (t, vm, uri, timeLimit = 5000) =>
    // When the number of threads reaches 0 the test is expected to be complete.
    new Promise((resolve, reject) => {
        const intervalId = setInterval(() => {
            let active = 0;
            const threads = vm.runtime.threads;
            for (let i = 0; i < threads.length; i++) {
                if (!threads[i].updateMonitor) {
                    active += 1;
                }
            }
            if (active === 0) {
                resolve();
            }
        }, 50);

        const timeoutId = setTimeout(() => {
            t.fail(`Timeout waiting for threads to complete: ${uri}`);
            reject(new Error('time limit reached'));
        }, timeLimit);

        // Clear the interval to allow the process to exit
        // naturally.
        t.tearDown(() => {
            clearInterval(intervalId);
            clearTimeout(timeoutId);
        });
    });

const executeDir = path.resolve(__dirname, '../fixtures/execute');

fs.readdirSync(executeDir)
    .filter(uri => uri.endsWith('.sb2'))
    .forEach(uri => {
        test(uri, t => {
            // Disable logging during this test.
            log.suggest.deny('vm', 'error');
            t.tearDown(() => log.suggest.clear());

            const vm = new VirtualMachine();

            // Map string messages to tap reporting methods. This will be used
            // with events from scratch's runtime emitted on block instructions.
            let didPlan;
            let didEnd;
            const reporters = {
                comment (message) {
                    t.comment(message);
                },
                pass (reason) {
                    t.pass(reason);
                },
                fail (reason) {
                    t.fail(reason);
                },
                plan (count) {
                    didPlan = true;
                    t.plan(Number(count));
                },
                end () {
                    didEnd = true;
                    vm.quit();
                    t.end();
                }
            };
            const reportVmResult = text => {
                const command = text.split(/\s+/, 1)[0].toLowerCase();
                if (reporters[command]) {
                    return reporters[command](text.substring(command.length).trim());
                }

                // Default to a comment with the full text if we didn't match
                // any command prefix
                return reporters.comment(text);
            };

            vm.attachStorage(makeTestStorage());

            // Start the VM and initialize some vm properties.
            // complete.
            vm.start();
            vm.clear();
            vm.setCompatibilityMode(false);
            vm.setTurboMode(false);

            // Stop the runtime interval once the test is complete so the test
            // process may naturally exit.
            t.tearDown(() => {
                clearInterval(vm.runtime._steppingInterval);
            });

            // Report the text of SAY events as testing instructions.
            vm.runtime.on('SAY', (target, type, text) => reportVmResult(text));

            const project = readFileToBuffer(path.resolve(executeDir, uri));

            // Load the project and once all threads are complete ensure that
            // the scratch project sent us a "end" message.
            return vm.loadProject(project)
                .then(() => vm.greenFlag())
                .then(() => whenThreadsComplete(t, vm, uri))
                .then(() => {
                    // Setting a plan is not required but is a good idea.
                    if (!didPlan) {
                        t.comment('did not say "plan NUMBER_OF_TESTS"');
                    }

                    // End must be called so that tap knows the test is done. If
                    // the test has an SAY "end" block but that block did not
                    // execute, this explicit failure will raise that issue so
                    // it can be resolved.
                    if (!didEnd) {
                        t.fail('did not say "end"');
                        vm.quit();
                        t.end();
                    }
                });
        });
    });
<|endoftext|>


<commit_before>// This file is automatically generated, do not edit
goog.provide('ol.renderer.webgl.map.shader.Default');
goog.require('ol.webgl.shader');
/**
 * @constructor
 * @extends {ol.webgl.shader.Fragment}
 */
ol.renderer.webgl.map.shader.DefaultFragment = function() {
  goog.base(this, ol.renderer.webgl.map.shader.DefaultFragment.SOURCE);
};
goog.inherits(ol.renderer.webgl.map.shader.DefaultFragment, ol.webgl.shader.Fragment);
goog.addSingletonGetter(ol.renderer.webgl.map.shader.DefaultFragment);
/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.map.shader.DefaultFragment.DEBUG_SOURCE = 'precision mediump float;\n//! NAMESPACE=ol.renderer.webgl.map.shader.Default\n//! CLASS=ol.renderer.webgl.map.shader.Default\n\n\n//! COMMON\nvarying vec2 v_texCoord;\n\n\n//! FRAGMENT\n// @see https://svn.webkit.org/repository/webkit/trunk/Source/WebCore/platform/graphics/filters/skia/SkiaImageFilterBuilder.cpp\nuniform float u_opacity;\nuniform sampler2D u_texture;\n\nvoid main(void) {\n  vec4 texColor = texture2D(u_texture, v_texCoord);\n  gl_FragColor.rgb = texColor.rgb;\n  gl_FragColor.a = texColor.a * u_opacity;\n}\n\n';
/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.map.shader.DefaultFragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;uniform float d;uniform sampler2D e;void main(){vec4 f=texture2D(e,a);gl_FragColor.rgb=f.rgb;gl_FragColor.a=f.a*d;}';
/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.map.shader.DefaultFragment.SOURCE = goog.DEBUG ?
    ol.renderer.webgl.map.shader.DefaultFragment.DEBUG_SOURCE :
    ol.renderer.webgl.map.shader.DefaultFragment.OPTIMIZED_SOURCE;
/**
 * @constructor
 * @extends {ol.webgl.shader.Vertex}
 */
ol.renderer.webgl.map.shader.DefaultVertex = function() {
  goog.base(this, ol.renderer.webgl.map.shader.DefaultVertex.SOURCE);
};
goog.inherits(ol.renderer.webgl.map.shader.DefaultVertex, ol.webgl.shader.Vertex);
goog.addSingletonGetter(ol.renderer.webgl.map.shader.DefaultVertex);
/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.map.shader.DefaultVertex.DEBUG_SOURCE = '//! NAMESPACE=ol.renderer.webgl.map.shader.Default\n//! CLASS=ol.renderer.webgl.map.shader.Default\n\n\n//! COMMON\nvarying vec2 v_texCoord;\n\n\n//! VERTEX\nattribute vec2 a_position;\nattribute vec2 a_texCoord;\n\nuniform mat4 u_texCoordMatrix;\nuniform mat4 u_projectionMatrix;\n\nvoid main(void) {\n  gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.);\n  v_texCoord = (u_texCoordMatrix * vec4(a_texCoord, 0., 1.)).st;\n}\n\n\n';
/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.map.shader.DefaultVertex.OPTIMIZED_SOURCE = 'varying vec2 a;attribute vec2 d,e;uniform mat4 b,c;void main(){gl_Position=c*vec4(d,0,1);a=(b*vec4(e,0,1)).st;}';
/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.map.shader.DefaultVertex.SOURCE = goog.DEBUG ?
    ol.renderer.webgl.map.shader.DefaultVertex.DEBUG_SOURCE :
    ol.renderer.webgl.map.shader.DefaultVertex.OPTIMIZED_SOURCE;
/**
 * @constructor
 * @param {WebGLRenderingContext} gl GL.
 * @param {WebGLProgram} program Program.
 */
ol.renderer.webgl.map.shader.Default.Locations = function(gl, program) {
  /**
   * @type {WebGLUniformLocation}
   */
  this.u_texCoordMatrix = gl.getUniformLocation(
      program, goog.DEBUG ? 'u_texCoordMatrix' : 'b');
  /**
   * @type {WebGLUniformLocation}
   */
  this.u_projectionMatrix = gl.getUniformLocation(
      program, goog.DEBUG ? 'u_projectionMatrix' : 'c');
  /**
   * @type {WebGLUniformLocation}
   */
  this.u_opacity = gl.getUniformLocation(
      program, goog.DEBUG ? 'u_opacity' : 'd');
  /**
   * @type {WebGLUniformLocation}
   */
  this.u_texture = gl.getUniformLocation(
      program, goog.DEBUG ? 'u_texture' : 'e');
  /**
   * @type {number}
   */
  this.a_position = gl.getAttribLocation(
      program, goog.DEBUG ? 'a_position' : 'd');
  /**
   * @type {number}
   */
  this.a_texCoord = gl.getAttribLocation(
      program, goog.DEBUG ? 'a_texCoord' : 'e');
};
<commit_msg>Remove stray comment<commit_after>// This file is automatically generated, do not edit
goog.provide('ol.renderer.webgl.map.shader.Default');
goog.require('ol.webgl.shader');
/**
 * @constructor
 * @extends {ol.webgl.shader.Fragment}
 */
ol.renderer.webgl.map.shader.DefaultFragment = function() {
  goog.base(this, ol.renderer.webgl.map.shader.DefaultFragment.SOURCE);
};
goog.inherits(ol.renderer.webgl.map.shader.DefaultFragment, ol.webgl.shader.Fragment);
goog.addSingletonGetter(ol.renderer.webgl.map.shader.DefaultFragment);
/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.map.shader.DefaultFragment.DEBUG_SOURCE = 'precision mediump float;\n//! NAMESPACE=ol.renderer.webgl.map.shader.Default\n//! CLASS=ol.renderer.webgl.map.shader.Default\n\n\n//! COMMON\nvarying vec2 v_texCoord;\n\n\n//! FRAGMENT\nuniform float u_opacity;\nuniform sampler2D u_texture;\n\nvoid main(void) {\n  vec4 texColor = texture2D(u_texture, v_texCoord);\n  gl_FragColor.rgb = texColor.rgb;\n  gl_FragColor.a = texColor.a * u_opacity;\n}\n\n';
/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.map.shader.DefaultFragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;uniform float d;uniform sampler2D e;void main(){vec4 f=texture2D(e,a);gl_FragColor.rgb=f.rgb;gl_FragColor.a=f.a*d;}';
/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.map.shader.DefaultFragment.SOURCE = goog.DEBUG ?
    ol.renderer.webgl.map.shader.DefaultFragment.DEBUG_SOURCE :
    ol.renderer.webgl.map.shader.DefaultFragment.OPTIMIZED_SOURCE;
/**
 * @constructor
 * @extends {ol.webgl.shader.Vertex}
 */
ol.renderer.webgl.map.shader.DefaultVertex = function() {
  goog.base(this, ol.renderer.webgl.map.shader.DefaultVertex.SOURCE);
};
goog.inherits(ol.renderer.webgl.map.shader.DefaultVertex, ol.webgl.shader.Vertex);
goog.addSingletonGetter(ol.renderer.webgl.map.shader.DefaultVertex);
/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.map.shader.DefaultVertex.DEBUG_SOURCE = '//! NAMESPACE=ol.renderer.webgl.map.shader.Default\n//! CLASS=ol.renderer.webgl.map.shader.Default\n\n\n//! COMMON\nvarying vec2 v_texCoord;\n\n\n//! VERTEX\nattribute vec2 a_position;\nattribute vec2 a_texCoord;\n\nuniform mat4 u_texCoordMatrix;\nuniform mat4 u_projectionMatrix;\n\nvoid main(void) {\n  gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.);\n  v_texCoord = (u_texCoordMatrix * vec4(a_texCoord, 0., 1.)).st;\n}\n\n\n';
/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.map.shader.DefaultVertex.OPTIMIZED_SOURCE = 'varying vec2 a;attribute vec2 d,e;uniform mat4 b,c;void main(){gl_Position=c*vec4(d,0,1);a=(b*vec4(e,0,1)).st;}';
/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.map.shader.DefaultVertex.SOURCE = goog.DEBUG ?
    ol.renderer.webgl.map.shader.DefaultVertex.DEBUG_SOURCE :
    ol.renderer.webgl.map.shader.DefaultVertex.OPTIMIZED_SOURCE;
/**
 * @constructor
 * @param {WebGLRenderingContext} gl GL.
 * @param {WebGLProgram} program Program.
 */
ol.renderer.webgl.map.shader.Default.Locations = function(gl, program) {
  /**
   * @type {WebGLUniformLocation}
   */
  this.u_texCoordMatrix = gl.getUniformLocation(
      program, goog.DEBUG ? 'u_texCoordMatrix' : 'b');
  /**
   * @type {WebGLUniformLocation}
   */
  this.u_projectionMatrix = gl.getUniformLocation(
      program, goog.DEBUG ? 'u_projectionMatrix' : 'c');
  /**
   * @type {WebGLUniformLocation}
   */
  this.u_opacity = gl.getUniformLocation(
      program, goog.DEBUG ? 'u_opacity' : 'd');
  /**
   * @type {WebGLUniformLocation}
   */
  this.u_texture = gl.getUniformLocation(
      program, goog.DEBUG ? 'u_texture' : 'e');
  /**
   * @type {number}
   */
  this.a_position = gl.getAttribLocation(
      program, goog.DEBUG ? 'a_position' : 'd');
  /**
   * @type {number}
   */
  this.a_texCoord = gl.getAttribLocation(
      program, goog.DEBUG ? 'a_texCoord' : 'e');
};
<|endoftext|>


<commit_before>/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

/*
 * Copyright 2019 Joyent, Inc.
 */

var restify = require('restify');

var common = require('../common');
var checkHeaders = common.checkHeaders;
var checkNotFound = common.checkNotFound;

var waitForJob = require('./common').waitForJob;

// --- Helpers

function checkMigration(t, migration) {
    t.ok(migration, 'migration ok');
    t.ok(migration.machine, 'migration machine ok');
    t.ok(migration.state, 'migration state ok');
    t.ok(migration.phase, 'migration phase ok');
    t.ok(migration.created_timestamp, 'migration created_timestamp ok');
}

// --- Tests

module.exports = function migrate(suite, client, other, machine, callback) {
    if (!machine) {
        callback();
        return;
    }

    var migrationFailed = false;

    var httpClient = restify.createHttpClient({
        url: client.url.href,
        version: client.headers['accept-version'],
        retryOptions: {
            retry: 0
        },
        log: client.log,
        rejectUnauthorized: false,
        signRequest: client.signRequest
    });
    httpClient.keyId = client.keyId;
    httpClient.privateKey = client.privateKey;
    httpClient.publicKey = client.publicKey;

    var watcher = {
        done: false,
        started: false,
        error: null,
        events: []
    };

    function watch() {
        watcher.done = false;

        var p = '/my/machines/' + machine + '/migrate?action=watch';

        httpClient.post(p, function reqCb(reqErr, req) {
            if (reqErr) {
                watcher.error = reqErr;
                watcher.done = true;
                return;
            }

            req.on('result', function resultCb(resErr, res) {
                if (resErr) {
                    watcher.error = resErr;
                    watcher.done = true;
                    return;
                }

                res.body = '';
                res.setEncoding('utf8');
                res.on('data', function onDataCb(chunk) {
                    res.body += chunk;
                });

                res.on('end', function onEndCb() {
                    res.body = res.body.trim().split('\n')
                        .map(function toJson(chunk) {
                            return JSON.parse(chunk);
                        });

                    watcher.events.push(res.body);
                    watcher.done = true;
                });
            });

            req.end();
        });
    }

    suite.test('estimate migration', function (t) {
        var url = '/my/machines/' + machine + '/migrate';
        client.get(url, function getEstimateCb(err, req, res, body) {
            t.ifError(err, 'migration estimate error');
            t.ok(body, 'migration estimate');
            t.ok(body.size, 'migration size');
            // TODO: Add other estimation fields when added
            t.end();
        });
    });

    var actions = ['begin', 'abort', 'begin', 'sync', 'sync', 'switch'];

    actions.forEach(function testMigrationAction(action) {
        suite.test(action + ' migration', function actionMigrCb(t) {
            if (migrationFailed) {
                t.end();
                return;
            }
            var url = '/my/machines/' + machine + '/migrate';
            client.post(url, {
                action: action
            }, function doPostCb(err, req, res, body) {
                t.ifError(err);
                if (err) {
                    migrationFailed = true;
                    t.end();
                    return;
                }
                t.equal(res.statusCode, 201);
                checkHeaders(t, res.headers);
                t.ok(body);
                checkMigration(t, body);
                if (!watcher.started) {
                    watcher.started = true;
                }
                watch();
                t.end();
            });
        });

        suite.test('Wait for migrate ' + action, function waitTest(t) {
            if (migrationFailed) {
                t.end();
                return;
            }
            client.vmapi.listJobs({
                vm_uuid: machine,
                task: 'migrate-' + action
            }, function listJobsCb(err, jobs) {
                t.ifError(err, 'list jobs error');
                t.ok(jobs, 'list jobs OK');
                t.ok(jobs.length, 'migrate jobs is array');
                var migrate_job = jobs[0];
                waitForJob(client, migrate_job.uuid, function waitCb(err2) {
                    if (err2) {
                        migrationFailed = true;
                    }
                    t.ifError(err2, 'Check state error');
                    t.end();
                });
            });
        });

        suite.test('Check watcher entries after ' + action, function wCb(t) {
            var count = 0;
            var maxSecs = 5 * 60; // 5 minutes

            function waitForWatcherEnd() {
                count += 5;
                if (!watcher.done) {
                    if (count > maxSecs) {
                        t.ok(false, 'Timed out waiting for the watcher to end');
                        t.done();
                        return;
                    }
                    setTimeout(waitForWatcherEnd, 5000);
                    return;
                }

                var events = watcher.events.pop();
                t.ok(events.length > 0, 'Should have seen events');
                var endEvent = events.filter(function filterEnd(evt) {
                    return evt.type === 'end';
                })[0];
                t.ok(endEvent, 'Should have seen end event');
                t.equal(endEvent.phase, action, 'Phase should be ' + action);
                var progressEvents = events.filter(function filterProgr(evt) {
                    return evt.type === 'progress';
                });
                progressEvents.forEach(function testPrgrEvt(evt) {
                    t.ok(evt.current_progress, 'Should have current_progress');
                    t.ok(evt.message, 'Should have a message');
                    t.ok(evt.state, 'Should have event state');
                    t.ok(evt.total_progress, 'Should have total_progress');
                    if (evt.started_timestamp) {
                        t.ok(evt.duration_ms,
                            'Should have duration_ms and started_timestamp');
                    }
                });
                t.done();
            }

            waitForWatcherEnd();
        });

        suite.test('List migrations after ' + action, function lsCb(t) {
            var url = '/my/migrations';
            // Should get at least a migration.
            client.get(url, function listCb(err, req, res, body) {
                t.ifError(err, 'list migrations error');
                t.ok(body, 'migrations list');
                t.ok(body.length, 'migrations length');
                checkMigration(t, body[0]);
                t.end();
            });
        });

        suite.test('Get migration after ' + action, function gCb(t) {
            var url = '/my/migrations/' + machine;
            client.get(url, function getCb(err, req, res, body) {
                t.ifError(err, 'get migration error');
                t.ok(body, 'migration');
                checkMigration(t, body);
                t.end();
            });
        });
    });

    suite.test('List migrations other', function (t) {
        other.get('/my/migrations', function otherLsCb(err, req, res, body) {
            t.ifError(err);
            t.deepEqual(body, []);
            t.end();
        });
    });

    suite.test('Get migration other', function (t) {
        var p = '/my/migrations/' + machine;
        other.get(p, function otherGetCb(err, req, res, body) {
            checkNotFound(t, err, req, res, body);
            t.end();
        });
    });

    suite.test('Close migrations watch client', function closeTest(t) {
        httpClient.close();
        t.end();
    });

    callback();
};
<commit_msg>TRITON-1835 cloudapi migration tests do not need to check the sync message field Reviewed by: Pedro Palazón Candel <pedro@joyent.com> Approved by: Pedro Palazón Candel <pedro@joyent.com><commit_after>/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

/*
 * Copyright 2019 Joyent, Inc.
 */

var restify = require('restify');

var common = require('../common');
var checkHeaders = common.checkHeaders;
var checkNotFound = common.checkNotFound;

var waitForJob = require('./common').waitForJob;

// --- Helpers

function checkMigration(t, migration) {
    t.ok(migration, 'migration ok');
    t.ok(migration.machine, 'migration machine ok');
    t.ok(migration.state, 'migration state ok');
    t.ok(migration.phase, 'migration phase ok');
    t.ok(migration.created_timestamp, 'migration created_timestamp ok');
}

// --- Tests

module.exports = function migrate(suite, client, other, machine, callback) {
    if (!machine) {
        callback();
        return;
    }

    var migrationFailed = false;

    var httpClient = restify.createHttpClient({
        url: client.url.href,
        version: client.headers['accept-version'],
        retryOptions: {
            retry: 0
        },
        log: client.log,
        rejectUnauthorized: false,
        signRequest: client.signRequest
    });
    httpClient.keyId = client.keyId;
    httpClient.privateKey = client.privateKey;
    httpClient.publicKey = client.publicKey;

    var watcher = {
        done: false,
        started: false,
        error: null,
        events: []
    };

    function watch() {
        watcher.done = false;

        var p = '/my/machines/' + machine + '/migrate?action=watch';

        httpClient.post(p, function reqCb(reqErr, req) {
            if (reqErr) {
                watcher.error = reqErr;
                watcher.done = true;
                return;
            }

            req.on('result', function resultCb(resErr, res) {
                if (resErr) {
                    watcher.error = resErr;
                    watcher.done = true;
                    return;
                }

                res.body = '';
                res.setEncoding('utf8');
                res.on('data', function onDataCb(chunk) {
                    res.body += chunk;
                });

                res.on('end', function onEndCb() {
                    res.body = res.body.trim().split('\n')
                        .map(function toJson(chunk) {
                            return JSON.parse(chunk);
                        });

                    watcher.events.push(res.body);
                    watcher.done = true;
                });
            });

            req.end();
        });
    }

    suite.test('estimate migration', function (t) {
        var url = '/my/machines/' + machine + '/migrate';
        client.get(url, function getEstimateCb(err, req, res, body) {
            t.ifError(err, 'migration estimate error');
            t.ok(body, 'migration estimate');
            t.ok(body.size, 'migration size');
            // TODO: Add other estimation fields when added
            t.end();
        });
    });

    var actions = ['begin', 'abort', 'begin', 'sync', 'sync', 'switch'];

    actions.forEach(function testMigrationAction(action) {
        suite.test(action + ' migration', function actionMigrCb(t) {
            if (migrationFailed) {
                t.end();
                return;
            }
            var url = '/my/machines/' + machine + '/migrate';
            client.post(url, {
                action: action
            }, function doPostCb(err, req, res, body) {
                t.ifError(err);
                if (err) {
                    migrationFailed = true;
                    t.end();
                    return;
                }
                t.equal(res.statusCode, 201);
                checkHeaders(t, res.headers);
                t.ok(body);
                checkMigration(t, body);
                if (!watcher.started) {
                    watcher.started = true;
                }
                watch();
                t.end();
            });
        });

        suite.test('Wait for migrate ' + action, function waitTest(t) {
            if (migrationFailed) {
                t.end();
                return;
            }
            client.vmapi.listJobs({
                vm_uuid: machine,
                task: 'migrate-' + action
            }, function listJobsCb(err, jobs) {
                t.ifError(err, 'list jobs error');
                t.ok(jobs, 'list jobs OK');
                t.ok(jobs.length, 'migrate jobs is array');
                var migrate_job = jobs[0];
                waitForJob(client, migrate_job.uuid, function waitCb(err2) {
                    if (err2) {
                        migrationFailed = true;
                    }
                    t.ifError(err2, 'Check state error');
                    t.end();
                });
            });
        });

        suite.test('Check watcher entries after ' + action, function wCb(t) {
            var count = 0;
            var maxSecs = 5 * 60; // 5 minutes

            function waitForWatcherEnd() {
                count += 5;
                if (!watcher.done) {
                    if (count > maxSecs) {
                        t.ok(false, 'Timed out waiting for the watcher to end');
                        t.done();
                        return;
                    }
                    setTimeout(waitForWatcherEnd, 5000);
                    return;
                }

                var events = watcher.events.pop();
                t.ok(events.length > 0, 'Should have seen events');
                var endEvent = events.filter(function filterEnd(evt) {
                    return evt.type === 'end';
                })[0];
                t.ok(endEvent, 'Should have seen end event');
                t.equal(endEvent.phase, action, 'Phase should be ' + action);
                var progressEvents = events.filter(function filterProgr(evt) {
                    return evt.type === 'progress';
                });
                progressEvents.forEach(function testPrgrEvt(evt) {
                    t.ok(evt.current_progress, 'Should have current_progress');
                    t.ok(evt.state, 'Should have event state');
                    t.ok(evt.total_progress, 'Should have total_progress');
                    if (evt.started_timestamp) {
                        t.ok(evt.duration_ms,
                            'Should have duration_ms and started_timestamp');
                    }
                });
                t.done();
            }

            waitForWatcherEnd();
        });

        suite.test('List migrations after ' + action, function lsCb(t) {
            var url = '/my/migrations';
            // Should get at least a migration.
            client.get(url, function listCb(err, req, res, body) {
                t.ifError(err, 'list migrations error');
                t.ok(body, 'migrations list');
                t.ok(body.length, 'migrations length');
                checkMigration(t, body[0]);
                t.end();
            });
        });

        suite.test('Get migration after ' + action, function gCb(t) {
            var url = '/my/migrations/' + machine;
            client.get(url, function getCb(err, req, res, body) {
                t.ifError(err, 'get migration error');
                t.ok(body, 'migration');
                checkMigration(t, body);
                t.end();
            });
        });
    });

    suite.test('List migrations other', function (t) {
        other.get('/my/migrations', function otherLsCb(err, req, res, body) {
            t.ifError(err);
            t.deepEqual(body, []);
            t.end();
        });
    });

    suite.test('Get migration other', function (t) {
        var p = '/my/migrations/' + machine;
        other.get(p, function otherGetCb(err, req, res, body) {
            checkNotFound(t, err, req, res, body);
            t.end();
        });
    });

    suite.test('Close migrations watch client', function closeTest(t) {
        httpClient.close();
        t.end();
    });

    callback();
};
<|endoftext|>


<commit_before>import Axis from "../Axis.js";
import Color from "../../Core/Color.js";
import defined from "../../Core/defined.js";
import destroyObject from "../../Core/destroyObject.js";
import Pass from "../../Renderer/Pass.js";
import ModelExperimental from "./ModelExperimental.js";

/**
 * Represents the contents of a glTF, glb or
 * {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/Batched3DModel|Batched 3D Model}
 * tile in a {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification|3D Tiles} tileset.
 *

 * Implements the {@link Cesium3DTileContent} interface.
 *

 * @alias ModelExperimental3DTileContent
 * @constructor
 * @private
 */
export default function ModelExperimental3DTileContent(
  tileset,
  tile,
  resource
) {
  this._tileset = tileset;
  this._tile = tile;
  this._resource = resource;

  this._model = undefined;
  this._groupMetadata = undefined;
}

Object.defineProperties(ModelExperimental3DTileContent.prototype, {
  featuresLength: {
    get: function () {
      const model = this._model;
      const featureTables = model.featureTables;
      const featureTableId = model.featureTableId;

      if (defined(featureTables) && defined(featureTables[featureTableId])) {
        return featureTables[featureTableId].featuresLength;
      }

      return 0;
    },
  },

  pointsLength: {
    get: function () {
      return 0;
    },
  },

  trianglesLength: {
    get: function () {
      return 0;
    },
  },

  geometryByteLength: {
    get: function () {
      return 0;
    },
  },

  texturesByteLength: {
    get: function () {
      return 0;
    },
  },

  batchTableByteLength: {
    get: function () {
      return 0;
    },
  },

  innerContents: {
    get: function () {
      return undefined;
    },
  },

  readyPromise: {
    get: function () {
      return this._model.readyPromise;
    },
  },

  tileset: {
    get: function () {
      return this._tileset;
    },
  },

  tile: {
    get: function () {
      return this._tile;
    },
  },

  url: {
    get: function () {
      return this._resource.getUrlComponent(true);
    },
  },

  batchTable: {
    get: function () {
      const model = this._model;
      const featureTables = model.featureTables;
      const featureTableId = model.featureTableId;

      if (defined(featureTables) && defined(featureTables[featureTableId])) {
        return featureTables[featureTableId];
      }

      return undefined;
    },
  },

  groupMetadata: {
    get: function () {
      return this._groupMetadata;
    },
    set: function (value) {
      this._groupMetadata = value;
    },
  },
});

ModelExperimental3DTileContent.prototype.getFeature = function (featureId) {
  const model = this._model;
  const featureTableId = model.featureTableId;
  if (!defined(featureTableId)) {
    return undefined;
  }

  const featureTable = model.featureTables[featureTableId];
  return featureTable.getFeature(featureId);
};

ModelExperimental3DTileContent.prototype.hasProperty = function (
  featureId,
  name
) {
  const model = this._model;
  const featureTableId = model.featureTableId;
  if (!defined(featureTableId)) {
    return false;
  }

  const featureTable = model.featureTables[featureTableId];
  return featureTable.hasProperty(featureId, name);
};

ModelExperimental3DTileContent.prototype.applyDebugSettings = function (
  enabled,
  color
) {
  color = enabled ? color : Color.WHITE;
  if (this.featuresLength === 0) {
    this._model.color = color;
  } else if (defined(this.batchTable)) {
    this.batchTable.setAllColor(color);
  }
};

ModelExperimental3DTileContent.prototype.applyStyle = function (style) {
  // the setter will call model.applyStyle()
  this._model.style = style;
};

ModelExperimental3DTileContent.prototype.update = function (
  tileset,
  frameState
) {
  const model = this._model;
  const tile = this._tile;

  model.colorBlendAmount = tileset.colorBlendAmount;
  model.colorBlendMode = tileset.colorBlendMode;
  model.modelMatrix = tile.computedTransform;
  model.customShader = tileset.customShader;
  model.pointCloudShading = tileset.pointCloudShading;
  model.featureIdIndex = tileset.featureIdIndex;
  model.instanceFeatureIdIndex = tileset.instanceFeatureIdIndex;
  model.backFaceCulling = tileset.backFaceCulling;

  model.update(frameState);
};

ModelExperimental3DTileContent.prototype.isDestroyed = function () {
  return false;
};

ModelExperimental3DTileContent.prototype.destroy = function () {
  this._model = this._model && this._model.destroy();
  return destroyObject(this);
};

ModelExperimental3DTileContent.fromGltf = function (
  tileset,
  tile,
  resource,
  gltf
) {
  const content = new ModelExperimental3DTileContent(tileset, tile, resource);

  const modelOptions = {
    gltf: gltf,
    cull: false, // The model is already culled by 3D Tiles
    releaseGltfJson: true, // Models are unique and will not benefit from caching so save memory
    opaquePass: Pass.CESIUM_3D_TILE, // Draw opaque portions of the model during the 3D Tiles pass
    basePath: resource,
    modelMatrix: tile.computedTransform,
    upAxis: tileset._gltfUpAxis,
    forwardAxis: Axis.X,
    incrementallyLoadTextures: false,
    customShader: tileset.customShader,
    content: content,
    colorBlendMode: tileset.colorBlendMode,
    colorBlendAmount: tileset.colorBlendAmount,
    backFaceCulling: tileset.backFaceCulling,
  };
  content._model = ModelExperimental.fromGltf(modelOptions);
  return content;
};

ModelExperimental3DTileContent.fromB3dm = function (
  tileset,
  tile,
  resource,
  arrayBuffer,
  byteOffset
) {
  const content = new ModelExperimental3DTileContent(tileset, tile, resource);

  const modelOptions = {
    arrayBuffer: arrayBuffer,
    byteOffset: byteOffset,
    resource: resource,
    cull: false, // The model is already culled by 3D Tiles
    releaseGltfJson: true, // Models are unique and will not benefit from caching so save memory
    opaquePass: Pass.CESIUM_3D_TILE, // Draw opaque portions of the model during the 3D Tiles pass
    modelMatrix: tile.computedTransform,
    upAxis: tileset._gltfUpAxis,
    forwardAxis: Axis.X,
    incrementallyLoadTextures: false,
    customShader: tileset.customShader,
    content: content,
    colorBlendMode: tileset.colorBlendMode,
    colorBlendAmount: tileset.colorBlendAmount,
  };
  content._model = ModelExperimental.fromB3dm(modelOptions);
  return content;
};

ModelExperimental3DTileContent.fromI3dm = function (
  tileset,
  tile,
  resource,
  arrayBuffer,
  byteOffset
) {
  const content = new ModelExperimental3DTileContent(tileset, tile, resource);

  const modelOptions = {
    arrayBuffer: arrayBuffer,
    byteOffset: byteOffset,
    resource: resource,
    cull: false, // The model is already culled by 3D Tiles
    releaseGltfJson: true, // Models are unique and will not benefit from caching so save memory
    opaquePass: Pass.CESIUM_3D_TILE, // Draw opaque portions of the model during the 3D Tiles pass
    modelMatrix: tile.computedTransform,
    upAxis: tileset._gltfUpAxis,
    forwardAxis: Axis.X,
    incrementallyLoadTextures: false,
    customShader: tileset.customShader,
    content: content,
    colorBlendMode: tileset.colorBlendMode,
    colorBlendAmount: tileset.colorBlendAmount,
  };
  content._model = ModelExperimental.fromI3dm(modelOptions);
  return content;
};

ModelExperimental3DTileContent.fromPnts = function (
  tileset,
  tile,
  resource,
  arrayBuffer,
  byteOffset
) {
  const content = new ModelExperimental3DTileContent(tileset, tile, resource);

  const modelOptions = {
    arrayBuffer: arrayBuffer,
    byteOffset: byteOffset,
    resource: resource,
    cull: false, // The model is already culled by 3D Tiles
    releaseGltfJson: true, // Models are unique and will not benefit from caching so save memory
    opaquePass: Pass.CESIUM_3D_TILE, // Draw opaque portions of the model during the 3D Tiles pass
    modelMatrix: tile.computedTransform,
    upAxis: tileset._gltfUpAxis,
    forwardAxis: Axis.X,
    incrementallyLoadTextures: false,
    customShader: tileset.customShader,
    content: content,
    colorBlendMode: tileset.colorBlendMode,
    colorBlendAmount: tileset.colorBlendAmount,
  };
  content._model = ModelExperimental.fromPnts(modelOptions);
  return content;
};
<commit_msg>Fix shadows of tileset<commit_after>import Axis from "../Axis.js";
import Color from "../../Core/Color.js";
import defined from "../../Core/defined.js";
import destroyObject from "../../Core/destroyObject.js";
import Pass from "../../Renderer/Pass.js";
import ModelExperimental from "./ModelExperimental.js";

/**
 * Represents the contents of a glTF, glb or
 * {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/Batched3DModel|Batched 3D Model}
 * tile in a {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification|3D Tiles} tileset.
 *

 * Implements the {@link Cesium3DTileContent} interface.
 *

 * @alias ModelExperimental3DTileContent
 * @constructor
 * @private
 */
export default function ModelExperimental3DTileContent(
  tileset,
  tile,
  resource
) {
  this._tileset = tileset;
  this._tile = tile;
  this._resource = resource;

  this._model = undefined;
  this._groupMetadata = undefined;
}

Object.defineProperties(ModelExperimental3DTileContent.prototype, {
  featuresLength: {
    get: function () {
      const model = this._model;
      const featureTables = model.featureTables;
      const featureTableId = model.featureTableId;

      if (defined(featureTables) && defined(featureTables[featureTableId])) {
        return featureTables[featureTableId].featuresLength;
      }

      return 0;
    },
  },

  pointsLength: {
    get: function () {
      return 0;
    },
  },

  trianglesLength: {
    get: function () {
      return 0;
    },
  },

  geometryByteLength: {
    get: function () {
      return 0;
    },
  },

  texturesByteLength: {
    get: function () {
      return 0;
    },
  },

  batchTableByteLength: {
    get: function () {
      return 0;
    },
  },

  innerContents: {
    get: function () {
      return undefined;
    },
  },

  readyPromise: {
    get: function () {
      return this._model.readyPromise;
    },
  },

  tileset: {
    get: function () {
      return this._tileset;
    },
  },

  tile: {
    get: function () {
      return this._tile;
    },
  },

  url: {
    get: function () {
      return this._resource.getUrlComponent(true);
    },
  },

  batchTable: {
    get: function () {
      const model = this._model;
      const featureTables = model.featureTables;
      const featureTableId = model.featureTableId;

      if (defined(featureTables) && defined(featureTables[featureTableId])) {
        return featureTables[featureTableId];
      }

      return undefined;
    },
  },

  groupMetadata: {
    get: function () {
      return this._groupMetadata;
    },
    set: function (value) {
      this._groupMetadata = value;
    },
  },
});

ModelExperimental3DTileContent.prototype.getFeature = function (featureId) {
  const model = this._model;
  const featureTableId = model.featureTableId;
  if (!defined(featureTableId)) {
    return undefined;
  }

  const featureTable = model.featureTables[featureTableId];
  return featureTable.getFeature(featureId);
};

ModelExperimental3DTileContent.prototype.hasProperty = function (
  featureId,
  name
) {
  const model = this._model;
  const featureTableId = model.featureTableId;
  if (!defined(featureTableId)) {
    return false;
  }

  const featureTable = model.featureTables[featureTableId];
  return featureTable.hasProperty(featureId, name);
};

ModelExperimental3DTileContent.prototype.applyDebugSettings = function (
  enabled,
  color
) {
  color = enabled ? color : Color.WHITE;
  if (this.featuresLength === 0) {
    this._model.color = color;
  } else if (defined(this.batchTable)) {
    this.batchTable.setAllColor(color);
  }
};

ModelExperimental3DTileContent.prototype.applyStyle = function (style) {
  // the setter will call model.applyStyle()
  this._model.style = style;
};

ModelExperimental3DTileContent.prototype.update = function (
  tileset,
  frameState
) {
  const model = this._model;
  const tile = this._tile;

  model.colorBlendAmount = tileset.colorBlendAmount;
  model.colorBlendMode = tileset.colorBlendMode;
  model.modelMatrix = tile.computedTransform;
  model.customShader = tileset.customShader;
  model.pointCloudShading = tileset.pointCloudShading;
  model.featureIdIndex = tileset.featureIdIndex;
  model.instanceFeatureIdIndex = tileset.instanceFeatureIdIndex;
  model.backFaceCulling = tileset.backFaceCulling;

  model.update(frameState);
};

ModelExperimental3DTileContent.prototype.isDestroyed = function () {
  return false;
};

ModelExperimental3DTileContent.prototype.destroy = function () {
  this._model = this._model && this._model.destroy();
  return destroyObject(this);
};

ModelExperimental3DTileContent.fromGltf = function (
  tileset,
  tile,
  resource,
  gltf
) {
  const content = new ModelExperimental3DTileContent(tileset, tile, resource);

  const modelOptions = {
    gltf: gltf,
    cull: false, // The model is already culled by 3D Tiles
    releaseGltfJson: true, // Models are unique and will not benefit from caching so save memory
    opaquePass: Pass.CESIUM_3D_TILE, // Draw opaque portions of the model during the 3D Tiles pass
    basePath: resource,
    modelMatrix: tile.computedTransform,
    upAxis: tileset._gltfUpAxis,
    forwardAxis: Axis.X,
    incrementallyLoadTextures: false,
    customShader: tileset.customShader,
    content: content,
    colorBlendMode: tileset.colorBlendMode,
    colorBlendAmount: tileset.colorBlendAmount,
    backFaceCulling: tileset.backFaceCulling,
    shadows: tileset.shadows,
  };
  content._model = ModelExperimental.fromGltf(modelOptions);
  return content;
};

ModelExperimental3DTileContent.fromB3dm = function (
  tileset,
  tile,
  resource,
  arrayBuffer,
  byteOffset
) {
  const content = new ModelExperimental3DTileContent(tileset, tile, resource);

  const modelOptions = {
    arrayBuffer: arrayBuffer,
    byteOffset: byteOffset,
    resource: resource,
    cull: false, // The model is already culled by 3D Tiles
    releaseGltfJson: true, // Models are unique and will not benefit from caching so save memory
    opaquePass: Pass.CESIUM_3D_TILE, // Draw opaque portions of the model during the 3D Tiles pass
    modelMatrix: tile.computedTransform,
    upAxis: tileset._gltfUpAxis,
    forwardAxis: Axis.X,
    incrementallyLoadTextures: false,
    customShader: tileset.customShader,
    content: content,
    colorBlendMode: tileset.colorBlendMode,
    colorBlendAmount: tileset.colorBlendAmount,
  };
  content._model = ModelExperimental.fromB3dm(modelOptions);
  return content;
};

ModelExperimental3DTileContent.fromI3dm = function (
  tileset,
  tile,
  resource,
  arrayBuffer,
  byteOffset
) {
  const content = new ModelExperimental3DTileContent(tileset, tile, resource);

  const modelOptions = {
    arrayBuffer: arrayBuffer,
    byteOffset: byteOffset,
    resource: resource,
    cull: false, // The model is already culled by 3D Tiles
    releaseGltfJson: true, // Models are unique and will not benefit from caching so save memory
    opaquePass: Pass.CESIUM_3D_TILE, // Draw opaque portions of the model during the 3D Tiles pass
    modelMatrix: tile.computedTransform,
    upAxis: tileset._gltfUpAxis,
    forwardAxis: Axis.X,
    incrementallyLoadTextures: false,
    customShader: tileset.customShader,
    content: content,
    colorBlendMode: tileset.colorBlendMode,
    colorBlendAmount: tileset.colorBlendAmount,
  };
  content._model = ModelExperimental.fromI3dm(modelOptions);
  return content;
};

ModelExperimental3DTileContent.fromPnts = function (
  tileset,
  tile,
  resource,
  arrayBuffer,
  byteOffset
) {
  const content = new ModelExperimental3DTileContent(tileset, tile, resource);

  const modelOptions = {
    arrayBuffer: arrayBuffer,
    byteOffset: byteOffset,
    resource: resource,
    cull: false, // The model is already culled by 3D Tiles
    releaseGltfJson: true, // Models are unique and will not benefit from caching so save memory
    opaquePass: Pass.CESIUM_3D_TILE, // Draw opaque portions of the model during the 3D Tiles pass
    modelMatrix: tile.computedTransform,
    upAxis: tileset._gltfUpAxis,
    forwardAxis: Axis.X,
    incrementallyLoadTextures: false,
    customShader: tileset.customShader,
    content: content,
    colorBlendMode: tileset.colorBlendMode,
    colorBlendAmount: tileset.colorBlendAmount,
  };
  content._model = ModelExperimental.fromPnts(modelOptions);
  return content;
};
<|endoftext|>


<commit_before>/*global define*/
define([
        '../../Core/Cartesian2',
        '../../Core/defaultValue',
        '../../Core/defined',
        '../../Core/defineProperties',
        '../../Core/DeveloperError',
        '../../Core/Event',
        '../../Scene/SceneTransforms',
        '../../ThirdParty/knockout',
        '../../ThirdParty/Tween'
    ], function(
        Cartesian2,
        defaultValue,
        defined,
        defineProperties,
        DeveloperError,
        Event,
        SceneTransforms,
        knockout,
        Tween) {
    "use strict";

    var screenSpacePos = new Cartesian2();

    function trackPosition(viewModel, position) {
        var container = viewModel._container;
        var containerWidth = container.parentNode.clientWidth;
        var containerHeight = container.parentNode.clientHeight;
        var indicatorSize = viewModel._selectionIndicatorElement.clientWidth;
        var halfSize = indicatorSize * 0.5;

        var posX = Math.min(Math.max(position.x, 0), containerWidth) - halfSize;
        var posY = Math.min(Math.max(position.y, 0), containerHeight) - halfSize;

        viewModel._positionX = Math.floor(posX + 0.25).toString() + 'px';
        viewModel._positionY = Math.floor(posY + 0.25).toString() + 'px';
    }

    /**
     * The view model for {@link SelectionIndicator}.
     * @alias SelectionIndicatorViewModel
     * @constructor
     *
     * @param {Scene} scene The scene instance to use.
     * @param {Element} selectionIndicatorElement The element containing all elements that make up the selection indicator.
     * @param {Element} container The DOM element that contains the widget.
     *
     * @exception {DeveloperError} scene is required.
     * @exception {DeveloperError} selectionIndicatorElement is required.
     * @exception {DeveloperError} container is required.
     *
     */
    var SelectionIndicatorViewModel = function(scene, selectionIndicatorElement, container) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(scene)) {
            throw new DeveloperError('scene is required.');
        }

        if (!defined(selectionIndicatorElement)) {
            throw new DeveloperError('selectionIndicatorElement is required.');
        }

        if (!defined(container)) {
            throw new DeveloperError('container is required.');
        }
        //>>includeEnd('debug')

        this._scene = scene;
        this._animationCollection = scene.getAnimations();
        this._container = defaultValue(container, document.body);
        this._selectionIndicatorElement = selectionIndicatorElement;
        this._computeScreenSpacePosition = function(position, result) {
            return SceneTransforms.wgs84ToWindowCoordinates(scene, position, result);
        };

        /**
         * The world position of the object for which to display the selection indicator.
         * @memberof SelectionIndicatorViewModel
         *
         * @type {Cartesian3}
         */
        this.position = undefined;

        /**
         * The x screen position of the selection indicator.
         * @memberof SelectionIndicatorViewModel.prototype
         *
         * @type {Number}
         */
        this._positionX = '-1000px';

        /**
         * The y screen position of the selection indicator.
         * @memberof SelectionIndicatorViewModel.prototype
         *
         * @type {Number}
         */
        this._positionY = '0';

        /**
         * The scale of the indicator relative to its default size.
         * @memberof SelectionIndicatorViewModel.prototype
         *
         * @type {Number}
         */
        this.scale = 1;

        /**
         * The rotation angle of the indicator, in degrees.
         * @memberof SelectionIndicatorViewModel.prototype
         *
         * @type {Number}
         */
        this.rotation = 0;

        /**
         * Gets or sets the maximum height of the info box in pixels.  This property is observable.
         * @type {Number}
         */
        this.maxHeight = 500;

        /**
         * Gets or sets the visibility of the selection indicator.
         * @memberof SelectionIndicatorViewModel.prototype
         *
         * @type {Boolean}
         */
        this.showSelection = false;

        knockout.track(this, ['position', '_positionX', '_positionY', 'scale', 'rotation', 'showSelection']);

        /**
         * Gets the visibility of the position indicator.
         * @memberof SelectionIndicatorViewModel.prototype
         *
         * @type {Boolean}
         */
        this.showPosition = undefined;
        knockout.defineProperty(this, 'showPosition', {
            get : function() {
                return this.showSelection && defined(this.position);
            }
        });

        knockout.defineProperty(this, '_transform', {
            get : function() {
                return 'rotate(' + (this.rotation) + ') scale(' + (this.scale) + ')';
            }
        });
    };

    /**
     * Updates the view of the selection indicator to match the position and content properties of the view model
     * @memberof SelectionIndicatorViewModel
     */
    SelectionIndicatorViewModel.prototype.update = function() {
        if (this.showSelection) {
            if (defined(this.position)) {
                var pos = this._computeScreenSpacePosition(this.position, screenSpacePos);
                trackPosition(this, pos);
            }
        }
    };

    /**
     * Animate the indicator to draw attention to the selection.
     * @memberof SelectionIndicatorViewModel
     */
    SelectionIndicatorViewModel.prototype.animateAppear = function() {
        var viewModel = this;
        this._animationCollection.add({
            startValue : {
                scale : 2
            },
            stopValue : {
                scale: 1
            },
            duration : 800,
            easingFunction : Tween.Easing.Exponential.Out,
            onUpdate : function (value) {
                viewModel.scale = value.scale;
            }
        });
    };

    /**
     * Animate the indicator to release the selection.
     * @memberof SelectionIndicatorViewModel
     */
    SelectionIndicatorViewModel.prototype.animateDepart = function() {
        var viewModel = this;
        this._animationCollection.add({
            startValue : {
                scale : viewModel.scale
            },
            stopValue : {
                scale : 1.5
            },
            duration : 800,
            easingFunction : Tween.Easing.Exponential.Out,
            onUpdate : function (value) {
                viewModel.scale = value.scale;
            }
        });
    };

    defineProperties(SelectionIndicatorViewModel.prototype, {
        /**
         * Gets the HTML element containing the selection indicator.
         * @memberof SelectionIndicatorViewModel.prototype
         *
         * @type {Element}
         */
        container : {
            get : function() {
                return this._container;
            }
        },
        /**
         * Gets the HTML element that holds the selection indicator.
         * @memberof SelectionIndicatorViewModel.prototype
         *
         * @type {Element}
         */
        selectionIndicatorElement : {
            get : function() {
                return this._selectionIndicatorElement;
            }
        },
        /**
         * Gets the scene to control.
         * @memberof SelectionIndicatorViewModel.prototype
         *
         * @type {Scene}
         */
        scene : {
            get : function() {
                return this._scene;
            }
        },
        /**
         * Gets or sets the function for converting the world position of the object to the screen space position.
         * Expects the {Cartesian3} parameter for the position and the optional {Cartesian2} parameter for the result.
         * Should return a {Cartesian2}.
         *
         * Defaults to SceneTransforms.wgs84ToWindowCoordinates
         *
         * @example
         * selectionIndicatorViewModel.computeScreenSpacePosition = function(position, result) {
         *     return Cartesian2.clone(position, result);
         * };
         *
         * @memberof SelectionIndicatorViewModel
         *
         * @type {Function}
         */
        computeScreenSpacePosition : {
            get : function() {
                return this._computeScreenSpacePosition;
            },
            set : function(value) {
                this._computeScreenSpacePosition = value;
            }
        }
    });

    return SelectionIndicatorViewModel;
});
<commit_msg>Don't document internal stuff.<commit_after>/*global define*/
define([
        '../../Core/Cartesian2',
        '../../Core/defaultValue',
        '../../Core/defined',
        '../../Core/defineProperties',
        '../../Core/DeveloperError',
        '../../Core/Event',
        '../../Scene/SceneTransforms',
        '../../ThirdParty/knockout',
        '../../ThirdParty/Tween'
    ], function(
        Cartesian2,
        defaultValue,
        defined,
        defineProperties,
        DeveloperError,
        Event,
        SceneTransforms,
        knockout,
        Tween) {
    "use strict";

    var screenSpacePos = new Cartesian2();

    function trackPosition(viewModel, position) {
        var container = viewModel._container;
        var containerWidth = container.parentNode.clientWidth;
        var containerHeight = container.parentNode.clientHeight;
        var indicatorSize = viewModel._selectionIndicatorElement.clientWidth;
        var halfSize = indicatorSize * 0.5;

        var posX = Math.min(Math.max(position.x, 0), containerWidth) - halfSize;
        var posY = Math.min(Math.max(position.y, 0), containerHeight) - halfSize;

        viewModel._positionX = Math.floor(posX + 0.25).toString() + 'px';
        viewModel._positionY = Math.floor(posY + 0.25).toString() + 'px';
    }

    /**
     * The view model for {@link SelectionIndicator}.
     * @alias SelectionIndicatorViewModel
     * @constructor
     *
     * @param {Scene} scene The scene instance to use.
     * @param {Element} selectionIndicatorElement The element containing all elements that make up the selection indicator.
     * @param {Element} container The DOM element that contains the widget.
     *
     * @exception {DeveloperError} scene is required.
     * @exception {DeveloperError} selectionIndicatorElement is required.
     * @exception {DeveloperError} container is required.
     *
     */
    var SelectionIndicatorViewModel = function(scene, selectionIndicatorElement, container) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(scene)) {
            throw new DeveloperError('scene is required.');
        }

        if (!defined(selectionIndicatorElement)) {
            throw new DeveloperError('selectionIndicatorElement is required.');
        }

        if (!defined(container)) {
            throw new DeveloperError('container is required.');
        }
        //>>includeEnd('debug')

        this._scene = scene;
        this._positionX = '-1000px';
        this._positionY = '0';
        this._animationCollection = scene.getAnimations();
        this._container = defaultValue(container, document.body);
        this._selectionIndicatorElement = selectionIndicatorElement;
        this._computeScreenSpacePosition = function(position, result) {
            return SceneTransforms.wgs84ToWindowCoordinates(scene, position, result);
        };

        /**
         * The world position of the object for which to display the selection indicator.
         * @memberof SelectionIndicatorViewModel
         *
         * @type {Cartesian3}
         */
        this.position = undefined;

        /**
         * The scale of the indicator relative to its default size.
         * @memberof SelectionIndicatorViewModel.prototype
         *
         * @type {Number}
         */
        this.scale = 1;

        /**
         * The rotation angle of the indicator, in degrees.
         * @memberof SelectionIndicatorViewModel.prototype
         *
         * @type {Number}
         */
        this.rotation = 0;

        /**
         * Gets or sets the maximum height of the info box in pixels.  This property is observable.
         * @type {Number}
         */
        this.maxHeight = 500;

        /**
         * Gets or sets the visibility of the selection indicator.
         * @memberof SelectionIndicatorViewModel.prototype
         *
         * @type {Boolean}
         */
        this.showSelection = false;

        knockout.track(this, ['position', '_positionX', '_positionY', 'scale', 'rotation', 'showSelection']);

        /**
         * Gets the visibility of the position indicator.
         * @memberof SelectionIndicatorViewModel.prototype
         *
         * @type {Boolean}
         */
        this.showPosition = undefined;
        knockout.defineProperty(this, 'showPosition', {
            get : function() {
                return this.showSelection && defined(this.position);
            }
        });

        knockout.defineProperty(this, '_transform', {
            get : function() {
                return 'rotate(' + (this.rotation) + ') scale(' + (this.scale) + ')';
            }
        });
    };

    /**
     * Updates the view of the selection indicator to match the position and content properties of the view model
     * @memberof SelectionIndicatorViewModel
     */
    SelectionIndicatorViewModel.prototype.update = function() {
        if (this.showSelection) {
            if (defined(this.position)) {
                var pos = this._computeScreenSpacePosition(this.position, screenSpacePos);
                trackPosition(this, pos);
            }
        }
    };

    /**
     * Animate the indicator to draw attention to the selection.
     * @memberof SelectionIndicatorViewModel
     */
    SelectionIndicatorViewModel.prototype.animateAppear = function() {
        var viewModel = this;
        this._animationCollection.add({
            startValue : {
                scale : 2
            },
            stopValue : {
                scale: 1
            },
            duration : 800,
            easingFunction : Tween.Easing.Exponential.Out,
            onUpdate : function (value) {
                viewModel.scale = value.scale;
            }
        });
    };

    /**
     * Animate the indicator to release the selection.
     * @memberof SelectionIndicatorViewModel
     */
    SelectionIndicatorViewModel.prototype.animateDepart = function() {
        var viewModel = this;
        this._animationCollection.add({
            startValue : {
                scale : viewModel.scale
            },
            stopValue : {
                scale : 1.5
            },
            duration : 800,
            easingFunction : Tween.Easing.Exponential.Out,
            onUpdate : function (value) {
                viewModel.scale = value.scale;
            }
        });
    };

    defineProperties(SelectionIndicatorViewModel.prototype, {
        /**
         * Gets the HTML element containing the selection indicator.
         * @memberof SelectionIndicatorViewModel.prototype
         *
         * @type {Element}
         */
        container : {
            get : function() {
                return this._container;
            }
        },
        /**
         * Gets the HTML element that holds the selection indicator.
         * @memberof SelectionIndicatorViewModel.prototype
         *
         * @type {Element}
         */
        selectionIndicatorElement : {
            get : function() {
                return this._selectionIndicatorElement;
            }
        },
        /**
         * Gets the scene to control.
         * @memberof SelectionIndicatorViewModel.prototype
         *
         * @type {Scene}
         */
        scene : {
            get : function() {
                return this._scene;
            }
        },
        /**
         * Gets or sets the function for converting the world position of the object to the screen space position.
         * Expects the {Cartesian3} parameter for the position and the optional {Cartesian2} parameter for the result.
         * Should return a {Cartesian2}.
         *
         * Defaults to SceneTransforms.wgs84ToWindowCoordinates
         *
         * @example
         * selectionIndicatorViewModel.computeScreenSpacePosition = function(position, result) {
         *     return Cartesian2.clone(position, result);
         * };
         *
         * @memberof SelectionIndicatorViewModel
         *
         * @type {Function}
         */
        computeScreenSpacePosition : {
            get : function() {
                return this._computeScreenSpacePosition;
            },
            set : function(value) {
                this._computeScreenSpacePosition = value;
            }
        }
    });

    return SelectionIndicatorViewModel;
});
<|endoftext|>


<commit_before>// These tests were written before rate-limit was factored outside of DDP Rate
// Limiter and thus are structured with DDP method invocations in mind. These
// rules still test abstract rate limit package behavior. The tests currently
// implemented are:
// * Empty rule set on RateLimiter construction
// * Multiple inputs, only 1 that matches rule and reaches rate limit
// * Multiple inputs, 1 hits rate limit, wait for reset, after which inputs
//   allowed
// * 2 rules, 3 inputs where 2/3 match 1 rule and thus hit rate limit. Second
//   input matches another rule and hits rate limit while 3rd rule not rate
//   limited
// * One rule affected by two inputs still throws
// * Global rule triggers on any invocation after reaching limit
// * Fuzzy rule matching triggers rate limit only when input has more keys than
//   rule
// * matchRule tests that have various levels of similarity in input and rule
// * generateKeyString tests for various matches creating appropriate string
//
// XXX These tests should be refactored to use Tinytest.add instead of
// testAsyncMulti as they're all on the server. Any future tests should be
// written that way.
Tinytest.add('rate limit tests - Check empty constructor creation',
  function (test) {
    r = new RateLimiter();
    test.equal(r.rules, {});
});

Tinytest.add('rate limit tests - Check single rule with multiple ' +
  'invocations, only 1 that matches',
  function (test) {
    r = new RateLimiter();
    var userIdOne = 1;
    var restrictJustUserIdOneRule = {
      userId: myUserId,
      IPAddr: null,
      method: null
    };

    r.addRule(restrictJustUserId1Rule, 1, 1000);
    var connectionHandle = createTempConnectionHandle(123, '127.0.0.1');
    var methodInvc1 = createTempMethodInvocation(userIdOne, connectionHandle,
      'login');
    var methodInvc2 = createTempMethodInvocation(2, connectionHandle,
      'login');
    for (var i = 0; i < 2; i++) {
      r.increment(methodInvc1);
      r.increment(methodInvc2);
    }
    test.equal(r.check(methodInvc1).allowed, false);
    test.equal(r.check(methodInvc2).allowed, true);
  });

testAsyncMulti("rate limit tests - Run multiple invocations and wait for one" +
  " to reset", [
  function (test, expect) {
    var self = this;
    self.r = new RateLimiter();
    self.userIdOne = 1;
    self.userIdTwo = 2;
    self.restrictJustUserIdOneRule = {
      userId: myUserId,
      IPAddr: null,
      method: null
    };
    self.r.addRule(self.restrictJustUserIdOneRule, 1, 1000);
    self.connectionHandle = createTempConnectionHandle(123, '127.0.0.1')
    self.methodInvc1 = createTempMethodInvocation(self.userIdOne,
      self.connectionHandle, 'login');
    self.methodInvc2 = createTempMethodInvocation(self.userIdTwo,
      self.connectionHandle, 'login');
    for (var i = 0; i < 2; i++) {
      self.r.increment(self.methodInvc1);
      self.r.increment(self.methodInvc2);
    }
    test.equal(self.r.check(self.methodInvc1).allowed, false);
    test.equal(self.r.check(self.methodInvc2).allowed, true);
    Meteor.setTimeout(expect(function () {}), 1000);
  },
  function (test, expect) {
    var self = this;
    for (var i = 0; i < 100; i++) {
      self.r.increment(self.methodInvc2);
    }

    test.equal(self.r.check(self.methodInvc1).allowed, true);
    test.equal(self.r.check(self.methodInvc2).allowed, true);
  }
]);

Tinytest.add('rate limit tests - Check two rules that affect same methodInvc' +
  ' still throw',
  function (test) {
    r = new RateLimiter();
    var loginMethodRule = {
      userId: null,
      IPAddr: null,
      method: 'login'
    };
    var onlyLimitEvenUserIdRule = {
      userId: function (userId) {
        return userId % 2 === 0
      },
      IPAddr: null,
      method: null
    };
    r.addRule(loginMethodRule, 10, 100);
    r.addRule(onlyLimitEvenUserIdRule, 4, 100);

    var connectionHandle = createTempConnectionHandle(1234, '127.0.0.1');
    var methodInvc1 = createTempMethodInvocation(1, connectionHandle,
      'login');
    var methodInvc2 = createTempMethodInvocation(2, connectionHandle,
      'login');
    var methodInvc3 = createTempMethodInvocation(3, connectionHandle,
      'test');

    for (var i = 0; i < 5; i++) {
      r.increment(methodInvc1);
      r.increment(methodInvc2);
      r.increment(methodInvc3);
    };

    // After for loop runs, we only have 10 runs, so that's under the limit
    test.equal(r.check(methodInvc1).allowed, true);
    // However, this triggers userId rule since this userId is even
    test.equal(r.check(methodInvc2).allowed, false);
    test.equal(r.check(methodInvc2).allowed, false);

    // Running one more test causes it to be false, since we're at 11 now.
    r.increment(methodInvc1);
    test.equal(r.check(methodInvc1).allowed, false);
    // 3rd Method Invocation isn't affected by either rules.
    test.equal(r.check(methodInvc3).allowed, true);

  });

Tinytest.add('rate limit tests - Check one rule affected by two different ' +
  'invocations',
  function (test) {
    r = new RateLimiter();
    var loginMethodRule = {
      userId: null,
      IPAddr: null,
      method: 'login'
    }
    r.addRule(loginMethodRule, 10, 10000);

    var connectionHandle = createTempConnectionHandle(1234, '127.0.0.1');
    var methodInvc1 = createTempMethodInvocation(1, connectionHandle,
      'login');
    var methodInvc2 = createTempMethodInvocation(2, connectionHandle,
      'login');

    for (var i = 0; i < 5; i++) {
      r.increment(methodInvc1);
      r.increment(methodInvc2);
    }
    // This throws us over the limit since both increment the login rule
    // counter
    r.increment(methodInvc1);

    test.equal(r.check(methodInvc1).allowed, false);
    test.equal(r.check(methodInvc2).allowed, false);
  });

Tinytest.add("rate limit tests - add global rule", function (test) {
  r = new RateLimiter();
  var globalRule = {
    userId: null,
    IPAddr: null,
    method: null
  }
  r.addRule(globalRule, 1, 10000);

  var connectionHandle = createTempConnectionHandle(1234, '127.0.0.1');
  var connectionHandle2 = createTempConnectionHandle(1234, '127.0.0.2');

  var methodInvc1 = createTempMethodInvocation(1, connectionHandle,
    'login');
  var methodInvc2 = createTempMethodInvocation(2, connectionHandle2,
    'test');
  var methodInvc3 = createTempMethodInvocation(3, connectionHandle,
    'user-accounts');

  // First invocation, all methods would still be allowed.
  r.increment(methodInvc2);
  test.equal(r.check(methodInvc1).allowed, true);
  test.equal(r.check(methodInvc2).allowed, true);
  test.equal(r.check(methodInvc3).allowed, true);
  // Second invocation, everything has reached common rate limit
  r.increment(methodInvc3);
  test.equal(r.check(methodInvc1).allowed, false);
  test.equal(r.check(methodInvc2).allowed, false);
  test.equal(r.check(methodInvc3).allowed, false);
});

Tinytest.add('rate limit tests - Fuzzy rule match does not trigger rate limit',
  function (test) {
    r = new RateLimiter();
    var rule = {
      a: function (inp) {
        return inp % 3 == 0
      },
      b: 5,
      c: "hi",
    }
    r.addRule(rule, 1, 10000);
    var input = {
      a: 3,
      b: 5
    }
    for (var i = 0; i < 5; i++) {
      r.increment(input);
    }
    test.equal(r.check(input).allowed, true);
    var matchingInput = {
      a: 3,
      b: 5,
      c: "hi",
      d: 1
    }
    r.increment(matchingInput);
    r.increment(matchingInput);
    // Past limit so should be false
    test.equal(r.check(matchingInput).allowed, false);

    // Add secondary rule and check that longer time is returned when multiple
    // rules limits are hit
    var newRule = {
      a: function (inp) {
        return inp % 3 == 0
      },
      b: 5,
      c: "hi",
      d: 1
    }
    r.addRule(newRule, 1, 10);
    // First rule should still throw while second rule will trigger as well,
    // causing us to return longer time to reset to user
    r.increment(matchingInput);
    r.increment(matchingInput);
    test.equal(r.check(matchingInput).timeToReset > 50, true);
  }
);

/****** Test Our Helper Methods *****/

Tinytest.add("rate limit tests - test matchRule method", function (test) {
  r = new RateLimiter();
  var globalRule = {
    userId: null,
    IPAddr: null,
    type: null,
    name: null
  }
  var globalRuleId = r.addRule(globalRule);

  var rateLimiterInput = {
    userId: 1023,
    IPAddr: "127.0.0.1",
    type: 'sub',
    name: 'getSubLists'
  };

  test.equal(r.rules[globalRuleId].match(rateLimiterInput), true);

  var oneNotNullRule = {
    userId: 102,
    IPAddr: null,
    type: null,
    name: null
  }

  var oneNotNullId = r.addRule(oneNotNullRule);
  test.equal(r.rules[oneNotNullId].match(RateLimiterInput), false);

  oneNotNullRule.userId = 1023;
  test.equal(r.rules[oneNotNullId].match(RateLimiterInput), true);

  var notCompleteInput = {
    userId: 102,
    IPAddr: '127.0.0.1'
  };
  test.equal(r.rules[globalRuleId].match(notCompleteInput), true);
  test.equal(r.rules[oneNotNullId].match(notCompleteInput), false);
});

Tinytest.add('rate limit tests - test generateMethodKey string',
  function (test) {
    r = new RateLimiter();
    var globalRule = {
      userId: null,
      IPAddr: null,
      type: null,
      name: null
    }
    var globalRuleId = r.addRule(globalRule);

    var rateLimiterInput = {
      userId: 1023,
      IPAddr: "127.0.0.1",
      type: 'sub',
      name: 'getSubLists'
    };

    test.equal(r.rules[globalRuleId]._generateKeyString(rateLimiterInput), "");
    globalRule.userId = 1023;

    test.equal(r.rules[globalRuleId]._generateKeyString(rateLimiterInput),
      "userId1023");

    var ruleWithFuncs = {
      userId: function (input) {
        return input % 2 === 0
      },
      IPAddr: null,
      type: null
    };
    var funcRuleId = r.addRule(ruleWithFuncs);
    test.equal(r.rules[funcRuleId]._generateKeyString(rateLimiterInput), "");
    rateLimiterInput.userId = 1024;
    test.equal(r.rules[funcRuleId]._generateKeyString(rateLimiterInput),
      "userId1024");

    var multipleRules = ruleWithFuncs;
    multipleRules.IPAddr = '127.0.0.1';
    var multipleRuleId = r.addRule(multipleRules);
    test.equal(r.rules[multipleRuleId]._generateKeyString(rateLimiterInput),
      "userId1024IPAddr127.0.0.1")
  }
);

function createTempConnectionHandle(id, clientIP) {
  return {
    id: id,
    close: function () {
      self.close();
    },
    onClose: function (fn) {
      var cb = Meteor.bindEnvironment(fn, "connection onClose callback");
      if (self.inQueue) {
        self._closeCallbacks.push(cb);
      } else {
        // if we're already closed, call the callback.
        Meteor.defer(cb);
      }
    },
    clientAddress: clientIP,
    httpHeaders: null
  };
}

function createTempMethodInvocation(userId, connectionHandle, methodName) {
  var methodInv = new DDPCommon.MethodInvocation({
    isSimulation: false,
    userId: userId,
    setUserId: null,
    unblock: false,
    connection: connectionHandle,
    randomSeed: 1234
  });
  methodInv.method = methodName;
  return methodInv;
}<commit_msg>Fixing rate limiter tests<commit_after>// These tests were written before rate-limit was factored outside of DDP Rate
// Limiter and thus are structured with DDP method invocations in mind. These
// rules still test abstract rate limit package behavior. The tests currently
// implemented are:
// * Empty rule set on RateLimiter construction
// * Multiple inputs, only 1 that matches rule and reaches rate limit
// * Multiple inputs, 1 hits rate limit, wait for reset, after which inputs
//   allowed
// * 2 rules, 3 inputs where 2/3 match 1 rule and thus hit rate limit. Second
//   input matches another rule and hits rate limit while 3rd rule not rate
//   limited
// * One rule affected by two inputs still throws
// * Global rule triggers on any invocation after reaching limit
// * Fuzzy rule matching triggers rate limit only when input has more keys than
//   rule
// * matchRule tests that have various levels of similarity in input and rule
// * generateKeyString tests for various matches creating appropriate string
//
// XXX These tests should be refactored to use Tinytest.add instead of
// testAsyncMulti as they're all on the server. Any future tests should be
// written that way.
Tinytest.add('rate limit tests - Check empty constructor creation',
  function (test) {
    r = new RateLimiter();
    test.equal(r.rules, {});
});

Tinytest.add('rate limit tests - Check single rule with multiple ' +
  'invocations, only 1 that matches',
  function (test) {
    r = new RateLimiter();
    var userIdOne = 1;
    var restrictJustUserIdOneRule = {
      userId: userIdOne,
      IPAddr: null,
      method: null
    };

    r.addRule(restrictJustUserIdOneRule, 1, 1000);
    var connectionHandle = createTempConnectionHandle(123, '127.0.0.1');
    var methodInvc1 = createTempMethodInvocation(userIdOne, connectionHandle,
      'login');
    var methodInvc2 = createTempMethodInvocation(2, connectionHandle,
      'login');
    for (var i = 0; i < 2; i++) {
      r.increment(methodInvc1);
      r.increment(methodInvc2);
    }
    test.equal(r.check(methodInvc1).allowed, false);
    test.equal(r.check(methodInvc2).allowed, true);
  });

testAsyncMulti("rate limit tests - Run multiple invocations and wait for one" +
  " to reset", [
  function (test, expect) {
    var self = this;
    self.r = new RateLimiter();
    self.userIdOne = 1;
    self.userIdTwo = 2;
    self.restrictJustUserIdOneRule = {
      userId: self.userIdOne,
      IPAddr: null,
      method: null
    };
    self.r.addRule(self.restrictJustUserIdOneRule, 1, 1000);
    self.connectionHandle = createTempConnectionHandle(123, '127.0.0.1')
    self.methodInvc1 = createTempMethodInvocation(self.userIdOne,
      self.connectionHandle, 'login');
    self.methodInvc2 = createTempMethodInvocation(self.userIdTwo,
      self.connectionHandle, 'login');
    for (var i = 0; i < 2; i++) {
      self.r.increment(self.methodInvc1);
      self.r.increment(self.methodInvc2);
    }
    test.equal(self.r.check(self.methodInvc1).allowed, false);
    test.equal(self.r.check(self.methodInvc2).allowed, true);
    Meteor.setTimeout(expect(function () {}), 1000);
  },
  function (test, expect) {
    var self = this;
    for (var i = 0; i < 100; i++) {
      self.r.increment(self.methodInvc2);
    }

    test.equal(self.r.check(self.methodInvc1).allowed, true);
    test.equal(self.r.check(self.methodInvc2).allowed, true);
  }
]);

Tinytest.add('rate limit tests - Check two rules that affect same methodInvc' +
  ' still throw',
  function (test) {
    r = new RateLimiter();
    var loginMethodRule = {
      userId: null,
      IPAddr: null,
      method: 'login'
    };
    var onlyLimitEvenUserIdRule = {
      userId: function (userId) {
        return userId % 2 === 0
      },
      IPAddr: null,
      method: null
    };
    r.addRule(loginMethodRule, 10, 100);
    r.addRule(onlyLimitEvenUserIdRule, 4, 100);

    var connectionHandle = createTempConnectionHandle(1234, '127.0.0.1');
    var methodInvc1 = createTempMethodInvocation(1, connectionHandle,
      'login');
    var methodInvc2 = createTempMethodInvocation(2, connectionHandle,
      'login');
    var methodInvc3 = createTempMethodInvocation(3, connectionHandle,
      'test');

    for (var i = 0; i < 5; i++) {
      r.increment(methodInvc1);
      r.increment(methodInvc2);
      r.increment(methodInvc3);
    };

    // After for loop runs, we only have 10 runs, so that's under the limit
    test.equal(r.check(methodInvc1).allowed, true);
    // However, this triggers userId rule since this userId is even
    test.equal(r.check(methodInvc2).allowed, false);
    test.equal(r.check(methodInvc2).allowed, false);

    // Running one more test causes it to be false, since we're at 11 now.
    r.increment(methodInvc1);
    test.equal(r.check(methodInvc1).allowed, false);
    // 3rd Method Invocation isn't affected by either rules.
    test.equal(r.check(methodInvc3).allowed, true);

  });

Tinytest.add('rate limit tests - Check one rule affected by two different ' +
  'invocations',
  function (test) {
    r = new RateLimiter();
    var loginMethodRule = {
      userId: null,
      IPAddr: null,
      method: 'login'
    }
    r.addRule(loginMethodRule, 10, 10000);

    var connectionHandle = createTempConnectionHandle(1234, '127.0.0.1');
    var methodInvc1 = createTempMethodInvocation(1, connectionHandle,
      'login');
    var methodInvc2 = createTempMethodInvocation(2, connectionHandle,
      'login');

    for (var i = 0; i < 5; i++) {
      r.increment(methodInvc1);
      r.increment(methodInvc2);
    }
    // This throws us over the limit since both increment the login rule
    // counter
    r.increment(methodInvc1);

    test.equal(r.check(methodInvc1).allowed, false);
    test.equal(r.check(methodInvc2).allowed, false);
  });

Tinytest.add("rate limit tests - add global rule", function (test) {
  r = new RateLimiter();
  var globalRule = {
    userId: null,
    IPAddr: null,
    method: null
  }
  r.addRule(globalRule, 1, 10000);

  var connectionHandle = createTempConnectionHandle(1234, '127.0.0.1');
  var connectionHandle2 = createTempConnectionHandle(1234, '127.0.0.2');

  var methodInvc1 = createTempMethodInvocation(1, connectionHandle,
    'login');
  var methodInvc2 = createTempMethodInvocation(2, connectionHandle2,
    'test');
  var methodInvc3 = createTempMethodInvocation(3, connectionHandle,
    'user-accounts');

  // First invocation, all methods would still be allowed.
  r.increment(methodInvc2);
  test.equal(r.check(methodInvc1).allowed, true);
  test.equal(r.check(methodInvc2).allowed, true);
  test.equal(r.check(methodInvc3).allowed, true);
  // Second invocation, everything has reached common rate limit
  r.increment(methodInvc3);
  test.equal(r.check(methodInvc1).allowed, false);
  test.equal(r.check(methodInvc2).allowed, false);
  test.equal(r.check(methodInvc3).allowed, false);
});

Tinytest.add('rate limit tests - Fuzzy rule match does not trigger rate limit',
  function (test) {
    r = new RateLimiter();
    var rule = {
      a: function (inp) {
        return inp % 3 == 0
      },
      b: 5,
      c: "hi",
    }
    r.addRule(rule, 1, 10000);
    var input = {
      a: 3,
      b: 5
    }
    for (var i = 0; i < 5; i++) {
      r.increment(input);
    }
    test.equal(r.check(input).allowed, true);
    var matchingInput = {
      a: 3,
      b: 5,
      c: "hi",
      d: 1
    }
    r.increment(matchingInput);
    r.increment(matchingInput);
    // Past limit so should be false
    test.equal(r.check(matchingInput).allowed, false);

    // Add secondary rule and check that longer time is returned when multiple
    // rules limits are hit
    var newRule = {
      a: function (inp) {
        return inp % 3 == 0
      },
      b: 5,
      c: "hi",
      d: 1
    }
    r.addRule(newRule, 1, 10);
    // First rule should still throw while second rule will trigger as well,
    // causing us to return longer time to reset to user
    r.increment(matchingInput);
    r.increment(matchingInput);
    test.equal(r.check(matchingInput).timeToReset > 50, true);
  }
);

/****** Test Our Helper Methods *****/

Tinytest.add("rate limit tests - test matchRule method", function (test) {
  r = new RateLimiter();
  var globalRule = {
    userId: null,
    IPAddr: null,
    type: null,
    name: null
  }
  var globalRuleId = r.addRule(globalRule);

  var rateLimiterInput = {
    userId: 1023,
    IPAddr: "127.0.0.1",
    type: 'sub',
    name: 'getSubLists'
  };

  test.equal(r.rules[globalRuleId].match(rateLimiterInput), true);

  var oneNotNullRule = {
    userId: 102,
    IPAddr: null,
    type: null,
    name: null
  }

  var oneNotNullId = r.addRule(oneNotNullRule);
  test.equal(r.rules[oneNotNullId].match(rateLimiterInput), false);

  oneNotNullRule.userId = 1023;
  test.equal(r.rules[oneNotNullId].match(rateLimiterInput), true);

  var notCompleteInput = {
    userId: 102,
    IPAddr: '127.0.0.1'
  };
  test.equal(r.rules[globalRuleId].match(notCompleteInput), true);
  test.equal(r.rules[oneNotNullId].match(notCompleteInput), false);
});

Tinytest.add('rate limit tests - test generateMethodKey string',
  function (test) {
    r = new RateLimiter();
    var globalRule = {
      userId: null,
      IPAddr: null,
      type: null,
      name: null
    }
    var globalRuleId = r.addRule(globalRule);

    var rateLimiterInput = {
      userId: 1023,
      IPAddr: "127.0.0.1",
      type: 'sub',
      name: 'getSubLists'
    };

    test.equal(r.rules[globalRuleId]._generateKeyString(rateLimiterInput), "");
    globalRule.userId = 1023;

    test.equal(r.rules[globalRuleId]._generateKeyString(rateLimiterInput),
      "userId1023");

    var ruleWithFuncs = {
      userId: function (input) {
        return input % 2 === 0
      },
      IPAddr: null,
      type: null
    };
    var funcRuleId = r.addRule(ruleWithFuncs);
    test.equal(r.rules[funcRuleId]._generateKeyString(rateLimiterInput), "");
    rateLimiterInput.userId = 1024;
    test.equal(r.rules[funcRuleId]._generateKeyString(rateLimiterInput),
      "userId1024");

    var multipleRules = ruleWithFuncs;
    multipleRules.IPAddr = '127.0.0.1';
    var multipleRuleId = r.addRule(multipleRules);
    test.equal(r.rules[multipleRuleId]._generateKeyString(rateLimiterInput),
      "userId1024IPAddr127.0.0.1")
  }
);

function createTempConnectionHandle(id, clientIP) {
  return {
    id: id,
    close: function () {
      self.close();
    },
    onClose: function (fn) {
      var cb = Meteor.bindEnvironment(fn, "connection onClose callback");
      if (self.inQueue) {
        self._closeCallbacks.push(cb);
      } else {
        // if we're already closed, call the callback.
        Meteor.defer(cb);
      }
    },
    clientAddress: clientIP,
    httpHeaders: null
  };
}

function createTempMethodInvocation(userId, connectionHandle, methodName) {
  var methodInv = new DDPCommon.MethodInvocation({
    isSimulation: false,
    userId: userId,
    setUserId: null,
    unblock: false,
    connection: connectionHandle,
    randomSeed: 1234
  });
  methodInv.method = methodName;
  return methodInv;
}<|endoftext|>