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"
+ }
+}