Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move parser tests from rtd-build repo #4225

Merged
merged 13 commits into from
Jun 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added readthedocs/config/__init__.py
Empty file.
22 changes: 22 additions & 0 deletions readthedocs/config/find.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Helper functions to search files."""

from __future__ import division, print_function, unicode_literals

import os


def find_all(path, filenames):
"""Find all files in ``path`` that match in ``filenames``."""
path = os.path.abspath(path)
for root, dirs, files in os.walk(path, topdown=True):
dirs.sort()
for filename in filenames:
if filename in files:
yield os.path.abspath(os.path.join(root, filename))


def find_one(path, filenames):
"""Find the first file in ``path`` that match in ``filenames``."""
for _path in find_all(path, filenames):
return _path
return ''
34 changes: 34 additions & 0 deletions readthedocs/config/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
"""YAML parser for the RTD configuration file."""

from __future__ import division, print_function, unicode_literals

import yaml

__all__ = ('parse', 'ParseError')


class ParseError(Exception):

"""Parser related errors."""

pass


def parse(stream):
"""
Take file-like object and return a list of project configurations.

The files need be valid YAML and only contain mappings as documents.
Everything else raises a ``ParseError``.
"""
try:
configs = list(yaml.safe_load_all(stream))
except yaml.YAMLError as error:
raise ParseError('YAML: {message}'.format(message=error))
if not configs:
raise ParseError('Empty config')
for config in configs:
if not isinstance(config, dict):
raise ParseError('Expected mapping')
return configs
Empty file.
96 changes: 96 additions & 0 deletions readthedocs/config/tests/test_find.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from __future__ import division, print_function, unicode_literals

import os

import pytest
import six

from readthedocs.config.find import find_all, find_one

from .utils import apply_fs


def test_find_no_files(tmpdir):
with tmpdir.as_cwd():
paths = list(find_all(os.getcwd(), ('readthedocs.yml',)))
assert len(paths) == 0


def test_find_at_root(tmpdir):
apply_fs(tmpdir, {'readthedocs.yml': '', 'otherfile.txt': ''})

base = str(tmpdir)
paths = list(find_all(base, ('readthedocs.yml',)))
assert paths == [
os.path.abspath(os.path.join(base, 'readthedocs.yml'))
]


def test_find_nested(tmpdir):
apply_fs(tmpdir, {
'first': {
'readthedocs.yml': '',
},
'second': {
'confuser.txt': 'content',
},
'third': {
'readthedocs.yml': 'content',
'Makefile': '',
},
})
apply_fs(tmpdir, {'first/readthedocs.yml': ''})

base = str(tmpdir)
paths = list(find_all(base, ('readthedocs.yml',)))
assert set(paths) == set([
str(tmpdir.join('first', 'readthedocs.yml')),
str(tmpdir.join('third', 'readthedocs.yml')),
])


def test_find_multiple_files(tmpdir):
apply_fs(tmpdir, {
'first': {
'readthedocs.yml': '',
'.readthedocs.yml': 'content',
},
'second': {
'confuser.txt': 'content',
},
'third': {
'readthedocs.yml': 'content',
'Makefile': '',
},
})
apply_fs(tmpdir, {'first/readthedocs.yml': ''})

base = str(tmpdir)
paths = list(find_all(base, ('readthedocs.yml',
'.readthedocs.yml')))
assert paths == [
str(tmpdir.join('first', 'readthedocs.yml')),
str(tmpdir.join('first', '.readthedocs.yml')),
str(tmpdir.join('third', 'readthedocs.yml')),
]

paths = list(find_all(base, ('.readthedocs.yml',
'readthedocs.yml')))
assert paths == [
str(tmpdir.join('first', '.readthedocs.yml')),
str(tmpdir.join('first', 'readthedocs.yml')),
str(tmpdir.join('third', 'readthedocs.yml')),
]


@pytest.mark.skipif(not six.PY2, reason='Only for python2')
def test_find_unicode_path(tmpdir):
base_path = os.path.abspath(
os.path.join(os.path.dirname(__file__), 'fixtures/bad_encode_project')
)
path = find_one(base_path, ('readthedocs.yml',))
assert path == ''
unicode_base_path = base_path.decode('utf-8')
assert isinstance(unicode_base_path, unicode)
path = find_one(unicode_base_path, ('readthedocs.yml',))
assert path == ''
56 changes: 56 additions & 0 deletions readthedocs/config/tests/test_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from __future__ import division, print_function, unicode_literals

from io import StringIO

from pytest import raises

from readthedocs.config.parser import ParseError, parse


def test_parse_empty_config_file():
buf = StringIO(u'')
with raises(ParseError):
parse(buf)


def test_parse_invalid_yaml():
buf = StringIO(u'- - !asdf')
with raises(ParseError):
parse(buf)


def test_parse_bad_type():
buf = StringIO(u'Hello')
with raises(ParseError):
parse(buf)


def test_parse_single_config():
buf = StringIO(u'base: path')
config = parse(buf)
assert isinstance(config, list)
assert len(config) == 1
assert config[0]['base'] == 'path'


def test_parse_empty_list():
buf = StringIO(u'base: []')
config = parse(buf)
assert config[0]['base'] == []


def test_parse_multiple_configs_in_one_file():
buf = StringIO(
u'''
base: path
---
base: other_path
name: second
nested:
works: true
''')
configs = parse(buf)
assert isinstance(configs, list)
assert len(configs) == 2
assert configs[0]['base'] == 'path'
assert configs[1]['nested'] == {'works': True}
28 changes: 28 additions & 0 deletions readthedocs/config/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from __future__ import division, print_function, unicode_literals

from .utils import apply_fs


def test_apply_fs_with_empty_contents(tmpdir):
# Doesn't do anything if second paramter is empty.
apply_fs(tmpdir, {})
assert tmpdir.listdir() == []


def test_apply_fs_create_empty_file(tmpdir):
# Create empty file.
apply_fs(tmpdir, {'file': ''})
assert len(tmpdir.listdir()) == 1
assert tmpdir.join('file').read() == ''


def test_apply_fs_create_file_with_content(tmpdir):
# Create file with content.
apply_fs(tmpdir, {'file': 'content'})
assert tmpdir.join('file').read() == 'content'


def test_apply_fs_create_subdirectory(tmpdir):
# Create file with content.
apply_fs(tmpdir, {'subdir': {'file': 'content'}})
assert tmpdir.join('subdir', 'file').read() == 'content'
16 changes: 16 additions & 0 deletions readthedocs/config/tests/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from __future__ import division, print_function, unicode_literals


def apply_fs(tmpdir, contents):
"""
Create the directory structure specified in ``contents``. It's a dict of
filenames as keys and the file contents as values. If the value is another
dict, it's a subdirectory.
"""
for filename, content in contents.items():
if hasattr(content, 'items'):
apply_fs(tmpdir.mkdir(filename), content)
else:
file = tmpdir.join(filename)
file.write(content)
return tmpdir