Coverage for hdl_registers/generator/test/test_register_code_generator.py: 100%
264 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-05-31 20:50 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-05-31 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# --------------------------------------------------------------------------------------------------
10import contextlib
11import io
12from pathlib import Path
13from unittest.mock import PropertyMock, patch
15import pytest
16from tsfpga.system_utils import create_directory, create_file
18from hdl_registers import __version__ as hdl_registers_version
19from hdl_registers.generator.register_code_generator import RegisterCodeGenerator
20from hdl_registers.parser.toml import from_toml
21from hdl_registers.register_list import RegisterList
22from hdl_registers.register_modes import REGISTER_MODES
25class CustomGenerator(RegisterCodeGenerator):
26 SHORT_DESCRIPTION = "for test"
27 COMMENT_START = "#"
28 __version__ = "3.0.1"
30 @property
31 def output_file(self):
32 return self.output_folder / f"{self.name}.x"
34 def get_code(
35 self,
36 **kwargs, # noqa: ARG002
37 ) -> str:
38 return "Nothing, its a stupid generator."
41@pytest.fixture
42def generator_from_toml(tmp_path):
43 def get(toml_extras=""):
44 toml_data = f"""\
45################################################################################
46[data]
48mode = "w"
49description = "My register"
51{toml_extras}
52"""
54 register_list = from_toml(
55 name="sensor", toml_file=create_file(tmp_path / "sensor_regs.toml", toml_data)
56 )
58 return CustomGenerator(register_list=register_list, output_folder=tmp_path)
60 return get
63def test_create_return_value(generator_from_toml):
64 generator = generator_from_toml()
66 status, artifact_path = generator.create_if_needed()
67 assert status is True
68 assert artifact_path == generator.output_file
70 status, artifact_path = generator.create_if_needed()
71 assert status is False
72 assert artifact_path == generator.output_file
74 artifact_path = generator.create()
75 assert artifact_path == generator.output_file
78def test_create_should_not_run_if_nothing_has_changed(generator_from_toml):
79 generator = generator_from_toml()
80 generator.register_list.add_constant(name="apa", value=3, description="")
81 generator.create_if_needed()
83 generator = generator_from_toml()
84 generator.register_list.add_constant(name="apa", value=3, description="")
85 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create:
86 generator.create_if_needed()
87 mocked_create.assert_not_called()
90def test_create_should_run_if_hash_or_version_can_not_be_read(generator_from_toml):
91 generator = generator_from_toml()
92 generator.create_if_needed()
94 # Overwrite the generated file, without a valid header
95 file_path = generator.output_folder / "sensor.x"
96 assert file_path.exists()
97 create_file(file_path, contents="# Mumbo jumbo\n")
99 generator = generator_from_toml()
100 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create:
101 generator.create_if_needed()
102 mocked_create.assert_called_once()
105def test_create_should_run_again_if_toml_file_has_changed(generator_from_toml):
106 generator = generator_from_toml()
107 generator.create_if_needed()
109 generator = generator_from_toml(
110 """
111[apa]
113type = "constant"
114value = 3
115"""
116 )
117 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create:
118 generator.create_if_needed()
119 mocked_create.assert_called_once()
122def test_create_should_not_run_again_if_toml_file_has_only_cosmetic_change(generator_from_toml):
123 generator = generator_from_toml()
124 generator.create_if_needed()
126 generator = generator_from_toml(
127 """
128################################################################################
129# A comment.
130"""
131 )
132 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create:
133 generator.create_if_needed()
134 mocked_create.assert_not_called()
137def test_create_should_run_again_if_register_list_is_modified(generator_from_toml):
138 generator = generator_from_toml()
139 generator.create_if_needed()
141 generator = generator_from_toml()
142 generator.register_list.add_constant(name="apa", value=3, description="")
143 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create:
144 generator.create_if_needed()
145 mocked_create.assert_called_once()
148def test_create_should_run_again_if_package_version_is_changed(generator_from_toml):
149 generator = generator_from_toml()
150 generator.create_if_needed()
152 with (
153 patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create,
154 patch(
155 "hdl_registers.generator.register_code_generator.hdl_registers_version", autospec=True
156 ) as _,
157 ):
158 generator.create_if_needed()
159 mocked_create.assert_called_once()
162def test_create_should_run_again_if_generator_version_is_changed(generator_from_toml):
163 generator = generator_from_toml()
164 generator.create_if_needed()
166 with (
167 patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create,
168 patch(
169 f"{__name__}.CustomGenerator.__version__", new_callable=PropertyMock
170 ) as mocked_generator_version,
171 ):
172 mocked_generator_version.return_value = "4.0.0"
174 generator.create_if_needed()
175 mocked_create.assert_called_once()
178@patch("hdl_registers.generator.register_code_generator.git_commands_are_available", autospec=True)
179@patch("hdl_registers.generator.register_code_generator.get_git_commit", autospec=True)
180@patch("hdl_registers.generator.register_code_generator.svn_commands_are_available", autospec=True)
181@patch(
182 "hdl_registers.generator.register_code_generator.get_svn_revision_information", autospec=True
183)
184@patch("hdl_registers.register_list.RegisterList.object_hash", new_callable=PropertyMock)
185def test_generated_source_info(
186 object_hash,
187 get_svn_revision_information,
188 svn_commands_are_available,
189 get_git_commit,
190 git_commands_are_available,
191):
192 register_list = RegisterList(name="", source_definition_file=Path("/apa/whatever/regs.toml"))
194 expected_first_line = (
195 f"This file is automatically generated by hdl-registers version {hdl_registers_version}."
196 )
197 expected_second_line = "Code generator CustomGenerator version 3.0.1."
198 object_hash.return_value = "REGISTER_SHA"
200 # Test with git information
201 git_commands_are_available.return_value = True
202 get_git_commit.return_value = "GIT_SHA"
204 got = CustomGenerator(register_list=register_list, output_folder=None).generated_source_info
206 assert got[0] == expected_first_line
207 assert got[1] == expected_second_line
208 assert " from file regs.toml at Git commit GIT_SHA." in got[2]
209 assert got[3] == "Register hash REGISTER_SHA."
211 # Test with SVN information
212 git_commands_are_available.return_value = False
213 svn_commands_are_available.return_value = True
214 get_svn_revision_information.return_value = "REVISION"
216 got = CustomGenerator(register_list=register_list, output_folder=None).generated_source_info
217 assert got[0] == expected_first_line
218 assert got[1] == expected_second_line
219 assert " from file regs.toml at SVN revision REVISION." in got[2]
220 assert got[3] == "Register hash REGISTER_SHA."
222 # Test with no source definition file
223 register_list.source_definition_file = None
225 got = CustomGenerator(register_list=register_list, output_folder=None).generated_source_info
226 assert got[0] == expected_first_line
227 assert got[1] == expected_second_line
228 assert "from file" not in got[2]
229 assert " at SVN revision REVISION." in got[2]
230 assert got[3] == "Register hash REGISTER_SHA."
233def test_constant_with_reserved_name_should_raise_exception(generator_from_toml):
234 generator = generator_from_toml(
235 """
236[for]
238type = "constant"
239value = 3
240"""
241 )
242 with pytest.raises(ValueError) as exception_info:
243 generator.create_if_needed()
244 assert (
245 str(exception_info.value)
246 == 'Error in register list "sensor": Constant name "for" is a reserved keyword.'
247 )
250def test_plain_register_with_reserved_name_should_raise_exception(generator_from_toml):
251 generator = generator_from_toml(
252 """
253[for]
255mode = "r_w"
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": Register name "for" is a reserved keyword.'
263 )
266def test_plain_register_field_with_reserved_name_should_raise_exception(generator_from_toml):
267 generator = generator_from_toml(
268 """
269[test]
271mode = "r_w"
273for.type = "bit"
274""",
275 )
276 with pytest.raises(ValueError) as exception_info:
277 generator.create_if_needed()
278 assert (
279 str(exception_info.value)
280 == 'Error in register list "sensor": Field name "for" is a reserved keyword.'
281 )
284def test_register_array_with_reserved_name_should_raise_exception(generator_from_toml):
285 generator = generator_from_toml(
286 """
287[for]
289type = "register_array"
290array_length = 3
292data.mode = "r_w"
293""",
294 )
295 with pytest.raises(ValueError) as exception_info:
296 generator.create_if_needed()
297 assert (
298 str(exception_info.value)
299 == 'Error in register list "sensor": Register array name "for" is a reserved keyword.'
300 )
303def test_array_register_with_reserved_name_should_raise_exception(generator_from_toml):
304 generator = generator_from_toml(
305 """
306[test]
308type = "register_array"
309array_length = 3
311for.mode = "r_w"
312""",
313 )
314 with pytest.raises(ValueError) as exception_info:
315 generator.create_if_needed()
316 assert (
317 str(exception_info.value)
318 == 'Error in register list "sensor": Register name "for" is a reserved keyword.'
319 )
322def test_array_register_field_with_reserved_name_should_raise_exception(generator_from_toml):
323 generator = generator_from_toml(
324 """
325[test]
327type = "register_array"
328array_length = 3
330data.mode = "r_w"
332data.for.type = "bit"
333""",
334 )
335 with pytest.raises(ValueError) as exception_info:
336 generator.create_if_needed()
337 assert (
338 str(exception_info.value)
339 == 'Error in register list "sensor": Field name "for" is a reserved keyword.'
340 )
343def test_enumeration_field_element_with_reserved_name_should_raise_exception(generator_from_toml):
344 generator = generator_from_toml(
345 """
346[test]
348mode = "r_w"
350apa.type = "enumeration"
351apa.element.okay_name = ""
352apa.element.signed = ""
353""",
354 )
355 with pytest.raises(ValueError) as exception_info:
356 generator.create_if_needed()
357 assert str(exception_info.value) == (
358 'Error in register list "sensor": Enumeration element name "signed" is a reserved keyword.'
359 )
362def test_reserved_name_check_works_even_with_strange_case(generator_from_toml):
363 generator = generator_from_toml(
364 """
365[FoR]
367mode = "r_w"
368"""
369 )
370 with pytest.raises(ValueError) as exception_info:
371 generator.create_if_needed()
372 assert (
373 str(exception_info.value)
374 == 'Error in register list "sensor": Register name "FoR" is a reserved keyword.'
375 )
378def test_two_constants_with_the_same_name_should_raise_exception(tmp_path):
379 register_list = RegisterList(name="test")
380 register_list.add_constant(name="apa", value=3, description="")
381 register_list.add_constant(name="apa", value=True, description="")
383 with pytest.raises(ValueError) as exception_info:
384 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
385 assert (
386 str(exception_info.value) == 'Error in register list "test": Duplicate constant name "apa".'
387 )
390def test_two_registers_with_the_same_name_should_raise_exception(tmp_path):
391 register_list = RegisterList(name="test")
392 register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="")
393 register_list.append_register(name="apa", mode=REGISTER_MODES["w"], description="")
395 with pytest.raises(ValueError) as exception_info:
396 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
397 assert (
398 str(exception_info.value)
399 == 'Error in register list "test": Duplicate plain register name "apa".'
400 )
403def test_register_with_the_same_name_as_register_array_should_raise_exception(tmp_path):
404 register_list = RegisterList(name="test")
405 register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="")
406 register_list.append_register_array(name="apa", length=2, description="")
408 with pytest.raises(ValueError) as exception_info:
409 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
410 assert (
411 str(exception_info.value)
412 == 'Error in register list "test": Register array "apa" may not have same name as register.'
413 )
416def test_two_plain_fields_with_the_same_name_should_raise_exception(tmp_path):
417 register_list = RegisterList(name="test")
418 register = register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="")
419 register.append_bit(name="hest", description="", default_value="0")
420 register.append_bit(name="hest", description="", default_value="0")
422 with pytest.raises(ValueError) as exception_info:
423 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
424 assert (
425 str(exception_info.value)
426 == 'Error in register list "test": Duplicate field name "hest" in register "apa".'
427 )
430def test_two_array_fields_with_the_same_name_should_raise_exception(tmp_path):
431 register_list = RegisterList(name="test")
432 array = register_list.append_register_array(name="apa", length=2, description="")
433 register = array.append_register(name="hest", mode=REGISTER_MODES["r_w"], description="")
434 register.append_bit(name="zebra", description="", default_value="0")
435 register.append_bit(name="zebra", description="", default_value="0")
437 with pytest.raises(ValueError) as exception_info:
438 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
439 assert (
440 str(exception_info.value)
441 == 'Error in register list "test": Duplicate field name "zebra" in register "apa.hest".'
442 )
445def test_two_register_arrays_with_the_same_name_should_raise_exception(tmp_path):
446 register_list = RegisterList(name="test")
447 register_list.append_register_array(name="apa", length=2, description="").append_register(
448 name="hest", mode=REGISTER_MODES["r"], description=""
449 )
450 register_list.append_register_array(name="apa", length=3, description="").append_register(
451 name="zebra", mode=REGISTER_MODES["w"], description=""
452 )
454 with pytest.raises(ValueError) as exception_info:
455 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
456 assert (
457 str(exception_info.value)
458 == 'Error in register list "test": Duplicate register array name "apa".'
459 )
462def test_array_register_with_same_qualified_name_as_plain_register_should_raise_exception(tmp_path):
463 register_list = RegisterList(name="test")
464 register_list.append_register(name="apa_hest", mode=REGISTER_MODES["r_w"], description="")
465 register_array = register_list.append_register_array(name="apa", length=3, description="")
466 register_array.append_register(name="hest", mode=REGISTER_MODES["r_w"], description="")
468 with pytest.raises(ValueError) as exception_info:
469 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
470 assert str(exception_info.value) == (
471 'Error in register list "test": Qualified name of register "apa.hest" '
472 '("test_apa_hest") clashes with another item.'
473 )
476def test_plain_field_with_same_qualified_name_as_plain_register_should_raise_exception(tmp_path):
477 register_list = RegisterList(name="test")
478 register_list.append_register(name="apa_hest", mode=REGISTER_MODES["r_w"], description="")
479 register = register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="")
480 register.append_bit(name="hest", description="", default_value="0")
482 with pytest.raises(ValueError) as exception_info:
483 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
484 assert str(exception_info.value) == (
485 'Error in register list "test": Qualified name of field "apa.hest" '
486 '("test_apa_hest") clashes with another item.'
487 )
490def test_plain_field_with_same_qualified_name_as_array_register_should_raise_exception(tmp_path):
491 register_list = RegisterList(name="test")
492 register = register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="")
493 register.append_bit(name="hest_zebra", description="", default_value="0")
494 array = register_list.append_register_array(name="apa_hest", length=2, description="")
495 array.append_register(name="zebra", mode=REGISTER_MODES["r_w"], description="")
497 with pytest.raises(ValueError) as exception_info:
498 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
499 assert str(exception_info.value) == (
500 'Error in register list "test": Qualified name of register "apa_hest.zebra" '
501 '("test_apa_hest_zebra") clashes with another item.'
502 )
505def test_array_field_with_same_qualified_name_as_plain_register_should_raise_exception(tmp_path):
506 register_list = RegisterList(name="test")
507 register_list.append_register(name="apa_hest_zebra", mode=REGISTER_MODES["r_w"], description="")
508 array = register_list.append_register_array(name="apa", length=3, description="")
509 register = array.append_register(name="hest", mode=REGISTER_MODES["r_w"], description="")
510 register.append_bit(name="zebra", description="", default_value="0")
512 with pytest.raises(ValueError) as exception_info:
513 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
514 assert str(exception_info.value) == (
515 'Error in register list "test": Qualified name of field "apa.hest.zebra" '
516 '("test_apa_hest_zebra") clashes with another item.'
517 )
520def test_relative_path_printout(tmp_path, monkeypatch):
521 register_list = RegisterList(name="test")
522 generator = CustomGenerator(register_list=register_list, output_folder=tmp_path / "out")
524 string_io = io.StringIO()
525 with contextlib.redirect_stdout(string_io):
526 monkeypatch.chdir(tmp_path)
527 generator.create()
528 stdout = string_io.getvalue()
530 # Prints one sub-folder.
531 assert f"file: {Path('out') / 'test.x'}" in stdout
533 string_io = io.StringIO()
534 with contextlib.redirect_stdout(string_io):
535 monkeypatch.chdir(tmp_path.parent)
536 generator.create()
537 stdout = string_io.getvalue()
539 # Prints multiple sub-folders.
540 assert f"file: {Path(tmp_path.name) / 'out' / 'test.x'}" in stdout
542 string_io = io.StringIO()
543 with contextlib.redirect_stdout(string_io):
544 path = create_directory(tmp_path / "apa")
545 monkeypatch.chdir(path)
546 generator.create()
547 stdout = string_io.getvalue()
549 # Prints full path since the output is not inside the CWD.
550 assert f"file: {tmp_path / 'out' / 'test.x'}" in stdout