05
Aug 11

Debugging Unit Tests without Flash Builder Premium

If you have a Flash Builder Standard license, chances are good that you’re lamenting the lack of FlexUnit support. You can run your tests from the command line but then you lose debugging and an integrated results display. You could add a second Application to your project to embed a test runner but due to an annoying bug in Flash Builder, this application file needs to be in your main source folder or else it can’t be added as a run configuration. I set out to find a new solution with the following in mind:

  • I want all test-related code to stay in the test sources folder. (src/test/flex since I’m using Maven)
  • I want a visual test runner to display which tests/failed passed, give timing information, stack traces, etc.
  • The tests must work as-is when committed and run through flexmojos on a CI machine (e.g. Jenkins)
  • Debugging support through Flash Builder must be available when running the tests locally.
I managed to find a solution that satisfied all my criteria and wasn’t too messy. I’m sure there’s still room for improvement so please comment if you have any suggestions.

Main Project Structure

Package Explorer view of DemoProject
First, make sure that your test cases are in a separate source tree. Create a test suite that references the test cases you want to run:
package com.instantdelay.flex.demo.test
{
   [Suite]
   [RunWith("org.flexunit.runners.Suite")]
   public class Suite
   {
      public var test1:TestWidget;

      public function Suite() {}
   }
}
Create a new MXML component that inherits from Sprite. I named mine DemoProjectTestModule. Declare a variable in this component of type Suite to force the compiler to include your test classes:
<mx:Sprite xmlns:fx="http://ns.adobe.com/mxml/2009"
           xmlns:mx="library://ns.adobe.com/flex/mx">
   <fx:Script>
      <![CDATA[
         public var s:Suite;
      ]]>
   </fx:Script>
</mx:Sprite>

Now, edit your project properties and create a module from this component. Set it to “Do not optimize”. Flash builder will now create a SWF containing all your tests and any application classes they reference.

The Test Project

Create a new Flex project to hold the test running code. I made mine an AIR app so I could run it in a window and named it TestDemoProject.
  1. In your test project properties, add a source folder pointing to the output directory of your main project which contains the test module SWF. (This will be a subdirectory of bin-debug.)
  2. In the main MXML file for your test project, add the following:
<ns:TestRunnerBase id="runner" />

and

import mx.events.FlexEvent;
import org.flexunit.runner.FlexUnitCore;

private static const TEST_MODULE:String = "DemoProjectTestModule.swf";
private static const TEST_SUITE:String = "com.instantdelay.flex.demo.test.Suite";

private function creationCompleteHandler(event:FlexEvent):void {
   var loader:Loader = new Loader();
   loader.contentLoaderInfo.addEventListener(Event.COMPLETE,
      loaderCompleteHandler);
   loader.load(new URLRequest(TEST_MODULE),
      new LoaderContext(false, ApplicationDomain.currentDomain));
}

private function loaderCompleteHandler(e:Event):void {
   var suite:Class = Class(getDefinitionByName(TEST_SUITE));
   runTestSuite(suite);
}

private function runTestSuite(suite:Class):void {
   var core:FlexUnitCore = new FlexUnitCore();
   core.addListener(runner);
   core.run(suite);
}

This code is where the majority of the work happens. A Loader loads the SWF containing the tests and all your main application classes into the current domain (your test project.) When it finishes, we retrieve the test suite and run it.

And now you’re done! This test project only needs to be built once. When you change your main project the module containing the test cases will build and the test project loads that during runtime. Run the test project in debug mode and debugging support will be enabled.

You also get this nice results view:


11
Aug 10

Actionscript: Determining if a Class object is a superclass or superinterface of another Class



Anyone who uses actionscript reflection in non-trivial cases will eventually run into this problem: The is operator only works on instances. For example:

var obj:Object = new Panel();
trace(obj is Panel);         //true
trace(obj is UIComponent);   //true
trace(obj is Object);        //true
trace(obj is IInvalidating); //true
trace(obj is CheckBox);      //false

var cls:Class = Panel;
trace(cls is Panel);         //false
trace(cls is UIComponent);   //false
trace(cls is Object);        //true
trace(cls is IInvalidating); //false
trace(cls is CheckBox);      //false

This behavior is expected, but not very useful. Since cls is actually an instance of the Class class, we are actually checking whether the Class class inherits from each of those 5 things. Since Class only extends Object, that’s the only one that’s true.

What happens if you only have a Class object and you need to check if it’s the subclass of another Class?

public function set myItemRenderer(value:Class):void {
   // We know the renderer needs to extend MyBaseRenderer
   // How do we check that the user provided a valid class?

   if (value is MyBaseRenderer) {
      // doesn't work
   }
}

Something equivalent to isAssignableFrom in Java would be excellent. Unfortunately, there is no simple solution built-in. There are, however, three solutions that I have come up with:

1. Make an instance

public function classIsClass(a:Class, b:Class):Boolean {
   return (new a()) is b;
}

This is the most direct method but it has some limitations:

  • Class a must provide a no-argument constructor.
  • You must deal with the overhead of instantiating a new object.
  • You can’t check if an interface extends another interface. Since you can’t instantiate an interface, a must be a regular class.
  • Class a must be constructable. Some classes like DisplayObject and Class do not allow instantiation.

2. Use getQualifiedSuperclassName()

private function classIsClass(clsA:Class, clsB:Class):Boolean {
   if (clsA == clsB) return true;

   do {
      var superName:String = getQualifiedSuperclassName(clsA);
      clsA = getDefinitionByName(superName) as Class;
      if (clsA == clsB) {
         return true;
      }
   }
   while (clsA != Object);

   return false;
}

This method can’t handle interfaces at all. Since getQualifiedSuperclassName only walks the class inheritance chain, it will never touch the interfaces of clsA. If clsB is an interface, this method will always return false. If clsA is an interface, this method will throw an error since getQualifiedSuperclassName returns null for interfaces.

3. Use describeType()

public function classIsClass(a:Class, b:Class):Boolean {
   if (a == b) return true;

   var aXML:XML = describeType(a);
   var bXML:XML = describeType(b);

   return aXML.factory.extendsClass.
          (@type == bXML.@name[0]).length() == 1 ||
        aXML.factory.implementsInterface.
           (@type == bXML.@name[0]).length() == 1;
}

This method eliminates the problems of the first 2 method, but at the cost of performance. describeType is an expensive method. It returns an XML document describing every aspect of a given object. If you are only running this check a few times, this cost is negligible, but for many calls it can add up quickly. describeType is also stupid; 2 successive requests to describe the same object will cause all the XML to be generated twice. If you know that you will be checking the same types many times, I strongly urge you to consider caching the description XML.

Performance Comparison

I set up a test with 25 random classes from the Flex API and used each method to compare every class to every other class (including themselves). I used the Flash Builder profiler to record time in each method.

Method # of test runs Cumulative time Average cumulative time
Create an instance 1625 381 ms 0.23 ms
Use getQualifiedSuperclassName 1625 181 ms 0.11 ms
Use describeType 325 11391 ms

(11027 ms of this spent running describeType)

35.05 ms
Use describeType with XML cached after first request 325 717 ms 2.2 ms


06
Aug 10

JBoss Maven Seam Integration dependency issue in eclipse

Today I was trying to install the JBoss Maven Seam Integration plug-in for eclipse but I kept getting this message about a missing dependency:

Cannot complete the install because one or more required items could not be found.
Software being installed: JBoss Maven Seam Integration 1.1.0.v20100720-0040-H162-M1 (org.jboss.tools.maven.seam.feature.feature.group 1.1.0.v20100720-0040-H162-M1)
Missing requirement: JBoss Maven Seam Integration 1.1.0.v20100720-0040-H162-M1 (org.jboss.tools.maven.seam.feature.feature.group 1.1.0.v20100720-0040-H162-M1) requires 'org.maven.ide.eclipse.wtp.feature.feature.group 0.10.0' but it could not be found

It turns out that the Maven/Seam plugin is dependent on Maven Integration for WTP. m2eclipse keeps this plugin on a separate update site for extra plugins which I hadn’t added. Eclipse only searches update sites added in the preferences when resolving dependencies.

Just add http://m2eclipse.sonatype.org/sites/m2e-extras to your update sites to fix this error.


04
Aug 10

Flex: Customizing data tip location and behavior in a LineChart

The Flex LineChart control has a feature to display pop-up “data tips” when the user hovers near a data point in the chart. These are basically just tooltips with information about the x and y values at the given point. Unfortunately, Flex offers little in the way of customizing when and how these data tips display. For example, the following is a LineChart using the horizontal-step style:

LineChart with circled data points

In a horizontal step chart, a line is drawn horizontally to the right of each data point, and vertically at each change in value. The circles around each data point indicate the area where hovering the mouse would produce a data tip. No other areas on the chart produce a data tip and this can be slightly confusing. The user might wonder why they can’t hover at other bends in the line to see the current value. Other users might want to simply hover anywhere along the line.

Solution

In order to gain control over how the LineChart decides where to show data tips, we will need to create a subclass of LineChart and override the findDataPoints function. This function is responsible for returning a list of data points “near” a given coordinate. We will change this to get the behavior we want.

public override function findDataPoints(x:Number, y:Number):Array {
   var result:Array = []; //array of HitData

   // Ignore mouse points outside of the data region
   // of the chart. (e.g. on the axis labels)
   if (!dataRegion.contains(x, y)) {
      return result;
   }

   // The x,y coordinates given to findDataPoints are
   // relative to the LineChart. Make them local to
   // the component which holds the data series
   var mouseLoc:Point = new Point(x - _seriesHolder.x,
      y - _seriesHolder.y);

   // Loop through each LineSeries displayed on the chart
   // This can also work with AreaSeries, etc. without too
   // much modification
   for each (var line:LineSeries in this.series) {

      if (!line.visible) {
         continue;
      }

      // Get the value on the chart at the given mouse point
      var valueAtMouse:Array = line.localToData(mouseLoc);

      // Loop through the data points for this line and find
      // the closest data point to the left of the mouse.
      // NOTE: This is specific to a "step" chart. For a regular
      // "segment" chart, you'll need to find the 2 points on either
      // side of the mouse and interpolate the value.
      var last:LineSeriesItem = null;
      for each (var item:LineSeriesItem in line.items) {
         if (item.xValue < valueAtMouse[0]) {
            last = item;
         }
         else {
            break;
         }
      }

      // If we found a data point and the line is within 50
      // pixels of the mouse vertically, we'll show a data tip
      // If you want to show data tips for ALL points at a specific
      // x-value, just remove the vertical proximity check
      if (last && Math.abs(last.y - mouseLoc.y) < 50) {

         // We need to clone the data point that we found and modify
         // its xValue to match that of our mouse location. This way
         // the popup data tip that displays will show the xValue at
         // the mouse instead of the nearest data point
         var hitPoint:LineSeriesItem = LineSeriesItem(last.clone());
         hitPoint.x = last.x;
         hitPoint.y = last.y;
         hitPoint.xValue = Math.floor(valueAtMouse[0]);
         hitPoint.yValue = last.yValue;

         // Compute the distance between the mouse and the data point.
         var dist:Number = Math.sqrt(Math.pow(mouseLoc.x - last.x, 2) +
            Math.pow(mouseLoc.y - last.y, 2));

         // Create a HitData object which the data tip will use. I ignore
         // the ID param for now since I didn't find it useful. If you
         // need to implement it, see LineSeries.createDataID
         var hitData:HitData = new HitData(0, dist, x,
            hitPoint.y + _seriesHolder.y, hitPoint);

         // Make the data tip color scheme match the line. If you do
         // anything fancy with your line coloring you'll probably have
         // to modify this.
         var stroke:SolidColorStroke = SolidColorStroke(
            line.getStyle('lineStroke'));
         hitData.contextColor = stroke.color;

         // Create the function which actually gives the text value
         // for the data tip. Normally the LineSeries constructs its
         // own HitData objects and is responsible for supplying
         // this function.
         hitData.dataTipFunction = makeDataTipFunction(line);

         result.push(hitData);
      }
   }

   return result;
}

protected function makeDataTipFunction(line:LineSeries):Function {

   // Create a data tip function for a given LineSeries. The code
   // below was copied almost exactly from LineSeries.formatDataTip
   return function(hd:HitData):String {
      var txt:String = "";

      var n:String = line.displayName;
      if (n && n != "") {
         txt += "<b>" + n + "</b><BR/>";
      }

      var xName:String = line.dataTransform.getAxis(
         CartesianTransform.HORIZONTAL_AXIS).displayName;

      if (xName != "") {
         txt += "<i>" + xName+ ":</i> ";
      }

      txt += line.dataTransform.getAxis(
         CartesianTransform.HORIZONTAL_AXIS).formatForScreen(
         LineSeriesItem(hd.chartItem).xValue) + "\n";

      var yName:String = line.dataTransform.getAxis(
         CartesianTransform.VERTICAL_AXIS).displayName;

      if (yName != "") {
         txt += "<i>" + yName + ":</i> ";
      }

      txt += line.dataTransform.getAxis(
         CartesianTransform.VERTICAL_AXIS).formatForScreen(
         LineSeriesItem(hd.chartItem).yValue) + "\n";

      return txt;
   }
}

Our new findDataPoints() finds the value of each line on the chart at the current mouse coordinates and returns and returns a HitData object for each value. The above code was written with step style LineCharts in mind, but can work with any style with a few modifications. The key is that you need to interpolate the value of each line at the mouse’s x-coordinate and use this value to display the data tip.

My code also only displays data tips for lines that are within 50 pixels of the mouse vertically. You can easily remove this check if you want to display tips for every line.

Here is the final result.