Tristan VALCKE 7 роки тому
батько
коміт
bb6a1aaafa
2 змінених файлів з 541 додано та 0 видалено
  1. 210 0
      test/unit/SmartComparer.js
  2. 331 0
      test/unit/qunit-utils.js

+ 210 - 0
test/unit/SmartComparer.js

@@ -0,0 +1,210 @@
+// Smart comparison of three.js objects.
+// Identifies significant differences between two objects.
+// Performs deep comparison.
+// Comparison stops after the first difference is found.
+// Provides an explanation for the failure.
+function SmartComparer() {
+	'use strict';
+
+	// Diagnostic message, when comparison fails.
+	var message;
+
+	return {
+
+		areEqual: areEqual,
+
+		getDiagnostic: function() {
+
+			return message;
+
+		}
+
+	};
+
+
+	// val1 - first value to compare (typically the actual value)
+	// val2 - other value to compare (typically the expected value)
+	function areEqual( val1, val2 ) {
+
+		// Values are strictly equal.
+		if ( val1 === val2 ) return true;
+
+		// Null or undefined values.
+		/* jshint eqnull:true */
+		if ( val1 == null || val2 == null ) {
+
+			if ( val1 != val2 ) {
+
+				return makeFail( 'One value is undefined or null', val1, val2 );
+
+			}
+
+			// Both null / undefined.
+			return true;
+		}
+
+		// Don't compare functions.
+		if ( isFunction( val1 ) && isFunction( val2 ) ) return true;
+
+		// Array comparison.
+		var arrCmp = compareArrays( val1, val2 );
+		if ( arrCmp !== undefined ) return arrCmp;
+
+		// Has custom equality comparer.
+		if ( val1.equals ) {
+
+			if ( val1.equals( val2 ) ) return true;
+
+			return makeFail( 'Comparison with .equals method returned false' );
+
+		}
+
+		// Object comparison.
+		var objCmp = compareObjects( val1, val2 );
+		if ( objCmp !== undefined ) return objCmp;
+
+		// if (JSON.stringify( val1 ) == JSON.stringify( val2 ) ) return true;
+
+		// Object differs (unknown reason).
+		return makeFail( 'Values differ', val1, val2 );
+	}
+
+	function isFunction(value) {
+
+		// The use of `Object#toString` avoids issues with the `typeof` operator
+		// in Safari 8 which returns 'object' for typed array constructors, and
+		// PhantomJS 1.9 which returns 'function' for `NodeList` instances.
+		var tag = isObject(value) ? Object.prototype.toString.call(value) : '';
+
+		return tag == '[object Function]' || tag == '[object GeneratorFunction]';
+
+  }
+
+	function isObject(value) {
+
+		// Avoid a V8 JIT bug in Chrome 19-20.
+		// See https://code.google.com/p/v8/issues/detail?id=2291 for more details.
+		var type = typeof value;
+
+		return !!value && (type == 'object' || type == 'function');
+
+  }
+
+	function compareArrays( val1, val2 ) {
+
+		var isArr1 = Array.isArray( val1 );
+		var isArr2 = Array.isArray( val2 );
+
+		// Compare type.
+		if ( isArr1 !== isArr2 ) return makeFail( 'Values are not both arrays' );
+
+		// Not arrays. Continue.
+		if ( !isArr1 ) return undefined;
+
+		// Compare length.
+		var N1 = val1.length;
+		var N2 = val2.length;
+		if ( N1 !== val2.length ) return makeFail( 'Array length differs', N1, N2 );
+
+		// Compare content at each index.
+		for ( var i = 0; i < N1; i ++ ) {
+
+			var cmp = areEqual( val1[ i ], val2[ i ] );
+			if ( !cmp )	return addContext( 'array index "' + i + '"' );
+
+		}
+
+		// Arrays are equal.
+		return true;
+	}
+
+
+	function compareObjects( val1, val2 ) {
+
+		var isObj1 = isObject( val1 );
+		var isObj2 = isObject( val2 );
+
+		// Compare type.
+		if ( isObj1 !== isObj2 ) return makeFail( 'Values are not both objects' );
+
+		// Not objects. Continue.
+		if ( !isObj1 ) return undefined;
+
+		// Compare keys.
+		var keys1 = Object.keys( val1 );
+		var keys2 = Object.keys( val2 );
+
+		for ( var i = 0, l = keys1.length; i < l; i++ ) {
+
+			if (keys2.indexOf(keys1[ i ]) < 0) {
+
+				return makeFail( 'Property "' + keys1[ i ] + '" is unexpected.' );
+
+			}
+
+		}
+
+		for ( var i = 0, l = keys2.length; i < l; i++ ) {
+
+			if (keys1.indexOf(keys2[ i ]) < 0) {
+
+				return makeFail( 'Property "' + keys2[ i ] + '" is missing.' );
+
+			}
+
+		}
+
+		// Keys are the same. For each key, compare content until a difference is found.
+		var hadDifference = false;
+
+		for ( var i = 0, l = keys1.length; i < l; i++ ) {
+
+			var key = keys1[ i ];
+
+			if (key === "uuid" || key === "id") {
+
+				continue;
+
+			}
+
+			var prop1 = val1[ key ];
+			var prop2 = val2[ key ];
+
+			// Compare property content.
+			var eq = areEqual( prop1, prop2 );
+
+			// In case of failure, an message should already be set.
+			// Add context to low level message.
+			if ( !eq ) {
+
+				addContext( 'property "' + key + '"' );
+				hadDifference = true;
+
+			}
+		}
+
+		return ! hadDifference;
+
+	}
+
+
+	function makeFail( msg, val1, val2 ) {
+
+		message = msg;
+		if ( arguments.length > 1) message += " (" + val1 + " vs " + val2 + ")";
+
+		return false;
+
+	}
+
+	function addContext( msg ) {
+
+		// There should already be a validation message. Add more context to it.
+		message = message || "Error";
+		message += ", at " + msg;
+
+		return false;
+
+	}
+
+}

+ 331 - 0
test/unit/qunit-utils.js

@@ -0,0 +1,331 @@
+//
+// Custom QUnit assertions.
+//
+
+QUnit.assert.success = function ( message ) {
+
+	// Equivalent to assert( true, message );
+	this.pushResult( {
+		result: true,
+		actual: undefined,
+		expected: undefined,
+		message: message
+	} );
+
+};
+
+QUnit.assert.fail = function ( message ) {
+
+	// Equivalent to assert( false, message );
+	this.pushResult( {
+		result: false,
+		actual: undefined,
+		expected: undefined,
+		message: message
+	} );
+
+};
+
+QUnit.assert.numEqual = function ( actual, expected, message ) {
+
+	var diff = Math.abs( actual - expected );
+	message = message || ( actual + " should be equal to " + expected );
+	this.pushResult( {
+		result: diff < 0.1,
+		actual: actual,
+		expected: expected,
+		message: message
+	} );
+
+};
+
+QUnit.assert.equalKey = function ( obj, ref, key ) {
+
+	var actual = obj[ key ];
+	var expected = ref[ key ];
+	var message = actual + ' should be equal to ' + expected + ' for key "' + key + '"';
+	this.pushResult( {
+		result: actual == expected,
+		actual: actual,
+		expected: expected,
+		message: message
+	} );
+
+};
+
+QUnit.assert.smartEqual = function ( actual, expected, message ) {
+
+	var cmp = new SmartComparer();
+
+	var same = cmp.areEqual( actual, expected );
+	var msg = cmp.getDiagnostic() || message;
+
+	this.pushResult( {
+		result: same,
+		actual: actual,
+		expected: expected,
+		message: msg
+	} );
+
+};
+
+//
+//	GEOMETRY TEST HELPERS
+//
+
+function checkGeometryClone( geom ) {
+
+	// Clone
+	var copy = geom.clone();
+	QUnit.assert.notEqual( copy.uuid, geom.uuid, "clone uuid should differ from original" );
+	QUnit.assert.notEqual( copy.id, geom.id, "clone id should differ from original" );
+
+	var excludedProperties = [ 'parameters', 'widthSegments', 'heightSegments', 'depthSegments' ];
+
+	var differingProp = getDifferingProp( geom, copy, excludedProperties );
+	QUnit.assert.ok( differingProp === undefined, 'properties are equal' );
+
+	differingProp = getDifferingProp( copy, geom, excludedProperties );
+	QUnit.assert.ok( differingProp === undefined, 'properties are equal' );
+
+	// json round trip with clone
+	checkGeometryJsonRoundtrip( copy );
+
+}
+
+function getDifferingProp( geometryA, geometryB, excludedProperties ) {
+
+	excludedProperties = excludedProperties || [];
+
+	var geometryKeys = Object.keys( geometryA );
+	var cloneKeys = Object.keys( geometryB );
+
+	var differingProp = undefined;
+
+	for ( var i = 0, l = geometryKeys.length; i < l; i ++ ) {
+
+		var key = geometryKeys[ i ];
+
+		if ( excludedProperties.indexOf( key ) >= 0 ) continue;
+
+		if ( cloneKeys.indexOf( key ) < 0 ) {
+
+			differingProp = key;
+			break;
+
+		}
+
+	}
+
+	return differingProp;
+
+}
+
+// Compare json file with its source geometry.
+function checkGeometryJsonWriting( geom, json ) {
+
+	QUnit.assert.equal( json.metadata.version, "4.5", "check metadata version" );
+	QUnit.assert.equalKey( geom, json, 'type' );
+	QUnit.assert.equalKey( geom, json, 'uuid' );
+	QUnit.assert.equal( json.id, undefined, "should not persist id" );
+
+	var params = geom.parameters;
+	if ( ! params ) {
+
+		return;
+
+	}
+
+	// All parameters from geometry should be persisted.
+	var keys = Object.keys( params );
+	for ( var i = 0, l = keys.length; i < l; i ++ ) {
+
+		QUnit.assert.equalKey( params, json, keys[ i ] );
+
+	}
+
+	// All parameters from json should be transfered to the geometry.
+	// json is flat. Ignore first level json properties that are not parameters.
+	var notParameters = [ "metadata", "uuid", "type" ];
+	var keys = Object.keys( json );
+	for ( var i = 0, l = keys.length; i < l; i ++ ) {
+
+		var key = keys[ i ];
+		if ( notParameters.indexOf( key ) === - 1 ) QUnit.assert.equalKey( params, json, key );
+
+	}
+
+}
+
+// Check parsing and reconstruction of json geometry
+function checkGeometryJsonReading( json, geom ) {
+
+	var wrap = [ json ];
+
+	var loader = new THREE.ObjectLoader();
+	var output = loader.parseGeometries( wrap );
+
+	QUnit.assert.ok( output[ geom.uuid ], 'geometry matching source uuid not in output' );
+	// QUnit.assert.smartEqual( output[ geom.uuid ], geom, 'Reconstruct geometry from ObjectLoader' );
+
+	var differing = getDifferingProp( output[ geom.uuid ], geom, [ 'bones' ] );
+	if ( differing ) console.log( differing );
+
+	var excludedProperties = [ 'bones' ];
+
+	var differingProp = getDifferingProp( output[ geom.uuid ], geom, excludedProperties );
+	QUnit.assert.ok( differingProp === undefined, 'properties are equal' );
+
+	differingProp = getDifferingProp( geom, output[ geom.uuid ], excludedProperties );
+	QUnit.assert.ok( differingProp === undefined, 'properties are equal' );
+
+}
+
+// Verify geom -> json -> geom
+function checkGeometryJsonRoundtrip( geom ) {
+
+	var json = geom.toJSON();
+	checkGeometryJsonWriting( geom, json );
+	checkGeometryJsonReading( json, geom );
+
+}
+
+// Look for undefined and NaN values in numerical fieds.
+function checkFinite( geom ) {
+
+	var allVerticesAreFinite = true;
+
+	var vertices = geom.vertices || [];
+
+	for ( var i = 0, l = vertices.length; i < l; i ++ ) {
+
+		var v = geom.vertices[ i ];
+
+		if ( ! ( isFinite( v.x ) || isFinite( v.y ) || isFinite( v.z ) ) ) {
+
+			allVerticesAreFinite = false;
+			break;
+
+		}
+
+	}
+
+	// TODO Buffers, normal, etc.
+
+	QUnit.assert.ok( allVerticesAreFinite, "contains only finite coordinates" );
+
+}
+
+// Run common geometry tests.
+function runStdGeometryTests( assert, geometries ) {
+
+	for ( var i = 0, l = geometries.length; i < l; i ++ ) {
+
+		var geom = geometries[ i ];
+
+		checkFinite( geom );
+
+		// Clone
+		checkGeometryClone( geom );
+
+		// json round trip
+		checkGeometryJsonRoundtrip( geom );
+
+	}
+
+}
+
+//
+//	LIGHT TEST HELPERS
+//
+
+// Run common light tests.
+function runStdLightTests( lights ) {
+
+	for ( var i = 0, l = lights.length; i < l; i ++ ) {
+
+		var light = lights[ i ];
+
+		// copy and clone
+		checkLightCopyClone( light );
+
+		// THREE.Light doesn't get parsed by ObjectLoader as it's only
+		// used as an abstract base class - so we skip the JSON tests
+		if ( light.type !== "Light" ) {
+
+			// json round trip
+			checkLightJsonRoundtrip( light );
+
+		}
+
+	}
+
+}
+
+function checkLightCopyClone( light ) {
+
+	// copy
+	var newLight = new light.constructor( 0xc0ffee );
+	newLight.copy( light );
+
+	QUnit.assert.notEqual( newLight.uuid, light.uuid, "Copied light's UUID differs from original" );
+	QUnit.assert.notEqual( newLight.id, light.id, "Copied light's id differs from original" );
+	QUnit.assert.smartEqual( newLight, light, "Copied light is equal to original" );
+
+	// real copy?
+	newLight.color.setHex( 0xc0ffee );
+	QUnit.assert.notStrictEqual(
+		newLight.color.getHex(), light.color.getHex(), "Copied light is independent from original"
+	);
+
+	// Clone
+	var clone = light.clone(); // better get a new var
+	QUnit.assert.notEqual( clone.uuid, light.uuid, "Cloned light's UUID differs from original" );
+	QUnit.assert.notEqual( clone.id, light.id, "Clone light's id differs from original" );
+	QUnit.assert.smartEqual( clone, light, "Clone light is equal to original" );
+
+	// real clone?
+	clone.color.setHex( 0xc0ffee );
+	QUnit.assert.notStrictEqual(
+		clone.color.getHex(), light.color.getHex(), "Clone light is independent from original"
+	);
+
+	if ( light.type !== "Light" ) {
+
+		// json round trip with clone
+		checkLightJsonRoundtrip( clone );
+
+	}
+
+}
+
+// Compare json file with its source Light.
+function checkLightJsonWriting( light, json ) {
+
+	QUnit.assert.equal( json.metadata.version, "4.5", "check metadata version" );
+
+	var object = json.object;
+	QUnit.assert.equalKey( light, object, 'type' );
+	QUnit.assert.equalKey( light, object, 'uuid' );
+	QUnit.assert.equal( object.id, undefined, "should not persist id" );
+
+}
+
+// Check parsing and reconstruction of json Light
+function checkLightJsonReading( json, light ) {
+
+	var loader = new THREE.ObjectLoader();
+	var outputLight = loader.parse( json );
+
+	QUnit.assert.smartEqual( outputLight, light, 'Reconstruct Light from ObjectLoader' );
+
+}
+
+// Verify light -> json -> light
+function checkLightJsonRoundtrip( light ) {
+
+	var json = light.toJSON();
+	checkLightJsonWriting( light, json );
+	checkLightJsonReading( json, light );
+
+}