Coverage for hdl_registers/generator/test/test_register_code_generator.py: 100%
259 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-28 20:51 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-28 20:51 +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, **kwargs) -> 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
63# False positive for pytest fixtures
64# pylint: disable=redefined-outer-name
67def test_create_return_value(generator_from_toml):
68 generator = generator_from_toml()
70 status, artifact_path = generator.create_if_needed()
71 assert status is True
72 assert artifact_path == generator.output_file
74 status, artifact_path = generator.create_if_needed()
75 assert status is False
76 assert artifact_path == generator.output_file
78 artifact_path = generator.create()
79 assert artifact_path == generator.output_file
82def test_create_should_not_run_if_nothing_has_changed(generator_from_toml):
83 generator = generator_from_toml()
84 generator.register_list.add_constant(name="apa", value=3, description="")
85 generator.create_if_needed()
87 generator = generator_from_toml()
88 generator.register_list.add_constant(name="apa", value=3, description="")
89 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create:
90 generator.create_if_needed()
91 mocked_create.assert_not_called()
94def test_create_should_run_if_hash_or_version_can_not_be_read(generator_from_toml):
95 generator = generator_from_toml()
96 generator.create_if_needed()
98 # Overwrite the generated file, without a valid header
99 file_path = generator.output_folder / "sensor.x"
100 assert file_path.exists()
101 create_file(file_path, contents="# Mumbo jumbo\n")
103 generator = generator_from_toml()
104 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create:
105 generator.create_if_needed()
106 mocked_create.assert_called_once()
109def test_create_should_run_again_if_toml_file_has_changed(generator_from_toml):
110 generator = generator_from_toml()
111 generator.create_if_needed()
113 generator = generator_from_toml(
114 """
115[apa]
117type = "constant"
118value = 3
119"""
120 )
121 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create:
122 generator.create_if_needed()
123 mocked_create.assert_called_once()
126def test_create_should_not_run_again_if_toml_file_has_only_cosmetic_change(generator_from_toml):
127 generator = generator_from_toml()
128 generator.create_if_needed()
130 generator = generator_from_toml(
131 """
132################################################################################
133# A comment.
134"""
135 )
136 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create:
137 generator.create_if_needed()
138 mocked_create.assert_not_called()
141def test_create_should_run_again_if_register_list_is_modified(generator_from_toml):
142 generator = generator_from_toml()
143 generator.create_if_needed()
145 generator = generator_from_toml()
146 generator.register_list.add_constant(name="apa", value=3, description="")
147 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create:
148 generator.create_if_needed()
149 mocked_create.assert_called_once()
152def test_create_should_run_again_if_package_version_is_changed(generator_from_toml):
153 generator = generator_from_toml()
154 generator.create_if_needed()
156 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create, patch(
157 "hdl_registers.generator.register_code_generator.hdl_registers_version", autospec=True
158 ) as _:
159 generator.create_if_needed()
160 mocked_create.assert_called_once()
163def test_create_should_run_again_if_generator_version_is_changed(generator_from_toml):
164 generator = generator_from_toml()
165 generator.create_if_needed()
167 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create, patch(
168 f"{__name__}.CustomGenerator.__version__", new_callable=PropertyMock
169 ) as mocked_generator_version:
170 mocked_generator_version.return_value = "4.0.0"
172 generator.create_if_needed()
173 mocked_create.assert_called_once()
176@patch("hdl_registers.generator.register_code_generator.git_commands_are_available", autospec=True)
177@patch("hdl_registers.generator.register_code_generator.get_git_commit", autospec=True)
178@patch("hdl_registers.generator.register_code_generator.svn_commands_are_available", autospec=True)
179@patch(
180 "hdl_registers.generator.register_code_generator.get_svn_revision_information", autospec=True
181)
182@patch("hdl_registers.register_list.RegisterList.object_hash", new_callable=PropertyMock)
183def test_generated_source_info(
184 object_hash,
185 get_svn_revision_information,
186 svn_commands_are_available,
187 get_git_commit,
188 git_commands_are_available,
189):
190 register_list = RegisterList(name="", source_definition_file=Path("/apa/whatever/regs.toml"))
192 expected_first_line = (
193 f"This file is automatically generated by hdl-registers version {hdl_registers_version}."
194 )
195 expected_second_line = "Code generator CustomGenerator version 3.0.1."
196 object_hash.return_value = "REGISTER_SHA"
198 # Test with git information
199 git_commands_are_available.return_value = True
200 get_git_commit.return_value = "GIT_SHA"
202 got = CustomGenerator(register_list=register_list, output_folder=None).generated_source_info
204 assert got[0] == expected_first_line
205 assert got[1] == expected_second_line
206 assert " from file regs.toml at commit GIT_SHA." in got[2]
207 assert got[3] == "Register hash REGISTER_SHA."
209 # Test with SVN information
210 git_commands_are_available.return_value = False
211 svn_commands_are_available.return_value = True
212 get_svn_revision_information.return_value = "REVISION"
214 got = CustomGenerator(register_list=register_list, output_folder=None).generated_source_info
215 assert got[0] == expected_first_line
216 assert got[1] == expected_second_line
217 assert " from file regs.toml at revision REVISION." in got[2]
218 assert got[3] == "Register hash REGISTER_SHA."
220 # Test with no source definition file
221 register_list.source_definition_file = None
223 got = CustomGenerator(register_list=register_list, output_folder=None).generated_source_info
224 assert got[0] == expected_first_line
225 assert got[1] == expected_second_line
226 assert "from file" not in got[2]
227 assert " at revision REVISION." in got[2]
228 assert got[3] == "Register hash REGISTER_SHA."
231def test_constant_with_reserved_name_should_raise_exception(generator_from_toml):
232 generator = generator_from_toml(
233 """
234[for]
236type = "constant"
237value = 3
238"""
239 )
240 with pytest.raises(ValueError) as exception_info:
241 generator.create_if_needed()
242 assert (
243 str(exception_info.value)
244 == 'Error in register list "sensor": Constant name "for" is a reserved keyword.'
245 )
248def test_plain_register_with_reserved_name_should_raise_exception(generator_from_toml):
249 generator = generator_from_toml(
250 """
251[for]
253mode = "r_w"
254"""
255 )
256 with pytest.raises(ValueError) as exception_info:
257 generator.create_if_needed()
258 assert (
259 str(exception_info.value)
260 == 'Error in register list "sensor": Register name "for" is a reserved keyword.'
261 )
264def test_plain_register_field_with_reserved_name_should_raise_exception(generator_from_toml):
265 generator = generator_from_toml(
266 """
267[test]
269mode = "r_w"
271for.type = "bit"
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": Field name "for" is a reserved keyword.'
279 )
282def test_register_array_with_reserved_name_should_raise_exception(generator_from_toml):
283 generator = generator_from_toml(
284 """
285[for]
287type = "register_array"
288array_length = 3
290data.mode = "r_w"
291""",
292 )
293 with pytest.raises(ValueError) as exception_info:
294 generator.create_if_needed()
295 assert (
296 str(exception_info.value)
297 == 'Error in register list "sensor": Register array name "for" is a reserved keyword.'
298 )
301def test_array_register_with_reserved_name_should_raise_exception(generator_from_toml):
302 generator = generator_from_toml(
303 """
304[test]
306type = "register_array"
307array_length = 3
309for.mode = "r_w"
310""",
311 )
312 with pytest.raises(ValueError) as exception_info:
313 generator.create_if_needed()
314 assert (
315 str(exception_info.value)
316 == 'Error in register list "sensor": Register name "for" is a reserved keyword.'
317 )
320def test_array_register_field_with_reserved_name_should_raise_exception(generator_from_toml):
321 generator = generator_from_toml(
322 """
323[test]
325type = "register_array"
326array_length = 3
328data.mode = "r_w"
330data.for.type = "bit"
331""",
332 )
333 with pytest.raises(ValueError) as exception_info:
334 generator.create_if_needed()
335 assert (
336 str(exception_info.value)
337 == 'Error in register list "sensor": Field name "for" is a reserved keyword.'
338 )
341def test_reserved_name_check_works_even_with_strange_case(generator_from_toml):
342 generator = generator_from_toml(
343 """
344[FoR]
346mode = "r_w"
347"""
348 )
349 with pytest.raises(ValueError) as exception_info:
350 generator.create_if_needed()
351 assert (
352 str(exception_info.value)
353 == 'Error in register list "sensor": Register name "FoR" is a reserved keyword.'
354 )
357def test_two_constants_with_the_same_name_should_raise_exception(tmp_path):
358 register_list = RegisterList(name="test")
359 register_list.add_constant(name="apa", value=3, description="")
360 register_list.add_constant(name="apa", value=True, description="")
362 with pytest.raises(ValueError) as exception_info:
363 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
364 assert (
365 str(exception_info.value) == 'Error in register list "test": Duplicate constant name "apa".'
366 )
369def test_two_registers_with_the_same_name_should_raise_exception(tmp_path):
370 register_list = RegisterList(name="test")
371 register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="")
372 register_list.append_register(name="apa", mode=REGISTER_MODES["w"], description="")
374 with pytest.raises(ValueError) as exception_info:
375 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
376 assert (
377 str(exception_info.value)
378 == 'Error in register list "test": Duplicate plain register name "apa".'
379 )
382def test_register_with_the_same_name_as_register_array_should_raise_exception(tmp_path):
383 register_list = RegisterList(name="test")
384 register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="")
385 register_list.append_register_array(name="apa", length=2, description="")
387 with pytest.raises(ValueError) as exception_info:
388 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
389 assert (
390 str(exception_info.value)
391 == 'Error in register list "test": Register array "apa" may not have same name as register.'
392 )
395def test_two_plain_fields_with_the_same_name_should_raise_exception(tmp_path):
396 register_list = RegisterList(name="test")
397 register = register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="")
398 register.append_bit(name="hest", description="", default_value="0")
399 register.append_bit(name="hest", description="", default_value="0")
401 with pytest.raises(ValueError) as exception_info:
402 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
403 assert (
404 str(exception_info.value)
405 == 'Error in register list "test": Duplicate field name "hest" in register "apa".'
406 )
409def test_two_array_fields_with_the_same_name_should_raise_exception(tmp_path):
410 register_list = RegisterList(name="test")
411 array = register_list.append_register_array(name="apa", length=2, description="")
412 register = array.append_register(name="hest", mode=REGISTER_MODES["r_w"], description="")
413 register.append_bit(name="zebra", description="", default_value="0")
414 register.append_bit(name="zebra", description="", default_value="0")
416 with pytest.raises(ValueError) as exception_info:
417 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
418 assert (
419 str(exception_info.value)
420 == 'Error in register list "test": Duplicate field name "zebra" in register "apa.hest".'
421 )
424def test_two_register_arrays_with_the_same_name_should_raise_exception(tmp_path):
425 register_list = RegisterList(name="test")
426 register_list.append_register_array(name="apa", length=2, description="").append_register(
427 name="hest", mode=REGISTER_MODES["r"], description=""
428 )
429 register_list.append_register_array(name="apa", length=3, description="").append_register(
430 name="zebra", mode=REGISTER_MODES["w"], description=""
431 )
433 with pytest.raises(ValueError) as exception_info:
434 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
435 assert (
436 str(exception_info.value)
437 == 'Error in register list "test": Duplicate register array name "apa".'
438 )
441def test_array_register_with_same_qualified_name_as_plain_register_should_raise_exception(tmp_path):
442 register_list = RegisterList(name="test")
443 register_list.append_register(name="apa_hest", mode=REGISTER_MODES["r_w"], description="")
444 register_array = register_list.append_register_array(name="apa", length=3, description="")
445 register_array.append_register(name="hest", mode=REGISTER_MODES["r_w"], description="")
447 with pytest.raises(ValueError) as exception_info:
448 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
449 assert str(exception_info.value) == (
450 'Error in register list "test": Qualified name of register "apa.hest" '
451 '("test_apa_hest") clashes with another item.'
452 )
455def test_plain_field_with_same_qualified_name_as_plain_register_should_raise_exception(tmp_path):
456 register_list = RegisterList(name="test")
457 register_list.append_register(name="apa_hest", mode=REGISTER_MODES["r_w"], description="")
458 register = register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="")
459 register.append_bit(name="hest", description="", default_value="0")
461 with pytest.raises(ValueError) as exception_info:
462 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
463 assert str(exception_info.value) == (
464 'Error in register list "test": Qualified name of field "apa.hest" '
465 '("test_apa_hest") clashes with another item.'
466 )
469def test_plain_field_with_same_qualified_name_as_array_register_should_raise_exception(tmp_path):
470 register_list = RegisterList(name="test")
471 register = register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="")
472 register.append_bit(name="hest_zebra", description="", default_value="0")
473 array = register_list.append_register_array(name="apa_hest", length=2, description="")
474 array.append_register(name="zebra", mode=REGISTER_MODES["r_w"], description="")
476 with pytest.raises(ValueError) as exception_info:
477 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
478 assert str(exception_info.value) == (
479 'Error in register list "test": Qualified name of register "apa_hest.zebra" '
480 '("test_apa_hest_zebra") clashes with another item.'
481 )
484def test_array_field_with_same_qualified_name_as_plain_register_should_raise_exception(tmp_path):
485 register_list = RegisterList(name="test")
486 register_list.append_register(name="apa_hest_zebra", mode=REGISTER_MODES["r_w"], description="")
487 array = register_list.append_register_array(name="apa", length=3, description="")
488 register = array.append_register(name="hest", mode=REGISTER_MODES["r_w"], description="")
489 register.append_bit(name="zebra", description="", default_value="0")
491 with pytest.raises(ValueError) as exception_info:
492 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
493 assert str(exception_info.value) == (
494 'Error in register list "test": Qualified name of field "apa.hest.zebra" '
495 '("test_apa_hest_zebra") clashes with another item.'
496 )
499def test_relative_path_printout(tmp_path, monkeypatch):
500 register_list = RegisterList(name="test")
501 generator = CustomGenerator(register_list=register_list, output_folder=tmp_path / "out")
503 string_io = io.StringIO()
504 with contextlib.redirect_stdout(string_io):
505 monkeypatch.chdir(tmp_path)
506 generator.create()
507 stdout = string_io.getvalue()
509 assert f"file: {Path('out') / 'test.x'}" in stdout
511 string_io = io.StringIO()
512 with contextlib.redirect_stdout(string_io):
513 path = create_directory(tmp_path / "apa")
514 monkeypatch.chdir(path)
515 generator.create()
516 stdout = string_io.getvalue()
518 assert f"file: {Path('..') / 'out' / 'test.x'}" in stdout
520 string_io = io.StringIO()
521 with contextlib.redirect_stdout(string_io):
522 monkeypatch.chdir(tmp_path.parent)
523 generator.create()
524 stdout = string_io.getvalue()
526 assert f"file: {Path(tmp_path.name) / 'out' / 'test.x'}" in stdout