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

123 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-25 02:31 +0000

1""" 

2Tests of the scons plugin_util.py functions. 

3""" 

4 

5import re 

6import os 

7from os.path import isfile, exists, join 

8import pytest 

9from SCons.Node.FS import FS 

10from SCons.Action import FunctionAction 

11from pytest import LogCaptureFixture 

12from tests.unit_tests.scons.testing import make_test_apio_env 

13from tests.conftest import ApioRunner 

14from apio.common.apio_console import cunstyle 

15from apio.common import apio_console 

16from apio.common.proto.apio_pb2 import ( 

17 TargetParams, 

18 UploadParams, 

19 LintParams, 

20 ApioEnvParams, 

21) 

22from apio.scons.plugin_util import ( 

23 get_constraint_file, 

24 verilog_src_scanner, 

25 get_programmer_cmd, 

26 map_params, 

27 make_verilator_config_builder, 

28 verilator_lint_action, 

29) 

30 

31 

32def test_get_constraint_file( 

33 apio_runner: ApioRunner, capsys: LogCaptureFixture 

34): 

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

36 

37 with apio_runner.in_sandbox() as sb: 

38 

39 apio_env = make_test_apio_env() 

40 

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

42 capsys.readouterr() # Reset capture 

43 with pytest.raises(SystemExit) as e: 

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

45 captured = capsys.readouterr() 

46 assert e.value.code == 1 

47 assert "No constraint file '*.pcf' found" in cunstyle(captured.out) 

48 

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

50 # -- in subdirectories as we test here 

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

52 sb.write_file(file1, "content1") 

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

54 captured = capsys.readouterr() 

55 assert captured.out == "" 

56 assert result == file1 

57 

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

59 file2 = "other.pcf" 

60 sb.write_file(file2, "content2") 

61 capsys.readouterr() # Reset capture 

62 with pytest.raises(SystemExit) as e: 

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

64 captured = capsys.readouterr() 

65 assert e.value.code == 1 

66 assert "Error: Found 2 constraint files '*.pcf'" in cunstyle( 

67 captured.out 

68 ) 

69 

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

71 # -- if it exists or not. 

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

73 capsys.readouterr() # Reset capture 

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

75 captured = capsys.readouterr() 

76 assert captured.out == "" 

77 assert result == "xyz.pcf" 

78 

79 # -- File extension should match the architecture. 

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

81 capsys.readouterr() # Reset capture 

82 with pytest.raises(SystemExit) as e: 

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

84 captured = capsys.readouterr() 

85 assert e.value.code == 1 

86 assert ( 

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

88 in cunstyle(captured.out) 

89 ) 

90 

91 # -- Path under _build is not allowed. 

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

93 capsys.readouterr() # Reset capture 

94 with pytest.raises(SystemExit) as e: 

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

96 captured = capsys.readouterr() 

97 assert e.value.code == 1 

98 assert ( 

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

100 in cunstyle(captured.out) 

101 ) 

102 

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

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

105 capsys.readouterr() # Reset capture 

106 with pytest.raises(SystemExit) as e: 

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

108 captured = capsys.readouterr() 

109 assert e.value.code == 1 

110 assert ( 

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

112 in cunstyle(captured.out) 

113 ) 

114 

115 

116def test_verilog_src_scanner(apio_runner: ApioRunner): 

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

118 reference of files it uses. 

119 """ 

120 

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

122 # -- references out of alphabetical order. 

123 file_content = """ 

124 // Dummy file for testing. 

125 

126 // Icestudio reference. 

127 parameter v771499 = "v771499.list" 

128 

129 // System verilog include reference. 

130 `include "apio_testing.vh" 

131 

132 // Duplicate icestudio reference. 

133 parameter v771499 = "v771499.list" 

134 

135 // Verilog include reference. 

136 `include "apio_testing.v 

137 

138 // $readmemh() function reference. 

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

140 """ 

141 

142 with apio_runner.in_sandbox() as sb: 

143 

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

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

146 

147 # -- Create a scanner 

148 apio_env = make_test_apio_env() 

149 scanner = verilog_src_scanner(apio_env) 

150 

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

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

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

154 

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

156 # has a file. 

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

158 assert file_names == [] 

159 

160 # -- Create file lists 

161 core_dependencies = [ 

162 "apio.ini", 

163 "boards.jsonc", 

164 "programmers.jsonc", 

165 "fpgas.jsonc", 

166 ] 

167 

168 file_dependencies = [ 

169 "apio_testing.vh", 

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

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

172 ] 

173 

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

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

176 # -- filtered out) 

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

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

179 

180 # -- Run the scanner again 

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

182 

183 # -- Check the dependencies 

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

185 assert file_names == sorted(core_dependencies + file_dependencies) 

186 

187 

188def test_get_programmer_cmd(): 

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

190 

191 apio_console.configure() 

192 

193 # -- Test a valid programmer command. 

194 apio_env = make_test_apio_env( 

195 targets=["upload"], 

196 target_params=TargetParams( 

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

198 ), 

199 ) 

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

201 

202 

203def test_map_params(): 

204 """Test the map_params() function.""" 

205 

206 # -- Empty cases 

207 assert map_params([], "x_{}_y") == "" 

208 assert map_params(["", " "], "x_{}_y") == "" 

209 

210 # -- Non empty cases 

211 assert map_params(["a"], "x_{}_y") == "x_a_y" 

212 assert map_params([" a "], "x_{}_y") == "x_a_y" 

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

214 

215 

216def test_make_verilator_config_builder(apio_runner: ApioRunner): 

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

218 

219 with apio_runner.in_sandbox() as sb: 

220 

221 # -- Create a test scons env. 

222 apio_env = make_test_apio_env() 

223 

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

225 builder = make_verilator_config_builder( 

226 sb.packages_dir, 

227 rules_to_supress=[ 

228 "SPECIFYIGN", 

229 ], 

230 ) 

231 

232 # -- Verify builder suffixes. 

233 assert builder.suffix == ".vlt" 

234 assert builder.src_suffix == [] 

235 

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

237 assert not exists("hardware.vlt") 

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

239 

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

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

242 assert isfile("hardware.vlt") 

243 

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

245 text = sb.read_file("hardware.vlt") 

246 assert "verilator_config" in text, text 

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

248 

249 

250def test_verilator_lint_action_min(apio_runner: ApioRunner): 

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

252 

253 with apio_runner.in_sandbox(): 

254 

255 # -- Create apio scons env. 

256 apio_env = make_test_apio_env( 

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

258 ) 

259 

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

261 action = verilator_lint_action( 

262 apio_env, extra_params=None, lib_dirs=None, lib_files=None 

263 ) 

264 

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

266 # -- a string with a command. 

267 assert isinstance(action, list) 

268 assert len(action) == 2 

269 assert isinstance(action[0], FunctionAction) 

270 assert isinstance(action[1], str) 

271 

272 # -- Collapse consecutive spaces in the string. 

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

274 

275 # -- Verify the string 

276 assert ( 

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

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

279 "--top-module main " 

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

281 == normalized_cmd 

282 ) 

283 

284 

285def test_verilator_lint_action_max(apio_runner: ApioRunner): 

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

287 

288 with apio_runner.in_sandbox(): 

289 

290 # -- Create apio scons env. 

291 apio_env = make_test_apio_env( 

292 targets=["lint"], 

293 apio_env_params=ApioEnvParams( 

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

295 ), 

296 target_params=TargetParams( 

297 lint=LintParams( 

298 top_module="my_top_module", 

299 ) 

300 ), 

301 ) 

302 

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

304 action = verilator_lint_action( 

305 apio_env, 

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

307 lib_dirs=["dir1", "dir2"], 

308 lib_files=["file1", "file2"], 

309 ) 

310 

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

312 # -- a string with a command. 

313 assert isinstance(action, list) 

314 assert len(action) == 2 

315 assert isinstance(action[0], FunctionAction) 

316 assert isinstance(action[1], str) 

317 

318 # -- Collapse consecutive spaces in the string. 

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

320 

321 # -- Verify the string 

322 assert ( 

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

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

325 "--opt1 --opt2 " 

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

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

328 "$SOURCES" == normalized_cmd 

329 )