summaryrefslogtreecommitdiff
path: root/test/column_grip.py
blob: ac0341c3dbe38df3190e7b4154f78b2faf82f394 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#!/usr/bin/env python3
"""Drag a column grip in the real SDL window with pixel mouse events.

WAYLAND_DISPLAY=/run/user/1000/wayland-1 \\
    python3 -B test/column_grip.py zig-out/bin/pardes-gui /tmp/pardes-column-grip [--pad 7]

The window is hidden (PARDES_TEST). Mouse events enter as ESC]777;mouse
sequences on stdin, which the GUI builds into SDL events and dispatches like
its own, so pixel-to-cell and tag hit testing run as they do under a hand.
9P observes the grid; each step leaves a PNG of the GPU frame in the output
directory: grip-rest, grip-resize-drag, grip-reorder-drag, grip-done.
"""
import argparse
import os
from pathlib import Path
import shutil
import subprocess
import time

from fs import session
from ui_review import require, wait_for


def rows(client):
    frame = client.screen()
    cols = frame['cols']
    return [''.join(cell[0] for cell in frame['cells'][at:at + cols]) for at in range(0, len(frame['cells']), cols)]


def rail(client):
    """The one column the drag preview's rail runs down, if any."""
    bottom = rows(client)[-1]
    found = [x for x, ch in enumerate(bottom) if ch == '╎']
    require(len(found) <= 1, f'more than one rail: {found}')
    return found[0] if found else None


def grab(root, label):
    time.sleep(.5)
    def convert():
        shutil.copyfile(root / 'latest.ppm', root / (label + '.ppm'))
        return subprocess.run(['magick', str(root / (label + '.ppm')), str(root / (label + '.png'))],
                              stderr=subprocess.DEVNULL).returncode == 0
    require(wait_for(convert, 8), 'GPU capture did not contain a complete image')
    return root / (label + '.png')


def pixel(path, x, y):
    out = subprocess.run(['magick', str(path), '-format', f'%[pixel:p{{{x},{y}}}]', 'info:'],
                         capture_output=True, text=True, check=True)
    return out.stdout.strip()


def run(args):
    root = args.output.resolve()
    shutil.rmtree(root, ignore_errors=True)
    root.mkdir(parents=True, mode=0o700)
    (root / 'config/pardes').mkdir(parents=True)
    (root / 'config/pardes/init').write_text('Shell /bin/sh\n')
    (root / 'w.txt').write_text('alpha\nbeta\n')
    name = 'column-grip-' + str(os.getpid())
    launch = ['-c', 'stty cols 100 rows 30; exec "$@"', 'column-grip',
              str(args.binary.resolve()), '--9p=' + name, str(root / 'w.txt')]
    env = {'PARDES_TEST': '1', 'PARDES_TEST_CAPTURE_DIR': str(root), 'PARDES_TEST_COLS': '100',
           'PARDES_TEST_ROWS': '30', 'PARDES_TEST_PAD': str(args.pad)}
    handles = {}
    with session('/bin/sh', root, name, socket_name=name, tty=True, launch=launch,
                 inherited=env, terminal=handles) as (client, _):
        def send(data):
            os.write(handles['input_fd'], data.encode())

        def mouse(kind, button, col, row):
            # the middle of a grid cell, in window pixels
            send(f'\x1b]777;mouse;{kind};{button};{(col + .5) * cw};{(row + .5) * ch}\x07')
            time.sleep(.15)

        require(wait_for(lambda: 'Newcol' in rows(client)[0]), 'workspace tag never drew')
        send('\x1bn')
        time.sleep(.3)
        send('\x1bc')
        require(wait_for(lambda: rows(client)[1].count('New Tty') == 2), 'no second column')
        require(wait_for(lambda: (root / 'latest.ppm').exists(), 8), 'GPU capture missing')
        time.sleep(.5)
        with open(root / 'latest.ppm', 'rb') as ppm:
            ppm.readline()
            width, height = map(int, ppm.readline().split())
        frame = client.screen()
        cw = (width - args.pad) // frame['cols']
        ch = (height - args.pad) // frame['rows']
        edge = rows(client)[1].index('New Tty', 10) - 3

        rest = grab(root, 'grip-rest')
        mouse('down', 1, edge, 1)
        held = grab(root, 'grip-held')
        require(pixel(held, int((edge + 1) * cw), int(1.5 * ch)) != pixel(rest, int((edge + 1) * cw), int(1.5 * ch)),
                'a grabbed grip looks the same as a resting one')
        require(rail(client) is None, 'a grip held in place previews a move')

        # Short of the left column's middle: the left edge follows, and the
        # rail is the seam cell left of where the rule will be.
        for col in range(edge, edge - 11, -1):
            mouse('motion', 1, col, 1)
        require(rail(client) == edge - 11, f'resize rail at {rail(client)}, wanted {edge - 11}')
        grab(root, 'grip-resize-drag')
        mouse('up', 1, edge - 10, 1)
        require(wait_for(lambda: rows(client)[1].index('New Tty', 10) - 3 == edge - 10), 'left edge did not follow the grip')
        edge -= 10

        # Over the left column's left half: it goes first, and the rail runs
        # just past the left window edge's scroll rail.
        mouse('down', 1, edge, 1)
        for col in range(edge, 4, -5):
            mouse('motion', 1, col, 10)
        require(rail(client) == 1, f'reorder rail at {rail(client)}, wanted 1')
        grab(root, 'grip-reorder-drag')
        mouse('up', 1, 5, 10)
        require(wait_for(lambda: 'alpha' in rows(client)[3][50:]), 'the column did not move first')
        require(rail(client) is None, 'the rail outlived the drag')
        grab(root, 'grip-done')
    print('ok', root)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('binary', type=Path)
    parser.add_argument('output', type=Path)
    parser.add_argument('--pad', type=int, default=0, help='leftover pixels past the grid (PARDES_TEST_PAD)')
    run(parser.parse_args())


if __name__ == '__main__':
    main()