# Copyright (c) 2017 Intel, Inc.
# Copyright (c) 2017 OpenStack Foundation
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import glob
import os
from unittest import mock

import fixtures
from six.moves import builtins

from zun.common import exception
from zun.pci import utils
from zun.tests import base


class PciDeviceMatchTestCase(base.TestCase):
    def setUp(self):
        super(PciDeviceMatchTestCase, self).setUp()
        self.fake_pci_1 = {'vendor_id': 'v1',
                           'device_id': 'd1',
                           'capabilities_network': ['cap1', 'cap2', 'cap3']}

    def test_single_spec_match(self):
        self.assertTrue(utils.pci_device_prop_match(
            self.fake_pci_1, [{'vendor_id': 'v1', 'device_id': 'd1'}]))

    def test_multiple_spec_match(self):
        self.assertTrue(utils.pci_device_prop_match(
            self.fake_pci_1,
            [{'vendor_id': 'v1', 'device_id': 'd1'},
             {'vendor_id': 'v3', 'device_id': 'd3'}]))

    def test_spec_dismatch(self):
        self.assertFalse(utils.pci_device_prop_match(
            self.fake_pci_1,
            [{'vendor_id': 'v4', 'device_id': 'd4'},
             {'vendor_id': 'v3', 'device_id': 'd3'}]))

    def test_spec_extra_key(self):
        self.assertFalse(utils.pci_device_prop_match(
            self.fake_pci_1,
            [{'vendor_id': 'v1', 'device_id': 'd1', 'wrong_key': 'k1'}]))

    def test_spec_list(self):
        self.assertTrue(utils.pci_device_prop_match(
            self.fake_pci_1, [{'vendor_id': 'v1', 'device_id': 'd1',
                               'capabilities_network': ['cap1', 'cap2',
                                                        'cap3']}]))
        self.assertTrue(utils.pci_device_prop_match(
            self.fake_pci_1, [{'vendor_id': 'v1', 'device_id': 'd1',
                               'capabilities_network': ['cap3', 'cap1']}]))

    def test_spec_list_no_matching(self):
        self.assertFalse(utils.pci_device_prop_match(
            self.fake_pci_1, [{'vendor_id': 'v1', 'device_id': 'd1',
                               'capabilities_network': ['cap1', 'cap33']}]))

    def test_spec_list_wrong_type(self):
        self.assertFalse(utils.pci_device_prop_match(
            self.fake_pci_1, [{'vendor_id': 'v1', 'device_id': ['d1']}]))


class PciDeviceAddressParserTestCase(base.TestCase):
    def test_parse_address(self):
        self.parse_result = utils.parse_address("0000:04:12.6")
        self.assertEqual(self.parse_result, ('0000', '04', '12', '6'))

    def test_parse_address_wrong(self):
        self.assertRaises(exception.PciDeviceWrongAddressFormat,
                          utils.parse_address, "0000:04.12:6")

    def test_parse_address_invalid_character(self):
        self.assertRaises(exception.PciDeviceWrongAddressFormat,
                          utils.parse_address, "0000:h4.12:6")


class GetFunctionByIfnameTestCase(base.TestCase):

    @mock.patch('os.path.isdir', return_value=True)
    @mock.patch.object(os, 'readlink')
    def test_virtual_function(self, mock_readlink, *args):
        mock_readlink.return_value = '../../../0000.00.00.1'
        with mock.patch.object(builtins, 'open', side_effect=IOError()):
            address, physical_function = utils.get_function_by_ifname('eth0')
            self.assertEqual(address, '0000.00.00.1')
            self.assertFalse(physical_function)

    @mock.patch('os.path.isdir', return_value=True)
    @mock.patch.object(os, 'readlink')
    def test_physical_function(self, mock_readlink, *args):
        ifname = 'eth0'
        totalvf_path = "/sys/class/net/%s/device/%s" % (ifname,
                                                        utils._SRIOV_TOTALVFS)
        mock_readlink.return_value = '../../../0000:00:00.1'
        with mock.patch.object(builtins, 'open',
                               mock.mock_open(read_data='4')) as mock_open:
            address, physical_function = utils.get_function_by_ifname('eth0')
            self.assertEqual(address, '0000:00:00.1')
            self.assertTrue(physical_function)
            mock_open.assert_called_once_with(totalvf_path)

    @mock.patch('os.path.isdir', return_value=False)
    def test_exception(self, *args):
        address, physical_function = utils.get_function_by_ifname('lo')
        self.assertIsNone(address)
        self.assertFalse(physical_function)


class IsPhysicalFunctionTestCase(base.TestCase):

    def setUp(self):
        super(IsPhysicalFunctionTestCase, self).setUp()
        self.pci_args = utils.get_pci_address_fields('0000:00:00.1')

    @mock.patch('os.path.isdir', return_value=True)
    def test_virtual_function(self, *args):
        with mock.patch.object(builtins, 'open', side_effect=IOError()):
            self.assertFalse(utils.is_physical_function(*self.pci_args))

    @mock.patch('os.path.isdir', return_value=True)
    def test_physical_function(self, *args):
        with mock.patch.object(builtins, 'open',
                               mock.mock_open(read_data='4')):
            self.assertTrue(utils.is_physical_function(*self.pci_args))

    @mock.patch('os.path.isdir', return_value=False)
    def test_exception(self, *args):
        self.assertFalse(utils.is_physical_function(*self.pci_args))


class GetIfnameByPciAddressTestCase(base.TestCase):

    def setUp(self):
        super(GetIfnameByPciAddressTestCase, self).setUp()
        self.pci_address = '0000:00:00.1'

    @mock.patch.object(os, 'listdir')
    def test_physical_function_inferface_name(self, mock_listdir):
        mock_listdir.return_value = ['foo', 'bar']
        ifname = utils.get_ifname_by_pci_address(
            self.pci_address, pf_interface=True)
        self.assertEqual(ifname, 'bar')

    @mock.patch.object(os, 'listdir')
    def test_virtual_function_inferface_name(self, mock_listdir):
        mock_listdir.return_value = ['foo', 'bar']
        ifname = utils.get_ifname_by_pci_address(
            self.pci_address, pf_interface=False)
        self.assertEqual(ifname, 'bar')

    @mock.patch.object(os, 'listdir')
    def test_exception(self, mock_listdir):
        mock_listdir.side_effect = OSError('No such file or directory')
        self.assertRaises(
            exception.PciDeviceNotFoundById,
            utils.get_ifname_by_pci_address,
            self.pci_address
        )


class GetMacByPciAddressTestCase(base.TestCase):
    def setUp(self):
        super(GetMacByPciAddressTestCase, self).setUp()
        self.pci_address = '0000:07:00.1'
        self.if_name = 'enp7s0f1'
        self.tmpdir = self.useFixture(fixtures.TempDir())
        self.fake_file = os.path.join(self.tmpdir.path, "address")
        with open(self.fake_file, "w") as f:
            f.write("a0:36:9f:72:00:00\n")

    @mock.patch.object(os, 'listdir')
    @mock.patch.object(os.path, 'join')
    def test_get_mac(self, mock_join, mock_listdir):
        mock_listdir.return_value = [self.if_name]
        mock_join.return_value = self.fake_file
        mac = utils.get_mac_by_pci_address(self.pci_address)
        mock_join.assert_called_once_with(
            "/sys/bus/pci/devices/%s/net" % self.pci_address, self.if_name,
            "address")
        self.assertEqual("a0:36:9f:72:00:00", mac)

    @mock.patch.object(os, 'listdir')
    @mock.patch.object(os.path, 'join')
    def test_get_mac_fails(self, mock_join, mock_listdir):
        os.unlink(self.fake_file)
        mock_listdir.return_value = [self.if_name]
        mock_join.return_value = self.fake_file
        self.assertRaises(
            exception.PciDeviceNotFoundById,
            utils.get_mac_by_pci_address, self.pci_address)

    @mock.patch.object(os, 'listdir')
    @mock.patch.object(os.path, 'join')
    def test_get_mac_fails_empty(self, mock_join, mock_listdir):
        with open(self.fake_file, "w") as f:
            f.truncate(0)
        mock_listdir.return_value = [self.if_name]
        mock_join.return_value = self.fake_file
        self.assertRaises(
            exception.PciDeviceNotFoundById,
            utils.get_mac_by_pci_address, self.pci_address)

    @mock.patch.object(os, 'listdir')
    @mock.patch.object(os.path, 'join')
    def test_get_physical_function_mac(self, mock_join, mock_listdir):
        mock_listdir.return_value = [self.if_name]
        mock_join.return_value = self.fake_file
        mac = utils.get_mac_by_pci_address(self.pci_address, pf_interface=True)
        mock_join.assert_called_once_with(
            "/sys/bus/pci/devices/%s/physfn/net" % self.pci_address,
            self.if_name, "address")
        self.assertEqual("a0:36:9f:72:00:00", mac)


class GetVfNumByPciAddressTestCase(base.TestCase):

    def setUp(self):
        super(GetVfNumByPciAddressTestCase, self).setUp()
        self.pci_address = '0000:00:00.1'
        self.paths = [
            '/sys/bus/pci/devices/0000:00:00.1/physfn/virtfn3',
        ]

    @mock.patch.object(os, 'readlink')
    @mock.patch.object(glob, 'iglob')
    def test_vf_number_found(self, mock_iglob, mock_readlink):
        mock_iglob.return_value = self.paths
        mock_readlink.return_value = '../../0000:00:00.1'
        vf_num = utils.get_vf_num_by_pci_address(self.pci_address)
        self.assertEqual(vf_num, '3')

    @mock.patch.object(os, 'readlink')
    @mock.patch.object(glob, 'iglob')
    def test_vf_number_not_found(self, mock_iglob, mock_readlink):
        mock_iglob.return_value = self.paths
        mock_readlink.return_value = '../../0000:00:00.2'
        self.assertRaises(
            exception.PciDeviceNotFoundById,
            utils.get_vf_num_by_pci_address,
            self.pci_address
        )

    @mock.patch.object(os, 'readlink')
    @mock.patch.object(glob, 'iglob')
    def test_exception(self, mock_iglob, mock_readlink):
        mock_iglob.return_value = self.paths
        mock_readlink.side_effect = OSError('No such file or directory')
        self.assertRaises(
            exception.PciDeviceNotFoundById,
            utils.get_vf_num_by_pci_address,
            self.pci_address
        )


class GetNetNameByVfPciAddressTestCase(base.TestCase):

    def setUp(self):
        super(GetNetNameByVfPciAddressTestCase, self).setUp()
        self._get_mac = mock.patch.object(utils, 'get_mac_by_pci_address')
        self.mock_get_mac = self._get_mac.start()
        self._get_ifname = mock.patch.object(
            utils, 'get_ifname_by_pci_address')
        self.mock_get_ifname = self._get_ifname.start()
        self.addCleanup(self._get_mac.stop)
        self.addCleanup(self._get_ifname.stop)

        self.mac = 'ca:fe:ca:fe:ca:fe'
        self.if_name = 'enp7s0f0'
        self.pci_address = '0000:07:02.1'

    def test_correct_behaviour(self):
        ref_net_name = ('net_enp7s0f0_ca_fe_ca_fe_ca_fe', 'enp7s0f0')
        self.mock_get_mac.return_value = self.mac
        self.mock_get_ifname.return_value = self.if_name
        net_name = utils.get_net_name_by_vf_pci_address(self.pci_address)
        self.assertEqual(ref_net_name, net_name)
        self.mock_get_mac.called_once_with(self.pci_address)
        self.mock_get_ifname.called_once_with(self.pci_address)

    def test_wrong_mac(self):
        self.mock_get_mac.side_effect = (
            exception.PciDeviceNotFoundById(self.pci_address))
        net_name = utils.get_net_name_by_vf_pci_address(self.pci_address)
        self.assertIsNone(net_name)
        self.mock_get_mac.called_once_with(self.pci_address)
        self.mock_get_ifname.assert_not_called()

    def test_wrong_ifname(self):
        self.mock_get_mac.return_value = self.mac
        self.mock_get_ifname.side_effect = (
            exception.PciDeviceNotFoundById(self.pci_address))
        net_name = utils.get_net_name_by_vf_pci_address(self.pci_address)
        self.assertIsNone(net_name)
        self.mock_get_mac.called_once_with(self.pci_address)
        self.mock_get_ifname.called_once_with(self.pci_address)
