Manual test suite: use more precise data when comparing image size.
[tails.git] / features / step_definitions / common_steps.rb
blob744184db07b24b99c56f198ac0d75be95af4cdc7
1 require 'fileutils'
3 def post_vm_start_hook
4   # Sometimes the first click is lost (presumably it's used to give
5   # focus to virt-viewer or similar) so we do that now rather than
6   # having an important click lost. The point we click should be
7   # somewhere where no clickable elements generally reside.
8   @screen.click_point(@screen.w - 1, @screen.h/2)
9 end
11 def context_menu_helper(top, bottom, menu_item)
12   try_for(60) do
13     t = @screen.wait(top, 10)
14     b = @screen.wait(bottom, 10)
15     # In Sikuli, lower x == closer to the left, lower y == closer to the top
16     assert(t.y < b.y)
17     center = Sikuli::Location.new(((t.x + t.w) + b.x)/2,
18                                   ((t.y + t.h) + b.y)/2)
19     @screen.right_click(center)
20     @screen.hide_cursor
21     @screen.wait_and_click(menu_item, 10)
22     return
23   end
24 end
26 def post_snapshot_restore_hook
27   $vm.wait_until_remote_shell_is_up
28   post_vm_start_hook
30   # The guest's Tor's circuits' states are likely to get out of sync
31   # with the other relays, so we ensure that we have fresh circuits.
32   # Time jumps and incorrect clocks also confuses Tor in many ways.
33   if $vm.has_network?
34     if $vm.execute("systemctl --quiet is-active tor@default.service").success?
35       $vm.execute("systemctl stop tor@default.service")
36       $vm.execute("systemctl --no-block restart tails-tor-has-bootstrapped.target")
37       $vm.host_to_guest_time_sync
38       $vm.execute("systemctl start tor@default.service")
39       wait_until_tor_is_working
40     end
41   else
42     $vm.host_to_guest_time_sync
43   end
44 end
46 Given /^a computer$/ do
47   $vm.destroy_and_undefine if $vm
48   $vm = VM.new($virt, VM_XML_PATH, $vmnet, $vmstorage, DISPLAY)
49 end
51 Given /^the computer is set to boot from the Tails DVD$/ do
52   $vm.set_cdrom_boot(TAILS_ISO)
53 end
55 Given /^the computer is set to boot from (.+?) drive "(.+?)"$/ do |type, name|
56   $vm.set_disk_boot(name, type.downcase)
57 end
59 Given /^I (temporarily )?create an? (\d+) ([[:alpha:]]+) (?:([[:alpha:]]+) )?disk named "([^"]+)"$/ do |temporary, size, unit, type, name|
60   type ||= "qcow2"
61   $vm.storage.create_new_disk(name, {:size => size, :unit => unit,
62                                      :type => type})
63   add_after_scenario_hook { $vm.storage.delete_volume(name) } if temporary
64 end
66 Given /^I plug (.+) drive "([^"]+)"$/ do |bus, name|
67   $vm.plug_drive(name, bus.downcase)
68   sleep 1
69   if $vm.is_running?
70     step "drive \"#{name}\" is detected by Tails"
71   end
72 end
74 Then /^drive "([^"]+)" is detected by Tails$/ do |name|
75   raise "Tails is not running" unless $vm.is_running?
76   try_for(20, :msg => "Drive '#{name}' is not detected by Tails") do
77     $vm.disk_detected?(name)
78   end
79 end
81 Given /^the network is plugged$/ do
82   $vm.plug_network
83 end
85 Given /^the network is unplugged$/ do
86   $vm.unplug_network
87 end
89 Given /^the network connection is ready(?: within (\d+) seconds)?$/ do |timeout|
90   timeout ||= 30
91   try_for(timeout.to_i) { $vm.has_network? }
92 end
94 Given /^the hardware clock is set to "([^"]*)"$/ do |time|
95   $vm.set_hardware_clock(DateTime.parse(time).to_time)
96 end
98 Given /^I capture all network traffic$/ do
99   @sniffer = Sniffer.new("sniffer", $vmnet)
100   @sniffer.capture
101   add_after_scenario_hook do
102     @sniffer.stop
103     @sniffer.clear
104   end
107 Given /^I set Tails to boot with options "([^"]*)"$/ do |options|
108   @boot_options = options
111 When /^I start the computer$/ do
112   assert(!$vm.is_running?,
113          "Trying to start a VM that is already running")
114   $vm.start
115   post_vm_start_hook
118 Given /^I start Tails( from DVD)?( with network unplugged)?( and I login)?$/ do |dvd_boot, network_unplugged, do_login|
119   step "the computer is set to boot from the Tails DVD" if dvd_boot
120   if network_unplugged
121     step "the network is unplugged"
122   else
123     step "the network is plugged"
124   end
125   step "I start the computer"
126   step "the computer boots Tails"
127   if do_login
128     step "I log in to a new session"
129     if network_unplugged
130       step "all notifications have disappeared"
131     else
132       step "Tor is ready"
133       step "all notifications have disappeared"
134       step "available upgrades have been checked"
135     end
136   end
139 Given /^I start Tails from (.+?) drive "(.+?)"( with network unplugged)?( and I login( with persistence enabled)?)?$/ do |drive_type, drive_name, network_unplugged, do_login, persistence_on|
140   step "the computer is set to boot from #{drive_type} drive \"#{drive_name}\""
141   if network_unplugged
142     step "the network is unplugged"
143   else
144     step "the network is plugged"
145   end
146   step "I start the computer"
147   step "the computer boots Tails"
148   if do_login
149     step "I enable persistence" if persistence_on
150     step "I log in to a new session"
151     if network_unplugged
152       step "all notifications have disappeared"
153     else
154       step "Tor is ready"
155       step "all notifications have disappeared"
156       step "available upgrades have been checked"
157     end
158   end
161 When /^I power off the computer$/ do
162   assert($vm.is_running?,
163          "Trying to power off an already powered off VM")
164   $vm.power_off
167 When /^I cold reboot the computer$/ do
168   step "I power off the computer"
169   step "I start the computer"
172 When /^I destroy the computer$/ do
173   $vm.destroy_and_undefine
176 def boot_menu_cmdline_image
177   case @os_loader
178   when "UEFI"
179     'TailsBootMenuKernelCmdlineUEFI.png'
180   else
181     'TailsBootMenuKernelCmdline.png'
182   end
185 def boot_menu_tab_msg_image
186   case @os_loader
187   when "UEFI"
188     'TailsBootSplashTabMsgUEFI.png'
189   else
190     'TailsBootSplashTabMsg.png'
191   end
194 Given /^Tails is at the boot menu's cmdline( after rebooting)?$/ do |reboot|
195   boot_timeout = 3*60
196   # Simply looking for the boot splash image is not robust; sometimes
197   # sikuli is not fast enough to see it. Here we hope that spamming
198   # TAB, which will halt the boot process by showing the prompt for
199   # the kernel cmdline, will make this a bit more robust. We want this
200   # spamming to happen in parallel with Sikuli waiting for the image,
201   # but multi-threading etc is working extremely poor in our Ruby +
202   # jrb environment when Sikuli is involved. Hence we run the spamming
203   # from a separate process.
204   tab_spammer_code = <<-EOF
205     require 'libvirt'
206     tab_key_code = 0xf
207     virt = Libvirt::open("qemu:///system")
208     begin
209       domain = virt.lookup_domain_by_name('#{$vm.domain_name}')
210       loop do
211         domain.send_key(Libvirt::Domain::KEYCODE_SET_LINUX, 0, [tab_key_code])
212         sleep 0.1
213       end
214     ensure
215       virt.close
216     end
217   EOF
218   # Our UEFI firmware (OVMF) has the interesting "feature" that pressing
219   # any button will open its setup menu, so we have to exit the setup,
220   # and to not have the TAB spammer potentially interfering we pause
221   # it meanwhile.
222   dealt_with_uefi_setup = false
223   # The below code is not completely reliable, so we might have to
224   # retry by rebooting.
225   try_for(boot_timeout) do
226     begin
227       tab_spammer = IO.popen(['ruby', '-e', tab_spammer_code])
228       if not(dealt_with_uefi_setup) && @os_loader == 'UEFI'
229         @screen.wait('UEFIFirmwareSetup.png', 30)
230         Process.kill("TSTP", tab_spammer.pid)
231         @screen.type(Sikuli::Key.ENTER)
232         Process.kill("CONT", tab_spammer.pid)
233         dealt_with_uefi_setup = true
234       end
235       @screen.wait(boot_menu_cmdline_image, 15)
236     rescue FindFailed => e
237       debug_log('We missed the boot menu before we could deal with it, ' +
238                 'resetting...')
239       dealt_with_uefi_setup = false
240       $vm.reset
241       raise e
242     ensure
243       Process.kill("TERM", tab_spammer.pid)
244       tab_spammer.close
245     end
246     true
247   end
250 Given /^the computer (re)?boots Tails$/ do |reboot|
251   step "Tails is at the boot menu's cmdline" + (reboot ? ' after rebooting' : '')
252   @screen.type(" autotest_never_use_this_option blacklist=psmouse #{@boot_options}" +
253                Sikuli::Key.ENTER)
254   @screen.wait('TailsGreeter.png', 5*60)
255   $vm.wait_until_remote_shell_is_up
256   step 'I configure Tails to use a simulated Tor network'
259 Given /^I log in to a new session(?: in )?(|German)$/ do |lang|
260   case lang
261   when 'German'
262     @language = "German"
263     @screen.wait_and_click('TailsGreeterLanguage.png', 10)
264     @screen.wait('TailsGreeterLanguagePopover.png', 10)
265     @screen.type(@language)
266     sleep(2) # Gtk needs some time to filter the results
267     @screen.type(Sikuli::Key.ENTER)
268     @screen.wait_and_click("TailsGreeterLoginButton#{@language}.png", 10)
269   when ''
270     @screen.wait_and_click('TailsGreeterLoginButton.png', 10)
271   else
272     raise "Unsupported language: #{lang}"
273   end
274   step 'Tails Greeter has applied all settings'
275   step 'the Tails desktop is ready'
278 def open_greeter_additional_settings
279   @screen.click('TailsGreeterAddMoreOptions.png')
280   @screen.wait('TailsGreeterAdditionalSettingsDialog.png', 10)
283 Given /^I open Tails Greeter additional settings dialog$/ do
284   open_greeter_additional_settings()
287 Given /^I enable the specific Tor configuration option$/ do
288   open_greeter_additional_settings()
289   @screen.wait_and_click('TailsGreeterNetworkConnection.png', 30)
290   @screen.wait_and_click("TailsGreeterSpecificTorConfiguration.png", 10)
291   @screen.wait_and_click("TailsGreeterAdditionalSettingsAdd.png", 10)
294 Given /^I set an administration password$/ do
295   open_greeter_additional_settings()
296   @screen.wait_and_click("TailsGreeterAdminPassword.png", 20)
297   @screen.type(@sudo_password)
298   @screen.type(Sikuli::Key.TAB)
299   @screen.type(@sudo_password)
300   @screen.type(Sikuli::Key.ENTER)
303 Given /^Tails Greeter has applied all settings$/ do
304   # I.e. it is done with PostLogin, which is ensured to happen before
305   # a logind session is opened for LIVE_USER.
306   try_for(120) {
307     $vm.execute_successfully("loginctl").stdout
308       .match(/^\s*\S+\s+\d+\s+#{LIVE_USER}\s+seat\d+\s+\S+\s*$/) != nil
309   }
312 Given /^the Tails desktop is ready$/ do
313   desktop_started_picture = "GnomeApplicationsMenu#{@language}.png"
314   @screen.wait(desktop_started_picture, 180)
315   # Workaround #13461 by restarting nautilus-desktop
316   # if Desktop icons are not visible
317   begin
318     @screen.wait("DesktopTailsDocumentation.png", 30)
319   rescue FindFailed
320     step 'I kill the process "nautilus-desktop"'
321     $vm.spawn('nautilus-desktop', user: LIVE_USER)
322     @screen.wait("DesktopTailsDocumentation.png", 30)
323   end
324   # Disable screen blanking since we sometimes need to wait long
325   # enough for it to activate, which can mess with Sikuli wait():ing
326   # for some image.
327   $vm.execute_successfully(
328     'gsettings set org.gnome.desktop.session idle-delay 0',
329     :user => LIVE_USER
330   )
331   # We need to enable the accessibility toolkit for dogtail.
332   $vm.execute_successfully(
333     'gsettings set org.gnome.desktop.interface toolkit-accessibility true',
334     :user => LIVE_USER,
335   )
338 When /^I see the "(.+)" notification(?: after at most (\d+) seconds)?$/ do |title, timeout|
339   timeout = timeout ? timeout.to_i : nil
340   gnome_shell = Dogtail::Application.new('gnome-shell')
341   notification_list = gnome_shell.child(
342     'No Notifications', roleName: 'label', showingOnly: false
343   ).parent.parent
344   try_for(timeout) do
345     notification_list.child?(title, roleName: 'label', showingOnly: false)
346   end
349 Given /^Tor is ready$/ do
350   step "Tor has built a circuit"
351   step "the time has synced"
352   begin
353     try_for(30) { $vm.execute('systemctl is-system-running').success? }
354   rescue Timeout::Error
355     jobs = $vm.execute('systemctl list-jobs').stdout
356     units_status = $vm.execute('systemctl').stdout
357     raise "The system is not fully running yet:\n#{jobs}\n#{units_status}"
358   end
361 Given /^Tor has built a circuit$/ do
362   wait_until_tor_is_working
365 class TimeSyncingError < StandardError
368 Given /^the time has synced$/ do
369   begin
370     ["/run/tordate/done", "/run/htpdate/success"].each do |file|
371       try_for(300) { $vm.execute("test -e #{file}").success? }
372     end
373   rescue
374     File.open("#{$config["TMPDIR"]}/log.htpdate", 'w') do |file|
375       file.write($vm.execute('cat /var/log/htpdate.log').stdout)
376     end
377     raise TimeSyncingError.new("Time syncing failed")
378   end
381 Given /^available upgrades have been checked$/ do
382   try_for(300) {
383     $vm.execute("test -e '/run/tails-upgrader/checked_upgrades'").success?
384   }
387 When /^I start the Tor Browser( in offline mode)?$/ do |offline|
388   step 'I start "Tor Browser" via GNOME Activities Overview'
389   if offline
390     offline_prompt = Dogtail::Application.new('zenity')
391                      .dialog('Tor is not ready')
392     start_button = offline_prompt.button('Start Tor Browser')
393     start_button.grabFocus
394     start_button.click
395   end
396   step "the Tor Browser has started#{offline}"
397   if offline
398     step 'the Tor Browser shows the "The proxy server is refusing connections" error'
399   end
402 Given /^the Tor Browser (?:has started|starts)( in offline mode)?$/ do |offline|
403   try_for(60) do
404     @torbrowser = Dogtail::Application.new('Firefox')
405     @torbrowser.child?(roleName: 'frame', recursive: false)
406   end
409 Given /^the Tor Browser loads the (startup page|Tails homepage|Tails roadmap)$/ do |page|
410   case page
411   when "startup page"
412     title = 'Tails'
413   when "Tails homepage"
414     title = 'Tails - Privacy for anyone anywhere'
415   when "Tails roadmap"
416     title = 'Roadmap - Tails - Tails Ticket Tracker'
417   else
418     raise "Unsupported page: #{page}"
419   end
420   step "\"#{title}\" has loaded in the Tor Browser"
423 When /^I request a new identity using Torbutton$/ do
424   @torbrowser.child('Tor Browser', roleName: 'push button').click
425   @torbrowser.child('New Identity', roleName: 'push button').click
428 When /^I acknowledge Torbutton's New Identity confirmation prompt$/ do
429   @screen.wait('GnomeQuestionDialogIcon.png', 30)
430   step 'I type "y"'
433 Given /^I add a bookmark to eff.org in the Tor Browser$/ do
434   url = "https://www.eff.org"
435   step "I open the address \"#{url}\" in the Tor Browser"
436   step 'the Tor Browser shows the "The proxy server is refusing connections" error'
437   @screen.type("d", Sikuli::KeyModifier.CTRL)
438   @screen.wait("TorBrowserBookmarkPrompt.png", 10)
439   @screen.type(url + Sikuli::Key.ENTER)
442 Given /^the Tor Browser has a bookmark to eff.org$/ do
443   @screen.type("b", Sikuli::KeyModifier.ALT)
444   @screen.wait("TorBrowserEFFBookmark.png", 10)
447 Given /^all notifications have disappeared$/ do
448   # These magic coordinates always locates GNOME's clock in the top
449   # bar, which when clicked opens the calendar.
450   x, y = 512, 10
451   gnome_shell = Dogtail::Application.new('gnome-shell')
452   retry_action(10, recovery_proc: Proc.new { @screen.type(Sikuli::Key.ESC) }) do
453     @screen.click_point(x, y)
454     unless gnome_shell.child?('No Notifications', roleName: 'label')
455       @screen.click('GnomeCloseAllNotificationsButton.png')
456     end
457     gnome_shell.child?('No Notifications', roleName: 'label')
458   end
459   @screen.type(Sikuli::Key.ESC)
462 Then /^I (do not )?see "([^"]*)" after at most (\d+) seconds$/ do |negation, image, time|
463   if negation
464     @screen.waitVanish(image, time.to_i)
465   else
466     @screen.wait(image, time.to_i)
467   end
470 Then /^all Internet traffic has only flowed through Tor$/ do
471   allowed_hosts = allowed_hosts_under_tor_enforcement
472   assert_all_connections(@sniffer.pcap_file) do |c|
473     allowed_hosts.include?({ address: c.daddr, port: c.dport })
474   end
477 Given /^I enter the sudo password in the pkexec prompt$/ do
478   step "I enter the \"#{@sudo_password}\" password in the pkexec prompt"
481 def deal_with_polkit_prompt(password, opts = {})
482   opts[:expect_success] ||= true
483   image = 'PolicyKitAuthPrompt.png'
484   @screen.wait(image, 60)
485   @screen.type(password)
486   @screen.type(Sikuli::Key.ENTER)
487   if opts[:expect_success]
488     @screen.waitVanish(image, 20)
489   else
490     @screen.wait('PolicyKitAuthFailure.png', 20)
491   end
494 Given /^I enter the "([^"]*)" password in the pkexec prompt$/ do |password|
495   deal_with_polkit_prompt(password)
498 Given /^process "([^"]+)" is (not )?running$/ do |process, not_running|
499   if not_running
500     assert(!$vm.has_process?(process), "Process '#{process}' is running")
501   else
502     assert($vm.has_process?(process), "Process '#{process}' is not running")
503   end
506 Given /^process "([^"]+)" is running within (\d+) seconds$/ do |process, time|
507   try_for(time.to_i, :msg => "Process '#{process}' is not running after " +
508                              "waiting for #{time} seconds") do
509     $vm.has_process?(process)
510   end
513 Given /^process "([^"]+)" has stopped running after at most (\d+) seconds$/ do |process, time|
514   try_for(time.to_i, :msg => "Process '#{process}' is still running after " +
515                              "waiting for #{time} seconds") do
516     not $vm.has_process?(process)
517   end
520 Given /^I kill the process "([^"]+)"$/ do |process|
521   $vm.execute("killall #{process}")
522   try_for(10, :msg => "Process '#{process}' could not be killed") {
523     !$vm.has_process?(process)
524   }
527 Then /^Tails eventually (shuts down|restarts)$/ do |mode|
528   try_for(3*60) do
529     if mode == 'restarts'
530       @screen.find('TailsGreeter.png')
531       true
532     else
533       ! $vm.is_running?
534     end
535   end
538 Given /^I shutdown Tails and wait for the computer to power off$/ do
539   $vm.spawn("poweroff")
540   step 'Tails eventually shuts down'
543 When /^I request a shutdown using the emergency shutdown applet$/ do
544   @screen.hide_cursor
545   @screen.wait_and_click('TailsEmergencyShutdownButton.png', 10)
546   # Sometimes the next button too fast, before the menu has settled
547   # down to its final size and the icon we want to click is in its
548   # final position. dogtail might allow us to fix that, but given how
549   # rare this problem is, it's not worth the effort.
550   step 'I wait 5 seconds'
551   @screen.wait_and_click('TailsEmergencyShutdownHalt.png', 10)
554 When /^I warm reboot the computer$/ do
555   $vm.spawn("reboot")
558 When /^I request a reboot using the emergency shutdown applet$/ do
559   @screen.hide_cursor
560   @screen.wait_and_click('TailsEmergencyShutdownButton.png', 10)
561   # See comment on /^I request a shutdown using the emergency shutdown applet$/
562   # that explains why we need to wait.
563   step 'I wait 5 seconds'
564   @screen.wait_and_click('TailsEmergencyShutdownReboot.png', 10)
567 Given /^the package "([^"]+)" is installed$/ do |package|
568   assert($vm.execute("dpkg -s '#{package}' 2>/dev/null | grep -qs '^Status:.*installed$'").success?,
569          "Package '#{package}' is not installed")
572 Given /^I add a ([a-z0-9.]+ |)wired DHCP NetworkManager connection called "([^"]+)"$/ do |version, con_name|
573   if version and version == '2.x'
574     con_content = <<EOF
575 [connection]
576 id=#{con_name}
577 uuid=b04afa94-c3a1-41bf-aa12-1a743d964162
578 interface-name=eth0
579 type=ethernet
581     con_file = "/etc/NetworkManager/system-connections/#{con_name}"
582     $vm.file_overwrite(con_file, con_content)
583     $vm.execute_successfully("chmod 600 '#{con_file}'")
584     $vm.execute_successfully("nmcli connection load '#{con_file}'")
585   elsif version and version == '3.x'
586     raise "Unsupported version '#{version}'"
587   else
588     $vm.execute_successfully(
589       "nmcli connection add con-name #{con_name} " + \
590       "type ethernet autoconnect yes ifname eth0"
591     )
592   end
593   try_for(10) {
594     nm_con_list = $vm.execute("nmcli --terse --fields NAME connection show").stdout
595     nm_con_list.split("\n").include? "#{con_name}"
596   }
599 Given /^I switch to the "([^"]+)" NetworkManager connection$/ do |con_name|
600   $vm.execute("nmcli connection up id #{con_name}")
601   try_for(60) do
602     $vm.execute("nmcli --terse --fields NAME,STATE connection show").stdout.chomp.split("\n").include?("#{con_name}:activated")
603   end
606 When /^I start and focus GNOME Terminal$/ do
607   step 'I start "GNOME Terminal" via GNOME Activities Overview'
608   @screen.wait('GnomeTerminalWindow.png', 40)
611 When /^I run "([^"]+)" in GNOME Terminal$/ do |command|
612   if !$vm.has_process?("gnome-terminal-server")
613     step "I start and focus GNOME Terminal"
614   else
615     @screen.wait_and_click('GnomeTerminalWindow.png', 20)
616   end
617   @screen.type(command + Sikuli::Key.ENTER)
620 When /^the file "([^"]+)" exists(?:| after at most (\d+) seconds)$/ do |file, timeout|
621   timeout = 0 if timeout.nil?
622   try_for(
623     timeout.to_i,
624     :msg => "The file #{file} does not exist after #{timeout} seconds"
625   ) {
626     $vm.file_exist?(file)
627   }
630 When /^the file "([^"]+)" does not exist$/ do |file|
631   assert(! ($vm.file_exist?(file)))
634 When /^the directory "([^"]+)" exists$/ do |directory|
635   assert($vm.directory_exist?(directory))
638 When /^the directory "([^"]+)" does not exist$/ do |directory|
639   assert(! ($vm.directory_exist?(directory)))
642 When /^I copy "([^"]+)" to "([^"]+)" as user "([^"]+)"$/ do |source, destination, user|
643   c = $vm.execute("cp \"#{source}\" \"#{destination}\"", :user => LIVE_USER)
644   assert(c.success?, "Failed to copy file:\n#{c.stdout}\n#{c.stderr}")
647 def is_persistent?(app)
648   conf = get_persistence_presets_config(true)["#{app}"]
649   c = $vm.execute("findmnt --noheadings --output SOURCE --target '#{conf}'")
650   # This check assumes that we haven't enabled read-only persistence.
651   c.success? and c.stdout.chomp != "aufs"
654 Then /^persistence for "([^"]+)" is (|not )enabled$/ do |app, enabled|
655   case enabled
656   when ''
657     assert(is_persistent?(app), "Persistence should be enabled.")
658   when 'not '
659     assert(!is_persistent?(app), "Persistence should not be enabled.")
660   end
663 Given /^I start "([^"]+)" via GNOME Activities Overview$/ do |app_name|
664   # Search disambiguations: below we assume that there is only one
665   # result, since multiple results introduces a race that leads to a
666   # non-deterministic choice (at least under load). To make the life
667   # easier for users of this step, let's collect workarounds here.
668   case app_name
669   when 'GNOME Terminal'
670     # "GNOME Terminal" and "Terminal" shows both the (non-Root)
671     # "Terminal" and "Root Terminal" search results, so let's use a
672     # keyword only found in the former's .desktop file.
673     app_name = 'commandline'
674   end
675   @screen.wait('GnomeApplicationsMenu.png', 10)
676   $vm.execute_successfully('xdotool key Super', user: LIVE_USER)
677   @screen.wait('GnomeActivitiesOverview.png', 10)
678   # Trigger startup of search providers
679   @screen.type(app_name[0])
680   # Give search providers some time to start (#13469#note-5) otherwise
681   # our search sometimes returns no results at all.
682   sleep 1
683   # Type the rest of the search query
684   @screen.type(app_name[1..-1])
685   @screen.type(Sikuli::Key.ENTER, Sikuli::KeyModifier.CTRL)
688 When /^I type "([^"]+)"$/ do |string|
689   @screen.type(string)
692 When /^I press the "([^"]+)" key$/ do |key|
693   begin
694     @screen.type(eval("Sikuli::Key.#{key}"))
695   rescue RuntimeError
696     raise "unsupported key #{key}"
697   end
700 Then /^the (amnesiac|persistent) Tor Browser directory (exists|does not exist)$/ do |persistent_or_not, mode|
701   case persistent_or_not
702   when "amnesiac"
703     dir = "/home/#{LIVE_USER}/Tor Browser"
704   when "persistent"
705     dir = "/home/#{LIVE_USER}/Persistent/Tor Browser"
706   end
707   step "the directory \"#{dir}\" #{mode}"
710 Then /^there is a GNOME bookmark for the (amnesiac|persistent) Tor Browser directory$/ do |persistent_or_not|
711   case persistent_or_not
712   when "amnesiac"
713     bookmark_image = 'TorBrowserAmnesicFilesBookmark.png'
714   when "persistent"
715     bookmark_image = 'TorBrowserPersistentFilesBookmark.png'
716   end
717   @screen.wait_and_click('GnomePlaces.png', 10)
718   @screen.wait(bookmark_image, 40)
719   @screen.type(Sikuli::Key.ESC)
722 Then /^there is no GNOME bookmark for the persistent Tor Browser directory$/ do
723   try_for(65) do
724     @screen.wait_and_click('GnomePlaces.png', 10)
725     @screen.wait("GnomePlacesWithoutTorBrowserPersistent.png", 10)
726     @screen.type(Sikuli::Key.ESC)
727   end
730 def pulseaudio_sink_inputs
731   pa_info = $vm.execute_successfully('pacmd info', :user => LIVE_USER).stdout
732   sink_inputs_line = pa_info.match(/^\d+ sink input\(s\) available\.$/)[0]
733   return sink_inputs_line.match(/^\d+/)[0].to_i
736 When /^(no|\d+) application(?:s?) (?:is|are) playing audio(?:| after (\d+) seconds)$/ do |nb, wait_time|
737   nb = 0 if nb == "no"
738   sleep wait_time.to_i if ! wait_time.nil?
739   assert_equal(nb.to_i, pulseaudio_sink_inputs)
742 When /^I double-click on the (Tails documentation|Report an Error) launcher on the desktop$/ do |launcher|
743   image = 'Desktop' + launcher.split.map { |s| s.capitalize } .join + '.png'
744   info = xul_application_info('Tor Browser')
745   # Sometimes the double-click is lost (#12131).
746   retry_action(10) do
747     @screen.wait_and_double_click(image, 10) if $vm.execute("pgrep --uid #{info[:user]} --full --exact '#{info[:cmd_regex]}'").failure?
748     step 'the Tor Browser has started'
749   end
752 When /^I (can|cannot) save the current page as "([^"]+[.]html)" to the (.*) directory$/ do |should_work, output_file, output_dir|
753   should_work = should_work == 'can' ? true : false
754   @screen.type("s", Sikuli::KeyModifier.CTRL)
755   @screen.wait("Gtk3SaveFileDialog.png", 10)
756   if output_dir == "persistent Tor Browser"
757     output_dir = "/home/#{LIVE_USER}/Persistent/Tor Browser"
758     @screen.wait_and_click("GtkTorBrowserPersistentBookmark.png", 10)
759     @screen.wait("GtkTorBrowserPersistentBookmarkSelected.png", 10)
760     # The output filename (without its extension) is already selected,
761     # let's use the keyboard shortcut to focus its field
762     @screen.type("n", Sikuli::KeyModifier.ALT)
763     @screen.wait("TorBrowserSaveOutputFileSelected.png", 10)
764   elsif output_dir == "default downloads"
765     output_dir = "/home/#{LIVE_USER}/Tor Browser"
766   else
767     @screen.type(output_dir + '/')
768   end
769   # Only the part of the filename before the .html extension can be easily replaced
770   # so we have to remove it before typing it into the arget filename entry widget.
771   @screen.type(output_file.sub(/[.]html$/, ''))
772   @screen.type(Sikuli::Key.ENTER)
773   if should_work
774     try_for(20, :msg => "The page was not saved to #{output_dir}/#{output_file}") {
775       $vm.file_exist?("#{output_dir}/#{output_file}")
776     }
777   else
778     @screen.wait("TorBrowserCannotSavePage.png", 10)
779   end
782 When /^I can print the current page as "([^"]+[.]pdf)" to the (default downloads|persistent Tor Browser) directory$/ do |output_file, output_dir|
783   if output_dir == "persistent Tor Browser"
784     output_dir = "/home/#{LIVE_USER}/Persistent/Tor Browser"
785   else
786     output_dir = "/home/#{LIVE_USER}/Tor Browser"
787   end
788   @screen.type("p", Sikuli::KeyModifier.CTRL)
789   print_dialog = @torbrowser.child('Print', roleName: 'dialog')
790   print_dialog.child('Print to File', 'table cell').click
791   print_dialog.child('~/Tor Browser/output.pdf', roleName: 'push button').click()
792   @screen.wait("Gtk3PrintFileDialog.png", 10)
793   # Only the file's basename is selected when the file selector dialog opens,
794   # so we type only the desired file's basename to replace it
795   $vm.set_clipboard(output_dir + '/' + output_file.sub(/[.]pdf$/, ''))
796   @screen.type('v', Sikuli::KeyModifier.CTRL)
797   @screen.type(Sikuli::Key.ENTER)
798   @screen.wait_and_click("Gtk3PrintButton.png", 10)
799   try_for(30, :msg => "The page was not printed to #{output_dir}/#{output_file}") {
800     $vm.file_exist?("#{output_dir}/#{output_file}")
801   }
804 Given /^a web server is running on the LAN$/ do
805   @web_server_ip_addr = $vmnet.bridge_ip_addr
806   @web_server_port = 8000
807   @web_server_url = "http://#{@web_server_ip_addr}:#{@web_server_port}"
808   web_server_hello_msg = "Welcome to the LAN web server!"
810   # I've tested ruby Thread:s, fork(), etc. but nothing works due to
811   # various strange limitations in the ruby interpreter. For instance,
812   # apparently concurrent IO has serious limits in the thread
813   # scheduler (e.g. sikuli's wait() would block WEBrick from reading
814   # from its socket), and fork():ing results in a lot of complex
815   # cucumber stuff (like our hooks!) ending up in the child process,
816   # breaking stuff in the parent process. After asking some supposed
817   # ruby pros, I've settled on the following.
818   code = <<-EOF
819   require "webrick"
820   STDOUT.reopen("/dev/null", "w")
821   STDERR.reopen("/dev/null", "w")
822   server = WEBrick::HTTPServer.new(:BindAddress => "#{@web_server_ip_addr}",
823                                    :Port => #{@web_server_port},
824                                    :DocumentRoot => "/dev/null")
825   server.mount_proc("/") do |req, res|
826     res.body = "#{web_server_hello_msg}"
827   end
828   server.start
830   add_lan_host(@web_server_ip_addr, @web_server_port)
831   proc = IO.popen(['ruby', '-e', code])
832   try_for(10, :msg => "It seems the LAN web server failed to start") do
833     Process.kill(0, proc.pid) == 1
834   end
836   add_after_scenario_hook { Process.kill("TERM", proc.pid) }
838   # It seems necessary to actually check that the LAN server is
839   # serving, possibly because it isn't doing so reliably when setting
840   # up. If e.g. the Unsafe Browser (which *should* be able to access
841   # the web server) tries to access it too early, Firefox seems to
842   # take some random amount of time to retry fetching. Curl gives a
843   # more consistent result, so let's rely on that instead. Note that
844   # this forces us to capture traffic *after* this step in case
845   # accessing this server matters, like when testing the Tor Browser..
846   try_for(30, :msg => "Something is wrong with the LAN web server") do
847     msg = $vm.execute_successfully("curl #{@web_server_url}",
848                                    :user => LIVE_USER).stdout.chomp
849     web_server_hello_msg == msg
850   end
853 When /^I open a page on the LAN web server in the (.*)$/ do |browser|
854   step "I open the address \"#{@web_server_url}\" in the #{browser}"
857 Given /^I wait (?:between (\d+) and )?(\d+) seconds$/ do |min, max|
858   if min
859     time = rand(max.to_i - min.to_i + 1) + min.to_i
860   else
861     time = max.to_i
862   end
863   puts "Slept for #{time} seconds"
864   sleep(time)
867 Given /^I (?:re)?start monitoring the AppArmor log of "([^"]+)"$/ do |profile|
868   # AppArmor log entries may be dropped if printk rate limiting is
869   # enabled.
870   $vm.execute_successfully('sysctl -w kernel.printk_ratelimit=0')
871   # We will only care about entries for this profile from this time
872   # and on.
873   guest_time = $vm.execute_successfully(
874     'date +"%Y-%m-%d %H:%M:%S"').stdout.chomp
875   @apparmor_profile_monitoring_start ||= Hash.new
876   @apparmor_profile_monitoring_start[profile] = guest_time
879 When /^AppArmor has (not )?denied "([^"]+)" from opening "([^"]+)"$/ do |anti_test, profile, file|
880   assert(@apparmor_profile_monitoring_start &&
881          @apparmor_profile_monitoring_start[profile],
882          "It seems the profile '#{profile}' isn't being monitored by the " +
883          "'I monitor the AppArmor log of ...' step")
884   audit_line_regex = 'apparmor="DENIED" operation="open" profile="%s" name="%s"' % [profile, file]
885   begin
886     try_for(10, { :delay => 1 }) {
887       audit_log = $vm.execute(
888         "journalctl --full --no-pager " +
889         "--since='#{@apparmor_profile_monitoring_start[profile]}' " +
890         "SYSLOG_IDENTIFIER=kernel | grep -w '#{audit_line_regex}'"
891       ).stdout.chomp
892       assert(audit_log.empty? == (anti_test ? true : false))
893       true
894     }
895   rescue Timeout::Error, Test::Unit::AssertionFailedError => e
896     raise e, "AppArmor has #{anti_test ? "" : "not "}denied the operation"
897   end
900 Then /^I force Tor to use a new circuit$/ do
901   force_new_tor_circuit
904 When /^I eject the boot medium$/ do
905   dev = boot_device
906   dev_type = device_info(dev)['ID_TYPE']
907   case dev_type
908   when 'cd'
909     $vm.eject_cdrom
910   when 'disk'
911     boot_disk_name = $vm.disk_name(dev)
912     $vm.unplug_drive(boot_disk_name)
913   else
914     raise "Unsupported medium type '#{dev_type}' for boot device '#{dev}'"
915   end
918 Given /^Tails is fooled to think it is running version (.+)$/ do |version|
919   $vm.execute_successfully(
920     "sed -i " +
921     "'s/^TAILS_VERSION_ID=.*$/TAILS_VERSION_ID=\"#{version}\"/' " +
922     "/etc/os-release"
923   )
926 Then /^Tails is running version (.+)$/ do |version|
927   v1 = $vm.execute_successfully('tails-version').stdout.split.first
928   assert_equal(version, v1, "The version doesn't match tails-version's output")
929   v2 = $vm.file_content('/etc/os-release')
930        .scan(/TAILS_VERSION_ID="(#{version})"/).flatten.first
931   assert_equal(version, v2, "The version doesn't match /etc/os-release")
934 def share_host_files(files)
935   files = [files] if files.class == String
936   assert_equal(Array, files.class)
937   disk_size = files.map { |f| File.new(f).size } .inject(0, :+)
938   # Let's add some extra space for filesystem overhead etc.
939   disk_size += [convert_to_bytes(1, 'MiB'), (disk_size * 0.15).ceil].max
940   disk = random_alpha_string(10)
941   step "I temporarily create an #{disk_size} bytes disk named \"#{disk}\""
942   step "I create a gpt partition labeled \"#{disk}\" with an ext4 " +
943        "filesystem on disk \"#{disk}\""
944   $vm.storage.guestfs_disk_helper(disk) do |g, _|
945     partition = g.list_partitions().first
946     g.mount(partition, "/")
947     files.each { |f| g.upload(f, "/" + File.basename(f)) }
948   end
949   step "I plug USB drive \"#{disk}\""
950   mount_dir = $vm.execute_successfully('mktemp -d').stdout.chomp
951   dev = $vm.disk_dev(disk)
952   partition = dev + '1'
953   $vm.execute_successfully("mount #{partition} #{mount_dir}")
954   $vm.execute_successfully("chmod -R a+rX '#{mount_dir}'")
955   return mount_dir
958 def mount_USB_drive(disk, fs_options = {})
959   fs_options[:encrypted] ||= false
960   @tmp_usb_drive_mount_dir = $vm.execute_successfully('mktemp -d').stdout.chomp
961   dev = $vm.disk_dev(disk)
962   partition = dev + '1'
963   if fs_options[:encrypted]
964     password = fs_options[:password]
965     assert_not_nil(password)
966     luks_mapping = "#{disk}_unlocked"
967     $vm.execute_successfully(
968       "echo #{password} | " +
969       "cryptsetup luksOpen #{partition} #{luks_mapping}"
970     )
971     $vm.execute_successfully(
972       "mount /dev/mapper/#{luks_mapping} #{@tmp_usb_drive_mount_dir}"
973     )
974   else
975     $vm.execute_successfully("mount #{partition} #{@tmp_usb_drive_mount_dir}")
976   end
977   @tmp_filesystem_disk = disk
978   @tmp_filesystem_options = fs_options
979   @tmp_filesystem_partition = partition
980   return @tmp_usb_drive_mount_dir
983 When(/^I plug and mount a (\d+) MiB USB drive with an? (.*)$/) do |size_MiB, fs|
984   disk_size = convert_to_bytes(size_MiB.to_i, 'MiB')
985   disk = random_alpha_string(10)
986   step "I temporarily create an #{disk_size} bytes disk named \"#{disk}\""
987   step "I create a gpt partition labeled \"#{disk}\" with " +
988        "an #{fs} on disk \"#{disk}\""
989   step "I plug USB drive \"#{disk}\""
990   fs_options = {}
991   fs_options[:filesystem] = /(.*) filesystem/.match(fs)[1]
992   if /\bencrypted with password\b/.match(fs)
993     fs_options[:encrypted] = true
994     fs_options[:password] = /encrypted with password "([^"]+)"/.match(fs)[1]
995   end
996   mount_dir = mount_USB_drive(disk, fs_options)
997   @tmp_filesystem_size_b = convert_to_bytes(
998     avail_space_in_mountpoint_kB(mount_dir),
999     'KB'
1000   )
1003 When(/^I mount the USB drive again$/) do
1004   mount_USB_drive(@tmp_filesystem_disk, @tmp_filesystem_options)
1007 When(/^I umount the USB drive$/) do
1008   $vm.execute_successfully("umount #{@tmp_usb_drive_mount_dir}")
1009   if @tmp_filesystem_options[:encrypted]
1010     $vm.execute_successfully("cryptsetup luksClose #{@tmp_filesystem_disk}_unlocked")
1011   end
1014 When /^Tails system time is magically synchronized$/ do
1015   $vm.host_to_guest_time_sync
1018 # Useful for debugging scenarios: e.g. inject this step in a scenario
1019 # at some point when you want to investigate the state.
1020 When /^I pause$/ do
1021   pause
1024 # Useful for debugging Tails features: let's say you want to fix a bug
1025 # exposed by $SCENARIO, and is working on a fix in $FILE locally. To
1026 # immediately test your fix, simply inject this step into $SCENARIO,
1027 # so that $FILE is put in place (obviously this depends on that no
1028 # extra steps are needed to make $FILE's changes go "live").
1029 When /^I upload "([^"]*)" to "([^"]*)"$/ do |source, destination|
1030   [source, destination].each { |s| s.sub!(/\/*$/, '') }
1031   Dir.glob(source).each do |path|
1032     if File.directory?(path)
1033       new_destination = "#{destination}/#{File.basename(path)}"
1034       $vm.execute_successfully("mkdir -p '#{new_destination}'")
1035       Dir.new(path).each do |child|
1036         next if child == '.' or child == '..'
1037         step "I upload \"#{path}/#{child}\" to \"#{new_destination}\""
1038       end
1039     else
1040       File.open(path) do |f|
1041         final_destination = destination
1042         if $vm.directory_exist?(final_destination)
1043           final_destination += "/#{File.basename(path)}"
1044         end
1045         $vm.file_overwrite(final_destination, f.read)
1046       end
1047     end
1048   end