From da080c472fc415b0ce918f4dd4a1ab143bb1bca4 Mon Sep 17 00:00:00 2001 From: Philip Hands Date: Mon, 14 Mar 2016 15:36:16 +0100 Subject: rough attempt to grab the good cucumber bits from recent tails --- features/support/hooks.rb | 306 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 212 insertions(+), 94 deletions(-) (limited to 'features/support/hooks.rb') diff --git a/features/support/hooks.rb b/features/support/hooks.rb index d9dc03a7..be8a0235 100644 --- a/features/support/hooks.rb +++ b/features/support/hooks.rb @@ -1,55 +1,145 @@ require 'fileutils' +require 'rb-inotify' require 'time' require 'tmpdir' -# For @product tests -#################### - -def delete_snapshot(snapshot) - if snapshot and File.exist?(snapshot) - File.delete(snapshot) +# Run once, before any feature +AfterConfiguration do |config| + # Reorder the execution of some features. As we progress through a + # run we accumulate more and more snapshots and hence use more and + # more disk space, but some features will leave nothing behind + # and/or possibly use large amounts of disk space temporarily for + # various reasons. By running these first we minimize the amount of + # disk space needed. + prioritized_features = [ + # Features not using snapshots but using large amounts of scratch + # space for other reasons: + 'features/erase_memory.feature', + 'features/untrusted_partitions.feature', + # Features using temporary snapshots: + 'features/apt.feature', + 'features/i2p.feature', + 'features/root_access_control.feature', + 'features/time_syncing.feature', + 'features/tor_bridges.feature', + # This feature needs the almost biggest snapshot (USB install, + # excluding persistence) and will create yet another disk and + # install Tails on it. This should be the peak of disk usage. + 'features/usb_install.feature', + ] + feature_files = config.feature_files + # The &-intersection is specified to keep the element ordering of + # the *left* operand. + intersection = prioritized_features & feature_files + if not intersection.empty? + feature_files -= intersection + feature_files = intersection + feature_files + config.define_singleton_method(:feature_files) { feature_files } end -rescue Errno::EACCES => e - STDERR.puts "Couldn't delete background snapshot: #{e.to_s}" -end -def delete_all_snapshots - Dir.glob("#{$tmp_dir}/*.state").each do |snapshot| - delete_snapshot(snapshot) - end -end + # Used to keep track of when we start our first @product feature, when + # we'll do some special things. + $started_first_product_feature = false -BeforeFeature('@product') do |feature| - if File.exist?($tmp_dir) - if !File.directory?($tmp_dir) - raise "Temporary directory '#{$tmp_dir}' exists but is not a " + + if File.exist?($config["TMPDIR"]) + if !File.directory?($config["TMPDIR"]) + raise "Temporary directory '#{$config["TMPDIR"]}' exists but is not a " + "directory" end - if !File.owned?($tmp_dir) - raise "Temporary directory '#{$tmp_dir}' must be owned by the " + + if !File.owned?($config["TMPDIR"]) + raise "Temporary directory '#{$config["TMPDIR"]}' must be owned by the " + "current user" end - FileUtils.chmod(0755, $tmp_dir) + FileUtils.chmod(0755, $config["TMPDIR"]) else begin - Dir.mkdir($tmp_dir) + FileUtils.mkdir_p($config["TMPDIR"]) rescue Errno::EACCES => e raise "Cannot create temporary directory: #{e.to_s}" end end - delete_all_snapshots if !$keep_snapshots - if $tails_iso.nil? + + # Start a thread that monitors a pseudo fifo file and debug_log():s + # anything written to it "immediately" (well, as fast as inotify + # detects it). We're forced to a convoluted solution like this + # because CRuby's thread support is horribly as soon as IO is mixed + # in (other threads get blocked). + FileUtils.rm(DEBUG_LOG_PSEUDO_FIFO) if File.exist?(DEBUG_LOG_PSEUDO_FIFO) + FileUtils.touch(DEBUG_LOG_PSEUDO_FIFO) + at_exit do + FileUtils.rm(DEBUG_LOG_PSEUDO_FIFO) if File.exist?(DEBUG_LOG_PSEUDO_FIFO) + end + Thread.new do + File.open(DEBUG_LOG_PSEUDO_FIFO) do |fd| + watcher = INotify::Notifier.new + watcher.watch(DEBUG_LOG_PSEUDO_FIFO, :modify) do + line = fd.read.chomp + debug_log(line) if line and line.length > 0 + end + watcher.run + end + end + # Fix Sikuli's debug_log():ing. + bind_java_to_pseudo_fifo_logger +end + +# Common +######## + +After do + if @after_scenario_hooks + @after_scenario_hooks.each { |block| block.call } + end + @after_scenario_hooks = Array.new +end + +BeforeFeature('@product', '@source') do |feature| + raise "Feature #{feature.file} is tagged both @product and @source, " + + "which is an impossible combination" +end + +at_exit do + $vm.destroy_and_undefine if $vm + if $virt + unless KEEP_SNAPSHOTS + VM.remove_all_snapshots + $vmstorage.clear_pool + end + $vmnet.destroy_and_undefine + $virt.close + end + # The artifacts directory is empty (and useless) if it contains + # nothing but the mandatory . and .. + if Dir.entries(ARTIFACTS_DIR).size <= 2 + FileUtils.rmdir(ARTIFACTS_DIR) + end +end + +# For @product tests +#################### + +def add_after_scenario_hook(&block) + @after_scenario_hooks ||= Array.new + @after_scenario_hooks << block +end + +def save_failure_artifact(type, path) + $failure_artifacts << [type, path] +end + +BeforeFeature('@product') do |feature| + if TAILS_ISO.nil? raise "No Tails ISO image specified, and none could be found in the " + "current directory" end - if File.exist?($tails_iso) + if File.exist?(TAILS_ISO) # Workaround: when libvirt takes ownership of the ISO image it may # become unreadable for the live user inside the guest in the # host-to-guest share used for some tests. - if !File.world_readable?($tails_iso) - if File.owned?($tails_iso) - File.chmod(0644, $tails_iso) + if !File.world_readable?(TAILS_ISO) + if File.owned?(TAILS_ISO) + File.chmod(0644, TAILS_ISO) else raise "warning: the Tails ISO image must be world readable or be " + "owned by the current user to be available inside the guest " + @@ -57,78 +147,120 @@ BeforeFeature('@product') do |feature| end end else - raise "The specified Tails ISO image '#{$tails_iso}' does not exist" + raise "The specified Tails ISO image '#{TAILS_ISO}' does not exist" + end + if !File.exist?(OLD_TAILS_ISO) + raise "The specified old Tails ISO image '#{OLD_TAILS_ISO}' does not exist" + end + if not($started_first_product_feature) + $virt = Libvirt::open("qemu:///system") + VM.remove_all_snapshots if !KEEP_SNAPSHOTS + $vmnet = VMNet.new($virt, VM_XML_PATH) + $vmstorage = VMStorage.new($virt, VM_XML_PATH) + $started_first_product_feature = true end - puts "Testing ISO image: #{File.basename($tails_iso)}" - base = File.basename(feature.file, ".feature").to_s - $background_snapshot = "#{$tmp_dir}/#{base}_background.state" end AfterFeature('@product') do - delete_snapshot($background_snapshot) if !$keep_snapshots - VM.storage.clear_volumes if VM.storage -end - -BeforeFeature('@product', '@old_iso') do - if $old_tails_iso.nil? - raise "No old Tails ISO image specified, and none could be found in the " + - "current directory" - end - if !File.exist?($old_tails_iso) - raise "The specified old Tails ISO image '#{$old_tails_iso}' does not exist" - end - if $tails_iso == $old_tails_iso - raise "The old Tails ISO is the same as the Tails ISO we're testing" + unless KEEP_SNAPSHOTS + checkpoints.each do |name, vals| + if vals[:temporary] and VM.snapshot_exists?(name) + VM.remove_snapshot(name) + end + end end - puts "Using old ISO image: #{File.basename($old_tails_iso)}" end -# BeforeScenario -Before('@product') do - @screen = Sikuli::Screen.new - if File.size?($background_snapshot) - @skip_steps_while_restoring_background = true - else - @skip_steps_while_restoring_background = false +# Cucumber Before hooks are executed in the order they are listed, and +# we want this hook to always run first, so it must always be the +# *first* Before hook matching @product listed in this file. +Before('@product') do |scenario| + $failure_artifacts = Array.new + if $config["CAPTURE"] + video_name = sanitize_filename("#{scenario.name}.mkv") + @video_path = "#{ARTIFACTS_DIR}/#{video_name}" + capture = IO.popen(['avconv', + '-f', 'x11grab', + '-s', '1024x768', + '-r', '15', + '-i', "#{$config['DISPLAY']}.0", + '-an', + '-c:v', 'libx264', + '-y', + @video_path, + :err => ['/dev/null', 'w'], + ]) + @video_capture_pid = capture.pid end - @theme = "gnome" + @screen = Sikuli::Screen.new + # English will be assumed if this is not overridden + @language = "" @os_loader = "MBR" + @sudo_password = "asdf" + @persistence_password = "asdf" end -# AfterScenario +# Cucumber After hooks are executed in the *reverse* order they are +# listed, and we want this hook to always run second last, so it must always +# be the *second* After hook matching @product listed in this file -- +# hooks added dynamically via add_after_scenario_hook() are supposed to +# truly be last. After('@product') do |scenario| - if (scenario.status != :passed) - time_of_fail = Time.now - $time_at_start + if @video_capture_pid + # We can be incredibly fast at detecting errors sometimes, so the + # screen barely "settles" when we end up here and kill the video + # capture. Let's wait a few seconds more to make it easier to see + # what the error was. + sleep 3 if scenario.failed? + Process.kill("INT", @video_capture_pid) + save_failure_artifact("Video", @video_path) + end + if scenario.failed? + time_of_fail = Time.now - TIME_AT_START secs = "%02d" % (time_of_fail % 60) mins = "%02d" % ((time_of_fail / 60) % 60) hrs = "%02d" % (time_of_fail / (60*60)) - STDERR.puts "Scenario failed at time #{hrs}:#{mins}:#{secs}" - base = File.basename(scenario.feature.file, ".feature").to_s - tmp = @screen.capture.getFilename - out = "#{$tmp_dir}/#{base}-#{DateTime.now}.png" - jenkins_live_screenshot = "#{$tmp_dir}/screenshot.png" - jenkins_live_thumb = "#{$tmp_dir}/screenshot-thumb.png" - FileUtils.mv(tmp, out) - FileUtils.cp(out, jenkins_live_screenshot) - STDERR.puts("Took screenshot \"#{out}\"") - if $pause_on_fail - STDERR.puts "" - STDERR.puts "Press ENTER to continue running the test suite" - STDIN.gets + elapsed = "#{hrs}:#{mins}:#{secs}" + info_log("Scenario failed at time #{elapsed}") + screen_capture = @screen.capture + save_failure_artifact("Screenshot", screen_capture.getFilename) + $failure_artifacts.sort! + $failure_artifacts.each do |type, file| + artifact_name = sanitize_filename("#{elapsed}_#{scenario.name}#{File.extname(file)}") + artifact_path = "#{ARTIFACTS_DIR}/#{artifact_name}" + assert(File.exist?(file)) + FileUtils.mv(file, artifact_path) + info_log + info_log_artifact_location(type, artifact_path) + end + pause("Scenario failed") if $config["PAUSE_ON_FAIL"] + else + if @video_path && File.exist?(@video_path) && not($config['CAPTURE_ALL']) + FileUtils.rm(@video_path) end end - unless system("convert #{jenkins_live_screenshot} -adaptive-resize 128x96 #{jenkins_live_thumb}") - raise StandardError.new("convert command exited with #{$?}") - end - if @sniffer - @sniffer.stop - @sniffer.clear +end + +Before('@product', '@check_tor_leaks') do |scenario| + @tor_leaks_sniffer = Sniffer.new(sanitize_filename(scenario.name), $vmnet) + @tor_leaks_sniffer.capture + add_after_scenario_hook do + @tor_leaks_sniffer.clear end - @vm.destroy if @vm end -After('@product', '~@keep_volumes') do - VM.storage.clear_volumes +After('@product', '@check_tor_leaks') do |scenario| + @tor_leaks_sniffer.stop + if scenario.passed? + if @bridge_hosts.nil? + expected_tor_nodes = get_all_tor_nodes + else + expected_tor_nodes = @bridge_hosts + end + leaks = FirewallLeakCheck.new(@tor_leaks_sniffer.pcap_file, + :accepted_hosts => expected_tor_nodes) + leaks.assert_no_leaks + end end # For @source tests @@ -146,17 +278,3 @@ After('@source') do Dir.chdir @orig_pwd FileUtils.remove_entry_secure @git_clone end - - -# Common -######## - -BeforeFeature('@product', '@source') do |feature| - raise "Feature #{feature.file} is tagged both @product and @source, " + - "which is an impossible combination" -end - -at_exit do - delete_all_snapshots if !$keep_snapshots - VM.storage.clear_pool if VM.storage -end -- cgit v1.2.3-70-g09d2