Quality Assurance

Creating high-quality software is inherently difficult. Fortunately, as the software development discipline has matured, effective proactive strategies have been identified in order to streamline and stabilize the development process. This collective wisdom is called Quality Assurance. It is a process-oriented methodology ”the philosophy is that improving the software development process automatically improves the end product. Quality Assurance is multifaceted; this section focuses on those aspects that are immediately relevant to Series 60 code generation.

While the later Testing and Debugging sections are of primary importance, once you have written your application, the advice and tips given in the Quality Assurance section should be heeded before you begin development. This should help you to prevent problems.

Coding Standards

As discussed in Chapter 3, Series 60 offers well-defined coding standards. Compliance is essential, because it offers numerous benefits ”for example, the standards encompass memory management techniques (in other words, proper use of the Cleanup Stack, two-phase construction and so on) to ensure that applications make responsible use of inherently limited memory.

The standards also provide class and function naming conventions, and these greatly improve the readability of code. Naming conventions convey important information about the purpose and behavior of classes and functions, without requiring full inspection of code logic. This information can aid in the debugging and maintenance of code, especially when these tasks are not being carried out by the original developer. For these reasons, coding standards are an important part of Quality Assurance.

Formal code reviews by peers or mentors are another good way to locate common programming errors and enforce coding standards.


Coding standards are covered in more detail in Chapter 3.

Common Coding Mistakes

Quality Assurance seeks, in part, to avoid past mistakes. In this spirit, common coding mistakes by Symbian OS developers are listed below, so that you are less likely to repeat them:

  • After becoming accustomed to the automatic zeroing of all member data for CBase -derived classes, developers often forget to initialize member data for non- CBase -derived classes. Automatic zero-initialization is CBase -specific behavior, so T -classes and all standard types should initialize their data to suitable values on construction.

  • Developers who write a C -class and then forget to derive it from CBase will leave themselves vulnerable to memory leaks. This is because objects pushed onto the Cleanup Stack that are not derived from CBase are pushed as TAny* . No destructor will be called when these objects are explicitly removed from the stack, either via PopAndDestroy() or when the stack implicitly performs cleanup upon leaving. (Note that it is fine for pointers to some objects, such as T -class objects, to be pushed onto the Cleanup Stack as TAny* , because they do not have destructors.) For this reason, remember to derive all C -classes from CBase .

  • Dangling pointers are easily created by forgetting to set pointers to NULL when explicitly deleting their corresponding objects. It is important to do this even if the next line of code allocates a new value, because the allocation itself may leave. One exception to this rule is in destructors, as member data pointers will fall out of scope as their class instance is destroyed .

  • Constructors and destructors should not call leaving functions. Doing this compromises the failsafe memory management measures offered by two-phase construction. Therefore, never call in a C++ constructor or destructor a function that can leave.

  • Virtual functions should not appear in constructors and destructors. The intention of a virtual function is to provide transparent access to its most derived implementation. This polymorphism feature is not available in constructors and destructors because the derived class will not exist at this time. In other words, the constructor of base class A does not know that it is actually creating a derived class B, and therefore a "virtual" function call within the constructor will only call class A's implementation.

  • Explicitly deleting an item that has already been placed on the Cleanup Stack is a mistake. By placing an item on the Cleanup Stack, you are relinquishing control of that item. By explicitly deleting the item, the Cleanup Stack is left with a dangling pointer to invalid memory ”its subsequent attempt to delete this memory will cause an access violation.

  • Developers often forget to adhere to function naming standards ”in particular, make sure any functions that can leave have a trailing " L ". (Symbian has developed a simple tool called Leavescan to check this for you ”this tool will be covered in the Testing section.)

  • Do not export inline functions, as this causes problems for dependent components at link time. Inline functions should be declared inline and an implementation should be provided in a separate .inl file.

  • Some of the application objects in a UI application ”for example, the CAknApplication -derived object ”are created before the Cleanup Stack. If a leave occurs before the Cleanup Stack is created, then the application will crash.

  • Developers often forget that their applications may eventually be ported to another language, and restrict descriptor sizes. Make sure that any descriptors that hold user -visible text have space to expand when localized, and that any controls that use such text can resize to fit.

  • Do not ignore compiler warnings. Although your application may still run, warnings often illustrate instances of bad coding practices.

Defensive Programming

Defensive programming is the practice of anticipating the possibility of application failure ”often creating a system to test for errors, and receiving notification when they occur. Defensive programming makes your code easier to maintain, while helping to avoid coding errors. In addition to general coding rules (such as always initializing local variables before use), a number of macros are provided that you should use to actively check the state of your code. Typically, this simply means checking, at critical points within the code, that the values of variables (or any resources whose corresponding values are susceptible to change) fall within expected boundaries.

Assertions

Assertions are designed to catch programming errors. They allow you to perform logic tests upon code and to specify the consequences of failure. (Failures not caused by programming errors, such as bad data, or out-of-memory conditions, should be handled by Leave exceptions.) Assertions, as shown below, are facilitated by a distinct coding mechanism. It is important to understand that this new mechanism does not offer new functionality as such ”you could perform the same tests and specify the same consequences through normal programming techniques. Nevertheless, it is good practice to use assertions, because they clearly define the developer's intentions and visibly differentiate these critical "logic checks" from normal program logic.

There are two types of assertion:

  • __ASSERT_ALWAYS catches programming errors in all builds.

  • __ASSERT_DEBUG catches programming errors for debug builds only.

They each have two parameters: the test to perform (a statement, clause or function that returns a Boolean value) and the code to call if that test fails ”it is common to Panic the application, providing distinct panic numbers for each assertion made, so it is easy to locate the origin.

Assertions are most often used in debug builds ”here their main role is to speed up development by facilitating early detection of errors. But, as shown by the existence of __ASSERT_ALWAYS , they are sometimes used in release builds as well. In release builds, assertions generally have a very specific role: to validate parameters that are passed into public APIs. They are used in this way in order to clearly define the range of parameters with which the API was designed to operate , and to offer a firm refusal should the inputs fall outside of clearly documented boundaries.

Side Effects of Assertions

Sometimes, inconsistent behavior arises between release and debug builds. If this situation occurs, there may be an assertion side effect in your testing code.

When resolving such discrepancies, you should first look out for any code in an #ifdef _DEBUG preprocessor directive. Such code will be included only in the debug version, and therefore it is an obvious place to look when locating differences. For exactly the same reason, another likely source of behavioral differences is __ ASSERT_DEBUG statements. Like conditional code specified by an #ifdef _DEBUG condition, debug assertions run only in the debug version. One common mistake that creeps into assertions is accidentally using an assignment operator in place of an equality operator in the assertion test:

 __ASSERT_DEBUG((aValue = 5), User::Panic(KMyPanicDescriptor)); 

The above code demonstrates the error. Instead of comparing aValue to 5 , the above code sets it to 5 . What is even more confusing is that the assignment operator returns "true," and so the assertion will always succeed.

Class Invariants

In brief, an invariant is a definition of an object's state that remains true throughout the object's lifetime ”typically, an object specifies allowable values and/or ranges for some or all of its member variables. You test an object's integrity by checking its state against its invariance definition at the beginning and end of every member function in a nontrivial class.

Note that invariants are tested only in debug builds , but it is important that the testing function is defined for all builds to avoid any binary compatibility issues. For efficiency, all test code can be compiled out of release builds using preprocessor directives, as it will never actually be called.

The macro __DECLARE_TEST should be inserted as the last item in the class declaration. (This is because it switches back to public access as part of the macro definition ”adding it as the last item avoids the possibility of changing the accessibility of other methods or attributes.)

This macro declares the class method void __DbgTestInvariant() const , and this method should be defined to check the object's state. If an illegal state is found, then it should call User::Invariant() , which will cause a Panic. Panics raised in this way are more difficult to track down than the numerically distinct panics raised by assertions. One way to proceed, however, is to put a breakpoint in the __DbgTestInvariant() function itself, just prior to panicking. This makes it possible to trace the stack to the function that first violated the object's invariance.

When testing invariance, if a base class defines a __DbgTestInvariant() function, then that should be called before any further checking. Note that a __DbgTestInvariant() function should not make any calls to other class functions that test invariance, which could potentially result in an infinite recursive loop.

The macro __TEST_INVARIANT is used to call __DbgTestInvariant() in debug builds. This should be called at the start of each function, and also at the end of each non-const function. Note that a function may have multiple returns (in other words, multiple endings), in which case invariance should be tested at every return point. Static functions cannot test invariance!

As a simple example, consider the code below ”it can be assumed that __DECLARE_TEST is the last item in the declaration of class TEgInvariant :

 void TEgInvariant::DoSomeThing()    {    // Calls __DbgTestInvariant() in debug builds.    __TEST_INVARIANT;    // Do something with iData    // ...    // Calls __DbgTestInvariant() in debug builds.    __TEST_INVARIANT;    } 

The __TEST_INVARIANT macro invokes a call to __DbgTestInvariant() , and this function tests that the value of iData remains within its upper bound:

 void TEgInvariant::__DbgTestInvariant() const    { #if defined(_DEBUG)    if (iData > 100) // Test iData is valid.       {       User::Invariant();       } #endif    } 

There is a difference between conceptual and bitwise const-ness . The bitwise representation of an object may not change in a const function, but an object that is owned by the class through a pointer may still be changed. If this is the case, invariance should also be tested at the end of the function.


Heap Testing

Series 60 provides macros to test heap allocation. Macros are also provided to simulate out-of-memory (OOM) conditions in order to test cleanup code, and how an application responds if system memory is exhausted. These macros are defined only for debug builds and so can be left in deliverable code. In fact, the application framework for GUI applications provides heap checking as standard, and this will detect any memory leaks when the application is closed.

An example of how to test for OOM is given in the Out-Of-Memory Testing section, later in this chapter.

Memory leaks often occur when applications exit, as a result of incomplete cleanup code. Therefore it is important to test code to destruction. You can accomplish this by using the Back or Exit command to terminate your application, rather than just closing the emulator.


It is most common to test the User heap, as this is where dynamic memory is generally allocated for the current thread. However, it is also possible to test the Kernel heap, or a specific heap (for example when using multiple threads, with multiple heaps), and details can be found in the SDK documentation under Memory Allocation . The main heap macros are listed in Table 13-1.

If you use the C standard library, then you have to call CloseSTDLib() from \epoc32\include\libc\sys\reent.h before the terminating __UHEAP_MARKEND macro of the application is called and after the C standard library's DLL is no longer needed. The reason is that the C standard library stores reentrant information in the Thread Local Storage (TLS), which is not automatically cleaned up.


Heap testing is covered further in "Resource Failure Methods" later in this chapter.



Developing Series 60 Applications. A Guide for Symbian OS C++ Developers
Developing Series 60 Applications: A Guide for Symbian OS C++ Developers: A Guide for Symbian OS C++ Developers
ISBN: 0321227220
EAN: 2147483647
Year: 2003
Pages: 139

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net