<jupyter_start><jupyter_text>Quality control **ENGSCI233: Computational Techniques and Computer Systems** *Department of Engineering Science, University of Auckland* It is not realistic to write code that is completely free from **bugs**. However, we should strive to eliminate as many as possible from our work. Although this is not a software design course, there are a number of good practices that we can borrow from that field. With practice, you will develop a set of useful habits - **unit testing, version control, and writing specifications** - that will help to minimise bugs, and make it easy for other people (and your future self) to understand, use and modify your code. You need to know:- How to write a unit test, a function that tests that a specific part of your code has been correctly implemented.- The key elements of a specification, a brief description that informs a user how your code works: inputs, outputs, purpose of a function, preconditions and post conditions, and writing a Python docstring.- Practical version control. A repository as a cloud-based copy of your code - you can clone a copy, make changes and map them back up to the cloud.<jupyter_code># loading support files - only execute this block if running via Google Colab; if so, execute it before anything else.
# download notebook files
!git clone https://github.com/bryan-ruddy/ENGSCI233_2021.git
%cd ENGSCI233_2021/quality_control
# imports and environment: this cell must be executed before any other in the notebook
%matplotlib notebook
from quality_control233 import*<jupyter_output><empty_output><jupyter_text>Content in this module has been drawn from this MIT OpenCourseWare course:https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-005-software-construction-spring-2016/readings/  1 Unit testing***Checking our code is bug-free.*** Large software projects might comprise thousands of lines of code. These must be written sufficiently well that the "whole program" achieves its objective. Code will generally be organised in a modular fashion, as a collection of functions and subroutines. Many of these will perform a small, specific task. These in turn will be called by other functions, achieving perhaps some task of intermediate complexity, and so on and so forth.A bug or error in an elementary function can propagate its effects to other parts of the code, compromising the software. **Unit testing** is the practice of checking for and catching these errors. We do this by actively **trying** to make the code fail. You should put aside any feelings of pride and accomplishment in your work. Instead, approach the task with the methodical, sociopathic brutality of a university lecturer writing a final exam0. 0  it's a joke settle down Let's look at an example: Consider the function below that computes the negative square of an input number, i.e., $-x^2$ for input $x$.<jupyter_code># run this cell to make the function available
def neg_square(x):
    return (-x)*(-x)<jupyter_output><empty_output><jupyter_text>You should immediately see that `neg_square` has been improperly implemented. However, **it will still return a result**, i.e., no error is raised. This means that, if we're not paying attention, this bug has the potential to cause mischief elsewhere in our code.   A **unit test** is another function that we write whose express purpose is to test that one of our functions is **working correctly**. But what does it mean to be "working correctly"?- The function should return the correct result.- The function should return the correct result **for every possible value** $x$.- The function should return the correct result **for every possible value** $x$, and **anticipate the stupidity** of the user, e.g., `neg_square('an apple')`.The right test depends on how rigourous you need to be, and the tolerance and implications of failure by your software. Let's take a look at one example.<jupyter_code># make sure to run the cell above defining `neg_square` before running this cell
def test_neg_square():
    assert neg_square(2) == -4
    
test_neg_square()<jupyter_output><empty_output><jupyter_text>The test above raises an `AssertionError` on the line with the `assert` command. Indeed, this is the express purpose of `assert` - to raise an error in the program when a condition evaluates to `False`. So the unit test is doing its job, signalling loud and clear that there is a bug in your code. ***Fix the implementation of ***`neg_square`*** above so that the it passes the unit test.***  1.1 Subdomains and edge cases You should now have a unit test verifying that `neg_square` works for the specific case of $x=2$. Common sense would tell us it should also work for $x=3$ or $x=1003$, but these are basically the same inputs: **positive integers larger than 1**. And it is not really practical to run a test for all integers larger than 1 - there are a lot of them.How about other input types? Negative integers? Floats? The special case of zero? An integer so large that squaring it will cause overflow? Strange and sometimes unexpected things can happen when you pass extreme or idiosyncratic values into your functions. When designing a unit test, you'll usually want to try an input from each of the different sub-domains - all positive integers, all negative integers - and edges between sub-domains - zero, negative infinity.***Copy-paste the unit test above and add `assert` statements to check proper behaviour for positive and negative integers and floats, zero and infinities.***<jupyter_code># use np.inf to represent "infinity"
import numpy as np

def test_neg_square():
    # your code here
    pass
    
test_neg_square()<jupyter_output><empty_output><jupyter_text>If your function contains `if/else` branches, then a single unit test may miss a buggy line of code if it is in the wrong branch. **Statement coverage** is the idea that you should write multiple unit tests to invoke code on the different branches, running as may lines of code (statements) as possible. One hundred percent statement coverage may not be practical.  1.2 Testing suites Even modestly complex programs can run to thousands of lines codes and tens or hundreds of functions. Writing and running unit tests for all of these can be exhausting **but is good practice**. When you discover bugs in your code, you should immediately write a unit test for it. Another coding philosophy is **test-driven development** or test-first programming: first, write a unit test, then, write a function that passes it. You won't need to do test-driven development in this course, but you may encounter it in industry (when doing internships, etc).In instrumentation development, some of your code may be written to interact directly with custom hardware. This may mean you need to test your code with the hardware, or with a physical prototype of it. This is called **hardware-in-the-loop** testing. Again, you won't need to do this in this course, but may encounter it in industry, especially if you work on safety-critical or failure critical systems. (Think ventilators, or [rockets](https://www.youtube.com/watch?v=xahiWQQKw7Y&t=101s).) Each time you sit down to write some code, you should run all your unit tests - the test suite - before starting (especially if you're working on someone else's code) and again once you have finished (especially if other people are working with yours!) With lots of tests, this can be a painful process. Thankfully, there are several automated testing programs to streamline the process. We will use one in the lab called `py.test`.    2 Specifications***Communicating the purpose of our code.*** **You write a function for someone else to use**1. Let's define some terminology and use it to unpack that statement.- The **implementor** (you). The person who **writes** the function.- The **client** (someone else). The person who **uses** the function.- The **contract**. The unspoken division of labour. You (the implementor) are writing the function and someone else (the client) is using it.- The **firewall**. The unspoken division of knowledge. You (the implementor) don't need to know **the context** in which the function is being used. Someone else (the client) doesn't need to know **the algorithmic implementation** of the function. Makes sense? Of course it does. But let's just think through some of the implications anyway...- The implementor can change the inner workings of a function, say, for efficiency, without consulting the client and **without breaking the client's code that uses the function**.- The client doesn't have to be an expert in efficient, robust or obscure algorithms. So far, this is all just philosophy. The **specification** is where we turn it into reality.1  Sometimes, the "someone else" is ourselves. But, because this person is in the future, we shall consider them a separate individual. If this is confusing, I recommend watching the movie Looper (2012).  2.1 Writing a specification The specification provides both the implementor and the client with an unambiguous, agreed upon description of the function. It should state:- Inputs/arguments/parameters to the functions.- Any preconditions on these inputs, e.g., input `a` must be a `True/False` boolean; input list `xs` must be sorted.- Outputs/returns of the function.- Any postconditions on the outputs, e.g., output `ix` is the **first** appearance of input `x` in input list `xs`, which potentially contains repetitions.In Python, we shall present the specification as a docstring, a concise commented description immediately below the function header. Let's look at an example:<jupyter_code>''' Find the position of a number in an array.
    
    Parameters:
    -----------
    x : float
        item to locate
    xs : array-like
        a list of values
    first : boolean (optional)
        if True, returns the index of the first appearance of x (default False)
    last : boolean (optional)
        if True, returns the index of the last appearance of x (default False)
    
    Returns:
    --------
    ix : array-like
        index location of x in xs
    
    Notes:
    ------
    xs should be sorted from smallest to largest
'''<jupyter_output><empty_output><jupyter_text>***- - - - QUESTIONS TO CONTEMPLATE - - - -***<jupyter_code># PART ONE
# --------
# What are the inputs for this specification?

# What are the outputs for this specification?

# PART TWO
# --------
# What are the preconditions for this specification?

# What are the postconditions for this specification?

# OPTIONAL CHALLENGE
# ------------------
# Can you think of any other preconditions that should be given?

# Often, there will be a heading "Raises:", which describes what should happen when an error occurs.
# Suggest an error that could occur for an implementation of this specification.<jupyter_output><empty_output><jupyter_text>An essential feature of the specification above is that it provides sufficient information to BOTH the implementor and the client to do their job. If I asked you **to implement** this specification, you could. If I gave you the name of a function that corresponded to this specification, you could **make use of it**. In addition, the specification provides **all the information you need** to write a unit-test. Details of the how the implementation works are not required.Finally, the specification is **language-agnostic** (notwithstanding, I have written it as the classic Python docstring). In practice, you should be able write a function in Python, MATLAB, C, etc., that conforms to the specification above.***Complete doc-strings for the functions below.***<jupyter_code>def neg_square2(x):
    ''' **your docstring here**
    '''
    return -x**2

def find_absolute_min(x, first=False, last = False):
    ''' **your docstring here**
    '''
    
    assert len(x)>0
    assert not (first and last)
    
    ax = abs(x)
    
    axmin = np.min(ax)
    
    ixmin = np.where(ax == axmin)[0]
    
    if first:
        ixmin = ixmin[0]
    if last:
        ixmin = ixmin[-1]
        
    return ixmin<jupyter_output><empty_output><jupyter_text>2.2 Errors and asserts Specifications as we have described them leave little room for **incompetence**. For instance, the implementor **assumes** that the client will satisfy the appropriate preconditions. Equally, the client **assumes**2 that the implementor has created a bug-free function. At least for the latter, the implementor could point to a **unit-test** as providing some guarantee of quality.But how should the implementor **guard against** incompetence on the part of the client? Here are two ways:- Explicitly check that preconditions are satisfied within the implementation. We do this using **assert** statements.- Monitor for anomalous or unexpected outcomes and `raise` an **error**. 2Remember that, when you *assume*, you make an "ass" out of "u" and "me"... The cell below calls a function that computes the **harmonic** mean of `xs`:\begin{equation}\tilde{x} = \left(\frac1n\sum\limits_{i=0}^{n}\frac{1}{x_i}\right)^{-1}\end{equation}It is not defined for any **zero values** of `xs`.<jupyter_code># harmonic mean calculation
xs = [1, 2, 3]
xharm = harmonic_mean(xs)
print(xharm)
# Run the cell above with xs = [1, 2, 3]. What happens?

# Try inserting a 0 value into xs and rerunning the cell. What happens?

# Try calling harmonic_mean with an empty list (xs = []). What happens?

# Try calling harmonic_mean with a non-numeric value (xs = [1, 'an apple', 2]). What happens?

# Which of these are 'checked' preconditions, and which are not?<jupyter_output><empty_output><jupyter_text>While it is sometimes a kindness on the part of the implementor to check preconditions, it may **not always be practical**. For example, the computational expense required to check the precondition *'input array `xs` must be sorted smallest to largest'* may be large compared to *'find the index position of the value `x`'*. Indeed, often the purpose of a precondition is to **save** the implementor some computational expense by guaranteeing desirable qualities of the inputs. The cell below calls a function that computes the **geometric** mean of `xs`:\begin{equation}\hat{x} = \sqrt[^n]{\prod\limits_{i=1}^n x_i}\end{equation}It is not defined if `xs` contains BOTH $0$ and $\infty$ ([what is zero times infinity?](https://img.huffingtonpost.com/asset/5b9282ac190000930a503a0f.jpeg?ops=1910_1000))<jupyter_code># geometric mean calculation
xs = [1, 2, 3]
xgeom = geometric_mean(xs)
print(xgeom)
# Run the cell above with xs = [1, 2, 3]. What happens?

# Try inserting a 0 value into xs and rerunning the cell. What happens?

# Try calling harmonic_mean with an empty list (xs = []). What happens?

# Try inserting an np.inf value into xs and rerunning the cell. What happens?

# Try inserting a 0 AND an np.inf value into xs and rerunning the cell. What happens?

# Which of these are 'checked' preconditions, which are `errors raised` due to anomalous behaviour, 
# and which are normal outcomes?

# OPTIONAL
# Check the implementation of geometric_mean - what fancy trick are we using to compute it? Does the client
# need to know about these fancy tricks?<jupyter_output><empty_output><jupyter_text>2.3 Raising and catching errors The implementor will sometimes **raise** an error when they want to signal to the client that things are not going well in the code. However, sometimes the client will be prepared to **tolerate and respond** to this misbehaviour. They can do this by **catching** the error with a `try` statement, and redirecting the code to an `except`. For instance, we have seen how the inputs below to `geometric_mean` raise a `ValueError`. We can catch this error by wrapping the error generating command (`geometric_mean`) inside a `try` block. If an error is raised, the code in the `except` block will be executed.<jupyter_code>xs = [1, 2, 3, 0, np.inf]

try:
    xgeom = geometric_mean(xs)
except:
    # default to 0 if mean is not undefined
    xgeom = 0.
    
print(xgeom)<jupyter_output><empty_output><jupyter_text>Catching and raising errors, using asserts to check for preconditions, and writing clear specifications, are all steps you can take to minimise the emergence and impact of bugs in your code. The final topic we need to cover is how to back-up and chronicle changes to your code: **version control**.   3 Version control***Backing up our code.*** Starting a new software project with promises of strict version control is the amateur coder's equivalent to a new year/new gym resolution. Its a good idea. You should do it. You **will** do it. Until one day it's inconvenient and you regress to lazy Leslie from 2018. Nevertheless, let's cover the basics3...3  For a *better* description of version control than I will give you here, see this [reading](https://ocw.mit.edu/ans7870/6/6.005/s16/classes/05-version-control/).     Your coding project is just a collection of files. > **If your computer dies tomorrow, wouldn't it be nice to have a backup?**You make your code better by making changes to those files.> **Wouldn't it be nice to have a record of all those changes?**Sometimes you'll make a change that actually makes your code worse.> **Wouldn't it be nice to roll back to a previous (better) version?**Sometimes you need to work at a desktop at university and sometimes you'll want work on your laptop at home.> **Wouldn't it be nice to sync your coding project between two or more machines?**Sometimes you'll work as part of a team developing different parts of the code.> **Wouldn't it be nice if there was a way to push out your changes to others, and pull their changes back?***The objective of version control is to address these issues.* We will be using a program called [**git**](https://git-scm.com/)4 to help us do that.    4In fact, if you are running this notebook on Google Colab, you are already using git!  3.1 Repositories At the heart of version control is the concept of the **repository**. This is an archive of the current contents of all the files in your code, safely located in the cloud. A repository can be **private**, accessible only to people selected by the **owner**, or **public**, accessible to any who want to look at it. (Typically, only people selected by the owner have the right to modify a public repository; this is why you can access the course files but you can't edit or replace them.)Anyone who has permission (including, of course, the owner) can `clone` a copy of the repository to their (say, university) computer. All the files will appear in their folders, and you can run them if you wish.In this, and following sections, I will be following `git` command line terminology. For example, to clone a repository, you would write at the command line> `git clone *name of repository*`The repository name typically combines the web address of the hosting entity (e.g., [BitBucket](https://bitbucket.org), [GitHub](https://github.com/)), your username, and a project name. Usually this will be obvious when visiting the repository web interface, e.g., to clone all the notebooks for this entire course> `git clone https://github.com/bryan-ruddy/ENGSCI233_2021.git`At the start of this notebook, if you ran it via Google, you will have run this very command so that Google's servers had access to all the support files for the notebook.  3.2 Recording your changes Sometimes, you will make a change, say by fixing a bug in one of your functions, thereby making a change to the file `super_func.py`. So that there is a record of this change, you will `add` (nominate new/modified files) and `commit` (record the change).Here is the command line terminology for `git`> `git add .`> `git commit -m "added a check for preconditions to super_func"`Other times, you might make a change by adding a unit test, written in a new file `func_i_test.py`. Once again, you will `add` this file to the repository, and then `commit` so there is a record of the change.> `git add .`> `git commit -m "added a unit test for super_func"`Your commits - records of change - are local to your computer. You rejoin them with the online **repository** using a `push`. > `git push`  3.3 Working from  multiple locations Now, if you go home and want to work on this code, you can `clone` a copy of the repository.> `git clone *name of repository*`Make changes to files at home. Then `add`, `commit` and `push` these changes up to the online repository.> `git add .`> `git commit -m "fixed a bug in the precondition check"`> `git push`Next time you are working at the university, use a `pull` to retrieve the changes made at home, **syncing** your local repository with the online one.> `git pull`  3.4 Managing conflicts Of course, there is no requirement that the university and home clones are owned by the same person. Suppose that your friend has cloned your repository and you both working on different parts of it **simultaneously**.Your friend finishes for the night at 8pm4 and `push`es their changes up to the online repository.Managing your deadlines and workload carefully, you finish at 1am, `commit` and try to `push` up your changes. Unfortunately, the `push` fails because both you and your friend have modified the same line of code, resulting in a conflict.To resolve the conflict, you must `fetch` the latest version that includes the modifications from your friend, and then manage the conflict locally using a `merge`.> `git fetch`> `git merge`Once the conflict is handled, you can `commit` and `push` a new version, that includes both your changes and the managed conflict.4lol, casual  3.5 Rolling back to previous versions Often you'll find that some magic-bullet change to your code that was going to make things more efficient just didn't pan out. Things don't even work now and you wish desperately you could roll back to the inefficient, but working, version.In keeping a record of all your changes - snapshots of your code at different moments in history - version control allows this sort of "saved game" approach to coding. There are two common options:1. If you want to take a peek at the older version of the code, but later return to the current version, a `checkout` allows you to temporarily roll back.2. If you want to reset permanently to the older version, you want to do a `revert`.For more on rolling back to an old version of your code, check out [this StackOverflow thread](https://stackoverflow.com/questions/4114095/how-to-revert-a-git-repository-to-a-previous-commit).<jupyter_code># TERMINOLOGY TEST
# ----------------
# Provide definitions for the following terms as they pertain to version control.
#
# Repository:
# Owner:
# Clone:
# Push/Pull:
# Commit:
# Add:
# Revert:
# Merge:
# Git:<jupyter_output><empty_output><jupyter_text>3.6 Final notes on `git` Because it is a command line tool, `git` has extraordinary flexibility in its mode of operation through the use of command line flags. For example, > `git commit -m "fixed a function"`associates the message `"fixed a function"` to the commit. `-m` is the command line flag that tells `git` "include a message, make it the text that follows". Adjusting this command slightly> `git commit -m "fixed a function" -q `will do the same as above, but now the `-q` will request `git` to suppress output information about what code has been changed for this commit.It is not possible to fully cover `git` functionality in one week of this course. For most users, it will be sufficient to Google what you want to achieve, and then read the explanation and instructions from the first StackOverflow link. For more help with `git` command line, try typing> `git help`> `git help add`  3.6.1 Ignoring files If you're writing and compiling code (even Python compiles itself, creating `*.pyc` files) there will be extra files created that you won't want to track. Placing a `.gitignore` file in your repository tells `git` not to include particular files in the repository.***Run the cell below to display the contents of `.gitignore`***<jupyter_code>%pycat .gitignore<jupyter_output><empty_output><jupyter_text>Here, we use a bit of special syntax (which you won't be assessed on) to show the file contents in a Jupyter notebook: the % tells the interactive Python interpreter that the line is actually a special command, not a piece of Python code. The `pycat` command shows the contents of a file on the screen, with syntax highlighting that assumes it's a piece of Python code. Similarly, the ! used with the `git` command at the start of the notebook tells the interpreter it is a shell command, not Python code.  Example unit test for `neg_square`<jupyter_code>def test_neg_square():
    assert neg_square(2) == -4
    assert neg_square(-2) == -4
    assert neg_square(2.5) == -6.25
    assert neg_square(-2.5) == -6.25
    assert neg_square(0) == 0
    assert neg_square(np.inf) == -np.inf<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.0749649228302265, 'fraction_numerical': 0.007977550611344959, 'mean_word_length': 4.390233362143475, 'pattern_counts': {'":': 0, '<': 53, '<?xml version=': 0, '>': 77, 'https://': 10, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '13', 'dir': 'jupyter-structured-clean-dedup', 'id': '419040', 'license': 'MIT', 'n_tokens_mistral': 6537, 'n_tokens_neox': 6217, 'n_words': 3675, 'path': 'quality_control/quality_control.ipynb', 'repo_name': 'bryan-ruddy/ENGSCI233_2021', 'size': '24945'}