diff --git a/.gitignore b/.gitignore index 3e1ba4a2caa..6ff593282ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.db +*.rdb *.egg-info *.log *.pyc diff --git a/readthedocs/rtd_tests/fixtures/spec/v2/schema.yml b/readthedocs/rtd_tests/fixtures/spec/v2/schema.yml new file mode 100644 index 00000000000..6570a895cd2 --- /dev/null +++ b/readthedocs/rtd_tests/fixtures/spec/v2/schema.yml @@ -0,0 +1,96 @@ +# Read the Docs configuration file + +# The version of the spec to be use +version: enum('2') + +# Formats of the documentation to be built +# Default: [] +formats: any(list(enum('htmlzip', 'pdf', 'epub')), enum('all'), required=False) + +# Configuration for Conda support +conda: include('conda', required=False) + +# Configuration for the documentation build process +build: include('build', required=False) + +# Configuration of the Python environment to be used +python: include('python', required=False) + +# Configuration for sphinx documentation +sphinx: include('sphinx', required=False) + +# Configuration for mkdocs documentation +mkdocs: include('mkdocs', required=False) + +# Submodules configuration +submodules: include('submodules', required=False) + +# Redirects for the current version to be built +# Key/value list, represent redirects of type `type` +# from url -> to url +# Default: null +redirects: map(enum('page'), map(str(), str()), required=False) + +--- + +conda: + # The path to the Conda environment file from the root of the project + environment: path() + +build: + # The build docker image to be used + # Default: 'latest' + image: enum('stable', 'latest', required=False) + +python: + # The Python version (this depends on the build image) + # Default: '3' + version: enum('2', '2.7', '3', '3.3', '3.4', '3.5', '3.6', required=False) + + # The path to the requirements file from the root of the project + # Default: null + requirements: path(required=False) + + # Install the project using python setup.py install or pip + # Default: null + install: enum('pip', 'setup.py', required=False) + + # Extra requirements sections to install in addition to the package dependencies + # Default: [] + extra_requirements: list(str(), required=False) + + # Give the virtual environment access to the global site-packages dir + # Default: false + system_packages: bool(required=False) + +sphinx: + # The path to the conf.py file + # Default: rtd will try to find it + configuration: path(required=False) + + # Add the -W option to sphinx-build + # Default: false + fail_on_warning: bool(required=False) + +mkdocs: + # The path to the mkdocs.yml file + # Default: rtd will try to find it + configuration: path(required=False) + + # Add the --strict optio to mkdocs build + # Default: false + fail_on_warning: bool(required=False) + + +submodules: + # List of submodules to be included + # Default: [] + include: any(list(str()), enum('all'), required=False) + + # List of submodules to be ignored + # Default: [] + exclude: any(list(str()), enum('all'), required=False) + + # Do a recursive clone? + # Default: false + recursive: bool(required=False) diff --git a/readthedocs/rtd_tests/tests/test_build_config.py b/readthedocs/rtd_tests/tests/test_build_config.py new file mode 100644 index 00000000000..73419c5cf48 --- /dev/null +++ b/readthedocs/rtd_tests/tests/test_build_config.py @@ -0,0 +1,557 @@ +from __future__ import division, print_function, unicode_literals + +from os import path + +import pytest +import six +import yamale +from readthedocs_build.testing import utils +from yamale.validators import DefaultValidators, Validator + +V2_SCHEMA = path.join( + path.dirname(__file__), + '../fixtures/spec/v2/schema.yml' +) + + +class PathValidator(Validator): + + """ + Path validator + + Checks if the given value is a string and a existing + file. + """ + + tag = 'path' + constraints = [] + configuration_file = '.' + + def _is_valid(self, value): + if isinstance(value, six.string_types): + file_ = path.join( + path.dirname(self.configuration_file), + value + ) + return path.exists(file_) + return False + + +def create_yaml(tmpdir, content): + fs = { + 'environment.yml': '', + 'mkdocs.yml': '', + 'rtd.yml': content, + 'docs': { + 'conf.py': '', + 'requirements.txt': '', + }, + } + utils.apply_fs(tmpdir, fs) + return path.join(tmpdir.strpath, 'rtd.yml') + + +def validate_schema(file): + validators = DefaultValidators.copy() + PathValidator.configuration_file = file + validators[PathValidator.tag] = PathValidator + + data = yamale.make_data(file) + schema = yamale.make_schema( + V2_SCHEMA, + validators=validators + ) + yamale.validate(schema, data) + + +def assertValidConfig(tmpdir, content): + file = create_yaml(tmpdir, content) + validate_schema(file) + + +def assertInvalidConfig(tmpdir, content, msgs=()): + file = create_yaml(tmpdir, content) + with pytest.raises(ValueError) as excinfo: + validate_schema(file) + for msg in msgs: + msg in str(excinfo.value) + + +def test_minimal_config(tmpdir): + assertValidConfig(tmpdir, 'version: "2"') + + +def test_invalid_version(tmpdir): + assertInvalidConfig( + tmpdir, + 'version: "latest"', + ['version:', "'latest' not in"] + ) + + +def test_invalid_version_1(tmpdir): + assertInvalidConfig( + tmpdir, + 'version: "1"', + ['version', "'1' not in"] + ) + + +def test_formats(tmpdir): + content = ''' +version: "2" +formats: + - pdf + ''' + assertValidConfig(tmpdir, content) + + +def test_formats_all(tmpdir): + content = ''' +version: "2" +formats: + - htmlzip + - pdf + - epub + ''' + assertValidConfig(tmpdir, content) + + +def test_formats_key_all(tmpdir): + content = ''' +version: "2" +formats: all + ''' + assertValidConfig(tmpdir, content) + + +def test_formats_invalid(tmpdir): + content = ''' +version: "2" +formats: + - invalidformat + - singlehtmllocalmedia + ''' + assertInvalidConfig( + tmpdir, + content, + ['formats', "'invalidformat' not in"] + ) + + +def test_formats_empty(tmpdir): + content = ''' +version: "2" +formats: [] + ''' + assertValidConfig(tmpdir, content) + + +def test_conda(tmpdir): + content = ''' +version: "2" +conda: + environment: environment.yml + ''' + assertValidConfig(tmpdir, content) + + +def test_conda_invalid(tmpdir): + content = ''' +version: "2" +conda: + environment: environment.yaml + ''' + assertInvalidConfig( + tmpdir, + content, + ['environment.yaml', 'is not a path'] + ) + + +def test_conda_missing_key(tmpdir): + content = ''' +version: "2" +conda: + files: environment.yml + ''' + assertInvalidConfig( + tmpdir, + content, + ['conda.environment: Required'] + ) + + +@pytest.mark.parametrize('value', ['stable', 'latest']) +def test_build(tmpdir, value): + content = ''' +version: "2" +build: + image: "{value}" + ''' + assertValidConfig(tmpdir, content.format(value=value)) + + +def test_build_missing_image_key(tmpdir): + content = ''' +version: "2" +build: + imagine: "2.0" # note the typo + ''' + assertValidConfig(tmpdir, content) + + +def test_build_invalid(tmpdir): + content = ''' +version: "2" +build: + image: "9.0" + ''' + assertInvalidConfig( + tmpdir, + content, + ["build.image: '9.0' not in"] + ) + + +@pytest.mark.parametrize('value', ['2', '2.7', '3', '3.5', '3.6']) +def test_python_version(tmpdir, value): + content = ''' +version: "2" +python: + version: "{value}" + ''' + assertValidConfig(tmpdir, content.format(value=value)) + + +def test_python_version_invalid(tmpdir): + content = ''' +version: "2" +python: + version: "4" + ''' + assertInvalidConfig( + tmpdir, + content, + ["version: '4' not in"] + ) + + +def test_python_version_no_key(tmpdir): + content = ''' +version: "2" +python: + guido: true + ''' + assertValidConfig(tmpdir, content) + + +def test_python_requirements(tmpdir): + content = ''' +version: "2" +python: + requirements: docs/requirements.txt + ''' + assertValidConfig(tmpdir, content) + + +def test_python_requirements_invalid(tmpdir): + content = ''' +version: "2" +python: + requirements: 23 + ''' + assertInvalidConfig( + tmpdir, + content, + ['requirements:', "'23' is not a path"] + ) + + +def test_python_requirements_null(tmpdir): + content = ''' +version: "2" +python: + requirements: null + ''' + assertValidConfig(tmpdir, content) + + +@pytest.mark.parametrize('value', ['pip', 'setup.py']) +def test_python_install(tmpdir, value): + content = ''' +version: "2" +python: + version: "3.6" + install: {value} + ''' + assertValidConfig(tmpdir, content.format(value=value)) + + +def test_python_install_invalid(tmpdir): + content = ''' +version: "2" +python: + install: guido + ''' + assertInvalidConfig( + tmpdir, + content, + ["python.install: 'guido' not in"] + ) + + +def test_python_install_null(tmpdir): + content = ''' +version: "2" +python: + install: null + ''' + assertValidConfig(tmpdir, content) + + +def test_python_extra_requirements(tmpdir): + content = ''' +version: "2" +python: + extra_requirements: + - test + - dev + ''' + assertValidConfig(tmpdir, content) + + +def test_python_extra_requirements_invalid(tmpdir): + content = ''' +version: "2" +python: + extra_requirements: + - 1 + - dev + ''' + assertInvalidConfig( + tmpdir, + content, + ["'1' is not a str"] + ) + + +@pytest.mark.parametrize('value', ['', 'null', '[]']) +def test_python_extra_requirements_empty(tmpdir, value): + content = ''' +version: "2" +python: + extra_requirements: {value} + ''' + assertValidConfig(tmpdir, content.format(value=value)) + + +@pytest.mark.parametrize('value', ['true', 'false']) +def test_python_system_packages(tmpdir, value): + content = ''' +version: "2" +python: + system_packages: {value} + ''' + assertValidConfig(tmpdir, content.format(value=value)) + + +@pytest.mark.parametrize('value', ['not true', "''", '[]']) +def test_python_system_packages_invalid(tmpdir, value): + content = ''' +version: "2" +python: + system_packages: {value} + ''' + assertInvalidConfig( + tmpdir, + content.format(value=value), + ['is not a bool'] + ) + + +def test_sphinx(tmpdir): + content = ''' +version: "2" +sphinx: + configuration: docs/conf.py + ''' + assertValidConfig(tmpdir, content) + + +def test_sphinx_default_value(tmpdir): + content = ''' +version: "2" +sphinx: + file: docs/conf.py # Default value for configuration key + ''' + assertValidConfig(tmpdir, content) + + +@pytest.mark.parametrize('value', ['2', 'non-existent-file.yml']) +def test_sphinx_invalid(tmpdir, value): + content = ''' +version: "2" +sphinx: + configuration: {value} + ''' + assertInvalidConfig( + tmpdir, + content, + ['is not a path'] + ) + + +def test_sphinx_fail_on_warning(tmpdir): + content = ''' +version: "2" +sphinx: + fail_on_warning: true + ''' + assertValidConfig(tmpdir, content) + + +@pytest.mark.parametrize('value', ['not true', "''", '[]']) +def test_sphinx_fail_on_warning_invalid(tmpdir, value): + content = ''' +version: "2" +sphinx: + fail_on_warning: {value} + ''' + assertInvalidConfig( + tmpdir, + content.format(value=value), + ['is not a bool'] + ) + + +def test_mkdocs(tmpdir): + content = ''' +version: "2" +mkdocs: + configuration: mkdocs.yml + ''' + assertValidConfig(tmpdir, content) + + +def test_mkdocs_default_value(tmpdir): + content = ''' +version: "2" +mkdocs: + file: mkdocs.yml # Default value for configuration key + ''' + assertValidConfig(tmpdir, content) + + +@pytest.mark.parametrize('value', ['2', 'non-existent-file.yml']) +def test_mkdocs_invalid(tmpdir, value): + content = ''' +version: "2" +mkdocs: + configuration: {value} + ''' + assertInvalidConfig( + tmpdir, + content, + ['is not a path'] + ) + + +def test_mkdocs_fail_on_warning(tmpdir): + content = ''' +version: "2" +mkdocs: + fail_on_warning: true + ''' + assertValidConfig(tmpdir, content) + + +@pytest.mark.parametrize('value', ['not true', "''", '[]']) +def test_mkdocs_fail_on_warning_invalid(tmpdir, value): + content = ''' +version: "2" +mkdocs: + fail_on_warning: {value} + ''' + assertInvalidConfig( + tmpdir, + content.format(value=value), + ['is not a bool'] + ) + + +def test_submodules_include(tmpdir): + content = ''' +version: "2" +submodules: + include: + - one + - two + - three + recursive: false + ''' + assertValidConfig(tmpdir, content) + + +def test_submodules_include_all(tmpdir): + content = ''' +version: "2" +submodules: +include: all + ''' + assertValidConfig(tmpdir, content) + + +def test_submodules_exclude(tmpdir): + content = ''' +version: "2" +submodules: +exclude: + - one + - two + - three + ''' + assertValidConfig(tmpdir, content) + + +def test_submodules_exclude_all(tmpdir): + content = ''' +version: "2" +submodules: + exclude: all + recursive: true + ''' + assertValidConfig(tmpdir, content) + + +def test_redirects(tmpdir): + content = ''' +version: "2" +redirects: + page: + 'guides/install.html': 'install.html' + ''' + assertValidConfig(tmpdir, content) + + +def test_redirects_invalid(tmpdir): + content = ''' +version: "2" +redirects: + page: + 'guides/install.html': true + ''' + assertInvalidConfig( + tmpdir, + content, + ['is not a str'] + ) + + +@pytest.mark.parametrize('value', ['', 'null', '{}']) +def test_redirects_empty(tmpdir, value): + content = ''' +version: "2" +redirects: {value} + ''' + assertValidConfig(tmpdir, content.format(value=value)) diff --git a/requirements/testing.txt b/requirements/testing.txt index 8c871f90c8f..786f2070276 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -8,6 +8,7 @@ pytest-xdist==1.22.0 apipkg==1.4 execnet==1.5.0 Mercurial==4.4.2 +yamale==1.7.0 pytest-mock==1.10.0 # local debugging tools