Coverage for hdl_registers/generator/test/test_register_code_generator.py: 100%
259 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 11:11 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 11:11 +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_reserved_name_check_works_even_with_strange_case(generator_from_toml):
344 generator = generator_from_toml(
345 """
346[FoR]
348mode = "r_w"
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": Register name "FoR" is a reserved keyword.'
356 )
359def test_two_constants_with_the_same_name_should_raise_exception(tmp_path):
360 register_list = RegisterList(name="test")
361 register_list.add_constant(name="apa", value=3, description="")
362 register_list.add_constant(name="apa", value=True, description="")
364 with pytest.raises(ValueError) as exception_info:
365 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
366 assert (
367 str(exception_info.value) == 'Error in register list "test": Duplicate constant name "apa".'
368 )
371def test_two_registers_with_the_same_name_should_raise_exception(tmp_path):
372 register_list = RegisterList(name="test")
373 register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="")
374 register_list.append_register(name="apa", mode=REGISTER_MODES["w"], description="")
376 with pytest.raises(ValueError) as exception_info:
377 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
378 assert (
379 str(exception_info.value)
380 == 'Error in register list "test": Duplicate plain register name "apa".'
381 )
384def test_register_with_the_same_name_as_register_array_should_raise_exception(tmp_path):
385 register_list = RegisterList(name="test")
386 register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="")
387 register_list.append_register_array(name="apa", length=2, description="")
389 with pytest.raises(ValueError) as exception_info:
390 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
391 assert (
392 str(exception_info.value)
393 == 'Error in register list "test": Register array "apa" may not have same name as register.'
394 )
397def test_two_plain_fields_with_the_same_name_should_raise_exception(tmp_path):
398 register_list = RegisterList(name="test")
399 register = register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="")
400 register.append_bit(name="hest", description="", default_value="0")
401 register.append_bit(name="hest", description="", default_value="0")
403 with pytest.raises(ValueError) as exception_info:
404 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
405 assert (
406 str(exception_info.value)
407 == 'Error in register list "test": Duplicate field name "hest" in register "apa".'
408 )
411def test_two_array_fields_with_the_same_name_should_raise_exception(tmp_path):
412 register_list = RegisterList(name="test")
413 array = register_list.append_register_array(name="apa", length=2, description="")
414 register = array.append_register(name="hest", mode=REGISTER_MODES["r_w"], description="")
415 register.append_bit(name="zebra", description="", default_value="0")
416 register.append_bit(name="zebra", description="", default_value="0")
418 with pytest.raises(ValueError) as exception_info:
419 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
420 assert (
421 str(exception_info.value)
422 == 'Error in register list "test": Duplicate field name "zebra" in register "apa.hest".'
423 )
426def test_two_register_arrays_with_the_same_name_should_raise_exception(tmp_path):
427 register_list = RegisterList(name="test")
428 register_list.append_register_array(name="apa", length=2, description="").append_register(
429 name="hest", mode=REGISTER_MODES["r"], description=""
430 )
431 register_list.append_register_array(name="apa", length=3, description="").append_register(
432 name="zebra", mode=REGISTER_MODES["w"], description=""
433 )
435 with pytest.raises(ValueError) as exception_info:
436 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
437 assert (
438 str(exception_info.value)
439 == 'Error in register list "test": Duplicate register array name "apa".'
440 )
443def test_array_register_with_same_qualified_name_as_plain_register_should_raise_exception(tmp_path):
444 register_list = RegisterList(name="test")
445 register_list.append_register(name="apa_hest", mode=REGISTER_MODES["r_w"], description="")
446 register_array = register_list.append_register_array(name="apa", length=3, description="")
447 register_array.append_register(name="hest", mode=REGISTER_MODES["r_w"], description="")
449 with pytest.raises(ValueError) as exception_info:
450 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
451 assert str(exception_info.value) == (
452 'Error in register list "test": Qualified name of register "apa.hest" '
453 '("test_apa_hest") clashes with another item.'
454 )
457def test_plain_field_with_same_qualified_name_as_plain_register_should_raise_exception(tmp_path):
458 register_list = RegisterList(name="test")
459 register_list.append_register(name="apa_hest", mode=REGISTER_MODES["r_w"], description="")
460 register = register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="")
461 register.append_bit(name="hest", description="", default_value="0")
463 with pytest.raises(ValueError) as exception_info:
464 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
465 assert str(exception_info.value) == (
466 'Error in register list "test": Qualified name of field "apa.hest" '
467 '("test_apa_hest") clashes with another item.'
468 )
471def test_plain_field_with_same_qualified_name_as_array_register_should_raise_exception(tmp_path):
472 register_list = RegisterList(name="test")
473 register = register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="")
474 register.append_bit(name="hest_zebra", description="", default_value="0")
475 array = register_list.append_register_array(name="apa_hest", length=2, description="")
476 array.append_register(name="zebra", mode=REGISTER_MODES["r_w"], description="")
478 with pytest.raises(ValueError) as exception_info:
479 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
480 assert str(exception_info.value) == (
481 'Error in register list "test": Qualified name of register "apa_hest.zebra" '
482 '("test_apa_hest_zebra") clashes with another item.'
483 )
486def test_array_field_with_same_qualified_name_as_plain_register_should_raise_exception(tmp_path):
487 register_list = RegisterList(name="test")
488 register_list.append_register(name="apa_hest_zebra", mode=REGISTER_MODES["r_w"], description="")
489 array = register_list.append_register_array(name="apa", length=3, description="")
490 register = array.append_register(name="hest", mode=REGISTER_MODES["r_w"], description="")
491 register.append_bit(name="zebra", description="", default_value="0")
493 with pytest.raises(ValueError) as exception_info:
494 CustomGenerator(register_list=register_list, output_folder=tmp_path).create()
495 assert str(exception_info.value) == (
496 'Error in register list "test": Qualified name of field "apa.hest.zebra" '
497 '("test_apa_hest_zebra") clashes with another item.'
498 )
501def test_relative_path_printout(tmp_path, monkeypatch):
502 register_list = RegisterList(name="test")
503 generator = CustomGenerator(register_list=register_list, output_folder=tmp_path / "out")
505 string_io = io.StringIO()
506 with contextlib.redirect_stdout(string_io):
507 monkeypatch.chdir(tmp_path)
508 generator.create()
509 stdout = string_io.getvalue()
511 # Prints one sub-folder.
512 assert f"file: {Path('out') / 'test.x'}" in stdout
514 string_io = io.StringIO()
515 with contextlib.redirect_stdout(string_io):
516 monkeypatch.chdir(tmp_path.parent)
517 generator.create()
518 stdout = string_io.getvalue()
520 # Prints multiple sub-folders.
521 assert f"file: {Path(tmp_path.name) / 'out' / 'test.x'}" in stdout
523 string_io = io.StringIO()
524 with contextlib.redirect_stdout(string_io):
525 path = create_directory(tmp_path / "apa")
526 monkeypatch.chdir(path)
527 generator.create()
528 stdout = string_io.getvalue()
530 # Prints full path since the output is not inside the CWD.
531 assert f"file: {tmp_path / 'out' / 'test.x'}" in stdout