##// END OF EJS Templates
progress: don't compute estimate without a total...
Augie Fackler -
r13154:e11c14f1 default
parent child Browse files
Show More
@@ -1,252 +1,254
1 1 # progress.py show progress bars for some actions
2 2 #
3 3 # Copyright (C) 2010 Augie Fackler <durin42@gmail.com>
4 4 #
5 5 # This program is free software; you can redistribute it and/or modify it
6 6 # under the terms of the GNU General Public License as published by the
7 7 # Free Software Foundation; either version 2 of the License, or (at your
8 8 # option) any later version.
9 9 #
10 10 # This program is distributed in the hope that it will be useful, but
11 11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
13 13 # Public License for more details.
14 14 #
15 15 # You should have received a copy of the GNU General Public License along
16 16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 18
19 19 """show progress bars for some actions
20 20
21 21 This extension uses the progress information logged by hg commands
22 22 to draw progress bars that are as informative as possible. Some progress
23 23 bars only offer indeterminate information, while others have a definite
24 24 end point.
25 25
26 26 The following settings are available::
27 27
28 28 [progress]
29 29 delay = 3 # number of seconds (float) before showing the progress bar
30 30 refresh = 0.1 # time in seconds between refreshes of the progress bar
31 31 format = topic bar number estimate # format of the progress bar
32 32 width = <none> # if set, the maximum width of the progress information
33 33 # (that is, min(width, term width) will be used)
34 34 clear-complete = True # clear the progress bar after it's done
35 35 disable = False # if true, don't show a progress bar
36 36 assume-tty = False # if true, ALWAYS show a progress bar, unless
37 37 # disable is given
38 38
39 39 Valid entries for the format field are topic, bar, number, unit,
40 40 estimate, and item. item defaults to the last 20 characters of the
41 41 item, but this can be changed by adding either ``-<num>`` which would
42 42 take the last num characters, or ``+<num>`` for the first num
43 43 characters.
44 44 """
45 45
46 46 import sys
47 47 import time
48 48
49 49 from mercurial.i18n import _
50 50 from mercurial import util
51 51
52 52 def spacejoin(*args):
53 53 return ' '.join(s for s in args if s)
54 54
55 55 def shouldprint(ui):
56 56 return (getattr(sys.stderr, 'isatty', None) and
57 57 (sys.stderr.isatty() or ui.configbool('progress', 'assume-tty')))
58 58
59 59 def fmtremaining(seconds):
60 60 if seconds < 60:
61 61 # i18n: format XX seconds as "XXs"
62 62 return _("%02ds") % (seconds)
63 63 minutes = seconds // 60
64 64 if minutes < 60:
65 65 seconds -= minutes * 60
66 66 # i18n: format X minutes and YY seconds as "XmYYs"
67 67 return _("%dm%02ds") % (minutes, seconds)
68 68 # we're going to ignore seconds in this case
69 69 minutes += 1
70 70 hours = minutes // 60
71 71 minutes -= hours * 60
72 72 # i18n: format X hours and YY minutes as "XhYYm"
73 73 return _("%dh%02dm") % (hours, minutes)
74 74
75 75 class progbar(object):
76 76 def __init__(self, ui):
77 77 self.ui = ui
78 78 self.resetstate()
79 79
80 80 def resetstate(self):
81 81 self.topics = []
82 82 self.topicstates = {}
83 83 self.starttimes = {}
84 84 self.startvals = {}
85 85 self.printed = False
86 86 self.lastprint = time.time() + float(self.ui.config(
87 87 'progress', 'delay', default=3))
88 88 self.indetcount = 0
89 89 self.refresh = float(self.ui.config(
90 90 'progress', 'refresh', default=0.1))
91 91 self.order = self.ui.configlist(
92 92 'progress', 'format',
93 93 default=['topic', 'bar', 'number', 'estimate'])
94 94
95 95 def show(self, now, topic, pos, item, unit, total):
96 96 if not shouldprint(self.ui):
97 97 return
98 98 termwidth = self.width()
99 99 self.printed = True
100 100 head = ''
101 101 needprogress = False
102 102 tail = ''
103 103 for indicator in self.order:
104 104 add = ''
105 105 if indicator == 'topic':
106 106 add = topic
107 107 elif indicator == 'number':
108 108 if total:
109 109 add = ('% ' + str(len(str(total))) +
110 110 's/%s') % (pos, total)
111 111 else:
112 112 add = str(pos)
113 113 elif indicator.startswith('item') and item:
114 114 slice = 'end'
115 115 if '-' in indicator:
116 116 wid = int(indicator.split('-')[1])
117 117 elif '+' in indicator:
118 118 slice = 'beginning'
119 119 wid = int(indicator.split('+')[1])
120 120 else:
121 121 wid = 20
122 122 if slice == 'end':
123 123 add = item[-wid:]
124 124 else:
125 125 add = item[:wid]
126 126 add += (wid - len(add)) * ' '
127 127 elif indicator == 'bar':
128 128 add = ''
129 129 needprogress = True
130 130 elif indicator == 'unit' and unit:
131 131 add = unit
132 132 elif indicator == 'estimate':
133 133 add = self.estimate(topic, pos, total, now)
134 134 if not needprogress:
135 135 head = spacejoin(head, add)
136 136 else:
137 137 tail = spacejoin(tail, add)
138 138 if needprogress:
139 139 used = 0
140 140 if head:
141 141 used += len(head) + 1
142 142 if tail:
143 143 used += len(tail) + 1
144 144 progwidth = termwidth - used - 3
145 145 if total and pos <= total:
146 146 amt = pos * progwidth // total
147 147 bar = '=' * (amt - 1)
148 148 if amt > 0:
149 149 bar += '>'
150 150 bar += ' ' * (progwidth - amt)
151 151 else:
152 152 progwidth -= 3
153 153 self.indetcount += 1
154 154 # mod the count by twice the width so we can make the
155 155 # cursor bounce between the right and left sides
156 156 amt = self.indetcount % (2 * progwidth)
157 157 amt -= progwidth
158 158 bar = (' ' * int(progwidth - abs(amt)) + '<=>' +
159 159 ' ' * int(abs(amt)))
160 160 prog = ''.join(('[', bar , ']'))
161 161 out = spacejoin(head, prog, tail)
162 162 else:
163 163 out = spacejoin(head, tail)
164 164 sys.stderr.write('\r' + out[:termwidth])
165 165 sys.stderr.flush()
166 166
167 167 def clear(self):
168 168 if not shouldprint(self.ui):
169 169 return
170 170 sys.stderr.write('\r%s\r' % (' ' * self.width()))
171 171
172 172 def complete(self):
173 173 if not shouldprint(self.ui):
174 174 return
175 175 if self.ui.configbool('progress', 'clear-complete', default=True):
176 176 self.clear()
177 177 else:
178 178 sys.stderr.write('\n')
179 179 sys.stderr.flush()
180 180
181 181 def width(self):
182 182 tw = self.ui.termwidth()
183 183 return min(int(self.ui.config('progress', 'width', default=tw)), tw)
184 184
185 185 def estimate(self, topic, pos, total, now):
186 if total is None:
187 return ''
186 188 initialpos = self.startvals[topic]
187 189 target = total - initialpos
188 190 delta = pos - initialpos
189 191 if delta > 0:
190 192 elapsed = now - self.starttimes[topic]
191 193 if elapsed > float(
192 194 self.ui.config('progress', 'estimate', default=2)):
193 195 seconds = (elapsed * (target - delta)) // delta + 1
194 196 return fmtremaining(seconds)
195 197 return ''
196 198
197 199 def progress(self, topic, pos, item='', unit='', total=None):
198 200 now = time.time()
199 201 if pos is None:
200 202 self.starttimes.pop(topic, None)
201 203 self.startvals.pop(topic, None)
202 204 self.topicstates.pop(topic, None)
203 205 # reset the progress bar if this is the outermost topic
204 206 if self.topics and self.topics[0] == topic and self.printed:
205 207 self.complete()
206 208 self.resetstate()
207 209 # truncate the list of topics assuming all topics within
208 210 # this one are also closed
209 211 if topic in self.topics:
210 212 self.topics = self.topics[:self.topics.index(topic)]
211 213 else:
212 214 if topic not in self.topics:
213 215 self.starttimes[topic] = now
214 216 self.startvals[topic] = pos
215 217 self.topics.append(topic)
216 218 self.topicstates[topic] = pos, item, unit, total
217 219 if now - self.lastprint >= self.refresh and self.topics:
218 220 self.lastprint = now
219 221 current = self.topics[-1]
220 222 self.show(now, topic, *self.topicstates[topic])
221 223
222 224 def uisetup(ui):
223 225 class progressui(ui.__class__):
224 226 _progbar = None
225 227
226 228 def progress(self, *args, **opts):
227 229 self._progbar.progress(*args, **opts)
228 230 return super(progressui, self).progress(*args, **opts)
229 231
230 232 def write(self, *args, **opts):
231 233 if self._progbar.printed:
232 234 self._progbar.clear()
233 235 return super(progressui, self).write(*args, **opts)
234 236
235 237 def write_err(self, *args, **opts):
236 238 if self._progbar.printed:
237 239 self._progbar.clear()
238 240 return super(progressui, self).write_err(*args, **opts)
239 241
240 242 # Apps that derive a class from ui.ui() can use
241 243 # setconfig('progress', 'disable', 'True') to disable this extension
242 244 if ui.configbool('progress', 'disable'):
243 245 return
244 246 if shouldprint(ui) and not ui.debugflag and not ui.quiet:
245 247 ui.__class__ = progressui
246 248 # we instantiate one globally shared progress bar to avoid
247 249 # competing progress bars when multiple UI objects get created
248 250 if not progressui._progbar:
249 251 progressui._progbar = progbar(ui)
250 252
251 253 def reposetup(ui, repo):
252 254 uisetup(repo.ui)
@@ -1,143 +1,149
1 1
2 2 $ cat > loop.py <<EOF
3 3 > from mercurial import commands
4 4 >
5 5 > def loop(ui, loops, **opts):
6 6 > loops = int(loops)
7 7 > total = None
8 8 > if loops >= 0:
9 9 > total = loops
10 10 > if opts.get('total', None):
11 11 > total = int(opts.get('total'))
12 12 > loops = abs(loops)
13 13 >
14 14 > for i in range(loops):
15 15 > ui.progress('loop', i, 'loop.%d' % i, 'loopnum', total)
16 16 > ui.progress('loop', None, 'loop.done', 'loopnum', total)
17 17 >
18 18 > commands.norepo += " loop"
19 19 >
20 20 > cmdtable = {
21 21 > "loop": (loop, [('', 'total', '', 'override for total')],
22 22 > 'hg loop LOOPS'),
23 23 > }
24 24 > EOF
25 25
26 26 $ echo "[extensions]" >> $HGRCPATH
27 27 $ echo "progress=" >> $HGRCPATH
28 28 $ echo "loop=`pwd`/loop.py" >> $HGRCPATH
29 29 $ echo "[progress]" >> $HGRCPATH
30 30 $ echo "format = topic bar number" >> $HGRCPATH
31 31 $ echo "assume-tty=1" >> $HGRCPATH
32 32 $ echo "width=60" >> $HGRCPATH
33 33
34 34 test default params, display nothing because of delay
35 35
36 36 $ hg -y loop 3 2>&1 | $TESTDIR/filtercr.py
37 37
38 38 $ echo "delay=0" >> $HGRCPATH
39 39 $ echo "refresh=0" >> $HGRCPATH
40 40
41 41 test with delay=0, refresh=0
42 42
43 43 $ hg -y loop 3 2>&1 | $TESTDIR/filtercr.py
44 44
45 45 loop [ ] 0/3
46 46 loop [===============> ] 1/3
47 47 loop [===============================> ] 2/3
48 48 \r (esc)
49 49
50 50 test refresh is taken in account
51 51
52 52 $ hg -y --config progress.refresh=100 loop 3 2>&1 | $TESTDIR/filtercr.py
53 53
54 54
55 55 test format options 1
56 56
57 57 $ hg -y --config 'progress.format=number topic item+2' loop 2 2>&1 \
58 58 > | $TESTDIR/filtercr.py
59 59
60 60 0/2 loop lo
61 61 1/2 loop lo
62 62 \r (esc)
63 63
64 64 test format options 2
65 65
66 66 $ hg -y --config 'progress.format=number item-3 bar' loop 2 2>&1 \
67 67 > | $TESTDIR/filtercr.py
68 68
69 69 0/2 p.0 [ ]
70 70 1/2 p.1 [=======================> ]
71 71 \r (esc)
72 72
73 73 test format options and indeterminate progress
74 74
75 75 $ hg -y --config 'progress.format=number item bar' loop -- -2 2>&1 \
76 76 > | $TESTDIR/filtercr.py
77 77
78 78 0 loop.0 [ <=> ]
79 79 1 loop.1 [ <=> ]
80 80 \r (esc)
81 81
82 82 make sure things don't fall over if count > total
83 83
84 84 $ hg -y loop --total 4 6 2>&1 | $TESTDIR/filtercr.py
85 85
86 86 loop [ ] 0/4
87 87 loop [===========> ] 1/4
88 88 loop [=======================> ] 2/4
89 89 loop [===================================> ] 3/4
90 90 loop [===============================================>] 4/4
91 91 loop [ <=> ] 5/4
92 92 \r (esc)
93 93
94 94 test immediate progress completion
95 95
96 96 $ hg -y loop 0 2>&1 | $TESTDIR/filtercr.py
97 97
98 98
99 99 test delay time estimates
100 100
101 101 $ cat > mocktime.py <<EOF
102 102 > import os
103 103 > import time
104 104 >
105 105 > class mocktime(object):
106 106 > def __init__(self, increment):
107 107 > self.time = 0
108 108 > self.increment = increment
109 109 > def __call__(self):
110 110 > self.time += self.increment
111 111 > return self.time
112 112 >
113 113 > def uisetup(ui):
114 114 > time.time = mocktime(int(os.environ.get('MOCKTIME', '11')))
115 115 > EOF
116 116
117 117 $ echo "[extensions]" > $HGRCPATH
118 118 $ echo "mocktime=`pwd`/mocktime.py" >> $HGRCPATH
119 119 $ echo "progress=" >> $HGRCPATH
120 120 $ echo "loop=`pwd`/loop.py" >> $HGRCPATH
121 121 $ echo "[progress]" >> $HGRCPATH
122 122 $ echo "assume-tty=1" >> $HGRCPATH
123 123 $ echo "delay=25" >> $HGRCPATH
124 124 $ echo "width=60" >> $HGRCPATH
125 125
126 126 $ hg -y loop 8 2>&1 | python $TESTDIR/filtercr.py
127 127
128 128 loop [=========> ] 2/8 1m07s
129 129 loop [===============> ] 3/8 56s
130 130 loop [=====================> ] 4/8 45s
131 131 loop [==========================> ] 5/8 34s
132 132 loop [================================> ] 6/8 23s
133 133 loop [=====================================> ] 7/8 12s
134 134 \r (esc)
135 135
136 136 $ MOCKTIME=10000 hg -y loop 4 2>&1 | python $TESTDIR/filtercr.py
137 137
138 138 loop [ ] 0/4
139 139 loop [=========> ] 1/4 8h21m
140 140 loop [====================> ] 2/4 5h34m
141 141 loop [==============================> ] 3/4 2h47m
142 142 \r (esc)
143 143
144 Time estimates should not fail when there's no end point:
145 $ hg -y loop -- -4 2>&1 | python $TESTDIR/filtercr.py
146
147 loop [ <=> ] 2
148 loop [ <=> ] 3
149 \r (esc)
General Comments 0
You need to be logged in to leave comments. Login now