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

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# -------------------------------------------------------------------------------------------------- 

9 

10import contextlib 

11import io 

12from pathlib import Path 

13from unittest.mock import PropertyMock, patch 

14 

15import pytest 

16from tsfpga.system_utils import create_directory, create_file 

17 

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 

23 

24 

25class CustomGenerator(RegisterCodeGenerator): 

26 SHORT_DESCRIPTION = "for test" 

27 COMMENT_START = "#" 

28 __version__ = "3.0.1" 

29 

30 @property 

31 def output_file(self): 

32 return self.output_folder / f"{self.name}.x" 

33 

34 def get_code( 

35 self, 

36 **kwargs, # noqa: ARG002 

37 ) -> str: 

38 return "Nothing, its a stupid generator." 

39 

40 

41@pytest.fixture 

42def generator_from_toml(tmp_path): 

43 def get(toml_extras=""): 

44 toml_data = f"""\ 

45################################################################################ 

46[data] 

47 

48mode = "w" 

49description = "My register" 

50 

51{toml_extras} 

52""" 

53 

54 register_list = from_toml( 

55 name="sensor", toml_file=create_file(tmp_path / "sensor_regs.toml", toml_data) 

56 ) 

57 

58 return CustomGenerator(register_list=register_list, output_folder=tmp_path) 

59 

60 return get 

61 

62 

63def test_create_return_value(generator_from_toml): 

64 generator = generator_from_toml() 

65 

66 status, artifact_path = generator.create_if_needed() 

67 assert status is True 

68 assert artifact_path == generator.output_file 

69 

70 status, artifact_path = generator.create_if_needed() 

71 assert status is False 

72 assert artifact_path == generator.output_file 

73 

74 artifact_path = generator.create() 

75 assert artifact_path == generator.output_file 

76 

77 

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() 

82 

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() 

88 

89 

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() 

93 

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") 

98 

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() 

103 

104 

105def test_create_should_run_again_if_toml_file_has_changed(generator_from_toml): 

106 generator = generator_from_toml() 

107 generator.create_if_needed() 

108 

109 generator = generator_from_toml( 

110 """ 

111[apa] 

112 

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() 

120 

121 

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() 

125 

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() 

135 

136 

137def test_create_should_run_again_if_register_list_is_modified(generator_from_toml): 

138 generator = generator_from_toml() 

139 generator.create_if_needed() 

140 

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() 

146 

147 

148def test_create_should_run_again_if_package_version_is_changed(generator_from_toml): 

149 generator = generator_from_toml() 

150 generator.create_if_needed() 

151 

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() 

160 

161 

162def test_create_should_run_again_if_generator_version_is_changed(generator_from_toml): 

163 generator = generator_from_toml() 

164 generator.create_if_needed() 

165 

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" 

173 

174 generator.create_if_needed() 

175 mocked_create.assert_called_once() 

176 

177 

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")) 

193 

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" 

199 

200 # Test with git information 

201 git_commands_are_available.return_value = True 

202 get_git_commit.return_value = "GIT_SHA" 

203 

204 got = CustomGenerator(register_list=register_list, output_folder=None).generated_source_info 

205 

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." 

210 

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" 

215 

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." 

221 

222 # Test with no source definition file 

223 register_list.source_definition_file = None 

224 

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." 

231 

232 

233def test_constant_with_reserved_name_should_raise_exception(generator_from_toml): 

234 generator = generator_from_toml( 

235 """ 

236[for] 

237 

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 ) 

248 

249 

250def test_plain_register_with_reserved_name_should_raise_exception(generator_from_toml): 

251 generator = generator_from_toml( 

252 """ 

253[for] 

254 

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 ) 

264 

265 

266def test_plain_register_field_with_reserved_name_should_raise_exception(generator_from_toml): 

267 generator = generator_from_toml( 

268 """ 

269[test] 

270 

271mode = "r_w" 

272 

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 ) 

282 

283 

284def test_register_array_with_reserved_name_should_raise_exception(generator_from_toml): 

285 generator = generator_from_toml( 

286 """ 

287[for] 

288 

289type = "register_array" 

290array_length = 3 

291 

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 ) 

301 

302 

303def test_array_register_with_reserved_name_should_raise_exception(generator_from_toml): 

304 generator = generator_from_toml( 

305 """ 

306[test] 

307 

308type = "register_array" 

309array_length = 3 

310 

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 ) 

320 

321 

322def test_array_register_field_with_reserved_name_should_raise_exception(generator_from_toml): 

323 generator = generator_from_toml( 

324 """ 

325[test] 

326 

327type = "register_array" 

328array_length = 3 

329 

330data.mode = "r_w" 

331 

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 ) 

341 

342 

343def test_enumeration_field_element_with_reserved_name_should_raise_exception(generator_from_toml): 

344 generator = generator_from_toml( 

345 """ 

346[test] 

347 

348mode = "r_w" 

349 

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 ) 

360 

361 

362def test_reserved_name_check_works_even_with_strange_case(generator_from_toml): 

363 generator = generator_from_toml( 

364 """ 

365[FoR] 

366 

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 ) 

376 

377 

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="") 

382 

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 ) 

388 

389 

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="") 

394 

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 ) 

401 

402 

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="") 

407 

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 ) 

414 

415 

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") 

421 

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 ) 

428 

429 

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") 

436 

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 ) 

443 

444 

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 ) 

453 

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 ) 

460 

461 

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="") 

467 

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 ) 

474 

475 

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") 

481 

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 ) 

488 

489 

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="") 

496 

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 ) 

503 

504 

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") 

511 

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 ) 

518 

519 

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") 

523 

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() 

529 

530 # Prints one sub-folder. 

531 assert f"file: {Path('out') / 'test.x'}" in stdout 

532 

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() 

538 

539 # Prints multiple sub-folders. 

540 assert f"file: {Path(tmp_path.name) / 'out' / 'test.x'}" in stdout 

541 

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() 

548 

549 # Prints full path since the output is not inside the CWD. 

550 assert f"file: {tmp_path / 'out' / 'test.x'}" in stdout