Asserting the Basics – Part IIb

No Gravatar

Hello, again! We’re back with another episode of “Asserting the Basics”. Last time we talked about our assertive philosophy on error handling. Today, we go into the dirty little details: the coding of asserts.

Asserts are not something that Actionscript3 offers naturally. Consequently, I added an Assert-method to our Debug class (remember from my first post?). Its job is (of course) to check whether a Boolean expression is true, and, if not, to eventually communicate this fact. Rather than showing all failed asserts directly, our method only stores them. I want to have control over when the asserts are shown, since there are times when you actually want asserts to fail: for instance when you want to try out your error handling in Unit Tests. Thus, there is another method to take care of displaying them, which can be called at an appropriate time. So our assert framework looks like this:

private static var m_asserts:Array;
/**
 * Asserts that a condition is met
 * @param    _statement condition to check
 * @param    _msg Message to describe the assert
 * @return true if the assert failed
 */
public static function Assert(_statement:Boolean, _msg:String):Boolean
{
    if (_statement != true)
    {
        var msg:String = "****************************\n "
            + "ASSERT FAILED!!\n "+_msg+"\n"+GetStackTrace()+"\n"
            + "****************************\n ";
        // record assert
        if (m_asserts == null)
            m_asserts = new Array();
        m_asserts.push(msg);
        Debug.Out(msg);
        return true;
    }
    return false;
}
public static function ShowAsserts(_stage:Stage) : void
{
    if (m_asserts == null)
        return;
    var assertBox:TextField = new TextField();
    assertBox.autoSize = TextFieldAutoSize.LEFT;
    assertBox.width = _stage.stageWidth;
    assertBox.textColor = 0xFF0000;
    assertBox.backgroundColor = 0x000000;
    assertBox.background = true;
    assertBox.wordWrap = true;
    assertBox.text = "";
    for (var i:int = 0; i < m_asserts.length; i++) {
        var assertString:String = m_asserts[i];
        assertBox.appendText(assertString + "\n");
    }
    _stage.addChild(assertBox);
}

You may have noticed that our assert method has a Boolean return value, returning true if the assert fails. This way we can use the method to exit early from a function that would crash otherwise. Since we cannot simply take the asserts back out, we might as well use them in a bigger context.

In our code the use of these asserts look like this:

public function Load(_path:String) : void {
    if (Debug.Assert(m_fileName != null, "Filename is null!!"))
        return;
    if (Debug.Assert(m_fileName.length != 0, "Filename is empty!!"))
        return;
    // yada yada … more code
}

To give our asserts even more value, I did some research on how to get stack traces. I found out that if you throw an error and catch it in a debug player, you can retrieve a stack trace from the error object, as seen here:

/**
 * Returns the stack trace (filtered)
 * @return stack trace
 */
public static function GetStackTrace() : String {
    if (Capabilities.isDebugger == true) {
        try { throw new Error(); }
        catch (e:Error) { return FilterStackTrace(e.getStackTrace()); }
        return "";
    }
    else
        return "Stack trace not available in non-debugger version.";
}

Unfortunately, the stack trace you get this way is really extensive and hard to read. Every line in the trace contains the complete path to the corresponding file on the local machine. So, I added a filtering function to remove the unnecessary path info. This method turns e.g. this:

tests::TestDynamicTextManager.TestTextEdit()Trace:
AssertionFailedError
	at tests::TestDynamicTextManager/TestTextEdit()
	[D:\projects\programming\bgame\svn\FlashComponents
	\client\src\tests\TestDynamicTextManager.as:15]

into this:

tests::TestDynamicTextManager.TestTextEdit()Trace:
AssertionFailedError
	at tests::TestDynamicTextManager/TestTextEdit() [line:15]

Here is the filter function:

public static function FilterStackTrace(stack:String):String {
    var lines:Array = stack.split("\n");
    // remove the path
    // it's too long and we can get the info from the method trace
    var regEx:RegExp = /\w:[\\\/]([\w-]+[\\\/])*\w+.as/ig;
    var newStack:String = new String("\n");
    for (var i:int = 0; i < lines.length; i++) {
        var line:String = lines[i];
        line = line.replace(regEx, "");
        line = line.replace("[:", " [line:");
        newStack = newStack + line + "\n";
    }
    return newStack;
}

Alrighty, now you’re set to do your own asserts. Next time, I’ll talk about unit testing and Actionscript3.
So, see you in a bit for a new episode of “Asserting the Basics”.

Happy coding, Manuel

Popularity: 12% [?]

  • Share/Bookmark
Tags: , , , , , ,

3 Responses to “Asserting the Basics – Part IIb”

  1. steffenj Says:

    if (assert(..)) return;

    Ouch! It really hurts me to see that. So you don’t want your function to crash? That’s ok if it’s a utilitarian function, like “GetDebugName” whose output is completely optional. Maybe even for something like a “Goto” command. So what if the NPC doesn’t move, as long as the game doesn’t crash. Or loading an asset, as in this case, then just use the default asset (checkerboard texture or default cube model or whatever).

    Good. But as a general thing to build your asserts with return values, i don’t know. First of all, there’s a very common advice called “don’t put logic inside your asserts”. I would turn this around and say “don’t build your logic around asserts”. Most often, if the assert fails, the game already is in an undefined state and shouldn’t continue.

    But then again, and maybe that’s the whole issue here, you are trying to build unit tests around your game code, and without some “softness” of error reporting (including delayed or disabled display of assert messages) it’s really hard to get right. Maybe that should have been stressed more.

    As a general rule of thumb i would say that unless you are 100% sure that exiting a function prematurely doesn’t corrupt the game state, don’t use assertion return values in your code flow.

    Your example is just a more convenient way to do the following:

    CClass* pPointer = GetPointer();
    Assert(pPointer);
    if (pPointer != NULL) { … pPointer used here … };

    I would prefer the explicitness in such cases.

  2. Manuel Says:

    Hi Steffen,

    Thanks for your insight! You are absolutely right! I agree with you in every respect … if I’d be working in a C or C++ environment like you do. I do know those rules you pointed out. Still, I decided to violate them and there are good reasons to do so:

    (1) Any return-logic inside an assert will never be removed.
    We do not work with a pre-processor. The asserts stay in the production code. Yet, we definately try to make sure, that asserts do not have any other side effects, though!

    (2) Early-outs prevent Flash from throwing any exceptions. While using exceptions is quite handy in a debug-player, they will silently crash any non-debug players without any message whatsoever. Doing it the way I decribed, we get a decent error message even when the game crashes in a web browser.

    (3) There is no way (that I know of) to savely show an assert message and then stop the processing of a Flash application as you can e.g. on PC or consoles. We solved this by simply leave the function early and then show a huge pop-up containing the assertion-message (in the next frame, which is simply the way flash works). The pop-up makes it impossible to continue to work with the game (although it still runs in the background). It doesn’t matter if the game state is corrupted afterwards, you won’t be able to see it.

    (4) And it is a more convenient way to do the code-snipped you mentioned. ;)

    Thinking about it makes me wonder if I should have given the whole thing a different name. This would prevent people from thinking I am an idiot for breaking the most vital rules associated with asserts. ;)

    Cheers,
    Manuel

  3. steffenj Says:

    Ok, i admit somewhere i forgot that you’re working with ActionScript. Obviously 3 mentions of AS weren’t enough for me. ;)

    Thanks especially for clarifying (2), that sounds a lot more reasonable to me now.

Leave a Reply