Skip to content

Commit

Permalink
tests/anthytest: Support GNOME Wayland and GTK4
Browse files Browse the repository at this point in the history
- Check "preedit-changed" signal is called twice in GNOME Wayland.
  Maybe a mutter bug.
- Make sure the preedit is cleared before the next test case runs
- Wait for 3 seconds in GNOME Wayland before the test cases run to
  get delayed focus events.
- Implement GTK4
  • Loading branch information
fujiwarat committed Sep 11, 2024
1 parent 9ae92d7 commit fc6e832
Showing 1 changed file with 146 additions and 33 deletions.
179 changes: 146 additions & 33 deletions tests/anthytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@

from __future__ import print_function

from gi import require_version as gi_require_version
gi_require_version('GLib', '2.0')
gi_require_version('Gdk', '3.0')
gi_require_version('Gio', '2.0')
gi_require_version('Gtk', '3.0')
gi_require_version('IBus', '1.0')
from gi import require_versions as gi_require_versions
gi_require_versions({'GLib': '2.0', 'Gio': '2.0', 'GObject': '2.0',
'IBus': '1.0'})
from gi.repository import GLib
from gi.repository import Gdk
from gi.repository import Gio
from gi.repository import Gtk
from gi.repository import GObject
from gi.repository import IBus

try:
gi_require_versions({'Gdk': '4.0', 'Gtk': '4.0'})
except ValueError:
gi_require_versions({'Gdk': '3.0', 'Gtk': '3.0'})

from gi.repository import Gdk
from gi.repository import Gtk

import argparse
import getopt
import os
Expand Down Expand Up @@ -70,7 +74,7 @@ def printerr(sentence):
from anthycases import TestCases


@unittest.skipIf(Gdk.Display.open('') == None, 'Display cannot be open.')
@unittest.skipIf(Gdk.Display.get_default() == None, 'Display cannot be open.')
class AnthyTest(unittest.TestCase):
global DONE_EXIT
ENGINE_PATH = '/com/redhat/IBus/engines/Anthy/Test/Engine'
Expand All @@ -83,9 +87,17 @@ def setUp(self):
self.__id = 0
self.__rerun = False
self.__test_index = 0
self.__preedit_changes = 0
self.__preedit_prev = None
self.__conversion_index = 0
self.__conversion_spaces = 0
self.__commit_done = False
self.__engine = None
self.__list_toplevel = False
self.__is_wayland = False
display = Gdk.Display.get_default()
if GObject.type_name(display.__gtype__) == 'GdkWaylandDisplay':
self.__is_wayland = True

def register_ibus_engine(self):
printflush('## Registering engine')
Expand Down Expand Up @@ -142,7 +154,7 @@ def __name_owner_changed_cb(self, connection, sender_name, object_path,
except ModuleNotFoundError as e:
with self.subTest(i = 'name-owner-changed'):
self.fail('NG: Not installed ibus-anthy %s' % str(e))
Gtk.main_quit()
self.__window_destroy_cb()
return
engine.Engine.CONFIG_RELOADED()

Expand All @@ -154,7 +166,7 @@ def __create_engine_cb(self, factory, engine_name):
except ModuleNotFoundError as e:
with self.subTest(i = 'create-engine'):
self.fail('NG: Not installed ibus-anthy %s' % str(e))
Gtk.main_quit()
self.__window_destroy_cb()
return
self.__id += 1
self.__engine = engine.Engine(self.__bus, '%s/%d' % (self.ENGINE_PATH, self.__id))
Expand All @@ -165,7 +177,7 @@ def __create_engine_cb(self, factory, engine_name):
def __engine_focus_in(self, engine):
if self.__test_index == len(TestCases['tests']):
if DONE_EXIT:
Gtk.main_quit()
self.__window_destroy_cb()
return
# Workaround because focus-out resets the preedit text
# ibus_bus_set_global_engine() calls bus_input_context_set_engine()
Expand All @@ -178,31 +190,96 @@ def __engine_focus_out(self, engine):
self.__rerun = True

def create_window(self):
match Gtk.MAJOR_VERSION:
case 4:
self.create_window_gtk4()
case 3:
self.create_window_gtk3()
case _:
self.gtk_version_exception()

def create_window_gtk4(self):
window = Gtk.Window()
self.__entry = entry = Gtk.Entry()
window.connect('destroy', self.__window_destroy_cb)
entry.connect('map', self.__entry_map_cb)
controller = Gtk.EventControllerFocus()
controller.set_propagation_phase(Gtk.PropagationPhase.BUBBLE)
controller.connect_after('enter', self.__controller_enter_cb)
text = entry.get_delegate()
text.add_controller(controller)
text.connect('preedit-changed', self.__entry_preedit_changed_cb)
buffer = entry.get_buffer()
buffer.connect('inserted-text', self.__buffer_inserted_text_cb)
window.set_child(entry)
window.set_focus(entry)
window.present()
printflush('## Build GTK4 window')

def create_window_gtk3(self):
window = Gtk.Window(type = Gtk.WindowType.TOPLEVEL)
self.__entry = entry = Gtk.Entry()
window.connect('destroy', Gtk.main_quit)
window.connect('destroy', self.__window_destroy_cb)
entry.connect('map', self.__entry_map_cb)
entry.connect('focus-in-event', self.__entry_focus_in_event_cb)
entry.connect('preedit-changed', self.__entry_preedit_changed_cb)
buffer = entry.get_buffer()
buffer.connect('inserted-text', self.__buffer_inserted_text_cb)
window.add(entry)
window.show_all()
printflush('## Build window')
printflush('## Build GTK3 window')

def gtk_version_exception(self):
raise Exception("GTK version %d is not supported" % Gtk.MAJOR_VERSION)

def is_integrated_desktop(self):
session_name = os.environ['XDG_SESSION_DESKTOP']
if session_name == None:
return False
if len(session_name) >= 4 and session_name[0:5] == 'gnome':
return True
return False

def __window_destroy_cb(self):
match Gtk.MAJOR_VERSION:
case 4:
self.__list_toplevel = False
case 3:
Gtk.main_quit()
case _:
self.gtk_version_exception()

def __entry_map_cb(self, entry):
printflush('## Map window')

def __controller_enter_cb(self, controller):
if self.is_integrated_desktop():
printflush('## Waiting for 3 secs')
GLib.timeout_add_seconds(3,
self.__controller_enter_delay,
controller)
else:
printflush('## No Wait')
GLib.idle_add(self.__controller_enter_delay,
controller)

def __controller_enter_delay(self, controller):
text = controller.get_widget()
if not text.get_realized():
return
self.__entry_focus_in_event_cb(None, None)

def __entry_focus_in_event_cb(self, entry, event):
printflush('## Get focus')
if self.__test_index == len(TestCases['tests']):
if DONE_EXIT:
Gtk.main_quit()
self.__window_destroy_cb()
return False
self.__bus.set_global_engine_async('testanthy', -1, None, self.__set_engine_cb)
return False

def __set_engine_cb(self, object, res):
printflush('## Set engine')
if not self.__bus.set_global_engine_async_finish(res):
with self.subTest(i = self.__test_index):
self.fail('set engine failed: ' + error.message)
Expand All @@ -217,24 +294,48 @@ def __get_test_condition_length(self, tag):
return len(cases[type])

def __entry_preedit_changed_cb(self, entry, preedit_str):
if len(preedit_str) == 0:
# Wait for clearing the preedit before the next __main_test() is called.
if self.__commit_done:
if len(preedit_str) == 0:
self.__preedit_changes = 0
self.__main_test()
else:
self.__preedit_changes += 1
return
if self.__is_wayland:
# Need to fix mutter
# GTK calls self.__entry_preedit_changed_cb() twice by the actual
# preedit update in GNOME Wayland in case the lookup window is not
# shown yet and the preedit is changed but not the cursor position
# only.
#
# I.e. GTK receives the struct zwp_text_input_v3_listener.done()
# from Wayland text-input protocol when the preedit is updated
# and the "preedit-changed" signal is called at first and the
# zwp_text_input_v3_listener.done() also calls
# zwp_text_input_v3_commit() to notify IM changes to mutter and
# mutter receives the struct
# zwp_text_input_v3_interface.commit_state() from Wayland text-input
# protocol and it causes the zwp_text_input_v3_listener.done() and
# the "preedit-changed" signal in GTK.
if self.__preedit_changes < 1 and self.__conversion_spaces < 2 \
and self.__preedit_prev != preedit_str:
self.__preedit_changes += 1
return
else:
self.__preedit_changes = 0
if self.__test_index == len(TestCases['tests']):
if DONE_EXIT:
Gtk.main_quit()
self.__window_destroy_cb()
return
self.__preedit_prev = preedit_str
conversion_length = self.__get_test_condition_length('conversion')
# Need to return again even if all the conversion is finished
# until the final Engine.update_preedit() is called.
if self.__conversion_index > conversion_length:
return
self.__run_cases('conversion',
self.__conversion_index,
self.__conversion_index + 1)
if self.__conversion_index < conversion_length:
self.__run_cases('conversion',
self.__conversion_index,
self.__conversion_index + 1)
self.__conversion_index += 1
return
self.__conversion_index += 1
self.__run_cases('commit')

def __enable_hiragana(self):
Expand All @@ -249,13 +350,12 @@ def __enable_hiragana(self):
printflush('## Already hiragana')

def __main_test(self):
printflush('## Run case %d' % self.__test_index)
self.__preedit_prev = None
self.__conversion_index = 0
self.__conversion_spaces = 0
self.__commit_done = False
self.__run_cases('preedit')
self.__run_cases('conversion',
self.__conversion_index,
self.__conversion_index + 1)
self.__conversion_index += 1

def __run_cases(self, tag, start=-1, end=-1):
tests = TestCases['tests'][self.__test_index]
Expand Down Expand Up @@ -288,6 +388,10 @@ def __run_cases(self, tag, start=-1, end=-1):
if start != -1 or end != -1:
printflush('test step: %s sequences: [0x%X, 0x%X, 0x%X]' \
% (tag, key[0], key[1], key[2]))
# Check if the lookup table is shown.
if tag == 'conversion' and \
(key[0] == IBus.KEY_Tab or key[0] == IBus.KEY_space):
self.__conversion_spaces += 1
self.__typing(key[0], key[1], key[2])
i += 1

Expand All @@ -306,21 +410,30 @@ def __buffer_inserted_text_cb(self, buffer, position, chars, nchars):
self.fail('NG: %d %s %s' \
% (self.__test_index, str(cases['string']), chars))
if DONE_EXIT:
Gtk.main_quit()
self.__window_destroy_cb()
self.__test_index += 1
if self.__test_index == len(TestCases['tests']):
if DONE_EXIT:
Gtk.main_quit()
self.__window_destroy_cb()
return
self.__entry.set_text('')
self.__main_test()
# ibus-anthy updates preedit after commits the text.
self.__commit_done = True

def main(self):
Gtk.main()
match Gtk.MAJOR_VERSION:
case 4:
while self.__list_toplevel:
GLib.MainContext.default().iteration(True)
case 3:
Gtk.main()
case _:
self.gtk_version_exception()

def test_typing(self):
if not self.register_ibus_engine():
sys.exit(-1)
self.__list_toplevel = True
self.create_window()
self.main()

Expand Down

0 comments on commit fc6e832

Please sign in to comment.