1 10-DebuggingTesting


Previous: 09-ModulesPackages.html

**Debugging is twice as hard as writing the code in the first place. **
**Therefore, if you write the code as cleverly as possible, you are, **
**by definition, not smart enough to debug it.**
    - Brian W. Kernighan

1.1 Training and expertise

1.2 Screencasts

1.3 Reading

1.4 Troubleshooting in general

https://en.wikipedia.org/wiki/Troubleshooting
* A basic principle in troubleshooting is to start from the simplest and most probable possible problems first.
* This is illustrated by the old saying “When you see hoof prints, look for horses, not zebras”, or to use another maxim, use the KISS principle
* https://en.wikipedia.org/wiki/KISS_principle
* This principle results in the common complaint about help desks or manuals, that they sometimes first ask:
* “Is it plugged in, and does that receptacle have power?”
* Always check the simple things first, before calling for help.
* A troubleshooter could check each component in a system one by one, substituting known good components for each potentially suspect one.
* However, this process of “serial substitution” can be considered degenerate when components are substituted without regard to a hypothesis concerning how their failure could result in the symptoms being diagnosed.
* Randomly changing your code is even worse!
* Simple and intermediate systems can be characterized by lists or trees of dependencies among their components or subsystems.
* More complex systems can contain cyclical dependencies or interactions (feedback loops), and require graph models.
* Such systems are less amenable to “bisection” troubleshooting techniques.

1.4.1 Half-splitting / binary search / bisection

1.4.2 Hypothesis testing

The general scientific method of abduction and hypothesis testing can be used to find bugs!
https://en.wikipedia.org/wiki/Abductive_reasoning
https://en.wikipedia.org/wiki/Scientific_method
https://en.wikipedia.org/wiki/Experiment

1.5 General programming errors (software bugs)

https://en.wikipedia.org/wiki/Software_bug
A software bug is an error, flaw, failure, or fault in a computer program or system that causes it to produce an incorrect or unexpected result, or to behave in unintended ways, often but not always crashing.

1.5.1 General types of errors

Programming errors come in several forms, and vary by language type:
https://en.wikipedia.org/wiki/Software_bug#Types

1.5.1.1 Arithmetic

1.5.1.2 Compile-time / syntax errors (won’t run at all)

1.5.1.3 Run-time errors (crashes)

1.5.1.4 Logic errors and semantics (strange outputs)

1.5.1.5 Resource errors / Environment issues

In some compiled languages (C, C++, assembly/asm) attempting to do low-level stuff:
* Null pointer de-reference.
* Using an uninitialized variable.
* Using an otherwise valid instruction on the wrong data type (see packed decimal/binary coded decimal).
* Access violations.
* Resource leaks, where a finite system resource (such as memory or file handles) become exhausted by repeated allocation without release.
* Buffer overflow, in which a program tries to store data past the end of allocated storage. This may or may not lead to an access violation or storage violation.
* These are known as security bugs.
* Excessive recursion which, though logically valid, causes stack overflow.
* Use-after-free error, where a pointer is used after the system has freed the memory it references.
* Double free error (double delete a memory allocation).
* These can be hard to find, but don’t have to be!
* Languages like https://www.rust-lang.org/ actually pre-check much of this, functionally reducing or eliminating such issues, in addition to pre-checking all the features normally checked by a compiled language, producing a security-safe language!

1.5.2 Practice on error types

++++++++++++++++++
Cahoot-10.1
What kind of error is this?
https://mst.instructure.com/courses/58101/quizzes/56033

10-DebuggingTesting/debug_error_00.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

def division(numerator: float, denominator: float) -> float:
    answer = numerator / denominator
    return answer

print(division(37, 0))

++++++++++++++++++
Cahoot-10.2
What kind of error is this?
https://mst.instructure.com/courses/58101/quizzes/56034

10-DebuggingTesting/debug_error_01.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

def division(numerator: float, denominator: float) -> float:
    answer = (numerator / denominator
    return answer

print(division(37, 2))

++++++++++++++++++
Cahoot-10.3
What kind of error is this?
https://mst.instructure.com/courses/58101/quizzes/56035

10-DebuggingTesting/debug_error_02.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

def division(numerator: float, denominator: float) -> float:
    answer = numerator * denominator
    return answer

print(division(37, 2))

++++++++++++++++++
Cahoot-10.4
What kind of error is this?
https://mst.instructure.com/courses/58101/quizzes/56036

10-DebuggingTesting/debug_error_03.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

from typing import List

small_array: List[int] = [0, 1, 2]
small_array[3] = 3

Show this one: $ ipython3 debug_error_03.py

1.6 Python error types

Python itself has error types (called exceptions)
https://docs.python.org/3/library/exceptions.html
10-DebuggingTesting/exception_hierarchy.png
10-DebuggingTesting/debug_exceptions.py (in spyder3)

#!/usr/bin/python3
# -*- coding: utf-8 -*-

while True print('Hello world'):

10 * (1 / 0)

'2' + 2

Print('hello world')

print 'hello world'

print(hello world)

hello world

d = {1: 1, 2: 2}
d[3]

li = [1, 2, 3]
li[4]

f = open('nonexistantfile.txt', 'r')

# Note: check BEFORE the line python whines about:
if 1 == 1:
    print('hey'
    print('another')

The full tree of python3’s built-in exceptions is:

BaseException
+– SystemExit
+– KeyboardInterrupt
+– GeneratorExit
+– Exception
+– StopIteration
+– StopAsyncIteration
+– ArithmeticError
| +– FloatingPointError
| +– OverflowError
| +– ZeroDivisionError
+– AssertionError
+– AttributeError
+– BufferError
+– EOFError
+– ImportError
| +– ModuleNotFoundError
+– LookupError
| +– IndexError
| +– KeyError
+– MemoryError
+– NameError
| +– UnboundLocalError
+– OSError
| +– BlockingIOError
| +– ChildProcessError
| +– ConnectionError
| | +– BrokenPipeError
| | +– ConnectionAbortedError
| | +– ConnectionRefusedError
| | +– ConnectionResetError
| +– FileExistsError
| +– FileNotFoundError
| +– InterruptedError
| +– IsADirectoryError
| +– NotADirectoryError
| +– PermissionError
| +– ProcessLookupError
| +– TimeoutError
+– ReferenceError
+– RuntimeError
| +– NotImplementedError
| +– RecursionError
+– SyntaxError
| +– IndentationError
| +– TabError
+– SystemError
+– TypeError
+– ValueError
| +– UnicodeError
| +– UnicodeDecodeError
| +– UnicodeEncodeError
| +– UnicodeTranslateError
+– Warning
+– DeprecationWarning
+– PendingDeprecationWarning
+– RuntimeWarning
+– SyntaxWarning
+– UserWarning
+– FutureWarning
+– ImportWarning
+– UnicodeWarning
+– BytesWarning
+– ResourceWarning

Tips for reading exceptions and finding little errors:
https://realpython.com/python-traceback/ (how to read basic errors!)
* Read them from the bottom up!
* Read them entirely!!
* Check it out in spyder3 (which uses the ipython3 terminal)
* Run in ipython3 instead of python3
* Syntax errors are often auto-pinpointed just AFTER they actually occur, so look before it tells you.
* Use auto-highlighting in your IDE, select ([{ by clicking at the end of the line!

1.7 Debugging

https://en.wikipedia.org/wiki/Debugging

1.7.0.1 Debugging as diagnosis

1.7.1 Programming process

1.7.2 Don’t randomly change your code

1.7.3 Hypothesis testing

While(bug)
    Ask, what is the simplest input that produces the bug?
    Identify assumptions that you made about program operation that could be false.
    Ask yourself "How does the outcome of this test/change guide me toward finding the problem?"

1.7.4 General principles

1.7.4.1 Basics

1.7.4.2 Debugging patterns

https://en.wikipedia.org/wiki/Debugging_patterns

Eliminate Noise Bug Pattern
* Isolate and expose a particular bug by eliminating all other noise in the system.
* This enables you to concentrate on finding the real issue.

Recurring Bug Pattern
* Expose a bug via a unit test.
* Run that unit test as part of a standard build from that moment on.
* This ensure that the bug will not recur.

Time Specific Bug Pattern
* Expose the bug by writing a continuous test that runs continuously and fails when an expected error occurs.
* This is useful for transient bugs.

1.7.5 Techniques

1.7.6 Software debuggers

Better known as “tracing” code.

1.7.6.1 Simple step-through software!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

03-IntroPython.html illustrates several good ones for python!

Note: Actually trace the code I give you in these!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

1.7.6.2 Active automated bug finding software

1.8 Heisenbugs

https://en.wikipedia.org/wiki/Heisenbug
:)
When you get to languages like C++, the very act of printing to find a bug, may hide your bug!
Oh the uncertainty!

1.9 Software testing

https://en.wikipedia.org/wiki/Software_testing

There are many approaches available in software testing.
Reviews, walk-throughs, or inspections are referred to as static testing, pre-processing, or linting, whereas executing programmed code with a given set of test cases is referred to as dynamic testing.

1.9.1 Static, dynamic, passive, active

1.9.2 Unit, integration, and validation testing

  1. Unit testing tests little code chunks
  2. Integration testing tests bigger aggregate chunks
  3. Validation testing tests for fulfilling purpose, often by humans

1.9.2.1 1. Unit testing

https://en.wikipedia.org/wiki/Unit_testing
* Unit testing is a software testing method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use.
* Unit tests are typically automated tests written and run by software developers to ensure that a section of an application (known as the “unit”) meets its design and behaves as intended.
* The unit is typically a function and it’s return value or changes produced.
* To isolate issues that may arise, each test case should be tested independently.

1.9.2.1.1 Flaws
1.9.2.1.2 Frameworks

Different pre-built testing frameworks exist in many languages:
https://en.wikipedia.org/wiki/List_of_unit_testing_frameworks

For example, in Python:
https://en.wikipedia.org/wiki/List_of_unit_testing_frameworks#Python

One really neat one we will cover later this semester is:
https://en.wikipedia.org/wiki/Doctest

1.9.2.2 2. Integration testing

https://en.wikipedia.org/wiki/Integration_testing
* Integration testing (sometimes called integration and testing, abbreviated I&T) is the phase in software testing in which individual software modules are combined and tested as a group.
* Integration testing is conducted to evaluate the compliance of a system or component with specified functional requirements.
* It occurs after unit testing and before validation testing.

1.9.2.3 3. Validation testing

https://en.wikipedia.org/wiki/Software_verification_and_validation
In software project management, software testing, and software engineering, verification and validation (V&V) is the process of checking that a software system meets specifications and that it fulfills its intended purpose.
It may also be referred to as software quality control.

1.9.3 Development strategies

https://en.wikipedia.org/wiki/Test-driven_development (I suggest reading this one)

1.10 Examples of debugging

1.10.1 Example 1: sorting bug

10-DebuggingTesting/debug_example1.py
10-DebuggingTesting/debug_example1_cfg.svg

#!/usr/bin/python3
# -*- coding: utf-8 -*-

from typing import List

# line 1
swapped: int = 1
a: List[int] = [3, 2, 1]

while swapped == 1:  # line 2
    swapped = 0  # line 3
    for i in range(1, len(a)):  # line 4
        if a[i - 1] > a[i]:  # line 5
            a[i] = a[i - 1]  # line 6
            a[i - 1] = a[i]  # line 7
            swapped = 1  # line 8
            # line 9
        # line 10
    # line 11
# line 12

Control flow graph for the code
10-DebuggingTesting/pasted_image.png
The code contains a bug (i.e., it does not produce correct results for some inputs).
Given below are the results for 5 test cases including a code trace for each test case:
10-DebuggingTesting/pasted_image001.png
Via graph analysis, we determine the bug is in lines 6-9 of the code.
Namely, the code isn’t swapping a[i] and a[i-1] correctly.

10-DebuggingTesting/debug_example1_fix.py

1.10.2 Example 2: convert to binary bug

10-DebuggingTesting/debug_example2.py
10-DebuggingTesting/debug_example2_cfg.svg

#!/usr/bin/python3
# -*- coding: utf-8 -*-

# line 1
n: int = 10
previous_power: int = 0
m: int = 0

gap: int = 0
total: int = 0

while n > 0:  # line 2
    m = 0  # line 3

    if (n % 2) != 0:  # line 4
        total = 1  # line 5
    # line 6
    else:
        total = 2  # line 7
    # line 8
    while (total * 2) <= n:  # line 9
        total = total * 2  # line 10
        m += 1  # line 11
    # line 12

    for gap in range(1, previous_power - m):  # line 13
        print(0)  # line 14
    # line 15

    print(1)  # line 16
    n = n - total  # line 17
    previous_power = m  # line 18
# line 19

for trailingZero in range(m, 0, -1):  # line 20
    print(0)  # line 21
# line 22

print("\n")  # line 23
# line 24

Control flow graph for the code:
10-DebuggingTesting/pasted_image002.png

10-DebuggingTesting/debug_example2_fix.py

1.10.3 Example 3: Caesar cipher unit tests (no bug)

Here are some unit tests for the Caesar functions we wrote last time:
10-DebuggingTesting/debug_unit_tests.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

from typing import List
import random

caesar_encoding: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ "

def key_gen() -> int:
    """
    Generates one Caesar key
    """
    # Correct
    key = random.randint(1, 26)

    # Bug
    # key = random.randint(0, 27)
    return key

def key_gen_test() -> bool:
    # What else could we test here?
    # What about randomness?
    # Will this test always work?
    # Does it guarantee correctness?
    for c in range(300):
        if (key_gen() > 26) or (key_gen() < 1):
            print("key_gen_test() failed")
            return False
    return True

def str_to_num_arr(message: str) -> List[int]:
    """
    Translates a string into a Caesar-encoded List
    """
    arr: List[int] = []
    for character in message:
        arr.append(caesar_encoding.find(character.upper()))
    return arr

def str_to_num_arr_test() -> bool:
    # TODO
    return True

def num_arr_to_str(encoded_arr: List[int]) -> str:
    """
    Translates a Caesar encoded list back into a string
    """
    plaintext: List[str] = []
    for counter, encoded_char in enumerate(encoded_arr):
        plaintext.append(caesar_encoding[encoded_char])
    plaintext_string = "".join(plaintext)
    return plaintext_string

def num_arr_to_str_test() -> bool:
    # TODO
    return True

def translate(encoded_arr: List[int], mode: int, key: int) -> List[int]:
    """
    Encrypts or decryps a Caesar-encoded List of ints
    """
    translated: List[int] = []
    for encoded_char in encoded_arr:
        if mode == 1:
            # 27 is the symbol set size (# letters in alphabet)
            # Note the space added above (bug from last time)!
            translated.append((encoded_char + key) % 27)
        else:
            translated.append((encoded_char - key) % 27)
    return translated

def translate_test() -> bool:
    # TODO
    return True

def encrypt_test() -> bool:
    for character in caesar_encoding:
        for key in range(1, 26):
            # Encrypt
            message_arr = str_to_num_arr(character)
            message_arr = translate(message_arr, 1, key)
            message = num_arr_to_str(message_arr)

            # Decrypt
            message_arr = str_to_num_arr(message)
            message_arr = translate(message_arr, 0, key)
            message = num_arr_to_str(message_arr)

            # check
            if message != character:
                print("encrypt_test() failed")
                return False
    return True

def run_tests() -> bool:
    """
    Note: greedy quitting with booleans means that
    you should put the more aggregate/complicated tests last,
    and the simple tests first.
    """
    return (
        key_gen_test()
        and str_to_num_arr_test()
        and num_arr_to_str_test()
        and translate_test()
        and encrypt_test()
    )

# Only runs main if tests pass:
if _name_ == "_main_" and run_tests():
    message: str = input("\nEnter your message, in English:\n")
    gen_key: str = input("Want to generate a key? (y/n)")

    if gen_key == "y":
        ok: int = 0
        while ok == 0:
            key = key_gen()
            print("Is the key it ok with you (1-yes, 0-no, make another): ")
            ok = int(input())
    else:
        key = int(input("What is your key (0-25)?"))

    print("\nYour Caesar key is: ")
    print(key)
    print("\n Share this with your partner. Don't tell anyone else\n")

    print("\nEnter 1 for encryption, and 0 for decryption: ")
    mode: int = int(input())
    message_arr = str_to_num_arr(message)
    message_arr = translate(message_arr, mode, key)
    message = num_arr_to_str(message_arr)
    print(message)

Ask in class: How would we complete the # TODO tests above?

You should write tests like this for every single programming assignment!

1.10.4 Example 4: hangman game (no overt bug)

A child’s game from the below book:
https://en.wikipedia.org/wiki/Hangman_(game)
http://inventwithpython.com/invent4thed/chapter7.html
http://inventwithpython.com/invent4thed/chapter8.html
http://inventwithpython.com/invent4thed/chapter9.html

10-DebuggingTesting/hangman.py (no typing)
10-DebuggingTesting/hangman_cfg.svg

#!/usr/bin/python3
# -*- coding: utf-8 -*-

# TODO next time, I'll show you how to auto-type code you download,
# like this I downloaded from one of the free books in this class:

import random

HANGMAN_PICS = [
    """
  +---+
      |
      |
      |
     ===""",
    """
  +---+
  O   |
      |
      |
     ===""",
    """
  +---+
  O   |
  |   |
      |
     ===""",
    """
  +---+
  O   |
 /|   |
      |
     ===""",
    """
  +---+
  O   |
 /|\  |
      |
     ===""",
    """
  +---+
  O   |
 /|\  |
 /    |
     ===""",
    """
  +---+
  O   |
 /|\  |
 / \  |
     ===""",
]
words = "ant baboon badger bat bear beaver camel cat clam cobra cougar coyote crow deer dog donkey duck eagle ferret fox frog goat goose hawk lion lizard llama mole monkey moose mouse mule newt otter owl panda parrot pigeon python rabbit ram rat raven rhino salmon seal shark sheep skunk sloth snake spider stork swan tiger toad trout turkey turtle weasel whale wolf wombat zebra".split()

def getRandomWord(wordList):
    # This function returns a random string from the passed list of strings.
    wordIndex = random.randint(0, len(wordList) - 1)
    return wordList[wordIndex]

def displayBoard(missedLetters, correctLetters, secretWord):
    print(HANGMAN_PICS[len(missedLetters)])
    print()

    print("Missed letters:", end=" ")
    for letter in missedLetters:
        print(letter, end=" ")
    print()

    blanks = "_" * len(secretWord)

    # replace blanks with correctly guessed letters
    for i in range(len(secretWord)):
        if secretWord[i] in correctLetters:
            blanks = blanks[:i] + secretWord[i] + blanks[i + 1 :]

    # show the secret word with spaces in between each letter
    for letter in blanks:
        print(letter, end=" ")
    print()

def getGuess(alreadyGuessed):
    # Returns the letter the player entered.
    # This function makes sure the player entered a single letter,
    # and not something else.
    while True:
        print("Guess a letter.")
        guess = input()
        guess = guess.lower()
        if len(guess) != 1:
            print("Please enter a single letter.")
        elif guess in alreadyGuessed:
            print("You have already guessed that letter. Choose again.")
        elif guess not in "abcdefghijklmnopqrstuvwxyz":
            print("Please enter a LETTER.")
        else:
            return guess

def playAgain():
    # This function returns True if the player wants to play again;
    # otherwise, it returns False.
    print("Do you want to play again? (yes or no)")
    return input().lower().startswith("y")

def main():
    print("H A N G M A N")
    missedLetters = ""
    correctLetters = ""
    secretWord = getRandomWord(words)
    gameIsDone = False

    while True:
        displayBoard(missedLetters, correctLetters, secretWord)

        # Let the player enter a letter.
        guess = getGuess(missedLetters + correctLetters)

        if guess in secretWord:
            correctLetters = correctLetters + guess

            # Check if the player has won.
            foundAllLetters = True
            for i in range(len(secretWord)):
                if secretWord[i] not in correctLetters:
                    foundAllLetters = False
                    break
            if foundAllLetters:
                print('Yes! The secret word is "' + secretWord + '"! You have won!')
                gameIsDone = True
        else:
            missedLetters = missedLetters + guess

            # Check if player has guessed too many times and lost.
            if len(missedLetters) == len(HANGMAN_PICS) - 1:
                displayBoard(missedLetters, correctLetters, secretWord)
                print(
                    "You have run out of guesses!\nAfter "
                    + str(len(missedLetters))
                    + " missed guesses and "
                    + str(len(correctLetters))
                    + ' correct guesses, the word was "'
                    + secretWord
                    + '"'
                )
                gameIsDone = True

        # Ask the player if they want to play again (but only if the game is done).
        if gameIsDone:
            if playAgain():
                missedLetters = ""
                correctLetters = ""
                gameIsDone = False
                secretWord = getRandomWord(words)
            else:
                break

if _name_ == "_main_":
    main()

How would you design unit tests for the functions in this game?
How would you design integration tests for the main in this game?
How could you check to make sure there are no superficial type-conflicts (a type of bug)?

1.11 Conclusions

Remember:
10-DebuggingTesting/planning_coding.jpg
…that was sarcastic, and in case you didn’t notice, reverse it!

Next: 11-TypeHints.html