sensors_harvard-computer-mkii-bug-closeup-cropped.jpg
Log Book With Computer Bug from Mark II computer at Harvard University (1947). Smithsonian Collection.

Tests using the unittest.mock framework have a lot of test_ prefixes: methods are prefixed with test_, classes are prefixed with Test_ and they are stored in files with filenames prefixed with test_.

Finding the Bug

The unit test below shows a simple test of a TemperaturePlotSource object created in Kelvin mode and tested with values starting at 20 degrees Celsius which would equate to 293.15K.

Download: file
# an excerpt from Test_TemperaturePlotSource class
    SENSOR_DATA = (20, 21.3, 22.0, 0.0, -40, 85)

    def test_kelvin(self):
        """Create the source in Kelvin mode and test with some values."""
        # Emulate the clue's temperature sensor by
        # returning a temperature from a small tuple
        # of test data
        mocked_clue = Mock()
        expected_data = (293.15, 294.45, 295.15,
                         273.15, 233.15, 358.15)
        type(mocked_clue).temperature = PropertyMock(side_effect=self.SENSOR_DATA)

        source = TemperaturePlotSource(mocked_clue,
                                       mode="Kelvin")

        for expected_value in expected_data:
            data = source.data()
            # self.assertEqual(data,
            #                 expected_value,
            #                 msg="An inappropriate check for floating-point")
            self.assertAlmostEqual(data,
                                   expected_value,
                                   msg="Checking converted temperature is correct")

There are three tests in a Test_TemperaturePlotSource class. Python on a desktop computer (or Raspberry Pi) can execute these. One run is shown below.

Download: file
$ python tests/test_PlotSource.py
test_celsius (__main__.Test_TemperaturePlotSource)
Create the source in Celsius mode and test with some values. ... ok
test_fahrenheit (__main__.Test_TemperaturePlotSource)
Create the source in Fahrenheit mode and test with some values. ... ok
test_kelvin (__main__.Test_TemperaturePlotSource)
Create the source in Kelvin mode and test with some values. ... FAIL

======================================================================
FAIL: test_kelvin (__main__.Test_TemperaturePlotSource)
Create the source in Kelvin mode and test with some values.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tests/test_PlotSource.py", line 101, in test_kelvin
    msg="Checking converted temperature is correct")
AssertionError: 20.0 != 293.15 within 7 places : Checking converted temperature is correct

----------------------------------------------------------------------
Ran 3 tests in 0.002s

FAILED (failures=1)

The Celsius and Fahrenheit tests are fine (ok output) but the Kelvin one is failing. The failure is reported with the value from the first comparison. For some reason, the value 20.0 is being returned when we expect 293.15, no conversion is taking place and the value has been left in Celsius for some reason.

sensors_jolni-thermometer.jpg
Dual scale Jolni thermometer. Photograph by Kevin Walters.

A previous code inspection showed an anomaly with the use of lower() vs lower. CircuitPython's REPL allows us to explore this interactively on the command line, using the CLUE board.

The absence of the round brackets on the end of the lower method call introduces a critical flaw into the code. CircuitPython is somehow comparing a method call with a string. This "chalk and cheese" comparison is very unfortunate here!

Download: file
Adafruit CircuitPython 5.0.0 on 2020-03-02; Adafruit CLUE nRF52840 Express with nRF52840
>>> mode="Kelvin"
>>> mode[0].lower == "k"
False
>>> mode[0].lower == "K"
False
>>> mode[0]
'K'
>>> mode[0].lower
<bound_method>
>>> mode[0].lower() == "k"
True

Python is often referred to as strongly and dynamically-typed but an equality test using == will compare anything against anything - if they are not equivalent in some way then it will evaluate as False.

For comparison, this is the approximate equivalent in C++ using a different member function (C++ terminology for a method) called front.

Download: file
std::string Family("Coronaviridae");

if (Family.front() == 'N') {
  std::cout << "Starts with N" << std::endl;
} else if (Family.front == 'C') {
  std::cout << "Starts with C" << std::endl;
}

The compilation aborts with an error because the types cannot be legitimately compared using the C++ type system. The relative strength of type systems is a subjective topic and can vary in practical terms based on how a language is used.

Download: file
$ g++ -std=c++11 -o type-demonstration type-demonstration.cpp     type-demonstration.cpp: In function ‘int main(int, char**)’:
type-demonstration.cpp:15:30: error: invalid operands of types ‘<unresolved overloaded function type>’ and ‘char’ to binary ‘operator==’
   } else if (Family.front == 'C') {
                              ^

A language which checks types at compile-time is referred to as statically-typed.

Sometimes other software tools can help find likely bugs. In this case pylint has some ability to find dubious comparisons but it needs to be able to determine the type accurately and this prevents it from finding this particular bug. The simpler case is shown below where pylint presents a warning (W in W0143) about a possible mistake by the programmer.

Download: file
STRING = "HELLO"
if STRING[0].lower == "h":  # not what programmer intended!
    print("Found an h or an H")  # won't even be executed
Download: file
$ pylint check_pylint_comparison_with_callable.py
************* Module check_pylint_comparison_with_callable
check_pylint_comparison_with_callable.py:4:3: W0143: Comparing against a callable, did you omit the parenthesis? (comparison-with-callable)

Fixing the Bug

Adding the missing () is all that's required but the test still fails. The error message has changed.

Download: file
AssertionError: -253.14999999999998 != 293.15 within 7 places : Checking converted temperature is correct
This guide was first published on Apr 01, 2020. It was last updated on Apr 01, 2020.
This page (Bug 1) was last updated on Jun 23, 2020.