-
Notifications
You must be signed in to change notification settings - Fork 32
/
Copy pathtest_diffsync_model.py
389 lines (287 loc) · 13.4 KB
/
test_diffsync_model.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
"""Unit tests for the DiffSyncModel class.
Copyright (c) 2020 Network To Code, LLC <info@networktocode.com>
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.
"""
from typing import List
import pytest
from diffsync import DiffSyncModel
from diffsync.enum import DiffSyncModelFlags
from diffsync.exceptions import ObjectStoreWrongType, ObjectAlreadyExists, ObjectNotFound
from .conftest import Device, Interface
def test_generic_diffsync_model_methods(generic_diffsync_model, make_site):
"""Check the default behavior of various APIs of a DiffSyncModel."""
assert str(generic_diffsync_model) == ""
assert repr(generic_diffsync_model) == 'diffsyncmodel ""'
assert generic_diffsync_model.get_type() == "diffsyncmodel"
assert generic_diffsync_model.get_identifiers() == {}
assert generic_diffsync_model.get_attrs() == {}
assert generic_diffsync_model.get_unique_id() == ""
assert generic_diffsync_model.get_shortname() == ""
with pytest.raises(ObjectStoreWrongType):
generic_diffsync_model.add_child(make_site())
def test_diffsync_model_dict_with_no_data(generic_diffsync_model):
assert generic_diffsync_model.dict() == {"model_flags": DiffSyncModelFlags.NONE}
def test_diffsync_model_json_with_no_data(generic_diffsync_model):
assert generic_diffsync_model.json() == "{}"
def test_diffsync_model_str_with_no_data(generic_diffsync_model):
assert generic_diffsync_model.str() == "diffsyncmodel: : {}"
def test_diffsync_model_subclass_getters(make_site, make_device, make_interface):
"""Check that the DiffSyncModel APIs work correctly for various subclasses."""
site1 = make_site()
device1 = make_device()
device1_eth0 = make_interface()
assert str(site1) == "site1"
assert str(device1) == "device1"
assert str(device1_eth0) == "device1__eth0"
assert repr(site1) == 'site "site1"'
assert repr(device1) == 'device "device1"'
assert repr(device1_eth0) == 'interface "device1__eth0"'
assert site1.get_type() == "site"
assert device1.get_type() == "device"
assert device1_eth0.get_type() == "interface"
assert site1.get_identifiers() == {"name": "site1"}
assert device1.get_identifiers() == {"name": "device1"}
assert device1_eth0.get_identifiers() == {"device_name": "device1", "name": "eth0"}
# Ordering of identifiers must be consistent
assert list(device1_eth0.get_identifiers().keys()) == ["device_name", "name"]
assert site1.get_attrs() == {} # note that identifiers are not included in get_attrs()
assert device1.get_attrs() == {"role": "default"} # site_name field is not in _attributes, so not in get_attrs
assert device1_eth0.get_attrs() == {"interface_type": "ethernet", "description": None}
# Ordering of attributes must be consistent
assert list(device1_eth0.get_attrs().keys()) == ["interface_type", "description"]
assert site1.get_unique_id() == "site1"
assert device1.get_unique_id() == "device1"
assert device1_eth0.get_unique_id() == "device1__eth0"
assert site1.get_shortname() == "site1"
assert device1.get_shortname() == "device1"
assert device1_eth0.get_shortname() == "eth0"
def test_diffsync_model_dict_with_data(make_interface):
intf = make_interface()
# dict() includes all fields, even those set to default values
assert intf.dict() == {
"description": None,
"device_name": "device1",
"interface_type": "ethernet",
"model_flags": DiffSyncModelFlags.NONE,
"name": "eth0",
}
def test_diffsync_model_json_with_data(make_interface):
intf = make_interface()
# json() omits default values for brevity
assert intf.json() == '{"device_name":"device1","name":"eth0"}'
def test_diffsync_model_str_with_data(make_interface):
intf = make_interface()
# str() only includes _attributes
assert intf.str() == "interface: device1__eth0: {'interface_type': 'ethernet', 'description': None}"
def test_diffsync_model_subclass_add_remove(make_site, make_device, make_interface):
"""Check that the DiffSyncModel add_child/remove_child APIs work correctly for various subclasses."""
site1 = make_site()
device1 = make_device()
device1_eth0 = make_interface()
assert site1.devices == []
site1.add_child(device1)
assert site1.devices == ["device1"]
with pytest.raises(ObjectStoreWrongType):
site1.add_child(device1_eth0)
with pytest.raises(ObjectAlreadyExists) as error:
site1.add_child(device1)
error_model = error.value.args[1]
assert isinstance(error_model, DiffSyncModel)
assert error_model is device1
site1.remove_child(device1)
assert site1.devices == []
with pytest.raises(ObjectStoreWrongType):
site1.remove_child(device1_eth0)
with pytest.raises(ObjectNotFound):
site1.remove_child(device1)
assert device1.interfaces == []
device1.add_child(device1_eth0)
assert device1.interfaces == ["device1__eth0"]
with pytest.raises(ObjectStoreWrongType):
device1.add_child(site1)
with pytest.raises(ObjectAlreadyExists) as error:
device1.add_child(device1_eth0)
error_model = error.value.args[1]
assert isinstance(error_model, DiffSyncModel)
assert error_model is device1_eth0
device1.remove_child(device1_eth0)
assert device1.interfaces == []
with pytest.raises(ObjectStoreWrongType):
device1.remove_child(site1)
with pytest.raises(ObjectNotFound):
device1.remove_child(device1_eth0)
def test_diffsync_model_dict_with_children(generic_adapter, make_site, make_device, make_interface):
site1 = make_site(diffsync=generic_adapter)
device1 = make_device(diffsync=generic_adapter)
device1_eth0 = make_interface(diffsync=generic_adapter)
site1.add_child(device1)
device1.add_child(device1_eth0)
# test error handling - diffsync knows about site and device but not interface
generic_adapter.add(site1)
generic_adapter.add(device1)
assert site1.dict() == {"devices": ["device1"], "model_flags": DiffSyncModelFlags.NONE, "name": "site1"}
def test_diffsync_model_json_with_children(generic_adapter, make_site, make_device, make_interface):
site1 = make_site(diffsync=generic_adapter)
device1 = make_device(diffsync=generic_adapter)
device1_eth0 = make_interface(diffsync=generic_adapter)
site1.add_child(device1)
device1.add_child(device1_eth0)
# test error handling - diffsync knows about site and device but not interface
generic_adapter.add(site1)
generic_adapter.add(device1)
assert site1.json() == '{"name":"site1","devices":["device1"]}'
def test_diffsync_model_str_with_children(generic_adapter, make_site, make_device, make_interface):
site1 = make_site(diffsync=generic_adapter)
device1 = make_device(diffsync=generic_adapter)
device1_eth0 = make_interface(diffsync=generic_adapter)
site1.add_child(device1)
device1.add_child(device1_eth0)
# test error handling - diffsync knows about site and device but not interface
generic_adapter.add(site1)
generic_adapter.add(device1)
assert (
site1.str()
== """\
site: site1: {}
devices
device: device1: {'role': 'default'}
interfaces
device1__eth0 (ERROR: details unavailable)\
"""
)
assert (
site1.str(include_children=False)
== """\
site: site1: {}
devices: ['device1']\
"""
)
def test_diffsync_model_subclass_crud(generic_adapter):
"""Test basic CRUD operations on generic DiffSyncModel subclasses."""
device1 = Device.create(generic_adapter, {"name": "device1"}, {"role": "spine"})
assert isinstance(device1, Device)
assert device1.adapter == generic_adapter
assert device1.name == "device1"
assert device1.role == "spine"
device1_eth0 = Interface.create(
generic_adapter,
{"name": "eth0", "device_name": "device1"},
{"description": "some description"},
)
assert isinstance(device1_eth0, Interface)
assert device1_eth0.adapter == generic_adapter
assert device1_eth0.name == "eth0"
assert device1_eth0.device_name == "device1"
assert device1_eth0.description == "some description"
device1 = device1.update({"site_name": "site1", "role": "leaf"})
assert isinstance(device1, Device)
assert device1.name == "device1"
assert device1.site_name == "site1"
assert device1.role == "leaf"
device1_eth0 = device1_eth0.update({"description": ""})
assert isinstance(device1_eth0, Interface)
assert device1_eth0.name == "eth0"
assert device1_eth0.device_name == "device1"
assert device1_eth0.description == ""
# TODO: negative tests - try to update identifiers with update(), for example
device1 = device1.delete()
assert isinstance(device1, Device)
device1_eth0.delete()
assert isinstance(device1_eth0, Interface)
def test_diffsync_model_subclass_validation():
"""Verify that invalid subclasses of DiffSyncModel are detected at declaration time."""
# Pylint would complain because we're not actually using any of the classes declared below
# pylint: disable=unused-variable
with pytest.raises(AttributeError) as excinfo:
class BadIdentifier(DiffSyncModel):
"""Model with an _identifiers referencing a nonexistent field."""
_identifiers = ("name",)
assert "_identifiers" in str(excinfo.value)
assert "name" in str(excinfo.value)
with pytest.raises(AttributeError) as excinfo:
class BadShortname(DiffSyncModel):
"""Model with a _shortname referencing a nonexistent field."""
_identifiers = ("name",)
_shortname = ("short_name",)
name: str
assert "_shortname" in str(excinfo.value)
assert "short_name" in str(excinfo.value)
with pytest.raises(AttributeError) as excinfo:
class BadAttributes(DiffSyncModel):
"""Model with _attributes referencing a nonexistent field."""
_identifiers = ("name",)
_shortname = ("short_name",)
_attributes = ("my_attr",)
name: str
# Note that short_name doesn't have a type annotation - making sure this works too
short_name: str = "short_name"
assert "_attributes" in str(excinfo.value)
assert "my_attr" in str(excinfo.value)
with pytest.raises(AttributeError) as excinfo:
class BadChildren(DiffSyncModel):
"""Model with _children referencing a nonexistent field."""
_identifiers = ("name",)
_shortname = ("short_name",)
_attributes = ("my_attr",)
_children = {"device": "devices"}
name: str
short_name: str = "short_name"
my_attr: int = 0
assert "_children" in str(excinfo.value)
assert "devices" in str(excinfo.value)
with pytest.raises(AttributeError) as excinfo:
class IdAttrOverlap(DiffSyncModel):
"""Model including a field in both _identifiers and _attributes."""
_identifiers = ("name",)
_attributes = ("name",)
name: str
assert "both _identifiers and _attributes" in str(excinfo.value)
assert "name" in str(excinfo.value)
with pytest.raises(AttributeError) as excinfo:
class IdChildOverlap(DiffSyncModel):
"""Model including a field in both _identifiers and _children."""
_identifiers = ("names",)
_children = {"name": "names"}
names: str
assert "both _identifiers and _children" in str(excinfo.value)
assert "names" in str(excinfo.value)
with pytest.raises(AttributeError) as excinfo:
class AttrChildOverlap(DiffSyncModel):
"""Model including a field in both _attributes and _children."""
_attributes = ("devices",)
_children = {"device": "devices"}
devices: List
assert "both _attributes and _children" in str(excinfo.value)
assert "devices" in str(excinfo.value)
def test_diffsync_model_subclass_inheritance():
"""Verify that the class validation works properly even with a hierarchy of subclasses."""
# Pylint would complain because we're not actually using any of the classes declared below
# pylint: disable=unused-variable
class Alpha(DiffSyncModel):
"""A model class representing a single Greek letter."""
_modelname = "alpha"
_identifiers = ("name",)
_shortname = ("name",)
_attributes = ("letter",)
_children = {"number": "numbers"}
name: str
letter: str
numbers: List = []
class Beta(Alpha):
"""A model class representing a single Greek letter in both English and Spanish."""
_modelname = "beta"
_identifiers = ("name", "nombre") # reference parent field, as well as a new field of our own
_attributes = ("letter", "letra") # reference parent field, as well as a new field of our own
nombre: str
letra: str
beta = Beta(name="Beta", letter="β", nombre="Beta", letra="β")
assert beta.get_unique_id() == "Beta__Beta"
assert beta.get_attrs() == {"letter": "β", "letra": "β"}