##// END OF EJS Templates
Added 256-color support to Qt console escape sequence processing.
epatters -
Show More
@@ -1,233 +1,334 b''
1 """ Utilities for processing ANSI escape codes and special ASCII characters.
1 """ Utilities for processing ANSI escape codes and special ASCII characters.
2 """
2 """
3 #-----------------------------------------------------------------------------
3 #-----------------------------------------------------------------------------
4 # Imports
4 # Imports
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6
6
7 # Standard library imports
7 # Standard library imports
8 from collections import namedtuple
8 from collections import namedtuple
9 import re
9 import re
10
10
11 # System library imports
11 # System library imports
12 from IPython.external.qt import QtCore, QtGui
12 from IPython.external.qt import QtCore, QtGui
13
13
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 # Constants and datatypes
15 # Constants and datatypes
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17
17
18 # An action for erase requests (ED and EL commands).
18 # An action for erase requests (ED and EL commands).
19 EraseAction = namedtuple('EraseAction', ['action', 'area', 'erase_to'])
19 EraseAction = namedtuple('EraseAction', ['action', 'area', 'erase_to'])
20
20
21 # An action for cursor move requests (CUU, CUD, CUF, CUB, CNL, CPL, CHA, CUP,
21 # An action for cursor move requests (CUU, CUD, CUF, CUB, CNL, CPL, CHA, CUP,
22 # and HVP commands).
22 # and HVP commands).
23 # FIXME: Not implemented in AnsiCodeProcessor.
23 # FIXME: Not implemented in AnsiCodeProcessor.
24 MoveAction = namedtuple('MoveAction', ['action', 'dir', 'unit', 'count'])
24 MoveAction = namedtuple('MoveAction', ['action', 'dir', 'unit', 'count'])
25
25
26 # An action for scroll requests (SU and ST) and form feeds.
26 # An action for scroll requests (SU and ST) and form feeds.
27 ScrollAction = namedtuple('ScrollAction', ['action', 'dir', 'unit', 'count'])
27 ScrollAction = namedtuple('ScrollAction', ['action', 'dir', 'unit', 'count'])
28
28
29 # Regular expressions.
30 CSI_COMMANDS = 'ABCDEFGHJKSTfmnsu'
31 CSI_SUBPATTERN = '\[(.*?)([%s])' % CSI_COMMANDS
32 OSC_SUBPATTERN = '\](.*?)[\x07\x1b]'
33 ANSI_PATTERN = re.compile('\x01?\x1b(%s|%s)\x02?' % \
34 (CSI_SUBPATTERN, OSC_SUBPATTERN))
35 SPECIAL_PATTERN = re.compile('([\f])')
36
29 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
30 # Classes
38 # Classes
31 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
32
40
33 class AnsiCodeProcessor(object):
41 class AnsiCodeProcessor(object):
34 """ Translates special ASCII characters and ANSI escape codes into readable
42 """ Translates special ASCII characters and ANSI escape codes into readable
35 attributes.
43 attributes. It also supports a few non-standard, xterm-specific codes.
36 """
44 """
37
45
38 # Whether to increase intensity or set boldness for SGR code 1.
46 # Whether to increase intensity or set boldness for SGR code 1.
39 # (Different terminals handle this in different ways.)
47 # (Different terminals handle this in different ways.)
40 bold_text_enabled = False
48 bold_text_enabled = False
41
49
42 # Protected class variables.
50 # We provide an empty default color map because subclasses will likely want
43 _ansi_commands = 'ABCDEFGHJKSTfmnsu'
51 # to use a custom color format.
44 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)([%s])\x02?' % _ansi_commands)
52 default_color_map = {}
45 _special_pattern = re.compile('([\f])')
46
53
47 #---------------------------------------------------------------------------
54 #---------------------------------------------------------------------------
48 # AnsiCodeProcessor interface
55 # AnsiCodeProcessor interface
49 #---------------------------------------------------------------------------
56 #---------------------------------------------------------------------------
50
57
51 def __init__(self):
58 def __init__(self):
52 self.actions = []
59 self.actions = []
60 self.color_map = self.default_color_map.copy()
53 self.reset_sgr()
61 self.reset_sgr()
54
62
55 def reset_sgr(self):
63 def reset_sgr(self):
56 """ Reset graphics attributs to their default values.
64 """ Reset graphics attributs to their default values.
57 """
65 """
58 self.intensity = 0
66 self.intensity = 0
59 self.italic = False
67 self.italic = False
60 self.bold = False
68 self.bold = False
61 self.underline = False
69 self.underline = False
62 self.foreground_color = None
70 self.foreground_color = None
63 self.background_color = None
71 self.background_color = None
64
72
65 def split_string(self, string):
73 def split_string(self, string):
66 """ Yields substrings for which the same escape code applies.
74 """ Yields substrings for which the same escape code applies.
67 """
75 """
68 self.actions = []
76 self.actions = []
69 start = 0
77 start = 0
70
78
71 for match in self._ansi_pattern.finditer(string):
79 for match in ANSI_PATTERN.finditer(string):
72 raw = string[start:match.start()]
80 raw = string[start:match.start()]
73 substring = self._special_pattern.sub(self._replace_special, raw)
81 substring = SPECIAL_PATTERN.sub(self._replace_special, raw)
74 if substring or self.actions:
82 if substring or self.actions:
75 yield substring
83 yield substring
76 start = match.end()
84 start = match.end()
77
85
78 self.actions = []
86 self.actions = []
79 try:
87 groups = filter(lambda x: x is not None, match.groups())
80 params = []
88 params = [ param for param in groups[1].split(';') if param ]
81 for param in match.group(1).split(';'):
89 if groups[0].startswith('['):
82 if param:
90 # Case 1: CSI code.
83 params.append(int(param))
91 try:
84 except ValueError:
92 params = map(int, params)
85 # Silently discard badly formed escape codes.
93 except ValueError:
86 pass
94 # Silently discard badly formed codes.
87 else:
95 pass
88 self.set_csi_code(match.group(2), params)
96 else:
97 self.set_csi_code(groups[2], params)
98
99 elif groups[0].startswith(']'):
100 # Case 2: OSC code.
101 self.set_osc_code(params)
89
102
90 raw = string[start:]
103 raw = string[start:]
91 substring = self._special_pattern.sub(self._replace_special, raw)
104 substring = SPECIAL_PATTERN.sub(self._replace_special, raw)
92 if substring or self.actions:
105 if substring or self.actions:
93 yield substring
106 yield substring
94
107
95 def set_csi_code(self, command, params=[]):
108 def set_csi_code(self, command, params=[]):
96 """ Set attributes based on CSI (Control Sequence Introducer) code.
109 """ Set attributes based on CSI (Control Sequence Introducer) code.
97
110
98 Parameters
111 Parameters
99 ----------
112 ----------
100 command : str
113 command : str
101 The code identifier, i.e. the final character in the sequence.
114 The code identifier, i.e. the final character in the sequence.
102
115
103 params : sequence of integers, optional
116 params : sequence of integers, optional
104 The parameter codes for the command.
117 The parameter codes for the command.
105 """
118 """
106 if command == 'm': # SGR - Select Graphic Rendition
119 if command == 'm': # SGR - Select Graphic Rendition
107 if params:
120 if params:
108 for code in params:
121 self.set_sgr_code(params)
109 self.set_sgr_code(code)
110 else:
122 else:
111 self.set_sgr_code(0)
123 self.set_sgr_code([0])
112
124
113 elif (command == 'J' or # ED - Erase Data
125 elif (command == 'J' or # ED - Erase Data
114 command == 'K'): # EL - Erase in Line
126 command == 'K'): # EL - Erase in Line
115 code = params[0] if params else 0
127 code = params[0] if params else 0
116 if 0 <= code <= 2:
128 if 0 <= code <= 2:
117 area = 'screen' if command == 'J' else 'line'
129 area = 'screen' if command == 'J' else 'line'
118 if code == 0:
130 if code == 0:
119 erase_to = 'end'
131 erase_to = 'end'
120 elif code == 1:
132 elif code == 1:
121 erase_to = 'start'
133 erase_to = 'start'
122 elif code == 2:
134 elif code == 2:
123 erase_to = 'all'
135 erase_to = 'all'
124 self.actions.append(EraseAction('erase', area, erase_to))
136 self.actions.append(EraseAction('erase', area, erase_to))
125
137
126 elif (command == 'S' or # SU - Scroll Up
138 elif (command == 'S' or # SU - Scroll Up
127 command == 'T'): # SD - Scroll Down
139 command == 'T'): # SD - Scroll Down
128 dir = 'up' if command == 'S' else 'down'
140 dir = 'up' if command == 'S' else 'down'
129 count = params[0] if params else 1
141 count = params[0] if params else 1
130 self.actions.append(ScrollAction('scroll', dir, 'line', count))
142 self.actions.append(ScrollAction('scroll', dir, 'line', count))
143
144 def set_osc_code(self, params):
145 """ Set attributes based on OSC (Operating System Command) parameters.
131
146
132 def set_sgr_code(self, code):
147 Parameters
133 """ Set attributes based on SGR (Select Graphic Rendition) code.
148 ----------
149 params : sequence of str
150 The parameters for the command.
134 """
151 """
152 try:
153 command = int(params.pop(0))
154 except (IndexError, ValueError):
155 return
156
157 if command == 4:
158 # xterm-specific: set color number to color spec.
159 try:
160 color = int(params.pop(0))
161 spec = params.pop(0)
162 self.color_map[color] = self._parse_xterm_color_spec(spec)
163 except (IndexError, ValueError):
164 pass
165
166 def set_sgr_code(self, params):
167 """ Set attributes based on SGR (Select Graphic Rendition) codes.
168
169 Parameters
170 ----------
171 params : sequence of ints
172 A list of SGR codes for one or more SGR commands. Usually this
173 sequence will have one element per command, although certain
174 xterm-specific commands requires multiple elements.
175 """
176 # Always consume the first parameter.
177 if not params:
178 return
179 code = params.pop(0)
180
135 if code == 0:
181 if code == 0:
136 self.reset_sgr()
182 self.reset_sgr()
137 elif code == 1:
183 elif code == 1:
138 if self.bold_text_enabled:
184 if self.bold_text_enabled:
139 self.bold = True
185 self.bold = True
140 else:
186 else:
141 self.intensity = 1
187 self.intensity = 1
142 elif code == 2:
188 elif code == 2:
143 self.intensity = 0
189 self.intensity = 0
144 elif code == 3:
190 elif code == 3:
145 self.italic = True
191 self.italic = True
146 elif code == 4:
192 elif code == 4:
147 self.underline = True
193 self.underline = True
148 elif code == 22:
194 elif code == 22:
149 self.intensity = 0
195 self.intensity = 0
150 self.bold = False
196 self.bold = False
151 elif code == 23:
197 elif code == 23:
152 self.italic = False
198 self.italic = False
153 elif code == 24:
199 elif code == 24:
154 self.underline = False
200 self.underline = False
155 elif code >= 30 and code <= 37:
201 elif code >= 30 and code <= 37:
156 self.foreground_color = code - 30
202 self.foreground_color = code - 30
203 elif code == 38 and params and params.pop(0) == 5:
204 # xterm-specific: 256 color support.
205 if params:
206 self.foreground_color = params.pop(0)
157 elif code == 39:
207 elif code == 39:
158 self.foreground_color = None
208 self.foreground_color = None
159 elif code >= 40 and code <= 47:
209 elif code >= 40 and code <= 47:
160 self.background_color = code - 40
210 self.background_color = code - 40
211 elif code == 48 and params and params.pop(0) == 5:
212 # xterm-specific: 256 color support.
213 if params:
214 self.background_color = params.pop(0)
161 elif code == 49:
215 elif code == 49:
162 self.background_color = None
216 self.background_color = None
163
217
218 # Recurse with unconsumed parameters.
219 self.set_sgr_code(params)
220
164 #---------------------------------------------------------------------------
221 #---------------------------------------------------------------------------
165 # Protected interface
222 # Protected interface
166 #---------------------------------------------------------------------------
223 #---------------------------------------------------------------------------
167
224
225 def _parse_xterm_color_spec(self, spec):
226 if spec.startswith('rgb:'):
227 return tuple(map(lambda x: int(x, 16), spec[4:].split('/')))
228 elif spec.startswith('rgbi:'):
229 return tuple(map(lambda x: int(float(x) * 255),
230 spec[5:].split('/')))
231 elif spec == '?':
232 raise ValueError('Unsupported xterm color spec')
233 return spec
234
168 def _replace_special(self, match):
235 def _replace_special(self, match):
169 special = match.group(1)
236 special = match.group(1)
170 if special == '\f':
237 if special == '\f':
171 self.actions.append(ScrollAction('scroll', 'down', 'page', 1))
238 self.actions.append(ScrollAction('scroll', 'down', 'page', 1))
172 return ''
239 return ''
173
240
174
241
175 class QtAnsiCodeProcessor(AnsiCodeProcessor):
242 class QtAnsiCodeProcessor(AnsiCodeProcessor):
176 """ Translates ANSI escape codes into QTextCharFormats.
243 """ Translates ANSI escape codes into QTextCharFormats.
177 """
244 """
178
245
179 # A map from color codes to RGB colors.
246 # A map from ANSI color codes to SVG color names or RGB(A) tuples.
180 default_map = (# Normal, Bright/Light ANSI color code
247 darkbg_color_map = {
181 ('black', 'grey'), # 0: black
248 0 : 'black', # black
182 ('darkred', 'red'), # 1: red
249 1 : 'darkred', # red
183 ('darkgreen', 'lime'), # 2: green
250 2 : 'darkgreen', # green
184 ('brown', 'yellow'), # 3: yellow
251 3 : 'brown', # yellow
185 ('darkblue', 'deepskyblue'), # 4: blue
252 4 : 'darkblue', # blue
186 ('darkviolet', 'magenta'), # 5: magenta
253 5 : 'darkviolet', # magenta
187 ('steelblue', 'cyan'), # 6: cyan
254 6 : 'steelblue', # cyan
188 ('grey', 'white')) # 7: white
255 7 : 'grey', # white
256 8 : 'grey', # black (bright)
257 9 : 'red', # red (bright)
258 10 : 'lime', # green (bright)
259 11 : 'yellow', # yellow (bright)
260 12 : 'deepskyblue', # blue (bright)
261 13 : 'magenta', # magenta (bright)
262 14 : 'cyan', # cyan (bright)
263 15 : 'white' } # white (bright)
264
265 # Set the default color map for super class.
266 default_color_map = darkbg_color_map.copy()
267
268 def get_color(self, color, intensity=0):
269 """ Returns a QColor for a given color code, or None if one cannot be
270 constructed.
271 """
272 if color is None:
273 return None
189
274
190 def __init__(self):
275 # Adjust for intensity, if possible.
191 super(QtAnsiCodeProcessor, self).__init__()
276 if color < 8 and intensity > 0:
192 self.color_map = self.default_map
277 color += 8
278
279 constructor = self.color_map.get(color, None)
280 if isinstance(constructor, basestring):
281 # If this is an X11 color name, we just hope there is a close SVG
282 # color name. We could use QColor's static method
283 # 'setAllowX11ColorNames()', but this is global and only available
284 # on X11. It seems cleaner to aim for uniformity of behavior.
285 return QtGui.QColor(constructor)
286
287 elif isinstance(constructor, (tuple, list)):
288 return QtGui.QColor(*constructor)
289
290 return None
193
291
194 def get_format(self):
292 def get_format(self):
195 """ Returns a QTextCharFormat that encodes the current style attributes.
293 """ Returns a QTextCharFormat that encodes the current style attributes.
196 """
294 """
197 format = QtGui.QTextCharFormat()
295 format = QtGui.QTextCharFormat()
198
296
199 # Set foreground color
297 # Set foreground color
200 if self.foreground_color is not None:
298 qcolor = self.get_color(self.foreground_color, self.intensity)
201 color = self.color_map[self.foreground_color][self.intensity]
299 if qcolor is not None:
202 format.setForeground(QtGui.QColor(color))
300 format.setForeground(qcolor)
203
301
204 # Set background color
302 # Set background color
205 if self.background_color is not None:
303 qcolor = self.get_color(self.background_color, self.intensity)
206 color = self.color_map[self.background_color][self.intensity]
304 if qcolor is not None:
207 format.setBackground(QtGui.QColor(color))
305 format.setBackground(qcolor)
208
306
209 # Set font weight/style options
307 # Set font weight/style options
210 if self.bold:
308 if self.bold:
211 format.setFontWeight(QtGui.QFont.Bold)
309 format.setFontWeight(QtGui.QFont.Bold)
212 else:
310 else:
213 format.setFontWeight(QtGui.QFont.Normal)
311 format.setFontWeight(QtGui.QFont.Normal)
214 format.setFontItalic(self.italic)
312 format.setFontItalic(self.italic)
215 format.setFontUnderline(self.underline)
313 format.setFontUnderline(self.underline)
216
314
217 return format
315 return format
218
316
219 def set_background_color(self, color):
317 def set_background_color(self, color):
220 """ Given a background color (a QColor), attempt to set a color map
318 """ Given a background color (a QColor), attempt to set a color map
221 that will be aesthetically pleasing.
319 that will be aesthetically pleasing.
222 """
320 """
223 if color.value() < 127:
321 # Set a new default color map.
224 # Colors appropriate for a terminal with a dark background.
322 self.default_color_map = self.darkbg_color_map.copy()
225 self.color_map = self.default_map
226
323
227 else:
324 if color.value() >= 127:
228 # Colors appropriate for a terminal with a light background. For
325 # Colors appropriate for a terminal with a light background. For
229 # now, only use non-bright colors...
326 # now, only use non-bright colors...
230 self.color_map = [ (pair[0], pair[0]) for pair in self.default_map ]
327 for i in xrange(8):
328 self.default_color_map[i + 8] = self.default_color_map[i]
231
329
232 # ...and replace white with black.
330 # ...and replace white with black.
233 self.color_map[7] = ('black', 'black')
331 self.default_color_map[7] = self.default_color_map[15] = 'black'
332
333 # Update the current color map with the new defaults.
334 self.color_map.update(self.default_color_map)
@@ -1,84 +1,107 b''
1 # Standard library imports
1 # Standard library imports
2 import unittest
2 import unittest
3
3
4 # Local imports
4 # Local imports
5 from IPython.frontend.qt.console.ansi_code_processor import AnsiCodeProcessor
5 from IPython.frontend.qt.console.ansi_code_processor import AnsiCodeProcessor
6
6
7
7
8 class TestAnsiCodeProcessor(unittest.TestCase):
8 class TestAnsiCodeProcessor(unittest.TestCase):
9
9
10 def setUp(self):
10 def setUp(self):
11 self.processor = AnsiCodeProcessor()
11 self.processor = AnsiCodeProcessor()
12
12
13 def testClear(self):
13 def test_clear(self):
14 """ Do control sequences for clearing the console work?
15 """
14 string = '\x1b[2J\x1b[K'
16 string = '\x1b[2J\x1b[K'
15 i = -1
17 i = -1
16 for i, substring in enumerate(self.processor.split_string(string)):
18 for i, substring in enumerate(self.processor.split_string(string)):
17 if i == 0:
19 if i == 0:
18 self.assertEquals(len(self.processor.actions), 1)
20 self.assertEquals(len(self.processor.actions), 1)
19 action = self.processor.actions[0]
21 action = self.processor.actions[0]
20 self.assertEquals(action.action, 'erase')
22 self.assertEquals(action.action, 'erase')
21 self.assertEquals(action.area, 'screen')
23 self.assertEquals(action.area, 'screen')
22 self.assertEquals(action.erase_to, 'all')
24 self.assertEquals(action.erase_to, 'all')
23 elif i == 1:
25 elif i == 1:
24 self.assertEquals(len(self.processor.actions), 1)
26 self.assertEquals(len(self.processor.actions), 1)
25 action = self.processor.actions[0]
27 action = self.processor.actions[0]
26 self.assertEquals(action.action, 'erase')
28 self.assertEquals(action.action, 'erase')
27 self.assertEquals(action.area, 'line')
29 self.assertEquals(action.area, 'line')
28 self.assertEquals(action.erase_to, 'end')
30 self.assertEquals(action.erase_to, 'end')
29 else:
31 else:
30 self.fail('Too many substrings.')
32 self.fail('Too many substrings.')
31 self.assertEquals(i, 1, 'Too few substrings.')
33 self.assertEquals(i, 1, 'Too few substrings.')
32
34
33 def testColors(self):
35 def test_colors(self):
34 string = "first\x1b[34mblue\x1b[0mlast"
36 """ Do basic controls sequences for colors work?
37 """
38 string = 'first\x1b[34mblue\x1b[0mlast'
35 i = -1
39 i = -1
36 for i, substring in enumerate(self.processor.split_string(string)):
40 for i, substring in enumerate(self.processor.split_string(string)):
37 if i == 0:
41 if i == 0:
38 self.assertEquals(substring, 'first')
42 self.assertEquals(substring, 'first')
39 self.assertEquals(self.processor.foreground_color, None)
43 self.assertEquals(self.processor.foreground_color, None)
40 elif i == 1:
44 elif i == 1:
41 self.assertEquals(substring, 'blue')
45 self.assertEquals(substring, 'blue')
42 self.assertEquals(self.processor.foreground_color, 4)
46 self.assertEquals(self.processor.foreground_color, 4)
43 elif i == 2:
47 elif i == 2:
44 self.assertEquals(substring, 'last')
48 self.assertEquals(substring, 'last')
45 self.assertEquals(self.processor.foreground_color, None)
49 self.assertEquals(self.processor.foreground_color, None)
46 else:
50 else:
47 self.fail('Too many substrings.')
51 self.fail('Too many substrings.')
48 self.assertEquals(i, 2, 'Too few substrings.')
52 self.assertEquals(i, 2, 'Too few substrings.')
49
53
50 def testScroll(self):
54 def test_colors_xterm(self):
55 """ Do xterm-specific control sequences for colors work?
56 """
57 string = '\x1b]4;20;rgb:ff/ff/ff\x1b' \
58 '\x1b]4;25;rgbi:1.0/1.0/1.0\x1b'
59 substrings = list(self.processor.split_string(string))
60 desired = { 20 : (255, 255, 255),
61 25 : (255, 255, 255) }
62 self.assertEquals(self.processor.color_map, desired)
63
64 string = '\x1b[38;5;20m\x1b[48;5;25m'
65 substrings = list(self.processor.split_string(string))
66 self.assertEquals(self.processor.foreground_color, 20)
67 self.assertEquals(self.processor.background_color, 25)
68
69 def test_scroll(self):
70 """ Do control sequences for scrolling the buffer work?
71 """
51 string = '\x1b[5S\x1b[T'
72 string = '\x1b[5S\x1b[T'
52 i = -1
73 i = -1
53 for i, substring in enumerate(self.processor.split_string(string)):
74 for i, substring in enumerate(self.processor.split_string(string)):
54 if i == 0:
75 if i == 0:
55 self.assertEquals(len(self.processor.actions), 1)
76 self.assertEquals(len(self.processor.actions), 1)
56 action = self.processor.actions[0]
77 action = self.processor.actions[0]
57 self.assertEquals(action.action, 'scroll')
78 self.assertEquals(action.action, 'scroll')
58 self.assertEquals(action.dir, 'up')
79 self.assertEquals(action.dir, 'up')
59 self.assertEquals(action.unit, 'line')
80 self.assertEquals(action.unit, 'line')
60 self.assertEquals(action.count, 5)
81 self.assertEquals(action.count, 5)
61 elif i == 1:
82 elif i == 1:
62 self.assertEquals(len(self.processor.actions), 1)
83 self.assertEquals(len(self.processor.actions), 1)
63 action = self.processor.actions[0]
84 action = self.processor.actions[0]
64 self.assertEquals(action.action, 'scroll')
85 self.assertEquals(action.action, 'scroll')
65 self.assertEquals(action.dir, 'down')
86 self.assertEquals(action.dir, 'down')
66 self.assertEquals(action.unit, 'line')
87 self.assertEquals(action.unit, 'line')
67 self.assertEquals(action.count, 1)
88 self.assertEquals(action.count, 1)
68 else:
89 else:
69 self.fail('Too many substrings.')
90 self.fail('Too many substrings.')
70 self.assertEquals(i, 1, 'Too few substrings.')
91 self.assertEquals(i, 1, 'Too few substrings.')
71
92
72 def testSpecials(self):
93 def test_specials(self):
94 """ Are special characters processed correctly?
95 """
73 string = '\f' # form feed
96 string = '\f' # form feed
74 self.assertEquals(list(self.processor.split_string(string)), [''])
97 self.assertEquals(list(self.processor.split_string(string)), [''])
75 self.assertEquals(len(self.processor.actions), 1)
98 self.assertEquals(len(self.processor.actions), 1)
76 action = self.processor.actions[0]
99 action = self.processor.actions[0]
77 self.assertEquals(action.action, 'scroll')
100 self.assertEquals(action.action, 'scroll')
78 self.assertEquals(action.dir, 'down')
101 self.assertEquals(action.dir, 'down')
79 self.assertEquals(action.unit, 'page')
102 self.assertEquals(action.unit, 'page')
80 self.assertEquals(action.count, 1)
103 self.assertEquals(action.count, 1)
81
104
82
105
83 if __name__ == '__main__':
106 if __name__ == '__main__':
84 unittest.main()
107 unittest.main()
General Comments 0
You need to be logged in to leave comments. Login now