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.
Files changed (30) hide show
  1. {nuts-3.2.0 → nuts-3.4.0}/PKG-INFO +8 -3
  2. {nuts-3.2.0 → nuts-3.4.0}/README.md +4 -0
  3. {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/napalm_bgp_neighbors.py +20 -7
  4. {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/napalm_get_arp.py +1 -0
  5. {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/napalm_get_config.py +8 -6
  6. {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/napalm_get_users.py +1 -0
  7. {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/napalm_get_vlans.py +2 -1
  8. {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/napalm_interfaces.py +1 -0
  9. {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/napalm_lldp_neighbors.py +4 -3
  10. {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/napalm_network_instances.py +18 -1
  11. {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/napalm_ping.py +1 -0
  12. {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/netmiko_cdp_neighbors.py +1 -0
  13. {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/netmiko_ospf_neighbors.py +1 -0
  14. {nuts-3.2.0 → nuts-3.4.0}/nuts/context.py +31 -0
  15. nuts-3.4.0/nuts/helpers/cache.py +58 -0
  16. {nuts-3.2.0 → nuts-3.4.0}/nuts/helpers/context.py +7 -3
  17. {nuts-3.2.0 → nuts-3.4.0}/nuts/helpers/filters.py +1 -0
  18. {nuts-3.2.0 → nuts-3.4.0}/nuts/helpers/result.py +1 -1
  19. nuts-3.4.0/nuts/hooks.py +9 -0
  20. {nuts-3.2.0 → nuts-3.4.0}/nuts/index.py +2 -0
  21. {nuts-3.2.0 → nuts-3.4.0}/nuts/plugin.py +44 -3
  22. {nuts-3.2.0 → nuts-3.4.0}/nuts/yamlloader.py +4 -1
  23. {nuts-3.2.0 → nuts-3.4.0}/pyproject.toml +19 -14
  24. {nuts-3.2.0 → nuts-3.4.0}/LICENSE +0 -0
  25. {nuts-3.2.0 → nuts-3.4.0}/nuts/__init__.py +0 -0
  26. {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/__init__.py +0 -0
  27. {nuts-3.2.0 → nuts-3.4.0}/nuts/base_tests/netmiko_iperf.py +1 -1
  28. {nuts-3.2.0 → nuts-3.4.0}/nuts/helpers/__init__.py +0 -0
  29. {nuts-3.2.0 → nuts-3.4.0}/nuts/helpers/converters.py +0 -0
  30. {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.2.0
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.4.0,<0.5.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.3.0,<8.0.0)
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
- from typing import Dict, Callable, List, Any
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 "global" not in neighbors:
26
+ if vrf not in neighbors:
18
27
  return {}
19
- global_scope = neighbors["global"]
20
- router_id = global_scope["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 global_scope["peers"].items()
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 len(single_result.result) == neighbor_count
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,5 @@
1
1
  """Query arp table of a device."""
2
+
2
3
  from typing import Dict, Callable, List, Any, Text
3
4
 
4
5
  import pytest
@@ -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
- assert (
37
- bool(
38
- single_result.result["config"]["startup"]
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 users of a device."""
2
+
2
3
  from typing import Dict, Callable, List, Any
3
4
 
4
5
  import pytest
@@ -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 interfaces and their information of a device."""
2
+
2
3
  from typing import Dict, Callable, List, Any
3
4
 
4
5
  import pytest
@@ -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
- "remote_port_expanded"
33
- ] = InterfaceNameConverter().expand_interface_name(element["remote_port"])
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
- assert single_result.result[network_instance]["interfaces"] == interfaces
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
  """Let a device ping another device."""
2
+
2
3
  from enum import Enum
3
4
  from typing import Dict, Callable, Any, List
4
5
 
@@ -1,4 +1,5 @@
1
1
  """Query CDP neighbors of a device."""
2
+
2
3
  from typing import Callable, Dict, Any
3
4
 
4
5
  import pytest
@@ -1,4 +1,5 @@
1
1
  """Query OSPF neighbors of a device or count them."""
2
+
2
3
  from typing import Callable, Dict, Any
3
4
 
4
5
  import pytest
@@ -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(module: types.ModuleType, params: List[Dict[str, Any]]) -> NutsContext:
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
@@ -2,6 +2,7 @@
2
2
  Functions to filter the nornir inventory and used in conjunction with
3
3
  a context's nornir_filter function.
4
4
  """
5
+
5
6
  from typing import Optional, Dict, Any, List, Union
6
7
  from nornir.core.filter import F, OR
7
8
 
@@ -46,7 +46,7 @@ class NutsResult:
46
46
  f"An exception has occurred while executing nornir:\n"
47
47
  f"{header}\n"
48
48
  f"{self._result}"
49
- )
49
+ ) from self.exception
50
50
  if self.failed:
51
51
  raise NutsNornirError(f"Nornir execution has failed:\n" f"{self._result}")
52
52
 
@@ -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(nuts_ctx: NutsContext, nuts_test_entry: Dict[str, Any]) -> NutsResult:
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 == ".yaml" and file_path.name.startswith("test"):
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.2.0"
7
+ version = "3.4.0"
8
8
  description = "Network Unit Testing System"
9
- authors = ["Lukas Murer, Méline Sieber, Urs Baumann, Matthias Gabriel, Florian Bruhin", "Marco Martinez", "Severin Grimm"]
10
- maintainers = ["Marco Martinez <marco.maritnez@ost.ch>", "Urs Baumann <github@m.ubaumann.ch>"]
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.3.0"
26
+ pytest = "^7"
20
27
  PyYAML = "^6.0"
21
28
  nornir = "^3.3.0"
22
- nornir-napalm = "^0.4.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 = "^23.3.0"
30
- sphinx = "^7.1.2"
36
+ black = "^24.2.0"
37
+ sphinx = "^7"
31
38
  mypy = "^1.0.1"
32
- flake8 = "^6.0.0"
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
- "nuts_test_ctx: A NutsContext to be used in tests."
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