Tests and Travis Integration (#9)

* Test for grapher and post_processor (To complete)

* Udpate readme 

* Add requirements.txt and update setup.py

* Add .travis.yml
This commit is contained in:
Haidara Mohamed El Mouctar 2017-11-21 12:17:33 +01:00 committed by GitHub
parent af9a03c99e
commit a363eae94b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 379 additions and 21 deletions

20
.travis.yml Normal file
View file

@ -0,0 +1,20 @@
language: python
python:
- "2.7"
- "3.5"
env:
- ANSIBLE_VERSION=2.4.0
- ANSIBLE_VERSION=2.4.1
before_install:
- sudo apt-get -qq update
- sudo apt-get install -y graphviz
install:
- pip install -q ansible==$ANSIBLE_VERSION
- pip install -q -r requirements.txt
- pip install .
script:
- make test

View file

@ -17,6 +17,9 @@ setup_virtualenv:
test_install: build setup_virtualenv
test:
pytest
clean:
@echo "Cleaning..."
@rm -rf ansible_playbook_grapher.egg-info build dist $(VIRTUALENV_DIR) example.svg

View file

@ -1,5 +1,6 @@
# Ansible Playbook Grapher
[![Build Status](https://travis-ci.org/haidaraM/ansible-playbook-grapher.svg?branch=master)](https://travis-ci.org/haidaraM/ansible-playbook-grapher)
[![PyPI version](https://badge.fury.io/py/ansible-playbook-grapher.svg)](https://badge.fury.io/py/ansible-playbook-grapher)
[ansible-playbook-grapher](https://github.com/haidaraM/ansible-playbook-grapher) is a command line tool to create a graph representing your Ansible playbook tasks and roles. The aim of
@ -7,11 +8,9 @@ this project is to quickly have an overview of your playbook.
Inspired by [Ansible Inventory Grapher](https://github.com/willthames/ansible-inventory-grapher).
## Pérequis
* **Ansible** >= 2.4: The script has not been tested yet with an earlier version of Ansible.
```
$ sudo pip3 install 'ansible>=2.4'
```
## Prerequisites
* **Ansible** >= 2.4: The script has not been tested with an earlier version of Ansible. If you still use an older version of Ansible, create an virtual environment and install ansible-playbook-grapher. **The installation process will install a version of Ansible >= 2.4**
* **graphviz**: The tool used to generate the graph in SVG.
```
$ sudo apt-get install graphviz
@ -19,7 +18,7 @@ Inspired by [Ansible Inventory Grapher](https://github.com/willthames/ansible-in
## Installation
```
$ sudo pip3 install ansible-playbook-grapher
$ pip install ansible-playbook-grapher
```
## Usage

View file

@ -128,7 +128,7 @@ class Grapher(object):
color, play_font_color = self._colors_for_play(play)
play_name = "hosts: {} ({})".format(clean_name(str(play)), nb_hosts)
play_name = "{} ({})".format(clean_name(str(play)), nb_hosts)
play_name = self.template(play_name, play_vars)
@ -227,9 +227,9 @@ class Grapher(object):
if output_filename is None:
output_filename = self.output_filename + ".svg"
post_processor = PostProcessor(svg_path=output_filename, graph_representation=self.graph_representation)
post_processor = PostProcessor(svg_path=output_filename)
post_processor.post_process()
post_processor.post_process(graph_representation=self.graph_representation)
post_processor.write()

View file

@ -77,14 +77,11 @@ class PostProcessor(object):
Post process the svg by adding some javascript and css
"""
def __init__(self, svg_path, graph_representation):
def __init__(self, svg_path):
self.svg_path = svg_path
self.graph_representation = graph_representation
self.tree = etree.parse(svg_path)
self.root = self.tree.getroot()
self.root.set('id', 'svg')
def insert_script_tag(self, index, attrib):
element_script_tag = etree.Element('script', attrib=attrib)
@ -107,7 +104,9 @@ class PostProcessor(object):
graph_group_element.remove(title_element)
def post_process(self, *args, **kwargs):
def post_process(self, graph_representation=None, *args, **kwargs):
self.root.set('id', 'svg')
jquery_tag_index = 0
javascript_tag_index = 1
@ -118,19 +117,19 @@ class PostProcessor(object):
# insert my javascript
highlight_script = _read_data("highlight-hover.js")
self.insert_cdata(javascript_tag_index, 'script', attrib={'type': 'text/javascript'},
self.insert_cdata(javascript_tag_index, 'script', attrib={'type': 'text/javascript', 'id': 'my_javascript'},
cdata_text=highlight_script)
css = _read_data("graph.css")
# insert my css
self.insert_cdata(css_tag_index, 'style', attrib={'type': 'text/css'}, cdata_text=css)
self.insert_cdata(css_tag_index, 'style', attrib={'type': 'text/css', 'id': 'my_css'}, cdata_text=css)
self._remove_title()
if self.graph_representation:
if graph_representation:
# insert the graph representation for the links between the nodes
self._insert_graph_representation()
self._insert_graph_representation(graph_representation)
def write(self, output_filename=None):
if output_filename is None:
@ -138,8 +137,8 @@ class PostProcessor(object):
self.tree.write(output_filename, xml_declaration=True, encoding="UTF-8")
def _insert_graph_representation(self):
for node, node_links in self.graph_representation.graph_dict.items():
def _insert_graph_representation(self, graph_representation):
for node, node_links in graph_representation.graph_dict.items():
# Find the group g with the specified id
element = self.root.xpath("ns:g/*[@id='%s']" % node, namespaces={'ns': SVG_NAMESPACE})[0]

3
pytest.ini Normal file
View file

@ -0,0 +1,3 @@
[pytest]
rootdir = tests
addopts = -v

4
requirements.txt Normal file
View file

@ -0,0 +1,4 @@
ansible>=2.4.0
graphviz==0.8.1
colour==0.1.5
lxml==4.1.1

View file

@ -14,7 +14,8 @@ setup(name=__prog__,
author="HAIDARA Mohamed El Mouctar",
author_email="elmhaidara@gmail.com",
license="MIT",
install_requires=['graphviz', 'colour', 'lxml'],
install_requires=['graphviz==0.8.1', 'colour==0.1.5', 'lxml==4.1.1', 'ansible>=2.4'],
tests_requires=['pytest'],
packages=find_packages(exclude=['tests']),
package_data={"ansible-playbook-grapher": ['data/*']},
include_package_data=True,

View file

@ -0,0 +1,5 @@
FIXTURES_DIR = 'tests/fixtures/'
INVENTORY_FILE = FIXTURES_DIR + "inventory"
SIMPLE_PLAYBOOK_YML = FIXTURES_DIR + "simple_playbook.yml"
SIMPLE_PLAYBOOK_SVG = FIXTURES_DIR + "simple_playbook_no_postproccess.svg"

40
tests/conftest.py Normal file
View file

@ -0,0 +1,40 @@
import pytest
from ansibleplaybookgrapher.grapher import Grapher
from tests import INVENTORY_FILE
@pytest.fixture(name='data_loader')
def fixture_data_loader():
"""
Return an Ansible DataLoader
:return:
"""
from ansible.parsing.dataloader import DataLoader
return DataLoader()
@pytest.fixture(name='inventory_manager')
def fixture_inventory_manager(data_loader):
"""
Return an Ansible InventoryManager
:return:
"""
from ansible.inventory.manager import InventoryManager
return InventoryManager(loader=data_loader, sources=INVENTORY_FILE)
@pytest.fixture(name='variable_manager')
def fixture_variable_manager(data_loader, inventory_manager):
"""
Return an Ansible VariableManager
:return:
"""
from ansible.vars.manager import VariableManager
return VariableManager(loader=data_loader, inventory=inventory_manager)
@pytest.fixture(name='grapher')
def fixture_simple_grapher(data_loader, inventory_manager, variable_manager, request):
return Grapher(data_loader=data_loader, inventory_manager=inventory_manager, variable_manager=variable_manager,
playbook_filename=request.param)

40
tests/fixtures/example.yml vendored Normal file
View file

@ -0,0 +1,40 @@
---
- hosts: all
vars:
backport: "stretch-backports"
packages:
- git
- tree
- curl
pre_tasks:
- name: Pre task 1
debug: msg="Pretask"
- name: Pre task 2
debug: msg="Pretask"
tasks:
- name: Add the backbport
become: yes
apt_repository:
repo: deb http://ftp.debian.org/debian {{backport}} main
filename: "{{backport}}"
state: present
- name: Install packages
become: yes
apt:
name: "{{item}}"
state: present
default_release: "{{backport}}"
with_items: "{{packages}}"
- name: Command that should fails
command: /bin/false
ignore_errors: true
post_tasks:
- name: Post task 1
debug: msg="Postask"
- name: Posttask 2
debug: msg="Postask"

6
tests/fixtures/inventory vendored Normal file
View file

@ -0,0 +1,6 @@
host1
host2
host3
host4
host5
host6

8
tests/fixtures/simple_playbook.yml vendored Normal file
View file

@ -0,0 +1,8 @@
---
- hosts: all
post_tasks:
- name: Post task 1
debug: msg="Postask"
- name: Posttask 2
debug: msg="Postask"

View file

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.38.0 (20140413.2041)
-->
<!-- Title: %3 Pages: 1 -->
<svg width="628pt" height="98pt"
viewBox="0.00 0.00 627.65 98.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 94)">
<title>%3</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-94 623.654,-94 623.654,4 -4,4"/>
<!-- tests/fixtures/simple_playbook.yml -->
<g id="node1" class="node">
<title>tests/fixtures/simple_playbook.yml</title>
<ellipse fill="none" stroke="black" stroke-dasharray="1,5" cx="135.838" cy="-45" rx="135.676" ry="18"/>
<text text-anchor="middle" x="135.838" y="-41.3" font-family="Times,serif" font-size="14.00">
tests/fixtures/simple_playbook.yml
</text>
</g>
<!-- hosts: all -->
<g id="play_hostsall" class="node">
<title>hosts: all</title>
<polygon fill="#483337" stroke="#483337"
points="379.676,-63 314.676,-63 314.676,-27 379.676,-27 379.676,-63"/>
<text text-anchor="middle" x="347.176" y="-41.3" font-family="Times,serif" font-size="14.00" fill="#ffffff">
hosts: all
</text>
</g>
<!-- tests/fixtures/simple_playbook.yml&#45;&gt;hosts: all -->
<g id="testsfixturessimple_playbookymlhostsall" class="edge">
<title>tests/fixtures/simple_playbook.yml&#45;&gt;hosts: all</title>
<path fill="none" stroke="#483337" stroke-width="2" d="M271.782,-45C283.402,-45 294.477,-45 304.364,-45"/>
<polygon fill="#483337" stroke="#483337" stroke-width="2"
points="304.595,-48.5001 314.595,-45 304.595,-41.5001 304.595,-48.5001"/>
<text text-anchor="middle" x="293.176" y="-48.8" font-family="Times,serif" font-size="14.00" fill="#483337">
1
</text>
</g>
<!-- [post_task] Post task 1 -->
<g id="post_taskPosttask1" class="node">
<title>[post_task] Post task 1</title>
<polygon fill="none" stroke="black"
points="619.643,-64.5442 619.643,-79.4558 561.956,-90 480.374,-90 422.687,-79.4558 422.687,-64.5442 480.374,-54 561.956,-54 619.643,-64.5442"/>
<text text-anchor="middle" x="521.165" y="-68.3" font-family="Times,serif" font-size="14.00">[post_task]
Post task 1
</text>
</g>
<!-- hosts: all&#45;&gt;[post_task] Post task 1 -->
<g id="play_hostsallpost_taskPosttask1" class="edge">
<title>hosts: all&#45;&gt;[post_task] Post task 1</title>
<path fill="none" stroke="#483337" stroke-width="2"
d="M379.721,-49.9522C395.567,-52.4397 415.602,-55.585 435.539,-58.7149"/>
<polygon fill="#483337" stroke="#483337" stroke-width="2"
points="435.193,-62.2034 445.615,-60.2967 436.279,-55.2881 435.193,-62.2034"/>
<text text-anchor="middle" x="401.176" y="-56.8" font-family="Times,serif" font-size="14.00" fill="#483337">
1
</text>
</g>
<!-- [post_task] Posttask 2 -->
<g id="post_taskPosttask2" class="node">
<title>[post_task] Posttask 2</title>
<polygon fill="none" stroke="black"
points="617.015,-10.5442 617.015,-25.4558 560.867,-36 481.462,-36 425.315,-25.4558 425.315,-10.5442 481.462,-3.55271e-15 560.867,-0 617.015,-10.5442"/>
<text text-anchor="middle" x="521.165" y="-14.3" font-family="Times,serif" font-size="14.00">[post_task]
Posttask 2
</text>
</g>
<!-- hosts: all&#45;&gt;[post_task] Posttask 2 -->
<g id="play_hostsallpost_taskPosttask2" class="edge">
<title>hosts: all&#45;&gt;[post_task] Posttask 2</title>
<path fill="none" stroke="#483337" stroke-width="2"
d="M379.747,-36.7686C385.678,-35.3868 391.847,-34.0598 397.676,-33 406.252,-31.4407 415.197,-29.9982 424.175,-28.6747"/>
<polygon fill="#483337" stroke="#483337" stroke-width="2"
points="424.903,-32.1065 434.309,-27.2311 423.916,-25.1764 424.903,-32.1065"/>
<text text-anchor="middle" x="401.176" y="-36.8" font-family="Times,serif" font-size="14.00" fill="#483337">
2
</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

25
tests/test_grapher.py Normal file
View file

@ -0,0 +1,25 @@
import os
from lxml import etree
from ansibleplaybookgrapher.grapher import Grapher
from tests import FIXTURES_DIR
def test_grapher_simple_playbook(data_loader, inventory_manager, variable_manager, tmpdir):
playbook = FIXTURES_DIR + "simple_playbook.yml"
output_filepath = tmpdir.join('output')
grapher = Grapher(data_loader=data_loader, inventory_manager=inventory_manager, variable_manager=variable_manager,
playbook_filename=playbook, output_filename=output_filepath.strpath)
grapher.make_graph()
grapher.render_graph()
grapher.post_process_svg()
svg_filepath = output_filepath.strpath + ".svg"
# test if the file exist. It will exist only if we write in it
assert os.path.isfile(svg_filepath)
tree = etree.parse(svg_filepath)

125
tests/test_postprocessor.py Normal file
View file

@ -0,0 +1,125 @@
import pytest
from lxml import etree
from ansibleplaybookgrapher.utils import PostProcessor, SVG_NAMESPACE, GraphRepresentation
from tests import SIMPLE_PLAYBOOK_SVG
@pytest.fixture(name='post_processor')
def fixture_simple_postprocessor(request):
"""
Return a post processor without a graph representation and with the simple_playbook_no_postproccess
:return:
"""
try:
svg_path = request.param
except AttributeError:
# if the svg is not provided, we use the simple one
svg_path = SIMPLE_PLAYBOOK_SVG
post_processor = PostProcessor(svg_path=svg_path)
return post_processor
def _assert_common_svg(svg_root):
"""
Assert some common structures of the generated svg
:param svg_root:
:return:
"""
assert svg_root.get('id') == 'svg'
# jquery must be the first element because the next script need jquery
assert svg_root[0].get('id') == 'jquery'
assert svg_root[1].get('id') == 'my_javascript'
assert svg_root[2].get('id') == 'my_css'
def test_post_processor_insert_tag(post_processor):
"""
Test method insert_tag of the PostProcessor
:param post_processor:
:return:
"""
post_processor.insert_script_tag(0, attrib={'id': 'toto'})
assert post_processor.root[0].tag == 'script'
assert post_processor.root[0].get('id') == 'toto'
def test_post_processor_remove_title(post_processor):
"""
Test method _remove_title of the PostProcessor
:param post_processor:
:return:
"""
post_processor._remove_title()
root = post_processor.root
resultats = root.xpath("ns:g[@id='graph0']/ns:title", namespaces={'ns': SVG_NAMESPACE})
assert len(resultats) == 0
def test_post_processor_write(post_processor, tmpdir):
"""
Test method write of the PostProcessor
:param post_processor:
:return:
"""
svg_post_proccessed_path = tmpdir.join("test_post_processor_write.svg")
post_processor.write(output_filename=svg_post_proccessed_path.strpath)
assert svg_post_proccessed_path.check(file=1)
@pytest.mark.parametrize("post_processor", [SIMPLE_PLAYBOOK_SVG], indirect=True)
def test_post_processor_without_graph_representation(post_processor, tmpdir):
"""
Test the post processor without a graph representation
:param post_processor:
:param tmpdir:
:return:
"""
svg_post_proccessed_path = tmpdir.join("simple_playbook_postproccess_no_graph.svg")
post_processor.post_process()
post_processor.write(output_filename=svg_post_proccessed_path.strpath)
assert svg_post_proccessed_path.check(file=1)
root = etree.parse(svg_post_proccessed_path.strpath).getroot()
_assert_common_svg(root)
# no links should be in the svg when there is no graph_representation
assert len(root.xpath("//ns:links", namespaces={'ns': SVG_NAMESPACE})) == 0
@pytest.mark.parametrize("post_processor", [SIMPLE_PLAYBOOK_SVG], indirect=True)
def test_post_processor_with_graph_representation(post_processor, tmpdir):
"""
Test the post processor a graph representation
:param post_processor:
:param tmpdir:
:return:
"""
graph_represention = GraphRepresentation()
svg_post_proccessed_path = tmpdir.join("simple_playbook_postproccess_graph.svg")
play_id = "play_hostsall"
# link from play to task edges
graph_represention.add_link(play_id, "play_hostsallpost_taskPosttask1")
graph_represention.add_link(play_id, "play_hostsallpost_taskPosttask2")
post_processor.post_process(graph_represention)
post_processor.write(output_filename=svg_post_proccessed_path.strpath)
assert svg_post_proccessed_path.check(file=1)
root = etree.parse(svg_post_proccessed_path.strpath).getroot()
_assert_common_svg(root)
assert len(root.xpath("ns:g/*[@id='%s']//ns:link" % play_id, namespaces={'ns': SVG_NAMESPACE})) == 2