Coverage for hdl_registers/generator/test/test_register_code_generator.py: 100%

267 statements  

« prev     ^ index     » next       coverage.py v7.6.8, created at 2024-12-01 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 

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, before_header="", **kwargs) -> str: 

38 return f"""\ 

39{before_header}{self.header} 

40Nothing else, its a stupid generator. 

41""" 

42 

43 

44@pytest.fixture 

45def generator_from_toml(tmp_path): 

46 def get(toml_extras=""): 

47 toml_data = f"""\ 

48################################################################################ 

49[data] 

50 

51mode = "w" 

52description = "My register" 

53 

54{toml_extras} 

55""" 

56 

57 register_list = from_toml( 

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

59 ) 

60 

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

62 

63 return get 

64 

65 

66# False positive for pytest fixtures 

67# pylint: disable=redefined-outer-name 

68 

69 

70def test_create_return_value(generator_from_toml): 

71 generator = generator_from_toml() 

72 

73 status, artifact_path = generator.create_if_needed() 

74 assert status is True 

75 assert artifact_path == generator.output_file 

76 

77 status, artifact_path = generator.create_if_needed() 

78 assert status is False 

79 assert artifact_path == generator.output_file 

80 

81 artifact_path = generator.create() 

82 assert artifact_path == generator.output_file 

83 

84 

85def test_create_should_not_run_if_nothing_has_changed(generator_from_toml): 

86 generator = generator_from_toml() 

87 generator.register_list.add_constant(name="apa", value=3, description="") 

88 generator.create_if_needed() 

89 

90 generator = generator_from_toml() 

91 generator.register_list.add_constant(name="apa", value=3, description="") 

92 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create: 

93 generator.create_if_needed() 

94 mocked_create.assert_not_called() 

95 

96 

97def test_create_should_run_if_hash_or_version_can_not_be_read(generator_from_toml): 

98 generator = generator_from_toml() 

99 generator.create_if_needed() 

100 

101 # Overwrite the generated file, without a valid header 

102 file_path = generator.output_folder / "sensor.x" 

103 assert file_path.exists() 

104 create_file(file_path, contents="# Mumbo jumbo\n") 

105 

106 generator = generator_from_toml() 

107 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create: 

108 generator.create_if_needed() 

109 mocked_create.assert_called_once() 

110 

111 

112def test_create_should_run_again_if_toml_file_has_changed(generator_from_toml): 

113 generator = generator_from_toml() 

114 generator.create_if_needed() 

115 

116 generator = generator_from_toml( 

117 """ 

118[apa] 

119 

120type = "constant" 

121value = 3 

122""" 

123 ) 

124 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create: 

125 generator.create_if_needed() 

126 mocked_create.assert_called_once() 

127 

128 

129def test_create_should_not_run_again_if_toml_file_has_only_cosmetic_change(generator_from_toml): 

130 generator = generator_from_toml() 

131 generator.create_if_needed() 

132 

133 generator = generator_from_toml( 

134 """ 

135################################################################################ 

136# A comment. 

137""" 

138 ) 

139 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create: 

140 generator.create_if_needed() 

141 mocked_create.assert_not_called() 

142 

143 

144def test_create_should_run_again_if_register_list_is_modified(generator_from_toml): 

145 generator = generator_from_toml() 

146 generator.create_if_needed() 

147 

148 generator = generator_from_toml() 

149 generator.register_list.add_constant(name="apa", value=3, description="") 

150 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create: 

151 generator.create_if_needed() 

152 mocked_create.assert_called_once() 

153 

154 

155def test_create_should_run_again_if_package_version_is_changed(generator_from_toml): 

156 generator = generator_from_toml() 

157 generator.create_if_needed() 

158 

159 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create, patch( 

160 "hdl_registers.generator.register_code_generator.hdl_registers_version", autospec=True 

161 ) as _: 

162 generator.create_if_needed() 

163 mocked_create.assert_called_once() 

164 

165 

166def test_create_should_run_again_if_generator_version_is_changed(generator_from_toml): 

167 generator = generator_from_toml() 

168 generator.create_if_needed() 

169 

170 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create, patch( 

171 f"{__name__}.CustomGenerator.__version__", new_callable=PropertyMock 

172 ) as mocked_generator_version: 

173 mocked_generator_version.return_value = "4.0.0" 

174 

175 generator.create_if_needed() 

176 mocked_create.assert_called_once() 

177 

178 

179def test_version_header_is_detected_even_if_not_on_first_line(generator_from_toml): 

180 before_header = """ 

181# ######################### 

182# Another header 

183# ######################### 

184""" 

185 generator = generator_from_toml() 

186 generator.create_if_needed(before_header=before_header) 

187 

188 generator = generator_from_toml() 

189 with patch(f"{__name__}.CustomGenerator.create", autospec=True) as mocked_create: 

190 generator.create_if_needed(before_header=before_header) 

191 mocked_create.assert_not_called() 

192 

193 

194@patch("hdl_registers.generator.register_code_generator.git_commands_are_available", autospec=True) 

195@patch("hdl_registers.generator.register_code_generator.get_git_commit", autospec=True) 

196@patch("hdl_registers.generator.register_code_generator.svn_commands_are_available", autospec=True) 

197@patch( 

198 "hdl_registers.generator.register_code_generator.get_svn_revision_information", autospec=True 

199) 

200@patch("hdl_registers.register_list.RegisterList.object_hash", new_callable=PropertyMock) 

201def test_generated_source_info( 

202 object_hash, 

203 get_svn_revision_information, 

204 svn_commands_are_available, 

205 get_git_commit, 

206 git_commands_are_available, 

207): 

208 register_list = RegisterList(name="", source_definition_file=Path("/apa/whatever/regs.toml")) 

209 

210 expected_first_line = ( 

211 f"This file is automatically generated by hdl-registers version {hdl_registers_version}." 

212 ) 

213 expected_second_line = "Code generator CustomGenerator version 3.0.1." 

214 object_hash.return_value = "REGISTER_SHA" 

215 

216 # Test with git information 

217 git_commands_are_available.return_value = True 

218 get_git_commit.return_value = "GIT_SHA" 

219 

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

221 

222 assert got[0] == expected_first_line 

223 assert got[1] == expected_second_line 

224 assert " from file regs.toml at commit GIT_SHA." in got[2] 

225 assert got[3] == "Register hash REGISTER_SHA." 

226 

227 # Test with SVN information 

228 git_commands_are_available.return_value = False 

229 svn_commands_are_available.return_value = True 

230 get_svn_revision_information.return_value = "REVISION" 

231 

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

233 assert got[0] == expected_first_line 

234 assert got[1] == expected_second_line 

235 assert " from file regs.toml at revision REVISION." in got[2] 

236 assert got[3] == "Register hash REGISTER_SHA." 

237 

238 # Test with no source definition file 

239 register_list.source_definition_file = None 

240 

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

242 assert got[0] == expected_first_line 

243 assert got[1] == expected_second_line 

244 assert "from file" not in got[2] 

245 assert " at revision REVISION." in got[2] 

246 assert got[3] == "Register hash REGISTER_SHA." 

247 

248 

249def test_constant_with_reserved_name_should_raise_exception(generator_from_toml): 

250 generator = generator_from_toml( 

251 """ 

252[for] 

253 

254type = "constant" 

255value = 3 

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": Constant name "for" is a reserved keyword.' 

263 ) 

264 

265 

266def test_plain_register_with_reserved_name_should_raise_exception(generator_from_toml): 

267 generator = generator_from_toml( 

268 """ 

269[for] 

270 

271mode = "r_w" 

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": Register name "for" is a reserved keyword.' 

279 ) 

280 

281 

282def test_plain_register_field_with_reserved_name_should_raise_exception(generator_from_toml): 

283 generator = generator_from_toml( 

284 """ 

285[test] 

286 

287mode = "r_w" 

288 

289for.type = "bit" 

290""", 

291 ) 

292 with pytest.raises(ValueError) as exception_info: 

293 generator.create_if_needed() 

294 assert ( 

295 str(exception_info.value) 

296 == 'Error in register list "sensor": Field name "for" is a reserved keyword.' 

297 ) 

298 

299 

300def test_register_array_with_reserved_name_should_raise_exception(generator_from_toml): 

301 generator = generator_from_toml( 

302 """ 

303[for] 

304 

305type = "register_array" 

306array_length = 3 

307 

308data.mode = "r_w" 

309""", 

310 ) 

311 with pytest.raises(ValueError) as exception_info: 

312 generator.create_if_needed() 

313 assert ( 

314 str(exception_info.value) 

315 == 'Error in register list "sensor": Register array name "for" is a reserved keyword.' 

316 ) 

317 

318 

319def test_array_register_with_reserved_name_should_raise_exception(generator_from_toml): 

320 generator = generator_from_toml( 

321 """ 

322[test] 

323 

324type = "register_array" 

325array_length = 3 

326 

327for.mode = "r_w" 

328""", 

329 ) 

330 with pytest.raises(ValueError) as exception_info: 

331 generator.create_if_needed() 

332 assert ( 

333 str(exception_info.value) 

334 == 'Error in register list "sensor": Register name "for" is a reserved keyword.' 

335 ) 

336 

337 

338def test_array_register_field_with_reserved_name_should_raise_exception(generator_from_toml): 

339 generator = generator_from_toml( 

340 """ 

341[test] 

342 

343type = "register_array" 

344array_length = 3 

345 

346data.mode = "r_w" 

347 

348data.for.type = "bit" 

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": Field name "for" is a reserved keyword.' 

356 ) 

357 

358 

359def test_reserved_name_check_works_even_with_strange_case(generator_from_toml): 

360 generator = generator_from_toml( 

361 """ 

362[FoR] 

363 

364mode = "r_w" 

365""" 

366 ) 

367 with pytest.raises(ValueError) as exception_info: 

368 generator.create_if_needed() 

369 assert ( 

370 str(exception_info.value) 

371 == 'Error in register list "sensor": Register name "FoR" is a reserved keyword.' 

372 ) 

373 

374 

375def test_two_constants_with_the_same_name_should_raise_exception(tmp_path): 

376 register_list = RegisterList(name="test") 

377 register_list.add_constant(name="apa", value=3, description="") 

378 register_list.add_constant(name="apa", value=True, description="") 

379 

380 with pytest.raises(ValueError) as exception_info: 

381 CustomGenerator(register_list=register_list, output_folder=tmp_path).create() 

382 assert ( 

383 str(exception_info.value) == 'Error in register list "test": Duplicate constant name "apa".' 

384 ) 

385 

386 

387def test_two_registers_with_the_same_name_should_raise_exception(tmp_path): 

388 register_list = RegisterList(name="test") 

389 register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="") 

390 register_list.append_register(name="apa", mode=REGISTER_MODES["w"], description="") 

391 

392 with pytest.raises(ValueError) as exception_info: 

393 CustomGenerator(register_list=register_list, output_folder=tmp_path).create() 

394 assert ( 

395 str(exception_info.value) 

396 == 'Error in register list "test": Duplicate plain register name "apa".' 

397 ) 

398 

399 

400def test_register_with_the_same_name_as_register_array_should_raise_exception(tmp_path): 

401 register_list = RegisterList(name="test") 

402 register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="") 

403 register_list.append_register_array(name="apa", length=2, description="") 

404 

405 with pytest.raises(ValueError) as exception_info: 

406 CustomGenerator(register_list=register_list, output_folder=tmp_path).create() 

407 assert ( 

408 str(exception_info.value) 

409 == 'Error in register list "test": Register array "apa" may not have same name as register.' 

410 ) 

411 

412 

413def test_two_plain_fields_with_the_same_name_should_raise_exception(tmp_path): 

414 register_list = RegisterList(name="test") 

415 register = register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="") 

416 register.append_bit(name="hest", description="", default_value="0") 

417 register.append_bit(name="hest", description="", default_value="0") 

418 

419 with pytest.raises(ValueError) as exception_info: 

420 CustomGenerator(register_list=register_list, output_folder=tmp_path).create() 

421 assert ( 

422 str(exception_info.value) 

423 == 'Error in register list "test": Duplicate field name "hest" in register "apa".' 

424 ) 

425 

426 

427def test_two_array_fields_with_the_same_name_should_raise_exception(tmp_path): 

428 register_list = RegisterList(name="test") 

429 array = register_list.append_register_array(name="apa", length=2, description="") 

430 register = array.append_register(name="hest", mode=REGISTER_MODES["r_w"], description="") 

431 register.append_bit(name="zebra", description="", default_value="0") 

432 register.append_bit(name="zebra", description="", default_value="0") 

433 

434 with pytest.raises(ValueError) as exception_info: 

435 CustomGenerator(register_list=register_list, output_folder=tmp_path).create() 

436 assert ( 

437 str(exception_info.value) 

438 == 'Error in register list "test": Duplicate field name "zebra" in register "apa.hest".' 

439 ) 

440 

441 

442def test_two_register_arrays_with_the_same_name_should_raise_exception(tmp_path): 

443 register_list = RegisterList(name="test") 

444 register_list.append_register_array(name="apa", length=2, description="").append_register( 

445 name="hest", mode=REGISTER_MODES["r"], description="" 

446 ) 

447 register_list.append_register_array(name="apa", length=3, description="").append_register( 

448 name="zebra", mode=REGISTER_MODES["w"], description="" 

449 ) 

450 

451 with pytest.raises(ValueError) as exception_info: 

452 CustomGenerator(register_list=register_list, output_folder=tmp_path).create() 

453 assert ( 

454 str(exception_info.value) 

455 == 'Error in register list "test": Duplicate register array name "apa".' 

456 ) 

457 

458 

459def test_array_register_with_same_qualified_name_as_plain_register_should_raise_exception(tmp_path): 

460 register_list = RegisterList(name="test") 

461 register_list.append_register(name="apa_hest", mode=REGISTER_MODES["r_w"], description="") 

462 register_array = register_list.append_register_array(name="apa", length=3, description="") 

463 register_array.append_register(name="hest", mode=REGISTER_MODES["r_w"], description="") 

464 

465 with pytest.raises(ValueError) as exception_info: 

466 CustomGenerator(register_list=register_list, output_folder=tmp_path).create() 

467 assert str(exception_info.value) == ( 

468 'Error in register list "test": Qualified name of register "apa.hest" ' 

469 '("test_apa_hest") clashes with another item.' 

470 ) 

471 

472 

473def test_plain_field_with_same_qualified_name_as_plain_register_should_raise_exception(tmp_path): 

474 register_list = RegisterList(name="test") 

475 register_list.append_register(name="apa_hest", mode=REGISTER_MODES["r_w"], description="") 

476 register = register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="") 

477 register.append_bit(name="hest", description="", default_value="0") 

478 

479 with pytest.raises(ValueError) as exception_info: 

480 CustomGenerator(register_list=register_list, output_folder=tmp_path).create() 

481 assert str(exception_info.value) == ( 

482 'Error in register list "test": Qualified name of field "apa.hest" ' 

483 '("test_apa_hest") clashes with another item.' 

484 ) 

485 

486 

487def test_plain_field_with_same_qualified_name_as_array_register_should_raise_exception(tmp_path): 

488 register_list = RegisterList(name="test") 

489 register = register_list.append_register(name="apa", mode=REGISTER_MODES["r_w"], description="") 

490 register.append_bit(name="hest_zebra", description="", default_value="0") 

491 array = register_list.append_register_array(name="apa_hest", length=2, description="") 

492 array.append_register(name="zebra", mode=REGISTER_MODES["r_w"], description="") 

493 

494 with pytest.raises(ValueError) as exception_info: 

495 CustomGenerator(register_list=register_list, output_folder=tmp_path).create() 

496 assert str(exception_info.value) == ( 

497 'Error in register list "test": Qualified name of register "apa_hest.zebra" ' 

498 '("test_apa_hest_zebra") clashes with another item.' 

499 ) 

500 

501 

502def test_array_field_with_same_qualified_name_as_plain_register_should_raise_exception(tmp_path): 

503 register_list = RegisterList(name="test") 

504 register_list.append_register(name="apa_hest_zebra", mode=REGISTER_MODES["r_w"], description="") 

505 array = register_list.append_register_array(name="apa", length=3, description="") 

506 register = array.append_register(name="hest", mode=REGISTER_MODES["r_w"], description="") 

507 register.append_bit(name="zebra", description="", default_value="0") 

508 

509 with pytest.raises(ValueError) as exception_info: 

510 CustomGenerator(register_list=register_list, output_folder=tmp_path).create() 

511 assert str(exception_info.value) == ( 

512 'Error in register list "test": Qualified name of field "apa.hest.zebra" ' 

513 '("test_apa_hest_zebra") clashes with another item.' 

514 ) 

515 

516 

517def test_relative_path_printout(tmp_path, monkeypatch): 

518 register_list = RegisterList(name="test") 

519 generator = CustomGenerator(register_list=register_list, output_folder=tmp_path / "out") 

520 

521 string_io = io.StringIO() 

522 with contextlib.redirect_stdout(string_io): 

523 monkeypatch.chdir(tmp_path) 

524 generator.create() 

525 stdout = string_io.getvalue() 

526 

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

528 

529 string_io = io.StringIO() 

530 with contextlib.redirect_stdout(string_io): 

531 path = create_directory(tmp_path / "apa") 

532 monkeypatch.chdir(path) 

533 generator.create() 

534 stdout = string_io.getvalue() 

535 

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

537 

538 string_io = io.StringIO() 

539 with contextlib.redirect_stdout(string_io): 

540 monkeypatch.chdir(tmp_path.parent) 

541 generator.create() 

542 stdout = string_io.getvalue() 

543 

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