hooks.rb 9.98 KB
Newer Older
1
require 'fileutils'
2
require 'rb-inotify'
3
require 'time'
4
5
require 'tmpdir'

6
7
# Run once, before any feature
AfterConfiguration do |config|
8
9
10
11
12
13
14
  # 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 = [
15
16
    # Features not using snapshots but using large amounts of scratch
    # space for other reasons:
17
18
    'features/erase_memory.feature',
    'features/untrusted_partitions.feature',
19
20
21
22
23
24
    # Features using temporary snapshots:
    'features/apt.feature',
    'features/i2p.feature',
    'features/root_access_control.feature',
    'features/time_syncing.feature',
    'features/tor_bridges.feature',
25
26
27
28
    # 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',
29
30
    # This feature needs a copy of the ISO and creates a new disk.
    'features/usb_upgrade.feature',
31
32
  ]
  feature_files = config.feature_files
intrigeri's avatar
intrigeri committed
33
  # The &-intersection is specified to keep the element ordering of
34
35
36
37
38
39
40
41
  # 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

42
43
44
  # Used to keep track of when we start our first @product feature, when
  # we'll do some special things.
  $started_first_product_feature = false
45

46
47
48
49
50
51
52
53
54
55
56
57
  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?($config["TMPDIR"])
      raise "Temporary directory '#{$config["TMPDIR"]}' must be owned by the " +
            "current user"
    end
    FileUtils.chmod(0755, $config["TMPDIR"])
  else
    begin
anonym's avatar
anonym committed
58
      FileUtils.mkdir_p($config["TMPDIR"])
59
60
61
62
    rescue Errno::EACCES => e
      raise "Cannot create temporary directory: #{e.to_s}"
    end
  end
63

64
65
66
67
68
  # 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).
69
  FileUtils.rm(DEBUG_LOG_PSEUDO_FIFO) if File.exist?(DEBUG_LOG_PSEUDO_FIFO)
70
  FileUtils.touch(DEBUG_LOG_PSEUDO_FIFO)
71
72
73
  at_exit do
    FileUtils.rm(DEBUG_LOG_PSEUDO_FIFO) if File.exist?(DEBUG_LOG_PSEUDO_FIFO)
  end
74
75
76
77
78
79
80
81
82
83
  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
84
85
  # Fix Sikuli's debug_log():ing.
  bind_java_to_pseudo_fifo_logger
86
87
end

anonym's avatar
anonym committed
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# 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
104
105
106
107
108
109
110
111
112
  $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
anonym's avatar
anonym committed
113
114
115
116
117
118
119
  # 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

120
121
122
# For @product tests
####################

123
def add_after_scenario_hook(&block)
anonym's avatar
anonym committed
124
125
  @after_scenario_hooks ||= Array.new
  @after_scenario_hooks << block
126
127
end

128
129
130
131
def save_failure_artifact(type, path)
  $failure_artifacts << [type, path]
end

132
133
134
135
136
137
138
139
140
141
142
# Due to Tails' Tor enforcement, we only allow contacting hosts that
# are Tor (or I2P) nodes or located on the LAN. However, when we try
# to verify that only such hosts are contacted we have a problem --
# we run all Tor nodes (via Chutney) *and* LAN hosts (used on some
# tests) on the same host, the one running the test suite. Hence we
# need to always explicitly track which nodes are LAN or not.
#
# Warning: when a host is added via this function, it is only added
# for the current scenario. As such, if this is done before saving a
# snapshot, it will not remain after the snapshot is loaded.
def add_lan_host(ipaddr, port)
anonym's avatar
anonym committed
143
  @lan_hosts ||= []
144
145
146
  @lan_hosts << { address: ipaddr, port: port }
end

147
BeforeFeature('@product') do |feature|
148
  if TAILS_ISO.nil?
149
150
151
    raise "No Tails ISO image specified, and none could be found in the " +
          "current directory"
  end
152
  if File.exist?(TAILS_ISO)
153
154
155
156
    # 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.

157
158
159
    if !File.world_readable?(TAILS_ISO)
      if File.owned?(TAILS_ISO)
        File.chmod(0644, TAILS_ISO)
160
161
162
163
164
165
166
      else
        raise "warning: the Tails ISO image must be world readable or be " +
              "owned by the current user to be available inside the guest " +
              "VM via host-to-guest shares, which is required by some tests"
      end
    end
  else
167
    raise "The specified Tails ISO image '#{TAILS_ISO}' does not exist"
168
  end
169
170
171
  if !File.exist?(OLD_TAILS_ISO)
    raise "The specified old Tails ISO image '#{OLD_TAILS_ISO}' does not exist"
  end
172
173
  if not($started_first_product_feature)
    $virt = Libvirt::open("qemu:///system")
174
    VM.remove_all_snapshots if !KEEP_SNAPSHOTS
175
176
177
178
    $vmnet = VMNet.new($virt, VM_XML_PATH)
    $vmstorage = VMStorage.new($virt, VM_XML_PATH)
    $started_first_product_feature = true
  end
anonym's avatar
anonym committed
179
  ensure_chutney_is_running
180
181
182
end

AfterFeature('@product') do
183
184
185
186
187
188
189
  unless KEEP_SNAPSHOTS
    checkpoints.each do |name, vals|
      if vals[:temporary] and VM.snapshot_exists?(name)
        VM.remove_snapshot(name)
      end
    end
  end
190
191
end

anonym's avatar
anonym committed
192
193
# 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
anonym's avatar
anonym committed
194
# *first* Before hook matching @product listed in this file.
anonym's avatar
anonym committed
195
Before('@product') do |scenario|
196
  $failure_artifacts = Array.new
anonym's avatar
anonym committed
197
  if $config["CAPTURE"]
198
199
    video_name = sanitize_filename("#{scenario.name}.mkv")
    @video_path = "#{ARTIFACTS_DIR}/#{video_name}"
anonym's avatar
anonym committed
200
201
202
203
204
205
    capture = IO.popen(['avconv',
                        '-f', 'x11grab',
                        '-s', '1024x768',
                        '-r', '15',
                        '-i', "#{$config['DISPLAY']}.0",
                        '-an',
206
                        '-c:v', 'libx264',
anonym's avatar
anonym committed
207
208
209
210
211
212
                        '-y',
                        @video_path,
                        :err => ['/dev/null', 'w'],
                       ])
    @video_capture_pid = capture.pid
  end
213
  @screen = Sikuli::Screen.new
214
215
  # English will be assumed if this is not overridden
  @language = ""
216
  @os_loader = "MBR"
217
  @sudo_password = "asdf"
218
  @persistence_password = "asdf"
219
  # See comment for add_lan_host() above.
anonym's avatar
anonym committed
220
  @lan_hosts ||= []
221
222
end

anonym's avatar
anonym committed
223
# Cucumber After hooks are executed in the *reverse* order they are
anonym's avatar
anonym committed
224
225
226
227
# 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.
228
After('@product') do |scenario|
anonym's avatar
anonym committed
229
  if @video_capture_pid
230
231
232
233
234
    # 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?
anonym's avatar
anonym committed
235
    Process.kill("INT", @video_capture_pid)
236
    save_failure_artifact("Video", @video_path)
anonym's avatar
anonym committed
237
  end
238
  if scenario.failed?
239
    time_of_fail = Time.now - TIME_AT_START
anonym's avatar
anonym committed
240
241
242
    secs = "%02d" % (time_of_fail % 60)
    mins = "%02d" % ((time_of_fail / 60) % 60)
    hrs  = "%02d" % (time_of_fail / (60*60))
243
244
245
246
    elapsed = "#{hrs}:#{mins}:#{secs}"
    info_log("Scenario failed at time #{elapsed}")
    screen_capture = @screen.capture
    save_failure_artifact("Screenshot", screen_capture.getFilename)
247
248
249
250
251
    if scenario.exception.kind_of?(FirewallAssertionFailedError)
      Dir.glob("#{$config["TMPDIR"]}/*.pcap").each do |pcap_file|
        save_failure_artifact("Network capture", pcap_file)
      end
    end
252
253
254
255
256
257
    $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)
anonym's avatar
anonym committed
258
      info_log
259
260
      info_log_artifact_location(type, artifact_path)
    end
261
    pause("Scenario failed") if $config["INTERACTIVE_DEBUGGING"]
262
  else
263
    if @video_path && File.exist?(@video_path) && not($config['CAPTURE_ALL'])
264
265
      FileUtils.rm(@video_path)
    end
266
  end
267
end
268

269
Before('@product', '@check_tor_leaks') do |scenario|
270
  @tor_leaks_sniffer = Sniffer.new(sanitize_filename(scenario.name), $vmnet)
271
  @tor_leaks_sniffer.capture
272
273
274
  add_after_scenario_hook do
    @tor_leaks_sniffer.clear
  end
275
276
277
end

After('@product', '@check_tor_leaks') do |scenario|
278
279
  @tor_leaks_sniffer.stop
  if scenario.passed?
280
    allowed_nodes = @bridge_hosts ? @bridge_hosts : allowed_hosts_under_tor_enforcement
anonym's avatar
anonym committed
281
    assert_all_connections(@tor_leaks_sniffer.pcap_file) do |c|
282
      allowed_nodes.include?({ address: c.daddr, port: c.dport })
283
    end
284
  end
285
end
286
287
288
289
290
291

# For @source tests
###################

# BeforeScenario
Before('@source') do
292
293
294
295
296
  @orig_pwd = Dir.pwd
  @git_clone = Dir.mktmpdir 'tails-apt-tests'
  Dir.chdir @git_clone
end

297
298
# AfterScenario
After('@source') do
299
  Dir.chdir @orig_pwd
300
  FileUtils.remove_entry_secure @git_clone
301
end