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