Coverage for tests/test_progress.py: 22%
146 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-21 09:55 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-21 09:55 +0000
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 program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
23import logging
24import unittest
25from contextlib import contextmanager
27import click
28from lsst.daf.butler.cli.progress import ClickProgressHandler
29from lsst.daf.butler.cli.utils import clickResultMsg
30from lsst.daf.butler.core.progress import Progress, ProgressHandler
33class MockProgressBar:
34 """Mock implementation of `ProgressBar` that remembers the status it
35 would report in a list.
37 Both the initial 0 and the end-of-iterable size are reported.
39 Parameters
40 ----------
41 iterable : `Iterable`
42 Iterable to wrap, or `None`.
43 total : `int` or `None`
44 Total value passed at progress-bar construction.
45 """
47 def __init__(self, iterable, total):
48 self._iterable = iterable
49 self._current = 0
50 self.reported = [self._current]
51 self.total = total
52 MockProgressBar.last = self
54 last = None
55 """Last instance of this class that was constructed, for test code that
56 cannot access it directly via other means.
57 """
59 def __iter__(self):
60 for element in self._iterable:
61 yield element
62 self._current += 1
63 self.reported.append(self._current)
65 def update(self, n: int = 1) -> None:
66 self._current += n
67 self.reported.append(self._current)
70class MockProgressHandler(ProgressHandler):
71 """A `ProgressHandler` implementation that returns `MockProgressBar`
72 instances.
73 """
75 @contextmanager
76 def get_progress_bar(self, iterable, desc, total, level):
77 yield MockProgressBar(iterable, total=total)
80class ClickProgressHandlerTestCase(unittest.TestCase):
81 """Test enabling and disabling progress in click commands.
83 It looks like click's testing harness doesn't ever actually let its
84 progress bar generate output, so the best we can do is check that using it
85 doesn't raise exceptions, and see if it looks like we're doing something
86 based on what our own progress-object state is.
87 """
89 def setUp(self):
90 # Set up a mock handler by default. Tests of click behavior will
91 # rely on this when they check that inside a click command we never
92 # end up with that mock.
93 self.logger = logging.getLogger("test_progress")
94 self.logger.setLevel(logging.INFO)
95 Progress.set_handler(MockProgressHandler())
96 self.runner = click.testing.CliRunner()
98 def tearDown(self):
99 MockProgressHandler.last = None
100 Progress.set_handler(None)
101 self.logger.setLevel(logging.NOTSET)
103 def get_cmd(self, level, enabled):
104 """Return a click command that uses a progress bar and tests that it
105 is or not enabled, as given.
106 """
108 @click.command()
109 @ClickProgressHandler.option
110 def cmd(progress):
111 p = Progress("test_progress", level=level)
112 with p.bar(range(5), desc="testing!") as bar:
113 self.assertFalse(isinstance(bar, MockProgressBar))
114 r = list(bar)
115 self.assertEqual(r, list(range(5)))
116 self.assertEqual(enabled, p.is_enabled())
118 return cmd
120 def test_click_disabled_by_default(self):
121 """Test that progress is disabled by default in click commands."""
122 result = self.runner.invoke(
123 self.get_cmd(logging.INFO, enabled=False),
124 [],
125 )
126 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
128 def test_click_enabled(self):
129 """Test turning on progress in click commands."""
130 result = self.runner.invoke(
131 self.get_cmd(logging.INFO, enabled=True),
132 ["--progress"],
133 )
134 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
136 def test_click_disabled_globally(self):
137 """Test turning on progress in click commands."""
138 result = self.runner.invoke(
139 self.get_cmd(logging.INFO, enabled=False),
140 ["--no-progress"],
141 )
142 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
144 def test_click_disabled_by_log_level(self):
145 """Test that progress reports below the current log level are disabled,
146 even if progress is globally enabled.
147 """
148 result = self.runner.invoke(
149 self.get_cmd(logging.DEBUG, enabled=False),
150 ["--progress"],
151 )
152 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
155class MockedProgressHandlerTestCase(unittest.TestCase):
156 """Test that the interface layer for progress reporting works by using
157 mock handler and progress bar objects.
158 """
160 def setUp(self):
161 self.logger = logging.getLogger("test_progress")
162 self.logger.setLevel(logging.INFO)
163 Progress.set_handler(MockProgressHandler())
164 self.progress = Progress("test_progress")
166 def tearDown(self):
167 MockProgressHandler.last = None
168 Progress.set_handler(None)
169 self.logger.setLevel(logging.NOTSET)
171 def test_bar_iterable(self):
172 """Test using `Progress.bar` to wrap an iterable."""
173 iterable = list(range(5))
174 with self.progress.bar(iterable) as bar:
175 r = list(bar)
176 self.assertEqual(r, iterable)
177 self.assertEqual(iterable + [len(iterable)], bar.reported)
179 def test_bar_update(self):
180 """Test using `Progress.bar` with manual updates."""
181 with self.progress.bar(total=10) as bar:
182 for i in range(5):
183 bar.update(2)
184 self.assertEqual(list(range(0, 12, 2)), bar.reported)
186 def test_iter_chunks_fully_sized(self):
187 """Test using `Progress.iter_chunks` with a sized iterable of sized
188 chunks.
189 """
190 iterable = [list(range(2)), list(range(3))]
191 seen = []
192 for chunk in self.progress.iter_chunks(iterable):
193 seen.extend(chunk)
194 self.assertEqual(seen, iterable[0] + iterable[1])
195 self.assertEqual(MockProgressBar.last.reported, [0, 2, 5])
196 self.assertEqual(MockProgressBar.last.total, 5)
198 def test_iter_chunks_with_total(self):
199 """Test using `Progress.iter_chunks` with total provided and
200 sized chunks.
201 """
202 iterable = [list(range(2)), list(range(3))]
203 seen = []
204 for chunk in self.progress.iter_chunks(iter(iterable), total=5):
205 seen.extend(chunk)
206 self.assertEqual(seen, iterable[0] + iterable[1])
207 self.assertEqual(MockProgressBar.last.reported, [0, 2, 5])
208 self.assertEqual(MockProgressBar.last.total, 5)
210 def test_iter_chunks_total_false(self):
211 """Test using `Progress.iter_chunks` with total=False and non-sized
212 chunks. This should display progress with the number of
213 chunks.
214 """
215 iterable = [iter(range(2)), iter(range(3))]
216 seen = []
217 for chunk in self.progress.iter_chunks(iterable, total=False):
218 seen.extend(chunk)
219 self.assertEqual(seen, list(range(2)) + list(range(3)))
220 self.assertEqual(MockProgressBar.last.reported, [0, 1, 2])
221 self.assertEqual(MockProgressBar.last.total, 2)
223 def test_iter_chunks_not_sized(self):
224 """Test using `Progress.iter_chunks` with an unsized iterable."""
225 iterable = [iter(range(2)), iter(range(3))]
226 seen = []
227 for chunk in self.progress.iter_chunks(iter(iterable)):
228 seen.extend(chunk)
229 self.assertEqual(seen, list(range(2)) + list(range(3)))
230 self.assertEqual(MockProgressBar.last.reported, [0, 1, 2])
231 self.assertEqual(MockProgressBar.last.total, None)
233 def test_iter_item_chunks_fully_sized(self):
234 """Test using `Progress.iter_item_chunks` with a sized iterable of
235 sized chunks.
236 """
237 mapping = {"x": list(range(2)), "y": list(range(3))}
238 seen = {}
239 for key, chunk in self.progress.iter_item_chunks(mapping.items()):
240 seen[key] = chunk
241 self.assertEqual(seen, mapping)
242 self.assertEqual(MockProgressBar.last.reported, [0, 2, 5])
243 self.assertEqual(MockProgressBar.last.total, 5)
245 def test_iter_item_chunks_with_total(self):
246 """Test using `Progress.iter_item_chunks` with total provided and
247 sized chunks.
248 """
249 mapping = {"x": list(range(2)), "y": list(range(3))}
250 seen = {}
251 for key, chunk in self.progress.iter_item_chunks(iter(mapping.items()), total=5):
252 seen[key] = chunk
253 self.assertEqual(seen, mapping)
254 self.assertEqual(MockProgressBar.last.reported, [0, 2, 5])
255 self.assertEqual(MockProgressBar.last.total, 5)
257 def test_iter_item_chunks_total_false(self):
258 """Test using `Progress.iter_item_chunks` with total=False and
259 non-sized chunks. This should display progress with the number of
260 chunks.
261 """
262 mapping = {"x": iter(range(2)), "y": iter(range(3))}
263 seen = {}
264 for key, chunk in self.progress.iter_item_chunks(mapping.items(), total=False):
265 seen[key] = list(chunk)
266 self.assertEqual(seen, {"x": list(range(2)), "y": list(range(3))})
267 self.assertEqual(MockProgressBar.last.reported, [0, 1, 2])
268 self.assertEqual(MockProgressBar.last.total, 2)
270 def test_iter_item_chunks_not_sized(self):
271 """Test using `Progress.iter_item_chunks` with an unsized iterable of
272 non-sized chunks.
273 """
274 mapping = {"x": iter(range(2)), "y": iter(range(3))}
275 seen = {}
276 for key, chunk in self.progress.iter_item_chunks(iter(mapping.items())):
277 seen[key] = list(chunk)
278 self.assertEqual(seen, {"x": list(range(2)), "y": list(range(3))})
279 self.assertEqual(MockProgressBar.last.reported, [0, 1, 2])
280 self.assertEqual(MockProgressBar.last.total, None)
283if __name__ == "__main__":
284 unittest.main()