File indexing completed on 2024-04-28 13:40:46
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 require 'tmpdir' 0011 0012 def at_bus_exists? 0013 IO.popen(['dbus-send', '--print-reply=literal', '--dest=org.freedesktop.DBus', '/org/freedesktop/DBus', 'org.freedesktop.DBus.ListNames'], 'r') do |io| 0014 io.read.include?('org.a11y.Bus') 0015 end 0016 end 0017 0018 def at_bus_address 0019 IO.popen(['dbus-send', '--print-reply=literal', '--dest=org.a11y.Bus', '/org/a11y/bus', 'org.a11y.Bus.GetAddress'], 'r') do |io| 0020 io.read.strip 0021 end 0022 end 0023 0024 def terminate_pgids(pgids) 0025 (pgids || []).reverse.each do |pgid| 0026 Process.kill('-TERM', pgid) 0027 Process.waitpid(pgid) 0028 rescue Errno::ECHILD => e 0029 warn "Process group not found #{e}" 0030 rescue Errno::ESRCH => e 0031 warn "Process not found #{e}" 0032 end 0033 end 0034 0035 def terminate_pids(pids) 0036 (pids || []).reverse.each do |pid| 0037 Process.kill('TERM', pid) 0038 Process.waitpid(pid) 0039 end 0040 end 0041 0042 class ATSPIBus 0043 def initialize(logger:) 0044 @logger = logger 0045 end 0046 0047 def with(&block) 0048 return block.yield if at_bus_exists? 0049 0050 bus_existed = at_bus_exists? 0051 0052 launcher_path = find_program('at-spi-bus-launcher') 0053 registry_path = find_program('at-spi2-registryd') 0054 @logger.warn "Testing with #{launcher_path} and #{registry_path}" 0055 0056 pids = [] 0057 pids << spawn(launcher_path, '--launch-immediately') 0058 pids << spawn(registry_path) 0059 block.yield 0060 ensure 0061 # NB: do not signal KILL the launcher, it only shutsdown the a11y dbus-daemon when terminated! 0062 terminate_pids(pids) 0063 # Restart the regular bus or the user may be left with malfunctioning accerciser 0064 # (intentionally ignoring the return value! it never passes in the CI & freebsd in absence of systemd) 0065 system('systemctl', 'restart', '--user', 'at-spi-dbus-bus.service') if !pids&.empty? && bus_existed 0066 end 0067 0068 private 0069 0070 def find_program(name) 0071 @atspi_paths ||= [ 0072 '/usr/lib/at-spi2-core/', # debians 0073 '/usr/libexec/', # newer debians 0074 '/usr/lib/at-spi2/', # suses 0075 '/usr/libexec/at-spi2/', # newer suses 0076 '/usr/lib/' # arch 0077 ] 0078 0079 @atspi_paths.each do |x| 0080 path = "#{x}/#{name}" 0081 return path if File.exist?(path) 0082 end 0083 raise "Could not resolve absolute path for #{name}; searched in #{@atspi_paths.join(', ')}" 0084 end 0085 end 0086 0087 def kwin_reexec! 0088 # KWin redirection is a bit tricky. We want to run this script itself under kwin so both the flask server and 0089 # the actual test script can inherit environment variables from that nested kwin. Most notably this is required 0090 # to have the correct DISPLAY set to access the xwayland instance. 0091 # As such this function has two behavior modes. If kwin redirection should run (that is: it's not yet inside kwin) 0092 # it will fork and exec into kwin. If redirection is not required it yields out. 0093 0094 return if ENV.include?('KWIN_PID') # already inside a kwin parent 0095 return if ENV['TEST_WITH_KWIN_WAYLAND'] == '0' 0096 0097 kwin_pid = fork do |pid| 0098 ENV['QT_QPA_PLATFORM'] = 'wayland' 0099 ENV['KWIN_SCREENSHOT_NO_PERMISSION_CHECKS'] = '1' 0100 ENV['KWIN_WAYLAND_NO_PERMISSION_CHECKS'] = '1' 0101 ENV['KWIN_PID'] = pid.to_s 0102 ENV['KWIN_XKB_DEFAULT_KEYMAP'] = 'true' 0103 # Don't set RULES or MODEL, they ought to be valid and we probably don't need to change them from whatever is set! 0104 ENV['XKB_DEFAULT_LAYOUT'] = 'us' 0105 ENV['XKB_DEFAULT_VARIANT'] = '' 0106 ENV['XKB_DEFAULT_OPTIONS'] = '' 0107 extra_args = [] 0108 extra_args << '--virtual' if ENV['LIBGL_ALWAYS_SOFTWARE'] 0109 extra_args << '--xwayland' if ENV.fetch('TEST_WITH_XWAYLAND', '0').to_i.positive? 0110 extra_args << '--no-global-shortcuts' if ENV.fetch('TEST_WITHOUT_GLOBAL_SHORTCUTS', '1').to_i.positive? 0111 # A bit awkward because of how argument parsing works on the kwin side: we must rely on shell word merging for 0112 # the __FILE__ ARGV bit, separate ARGVs to kwin_wayland would be distinct subprocesses to start but we want 0113 # one processes with a bunch of arguments. 0114 exec('kwin_wayland', '--no-lockscreen', *extra_args, 0115 '--exit-with-session', "#{__FILE__} #{ARGV.shelljoin}") 0116 end 0117 _pid, status = Process.waitpid2(kwin_pid) 0118 status.success? ? exit : abort 0119 end 0120 0121 def dbus_reexec!(logger:) 0122 return if ENV.include?('CUSTOM_BUS') # already inside a nested bus 0123 0124 if ENV.fetch('USE_CUSTOM_BUS', '0').to_i.zero? && # not explicitly enabled 0125 at_bus_exists? # already have an a11y bus, use it 0126 0127 logger.info('using existing dbus session') 0128 return 0129 end 0130 0131 logger.info('starting dbus session') 0132 ENV['CUSTOM_BUS'] = '1' 0133 # Using spawn() rather than exec() so we can print useful debug information after the run 0134 # (useful to debug problems with shutdown of started processes) 0135 pid = spawn('dbus-run-session', '--', __FILE__, *ARGV, pgroup: true) 0136 pgid = Process.getpgid(pid) 0137 _pid, status = Process.waitpid2(pid) 0138 terminate_pgids([pgid]) 0139 logger.info('dbus session ended') 0140 system('ps fja') 0141 status.success? ? exit : abort 0142 end 0143 0144 # Video recording wrapper 0145 class Recorder 0146 def self.with(&block) 0147 return block.yield unless ENV['RECORD_VIDEO_NAME'] 0148 0149 abort 'RECORD_VIDEO requires that a nested kwin wayland be used! (TEST_WITH_KWIN_WAYLAND)' unless ENV['KWIN_PID'] 0150 0151 # Make sure kwin is up. This can be removed once the code was changed to re-exec as part of a kwin 0152 # subprocess, then the wayland server is ready by the time we get re-executed. 0153 sleep(5) 0154 FileUtils.rm_f(ENV['RECORD_VIDEO_NAME']) 0155 pids = [] 0156 pids << spawn('pipewire') 0157 pids << spawn('wireplumber') 0158 pids << spawn(find_program('xdg-desktop-portal-kde')) 0159 pids << spawn('selenium-webdriver-at-spi-recorder', '--output', ENV.fetch('RECORD_VIDEO_NAME')) 0160 5.times do 0161 break if File.exist?(ENV['RECORD_VIDEO_NAME']) 0162 0163 sleep(1) 0164 end 0165 block.yield 0166 ensure 0167 terminate_pids(pids) 0168 if ENV['RECORD_VIDEO_NAME'] && 0169 (!File.exist?(ENV['RECORD_VIDEO_NAME']) || File.size(ENV['RECORD_VIDEO_NAME']) < 256_000) 0170 warn "recording apparently didn't work properly" 0171 end 0172 end 0173 0174 def self.find_program(name) 0175 @paths ||= ENV.fetch('LD_LIBRARY_PATH', '').split(':').map { |x| "#{x}/libexec" } + 0176 [ 0177 '/usr/lib/*/libexec/', # debian 0178 '/usr/libexec/', # suse 0179 '/usr/lib/' # arch 0180 ] 0181 0182 @paths.each do |x| 0183 path = "#{x}/#{name}" 0184 return path if Dir.glob(path)&.first 0185 end 0186 raise "Could not resolve absolute path for #{name}; searched in #{@paths.join(', ')}" 0187 end 0188 end 0189 0190 class Driver 0191 def self.with(datadir, &block) 0192 pids = [] 0193 env = { 'FLASK_ENV' => 'production', 'FLASK_APP' => 'selenium-webdriver-at-spi.py' } 0194 env['GDK_BACKEND'] = 'wayland' if ENV['KWIN_PID'] 0195 pids << spawn(env, 0196 'flask', 'run', '--port', PORT, '--no-reload', 0197 chdir: datadir) 0198 block.yield 0199 ensure 0200 terminate_pids(pids) 0201 end 0202 end 0203 0204 PORT = '4723' 0205 $stdout.sync = true # force immediate flushing without internal caching 0206 logger = Logger.new($stdout) 0207 0208 # Tweak the CIs logging rules. They are way too verbose for our purposes 0209 if ENV['KDECI_BUILD'] == 'TRUE' 0210 ENV['QT_LOGGING_RULES'] = <<-RULES.gsub(/\s/, '') 0211 default=true;*.debug=true;kf.globalaccel.kglobalacceld=false;kf.wayland.client=false; 0212 qt.scenegraph.*=false;qt.qml.diskcache=false; 0213 qt.qml.*=false;qt.qpa.wayland.*=false;qt.quick.dirty=false;qt.accessibility.cache=false;qt.v4.asm=false; 0214 qt.opengl.diskcache=false;qt.qpa.fonts=false;kf.kio.workers.http=false; 0215 qt.quick.*=false;qt.text.*=false;qt.qpa.input.methods=false; 0216 qt.qpa.backingstore=false;qt.gui.*=false;qt.core.plugin.loader=false; 0217 RULES 0218 ENV['QT_LOGGING_RULES'] = ENV['QT_LOGGING_RULES_OVERRIDE'] if ENV.include?('QT_LOGGING_RULES_OVERRIDE') 0219 end 0220 0221 logger.info 'Installing dependencies' 0222 datadir = File.absolute_path("#{__dir__}/../share/selenium-webdriver-at-spi/") 0223 requirements_installed_marker = "#{Dir.tmpdir}/selenium-requirements-installed" 0224 if !File.exist?(requirements_installed_marker) && File.exist?("#{datadir}/requirements.txt") 0225 raise 'pip3 not found in PATH!' unless system('which', 'pip3') 0226 unless system('pip3', 'install', '-r', 'requirements.txt', chdir: datadir) 0227 unless system('pip3', 'install', '--break-system-packages', '-r', 'requirements.txt', chdir: datadir) 0228 raise 'Failed to run pip3 install!' 0229 end 0230 end 0231 0232 if ENV['KDECI_BUILD'] == 'TRUE' 0233 File.open(requirements_installed_marker, "w") do |file| 0234 # create an empty file so tests in the same CI container can skip the process 0235 end 0236 end 0237 end 0238 0239 ENV['PATH'] = "#{Dir.home}/.local/bin:#{ENV.fetch('PATH')}" 0240 0241 ret = false 0242 0243 # create a throw-away XDG home, so the test starts with a clean slate 0244 # with every run, and doesn't mess with your local installation 0245 Dir.mktmpdir('selenium') do |xdg_home| 0246 %w[CACHE CONFIG DATA STATE].each do |d| 0247 Dir.mkdir("#{xdg_home}/#{d}") 0248 ENV["XDG_#{d}_HOME"] = "#{xdg_home}/#{d}" 0249 end 0250 0251 dbus_reexec!(logger: logger) 0252 kwin_reexec! 0253 if ENV['KDECI_BUILD'] == 'TRUE' 0254 raise 'Failed to set dbus env' unless system('dbus-update-activation-environment', '--all') 0255 end 0256 0257 ATSPIBus.new(logger: logger).with do 0258 # Prevent a race condition in Qt when it tries to figure out the bus address, 0259 # instead just tell it the address explicitly. 0260 # https://codereview.qt-project.org/c/qt/qtbase/+/493700/2 0261 ENV['AT_SPI_BUS_ADDRESS'] = at_bus_address 0262 Recorder.with do 0263 Driver.with(datadir) do 0264 i = 0 0265 begin 0266 require 'net/http' 0267 Net::HTTP.get(URI("http://localhost:#{PORT}/status")) 0268 rescue => e 0269 i += 1 0270 if i < 30 0271 logger.info 'not up yet' 0272 sleep 0.5 0273 retry 0274 end 0275 raise e 0276 end 0277 0278 logger.info "starting test #{ARGV}" 0279 IO.popen(ARGV, 'r', &:readlines) 0280 ret = $?.success? 0281 logger.info 'tests done' 0282 end 0283 end 0284 end 0285 end 0286 0287 logger.info "run.rb exiting #{ret}" 0288 ret ? exit : abort