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

163 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-17 20:51 +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://github.com/tsfpga/tsfpga 

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

8 

9from collections import OrderedDict 

10from pathlib import Path 

11from unittest.mock import MagicMock 

12 

13import pytest 

14 

15from tsfpga.build_step_tcl_hook import BuildStepTclHook 

16from tsfpga.constraint import Constraint 

17from tsfpga.ip_core_file import IpCoreFile 

18from tsfpga.module import BaseModule, get_modules 

19from tsfpga.system_utils import create_file 

20from tsfpga.vivado.common import to_tcl_path 

21from tsfpga.vivado.generics import BitVectorGenericValue, StringGenericValue 

22from tsfpga.vivado.tcl import VivadoTcl 

23 

24 

25def test_set_create_run_index(): 

26 tcl = VivadoTcl(name="").create(project_folder=Path(), modules=[], part="", top="", run_index=2) 

27 assert '\ncurrent_run [get_runs "synth_2"]\n' in tcl 

28 

29 

30def test_static_generics(): 

31 # Use OrderedDict here in test so that order will be preserved and we can test for equality. 

32 # In real world case a normal dict can be used. 

33 generics = OrderedDict( 

34 enable=True, 

35 disable=False, 

36 integer=123, 

37 slv=BitVectorGenericValue("0101"), 

38 string=StringGenericValue("apa"), 

39 ) 

40 

41 tcl = VivadoTcl(name="").create( 

42 project_folder=Path(), modules=[], part="", top="", run_index=1, generics=generics 

43 ) 

44 expected = ( 

45 '\nset_property "generic" ' 

46 "{enable=1'b1 disable=1'b0 integer=123 slv=4'b0101 string=\"apa\"} " 

47 "[current_fileset]\n" 

48 ) 

49 assert expected in tcl 

50 

51 

52def test_build_step_hooks(tmp_path): 

53 project_folder = tmp_path / "dummy_project_folder" 

54 

55 dummy1 = BuildStepTclHook(Path("dummy.tcl"), "STEPS.SYNTH_DESIGN.TCL.PRE") 

56 dummy2 = BuildStepTclHook(Path("files.tcl"), "STEPS.ROUTE_DESIGN.TCL.PRE") 

57 

58 tcl = VivadoTcl(name="").create( 

59 project_folder=Path(), 

60 modules=[], 

61 part="part", 

62 top="", 

63 run_index=1, 

64 build_step_hooks={ 

65 dummy1.hook_step: (project_folder / "synth.tcl", [dummy1]), 

66 dummy2.hook_step: (project_folder / "impl.tcl", [dummy2]), 

67 }, 

68 ) 

69 

70 assert ( 

71 'foreach run [get_runs "synth_*"] {\n set_property "STEPS.SYNTH_DESIGN.TCL.PRE" {' 

72 + to_tcl_path(project_folder / "synth.tcl") 

73 + "} ${run}\n" 

74 ) in tcl 

75 assert ( 

76 'foreach run [get_runs "impl_*"] {\n set_property "STEPS.ROUTE_DESIGN.TCL.PRE" {' 

77 + to_tcl_path(project_folder / "impl.tcl") 

78 + "} ${run}\n" 

79 ) in tcl 

80 

81 

82def test_ip_cache_location(tmp_path): 

83 tcl = VivadoTcl(name="").create( 

84 project_folder=Path(), modules=[], part="part", top="", run_index=1 

85 ) 

86 assert "config_ip_cache" not in tcl 

87 

88 tcl = VivadoTcl(name="").create( 

89 project_folder=Path(), modules=[], part="part", top="", run_index=1, ip_cache_path=tmp_path 

90 ) 

91 assert f"\nconfig_ip_cache -use_cache_location {{{to_tcl_path(tmp_path)}}}\n" in tcl 

92 

93 

94def test_multiple_threads_is_capped_by_vivado_limits(): 

95 num_threads = 128 

96 tcl = VivadoTcl(name="").build( 

97 project_file=Path(), output_path=Path(), num_threads=num_threads, run_index=1 

98 ) 

99 assert 'set_param "general.maxThreads" 32' in tcl 

100 assert 'set_param "synth.maxThreads" 8' in tcl 

101 assert "launch_runs ${run} -jobs 128" in tcl 

102 assert "launch_runs ${run} -jobs 128" in tcl 

103 

104 

105def test_set_build_run_index(): 

106 tcl = VivadoTcl(name="").build( 

107 project_file=Path(), output_path=Path(), num_threads=0, run_index=1 

108 ) 

109 assert "impl_1" in tcl 

110 assert "synth_1" in tcl 

111 assert "impl_2" not in tcl 

112 assert "synth_2" not in tcl 

113 

114 tcl = VivadoTcl(name="").build( 

115 project_file=Path(), output_path=Path(), num_threads=0, run_index=2 

116 ) 

117 assert "impl_2" in tcl 

118 assert "synth_2" in tcl 

119 assert "impl_1" not in tcl 

120 assert "synth_1" not in tcl 

121 

122 

123def test_runtime_generics(): 

124 tcl = VivadoTcl(name="").build( 

125 project_file=Path(), 

126 output_path=Path(), 

127 num_threads=0, 

128 run_index=0, 

129 generics={"dummy": True}, 

130 ) 

131 expected = '\nset_property "generic" {dummy=1\'b1} [current_fileset]\n' 

132 assert expected in tcl 

133 

134 

135def test_build_with_synth_only(): 

136 tcl = VivadoTcl(name="").build( 

137 project_file=Path(), output_path=Path(), num_threads=0, run_index=0, synth_only=False 

138 ) 

139 assert "synth_" in tcl 

140 assert "impl_" in tcl 

141 

142 tcl = VivadoTcl(name="").build( 

143 project_file=Path(), output_path=Path(), num_threads=0, run_index=0, synth_only=True 

144 ) 

145 assert "synth_" in tcl 

146 assert "impl_" not in tcl 

147 

148 

149def test_build_with_from_impl(): 

150 tcl = VivadoTcl(name="").build( 

151 project_file=Path(), output_path=Path(), num_threads=0, run_index=0, from_impl=False 

152 ) 

153 assert "synth_" in tcl 

154 assert "impl_" in tcl 

155 

156 tcl = VivadoTcl(name="").build( 

157 project_file=Path(), output_path=Path(), num_threads=0, run_index=0, from_impl=True 

158 ) 

159 assert "synth_" not in tcl 

160 assert "impl_" in tcl 

161 

162 

163def test_module_getters_are_called_with_correct_arguments(): 

164 modules = [MagicMock(spec=BaseModule)] 

165 VivadoTcl(name="").create( 

166 project_folder=Path(), 

167 modules=modules, 

168 part="", 

169 top="", 

170 run_index=1, 

171 other_arguments={"apa": 123, "hest": 456}, 

172 ) 

173 

174 modules[0].get_synthesis_files.assert_called_once_with(apa=123, hest=456) 

175 modules[0].get_scoped_constraints.assert_called_once_with(apa=123, hest=456) 

176 modules[0].get_ip_core_files.assert_called_once_with(apa=123, hest=456) 

177 

178 

179@pytest.fixture 

180def vivado_tcl_test(tmp_path): 

181 class VivadoTclTest: 

182 def __init__(self): 

183 self.modules_folder = tmp_path / "modules" 

184 

185 # A library with some synth files and some test files 

186 self.a_vhd = to_tcl_path(create_file(self.modules_folder / "apa" / "a.vhd")) 

187 self.b_vhd = to_tcl_path(create_file(self.modules_folder / "apa" / "b.vhd")) 

188 self.tb_a_vhd = to_tcl_path( 

189 create_file(self.modules_folder / "apa" / "test" / "tb_a.vhd") 

190 ) 

191 self.a_xdc = to_tcl_path( 

192 create_file(self.modules_folder / "apa" / "scoped_constraints" / "a.xdc") 

193 ) 

194 

195 self.c_v = to_tcl_path(create_file(self.modules_folder / "apa" / "c.v")) 

196 self.b_tcl = to_tcl_path( 

197 create_file(self.modules_folder / "apa" / "scoped_constraints" / "b.tcl") 

198 ) 

199 

200 self.c_tcl = to_tcl_path( 

201 create_file(self.modules_folder / "apa" / "ip_cores" / "c.tcl") 

202 ) 

203 

204 # A library with only test files 

205 self.d_vhd = to_tcl_path(create_file(self.modules_folder / "zebra" / "test" / "d.vhd")) 

206 

207 self.modules = get_modules(self.modules_folder) 

208 

209 self.tcl = VivadoTcl(name="name") 

210 

211 return VivadoTclTest() 

212 

213 

214def test_source_file_list_is_correctly_formatted(vivado_tcl_test): 

215 tcl = vivado_tcl_test.tcl.create( 

216 project_folder=Path(), modules=vivado_tcl_test.modules, part="", top="", run_index=1 

217 ) 

218 

219 # Order of files is not really deterministic 

220 expected_1 = ( 

221 '\nread_vhdl -library "apa" -vhdl2008 ' 

222 f"{{{{{vivado_tcl_test.b_vhd}}} {{{vivado_tcl_test.a_vhd}}}}}\n" 

223 ) 

224 expected_2 = ( 

225 '\nread_vhdl -library "apa" -vhdl2008 ' 

226 f"{{{{{vivado_tcl_test.a_vhd}}} {{{vivado_tcl_test.b_vhd}}}}}\n" 

227 ) 

228 assert expected_1 in tcl or expected_2 in tcl 

229 

230 expected = f"\nread_verilog {{{vivado_tcl_test.c_v}}}\n" 

231 assert expected in tcl 

232 

233 

234def test_only_synthesis_files_added_to_create_project_tcl(vivado_tcl_test): 

235 tcl = vivado_tcl_test.tcl.create( 

236 project_folder=Path(), modules=vivado_tcl_test.modules, part="", top="", run_index=1 

237 ) 

238 assert vivado_tcl_test.a_vhd in tcl 

239 assert vivado_tcl_test.c_v in tcl 

240 assert vivado_tcl_test.tb_a_vhd not in tcl 

241 assert "tb_a.vhd" not in tcl 

242 

243 

244def test_constraints(vivado_tcl_test): 

245 constraint_folder = vivado_tcl_test.modules[0].path.parent.resolve() / "z" / "tcl" 

246 

247 tcl = vivado_tcl_test.tcl.create( 

248 project_folder=Path(), 

249 modules=vivado_tcl_test.modules, 

250 part="part", 

251 top="", 

252 run_index=1, 

253 constraints=[ 

254 Constraint(constraint_folder / "x.xdc"), 

255 Constraint(constraint_folder / "y.tcl", used_in_synthesis=False), 

256 Constraint( 

257 constraint_folder / "z.xdc", used_in_implementation=False, processing_order="early" 

258 ), 

259 ], 

260 ) 

261 

262 # Scoped constraints from the modules. 

263 assert f'\nread_xdc -ref "a" {{{vivado_tcl_test.a_xdc}}}\n' in tcl 

264 assert f'\nread_xdc -ref "b" -unmanaged {{{vivado_tcl_test.b_tcl}}}\n' in tcl 

265 

266 # Regular constraints 

267 constraint_file = to_tcl_path(constraint_folder / "x.xdc") 

268 assert f"\nread_xdc {{{constraint_file}}}\n" in tcl 

269 assert f'"PROCESSING_ORDER" "NORMAL" [get_files {{{constraint_file}}}' in tcl 

270 assert f"false [get_files {{{constraint_file}}}" not in tcl 

271 

272 constraint_file = to_tcl_path(constraint_folder / "y.tcl") 

273 assert f"\nread_xdc -unmanaged {{{constraint_file}}}\n" in tcl 

274 assert f'"PROCESSING_ORDER" "NORMAL" [get_files {{{constraint_file}}}' in tcl 

275 assert f'"USED_IN_SYNTHESIS" false [get_files {{{constraint_file}}}' in tcl 

276 assert f'"USED_IN_IMPLEMENTATION" false [get_files {{{constraint_file}}}' not in tcl 

277 

278 constraint_file = to_tcl_path(constraint_folder / "z.xdc") 

279 assert f"\nread_xdc {{{constraint_file}}}\n" in tcl 

280 assert f'"PROCESSING_ORDER" "EARLY" [get_files {{{constraint_file}}}' in tcl 

281 assert f'"USED_IN_SYNTHESIS" false [get_files {{{constraint_file}}}' not in tcl 

282 assert f'"USED_IN_IMPLEMENTATION" false [get_files {{{constraint_file}}}' in tcl 

283 

284 

285def test_ip_core_files(vivado_tcl_test): 

286 ip_core_file_path = vivado_tcl_test.modules_folder.parent / "my_name.tcl" 

287 module = MagicMock(spec=BaseModule) 

288 module.get_ip_core_files.return_value = [ 

289 IpCoreFile(path=ip_core_file_path, apa="hest", zebra=123) 

290 ] 

291 

292 vivado_tcl_test.modules.append(module) 

293 

294 tcl = vivado_tcl_test.tcl.create( 

295 project_folder=Path(), modules=vivado_tcl_test.modules, part="part", top="", run_index=1 

296 ) 

297 

298 assert ( 

299 f""" 

300proc create_ip_core_c {{}} {{ 

301 source -notrace {{{vivado_tcl_test.c_tcl}}} 

302}} 

303create_ip_core_c 

304""" 

305 in tcl 

306 ) 

307 

308 assert ( 

309 f""" 

310proc create_ip_core_my_name {{}} {{ 

311 set apa "hest" 

312 set zebra "123" 

313 source -notrace {{{to_tcl_path(ip_core_file_path)}}} 

314}} 

315create_ip_core_my_name 

316""" 

317 in tcl 

318 ) 

319 

320 

321def test_create_with_ip_cores_only(vivado_tcl_test): 

322 tcl = vivado_tcl_test.tcl.create( 

323 project_folder=Path(), 

324 modules=vivado_tcl_test.modules, 

325 part="part", 

326 top="", 

327 run_index=1, 

328 ip_cores_only=True, 

329 ) 

330 assert vivado_tcl_test.c_tcl in tcl 

331 assert vivado_tcl_test.a_vhd not in tcl 

332 

333 

334def test_empty_library_not_in_create_project_tcl(vivado_tcl_test): 

335 tcl = vivado_tcl_test.tcl.create( 

336 project_folder=Path(), modules=vivado_tcl_test.modules, part="part", top="", run_index=1 

337 ) 

338 assert "zebra" not in tcl 

339 

340 

341def test_multiple_tcl_sources(vivado_tcl_test): 

342 extra_tcl_sources = [Path("dummy.tcl"), Path("files.tcl")] 

343 tcl = vivado_tcl_test.tcl.create( 

344 project_folder=Path(), 

345 modules=vivado_tcl_test.modules, 

346 part="part", 

347 top="", 

348 run_index=1, 

349 tcl_sources=extra_tcl_sources, 

350 ) 

351 

352 for filename in extra_tcl_sources: 

353 assert f"\nsource -notrace {{{to_tcl_path(filename)}}}\n" in tcl 

354 

355 

356def test_io_buffer_setting(vivado_tcl_test): 

357 tcl = vivado_tcl_test.tcl.create( 

358 project_folder=Path(), 

359 modules=vivado_tcl_test.modules, 

360 part="part", 

361 top="", 

362 run_index=1, 

363 disable_io_buffers=True, 

364 ) 

365 

366 no_io_buffers_tcl = ( 

367 '\nset_property -name "STEPS.SYNTH_DESIGN.ARGS.MORE OPTIONS" ' 

368 '-value "-no_iobuf" -objects [get_runs "synth_1"]\n' 

369 ) 

370 assert no_io_buffers_tcl in tcl 

371 

372 tcl = vivado_tcl_test.tcl.create( 

373 project_folder=Path(), 

374 modules=vivado_tcl_test.modules, 

375 part="part", 

376 top="", 

377 run_index=1, 

378 disable_io_buffers=False, 

379 ) 

380 

381 assert no_io_buffers_tcl not in tcl 

382 

383 

384def test_analyze_synthesis_settings_on_and_off(vivado_tcl_test): 

385 tcl = vivado_tcl_test.tcl.build( 

386 project_file=Path(), 

387 output_path=Path(), 

388 num_threads=1, 

389 run_index=1, 

390 open_and_analyze_synthesized_design=True, 

391 ) 

392 assert "open_run" in tcl 

393 assert "report_clock_interaction" in tcl 

394 

395 tcl = vivado_tcl_test.tcl.build( 

396 project_file=Path(), 

397 output_path=Path(), 

398 num_threads=1, 

399 run_index=1, 

400 open_and_analyze_synthesized_design=False, 

401 ) 

402 # When disabled, the run should not even be opened, which saves time 

403 assert "open_run" not in tcl 

404 assert "report_clock_interaction" not in tcl 

405 

406 

407def test_impl_explore(vivado_tcl_test): 

408 num_runs = 4 

409 

410 tcl = vivado_tcl_test.tcl.build( 

411 project_file=Path(), 

412 output_path=Path(), 

413 num_threads=num_runs, 

414 run_index=1, 

415 impl_explore=True, 

416 ) 

417 

418 assert ( 

419 f'launch_runs -jobs {num_runs} [get_runs "impl_explore_*"] -to_step "write_bitstream"' 

420 in tcl 

421 ) 

422 assert ( 

423 'wait_on_runs -quiet -exit_condition ANY_ONE_MET_TIMING [get_runs "impl_explore_*"]' in tcl 

424 ) 

425 assert 'reset_runs [get_runs -filter {STATUS == "Queued..."}]' in tcl 

426 assert ( 

427 'wait_on_runs -quiet [get_runs -filter {STATUS != "Not started"} "impl_explore_*"]' in tcl 

428 ) 

429 assert 'foreach run [get_runs -filter {PROGRESS == "100%"} "impl_explore_*"]' in tcl