Warning, file /sdk/selenium-webdriver-at-spi/run.rb was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001 #!/usr/bin/env ruby 0002 # frozen_string_literal: true 0003 0004 # SPDX-License-Identifier: AGPL-3.0-or-later 0005 # SPDX-FileCopyrightText: 2022-2023 Harald Sitter <sitter@kde.org> 0006 0007 require 'fileutils' 0008 require 'logger' 0009 require 'shellwords' 0010 0011 def at_bus_exists? 0012 IO.popen(['dbus-send', '--print-reply', '--dest=org.freedesktop.DBus', '/org/freedesktop/DBus', 'org.freedesktop.DBus.ListNames'], 'r') do |io| 0013 io.read.include?('"org.a11y.Bus"') 0014 end 0015 end 0016 0017 def terminate_pgids(pgids) 0018 (pgids || []).reverse.each do |pgid| 0019 Process.kill('-TERM', pgid) 0020 Process.waitpid(pgid) 0021 rescue Errno::ECHILD => e 0022 warn "Process group not found #{e}" 0023 end 0024 end 0025 0026 def terminate_pids(pids) 0027 (pids || []).reverse.each do |pid| 0028 Process.kill('TERM', pid) 0029 Process.waitpid(pid) 0030 end 0031 end 0032 0033 class ATSPIBus 0034 def initialize(logger:) 0035 @logger = logger 0036 end 0037 0038 def with(&block) 0039 return block.yield if at_bus_exists? 0040 0041 bus_existed = at_bus_exists? 0042 0043 launcher_path = find_program('at-spi-bus-launcher') 0044 registry_path = find_program('at-spi2-registryd') 0045 @logger.warn "Testing with #{launcher_path} and #{registry_path}" 0046 0047 pids = [] 0048 pids << spawn(launcher_path, '--launch-immediately') 0049 pids << spawn(registry_path) 0050 block.yield 0051 ensure 0052 # NB: do not signal KILL the launcher, it only shutsdown the a11y dbus-daemon when terminated! 0053 terminate_pids(pids) 0054 # Restart the regular bus or the user may be left with malfunctioning accerciser 0055 # (intentionally ignoring the return value! it never passes in the CI & freebsd in absence of systemd) 0056 system('systemctl', 'restart', '--user', 'at-spi-dbus-bus.service') if !pids&.empty? && bus_existed 0057 end 0058 0059 private 0060 0061 def find_program(name) 0062 @atspi_paths ||= [ 0063 '/usr/lib/at-spi2-core/', # debians 0064 '/usr/libexec/', # newer debians 0065 '/usr/lib/at-spi2/', # suses 0066 '/usr/libexec/at-spi2/', # newer suses 0067 '/usr/lib/' # arch 0068 ] 0069 0070 @atspi_paths.each do |x| 0071 path = "#{x}/#{name}" 0072 return path if File.exist?(path) 0073 end 0074 raise "Could not resolve absolute path for #{name}; searched in #{@atspi_paths.join(', ')}" 0075 end 0076 end 0077 0078 def kwin_reexec! 0079 # KWin redirection is a bit tricky. We want to run this script itself under kwin so both the flask server and 0080 # the actual test script can inherit environment variables from that nested kwin. Most notably this is required 0081 # to have the correct DISPLAY set to access the xwayland instance. 0082 # As such this function has two behavior modes. If kwin redirection should run (that is: it's not yet inside kwin) 0083 # it will fork and exec into kwin. If redirection is not required it yields out. 0084 0085 return if ENV.include?('KWIN_PID') # already inside a kwin parent 0086 return if ENV['TEST_WITH_KWIN_WAYLAND'] == '0' 0087 0088 kwin_pid = fork do |pid| 0089 ENV['QT_QPA_PLATFORM'] = 'wayland' 0090 ENV['KWIN_SCREENSHOT_NO_PERMISSION_CHECKS'] = '1' 0091 ENV['KWIN_WAYLAND_NO_PERMISSION_CHECKS'] = '1' 0092 ENV['KWIN_PID'] = pid.to_s 0093 extra_args = [] 0094 extra_args << '--virtual' if ENV['LIBGL_ALWAYS_SOFTWARE'] 0095 extra_args << '--xwayland' if ENV.fetch('TEST_WITH_XWAYLAND', '0').to_i.positive? 0096 # A bit awkward because of how argument parsing works on the kwin side: we must rely on shell word merging for 0097 # the __FILE__ ARGV bit, separate ARGVs to kwin_wayland would be distinct subprocesses to start but we want 0098 # one processes with a bunch of arguments. 0099 exec('kwin_wayland', '--no-lockscreen', '--no-global-shortcuts', *extra_args, 0100 '--exit-with-session', "#{__FILE__} #{ARGV.shelljoin}") 0101 end 0102 _pid, status = Process.waitpid2(kwin_pid) 0103 status.success? ? exit : abort 0104 end 0105 0106 def dbus_reexec!(logger:) 0107 return if ENV.include?('CUSTOM_BUS') # already inside a nested bus 0108 0109 if ENV.fetch('USE_CUSTOM_BUS', '0').to_i.zero? && # not explicitly enabled 0110 at_bus_exists? # already have an a11y bus, use it 0111 0112 logger.info('using existing dbus session') 0113 return 0114 end 0115 0116 logger.info('starting dbus session') 0117 ENV['CUSTOM_BUS'] = '1' 0118 # Using spawn() rather than exec() so we can print useful debug information after the run 0119 # (useful to debug problems with shutdown of started processes) 0120 pid = spawn('dbus-run-session', '--', __FILE__, *ARGV, pgroup: true) 0121 pgid = Process.getpgid(pid) 0122 _pid, status = Process.waitpid2(pid) 0123 terminate_pgids([pgid]) 0124 logger.info('dbus session ended') 0125 system('ps fja') 0126 status.success? ? exit : abort 0127 end 0128 0129 # Video recording wrapper 0130 class Recorder 0131 def self.with(&block) 0132 return block.yield unless ENV['RECORD_VIDEO_NAME'] 0133 0134 abort 'RECORD_VIDEO requires that a nested kwin wayland be used! (TEST_WITH_KWIN_WAYLAND)' unless ENV['KWIN_PID'] 0135 0136 # Make sure kwin is up. This can be removed once the code was changed to re-exec as part of a kwin 0137 # subprocess, then the wayland server is ready by the time we get re-executed. 0138 sleep(5) 0139 FileUtils.rm_f(ENV['RECORD_VIDEO_NAME']) 0140 pids = [] 0141 pids << spawn('pipewire') 0142 pids << spawn('wireplumber') 0143 pids << spawn(find_program('xdg-desktop-portal-kde')) 0144 pids << spawn('selenium-webdriver-at-spi-recorder', '--output', ENV.fetch('RECORD_VIDEO_NAME')) 0145 5.times do 0146 break if File.exist?(ENV['RECORD_VIDEO_NAME']) 0147 0148 sleep(1) 0149 end 0150 block.yield 0151 ensure 0152 terminate_pids(pids) 0153 if ENV['RECORD_VIDEO_NAME'] && 0154 (!File.exist?(ENV['RECORD_VIDEO_NAME']) || File.size(ENV['RECORD_VIDEO_NAME']) < 256_000) 0155 warn "recording apparently didn't work properly" 0156 end 0157 end 0158 0159 def self.find_program(name) 0160 @paths ||= ENV.fetch('LD_LIBRARY_PATH', '').split(':').map { |x| "#{x}/libexec" } + 0161 [ 0162 '/usr/lib/*/libexec/', # debian 0163 '/usr/libexec/', # suse 0164 '/usr/lib/' # arch 0165 ] 0166 0167 @paths.each do |x| 0168 path = "#{x}/#{name}" 0169 return path if Dir.glob(path)&.first 0170 end 0171 raise "Could not resolve absolute path for #{name}; searched in #{@paths.join(', ')}" 0172 end 0173 end 0174 0175 class Driver 0176 def self.with(datadir, &block) 0177 pids = [] 0178 env = { 'FLASK_ENV' => 'production', 'FLASK_APP' => 'selenium-webdriver-at-spi.py' } 0179 env['GDK_BACKEND'] = "wayland" if ENV['KWIN_PID'] 0180 pids << spawn(env, 0181 'flask', 'run', '--port', PORT, '--no-reload', 0182 chdir: datadir) 0183 block.yield 0184 ensure 0185 terminate_pids(pids) 0186 end 0187 end 0188 0189 PORT = '4723' 0190 $stdout.sync = true # force immediate flushing without internal caching 0191 logger = Logger.new($stdout) 0192 0193 # Tweak the CIs logging rules. They are way too verbose for our purposes 0194 ENV['QT_LOGGING_RULES'] = <<-RULES.gsub(/\s/, '') 0195 default=true;*.debug=true;kf.globalaccel.kglobalacceld=false;kf.wayland.client=false; 0196 qt.scenegraph.*=false;qt.qml.diskcache=false; 0197 qt.qml.*=false;qt.qpa.wayland.*=false;qt.quick.dirty=false;qt.accessibility.cache=false;qt.v4.asm=false; 0198 qt.opengl.diskcache=false;qt.qpa.fonts=false;kf.kio.workers.http=false; 0199 qt.quick.*=false;qt.text.*=false;qt.qpa.input.methods=false; 0200 qt.qpa.backingstore=false;qt.gui.*=false; 0201 RULES 0202 ENV['QT_LOGGING_RULES'] = ENV['QT_LOGGING_RULES_OVERRIDE'] if ENV.include?('QT_LOGGING_RULES_OVERRIDE') 0203 0204 dbus_reexec!(logger: logger) 0205 kwin_reexec! 0206 raise 'Failed to set dbus env' unless system('dbus-update-activation-environment', '--all') 0207 0208 logger.info 'Installing dependencies' 0209 datadir = File.absolute_path("#{__dir__}/../share/selenium-webdriver-at-spi/") 0210 if File.exist?("#{datadir}/requirements.txt") 0211 system('pip3', 'install', '-r', 'requirements.txt', chdir: datadir) || raise 0212 ENV['PATH'] = "#{Dir.home}/.local/bin:#{ENV.fetch('PATH')}" 0213 end 0214 0215 ret = false 0216 ATSPIBus.new(logger: logger).with do 0217 Recorder.with do 0218 Driver.with(datadir) do 0219 i = 0 0220 begin 0221 require 'net/http' 0222 Net::HTTP.get(URI("http://localhost:#{PORT}/status")) 0223 rescue => e 0224 i += 1 0225 if i < 30 0226 logger.info 'not up yet' 0227 sleep 0.5 0228 retry 0229 end 0230 raise e 0231 end 0232 0233 logger.info "starting test #{ARGV}" 0234 IO.popen(ARGV, 'r', &:readlines) 0235 ret = $?.success? 0236 logger.info 'tests done' 0237 end 0238 end 0239 end 0240 0241 logger.info "run.rb exiting #{ret}" 0242 ret ? exit : abort