Testing changes when contributing to Ansible

This is by no means a detailed post about testing Ansible.  The main reason I’m writing this down is because I know I’ll forget by a few months time.

Goal:

  • Create an Ansible plugin filter to calculate the distance between two coordinates (haversine)
  • Write and test the required unit tests (and integration?) for the code to be accepted into Ansible
  • Raise a PR to the main project

Background

I was writing an Ansible playbook to check the local traffic site for accidents.  The site returns a list of incidents state wide, which, is great, but I was only interested in a 10km radius.  Eg:

 .....
 with_items: “{{ traffic.json.features }}”

 when: item | distance(myLong,myLat, item.geometry.coordinates.1, item.geometry.coordinates.0)|int < 10

This looped over a variable with_items and then passed some coordinates through the a custom plugin filter called distance.  (Note: Distance was a custom plugin filter which I stored in the plugin_filters directory.  I figured I’d go a little further and try to submit to Ansible core.)

Fork

  • I’ve forked ansible/ansible from github into my own repo.
  • I’ve cloned the repo to my development machine
  • Changed directory into the freshly cloned ansible directory and ran `source ./hacking/env-setup`.

Finding where to add the filter code

Haversine (wikipedia) is a maths formula, so, it’s probably best to hang out with other Ansible filters such as power, logarithm, min, max and friends.

Grepping through the source for above brought up this:

./lib/ansible/plugins/filter/mathstuff.py

Perfect!  Hopefully.

The code…

Added a function to this file:

def haversine(measurement, lat1, lon1, lat2, lon2):

    from math import radians, sin, cos, sqrt, asin

    diameter = {

        ‘m’: 7917.5,

        ‘km’: 12742}

    try:

        dlat = radians(float(lat2) – float(lat1))

        dlon = radians(float(lon2) – float(lon1))

        lat1 = radians(float(lat1))

        lat2 = radians(float(lat2))

        a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2

        c = 2 * asin(sqrt(a))

    except (ValueError, TypeError) as e:

        raise errors.AnsibleFilterError(‘haversine() only accepts floats: %s’ % str(e))

    if measurement in diameter:

        return round(diameter[measurement] / 2 * c, 2)

    else:

        raise errors.AnsibleFilterError(‘haversine() can only be called with km or m’)

Testing

Ansible has a great test setup.  I started with the unit tests.

Once again, needed to find out where the math stuff testing hung out.  Grep’ing through the test directory found:  test/units/plugins/filter/test_mathstuff.py

Copying the format of existing tests I added a few tests:

class TestHaversine:

    def test_haversine_non_number(self):

        with pytest.raises(AnsibleFilterError, message=’haversine() only accepts floats’):

            ms.haversine(‘km’, ‘a’, ‘b’, ‘c’, ‘d’)

        with pytest.raises(AnsibleFilterError, message=’haversine() only accepts floats’):

            ms.haversine(‘m’, ‘a’, ‘b’, ‘c’, ‘d’)

        with pytest.raises(AnsibleFilterError, message=’haversine() can only be called with km or m’):

            ms.haversine(‘z’, ‘35.9914928’,’-78.907046′, ‘-33.8523063’, ‘151.2085984’)

    def test_km(self):

        assert ms.haversine(‘km’, ‘35.9914928’, ‘-78.907046’, ‘-33.8523063’, ‘151.2085984’) == 15490.46

    def test_m(self):

        assert ms.haversine(‘m’, ‘35.9914928’, ‘-78.907046’, ‘-33.8523063’, ‘151.2085984’) == 9625.31

This was to test:

  • That proper float values were passed through to the function
  • That a proper measure was used (km or m)
  • That two test calculations calculated and rounded nicely

The quickest way to test after making the change was:

ansible-test units --tox --python 2.7 test/units/plugins/filter/test_mathstuff.py

Other types of testing in Ansible were here.

Test playbook

Of course, better test to ensure it’s actually usable in a playbook:

– hosts: localhost

  tasks:

    – name: Haversine distance between two lon/lat co-ordinates

      debug:

        msg: “{{ ‘km’|haversine(‘35.9914928′,’-78.907046′, ‘-33.8523063’, ‘151.2085984’) }}”

    – name: Haversine distance between two lon/lat co-ordinates

      debug:

        msg: “{{ ‘m’|haversine(‘35.9914928′,’-78.907046′, ‘-33.8523063’, ‘151.2085984’) }}”

    – name: Haversine distance between two lon/lat co-ordinates

      debug:

        msg: “{{ ‘m’|haversine(‘a35.9914928′,’-78.907046′, ‘-33.8523063’, ‘151.2085984’) }}”

Which resulted in:

$ ansible-playbook only_filter.yml

PLAY [localhost] *********************************************************************************************************************************************

TASK [Gathering Facts] ***************************************************************************************************************************************

ok: [localhost]

TASK [Haversine distance between two lon/lat co-ordinates] ***************************************************************************************************

ok: [localhost] => {

    “msg”: “15490.46”

}

TASK [Haversine distance between two lon/lat co-ordinates] ***************************************************************************************************

ok: [localhost] => {

    “msg”: “9625.31”

}

TASK [Haversine distance between two lon/lat co-ordinates] ***************************************************************************************************

fatal: [localhost]: FAILED! => {“msg”: “haversine() only accepts floats: could not convert string to float: a35.9914928”}

to retry, use: –limit @/home/johni/ansible/hacking/ansible/only_filter.retry

PLAY RECAP ***************************************************************************************************************************************************

localhost                  : ok=3    changed=0    unreachable=0    failed=1