Coverage for tsfpga/vivado/test/test_project.py: 100%

239 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-29 20:01 +0000

1# -------------------------------------------------------------------------------------------------- 

2# Copyright (c) Lukas Vik. All rights reserved. 

3# 

4# This file is part of the tsfpga project, a project platform for modern FPGA development. 

5# https://tsfpga.com 

6# https://gitlab.com/tsfpga/tsfpga 

7# -------------------------------------------------------------------------------------------------- 

8 

9# Standard libraries 

10import unittest 

11from pathlib import Path 

12from unittest.mock import MagicMock, patch 

13 

14# Third party libraries 

15import pytest 

16 

17# First party libraries 

18from tsfpga.module import BaseModule 

19from tsfpga.system_utils import create_directory, create_file 

20 

21# pylint: disable=unused-import 

22from tsfpga.test.conftest import fixture_tmp_path # noqa: F401 

23from tsfpga.vivado.generics import StringGenericValue 

24from tsfpga.vivado.project import VivadoProject, copy_and_combine_dicts 

25 

26 

27def test_casting_to_string(): 

28 project = VivadoProject(name="my_project", modules=[], part="") 

29 assert ( 

30 str(project) 

31 == """\ 

32my_project 

33Type: VivadoProject 

34Top level: my_project_top 

35Generics: - 

36""" 

37 ) 

38 

39 project = VivadoProject( 

40 name="my_project", 

41 modules=[], 

42 part="", 

43 top="apa", 

44 generics=dict(hest=True, zebra=3, foo=StringGenericValue("/home/test.vhd")), 

45 ) 

46 assert ( 

47 str(project) 

48 == """\ 

49my_project 

50Type: VivadoProject 

51Top level: apa 

52Generics: hest=True, zebra=3, foo=/home/test.vhd 

53""" 

54 ) 

55 

56 project = VivadoProject(name="my_project", modules=[], part="", apa=123, hest=456) 

57 assert ( 

58 str(project) 

59 == """\ 

60my_project 

61Type: VivadoProject 

62Top level: my_project_top 

63Generics: - 

64Arguments: apa=123, hest=456 

65""" 

66 ) 

67 

68 

69def test_modules_list_should_be_copied(): 

70 modules = [1] 

71 proj = VivadoProject(name="name", modules=modules, part="part") 

72 

73 modules.append(2) 

74 assert len(proj.modules) == 1 

75 

76 

77def test_static_generics_dictionary_should_be_copied(): 

78 generics = dict(apa=3) 

79 proj = VivadoProject(name="name", modules=[], part="part", generics=generics) 

80 

81 generics["apa"] = False 

82 assert proj.static_generics["apa"] == 3 

83 

84 

85def test_constraints_list_should_be_copied(): 

86 constraints = [1] 

87 proj = VivadoProject(name="name", modules=[], part="part", constraints=constraints) 

88 

89 constraints.append(2) 

90 assert len(proj.constraints) == 1 

91 

92 

93def test_create_should_raise_exception_if_project_path_already_exists(tmp_path): 

94 create_directory(tmp_path / "projects" / "name") 

95 proj = VivadoProject(name="name", modules=[], part="part") 

96 with pytest.raises(ValueError) as exception_info: 

97 proj.create(tmp_path / "projects") 

98 assert str(exception_info.value).startswith("Folder already exists") 

99 

100 

101def test_build_should_raise_exception_if_project_does_not_exists(tmp_path): 

102 create_directory(tmp_path / "projects") 

103 proj = VivadoProject(name="name", modules=[], part="part") 

104 with pytest.raises(ValueError) as exception_info: 

105 proj.build(tmp_path / "projects", synth_only=True) 

106 assert str(exception_info.value).startswith("Project file does not exist") 

107 

108 

109def test_build_with_impl_run_should_raise_exception_if_no_output_path_is_given(): 

110 proj = VivadoProject(name="name", modules=[], part="part") 

111 with pytest.raises(ValueError) as exception_info: 

112 proj.build("None") 

113 assert str(exception_info.value).startswith("Must specify output_path") 

114 

115 

116def test_top_name(): 

117 assert VivadoProject(name="apa", modules=[], part="").top == "apa_top" 

118 assert VivadoProject(name="apa", modules=[], part="", top="hest").top == "hest" 

119 

120 

121def test_project_file_name_is_same_as_project_name(): 

122 project_path = Path("projects/apa") 

123 assert ( 

124 VivadoProject(name="apa", modules=[], part="").project_file(project_path) 

125 == project_path / "apa.xpr" 

126 ) 

127 

128 

129def test_project_create(tmp_path): 

130 with patch("tsfpga.vivado.project.run_vivado_tcl", autospec=True) as _: 

131 assert VivadoProject(name="apa", modules=[], part="").create(tmp_path / "projects" / "apa") 

132 assert (tmp_path / "projects" / "apa" / "create_vivado_project.tcl").exists() 

133 

134 

135def test_project_create_should_raise_exception_if_project_path_already_exists(tmp_path): 

136 project_path = create_directory(tmp_path / "projects" / "apa") 

137 with pytest.raises(ValueError) as exception_info: 

138 VivadoProject(name="apa", modules=[], part="").create(project_path) 

139 assert str(exception_info.value) == f"Folder already exists: {project_path}" 

140 

141 

142def test_copy_and_combine_dict_with_both_arguments_none(): 

143 assert copy_and_combine_dicts(None, None) is None 

144 

145 

146def test_copy_and_combine_dict_with_first_argument_valid(): 

147 dict_first = dict(first=1) 

148 

149 result = copy_and_combine_dicts(dict_first, None) 

150 assert result == dict(first=1) 

151 assert dict_first == dict(first=1) 

152 

153 dict_first["first_dummy"] = True 

154 assert result == dict(first=1) 

155 

156 

157def test_copy_and_combine_dict_with_second_argument_valid(): 

158 dict_second = dict(second=2) 

159 

160 result = copy_and_combine_dicts(None, dict_second) 

161 assert result == dict(second=2) 

162 assert dict_second == dict(second=2) 

163 

164 dict_second["second_dummy"] = True 

165 assert result == dict(second=2) 

166 

167 

168def test_copy_and_combine_dict_with_both_arguments_valid(): 

169 dict_first = dict(first=1) 

170 dict_second = dict(second=2) 

171 

172 result = copy_and_combine_dicts(dict_first, dict_second) 

173 assert result == dict(first=1, second=2) 

174 assert dict_first == dict(first=1) 

175 assert dict_second == dict(second=2) 

176 

177 dict_first["first_dummy"] = True 

178 dict_second["second_dummy"] = True 

179 assert result == dict(first=1, second=2) 

180 

181 

182def test_copy_and_combine_dict_with_both_arguments_valid_and_same_key(): 

183 dict_first = dict(first=1, common=3) 

184 dict_second = dict(second=2, common=4) 

185 

186 result = copy_and_combine_dicts(dict_first, dict_second) 

187 assert result == dict(first=1, second=2, common=4) 

188 assert dict_first == dict(first=1, common=3) 

189 assert dict_second == dict(second=2, common=4) 

190 

191 dict_first["first_dummy"] = True 

192 dict_second["second_dummy"] = True 

193 assert result == dict(first=1, second=2, common=4) 

194 

195 

196# pylint: disable=too-many-instance-attributes 

197@pytest.mark.usefixtures("fixture_tmp_path") 

198class TestVivadoProject(unittest.TestCase): 

199 

200 tmp_path = None 

201 

202 def setUp(self): 

203 self.project_path = self.tmp_path / "projects" / "apa" / "project" 

204 self.output_path = self.tmp_path / "projects" / "apa" 

205 self.ip_cache_path = MagicMock() 

206 self.build_time_generics = dict(enable=True) 

207 self.num_threads = 4 

208 self.run_index = 3 

209 self.synth_only = False 

210 self.from_impl = False 

211 

212 self.mocked_run_vivado_tcl = None 

213 

214 def _create(self, project, **other_arguments): 

215 with patch( 

216 "tsfpga.vivado.project.run_vivado_tcl", autospec=True 

217 ) as self.mocked_run_vivado_tcl: 

218 return project.create( 

219 project_path=self.project_path, ip_cache_path=self.ip_cache_path, **other_arguments 

220 ) 

221 

222 def test_default_pre_create_hook_should_pass(self): 

223 class CustomVivadoProject(VivadoProject): 

224 

225 pass 

226 

227 project = CustomVivadoProject(name="apa", modules=[], part="") 

228 self._create(project) 

229 self.mocked_run_vivado_tcl.assert_called_once() 

230 

231 def test_project_pre_create_hook_returning_false_should_fail_and_not_call_vivado_run(self): 

232 class CustomVivadoProject(VivadoProject): 

233 def pre_create(self, **kwargs): # pylint: disable=unused-argument 

234 return False 

235 

236 assert not self._create(CustomVivadoProject(name="apa", modules=[], part="")) 

237 self.mocked_run_vivado_tcl.assert_not_called() 

238 

239 def test_create_should_call_pre_create_with_correct_parameters(self): 

240 project = VivadoProject(name="apa", modules=[], part="", generics=dict(apa=123), hest=456) 

241 with patch("tsfpga.vivado.project.VivadoProject.pre_create") as mocked_pre_create: 

242 self._create(project, zebra=789) 

243 mocked_pre_create.assert_called_once_with( 

244 project_path=self.project_path, 

245 ip_cache_path=self.ip_cache_path, 

246 part="", 

247 generics=dict(apa=123), 

248 hest=456, 

249 zebra=789, 

250 ) 

251 self.mocked_run_vivado_tcl.assert_called_once() 

252 

253 def _build(self, project): 

254 with patch( 

255 "tsfpga.vivado.project.run_vivado_tcl", autospec=True 

256 ) as self.mocked_run_vivado_tcl, patch( 

257 "tsfpga.vivado.project.VivadoProject._get_size", autospec=True 

258 ) as _, patch( 

259 "tsfpga.vivado.project.shutil.copy2", autospec=True 

260 ) as _: 

261 create_file(self.project_path / "apa.xpr") 

262 return project.build( 

263 project_path=self.project_path, 

264 output_path=self.output_path, 

265 run_index=self.run_index, 

266 generics=self.build_time_generics, 

267 synth_only=self.synth_only, 

268 num_threads=self.num_threads, 

269 other_parameter="hest", 

270 ) 

271 

272 def test_build_module_pre_build_hook_and_create_regs_are_called(self): 

273 project = VivadoProject( 

274 name="apa", 

275 modules=[MagicMock(spec=BaseModule), MagicMock(spec=BaseModule)], 

276 part="", 

277 apa=123, 

278 ) 

279 build_result = self._build(project) 

280 assert build_result.success 

281 

282 for module in project.modules: 

283 module.pre_build.assert_called_once_with( 

284 project=project, 

285 other_parameter="hest", 

286 apa=123, 

287 project_path=self.project_path, 

288 output_path=self.output_path, 

289 run_index=self.run_index, 

290 generics=self.build_time_generics, 

291 synth_only=self.synth_only, 

292 from_impl=self.from_impl, 

293 num_threads=self.num_threads, 

294 ) 

295 module.create_regs_vhdl_package.assert_called_once() 

296 

297 def test_default_pre_and_post_build_hooks_should_pass(self): 

298 class CustomVivadoProject(VivadoProject): 

299 

300 pass 

301 

302 build_result = self._build(CustomVivadoProject(name="apa", modules=[], part="")) 

303 assert build_result.success 

304 self.mocked_run_vivado_tcl.assert_called_once() 

305 

306 def test_project_pre_build_hook_returning_false_should_fail_and_not_call_vivado_run(self): 

307 class CustomVivadoProject(VivadoProject): 

308 def pre_build(self, **kwargs): # pylint: disable=unused-argument 

309 return False 

310 

311 build_result = self._build(CustomVivadoProject(name="apa", modules=[], part="")) 

312 assert not build_result.success 

313 self.mocked_run_vivado_tcl.assert_not_called() 

314 

315 def test_project_post_build_hook_returning_false_should_fail(self): 

316 class CustomVivadoProject(VivadoProject): 

317 def post_build(self, **kwargs): # pylint: disable=unused-argument 

318 return False 

319 

320 build_result = self._build(CustomVivadoProject(name="apa", modules=[], part="")) 

321 assert not build_result.success 

322 self.mocked_run_vivado_tcl.assert_called_once() 

323 

324 def test_project_build_hooks_should_be_called_with_correct_parameters(self): 

325 project = VivadoProject( 

326 name="apa", modules=[], part="", generics=dict(static_generic=2), apa=123 

327 ) 

328 with patch("tsfpga.vivado.project.VivadoProject.pre_build") as mocked_pre_build, patch( 

329 "tsfpga.vivado.project.VivadoProject.post_build" 

330 ) as mocked_post_build: 

331 self._build(project) 

332 

333 arguments = dict( 

334 project_path=self.project_path, 

335 output_path=self.output_path, 

336 run_index=self.run_index, 

337 generics=copy_and_combine_dicts(dict(static_generic=2), self.build_time_generics), 

338 synth_only=self.synth_only, 

339 from_impl=self.from_impl, 

340 num_threads=self.num_threads, 

341 other_parameter="hest", 

342 apa=123, 

343 ) 

344 mocked_pre_build.assert_called_once_with(**arguments) 

345 

346 # Could be improved by actually checking the build_result object. 

347 # See https://gitlab.com/tsfpga/tsfpga/-/issues/39 

348 arguments.update(build_result=unittest.mock.ANY) 

349 mocked_post_build.assert_called_once_with(**arguments) 

350 

351 def test_module_pre_build_hook_returning_false_should_fail_and_not_call_vivado(self): 

352 module = MagicMock(spec=BaseModule) 

353 module.name = "whatever" 

354 project = VivadoProject(name="apa", modules=[module], part="") 

355 

356 project.modules[0].pre_build.return_value = True 

357 build_result = self._build(project) 

358 assert build_result.success 

359 self.mocked_run_vivado_tcl.assert_called_once() 

360 

361 project.modules[0].pre_build.return_value = False 

362 build_result = self._build(project) 

363 assert not build_result.success 

364 self.mocked_run_vivado_tcl.assert_not_called() 

365 

366 @patch("tsfpga.vivado.project.VivadoTcl", autospec=True) 

367 def test_different_generic_combinations(self, mocked_vivado_tcl): 

368 mocked_vivado_tcl.return_value.build.return_value = "" 

369 

370 # No generics 

371 self.build_time_generics = None 

372 build_result = self._build(VivadoProject(name="apa", modules=[], part="")) 

373 assert build_result.success 

374 # Note: In python 3.8 we can use call_args.kwargs straight away 

375 _, kwargs = mocked_vivado_tcl.return_value.build.call_args 

376 assert kwargs["generics"] == {} 

377 

378 # Only build time generics 

379 self.build_time_generics = dict(runtime="value") 

380 build_result = self._build(VivadoProject(name="apa", modules=[], part="")) 

381 assert build_result.success 

382 _, kwargs = mocked_vivado_tcl.return_value.build.call_args 

383 assert kwargs["generics"] == dict(runtime="value") 

384 

385 # Static and build time generics 

386 self.build_time_generics = dict(runtime="value") 

387 build_result = self._build( 

388 VivadoProject(name="apa", modules=[], part="", generics=dict(static="a value")) 

389 ) 

390 assert build_result.success 

391 _, kwargs = mocked_vivado_tcl.return_value.build.call_args 

392 assert kwargs["generics"] == dict(runtime="value", static="a value") 

393 

394 # Same key in both static and build time generic. Should prefer build time. 

395 self.build_time_generics = dict(static_and_runtime="build value") 

396 build_result = self._build( 

397 VivadoProject( 

398 name="apa", modules=[], part="", generics=dict(static_and_runtime="static value") 

399 ) 

400 ) 

401 assert build_result.success 

402 _, kwargs = mocked_vivado_tcl.return_value.build.call_args 

403 assert kwargs["generics"] == dict(static_and_runtime="build value") 

404 

405 # Only static generics 

406 self.build_time_generics = None 

407 build_result = self._build( 

408 VivadoProject(name="apa", modules=[], part="", generics=dict(runtime="a value")) 

409 ) 

410 assert build_result.success 

411 _, kwargs = mocked_vivado_tcl.return_value.build.call_args 

412 assert kwargs["generics"] == dict(runtime="a value") 

413 

414 @patch("tsfpga.vivado.project.VivadoTcl", autospec=True) 

415 def test_build_time_generics_are_copied(self, mocked_vivado_tcl): 

416 mocked_vivado_tcl.return_value.build.return_value = "" 

417 

418 self.build_time_generics = dict(runtime="value") 

419 build_result = self._build( 

420 VivadoProject(name="apa", modules=[], part="", generics=dict(static="a value")) 

421 ) 

422 assert build_result.success 

423 assert self.build_time_generics == dict(runtime="value") 

424 

425 def test_modules_are_deep_copied_before_pre_create_hook(self): 

426 class CustomVivadoProject(VivadoProject): 

427 def pre_create(self, **kwargs): 

428 self.modules[0].registers = "Some other value" 

429 return True 

430 

431 module = MagicMock(spec=BaseModule) 

432 module.registers = "Some value" 

433 

434 project = CustomVivadoProject(name="apa", modules=[module], part="") 

435 assert self._create(project) 

436 

437 assert module.registers == "Some value" 

438 

439 def test_modules_are_deep_copied_before_pre_build_hook(self): 

440 class CustomVivadoProject(VivadoProject): 

441 def pre_build(self, **kwargs): 

442 self.modules[0].registers = "Some other value" 

443 return True 

444 

445 module = MagicMock(spec=BaseModule) 

446 module.registers = "Some value" 

447 

448 project = CustomVivadoProject(name="apa", modules=[module], part="") 

449 assert self._build(project).success 

450 

451 assert module.registers == "Some value"