nuts 3.2.0__tar.gz → 3.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nuts-3.2.0 → nuts-3.4.0}/PKG-INFO +8 -3
- {nuts-3.2.0 → nuts-3.4.0}/README.md +4 -0
- {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/napalm_bgp_neighbors.py +20 -7
- {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/napalm_get_arp.py +1 -0
- {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/napalm_get_config.py +8 -6
- {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/napalm_get_users.py +1 -0
- {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/napalm_get_vlans.py +2 -1
- {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/napalm_interfaces.py +1 -0
- {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/napalm_lldp_neighbors.py +4 -3
- {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/napalm_network_instances.py +18 -1
- {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/napalm_ping.py +1 -0
- {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/netmiko_cdp_neighbors.py +1 -0
- {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/netmiko_ospf_neighbors.py +1 -0
- {nuts-3.2.0 → nuts-3.4.0}/nuts/context.py +31 -0
- nuts-3.4.0/nuts/helpers/cache.py +58 -0
- {nuts-3.2.0 → nuts-3.4.0}/nuts/helpers/context.py +7 -3
- {nuts-3.2.0 → nuts-3.4.0}/nuts/helpers/filters.py +1 -0
- {nuts-3.2.0 → nuts-3.4.0}/nuts/helpers/result.py +1 -1
- nuts-3.4.0/nuts/hooks.py +9 -0
- {nuts-3.2.0 → nuts-3.4.0}/nuts/index.py +2 -0
- {nuts-3.2.0 → nuts-3.4.0}/nuts/plugin.py +44 -3
- {nuts-3.2.0 → nuts-3.4.0}/nuts/yamlloader.py +4 -1
- {nuts-3.2.0 → nuts-3.4.0}/pyproject.toml +19 -14
- {nuts-3.2.0 → nuts-3.4.0}/LICENSE +0 -0
- {nuts-3.2.0 → nuts-3.4.0}/nuts/__init__.py +0 -0
- {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/__init__.py +0 -0
- {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/netmiko_iperf.py +1 -1
- {nuts-3.2.0 → nuts-3.4.0}/nuts/helpers/__init__.py +0 -0
- {nuts-3.2.0 → nuts-3.4.0}/nuts/helpers/converters.py +0 -0
- {nuts-3.2.0 → nuts-3.4.0}/nuts/helpers/errors.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: nuts
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.0
|
|
4
4
|
Summary: Network Unit Testing System
|
|
5
5
|
Home-page: https://github.com/network-unit-testing-system/nuts
|
|
6
6
|
License: MIT
|
|
@@ -14,13 +14,14 @@ Classifier: Programming Language :: Python :: 3
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.9
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
18
|
Classifier: Topic :: System :: Networking
|
|
18
19
|
Requires-Dist: PyYAML (>=6.0,<7.0)
|
|
19
20
|
Requires-Dist: nornir (>=3.3.0,<4.0.0)
|
|
20
|
-
Requires-Dist: nornir-napalm (>=0.
|
|
21
|
+
Requires-Dist: nornir-napalm (>=0.5.0,<0.6.0)
|
|
21
22
|
Requires-Dist: nornir-netmiko (>=1.0.0,<2.0.0)
|
|
22
23
|
Requires-Dist: nornir-utils (>=0.2.0,<0.3.0)
|
|
23
|
-
Requires-Dist: pytest (>=7
|
|
24
|
+
Requires-Dist: pytest (>=7,<8)
|
|
24
25
|
Project-URL: Repository, https://github.com/network-unit-testing-system/nuts
|
|
25
26
|
Description-Content-Type: text/markdown
|
|
26
27
|
|
|
@@ -76,6 +77,7 @@ Each test bundle contains the following structure:
|
|
|
76
77
|
test_class: <name of the test class>
|
|
77
78
|
label: <label to uniquely identify the test> # optional
|
|
78
79
|
test_execution: <additional data used to execute the test> # optional
|
|
80
|
+
test_extras: <additional data can be provided to the context for custom usage> # optional
|
|
79
81
|
test_data: <data used to generate the test cases>
|
|
80
82
|
...
|
|
81
83
|
```
|
|
@@ -95,6 +97,9 @@ Therefore the key-value pairs must be consistent with the key-value pairs of the
|
|
|
95
97
|
As an example, the test definition `napalm_ping.py` calls a nornir task to execute napalm's ping-command.
|
|
96
98
|
This allows the additional `max_drop` parameter in `test_execution`, since it is in turn pre-defined by napalm.
|
|
97
99
|
|
|
100
|
+
`test_extras`: Additional data that can be accessed through the `nuts_parameters` property.
|
|
101
|
+
These data are not internally utilized and can be passed for use in custom code.
|
|
102
|
+
|
|
98
103
|
`test_data`: Data that is used to parametrize the tests in the test class which have the `pytest.mark.nuts` annotation. It is additionally part of the `nuts_parameters` property.
|
|
99
104
|
|
|
100
105
|
### Example: CDP Neighbors
|
|
@@ -50,6 +50,7 @@ Each test bundle contains the following structure:
|
|
|
50
50
|
test_class: <name of the test class>
|
|
51
51
|
label: <label to uniquely identify the test> # optional
|
|
52
52
|
test_execution: <additional data used to execute the test> # optional
|
|
53
|
+
test_extras: <additional data can be provided to the context for custom usage> # optional
|
|
53
54
|
test_data: <data used to generate the test cases>
|
|
54
55
|
...
|
|
55
56
|
```
|
|
@@ -69,6 +70,9 @@ Therefore the key-value pairs must be consistent with the key-value pairs of the
|
|
|
69
70
|
As an example, the test definition `napalm_ping.py` calls a nornir task to execute napalm's ping-command.
|
|
70
71
|
This allows the additional `max_drop` parameter in `test_execution`, since it is in turn pre-defined by napalm.
|
|
71
72
|
|
|
73
|
+
`test_extras`: Additional data that can be accessed through the `nuts_parameters` property.
|
|
74
|
+
These data are not internally utilized and can be passed for use in custom code.
|
|
75
|
+
|
|
72
76
|
`test_data`: Data that is used to parametrize the tests in the test class which have the `pytest.mark.nuts` annotation. It is additionally part of the `nuts_parameters` property.
|
|
73
77
|
|
|
74
78
|
### Example: CDP Neighbors
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Query BGP neighbors of a device or count them."""
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Callable, List, Any, Tuple
|
|
3
4
|
|
|
4
5
|
import pytest
|
|
5
6
|
from nornir.core.task import MultiResult, Result
|
|
@@ -12,15 +13,23 @@ from nuts.helpers.result import AbstractHostResultExtractor, NutsResult
|
|
|
12
13
|
class BgpNeighborsExtractor(AbstractHostResultExtractor):
|
|
13
14
|
def single_transform(
|
|
14
15
|
self, single_result: MultiResult
|
|
15
|
-
) -> Dict[str, Dict[str, NutsResult]]:
|
|
16
|
+
) -> Dict[Tuple[str, str], Dict[str, NutsResult]]:
|
|
17
|
+
test_exectuion = self._nuts_ctx.nuts_parameters.get("test_execution")
|
|
18
|
+
if test_exectuion:
|
|
19
|
+
vrf = test_exectuion.get("vrf", "global")
|
|
20
|
+
if not vrf:
|
|
21
|
+
vrf = "global"
|
|
22
|
+
else:
|
|
23
|
+
vrf = "global"
|
|
24
|
+
|
|
16
25
|
neighbors = self._simple_extract(single_result)["bgp_neighbors"]
|
|
17
|
-
if
|
|
26
|
+
if vrf not in neighbors:
|
|
18
27
|
return {}
|
|
19
|
-
|
|
20
|
-
router_id =
|
|
28
|
+
scope = neighbors[vrf]
|
|
29
|
+
router_id = scope["router_id"]
|
|
21
30
|
return {
|
|
22
31
|
peer: self._add_local_id(details, router_id)
|
|
23
|
-
for peer, details in
|
|
32
|
+
for peer, details in scope["peers"].items()
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
def _add_local_id(self, element: Dict[str, Any], router_id: str) -> Dict[str, Any]:
|
|
@@ -33,6 +42,8 @@ class BgpNeighborsContext(NornirNutsContext):
|
|
|
33
42
|
return napalm_get
|
|
34
43
|
|
|
35
44
|
def nuts_arguments(self) -> Dict[str, List[str]]:
|
|
45
|
+
# Overrides nuts_arguments.nuts_arguments
|
|
46
|
+
# "test_execution" is not passed to the nuts_task
|
|
36
47
|
return {"getters": ["bgp_neighbors"]}
|
|
37
48
|
|
|
38
49
|
def nuts_extractor(self) -> BgpNeighborsExtractor:
|
|
@@ -48,7 +59,9 @@ class TestNapalmBgpNeighborsCount:
|
|
|
48
59
|
self, single_result: NutsResult, neighbor_count: int
|
|
49
60
|
) -> None:
|
|
50
61
|
assert single_result.result is not None
|
|
51
|
-
assert
|
|
62
|
+
assert (
|
|
63
|
+
len(single_result.result) == neighbor_count
|
|
64
|
+
), f"Expected {neighbor_count}; got {len(single_result.result)}"
|
|
52
65
|
|
|
53
66
|
|
|
54
67
|
class TestNapalmBgpNeighbors:
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
"""Query config of a device."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
2
4
|
from typing import Dict, Callable, List, Any
|
|
3
5
|
|
|
4
6
|
import pytest
|
|
@@ -33,10 +35,10 @@ class TestNapalmConfig:
|
|
|
33
35
|
def test_startup_equals_running_config(
|
|
34
36
|
self, single_result, startup_equals_running_config
|
|
35
37
|
):
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
== single_result.result["config"]["running"]
|
|
40
|
-
)
|
|
41
|
-
== startup_equals_running_config
|
|
38
|
+
pattern = (
|
|
39
|
+
r"! Command: show (startup|running)-config\n"
|
|
40
|
+
r"(! Startup-config last modified at .* by .*\n)?"
|
|
42
41
|
)
|
|
42
|
+
startup = re.sub(pattern, "", single_result.result["config"]["startup"])
|
|
43
|
+
running = re.sub(pattern, "", single_result.result["config"]["running"])
|
|
44
|
+
assert bool(startup == running) == startup_equals_running_config
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Query vlans of a device."""
|
|
2
|
+
|
|
2
3
|
from typing import Dict, Callable, List, Any
|
|
3
4
|
|
|
4
5
|
import pytest
|
|
@@ -49,7 +50,7 @@ class TestNapalmVlans:
|
|
|
49
50
|
class TestNapalmOnlyDefinedVlansExist:
|
|
50
51
|
@pytest.mark.nuts("vlan_tags")
|
|
51
52
|
def test_no_rogue_vlans(self, single_result, vlan_tags):
|
|
52
|
-
assert list(single_result.result.keys()) == sorted(vlan_tags)
|
|
53
|
+
assert sorted(list(single_result.result.keys())) == sorted(vlan_tags)
|
|
53
54
|
|
|
54
55
|
|
|
55
56
|
class TestNapalmInterfaceInVlan:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Query LLDP neighbors of a device."""
|
|
2
|
+
|
|
2
3
|
from typing import Dict, Callable, List, Any
|
|
3
4
|
|
|
4
5
|
import pytest
|
|
@@ -28,9 +29,9 @@ class LldpNeighborsExtractor(AbstractHostResultExtractor):
|
|
|
28
29
|
return element
|
|
29
30
|
|
|
30
31
|
def _add_expanded_remote_port(self, element: Dict[str, Any]) -> Dict[str, Any]:
|
|
31
|
-
element[
|
|
32
|
-
"
|
|
33
|
-
|
|
32
|
+
element["remote_port_expanded"] = (
|
|
33
|
+
InterfaceNameConverter().expand_interface_name(element["remote_port"])
|
|
34
|
+
)
|
|
34
35
|
return element
|
|
35
36
|
|
|
36
37
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
"""Query network instances of a device."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import re
|
|
2
5
|
from typing import Dict, List, Callable, Any
|
|
3
6
|
|
|
4
7
|
import pytest
|
|
@@ -49,7 +52,21 @@ class TestNapalmNetworkInstances:
|
|
|
49
52
|
def test_network_instance_contains_interfaces(
|
|
50
53
|
self, single_result: NutsResult, network_instance: str, interfaces: List[str]
|
|
51
54
|
) -> None:
|
|
52
|
-
|
|
55
|
+
result = copy.deepcopy(single_result.result[network_instance]["interfaces"])
|
|
56
|
+
patterns = len(interfaces)
|
|
57
|
+
matches = 0
|
|
58
|
+
for interface in interfaces:
|
|
59
|
+
pattern = re.compile(interface)
|
|
60
|
+
for i in result:
|
|
61
|
+
if pattern.match(i):
|
|
62
|
+
single_result.result[network_instance]["interfaces"].remove(i)
|
|
63
|
+
if len(result) != len(single_result.result[network_instance]["interfaces"]):
|
|
64
|
+
result = copy.deepcopy(
|
|
65
|
+
single_result.result[network_instance]["interfaces"]
|
|
66
|
+
)
|
|
67
|
+
matches += 1
|
|
68
|
+
assert patterns == matches
|
|
69
|
+
assert result == []
|
|
53
70
|
|
|
54
71
|
@pytest.mark.nuts("network_instance,route_distinguisher")
|
|
55
72
|
def test_route_distinguisher(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Provide necessary information that is needed for a specific test."""
|
|
2
|
+
|
|
2
3
|
import pathlib
|
|
3
4
|
from typing import Any, Callable, Optional, Dict, List
|
|
4
5
|
from pytest import Config
|
|
@@ -7,10 +8,12 @@ from nornir import InitNornir
|
|
|
7
8
|
from nornir.core import Nornir
|
|
8
9
|
from nornir.core.task import AggregatedResult, Result
|
|
9
10
|
from nornir.core.filter import F
|
|
11
|
+
from nornir.core.plugins.inventory import InventoryPluginRegister
|
|
10
12
|
|
|
11
13
|
from nuts.helpers.errors import NutsSetupError
|
|
12
14
|
from nuts.helpers.result import AbstractResultExtractor
|
|
13
15
|
from nuts.helpers.filters import filter_hosts, get_filter_object
|
|
16
|
+
from nuts.helpers.cache import serialize_inventory, CacheInventory
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
class NutsContext:
|
|
@@ -104,6 +107,10 @@ class NornirNutsContext(NutsContext):
|
|
|
104
107
|
self.nornir: Optional[Nornir] = None
|
|
105
108
|
|
|
106
109
|
def initialize(self) -> None:
|
|
110
|
+
"""
|
|
111
|
+
Checks if inventory should be cached, then use global inventory otherwise
|
|
112
|
+
regenerate it continuously.
|
|
113
|
+
"""
|
|
107
114
|
if self.pytestconfig:
|
|
108
115
|
config_file = pathlib.Path(
|
|
109
116
|
self.pytestconfig.getoption("nornir_configuration")
|
|
@@ -111,10 +118,33 @@ class NornirNutsContext(NutsContext):
|
|
|
111
118
|
else:
|
|
112
119
|
config_file = pathlib.Path(self.DEFAULT_NORNIR_CONFIG_FILE)
|
|
113
120
|
|
|
121
|
+
if self.pytestconfig and self.pytestconfig.cache:
|
|
122
|
+
if nornir_inventory := self.pytestconfig.cache.get(
|
|
123
|
+
"nuts/NORNIR_CACHE", None
|
|
124
|
+
):
|
|
125
|
+
InventoryPluginRegister.register("NutsCacheInventory", CacheInventory)
|
|
126
|
+
|
|
127
|
+
self.nornir = InitNornir(
|
|
128
|
+
config_file=str(config_file),
|
|
129
|
+
logging={"enabled": False},
|
|
130
|
+
inventory={
|
|
131
|
+
"plugin": "NutsCacheInventory",
|
|
132
|
+
"options": nornir_inventory,
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
return
|
|
136
|
+
|
|
114
137
|
self.nornir = InitNornir(
|
|
115
138
|
config_file=str(config_file),
|
|
116
139
|
logging={"enabled": False},
|
|
117
140
|
)
|
|
141
|
+
if self.pytestconfig and not self.pytestconfig.getoption(
|
|
142
|
+
"nornir_cache_disabled"
|
|
143
|
+
):
|
|
144
|
+
# pytest cash needs json encodable values
|
|
145
|
+
inventory = serialize_inventory(self.nornir.inventory)
|
|
146
|
+
if self.pytestconfig and self.pytestconfig.cache:
|
|
147
|
+
self.pytestconfig.cache.set("nuts/NORNIR_CACHE", inventory)
|
|
118
148
|
|
|
119
149
|
def nuts_task(self) -> Callable[..., Result]:
|
|
120
150
|
"""
|
|
@@ -202,6 +232,7 @@ class NornirNutsContext(NutsContext):
|
|
|
202
232
|
)
|
|
203
233
|
|
|
204
234
|
self.teardown()
|
|
235
|
+
selected_hosts.close_connections(on_good=True, on_failed=True)
|
|
205
236
|
return overall_results
|
|
206
237
|
|
|
207
238
|
def setup(self) -> None:
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from typing import Any, Dict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from nornir.core.inventory import Group, Groups, Host, Hosts, Inventory, ParentGroups
|
|
5
|
+
from nornir.plugins.inventory.simple import _get_defaults, _get_inventory_element
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def serialize_inventory(inventory: Inventory) -> Dict[str, Dict[str, Any]]:
|
|
9
|
+
data = {
|
|
10
|
+
"hosts": {host: data.dict() for host, data in inventory.hosts.items()},
|
|
11
|
+
"groups": {group: data.dict() for group, data in inventory.groups.items()},
|
|
12
|
+
"defaults": inventory.defaults.dict(),
|
|
13
|
+
}
|
|
14
|
+
return data
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CacheInventory:
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
hosts: Dict[str, Dict[str, Any]],
|
|
21
|
+
groups: Dict[str, Dict[str, Any]],
|
|
22
|
+
defaults: Dict[str, Any],
|
|
23
|
+
) -> None:
|
|
24
|
+
"""
|
|
25
|
+
CacheInventory inspired by the SimpleInventory.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
|
|
29
|
+
hosts: Dict with host name and host.dict() data
|
|
30
|
+
groups: Dict with group name and group.dict() data
|
|
31
|
+
default: defaults.dict() dict data
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
self.hosts_dict = hosts
|
|
35
|
+
self.groups_dict = groups
|
|
36
|
+
self.defaults_dict = defaults
|
|
37
|
+
|
|
38
|
+
def load(self) -> Inventory:
|
|
39
|
+
|
|
40
|
+
defaults = _get_defaults(self.defaults_dict)
|
|
41
|
+
|
|
42
|
+
hosts = Hosts()
|
|
43
|
+
|
|
44
|
+
for n, h in self.hosts_dict.items():
|
|
45
|
+
hosts[n] = _get_inventory_element(Host, h, n, defaults)
|
|
46
|
+
|
|
47
|
+
groups = Groups()
|
|
48
|
+
|
|
49
|
+
for n, g in self.groups_dict.items():
|
|
50
|
+
groups[n] = _get_inventory_element(Group, g, n, defaults)
|
|
51
|
+
|
|
52
|
+
for group in groups.values():
|
|
53
|
+
group.groups = ParentGroups([groups[str(g)] for g in group.groups])
|
|
54
|
+
|
|
55
|
+
for host in hosts.values():
|
|
56
|
+
host.groups = ParentGroups([groups[str(g)] for g in host.groups])
|
|
57
|
+
|
|
58
|
+
return Inventory(hosts=hosts, groups=groups, defaults=defaults)
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Context helper functions
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import types
|
|
5
|
-
from nuts.context import NutsContext
|
|
6
6
|
from typing import Dict, List, Any
|
|
7
|
+
from pytest import Config
|
|
8
|
+
from nuts.context import NutsContext
|
|
7
9
|
|
|
8
10
|
|
|
9
|
-
def load_context(
|
|
11
|
+
def load_context(
|
|
12
|
+
module: types.ModuleType, params: List[Dict[str, Any]], config: Config
|
|
13
|
+
) -> NutsContext:
|
|
10
14
|
context_class = getattr(module, "CONTEXT", NutsContext)
|
|
11
|
-
ctx = context_class(params)
|
|
15
|
+
ctx = context_class(params, pytestconfig=config)
|
|
12
16
|
ctx.initialize()
|
|
13
17
|
return ctx
|
nuts-3.4.0/nuts/hooks.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from pytest import FixtureRequest
|
|
2
|
+
from nuts.context import NutsContext
|
|
3
|
+
from nuts.helpers.result import NutsResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def pytest_nuts_single_result(
|
|
7
|
+
request: FixtureRequest, nuts_ctx: NutsContext, result: NutsResult
|
|
8
|
+
) -> None:
|
|
9
|
+
"""Called in the single result fixture"""
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Allows to indicate only the test class name in a test bundle."""
|
|
2
|
+
|
|
2
3
|
from typing import Optional
|
|
3
4
|
|
|
4
5
|
default_index = {
|
|
@@ -10,6 +11,7 @@ default_index = {
|
|
|
10
11
|
"TestNapalmNetworkInstances": "nuts.base_tests.napalm_network_instances",
|
|
11
12
|
"TestNapalmPing": "nuts.base_tests.napalm_ping",
|
|
12
13
|
"TestNapalmUsers": "nuts.base_tests.napalm_get_users",
|
|
14
|
+
"TestNapalmOnlyDefinedUsersExist": "nuts.base_tests.napalm_get_users",
|
|
13
15
|
"TestNapalmConfig": "nuts.base_tests.napalm_get_config",
|
|
14
16
|
"TestNapalmVlans": "nuts.base_tests.napalm_get_vlans",
|
|
15
17
|
"TestNapalmOnlyDefinedVlansExist": "nuts.base_tests.napalm_get_vlans",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Fixtures"""
|
|
2
|
+
|
|
2
3
|
from typing import Optional, Dict, Any
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
@@ -16,6 +17,12 @@ from nuts.helpers.result import NutsResult
|
|
|
16
17
|
from nuts.yamlloader import NutsYamlFile, get_parametrize_data
|
|
17
18
|
|
|
18
19
|
|
|
20
|
+
def pytest_addhooks(pluginmanager):
|
|
21
|
+
from nuts import hooks
|
|
22
|
+
|
|
23
|
+
pluginmanager.add_hookspecs(hooks)
|
|
24
|
+
|
|
25
|
+
|
|
19
26
|
@pytest.fixture(scope="class")
|
|
20
27
|
def nuts_ctx(request: FixtureRequest) -> NutsContext:
|
|
21
28
|
params = request.node.params
|
|
@@ -26,7 +33,9 @@ def nuts_ctx(request: FixtureRequest) -> NutsContext:
|
|
|
26
33
|
|
|
27
34
|
|
|
28
35
|
@pytest.fixture
|
|
29
|
-
def single_result(
|
|
36
|
+
def single_result(
|
|
37
|
+
nuts_ctx: NutsContext, nuts_test_entry: Dict[str, Any], request: FixtureRequest
|
|
38
|
+
) -> NutsResult:
|
|
30
39
|
"""
|
|
31
40
|
Returns the result which belongs to a specific host
|
|
32
41
|
out of the overall set of results that has been returned by nornir's task.
|
|
@@ -39,6 +48,12 @@ def single_result(nuts_ctx: NutsContext, nuts_test_entry: Dict[str, Any]) -> Nut
|
|
|
39
48
|
"""
|
|
40
49
|
res = nuts_ctx.extractor.single_result(nuts_test_entry)
|
|
41
50
|
res.validate()
|
|
51
|
+
|
|
52
|
+
# Invoke the pytest_nuts_single_result hook to extend result reports.
|
|
53
|
+
request.config.hook.pytest_nuts_single_result(
|
|
54
|
+
request=request, nuts_ctx=nuts_ctx, result=res
|
|
55
|
+
)
|
|
56
|
+
|
|
42
57
|
return res
|
|
43
58
|
|
|
44
59
|
|
|
@@ -64,12 +79,12 @@ def pytest_collect_file(parent: Session, file_path: Path) -> Optional[Collector]
|
|
|
64
79
|
"""
|
|
65
80
|
Performs the collection phase for the given pytest session.
|
|
66
81
|
Collects all test bundles if available, i.e. files starting
|
|
67
|
-
with 'test' and ending in .yaml.
|
|
82
|
+
with 'test' and ending in .yaml or .yml.
|
|
68
83
|
:param parent: pytest session object
|
|
69
84
|
:param file_path: path to test file(s)
|
|
70
85
|
:return: The pytest collector if found
|
|
71
86
|
"""
|
|
72
|
-
if file_path.suffix
|
|
87
|
+
if file_path.suffix in [".yaml", ".yml"] and file_path.name.startswith("test"):
|
|
73
88
|
return NutsYamlFile.from_parent(parent, path=file_path)
|
|
74
89
|
return None
|
|
75
90
|
|
|
@@ -88,3 +103,29 @@ def pytest_addoption(parser: Parser) -> None:
|
|
|
88
103
|
metavar="NORNIR_CONFIG",
|
|
89
104
|
help="nuts nornir configuration file. Default is nr-config.yaml",
|
|
90
105
|
)
|
|
106
|
+
|
|
107
|
+
group.addoption(
|
|
108
|
+
"--nornir-cache-disable",
|
|
109
|
+
action="store_true",
|
|
110
|
+
dest="nornir_cache_disabled",
|
|
111
|
+
default=False,
|
|
112
|
+
help="disable caching of nornir inventory between executions",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
group.addoption(
|
|
116
|
+
"--nornir-cached-inventory",
|
|
117
|
+
action="store_true",
|
|
118
|
+
dest="nornir_cached_inventory",
|
|
119
|
+
default=False,
|
|
120
|
+
help="Uses the chached inventory from the last executions if possible",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def pytest_sessionstart(session: Session) -> None:
|
|
125
|
+
"""Called after the ``Session`` object has been created
|
|
126
|
+
and before performing collection and entering the run test loop.
|
|
127
|
+
|
|
128
|
+
:param pytest.Session session: The pytest session object.
|
|
129
|
+
"""
|
|
130
|
+
if not session.config.getoption("nornir_cached_inventory") and session.config.cache:
|
|
131
|
+
session.config.cache.set("nuts/NORNIR_CACHE", None)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Converts a test bundle (YAML file) into a test class for pytest.
|
|
2
2
|
Based on https://docs.pytest.org/en/stable/example/nonpython.html#yaml-plugin
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import importlib
|
|
5
6
|
from nuts.helpers.context import load_context
|
|
6
7
|
import types
|
|
@@ -106,12 +107,14 @@ class NutsTestFile(pytest.Module):
|
|
|
106
107
|
|
|
107
108
|
test_data = self.test_entry.get("test_data", [])
|
|
108
109
|
test_execution = self.test_entry.get("test_execution")
|
|
110
|
+
test_extras = self.test_entry.get("test_extras")
|
|
109
111
|
yield NutsTestClass.from_parent(
|
|
110
112
|
self,
|
|
111
113
|
name=name,
|
|
112
114
|
class_name=class_name,
|
|
113
115
|
test_data=test_data,
|
|
114
116
|
test_execution=test_execution,
|
|
117
|
+
test_extras=test_extras,
|
|
115
118
|
)
|
|
116
119
|
|
|
117
120
|
|
|
@@ -195,7 +198,7 @@ def get_parametrize_data(
|
|
|
195
198
|
|
|
196
199
|
data = getattr(nuts_test_instance, "test_entry")
|
|
197
200
|
|
|
198
|
-
ctx = load_context(nuts_test_instance.module, data)
|
|
201
|
+
ctx = load_context(nuts_test_instance.module, data, metafunc.config)
|
|
199
202
|
data["test_data"] = ctx.parametrize(data.get("test_data", []))
|
|
200
203
|
|
|
201
204
|
return (
|
|
@@ -4,10 +4,17 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "nuts"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.4.0"
|
|
8
8
|
description = "Network Unit Testing System"
|
|
9
|
-
authors = [
|
|
10
|
-
|
|
9
|
+
authors = [
|
|
10
|
+
"Lukas Murer, Méline Sieber, Urs Baumann, Matthias Gabriel, Florian Bruhin",
|
|
11
|
+
"Marco Martinez",
|
|
12
|
+
"Severin Grimm",
|
|
13
|
+
]
|
|
14
|
+
maintainers = [
|
|
15
|
+
"Marco Martinez <marco.maritnez@ost.ch>",
|
|
16
|
+
"Urs Baumann <github@m.ubaumann.ch>",
|
|
17
|
+
]
|
|
11
18
|
classifiers = ["Framework :: Pytest", "Topic :: System :: Networking"]
|
|
12
19
|
homepage = "https://github.com/network-unit-testing-system/nuts"
|
|
13
20
|
repository = "https://github.com/network-unit-testing-system/nuts"
|
|
@@ -16,26 +23,26 @@ readme = "README.md"
|
|
|
16
23
|
|
|
17
24
|
[tool.poetry.dependencies]
|
|
18
25
|
python = "^3.8.1"
|
|
19
|
-
pytest = "^7
|
|
26
|
+
pytest = "^7"
|
|
20
27
|
PyYAML = "^6.0"
|
|
21
28
|
nornir = "^3.3.0"
|
|
22
|
-
nornir-napalm = "^0.
|
|
29
|
+
nornir-napalm = "^0.5.0"
|
|
23
30
|
nornir-netmiko = "^1.0.0"
|
|
24
31
|
nornir-utils = "^0.2.0"
|
|
25
32
|
|
|
26
33
|
[tool.poetry.dev-dependencies]
|
|
27
34
|
tox = "^4.4.11"
|
|
28
35
|
pytest-cov = "^4.0.0"
|
|
29
|
-
black = "^
|
|
30
|
-
sphinx = "^7
|
|
36
|
+
black = "^24.2.0"
|
|
37
|
+
sphinx = "^7"
|
|
31
38
|
mypy = "^1.0.1"
|
|
32
|
-
flake8 = "^
|
|
39
|
+
flake8 = "^7.0.0"
|
|
33
40
|
types-PyYAML = "*"
|
|
34
41
|
types-setuptools = "*"
|
|
35
42
|
types-toml = "*"
|
|
36
43
|
|
|
37
44
|
[tool.poetry.plugins."pytest11"]
|
|
38
|
-
"nuts"="nuts.plugin"
|
|
45
|
+
"nuts" = "nuts.plugin"
|
|
39
46
|
# why "pytest11": https://github.com/pytest-dev/pytest/commit/ed03eef81b220199a819632a27f9c452b8e1fb81
|
|
40
47
|
# https://docs.pytest.org/en/latest/how-to/writing_plugins.html#making-your-plugin-installable-by-others
|
|
41
48
|
|
|
@@ -50,12 +57,11 @@ filterwarnings = [
|
|
|
50
57
|
"ignore:Using or importing the ABCs from 'collections':DeprecationWarning:jnpr\\.junos\\.factcache",
|
|
51
58
|
"ignore:NutsYamlFile\\.fspath is deprecated and will be replaced by NutsYamlFile\\.path\\.",
|
|
52
59
|
]
|
|
53
|
-
markers = [
|
|
54
|
-
|
|
55
|
-
]
|
|
60
|
+
markers = ["nuts_test_ctx: A NutsContext to be used in tests."]
|
|
61
|
+
norecursedirs = "mock_project"
|
|
56
62
|
|
|
57
63
|
[tool.mypy]
|
|
58
|
-
python_version = 3.8
|
|
64
|
+
python_version = "3.8"
|
|
59
65
|
|
|
60
66
|
### --strict
|
|
61
67
|
warn_unused_configs = true
|
|
@@ -149,4 +155,3 @@ commands = pytest {posargs} --junitxml=test-reports/pytest.xml --cov="{envsitepa
|
|
|
149
155
|
# envsitepackagesdir see: https://tox.readthedocs.io/en/latest/example/pytest.html
|
|
150
156
|
|
|
151
157
|
"""
|
|
152
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Query bandwidth performance between two devices."""
|
|
2
|
+
|
|
2
3
|
import pytest
|
|
3
4
|
import json
|
|
4
5
|
from typing import Dict, Callable, Any
|
|
@@ -106,7 +107,6 @@ CONTEXT = IperfContext
|
|
|
106
107
|
|
|
107
108
|
|
|
108
109
|
class IperfResultError(Error):
|
|
109
|
-
|
|
110
110
|
"""Error in iperf result JSON."""
|
|
111
111
|
|
|
112
112
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|