Coverage for hdl_registers/generator/test/test_register_code_generator.py: 100%
267 statements
« prev ^ index » next coverage.py v7.6.8, created at 2024-12-01 20:50 +0000
« prev ^ index » next coverage.py v7.6.8, created at 2024-12-01 20:50 +0000
1# --------------------------------------------------------------------------------------------------
2# Copyright (c) Lukas Vik. All rights reserved.
3#
4# This file is part of the hdl-registers project, an HDL register generator fast enough to run
5# in real time.
6# https://hdl-registers.com
7# https://github.com/hdl-registers/hdl-registers
8# --------------------------------------------------------------------------------------------------
10# Standard libraries
11import contextlib
12import io
13from pathlib import Path
14from unittest.mock import PropertyMock, patch
16# Third party libraries
17import pytest
18from tsfpga.system_utils import create_directory, create_file
20# First party libraries
21from hdl_registers import __version__ as hdl_registers_version
22from hdl_registers.generator.register_code_generator import RegisterCodeGenerator
23from hdl_registers.parser.toml import from_toml
24from hdl_registers.register_list import RegisterList
25from hdl_registers.register_modes import REGISTER_MODES
28class CustomGenerator(RegisterCodeGenerator):
29 SHORT_DESCRIPTION = "for test"
30 COMMENT_START = "#"
31 __version__ = "3.0.1"
33 @property
34 def output_file(self):
35 return self.output_folder / f"{self.name}.x"
37 def get_code(self, before_header="", **kwargs) -> str:
38 return f"""\
39{before_header}{self.header}
40Nothing else, its a stupid generator.
41"""
44@pytest.fixture
45def generator_from_toml(tmp_path):
46 def get(toml_extras=""):
47 toml_data = f"""\
48################################################################################
49[data]
51mode = "w"
52description = "My register"
54{toml_extras}
55"""
57 register_list = from_toml(
58 name="sensor", toml_file=create_file(tmp_path / "sensor_regs.toml", toml_data)
59 )
61 return CustomGenerator(register_list=register_list, output_folder=tmp_path)
63 return get
66# False positive for pytest fixtures
67# pylint: disable=redefined-outer-name
70def test_create_return_value(generator_from_toml):
71 generator = generator_from_toml()
73 status, artifact_path = generator.create_if_needed()
74 assert status is True
75 assert artifact_path == generator.output_file
77 status, artifact_path = generator.create_if_needed()
78 assert status is False
79 assert artifact_path == generator.output_file
81 artifact_path = generator.create()
82 assert artifact_path == generator.output_file
85def test_create_should_not_run_if_nothing_has_changed(generator_from_toml):
86 generator = generator_from_toml()
87 generator.register_list.add_constant(name="apa", value=3, description="")
88 generator.create_if_needed()
90 generator = generator_from_toml()
91 generator.register_list.add_constant(name="apa", value=3, description="")
92 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create:
93 generator.create_if_needed()
94 mocked_create.assert_not_called()
97def test_create_should_run_if_hash_or_version_can_not_be_read(generator_from_toml):
98 generator = generator_from_toml()
99 generator.create_if_needed()
101 # Overwrite the generated file, without a valid header
102 file_path = generator.output_folder / "sensor.x"
103 assert file_path.exists()
104 create_file(file_path, contents="# Mumbo jumbo\n")
106 generator = generator_from_toml()
107 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create:
108 generator.create_if_needed()
109 mocked_create.assert_called_once()
112def test_create_should_run_again_if_toml_file_has_changed(generator_from_toml):
113 generator = generator_from_toml()
114 generator.create_if_needed()
116 generator = generator_from_toml(
117 """
118[apa]
120type = "constant"
121value = 3
122"""
123 )
124 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create:
125 generator.create_if_needed()
126 mocked_create.assert_called_once()
129def test_create_should_not_run_again_if_toml_file_has_only_cosmetic_change(generator_from_toml):
130 generator = generator_from_toml()
131 generator.create_if_needed()
133 generator = generator_from_toml(
134 """
135################################################################################
136# A comment.
137"""
138 )
139 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create:
140 generator.create_if_needed()
141 mocked_create.assert_not_called()
144def test_create_should_run_again_if_register_list_is_modified(generator_from_toml):
145 generator = generator_from_toml()
146 generator.create_if_needed()
148 generator = generator_from_toml()
149 generator.register_list.add_constant(name="apa", value=3, description="")
150 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create:
151 generator.create_if_needed()
152 mocked_create.assert_called_once()
155def test_create_should_run_again_if_package_version_is_changed(generator_from_toml):
156 generator = generator_from_toml()
157 generator.create_if_needed()
159 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create, patch(
160 "hdl_registers.generator.register_code_generator.hdl_registers_version", autospec=True
161 ) as _:
162 generator.create_if_needed()
163 mocked_create.assert_called_once()
166def test_create_should_run_again_if_generator_version_is_changed(generator_from_toml):
167 generator = generator_from_toml()
168 generator.create_if_needed()
170 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create, patch(
171 f"{__name__}.CustomGenerator.__version__", new_callable=PropertyMock
172 ) as mocked_generator_version:
173 mocked_generator_version.return_value = "4.0.0"
175 generator.create_if_needed()
176 mocked_create.assert_called_once()
179def test_version_header_is_detected_even_if_not_on_first_line(generator_from_toml):
180 before_header = """
181# #########################
182# Another header
183# #########################
184"""
185 generator = generator_from_toml()
186 generator.create_if_needed(before_header=before_header)
188 generator = generator_from_toml()
189 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create:
190 generator.create_if_needed(before_header=before_header)
191 mocked_create.assert_not_called()
194@patch("hdl_registers.generator.register_code_generator.git_commands_are_available", autospec=True)
195@patch("hdl_registers.generator.register_code_generator.get_git_commit", autospec=True)
196@patch("hdl_registers.generator.register_code_generator.svn_commands_are_available", autospec=True)
197@patch(
198 "hdl_registers.generator.register_code_generator.get_svn_revision_information", autospec=True
199)
200@patch("hdl_registers.register_list.RegisterList.object_hash", new_callable=PropertyMock)
201def test_generated_source_info(
202 object_hash,
203 get_svn_revision_information,
204 svn_commands_are_available,
205 get_git_commit,
206 git_commands_are_available,
207):
208 register_list = RegisterList(name="", source_definition_file=Path("/apa/whatever/regs.toml"))
210 expected_first_line = (
211 f"This file is automatically generated by hdl-registers version {hdl_registers_version}."
212 )
213 expected_second_line = "Code generator CustomGenerator version 3.0.1."
214 object_hash.return_value = "REGISTER_SHA"
216 # Test with git information
217 git_commands_are_available.return_value = True
218 get_git_commit.return_value = "GIT_SHA"
220 got = CustomGenerator(register_list=register_list, output_folder=None).generated_source_info
222 assert got[0] == expected_first_line
223 assert got[1] == expected_second_line
224 assert " from file regs.toml at commit GIT_SHA." in got[2]
225 assert got[3] == "Register hash REGISTER_SHA."
227 # Test with SVN information
228 git_commands_are_available.return_value = False
229 svn_commands_are_available.return_value = True
230 get_svn_revision_information.return_value = "REVISION"
232 got = CustomGenerator(register_list=register_list, output_folder=None).generated_source_info
233 assert got[0] == expected_first_line
234 assert got[1] == expected_second_line
235 assert " from file regs.toml at revision REVISION." in got[2]
236 assert got[3] == "Register hash REGISTER_SHA."
238 # Test with no source definition file
239 register_list.source_definition_file = None
241 got = CustomGenerator(register_list=register_list, output_folder=None).generated_source_info
242 assert got[0] == expected_first_line
243 assert got[1] == expected_second_line
244 assert "from file" not in got[2]
245 assert " at revision REVISION." in got[2]
246 assert got[3] == "Register hash REGISTER_SHA."
249def test_constant_with_reserved_name_should_raise_exception(generator_from_toml):
250 generator = generator_from_toml(
251 """
252[for]
254type = "constant"
255value = 3
256"""
257 )
258 with pytest.raises(ValueError) as exception_info:
259 generator.create_if_needed()
260 assert (
261 str(exception_info.value)
262 == 'Error in register list "sensor": Constant name "for" is a reserved keyword.'
263 )
266def test_plain_register_with_reserved_name_should_raise_exception(generator_from_toml):
267 generator = generator_from_toml(
268 """
269[for]
271mode = "r_w"
272"""
273 )
274 with pytest.raises(ValueError) as exception_info:
275 generator.create_if_needed()
276 assert (
277 str(exception_info.value)
278 == 'Error in register list "sensor": Register name "for" is a reserved keyword.'
279 )
282def test_plain_register_field_with_reserved_name_should_raise_exception(generator_from_toml):
283 generator = generator_from_toml(
284 """
285[test]
287mode = "r_w"
289for.type = "bit"
290""",
291 )
292 with pytest.raises(ValueError) as exception_info:
293 generator.create_if_needed()
294 assert (
295 str(exception_info.value)
296 == 'Error in register list "sensor": Field name "for" is a reserved keyword.'
297 )
300def test_register_array_with_reserved_name_should_raise_exception(generator_from_toml):
301 generator = generator_from_toml(
302 """
303[for]
305type = "register_array"
306array_length = 3
308data.mode = "r_w"
309""",
310 )
311 with pytest.raises(ValueError) as exception_info:
312 generator.create_if_needed()
313 assert (
314 str(exception_info.value)
315 == 'Error in register list "sensor": Register array name "for" is a reserved keyword.'
316 )
319def test_array_register_with_reserved_name_should_raise_exception(generator_from_toml):
320 generator = generator_from_toml(
321 """
322[test]
324type = "register_array"
325array_length = 3
327for.mode = "r_w"
328""",
329 )
330 with pytest.raises(ValueError) as exception_info:
331 generator.create_if_needed()
332 assert (
333 str(exception_info.value)
334 == 'Error in register list "sensor": Register name "for" is a reserved keyword.'
335 )
338def test_array_register_field_with_reserved_name_should_raise_exception(generator_from_toml):
339 generator = generator_from_toml(
340 """
341[test]
343type = "register_array"
344array_length = 3
346data.mode = "r_w"
348data.for.type = "bit"
349""",
350 )
351 with pytest.raises(ValueError) as exception_info:
352 generator.create_if_needed()
353 assert (
354 str(exception_info.value)
355 == 'Error in register list "sensor": Field name "for" is a reserved keyword.'
356 )
359def test_reserved_name_check_works_even_with_strange_case(generator_from_toml):
360 generator = generator_from_toml(
361 """
362[FoR]
364mode = "r_w"
365"""
366 )
367 with pytest.raises(ValueError) as exception_info:
368 generator.create_if_needed()
369 assert (
370 str(exception_info.value)
371 == 'Error in register list "sensor": Register name "FoR" is a reserved keyword.'
372 )
375def test_two_constants_with_the_same_name_should_raise_exception(tmp_path):
376 register_list = RegisterList(name="test")
377 register_list.add_constant(name="apa", value=3, description="")
378 register_list.add_constant(name="apa", value=True, description="")
380 with pytest.raises(ValueError) as exception_info:
381 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
382 assert (
383 str(exception_info.value) == 'Error in register list "test": Duplicate constant name "apa".'
384 )
387def test_two_registers_with_the_same_name_should_raise_exception(tmp_path):
388 register_list = RegisterList(name="test")
389 register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="")
390 register_list.append_register(name="apa", mode=REGISTER_MODES["w"], description="")
392 with pytest.raises(ValueError) as exception_info:
393 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
394 assert (
395 str(exception_info.value)
396 == 'Error in register list "test": Duplicate plain register name "apa".'
397 )
400def test_register_with_the_same_name_as_register_array_should_raise_exception(tmp_path):
401 register_list = RegisterList(name="test")
402 register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="")
403 register_list.append_register_array(name="apa", length=2, description="")
405 with pytest.raises(ValueError) as exception_info:
406 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
407 assert (
408 str(exception_info.value)
409 == 'Error in register list "test": Register array "apa" may not have same name as register.'
410 )
413def test_two_plain_fields_with_the_same_name_should_raise_exception(tmp_path):
414 register_list = RegisterList(name="test")
415 register = register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="")
416 register.append_bit(name="hest", description="", default_value="0")
417 register.append_bit(name="hest", description="", default_value="0")
419 with pytest.raises(ValueError) as exception_info:
420 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
421 assert (
422 str(exception_info.value)
423 == 'Error in register list "test": Duplicate field name "hest" in register "apa".'
424 )
427def test_two_array_fields_with_the_same_name_should_raise_exception(tmp_path):
428 register_list = RegisterList(name="test")
429 array = register_list.append_register_array(name="apa", length=2, description="")
430 register = array.append_register(name="hest", mode=REGISTER_MODES["r_w"], description="")
431 register.append_bit(name="zebra", description="", default_value="0")
432 register.append_bit(name="zebra", description="", default_value="0")
434 with pytest.raises(ValueError) as exception_info:
435 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
436 assert (
437 str(exception_info.value)
438 == 'Error in register list "test": Duplicate field name "zebra" in register "apa.hest".'
439 )
442def test_two_register_arrays_with_the_same_name_should_raise_exception(tmp_path):
443 register_list = RegisterList(name="test")
444 register_list.append_register_array(name="apa", length=2, description="").append_register(
445 name="hest", mode=REGISTER_MODES["r"], description=""
446 )
447 register_list.append_register_array(name="apa", length=3, description="").append_register(
448 name="zebra", mode=REGISTER_MODES["w"], description=""
449 )
451 with pytest.raises(ValueError) as exception_info:
452 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
453 assert (
454 str(exception_info.value)
455 == 'Error in register list "test": Duplicate register array name "apa".'
456 )
459def test_array_register_with_same_qualified_name_as_plain_register_should_raise_exception(tmp_path):
460 register_list = RegisterList(name="test")
461 register_list.append_register(name="apa_hest", mode=REGISTER_MODES["r_w"], description="")
462 register_array = register_list.append_register_array(name="apa", length=3, description="")
463 register_array.append_register(name="hest", mode=REGISTER_MODES["r_w"], description="")
465 with pytest.raises(ValueError) as exception_info:
466 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
467 assert str(exception_info.value) == (
468 'Error in register list "test": Qualified name of register "apa.hest" '
469 '("test_apa_hest") clashes with another item.'
470 )
473def test_plain_field_with_same_qualified_name_as_plain_register_should_raise_exception(tmp_path):
474 register_list = RegisterList(name="test")
475 register_list.append_register(name="apa_hest", mode=REGISTER_MODES["r_w"], description="")
476 register = register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="")
477 register.append_bit(name="hest", description="", default_value="0")
479 with pytest.raises(ValueError) as exception_info:
480 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
481 assert str(exception_info.value) == (
482 'Error in register list "test": Qualified name of field "apa.hest" '
483 '("test_apa_hest") clashes with another item.'
484 )
487def test_plain_field_with_same_qualified_name_as_array_register_should_raise_exception(tmp_path):
488 register_list = RegisterList(name="test")
489 register = register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="")
490 register.append_bit(name="hest_zebra", description="", default_value="0")
491 array = register_list.append_register_array(name="apa_hest", length=2, description="")
492 array.append_register(name="zebra", mode=REGISTER_MODES["r_w"], description="")
494 with pytest.raises(ValueError) as exception_info:
495 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
496 assert str(exception_info.value) == (
497 'Error in register list "test": Qualified name of register "apa_hest.zebra" '
498 '("test_apa_hest_zebra") clashes with another item.'
499 )
502def test_array_field_with_same_qualified_name_as_plain_register_should_raise_exception(tmp_path):
503 register_list = RegisterList(name="test")
504 register_list.append_register(name="apa_hest_zebra", mode=REGISTER_MODES["r_w"], description="")
505 array = register_list.append_register_array(name="apa", length=3, description="")
506 register = array.append_register(name="hest", mode=REGISTER_MODES["r_w"], description="")
507 register.append_bit(name="zebra", description="", default_value="0")
509 with pytest.raises(ValueError) as exception_info:
510 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
511 assert str(exception_info.value) == (
512 'Error in register list "test": Qualified name of field "apa.hest.zebra" '
513 '("test_apa_hest_zebra") clashes with another item.'
514 )
517def test_relative_path_printout(tmp_path, monkeypatch):
518 register_list = RegisterList(name="test")
519 generator = CustomGenerator(register_list=register_list, output_folder=tmp_path / "out")
521 string_io = io.StringIO()
522 with contextlib.redirect_stdout(string_io):
523 monkeypatch.chdir(tmp_path)
524 generator.create()
525 stdout = string_io.getvalue()
527 assert f"file: {Path('out') / 'test.x'}" in stdout
529 string_io = io.StringIO()
530 with contextlib.redirect_stdout(string_io):
531 path = create_directory(tmp_path / "apa")
532 monkeypatch.chdir(path)
533 generator.create()
534 stdout = string_io.getvalue()
536 assert f"file: {Path('..') / 'out' / 'test.x'}" in stdout
538 string_io = io.StringIO()
539 with contextlib.redirect_stdout(string_io):
540 monkeypatch.chdir(tmp_path.parent)
541 generator.create()
542 stdout = string_io.getvalue()
544 assert f"file: {Path(tmp_path.name) / 'out' / 'test.x'}" in stdout