CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
Ardupilot

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.

GitHub Repository: Ardupilot/ardupilot
Path: blob/master/Tools/autotest/unittest/annotate_params_unittest.py
Views: 1799
1
#!/usr/bin/env python3
2
3
'''
4
These are the unit tests for the python script that fetches online ArduPilot
5
parameter documentation (if not cached) and adds it to the specified file or
6
to all *.param and *.parm files in the specified directory.
7
8
AP_FLAKE8_CLEAN
9
10
Author: Amilcar do Carmo Lucas, IAV GmbH
11
'''
12
13
import tempfile
14
from unittest.mock import patch, mock_open
15
import os
16
import unittest
17
import xml.etree.ElementTree as ET
18
import requests
19
import mock
20
from annotate_params import get_xml_data, remove_prefix, split_into_lines, create_doc_dict, \
21
format_columns, update_parameter_documentation, print_read_only_params, \
22
BASE_URL, PARAM_DEFINITION_XML_FILE
23
24
25
class TestParamDocsUpdate(unittest.TestCase):
26
27
def setUp(self):
28
# Create a temporary directory
29
self.temp_dir = tempfile.mkdtemp()
30
31
# Create a temporary file
32
self.temp_file = tempfile.NamedTemporaryFile(delete=False)
33
34
# Create a dictionary of parameter documentation
35
self.doc_dict = {
36
"PARAM1": {
37
"humanName": "Param 1",
38
"documentation": ["Documentation for Param 1"],
39
"fields": {"Field1": "Value1", "Field2": "Value2"},
40
"values": {"Code1": "Value1", "Code2": "Value2"}
41
},
42
"PARAM2": {
43
"humanName": "Param 2",
44
"documentation": ["Documentation for Param 2"],
45
"fields": {"Field3": "Value3", "Field4": "Value4"},
46
"values": {"Code3": "Value3", "Code4": "Value4"}
47
},
48
"PARAM_1": {
49
"humanName": "Param _ 1",
50
"documentation": ["Documentation for Param_1"],
51
"fields": {"Field_1": "Value_1", "Field_2": "Value_2"},
52
"values": {"Code_1": "Value_1", "Code_2": "Value_2"}
53
},
54
}
55
56
@patch('builtins.open', new_callable=mock_open, read_data='<root></root>')
57
@patch('os.path.isfile')
58
def test_get_xml_data_local_file(self, mock_isfile, mock_open):
59
# Mock the isfile function to return True
60
mock_isfile.return_value = True
61
62
# Call the function with a local file
63
result = get_xml_data("/path/to/local/file/", ".", "test.xml")
64
65
# Check the result
66
self.assertIsInstance(result, ET.Element)
67
68
# Assert that the file was opened correctly
69
mock_open.assert_called_once_with('./test.xml', 'r', encoding='utf-8')
70
71
@patch('requests.get')
72
def test_get_xml_data_remote_file(self, mock_get):
73
# Mock the response
74
mock_get.return_value.status_code = 200
75
mock_get.return_value.text = "<root></root>"
76
77
# Remove the test.xml file if it exists
78
try:
79
os.remove("test.xml")
80
except FileNotFoundError:
81
pass
82
83
# Call the function with a remote file
84
result = get_xml_data("http://example.com/", ".", "test.xml")
85
86
# Check the result
87
self.assertIsInstance(result, ET.Element)
88
89
# Assert that the requests.get function was called once
90
mock_get.assert_called_once_with("http://example.com/test.xml", timeout=5)
91
92
@patch('os.path.isfile')
93
def test_get_xml_data_script_dir_file(self, mock_isfile):
94
# Mock the isfile function to return False for the current directory and True for the script directory
95
def side_effect(filename):
96
return True
97
mock_isfile.side_effect = side_effect
98
99
# Mock the open function to return a dummy XML string
100
mock_open = mock.mock_open(read_data='<root></root>')
101
with patch('builtins.open', mock_open):
102
# Call the function with a filename that exists in the script directory
103
result = get_xml_data(BASE_URL, ".", PARAM_DEFINITION_XML_FILE)
104
105
# Check the result
106
self.assertIsInstance(result, ET.Element)
107
108
# Assert that the file was opened correctly
109
mock_open.assert_called_once_with(os.path.join('.', PARAM_DEFINITION_XML_FILE), 'r', encoding='utf-8')
110
111
def test_get_xml_data_no_requests_package(self):
112
# Temporarily remove the requests module
113
with patch.dict('sys.modules', {'requests': None}):
114
115
# Remove the test.xml file if it exists
116
try:
117
os.remove("test.xml")
118
except FileNotFoundError:
119
pass
120
121
# Call the function with a remote file
122
with self.assertRaises(SystemExit):
123
get_xml_data("http://example.com/", ".", "test.xml")
124
125
@patch('requests.get')
126
def test_get_xml_data_request_failure(self, mock_get):
127
# Mock the response
128
mock_get.side_effect = requests.exceptions.RequestException
129
130
# Remove the test.xml file if it exists
131
try:
132
os.remove("test.xml")
133
except FileNotFoundError:
134
pass
135
136
# Call the function with a remote file
137
with self.assertRaises(SystemExit):
138
get_xml_data("http://example.com/", ".", "test.xml")
139
140
@patch('requests.get')
141
def test_get_xml_data_valid_xml(self, mock_get):
142
# Mock the response
143
mock_get.return_value.status_code = 200
144
mock_get.return_value.text = "<root></root>"
145
146
# Call the function with a remote file
147
result = get_xml_data("http://example.com/", ".", "test.xml")
148
149
# Check the result
150
self.assertIsInstance(result, ET.Element)
151
152
@patch('requests.get')
153
def test_get_xml_data_invalid_xml(self, mock_get):
154
# Mock the response
155
mock_get.return_value.status_code = 200
156
mock_get.return_value.text = "<root><invalid></root>"
157
158
# Remove the test.xml file if it exists
159
try:
160
os.remove("test.xml")
161
except FileNotFoundError:
162
pass
163
164
# Call the function with a remote file
165
with self.assertRaises(ET.ParseError):
166
get_xml_data("http://example.com/", ".", "test.xml")
167
168
@patch('requests.get')
169
@patch('os.path.isfile')
170
def test_get_xml_data_missing_file(self, mock_isfile, mock_get):
171
# Mock the isfile function to return False
172
mock_isfile.return_value = False
173
# Mock the requests.get call to raise FileNotFoundError
174
mock_get.side_effect = FileNotFoundError
175
176
# Remove the test.xml file if it exists
177
try:
178
os.remove("test.xml")
179
except FileNotFoundError:
180
pass
181
182
# Call the function with a local file
183
with self.assertRaises(FileNotFoundError):
184
get_xml_data("/path/to/local/file/", ".", "test.xml")
185
186
@patch('requests.get')
187
def test_get_xml_data_network_issue(self, mock_get):
188
# Mock the response
189
mock_get.side_effect = requests.exceptions.ConnectionError
190
191
# Call the function with a remote file
192
with self.assertRaises(SystemExit):
193
get_xml_data("http://example.com/", ".", "test.xml")
194
195
def test_remove_prefix(self):
196
# Test case 1: Normal operation
197
self.assertEqual(remove_prefix("prefix_test", "prefix_"), "test")
198
199
# Test case 2: Prefix not present
200
self.assertEqual(remove_prefix("test", "prefix_"), "test")
201
202
# Test case 3: Empty string
203
self.assertEqual(remove_prefix("", "prefix_"), "")
204
205
def test_split_into_lines(self):
206
# Test case 1: Normal operation
207
string_to_split = "This is a test string. It should be split into several lines."
208
maximum_line_length = 12
209
expected_output = ["This is a", "test string.", "It should be", "split into", "several", "lines."]
210
self.assertEqual(split_into_lines(string_to_split, maximum_line_length), expected_output)
211
212
# Test case 2: String shorter than maximum line length
213
string_to_split = "Short"
214
maximum_line_length = 10
215
expected_output = ["Short"]
216
self.assertEqual(split_into_lines(string_to_split, maximum_line_length), expected_output)
217
218
# Test case 3: Empty string
219
string_to_split = ""
220
maximum_line_length = 10
221
expected_output = []
222
self.assertEqual(split_into_lines(string_to_split, maximum_line_length), expected_output)
223
224
def test_create_doc_dict(self):
225
# Mock XML data
226
xml_data = '''
227
<root>
228
<param name="PARAM1" humanName="Param 1" documentation="Documentation for Param 1">
229
<field name="Field1">Value1</field>
230
<field name="Field2">Value2</field>
231
<values>
232
<value code="Code1">Value1</value>
233
<value code="Code2">Value2</value>
234
</values>
235
</param>
236
<param name="PARAM2" humanName="Param 2" documentation="Documentation for Param 2">
237
<field name="Units">m/s</field>
238
<field name="UnitText">meters per second</field>
239
<values>
240
<value code="Code3">Value3</value>
241
<value code="Code4">Value4</value>
242
</values>
243
</param>
244
</root>
245
'''
246
root = ET.fromstring(xml_data)
247
248
# Expected output
249
expected_output = {
250
"PARAM1": {
251
"humanName": "Param 1",
252
"documentation": ["Documentation for Param 1"],
253
"fields": {"Field1": "Value1", "Field2": "Value2"},
254
"values": {"Code1": "Value1", "Code2": "Value2"}
255
},
256
"PARAM2": {
257
"humanName": "Param 2",
258
"documentation": ["Documentation for Param 2"],
259
"fields": {"Units": "m/s (meters per second)"},
260
"values": {"Code3": "Value3", "Code4": "Value4"}
261
}
262
}
263
264
# Call the function with the mock XML data
265
result = create_doc_dict(root, "VehicleType")
266
267
# Check the result
268
self.assertEqual(result, expected_output)
269
270
def test_format_columns(self):
271
# Define the input
272
values = {
273
"Key1": "Value1",
274
"Key2": "Value2",
275
"Key3": "Value3",
276
"Key4": "Value4",
277
"Key5": "Value5",
278
"Key6": "Value6",
279
"Key7": "Value7",
280
"Key8": "Value8",
281
"Key9": "Value9",
282
"Key10": "Value10",
283
"Key11": "Value11",
284
"Key12": "Value12",
285
}
286
287
# Define the expected output
288
expected_output = [
289
'Key1: Value1 Key7: Value7',
290
'Key2: Value2 Key8: Value8',
291
'Key3: Value3 Key9: Value9',
292
'Key4: Value4 Key10: Value10',
293
'Key5: Value5 Key11: Value11',
294
'Key6: Value6 Key12: Value12',
295
]
296
297
# Call the function with the input
298
result = format_columns(values)
299
300
# Check the result
301
self.assertEqual(result, expected_output)
302
303
self.assertEqual(format_columns({}), [])
304
305
def test_update_parameter_documentation(self):
306
# Write some initial content to the temporary file
307
with open(self.temp_file.name, "w", encoding="utf-8") as file:
308
file.write("PARAM1 100\n")
309
310
# Call the function with the temporary file
311
update_parameter_documentation(self.doc_dict, self.temp_file.name)
312
313
# Read the updated content from the temporary file
314
with open(self.temp_file.name, "r", encoding="utf-8") as file:
315
updated_content = file.read()
316
317
# Check if the file has been updated correctly
318
self.assertIn("Param 1", updated_content)
319
self.assertIn("Documentation for Param 1", updated_content)
320
self.assertIn("Field1: Value1", updated_content)
321
self.assertIn("Field2: Value2", updated_content)
322
self.assertIn("Code1: Value1", updated_content)
323
self.assertIn("Code2: Value2", updated_content)
324
325
def test_update_parameter_documentation_sorting_none(self):
326
# Write some initial content to the temporary file
327
# With stray leading and trailing whitespaces
328
with open(self.temp_file.name, "w", encoding="utf-8") as file:
329
file.write("PARAM2 100\n PARAM_1 100 \nPARAM3 3\nPARAM4 4\nPARAM5 5\nPARAM1 100\n")
330
331
# Call the function with the temporary file
332
update_parameter_documentation(self.doc_dict, self.temp_file.name)
333
334
# Read the updated content from the temporary file
335
with open(self.temp_file.name, "r", encoding="utf-8") as file:
336
updated_content = file.read()
337
338
expected_content = '''# Param 2
339
# Documentation for Param 2
340
# Field3: Value3
341
# Field4: Value4
342
# Code3: Value3
343
# Code4: Value4
344
PARAM2 100
345
346
# Param _ 1
347
# Documentation for Param_1
348
# Field_1: Value_1
349
# Field_2: Value_2
350
# Code_1: Value_1
351
# Code_2: Value_2
352
PARAM_1 100
353
PARAM3 3
354
PARAM4 4
355
PARAM5 5
356
357
# Param 1
358
# Documentation for Param 1
359
# Field1: Value1
360
# Field2: Value2
361
# Code1: Value1
362
# Code2: Value2
363
PARAM1 100
364
'''
365
self.assertEqual(updated_content, expected_content)
366
367
def test_update_parameter_documentation_sorting_missionplanner(self):
368
# Write some initial content to the temporary file
369
with open(self.temp_file.name, "w", encoding="utf-8") as file:
370
file.write("PARAM2 100 # ignore, me\nPARAM_1\t100\nPARAM1,100\n")
371
372
# Call the function with the temporary file
373
update_parameter_documentation(self.doc_dict, self.temp_file.name, "missionplanner")
374
375
# Read the updated content from the temporary file
376
with open(self.temp_file.name, "r", encoding="utf-8") as file:
377
updated_content = file.read()
378
379
expected_content = '''# Param _ 1
380
# Documentation for Param_1
381
# Field_1: Value_1
382
# Field_2: Value_2
383
# Code_1: Value_1
384
# Code_2: Value_2
385
PARAM_1\t100
386
387
# Param 1
388
# Documentation for Param 1
389
# Field1: Value1
390
# Field2: Value2
391
# Code1: Value1
392
# Code2: Value2
393
PARAM1,100
394
395
# Param 2
396
# Documentation for Param 2
397
# Field3: Value3
398
# Field4: Value4
399
# Code3: Value3
400
# Code4: Value4
401
PARAM2 100 # ignore, me
402
'''
403
self.assertEqual(updated_content, expected_content)
404
405
def test_update_parameter_documentation_sorting_mavproxy(self):
406
# Write some initial content to the temporary file
407
with open(self.temp_file.name, "w", encoding="utf-8") as file:
408
file.write("PARAM2 100\nPARAM_1\t100\nPARAM1,100\n")
409
410
# Call the function with the temporary file
411
update_parameter_documentation(self.doc_dict, self.temp_file.name, "mavproxy")
412
413
# Read the updated content from the temporary file
414
with open(self.temp_file.name, "r", encoding="utf-8") as file:
415
updated_content = file.read()
416
417
expected_content = '''# Param 1
418
# Documentation for Param 1
419
# Field1: Value1
420
# Field2: Value2
421
# Code1: Value1
422
# Code2: Value2
423
PARAM1,100
424
425
# Param 2
426
# Documentation for Param 2
427
# Field3: Value3
428
# Field4: Value4
429
# Code3: Value3
430
# Code4: Value4
431
PARAM2 100
432
433
# Param _ 1
434
# Documentation for Param_1
435
# Field_1: Value_1
436
# Field_2: Value_2
437
# Code_1: Value_1
438
# Code_2: Value_2
439
PARAM_1\t100
440
'''
441
self.assertEqual(updated_content, expected_content)
442
443
def test_update_parameter_documentation_invalid_line_format(self):
444
# Write some initial content to the temporary file with an invalid line format
445
with open(self.temp_file.name, "w", encoding="utf-8") as file:
446
file.write("%INVALID_LINE_FORMAT\n")
447
448
# Call the function with the temporary file
449
with self.assertRaises(SystemExit) as cm:
450
update_parameter_documentation(self.doc_dict, self.temp_file.name)
451
452
# Check if the SystemExit exception contains the expected message
453
self.assertEqual(cm.exception.code, "Invalid line in input file")
454
455
@patch('logging.Logger.info')
456
def test_print_read_only_params(self, mock_info):
457
# Mock XML data
458
xml_data = '''
459
<root>
460
<param name="PARAM1" humanName="Param 1" documentation="Documentation for Param 1">
461
<field name="ReadOnly">True</field>
462
<field name="Field1">Value1</field>
463
<field name="Field2">Value2</field>
464
<values>
465
<value code="Code1">Value1</value>
466
<value code="Code2">Value2</value>
467
</values>
468
</param>
469
<param name="PARAM2" humanName="Param 2" documentation="Documentation for Param 2">
470
<field name="Field3">Value3</field>
471
<field name="Field4">Value4</field>
472
<values>
473
<value code="Code3">Value3</value>
474
<value code="Code4">Value4</value>
475
</values>
476
</param>
477
</root>
478
'''
479
root = ET.fromstring(xml_data)
480
doc_dict = create_doc_dict(root, "VehicleType")
481
482
# Call the function with the mock XML data
483
print_read_only_params(doc_dict)
484
485
# Check if the parameter name was logged
486
mock_info.assert_has_calls([mock.call('ReadOnly parameters:'), mock.call('PARAM1')])
487
488
def test_update_parameter_documentation_invalid_target(self):
489
# Call the function with an invalid target
490
with self.assertRaises(ValueError):
491
update_parameter_documentation(self.doc_dict, "invalid_target")
492
493
def test_invalid_parameter_name(self):
494
# Write some initial content to the temporary file
495
with open(self.temp_file.name, "w", encoding="utf-8") as file:
496
file.write("INVALID_$PARAM 100\n")
497
498
# Call the function with the temporary file
499
with self.assertRaises(SystemExit):
500
update_parameter_documentation(self.doc_dict, self.temp_file.name)
501
502
def test_update_parameter_documentation_too_long_parameter_name(self):
503
# Write some initial content to the temporary file
504
with open(self.temp_file.name, "w", encoding="utf-8") as file:
505
file.write("TOO_LONG_PARAMETER_NAME 100\n")
506
507
# Call the function with the temporary file
508
with self.assertRaises(SystemExit):
509
update_parameter_documentation(self.doc_dict, self.temp_file.name)
510
511
@patch('logging.Logger.warning')
512
def test_missing_parameter_documentation(self, mock_warning):
513
# Write some initial content to the temporary file
514
with open(self.temp_file.name, "w", encoding="utf-8") as file:
515
file.write("MISSING_DOC_PARA 100\n")
516
517
# Call the function with the temporary file
518
update_parameter_documentation(self.doc_dict, self.temp_file.name)
519
520
# Check if the warnings were logged
521
mock_warning.assert_has_calls([
522
mock.call('Read file %s with %d parameters, but only %s of which got documented', self.temp_file.name, 1, 0),
523
mock.call('No documentation found for: %s', 'MISSING_DOC_PARA')
524
])
525
526
def test_empty_parameter_file(self):
527
# Call the function with the temporary file
528
update_parameter_documentation(self.doc_dict, self.temp_file.name)
529
530
# Read the updated content from the temporary file
531
with open(self.temp_file.name, "r", encoding="utf-8") as file:
532
updated_content = file.read()
533
534
# Check if the file is still empty
535
self.assertEqual(updated_content, "")
536
537
538
if __name__ == '__main__':
539
unittest.main()
540
541