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

171 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-01 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.test.test_utils import file_contains_string 

21from tsfpga.vivado.common import to_tcl_path 

22from tsfpga.vivado.generics import BitVectorGenericValue, StringGenericValue 

23from tsfpga.vivado.tcl import VivadoTcl 

24 

25 

26def test_set_create_run_index(): 

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

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

29 

30 

31def test_static_generics(): 

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

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

34 generics = OrderedDict( 

35 enable=True, 

36 disable=False, 

37 integer=123, 

38 slv=BitVectorGenericValue("0101"), 

39 string=StringGenericValue("apa"), 

40 ) 

41 

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

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

44 ) 

45 expected = ( 

46 '\nset_property "generic" ' 

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

48 "[current_fileset]\n" 

49 ) 

50 assert expected in tcl 

51 

52 

53def test_build_step_hooks(): 

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

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

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

57 project_folder=Path(), 

58 modules=[], 

59 part="part", 

60 top="", 

61 run_index=1, 

62 build_step_hooks=[dummy, files], 

63 ) 

64 

65 assert ( 

66 f"\n " 

67 f'set_property "STEPS.SYNTH_DESIGN.TCL.PRE" { {to_tcl_path(dummy.tcl_file)}} ${ run} \n' 

68 ) in tcl 

69 assert ( 

70 f"\n " 

71 f'set_property "STEPS.ROUTE_DESIGN.TCL.PRE" { {to_tcl_path(files.tcl_file)}} ${ run} \n' 

72 ) in tcl 

73 

74 

75def test_build_step_hooks_with_same_hook_step(tmp_path): 

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

77 files = BuildStepTclHook(Path("files.tcl"), "STEPS.SYNTH_DESIGN.TCL.PRE") 

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

79 project_folder=tmp_path / "dummy_project_folder", 

80 modules=[], 

81 part="part", 

82 top="", 

83 run_index=1, 

84 build_step_hooks=[dummy, files], 

85 ) 

86 

87 hook_file = tmp_path / "dummy_project_folder" / "hook_STEPS_SYNTH_DESIGN_TCL_PRE.tcl" 

88 

89 assert file_contains_string(hook_file, f"source { {to_tcl_path(dummy.tcl_file)}} ") 

90 assert file_contains_string(hook_file, f"source { {to_tcl_path(files.tcl_file)}} ") 

91 

92 assert ( 

93 f'\n set_property "STEPS.SYNTH_DESIGN.TCL.PRE" { {to_tcl_path(hook_file)}} ${ run} \n' 

94 in tcl 

95 ) 

96 

97 

98def test_ip_cache_location(tmp_path): 

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

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

101 ) 

102 assert "config_ip_cache" not in tcl 

103 

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

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

106 ) 

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

108 

109 

110def test_multiple_threads_is_capped_by_vivado_limits(): 

111 num_threads = 128 

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

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

114 ) 

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

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

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

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

119 

120 

121def test_set_build_run_index(): 

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

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

124 ) 

125 assert "impl_1" in tcl 

126 assert "synth_1" in tcl 

127 assert "impl_2" not in tcl 

128 assert "synth_2" not in tcl 

129 

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

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

132 ) 

133 assert "impl_2" in tcl 

134 assert "synth_2" in tcl 

135 assert "impl_1" not in tcl 

136 assert "synth_1" not in tcl 

137 

138 

139def test_runtime_generics(): 

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

141 project_file=Path(), 

142 output_path=Path(), 

143 num_threads=0, 

144 run_index=0, 

145 generics={"dummy": True}, 

146 ) 

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

148 assert expected in tcl 

149 

150 

151def test_build_with_synth_only(): 

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

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

154 ) 

155 assert "synth_" in tcl 

156 assert "impl_" in tcl 

157 

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

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

160 ) 

161 assert "synth_" in tcl 

162 assert "impl_" not in tcl 

163 

164 

165def test_build_with_from_impl(): 

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

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

168 ) 

169 assert "synth_" in tcl 

170 assert "impl_" in tcl 

171 

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

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

174 ) 

175 assert "synth_" not in tcl 

176 assert "impl_" in tcl 

177 

178 

179def test_module_getters_are_called_with_correct_arguments(): 

180 modules = [MagicMock(spec=BaseModule)] 

181 VivadoTcl(name="").create( 

182 project_folder=Path(), 

183 modules=modules, 

184 part="", 

185 top="", 

186 run_index=1, 

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

188 ) 

189 

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

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

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

193 

194 

195@pytest.fixture 

196def vivado_tcl_test(tmp_path): 

197 class VivadoTclTest: 

198 def __init__(self): 

199 self.modules_folder = tmp_path / "modules" 

200 

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

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

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

204 self.tb_a_vhd = to_tcl_path( 

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

206 ) 

207 self.a_xdc = to_tcl_path( 

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

209 ) 

210 

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

212 self.b_tcl = to_tcl_path( 

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

214 ) 

215 

216 self.c_tcl = to_tcl_path( 

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

218 ) 

219 

220 # A library with only test files 

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

222 

223 self.modules = get_modules(self.modules_folder) 

224 

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

226 

227 return VivadoTclTest() 

228 

229 

230def test_source_file_list_is_correctly_formatted(vivado_tcl_test): 

231 tcl = vivado_tcl_test.tcl.create( 

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

233 ) 

234 

235 # Order of files is not really deterministic 

236 expected_1 = ( 

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

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

239 ) 

240 expected_2 = ( 

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

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

243 ) 

244 assert expected_1 in tcl or expected_2 in tcl 

245 

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

247 assert expected in tcl 

248 

249 

250def test_only_synthesis_files_added_to_create_project_tcl(vivado_tcl_test): 

251 tcl = vivado_tcl_test.tcl.create( 

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

253 ) 

254 assert vivado_tcl_test.a_vhd in tcl 

255 assert vivado_tcl_test.c_v in tcl 

256 assert vivado_tcl_test.tb_a_vhd not in tcl 

257 assert "tb_a.vhd" not in tcl 

258 

259 

260def test_constraints(vivado_tcl_test): 

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

262 

263 tcl = vivado_tcl_test.tcl.create( 

264 project_folder=Path(), 

265 modules=vivado_tcl_test.modules, 

266 part="part", 

267 top="", 

268 run_index=1, 

269 constraints=[ 

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

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

272 Constraint( 

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

274 ), 

275 ], 

276 ) 

277 

278 # Scoped constraints from the modules. 

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

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

281 

282 # Regular constraints 

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

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

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

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

287 

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

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

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

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

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

293 

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

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

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

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

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

299 

300 

301def test_ip_core_files(vivado_tcl_test): 

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

303 module = MagicMock(spec=BaseModule) 

304 module.get_ip_core_files.return_value = [ 

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

306 ] 

307 

308 vivado_tcl_test.modules.append(module) 

309 

310 tcl = vivado_tcl_test.tcl.create( 

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

312 ) 

313 

314 assert ( 

315 f""" 

316proc create_ip_core_c { } { 

317 source -notrace { {vivado_tcl_test.c_tcl}} 

318} 

319create_ip_core_c 

320""" 

321 in tcl 

322 ) 

323 

324 assert ( 

325 f""" 

326proc create_ip_core_my_name { } { 

327 set apa "hest" 

328 set zebra "123" 

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

330} 

331create_ip_core_my_name 

332""" 

333 in tcl 

334 ) 

335 

336 

337def test_create_with_ip_cores_only(vivado_tcl_test): 

338 tcl = vivado_tcl_test.tcl.create( 

339 project_folder=Path(), 

340 modules=vivado_tcl_test.modules, 

341 part="part", 

342 top="", 

343 run_index=1, 

344 ip_cores_only=True, 

345 ) 

346 assert vivado_tcl_test.c_tcl in tcl 

347 assert vivado_tcl_test.a_vhd not in tcl 

348 

349 

350def test_empty_library_not_in_create_project_tcl(vivado_tcl_test): 

351 tcl = vivado_tcl_test.tcl.create( 

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

353 ) 

354 assert "zebra" not in tcl 

355 

356 

357def test_multiple_tcl_sources(vivado_tcl_test): 

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

359 tcl = vivado_tcl_test.tcl.create( 

360 project_folder=Path(), 

361 modules=vivado_tcl_test.modules, 

362 part="part", 

363 top="", 

364 run_index=1, 

365 tcl_sources=extra_tcl_sources, 

366 ) 

367 

368 for filename in extra_tcl_sources: 

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

370 

371 

372def test_io_buffer_setting(vivado_tcl_test): 

373 tcl = vivado_tcl_test.tcl.create( 

374 project_folder=Path(), 

375 modules=vivado_tcl_test.modules, 

376 part="part", 

377 top="", 

378 run_index=1, 

379 disable_io_buffers=True, 

380 ) 

381 

382 no_io_buffers_tcl = ( 

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

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

385 ) 

386 assert no_io_buffers_tcl in tcl 

387 

388 tcl = vivado_tcl_test.tcl.create( 

389 project_folder=Path(), 

390 modules=vivado_tcl_test.modules, 

391 part="part", 

392 top="", 

393 run_index=1, 

394 disable_io_buffers=False, 

395 ) 

396 

397 assert no_io_buffers_tcl not in tcl 

398 

399 

400def test_analyze_synthesis_settings_on_and_off(vivado_tcl_test): 

401 tcl = vivado_tcl_test.tcl.build( 

402 project_file=Path(), 

403 output_path=Path(), 

404 num_threads=1, 

405 run_index=1, 

406 analyze_synthesis_timing=True, 

407 ) 

408 assert "open_run" in tcl 

409 assert "report_clock_interaction" in tcl 

410 

411 tcl = vivado_tcl_test.tcl.build( 

412 project_file=Path(), 

413 output_path=Path(), 

414 num_threads=1, 

415 run_index=1, 

416 analyze_synthesis_timing=False, 

417 ) 

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

419 assert "open_run" not in tcl 

420 assert "report_clock_interaction" not in tcl 

421 

422 

423def test_impl_explore(vivado_tcl_test): 

424 num_runs = 4 

425 

426 tcl = vivado_tcl_test.tcl.build( 

427 project_file=Path(), 

428 output_path=Path(), 

429 num_threads=num_runs, 

430 run_index=1, 

431 impl_explore=True, 

432 ) 

433 

434 assert ( 

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

436 in tcl 

437 ) 

438 assert ( 

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

440 ) 

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

442 assert ( 

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

444 ) 

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