<jupyter_start><jupyter_text>Code Testing and CIThis notebook contains a tutorial for using testing frameworks and continuous integration with Travis CI.These problems assume you are familiar enough with code repositories and packaging to have a functioning repository with a Python package (even if it doesn't do anything particularly useful). If you don't have that, you might consider starting with [the packaging tutorial](packaging.ipynb).This tutorial focuses on the concept of "unit testing" - unit testing is essentially the most basic form of automatable testing. For many scientific applications, it's sufficient to use unit testing along sode some "end-to-end" scientific accuracy tests, so this tutorial focuses on what you'd need to do that.  But if you're interested in going deeper into the concepts of software testing, you may be interested in [this site](http://softwaretestingfundamentals.com/), particularly its description of the [levels of testing](http://softwaretestingfundamentals.com/software-testing-levels/) or [types of testing](http://softwaretestingfundamentals.com/software-testing-types/).  Problem 1: Set up py.test in you repo In this problem we'll aim to get the [py.test](https://docs.pytest.org/en/latest/) testing framework up and running in the code repository you set up in the last set of problems.  We can then use it to collect and run tests of the code.  1a: Ensure py.test is installedOf course ``py.test`` must actually be installed before you can use it. The commands below should work for the Anaconda Python Distribution, but if you have some other Python installation you'll want to install `pytest` (and its coverage plugin) as directed in the install instructions for ``py.test``.<jupyter_code>!conda install pytest pytest-cov<jupyter_output><empty_output><jupyter_text>1b: Ensure your repo has code suitable for unit testsDepending on what your code actually does, you might need to modify it to actually perform something testable.  For example, if all it does is print something, you might find it difficult to write an effective unit test.  Try adding a function that actually performs some operation and returns something different depending on various inputs.  That tends to be the easiest function to unit-test: one with a clear "right" answer in certain situations. Also be sure you have `cd`ed to the *root* of the repo for `pytest` to operate correctly.  1c: Add a test file with a test functionThe test must be part of the package and follow the convention that the file and the function begin with ``test`` to get picked up by the test collection machinery.  Inside the test function, you'll need some code that fails if the test condition fails.  The easiest way to do this is with an ``assert`` statement, which raises an error if its first argument is False.*Hint: remember that to be a valid python package, a directory must have an ``__init__.py``*<jupyter_code>!mkdir #complete
!touch #complete
%%file <yourpackage>/tests/test_something.py

def test_something_func():
    assert #complete<jupyter_output><empty_output><jupyter_text>1d: Run the test directlyWhile this is not how you'd ordinarily run the tests, it's instructive to first try to execute the test *directly*, without using any fancy test framework.  If your test function just runs, all is good.  If you get an exception, the test failed (which in this case might be *good*).*Hint: you may need to use `reload` or just re-start your notebook kernel to get the cell below to recognize the changes.*<jupyter_code>from <yourpackage>.tests import test_something

test_something.test_something_func()<jupyter_output><empty_output><jupyter_text>1e: Run the tests with ``py.test``Once you have an example test, you can try invoking ``py.test``, which is how you should run the tests in the future.  This should yield a report that shows a dot for each test. If all you see are dots, the tests ran sucessfully.  But if there's a failure, you'll see the error, and the traceback showing where the error happened.<jupyter_code>!py.test<jupyter_output><empty_output><jupyter_text>1f: Make the test fail (or succeed...)If your test failed when you ran it, you should now try to fix the test (or the code...) to make it work.  Try running (Modify your test to fail if it succeeded before, or vice versa)<jupyter_code>!py.test<jupyter_output><empty_output><jupyter_text>1g: Check coverageThe coverage plugin we installed will let you check which lines of your code are actually run by the testing suite.<jupyter_code>!py.test --cov=<yourproject> tests/ #complete<jupyter_output><empty_output><jupyter_text>This should yield a report, which you can use to decide if you need to add more tests to acheive complete coverage.  Check out the command line arguments to see if you can get a more detailed line-by-line report.  Problem 2: Implement some unit tests The sub-problems below each contain different unit testing complications.  Place the code from the snippets in your repository (either using an editor or the ``%%file`` trick), and write tests to ensure the correctness of the functions.  Try to achieve 100% coverage for all of them (especially to catch some hidden bugs!).Also, note that some of these examples are not really practical - that is, you wouldn't want to do this in *real* code because there's better ways to do it.  But because of that, they are good examples of where something can go subtly wrong... and therefore where you want to make tests!  2aWhen you have a function with a default, it's wise to test both the with-default call (``function_b()``), and when you give a value (``function_b(1.2)``)*Hint: Beware of numbers that come close to 0... write your tests to accomodate floating-point errors!*<jupyter_code>#%%file <yourproject>/<filename>.py #complete, or just use your editor

# `math` here is for *scalar* math... normally you'd use numpy but this makes it a bit simpler to debug

import math 

inf = float('inf')  # this is a quick-and-easy way to get the "infinity" value 

def function_a(angle=180):
    anglerad = math.radians(angle)
    return math.sin(anglerad/2)/math.sin(anglerad)<jupyter_output><empty_output><jupyter_text>2bThis test has an intentional bug... but depending how you right the test you *might* not catch it...  Use unit tests to find it! (and then fix it...)<jupyter_code>#%%file <yourproject>/<filename>.py #complete, or just use your editor

def function_b(value):
    if value < 0:
        return value - 1
    else:
        value2 = subfunction_b(value + 1)
        return value + value2
    
def subfunction_b(inp):
    vals_to_accum = []
    for i in range(10):
        vals_to_accum.append(inp ** (i/10))
    if vals_to_accum[-1] > 2:
        vals.append(100)
    # really you would use numpy to do this kind of number-crunching... but we're doing this for the sake of example right now
    return sum(vals_to_accum)<jupyter_output><empty_output><jupyter_text>2cThere are (at least) *two* significant bugs in this code (one fairly apparent, one much more subtle).  Try to catch them both, and write a regression test that covers those cases once you've found them.One note about this function: in real code you're probably better off just using [the Angle object from `astropy.coordinates`](http://docs.astropy.org/en/stable/coordinates/angles.html).  But this example demonstrates one of the reasons *why* that was created, as it's very easy to write a buggy version of this code.*Hint: you might find it useful to use `astropy.coordinates.Angle` to create test cases...*<jupyter_code>#%%file <yourproject>/<filename>.py #complete, or just use your editor

import math

# know that to not have to worry about this, you should just use `astropy.coordinates`.
def angle_to_sexigesimal(angle_in_degrees, decimals=3):
    """
    Convert the given angle to a sexigesimal string of hours of RA.
    
    Parameters
    ----------
    angle_in_degrees : float
        A scalar angle, expressed in degrees
    
    Returns
    -------
    hms_str : str
        The sexigesimal string giving the hours, minutes, and seconds of RA for the given `angle_in_degrees`
        
    """
    if math.floor(decimals) != decimals:
        raise ValueError('decimals should be an integer!')
    
    hours_num = angle_in_degrees*24/180
    hours = math.floor(hours_num)
    
    min_num = (hours_num - hours)*60
    minutes = math.floor(min_num)
    
    seconds = (min_num - minutes)*60

    format_string = '{}:{}:{:.' + str(decimals) + 'f}'
    return format_string.format(hours, minutes, seconds)<jupyter_output><empty_output><jupyter_text>2d*Hint: numpy has some useful functions in [numpy.testing](https://docs.scipy.org/doc/numpy/reference/routines.testing.html) for comparing arrays.*<jupyter_code>#%%file <yourproject>/<filename>.py #complete, or just use your editor

import numpy as np

def function_d(array1=np.arange(10)*2, array2=np.arange(10), operation='-'):
    """
    Makes a matrix where the [i,j]th element is array1[i] <operation> array2[j]
    """
    if operation == '+':
        return array1[:, np.newaxis] + array2
    elif operation == '-':
        return array1[:, np.newaxis] - array2
    elif operation == '*':
        return array1[:, np.newaxis] * array2
    elif operation == '/':
        return array1[:, np.newaxis] / array2
    else:
        raise ValueError('Unrecognized operation "{}"'.format(operation))<jupyter_output><empty_output><jupyter_text>Problem 3: Set up travis to run your tests whenever a change is made Now that you have a testing suite set up, you can try to turn on a continuous integration service to constantly check that any update you might send doesn't create a bug.  We will the [Travis-CI](https://travis-ci.org/) service for this purpose, as it has one of the lowest barriers to entry from Github.  3a: Ensure the test suite is passing locallySeems obvious, but it's easy to forget to check this and only later realize that all the trouble you thought you had setting up the CI service was because the tests were actually broken...<jupyter_code>!py.test<jupyter_output><empty_output><jupyter_text>3b: Set up an account on travisThis turns out to be quite convenient.  If you go to the [Travis web site](https://travis-ci.org/), you'll see a "Sign in with GitHub" button.  You'll need to authorize Travis, but once you've done so it will automatically log you in and know which repositories are yours.  3c: Create a minimal ``.travis.yml`` file.Before we can activate travis on our repo, we need to tell travis a variety of metadata about what's in the repository and how to run it.  The template below should be sufficient for the simplest needs.<jupyter_code>%%file .travis.yml

language: python
python:
  - "3.6"
# command to install dependencies
#install: "pip install numpy"  #uncomment this if your code depends on numpy or similar
# command to run tests
script: pytest<jupyter_output><empty_output><jupyter_text>Be sure to commit and push this to github before proceeding:<jupyter_code>!git #complete<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08020231213872832, 'fraction_numerical': 0.006322254335260116, 'mean_word_length': 4.34927536231884, 'pattern_counts': {'":': 0, '<': 66, '<?xml version=': 0, '>': 66, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '13', 'dir': 'jupyter-structured-clean-dedup', 'id': '34469', 'license': 'CC-BY-4.0', 'n_tokens_mistral': 3121, 'n_tokens_neox': 3018, 'n_words': 1446, 'path': 'notebooks/Part4_testing_and_ci.ipynb', 'repo_name': 'jupytercon/2020-eteq', 'size': '11072'}