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

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 

10# Standard libraries 

11import contextlib 

12import io 

13from pathlib import Path 

14from unittest.mock import PropertyMock, patch 

15 

16# Third party libraries 

17import pytest 

18from tsfpga.system_utils import create_directory, create_file 

19 

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 

26 

27 

28class CustomGenerator(RegisterCodeGenerator): 

29 SHORT_DESCRIPTION = "for test" 

30 COMMENT_START = "#" 

31 __version__ = "3.0.1" 

32 

33 @property 

34 def output_file(self): 

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

36 

37 def get_code(self, **kwargs) -> 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 

63# False positive for pytest fixtures 

64# pylint: disable=redefined-outer-name 

65 

66 

67def test_create_return_value(generator_from_toml): 

68 generator = generator_from_toml() 

69 

70 status, artifact_path = generator.create_if_needed() 

71 assert status is True 

72 assert artifact_path == generator.output_file 

73 

74 status, artifact_path = generator.create_if_needed() 

75 assert status is False 

76 assert artifact_path == generator.output_file 

77 

78 artifact_path = generator.create() 

79 assert artifact_path == generator.output_file 

80 

81 

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

86 

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

92 

93 

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

97 

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

102 

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

107 

108 

109def test_create_should_run_again_if_toml_file_has_changed(generator_from_toml): 

110 generator = generator_from_toml() 

111 generator.create_if_needed() 

112 

113 generator = generator_from_toml( 

114 """ 

115[apa] 

116 

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

124 

125 

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

129 

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

139 

140 

141def test_create_should_run_again_if_register_list_is_modified(generator_from_toml): 

142 generator = generator_from_toml() 

143 generator.create_if_needed() 

144 

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

150 

151 

152def test_create_should_run_again_if_package_version_is_changed(generator_from_toml): 

153 generator = generator_from_toml() 

154 generator.create_if_needed() 

155 

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

161 

162 

163def test_create_should_run_again_if_generator_version_is_changed(generator_from_toml): 

164 generator = generator_from_toml() 

165 generator.create_if_needed() 

166 

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" 

171 

172 generator.create_if_needed() 

173 mocked_create.assert_called_once() 

174 

175 

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

191 

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" 

197 

198 # Test with git information 

199 git_commands_are_available.return_value = True 

200 get_git_commit.return_value = "GIT_SHA" 

201 

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

203 

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

208 

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" 

213 

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

219 

220 # Test with no source definition file 

221 register_list.source_definition_file = None 

222 

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

229 

230 

231def test_constant_with_reserved_name_should_raise_exception(generator_from_toml): 

232 generator = generator_from_toml( 

233 """ 

234[for] 

235 

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 ) 

246 

247 

248def test_plain_register_with_reserved_name_should_raise_exception(generator_from_toml): 

249 generator = generator_from_toml( 

250 """ 

251[for] 

252 

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 ) 

262 

263 

264def test_plain_register_field_with_reserved_name_should_raise_exception(generator_from_toml): 

265 generator = generator_from_toml( 

266 """ 

267[test] 

268 

269mode = "r_w" 

270 

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 ) 

280 

281 

282def test_register_array_with_reserved_name_should_raise_exception(generator_from_toml): 

283 generator = generator_from_toml( 

284 """ 

285[for] 

286 

287type = "register_array" 

288array_length = 3 

289 

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 ) 

299 

300 

301def test_array_register_with_reserved_name_should_raise_exception(generator_from_toml): 

302 generator = generator_from_toml( 

303 """ 

304[test] 

305 

306type = "register_array" 

307array_length = 3 

308 

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 ) 

318 

319 

320def test_array_register_field_with_reserved_name_should_raise_exception(generator_from_toml): 

321 generator = generator_from_toml( 

322 """ 

323[test] 

324 

325type = "register_array" 

326array_length = 3 

327 

328data.mode = "r_w" 

329 

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 ) 

339 

340 

341def test_reserved_name_check_works_even_with_strange_case(generator_from_toml): 

342 generator = generator_from_toml( 

343 """ 

344[FoR] 

345 

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 ) 

355 

356 

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

361 

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 ) 

367 

368 

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

373 

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 ) 

380 

381 

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

386 

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 ) 

393 

394 

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

400 

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 ) 

407 

408 

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

415 

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 ) 

422 

423 

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 ) 

432 

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 ) 

439 

440 

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

446 

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 ) 

453 

454 

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

460 

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 ) 

467 

468 

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

475 

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 ) 

482 

483 

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

490 

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 ) 

497 

498 

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

502 

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

508 

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

510 

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

517 

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

519 

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

525 

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