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

123 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-24 01:53 +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 TargetParams, UploadParams, LintParams 

17from apio.scons.plugin_util import ( 

18 get_constraint_file, 

19 verilog_src_scanner, 

20 get_programmer_cmd, 

21 map_params, 

22 make_verilator_config_builder, 

23 verilator_lint_action, 

24) 

25 

26 

27def test_get_constraint_file( 

28 apio_runner: ApioRunner, capsys: LogCaptureFixture 

29): 

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

31 

32 with apio_runner.in_sandbox() as sb: 

33 

34 apio_env = make_test_apio_env() 

35 

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

37 capsys.readouterr() # Reset capture 

38 with pytest.raises(SystemExit) as e: 

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

40 captured = capsys.readouterr() 

41 assert e.value.code == 1 

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

43 

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

45 # -- in subdirectories as we test here 

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

47 sb.write_file(file1, "content1") 

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

49 captured = capsys.readouterr() 

50 assert captured.out == "" 

51 assert result == file1 

52 

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

54 file2 = "other.pcf" 

55 sb.write_file(file2, "content2") 

56 capsys.readouterr() # Reset capture 

57 with pytest.raises(SystemExit) as e: 

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

59 captured = capsys.readouterr() 

60 assert e.value.code == 1 

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

62 captured.out 

63 ) 

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 capsys.readouterr() # Reset capture 

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

70 captured = capsys.readouterr() 

71 assert captured.out == "" 

72 assert result == "xyz.pcf" 

73 

74 # -- File extension should match the architecture. 

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

76 capsys.readouterr() # Reset capture 

77 with pytest.raises(SystemExit) as e: 

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

79 captured = capsys.readouterr() 

80 assert e.value.code == 1 

81 assert ( 

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

83 in cunstyle(captured.out) 

84 ) 

85 

86 # -- Path under _build is not allowed. 

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

88 capsys.readouterr() # Reset capture 

89 with pytest.raises(SystemExit) as e: 

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

91 captured = capsys.readouterr() 

92 assert e.value.code == 1 

93 assert ( 

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

95 in cunstyle(captured.out) 

96 ) 

97 

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

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

100 capsys.readouterr() # Reset capture 

101 with pytest.raises(SystemExit) as e: 

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

103 captured = capsys.readouterr() 

104 assert e.value.code == 1 

105 assert ( 

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

107 in cunstyle(captured.out) 

108 ) 

109 

110 

111def test_verilog_src_scanner(apio_runner: ApioRunner): 

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

113 reference of files it uses. 

114 """ 

115 

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

117 # -- references out of alphabetical order. 

118 file_content = """ 

119 // Dummy file for testing. 

120 

121 // Icestudio reference. 

122 parameter v771499 = "v771499.list" 

123 

124 // System verilog include reference. 

125 `include "apio_testing.vh" 

126 

127 // Duplicate icestudio reference. 

128 parameter v771499 = "v771499.list" 

129 

130 // Verilog include reference. 

131 `include "apio_testing.v 

132 

133 // $readmemh() function reference. 

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

135 """ 

136 

137 with apio_runner.in_sandbox() as sb: 

138 

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

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

141 

142 # -- Create a scanner 

143 apio_env = make_test_apio_env() 

144 scanner = verilog_src_scanner(apio_env) 

145 

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

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

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

149 

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

151 # has a file. 

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

153 assert file_names == [] 

154 

155 # -- Create file lists 

156 core_dependencies = [ 

157 "apio.ini", 

158 "boards.jsonc", 

159 "programmers.jsonc", 

160 "fpgas.jsonc", 

161 ] 

162 

163 file_dependencies = [ 

164 "apio_testing.vh", 

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

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

167 ] 

168 

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

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

171 # -- filtered out) 

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

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

174 

175 # -- Run the scanner again 

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

177 

178 # -- Check the dependencies 

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

180 assert file_names == sorted(core_dependencies + file_dependencies) 

181 

182 

183def test_get_programmer_cmd(): 

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

185 

186 apio_console.configure() 

187 

188 # -- Test a valid programmer command. 

189 apio_env = make_test_apio_env( 

190 targets=["upload"], 

191 target_params=TargetParams( 

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

193 ), 

194 ) 

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

196 

197 

198def test_map_params(): 

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

200 

201 # -- Empty cases 

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

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

204 

205 # -- Non empty cases 

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

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

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

209 

210 

211def test_make_verilator_config_builder(apio_runner: ApioRunner): 

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

213 

214 with apio_runner.in_sandbox() as sb: 

215 

216 # -- Create a test scons env. 

217 apio_env = make_test_apio_env() 

218 

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

220 builder = make_verilator_config_builder(sb.packages_dir) 

221 

222 # -- Verify builder suffixes. 

223 assert builder.suffix == ".vlt" 

224 assert builder.src_suffix == [] 

225 

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

227 assert not exists("hardware.vlt") 

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

229 

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

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

232 assert isfile("hardware.vlt") 

233 

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

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

236 assert "verilator_config" in text, text 

237 assert "lint_off -rule COMBDLY" in text, text 

238 

239 

240def test_verilator_lint_action_min(apio_runner: ApioRunner): 

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

242 

243 with apio_runner.in_sandbox(): 

244 

245 # -- Create apio scons env. 

246 apio_env = make_test_apio_env( 

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

248 ) 

249 

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

251 action = verilator_lint_action( 

252 apio_env, extra_params=None, lib_dirs=None, lib_files=None 

253 ) 

254 

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

256 # -- a string with a command. 

257 assert isinstance(action, list) 

258 assert len(action) == 2 

259 assert isinstance(action[0], FunctionAction) 

260 assert isinstance(action[1], str) 

261 

262 # -- Collapse consecutive spaces in the string. 

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

264 

265 # -- Verify the string 

266 assert ( 

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

268 "-Wno-TIMESCALEMOD -Wno-MULTITOP -DAPIO_SIM=0 --top-module main " 

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

270 == normalized_cmd 

271 ) 

272 

273 

274def test_verilator_lint_action_max(apio_runner: ApioRunner): 

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

276 

277 with apio_runner.in_sandbox(): 

278 

279 # -- Create apio scons env. 

280 apio_env = make_test_apio_env( 

281 targets=["lint"], 

282 target_params=TargetParams( 

283 lint=LintParams( 

284 top_module="my_top_module", 

285 verilator_all=True, 

286 verilator_no_style=True, 

287 verilator_no_warns=["aa", "bb"], 

288 verilator_warns=["cc", "dd"], 

289 ) 

290 ), 

291 ) 

292 

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

294 action = verilator_lint_action( 

295 apio_env, 

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

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

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

299 ) 

300 

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

302 # -- a string with a command. 

303 assert isinstance(action, list) 

304 assert len(action) == 2 

305 assert isinstance(action[0], FunctionAction) 

306 assert isinstance(action[1], str) 

307 

308 # -- Collapse consecutive spaces in the string. 

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

310 # -- Verify the string 

311 assert ( 

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

313 "-Wno-TIMESCALEMOD -Wno-MULTITOP -DAPIO_SIM=0 -Wall -Wno-style " 

314 "-Wno-aa -Wno-bb -Wwarn-cc -Wwarn-dd --top-module my_top_module " 

315 'param1 param2 -I"dir1" -I"dir2" ' 

316 f"_build{os.sep}default{os.sep}hardware.vlt " 

317 '"file1" "file2" $SOURCES' == normalized_cmd 

318 )