summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/unit/apiserver/__init__.py35
-rw-r--r--tests/unit/apiserver/resources/__init__.py0
-rw-r--r--tests/unit/apiserver/resources/test_env_action.py32
-rw-r--r--tests/unit/apiserver/utils/test_common.py (renamed from tests/unit/api/utils/test_common.py)0
-rw-r--r--tests/unit/apiserver/utils/test_influx.py (renamed from tests/unit/api/utils/test_influx.py)29
-rw-r--r--tests/unit/benchmark/contexts/standalone_duplicate_sample.yaml135
-rw-r--r--tests/unit/benchmark/contexts/standalone_sample.yaml112
-rw-r--r--tests/unit/benchmark/contexts/test_node.py4
-rw-r--r--tests/unit/benchmark/contexts/test_standalone.py131
-rw-r--r--tests/unit/network_services/__init__.py0
-rw-r--r--tests/unit/network_services/collector/__init__.py0
-rw-r--r--tests/unit/network_services/collector/test_publisher.py41
-rw-r--r--tests/unit/network_services/collector/test_subscriber.py69
13 files changed, 557 insertions, 31 deletions
diff --git a/tests/unit/apiserver/__init__.py b/tests/unit/apiserver/__init__.py
new file mode 100644
index 000000000..021415296
--- /dev/null
+++ b/tests/unit/apiserver/__init__.py
@@ -0,0 +1,35 @@
+from __future__ import absolute_import
+
+import os
+import unittest
+import tempfile
+
+from oslo_serialization import jsonutils
+
+from yardstick.common import constants as consts
+
+
+class APITestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.db_fd, self.db_path = tempfile.mkstemp()
+ consts.SQLITE = 'sqlite:///{}'.format(self.db_path)
+ from api import server
+
+ server.app.config['TESTING'] = True
+ self.app = server.app.test_client()
+
+ server.init_db()
+
+ def tearDown(self):
+ os.close(self.db_fd)
+ os.unlink(self.db_path)
+
+ def _post(self, url, data):
+ headers = {'Content-Type': 'application/json'}
+ resp = self.app.post(url, data=jsonutils.dumps(data), headers=headers)
+ return jsonutils.loads(resp.data)
+
+ def _get(self, url):
+ resp = self.app.get(url)
+ return jsonutils.loads(resp.data)
diff --git a/tests/unit/apiserver/resources/__init__.py b/tests/unit/apiserver/resources/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/unit/apiserver/resources/__init__.py
diff --git a/tests/unit/apiserver/resources/test_env_action.py b/tests/unit/apiserver/resources/test_env_action.py
new file mode 100644
index 000000000..e8f99b706
--- /dev/null
+++ b/tests/unit/apiserver/resources/test_env_action.py
@@ -0,0 +1,32 @@
+from __future__ import absolute_import
+
+import time
+import unittest
+
+from tests.unit.apiserver import APITestCase
+
+
+class EnvTestCase(APITestCase):
+
+ def test_create_grafana(self):
+ url = 'yardstick/env/action'
+ data = dict(action='createGrafanaContainer')
+ resp = self._post(url, data)
+
+ time.sleep(1)
+
+ task_id = resp['result']['task_id']
+ url = '/yardstick/asynctask?task_id={}'.format(task_id)
+ resp = self._get(url)
+
+ time.sleep(2)
+
+ self.assertTrue(u'status' in resp)
+
+
+def main():
+ unittest.main()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tests/unit/api/utils/test_common.py b/tests/unit/apiserver/utils/test_common.py
index acf6e41b1..acf6e41b1 100644
--- a/tests/unit/api/utils/test_common.py
+++ b/tests/unit/apiserver/utils/test_common.py
diff --git a/tests/unit/api/utils/test_influx.py b/tests/unit/apiserver/utils/test_influx.py
index 90aef2cab..aff0cab5c 100644
--- a/tests/unit/api/utils/test_influx.py
+++ b/tests/unit/apiserver/utils/test_influx.py
@@ -9,8 +9,6 @@
from __future__ import absolute_import
import unittest
import mock
-import uuid
-import datetime
from api.utils import influx
@@ -41,33 +39,6 @@ class GetIpTestCase(unittest.TestCase):
self.assertEqual(result, output)
-class WriteDataTestCase(unittest.TestCase):
-
- @mock.patch('api.utils.influx.get_data_db_client')
- def test_write_data(self, mock_get_client):
- measurement = 'tasklist'
- field = {'status': 1}
- timestamp = datetime.datetime.now()
- tags = {'task_id': str(uuid.uuid4())}
-
- influx._write_data(measurement, field, timestamp, tags)
- mock_get_client.assert_called_with()
-
-
-class WriteDataTasklistTestCase(unittest.TestCase):
-
- @mock.patch('api.utils.influx._write_data')
- def test_write_data_tasklist(self, mock_write_data):
- task_id = str(uuid.uuid4())
- timestamp = datetime.datetime.now()
- status = 1
- influx.write_data_tasklist(task_id, timestamp, status)
-
- field = {'status': status, 'error': ''}
- tags = {'task_id': task_id}
- mock_write_data.assert_called_with('tasklist', field, timestamp, tags)
-
-
class QueryTestCase(unittest.TestCase):
@mock.patch('api.utils.influx.ConfigParser')
diff --git a/tests/unit/benchmark/contexts/standalone_duplicate_sample.yaml b/tests/unit/benchmark/contexts/standalone_duplicate_sample.yaml
new file mode 100644
index 000000000..e468d0465
--- /dev/null
+++ b/tests/unit/benchmark/contexts/standalone_duplicate_sample.yaml
@@ -0,0 +1,135 @@
+# Copyright (c) 2016-2017 Intel Corporation
+#
+# 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.
+#
+---
+# Sample config file about the POD information, including the
+# name/IP/user/ssh key of Bare Metal and Controllers/Computes
+#
+# The options of this config file include:
+# name: the name of this node
+# role: node's role, support role: Master/Controller/Comupte/BareMetal
+# ip: the node's IP address
+# user: the username for login
+# key_filename:the path of the private key file for login
+
+nodes:
+-
+ name: node1
+ role: TrafficGen
+ ip: 1.1.1.1
+ user: root
+ password: r00t
+ interfaces:
+ xe0: # logical name from topology.yaml and vnfd.yaml
+ vpci: "0000:05:00.0"
+ driver: i40e
+ dpdk_port_num: 0
+ local_ip: "152.16.100.20"
+ netmask: "255.255.255.0"
+ local_mac: "00:00:00:00:00:01"
+ xe1: # logical name from topology.yaml and vnfd.yaml
+ vpci: "0000:05:00.1"
+ driver: i40e
+ dpdk_port_num: 1
+ local_ip: "152.16.100.21"
+ netmask: "255.255.255.0"
+ local_mac: "00:00:00:00:00:02"
+-
+ name: node2
+ role: nfvi_node
+ class: OvsDpdk
+ ip: 1.1.1.2
+ user: root
+ password: r00t
+ vports:
+ - dpdkvhostuser0
+ - dpdkvhostuser1
+ vports_mac:
+ - "00:00:00:00:00:03"
+ - "00:00:00:00:00:04"
+ phy_ports: # Physical ports to configure sriov
+ - "0000:05:00.0"
+ - "0000:05:00.1"
+ flow:
+ - ovs-ofctl add-flow br0 in_port=1,action=output:3
+ - ovs-ofctl add-flow br0 in_port=3,action=output:1
+ - ovs-ofctl add-flow br0 in_port=4,action=output:2
+ - ovs-ofctl add-flow br0 in_port=2,action=output:4
+ phy_driver: i40e # kernel driver
+ images: "/var/lib/libvirt/images/ubuntu.qcow2"
+-
+ name: node2
+ role: nfvi_node
+ class: OvsDpdk
+ ip: 1.1.1.5
+ user: root
+ password: r00t
+ vports:
+ - dpdkvhostuser0
+ - dpdkvhostuser1
+ vports_mac:
+ - "00:00:00:00:00:03"
+ - "00:00:00:00:00:04"
+ phy_ports: # Physical ports to configure sriov
+ - "0000:05:00.0"
+ - "0000:05:00.1"
+ flow:
+ - ovs-ofctl add-flow br0 in_port=1,action=output:3
+ - ovs-ofctl add-flow br0 in_port=3,action=output:1
+ - ovs-ofctl add-flow br0 in_port=4,action=output:2
+ - ovs-ofctl add-flow br0 in_port=2,action=output:4
+ phy_driver: i40e # kernel driver
+ images: "/var/lib/libvirt/images/ubuntu.qcow2"
+
+-
+ name: node3
+ role: vnf
+ ip: 1.1.1.3
+ user: root
+ password: r00t
+ host: 1.1.1.1
+ interfaces:
+ xe0: # logical name from topology.yaml and vnfd.yaml
+ vpci: "0000:00:04.0"
+ driver: virtio-pci
+ dpdk_port_num: 0
+ local_ip: "152.16.100.19"
+ netmask: "255.255.255.0"
+ local_mac: "00:00:00:00:00:05"
+
+ xe1: # logical name from topology.yaml and vnfd.yaml
+ vpci: "0000:00:05.0"
+ driver: virtio-pci
+ dpdk_port_num: 1
+ local_ip: "152.16.40.19"
+ netmask: "255.255.255.0"
+ local_mac: "00:00:00:00:00:06"
+ routing_table:
+ - network: "152.16.100.20"
+ netmask: "255.255.255.0"
+ gateway: "152.16.100.20"
+ if: "xe0"
+ - network: "152.16.40.20"
+ netmask: "255.255.255.0"
+ gateway: "152.16.40.20"
+ if: "xe1"
+ nd_route_tbl:
+ - network: "0064:ff9b:0:0:0:0:9810:6414"
+ netmask: "112"
+ gateway: "0064:ff9b:0:0:0:0:9810:6414"
+ if: "xe0"
+ - network: "0064:ff9b:0:0:0:0:9810:2814"
+ netmask: "112"
+ gateway: "0064:ff9b:0:0:0:0:9810:2814"
+ if: "xe1"
diff --git a/tests/unit/benchmark/contexts/standalone_sample.yaml b/tests/unit/benchmark/contexts/standalone_sample.yaml
new file mode 100644
index 000000000..95e12d62f
--- /dev/null
+++ b/tests/unit/benchmark/contexts/standalone_sample.yaml
@@ -0,0 +1,112 @@
+# Copyright (c) 2016-2017 Intel Corporation
+#
+# 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.
+#
+---
+# Sample config file about the POD information, including the
+# name/IP/user/ssh key of Bare Metal and Controllers/Computes
+#
+# The options of this config file include:
+# name: the name of this node
+# role: node's role, support role: Master/Controller/Comupte/BareMetal
+# ip: the node's IP address
+# user: the username for login
+# key_filename:the path of the private key file for login
+
+nodes:
+-
+ name: node1
+ role: TrafficGen
+ ip: 1.1.1.1
+ user: root
+ password: r00t
+ interfaces:
+ xe0: # logical name from topology.yaml and vnfd.yaml
+ vpci: "0000:05:00.0"
+ driver: i40e
+ dpdk_port_num: 0
+ local_ip: "152.16.100.20"
+ netmask: "255.255.255.0"
+ local_mac: "00:00:00:00:00:01"
+ xe1: # logical name from topology.yaml and vnfd.yaml
+ vpci: "0000:05:00.1"
+ driver: i40e
+ dpdk_port_num: 1
+ local_ip: "152.16.100.21"
+ netmask: "255.255.255.0"
+ local_mac: "00:00:00:00:00:02"
+-
+ name: node2
+ role: nfvi_node
+ class: OvsDpdk
+ ip: 1.1.1.2
+ user: root
+ password: r00t
+ vports:
+ - dpdkvhostuser0
+ - dpdkvhostuser1
+ vports_mac:
+ - "00:00:00:00:00:03"
+ - "00:00:00:00:00:04"
+ phy_ports: # Physical ports to configure sriov
+ - "0000:05:00.0"
+ - "0000:05:00.1"
+ flow:
+ - ovs-ofctl add-flow br0 in_port=1,action=output:3
+ - ovs-ofctl add-flow br0 in_port=3,action=output:1
+ - ovs-ofctl add-flow br0 in_port=4,action=output:2
+ - ovs-ofctl add-flow br0 in_port=2,action=output:4
+ phy_driver: i40e # kernel driver
+ images: "/var/lib/libvirt/images/ubuntu.qcow2"
+
+-
+ name: node3
+ role: vnf
+ ip: 1.1.1.3
+ user: root
+ password: r00t
+ host: 1.1.1.1
+ interfaces:
+ xe0: # logical name from topology.yaml and vnfd.yaml
+ vpci: "0000:00:04.0"
+ driver: virtio-pci
+ dpdk_port_num: 0
+ local_ip: "152.16.100.19"
+ netmask: "255.255.255.0"
+ local_mac: "00:00:00:00:00:05"
+
+ xe1: # logical name from topology.yaml and vnfd.yaml
+ vpci: "0000:00:05.0"
+ driver: virtio-pci
+ dpdk_port_num: 1
+ local_ip: "152.16.40.19"
+ netmask: "255.255.255.0"
+ local_mac: "00:00:00:00:00:06"
+ routing_table:
+ - network: "152.16.100.20"
+ netmask: "255.255.255.0"
+ gateway: "152.16.100.20"
+ if: "xe0"
+ - network: "152.16.40.20"
+ netmask: "255.255.255.0"
+ gateway: "152.16.40.20"
+ if: "xe1"
+ nd_route_tbl:
+ - network: "0064:ff9b:0:0:0:0:9810:6414"
+ netmask: "112"
+ gateway: "0064:ff9b:0:0:0:0:9810:6414"
+ if: "xe0"
+ - network: "0064:ff9b:0:0:0:0:9810:2814"
+ netmask: "112"
+ gateway: "0064:ff9b:0:0:0:0:9810:2814"
+ if: "xe1"
diff --git a/tests/unit/benchmark/contexts/test_node.py b/tests/unit/benchmark/contexts/test_node.py
index de5ba7066..64fe4a566 100644
--- a/tests/unit/benchmark/contexts/test_node.py
+++ b/tests/unit/benchmark/contexts/test_node.py
@@ -42,7 +42,7 @@ class NodeContextTestCase(unittest.TestCase):
'file': self._get_file_abspath("error_file")
}
- self.assertRaises(SystemExit, self.test_context.init, attrs)
+ self.assertRaises(IOError, self.test_context.init, attrs)
def test_successful_init(self):
@@ -100,7 +100,7 @@ class NodeContextTestCase(unittest.TestCase):
attr_name = 'node1.foo'
- self.assertRaises(SystemExit, self.test_context._get_server, attr_name)
+ self.assertRaises(ValueError, self.test_context._get_server, attr_name)
def test__get_server_found(self):
diff --git a/tests/unit/benchmark/contexts/test_standalone.py b/tests/unit/benchmark/contexts/test_standalone.py
new file mode 100644
index 000000000..687ef7305
--- /dev/null
+++ b/tests/unit/benchmark/contexts/test_standalone.py
@@ -0,0 +1,131 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2016-2017 Intel Corporation
+#
+# 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.
+#
+
+# Unittest for yardstick.benchmark.contexts.standalone
+
+from __future__ import absolute_import
+import os
+import unittest
+
+from yardstick.benchmark.contexts import standalone
+
+
+class StandaloneContextTestCase(unittest.TestCase):
+
+ NODES_SAMPLE = "standalone_sample.yaml"
+ NODES_DUPLICATE_SAMPLE = "standalone_duplicate_sample.yaml"
+
+ def setUp(self):
+ self.test_context = standalone.StandaloneContext()
+
+ def test_construct(self):
+
+ self.assertIsNone(self.test_context.name)
+ self.assertIsNone(self.test_context.file_path)
+ self.assertEqual(self.test_context.nodes, [])
+ self.assertEqual(self.test_context.nfvi_node, [])
+
+ def test_unsuccessful_init(self):
+
+ attrs = {
+ 'name': 'foo',
+ 'file': self._get_file_abspath("error_file")
+ }
+
+ self.assertRaises(IOError, self.test_context.init, attrs)
+
+ def test_successful_init(self):
+
+ attrs = {
+ 'name': 'foo',
+ 'file': self._get_file_abspath(self.NODES_SAMPLE)
+ }
+
+ self.test_context.init(attrs)
+
+ self.assertEqual(self.test_context.name, "foo")
+ self.assertEqual(len(self.test_context.nodes), 3)
+ self.assertEqual(len(self.test_context.nfvi_node), 1)
+ self.assertEqual(self.test_context.nfvi_node[0]["name"], "node2")
+
+ def test__get_server_with_dic_attr_name(self):
+
+ attrs = {
+ 'name': 'foo',
+ 'file': self._get_file_abspath(self.NODES_SAMPLE)
+ }
+
+ self.test_context.init(attrs)
+
+ attr_name = {'name': 'foo.bar'}
+ result = self.test_context._get_server(attr_name)
+
+ self.assertEqual(result, None)
+
+ def test__get_server_not_found(self):
+
+ attrs = {
+ 'name': 'foo',
+ 'file': self._get_file_abspath(self.NODES_SAMPLE)
+ }
+
+ self.test_context.init(attrs)
+
+ attr_name = 'bar.foo'
+ result = self.test_context._get_server(attr_name)
+
+ self.assertEqual(result, None)
+
+ def test__get_server_duplicate(self):
+
+ attrs = {
+ 'name': 'foo',
+ 'file': self._get_file_abspath(self.NODES_DUPLICATE_SAMPLE)
+ }
+
+ self.test_context.init(attrs)
+
+ attr_name = 'node2.foo'
+
+ self.assertRaises(ValueError, self.test_context._get_server, attr_name)
+
+ def test__get_server_found(self):
+
+ attrs = {
+ 'name': 'foo',
+ 'file': self._get_file_abspath(self.NODES_SAMPLE)
+ }
+
+ self.test_context.init(attrs)
+
+ attr_name = 'node1.foo'
+ result = self.test_context._get_server(attr_name)
+
+ self.assertEqual(result['ip'], '1.1.1.1')
+ self.assertEqual(result['name'], 'node1.foo')
+ self.assertEqual(result['user'], 'root')
+
+ def test_deploy(self):
+ self.assertIsNone(self.test_context.deploy())
+
+ def test_undeploy(self):
+ self.assertIsNone(self.test_context.undeploy())
+
+ def _get_file_abspath(self, filename):
+ curr_path = os.path.dirname(os.path.abspath(__file__))
+ file_path = os.path.join(curr_path, filename)
+ return file_path
diff --git a/tests/unit/network_services/__init__.py b/tests/unit/network_services/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/unit/network_services/__init__.py
diff --git a/tests/unit/network_services/collector/__init__.py b/tests/unit/network_services/collector/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/unit/network_services/collector/__init__.py
diff --git a/tests/unit/network_services/collector/test_publisher.py b/tests/unit/network_services/collector/test_publisher.py
new file mode 100644
index 000000000..d1e56e114
--- /dev/null
+++ b/tests/unit/network_services/collector/test_publisher.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2016-2017 Intel Corporation
+#
+# 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.
+#
+
+# Unittest for yardstick.network_services.collector.publisher
+
+from __future__ import absolute_import
+import unittest
+
+from yardstick.network_services.collector import publisher
+
+
+class PublisherTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.test_publisher = publisher.Publisher()
+
+ def test_successful_init(self):
+ pass
+
+ def test_unsuccessful_init(self):
+ pass
+
+ def test_start(self):
+ self.assertIsNone(self.test_publisher.start())
+
+ def test_stop(self):
+ self.assertIsNone(self.test_publisher.stop())
diff --git a/tests/unit/network_services/collector/test_subscriber.py b/tests/unit/network_services/collector/test_subscriber.py
new file mode 100644
index 000000000..373f5dccf
--- /dev/null
+++ b/tests/unit/network_services/collector/test_subscriber.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2016-2017 Intel Corporation
+#
+# 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.
+#
+
+# Unittest for yardstick.network_services.collector.subscriber
+
+from __future__ import absolute_import
+import unittest
+
+from yardstick.network_services.collector import subscriber
+
+
+class CollectorTestCase(unittest.TestCase):
+
+ TRAFFIC_PROFILE = {}
+ VNFS = {}
+
+ def setUp(self):
+ self.test_subscriber = subscriber.Collector(self.TRAFFIC_PROFILE,
+ self.VNFS)
+
+ def test_successful_init(self):
+
+ self.assertEqual(self.test_subscriber.traffic_profile, {})
+ self.assertEqual(self.test_subscriber.service, {})
+
+ def test_unsuccessful_init(self):
+ pass
+
+ def test_start(self):
+ self.assertIsNone(self.test_subscriber.start())
+
+ def test_stop(self):
+ self.assertIsNone(self.test_subscriber.stop())
+
+ def test_get_kpi(self):
+
+ class VnfAprrox(object):
+ def __init__(self):
+ self.result = {}
+ self.name = "vnf__1"
+
+ def collect_kpi(self):
+ self.result = {'pkt_in_up_stream': 100,
+ 'pkt_drop_up_stream': 5,
+ 'pkt_in_down_stream': 50,
+ 'pkt_drop_down_stream': 40}
+ return self.result
+
+ vnf = VnfAprrox()
+ result = self.test_subscriber.get_kpi(vnf)
+
+ self.assertEqual(result["vnf__1"]["pkt_in_up_stream"], 100)
+ self.assertEqual(result["vnf__1"]["pkt_drop_up_stream"], 5)
+ self.assertEqual(result["vnf__1"]["pkt_in_down_stream"], 50)
+ self.assertEqual(result["vnf__1"]["pkt_drop_down_stream"], 40)