Blob Blame History Raw
################################################################################
# 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.
################################################################################

"""
Unit tests for rest module POOLs
"""

import json
from jsonschema import validate
import mock
import pytest

import common

from rest_common import get_config, load_json_schema, get_max_cos_id, REST, CONFIG, CONFIG_EMPTY


class TestPools:

    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    def test_get(self):
        response = REST.get("/pools")
        data = json.loads(response.data.decode('utf-8'))

        assert response.status_code == 200

        # validate get all pools response schema
        schema, resolver = load_json_schema('get_pool_all_response.json')
        validate(data, schema, resolver=resolver)

        # assert 4 pools are returned
        # structure, types and required fields are validated using schema
        assert len(data) == len(CONFIG['pools'])


    @mock.patch("common.CONFIG_STORE.get_config", mock.MagicMock(return_value=CONFIG_EMPTY))
    def test_get_empty(self):
        response = REST.get("/pools")
        data = json.loads(response.data.decode('utf-8'))

        assert response.status_code == 404
        assert "No pools in config" in data["message"]


    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    def test_get_invalid_id(self):
        response = REST.get("/apps/5")
        data = json.loads(response.data.decode('utf-8'))

        assert response.status_code == 404
        assert "not found in config" in data["message"]


class TestPool_2:
    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    def test_get(self):
        response = REST.get("/pools/3")
        data = json.loads(response.data.decode('utf-8'))

        assert response.status_code == 200

        # validate get 1 pool response schema
        schema, resolver = load_json_schema('get_pool_response.json')
        validate(data, schema, resolver=resolver)

        # assert 1 pool is returned
        # structure, types and required fields are validated using schema
        assert data['id'] == 3


    @mock.patch("common.CONFIG_STORE.get_config", mock.MagicMock(return_value=CONFIG_EMPTY))
    def test_get_empty(self):
        response = REST.get("/pools/5")
        data = json.loads(response.data.decode('utf-8'))

        assert response.status_code == 404
        assert "No pools in config" in data["message"]


    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    def test_get_invalid_id(self):
        response = REST.get("/pools/5")
        data = json.loads(response.data.decode('utf-8'))

        assert response.status_code == 404
        assert "not found in config" in data["message"]


    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    def test_delete(self):
        def set_config(data):
            for pool in data['pools']:
                assert pool['id'] != 3

        with mock.patch('common.CONFIG_STORE.set_config', side_effect=set_config) as func_mock:
            response = REST.delete("/pools/3")
            func_mock.assert_called_once()

        assert response.status_code == 200


    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    def test_delete_invalid_id(self):
        with mock.patch('common.CONFIG_STORE.set_config') as func_mock:
            response = REST.delete("/pools/10")
            func_mock.assert_not_called()
        data = json.loads(response.data.decode('utf-8'))

        # assert 0 apps are returned
        assert response.status_code == 404
        assert "POOL 10 not found in config" in data["message"]


    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    def test_delete_default(self):
        with mock.patch('common.CONFIG_STORE.set_config') as func_mock:
            response = REST.delete("/pools/0")
            func_mock.assert_not_called()
        data = json.loads(response.data.decode('utf-8'))

        # assert 0 apps are returned
        assert response.status_code == 400
        assert "is Default, cannot delete" in data["message"]


    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    def test_delete_not_empty(self):
        with mock.patch('common.CONFIG_STORE.set_config') as func_mock:
            response = REST.delete("/pools/1")
            func_mock.assert_not_called()
        data = json.loads(response.data.decode('utf-8'))

        # assert 0 apps are returned
        assert response.status_code == 400
        assert "POOL 1 is not empty" in data["message"]


    @mock.patch("common.CONFIG_STORE.get_config", mock.MagicMock(return_value=CONFIG_EMPTY))
    def test_delete_empty_config(self):
        with mock.patch('common.CONFIG_STORE.set_config') as func_mock:
            response = REST.delete("/pools/1")
            func_mock.assert_not_called()
        data = json.loads(response.data.decode('utf-8'))

        assert response.status_code == 404
        assert "No pools in config" in data["message"]


    @mock.patch("common.CONFIG_STORE.get_config", mock.MagicMock(return_value=CONFIG_EMPTY))
    def test_put_empty_config(self):
        with mock.patch('common.CONFIG_STORE.set_config') as func_mock:
            response = REST.put("/pools/1", {"cbm": "0xc"})
            func_mock.assert_not_called()
        data = json.loads(response.data.decode('utf-8'))

        assert response.status_code == 404
        assert "No pools in config" in data["message"]


    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    @mock.patch("common.PQOS_API.check_core", mock.MagicMock(return_value=True))
    @mock.patch("common.PQOS_API.get_max_cos_id", new=get_max_cos_id)
    @mock.patch("caps.cat_supported", mock.MagicMock(return_value=True))
    @mock.patch("caps.mba_supported", mock.MagicMock(return_value=True))
    @mock.patch("power.validate_power_profiles", mock.MagicMock(return_value=True))
    def test_put_cbm(self):
        def set_config(data):
            for pool in data['pools']:
                if pool['id'] == 1:
                    assert pool['cbm'] == 0xc

        with mock.patch('common.CONFIG_STORE.set_config', side_effect=set_config) as func_mock,\
             mock.patch('pid_ops.is_pid_valid', return_value=True):
            response = REST.put("/pools/1", {"cbm": "0xc"})
            func_mock.assert_called_once()

        assert response.status_code == 200


    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    @mock.patch("common.PQOS_API.check_core", mock.MagicMock(return_value=True))
    @mock.patch("common.PQOS_API.get_max_cos_id", new=get_max_cos_id)
    @mock.patch("caps.cat_supported", mock.MagicMock(return_value=False))
    @mock.patch("caps.mba_supported", mock.MagicMock(return_value=True))
    @mock.patch("power.validate_power_profiles", mock.MagicMock(return_value=True))
    def test_put_cbm_unsupported(self):
        with mock.patch('common.CONFIG_STORE.set_config') as func_mock:
            response = REST.put("/pools/0", {"cbm": 0x1})
            func_mock.assert_not_called()
        data = json.loads(response.data.decode('utf-8'))

        assert response.status_code == 400
        assert "System does not support CAT" in data["message"]


    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    @mock.patch("common.PQOS_API.check_core", mock.MagicMock(return_value=True))
    @mock.patch("common.PQOS_API.get_max_cos_id", new=get_max_cos_id)
    @mock.patch("caps.cat_supported", mock.MagicMock(return_value=True))
    @mock.patch("caps.mba_supported", mock.MagicMock(return_value=True))
    @mock.patch("power.validate_power_profiles", mock.MagicMock(return_value=True))
    def test_put_mba(self):
        def set_config(data):
            for pool in data['pools']:
                if pool['id'] == 1:
                    assert pool['mba'] == 30

        with mock.patch('common.CONFIG_STORE.set_config', side_effect=set_config) as func_mock,\
             mock.patch('pid_ops.is_pid_valid', return_value=True):
            response = REST.put("/pools/1", {"mba": 30})
            func_mock.assert_called_once()

        assert response.status_code == 200


    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    @mock.patch("common.PQOS_API.check_core", mock.MagicMock(return_value=True))
    @mock.patch("common.PQOS_API.get_max_cos_id", new=get_max_cos_id)
    @mock.patch("caps.cat_supported", mock.MagicMock(return_value=True))
    @mock.patch("caps.mba_supported", mock.MagicMock(return_value=False))
    @mock.patch("power.validate_power_profiles", mock.MagicMock(return_value=True))
    def test_put_mba_unsupported(self):
        with mock.patch('common.CONFIG_STORE.set_config') as func_mock:
            response = REST.put("/pools/0", {"mba": 30})
            func_mock.assert_not_called()
        data = json.loads(response.data.decode('utf-8'))

        assert response.status_code == 400
        assert "System does not support MBA" in data["message"]


    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    @mock.patch("common.PQOS_API.get_max_cos_id", new=get_max_cos_id)
    @mock.patch("common.PQOS_API.check_core", mock.MagicMock(return_value=True))
    @mock.patch("caps.cat_supported", mock.MagicMock(return_value=True))
    @mock.patch("caps.mba_supported", mock.MagicMock(return_value=True))
    @mock.patch("power.validate_power_profiles", mock.MagicMock(return_value=True))
    def test_put_cores(self):
        def set_config(data):
            for pool in data['pools']:
                if pool['id'] == 2:
                    assert pool['cores'] == [2, 3, 11]

        with mock.patch('common.CONFIG_STORE.set_config', side_effect=set_config) as func_mock,\
             mock.patch('pid_ops.is_pid_valid', return_value=True):
            response = REST.put("/pools/2", {"cores": [2, 3, 11]})
            func_mock.assert_called_once()

        assert response.status_code == 200


    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    @mock.patch("common.PQOS_API.get_max_cos_id", new=get_max_cos_id)
    @mock.patch("common.PQOS_API.check_core", mock.MagicMock(return_value=True))
    @mock.patch("caps.cat_supported", mock.MagicMock(return_value=True))
    @mock.patch("caps.mba_supported", mock.MagicMock(return_value=True))
    @mock.patch("power.validate_power_profiles", mock.MagicMock(return_value=True))
    def test_put_name(self):
        def set_config(data):
            for pool in data['pools']:
                if pool['id'] == 2:
                    assert pool['name'] == "test"

        with mock.patch('common.CONFIG_STORE.set_config', side_effect=set_config) as func_mock,\
             mock.patch('pid_ops.is_pid_valid', return_value=True):
            response = REST.put("/pools/2", {"name": "test"})
            func_mock.assert_called_once()

        assert response.status_code == 200


    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    @mock.patch("common.PQOS_API.get_max_cos_id", new=get_max_cos_id)
    @mock.patch("common.PQOS_API.check_core", mock.MagicMock(return_value=True))
    @mock.patch("caps.cat_supported", mock.MagicMock(return_value=True))
    @mock.patch("caps.mba_supported", mock.MagicMock(return_value=True))
    def test_put_duplicate_cores(self):
        with mock.patch('common.CONFIG_STORE.set_config') as func_mock:
            response = REST.put("/pools/2", {"cores": [1, 2, 3, 11]})
            func_mock.assert_not_called()
        data = json.loads(response.data.decode('utf-8'))

        assert response.status_code == 400
        assert "already assigned to another pool" in data["message"]


    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    @mock.patch("common.PQOS_API.get_max_cos_id", new=get_max_cos_id)
    @mock.patch("common.PQOS_API.check_core", mock.MagicMock(return_value=True))
    def test_put_empty_cores(self):
        with mock.patch('common.CONFIG_STORE.set_config') as func_mock:
            response = REST.put("/pools/2", {"cores": []})
            func_mock.assert_not_called()

        assert response.status_code == 400


    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    @mock.patch("common.PQOS_API.get_max_cos_id", new=get_max_cos_id)
    @mock.patch("common.PQOS_API.check_core", mock.MagicMock(return_value=True))
    def test_put_not_exist(self):
        with mock.patch('common.CONFIG_STORE.set_config') as func_mock:
            response = REST.put("/pools/10", {"cores": [2, 3, 11]})
            func_mock.assert_not_called()
        data = json.loads(response.data.decode('utf-8'))

        assert response.status_code == 404
        assert "not found in config" in data["message"]


    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    @mock.patch("common.CONFIG_STORE.get_new_pool_id", mock.MagicMock(return_value=5))
    @mock.patch("common.PQOS_API.check_core", mock.MagicMock(return_value=True))
    @mock.patch("caps.cat_supported", mock.MagicMock(return_value=True))
    @mock.patch("caps.mba_supported", mock.MagicMock(return_value=True))
    @mock.patch("power.validate_power_profiles", mock.MagicMock(return_value=True))
    @pytest.mark.parametrize("pool_config", [
        {"cores":[11, 12], "cbm": "0xf"},                                    # no name
        {"name":"hello", "cores":[13, 17], "cbm": "0xf"},                    # cbm string
        {"name":"hello", "cores":[11, 16], "cbm": 3},                        # cbm int
        {"name":"hello_mba", "cores":[6, 7], "mba": 50},                     # mba
        {"name":"hello_mba_cbm", "cores":[14, 18], "mba": 50, "cbm": "0xf0"} # cbm & mba
    ])
    def test_post(self, pool_config):
        with mock.patch('common.CONFIG_STORE.set_config') as func_mock,\
             mock.patch('pid_ops.is_pid_valid', return_value=True):
            response = REST.post("/pools", pool_config)
            func_mock.assert_called_once()
        data = json.loads(response.data.decode('utf-8'))

        #validate add pool response schema
        schema, resolver = load_json_schema('add_pool_response.json')
        validate(data, schema, resolver=resolver)

        assert response.status_code == 201
        assert data['id'] == 5


    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    @mock.patch("common.CONFIG_STORE.get_new_pool_id", mock.MagicMock(return_value=None))
    def test_post_exceed_max_number(self):
        with mock.patch('common.CONFIG_STORE.set_config') as func_mock:
            response = REST.post("/pools", {"cores":[11, 12], "cbm": "0xf"})
            func_mock.assert_not_called()

        assert response.status_code == 500


    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    @mock.patch("common.CONFIG_STORE.get_new_pool_id", mock.MagicMock(return_value=5))
    @mock.patch("common.PQOS_API.check_core", mock.MagicMock(return_value=True))
    def test_post_duplicate_core(self):
        with mock.patch('common.CONFIG_STORE.set_config') as func_mock:
            response = REST.post("/pools", {"cores":[1, 2, 3], "cbm": "0xf"})
            func_mock.assert_not_called()

        assert response.status_code == 400


    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    def test_post_unknown_param(self):
        with mock.patch('common.CONFIG_STORE.set_config') as func_mock:
            response = REST.post("/pools", {"cores":[20], "cbm": "0xf", "unknonw": 1})
            func_mock.assert_not_called()

        assert response.status_code == 400


    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    @pytest.mark.parametrize("pool_config", [
        {"cores": "invalid", "cbm": "0xf"},
        {"cores": [20], "cbm": "invalid"},
        {"cores": [20], "mba": "invalid"}
    ])
    def test_post_invalid_value(self, pool_config):
        with mock.patch('common.CONFIG_STORE.set_config') as func_mock:
            response = REST.post("/pools", pool_config)
            func_mock.assert_not_called()

        assert response.status_code == 400


    @mock.patch("common.CONFIG_STORE.get_config", new=get_config)
    @pytest.mark.parametrize("no_req_fields_json", [
        {"mba": 10},                                # missing cores
        {"cbm": "0xf"},                             # missing cores
        {"mba": 10, "cbm": "0xf"},                  # missing cores
        {"name":"hello", "mba": 10, "cbm": "0xf"},  # missing cores
        {"name":"hello", "cbm": "0xf"},             # missing cores

        {"cores":[3, 10]},                  # missing at least one alloc technology
        {"name":"hello", "cores":[3, 10]},  # missing at least one alloc technology

        {"name":"hello"},                   # missing at least one alloc technology and cores

        {"cores":[3, 10], "cbm": "0xf", "apps":[1]} # extra property "apps"
    ])
    def test_post_no_req_fields(self, no_req_fields_json):
        with mock.patch('common.CONFIG_STORE.set_config') as func_mock:
            response = REST.post("/pools", no_req_fields_json)
            func_mock.assert_not_called()
        assert response.status_code == 400