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

270

DEBUG MACROS AND TEST CLASSES

17.2Object Invariance Macros

The __DECLARE_TEST and __TEST_INVARIANT macros allow you to check the state of an object, for example at the beginning and end of a complex function. The __DECLARE_TEST macro provides a standard way to add a function to a class that allows object invariance to be tested. This function is named __DbgTestInvariant() and you should define it as necessary. When performing the invariance testing, rather than call the function directly, you should use the __TEST_INVARIANT macro, as I’ll show shortly.

__DECLARE_TEST is defined as follows in e32def.h:

#if defined(__DLL__)

#define __DECLARE_TEST public: IMPORT_C void __DbgTestInvariant() const; void __DbgTest(TAny *aPtr) const

#else

#define __DECLARE_TEST public: void __DbgTestInvariant() const; void __DbgTest(TAny *aPtr) const

#endif

You should add it to any class for which you wish to perform an object invariance test. It’s best to add it to the bottom of the class declaration because it uses the public access specifier. There are separate definitions for DLLs and EXEs because the function must be exported from a DLL so it may be called from a separate module. The second function defined by __DECLARE_TEST, __DbgTest(), is designed to allow test code to access non-public members of the class, if such testing is required. Because any test code will implement this function itself, it is not exported.

The __DECLARE_TEST macro is defined to be the same for both release and debug builds, because otherwise the export list for debug builds would have an extra function (__DbgTestInvariant()) compared to the release version. This could cause binary compatibility problems, by changing the ordinal numbers of other exported functions in the module. (Chapter 18 discusses binary compatibility in more detail.)

The __TEST_INVARIANT macro is defined below; as you can see, it doesn’t call __DbgTestInvariant() in release builds. However, if you don’t want the invariance test code to be built into release builds, you should surround it with preprocessor directives as appropriate to build the invariance testing into debug builds only.

#if defined(_DEBUG)

#define __TEST_INVARIANT __DbgTestInvariant()

#else

#define __TEST_INVARIANT

#endif

OBJECT INVARIANCE MACROS

271

The invariance test function, __DbgTestInvariant(), typically checks that the object’s state is correct at the beginning and end of a function. If the state is invalid, a panic should be raised by calling User::Invariant() (which panics with category USER, reason 0). You may recall, from Chapter 16, that this is the panic raised by the ASSERT macro. Thus a simple way to implement the invariance testing in __DbgTestInvariant() is to perform the invariance checks inside ASSERT statements.

Here’s an example of a class that represents a living person and uses the __DECLARE_TEST invariance test to verify that:

the person has a gender

if the name is set, it is a valid string of length no greater than

250 characters

the given age in years matches the age calculated for the given date of birth and is no greater than 140 years.

class CLivingPerson: public CBase

{

public:

enum TGender {EMale, EFemale}; public:

static CLivingPerson* NewL(const TDesC& aName, CLivingPerson::TGender aGender);

CLivingPerson(); public:

... // Other methods omitted for clarity

void SetDOBAndAge(const TTime& aDOB, const TInt aAge); private:

CLivingPerson(TGender aGender); void ConstructL(const TDesC& aName);

private:

HBufC* iName;

TGender iGender;

TInt iAgeInYears;

TTime iDOB; // Date of Birth __DECLARE_TEST; // Object invariance testing };

CLivingPerson::CLivingPerson(TGender aGender) : iGender(aGender) {}

CLivingPerson:: CLivingPerson() {delete iName;}

CLivingPerson* CLivingPerson::NewL(const TDesC& aName,

CLivingPerson::TGender aGender)

{

CLivingPerson* me = new (ELeave) CLivingPerson(aGender); CleanupStack::PushL(me);

me->ConstructL(aName); CleanupStack::Pop(me);

272

DEBUG MACROS AND TEST CLASSES

return (me);

}

void CLivingPerson::ConstructL(const TDesC& aName)

{

__TEST_INVARIANT; iName = aName.AllocL(); __TEST_INVARIANT;

}

void CLivingPerson::SetDOBAndAge(const TTime& aDOB, const TInt aAge) {// Store the DOB and age and check object invariance __TEST_INVARIANT;

iDOB=aDOB;

iAgeInYears=aAge; __TEST_INVARIANT;

}

void CLivingPerson::__DbgTestInvariant() const

{

#ifdef _DEBUG // Built into debug code only if (iName)

{// Name should be more than 0 but not longer than 250 chars TInt len = iName->Length();

ASSERT(len>0); // A zero length name is invalid ASSERT(len<250); // The name should be less than 250 characters

}

// Person should male or female ASSERT((EMale==iGender)||(EFemale==iGender));

if (iDOB>(TTime)0)

{// DOB is set, check that age is correct and reasonable TTime now;

now.HomeTime();

TTimeIntervalYears years = now.YearsFrom(iDOB);

TInt ageCalc = years.Int(); // Calculate age in years today ASSERT(iAgeInYears==ageCalc);// Compare with the stored age ASSERT(iAgeInYears>0); // Stored value shouldn’t be 0 or less ASSERT(iAgeInYears<140); // A living person is less than 140

}

#endif

}

void TestPersonL()

{

_LIT(KSherlockHolmes, "Sherlock Holmes");

CLivingPerson* holmes = CLivingPerson::NewL(KSherlockHolmes, CLivingPerson::EMale);

//Holmes was (apparently) born on 6th January 1854, so he would

//have been 150 years old at the time of writing (2004) TDateTime oldTime(1854, EJanuary, 5, 0, 1, 0, 0);

//This will panic. Holmes is older than 140!

holmes->SetDOBAndAge(TTime(oldTime), 150);

delete holmes;

}

CONSOLE TESTS USING RTest

273

One last point: if the class derives from a class which also implements the invariance test function, __DbgTestInvariant() should be called for the base class first, then any checking done on the derived object.

17.3 Console Tests Using RTest

Class RTest is not documented, but is a useful test utility class, used by a number of Symbian OS test harnesses. You’ll find the class definition in \epoc32\include\e32test.h. You should bear in mind that, because the RTest API is not published, there are no firm guarantees that it will not change in future releases of Symbian OS.

RTest is useful for test code that runs in a simple console. You can use the class to display text to the screen, wait for input, and check a conditional expression, rather like an assertion, raising a panic if the result is false.

The main methods of RTest are Start(), End() and Next(), which number the tests, and operator(), which evaluates the conditional expression. The first call to Start() begins numbering at 001 and the numbering is incremented each time Next() is called. Nested levels of numbering can be achieved by using additional calls to Start(), as shown in the example. Each call to RTest::Start() must be matched by a call to RTest::End().

The following source code can be used as the basis of a simple consolebased test project, using targettype exe, as described in Chapter 13. Remember, if you want to use the cleanup stack in console-based test code, or link against a component which does so, you will need to create a cleanup stack at the beginning of the test, as discussed in Chapter 3. Likewise, if you write or link against any code which uses active objects, you will need to create an active scheduler, as I described in Chapters 8 and 9.

#include <e32test.h>

GLDEF_D RTest test(_L("CONSTEST"));

TInt Test1()

{

test.Next(_L("Test1")); return (KErrNone);

}

TBool Test2()

{

test.Next(_L("Test2")); return (ETrue);

}

void RunTests()

{

274

DEBUG MACROS AND TEST CLASSES

test.Start(_L("RunTests()")); TInt r = Test1(); test(KErrNone==r); test(Test2());

test.End();

}

TInt E32Main()

{

__UHEAP_MARK; // Heap checking, as described above test.Title();

test.Start(_L("Console Test")); TRAPD(r,RunTests()); test.End();

test.Close(); __UHEAP_MARKEND; return (KErrNone);

}

You’ll notice that I’m using the _L form of a literal descriptor in the test code, despite mentioning in Chapter 5 that it is deprecated because it constructs a temporary stack-based TPtrC object at runtime. However, it’s acceptable to use it for debug and test code which is never released and, in this test example, I think it’s clearer than predefining a set of _LIT literal descriptors.

The code above gives the following output:

RTEST TITLE: CONSTEST X.XX(XXX) // Build version and release number

Epoc/32 X.XX(XXX)

// Build version and release number

RTEST: Level

001

 

Next test -

Console Test

 

RTEST: Level

001.01

 

Next test -

RunTests()

 

RTEST: Level 001.02

Next test - Test1

RTEST: Level 001.03

Next test - Test2

RTEST: SUCCESS : CONSTEST test completed O.K.

RTest::operator() can be used to evaluate boolean expressions; if the expression is false, a USER 84 panic occurs. For example, if Test2() above is modified to return EFalse, running the test gives following output:

RTEST TITLE: CONSTEST X.XX(XXX) // Build version and release number

Epoc/32 X.XX(XXX)

// Build version and release number

CONSOLE TESTS USING RTest

275

RTEST: Level 001

Next test - Console Test

RTEST: Level 001.01

Next test - RunTests()

RTEST: Level 001.02

Next test - Test1

RTEST: Level 001.03

Next test - Test2

RTEST: Level 001.03

: FAIL : CONSTEST failed check 1 in Constest.cpp at line number 21

RTEST: Checkpoint-fail

You’ll notice this #define statement in e32test.h:

#define test(x) __test(x,__LINE__,__FILE__)

This is useful, because, if you use a variable name of test for the RTest object as in the example, even if you use the simplest overload of operator() which just takes the conditional expression, the actual overload invoked includes the source file and line number at which the test occurs. However, if you prefer not to display and log this additional information, you should simply give the RTest object a variable name other than test.

All the RTest functions which display data on the screen also send it to the debug output using a call to RDebug::Print(). If you’re running the code on the Windows emulator, this function passes the descriptor to the debugger for output. On hardware, the function uses a serial port which can be connected to a PC running a terminal emulator to view the output (for this output, of course, you need a spare serial port on the phone and a connection to a PC). You can set the port that should be used by calling Hal::Set(EDebugPort,aPortNumber). You’ll find a debug log can be extremely useful when running your code on the phone as you develop it. Class RDebug is defined in e32svr.h and provides a number of other useful functions, such as support for profiling.

On the emulator, the debug output is also written to file, so you can run a batch of tests and inspect the output of each later. The emulator uses the TEMP environment variable to determine where to store the file, which it names epocwind.out (%TEMP%\epocwind.out).3

3 On Windows NT this is usually C:\TEMP, while on versions of Windows later than Windows 2000, each user has a temporary directory under their Local Settings folder (C:\Documents and Settings\UserName\Local Settings\Temp). Of course, you can change the TEMP environment variable to any location you want to make access to this file convenient.