hive-runner 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +120 -0
- data/bin/hive_setup +182 -0
- data/bin/hived +35 -0
- data/bin/start_hive +120 -0
- data/lib/hive/controller/shell.rb +23 -0
- data/lib/hive/controller.rb +32 -0
- data/lib/hive/device/shell.rb +14 -0
- data/lib/hive/device.rb +88 -0
- data/lib/hive/diagnostic.rb +54 -0
- data/lib/hive/diagnostic_runner.rb +32 -0
- data/lib/hive/execution_script.rb +121 -0
- data/lib/hive/file_system.rb +103 -0
- data/lib/hive/log.rb +60 -0
- data/lib/hive/port_allocator.rb +79 -0
- data/lib/hive/register.rb +120 -0
- data/lib/hive/results.rb +20 -0
- data/lib/hive/worker/shell.rb +16 -0
- data/lib/hive/worker.rb +400 -0
- data/lib/hive.rb +118 -0
- data/scripts/hive-script-helper.sh +17 -0
- metadata +262 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
module Hive
|
2
|
+
class DiagnosticRunner
|
3
|
+
attr_accessor :diagnostics, :options
|
4
|
+
|
5
|
+
def initialize(options, diagnostics_config, platform)
|
6
|
+
@options = options
|
7
|
+
@platform = platform
|
8
|
+
@diagnostics = self.initialize_diagnostics(diagnostics_config[@platform]) if diagnostics_config.has_key?(@platform)
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize_diagnostics(diagnostics_config)
|
12
|
+
if diagnostics_config
|
13
|
+
@diagnostics = diagnostics_config.collect do |component, config|
|
14
|
+
Hive.logger.info("Initializing #{component.capitalize} component for #{@platform.capitalize} diagnostic")
|
15
|
+
require "hive/diagnostic/#{@platform}/#{component}"
|
16
|
+
Object.const_get('Hive').const_get('Diagnostic').const_get(@platform.capitalize).const_get(component.capitalize).new(config, @options)
|
17
|
+
end
|
18
|
+
else
|
19
|
+
Hive.logger.info("No diagnostic specified for #{@platform}")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
results = @diagnostics.collect do |diagnostic|
|
25
|
+
diagnostic.run
|
26
|
+
end
|
27
|
+
|
28
|
+
failures = results.select { |r| r.failed? }
|
29
|
+
failures.count == 0
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
|
3
|
+
module Hive
|
4
|
+
class ExecutionScript
|
5
|
+
def initialize(config)
|
6
|
+
@path = config[:file_system].executed_script_path
|
7
|
+
@log_path = config[:file_system].logs_path
|
8
|
+
@log = config[:log]
|
9
|
+
@keep_running = config[:keep_running]
|
10
|
+
@log.debug "Creating execution script with path=#{@path}"
|
11
|
+
@env = {
|
12
|
+
'HIVE_SCHEDULER' => Hive.config.network.scheduler,
|
13
|
+
'HIVE_WORKING_DIRECTORY' => config[:file_system].testbed_path
|
14
|
+
}
|
15
|
+
@env_unset = [
|
16
|
+
'BUNDLE_GEMFILE',
|
17
|
+
'BUNDLE_BIN_PATH',
|
18
|
+
'GEM_PATH',
|
19
|
+
'RUBYOPT',
|
20
|
+
'rvm_'
|
21
|
+
]
|
22
|
+
# Environment variables that should not be made visible in the execution
|
23
|
+
# script uploaded with the results
|
24
|
+
@env_secure = {
|
25
|
+
'HIVE_CERT' => Hive.config.network.cert
|
26
|
+
}
|
27
|
+
@script_lines = []
|
28
|
+
end
|
29
|
+
|
30
|
+
def prepend_bash_cmd(shell_command)
|
31
|
+
@log.debug "bash.rb - Prepending bash command to #{@path} script: " + shell_command
|
32
|
+
@script_lines = ([] << shell_command << @script_lines).flatten
|
33
|
+
end
|
34
|
+
|
35
|
+
def append_bash_cmd(shell_command)
|
36
|
+
@log.debug "bash.rb - Appending bash command to #{@path} script: " + shell_command
|
37
|
+
@script_lines << shell_command
|
38
|
+
end
|
39
|
+
|
40
|
+
def set_env(var, value)
|
41
|
+
@env[var] = value
|
42
|
+
|
43
|
+
# TODO What if the element appears multiple times?
|
44
|
+
if (i = @env_unset.index(var))
|
45
|
+
@env_unset.delete(i)
|
46
|
+
end
|
47
|
+
|
48
|
+
## In Ruby 2, replace the above 'if' block with ...
|
49
|
+
#@env_unset.remove(var)
|
50
|
+
end
|
51
|
+
|
52
|
+
def unset_env(var)
|
53
|
+
@env.delete(var)
|
54
|
+
@env_unset << var
|
55
|
+
end
|
56
|
+
|
57
|
+
def helper_path
|
58
|
+
scripts_dir = File.expand_path(File.dirname(__FILE__) + "../../../scripts/")
|
59
|
+
File.join(scripts_dir, 'hive-script-helper.sh')
|
60
|
+
end
|
61
|
+
|
62
|
+
def run
|
63
|
+
@log.info 'bash.rb - Writing script out to file'
|
64
|
+
File.open(@path, 'w') do |f|
|
65
|
+
f.write("#!/bin/bash --login\n")
|
66
|
+
f.write(". #{helper_path}\n")
|
67
|
+
f.write("# Set environment\n")
|
68
|
+
@env.each do |key, value|
|
69
|
+
# An escaped ' in a single quoted string in bash looks like '"'"'
|
70
|
+
if value.kind_of?(Array)
|
71
|
+
f.write("export #{key}=(" + value.collect { |v| "'#{v.to_s.gsub("'", '\'"\'"\'')}'" }.join(' ') + ")\n" )
|
72
|
+
else
|
73
|
+
f.write("export #{key}='#{value.to_s.gsub("'", '\'"\'"\'')}'\n")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
@env_unset.each do |var|
|
77
|
+
f.write("unset #{var}\n")
|
78
|
+
end
|
79
|
+
f.write("cd $HIVE_WORKING_DIRECTORY")
|
80
|
+
f.write("\n# Test execution\n")
|
81
|
+
f.write(@script_lines.join("\n"))
|
82
|
+
end
|
83
|
+
File.chmod(0700, @path)
|
84
|
+
|
85
|
+
pid = Process.spawn @env_secure, "#{@path}", pgroup: true, in: '/dev/null', out: "#{@log_path}/stdout.log", err: "#{@log_path}/stderr.log"
|
86
|
+
@pgid = Process.getpgid(pid)
|
87
|
+
|
88
|
+
exit_value = nil
|
89
|
+
running = true
|
90
|
+
while running
|
91
|
+
begin
|
92
|
+
Timeout.timeout(30) do
|
93
|
+
Process.wait pid
|
94
|
+
exit_value = $?.exitstatus
|
95
|
+
running = false
|
96
|
+
end
|
97
|
+
rescue Timeout::Error
|
98
|
+
Process.kill(-9, @pgid) if ! ( @keep_running.nil? || @keep_running.call )
|
99
|
+
# Do something. Eg, upload log files.
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Kill off anything that is still running
|
104
|
+
terminate
|
105
|
+
|
106
|
+
# Return exit value of the script
|
107
|
+
exit_value
|
108
|
+
end
|
109
|
+
|
110
|
+
def terminate
|
111
|
+
if @pgid
|
112
|
+
begin
|
113
|
+
Process.kill(-9, @pgid)
|
114
|
+
rescue => e
|
115
|
+
@log.warn e
|
116
|
+
end
|
117
|
+
@pgid = nil
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
|
3
|
+
module Hive
|
4
|
+
class FileSystem
|
5
|
+
def initialize(job_id, home_directory, log)
|
6
|
+
@job_id = job_id
|
7
|
+
@home_directory = home_directory
|
8
|
+
@log = log
|
9
|
+
@log.debug "Creating job paths with id=#{@job_id} and home=#{@home_directory}"
|
10
|
+
make_directory(home_path)
|
11
|
+
make_directory(results_path)
|
12
|
+
make_directory(logs_path)
|
13
|
+
make_directory(testbed_path)
|
14
|
+
end
|
15
|
+
|
16
|
+
def home_path
|
17
|
+
@home_path ||= "#{@home_directory}/#{@job_id.to_s}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def results_path
|
21
|
+
@results_path ||= "#{home_path}/results"
|
22
|
+
end
|
23
|
+
|
24
|
+
def logs_path
|
25
|
+
@logs_path ||= "#{home_path}/logs"
|
26
|
+
end
|
27
|
+
|
28
|
+
def testbed_path
|
29
|
+
@testbed_path ||= "#{home_path}/test_code"
|
30
|
+
end
|
31
|
+
|
32
|
+
def executed_script_path
|
33
|
+
@bash_script_path ||= "#{testbed_path}/executed_script.sh"
|
34
|
+
end
|
35
|
+
|
36
|
+
# Copy useful stuff into the results directory
|
37
|
+
def finalise_results_directory
|
38
|
+
copy_file(executed_script_path, "#{results_path}/executed_script.sh")
|
39
|
+
end
|
40
|
+
|
41
|
+
def fetch_build(build_url, destination_path)
|
42
|
+
if !fetch_build_with_curl(build_url, destination_path)
|
43
|
+
@log.info( "Initial build fetch failed -- trying again shortly")
|
44
|
+
sleep 5
|
45
|
+
if !fetch_build_with_curl(build_url, destination_path)
|
46
|
+
raise "Build could not be downloaded"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def fetch_build_with_curl(build_url, destination_path)
|
52
|
+
cert_path = Hive.config.network['cert']
|
53
|
+
cabundle_path = Hive.config.network['cafile']
|
54
|
+
base_url = Hive.config.network['scheduler']
|
55
|
+
apk_url = base_url + '/' + build_url
|
56
|
+
curl_line = "curl -L -m 60 #{apk_url} --cert #{cert_path} --cacert #{cabundle_path} --retry 3 -o #{destination_path}"
|
57
|
+
|
58
|
+
@log.info("Fetching build from hive-scheduler: #{curl_line}")
|
59
|
+
@log.debug("CURL line: #{curl_line}")
|
60
|
+
response = `#{curl_line}`
|
61
|
+
if $? != 0
|
62
|
+
@log.info("Curl error #{$?}: #{response.to_s}")
|
63
|
+
false
|
64
|
+
Hive::Messages
|
65
|
+
else
|
66
|
+
@log.info("Curl seems happy, checking integrity of downloaded file")
|
67
|
+
check_build_integrity( destination_path )
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def check_build_integrity( destination_path )
|
72
|
+
output = `file #{destination_path}`
|
73
|
+
if output =~ /zip/
|
74
|
+
result = `zip -T #{destination_path}`
|
75
|
+
@log.info(result)
|
76
|
+
$? == 0
|
77
|
+
else
|
78
|
+
true
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def copy_file(src, dest)
|
85
|
+
begin
|
86
|
+
FileUtils.cp(src, dest)
|
87
|
+
@log.debug("Copied file #{src} -> #{dest}")
|
88
|
+
rescue => e
|
89
|
+
@log.error(e.message)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def make_directory(directory)
|
94
|
+
begin
|
95
|
+
FileUtils.rm_r(directory) if File.directory?(directory)
|
96
|
+
FileUtils.mkdir_p(directory)
|
97
|
+
@log.debug("Created directory: #{directory}")
|
98
|
+
rescue => e
|
99
|
+
@log.fatal(e.message)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/lib/hive/log.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'mono_logger'
|
2
|
+
|
3
|
+
module Hive
|
4
|
+
# Hive logging
|
5
|
+
# Allow logging to be written to multiple locations.
|
6
|
+
class Log
|
7
|
+
# Create the logger:
|
8
|
+
#
|
9
|
+
# # No log files will be written
|
10
|
+
# log = Hive::Log.new()
|
11
|
+
# # Write log files to standard out and a log file
|
12
|
+
# log = Hive::Log.new( [
|
13
|
+
# {
|
14
|
+
# stream: 'Filename.log',
|
15
|
+
# level: 'DEBUG'
|
16
|
+
# },
|
17
|
+
# {
|
18
|
+
# stream: STDOUT,
|
19
|
+
# level: 'INFO'
|
20
|
+
# },
|
21
|
+
# ] )
|
22
|
+
def initialize(args = [])
|
23
|
+
@loggers = {}
|
24
|
+
args.each do |l|
|
25
|
+
add_logger(l[:stream], l[:level])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Add a new log location:
|
30
|
+
#
|
31
|
+
# # INFO level log to 'Filename.log'
|
32
|
+
# log.add_logger( 'Filename.log', 'INFO' )
|
33
|
+
# # DEBUG level log to standard output
|
34
|
+
# log.add_logger( STDOUT, 'DEBUG' )
|
35
|
+
def add_logger(stream, level)
|
36
|
+
log = MonoLogger.new(stream)
|
37
|
+
log.formatter = proc do |severity, datetime, _progname, msg|
|
38
|
+
"#{severity[0, 1]} #{datetime.strftime('%Y-%m-%d %H:%M:%S')}: #{msg}\n"
|
39
|
+
end
|
40
|
+
log.level = MonoLogger.const_get(level)
|
41
|
+
@loggers[stream] = log
|
42
|
+
end
|
43
|
+
|
44
|
+
# Stop a log stream:
|
45
|
+
#
|
46
|
+
# # Stop the log to standard output
|
47
|
+
# log.stop_logger( STDOUT )
|
48
|
+
# # Stop the log to 'Filename.log'
|
49
|
+
# log.stop_logger( 'Filename.log' )
|
50
|
+
def stop_logger(stream)
|
51
|
+
@loggers.delete(stream)
|
52
|
+
end
|
53
|
+
|
54
|
+
MonoLogger::Severity.constants.each do |level|
|
55
|
+
define_method(level.downcase) do |*args|
|
56
|
+
@loggers.each { |_s, l| l.send(level.downcase, *args) }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Hive
|
2
|
+
class PortAllocator
|
3
|
+
class NoPortsAvailable < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
# Create a port allocator
|
7
|
+
#
|
8
|
+
# For ports in the range 4000-5000
|
9
|
+
# Hive::PortAllocator.new(minimum: 4000, maximum: 5000)
|
10
|
+
#
|
11
|
+
# For ports 6000, 6050 and 7433
|
12
|
+
# Hive::PortAllocator.new(ports: [6000, 6050, 7433])
|
13
|
+
#
|
14
|
+
def initialize(config)
|
15
|
+
@allocated_ports = []
|
16
|
+
if config.has_key?(:minimum) and config.has_key?(:maximum) and config[:minimum] > 0 and config[:minimum] <= config[:maximum]
|
17
|
+
@free_ports = Array(config[:minimum]..config[:maximum])
|
18
|
+
elsif config.has_key?(:ports) and config[:ports].is_a? Array
|
19
|
+
config[:ports].each do |p|
|
20
|
+
raise ArgumentError if ! p.is_a? Integer or p <= 0
|
21
|
+
end
|
22
|
+
@free_ports = config[:ports]
|
23
|
+
else
|
24
|
+
raise ArgumentError
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Allocate a single port in the range
|
29
|
+
def allocate_port
|
30
|
+
if p = @free_ports.pop
|
31
|
+
@allocated_ports << p
|
32
|
+
p
|
33
|
+
else
|
34
|
+
raise NoPortsAvailable
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Relase a single port in the range
|
39
|
+
def release_port(p)
|
40
|
+
@free_ports << p if @allocated_ports.delete(p)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Create a new Hive::PortAllocator instance with a number of ports from
|
44
|
+
# the range
|
45
|
+
def allocate_port_range(n)
|
46
|
+
if n <= @free_ports.length
|
47
|
+
ps = @free_ports.take(n)
|
48
|
+
@free_ports = @free_ports.drop(n)
|
49
|
+
@allocated_ports.concat(ps)
|
50
|
+
PortAllocator.new(ports: ps)
|
51
|
+
else
|
52
|
+
raise NoPortsAvailable
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Release ports that were previously allocated to another
|
57
|
+
# Hive::PortAllocator
|
58
|
+
#
|
59
|
+
# Note, this will fail silently if 'range' contains ports that are not
|
60
|
+
# allocated in the current instance
|
61
|
+
def release_port_range(range)
|
62
|
+
if range.ports - @allocated_ports == []
|
63
|
+
@free_ports.concat(range.ports)
|
64
|
+
@allocated_ports = @allocated_ports - range.ports
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Release all ports
|
69
|
+
def release_all_ports
|
70
|
+
@free_ports.concat(@allocated_ports)
|
71
|
+
@allocated_ports = []
|
72
|
+
end
|
73
|
+
|
74
|
+
# Full list of all ports, either free or allocated
|
75
|
+
def ports
|
76
|
+
[@free_ports, @allocated_ports].flatten
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'hive'
|
2
|
+
require 'hive/port_allocator'
|
3
|
+
|
4
|
+
module Hive
|
5
|
+
# Central register of devices and workers in the hive
|
6
|
+
class Register
|
7
|
+
attr_reader :controllers
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@controllers = []
|
11
|
+
@devices = {}
|
12
|
+
@max_devices = 5 # TODO Add to configuration file
|
13
|
+
if Hive.config.ports?
|
14
|
+
@port_allocator = Hive::PortAllocator.new(minimum: Hive.config.ports.minimum, maximum: Hive.config.ports.maximum)
|
15
|
+
else
|
16
|
+
@port_allocator = Hive::PortAllocator.new(ports: [])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def devices
|
21
|
+
list = []
|
22
|
+
@devices.each do |controller, device_list|
|
23
|
+
list.concat(device_list)
|
24
|
+
end
|
25
|
+
list
|
26
|
+
end
|
27
|
+
|
28
|
+
def worker_pids
|
29
|
+
self.devices.collect{ |d| d.worker_pid }.compact
|
30
|
+
end
|
31
|
+
|
32
|
+
def instantiate_controllers(controller_details = Hive.config.controllers)
|
33
|
+
controller_details.each do |type, opts|
|
34
|
+
Hive.logger.info("Adding controller for '#{type}'")
|
35
|
+
require "hive/controller/#{type}"
|
36
|
+
controller = Object.const_get('Hive').const_get('Controller').const_get(type.capitalize).new(opts.to_hash)
|
37
|
+
@controllers << controller
|
38
|
+
end
|
39
|
+
check_controllers
|
40
|
+
@controllers
|
41
|
+
end
|
42
|
+
|
43
|
+
def run
|
44
|
+
loop do
|
45
|
+
Hive.poll
|
46
|
+
housekeeping
|
47
|
+
check_controllers
|
48
|
+
sleep Hive.config.timings.controller_loop_interval
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def check_controllers
|
53
|
+
Hive.logger.debug("Devices before update: #{@devices.inspect}")
|
54
|
+
new_device_list = {}
|
55
|
+
@controllers.each do |c|
|
56
|
+
begin
|
57
|
+
new_device_list[c.class] = []
|
58
|
+
@devices[c.class] = [] if ! @devices.has_key?(c.class)
|
59
|
+
Hive.logger.info("Checking controller #{c.class}")
|
60
|
+
c.detect.each do |device|
|
61
|
+
Hive.logger.debug("Found #{device.inspect}")
|
62
|
+
i = @devices[c.class].find_index(device)
|
63
|
+
if i
|
64
|
+
@devices[c.class][i].status = device.status
|
65
|
+
new_device_list[c.class] << @devices[c.class][i]
|
66
|
+
else
|
67
|
+
device.port_allocator = @port_allocator.allocate_port_range(c.port_range_size)
|
68
|
+
new_device_list[c.class] << device
|
69
|
+
end
|
70
|
+
end
|
71
|
+
Hive.logger.debug("new_device_list: #{new_device_list.inspect}")
|
72
|
+
|
73
|
+
# Remove any devices that have not been rediscovered
|
74
|
+
(@devices[c.class] - new_device_list[c.class]).each do |d|
|
75
|
+
@port_allocator.release_port_range(d.port_allocator)
|
76
|
+
d.stop
|
77
|
+
@devices[c.class].delete(d)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Add any new devices
|
81
|
+
(new_device_list[c.class] - @devices[c.class]).each do |d|
|
82
|
+
@devices[c.class] << d
|
83
|
+
end
|
84
|
+
# Check that all known devices have running workers
|
85
|
+
@devices[c.class].each do |d|
|
86
|
+
if d.claimed?
|
87
|
+
d.stop if d.running?
|
88
|
+
else
|
89
|
+
d.start if ! d.running?
|
90
|
+
end
|
91
|
+
end
|
92
|
+
rescue Hive::Controller::DeviceDetectionFailed
|
93
|
+
Hive.logger.warn("Failed to detect devices for #{c.class}")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
Hive.logger.debug("Devices after update: #{@devices.inspect}")
|
97
|
+
end
|
98
|
+
|
99
|
+
def housekeeping
|
100
|
+
clear_workspaces
|
101
|
+
end
|
102
|
+
|
103
|
+
def clear_workspaces
|
104
|
+
candidates = Dir.glob("#{Hive.config.logging.home}/*")
|
105
|
+
.select{ |f|
|
106
|
+
File.directory?(f) \
|
107
|
+
&& File.exists?("#{f}/job_info") \
|
108
|
+
&& File.read("#{f}/job_info").chomp.to_s =~ /completed/
|
109
|
+
}.sort_by{ |f|
|
110
|
+
File.mtime(f)
|
111
|
+
}.reverse
|
112
|
+
if candidates && candidates.length > Hive.config.logging.homes_to_keep
|
113
|
+
candidates[Hive.config.logging.homes_to_keep..-1].each do |dir|
|
114
|
+
Hive.logger.info("Found (and deleting) #{dir}")
|
115
|
+
FileUtils.rm_rf(dir)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
data/lib/hive/results.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
|
2
|
+
module Hive
|
3
|
+
class Results
|
4
|
+
attr_reader :timestamp
|
5
|
+
def initialize( state, message, data = {})
|
6
|
+
@state = state
|
7
|
+
@message = message
|
8
|
+
@data = data
|
9
|
+
@timestamp = Time.now
|
10
|
+
end
|
11
|
+
|
12
|
+
def failed?
|
13
|
+
@state == 'fail'
|
14
|
+
end
|
15
|
+
|
16
|
+
def passed?
|
17
|
+
@state == 'pass'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|