view tests/conftest.py @ 347:76c838453dbe

tests: Support for YAML-based baking tests. Convert old code-based ones.
author Ludovic Chabant <ludovic@chabant.com>
date Wed, 15 Apr 2015 16:39:35 -0700
parents 1cd67680c38c
children 1f22d4b10fef
line wrap: on
line source

import sys
import pprint
import os.path
import logging
import pytest
import yaml
from piecrust.configuration import merge_dicts
from .mockutil import mock_fs, mock_fs_scope


def pytest_runtest_setup(item):
    pass


def pytest_addoption(parser):
    parser.addoption(
            '--log-debug',
            action='store_true',
            help="Sets the PieCrust logger to output debug info to stdout.")


def pytest_configure(config):
    if config.getoption('--log-debug'):
        hdl = logging.StreamHandler(stream=sys.stdout)
        logging.getLogger('piecrust').addHandler(hdl)
        logging.getLogger('piecrust').setLevel(logging.DEBUG)


def pytest_collect_file(parent, path):
    if path.ext == ".bake" and path.basename.startswith("test"):
        return BakeTestFile(path, parent)


class BakeTestFile(pytest.File):
    def collect(self):
        spec = yaml.load_all(self.fspath.open())
        for i, item in enumerate(spec):
            name = '%s_%d' % (self.fspath.basename, i)
            if 'test_name' in item:
                name += '_%s' % item['test_name']
            yield BakeTestItem(name, self, item)


class BakeTestItem(pytest.Item):
    def __init__(self, name, parent, spec):
        super(BakeTestItem, self).__init__(name, parent)
        self.spec = spec

    def runtest(self):
        fs = mock_fs()

        # Website config.
        config = {
                'site': {
                    'default_format': 'none',
                    'default_page_layout': 'none',
                    'default_post_layout': 'none'}
                }
        test_config = self.spec.get('config')
        if test_config is not None:
            merge_dicts(config, test_config)
        fs.withConfig(config)

        # Input file-system.
        input_files = self.spec.get('in')
        if input_files is not None:
            _add_mock_files(fs, '/kitchen', input_files)

        # Output file-system.
        expected_output_files = self.spec.get('out')
        expected_partial_files = self.spec.get('outfiles')

        # Bake!
        from piecrust.baking.baker import Baker
        with mock_fs_scope(fs):
            out_dir = fs.path('kitchen/_counter')
            app = fs.getApp()
            baker = Baker(app, out_dir)
            baker.bake()

        if expected_output_files:
            actual = fs.getStructure('kitchen/_counter')
            error = _compare_dicts(actual, expected_output_files)
            if error:
                raise ExpectedBakeOutputError(error)

        if expected_partial_files:
            for key, content in expected_partial_files.items():
                try:
                    actual = fs.getStructure('kitchen/_counter/' +
                                             key.lstrip('/'))
                except Exception:
                    raise ExpectedBakeOutputError([
                        "Missing expected output file: %s" % key])
                if not isinstance(actual, str):
                    raise ExpectedBakeOutputError([
                        "Expected output file is a directory: %s" % key])
                if actual != content:
                    raise ExpectedBakeOutputError([
                        "Unexpected output file contents:",
                        "%s: %s" % (key, content),
                        "%s: %s" % (key, actual)])

    def reportinfo(self):
        return self.fspath, 0, "bake: %s" % self.name

    def repr_failure(self, excinfo):
        if isinstance(excinfo.value, ExpectedBakeOutputError):
            return ('\n'.join(excinfo.value.args[0]))
        return super(BakeTestItem, self).repr_failure(excinfo)


class ExpectedBakeOutputError(Exception):
    pass


def _add_mock_files(fs, parent_path, spec):
    for name, subspec in spec.items():
        path = os.path.join(parent_path, name)
        if isinstance(subspec, str):
            fs.withFile(path, subspec)
        elif isinstance(subspec, dict):
            _add_mock_files(fs, path, subspec)


def _compare_dicts(left, right, basepath=''):
    key_diff = set(left.keys()) ^ set(right.keys())
    if key_diff:
        extra_left = set(left.keys()) - set(right.keys())
        if extra_left:
            return (["Left contains more items: "] +
                    ['- %s/%s' % (basepath, k) for k in extra_left])
        extra_right = set(right.keys()) - set(left.keys())
        if extra_right:
            return (["Right contains more items: "] +
                    ['- %s/%s' % (basepath, k) for k in extra_right])
        return ["Unknown difference"]

    for key in left.keys():
        lv = left[key]
        rv = right[key]
        childpath = basepath + '/' + key
        if type(lv) != type(rv):
            return (["Different items: ",
                     "%s/%s: %s" % (basepath, key, pprint.pformat(lv)),
                     "%s/%s: %s" % (basepath, key, pprint.pformat(rv))])

        if isinstance(lv, dict):
            r = _compare_dicts(lv, rv, childpath)
            if r:
                return r
        elif isinstance(lv, list):
            r = _compare_lists(lv, rv, childpath)
            if r:
                return r
        elif lv != rv:
            return (["Different items: ",
                     "%s/%s: %s" % (basepath, key, pprint.pformat(lv)),
                     "%s/%s: %s" % (basepath, key, pprint.pformat(rv))])
    return None


def _compare_lists(left, right):
    for i in range(min(len(left), len(right))):
        l = left[i]
        r = right[i]
        if type(l) != type(r):
            return ['Different items at index %d:' % i,
                    pprint.pformat(l),
                    pprint.pformat(r)]
        if isinstance(l, dict):
            r = _compare_dicts(l, r)
            if r:
                return r
        elif isinstance(l, list):
            r = _compare_lists(l, r)
            if r:
                return r
        elif l != r:
            return ['Different items at index %d:' % i,
                    pprint.pformat(l),
                    pprint.pformat(r)]
    return None