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

114 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-06 10:20 +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 ( 

43 "Error: No constraint file '*.pcf' found, expected exactly one" 

44 in cunstyle(captured.out) 

45 ) 

46 

47 # -- If a single .pcf file, return it. 

48 sb.write_file("pinout.pcf", "content") 

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

50 captured = capsys.readouterr() 

51 assert captured.out == "" 

52 assert result == "pinout.pcf" 

53 

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

55 sb.write_file("other.pcf", "content") 

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 multiple '*.pcf'" in cunstyle(captured.out) 

62 

63 # -- If the user specified a valid file then return it. 

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

65 capsys.readouterr() # Reset capture 

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

67 captured = capsys.readouterr() 

68 assert captured.out == "" 

69 assert result == "xyz.pcf" 

70 

71 # -- If the user specified a constrain file with an extension that 

72 # -- doesn't match the architecture, exit with an error 

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

74 capsys.readouterr() # Reset capture 

75 with pytest.raises(SystemExit) as e: 

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

77 captured = capsys.readouterr() 

78 assert e.value.code == 1 

79 assert ( 

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

81 in cunstyle(captured.out) 

82 ) 

83 

84 # -- If the user specified a constrain file with forbidden char, 

85 # -- exit with an error 

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

87 capsys.readouterr() # Reset capture 

88 with pytest.raises(SystemExit) as e: 

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

90 captured = capsys.readouterr() 

91 assert e.value.code == 1 

92 assert ( 

93 "Constrain filename 'a/xyz.pcf' contains an illegal character: '/'" 

94 in cunstyle(captured.out) 

95 ) 

96 

97 

98def test_verilog_src_scanner(apio_runner: ApioRunner): 

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

100 reference of files it uses. 

101 """ 

102 

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

104 # -- references out of alphabetical order. 

105 file_content = """ 

106 // Dummy file for testing. 

107 

108 // Icestudio reference. 

109 parameter v771499 = "v771499.list" 

110 

111 // System verilog include reference. 

112 `include "apio_testing.vh" 

113 

114 // Duplicate icestudio reference. 

115 parameter v771499 = "v771499.list" 

116 

117 // Verilog include reference. 

118 `include "apio_testing.v 

119 

120 // $readmemh() function reference. 

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

122 """ 

123 

124 with apio_runner.in_sandbox() as sb: 

125 

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

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

128 

129 # -- Create a scanner 

130 apio_env = make_test_apio_env() 

131 scanner = verilog_src_scanner(apio_env) 

132 

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

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

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

136 

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

138 # has a file. 

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

140 assert file_names == [] 

141 

142 # -- Create file lists 

143 core_dependencies = [ 

144 "apio.ini", 

145 "boards.jsonc", 

146 "programmers.jsonc", 

147 "fpgas.jsonc", 

148 ] 

149 

150 file_dependencies = [ 

151 "apio_testing.vh", 

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

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

154 ] 

155 

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

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

158 # -- filtered out) 

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

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

161 

162 # -- Run the scanner again 

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

164 

165 # -- Check the dependencies 

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

167 assert file_names == sorted(core_dependencies + file_dependencies) 

168 

169 

170def test_get_programmer_cmd(): 

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

172 

173 apio_console.configure() 

174 

175 # -- Test a valid programmer command. 

176 apio_env = make_test_apio_env( 

177 targets=["upload"], 

178 target_params=TargetParams( 

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

180 ), 

181 ) 

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

183 

184 

185def test_map_params(): 

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

187 

188 # -- Empty cases 

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

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

191 

192 # -- Non empty cases 

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

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

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

196 

197 

198def test_make_verilator_config_builder(apio_runner: ApioRunner): 

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

200 

201 with apio_runner.in_sandbox() as sb: 

202 

203 # -- Create a test scons env. 

204 apio_env = make_test_apio_env() 

205 

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

207 builder = make_verilator_config_builder(sb.packages_dir) 

208 

209 # -- Verify builder suffixes. 

210 assert builder.suffix == ".vlt" 

211 assert builder.src_suffix == [] 

212 

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

214 assert not exists("hardware.vlt") 

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

216 

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

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

219 assert isfile("hardware.vlt") 

220 

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

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

223 assert "verilator_config" in text, text 

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

225 

226 

227def test_verilator_lint_action_min(apio_runner: ApioRunner): 

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

229 

230 with apio_runner.in_sandbox(): 

231 

232 # -- Create apio scons env. 

233 apio_env = make_test_apio_env( 

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

235 ) 

236 

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

238 action = verilator_lint_action( 

239 apio_env, extra_params=None, lib_dirs=None, lib_files=None 

240 ) 

241 

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

243 # -- a string with a command. 

244 assert isinstance(action, list) 

245 assert len(action) == 2 

246 assert isinstance(action[0], FunctionAction) 

247 assert isinstance(action[1], str) 

248 

249 # -- Collapse consecutive spaces in the string. 

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

251 

252 # -- Verify the string 

253 assert ( 

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

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

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

257 == normalized_cmd 

258 ) 

259 

260 

261def test_verilator_lint_action_max(apio_runner: ApioRunner): 

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

263 

264 with apio_runner.in_sandbox(): 

265 

266 # -- Create apio scons env. 

267 apio_env = make_test_apio_env( 

268 targets=["lint"], 

269 target_params=TargetParams( 

270 lint=LintParams( 

271 top_module="my_top_module", 

272 verilator_all=True, 

273 verilator_no_style=True, 

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

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

276 ) 

277 ), 

278 ) 

279 

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

281 action = verilator_lint_action( 

282 apio_env, 

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

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

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

286 ) 

287 

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

289 # -- a string with a command. 

290 assert isinstance(action, list) 

291 assert len(action) == 2 

292 assert isinstance(action[0], FunctionAction) 

293 assert isinstance(action[1], str) 

294 

295 # -- Collapse consecutive spaces in the string. 

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

297 # -- Verify the string 

298 assert ( 

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

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

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

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

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

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

305 )