60
Debugging and Testing (Chapter 8)

Debugging and Testing

  • Upload
    evelyn

  • View
    54

  • Download
    0

Embed Size (px)

DESCRIPTION

(Chapter 8). Debugging and Testing. Testing. Testing is the process of running a program with the intent of finding bugs Requires running the program on sample data Might require writing extra code to emulate missing parts of the system Requires the programmer to be "egoless" - PowerPoint PPT Presentation

Citation preview

Page 1: Debugging and Testing

Debugging and Testing

(Chapter 8)

Page 2: Debugging and Testing

Testing

● Testing is the process of running a program with the intent of finding bugs

● Requires running the program on sample data● Might require writing extra code to emulate

missing parts of the system● Requires the programmer to be "egoless"● Often performed by programmers other than the

developers

Page 3: Debugging and Testing

Types of Testing

● Static: Done without actually executing program– Code inspections– Walkthroughs

● Dynamic: Done by executing program or its parts– Module or unit testing– Integration testing– System testing

Page 4: Debugging and Testing

Module or Unit Testing

● Tests individual classes or small sets of classes● Generally done by the programmer responsible

for the classes● Bottom-up process● May require the use of a throw-away test driver

program

Page 5: Debugging and Testing

Test Driver for FarmerStateInfoint main() {

Side farmer = EAST; Side wolf = EAST; Side goat = WEST; Side cabbage = WEST;

FarmerState s = new FarmerStateInfo(farmer, wolf, goat, cabbage); s->display();

cout << "Testing safety:" << endl; if ( s->isSafe() ) cout << "Safety test wrong" << endl; else cout << "Safety test OK" << endl;

farmer = WEST; s = new FarmerStateInfo(farmer, wolf, goat, cabbage); s->display();

cout << "Testing safety:" << endl; if ( s->isSafe() ) cout << "Safety test OK" << endl; else cout << "Safety test wrong" << endl;}

Page 6: Debugging and Testing

Test Driver Output

67% farmer ||F ||WG|| C||

Testing safety:Safety test OKF|| ||WG|| C||

Testing safety:Safety test OK68%

Page 7: Debugging and Testing

Notes on the Output

● What gets tested:– The constructor– The display() method– Safety of the cabbage, both when it's vulnerable and

when it's not– The safety of the goat when it's not vulnerable

● What does not get tested:– The safety of the goat when it is vulnerable

● Note: the isSafe() method had to be made temporarily public

Page 8: Debugging and Testing

Testing State-Changing Methodsint main() {

FarmerState s = new FarmerStateInfo(WEST, WEST, WEST, WEST); s->display();

State newst = FarmerStateInfo::self(s); cout << "Action farmer-takes-self " << endl; if ( newst != NULL ) newst->display(); else cout << "not allowed" << endl << endl;

newst = FarmerStateInfo::wolf(s); cout << "Action farmer-takes-wolf " << endl; if ( newst != NULL ) newst->display(); else cout << "not allowed" << endl << endl; . . .

newst = FarmerStateInfo::cabbage(s); cout << "Action farmer-takes-cabbage " << endl; if ( newst != NULL ) newst->display(); else cout << "not allowed" << endl << endl;}

Page 9: Debugging and Testing

State-Changing Test Output

69% farmerF|| W|| G|| C||

Action farmer-takes-self not allowed

Action farmer-takes-wolf not allowed

Action farmer-takes-goat ||FW|| ||GC||

Action farmer-takes-cabbage not allowed

70%

Page 10: Debugging and Testing

Testing FarmerProblemInfo and State Equality

int main() {

Problem p = new FarmerProblemInfo(); State start = p->getStartState(); State final = p->getFinalState();

cout << "Testing state display:" << endl;

start->display(); final->display();

cout << "Testing state equality:" << endl;

if ( start->equals(final) ) cout << "Equality check wrong" << endl; else cout << "Equality check OK" << endl;

if ( start->equals(start) ) cout << "Equality check OK" << endl; else cout << "Equality check wrong" << endl;

}

Page 11: Debugging and Testing

Equality Test Output71% farmerTesting state display:F|| W|| G|| C||

||F ||W ||G ||C

Testing state equality:Equality check OKEquality check OK72%

Note that the test driver could be used for otherproblems just by changing the constructor call.

Page 12: Debugging and Testing

Testing expand()

int main() {

Problem p = new FarmerProblemInfo(); State start = p->getStartState(); start->display();

Item testItem = new ItemInfo(start, NULL, NULL, 0, 0); ItemArray children = testItem->expand(p); cout << "Num children: " << testItem->getNumChildren() << endl;

for (Integer i = 0; i < testItem->getNumChildren(); i++) { State nextState = children[i]->getState(); nextState->display(); }

}

Page 13: Debugging and Testing

expand() Test Output

73% farmerF|| W|| G|| C||

Num children: 1 ||FW|| ||GC||

74%

Page 14: Debugging and Testing

Integration Testing

● Putting together the results of module testing● Top-down approach● Example: testing the SolverInfo class tests

the integration of ProblemInfo and QueueInfo

● Often includes a primitive graphical user interface (GUI) so that:– system has look and feel of final product– separate drivers not needed

● Might require stubs or dummy routines for code to be added later

Page 15: Debugging and Testing

System Testing● Attempts to find problems in a complete program● Done when

– program is first completed, and– as the program evolves (maintenance testing)

● In industry, often done by an independent group of programmers

● Requires creation of a test suite that:– is used over and over as program evolves– attempts to identify all combinations of inputs and

actions that could cause program to fail

Page 16: Debugging and Testing

Testing and Proof of Correctness

● It is not possible to test a nontrivial system with all possible inputs and outputs

● So testing cannot formally prove that a system is correct, only give some level of confidence in it

● Formal proofs of dynamic program correctness are not possible either, because executing programs are subject to physical influences

● Formal proofs of abstract algorithm correctness are possible though difficult, and require machine assistance

Page 17: Debugging and Testing

Debugging

● The process of fixing problems (bugs) that testing discovers

● Debugging has two parts:– finding the cause of a bug (can be 95% of debugging)– fixing the bug

● A good programmer becomes adept at using a run-time debugger

Page 18: Debugging and Testing

Types of Program Bugs

● Syntactic bugs not caught by the compiler

Example:for (... ; ... ; ...);{

...

}● Design bugs: easy to find, can be difficult to fix● Logic bugs: can be difficult to find, easy to fix

Page 19: Debugging and Testing

Types of Bugs (cont'd)

● Interface bugs: – When assumptions made by a method caller are

different than those of the callee. – Can be difficult to find, easy to fix

● Memory bugs:– Freeing or overwriting storage that is later used– Especially difficult to find– Example:

IntArray A; // = new Integer[4]; ...A[0] = 10;A[1] = 20: ...

Page 20: Debugging and Testing

Fixing Bugs● Fixing might entail correcting a single line of

code● Fixing might entail redesign, recoding, and

further testing:– For example, incorrect circularity check in expand()

● Fixing a bug might cause other bugs to appear● It has been observed that code in which a bug has

been found is more likely to still contain bugs

Page 21: Debugging and Testing

Defensive Programming

● The easiest way to do debugging is to not have bugs

● Failing that, one should strive for locality:

Ensure that bugs that are identified are local problems found where and when they occur

● Locality is the goal of defensive programming● Defensive programming begins with defensive

design

Page 22: Debugging and Testing

Defensive Design

● First goal: Simplicity of Design

Example: Take the time and trouble to make a repeated piece of code a method, even if small

● Also: Simplicity of Class Interfaces

Example: refactoring the abstract StateInfo and ProblemInfo classes

● Another goal: Program for Errors– Anticipate exceptions even when they are not

expected– Example: add(Item) and remove() should check

for fullness and emptiness even though currently all callers check first

Page 23: Debugging and Testing

Program Robustness

void FrontQueueInfo::add(Item item) { front++; items[front] = item; numItemsAdded++;}

Consider this add method for FrontQueueInfo:

If add is called and the queue is full, a probable segmentation fault will occur.

As written, this code is brittle.

A robust program will not crash and burn whensomething unexpected happens.

Page 24: Debugging and Testing

Relying On the Caller

FrontQueue q = new FrontQueue(n); . . .if ( !q->full() ) {

q->add(item);}else {

<deal with full queue>}

However, as the program evolves, it is possible thata call will be added that is not this protective.

One approach: try to ensure that every call protectsitself:

Page 25: Debugging and Testing

Defensive Coding Using assert

add itself could have some kind of defense againstan incorrect call:

void FrontQueueInfo::add(Item item) {assert( !full() );front++;items[front] = item;numItemsAdded++;

}

If the queue is full, this will result in a helpfulmessage and a program halt.

Although this makes it easier to debug, the programis still brittle.

Page 26: Debugging and Testing

Defensive Coding Through Exception Handling

An exception is an unexpected error condition:

1)Array index out of bounds

2)Divide by zero

3)Heap memory exhausted

4)Keyboard interrupt signal

5)Abort signal

6)Conditions specific to application

Page 27: Debugging and Testing

C++ Approaches to Exception Handling

1 Try to guarantee that an exception condition does not exist through an assertion, and generate an unrecoverable error if the assertion is false.

2 Deal with an exception condition through the use of a handler function that is called when a system-defined exception signal is raised.

3 Deal with general exception conditions by unwinding the stack with catches and throws.

Page 28: Debugging and Testing

Defensive Coding Example: Safe Arrays

expected output: 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

actual output:

0 1 2 3 4 5 6 7 0 1 0 1 2 3 4 5 6 7 8 9

problem: accessing array elements that are out of bounds

typedef int Integer;typedef Integer * IntegerArray;

main () { IntegerArray a = new Integer[5]; IntegerArray b = new Integer[5]; for (Integer i = 0; i < 10; i++) a[i] = i; for (Integer i = 0; i < 10; i++) b[i] = i; for (Integer i = 0; i < 10; i++) cout << a[i] << " "; cout << endl; for (Integer i = 0; i < 10; i++) cout << b[i] << " "; cout << endl;}

Page 29: Debugging and Testing

A Safe Array VectorInfo Class#include <assert.h>

typedef int Integer;typedef Integer * IntegerArray;typedef class VectorInfo * Vector;

class VectorInfo {private: IntegerArray p; Integer size;public: VectorInfo(); VectorInfo(Integer n); ~VectorInfo(); Integer& element(Integer i);};

Page 30: Debugging and Testing

Notes On VectorInfo

● One class attribute is a dynamic array● One class attribute is the maximum array size● One constructor will implement a default array

size● The element method will check for index out

of bounds● assert.h is included

Page 31: Debugging and Testing

Exception Handling Using assert

VectorInfo::VectorInfo() { size = 10; p = new Integer[size];}

VectorInfo::VectorInfo(int n) { assert ( n >= 1 ); size = n; p = new Integer[size]; assert (p != 0); // in case heap used up}

Integer& VectorInfo::element(Integer i) { assert (i >= 0 && i < size); // line 40 return (p[i]);}

Page 32: Debugging and Testing

Main Programmain () { Vector a = new VectorInfo(5); Vector b = new VectorInfo(5); for (int i = 0; i < 10; i++) a->element(i) = i; for (int i = 0; i < 10; i++) b->element(i) = i; for (int i = 0; i < 10; i++) cout << a->element(i) << " "; cout << endl; for (int i = 0; i < 10; i++) cout << b->element(i) << " "; cout << endl;}

Page 33: Debugging and Testing

Main Program Output

VectorInfo2.cc:40: failed assertion `i >= 0 && i < size'Abort

Although this code aborts, it is preferable becauseit gives the programmer useful information.

From the point of view of the user, it is still brittle.

Page 34: Debugging and Testing

Exception Handling with signal.h● The signal.h file provides a standard mechanism for

handling system-defined exceptions. Some:#define SIGHUP 1 /* hangup */

#define SIGINT 2 /* interrupt */

#define SIGILL 4 /* illegal instruction */

#define SIGFPE 8 /* floating point exception */

#define SIGBUS 10 /* bus error */

#define SIGSEGV 11 /* segmentation violation */

A programmer can arrange to have a handler function called when an exception arises using:

signal(signal, handler);

Page 35: Debugging and Testing

Signal Handler Function● Returns void and takes an integer (signal number) as

argument:– void handler(Integer signal);

● If part of a class, must be static ● Installed by associating it with a signal:

– void signal(Integer signal, void (*handler) (Integer))

● Automatically invoked when an exception of the associate signal type occurs

● Can be intentionally invoked by explicitly raising the associated signal:– void raise(Integer signal);

Page 36: Debugging and Testing

Signal Example: Keyboard InterruptMain Program

#include <signal.h> ...void cntrl_c_handler(int sig); // handler prototype

main() { int i = 0, j; cout << "COUNT TO J MILLION, Enter j: "; cin >> j; j *= 1000000;

signal(SIGINT, cntrl_c_handler); // set interrupt ``trap''

while (i < j) // interrupt with ctl-C during this loop ++i;

cout << " HIT " << j/1000000 << " MILLION" << endl; }

Page 37: Debugging and Testing

Keyboard Interrupt Handler

void cntrl_c_handler(int sig){ char c;

cout << "KEYBOARD INTERRUPT"; cout << "\ntype y to continue: "; cin >> c; if (c != 'y') exit(0); signal(SIGINT, cntrl_c_handler); // reset "trap" // and return}

Page 38: Debugging and Testing

Signal Example Output

6% testCOUNT TO J MILLION, Enter j: 100

[control-C from keyboard]

KEYBOARD INTERRUPTtype y to continue: y

[control-C from keyboard]

KEYBOARD INTERRUPTtype y to continue: yHIT 100 MILLION7%

Page 39: Debugging and Testing

Raising An Exception Under Program Control

#include <signal.h> ...void cntrl_c_handler(int sig); // handler prototype

main() { int i = 0, j; cout << "COUNT TO J MILLION, Enter j: "; cin >> j; j *= 1000000;

signal(SIGINT, cntrl_c_handler); // set interrupt ``trap''

while (i < j) // interrupt with ctl-C during this loop ++i;

if (i % 1000000 == 0) // generate interrupt after raise(SIGINT); // each million

cout << " HIT " << j/1000000 << " MILLION" << endl; }

Page 40: Debugging and Testing

Signal Example Output

9% testCOUNT TO J MILLION, Enter j: 4KEYBOARD INTERRUPTtype y to continue: yKEYBOARD INTERRUPTtype y to continue: yKEYBOARD INTERRUPTtype y to continue: yKEYBOARD INTERRUPTtype y to continue: n530%

Note that the program does not continue to completion.

Page 41: Debugging and Testing

User-Defined Signals

. . .#define SIGUSR1 16 /* user defined signal 1 */#define SIGUSR2 17 /* user defined signal 2 */ . . .

Two signal codes are designed to be raised by programmers in application-specific circumstances:

Page 42: Debugging and Testing

User-Defined Signal Example

Recall the safe array constructor:

If the heap is exhausted (p==0), this code will abort.

Suppose the programmer can do some garbagecollection to reclaim parts of the heap.

VectorInfo::VectorInfo(int n) { assert ( n >= 1 ); size = n; p = new Integer[size]; assert (p != 0); // in case heap used up}

Page 43: Debugging and Testing

User-Defined Signal Example (cont'd)#define SIGHEAP SIGUSR1

#include <assert.h>

typedef int Integer;typedef Integer * IntegerArray;typedef class VectorInfo * Vector;

class VectorInfo {private: IntegerArray p; Integer size;public:

VectorInfo();VectorInfo(Integer n);~VectorInfo();Integer& element(Integer i);static void vectorHeapHandler(Integer

sig); };

Page 44: Debugging and Testing

User-Defined Signal Example (cont'd)

VectorInfo::VectorInfo(int n) { assert ( n >= 1 ); size = n; p = new Integer[size]; if (p == 0) { // invoke handler to do

raise(SIGHEAP); // garbage collection p = new int[size]; // try again to create

array }}

main () {signal(SIGHEAP, VectorInfo::vectorHeapHandler);vect a(5);vect b(5); . . .

}

void VectorInfo::vectorHeapHandler(int sig) { // possible action to reclaim heap storage}

Page 45: Debugging and Testing

Limitations of Signal Handling for C++

It would be nice to handle the other VectorInfo exceptions in this way, for example:

#define SIGHEAP SIGUSR1#define SIGSIZE SIGUSR2

class VectorInfo {private: IntegerArray p; Integer size;public:

VectorInfo();VectorInfo(Integer n);~VectorInfo();Integer& element(Integer i);static void vectorHeapHandler(Integer

sig);static void vectorSizeHandler(Integer

sig); };

Page 46: Debugging and Testing

Limitations of Signal Handling for C++ (cont'd)

VectorInfo::VectorInfo(int n) { if ( n < 1 ) {

raise(SIGSIZE); // bad vector size signal}

size = n; p = new Integer[size]; if (p == 0) {

raise(SIGHEAP); p = new int[size];

}}

Page 47: Debugging and Testing

Limitations (cont'd)

main () {signal(SIGHEAP,

VectorInfo::vectorHeapHandler);signal(SIGSIZE,

VectorInfo::vectorSizeHandler);vect a(5);vect b(5); . . .

}

void VectorInfo::vectorSizeHandler(int sig) { cout << "Size error. Going with default." size = 10; p = new int[size];}Unfortunately, this will not work since vectorSizeHandler must be static. It therefore has no access to size and p.

Page 48: Debugging and Testing

Exception Handling with catch/throw

● A more sophisticated and general form of exception handling

● Not intended to handle asynchronous exceptions defined in signal.h

● Works by causing dynamic, non-local exit from runtime stack frames ("unwinding" the stack)

Page 49: Debugging and Testing

Normal Runtime Stack Handling

main() {. . .foo();. . .

}

void foo() {. . .bar();. . .

}

void bar() {. . .wowie();. . .

}

void wowie() {. . .zowie();. . .

}

void zowie() {. . .. . .

}

call

return

call

return

call

return

call

return

Page 50: Debugging and Testing

Normal Runtime Stack Handling (cont'd)

In zowie, the runtime stack looks like:

zowiewowie

barfoo

main

Each normal return causes the stack to be popped:

zowiewowie

barfoo

main

wowiebarfoo

main

barfoo

mainfoo

main main

Page 51: Debugging and Testing

Non-Local Exits Using catch/throw

● Non-local exits from stack frames are caused by throwing an exception to a handler (a catch)

● The throw and the catch can be widely separated on the stack

● All intervening stack frames are "unwound" (removed from stack along with all automatic values)

● Control does not pass to the code after a call● Control ends up at the catch, which should take

appropriate action

Page 52: Debugging and Testing

Non-Local Exits Using catch/throw (cont'd)

zowiewowie

barfoo

main main

main () { try {

foo(); } catch(int n) { . . . }}

. . .

void zowie () { int i; . . . throw i; . . .}

● The try block establishes the context for any throws done within its dynamic extent

● The try block is immediately followed by handlers which catch a thrown exception

● There can be more than one handler which differ according to parameter type signature

code stack

Page 53: Debugging and Testing

Using catch/throw in VectorInfo

VectorInfo::VectorInfo(Integer n){ if ( n < 1 ) throw (n); size = n; p = new Integer[size]; if ( p == 0 ) throw ("Free Store Exhausted");}

Integer& VectorInfo::element(Integer i){ if (i < 0 || i >= size) throw ("Vector Index Out Of Bounds"); return (p[i]);}

Page 54: Debugging and Testing

VectorInfo Main Program

void process(Integer m) { try { Vector a = new VectorInfo(m); Vector b = new VectorInfo(m); for (int i = 0; i < 10; i++) a->element(i) = i; for (int i = 0; i < 10; i++) b->element(i) = i; for (int i = 0; i < 10; i++) cout << a->element(i) << " "; cout << endl; for (int i = 0; i < 10; i++) cout << b->element(i) << " "; cout << endl; } ...

main (Integer argc, StringArray argv) { Integer n = atoi(argv[1]); process(n);}

Page 55: Debugging and Testing

VectorInfo Main Program (cont'd)

. . . catch (Integer n) { cerr << "Size error. Going with default." << endl; process(10); } catch (ConstString message) { cerr << message << endl; exit(0); }} 7% test 10

0 1 2 3 4 5 6 7 8 90 1 2 3 4 5 6 7 8 9

8% test 0Size error. Going with default.0 1 2 3 4 5 6 7 8 90 1 2 3 4 5 6 7 8 9

9% test 5Vector Index Out Of Bounds

Output:

Page 56: Debugging and Testing

catch/throw in the Queue Command Interpreter

void CommandInterpreterInfo::execute() { cin >> cmd; try { while ( !cin.eof() ) { count++; process(); cin >> cmd; } } catch (ConstString message) { cout << message << endl; } cout << count << " commands processed." << endl; free();}

Page 57: Debugging and Testing

Command Interpreter (cont'd)

void CommandInterpreterInfo::process() { if (cmd == '#') { processComment(); count--; } else if (count == 1) { processCreate(); } else { processOther(); }}

Page 58: Debugging and Testing

Command Interpreter (cont'd)

void CommandInterpreterInfo::processCreate() { if (cmd != 'c') throw ("First command must be a create..."); cin >> qtype; checkEOF(); if ( (qtype == 'f') || (qtype == 'r') || (qtype == 'p') ) { if (qtype == 'p') { cin >> pqtype; checkEOF(); if ( !(pqtype == '>' ) && !(pqtype == '<' ) ) { throw ("Priority queue type must be > or <."); } } cin >> qsize; checkEOF(); switch ( qtype ) { case 'f': q = new FrontQueueInfo(qsize); ... case 'r': q = new RearQueueInfo(qsize+1); ... case 'p': switch ( pqtype ) { case '>': q = new MaxPriorityQueueInfo(qsize); ... case '<': q = new MinPriorityQueueInfo(qsize); ... } } } else throw ("Queue type must be f, r, or p.");}

Page 59: Debugging and Testing

Command Interpreter (cont'd)

void CommandInterpreterInfo::checkEOF() { if ( cin.eof() ) { throw ("Unexpected end of file"); }}

execute

process

processCreate

checkEOF

RuntimeStack:

try/catch here

throw from here

throw from here

Page 60: Debugging and Testing

Philosophy of Error Recovery Using Exception Handling

● Exception handling is about error recovery and secondarily about transfer of control

● Undisciplined transfer of control leads to chaos (like the days before structured programming)

● In most cases programming that raises exceptions should print a diagnostic message and gracefully terminate

● Heroic attempts at repair are legitimate in real-time processing and fault-tolerant computing