Coverage for tests/unit_tests/scons/test_plugin_util.py: 100%

131 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-23 03:53 +0000

1""" 

2Tests of the scons plugin_util.py functions. 

3""" 

4 

5import re 

6import os 

7from os.path import isfile, exists, join 

8from pathlib import Path 

9import pytest 

10from SCons.Node.FS import FS 

11from SCons.Action import FunctionAction 

12from tests.unit_tests.scons.testing import make_test_apio_env 

13from tests.conftest import ApioRunner 

14from apio.common import apio_console 

15from apio.common.proto.apio_scons_pb2 import ( 

16 TargetParams, 

17 UploadParams, 

18 LintParams, 

19 ApioEnvParams, 

20) 

21from apio.scons.plugin_util import ( 

22 get_constraint_file, 

23 verilog_src_scanner, 

24 get_programmer_cmd, 

25 map_str_params, 

26 map_path_params, 

27 make_verilator_config_builder, 

28 verilator_lint_action, 

29 iverilog_action, 

30) 

31 

32 

33def test_get_constraint_file(apio_runner: ApioRunner): 

34 """Test the get_constraint_file() method.""" 

35 

36 with apio_runner.in_sandbox() as sb: 

37 

38 apio_env = make_test_apio_env() 

39 

40 # -- If not .pcf files, should print an error and exit. 

41 with apio_runner.with_logger() as log: 

42 with pytest.raises(SystemExit) as e: 

43 result = get_constraint_file(apio_env, ".pcf") 

44 assert e.value.code == 1 

45 assert "No constraint file '*.pcf' found" in log.out 

46 

47 # -- If a single .pcf file, return it. Constraint file can also be 

48 # -- in subdirectories as we test here 

49 file1 = os.path.join("lib", "pinout.pcf") 

50 sb.write_file(file1, "content1") 

51 with apio_runner.with_logger() as log: 

52 result = get_constraint_file(apio_env, ".pcf") 

53 assert log.out == "" 

54 assert result == file1 

55 

56 # -- If there is more than one, exit with an error message. 

57 file2 = "other.pcf" 

58 sb.write_file(file2, "content2") 

59 with apio_runner.with_logger() as log: 

60 with pytest.raises(SystemExit) as e: 

61 result = get_constraint_file(apio_env, ".pcf") 

62 assert e.value.code == 1 

63 assert "Error: Found 2 constraint files '*.pcf'" in log.out 

64 

65 # -- If the user specified a valid file then return it, regardless 

66 # -- if it exists or not. 

67 apio_env.params.apio_env_params.constraint_file = "xyz.pcf" 

68 with apio_runner.with_logger() as log: 

69 result = get_constraint_file(apio_env, ".pcf") 

70 assert log.out == "" 

71 assert result == "xyz.pcf" 

72 

73 # -- File extension should match the architecture. 

74 apio_env.params.apio_env_params.constraint_file = "xyz.bad" 

75 with apio_runner.with_logger() as log: 

76 with pytest.raises(SystemExit) as e: 

77 result = get_constraint_file(apio_env, ".pcf") 

78 assert e.value.code == 1 

79 assert ( 

80 "Constraint file should have the extension '.pcf': xyz.bad" 

81 in log.out 

82 ) 

83 

84 # -- Path under _build is not allowed. 

85 apio_env.params.apio_env_params.constraint_file = "_build/xyz.pcf" 

86 with apio_runner.with_logger() as log: 

87 with pytest.raises(SystemExit) as e: 

88 result = get_constraint_file(apio_env, ".pcf") 

89 assert e.value.code == 1 

90 assert ( 

91 "Error: Constraint file should not be under _build: _build/xyz.pcf" 

92 in log.out 

93 ) 

94 

95 # -- Path should not contain '../ 

96 apio_env.params.apio_env_params.constraint_file = "a/../xyz.pcf" 

97 with apio_runner.with_logger() as log: 

98 with pytest.raises(SystemExit) as e: 

99 result = get_constraint_file(apio_env, ".pcf") 

100 assert e.value.code == 1 

101 assert ( 

102 "Error: Constraint file path should not contain '..': a/../xyz.pcf" 

103 in log.out 

104 ) 

105 

106 

107def test_verilog_src_scanner(apio_runner: ApioRunner): 

108 """Test the verilog scanner which scans a verilog file and extract 

109 reference of files it uses. 

110 """ 

111 

112 # -- Test file content with references. Contains duplicates and 

113 # -- references out of alphabetical order. 

114 file_content = """ 

115 // Dummy file for testing. 

116 

117 // Icestudio reference. 

118 parameter v771499 = "v771499.list" 

119 

120 // System verilog include reference. 

121 `include "apio_testing.vh" 

122 

123 // Duplicate icestudio reference. 

124 parameter v771499 = "v771499.list" 

125 

126 // Verilog include reference. 

127 `include "apio_testing.v 

128 

129 // $readmemh() function reference. 

130 $readmemh("subdir2/my_data.hex", State_buff); 

131 """ 

132 

133 with apio_runner.in_sandbox() as sb: 

134 

135 # -- Write a test file name in the current directory. 

136 sb.write_file("subdir1/test_file.v", file_content) 

137 

138 # -- Create a scanner 

139 apio_env = make_test_apio_env() 

140 scanner = verilog_src_scanner(apio_env) 

141 

142 # -- Run the scanner. It returns a list of File. 

143 file = FS.File(FS(), "subdir1/test_file.v") 

144 dependency_files = scanner.function(file, apio_env, None) 

145 

146 # -- Files list should be empty since none of the dependency candidate 

147 # has a file. 

148 file_names = [f.name for f in dependency_files] 

149 assert file_names == [] 

150 

151 # -- Create file lists 

152 core_dependencies = [ 

153 "apio.ini", 

154 "boards.jsonc", 

155 "programmers.jsonc", 

156 "fpgas.jsonc", 

157 ] 

158 

159 file_dependencies = [ 

160 "apio_testing.vh", 

161 join("subdir2", "my_data.hex"), 

162 join("subdir1", "v771499.list"), 

163 ] 

164 

165 # -- Create dummy files. This should cause the dependencies to be 

166 # -- reported. (Candidate dependencies with no matching file are 

167 # -- filtered out) 

168 for f in core_dependencies + file_dependencies + ["non-related.txt"]: 

169 sb.write_file(f, "dummy-file") 

170 

171 # -- Run the scanner again 

172 dependency_files = scanner.function(file, apio_env, None) 

173 

174 # -- Check the dependencies 

175 file_names = [f.path for f in dependency_files] 

176 assert file_names == sorted(core_dependencies + file_dependencies) 

177 

178 

179def test_get_programmer_cmd(): 

180 """Tests the function programmer_cmd().""" 

181 

182 apio_console.configure() 

183 

184 # -- Test a valid programmer command. 

185 apio_env = make_test_apio_env( 

186 targets=["upload"], 

187 target_params=TargetParams( 

188 upload=UploadParams(programmer_cmd="my_prog aa $SOURCE bb") 

189 ), 

190 ) 

191 assert get_programmer_cmd(apio_env) == "my_prog aa $SOURCE bb" 

192 

193 

194def test_map_str_params(): 

195 """Test the map_str_params() function.""" 

196 

197 # -- Empty cases 

198 assert map_str_params([], "x_{}_y") == "" 

199 assert map_str_params(["", " "], "x_{}_y") == "" 

200 

201 # -- Non empty cases 

202 assert map_str_params(["a"], "x_{}_y") == "x_a_y" 

203 assert map_str_params([" a "], "x_{}_y") == "x_a_y" 

204 assert map_str_params(["a", "a", "b"], "x_{}_y") == "x_a_y x_a_y x_b_y" 

205 

206 

207def test_map_path_params(): 

208 """Test the map_path_params() function.""" 

209 

210 assert map_path_params([], "x_{}_y") == "" 

211 

212 assert ( 

213 map_path_params([Path("aa/bb"), Path(".")], "x/_{}_/y") 

214 == "x/_aa" + os.sep + "bb_/y" + " " + "x/_._/y" 

215 ) 

216 

217 

218def test_make_verilator_config_builder(apio_runner: ApioRunner): 

219 """Tests the make_verilator_config_builder() function.""" 

220 

221 with apio_runner.in_sandbox() as sb: 

222 

223 # -- Create a test scons env. 

224 apio_env = make_test_apio_env() 

225 

226 # -- Call the tested method to create a builder. 

227 builder = make_verilator_config_builder( 

228 sb.packages_dir, 

229 rules_to_suppress=[ 

230 "SPECIFYIGN", 

231 ], 

232 ) 

233 

234 # -- Verify builder suffixes. 

235 assert builder.src_suffix == [] 

236 assert builder.suffix == ".vlt" 

237 

238 # -- Create a target that doesn't exist yet. 

239 assert not exists("hardware.vlt") 

240 target = FS.File(FS(), "hardware.vlt") 

241 

242 # -- Invoke the builder's action to create the target. 

243 builder.action(target, [], apio_env.scons_env) 

244 assert isfile("hardware.vlt") 

245 

246 # -- Verify that the file was created with the given text. 

247 text = sb.read_file_text("hardware.vlt") 

248 assert "verilator_config" in text, text 

249 assert "lint_off -rule SPECIFYIGN" in text, text 

250 

251 

252def test_iverilog_action_has_no_vcd_output_macro(apio_runner: ApioRunner): 

253 """Tests iverilog_action() does not define the retired VCD_OUTPUT macro.""" 

254 

255 with apio_runner.in_sandbox(): 

256 

257 apio_env = make_test_apio_env() 

258 action = iverilog_action(apio_env, verbose=False, is_interactive=False) 

259 

260 normalized_cmd = re.sub(r"\s+", " ", action) 

261 

262 assert "VCD_OUTPUT" not in normalized_cmd 

263 assert "-DAPIO_SIM=0" in normalized_cmd 

264 assert normalized_cmd.startswith("iverilog -g2012") 

265 

266 action_sim = iverilog_action( 

267 apio_env, verbose=True, is_interactive=True 

268 ) 

269 normalized_sim = re.sub(r"\s+", " ", action_sim) 

270 assert "-DAPIO_SIM=1" in normalized_sim 

271 assert "iverilog -g2012 -v " in normalized_sim 

272 

273 

274def test_verilator_lint_action_min(apio_runner: ApioRunner): 

275 """Tests the verilator_lint_action() function with minimal params.""" 

276 

277 with apio_runner.in_sandbox(): 

278 

279 # -- Create apio scons env. 

280 apio_env = make_test_apio_env( 

281 targets=["lint"], target_params=TargetParams(lint=LintParams()) 

282 ) 

283 

284 # -- Call the tested function with minimal args. 

285 action = verilator_lint_action( 

286 apio_env, extra_params=None, lib_dirs=None, lib_files=None 

287 ) 

288 

289 # -- The return action is a list of two steps, a function to call and 

290 # -- a string with a command. 

291 assert isinstance(action, list) 

292 assert len(action) == 2 

293 assert isinstance(action[0], FunctionAction) 

294 assert isinstance(action[1], str) 

295 

296 # -- Collapse consecutive spaces in the string. 

297 normalized_cmd = re.sub(r"\s+", " ", action[1]) 

298 

299 # -- Verify the string 

300 assert ( 

301 "verilator_bin --lint-only --quiet --bbox-unsup --timing " 

302 "-Wno-TIMESCALEMOD -Wno-MULTITOP -DSYNTHESIZE -DAPIO_SIM=0 " 

303 "--top-module main " 

304 f"_build{os.sep}default{os.sep}hardware.vlt $SOURCES" 

305 == normalized_cmd 

306 ) 

307 

308 

309def test_verilator_lint_action_max(apio_runner: ApioRunner): 

310 """Tests the verilator_lint_action() function with maximal params.""" 

311 

312 with apio_runner.in_sandbox(): 

313 

314 # -- Create apio scons env. 

315 apio_env = make_test_apio_env( 

316 targets=["lint"], 

317 apio_env_params=ApioEnvParams( 

318 verilator_extra_options=["--opt1", "--opt2"] 

319 ), 

320 target_params=TargetParams( 

321 lint=LintParams( 

322 top_module="my_top_module", 

323 ) 

324 ), 

325 ) 

326 

327 # -- Call the tested function with minimal args. 

328 action = verilator_lint_action( 

329 apio_env, 

330 extra_params=["param1", "param2"], 

331 lib_dirs=[Path("dir1"), Path("dir2")], 

332 lib_files=[Path("file1"), Path("file2")], 

333 ) 

334 

335 # -- The return action is a list of two steps, a function to call and 

336 # -- a string with a command. 

337 assert isinstance(action, list) 

338 assert len(action) == 2 

339 assert isinstance(action[0], FunctionAction) 

340 assert isinstance(action[1], str) 

341 

342 # -- Collapse consecutive spaces in the string. 

343 normalized_cmd = re.sub(r"\s+", " ", action[1]) 

344 

345 # -- Verify the string 

346 assert ( 

347 "verilator_bin --lint-only --quiet --bbox-unsup --timing " 

348 "-Wno-TIMESCALEMOD -Wno-MULTITOP -DSYNTHESIZE -DAPIO_SIM=0 " 

349 "--opt1 --opt2 " 

350 '--top-module my_top_module param1 param2 -I"dir1" -I"dir2" ' 

351 f'_build{os.sep}default{os.sep}hardware.vlt "file1" "file2" ' 

352 "$SOURCES" == normalized_cmd 

353 )