Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Symbian OS Explained - Effective C++ Programming For Smartphones (2005) [eng].pdf
Скачиваний:
60
Добавлен:
16.08.2013
Размер:
2.62 Mб
Скачать

16

Bug Detection Using Assertions

On the contrary!

As Ibsen lay dying, his nurse brought him some visitors. ”Our patient is feeling much better today,” she told them. Ibsen woke up, made the exclamation above, and died.

In C++, assertions are used to check that assumptions made about code are correct and that the state, for example, of objects, function parameters or return values, is as expected. Typically, an assertion evaluates a statement and, if it is false, halts execution of the code and perhaps prints out a message indicating what failed the test, or where in code the failure occurred.

On Symbian OS, you’ll find the definition of two assertion macros1 in e32def.h:

#define __ASSERT_ALWAYS(c,p) (void)((c)||(p,0))

...

#if defined(_DEBUG)

#define __ASSERT_DEBUG(c,p) (void)((c)||(p,0))

#endif

As you can see from the definition, if the assertion of condition c is false, procedure p is called; this should always halt the flow of execution, typically by panicking (panics are described in detail in Chapter 15). You can apply the assertion either in debug code only or in both debug and release builds. I’ll discuss how you decide which is appropriate later in the chapter.

1 At first sight, these definitions seem more complex than they need to be, when the following simpler definition could be used:

#define __ASSERT_ALWAYS(c,p) ((c)||(p))

The reason for the (p,0) expression is for cases where the type returned from p is void (the case when p is a typical Panic() function) or a value that can’t be converted to an integer type for evaluation. The cast to void is present to prevent the return value of the expression being used inadvertently.

256

BUG DETECTION USING ASSERTIONS

You’ll notice that the assertion macros do not panic by default, but allow you to specify what procedure to call should the assertion fail. This gives you more control, but you should always terminate the running code and flag up the failure, rather than return an error or leave. Assertions help you detect invalid states or bad program logic so you can fix your code as early as possible. It makes sense to stop the code at the point of error, thus forcing you to fix the problem (or remove the assertion statement if your assumption is invalid). If the assertion simply returns an error on failure, not only does it alter the program flow, but it also makes it harder to track down the bug.

You should always raise a panic when an assertion statement fails.

16.1 −−−ASSERTDEBUG

Here’s one example of how to use the debug assertion macro:

void CTestClass::TestValue(TInt aValue)

{

#ifdef _DEBUG

_LIT(KPanicDescriptor, "TestValue"); // Literal descriptor

#endif

__ASSERT_DEBUG((aValue>=0), User::Panic(KMyPanicDescriptor,

KErrArgument));

...

}

Of course, this is somewhat awkward, especially if you expect to use a number of assertions to validate your code, so you’ll probably define a panic utility function for your module, with its own panic category string and a set of panic enumerators specific to the class. So, for example, you’d add the following enumeration to CTestClass, so as not to pollute the global namespace:

enum TTestClassPanic

{

EInvalidData,

// =0

EUninitializedValue // =1

...

};

Then define a panic function, either as a member of the class or as a static function within the file containing the implementation of the class:

−−−ASSERTDEBUG

257

static void Panic(TInt aCategory)

{

_LIT(KTestClassPanic, "CTestClass-Panic");

User::Panic(KTestClassPanic, aCategory);

}

You could then write the assertion in TestValue() as follows:

void CTestClass::TestValue(TInt aValue)

{

__ASSERT_DEBUG((aValue> =0), Panic(EInvalidTestValueInput));

...

}

The advantage of using an identifiable panic descriptor and enumerated values for different assertion conditions is traceability, both for yourself and clients of your code, when an assertion fails and a panic occurs. This is particularly useful for others using your libraries, since they may not have access to your code in its entirety, but merely to the header files. If your panic string is clear and unique, they should be able to locate the appropriate class and use the panic category enumeration to find the associated failure, which you will have named and documented clearly to explain why the assertion failed.

There may be cases where there’s nothing more a client programmer can do other than report the bug to you, the author of the code; alternatively, the problem could be down to their misuse of your library, which they’ll be able to correct. I’ll discuss the pros and cons of using assertions to protect your code against badly-programmed calling code later in this chapter.

If you don’t want or need an extensive set of enumerated panic values, and you don’t expect external callers to need to trace a panic, you may consider using a more lightweight and anonymous assertion. A good example of this is to test the internal state of an object, which could not possibly be modified by an external caller, and thus should always be valid unless you have a bug in your code. Assertions can be added early in the development process, but left in the code, in debug builds, to validate the code as it is maintained and refactored. In these cases, you may consider using the ASSERT macro, defined in e32def.h as follows:

#define ASSERT(x) __ASSERT_DEBUG(x, User::Invariant())

I like this macro because it doesn’t need you to provide a panic category or descriptor. If condition x is false, in debug builds only, it calls User::Invariant() which itself panics with category USER and reason 0. The macro can be used as follows:

258

BUG DETECTION USING ASSERTIONS

ASSERT(iClanger>0);

As an alternative to using ASSERT to test the internal state of your object, you may wish to consider using the __TEST_INVARIANT macro, which I discuss in more detail in Chapter 17.

An alternative, useful definition of ASSERT which you may see in some Symbian OS code is as follows:

#ifdef _DEBUG #ifdef ASSERT #undef ASSERT #endif

#define __ASSERT_FILE__(s) _LIT(KPanicFileName,s)

#define __ASSERT_PANIC__(l) User::Panic(KPanicFileName().Right(12),l) #define ASSERT(x) { __ASSERT_FILE__(__FILE__);

__ASSERT_DEBUG(x, __ASSERT_PANIC__(__LINE__) ); }

#endif

This slightly alarming construction is actually quite simple; in debug builds, if condition x is false, the code is halted by a panic identifying the exact place in code (in the panic descriptor – which contains the last 12 characters of the filename) and the panic category (which contains the line of code at which the assertion failed). The disadvantage of using this construct is that you are coupling the compiled binary directly to the source file. You cannot later modify your code file, even to make non-functional changes to comments or white space lines, without recompiling it to update the assertion statements. The resulting binary will differ from the original, regardless of the nature of the changes. Depending on how you deliver your code, this limitation may prohibit you from using this macro.

Let’s move on from how to use the Symbian OS assertion syntax to consider when you should use assertions and, perhaps more importantly, when you should not.

Firstly, don’t put code with side effects into assertion statements. By this, I mean code which is evaluated before a condition can be verified.

For example:

__ASSERT_DEBUG(FunctionReturningTrue(), Panic(EUnexpectedReturnValue));

__ASSERT_DEBUG(++index<=KMaxValue, Panic(EInvalidIndex));

The reason for this is clear; the code may well behave as you expect in debug mode, but in release builds the assertion statements are removed by the preprocessor, and with them potentially vital steps in your programming logic. Rather than use the abbreviated cases above, you should perform the evaluations first and then pass the returned values into the

−−−ASSERTDEBUG

259

assertion. You should follow this rule for both __ASSERT_DEBUG and __ASSERT_ALWAYS statements, despite the fact that the latter are compiled into release code, because, while you may initially decide the assertion applies in release builds, this may change during the development or maintenance process. You could be storing up a future bug for the sake of avoiding an extra line of code.

You must also make a clear distinction between programming errors (”bugs”) and exceptional conditions. Examples of bugs might be contradictory assumptions, unexpected design errors or genuine implementation errors, such as writing off the end of an array or trying to write to a file before opening it. These are persistent, unrecoverable errors which should be detected and corrected in your code at the earliest opportunity. An exceptional condition is different in that it may legitimately arise, although it is rare (hence the term ”exceptional”) and is not consistent with typical or expected execution. It is not possible to stop exceptions occurring, so your code should implement a graceful recovery strategy. A good example of an exceptional condition that may occur on Symbian OS is an out-of-memory failure, because it is designed to run constantly on devices with limited resources for long periods of time without a system reset.

To distinguish between bugs and exceptions, you should consider the following question. Can a scenario arise legitimately, and if it can, is there anything you should or could do to handle it? If your answer is ”yes”, you’re looking at an exceptional condition – on Symbian OS, this is exhibited as a leave (leaving is discussed in Chapter 2). If the answer is ”no”, you should consider the situation to be caused by a bug which should be tracked down and fixed. The rest of this chapter will focus on the use of assertions to highlight such programming errors.

When code encounters a bug, it should be flagged up at the point at which it occurs so it can be fixed, rather than handled or ignored (which can at best complicate the issue and, at worst, make the bug more difficult to find or introduce additional defects as you ”code around it”). You could consider assertions as an annoying colleague, leaning over your shoulder pointing out defects for you as your code runs. They don’t prevent problems, but make them obvious as they arise so you can fix them.

If you add assertion statements liberally as you write code, they effectively document assumptions you make about your program logic and may, in addition, flag up unexpected problems. As you consider which assertions to apply, you are actually asking yourself what implicit assumptions apply to the code and how you can test them. By thinking about each piece of code you write in this way, you may well discover other conditions to test or eliminate that would not have been immediately obvious. Frequent application of assertions as you code can thus help you to pre-empt bugs, as well as catch those already in existence.

260

BUG DETECTION USING ASSERTIONS

And there’s more! Another benefit of assertions is the confidence that your code is behaving correctly, and that you are not ignoring defects which may later manifest themselves where they are hard to track down, for example intermittently or through behavior seemingly unrelated to the code that contains the error. Say you write some library code containing no assertions and then create some code to test it, which runs and returns no errors; are you confident that everything behaves as you expect and the test code checks every boundary condition? Are you sure? Certain?

Adding assertions to test fundamental assumptions in your code will show you immediately if it is swallowing or masking defects – and it should give you more confidence that the test results are valid. Sure, you might hit some unpleasant surprises as the test code runs for the first time, but once you’ve ironed out any failures, you can be more confident about its overall quality. What’s more, the addition of assertions also protects your code against any regressions that may be introduced during maintenance and refactoring but which would not otherwise be picked up by your test code. What more could you ask?

The cases so far could be considered as ”self defense”, in that I’ve discussed using assertions to catch bugs in your code. Let’s move on to consider defensive programming in general. Defensive programming is not about retorting ”It works OK on my machine” after being informed that your code doesn’t work as expected. It’s based on defending your code against irresponsible use or downright abuse by code that calls it. Defensive code protects functions against invalid input, by inspecting data passed in and rejecting corrupt or otherwise flawed parameters, such as strings that are too long or out-of-range numerical values.

You’ll need to consider how to handle bad parameters depending on how your code is called; for example, you may want to assert that the data is good, terminating with a panic if it is not. Alternatively, you may decide to continue the flow of execution, so instead of assertions, you’ll check each incoming parameter (e.g. using if statements) and return to the caller if invalid data is detected – either with an error value or a leave code. Another method would be to check incoming data and, if a parameter is invalid, substitute it with a default parameter or continue with the closest legal value. What you don’t want to do is ignore invalid input and carry on regardless, since this could lead to problems later on, such as data corruption. Whatever method you use to handle illegal input, it should be consistent throughout your code.

Your clients should be testing with debug versions of your libraries and thus you could use __ASSERT_DEBUG statements to alert them of invalid input, or other misuse, so they can correct it.

−−−ASSERTALWAYS

261

__ASSERT_DEBUG assertions can be added early in the development process to highlight programming errors and can be left in to validate the code as it is maintained and refactored – acting as a means to ”design by contract”.

16.2 −−−ASSERTALWAYS

You still need to be defensive by checking for illegal usage in release builds too, but the case for using __ASSERT_ALWAYS isn’t clear cut. Remember, your assertions will terminate the flow of execution and panic the library, displaying a nasty ”program closed” dialog to the user, which is generally best avoided where possible. Additionally, you should consider the impact on the speed and size of your code if you apply assertion statements liberally in release builds.

If you decide not to use __ASSERT_ALWAYS to check incoming values, you should use another defensive technique to guard against illegal input, such as a set of if statements to check values and return error codes or leave when data is unusable. You could use these in combination with

aset of __ASSERT_DEBUG statements to alert the client programmer to invalid use in debug builds, but often it is preferable to keep the flow of execution the same in both debug and release builds. In such cases, I suggest you don’t use debug assertions to check input, but instead use if statement checking in both modes, and document each expected return value for your functions. Client programmers should understand their responsibility to interpret the return value and act accordingly. I’ll illustrate this with an example later in this chapter.

To determine whether you should use __ASSERT_ALWAYS or another, less terminal, defense, I recommend that you consider whether the calling code may be able to take a different action if you do return an error. Invalid input is a bug from the perspective of your code, but may be caused by an exceptional condition in the calling code which can be handled.

A simplistic example would be a call to your code to open and write to

afile, where the caller passes in the full file name and path, as well as the data to be written to the file. If the file does not exist, it is probably more appropriate to return this information to the caller through a returned error code or leave value than to assert in a release build. Client code can then anticipate this and deal with it, without the need for your library to panic and alarm the user accordingly.

262

BUG DETECTION USING ASSERTIONS

Here’s an example of this scenario which illustrates how to defend against illegal parameters without assertions. This code returns an error to allow the caller to recover if they pass in an invalid parameter. Of course, if the calling code is written in such a way that each parameter should be correct and that only a bug could result in them being invalid, it can assert that the return value from a call to WriteToFile() is KErrNone. On the other hand, if it’s an exceptional circumstance that the file is missing or the data is non-existent, it can handle it gracefully.

TInt CTestClass::WriteToFile(const TDesC& aFilename,

const TDesC8& aData)

{

TInt r = KErrNone;

if (KNullDesC8==aData)

{// No data to write – invalid! r = KErrArgument;

}

else

{

RFile file;

__ASSERT_DEBUG(iFs, Panic(EUninitializedValue)); r = file.Open(iFs, aFilename, EFileWrite);

if (KErrNone==r)

{ // Only executes if the file can be opened

... // Writes aData to file, closes file etc

}

}

return (r);

}

You’ll notice that I’ve included an __ASSERT_DEBUG statement to verify internal state in my code and catch any defects (such as attempting to use the file server handle before it has been initialized) in the test phase.

One case where you may prefer to use __ASSERT_ALWAYS to protect your code against illegal input is where that input could only have arisen through a bug in calling code and will cause ”bad things”, such as memory corruption, to occur. You could return an error to the caller, but it’s probably clearer for the calling code if you flag up the problem so it can be fixed. A good example of this is in the Symbian OS array classes (RArray and RPointerArray), which have __ASSERT_ALWAYS guards to prevent a caller passing an invalid index to methods that access the array. The class provides functions to determine the size of the array, so if a caller attempts to write off the end of the array, it can only be doing so because of a bug.

Likewise, in the code above, if the context of the function means that the second parameter, aData, should never be an empty string, you can replace the first if statement check with an __ASSERT_ALWAYS statement. But this assumes knowledge of how clients expect to call it