Quickstart

The goals of this section of the guide are to provide a detailed explaination of how to use some of the basic OpenCafe components, as well as to explain what types of problems are solved by using OpenCafe. Further details on each component can be found in the rest of the documentation.

Installation

To get started, We should install and initialize OpenCafe. We can do this by running the following commands:

pip install opencafe
cafe-config init

Making HTTP Requests

As an example, we can write a few tests for the GitHub API. For the sake of this example, we will assume that we don’t have language bindings or SDKs for our API which is common for APIs in development. We can create a simple client using the BaseHTTPClient class provided by the OpenCafe HTTP plugin, which is a lightweight wrapper for the requests package. First, we’ll need to install the HTTP plugin:

cafe-config plugin install http

Now we can create a simple python script to request the details of a GitHub issue:

import json
import os

from cafe.engine.http.client import BaseHTTPClient

# Opencafe normally expects for a configuration data file to be set beforeit is
# run. For these examples this isn't necessary, but the value still needs to be set.
# This is behavior that we should fix, but hasn't been at the time this guide was written
os.environ['CAFE_ENGINE_CONFIG_FILE_PATH']='.'
client = BaseHTTPClient()
response = client.get('https://api.github.com/repos/cafehub/opencafe/issues/42')
print json.dumps(response.json(), indent=2)

This will generate some warnings that we will address later, but the outcome should be similar to the following:

{
    "labels": [],
    "number": 42,
    "assignee": null,
    "repository_url": "https://api.github.com/repos/CafeHub/opencafe",
    "closed_at": "2017-02-28T16:32:28Z",
    "id": 210568122,
    "title": "Adds a Travis CI config to run tox",
    "pull_request": {
        "url": "https://api.github.com/repos/CafeHub/opencafe/pulls/42",
        "diff_url": "https://github.com/CafeHub/opencafe/pull/42.diff",
        "html_url": "https://github.com/CafeHub/opencafe/pull/42",
        "patch_url": "https://github.com/CafeHub/opencafe/pull/42.patch"
    },
    "comments": 0,
    "state": "closed",
    "body": "",
    "labels_url": "https://api.github.com/repos/CafeHub/opencafe/issues/42/labels{/name}",
    "events_url": "https://api.github.com/repos/CafeHub/opencafe/issues/42/events",
    "comments_url": "https://api.github.com/repos/CafeHub/opencafe/issues/42/comments",
    "html_url": "https://github.com/CafeHub/opencafe/pull/42",
    "updated_at": "2017-02-28T16:32:28Z",
    "user": {
        "following_url": "https://api.github.com/users/dwalleck/following{/other_user}",
        "events_url": "https://api.github.com/users/dwalleck/events{/privacy}",
        "organizations_url": "https://api.github.com/users/dwalleck/orgs",
        "url": "https://api.github.com/users/dwalleck",
        "gists_url": "https://api.github.com/users/dwalleck/gists{/gist_id}",
        "html_url": "https://github.com/dwalleck",
        "subscriptions_url": "https://api.github.com/users/dwalleck/subscriptions",
        "avatar_url": "https://avatars2.githubusercontent.com/u/843116?v=3",
        "repos_url": "https://api.github.com/users/dwalleck/repos",
        "received_events_url": "https://api.github.com/users/dwalleck/received_events",
        "gravatar_id": "",
        "starred_url": "https://api.github.com/users/dwalleck/starred{/owner}{/repo}",
        "site_admin": false,
        "login": "dwalleck",
        "type": "User",
        "id": 843116,
        "followers_url": "https://api.github.com/users/dwalleck/followers"
    },
    "milestone": null,
    "closed_by": {
        "following_url": "https://api.github.com/users/jidar/following{/other_user}",
        "events_url": "https://api.github.com/users/jidar/events{/privacy}",
        "organizations_url": "https://api.github.com/users/jidar/orgs",
        "url": "https://api.github.com/users/jidar",
        "gists_url": "https://api.github.com/users/jidar/gists{/gist_id}",
        "html_url": "https://github.com/jidar",
        "subscriptions_url": "https://api.github.com/users/jidar/subscriptions",
        "avatar_url": "https://avatars2.githubusercontent.com/u/1134139?v=3",
        "repos_url": "https://api.github.com/users/jidar/repos",
        "received_events_url": "https://api.github.com/users/jidar/received_events",
        "gravatar_id": "",
        "starred_url": "https://api.github.com/users/jidar/starred{/owner}{/repo}",
        "site_admin": false,
        "login": "jidar",
        "type": "User",
        "id": 1134139,
        "followers_url": "https://api.github.com/users/jidar/followers"
    },
    "locked": false,
    "url": "https://api.github.com/repos/CafeHub/opencafe/issues/42",
    "created_at": "2017-02-27T18:34:21Z",
    "assignees": []
}

The BaseHTTPClient returns the same response that requests would, so we can treat the response similarly to view its content. At this point, it doesn’t look like the OpenCafe HTTP plugin is adding any more value than requests would. Let’s see what we can do about that. First, let’s setup logging and see what happens.

import json
import logging
import os
import sys

from cafe.engine.http.client import BaseHTTPClient
from cafe.common.reporting import cclogging


os.environ['CAFE_ENGINE_CONFIG_FILE_PATH']='.'
cclogging.init_root_log_handler()
root_log = logging.getLogger()
root_log.addHandler(logging.StreamHandler(stream=sys.stderr))
root_log.setLevel(logging.DEBUG)

client = BaseHTTPClient()
response = client.get('https://api.github.com/repos/cafehub/opencafe/issues/42')

The cclogging package simplifies parts of working with the standard Python logger, such as creating and initializing a logger. With logging enabled, let’s execute our script again to see the difference.

(cafe-demo) dwalleck@MINERVA:~$ python demo.py
Environment variable 'CAFE_MASTER_LOG_FILE_NAME' is not set. A null root log handler will be used, no logs will be written.(<cafe.engine.http.client.BaseHTTPClient object at 0x7fd2a58cf550>, 'GET', 'https://api.github.com/repos/cafehub/opencafe/issues/42') {}
No section: 'PLUGIN.HTTP'.  Using default value '0' instead
Starting new HTTPS connection (1): api.github.com
https://api.github.com:443 "GET /repos/cafehub/opencafe/issues/42 HTTP/1.1" 200 None

------------
REQUEST SENT
------------
request method..: GET
request url.....: https://api.github.com/repos/cafehub/opencafe/issues/42
request params..:
request headers.: {'Connection': 'keep-alive', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'User-Agent': 'python-requests/2.13.0'}
request body....: None


-----------------
RESPONSE RECEIVED
-----------------
response status..: <Response [200]>
response time....: 0.35421204567
response headers.: {'X-XSS-Protection': '1; mode=block', 'Content-Security-Policy': "default-src 'none'", 'Access-Control-Expose-Headers': 'ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval', 'Transfer-Encoding': 'chunked', 'Last-Modified': 'Thu, 13 Apr 2017 19:13:26 GMT', 'Access-Control-Allow-Origin': '*', 'X-Frame-Options': 'deny', 'Status': '200 OK', 'X-Served-By': 'eef8b8685a106934dcbb4b7c59fba0bf', 'X-GitHub-Request-Id': 'FA86:30F6:B12CE5:ED8475:58F8F029', 'ETag': 'W/"2fbeb849316f7b18e9138ea40d150441"', 'Date': 'Thu, 20 Apr 2017 17:30:17 GMT', 'X-RateLimit-Remaining': '59', 'Strict-Transport-Security': 'max-age=31536000; includeSubdomains; preload', 'Server': 'GitHub.com', 'X-GitHub-Media-Type': 'github.v3; format=json', 'X-Content-Type-Options': 'nosniff', 'Content-Encoding': 'gzip', 'Vary': 'Accept, Accept-Encoding', 'X-RateLimit-Limit': '60', 'Cache-Control': 'public, max-age=60, s-maxage=60', 'Content-Type': 'application/json; charset=utf-8', 'X-RateLimit-Reset': '1492713017'}
response body....: {"url":"https://api.github.com/repos/CafeHub/opencafe/issues/42","repository_url":"https://api.github.com/repos/CafeHub/opencafe","labels_url":"https://api.github.com/repos/CafeHub/opencafe/issues/42/labels{/name}","comments_url":"https://api.github.com/repos/CafeHub/opencafe/issues/42/comments","events_url":"https://api.github.com/repos/CafeHub/opencafe/issues/42/events","html_url":"https://github.com/CafeHub/opencafe/pull/42","id":210568122,"number":42,"title":"Adds a Travis CI config to run tox","user":{"login":"dwalleck","id":843116,"avatar_url":"https://avatars2.githubusercontent.com/u/843116?v=3","gravatar_id":"","url":"https://api.github.com/users/dwalleck","html_url":"https://github.com/dwalleck","followers_url":"https://api.github.com/users/dwalleck/followers","following_url":"https://api.github.com/users/dwalleck/following{/other_user}","gists_url":"https://api.github.com/users/dwalleck/gists{/gist_id}","starred_url":"https://api.github.com/users/dwalleck/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dwalleck/subscriptions","organizations_url":"https://api.github.com/users/dwalleck/orgs","repos_url":"https://api.github.com/users/dwalleck/repos","events_url":"https://api.github.com/users/dwalleck/events{/privacy}","received_events_url":"https://api.github.com/users/dwalleck/received_events","type":"User","site_admin":false},"labels":[],"state":"closed","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":0,"created_at":"2017-02-27T18:34:21Z","updated_at":"2017-02-28T16:32:28Z","closed_at":"2017-02-28T16:32:28Z","pull_request":{"url":"https://api.github.com/repos/CafeHub/opencafe/pulls/42","html_url":"https://github.com/CafeHub/opencafe/pull/42","diff_url":"https://github.com/CafeHub/opencafe/pull/42.diff","patch_url":"https://github.com/CafeHub/opencafe/pull/42.patch"},"body":"","closed_by":{"login":"jidar","id":1134139,"avatar_url":"https://avatars2.githubusercontent.com/u/1134139?v=3","gravatar_id":"","url":"https://api.github.com/users/jidar","html_url":"https://github.com/jidar","followers_url":"https://api.github.com/users/jidar/followers","following_url":"https://api.github.com/users/jidar/following{/other_user}","gists_url":"https://api.github.com/users/jidar/gists{/gist_id}","starred_url":"https://api.github.com/users/jidar/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jidar/subscriptions","organizations_url":"https://api.github.com/users/jidar/orgs","repos_url":"https://api.github.com/users/jidar/repos","events_url":"https://api.github.com/users/jidar/events{/privacy}","received_events_url":"https://api.github.com/users/jidar/received_events","type":"User","site_admin":false}}
-------------------------------------------------------------------------------

That’s a little better. We get a verbose log entry for the details of request made and the response we received. The output from the HTTP client is meant to be human readable and to create an audit trail of what occurred while a test or script was executed.

Creating a Basic Application Client

Now let’s add a few more requests to our script:

import json
import logging
import os
import sys

from cafe.engine.http.client import BaseHTTPClient
from cafe.common.reporting import cclogging


os.environ['CAFE_ENGINE_CONFIG_FILE_PATH']='.'
cclogging.init_root_log_handler()
root_log = logging.getLogger()
root_log.addHandler(logging.StreamHandler(stream=sys.stderr))
root_log.setLevel(logging.DEBUG)


client = BaseHTTPClient()
response = client.get('https://api.github.com/repos/cafehub/opencafe/issues/42')
response = client.get('https://api.github.com/repos/cafehub/opencafe/commits')
response = client.get('https://api.github.com/repos/cafehub/opencafe/forks')

As we make more requests, a few concerns come to mind. Right now we are hard-coding the base url (https://api.github.com) in each request. The organization and project names are both something that could change. At the very least, we should factor out what is common between the requests or what is likely to change as we grow this script:

import json
import logging
import os
import sys

from cafe.engine.http.client import BaseHTTPClient
from cafe.common.reporting import cclogging


os.environ['CAFE_ENGINE_CONFIG_FILE_PATH']='.'
cclogging.init_root_log_handler()
root_log = logging.getLogger()
root_log.addHandler(logging.StreamHandler(stream=sys.stderr))
root_log.setLevel(logging.DEBUG)

client = BaseHTTPClient()
base_url = 'https://api.github.com'
organization = 'cafehub'
project = 'opencafe'
issue_id = 42

response = client.get(
    '{base_url}/repos/{org}/{project}/commits'.format(
        base_url=base_url, org=organization, project=project))

response = client.get(
    '{base_url}/repos/{org}/{project}/issues/{issue_id}'.format(
        base_url=base_url, org=organization, project=project,
        issue_id=issue_id))

response = client.get(
    '{base_url}/repos/{org}/{project}/forks'.format(
        base_url=base_url, org=organization, project=project))

The GitHub API is expansive, so we could go on for some time defining more requests. Rather than defining these in-line, defining these functions in a common class would make more sense from an organization sense.

import json
import logging
import os
import sys

from cafe.engine.clients.base import BaseClient
from cafe.engine.http.client import BaseHTTPClient
from cafe.common.reporting import cclogging


class GitHubClient(BaseHTTPClient):

    def __init__(self, base_url):
        super(GitHubClient, self).__init__()
        self.base_url = base_url

    def get_project_commits(self, org_name, project_name):
        return self.get(
            '{base_url}/repos/{org}/{project}/commits'.format(
                base_url=base_url, org=org_name, project=project))

    def get_issue_by_id(self, org_name, project_name, issue_id):
        return self.get(
            '{base_url}/repos/{org}/{project}/issues/{issue_id}'.format(
                base_url=base_url, org=org_name, project=project,
                issue_id=issue_id))

    def get_project_forks(self, org_name, project_name):
        return self.get(
            '{base_url}/repos/{org}/{project}/forks'.format(
                base_url=base_url, org=org_name, project=project))

os.environ['CAFE_ENGINE_CONFIG_FILE_PATH']='.'
cclogging.init_root_log_handler()
root_log = logging.getLogger()
root_log.addHandler(logging.StreamHandler(stream=sys.stderr))
root_log.setLevel(logging.DEBUG)

base_url = 'https://api.github.com'
organization = 'cafehub'
project = 'opencafe'
issue_id = 42
client = GitHubClient(base_url)

resp1 = client.get_project_commits(org_name=organization, project_name=project)
resp2 = client.get_issue_by_id(org_name=organization, project_name=project, issue_id=issue_id)
resp3 = client.get_project_forks(org_name=organization, project_name=project)

Our client subclasses the BaseHTTPClient, so there’s no longer a need to create an instance of the client. This creates the foundation for a simple language binding for our API under test.

Now that our HTTP requests are in better shape, let’s talk about dealing with the responses. The requests response object has a json method that will transform the body of the response into a Python dictionary. While treating the response content as a dictionary is good enough for quick scripts and possibly for working with very stable APIs, it has challenges that we should consider before going further.

Accessing the response as a dictionary isn’t too difficult when a response body has one or two properties, but let’s jump back to the first response output we looked at. It has dozens of properties, including ones that are nested. Using the response as-is requires memorizing the response structure or constantly referencing API documentation as you code. If you make a mistake with the name of a property, you may not find that out until you run the code. Also, when the name of one of the properties or the structure of the API response changes, this means tediously changing the property each place it is used or trying to do a string replace across the project, which is an error-prone process.

Writing Request and Response Models

An alternate approach is to deserialize the JSON response to an object. This is the approach that most SDKs and language bindings use. This greatly simplifies refactoring of response properties and has the added bonus of error detection by linters if you use an invalid property name. If you’re using a code editor which offers autocomplete functionality, you can also use that when developing new tests, which removes most of the need to reference API documentation after you’ve done the groundwork developing the response models. Here’s an example of what the response model for our first request would look like:

class Issue(AutoMarshallingModel):

    def __init__(self, url, repository_url, labels_url, comments_url, events_url,
                html_url, id, number, title, user, labels, state, locked,
                assignee, assignees, milestone, comments, created_at,
                updated_at, closed_at, body, closed_by):

        self.url = url
        self.repository_url = repository_url
        self.labels_url = labels_url
        self.comments_url = comments_url
        self.events_url = events_url
        self.html_url = html_url
        self.id = id
        self.number = number
        self.title = title
        self.user = user
        self.labels = labels
        self.state = state
        self.locked = locked
        self.assignee = assignee
        self.assignees = assignees
        self.milestone = milestone
        self.comments = comments
        self.created_at = created_at
        self.updated_at = updated_at
        self.closed_at = closed_at
        self.body = body
        self.closed_by = closed_by

    @classmethod
    def _json_to_obj(cls, serialized_str):
        resp_dict = json.loads(serialized_str)
        user = User(**resp_dict.get('user'))

        assignees = []
        for assignee in resp_dict.get('assignees'):
            assignees.append(User(**assignee))

        assignee = None
        if resp_dict.get('assignee'):
            assignee = User(**resp_dict.get('assignee'))

        labels = []
        for label in labels:
            labels.append(Label(**label))

        return Issue(
            url=resp_dict.get('url'),
            repository_url=resp_dict.get('repository_url'),
            labels_url=resp_dict.get('labels_url'),
            comments_url=resp_dict.get('comments_url'),
            events_url=resp_dict.get('events_url'),
            html_url=resp_dict.get('html_url'),
            id=resp_dict.get('id'),
            number=resp_dict.get('number'),
            title=resp_dict.get('title'),
            user=user,
            labels=labels,
            state=resp_dict.get('state'),
            locked=resp_dict.get('locked'),
            assignee=assignee,
            assignees=assignees,
            milestone=resp_dict.get('milestone'),
            comments=resp_dict.get('comments'),
            created_at=resp_dict.get('created_at'),
            updated_at=resp_dict.get('updated_at'),
            closed_at=resp_dict.get('closed_at'),
            body=resp_dict.get('body'),
            closed_by=resp_dict.get('closed_by'))


class User(AutoMarshallingModel):

    def __init__(self, login, id, avatar_url, gravatar_id, url, html_url,
                followers_url, following_url, gists_url, starred_url,
                subscriptions_url, organizations_url, repos_url, events_url,
                received_events_url, type, site_admin):

        self.login = login
        self.id = id
        self.avatar_url = avatar_url
        self.gravatar_id = gravatar_id
        self.url = url
        self.html_url = html_url
        self.followers_url = followers_url
        self.following_url = following_url
        self.gists_url = gists_url
        self.starred_url = starred_url
        self.subscriptions_url = subscriptions_url
        self.organizations_url = organizations_url
        self.repos_url = repos_url
        self.events_url = events_url
        self.received_events_url = received_events_url
        self.type = type
        self.site_admin = site_admin

    @classmethod
    def _json_to_obj(cls, serialized_str):
        resp_dict = json.loads(serialized_str)
        return User(**resp_dict)


class Label(AutoMarshallingModel):

    def __init__(self, id, url, name, color, default):

        self.id = id
        self.url = url
        self.name = name
        self.color = color
        self.default = default

    @classmethod
    def _json_to_obj(cls, serialized_str):
        resp_dict = json.loads(serialized_str)
        return Label(**resp_dict)

Any class that inherits from the AutoMarshallingModel class is expected to implement the _json_to_obj method, _obj_to_json method, or both. This depends on whether the model is being used to handle requests, responses, or both.

This example creates quite a bit of boilerplate code. We used an explicit example so that it would be easy to understand what this code does. However, because these objects are explicitly defined, static analysis tools will be able to assist us going forward. It also allows code editors that support Python autocompletion to work with our models. In more practical implementations, you may want to take advantage of Python’s dynamic nature to simplify the setting of properties.

Writing an Auto-Serializing Client

Now that we have response models, we can refactor our client to use them.

from cafe.engine.http.client import AutoMarshallingHTTPClient


class GitHubClient(AutoMarshallingHTTPClient):

    def __init__(self, base_url):
        super(GitHubClient, self).__init__(
            serialize_format='json', deserialize_format='json')
        self.base_url = base_url

    def get_issue_by_id(self, org_name, project_name, issue_id):
        url = '{base_url}/repos/{org}/{project}/issues/{issue_id}'.format(
            base_url=self.base_url, org=organization, project=project,
            issue_id=issue_id)
        return self.get(url, response_entity_type=Issue)

There’s a few changes to note. The AutoMarshallingHTTPClient class replaces BaseHTTPClient as the parent class because it is aware of request and response content types. The response_entity_type parameter defines what type to expect the response to be. This together with serialization formats set when the client was instantiated determine which serialization methods are called on the response contents. This can be used to create a single API client that can handle both JSON and XML response types. This can be an extremely useful capability to have when you want to write code a single that is able to test both the JSON and XML capabilities of an API.

Managing Test Data

Before we start writing our tests, let’s step back and deal with one more issue. In the original code, we had statically defined certain data such as the GitHub URL, the organization name, and the project name. There are many reasons why you should not hardcode these types of values in your code. Of those, the most important to us is that we should not have to make code changes whenever we want to use different test data. We should be able to provide the test data at runtime, which allows our code to be more flexible and portable.

There are many sources we could use for our test data, but for this example we will use a plain text file with headers that can be parsed by Python’s SafeConfigParser. For this to work, we will need to create a class that represents the data that we want to store in the file.

from cafe.engine.models.data_interfaces import ConfigSectionInterface


class GitHubConfig(ConfigSectionInterface):

    SECTION_NAME = 'GitHub'

    @property
    def base_url(self):
        return self.get('base_url')

    @property
    def organization(self):
        return self.get('organization')

    @property
    def project(self):
        return self.get('project')

    @property
    def issue_id(self):
        return self.get('issue_id')

Note that there is nothing in this class that explicitly states the type of the data source. This is because the OpenCafe data_interfaces package provides a generic interface for data sources including environment variables and JSON data. For the purpose of this guide, we will just use plain text files.

Our class defines that there should have a section titled GitHub in our configuration file with four properties. The actual configuration file would look similar to the following example:

[GitHub]
base_url = https://api.github.com
organization = cafehub
project = opencafe
issue_id = 42

Writing and Running a Test

From this point in the demo, you can use the opencafe-demo project to follow along with the guide if you want to execute the steps yourself.

Now that we have our test infrastructure in order, we can write several tests to see how OpenCafe operates.

from cafe.drivers.unittest.fixtures import BaseTestFixture

from opencafe_demo.github.github_client import GitHubClient
from opencafe_demo.github.github_config import GitHubConfig


class BasicGitHubTest(BaseTestFixture):

    @classmethod
    def setUpClass(cls):
        super(BasicGitHubTest, cls).setUpClass()  # Sets up logging/reporting for the test
        cls.config_data = GitHubConfig()

        cls.organization = cls.config_data.organization
        cls.project = cls.config_data.project
        cls.issue_id = cls.config_data.issue_id
        cls.client = GitHubClient(cls.config_data.base_url)

    def test_get_issue_response_code_is_200(self):
        response = self.client.get_project_issue(
            self.organization, self.project, self.issue_id)
        self.assertEqual(response.status_code, 200)

    def test_id_is_not_null_for_get_issue_request(self):
        response = self.client.get_project_issue(
            self.organization, self.project, self.issue_id)
        # The response signature is the raw response from Requests except
        # for the `entity` property, which is the object that represents
        # the response content
        issue = response.entity
        self.assertIsNotNone(issue.id)

In this test class, we inherit from OpenCafe’s BaseTestFixture class. This base class automatically handles all of the logging setup that we were previously doing by hand. The BaseTestFixture class inherits from Python’s unittest.TestCase, so for all intents and purposes it behaves the same as any other unittest-based test.

Before we can run this test, we need to get our configuration data file in place. When we executed the cafe-config init command at the start of the guide, you may have noticed in the output that several directories were created. You should now have a .opencafe directory, which is where all configuration data and test logs will be stored by default (these paths can be changed in the .opencafe/engine.config file. See the full documentation for further details). We will need to create a directory named GitHub in which we will put our configuration file which we will call prod.config. The names used are arbitrary, but they create a convention that will be used when we begin running our tests.

OpenCafe uses a convention based on <product-name> and <config-file-name> for finding configuration data and setting logging locations. For configuration files, the <config-file-name> file will be loaded from the .opencafe/configs/<product-name> directory. For logging, logs for each test run will be saved in a unique directory named by the date time stamp of when the tests were run in the .opencafe/logs/<product-name>/<config-file-name> directory.

For this guide, I’ll be using OpenCafe’s unittest-based runner to execute the tests. All the tests in the github project can be run by executing cafe-runner github prod.config.

(cafe-demo) dwalleck@minerva:~$ cafe-runner github prod.config

    ( (
    ) )
.........
|       |___
|       |_  |
|  :-)  |_| |
|       |___|
|_______|
=== CAFE Runner ===
======================================================================================================================================================
Percolated Configuration
------------------------------------------------------------------------------------------------------------------------------------------------------
BREWING FROM: ....: /home/dwalleck/cafe-demo/local/lib/python2.7/site-packages/opencafe_demo
ENGINE CONFIG FILE: /home/dwalleck/cafe-demo/.opencafe/engine.config
TEST CONFIG FILE..: /home/dwalleck/cafe-demo/.opencafe/configs/github/prod.config
DATA DIRECTORY....: /home/dwalleck/cafe-demo/.opencafe/data
LOG PATH..........: /home/dwalleck/cafe-demo/.opencafe/logs/github/prod.config/2017-04-19_11_26_38.698599
======================================================================================================================================================
test_get_issue_response_code_is_200 (opencafe_demo.github.test_issues_api.BasicGitHubTest) ... ok
test_id_is_not_null_for_get_issue_request (opencafe_demo.github.test_issues_api.BasicGitHubTest) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.543s

OK
======================================================================================================================================================
Detailed logs: /home/dwalleck/cafe-demo/.opencafe/logs/github/prod.config/2017-04-19_11_26_38.698599
------------------------------------------------------------------------------------------------------------------------------------------------------

The preamble output from the test runner pretty prints the location of all configuration files used for the test run, as well as the the location of the logs generated during the test run. Here’s what the contents of the log directory look like:

(cafe-demo) dwalleck@minerva:~$ cd /home/dwalleck/cafe-demo/.opencafe/logs/github/prod.config/2017-04-19_11_26_38.698599
(cafe-demo) dwalleck@minerva:~/cafe-demo/.opencafe/logs/github/prod.config/2017-04-19_11_26_38.698599$ ls -la
total 36
drwxrwxrwx 0 dwalleck dwalleck   512 Apr 19 11:26 .
drwxrwxrwx 0 dwalleck dwalleck   512 Apr 19 11:26 ..
-rw-rw-rw- 1 dwalleck dwalleck 15613 Apr 19 11:26 cafe.master.log
-rw-rw-rw- 1 dwalleck dwalleck 15353 Apr 19 11:26 opencafe_demo.github.test_issues_api.BasicGitHubTest.log

Two log files were generated by this test run. The second log file is named by the full package name of the test class that was run. If there had been multiple test classes loaded for execution, there would be one file per class run. The benefit of this is to be able to jump directly to the log file that you are interested in inspecting. The contents of the logs contain the HTTP requests made during test execution, but they also contain headers to mark what point the in the lifecycle of the test is being executed:

2017-04-19 11:26:38,838: INFO: root: ========================================================
2017-04-19 11:26:38,840: INFO: root: Fixture......: opencafe_demo.github.test_issues_api.BasicGitHubTest
2017-04-19 11:26:38,840: INFO: root: Created At...: 2017-04-19 11:26:38.838285
2017-04-19 11:26:38,840: INFO: root: ========================================================
2017-04-19 11:26:38,842: INFO: root: ========66================================================
2017-04-19 11:26:38,842: INFO: root: Test Case....: test_get_issue_response_code_is_200
2017-04-19 11:26:38,843: INFO: root: Created At...: 2017-04-19 11:26:38.838285
2017-04-19 11:26:38,843: INFO: root: No Test description.
2017-04-19 11:26:38,843: INFO: root: ========================================================

The other file, cafe.master.log is a summation of the other log files in the order the tests were executed. This allows the user to consume the logs however they find easiest.