blob: 52c4df86e6fd3915fe075155353230622093fede [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package {
import flash.display.BitmapData;
import flash.display.DisplayObject;
import flash.display.DisplayObjectContainer;
import flash.display.IBitmapDrawable;
import flash.display.Loader;
import flash.events.Event;
import flash.events.IOErrorEvent;
import flash.events.SecurityErrorEvent;
import flash.events.StatusEvent;
import flash.geom.ColorTransform;
import flash.geom.Matrix;
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.net.LocalConnection;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.text.TextField;
import flash.text.TextFormat;
import flash.text.engine.ContentElement;
import flash.text.engine.TextBlock;
import flash.text.engine.TextLine;
import flash.utils.ByteArray;
import flash.utils.getQualifiedClassName;
import mx.core.IChildList;
import mx.core.IRawChildrenContainer;
import mx.core.mx_internal;
use namespace mx_internal;
/**
* Vector of conditionalValue objects.
**/
[DefaultProperty("conditionalValues")]
/**
* The test step that compares a bitmap against a reference bitmap
* MXML attributes:
* target
* url
* timeout (optional);
* maxColorVariance
* numColorVariances
* waitTarget Do Not Use
* waitEvent Do Not Use
* stageText - placeholder, does nothing
*
*
* Do not set waitEvent or waitTarget on this step. They are set internally
* to manage the loading of the reference bitmap. The step prior to this
* step must wait for the system to synch up.
*
* CompareBitmap will parse the url attribute for $testID, replacing with the current testID
*/
public class CompareBitmap extends Assert
{
public static var useRemoteDiffer:Boolean = false;
private static var identityMatrix:String = new Matrix().toString();
private static var identityColorTransform:String = new ColorTransform().toString();
public static var DEFAULT_MAX_COLOR_VARIANCE:int = 0;
public static var DEFAULT_NUM_COLOR_VARIANCES:int = 0;
public static var DEFAULT_MAX_MATRIX_VARIANCE:Number = 0.1;
// This is the default property.
public var conditionalValues:Vector.<ConditionalValue> = null;
/**
* The url of the file to read. If UnitTester.createBitmapReferences = true,
* the url to store the bitmap
*/
public var url:String;
/**
* placeHolder for stageText, does nothing
*/
public var stageText:String;
private var _maxColorVariance:int = 0;
/**
* The maximum color variation allowed in a bitmap compare.
* Some machines render slightly differently and thus we have
* to allow the the compare to not be exact.
*/
public function get maxColorVariance():int
{
if (_maxColorVariance)
return _maxColorVariance;
return DEFAULT_MAX_COLOR_VARIANCE;
}
public function set maxColorVariance(value:int):void
{
_maxColorVariance = value;
}
private var _ignoreMaxColorVariance:Boolean = false;
/**
* Sometimes you have numColorVariance defined and you don't really care by how much the pixel really differ.
* as long as the number of mismatching pixels is &lt;= numColorVariance
* Setting this to true will skip the maxColorVariance check (and take the guess work out of picture). default is false
*/
public function get ignoreMaxColorVariance():Boolean
{
return _ignoreMaxColorVariance;
}
public function set ignoreMaxColorVariance(value:Boolean):void
{
_ignoreMaxColorVariance = value;
}
private var _numColorVariances:int = 0;
/**
* The number of color variation allowed in a bitmap compare.
* Some machines render slightly differently and thus we have
* to allow the the compare to not be exact.
*/
public function get numColorVariances():int
{
if (_numColorVariances)
return _numColorVariances;
return DEFAULT_NUM_COLOR_VARIANCES;
}
public function set numColorVariances(value:int):void
{
_numColorVariances = value;
}
private var _maxMatrixVariance:Number = 0.1;
/**
* The maximum color variation allowed in a bitmap compare.
* Some machines render slightly differently and thus we have
* to allow the the compare to not be exact.
*/
public function get maxMatrixVariance():Number
{
if (_maxMatrixVariance)
return _maxMatrixVariance;
return DEFAULT_MAX_MATRIX_VARIANCE;
}
public function set maxMatrixVariance(value:Number):void
{
_maxMatrixVariance = value;
}
/**
* Suffix to add to the file being written out (the case of a compare failure)
*/
public static var fileSuffix:String = "";
private var reader:Loader;
private var xmlreader:URLLoader;
private var writer:URLLoader;
private static var connection:LocalConnection;
private static var commandconnection:LocalConnection;
private var baselineMissing:Boolean = false;
private var baselineMissingMessage:String = "Baseline image could not be read. Created image file as a .bad.png.";
private var baselineMissingMessageFail:String = "Baseline image could not be read, and we failed to write the .bad.png";
private function statusHandler(event:Event):void
{
}
/**
* Constructor
*/
public function CompareBitmap()
{
if (useRemoteDiffer)
{
if (!connection)
{
connection = new LocalConnection();
connection.allowDomain("*");
connection.addEventListener(StatusEvent.STATUS, statusHandler);
commandconnection = new LocalConnection();
commandconnection.allowDomain("*");
try
{
commandconnection.connect("_ImageDifferCommands");
}
catch (e:Error)
{
trace("connection failed");
}
}
}
}
override public function execute(root:DisplayObject, context:UnitTester, testCase:TestCase, testResult:TestResult):Boolean
{
var cv:ConditionalValue = null;
var configID:String = null;
// Use MultiResult to determine the proper URL.
if(conditionalValues){
cv = new MultiResult().chooseCV(conditionalValues);
if(cv){
// This way, we use CompareBitmap's url (directory) if the CV's is not set.
if( cv.url != null ){
url = cv.url;
}
}
}else{
// We do not have ConditionalValues. If the current config is unknown, it is probably
// a desktop AIR run, and we should just let things take the course they always have.
// If a config is known, then we want to use the new config ID suffix mechanism later.
configID = TargetConfigurations.getTargetConfigID( UnitTester.cv );
if( configID ){
trace( "CompareBitmap: No ConditionalValues found. configID is " + configID.toString() );
}else{
trace( "CompareBitmap: No ConditionalValues found. configID is " + configID );
}
}
if( url == null ){
if( cv == null ){
throw new Error("Found no url on the CompareBitmap for test case " + testCase.testID);
}else{
throw new Error("Found no url on the ConditionalValue for test case " + testCase.testID + ", ConditionalValue: " + cv.toString());
}
}
// See if url ends with .png. If not, create a file name.
if( url.lastIndexOf( ".png" ) != url.length - 4 ){
// Add a path separator if necessary.
if( url.lastIndexOf( "/" ) != url.length - 1 ){
url += "/";
}
// Decide on a file name.
if( conditionalValues ){
// If we ended up with a matching CV, ask it to create a file name.
// Otherwise, go with the test ID.
// Keep this path alive until (if ever) ConditionalValues in CompareBitmaps have all been removed.
if(cv){
trace( "CompareBitmap: Asking the ConditionalValue to create the file name." );
url += cv.createFilename( testCase.testID );
} else {
trace( "CompareBitmap: Creating the file name from the testID." );
url += testCase.testID + ".png";
}
}else if( configID ){
// We have no ConditionalValues and we're running a known config,
// so use the config id in the suffix.
trace( "CompareBitmap: Creating the file name from the configID." );
url += testCase.testID + "@" + configID + ".png";
}else{
trace( "There is no file name, there are no Conditional Values, and there is no configID. There's not much we can do now, is there?" );
}
}
if (url != null && url.indexOf ("$testID") != -1) {
trace ("SAW THE REF, I'll plug it");
url = url.replace ("$testID", UnitTester.currentTestID);
trace ("result 2: " + url);
}
if (url == null)
trace ("URL was null at execute time");
if (commandconnection)
commandconnection.client = this;
var actualTarget:DisplayObject = DisplayObject(context.stringToObject(target));
if (!actualTarget)
{
testResult.doFail("Target " + target + " not found");
return true;
}
if (stageText)
updateStageTexts(actualTarget);
this.root = root;
this.context = context;
this.testResult = testResult;
if (UnitTester.createBitmapReferences)
{
if (UnitTester.checkEmbeddedFonts)
{
if (!checkEmbeddedFonts(actualTarget))
{
testResult.doFail ("Target " + actualTarget + " is using non-embedded or advanced anti-aliased fonts");
return true;
}
}
writeBaselines(actualTarget);
return false;
}
else
{
readPNG();
return false;
}
}
// there are a few mobile tests that use StageText in the bitmaps
// which are TextFields in the emulator. This makes them use
// embedded fonts to get consistency across platforms.
private function updateStageTexts(target:DisplayObject):void
{
var doc:DisplayObjectContainer = target as DisplayObjectContainer;
var tf:Object;
tf = findTextWidget(doc);
if (tf)
{
var n:int = target.stage.numChildren;
for (var i:int = 0; i < n; i++)
{
var stf:TextField = target.stage.getChildAt(i) as TextField;
if (stf)
{
var stfm:TextFormat = new TextFormat(tf.getStyle("fontFamily"),
tf.getStyle("fontSize"),
tf.getStyle("color"),
tf.getStyle("fontWeight") == "bold",
tf.getStyle("fontStyle") == "italic"
);
stf.defaultTextFormat = stfm;
stf.embedFonts = true;
}
}
}
}
private function findTextWidget(doc:DisplayObjectContainer):Object
{
if (!doc) return null;
var n:int = doc.numChildren;
for (var i:int = 0; i < n; i++)
{
var child:DisplayObject = doc.getChildAt(i);
var className:String = getQualifiedClassName(child);
if (className.indexOf("StyleableStageText") > -1)
return child;
else if (child is DisplayObjectContainer)
{
var tf:Object = findTextWidget(child as DisplayObjectContainer);
if (tf) return tf;
}
}
return null;
}
private function getTargetSize(target:DisplayObject):Point
{
var width:Number;
var height:Number;
try
{
width = target["getUnscaledWidth"]() * Math.abs(target.scaleX) * target.root.scaleX;
height = target["getUnscaledHeight"]() * Math.abs(target.scaleY) * target.root.scaleY;
}
catch(e:ReferenceError)
{
width = target.width * target.root.scaleX;
height = target.height * target.root.scaleY;
}
trace("getTargetSize: height: ", target.height);
trace("getTargetSize: root.height: ", target.root.height);
trace("getTargetSize: stageHeight: ", target.stage.stageHeight);
try {
trace("getTargetSize: loaderInfo.height: ", target.loaderInfo.height);
} catch (e:Error) {};
return new Point(width, height);
}
// Given a displayObject, sets up the screenBits.
private function getScreenBits(target:DisplayObject):void{
try
{
var targetSize:Point = getTargetSize(target);
var stagePt:Point = target.localToGlobal(new Point(0, 0));
var altPt:Point = target.localToGlobal(targetSize);
stagePt.x = Math.min(stagePt.x, altPt.x);
stagePt.y = Math.min(stagePt.y, altPt.y);
screenBits = new BitmapData(targetSize.x, targetSize.y);
screenBits.draw(target.stage, new Matrix(1, 0, 0, 1, -stagePt.x, -stagePt.y));
}
catch (se:SecurityError)
{
UnitTester.hideSandboxes();
try
{
screenBits.draw(target.stage, new Matrix(1, 0, 0, 1, -stagePt.x, -stagePt.y));
}
catch (se2:Error)
{
try
{
// if we got a security error and ended up here, assume we're in the
// genericLoader loads us scenario
screenBits.draw(target.root, new Matrix(1, 0, 0, 1, -stagePt.x, -stagePt.y));
}
catch (se3:Error)
{
}
}
UnitTester.showSandboxes();
var sb:Array = UnitTester.getSandboxBitmaps();
var n:int = sb.length;
for (var i:int = 0; i < n; i++)
{
mergeSandboxBitmap(target, stagePt, screenBits, sb[i]);
}
}
catch (e:Error)
{
testResult.doFail (e.getStackTrace());
}
}
private var MAX_LC:int = 12000;
private var screenBits:BitmapData;
private var baselineBits:BitmapData;
private var compareVal:Object;
public function comparePNG(target:DisplayObject):Boolean
{
if (UnitTester.checkEmbeddedFonts)
{
if (!checkEmbeddedFonts(target))
{
testResult.doFail ("Target " + target + " is using non-embedded or advanced anti-aliased fonts");
return true;
}
}
try {
if (!reader.content)
{
testResult.doFail ("baseline image not available");
return true;
}
} catch( e:Error ) {
testResult.doFail ("CompareBitmap BIG FAIL! Content reader is null!");
return true;
}
getScreenBits(target);
try
{
baselineBits = new BitmapData(reader.content.width, reader.content.height);
baselineBits.draw(reader.content, new Matrix());
compareVal = baselineBits.compare (screenBits);
if (compareVal is BitmapData && numColorVariances)
compareVal = compareWithVariances(compareVal as BitmapData)
if (compareVal != 0)
{
trace ("compare returned" + compareVal);
var req:URLRequest = new URLRequest();
if (UnitTester.isApollo)
{
req.url = encodeURI2(CompareBitmap.adjustPath (url));
}
else
{
req.url = url;
var base:String = normalizeURL(context.application.url);
base = base.substring(0, base.lastIndexOf("/"));
while (req.url.indexOf("../") == 0)
{
base = base.substring(0, base.lastIndexOf("/"));
req.url = req.url.substring(3);
}
req.url = encodeURI2(base + "/" + req.url);
}
req.url += ".xml";
xmlreader = new URLLoader();
xmlreader.addEventListener(Event.COMPLETE, readXMLCompleteHandler);
xmlreader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, readErrorHandler);
xmlreader.addEventListener(IOErrorEvent.IO_ERROR, readXMLIOErrorHandler);
xmlreader.load (req);
return false;
}
}
catch (e:Error)
{
testResult.doFail (e.getStackTrace());
}
return true;
}
private function readXMLCompleteHandler(event:Event):void
{
var actualTarget:DisplayObject = DisplayObject(context.stringToObject(target));
var s:String = getDisplayListXML(actualTarget).toXMLString();
var t:String = xmlreader.data;
s = s.replace(/\r/g, "");
t = t.replace(/\r/g, "");
if (s !== t && xmldiffer(s, t))
{
testResult.doFail ("compare returned" + compareVal, absolutePathResult(url) + ".bad.png");
if (useRemoteDiffer)
{
sendImagesToDiffer();
}
else if (fileSuffix != "")
{
writeBaselines (actualTarget);
}
}
else
stepComplete();
}
private function readXMLIOErrorHandler(event:Event):void
{
if (useRemoteDiffer)
{
sendImagesToDiffer();
}
else if (fileSuffix != "")
{
testResult.doFail ("compare returned" + compareVal, absolutePathResult(url) + ".bad.png");
var actualTarget:DisplayObject = DisplayObject(context.stringToObject(target));
writePNG (actualTarget);
}
}
private function mergeSandboxBitmap(target:DisplayObject, pt:Point, bm:BitmapData, obj:Object):void
{
var targetSize:Point = getTargetSize(target);
var sbm:BitmapData = new BitmapData(obj.width, obj.height);
var srcRect:Rectangle = new Rectangle(0, 0, obj.width, obj.height);
sbm.setPixels(srcRect, obj.bits);
var targetRect:Rectangle = new Rectangle(pt.x, pt.y, targetSize.x, targetSize.y);
var sbRect:Rectangle = new Rectangle(obj.x, obj.y, obj.width, obj.height);
var area:Rectangle = targetRect.intersection(sbRect);
if (area)
bm.copyPixels(sbm, srcRect, target.globalToLocal(area.topLeft));
}
private function sendImagesToDiffer():void
{
UnitTester.callback = stringifyScreen;
}
private var ba:ByteArray;
private function stringifyScreen():void
{
ba = screenBits.getPixels(screenBits.rect);
ba.position = 0;
connection.send("_ImageDiffer", "startScreenData", screenBits.width, screenBits.height, ba.length, UnitTester.currentTestID, UnitTester.currentScript);
UnitTester.callback = sendScreen;
}
private function sendScreen():void
{
if (ba.position + MAX_LC < ba.length)
{
connection.send("_ImageDiffer", "addScreenData", stringify(ba));
UnitTester.callback = sendScreen;
}
else
{
connection.send("_ImageDiffer", "addScreenData", stringify(ba));
UnitTester.callback = stringifyBase;
}
}
private function stringifyBase():void
{
ba = baselineBits.getPixels(baselineBits.rect);
ba.position = 0;
connection.send("_ImageDiffer", "startBaseData", baselineBits.width, baselineBits.height, ba.length);
UnitTester.callback = sendBase;
}
private function sendBase():void
{
if (ba.position + MAX_LC < ba.length)
{
connection.send("_ImageDiffer", "addBaseData", stringify(ba));
UnitTester.callback = sendBase;
}
else
{
connection.send("_ImageDiffer", "addBaseData", stringify(ba));
connection.send("_ImageDiffer", "compareBitmaps");
}
}
private function stringify(ba:ByteArray):String
{
var n:int = Math.min(ba.length - ba.position, MAX_LC);
var arr:Array = [];
for (var i:int = 0; i < n; i++)
{
var b:int = ba.readUnsignedByte();
arr.push(b.toString(16))
}
return arr.toString();
}
private function readCompleteHandler(event:Event):void
{
var actualTarget:DisplayObject = DisplayObject(context.stringToObject(target));
if (comparePNG(actualTarget))
preStepComplete();
}
private function readErrorHandler(event:Event):void
{
var actualTarget:DisplayObject = DisplayObject(context.stringToObject(target));
getScreenBits(actualTarget);
baselineMissing = true;
writePNG(actualTarget);
// writePNG() creates error handlers which will handle the fail and stepComplete().
}
public function readPNG():void
{
var req:URLRequest = new URLRequest();
if (UnitTester.isApollo)
{
req.url = encodeURI2(CompareBitmap.adjustPath (url));
}
else
{
req.url = url;
var base:String = normalizeURL(context.application.url);
base = base.substring(0, base.lastIndexOf("/"));
while (req.url.indexOf("../") == 0)
{
base = base.substring(0, base.lastIndexOf("/"));
req.url = req.url.substring(3);
}
req.url = encodeURI2(base + "/" + req.url);
}
// req.url = encodeURI2(url);
// }
reader = new Loader();
trace ("readPNG:requesting url: " + req.url);
reader.contentLoaderInfo.addEventListener(Event.COMPLETE, readCompleteHandler);
reader.contentLoaderInfo.addEventListener(SecurityErrorEvent.SECURITY_ERROR, readErrorHandler);
reader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, readErrorHandler);
reader.load (req);
}
public static var adjustPath:Function = function(url:String):String { return url; };
public function getPngByteArray(target:DisplayObject, bitmapData:BitmapData):ByteArray
{
// add png headers
if (UnitTester.createBitmapReferences)
{
var targetSize:Point = getTargetSize(target);
var stagePt:Point = target.localToGlobal(new Point(0, 0));
var altPt:Point = target.localToGlobal(targetSize);
stagePt.x = Math.min(stagePt.x, altPt.x);
stagePt.y = Math.min(stagePt.y, altPt.y);
bitmapData = new BitmapData(targetSize.x, targetSize.y);
bitmapData.draw(target.stage, new Matrix(1, 0, 0, 1, -stagePt.x, -stagePt.y));
}
var png:MustellaPNGEncoder = new MustellaPNGEncoder();
var ba:ByteArray = png.encode (bitmapData);
return ba;
}
public function writeBaselines(target:DisplayObject, writeDisplayList:Boolean = true):void
{
var req:URLRequest = new URLRequest();
writer = new URLLoader();
req.method = "POST";
/**
* either we got called here to write new baselines
* or to save a .bad.png for investigation
* in addition, with failures, we upload baseline and failure to a server
*/
if (UnitTester.createBitmapReferences)
{
fileSuffix = "";
}
if (writeDisplayList)
{
var s:String = getDisplayListXML(target).toXMLString();
// request data goes on the URL Request
req.data = s;
req.contentType = "text/xml";
if (UnitTester.isApollo)
{
req.url = encodeURI2(UnitTester.bitmapServerPrefix + adjustWriteURI(adjustPath(url))) + fileSuffix + ".xml";
} else
{
req.url = encodeURI2(UnitTester.bitmapServerPrefix + absolutePath(url)) + fileSuffix + ".xml";
}
trace ("writing url: " + req.url);
writer.addEventListener(Event.COMPLETE, writeXMLCompleteHandler);
writer.addEventListener(SecurityErrorEvent.SECURITY_ERROR, writeErrorHandler);
writer.addEventListener(IOErrorEvent.IO_ERROR, writeErrorHandler);
writer.load (req);
}
}
private function writeXMLCompleteHandler(event:Event):void
{
var actualTarget:DisplayObject = DisplayObject(context.stringToObject(target));
writePNG(actualTarget);
}
private function writePNG(target:DisplayObject):void
{
var req:URLRequest = new URLRequest();
writer = new URLLoader();
req.method = "POST";
var ba:ByteArray = getPngByteArray(target, screenBits);
trace ("image size: " + ba.length);
// request data goes on the URL Request
req.data = ba;
// can't send this, don't need to anyway var rhArray:Array = new Array(new URLRequestHeader("Content-Length", new String(ba.length) ));
req.contentType = "image/png";
if (UnitTester.isApollo)
{
req.url = encodeURI2(UnitTester.bitmapServerPrefix + adjustWriteURI(adjustPath(url))) + fileSuffix;
} else
{
req.url = encodeURI2(UnitTester.bitmapServerPrefix + absolutePath(url)) + fileSuffix;
}
trace ("writing url: " + req.url);
writer.addEventListener(Event.COMPLETE, writeCompleteHandler);
writer.addEventListener(SecurityErrorEvent.SECURITY_ERROR, writeErrorHandler);
writer.addEventListener(IOErrorEvent.IO_ERROR, writeErrorHandler);
writer.load (req);
/// If this is about creating bitmaps, skip the upload, we're done
if (UnitTester.createBitmapReferences || UnitTester.run_id == "-1" || baselineMissing)
return;
//// Upload
var writer2:URLLoader = new URLLoader();
var reqScreen:URLRequest = new URLRequest();
reqScreen.method = "POST";
/// we already have the screen data in hand:
reqScreen.data = ba;
/// fill in the blanks
reqScreen.contentType = "image/png";
reqScreen.url = UnitTester.urlAssemble ("screen",
context.testDir, context.scriptName, this.testResult.testID, UnitTester.run_id);
trace ("upload: " + reqScreen.url);
writer2.addEventListener(Event.COMPLETE, uploadCompleteHandler);
writer2.addEventListener(SecurityErrorEvent.SECURITY_ERROR, uploadErrorHandler);
writer2.addEventListener(IOErrorEvent.IO_ERROR, uploadErrorHandler);
writer2.load (reqScreen);
/// get the baseline stuff:
var writer3:URLLoader = new URLLoader();
var reqBaseline:URLRequest = new URLRequest();
/// needed?
var baBase:ByteArray = getPngByteArray(target, baselineBits);
reqBaseline.data = baBase;
reqBaseline.contentType = "image/png";
reqBaseline.method = "POST";
reqBaseline.url = UnitTester.urlAssemble ("baseline",
context.testDir, context.scriptName, this.testResult.testID, UnitTester.run_id);
trace ("upload: " + reqBaseline.url);
writer3.addEventListener(Event.COMPLETE, upload2CompleteHandler);
writer3.addEventListener(SecurityErrorEvent.SECURITY_ERROR, upload2ErrorHandler);
writer3.addEventListener(IOErrorEvent.IO_ERROR, upload2ErrorHandler);
writer3.load (reqBaseline);
}
private function adjustWriteURI(url:String):String
{
var pos:int = url.indexOf("file:///");
if (pos != 0)
{
return url;
}
url = url.substring(8);
pos = url.indexOf("|");
if (pos != 1)
{
return url;
}
var drive:String = url.substring(0, 1);
drive = drive.toLowerCase();
return drive + ":" + url.substring(2);
}
private var screenDone:Boolean = false;
private var baselineDone:Boolean = false;
private function writeCompleteHandler(event:Event):void
{
trace("baseline write successful " + event);
if( baselineMissing ){
baselineMissing = false;
testResult.doFail( baselineMissingMessage );
}
stepComplete();
}
private function uploadCompleteHandler(event:Event):void
{
trace("screen image upload successful " + event);
screenDone = true;
checkForStepComplete();
}
private function upload2CompleteHandler(event:Event):void
{
trace("baseline image upload successful " + event);
baselineDone = true;
checkForStepComplete();
}
private function writeErrorHandler(event:Event):void
{
if( baselineMissing ){
baselineMissing = false;
testResult.doFail( baselineMissingMessageFail );
stepComplete();
}else{
testResult.doFail ("error on baseline write: " + event);
trace("Image baseline write failed " + event);
if (UnitTester.createBitmapReferences)
stepComplete();
}
}
private function uploadErrorHandler(event:Event):void
{
testResult.doFail ("error on baseline write: " + event);
trace("Image screen upload failed " + event);
screenDone = true;
checkForStepComplete();
}
private function upload2ErrorHandler(event:Event):void
{
testResult.doFail ("error on baseline write: " + event);
trace("Image baseline upload failed " + event);
baselineDone = true;
checkForStepComplete();
}
private function checkForStepComplete():void
{
if (baselineDone && screenDone)
preStepComplete();
}
/**
* customize string representation
*/
override public function toString():String
{
var s:String = (UnitTester.createBitmapReferences) ? "CreateBitmap: " : "CompareBitmap";
if (target)
s += ": target = " + target;
if (url)
s += ", url = " + url;
return s;
}
private function absolutePathResult(url:String):String
{
var base:String = null;
if (UnitTester.isApollo)
{
base = adjustWriteURI(adjustPath (url));
} else
{
base = context.application.url;
}
base = normalizeURL(base);
base = base.substring (base.indexOf ("mustella/tests")+14);
if (!UnitTester.isApollo)
{
base = base.substring(0, base.lastIndexOf("/"));
var tmp:String = url;
while (tmp.indexOf("../") == 0)
{
base = base.substring(0, base.lastIndexOf("/"));
tmp = tmp.substring(3);
}
return base +"/" + tmp;
} else
{
return base;
}
}
private function absolutePathHttp(url:String):String
{
if (url.indexOf ("..") == 0)
return url.substring (3);
else
return url;
}
private function absolutePath(url:String):String
{
var swf:String = normalizeURL(root.loaderInfo.url);
var pos:int = swf.indexOf("file:///");
if (pos != 0)
{
var posH:int = swf.indexOf("http://");
if (posH == 0)
{
return absolutePathHttp (url);
} else
{
trace("WARNING: unexpected swf url format, no file:/// at offset 0");
return url;
}
}
swf = swf.substring(8);
pos = swf.indexOf("|");
if (pos != 1)
{
trace("WARNING: unexpected swf url format, no | at offset 1 in: " + swf);
// assume we're on a mac or other unix box, it will do no harm
return "/" + swf.substring(0, swf.lastIndexOf ("/")+1) + url;
}
var drive:String = swf.substring(0, 1);
drive = drive.toLowerCase();
return drive + ":" + swf.substring(2, swf.lastIndexOf("/") + 1) + url;
}
public static function normalizeURL(url:String):String
{
var results:Array = url.split("/[[DYNAMIC]]/");
return results[0];
}
public function keepGoing():void
{
trace("keepgoing", url, hasEventListener("stepComplete"));
preStepComplete();
}
private function encodeURI2(s:String):String
{
var pos:int = s.lastIndexOf("/");
if (pos != -1)
{
var fragment:String = s.substring(pos + 1);
s = s.substring(0, pos + 1);
fragment= encodeURIComponent(fragment);
s = s + fragment;
}
return s;
}
private function compareWithVariances(bm:BitmapData):Object
{
var totalAllowed:int = numColorVariances * UnitTester.pixelToleranceMultiplier;
var allowed:int = totalAllowed;
var n:int = bm.height;
var m:int = bm.width;
for (var i:int = 0; i < n; i++)
{
for (var j:int = 0; j < m; j++)
{
var pix:int = bm.getPixel(j, i);
if (pix)
{
if(!ignoreMaxColorVariance)
{
var red:int = pix >> 16 & 0xff;
var green:int = pix >> 8 & 0xff;
var blue:int = pix & 0xff;
if (red & 0x80)
red = 256 - red;
if (blue & 0x80)
blue = 256 - blue;
if (green & 0x80)
green = 256 - green;
if (red > maxColorVariance ||
blue > maxColorVariance ||
green > maxColorVariance)
{
var max:int = Math.max(Math.max(red, blue), green);
trace("CompareBitmap: exceeded maxColorVariance=" + maxColorVariance + " max(red,green,blue)=" + max);
return bm;
}
}
allowed--;
if (allowed < 0)
{
trace("CompareBitmap: exceeded numColorVariances=" + numColorVariances);
return bm;
}
}
}
}
trace("CompareBitmap: numColorVariances seen=" + String(totalAllowed - allowed));
return 0;
}
private function checkEmbeddedFonts(target:Object):Boolean
{
if ("rawChildren" in target)
target = target.rawChildren;
if (target is TextField)
{
if (target.embedFonts == false)
return false;
if (target.antiAliasType == "advanced")
return false;
return true;
}
else if ("numChildren" in target)
{
var n:int = target.numChildren;
for (var i:int = 0; i < n; i++)
{
if (!checkEmbeddedFonts(target.getChildAt(i)))
return false;
}
}
return true;
}
protected function preStepComplete():void
{
if (baselineBits != null)
baselineBits.dispose();
if (screenBits != null)
screenBits.dispose();
reader=null;
writer=null;
stepComplete();
}
/* this was sometimes getting called before the image was loaded into
'reader' which made it null and then the readCompleteHandler failed.
override protected function stepComplete():void
{
if (baselineBits != null)
baselineBits.dispose();
if (screenBits != null)
screenBits.dispose();
reader=null;
writer=null;
super.stepComplete();
}*/
/****** DisplayList Comparision ******/
protected function getDisplayListProperties(d:DisplayObject, noMask:Boolean = false):XML
{
var xml:XML;
var n:int;
var i:int;
var childXML:XML;
var s:String = getQualifiedClassName(d);
s = s.replace("::", ".");
xml = new XML("<" + s + "/>");
s = d.transform.concatenatedColorTransform.toString();
if (s != identityColorTransform)
xml.@concatenatedColorTransform = s;
if (d.transform.matrix)
{
s = d.transform.matrix.toString();
if (s != identityMatrix)
{
if (s.indexOf("(a=1, b=0, c=0, d=1, ") == -1)
xml.@matrix = s;
}
}
else
{
s = d.transform.matrix3D.rawData.toString();
xml.@matrix3D = s;
}
if (d.x != 0)
xml.@x = d.x;
if (d.y != 0)
xml.@y = d.y;
xml.@width = d.width;
xml.@height = d.height;
if (xml.visible == false)
xml.@visible = "false";
if (d.mask && d.mask != d.parent && !noMask)
{
xml.mask = <mask/>;
childXML = getDisplayListProperties(d.mask, true);
xml.mask.appendChild = childXML;
}
if (d.scrollRect)
{
s = d.scrollRect.toString();
xml.@scrollRect = s;
}
if (d.blendMode && d.blendMode != "normal")
xml.@blendMode = d.blendMode;
if (d.cacheAsBitmap)
xml.@cacheAsBitmap = "true";
try {
if (d.filters && d.filters.length > 0)
{
s = d.filters.toString();
xml.@filters = s;
}
} catch (e:Error)
{
// seems to throw arg error when Shader applied
}
if (d.opaqueBackground)
xml.@opaqueBackground = "true";
if (d.scale9Grid)
{
s = d.scale9Grid.toString();
xml.@scale9Grid = s;
}
if (d is TextField)
{
xml.@underline = TextField(d).defaultTextFormat.underline;
xml.htmlText = TextField(d).htmlText;
}
if (d is Loader && Loader(d).contentLoaderInfo.contentType.indexOf("image") != -1)
{
s = Loader(d).contentLoaderInfo.url;
s = s.substring(s.lastIndexOf("/") + 1);
xml.@loaderbitmap = s;
}
if (d is TextLine)
{
var tl:TextLine = TextLine(d);
xml.@ascent = tl.ascent;
xml.@descent = tl.descent;
xml.@atomCount = tl.atomCount;
xml.@hasGraphicElement = tl.hasGraphicElement;
if (tl.textBlock)
{
var tb:TextBlock = TextLine(d).textBlock;
var ce:ContentElement = tb.content;
s = ce.rawText.substr(tl.textBlockBeginIndex, tl.rawTextLength);
xml.@text = s;
}
}
if (d is IRawChildrenContainer)
{
var rawChildren:IChildList = IRawChildrenContainer(d).rawChildren;
n = rawChildren.numChildren;
for (i = 0; i < n; i++)
{
childXML = getDisplayListProperties(rawChildren.getChildAt(i));
xml.appendChild(childXML);
}
}
else if (d is DisplayObjectContainer)
{
var doc:DisplayObjectContainer = d as DisplayObjectContainer;
n = doc.numChildren;
for (i = 0; i < n; i++)
{
var child:DisplayObject = doc.getChildAt(i);
if (child) // was null in an FCK test.
{
childXML = getDisplayListProperties(child);
xml.appendChild(childXML);
}
else
xml.appendChild(<NullChild />);
}
}
return xml;
}
// scan entire display list, but only dump objects intersecting target
protected function getDisplayListXML(target:DisplayObject):XML
{
var i:int;
var n:int;
var child:DisplayObject;
var childXML:XML;
var doc:DisplayObjectContainer = DisplayObjectContainer(target.root);
var xml:XML = <DisplayList />;
if (doc is IRawChildrenContainer)
{
var rawChildren:IChildList = IRawChildrenContainer(doc).rawChildren;
n = rawChildren.numChildren;
for (i = 0; i < n; i++)
{
child = rawChildren.getChildAt(i);
if (target.hitTestObject(child))
{
childXML = getDisplayListProperties(child);
xml.appendChild(childXML);
}
}
}
else
{
n = doc.numChildren;
for (i = 0; i < n; i++)
{
child = doc.getChildAt(i);
if (target.hitTestObject(child))
{
childXML = getDisplayListProperties(child);
xml.appendChild(childXML);
}
}
}
return xml;
}
private function differ(s:String, t:String):Boolean
{
var retval:Boolean = false;
var sl:Array = s.split("\n");
var tl:Array = t.split("\n");
trace(sl.length, tl.length);
var n:int = Math.max(sl.length, tl.length);
for (var i:int = 0; i < n; i++)
{
var a:String = (i < sl.length) ? sl[i] : "";
var b:String = (i < tl.length) ? tl[i] : "";
if (a != b)
{
a = trimTag(a);
b = trimTag(b);
if (a != b && !nullChildOrStaticText(a, b))
{
retval = true;
var c:String = "";
var d:String = "";
trace(i, "cur: ", a);
trace(i, "xml: ", b);
var m:int = Math.max(a.length, b.length);
for (var j:int = 0; j < m; j++)
{
c += a.charCodeAt(j) + " ";
d += b.charCodeAt(j) + " ";
}
trace(i, "cur: ", c);
trace(i, "xml: ", d);
}
}
}
return retval;
}
// attempt to strip off random unique name chars for embedded assets
private function trimTag(a:String):String
{
var c:int;
var d:int;
d = a.indexOf("<");
if (d != -1)
{
c = a.indexOf(" ", d);
if (c == -1 && a.length > d + 2 && a.charAt(d + 1) == '/')
c = a.indexOf(">"); // closing tag
if (c != -1)
{
var rest:String = a.substring(c);
for (var i:int = c - 1;i > 0; i--)
{
var ch:String = a.charAt(i);
if ((ch >= '0' && ch <= '9') || ch == '_')
{
// assume it is a random char
}
else
break;
}
return a.substring(0, i + 1) + rest;
}
}
return a;
}
// attempt to strip off random unique name chars for embedded assets
private function trimName(a:String):String
{
var c:int;
var d:int;
c = a.length;
for (var i:int = c - 1;i >= 0; i--)
{
var ch:String = a.charAt(i);
if ((ch >= '0' && ch <= '9') || ch == '_')
{
// assume it is a random char
}
else
break;
}
return a.substring(0, i + 1);
}
// static text seems to float around a bit so ignore it.
private function nullChildOrStaticText(a:String, b:String):Boolean
{
if (a.indexOf("<NullChild") != -1)
return true;
if (b.indexOf("<NullChild") != -1)
return true;
if (a.indexOf("<flash.text.StaticText") != -1)
return true;
if (b.indexOf("<flash.text.StaticText") != -1)
return true;
return false;
}
private function xmldiffer(s:String, t:String):Boolean
{
var retval:Boolean = false;
var xmls:XML = XML(s);
var xmlt:XML = XML(t);
retval = compareNodes(xmls, xmlt);
if (retval)
differ(s, t);
return retval;
}
private static var xywidthheight:Object = { x: 1, y: 1, width: 1, height: 1};
private function compareNodes(s:XML, t:XML):Boolean
{
var retval:Boolean = false;
var q:String;
var i:int;
var j:int;
var n:int;
var m:int;
var st:String;
var tt:String;
var sv:String;
var tv:String;
if (s.toXMLString() == t.toXMLString())
return false;
// compare tag names
var sn:String = s.name().toString();
var tn:String = t.name().toString();
var sparts:Array = sn.split(".");
var tparts:Array = tn.split(".");
n = sparts.length;
for (i = 0; i < n; i++)
sparts[i] = trimName(sparts[i]);
n = tparts.length;
for (i = 0; i < n; i++)
tparts[i] = trimName(tparts[i]);
sn = sparts.join(".");
tn = tparts.join(".");
if (tn != sn)
{
if (sn == "NullChild" || sn == "flash.display.StaticText" ||
tn == "NullChild" || tn == "flash.display.StaticText")
{
// inconsistent behavior around StaticText
return false;
}
else if (!oneMoreNameCompare(sn, tn))
{
trace("tag name mismatch: cur=", sn, "xml=", tn);
retval = true;
}
}
var sa:XMLList = s.attributes();
var ta:XMLList = t.attributes();
n = sa.length();
m = ta.length();
if (n != m)
{
trace(sn, "different number of attributes: cur=", n, "xml=", m);
retval = true;
}
else
{
for (i = 0; i < n; i++)
{
st = sa[i].name().toString();
tt = ta[i].name().toString();
if (st != tt)
{
trace(sn, "attribute name mismatch: cur=", st, "xml=", tt);
retval = true;
}
else
{
sv = s['@' + st].toString();
tv = t['@' + tt].toString();
if (sv != tv)
{
if (xywidthheight[st] == 1)
{
var sf:Number = Number(sv);
var tf:Number = Number(tv);
if (Math.abs(tf - sf) > maxMatrixVariance)
{
trace(sn + '@' + st, "attribute value mismatch: cur=", sv, "xml=", tv);
retval = true;
}
}
else if (st == "matrix")
{
// strip parens
sv = sv.substring(1, sv.length - 2);
tv = tv.substring(1, tv.length - 2);
sparts = sv.split(",");
tparts = tv.split(",");
m = sparts.length;
for (j = 0; j < m; j++)
{
sv = sparts[j];
tv = tparts[j];
sv = sv.split("=")[1];
tv = tv.split("=")[1];
sf = Number(sv);
tf = Number(tv);
if (Math.abs(tf - sf) > maxMatrixVariance)
{
trace(sn + '@' + st, "matrix value mismatch: cur=", sv, "xml=", tv);
retval = true;
}
}
}
else
{
trace(sn + '@' + st, "attribute value mismatch: cur=", sv, "xml=", tv);
retval = true;
}
}
}
}
}
var sl:XMLList = s.children();
var tl:XMLList = t.children();
n = sl.length();
m = tl.length();
if (n != m)
{
trace(sn, "different number of children: cur=", n, "xml=", m);
retval = true;
}
else
{
for (i = 0; i < n; i++)
{
var nk:String = sl[i].nodeKind();
if (nk == "text")
{
if (sl[i].text() != tl[i].text())
{
trace(sn, "different number of text nodes: cur=", sl[i].text(), "xml=", tl[i].text());
retval = true;
}
}
else if (nk == "element")
{
if (compareNodes(sl[i], tl[i]))
retval = true;
}
}
}
return retval;
}
private function oneMoreNameCompare(a:String, b:String):Boolean
{
var aParts:Array = a.split("_");
var bParts:Array = b.split("_");
var i:int;
var n:int;
n = aParts.length;
for (i = 0; i < n; i++)
aParts[i] = trimName(aParts[i]);
n = bParts.length;
for (i = 0; i < n; i++)
bParts[i] = trimName(bParts[i]);
a = aParts.join("_");
b = bParts.join("_");
return a == b;
}
}
}