################################################################################
# BSD LICENSE
#
# Copyright(c) 2019-2020 Intel Corporation. All rights reserved.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
#   * Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
#   * Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in
#     the documentation and/or other materials provided with the
#     distribution.
#   * Neither the name of Intel Corporation nor the names of its
#     contributors may be used to endorse or promote products derived
#     from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
################################################################################

import pytest
import mock

import common

from cache_ops import *

def test_configure_rdt():
    Pool.pools[1] = {}
    Pool.pools[1]['cores'] = [1, 101]
    Pool.pools[1]['cbm'] = 0x100
    Pool.pools[1]['mba'] = 11

    Pool.pools[2] = {}
    Pool.pools[2]['cores'] = [2, 202]
    Pool.pools[2]['cbm'] = 0x200
    Pool.pools[2]['mba'] = 22

    with mock.patch('common.CONFIG_STORE.get_pool_attr', return_value=[1]) as mock_get_pool_attr,\
         mock.patch('cache_ops.Pool.cores_set') as mock_cores_set,\
         mock.patch('cache_ops.Pool.configure', return_value=0) as mock_pool_configure,\
         mock.patch('cache_ops.Apps.configure', return_value=0) as mock_apps_configure,\
         mock.patch('common.PQOS_API.init', return_value=0),\
         mock.patch('common.PQOS_API.enable_mba_bw', return_value=0),\
         mock.patch('common.CONFIG_STORE.recreate_default_pool', return_value=0):

        assert not configure_rdt()

        mock_cores_set.assert_called_once_with([])
        mock_pool_configure.assert_called_once()
        mock_apps_configure.assert_called_once()

        mock_pool_configure.return_value = -1
        assert configure_rdt() == -1

        mock_pool_configure.return_value = 0
        mock_get_pool_attr.return_value = []
        assert configure_rdt() == -1


class TestPools(object):

    ## @cond
    @pytest.fixture(autouse=True)
    def init(self):
        Pool.pools= {}
    ## @endcond


    @mock.patch("caps.cat_supported", mock.MagicMock(return_value=True))
    @mock.patch("caps.mba_supported", mock.MagicMock(return_value=True))
    def test_configure(self):
        def get_attr(attr, pool_id):
            config = {
                'cores': [1, 2],
                'cbm': 15,
                'mba': 88,
                'apps': [1],
                'pids': [11, 22]
            }

            if attr in config:
                return config[attr]
            else:
                return None

        with mock.patch('common.CONFIG_STORE.get_pool_attr', new=get_attr),\
             mock.patch('common.CONFIG_STORE.get_app_attr', new=get_attr),\
             mock.patch('cache_ops.Pool.cbm_set') as mock_cbm_set,\
             mock.patch('cache_ops.Pool.mba_set') as mock_mba_set,\
             mock.patch('cache_ops.Pool.cores_set') as mock_cores_set,\
             mock.patch('cache_ops.Pool.pids_set') as mock_pids_set,\
             mock.patch('cache_ops.Pool.apply') as mock_apply:

             Pool(1).configure()

             mock_cbm_set.assert_called_once_with(15)
             mock_mba_set.assert_called_once_with(88)
             mock_cores_set.assert_called_once_with([1,2])
             mock_pids_set.assert_called_once_with([11,22])
             mock_apply.assert_called_once_with(1)


    def test_cbm_get(self):
        Pool.pools[3] = {}
        Pool.pools[3]['cbm'] = 0xf
        Pool.pools[1] = {}

        assert Pool(3).cbm_get() == 0xf
        assert not Pool(1).cbm_get()
        assert not Pool(2).cbm_get()


    def test_cbm_set(self):
        Pool.pools[3] = {}
        Pool.pools[3]['cbm'] = 0xf
        Pool.pools[1] = {}

        Pool(3).cbm_set(0x1)

        assert 0x1 == Pool.pools[3]['cbm']


    def test_mba_get(self):
        Pool.pools[11] = {}
        Pool.pools[11]['mba'] = 10
        Pool.pools[33] = {}
        Pool.pools[33]['mba'] = 30

        assert Pool(11).mba_get() == 10
        assert not Pool(20).mba_get()
        assert Pool(33).mba_get() == 30


    def test_mba_set(self):
        Pool.pools[11] = {}
        Pool.pools[11]['mba'] = 10
        Pool.pools[33] = {}
        Pool.pools[33]['mba'] = 30

        Pool(11).mba_set(11+1)
        Pool(33).mba_set(33+3)

        assert 11+1 == Pool.pools[11]['mba']
        assert 33+3 == Pool.pools[33]['mba']


    def test_pids_get(self):
        Pool.pools[1] = {}
        Pool.pools[1]['pids'] = [1, 10,1010]
        Pool.pools[30] = {}
        Pool.pools[30]['pids'] = [3, 30, 3030]

        assert Pool(1).pids_get() == [1, 10,1010]
        assert Pool(2).pids_get() == []
        assert Pool(30).pids_get() == [3, 30, 3030]


    def test_pids_set(self):
        Pool.pools[1] = {}
        Pool.pools[1]['pids'] = [1, 10,1010]
        Pool.pools[30] = {}
        Pool.pools[30]['pids'] = [3, 30, 3030]

        with mock.patch('common.CONFIG_STORE.get_pool_attr', return_value=[1, 44, 66]),\
             mock.patch('os.sched_setaffinity') as set_aff_mock:
            Pool(1).pids_set([1, 10])
            Pool(30).pids_set([30, 3030])

            set_aff_mock.assert_any_call(1010, [1, 44, 66])
            set_aff_mock.assert_any_call(3, [1, 44, 66])


    def test_cores_get(self):
        Pool.pools[12] = {}
        Pool.pools[12]['cores'] = [1, 10,11]
        Pool.pools[35] = {}
        Pool.pools[35]['cores'] = [3, 30, 33]

        assert Pool(12).cores_get() == [1, 10,11]
        assert Pool(20).cores_get() == []
        assert Pool(35).cores_get() == [3, 30, 33]


    @mock.patch('common.PQOS_API.alloc_assoc_set')
    @mock.patch('common.PQOS_API.release')
    def test_cores_set(self, mock_release, mock_alloc_assoc_set):
        Pool.pools[1] = {}
        Pool.pools[1]['cores'] = []
        Pool.pools[2] = {}
        Pool.pools[2]['cores'] = [4, 5, 6]

        Pool(1).cores_set([1, 2])
        assert Pool.pools[1]['cores'] == [1, 2]
        mock_alloc_assoc_set.assert_called_once_with([1, 2], 1)

        Pool(1).cores_set([1, 3])
        assert Pool.pools[1]['cores'] == [1, 3]
        mock_alloc_assoc_set.assert_any_call([1, 3], 1)
        mock_release.assert_called_once_with([2])


    @mock.patch('common.PQOS_API.l3ca_set')
    @mock.patch('common.PQOS_API.alloc_assoc_set')
    def test_apply_not_configured(self, mock_l3ca_set, mock_alloc_assoc_set):
        result = Pool.apply(1)

        assert result == -1

        mock_l3ca_set.assert_not_called()
        mock_alloc_assoc_set.assert_not_called()


    @mock.patch('common.PQOS_API.mba_set')
    @mock.patch('common.PQOS_API.l3ca_set')
    @mock.patch('common.PQOS_API.alloc_assoc_set')
    @mock.patch('common.PQOS_API.get_sockets')
    @mock.patch('common.CONFIG_STORE.get_mba_ctrl_enabled')
    def test_apply(self, mock_get_mba_ctrl_enabled, mock_get_socket, mock_alloc_assoc_set, mock_l3ca_set, mock_mba_set):
        Pool.pools[2] = {}
        Pool.pools[2]['cores'] = [1]
        Pool.pools[2]['cbm'] = 0xc00
        Pool.pools[2]['mba'] = 99

        Pool.pools[1] = {}
        Pool.pools[1]['cores'] = [2, 3]
        Pool.pools[1]['cbm'] = 0x300
        Pool.pools[1]['mba'] = 11

        mock_alloc_assoc_set.return_value = 0
        mock_mba_set.return_value = 0
        mock_l3ca_set.return_value = 0
        mock_get_socket.return_value = [0, 2]
        mock_get_mba_ctrl_enabled.return_value = False

        result = Pool.apply(2)
        assert result == 0
        result = Pool.apply(1)
        assert result == 0

        mock_mba_set.assert_any_call([0, 2], 2, 99, False)
        mock_mba_set.assert_any_call([0, 2], 1, 11, False)
        mock_l3ca_set.assert_any_call([0, 2], 2, 0xc00)
        mock_l3ca_set.assert_any_call([0, 2], 1, 0x300)
        mock_alloc_assoc_set.assert_any_call([1], 2)
        mock_alloc_assoc_set.assert_any_call([2, 3], 1)

        # libpqos fails
        mock_get_socket.return_value = None
        result = Pool.apply(2)
        assert result != 0
        result = Pool.apply(1)
        assert result != 0

        mock_get_socket.return_value = [0,2]
        mock_alloc_assoc_set.return_value = -1
        mock_l3ca_set.return_value = 0
        result = Pool.apply(2)
        assert result != 0
        result = Pool.apply(1)
        assert result != 0

        mock_alloc_assoc_set.return_value = 0
        mock_l3ca_set.return_value = -1
        result = Pool.apply(2)
        assert result != 0
        result = Pool.apply(1)
        assert result != 0

        mock_l3ca_set.return_value = 0
        mock_mba_set.return_value = -1
        result = Pool.apply(2)
        assert result != 0
        result = Pool.apply(1)
        assert result != 0

        mock_alloc_assoc_set.return_value = -1
        mock_l3ca_set.return_value = -1
        result = Pool.apply(2)
        assert result != 0
        result = Pool.apply(1)
        assert result != 0


    def test_reset(self):
        Pool.pools[2] = {}
        Pool.pools[2]['cores'] = [1]
        Pool.pools[2]['cbm'] = 0xc00

        Pool.reset()
        assert 2 not in Pool.pools


class TestApps(object):

    def test_apps_configure(self):

        CONFIG = {"apps": [
            {
                "cores": [1],
                "id": 1,
                "pids": [1]
            },
            {
                "cores": [2],
                "id": 2,
                "pids": [2, 22]
            },
            {
                "cores": [10],
                "id": 10,
                "pids": [10]
            },
            {
                "cores": [10],
                "id": 11
            }
        ]}

        def get_pool_attr(attr, pool_id):
            if attr == 'cores':
                return [1, 2, 3, 4]
            else:
                return None

        with mock.patch('common.CONFIG_STORE.get_config', return_value={}),\
             mock.patch('common.CONFIG_STORE.app_to_pool') as atp_mock,\
             mock.patch('common.CONFIG_STORE.get_pool_attr') as gpa_mock,\
             mock.patch('cache_ops.Apps.set_affinity') as sa_mock:

                Apps.configure()

                atp_mock.assert_not_called()
                gpa_mock.assert_not_called()
                sa_mock.assert_not_called()


        with mock.patch('common.CONFIG_STORE.get_config', return_value=CONFIG),\
             mock.patch('common.CONFIG_STORE.app_to_pool', return_value=1),\
             mock.patch('common.CONFIG_STORE.get_pool_attr', new=get_pool_attr),\
             mock.patch('cache_ops.Apps.set_affinity') as set_aff_mock:

                Apps.configure()

                set_aff_mock.assert_any_call([1], [1])
                set_aff_mock.assert_any_call([2, 22], [2])
                set_aff_mock.assert_any_call([10], [1, 2, 3, 4])


    def test_apps_set_affinity(self):
        with mock.patch('os.sched_setaffinity') as set_aff_mock:
            Apps.set_affinity([1000, 1001, 1002], [0,1,2,3])
            set_aff_mock.assert_any_call(1000, [0,1,2,3])
            set_aff_mock.assert_any_call(1001, [0,1,2,3])
            set_aff_mock.assert_any_call(1002, [0,1,2,3])

        with mock.patch('os.sched_setaffinity', side_effect=OSError()):
            Apps.set_affinity([1000, 1001, 1002], [0,1,2,3])
