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

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_reserved_name_check_works_even_with_strange_case(generator_from_toml): 

344 generator = generator_from_toml( 

345 """ 

346[FoR] 

347 

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 ) 

357 

358 

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

363 

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 ) 

369 

370 

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

375 

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 ) 

382 

383 

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

388 

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 ) 

395 

396 

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

402 

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 ) 

409 

410 

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

417 

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 ) 

424 

425 

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 ) 

434 

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 ) 

441 

442 

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

448 

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 ) 

455 

456 

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

462 

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 ) 

469 

470 

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

477 

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 ) 

484 

485 

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

492 

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 ) 

499 

500 

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

504 

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

510 

511 # Prints one sub-folder. 

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

513 

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

519 

520 # Prints multiple sub-folders. 

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

522 

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

529 

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

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