# coding=utf-8
# Copyright (c) 2014, 2016, 2019 Intel Corporation

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

""" Module providing tests for the core module """

import collections
import errno
import os
import textwrap
from unittest import mock

import pytest

from framework import core
from framework import exceptions

from . import skip

# Making good test names often flies in the face of PEP8 recommendations, ignore
# those
# pylint: disable=invalid-name
#
# For testing purposes (and to get good names) it's useful to use classes for
# grouping tests. Often these tests share no state, which would be bad if they
# were not tests, but that's kind of the way python testing works.
# pylint: disable=no-self-use


class TestParseListfile(object):
    """Tests for core.parse_listfile."""

    def test_parse_listfile_return(self, tmpdir):
        """core.parse_listfile(): returns a list-like object.

        Given a file with a newline separated list of results, parse_listfile
        should return a list of files with no whitespace.
        """
        f = tmpdir.join('test.list')
        f.write("/tmp/foo\n/tmp/bar\n")
        results = core.parse_listfile(str(f))
        assert isinstance(results, collections.abc.Container)

    def test_parse_listfile_whitespace(self, tmpdir):
        """parse_listfile should remove various kinds of trailing whitespace.

        It is important it doesn't touch whitespace in lines, however.
        """
        f = tmpdir.join('test.list')
        f.write(textwrap.dedent("""\
            space between
            tab\t
            space
            newline
        """))
        results = core.parse_listfile(str(f))

        assert results[0] == 'space between'
        assert results[1] == 'tab'
        assert results[2] == 'space'
        assert results[3] == 'newline'

    def test_parse_listfile_tilde_posix(self, tmpdir):
        """core.parse_listfile(): tildes (~) are properly expanded.

        According to the python docs for python 2.7
        (http://docs.python.org/2/library/os.path.html#module-os.path), both
        os.path.expanduser and os.path.expandvars work on both *nix systems
        (Linux, *BSD, OSX) and Windows.
        """
        if os.name == 'posix':
            expected = os.path.expandvars('$HOME')
        else:
            expected = os.path.expandvars('%USERPROFILE%')
        expected = os.path.normpath(os.path.join(expected, 'foo'))

        f = tmpdir.join('test.list')
        f.write("~/foo\n")
        results = core.parse_listfile(str(f))
        assert os.path.normpath(results[0]) == expected


class TestGetConfig(object):
    """Tests for core.get_config."""
    _CONF_FILE = textwrap.dedent("""\
        [nose-test]
        ; a section for testing behavior
        dir = foo""")

    @skip.linux
    def test_config_in_xdg_config_home(self, tmpdir, mocker):
        """core.get_config() finds $XDG_CONFIG_HOME/piglit.conf"""
        env = mocker.patch('framework.core.os.environ', new={})
        env['XDG_CONFIG_HOME'] = str(tmpdir)
        conf = tmpdir.join('piglit.conf')
        conf.write(self._CONF_FILE)
        core.get_config()

        assert core.PIGLIT_CONFIG.has_section('nose-test')

    @skip.linux
    def test_config_in_home_dir(self, tmpdir, mocker):
        """core.get_config() finds $HOME/.config/piglit.conf"""
        env = mocker.patch('framework.core.os.environ', new={})
        env['HOME'] = str(tmpdir)
        conf = tmpdir.join('piglit.conf')
        conf.write(self._CONF_FILE)
        core.get_config()

        assert core.PIGLIT_CONFIG.has_section('nose-test')

    def test_config_in_current(self, tmpdir, mocker):
        """core.get_config() finds ./piglit.conf"""
        mocker.patch('framework.core.os.environ', new={})
        conf = tmpdir.join('piglit.conf')
        conf.write(self._CONF_FILE)

        ret = tmpdir.chdir()
        try:
            core.get_config()
        finally:
            os.chdir(str(ret))

        assert core.PIGLIT_CONFIG.has_section('nose-test')

    def test_config_in_piglit_root(self, mocker, tmpdir):
        """core.get_config() finds "piglit root"/piglit.conf"""
        # Mock the __file__ attribute of the core module, since that's how
        # piglit decides where the root of the piglit directory is.
        mocker.patch('framework.core.__file__',
                     str(tmpdir.join('framework', 'core.py')))
        mocker.patch('framework.core.os.environ', new={})
        conf = tmpdir.join('piglit.conf')
        conf.write(self._CONF_FILE)
        core.get_config()

        assert core.PIGLIT_CONFIG.has_section('nose-test')


class TestPiglitConfig(object):
    """Tests for PiglitConfig methods."""
    @classmethod
    def setup_class(cls):
        cls.conf = core.PiglitConfig()
        cls.conf.add_section('set')
        cls.conf.set('set', 'options', 'bool')

    def test_safe_get_valid(self):
        """core.PiglitConfig: safe_get returns a value if its in the Config."""
        assert self.conf.safe_get('set', 'options') == 'bool'

    def test_PiglitConfig_required_get_valid(self):
        """core.PiglitConfig: required_get returns a value if its in the
        Config."""
        assert self.conf.required_get('set', 'options') == 'bool'

    def test_safe_get_missing_option(self):
        """core.PiglitConfig: safe_get returns None if the option is missing.
        """
        assert self.conf.safe_get('set', 'invalid') is None

    def test_safe_get_missing_section(self):
        """core.PiglitConfig: safe_get returns None if the section is missing.
        """
        assert self.conf.safe_get('invalid', 'invalid') is None

    def test_required_get_missing_option(self):
        """core.PiglitConfig: required_get raises PiglitFatalError if the
        option is missing."""
        with pytest.raises(exceptions.PiglitFatalError):
            self.conf.required_get('set', 'invalid')

    def test_required_get_missing_section(self):
        """core.PiglitConfig: required_get raises PiglitFatalError if the
        section is missing."""
        with pytest.raises(exceptions.PiglitFatalError):
            self.conf.required_get('invalid', 'invalid')

    def test_safe_get_fallback(self):
        """core.PiglitConfig: safe_get returns the value of fallback when the
        section or option is missing."""
        assert self.conf.safe_get('invalid', 'invalid', fallback='foo') == 'foo'


class TestGetOptions(object):
    """Tests for the get_option function."""

    @pytest.fixture
    def env(self, mocker):
        """Create a mocked os.environ."""
        return mocker.patch('framework.core.os.environ', {})

    @pytest.fixture
    def conf(self, mocker):
        """Create a mocked piglit.conf."""
        return mocker.patch('framework.core.PIGLIT_CONFIG.safe_get',
                            mocker.Mock(return_value=None))

    def test_from_default(self):
        """core.get_option: if env is set it overrides piglit.conf."""
        # The mock means that only the first value matters
        actual = core.get_option('foo', ('foo', 'foo'),
                                 default=mock.sentinel.good)

        assert actual is mock.sentinel.good

    def test_from_conf(self, conf):
        """core.get_option: if env is not set a value is taken from
        piglit.conf.
        """
        conf.return_value = mock.sentinel

        # The mock means that these values don't actually matter
        actual = core.get_option('foo', ('foo', 'foo'))

        assert actual is mock.sentinel

    def test_from_env(self, env, conf):
        """core.get_option: if env is set it overrides piglit.conf."""
        conf.return_value = mock.sentinel.bad
        env['TEST'] = mock.sentinel.good

        # The mock means that only the first value matters
        actual = core.get_option('TEST', ('foo', 'foo'))

        assert actual is mock.sentinel.good

    def test_get_option_required(self, mocker):
        """core.get_option: dies if a required option cannot be retrieved."""
        mocker.patch('framework.core.os.environ', {}, True)

        with pytest.raises(exceptions.PiglitFatalError):
            core.get_option('NOT_REAL', ('fake', 'fake'), default='',
                            required=True)


class TestCheckDir(object):
    """Tests for core.check_dir."""

    def test_exists_fail(self, mocker, tmpdir):
        """core.check_dir: if the directory exists and failifexsits is True
        fail."""
        tmpdir.chdir()
        mocker.patch('framework.core.os.stat', mocker.Mock(side_effect=OSError))
        with pytest.raises(exceptions.PiglitException):
            core.check_dir('foo', True)

    def test_not_exists_and_not_fail(self, mocker, tmpdir):
        """core.check_dir: if the directory doesn't exists (ENOENT) and
        failifexists is False continue."""
        tmpdir.chdir()
        mocker.patch('framework.core.os.stat',
                     mocker.Mock(side_effect=OSError('foo', errno.ENOENT)))
        makedirs = mocker.patch('framework.core.os.makedirs')

        core.check_dir('foo', False)

        assert makedirs.called == 1

    def test_exists_and_not_fail(self, mocker, tmpdir):
        """core.check_dir: If makedirs fails with EEXIST pass"""
        tmpdir.chdir()
        mocker.patch('framework.core.os.stat', mocker.Mock())
        makedirs = mocker.patch(
            'framework.core.os.makedirs',
            mocker.Mock(side_effect=OSError(errno.EEXIST, 'foo')))

        core.check_dir('foo', False)

        assert makedirs.called == 0

    def test_makedirs_fail(self, mocker, tmpdir):
        """core.check_dir: If makedirs fails with any other raise that error."""
        tmpdir.chdir()
        mocker.patch('framework.core.os.makedirs',
                     mocker.Mock(side_effect=OSError))

        with pytest.raises(OSError):
            core.check_dir('foo', False)

    def test_handler(self, mocker, tmpdir):
        """core.check_dir: Handler is called if not failifexists."""
        class Sentinel(Exception):
            pass

        tmpdir.chdir()

        mocker.patch('framework.core.os.stat',
                     mocker.Mock(side_effect=OSError('foo', errno.ENOTDIR)))
        with pytest.raises(Sentinel):
            core.check_dir('foo', handler=mocker.Mock(side_effect=Sentinel))

    def test_stat_FileNotFoundError(self, mocker, tmpdir):
        """core.check_dir: FileNotFoundError is raised and failifexsits is
        False continue."""
        tmpdir.chdir()
        mocker.patch('framework.core.os.stat',
                     mocker.Mock(side_effect=FileNotFoundError))
        makedirs = mocker.patch('framework.core.os.makedirs')

        core.check_dir('foo', False)

        assert makedirs.called == 1
