Skip to content

Commit d16a843

Browse files
Move json utils tests
1 parent 623ab4e commit d16a843

File tree

3 files changed

+86
-158
lines changed

3 files changed

+86
-158
lines changed

shared/python/utils.py

Lines changed: 0 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,6 @@ def get_azure_role_guid(role_name: str) -> Optional[str]:
628628

629629
return None
630630

631-
632631
def create_bicep_deployment_group(rg_name: str, rg_location: str, deployment: str | INFRASTRUCTURE, bicep_parameters: dict, bicep_parameters_file: str = 'params.json', rg_tags: dict | None = None, is_debug: bool = False) -> Output:
633632
"""
634633
Create a Bicep deployment in a resource group, writing parameters to a file and running the deployment.
@@ -693,7 +692,6 @@ def create_bicep_deployment_group(rg_name: str, rg_location: str, deployment: st
693692
print('\nDeploying bicep...\n')
694693
return run(cmd, f"Deployment '{deployment_name}' succeeded", f"Deployment '{deployment_name}' failed.", print_command_to_run = False)
695694

696-
697695
# TODO: Reconcile this with apimtypes.py _get_project_root
698696
def find_project_root() -> str:
699697
"""
@@ -720,7 +718,6 @@ def find_project_root() -> str:
720718
# If we can't find the project root, raise an error
721719
raise FileNotFoundError('Could not determine project root directory')
722720

723-
724721
def create_bicep_deployment_group_for_sample(sample_name: str, rg_name: str, rg_location: str, bicep_parameters: dict, bicep_parameters_file: str = 'params.json', rg_tags: dict | None = None, is_debug: bool = False) -> Output:
725722
"""
726723
Create a Bicep deployment for a sample, handling the working directory change automatically.
@@ -774,7 +771,6 @@ def create_bicep_deployment_group_for_sample(sample_name: str, rg_name: str, rg_
774771
os.chdir(original_cwd)
775772
print(f'📁 Restored working directory to: {original_cwd}')
776773

777-
778774
def create_resource_group(rg_name: str, resource_group_location: str | None = None, tags: dict | None = None) -> None:
779775
"""
780776
Create a resource group in Azure if it does not already exist.
@@ -1065,80 +1061,6 @@ def read_policy_xml(policy_xml_filepath_or_filename: str, named_values: dict[str
10651061

10661062
return policy_template_xml
10671063

1068-
def extract_json(text: str) -> Any:
1069-
"""
1070-
Extract the first valid JSON object or array from a string and return it as a Python object.
1071-
1072-
This function searches the input string for the first occurrence of a JSON object or array (delimited by '{' or '['),
1073-
and attempts to decode it using json.JSONDecoder().raw_decode. If the input is already valid JSON, it is returned as a Python object.
1074-
If no valid JSON is found, None is returned.
1075-
1076-
Args:
1077-
text (str): The string to search for a JSON object or array.
1078-
1079-
Returns:
1080-
Any | None: The extracted JSON as a Python object (dict or list), or None if not found or not valid.
1081-
"""
1082-
1083-
if not isinstance(text, str):
1084-
return None
1085-
1086-
# If the string is already valid JSON, parse and return it as a Python object.
1087-
if is_string_json(text):
1088-
try:
1089-
return json.loads(text)
1090-
except json.JSONDecodeError:
1091-
# If JSON parsing fails despite is_string_json returning True,
1092-
# fall through to substring search
1093-
pass
1094-
1095-
decoder = json.JSONDecoder()
1096-
1097-
for start in range(len(text)):
1098-
if text[start] in ('{', '['):
1099-
try:
1100-
obj, _ = decoder.raw_decode(text[start:])
1101-
return obj
1102-
except Exception:
1103-
continue
1104-
1105-
return None
1106-
1107-
def is_string_json(text: str) -> bool:
1108-
"""
1109-
Check if the provided string is a valid JSON object or array.
1110-
1111-
Args:
1112-
text (str): The string to check.
1113-
1114-
Returns:
1115-
bool: True if the string is valid JSON, False otherwise.
1116-
"""
1117-
1118-
# Accept only str, bytes, or bytearray as valid input for JSON parsing.
1119-
if not isinstance(text, (str, bytes, bytearray)):
1120-
return False
1121-
1122-
# Skip empty or whitespace-only strings
1123-
if not text or not text.strip():
1124-
return False
1125-
1126-
# First try JSON parsing (handles double quotes)
1127-
try:
1128-
json.loads(text)
1129-
return True
1130-
except json.JSONDecodeError:
1131-
pass
1132-
1133-
# If JSON fails, try Python literal evaluation (handles single quotes)
1134-
try:
1135-
ast.literal_eval(text)
1136-
return True
1137-
except (ValueError, SyntaxError):
1138-
pass
1139-
1140-
return False
1141-
11421064
def get_account_info() -> Tuple[str, str, str, str]:
11431065
"""
11441066
Retrieve the current Azure account information using the Azure CLI.
@@ -1225,7 +1147,6 @@ def get_frontdoor_url(deployment_name: INFRASTRUCTURE, rg_name: str) -> str | No
12251147

12261148
return afd_endpoint_url
12271149

1228-
12291150
def get_apim_url(rg_name: str) -> str | None:
12301151
"""
12311152
Retrieve the gateway URL for the API Management service in the specified resource group.
@@ -1255,7 +1176,6 @@ def get_apim_url(rg_name: str) -> str | None:
12551176

12561177
return apim_endpoint_url
12571178

1258-
12591179
def get_appgw_endpoint(rg_name: str) -> tuple[str | None, str | None]:
12601180
"""
12611181
Retrieve the hostname and public IP address for the Application Gateway in the specified resource group.
@@ -1661,8 +1581,6 @@ def test_url_preflight_check(deployment: INFRASTRUCTURE, rg_name: str, apim_gate
16611581

16621582
return endpoint_url
16631583

1664-
1665-
16661584
def get_endpoints(deployment: INFRASTRUCTURE, rg_name: str) -> Endpoints:
16671585
print_message(f'Identifying possible endpoints for infrastructure {deployment}...')
16681586

tests/python/test_json_utils.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""
2+
Unit tests for test_utils.py.
3+
"""
4+
5+
import pytest
6+
import json
7+
import json_utils
8+
9+
10+
# ------------------------------
11+
# is_string_json
12+
# ------------------------------
13+
14+
@pytest.mark.parametrize(
15+
'input_str,expected',
16+
[
17+
('{\"a\": 1}', True),
18+
('[1, 2, 3]', True),
19+
('not json', False),
20+
('{\"a\": 1', False),
21+
('', False),
22+
(None, False),
23+
(123, False),
24+
]
25+
)
26+
def test_is_string_json(input_str, expected):
27+
assert json_utils.is_string_json(input_str) is expected
28+
29+
30+
# ------------------------------
31+
# EXTRACT_JSON EDGE CASES
32+
# ------------------------------
33+
34+
@pytest.mark.parametrize(
35+
'input_val,expected',
36+
[
37+
(None, None),
38+
(123, None),
39+
([], None),
40+
('', None),
41+
(' ', None),
42+
('not json', None),
43+
('{\"a\": 1}', {'a': 1}),
44+
('[1, 2, 3]', [1, 2, 3]),
45+
(' {\"a\": 1} ', {'a': 1}),
46+
('prefix {\"foo\": 42} suffix', {'foo': 42}),
47+
('prefix [1, 2, 3] suffix', [1, 2, 3]),
48+
('{\"a\": 1}{\"b\": 2}', {'a': 1}), # Only first JSON object
49+
('[1, 2, 3][4, 5, 6]', [1, 2, 3]), # Only first JSON array
50+
('{\"a\": [1, 2, {\"b\": 3}]}', {'a': [1, 2, {'b': 3}]}),
51+
('\n\t{\"a\": 1}\n', {'a': 1}),
52+
('{\"a\": \"b \\u1234\"}', {'a': 'b \u1234'}),
53+
('{\"a\": 1} [2, 3]', {'a': 1}), # Object before array
54+
('[2, 3] {\"a\": 1}', [2, 3]), # Array before object
55+
('{\"a\": 1, \"b\": {\"c\": 2}}', {'a': 1, 'b': {'c': 2}}),
56+
('{\"a\": 1, \"b\": [1, 2, 3]}', {'a': 1, 'b': [1, 2, 3]}),
57+
('\n\n[\n1, 2, 3\n]\n', [1, 2, 3]),
58+
('{\"a\": 1, \"b\": null}', {'a': 1, 'b': None}),
59+
('{\"a\": true, \"b\": false}', {'a': True, 'b': False}),
60+
('{\"a\": 1, \"b\": \"c\"}', {'a': 1, 'b': 'c'}),
61+
('{\"a\": 1, \"b\": [1, 2, {\"c\": 3}]} ', {'a': 1, 'b': [1, 2, {'c': 3}]}),
62+
('{\"a\": 1, \"b\": [1, 2, {\"c\": 3, \"d\": [4, 5]}]} ', {'a': 1, 'b': [1, 2, {'c': 3, 'd': [4, 5]}]}),
63+
]
64+
)
65+
def test_extract_json_edge_cases(input_val, expected):
66+
"""Test extract_json with a wide range of edge cases and malformed input."""
67+
result = json_utils.extract_json(input_val)
68+
assert result == expected
69+
70+
def test_extract_json_large_object():
71+
"""Test extract_json with a large JSON object."""
72+
large_obj = {'a': list(range(1000)), 'b': {'c': 'x' * 1000}}
73+
s = json.dumps(large_obj)
74+
assert json_utils.extract_json(s) == large_obj
75+
76+
def test_extract_json_multiple_json_types():
77+
"""Test extract_json returns the first valid JSON (object or array) in the string."""
78+
s = '[1,2,3]{"a": 1}'
79+
assert json_utils.extract_json(s) == [1, 2, 3]
80+
s2 = '{"a": 1}[1,2,3]'
81+
assert json_utils.extract_json(s2) == {'a': 1}

tests/python/test_utils.py

Lines changed: 5 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,7 @@
1111
import pytest
1212
from apimtypes import INFRASTRUCTURE, APIM_SKU
1313
import utils
14-
15-
# ------------------------------
16-
# is_string_json
17-
# ------------------------------
18-
19-
@pytest.mark.parametrize(
20-
'input_str,expected',
21-
[
22-
('{\"a\": 1}', True),
23-
('[1, 2, 3]', True),
24-
('not json', False),
25-
('{\"a\": 1', False),
26-
('', False),
27-
(None, False),
28-
(123, False),
29-
]
30-
)
31-
def test_is_string_json(input_str, expected):
32-
assert utils.is_string_json(input_str) is expected
14+
import json_utils
3315

3416
# ------------------------------
3517
# get_account_info
@@ -234,59 +216,6 @@ def mock_inspect_currentframe():
234216
utils.read_policy_xml('policy.xml', {'key': 'value'})
235217

236218

237-
# ------------------------------
238-
# EXTRACT_JSON EDGE CASES
239-
# ------------------------------
240-
241-
@pytest.mark.parametrize(
242-
'input_val,expected',
243-
[
244-
(None, None),
245-
(123, None),
246-
([], None),
247-
('', None),
248-
(' ', None),
249-
('not json', None),
250-
('{\"a\": 1}', {'a': 1}),
251-
('[1, 2, 3]', [1, 2, 3]),
252-
(' {\"a\": 1} ', {'a': 1}),
253-
('prefix {\"foo\": 42} suffix', {'foo': 42}),
254-
('prefix [1, 2, 3] suffix', [1, 2, 3]),
255-
('{\"a\": 1}{\"b\": 2}', {'a': 1}), # Only first JSON object
256-
('[1, 2, 3][4, 5, 6]', [1, 2, 3]), # Only first JSON array
257-
('{\"a\": [1, 2, {\"b\": 3}]}', {'a': [1, 2, {'b': 3}]}),
258-
('\n\t{\"a\": 1}\n', {'a': 1}),
259-
('{\"a\": \"b \\u1234\"}', {'a': 'b \u1234'}),
260-
('{\"a\": 1} [2, 3]', {'a': 1}), # Object before array
261-
('[2, 3] {\"a\": 1}', [2, 3]), # Array before object
262-
('{\"a\": 1, \"b\": {\"c\": 2}}', {'a': 1, 'b': {'c': 2}}),
263-
('{\"a\": 1, \"b\": [1, 2, 3]}', {'a': 1, 'b': [1, 2, 3]}),
264-
('\n\n[\n1, 2, 3\n]\n', [1, 2, 3]),
265-
('{\"a\": 1, \"b\": null}', {'a': 1, 'b': None}),
266-
('{\"a\": true, \"b\": false}', {'a': True, 'b': False}),
267-
('{\"a\": 1, \"b\": \"c\"}', {'a': 1, 'b': 'c'}),
268-
('{\"a\": 1, \"b\": [1, 2, {\"c\": 3}]} ', {'a': 1, 'b': [1, 2, {'c': 3}]}),
269-
('{\"a\": 1, \"b\": [1, 2, {\"c\": 3, \"d\": [4, 5]}]} ', {'a': 1, 'b': [1, 2, {'c': 3, 'd': [4, 5]}]}),
270-
]
271-
)
272-
def test_extract_json_edge_cases(input_val, expected):
273-
"""Test extract_json with a wide range of edge cases and malformed input."""
274-
result = utils.extract_json(input_val)
275-
assert result == expected
276-
277-
def test_extract_json_large_object():
278-
"""Test extract_json with a large JSON object."""
279-
large_obj = {'a': list(range(1000)), 'b': {'c': 'x' * 1000}}
280-
s = json.dumps(large_obj)
281-
assert utils.extract_json(s) == large_obj
282-
283-
def test_extract_json_multiple_json_types():
284-
"""Test extract_json returns the first valid JSON (object or array) in the string."""
285-
s = '[1,2,3]{"a": 1}'
286-
assert utils.extract_json(s) == [1, 2, 3]
287-
s2 = '{"a": 1}[1,2,3]'
288-
assert utils.extract_json(s2) == {'a': 1}
289-
290219
# ------------------------------
291220
# validate_infrastructure
292221
# ------------------------------
@@ -801,10 +730,10 @@ def mock_chdir(path):
801730

802731
def test_extract_json_invalid_input():
803732
"""Test extract_json with various invalid inputs."""
804-
assert utils.extract_json(None) is None
805-
assert utils.extract_json(123) is None
806-
assert utils.extract_json([1, 2, 3]) is None
807-
assert utils.extract_json('not json at all') is None
733+
assert json_utils.extract_json(None) is None
734+
assert json_utils.extract_json(123) is None
735+
assert json_utils.extract_json([1, 2, 3]) is None
736+
assert json_utils.extract_json('not json at all') is None
808737

809738

810739
def test_generate_signing_key_format():

0 commit comments

Comments
 (0)