1# -*- test-case-name: twisted.conch.test.test_helper -*-
2# Copyright (c) Twisted Matrix Laboratories.
3# See LICENSE for details.
4
5from twisted.conch.insults import helper
6from twisted.conch.insults.insults import G0, G1, G2, G3
7from twisted.conch.insults.insults import modes, privateModes
8from twisted.conch.insults.insults import (
9    NORMAL, BOLD, UNDERLINE, BLINK, REVERSE_VIDEO)
10
11from twisted.trial import unittest
12
13WIDTH = 80
14HEIGHT = 24
15
16class BufferTestCase(unittest.TestCase):
17    def setUp(self):
18        self.term = helper.TerminalBuffer()
19        self.term.connectionMade()
20
21    def testInitialState(self):
22        self.assertEqual(self.term.width, WIDTH)
23        self.assertEqual(self.term.height, HEIGHT)
24        self.assertEqual(str(self.term),
25                          '\n' * (HEIGHT - 1))
26        self.assertEqual(self.term.reportCursorPosition(), (0, 0))
27
28
29    def test_initialPrivateModes(self):
30        """
31        Verify that only DEC Auto Wrap Mode (DECAWM) and DEC Text Cursor Enable
32        Mode (DECTCEM) are initially in the Set Mode (SM) state.
33        """
34        self.assertEqual(
35            {privateModes.AUTO_WRAP: True,
36             privateModes.CURSOR_MODE: True},
37            self.term.privateModes)
38
39
40    def test_carriageReturn(self):
41        """
42        C{"\r"} moves the cursor to the first column in the current row.
43        """
44        self.term.cursorForward(5)
45        self.term.cursorDown(3)
46        self.assertEqual(self.term.reportCursorPosition(), (5, 3))
47        self.term.insertAtCursor("\r")
48        self.assertEqual(self.term.reportCursorPosition(), (0, 3))
49
50
51    def test_linefeed(self):
52        """
53        C{"\n"} moves the cursor to the next row without changing the column.
54        """
55        self.term.cursorForward(5)
56        self.assertEqual(self.term.reportCursorPosition(), (5, 0))
57        self.term.insertAtCursor("\n")
58        self.assertEqual(self.term.reportCursorPosition(), (5, 1))
59
60
61    def test_newline(self):
62        """
63        C{write} transforms C{"\n"} into C{"\r\n"}.
64        """
65        self.term.cursorForward(5)
66        self.term.cursorDown(3)
67        self.assertEqual(self.term.reportCursorPosition(), (5, 3))
68        self.term.write("\n")
69        self.assertEqual(self.term.reportCursorPosition(), (0, 4))
70
71
72    def test_setPrivateModes(self):
73        """
74        Verify that L{helper.TerminalBuffer.setPrivateModes} changes the Set
75        Mode (SM) state to "set" for the private modes it is passed.
76        """
77        expected = self.term.privateModes.copy()
78        self.term.setPrivateModes([privateModes.SCROLL, privateModes.SCREEN])
79        expected[privateModes.SCROLL] = True
80        expected[privateModes.SCREEN] = True
81        self.assertEqual(expected, self.term.privateModes)
82
83
84    def test_resetPrivateModes(self):
85        """
86        Verify that L{helper.TerminalBuffer.resetPrivateModes} changes the Set
87        Mode (SM) state to "reset" for the private modes it is passed.
88        """
89        expected = self.term.privateModes.copy()
90        self.term.resetPrivateModes([privateModes.AUTO_WRAP, privateModes.CURSOR_MODE])
91        del expected[privateModes.AUTO_WRAP]
92        del expected[privateModes.CURSOR_MODE]
93        self.assertEqual(expected, self.term.privateModes)
94
95
96    def testCursorDown(self):
97        self.term.cursorDown(3)
98        self.assertEqual(self.term.reportCursorPosition(), (0, 3))
99        self.term.cursorDown()
100        self.assertEqual(self.term.reportCursorPosition(), (0, 4))
101        self.term.cursorDown(HEIGHT)
102        self.assertEqual(self.term.reportCursorPosition(), (0, HEIGHT - 1))
103
104    def testCursorUp(self):
105        self.term.cursorUp(5)
106        self.assertEqual(self.term.reportCursorPosition(), (0, 0))
107
108        self.term.cursorDown(20)
109        self.term.cursorUp(1)
110        self.assertEqual(self.term.reportCursorPosition(), (0, 19))
111
112        self.term.cursorUp(19)
113        self.assertEqual(self.term.reportCursorPosition(), (0, 0))
114
115    def testCursorForward(self):
116        self.term.cursorForward(2)
117        self.assertEqual(self.term.reportCursorPosition(), (2, 0))
118        self.term.cursorForward(2)
119        self.assertEqual(self.term.reportCursorPosition(), (4, 0))
120        self.term.cursorForward(WIDTH)
121        self.assertEqual(self.term.reportCursorPosition(), (WIDTH, 0))
122
123    def testCursorBackward(self):
124        self.term.cursorForward(10)
125        self.term.cursorBackward(2)
126        self.assertEqual(self.term.reportCursorPosition(), (8, 0))
127        self.term.cursorBackward(7)
128        self.assertEqual(self.term.reportCursorPosition(), (1, 0))
129        self.term.cursorBackward(1)
130        self.assertEqual(self.term.reportCursorPosition(), (0, 0))
131        self.term.cursorBackward(1)
132        self.assertEqual(self.term.reportCursorPosition(), (0, 0))
133
134    def testCursorPositioning(self):
135        self.term.cursorPosition(3, 9)
136        self.assertEqual(self.term.reportCursorPosition(), (3, 9))
137
138    def testSimpleWriting(self):
139        s = "Hello, world."
140        self.term.write(s)
141        self.assertEqual(
142            str(self.term),
143            s + '\n' +
144            '\n' * (HEIGHT - 2))
145
146    def testOvertype(self):
147        s = "hello, world."
148        self.term.write(s)
149        self.term.cursorBackward(len(s))
150        self.term.resetModes([modes.IRM])
151        self.term.write("H")
152        self.assertEqual(
153            str(self.term),
154            ("H" + s[1:]) + '\n' +
155            '\n' * (HEIGHT - 2))
156
157    def testInsert(self):
158        s = "ello, world."
159        self.term.write(s)
160        self.term.cursorBackward(len(s))
161        self.term.setModes([modes.IRM])
162        self.term.write("H")
163        self.assertEqual(
164            str(self.term),
165            ("H" + s) + '\n' +
166            '\n' * (HEIGHT - 2))
167
168    def testWritingInTheMiddle(self):
169        s = "Hello, world."
170        self.term.cursorDown(5)
171        self.term.cursorForward(5)
172        self.term.write(s)
173        self.assertEqual(
174            str(self.term),
175            '\n' * 5 +
176            (self.term.fill * 5) + s + '\n' +
177            '\n' * (HEIGHT - 7))
178
179    def testWritingWrappedAtEndOfLine(self):
180        s = "Hello, world."
181        self.term.cursorForward(WIDTH - 5)
182        self.term.write(s)
183        self.assertEqual(
184            str(self.term),
185            s[:5].rjust(WIDTH) + '\n' +
186            s[5:] + '\n' +
187            '\n' * (HEIGHT - 3))
188
189    def testIndex(self):
190        self.term.index()
191        self.assertEqual(self.term.reportCursorPosition(), (0, 1))
192        self.term.cursorDown(HEIGHT)
193        self.assertEqual(self.term.reportCursorPosition(), (0, HEIGHT - 1))
194        self.term.index()
195        self.assertEqual(self.term.reportCursorPosition(), (0, HEIGHT - 1))
196
197    def testReverseIndex(self):
198        self.term.reverseIndex()
199        self.assertEqual(self.term.reportCursorPosition(), (0, 0))
200        self.term.cursorDown(2)
201        self.assertEqual(self.term.reportCursorPosition(), (0, 2))
202        self.term.reverseIndex()
203        self.assertEqual(self.term.reportCursorPosition(), (0, 1))
204
205    def test_nextLine(self):
206        """
207        C{nextLine} positions the cursor at the beginning of the row below the
208        current row.
209        """
210        self.term.nextLine()
211        self.assertEqual(self.term.reportCursorPosition(), (0, 1))
212        self.term.cursorForward(5)
213        self.assertEqual(self.term.reportCursorPosition(), (5, 1))
214        self.term.nextLine()
215        self.assertEqual(self.term.reportCursorPosition(), (0, 2))
216
217    def testSaveCursor(self):
218        self.term.cursorDown(5)
219        self.term.cursorForward(7)
220        self.assertEqual(self.term.reportCursorPosition(), (7, 5))
221        self.term.saveCursor()
222        self.term.cursorDown(7)
223        self.term.cursorBackward(3)
224        self.assertEqual(self.term.reportCursorPosition(), (4, 12))
225        self.term.restoreCursor()
226        self.assertEqual(self.term.reportCursorPosition(), (7, 5))
227
228    def testSingleShifts(self):
229        self.term.singleShift2()
230        self.term.write('Hi')
231
232        ch = self.term.getCharacter(0, 0)
233        self.assertEqual(ch[0], 'H')
234        self.assertEqual(ch[1].charset, G2)
235
236        ch = self.term.getCharacter(1, 0)
237        self.assertEqual(ch[0], 'i')
238        self.assertEqual(ch[1].charset, G0)
239
240        self.term.singleShift3()
241        self.term.write('!!')
242
243        ch = self.term.getCharacter(2, 0)
244        self.assertEqual(ch[0], '!')
245        self.assertEqual(ch[1].charset, G3)
246
247        ch = self.term.getCharacter(3, 0)
248        self.assertEqual(ch[0], '!')
249        self.assertEqual(ch[1].charset, G0)
250
251    def testShifting(self):
252        s1 = "Hello"
253        s2 = "World"
254        s3 = "Bye!"
255        self.term.write("Hello\n")
256        self.term.shiftOut()
257        self.term.write("World\n")
258        self.term.shiftIn()
259        self.term.write("Bye!\n")
260
261        g = G0
262        h = 0
263        for s in (s1, s2, s3):
264            for i in range(len(s)):
265                ch = self.term.getCharacter(i, h)
266                self.assertEqual(ch[0], s[i])
267                self.assertEqual(ch[1].charset, g)
268            g = g == G0 and G1 or G0
269            h += 1
270
271    def testGraphicRendition(self):
272        self.term.selectGraphicRendition(BOLD, UNDERLINE, BLINK, REVERSE_VIDEO)
273        self.term.write('W')
274        self.term.selectGraphicRendition(NORMAL)
275        self.term.write('X')
276        self.term.selectGraphicRendition(BLINK)
277        self.term.write('Y')
278        self.term.selectGraphicRendition(BOLD)
279        self.term.write('Z')
280
281        ch = self.term.getCharacter(0, 0)
282        self.assertEqual(ch[0], 'W')
283        self.assertTrue(ch[1].bold)
284        self.assertTrue(ch[1].underline)
285        self.assertTrue(ch[1].blink)
286        self.assertTrue(ch[1].reverseVideo)
287
288        ch = self.term.getCharacter(1, 0)
289        self.assertEqual(ch[0], 'X')
290        self.assertFalse(ch[1].bold)
291        self.assertFalse(ch[1].underline)
292        self.assertFalse(ch[1].blink)
293        self.assertFalse(ch[1].reverseVideo)
294
295        ch = self.term.getCharacter(2, 0)
296        self.assertEqual(ch[0], 'Y')
297        self.assertTrue(ch[1].blink)
298        self.assertFalse(ch[1].bold)
299        self.assertFalse(ch[1].underline)
300        self.assertFalse(ch[1].reverseVideo)
301
302        ch = self.term.getCharacter(3, 0)
303        self.assertEqual(ch[0], 'Z')
304        self.assertTrue(ch[1].blink)
305        self.assertTrue(ch[1].bold)
306        self.assertFalse(ch[1].underline)
307        self.assertFalse(ch[1].reverseVideo)
308
309    def testColorAttributes(self):
310        s1 = "Merry xmas"
311        s2 = "Just kidding"
312        self.term.selectGraphicRendition(helper.FOREGROUND + helper.RED,
313                                         helper.BACKGROUND + helper.GREEN)
314        self.term.write(s1 + "\n")
315        self.term.selectGraphicRendition(NORMAL)
316        self.term.write(s2 + "\n")
317
318        for i in range(len(s1)):
319            ch = self.term.getCharacter(i, 0)
320            self.assertEqual(ch[0], s1[i])
321            self.assertEqual(ch[1].charset, G0)
322            self.assertEqual(ch[1].bold, False)
323            self.assertEqual(ch[1].underline, False)
324            self.assertEqual(ch[1].blink, False)
325            self.assertEqual(ch[1].reverseVideo, False)
326            self.assertEqual(ch[1].foreground, helper.RED)
327            self.assertEqual(ch[1].background, helper.GREEN)
328
329        for i in range(len(s2)):
330            ch = self.term.getCharacter(i, 1)
331            self.assertEqual(ch[0], s2[i])
332            self.assertEqual(ch[1].charset, G0)
333            self.assertEqual(ch[1].bold, False)
334            self.assertEqual(ch[1].underline, False)
335            self.assertEqual(ch[1].blink, False)
336            self.assertEqual(ch[1].reverseVideo, False)
337            self.assertEqual(ch[1].foreground, helper.WHITE)
338            self.assertEqual(ch[1].background, helper.BLACK)
339
340    def testEraseLine(self):
341        s1 = 'line 1'
342        s2 = 'line 2'
343        s3 = 'line 3'
344        self.term.write('\n'.join((s1, s2, s3)) + '\n')
345        self.term.cursorPosition(1, 1)
346        self.term.eraseLine()
347
348        self.assertEqual(
349            str(self.term),
350            s1 + '\n' +
351            '\n' +
352            s3 + '\n' +
353            '\n' * (HEIGHT - 4))
354
355    def testEraseToLineEnd(self):
356        s = 'Hello, world.'
357        self.term.write(s)
358        self.term.cursorBackward(5)
359        self.term.eraseToLineEnd()
360        self.assertEqual(
361            str(self.term),
362            s[:-5] + '\n' +
363            '\n' * (HEIGHT - 2))
364
365    def testEraseToLineBeginning(self):
366        s = 'Hello, world.'
367        self.term.write(s)
368        self.term.cursorBackward(5)
369        self.term.eraseToLineBeginning()
370        self.assertEqual(
371            str(self.term),
372            s[-4:].rjust(len(s)) + '\n' +
373            '\n' * (HEIGHT - 2))
374
375    def testEraseDisplay(self):
376        self.term.write('Hello world\n')
377        self.term.write('Goodbye world\n')
378        self.term.eraseDisplay()
379
380        self.assertEqual(
381            str(self.term),
382            '\n' * (HEIGHT - 1))
383
384    def testEraseToDisplayEnd(self):
385        s1 = "Hello world"
386        s2 = "Goodbye world"
387        self.term.write('\n'.join((s1, s2, '')))
388        self.term.cursorPosition(5, 1)
389        self.term.eraseToDisplayEnd()
390
391        self.assertEqual(
392            str(self.term),
393            s1 + '\n' +
394            s2[:5] + '\n' +
395            '\n' * (HEIGHT - 3))
396
397    def testEraseToDisplayBeginning(self):
398        s1 = "Hello world"
399        s2 = "Goodbye world"
400        self.term.write('\n'.join((s1, s2)))
401        self.term.cursorPosition(5, 1)
402        self.term.eraseToDisplayBeginning()
403
404        self.assertEqual(
405            str(self.term),
406            '\n' +
407            s2[6:].rjust(len(s2)) + '\n' +
408            '\n' * (HEIGHT - 3))
409
410    def testLineInsertion(self):
411        s1 = "Hello world"
412        s2 = "Goodbye world"
413        self.term.write('\n'.join((s1, s2)))
414        self.term.cursorPosition(7, 1)
415        self.term.insertLine()
416
417        self.assertEqual(
418            str(self.term),
419            s1 + '\n' +
420            '\n' +
421            s2 + '\n' +
422            '\n' * (HEIGHT - 4))
423
424    def testLineDeletion(self):
425        s1 = "Hello world"
426        s2 = "Middle words"
427        s3 = "Goodbye world"
428        self.term.write('\n'.join((s1, s2, s3)))
429        self.term.cursorPosition(9, 1)
430        self.term.deleteLine()
431
432        self.assertEqual(
433            str(self.term),
434            s1 + '\n' +
435            s3 + '\n' +
436            '\n' * (HEIGHT - 3))
437
438class FakeDelayedCall:
439    called = False
440    cancelled = False
441    def __init__(self, fs, timeout, f, a, kw):
442        self.fs = fs
443        self.timeout = timeout
444        self.f = f
445        self.a = a
446        self.kw = kw
447
448    def active(self):
449        return not (self.cancelled or self.called)
450
451    def cancel(self):
452        self.cancelled = True
453#        self.fs.calls.remove(self)
454
455    def call(self):
456        self.called = True
457        self.f(*self.a, **self.kw)
458
459class FakeScheduler:
460    def __init__(self):
461        self.calls = []
462
463    def callLater(self, timeout, f, *a, **kw):
464        self.calls.append(FakeDelayedCall(self, timeout, f, a, kw))
465        return self.calls[-1]
466
467class ExpectTestCase(unittest.TestCase):
468    def setUp(self):
469        self.term = helper.ExpectableBuffer()
470        self.term.connectionMade()
471        self.fs = FakeScheduler()
472
473    def testSimpleString(self):
474        result = []
475        d = self.term.expect("hello world", timeout=1, scheduler=self.fs)
476        d.addCallback(result.append)
477
478        self.term.write("greeting puny earthlings\n")
479        self.assertFalse(result)
480        self.term.write("hello world\n")
481        self.assertTrue(result)
482        self.assertEqual(result[0].group(), "hello world")
483        self.assertEqual(len(self.fs.calls), 1)
484        self.assertFalse(self.fs.calls[0].active())
485
486    def testBrokenUpString(self):
487        result = []
488        d = self.term.expect("hello world")
489        d.addCallback(result.append)
490
491        self.assertFalse(result)
492        self.term.write("hello ")
493        self.assertFalse(result)
494        self.term.write("worl")
495        self.assertFalse(result)
496        self.term.write("d")
497        self.assertTrue(result)
498        self.assertEqual(result[0].group(), "hello world")
499
500
501    def testMultiple(self):
502        result = []
503        d1 = self.term.expect("hello ")
504        d1.addCallback(result.append)
505        d2 = self.term.expect("world")
506        d2.addCallback(result.append)
507
508        self.assertFalse(result)
509        self.term.write("hello")
510        self.assertFalse(result)
511        self.term.write(" ")
512        self.assertEqual(len(result), 1)
513        self.term.write("world")
514        self.assertEqual(len(result), 2)
515        self.assertEqual(result[0].group(), "hello ")
516        self.assertEqual(result[1].group(), "world")
517
518    def testSynchronous(self):
519        self.term.write("hello world")
520
521        result = []
522        d = self.term.expect("hello world")
523        d.addCallback(result.append)
524        self.assertTrue(result)
525        self.assertEqual(result[0].group(), "hello world")
526
527    def testMultipleSynchronous(self):
528        self.term.write("goodbye world")
529
530        result = []
531        d1 = self.term.expect("bye")
532        d1.addCallback(result.append)
533        d2 = self.term.expect("world")
534        d2.addCallback(result.append)
535
536        self.assertEqual(len(result), 2)
537        self.assertEqual(result[0].group(), "bye")
538        self.assertEqual(result[1].group(), "world")
539
540    def _cbTestTimeoutFailure(self, res):
541        self.assertTrue(hasattr(res, 'type'))
542        self.assertEqual(res.type, helper.ExpectationTimeout)
543
544    def testTimeoutFailure(self):
545        d = self.term.expect("hello world", timeout=1, scheduler=self.fs)
546        d.addBoth(self._cbTestTimeoutFailure)
547        self.fs.calls[0].call()
548
549    def testOverlappingTimeout(self):
550        self.term.write("not zoomtastic")
551
552        result = []
553        d1 = self.term.expect("hello world", timeout=1, scheduler=self.fs)
554        d1.addBoth(self._cbTestTimeoutFailure)
555        d2 = self.term.expect("zoom")
556        d2.addCallback(result.append)
557
558        self.fs.calls[0].call()
559
560        self.assertEqual(len(result), 1)
561        self.assertEqual(result[0].group(), "zoom")
562
563
564
565class CharacterAttributeTests(unittest.TestCase):
566    """
567    Tests for L{twisted.conch.insults.helper.CharacterAttribute}.
568    """
569    def test_equality(self):
570        """
571        L{CharacterAttribute}s must have matching character attribute values
572        (bold, blink, underline, etc) with the same values to be considered
573        equal.
574        """
575        self.assertEqual(
576            helper.CharacterAttribute(),
577            helper.CharacterAttribute())
578
579        self.assertEqual(
580            helper.CharacterAttribute(),
581            helper.CharacterAttribute(charset=G0))
582
583        self.assertEqual(
584            helper.CharacterAttribute(
585                bold=True, underline=True, blink=False, reverseVideo=True,
586                foreground=helper.BLUE),
587            helper.CharacterAttribute(
588                bold=True, underline=True, blink=False, reverseVideo=True,
589                foreground=helper.BLUE))
590
591        self.assertNotEqual(
592            helper.CharacterAttribute(),
593            helper.CharacterAttribute(charset=G1))
594
595        self.assertNotEqual(
596            helper.CharacterAttribute(bold=True),
597            helper.CharacterAttribute(bold=False))
598
599
600    def test_wantOneDeprecated(self):
601        """
602        L{twisted.conch.insults.helper.CharacterAttribute.wantOne} emits
603        a deprecation warning when invoked.
604        """
605        # Trigger the deprecation warning.
606        helper._FormattingState().wantOne(bold=True)
607
608        warningsShown = self.flushWarnings([self.test_wantOneDeprecated])
609        self.assertEqual(len(warningsShown), 1)
610        self.assertEqual(warningsShown[0]['category'], DeprecationWarning)
611        self.assertEqual(
612            warningsShown[0]['message'],
613            'twisted.conch.insults.helper.wantOne was deprecated in '
614            'Twisted 13.1.0')
615