Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 21 additions & 13 deletions eden/scm/sapling/ext/interactiveui.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import os
import sys
from enum import Enum
from typing import Union
from typing import Callable, Union

from sapling import error, scmutil, util
from sapling.i18n import _, _x
Expand Down Expand Up @@ -73,26 +73,34 @@ def clearscreen(out):
# Note: some changes have been made from the source code


def getchar() -> Union[None, bytes, str]:
fd = sys.stdin.fileno()
def _readraw(fd: int) -> bytes:
"""read the bytes currently available on `fd` with the tty in raw mode"""
attr = termios.tcgetattr(fd)
try:
tty.setraw(fd)
return os.read(fd, 32)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, attr)


def getchar(
stdin=None, readraw: Callable[[int], bytes] = _readraw
) -> Union[None, bytes, str]:
# `stdin` and `readraw` are seams for tests. Production callers leave them
# unset and get sys.stdin plus the real raw terminal read.
if stdin is None:
stdin = sys.stdin
fd = stdin.fileno()
if not os.isatty(fd):
# TODO: figure out tests
return None
ch = None
try:
attr = termios.tcgetattr(fd)
try:
tty.setraw(fd)
ch = os.read(fd, 32)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, attr)
ch = readraw(fd)
except termios.error:
# pyre-fixme[61]: `ch` is undefined, or not always defined.
if ch is None:
ch = ""
# pyre-fixme[61]: `ch` is undefined, or not always defined.
if ch == b"\x03" or ch == b"\x04":
return None
# pyre-fixme[61]: `ch` is undefined, or not always defined.
return ch


Expand Down
102 changes: 102 additions & 0 deletions eden/scm/tests/test-interactiveui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2.

import os
import unittest

import silenttestrunner
from sapling import util
from sapling.ext import interactiveui


class fakestdin:
"""stands in for sys.stdin, backed by a real fd"""

def __init__(self, fd):
self._fd = fd

def fileno(self):
return self._fd


@unittest.skipIf(util.iswindows, "interactiveui does not support Windows")
class testgetchar(unittest.TestCase):
def setUp(self):
# a pipe is a real non-tty fd; a pty secondary is a real tty fd
self.pipefd, writefd = os.pipe()
self.addCleanup(os.close, self.pipefd)
self.addCleanup(os.close, writefd)
primary, self.ttyfd = os.openpty()
self.addCleanup(os.close, self.ttyfd)
self.addCleanup(os.close, primary)

def _getchar(self, keys, fd=None):
"""run getchar() against `fd`, with the raw read faked to yield `keys`

Returns the getchar() result and the fds the read was attempted on, so
tests can assert the terminal is left alone on the non-tty path.
"""
reads = []

def readraw(fd):
reads.append(fd)
return keys

result = interactiveui.getchar(
stdin=fakestdin(self.ttyfd if fd is None else fd), readraw=readraw
)
return result, reads

def testnottty(self):
result, reads = self._getchar(b"j", fd=self.pipefd)
self.assertIsNone(result)
# the terminal must not be touched at all when stdin is not a tty
self.assertEqual(reads, [])

def testkeypress(self):
result, reads = self._getchar(b"j")
self.assertEqual(result, b"j")
self.assertEqual(reads, [self.ttyfd])

def testinterrupt(self):
# ctrl-c and ctrl-d end the session rather than returning a keypress
self.assertIsNone(self._getchar(b"\x03")[0])
self.assertIsNone(self._getchar(b"\x04")[0])

def testescapesequence(self):
result, _reads = self._getchar(b"\x1b[A")
self.assertEqual(result, interactiveui.viewframe.KEY_UP)
self.assertEqual(
interactiveui._splitkeypresses(result),
[interactiveui.viewframe.KEY_UP],
)

def testescapesequencerun(self):
# a single read can deliver several arrow keys plus a normal key
result, _reads = self._getchar(b"\x1b[A\x1b[Dj")
self.assertEqual(
interactiveui._splitkeypresses(result),
[
interactiveui.viewframe.KEY_UP,
interactiveui.viewframe.KEY_LEFT,
b"j",
],
)


class testrealread(unittest.TestCase):
def testnottty(self):
# the default readraw is never reached for a non-tty, so this exercises
# the real production code path end to end
readfd, writefd = os.pipe()
try:
self.assertIsNone(interactiveui.getchar(stdin=fakestdin(readfd)))
finally:
os.close(readfd)
os.close(writefd)


if __name__ == "__main__":
silenttestrunner.main(__name__)
Loading