Beispiel #1
0
    def test_screen_events_empty_groups(self):
        """screen_events should never return an empty group of events"""
        records = [
            AsciiCastV2Header(version=2, width=80, height=24, theme=THEME),
            AsciiCastV2Event(0, 'o', 'i', None),
            AsciiCastV2Event(1, 'o', '', None),
            AsciiCastV2Event(2, 'o', '', None),
            AsciiCastV2Event(3, 'o', '', None),
        ]
        expected_events = [
            term.Configuration(80, 24),
            [
                term.DisplayLine(0, {
                    0: anim.CharacterCell('i'),
                    1: CURSOR_CHAR
                }, 0),
            ],
            [
                term.DisplayLine(0, {
                    0: anim.CharacterCell('i'),
                    1: CURSOR_CHAR
                }, 0, 4000),
            ]
        ]

        events = term.screen_events(records, 1, None, last_frame_dur=1000)

        list_events = [next(events)]
        list_events.extend([le for le in events])

        z = itertools.zip_longest(expected_events, list_events)
        for count, (expected_item, item) in enumerate(z):
            with self.subTest(case='No empty group - item #{}'.format(count)):
Beispiel #2
0
def _group_by_time(event_records, min_rec_duration, max_rec_duration, last_rec_duration):
    """Merge event records together if they are close enough

    The time elapsed between two consecutive event records returned by this
    function is guaranteed to be at least min_rec_duration.

    The duration of each record is also computed. Any record with a duration
    greater than `max_rec_duration` will see its duration reduce to this value.
    The duration of the last record can't be computed and is simply set to
    `last_rec_duration`.

    :param event_records: Sequence of records in asciicast v2 format
    :param min_rec_duration: Minimum time between two records returned by the
    function in milliseconds.
    :param max_rec_duration: Maximum duration of a record in milliseconds
    :param last_rec_duration: Duration of the last record in milliseconds
    :return: Sequence of records with duration
    """
    # TODO: itertools.accumulate?
    current_string = ''
    current_time = 0
    dropped_time = 0

    if max_rec_duration:
        max_rec_duration /= 1000

    for event_record in event_records:
        assert isinstance(event_record, AsciiCastV2Event)
        # Silently ignoring the duration on input records is a source
        # of confusion so fail hard if the duration is set
        assert event_record.duration is None
        if event_record.event_type != 'o':
            continue

        time_between_events = event_record.time - (current_time + dropped_time)
        if time_between_events * 1000 >= min_rec_duration:
            if max_rec_duration:
                if max_rec_duration < time_between_events:
                    dropped_time += time_between_events - max_rec_duration
                    time_between_events = max_rec_duration
            accumulator_event = AsciiCastV2Event(time=current_time,
                                                 event_type='o',
                                                 event_data=current_string,
                                                 duration=time_between_events)
            yield accumulator_event
            current_string = ''
            current_time += time_between_events

        current_string += event_record.event_data

    accumulator_event = AsciiCastV2Event(time=current_time,
                                         event_type='o',
                                         event_data=current_string,
                                         duration=last_rec_duration / 1000)
    yield accumulator_event
Beispiel #3
0
    def test__group_by_time(self):
        event_records = [
            AsciiCastV2Event(0, 'o', b'1', None),
            AsciiCastV2Event(5, 'o', b'2', None),
            AsciiCastV2Event(8, 'o', b'3', None),
            AsciiCastV2Event(20, 'o', b'4', None),
            AsciiCastV2Event(21, 'o', b'5', None),
            AsciiCastV2Event(30, 'o', b'6', None),
            AsciiCastV2Event(31, 'o', b'7', None),
            AsciiCastV2Event(32, 'o', b'8', None),
            AsciiCastV2Event(33, 'o', b'9', None),
            AsciiCastV2Event(43, 'o', b'10', None),
        ]

        with self.subTest(case='maximum record duration'):
Beispiel #4
0
    def test_timed_frames_unprintable_chars(self):
        # Ensure zero width characters in terminal output does not result
        # in Pyte dropping all following data
        # Issue https://github.com/nbedos/termtosvg/issues/89

        # test_text = "e🕵️‍a"
        test_text = (
            b'e' + b'\xf0\x9f\x95\xb5' +  # sleuth emoji
            b'\xef\xb8\x8f' +  # variation selector 16
            b'\xe2\x80\x8d' +  # zero width joiner
            b'a').decode('utf-8')  # character that should be preserved

        records = [
            AsciiCastV2Header(version=2, width=80, height=24, theme=THEME),
            AsciiCastV2Event(0, 'o', test_text, None),
        ]
        _, events = term.timed_frames(records, 1, None, last_frame_dur=1000)

        frame = next(events)
        characters = ''.join(frame.buffer[0][col].text
                             for col in frame.buffer[0])

        # Ensure data following sleuth emoji wasn't ignored
        # (rstrip() removes blank cursor character at end of line)
        self.assertEqual(characters.rstrip()[-1], test_text[-1])
Beispiel #5
0
    def test_screen_events_simple_events(self):
        records = [AsciiCastV2Header(version=2, width=80, height=24, theme=THEME)] + \
                  [AsciiCastV2Event(time=i,
                                    event_type='o',
                                    event_data='{}\r\n'.format(i),
                                    duration=None)
                   for i in range(0, 2)]
        events = term.screen_events(records, 1, None, 42)
        list_events = [next(events)]
        list_events.extend([list(le) for le in events])

        expected_events = [
            term.Configuration(80, 24),
            [
                term.DisplayLine(0, {0: anim.CharacterCell('0')}, 0),
                term.DisplayLine(1, {0: CURSOR_CHAR}, 0),
            ],
            [
                term.DisplayLine(1, {0: CURSOR_CHAR}, 0, 1000),
                term.DisplayLine(1, {0: anim.CharacterCell('1')}, 1000),
                term.DisplayLine(2, {0: CURSOR_CHAR}, 1000),
            ],
            [
                term.DisplayLine(0, {0: anim.CharacterCell('0')}, 0, 1042),
                term.DisplayLine(1, {0: anim.CharacterCell('1')}, 1000, 42),
                term.DisplayLine(2, {0: CURSOR_CHAR}, 1000, 42),
            ],
        ]

        z = itertools.zip_longest(expected_events, list_events)
        for count, (expected_item, item) in enumerate(z):
            with self.subTest(case='Simple events - item #{}'.format(count)):
Beispiel #6
0
def _group_by_time(event_records, min_rec_duration, max_rec_duration,
                   last_rec_duration):
    """Merge event records together if they are close enough and compute the duration between
    consecutive events. The duration between two consecutive event records returned by the function
    is guaranteed to be at least min_rec_duration.

    :param event_records: Sequence of records in asciicast v2 format
    :param min_rec_duration: Minimum time between two records returned by the function in
    milliseconds. This helps avoiding 0s duration animations which break SVG animations.
    :param max_rec_duration: Limit of the time elapsed between two records
    :param last_rec_duration: Duration of the last record in milliseconds
    :return: Sequence of records
    """
    current_string = b''
    current_time = 0
    dropped_time = 0

    for event_record in event_records:
        if event_record.event_type != 'o':
            continue

        time_between_events = event_record.time - (current_time + dropped_time)
        if time_between_events * 1000 >= min_rec_duration:
            if max_rec_duration:
                if max_rec_duration / 1000 < time_between_events:
                    dropped_time += time_between_events - (max_rec_duration /
                                                           1000)
                    time_between_events = max_rec_duration / 1000
            accumulator_event = AsciiCastV2Event(time=current_time,
                                                 event_type='o',
                                                 event_data=current_string,
                                                 duration=time_between_events)
            yield accumulator_event
            current_string = b''
            current_time += time_between_events

        current_string += event_record.event_data

    if current_string:
        accumulator_event = AsciiCastV2Event(time=current_time,
                                             event_type='o',
                                             event_data=current_string,
                                             duration=last_rec_duration / 1000)
        yield accumulator_event
Beispiel #7
0
def _group_by_time(event_records, min_rec_duration, last_rec_duration):
    # type: (Iterable[AsciiCastV2Event], float, float) -> Generator[AsciiCastV2Event, None, None]
    """Merge event records together if they are close enough and compute the duration between
    consecutive events. The duration between two consecutive event records returned by the function
    is guaranteed to be at least min_rec_duration.

    :param event_records: Sequence of records in asciicast v2 format
    :param min_rec_duration: Minimum time between two records returned by the function in seconds.
    This helps avoiding 0s duration animations which break SVG animations.
    :param last_rec_duration: Duration of the last record in seconds
    :return: Sequence of records
    """
    current_string = b''
    current_time = None

    for event_record in event_records:
        if event_record.event_type != 'o':
            continue

        if current_time is not None:
            time_between_events = event_record.time - current_time
            if time_between_events >= min_rec_duration:
                accumulator_event = AsciiCastV2Event(
                    time=current_time,
                    event_type='o',
                    event_data=current_string,
                    duration=time_between_events)
                yield accumulator_event
                current_string = b''
                current_time = event_record.time
        else:
            current_time = event_record.time

        current_string += event_record.event_data

    if current_string:
        accumulator_event = AsciiCastV2Event(time=current_time,
                                             event_type='o',
                                             event_data=current_string,
                                             duration=last_rec_duration)
        yield accumulator_event
Beispiel #8
0
def record(columns, lines, input_fileno, output_fileno):
    """Record a terminal session in asciicast v2 format

    The records returned are of two types:
        - a single header with configuration information
        - multiple event records with data captured from the terminal and timing information
    """
    yield AsciiCastV2Header(version=2, width=columns, height=lines, theme=None)

    start = None
    for data, time in _record(columns, lines, input_fileno, output_fileno):
        if start is None:
            start = time

        yield AsciiCastV2Event(time=(time - start).total_seconds(),
                               event_type='o',
                               event_data=data,
                               duration=None)
Beispiel #9
0
    def test_timed_frames_simple_events(self):
        records = [AsciiCastV2Header(version=2, width=80, height=24, theme=THEME)] + \
                  [AsciiCastV2Event(time=i,
                                    event_type='o',
                                    event_data='{}\r\n'.format(i),
                                    duration=None)
                   for i in range(0, 2)]
        geometry, frames = term.timed_frames(records, 1, None, 42)

        self.assertEqual(geometry, (80, 24))

        expected_frames = [
            term.TimedFrame(0, 1000, {
                0: {
                    0: anim.CharacterCell('0')
                },
                1: {
                    0: CURSOR_CHAR
                },
            }),
            term.TimedFrame(
                1000, 42, {
                    0: {
                        0: anim.CharacterCell('0')
                    },
                    1: {
                        0: anim.CharacterCell('1')
                    },
                    2: {
                        0: CURSOR_CHAR
                    },
                })
        ]

        z = itertools.zip_longest(expected_frames, frames)
        for (expected_frame, frame) in z:
            self.assertEqual(expected_frame.time, frame.time)
            self.assertEqual(expected_frame.duration, frame.duration)
            for row in frame.buffer:
                if row in expected_frame.buffer:
                    self.assertEqual(expected_frame.buffer[row],
                                     frame.buffer[row])
                else:
                    self.assertEqual({}, frame.buffer[row])
Beispiel #10
0
def record(process_args, columns, lines, input_fileno, output_fileno):
    """Record a process in asciicast v2 format

    The records returned by this method are:
        - a single header containing configuration information
        - multiple event records made of data captured from the terminal and
        timing information (except for record duration which needs to be
        computed separately)

    :param process_args: Arguments required to spawn the process (list of
    string)
    :param columns: Width of the terminal screen (integer)
    :param lines: Height of the terminal screen (integer)
    :param input_fileno: File descriptor that will be used as the standard
    input of the process
    :param output_fileno: File descriptor that will be used as the standard
    output of the process

    When using `sys.stdout.fileno()` for `output_fileno` there is a risk
    that the terminal is left in an unusable state if `record` fails. To
    prevent this, `record` should be called inside the `TerminalMode`
    context manager.
    """
    yield AsciiCastV2Header(version=2, width=columns, height=lines, theme=None)

    # TODO: why start != 0?
    start = None
    utf8_decoder = codecs.getincrementaldecoder('utf-8')('replace')
    for data, time in _record(process_args, columns, lines, input_fileno, output_fileno):
        if start is None:
            start = time

        yield AsciiCastV2Event(time=(time - start).total_seconds(),
                               event_type='o',
                               event_data=utf8_decoder.decode(data),
                               duration=None)
Beispiel #11
0
            for _ in term.record(columns, lines, fd_in_read, fd_out_write):
                pass

        os.waitpid(pid, 0)
        for fd in fd_in_read, fd_in_write, fd_out_read, fd_out_write:
            os.close(fd)

    def test_replay(self):
        theme = AsciiCastV2Theme('#000000', '#FFFFFF', ':'.join(['#123456'] * 16))

        with self.subTest(case='One shell command per event'):
            nbr_records = 5

            records = [AsciiCastV2Header(version=2, width=80, height=24, theme=theme)] + \
                      [AsciiCastV2Event(time=i,
                                        event_type='o',
                                        event_data='{}\r\n'.format(i).encode('utf-8'),
                                        duration=None)
                       for i in range(1, nbr_records)]

            records = term.replay(records, lambda x: x.data, 5000, None, 1000)
            # Last blank line is the cursor
            lines = [str(i) for i in range(nbr_records)] + [' ']
            for i, record in enumerate(records):
                # Skip header and cursor line
                if i != 0:
                    self.assertEqual(record.line[0], lines[i])

        with self.subTest(case='Shell command spread over multiple lines'):
            records = [AsciiCastV2Header(version=2, width=80, height=24, theme=theme)] + \
                      [AsciiCastV2Event(time=i * 60,
                                        event_type='o',
Beispiel #12
0
    def test_replay(self):
        def pyte_to_str(x, _):
            return x.data

        fallback_theme = AsciiCastV2Theme('#000000', '#000000',
                                          ':'.join(['#000000'] * 16))
        theme = AsciiCastV2Theme('#000000', '#FFFFFF',
                                 ':'.join(['#123456'] * 16))

        with self.subTest(case='One shell command per event'):
            nbr_records = 5

            records = [AsciiCastV2Header(version=2, width=80, height=24, theme=theme)] + \
                      [AsciiCastV2Event(time=i,
                                        event_type='o',
                                        event_data='{}\r\n'.format(i).encode('utf-8'),
                                        duration=None)
                       for i in range(1, nbr_records)]

            records = term.replay(records, pyte_to_str, None, fallback_theme,
                                  50, 1000)
            # Last blank line is the cursor
            lines = [str(i) for i in range(nbr_records)] + [' ']
            for i, record in enumerate(records):
                # Skip header and cursor line
                if i == 0:
                    pass
                else:
                    self.assertEqual(record.line[0], lines[i])

        with self.subTest(
Beispiel #13
0
        palette='#000000:#111111:'
        '#222222:#333333:#444444:#555555:#666666:#777777')
    color_theme_16 = AsciiCastV2Theme(
        fg='#000000',
        bg='#AAAAAA',
        palette='#000000:#111111:'
        '#222222:#333333:#444444:#555555:#666666:#777777:#888888:'
        '#999999:#AAAAAA:#bbbbbb:#CCCCCC:#DDDDDD:#EEEEEE:#ffffff')
    cast_v2_events = [
        AsciiCastV2Header(2, 212, 53, None),
        AsciiCastV2Header(2, 212, 53, color_theme_8),
        AsciiCastV2Header(2, 212, 53, color_theme_16),
        AsciiCastV2Header(2, 212, 53, None),
        AsciiCastV2Header(2, 212, 53, None, 42),
        AsciiCastV2Header(2, 212, 53, None, 1.234),
        AsciiCastV2Event(0.010303, 'o',
                         '\u001b[1;31mnico \u001b[0;34m~\u001b[0m', None),
        AsciiCastV2Event(1.146397, 'o', '❤ ☀ ☆ ☂ ☻ ♞ ☯ ☭ ☢ € →', None),
        AsciiCastV2Event(2, 'o', '\r\n', None),
    ]

    def test_from_json(self):
        test_cases = zip(TestAsciicast.cast_v2_lines,
                         TestAsciicast.cast_v2_events)
        for index, (line, event) in enumerate(test_cases):
            with self.subTest(case='line #{}'.format(index)):
                self.assertEqual(event, AsciiCastV2Record.from_json_line(line))

        failure_test_cases = [
            ('header v2: invalid version',
             '{"version": "x", "width": 212, "height": 53}'),
            ('header v2: invalid width',
Beispiel #14
0
        event_records = [
            AsciiCastV2Event(0, 'o', '1', None),
            AsciiCastV2Event(5, 'o', '2', None),
            AsciiCastV2Event(8, 'o', '3', None),
            AsciiCastV2Event(20, 'o', '4', None),
            AsciiCastV2Event(21, 'o', '5', None),
            AsciiCastV2Event(30, 'o', '6', None),
            AsciiCastV2Event(31, 'o', '7', None),
            AsciiCastV2Event(32, 'o', '8', None),
            AsciiCastV2Event(33, 'o', '9', None),
            AsciiCastV2Event(43, 'o', '10', None),
        ]

        with self.subTest(case='maximum record duration'):
            grouped_event_records_max = [
                AsciiCastV2Event(0, 'o', '1', 5),
                AsciiCastV2Event(5, 'o', '23', 6),
                AsciiCastV2Event(11, 'o', '45', 6),
                AsciiCastV2Event(17, 'o', '6789', 6),
                AsciiCastV2Event(23, 'o', '10', 1.234),
            ]
            result = list(term._group_by_time(event_records, 5000, 6000, 1234))
            self.assertEqual(grouped_event_records_max, result)

        with self.subTest(case='no maximum record duration'):
            grouped_event_records_no_max = [
                AsciiCastV2Event(0, 'o', '1', 5),
                AsciiCastV2Event(5, 'o', '23', 15),
                AsciiCastV2Event(20, 'o', '45', 10),
                AsciiCastV2Event(30, 'o', '6789', 13),
                AsciiCastV2Event(43, 'o', '10', 1.234),
Beispiel #15
0
                pass

        os.waitpid(pid, 0)
        for fd in fd_in_read, fd_in_write, fd_out_read, fd_out_write:
            os.close(fd)

    def test_replay(self):
        theme = AsciiCastV2Theme('#000000', '#FFFFFF',
                                 ':'.join(['#123456'] * 16))

        with self.subTest(case='One shell command per event'):
            nbr_records = 5

            records = [AsciiCastV2Header(version=2, width=80, height=24, theme=theme)] + \
                      [AsciiCastV2Event(time=i,
                                        event_type='o',
                                        event_data='{}\r\n'.format(i).encode('utf-8'),
                                        duration=None)
                       for i in range(1, nbr_records)]

            records = term.replay(records, lambda x: x.data, 50, 1000)
            # Last blank line is the cursor
            lines = [str(i) for i in range(nbr_records)] + [' ']
            for i, record in enumerate(records):
                # Skip header and cursor line
                if i == 0:
                    pass
                else:
                    self.assertEqual(record.line[0], lines[i])

        with self.subTest(case='Shell command spread over multiple lines'):
            records = [AsciiCastV2Header(version=2, width=80, height=24, theme=theme)] + \
Beispiel #16
0
    def test__group_by_time(self):
        event_records = [
            AsciiCastV2Event(0, 'o', b'1', None),
            AsciiCastV2Event(50, 'o', b'2', None),
            AsciiCastV2Event(80, 'o', b'3', None),
            AsciiCastV2Event(200, 'o', b'4', None),
            AsciiCastV2Event(210, 'o', b'5', None),
            AsciiCastV2Event(300, 'o', b'6', None),
            AsciiCastV2Event(310, 'o', b'7', None),
            AsciiCastV2Event(320, 'o', b'8', None),
            AsciiCastV2Event(330, 'o', b'9', None)
        ]

        grouped_event_records = [
            AsciiCastV2Event(0, 'o', b'1', 50),
            AsciiCastV2Event(50, 'o', b'23', 150),
            AsciiCastV2Event(200, 'o', b'45', 100),
            AsciiCastV2Event(300, 'o', b'6789', 1234)
        ]

        result = list(term._group_by_time(event_records, 50, 1234))
        self.assertEqual(grouped_event_records, result)
Beispiel #17
0
        palette='#000000:#111111:'
        '#222222:#333333:#444444:#555555:#666666:#777777')
    color_theme_16 = AsciiCastV2Theme(
        fg='#000000',
        bg='#AAAAAA',
        palette='#000000:#111111:'
        '#222222:#333333:#444444:#555555:#666666:#777777:#888888:'
        '#999999:#AAAAAA:#bbbbbb:#CCCCCC:#DDDDDD:#EEEEEE:#ffffff')
    cast_v2_events = [
        AsciiCastV2Header(2, 212, 53, None),
        AsciiCastV2Header(2, 212, 53, color_theme_8),
        AsciiCastV2Header(2, 212, 53, color_theme_16),
        AsciiCastV2Header(2, 212, 53, None),
        AsciiCastV2Header(2, 212, 53, None, 42),
        AsciiCastV2Event(
            0.010303, 'o',
            '\u001b[1;31mnico \u001b[0;34m~\u001b[0m'.encode('utf-8'), None),
        AsciiCastV2Event(1.146397, 'o',
                         '❤ ☀ ☆ ☂ ☻ ♞ ☯ ☭ ☢ € →'.encode('utf-8'), None),
        AsciiCastV2Event(2, 'o', b'\r\n', None),
    ]

    def test_from_json(self):
        test_cases = zip(TestAsciicast.cast_v2_lines,
                         TestAsciicast.cast_v2_events)
        for index, (line, event) in enumerate(test_cases):
            with self.subTest(case='line #{}'.format(index)):
                self.assertEqual(event, AsciiCastV2Record.from_json_line(line))

        failure_test_cases = [
            ('header v2: invalid version',