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

239 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-05-28 04:01 +0000

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

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

3# 

4# This file is part of the tsfpga project. 

5# https://tsfpga.com 

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

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

8 

9import unittest 

10from unittest.mock import MagicMock, patch 

11from pathlib import Path 

12 

13import pytest 

14 

15from tsfpga.module import BaseModule 

16from tsfpga.system_utils import create_directory, create_file 

17from tsfpga.vivado.project import VivadoProject, copy_and_combine_dicts 

18from tsfpga.vivado.generics import StringGenericValue 

19 

20# pylint: disable=unused-import 

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

22 

23 

24def test_casting_to_string(): 

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

26 assert ( 

27 str(project) 

28 == """\ 

29my_project 

30Type: VivadoProject 

31Top level: my_project_top 

32Generics: - 

33""" 

34 ) 

35 

36 project = VivadoProject( 

37 name="my_project", 

38 modules=[], 

39 part="", 

40 top="apa", 

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

42 ) 

43 assert ( 

44 str(project) 

45 == """\ 

46my_project 

47Type: VivadoProject 

48Top level: apa 

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

50""" 

51 ) 

52 

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

54 assert ( 

55 str(project) 

56 == """\ 

57my_project 

58Type: VivadoProject 

59Top level: my_project_top 

60Generics: - 

61Arguments: apa=123, hest=456 

62""" 

63 ) 

64 

65 

66def test_modules_list_should_be_copied(): 

67 modules = [1] 

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

69 

70 modules.append(2) 

71 assert len(proj.modules) == 1 

72 

73 

74def test_static_generics_dictionary_should_be_copied(): 

75 generics = dict(apa=3) 

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

77 

78 generics["apa"] = False 

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

80 

81 

82def test_constraints_list_should_be_copied(): 

83 constraints = [1] 

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

85 

86 constraints.append(2) 

87 assert len(proj.constraints) == 1 

88 

89 

90def test_create_should_raise_exception_if_project_path_already_exists(tmp_path): 

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

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

93 with pytest.raises(ValueError) as exception_info: 

94 proj.create(tmp_path / "projects") 

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

96 

97 

98def test_build_should_raise_exception_if_project_does_not_exists(tmp_path): 

99 create_directory(tmp_path / "projects") 

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

101 with pytest.raises(ValueError) as exception_info: 

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

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

104 

105 

106def test_build_with_impl_run_should_raise_exception_if_no_output_path_is_given(): 

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

108 with pytest.raises(ValueError) as exception_info: 

109 proj.build("None") 

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

111 

112 

113def test_top_name(): 

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

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

116 

117 

118def test_project_file_name_is_same_as_project_name(): 

119 project_path = Path("projects/apa") 

120 assert ( 

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

122 == project_path / "apa.xpr" 

123 ) 

124 

125 

126def test_project_create(tmp_path): 

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

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

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

130 

131 

132def test_project_create_should_raise_exception_if_project_path_already_exists(tmp_path): 

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

134 with pytest.raises(ValueError) as exception_info: 

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

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

137 

138 

139def test_copy_and_combine_dict_with_both_arguments_none(): 

140 assert copy_and_combine_dicts(None, None) is None 

141 

142 

143def test_copy_and_combine_dict_with_first_argument_valid(): 

144 dict_first = dict(first=1) 

145 

146 result = copy_and_combine_dicts(dict_first, None) 

147 assert result == dict(first=1) 

148 assert dict_first == dict(first=1) 

149 

150 dict_first["first_dummy"] = True 

151 assert result == dict(first=1) 

152 

153 

154def test_copy_and_combine_dict_with_second_argument_valid(): 

155 dict_second = dict(second=2) 

156 

157 result = copy_and_combine_dicts(None, dict_second) 

158 assert result == dict(second=2) 

159 assert dict_second == dict(second=2) 

160 

161 dict_second["second_dummy"] = True 

162 assert result == dict(second=2) 

163 

164 

165def test_copy_and_combine_dict_with_both_arguments_valid(): 

166 dict_first = dict(first=1) 

167 dict_second = dict(second=2) 

168 

169 result = copy_and_combine_dicts(dict_first, dict_second) 

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

171 assert dict_first == dict(first=1) 

172 assert dict_second == dict(second=2) 

173 

174 dict_first["first_dummy"] = True 

175 dict_second["second_dummy"] = True 

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

177 

178 

179def test_copy_and_combine_dict_with_both_arguments_valid_and_same_key(): 

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

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

182 

183 result = copy_and_combine_dicts(dict_first, dict_second) 

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

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

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

187 

188 dict_first["first_dummy"] = True 

189 dict_second["second_dummy"] = True 

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

191 

192 

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

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

195class TestVivadoProject(unittest.TestCase): 

196 

197 tmp_path = None 

198 

199 def setUp(self): 

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

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

202 self.ip_cache_path = MagicMock() 

203 self.build_time_generics = dict(enable=True) 

204 self.num_threads = 4 

205 self.run_index = 3 

206 self.synth_only = False 

207 self.from_impl = False 

208 

209 self.mocked_run_vivado_tcl = None 

210 

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

212 with patch( 

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

214 ) as self.mocked_run_vivado_tcl: 

215 return project.create( 

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

217 ) 

218 

219 def test_default_pre_create_hook_should_pass(self): 

220 class CustomVivadoProject(VivadoProject): 

221 

222 pass 

223 

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

225 self._create(project) 

226 self.mocked_run_vivado_tcl.assert_called_once() 

227 

228 def test_project_pre_create_hook_returning_false_should_fail_and_not_call_vivado_run(self): 

229 class CustomVivadoProject(VivadoProject): 

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

231 return False 

232 

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

234 self.mocked_run_vivado_tcl.assert_not_called() 

235 

236 def test_create_should_call_pre_create_with_correct_parameters(self): 

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

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

239 self._create(project, zebra=789) 

240 mocked_pre_create.assert_called_once_with( 

241 project_path=self.project_path, 

242 ip_cache_path=self.ip_cache_path, 

243 part="", 

244 generics=dict(apa=123), 

245 hest=456, 

246 zebra=789, 

247 ) 

248 self.mocked_run_vivado_tcl.assert_called_once() 

249 

250 def _build(self, project): 

251 with patch( 

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

253 ) as self.mocked_run_vivado_tcl, patch( 

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

255 ) as _, patch( 

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

257 ) as _: 

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

259 return project.build( 

260 project_path=self.project_path, 

261 output_path=self.output_path, 

262 run_index=self.run_index, 

263 generics=self.build_time_generics, 

264 synth_only=self.synth_only, 

265 num_threads=self.num_threads, 

266 other_parameter="hest", 

267 ) 

268 

269 def test_build_module_pre_build_hook_and_create_regs_are_called(self): 

270 project = VivadoProject( 

271 name="apa", 

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

273 part="", 

274 apa=123, 

275 ) 

276 build_result = self._build(project) 

277 assert build_result.success 

278 

279 for module in project.modules: 

280 module.pre_build.assert_called_once_with( 

281 project=project, 

282 other_parameter="hest", 

283 apa=123, 

284 project_path=self.project_path, 

285 output_path=self.output_path, 

286 run_index=self.run_index, 

287 generics=self.build_time_generics, 

288 synth_only=self.synth_only, 

289 from_impl=self.from_impl, 

290 num_threads=self.num_threads, 

291 ) 

292 module.create_regs_vhdl_package.assert_called_once() 

293 

294 def test_default_pre_and_post_build_hooks_should_pass(self): 

295 class CustomVivadoProject(VivadoProject): 

296 

297 pass 

298 

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

300 assert build_result.success 

301 self.mocked_run_vivado_tcl.assert_called_once() 

302 

303 def test_project_pre_build_hook_returning_false_should_fail_and_not_call_vivado_run(self): 

304 class CustomVivadoProject(VivadoProject): 

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

306 return False 

307 

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

309 assert not build_result.success 

310 self.mocked_run_vivado_tcl.assert_not_called() 

311 

312 def test_project_post_build_hook_returning_false_should_fail(self): 

313 class CustomVivadoProject(VivadoProject): 

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

315 return False 

316 

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

318 assert not build_result.success 

319 self.mocked_run_vivado_tcl.assert_called_once() 

320 

321 def test_project_build_hooks_should_be_called_with_correct_parameters(self): 

322 project = VivadoProject( 

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

324 ) 

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

326 "tsfpga.vivado.project.VivadoProject.post_build" 

327 ) as mocked_post_build: 

328 self._build(project) 

329 

330 arguments = dict( 

331 project_path=self.project_path, 

332 output_path=self.output_path, 

333 run_index=self.run_index, 

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

335 synth_only=self.synth_only, 

336 from_impl=self.from_impl, 

337 num_threads=self.num_threads, 

338 other_parameter="hest", 

339 apa=123, 

340 ) 

341 mocked_pre_build.assert_called_once_with(**arguments) 

342 

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

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

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

346 mocked_post_build.assert_called_once_with(**arguments) 

347 

348 def test_module_pre_build_hook_returning_false_should_fail_and_not_call_vivado(self): 

349 module = MagicMock(spec=BaseModule) 

350 module.name = "whatever" 

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

352 

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

354 build_result = self._build(project) 

355 assert build_result.success 

356 self.mocked_run_vivado_tcl.assert_called_once() 

357 

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

359 build_result = self._build(project) 

360 assert not build_result.success 

361 self.mocked_run_vivado_tcl.assert_not_called() 

362 

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

364 def test_different_generic_combinations(self, mocked_vivado_tcl): 

365 mocked_vivado_tcl.return_value.build.return_value = "" 

366 

367 # No generics 

368 self.build_time_generics = None 

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

370 assert build_result.success 

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

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

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

374 

375 # Only build time generics 

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

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

378 assert build_result.success 

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

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

381 

382 # Static and build time generics 

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

384 build_result = self._build( 

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

386 ) 

387 assert build_result.success 

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

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

390 

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

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

393 build_result = self._build( 

394 VivadoProject( 

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

396 ) 

397 ) 

398 assert build_result.success 

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

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

401 

402 # Only static generics 

403 self.build_time_generics = None 

404 build_result = self._build( 

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

406 ) 

407 assert build_result.success 

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

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

410 

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

412 def test_build_time_generics_are_copied(self, mocked_vivado_tcl): 

413 mocked_vivado_tcl.return_value.build.return_value = "" 

414 

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

416 build_result = self._build( 

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

418 ) 

419 assert build_result.success 

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

421 

422 def test_modules_are_deep_copied_before_pre_create_hook(self): 

423 class CustomVivadoProject(VivadoProject): 

424 def pre_create(self, **kwargs): 

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

426 return True 

427 

428 module = MagicMock(spec=BaseModule) 

429 module.registers = "Some value" 

430 

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

432 assert self._create(project) 

433 

434 assert module.registers == "Some value" 

435 

436 def test_modules_are_deep_copied_before_pre_build_hook(self): 

437 class CustomVivadoProject(VivadoProject): 

438 def pre_build(self, **kwargs): 

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

440 return True 

441 

442 module = MagicMock(spec=BaseModule) 

443 module.registers = "Some value" 

444 

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

446 assert self._build(project).success 

447 

448 assert module.registers == "Some value"