Skip to content

Commit e484357

Browse files
[READY] Datasource testing cleanup (#1073)
2 parents 7b01378 + faad710 commit e484357

File tree

2 files changed

+101
-147
lines changed

2 files changed

+101
-147
lines changed

facts/datasource.py

Lines changed: 65 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -22,36 +22,24 @@
2222
# =============================================================================
2323

2424

25-
def isuntested(value):
26-
# pylint: disable=unused-argument
27-
"""dummy function for untested values"""
25+
def is_untested(_val) -> bool:
26+
"""Dummy validator for untested fields, always passes."""
2827
return True
2928

3029

31-
def isvalidhostname(hostname):
32-
"""
33-
test for valid short hostname with letters, numbers, and dashes
34-
cannot begin or end with a dash
35-
"""
36-
pattern = r"^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])$"
37-
result = re.match(pattern, hostname)
38-
if result:
39-
return True
40-
return False
30+
def is_valid_hostname(val: str) -> bool:
31+
"""Test for valid short hostname: letters, numbers, dashes. No leading/trailing dash."""
32+
return bool(re.match(r"^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$", val))
4133

4234

43-
def is_valid_asset_id(asset_id):
44-
"""
45-
test for valid asset ID, which has the same constraints as a hostname
46-
"""
47-
return isvalidhostname(asset_id)
35+
def is_valid_asset_id(val: str) -> bool:
36+
"""Test for valid asset ID (same constraints as hostname)."""
37+
return is_valid_hostname(val)
4838

4939

50-
def isvalidmodel(model):
51-
"""
52-
test for valid switch model (enumerated)
53-
"""
54-
return model in {
40+
def is_valid_switch_model(val: str) -> bool:
41+
"""Test for valid switch model (enumerated)."""
42+
return val in {
5543
"ex2200-48p",
5644
"ex2200-48t",
5745
"ex2200-24p",
@@ -70,62 +58,62 @@ def isvalidmodel(model):
7058
}
7159

7260

73-
def isvalidip(addr):
74-
"""test for valid v4 or v6 ip"""
61+
def is_valid_ipv4_address(val: str) -> bool:
62+
"""Test for valid IPv4 address."""
7563
try:
76-
ipaddress.ip_address(addr)
64+
ipaddress.IPv4Address(val)
65+
return True
7766
except ValueError:
7867
return False
79-
return True
8068

8169

82-
def is_valid_v6_suffix(suffix):
83-
"""
84-
test for valid v6 suffix
85-
"""
86-
pattern = r"^[0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4})*$"
87-
88-
if not re.match(pattern, suffix):
70+
def is_valid_ipv6_address(val: str) -> bool:
71+
"""Test for valid IPv6 address."""
72+
try:
73+
ipaddress.IPv6Address(val)
74+
return True
75+
except ValueError:
8976
return False
9077

91-
groups = suffix.split(":")
92-
return all(group and len(group) <= 4 for group in groups)
78+
79+
def is_valid_ipv6_suffix(val: str) -> bool:
80+
"""Test for valid IPv6 suffix by prepending a dummy prefix."""
81+
return is_valid_ipv6_address(f"fe80::{val}")
9382

9483

95-
def isvalidsubnet(subnet):
96-
"""test for valid v4 or v6 subnet"""
84+
def is_valid_ipv4_subnet(val: str) -> bool:
85+
"""Test for valid IPv4 subnet."""
9786
try:
98-
ipaddress.ip_network(subnet, strict=True)
87+
ipaddress.IPv4Network(val, strict=True)
88+
return True
9989
except ValueError:
10090
return False
101-
return True
10291

10392

104-
def isvalidiporempty(val):
105-
"""test for valid ip or empty"""
106-
return isvalidip(val) or val == ""
93+
def is_valid_ipv6_subnet(val: str) -> bool:
94+
"""Test for valid IPv6 subnet."""
95+
try:
96+
ipaddress.IPv6Network(val, strict=True)
97+
return True
98+
except ValueError:
99+
return False
107100

108101

109-
def isvalidmac(macaddr):
110-
"""test for valid colon seperate mac address"""
111-
pattern = r"^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})$"
112-
result = re.match(pattern, macaddr)
113-
if result:
114-
return True
115-
return False
102+
# try to avoid regex but netaddr.mac_unix_expanded would lead to
103+
# surprises, this is readable enough
104+
def is_valid_mac_address(val: str) -> bool:
105+
"""Test for valid colon-separated MAC address."""
106+
return bool(re.match(r"^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$", val))
116107

117108

118-
def isvalidwifi24chan(chan):
119-
"""test for valid 2.4Ghz WiFi channel"""
120-
return isint(chan) and int(chan) in {1, 6, 11}
109+
def is_valid_wifi_24ghz_chan(val: int | str) -> bool:
110+
"""Test for valid 2.4GHz WiFi channel."""
111+
return is_non_negative_int(val) and int(val) in {1, 6, 11}
121112

122113

123-
def isvalidwifi5chan(chan):
124-
"""
125-
test for valid 5Ghz WiFi channel
126-
allows DFS channels
127-
"""
128-
return isint(chan) and int(chan) in {
114+
def is_valid_wifi_5ghz_chan(val: int | str) -> bool:
115+
"""Test for valid 5GHz WiFi channel, including DFS channels."""
116+
return is_non_negative_int(val) and int(val) in {
129117
32,
130118
36,
131119
40,
@@ -158,71 +146,34 @@ def isvalidwifi5chan(chan):
158146
}
159147

160148

161-
def is_valid_pi_vlan(vlan):
162-
"""test for valid PI vlan"""
163-
# we currently constrain PI use to 3 existing vlans
164-
# this can be extended later as needed
165-
return isint(vlan) and int(vlan) in {
166-
107,
167-
110,
168-
507,
169-
}
149+
def is_valid_pi_vlan(val: int | str) -> bool:
150+
"""Test for valid PI VLAN: currently constrained to 107, 110, 507."""
151+
return is_non_negative_int(val) and int(val) in {107, 110, 507}
170152

171153

172-
def isint(val):
173-
"""test for integer"""
154+
def is_non_negative_int(val: int | str) -> bool:
155+
"""Test for non-negative integer (0 or greater)."""
156+
if isinstance(val, int):
157+
return val >= 0
174158
return val.isdigit()
175159

176160

177-
def isintorempty(val):
178-
"""test for integer or empty"""
179-
return val.isdigit() or val == ""
180-
181-
182-
def isvalidhierarchy(val):
183-
"""test for valid switch hierarchy"""
184-
pattern = r"^([A-Z]+[.][0-9])$"
185-
result = re.match(pattern, val)
186-
if result:
187-
return True
188-
return False
161+
def is_valid_switch_hierarchy(val: str) -> bool:
162+
"""Test for valid switch hierarchy (e.g. ABC.1)."""
163+
return bool(re.match(r"^[A-Z]+\.[0-9]$", val))
189164

190165

191-
def isvalid_p_o_e(val):
192-
"""test for valid POE flag"""
193-
pattern = r"^(POE)|(-)$"
194-
result = re.match(pattern, val)
195-
if result:
196-
return True
197-
return False
166+
def is_in_ap_list(val: str) -> bool:
167+
"""Test for existence of serial number in aps.csv."""
168+
df = pd.read_csv("aps/aps.csv")
169+
return val in df["serial"].values
198170

199171

200-
def isvalidnoiselevel(val):
201-
"""test for valid noise level [Quiet, Normal, Loud]"""
202-
if val in ["Quiet", "Normal", "Loud", "??"]:
203-
return True
204-
return False
205-
206-
207-
def isinaplist(val):
208-
"""test for existence of the value in apuse.csv"""
209-
lines = []
210-
with open("aps/aps.csv", "r", encoding="utf-8") as fh:
211-
lines = fh.readlines()
212-
aplist = set()
213-
for line in lines[1:]:
214-
cols = line.split(",")
215-
aplist.add(cols[0])
216-
return val in aplist
217-
218-
219-
def isvalidtype(val):
172+
def is_valid_switch_type(val: str) -> bool:
220173
"""test for valid switch type, denoted by existence of file in types dir"""
221174
type_path = "../switch-configuration/config/types/"
222175
valid = [f for f in listdir(type_path) if isfile(join(type_path, f))]
223-
if val in valid:
224-
return True
225-
return False
176+
return val in valid
226177

227178

228179
def is_valid_map_coordinate(val: int | float | str) -> bool:
@@ -408,7 +359,7 @@ def validate_dataframe(
408359
Args:
409360
df: DataFrame to validate
410361
validators: List of validator functions, one per column.
411-
Use None or isuntested to skip validation for a column.
362+
Use None or is_untested to skip validation for a column.
412363
filename: Filename for error messages
413364
skip_header: If True, skip the first row
414365
@@ -422,7 +373,7 @@ def validate_dataframe(
422373
# Skip if no validator or beyond available columns
423374
if validator is None or col_idx >= len(df.columns):
424375
continue
425-
if validator is isuntested:
376+
if validator is is_untested:
426377
continue
427378

428379
value = df.iloc[row_idx, col_idx]

facts/test_datasources.py

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ def test_apuse_csv():
1414
"header": True,
1515
"count": 9,
1616
"cols": [
17-
ds.isvalidhostname,
18-
ds.isinaplist,
19-
ds.isvalidip,
20-
ds.isvalidwifi24chan,
21-
ds.isvalidwifi5chan,
22-
ds.isint,
23-
ds.isintorempty,
17+
ds.is_valid_hostname,
18+
ds.is_in_ap_list,
19+
ds.is_valid_ipv4_address,
20+
ds.is_valid_wifi_24ghz_chan,
21+
ds.is_valid_wifi_5ghz_chan,
22+
ds.is_non_negative_int, # config version
23+
ds.is_non_negative_int, # map id
2424
ds.is_valid_map_coordinate,
2525
ds.is_valid_map_coordinate,
2626
],
@@ -36,8 +36,8 @@ def test_aps_csv():
3636
"header": True,
3737
"count": 2,
3838
"cols": [
39-
ds.isuntested,
40-
ds.isvalidmac,
39+
ds.is_untested,
40+
ds.is_valid_mac_address,
4141
],
4242
}
4343
result, err = ds.test_csvfile(meta)
@@ -52,8 +52,8 @@ def test_pis_csv():
5252
"count": 3,
5353
"cols": [
5454
ds.is_valid_asset_id,
55-
ds.isvalidmac,
56-
ds.is_valid_v6_suffix,
55+
ds.is_valid_mac_address,
56+
ds.is_valid_ipv6_suffix,
5757
],
5858
}
5959
result, err = ds.test_csvfile(meta)
@@ -67,7 +67,7 @@ def test_piuse_csv():
6767
"header": True,
6868
"count": 3,
6969
"cols": [
70-
ds.isvalidhostname,
70+
ds.is_valid_hostname,
7171
ds.is_valid_asset_id,
7272
ds.is_valid_pi_vlan,
7373
],
@@ -82,7 +82,10 @@ def test_routerlist_csv():
8282
"file": "./routers/routerlist.csv",
8383
"header": True,
8484
"count": 2,
85-
"cols": [ds.isvalidhostname, ds.isvalidip],
85+
"cols": [
86+
ds.is_valid_hostname,
87+
ds.is_valid_ipv6_address,
88+
],
8689
}
8790
result, err = ds.test_csvfile(meta)
8891
assert result, err
@@ -95,11 +98,11 @@ def test_serverlist_csv():
9598
"header": True,
9699
"count": 5,
97100
"cols": [
98-
ds.isvalidhostname,
99-
ds.isvalidmac,
100-
ds.isvalidiporempty,
101-
ds.isvalidiporempty,
102-
ds.isuntested,
101+
ds.is_valid_hostname,
102+
ds.is_valid_mac_address,
103+
ds.is_valid_ipv6_address,
104+
ds.is_valid_ipv4_address,
105+
ds.is_untested,
103106
],
104107
}
105108
result, err = ds.test_csvfile(meta)
@@ -113,15 +116,15 @@ def test_switchtypes_tsv():
113116
"header": False,
114117
"count": "9+",
115118
"cols": [
116-
ds.isvalidhostname,
117-
ds.isint,
118-
ds.isint,
119-
ds.isvalidip,
120-
ds.isvalidtype,
121-
ds.isvalidhierarchy,
122-
ds.isuntested,
123-
ds.isvalidmodel,
124-
ds.isvalidmac,
119+
ds.is_valid_hostname,
120+
ds.is_non_negative_int,
121+
ds.is_non_negative_int,
122+
ds.is_valid_ipv6_address,
123+
ds.is_valid_switch_type,
124+
ds.is_valid_switch_hierarchy,
125+
ds.is_untested,
126+
ds.is_valid_switch_model,
127+
ds.is_valid_mac_address,
125128
],
126129
}
127130
result, err = ds.test_tsvfile(meta)
@@ -138,12 +141,12 @@ def test_vlansd_tsv():
138141
"header": False,
139142
"count": "6+",
140143
"cols": [
141-
ds.isuntested,
142-
ds.isuntested,
143-
ds.isuntested,
144-
ds.isvalidsubnet,
145-
ds.isvalidsubnet,
146-
ds.isuntested,
144+
ds.is_untested,
145+
ds.is_untested,
146+
ds.is_untested,
147+
ds.is_valid_ipv6_subnet,
148+
ds.is_valid_ipv4_subnet,
149+
ds.is_untested,
147150
],
148151
}
149152
result, err = ds.test_tsvfile(meta)

0 commit comments

Comments
 (0)