From 143b67e47dafd7cd1a042ec5ed6b43dd30b665b4 Mon Sep 17 00:00:00 2001 From: humbletim Date: Wed, 1 Mar 2017 09:20:53 -0500 Subject: [PATCH] Add require/module unit tests --- .../developer/libraries/jasmine/hifi-boot.js | 11 +- .../tests/unit_tests/moduleTests/cycles/a.js | 9 + .../tests/unit_tests/moduleTests/cycles/b.js | 9 + .../unit_tests/moduleTests/cycles/main.js | 13 + .../entity/entityConstructorAPIException.js | 12 + .../entity/entityConstructorModule.js | 21 ++ .../entity/entityConstructorNested.js | 13 + .../entity/entityConstructorNested2.js | 24 ++ .../entityConstructorRequireException.js | 9 + .../entity/entityPreloadAPIError.js | 12 + .../entity/entityPreloadRequire.js | 10 + .../tests/unit_tests/moduleTests/example.json | 9 + .../moduleTests/exceptions/exception.js | 3 + .../exceptions/exceptionInFunction.js | 37 ++ .../tests/unit_tests/moduleUnitTests.js | 317 ++++++++++++++++++ .../developer/tests/unit_tests/package.json | 6 + 16 files changed, 513 insertions(+), 2 deletions(-) create mode 100644 scripts/developer/tests/unit_tests/moduleTests/cycles/a.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/cycles/b.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/cycles/main.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/example.json create mode 100644 scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js create mode 100644 scripts/developer/tests/unit_tests/moduleUnitTests.js create mode 100644 scripts/developer/tests/unit_tests/package.json diff --git a/scripts/developer/libraries/jasmine/hifi-boot.js b/scripts/developer/libraries/jasmine/hifi-boot.js index f490a3618f..8757550ae8 100644 --- a/scripts/developer/libraries/jasmine/hifi-boot.js +++ b/scripts/developer/libraries/jasmine/hifi-boot.js @@ -6,7 +6,7 @@ var lastSpecStartTime; function ConsoleReporter(options) { var startTime = new Date().getTime(); - var errorCount = 0; + var errorCount = 0, pending = []; this.jasmineStarted = function (obj) { print('Jasmine started with ' + obj.totalSpecsDefined + ' tests.'); }; @@ -15,11 +15,14 @@ var endTime = new Date().getTime(); print('
'); if (errorCount === 0) { - print ('All tests passed!'); + print ('All enabled tests passed!'); } else { print('Tests completed with ' + errorCount + ' ' + ERROR + '.'); } + if (pending.length) + print ('disabled:
   '+ + pending.join('
   ')+'
'); print('Tests completed in ' + (endTime - startTime) + 'ms.'); }; this.suiteStarted = function(obj) { @@ -32,6 +35,10 @@ lastSpecStartTime = new Date().getTime(); }; this.specDone = function(obj) { + if (obj.status === 'pending') { + pending.push(obj.fullName); + return print('...(pending ' + obj.fullName +')'); + } var specEndTime = new Date().getTime(); var symbol = obj.status === PASSED ? '' + CHECKMARK + '' : diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js new file mode 100644 index 0000000000..7934180da7 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js @@ -0,0 +1,9 @@ +var a = exports; +a.done = false; +var b = require('./b.js'); +a.done = true; +a.name = 'a'; +a['a.done?'] = a.done; +a['b.done?'] = b.done; + +print('from a.js a.done =', a.done, '/ b.done =', b.done); diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js new file mode 100644 index 0000000000..285f176597 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js @@ -0,0 +1,9 @@ +var b = exports; +b.done = false; +var a = require('./a.js'); +b.done = true; +b.name = 'b'; +b['a.done?'] = a.done; +b['b.done?'] = b.done; + +print('from b.js a.done =', a.done, '/ b.done =', b.done); diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js new file mode 100644 index 0000000000..2e9a878c82 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js @@ -0,0 +1,13 @@ +print('main.js'); +var a = require('./a.js'), + b = require('./b.js'); + +print('from main.js a.done =', a.done, 'and b.done =', b.done); + +module.exports = { + name: 'main', + a: a, + b: b, + 'a.done?': a.done, + 'b.done?': b.done, +}; diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js new file mode 100644 index 0000000000..b1bc0e33e4 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js @@ -0,0 +1,12 @@ +// test module method exception being thrown within main constructor +(function() { + var apiMethod = Script.require('../exceptions/exceptionInFunction.js'); + print(Script.resolvePath(''), "apiMethod", apiMethod); + // this next line throws from within apiMethod + print(apiMethod()); + return { + preload: function(uuid) { + print("entityConstructorAPIException::preload -- never seen --", uuid, Script.resolvePath('')); + }, + }; +}) diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js new file mode 100644 index 0000000000..5f0e8a5938 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js @@ -0,0 +1,21 @@ +// test dual-purpose module and standalone Entity script +function MyEntity(filename) { + return { + preload: function(uuid) { + print("entityConstructorModule.js::preload"); + if (typeof module === 'object') { + print("module.filename", module.filename); + print("module.parent.filename", module.parent && module.parent.filename); + } + }, + clickDownOnEntity: function(uuid, evt) { + print("entityConstructorModule.js::clickDownOnEntity"); + }, + }; +} + +try { + module.exports = MyEntity; +} catch(e) {} +print('entityConstructorModule::MyEntity', typeof MyEntity); +(MyEntity) diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js new file mode 100644 index 0000000000..5a2b8d5974 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js @@ -0,0 +1,13 @@ +// test Entity constructor based on inherited constructor from a module +function constructor() { + print("entityConstructorNested::constructor"); + var MyEntity = Script.require('./entityConstructorModule.js'); + return new MyEntity("-- created from entityConstructorNested --"); +} + +try { + module.exports = constructor; +} catch(e) { + constructor; +} + diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js new file mode 100644 index 0000000000..85a6b977b0 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js @@ -0,0 +1,24 @@ +// test Entity constructor based on nested, inherited module constructors +function constructor() { + print("entityConstructorNested2::constructor"); + + // inherit from entityConstructorNested + var Entity = Script.require('./entityConstructorNested.js'); + function SubEntity() {} + SubEntity.prototype = new MyEntity('-- created from entityConstructorNested2 --'); + + // create new instance + var entity = new SubEntity(); + // "override" clickDownOnEntity for just this new instance + entity.clickDownOnEntity = function(uuid, evt) { + print("entityConstructorNested2::clickDownOnEntity"); + SubEntity.prototype.clickDownOnEntity.apply(this, arguments); + }; + return entity; +} + +try { + module.exports = constructor; +} catch(e) { + constructor; +} diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js new file mode 100644 index 0000000000..269ca8e7f0 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js @@ -0,0 +1,9 @@ +// test module-related exception from within "require" evaluation itself +(function() { + var mod = Script.require('../exceptions/exception.js'); + return { + preload: function(uuid) { + print("entityConstructorRequireException::preload (never happens)", uuid, Script.resolvePath('')); + }, + }; +}) diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js new file mode 100644 index 0000000000..3be0b50d43 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js @@ -0,0 +1,12 @@ +// test module method exception being thrown within preload +(function() { + var apiMethod = Script.require('../exceptions/exceptionInFunction.js'); + print(Script.resolvePath(''), "apiMethod", apiMethod); + return { + preload: function(uuid) { + // this next line throws from within apiMethod + print(apiMethod()); + print("entityPreloadAPIException::preload -- never seen --", uuid, Script.resolvePath('')); + }, + }; +}) diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js new file mode 100644 index 0000000000..fc70838c80 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js @@ -0,0 +1,10 @@ +// test requiring a module from within preload +(function constructor() { + return { + preload: function(uuid) { + print("entityPreloadRequire::preload"); + var example = Script.require('../example.json'); + print("entityPreloadRequire::example::name", example.name); + }, + }; +}) diff --git a/scripts/developer/tests/unit_tests/moduleTests/example.json b/scripts/developer/tests/unit_tests/moduleTests/example.json new file mode 100644 index 0000000000..42d7fe07da --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/example.json @@ -0,0 +1,9 @@ +{ + "name": "Example JSON Module", + "last-modified": 1485789862, + "config": { + "title": "My Title", + "width": 800, + "height": 600 + } +} diff --git a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js new file mode 100644 index 0000000000..636ee82f79 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js @@ -0,0 +1,3 @@ +module.exports = "n/a"; +throw new Error('exception on line 2'); + diff --git a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js new file mode 100644 index 0000000000..dc2ce3c438 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js @@ -0,0 +1,37 @@ +// dummy lines to make sure exception line number is well below parent test script +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + + +function myfunc() { + throw new Error('exception on line 32 in myfunc'); + return "myfunc"; +} +module.exports = myfunc; +if (Script[module.filename] === 'throw') + myfunc(); diff --git a/scripts/developer/tests/unit_tests/moduleUnitTests.js b/scripts/developer/tests/unit_tests/moduleUnitTests.js new file mode 100644 index 0000000000..c1c20d6980 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleUnitTests.js @@ -0,0 +1,317 @@ +/* eslint-env jasmine */ + +var isNode = instrument_testrunner(); + +var NETWORK_describe = xdescribe, + INTERFACE_describe = !isNode ? describe : xdescribe, + NODE_describe = isNode ? describe : xdescribe; + +print("DESCRIBING"); +describe('require', function() { + describe('resolve', function() { + it('should resolve relative filenames', function() { + var expected = Script.resolvePath('./moduleTests/example.json'); + expect(require.resolve('./moduleTests/example.json')).toEqual(expected); + }); + }); + + describe('JSON', function() { + it('should import .json modules', function() { + var example = require('./moduleTests/example.json'); + expect(example.name).toEqual('Example JSON Module'); + }); + INTERFACE_describe('inteface', function() { + NETWORK_describe('network', function() { + //xit('should import #content-type=application/json modules', function() { + // var results = require('https://jsonip.com#content-type=application/json'); + // expect(results.ip).toMatch(/^[.0-9]+$/); + //}); + it('should import content-type: application/json modules', function() { + var scope = { 'content-type': 'application/json' }; + var results = require.call(scope, 'https://jsonip.com'); + expect(results.ip).toMatch(/^[.0-9]+$/); + }); + }); + }); + + }); + + INTERFACE_describe('system', function() { + it('require(id)', function() { + expect(require('vec3')).toEqual(jasmine.any(Function)); + }); + it('require(id).function', function() { + expect(require('vec3')().isValid).toEqual(jasmine.any(Function)); + }); + }); + + describe('exceptions', function() { + it('should reject blank "" module identifiers', function() { + expect(function() { + require.resolve(''); + }).toThrowError(/Cannot find/); + }); + it('should reject excessive identifier sizes', function() { + expect(function() { + require.resolve(new Array(8193).toString()); + }).toThrowError(/Cannot find/); + }); + it('should reject implicitly-relative filenames', function() { + expect(function() { + var mod = require.resolve('example.js'); + }).toThrowError(/Cannot find/); + }); + it('should reject non-existent filenames', function() { + expect(function() { + var mod = require.resolve('./404error.js'); + }).toThrowError(/Cannot find/); + }); + it('should reject identifiers resolving to a directory', function() { + expect(function() { + var mod = require.resolve('.'); + //console.warn('resolved(.)', mod); + }).toThrowError(/Cannot find/); + expect(function() { + var mod = require.resolve('..'); + //console.warn('resolved(..)', mod); + }).toThrowError(/Cannot find/); + expect(function() { + var mod = require.resolve('../'); + //console.warn('resolved(../)', mod); + }).toThrowError(/Cannot find/); + }); + if (typeof MODE !== 'undefined' && MODE !== 'node') { + it('should reject non-system, extensionless identifiers', function() { + expect(function() { + require.resolve('./example'); + }).toThrowError(/Cannot find/); + }); + } + }); + + describe('cache', function() { + it('should cache modules by resolved module id', function() { + var value = new Date; + var example = require('./moduleTests/example.json'); + example['.test'] = value; + var example2 = require('../../tests/unit_tests/moduleTests/example.json'); + expect(example2).toBe(example); + expect(example2['.test']).toBe(example['.test']); + }); + it('should reload cached modules set to null', function() { + var value = new Date; + var example = require('./moduleTests/example.json'); + example['.test'] = value; + require.cache[require.resolve('../../tests/unit_tests/moduleTests/example.json')] = null; + var example2 = require('../../tests/unit_tests/moduleTests/example.json'); + expect(example2).not.toBe(example); + expect(example2['.test']).not.toBe(example['.test']); + }); + it('should reload when module property is deleted', function() { + var value = new Date; + var example = require('./moduleTests/example.json'); + example['.test'] = value; + delete require.cache[require.resolve('../../tests/unit_tests/moduleTests/example.json')]; + var example2 = require('../../tests/unit_tests/moduleTests/example.json'); + expect(example2).not.toBe(example); + expect(example2['.test']).not.toBe(example['.test']); + }); + }); + + describe('cyclic dependencies', function() { + describe('should allow lazy-ref cyclic module resolution', function() { + const MODULE_PATH = './moduleTests/cycles/main.js'; + var main; + beforeEach(function() { + try { this._print = print; } catch(e) {} + // for this test print is no-op'd so it doesn't disrupt the reporter output + //console = typeof console === 'object' ? console : { log: function() {} }; + print = function() {}; + Script.resetModuleCache(); + }); + afterEach(function() { + print = this._print; + }); + it('main requirable', function() { + main = require(MODULE_PATH); + expect(main).toEqual(jasmine.any(Object)); + }); + it('main with both a and b', function() { + expect(main.a['b.done?']).toBe(true); + expect(main.b['a.done?']).toBe(false); + }); + it('a.done?', function() { + expect(main['a.done?']).toBe(true); + }); + it('b.done?', function() { + expect(main['b.done?']).toBe(true); + }); + }); + }); + + describe('JS', function() { + it('should throw catchable local file errors', function() { + expect(function() { + require('file:///dev/null/non-existent-file.js'); + }).toThrowError(/path not found|Cannot find.*non-existent-file/); + }); + it('should throw catchable invalid id errors', function() { + expect(function() { + require(new Array(4096 * 2).toString()); + }).toThrowError(/invalid.*size|Cannot find.*,{30}/); + }); + it('should throw catchable unresolved id errors', function() { + expect(function() { + require('foobar:/baz.js'); + }).toThrowError(/could not resolve|Cannot find.*foobar:/); + }); + + NETWORK_describe('network', function() { + // note: with retries these tests can take up to 60 seconds each to timeout + var timeout = 75 * 1000; + it('should throw catchable host errors', function() { + expect(function() { + var mod = require('http://non.existent.highfidelity.io/moduleUnitTest.js'); + print("mod", Object.keys(mod)); + }).toThrowError(/error retrieving script .ServerUnavailable.|Cannot find.*non.existent/); + }, timeout); + it('should throw catchable network timeouts', function() { + expect(function() { + require('http://ping.highfidelity.io:1024'); + }).toThrowError(/error retrieving script .Timeout.|Cannot find.*ping.highfidelity/); + }, timeout); + }); + }); + + INTERFACE_describe('entity', function() { + var sampleScripts = [ + 'entityConstructorAPIException.js', + 'entityConstructorModule.js', + 'entityConstructorNested2.js', + 'entityConstructorNested.js', + 'entityConstructorRequireException.js', + 'entityPreloadAPIError.js', + 'entityPreloadRequire.js', + ].filter(Boolean).map(function(id) { return Script.require.resolve('./moduleTests/entity/'+id); }); + + var uuids = []; + + for(var i=0; i < sampleScripts.length; i++) { + (function(i) { + var script = sampleScripts[ i % sampleScripts.length ]; + var shortname = '['+i+'] ' + script.split('/').pop(); + var position = MyAvatar.position; + position.y -= i/2; + it(shortname, function(done) { + var uuid = Entities.addEntity({ + text: shortname, + description: Script.resolvePath('').split('/').pop(), + type: 'Text', + position: position, + rotation: MyAvatar.orientation, + script: script, + scriptTimestamp: +new Date, + lifetime: 20, + lineHeight: 1/8, + dimensions: { x: 2, y: .5, z: .01 }, + backgroundColor: { red: 0, green: 0, blue: 0 }, + color: { red: 0xff, green: 0xff, blue: 0xff }, + }, !Entities.serversExist() || !Entities.canRezTmp()); + uuids.push(uuid); + var ii = Script.setInterval(function() { + Entities.queryPropertyMetadata(uuid, "script", function(err, result) { + if (err) { + throw new Error(err); + } + if (result.success) { + clearInterval(ii); + if (/Exception/.test(script)) + expect(result.status).toMatch(/^error_(loading|running)_script$/); + else + expect(result.status).toEqual("running"); + done(); + } else { + print('!result.success', JSON.stringify(result)); + } + }); + }, 100); + Script.setTimeout(function() { + Script.clearInterval(ii); + }, 4900); + }, 5000 /* timeout */); + })(i); + } + Script.scriptEnding.connect(function() { + uuids.forEach(function(uuid) { Entities.deleteEntity(uuid); }); + }); + }); +}); + +function run() {} +function instrument_testrunner() { + var isNode = typeof process === 'object' && process.title === 'node'; + if (isNode) { + // for consistency this still uses the same local jasmine.js library + var jasmineRequire = require('../../libraries/jasmine/jasmine.js'); + var jasmine = jasmineRequire.core(jasmineRequire); + var env = jasmine.getEnv(); + var jasmineInterface = jasmineRequire.interface(jasmine, env); + for (var p in jasmineInterface) + global[p] = jasmineInterface[p]; + env.addReporter(new (require('jasmine-console-reporter'))); + // testing mocks + Script = { + resetModuleCache: function() { + module.require.cache = {}; + }, + setTimeout: setTimeout, + clearTimeout: clearTimeout, + resolvePath: function(id) { + // this attempts to accurately emulate how Script.resolvePath works + var trace = {}; Error.captureStackTrace(trace); + var base = trace.stack.split('\n')[2].replace(/^.*[(]|[)].*$/g,'').replace(/:[0-9]+:[0-9]+.*$/,''); + if (!id) + return base; + var rel = base.replace(/[^\/]+$/, id); + console.info('rel', rel); + return require.resolve(rel); + }, + require: function(mod) { + return require(Script.require.resolve(mod)); + } + }; + Script.require.cache = require.cache; + Script.require.resolve = function(mod) { + if (mod === '.' || /^\.\.($|\/)/.test(mod)) + throw new Error("Cannot find module '"+mod+"' (is dir)"); + var path = require.resolve(mod); + //console.info('node-require-reoslved', mod, path); + try { + if (require('fs').lstatSync(path).isDirectory()) { + throw new Error("Cannot find module '"+path+"' (is directory)"); + } + //console.info('!path', path); + } catch(e) { console.info(e) } + return path; + }; + print = console.info.bind(console, '[print]'); + } else { + global = this; + // Interface Test mode + Script.require('../../../system/libraries/utils.js'); + this.jasmineRequire = Script.require('../../libraries/jasmine/jasmine.js'); + Script.require('../../libraries/jasmine/hifi-boot.js') + require = Script.require; + // polyfill console + console = { + log: print, + info: print.bind(this, '[info]'), + warn: print.bind(this, '[warn]'), + error: print.bind(this, '[error]'), + debug: print.bind(this, '[debug]'), + }; + } + run = function() { global.jasmine.getEnv().execute(); }; + return isNode; +} +run(); diff --git a/scripts/developer/tests/unit_tests/package.json b/scripts/developer/tests/unit_tests/package.json new file mode 100644 index 0000000000..91d719b687 --- /dev/null +++ b/scripts/developer/tests/unit_tests/package.json @@ -0,0 +1,6 @@ +{ + "name": "unit_tests", + "devDependencies": { + "jasmine-console-reporter": "^1.2.7" + } +}