Coverage for tests/test_progress.py: 22%

146 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-26 02:48 -0700

1# This file is part of daf_butler. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

16# it under the terms of the GNU General Public License as published by 

17# the Free Software Foundation, either version 3 of the License, or 

18# (at your option) any later version. 

19# 

20# This program is distributed in the hope that it will be useful, 

21# but WITHOUT ANY WARRANTY; without even the implied warranty of 

22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23# GNU General Public License for more details. 

24# 

25# You should have received a copy of the GNU General Public License 

26# along with this program. If not, see <http://www.gnu.org/licenses/>. 

27 

28 

29import logging 

30import unittest 

31from contextlib import contextmanager 

32 

33import click 

34from lsst.daf.butler.cli.progress import ClickProgressHandler 

35from lsst.daf.butler.cli.utils import clickResultMsg 

36from lsst.daf.butler.progress import Progress, ProgressHandler 

37 

38 

39class MockProgressBar: 

40 """Mock implementation of `ProgressBar` that remembers the status it 

41 would report in a list. 

42 

43 Both the initial 0 and the end-of-iterable size are reported. 

44 

45 Parameters 

46 ---------- 

47 iterable : `Iterable` 

48 Iterable to wrap, or `None`. 

49 total : `int` or `None` 

50 Total value passed at progress-bar construction. 

51 """ 

52 

53 def __init__(self, iterable, total): 

54 self._iterable = iterable 

55 self._current = 0 

56 self.reported = [self._current] 

57 self.total = total 

58 MockProgressBar.last = self 

59 

60 last = None 

61 """Last instance of this class that was constructed, for test code that 

62 cannot access it directly via other means. 

63 """ 

64 

65 def __iter__(self): 

66 for element in self._iterable: 

67 yield element 

68 self._current += 1 

69 self.reported.append(self._current) 

70 

71 def update(self, n: int = 1) -> None: 

72 self._current += n 

73 self.reported.append(self._current) 

74 

75 

76class MockProgressHandler(ProgressHandler): 

77 """A `ProgressHandler` implementation that returns `MockProgressBar` 

78 instances. 

79 """ 

80 

81 @contextmanager 

82 def get_progress_bar(self, iterable, desc, total, level): 

83 yield MockProgressBar(iterable, total=total) 

84 

85 

86class ClickProgressHandlerTestCase(unittest.TestCase): 

87 """Test enabling and disabling progress in click commands. 

88 

89 It looks like click's testing harness doesn't ever actually let its 

90 progress bar generate output, so the best we can do is check that using it 

91 doesn't raise exceptions, and see if it looks like we're doing something 

92 based on what our own progress-object state is. 

93 """ 

94 

95 def setUp(self): 

96 # Set up a mock handler by default. Tests of click behavior will 

97 # rely on this when they check that inside a click command we never 

98 # end up with that mock. 

99 self.logger = logging.getLogger("test_progress") 

100 self.logger.setLevel(logging.INFO) 

101 Progress.set_handler(MockProgressHandler()) 

102 self.runner = click.testing.CliRunner() 

103 

104 def tearDown(self): 

105 MockProgressHandler.last = None 

106 Progress.set_handler(None) 

107 self.logger.setLevel(logging.NOTSET) 

108 

109 def get_cmd(self, level, enabled): 

110 """Return a click command that uses a progress bar and tests that it 

111 is or not enabled, as given. 

112 """ 

113 

114 @click.command() 

115 @ClickProgressHandler.option 

116 def cmd(progress): 

117 p = Progress("test_progress", level=level) 

118 with p.bar(range(5), desc="testing!") as bar: 

119 self.assertFalse(isinstance(bar, MockProgressBar)) 

120 r = list(bar) 

121 self.assertEqual(r, list(range(5))) 

122 self.assertEqual(enabled, p.is_enabled()) 

123 

124 return cmd 

125 

126 def test_click_disabled_by_default(self): 

127 """Test that progress is disabled by default in click commands.""" 

128 result = self.runner.invoke( 

129 self.get_cmd(logging.INFO, enabled=False), 

130 [], 

131 ) 

132 self.assertEqual(result.exit_code, 0, clickResultMsg(result)) 

133 

134 def test_click_enabled(self): 

135 """Test turning on progress in click commands.""" 

136 result = self.runner.invoke( 

137 self.get_cmd(logging.INFO, enabled=True), 

138 ["--progress"], 

139 ) 

140 self.assertEqual(result.exit_code, 0, clickResultMsg(result)) 

141 

142 def test_click_disabled_globally(self): 

143 """Test turning on progress in click commands.""" 

144 result = self.runner.invoke( 

145 self.get_cmd(logging.INFO, enabled=False), 

146 ["--no-progress"], 

147 ) 

148 self.assertEqual(result.exit_code, 0, clickResultMsg(result)) 

149 

150 def test_click_disabled_by_log_level(self): 

151 """Test that progress reports below the current log level are disabled, 

152 even if progress is globally enabled. 

153 """ 

154 result = self.runner.invoke( 

155 self.get_cmd(logging.DEBUG, enabled=False), 

156 ["--progress"], 

157 ) 

158 self.assertEqual(result.exit_code, 0, clickResultMsg(result)) 

159 

160 

161class MockedProgressHandlerTestCase(unittest.TestCase): 

162 """Test that the interface layer for progress reporting works by using 

163 mock handler and progress bar objects. 

164 """ 

165 

166 def setUp(self): 

167 self.logger = logging.getLogger("test_progress") 

168 self.logger.setLevel(logging.INFO) 

169 Progress.set_handler(MockProgressHandler()) 

170 self.progress = Progress("test_progress") 

171 

172 def tearDown(self): 

173 MockProgressHandler.last = None 

174 Progress.set_handler(None) 

175 self.logger.setLevel(logging.NOTSET) 

176 

177 def test_bar_iterable(self): 

178 """Test using `Progress.bar` to wrap an iterable.""" 

179 iterable = list(range(5)) 

180 with self.progress.bar(iterable) as bar: 

181 r = list(bar) 

182 self.assertEqual(r, iterable) 

183 self.assertEqual(iterable + [len(iterable)], bar.reported) 

184 

185 def test_bar_update(self): 

186 """Test using `Progress.bar` with manual updates.""" 

187 with self.progress.bar(total=10) as bar: 

188 for _ in range(5): 

189 bar.update(2) 

190 self.assertEqual(list(range(0, 12, 2)), bar.reported) 

191 

192 def test_iter_chunks_fully_sized(self): 

193 """Test using `Progress.iter_chunks` with a sized iterable of sized 

194 chunks. 

195 """ 

196 iterable = [list(range(2)), list(range(3))] 

197 seen = [] 

198 for chunk in self.progress.iter_chunks(iterable): 

199 seen.extend(chunk) 

200 self.assertEqual(seen, iterable[0] + iterable[1]) 

201 self.assertEqual(MockProgressBar.last.reported, [0, 2, 5]) 

202 self.assertEqual(MockProgressBar.last.total, 5) 

203 

204 def test_iter_chunks_with_total(self): 

205 """Test using `Progress.iter_chunks` with total provided and 

206 sized chunks. 

207 """ 

208 iterable = [list(range(2)), list(range(3))] 

209 seen = [] 

210 for chunk in self.progress.iter_chunks(iter(iterable), total=5): 

211 seen.extend(chunk) 

212 self.assertEqual(seen, iterable[0] + iterable[1]) 

213 self.assertEqual(MockProgressBar.last.reported, [0, 2, 5]) 

214 self.assertEqual(MockProgressBar.last.total, 5) 

215 

216 def test_iter_chunks_total_false(self): 

217 """Test using `Progress.iter_chunks` with total=False and non-sized 

218 chunks. This should display progress with the number of 

219 chunks. 

220 """ 

221 iterable = [iter(range(2)), iter(range(3))] 

222 seen = [] 

223 for chunk in self.progress.iter_chunks(iterable, total=False): 

224 seen.extend(chunk) 

225 self.assertEqual(seen, list(range(2)) + list(range(3))) 

226 self.assertEqual(MockProgressBar.last.reported, [0, 1, 2]) 

227 self.assertEqual(MockProgressBar.last.total, 2) 

228 

229 def test_iter_chunks_not_sized(self): 

230 """Test using `Progress.iter_chunks` with an unsized iterable.""" 

231 iterable = [iter(range(2)), iter(range(3))] 

232 seen = [] 

233 for chunk in self.progress.iter_chunks(iter(iterable)): 

234 seen.extend(chunk) 

235 self.assertEqual(seen, list(range(2)) + list(range(3))) 

236 self.assertEqual(MockProgressBar.last.reported, [0, 1, 2]) 

237 self.assertEqual(MockProgressBar.last.total, None) 

238 

239 def test_iter_item_chunks_fully_sized(self): 

240 """Test using `Progress.iter_item_chunks` with a sized iterable of 

241 sized chunks. 

242 """ 

243 mapping = {"x": list(range(2)), "y": list(range(3))} 

244 seen = {} 

245 for key, chunk in self.progress.iter_item_chunks(mapping.items()): 

246 seen[key] = chunk 

247 self.assertEqual(seen, mapping) 

248 self.assertEqual(MockProgressBar.last.reported, [0, 2, 5]) 

249 self.assertEqual(MockProgressBar.last.total, 5) 

250 

251 def test_iter_item_chunks_with_total(self): 

252 """Test using `Progress.iter_item_chunks` with total provided and 

253 sized chunks. 

254 """ 

255 mapping = {"x": list(range(2)), "y": list(range(3))} 

256 seen = {} 

257 for key, chunk in self.progress.iter_item_chunks(iter(mapping.items()), total=5): 

258 seen[key] = chunk 

259 self.assertEqual(seen, mapping) 

260 self.assertEqual(MockProgressBar.last.reported, [0, 2, 5]) 

261 self.assertEqual(MockProgressBar.last.total, 5) 

262 

263 def test_iter_item_chunks_total_false(self): 

264 """Test using `Progress.iter_item_chunks` with total=False and 

265 non-sized chunks. This should display progress with the number of 

266 chunks. 

267 """ 

268 mapping = {"x": iter(range(2)), "y": iter(range(3))} 

269 seen = {} 

270 for key, chunk in self.progress.iter_item_chunks(mapping.items(), total=False): 

271 seen[key] = list(chunk) 

272 self.assertEqual(seen, {"x": list(range(2)), "y": list(range(3))}) 

273 self.assertEqual(MockProgressBar.last.reported, [0, 1, 2]) 

274 self.assertEqual(MockProgressBar.last.total, 2) 

275 

276 def test_iter_item_chunks_not_sized(self): 

277 """Test using `Progress.iter_item_chunks` with an unsized iterable of 

278 non-sized chunks. 

279 """ 

280 mapping = {"x": iter(range(2)), "y": iter(range(3))} 

281 seen = {} 

282 for key, chunk in self.progress.iter_item_chunks(iter(mapping.items())): 

283 seen[key] = list(chunk) 

284 self.assertEqual(seen, {"x": list(range(2)), "y": list(range(3))}) 

285 self.assertEqual(MockProgressBar.last.reported, [0, 1, 2]) 

286 self.assertEqual(MockProgressBar.last.total, None) 

287 

288 

289if __name__ == "__main__": 

290 unittest.main()