3
from twisted.trial import unittest
4
from twisted.conch.insults import insults, helper
6
from imaginary.wiring import textserver
7
from imaginary import text as T
8
from imaginary import unc
11
if "currentAttrs" not in kw:
12
kw["currentAttrs"] = T.AttributeSet()
13
return ''.join(list(T.flatten(a, **kw)))
17
nkw = dict.fromkeys(T.AttributeSet.__names__, T.unset)
19
AS = T.AttributeSet(**nkw)
20
x = T.neutral.clone().update(AS)
24
return '\x1b[' + ';'.join(s) + 'm'
26
class AttributeSetting(unittest.TestCase):
28
B, U, R, L = '1', '4', '7', '5'
31
# From To Expected Name?
32
(AS(), AS(), '', 'no transition'),
33
(AS(), NN(bold=True), E(B), 'enable bold'),
34
(AS(), NN(), '', 'do not care about bold'),
35
(AS(), NN(fg='1'), E('31'), 'red foreground'),
36
(AS(), NN(bg='2'), E('42'), 'green background')]
38
blink = AS(blink=True)
40
(blink, NN(blink=True), '', 'no transition'),
41
(blink, NN(), '', 'do not care'),
42
(blink, NN(blink=False), E('0'), 'turn off blink')])
44
bu = AS(blink=True, underline=True)
46
(bu, bu, '', 'no transition'),
47
(bu, NN(blink=True), '', 'do not care'),
48
(bu, NN(blink=True, underline=False), E('0', L), 'disable underline'),
49
(bu, NN(underline=False), E('0', L), 'disable underline'),
50
(bu, NN(blink=False, underline=False), E('0'), 'turn it all off')])
53
(AS(), NN(fg='1'), E('31'), 'set fg red'),
54
(AS(), NN(bg='2'), E('42'), 'set bg green'),
55
(AS(), NN(fg='3', bg='4'), E('33', '44'), 'set fg and bg'),
56
(AS(fg='5'), NN(fg='9'), E('0'), 'reset fg'),
57
(AS(bg='6'), NN(bg='9'), E('0'), 'reset bg'),
58
(AS(fg='7', bg='0'), NN(), '', 'do nothing'),
59
(AS(fg='1', bg='2'), NN(fg='9'), E('0', '42'), 'reset fg'),
60
(AS(fg='3', bg='4'), NN(bg='9'), E('0', '33'), 'reset bg'),
61
(AS(fg='5'), NN(bg='3'), E('43'), 'enable bg'),
62
(AS(bg='7'), NN(bg='3'), E('43'), 'switch bg'),
63
(AS(fg='2'), NN(fg='1'), E('31'), 'switch fg'),
64
(AS(fg='6'), NN(fg='6'), '', 'request same'),
68
# trial should support this use case.
70
for n, (start, finish, output, msg) in enumerate(self.testData):
71
got = finish.toVT102(start)
74
failures.append((got, output, str(n) + ': ' + msg))
78
('received', 'expected', "what's up"))
79
raise unittest.FailTest(pprint.pformat(failures))
82
class Colorization(unittest.TestCase):
83
def testTrivialStringOnly(self):
89
F('hello', ' ', 'world'),
96
def testNoColors(self):
98
F([[T.fg.green, "hi", T.bg.yellow, "there", T.bold, "no"], "normal", [T.fg.blue, "blue"], "notblue"],
100
"hitherenonormalbluenotblue")
102
def testTrivialStringAndList(self):
108
F(['hello', ' ', 'world']),
112
F(['hello', [' ', ['world']]]),
116
F(['hello', [' '], 'world']),
120
F([[['hello'], ' '], 'world']),
123
def testTrivialStringWithAttributes(self):
125
F('hello world', currentAttrs=T.fg.normal),
128
def testForegroundColorization(self):
129
for code, sym in [(30, T.fg.black),
138
F(sym, 'hello world'),
139
'\x1b[%dmhello world\x1b[0m' % (code,))
141
def testBackgroundColorization(self):
142
for code, sym in [(40, T.bg.black),
151
F(sym, 'hello world'),
152
'\x1b[%dmhello world\x1b[0m' % (code,))
154
def testExtraAttributes(self):
155
for code, sym in [(1, T.bold),
160
F(sym, 'hello world'),
161
'\x1b[%dmhello world\x1b[0m' % (code,))
163
self.assertEquals(F(T.fg.normal, 'hello world'), 'hello world')
164
self.assertEquals(F(T.bg.normal, 'hello world'), 'hello world')
166
def testSmallerNestedCharacterState(self):
167
a = F([T.fg.red, 'red', [T.reverseVideo, '!red']])
168
b = '\x1b[31mred\x1b[7m!red\x1b[0m'
170
self.assertEquals(a, b)
172
def _assertECMA48Equality(self, a, b):
173
errorLines = ['received\t\texpected']
174
for la, lb in zip(unc.prettystring(a).splitlines(),
175
unc.prettystring(b).splitlines()):
176
errorLines.append(la + '\t\t' + lb)
178
self.assertEquals(a, b, '\nERROR!\n' + '\n'.join(errorLines))
180
def testNestedCharacterState(self):
181
a = F([T.fg.red, 'hello'],
182
[T.bg.green, 'world',
183
[T.bg.normal, 'more worlds']],
185
[T.fg.red, T.bg.blue, 'multicolor',
186
[T.reverseVideo, 'colormulti']])
191
'\x1b[0;31;44mmulticolor'
192
'\x1b[7mcolormulti\x1b[0m')
193
self._assertECMA48Equality(a, b)
195
self._assertECMA48Equality(
196
F([T.fg.red, [T.bg.green, [T.blink, 'hello'], ' '], 'world']),
197
'\x1b[5;31;42mhello\x1b[0;31;42m \x1b[0;31mworld\x1b[0m')
199
self._assertECMA48Equality(
203
[T.fg.normal, 'world'],
205
'\x1b[36;45mHello \x1b[0;45mworld\x1b[36m.\x1b[0m')
208
class AsynchronousIncrementalUTF8DecoderTestCase(unittest.TestCase):
210
Test L{imaginary.wiring.textserver.AsynchronousIncrementalUTF8Decoder}
213
self.a = textserver.AsynchronousIncrementalUTF8Decoder()
217
for ch in 'hello, world':
219
self.assertEquals(self.a.get(), u'hello, world')
222
def testTwoBytes(self):
223
character = u'\N{LATIN CAPITAL LETTER A WITH GRAVE}'
224
byte1, byte2 = character.encode('utf-8')
226
self.assertEquals(self.a.get(), u'')
228
self.assertEquals(self.a.get(), character)
231
def testThreeBytes(self):
232
character = u'\N{HIRAGANA LETTER A}'
233
byte1, byte2, byte3 = character.encode('utf-8')
235
self.assertEquals(self.a.get(), u'')
237
self.assertEquals(self.a.get(), u'')
239
self.assertEquals(self.a.get(), character)
242
def testFourBytes(self):
243
character = u'\N{BYZANTINE MUSICAL SYMBOL PSILI}'
244
byte1, byte2, byte3, byte4 = character.encode('utf-8')
246
self.assertEquals(self.a.get(), u'')
248
self.assertEquals(self.a.get(), u'')
250
self.assertEquals(self.a.get(), u'')
252
self.assertEquals(self.a.get(), character)
255
def testSeveralCharacters(self):
256
char1 = u'\N{LATIN CAPITAL LETTER A WITH GRAVE}'
257
byte1, byte2 = char1.encode('utf-8')
259
self.assertEquals(self.a.get(), u'')
261
self.assertEquals(self.a.get(), char1)
263
char2 = u'\N{HIRAGANA LETTER A}'
264
byte1, byte2, byte3 = char2.encode('utf-8')
266
self.assertEquals(self.a.get(), char1)
268
self.assertEquals(self.a.get(), char1)
270
self.assertEquals(self.a.get(), char1 + char2)
272
char3 = u'\N{BYZANTINE MUSICAL SYMBOL PSILI}'
273
byte1, byte2, byte3, byte4 = char3.encode('utf-8')
275
self.assertEquals(self.a.get(), char1 + char2)
277
self.assertEquals(self.a.get(), char1 + char2)
279
self.assertEquals(self.a.get(), char1 + char2)
281
self.assertEquals(self.a.get(), char1 + char2 + char3)
287
self.assertEquals(self.a.get(), u'')
290
def testNarrowWidth(self):
292
self.assertEquals(self.a.width(), 1)
295
def testWideWidth(self):
296
map(self.a.add, u'\N{HIRAGANA LETTER A}'.encode('utf-8'))
297
self.assertEquals(self.a.width(), 2)
300
def testMixedWidth(self):
302
map(self.a.add, u'\N{HIRAGANA LETTER A}'.encode('utf-8'))
303
self.assertEquals(self.a.width(), 3)
304
map(self.a.add, u'\N{HIRAGANA LETTER A}'.encode('utf-8'))
306
self.assertEquals(self.a.width(), 6)
311
self.assertEquals(self.a.pop(), u'a')
312
self.assertEquals(self.a.get(), u'')
315
def testPopIncompleteCharacter(self):
316
self.a.add(u'\N{LATIN CAPITAL LETTER A WITH GRAVE}'.encode('utf-8')[0])
317
self.assertRaises(ValueError, self.a.pop)
320
def testPopEmpty(self):
321
self.assertRaises(IndexError, self.a.pop)
325
class UTF8TerminalBuffer(helper.TerminalBuffer):
326
# An unfortunate hack'n'paste of
327
# helper.TerminalBuffer.insertAtCursor
328
def insertAtCursor(self, b):
331
elif b == '\n' or self.x >= self.width:
334
# The following conditional has been changed from the original.
335
if (b > '\x7f' or b in string.printable) and b not in '\r\n':
336
ch = (b, self._currentCharacterAttributes())
337
if self.modes.get(insults.modes.IRM):
338
self.lines[self.y][self.x:self.x] = [ch]
339
self.lines[self.y].pop()
341
self.lines[self.y][self.x] = ch
346
class TextServerTestCase(unittest.TestCase):
348
Tests for L{imaginary.wiring.textserver}
352
self.terminal = UTF8TerminalBuffer()
353
self.terminal.connectionMade()
354
self.protocol = textserver.TextServerBase()
355
self.protocol.makeConnection(self.terminal)
356
self.terminal.reset()
359
def testUTF8Input(self):
360
character = u'\N{HIRAGANA LETTER A}'
361
for ch in character.encode('utf-8'):
362
self.protocol.keystrokeReceived(ch, None)
363
self.assertEquals(str(self.terminal).strip(),
364
character.encode('utf-8'))
367
def testNonPrintableInput(self):
369
self.protocol.lineReceived = lines.append
371
bytes = range(0, 127)
372
for special in [8, 10, 13]:
373
bytes.remove(special)
374
bytes = ''.join(map(chr, bytes)) + 'WOO'
376
expected = ''.join([byte for byte in bytes
377
if byte >= ' ' and byte != '\x7f'])
378
expected = expected.decode('ascii')
381
self.protocol.keystrokeReceived(byte, None)
382
self.protocol.keystrokeReceived('\n', None)
383
self.assertEquals(lines, [expected])
386
def _backspaceTest(self, character, width):
388
Verify that a character is erased properly by a backspace.
390
@param character: The character to receive, echo, and then have
392
@type character: C{unicode}
394
@param width: How many columns the given character is expected to be
395
when rendered. This is how many backspaces are expected to be
396
required to erase it.
400
self.protocol.lineReceived = lines.append
402
# The terminal emulator we use doesn't know enough unicode for this
403
# test to work out right. So we'll fake it ourselves here.
405
self.terminal.write = written.append
407
self.protocol.keystrokeReceived('a', None)
408
self.protocol.keystrokeReceived(character.encode('utf-8'), None)
409
self.protocol.keystrokeReceived(self.terminal.BACKSPACE, None)
413
'a' + character.encode('utf-8') +
414
'\b' * width + ' ' * width + '\b' * width)
417
def test_eraseNarrowWithBackspace(self):
419
If a backspace keystroke is received when the cursor is positioned
420
directly after a character with an I{east asian width} of I{narrow},
421
the character is removed from the input buffer and the character is
422
erased from the client display with a C{'\b \b'} sequence.
424
self._backspaceTest(u'x', 1)
427
def test_eraseWideWithBackspace(self):
429
If a backspace keystroke is received when the cursor is positioned
430
directly after a character with an I{east asian width} of I{wide}, the
431
character is removed from the input buffer and the character is
432
erased from the client display with a C{'\b\b \b\b'} sequence.
434
self._backspaceTest(u'\u1100', 2)
437
def test_eraseFullwidthWithBackspace(self):
439
If a backspace keystroke is received when the cursor is positioned
440
directly after a character with an I{east asian width} of
441
I{fullwidth}, character is removed from the input buffer and the
442
character is erased from the client display with a C{'\b\b \b\b'}
445
self._backspaceTest(u'\u3000', 2)
448
def test_eraseHalfwidthWithBackspace(self):
450
If a backspace keystroke is received when the cursor is positioned
451
directly after a character with an I{east asian width} of
452
I{halfwidth}, character is removed from the input buffer and the
453
character is erased from the client display with a C{'\b \b'}
456
self._backspaceTest(u'\u20a9', 1)
459
def test_eraseNeutralWithBackspace(self):
461
If a backspace keystroke is received when the cursor is positioned
462
directly after a character with an I{east asian width} of
463
I{neutral}, character is removed from the input buffer and the
464
character is erased from the client display with a C{'\b \b'}
467
self._backspaceTest(u'\xa0', 1)
470
def test_eraseAmbiguousWithBackspace(self):
472
If a backspace keystroke is received when the cursor is positioned
473
directly after a character with an I{east asian width} of
474
I{neutral}, character is removed from the input buffer and the
475
character is erased from the client display with a C{'\b \b'}
478
self._backspaceTest(u'\xa1', 1)