spectre-core 1.8.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/exe/spectre +487 -0
- data/lib/spectre.rb +434 -0
- data/lib/spectre/assertion.rb +277 -0
- data/lib/spectre/bag.rb +19 -0
- data/lib/spectre/curl.rb +368 -0
- data/lib/spectre/database/postgres.rb +78 -0
- data/lib/spectre/diagnostic.rb +29 -0
- data/lib/spectre/environment.rb +26 -0
- data/lib/spectre/ftp.rb +195 -0
- data/lib/spectre/helpers.rb +64 -0
- data/lib/spectre/http.rb +343 -0
- data/lib/spectre/http/basic_auth.rb +22 -0
- data/lib/spectre/http/keystone.rb +98 -0
- data/lib/spectre/logger.rb +144 -0
- data/lib/spectre/logger/console.rb +142 -0
- data/lib/spectre/logger/file.rb +96 -0
- data/lib/spectre/mixin.rb +41 -0
- data/lib/spectre/mysql.rb +97 -0
- data/lib/spectre/reporter/console.rb +103 -0
- data/lib/spectre/reporter/junit.rb +98 -0
- data/lib/spectre/resources.rb +46 -0
- data/lib/spectre/ssh.rb +149 -0
- metadata +140 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Spectre
|
4
|
+
module Mixin
|
5
|
+
class << self
|
6
|
+
@@mixins = {}
|
7
|
+
|
8
|
+
def mixin desc, &block
|
9
|
+
@@mixins[desc] = block
|
10
|
+
end
|
11
|
+
|
12
|
+
def run desc, with: []
|
13
|
+
raise "no mixin with desc '#{desc}' defined" unless @@mixins.has_key? desc
|
14
|
+
Logger.log_debug "running mixin '#{desc}'"
|
15
|
+
|
16
|
+
if with.is_a? Array
|
17
|
+
@@mixins[desc].call *with
|
18
|
+
else
|
19
|
+
@@mixins[desc].call with
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
alias_method :also, :run
|
24
|
+
alias_method :step, :run
|
25
|
+
end
|
26
|
+
|
27
|
+
Spectre.register do |config|
|
28
|
+
if not config.has_key? 'mixin_patterns'
|
29
|
+
return
|
30
|
+
end
|
31
|
+
|
32
|
+
config['mixin_patterns'].each do |pattern|
|
33
|
+
Dir.glob(pattern).each do|f|
|
34
|
+
require_relative File.join(Dir.pwd, f)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
Spectre.delegate :mixin, :run, :also, :step, to: Mixin
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'mysql2'
|
2
|
+
|
3
|
+
module Spectre
|
4
|
+
module MySQL
|
5
|
+
|
6
|
+
class MySqlQuery < DslClass
|
7
|
+
def initialize query
|
8
|
+
@__query = query
|
9
|
+
end
|
10
|
+
|
11
|
+
def host hostname
|
12
|
+
@__query['host'] = hostname
|
13
|
+
end
|
14
|
+
|
15
|
+
def username user
|
16
|
+
@__query['username'] = user
|
17
|
+
end
|
18
|
+
|
19
|
+
def password pass
|
20
|
+
@__query['password'] = pass
|
21
|
+
end
|
22
|
+
|
23
|
+
def database name
|
24
|
+
@__query['database'] = name
|
25
|
+
end
|
26
|
+
|
27
|
+
def query statement
|
28
|
+
@__query['query'] = [] if not @__query.has_key? 'query'
|
29
|
+
@__query['query'].append(statement)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
@@mysql_cfg = {}
|
35
|
+
@@result = nil
|
36
|
+
@@last_conn = nil
|
37
|
+
|
38
|
+
def mysql name = nil, &block
|
39
|
+
query = {}
|
40
|
+
|
41
|
+
if name != nil and @@mysql_cfg.has_key? name
|
42
|
+
query.merge! @@mysql_cfg[name]
|
43
|
+
raise "No `host' set for MySQL client '#{name}'. Check your MySQL config in your environment." if !query['host']
|
44
|
+
elsif name != nil
|
45
|
+
query['host'] = name
|
46
|
+
elsif @@last_conn == nil
|
47
|
+
raise 'No name given and there was no previous MySQL connection to use'
|
48
|
+
end
|
49
|
+
|
50
|
+
MySqlQuery.new(query).instance_eval(&block) if block_given?
|
51
|
+
|
52
|
+
if name != nil
|
53
|
+
@@last_conn = {
|
54
|
+
host: query['host'],
|
55
|
+
username: query['username'],
|
56
|
+
password: query['password'],
|
57
|
+
database: query['database']
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
@@logger.info "Connecting to database #{query['username']}@#{query['host']}:#{query['database']}"
|
62
|
+
|
63
|
+
client = ::Mysql2::Client.new(**@@last_conn)
|
64
|
+
|
65
|
+
res = []
|
66
|
+
|
67
|
+
query['query'].each do |statement|
|
68
|
+
@@logger.info 'Executing statement "' + statement + '"'
|
69
|
+
res = client.query(statement, cast_booleans: true)
|
70
|
+
end if query['query']
|
71
|
+
|
72
|
+
@@result = res.map { |row| OpenStruct.new row } if res
|
73
|
+
|
74
|
+
client.close
|
75
|
+
end
|
76
|
+
|
77
|
+
def result
|
78
|
+
raise 'No MySQL query has been executed yet' unless @@result
|
79
|
+
@@result
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
Spectre.register do |config|
|
84
|
+
@@logger = ::Logger.new config['log_file'], progname: 'spectre/mysql'
|
85
|
+
|
86
|
+
if config.has_key? 'mysql'
|
87
|
+
@@mysql_cfg = {}
|
88
|
+
|
89
|
+
config['mysql'].each do |name, cfg|
|
90
|
+
@@mysql_cfg[name] = cfg
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
Spectre.delegate :mysql, :result, to: self
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Spectre::Reporter
|
2
|
+
class Console
|
3
|
+
def initialize config
|
4
|
+
@config = config
|
5
|
+
end
|
6
|
+
|
7
|
+
def report run_infos
|
8
|
+
|
9
|
+
report_str = ''
|
10
|
+
|
11
|
+
errors = 0
|
12
|
+
failures = 0
|
13
|
+
skipped = run_infos.select { |x| x.skipped? }.count
|
14
|
+
|
15
|
+
run_infos
|
16
|
+
.select { |x| x.error != nil or x.failure != nil }
|
17
|
+
.each_with_index do |run_info, index|
|
18
|
+
|
19
|
+
spec = run_info.spec
|
20
|
+
|
21
|
+
report_str += "\n#{index+1}) #{format_title(run_info)}\n"
|
22
|
+
|
23
|
+
if run_info.failure
|
24
|
+
report_str += " Expected #{run_info.failure.expectation}"
|
25
|
+
report_str += " with #{run_info.data}" if run_info.data
|
26
|
+
report_str += " during #{spec.context.__desc}" if spec.context.__desc
|
27
|
+
|
28
|
+
report_str += " but it failed"
|
29
|
+
|
30
|
+
if run_info.failure.cause
|
31
|
+
report_str += "\n with an unexpected error:\n"
|
32
|
+
report_str += format_exception(run_info.failure.cause)
|
33
|
+
|
34
|
+
elsif run_info.failure.message and not run_info.failure.message.empty?
|
35
|
+
report_str += " with:\n #{run_info.failure.message}"
|
36
|
+
|
37
|
+
else
|
38
|
+
report_str += '.'
|
39
|
+
end
|
40
|
+
|
41
|
+
report_str += "\n"
|
42
|
+
failures += 1
|
43
|
+
|
44
|
+
else
|
45
|
+
report_str += " but an unexpected error occured during run\n"
|
46
|
+
report_str += format_exception(run_info.error)
|
47
|
+
errors += 1
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
if failures + errors > 0
|
52
|
+
summary = ''
|
53
|
+
summary += "#{run_infos.length - failures - errors - skipped} succeeded "
|
54
|
+
summary += "#{failures} failures " if failures > 0
|
55
|
+
summary += "#{errors} errors " if errors > 0
|
56
|
+
summary += "#{skipped} skipped " if skipped > 0
|
57
|
+
summary += "#{run_infos.length} total"
|
58
|
+
print "\n#{summary}\n".red
|
59
|
+
else
|
60
|
+
summary = ''
|
61
|
+
summary = "\nRun finished successfully"
|
62
|
+
summary += " (#{skipped} skipped)" if skipped > 0
|
63
|
+
print "#{summary}\n".green
|
64
|
+
end
|
65
|
+
|
66
|
+
puts report_str.red
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def format_title run_info
|
72
|
+
title = run_info.spec.subject.desc
|
73
|
+
title += ' ' + run_info.spec.desc
|
74
|
+
title += " (#{'%.3f' % run_info.duration}s)"
|
75
|
+
title += " [#{run_info.spec.name}]"
|
76
|
+
title
|
77
|
+
end
|
78
|
+
|
79
|
+
def format_exception error
|
80
|
+
non_spectre_files = error.backtrace.select { |x| !x.include? 'lib/spectre' }
|
81
|
+
|
82
|
+
if non_spectre_files.count > 0
|
83
|
+
causing_file = non_spectre_files.first
|
84
|
+
else
|
85
|
+
causing_file = error.backtrace[0]
|
86
|
+
end
|
87
|
+
|
88
|
+
matches = causing_file.match(/(.*\.rb):(\d+)/)
|
89
|
+
|
90
|
+
return '' unless matches
|
91
|
+
|
92
|
+
file, line = matches.captures
|
93
|
+
file.slice!(Dir.pwd + '/')
|
94
|
+
|
95
|
+
str = ''
|
96
|
+
str += " file.....: #{file}\n"
|
97
|
+
str += " line.....: #{line}\n"
|
98
|
+
str += " type.....: #{error.class}\n"
|
99
|
+
str += " message..: #{error.message}\n"
|
100
|
+
str
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# https://llg.cubic.org/docs/junit/
|
2
|
+
# Azure mappings: https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/test/publish-test-results?view=azure-devops&tabs=junit%2Cyaml
|
3
|
+
|
4
|
+
module Spectre::Reporter
|
5
|
+
class JUnit
|
6
|
+
def initialize config
|
7
|
+
@config = config
|
8
|
+
end
|
9
|
+
|
10
|
+
def report run_infos
|
11
|
+
now = Time.now.getutc
|
12
|
+
timestamp = now.strftime('%s')
|
13
|
+
datetime = now.strftime('%FT%T%:z')
|
14
|
+
|
15
|
+
xml_str = '<?xml version="1.0" encoding="UTF-8" ?>'
|
16
|
+
xml_str += '<testsuites>'
|
17
|
+
|
18
|
+
suite_id = 0
|
19
|
+
|
20
|
+
run_infos.group_by { |x| x.spec.subject }.each do |subject, run_infos|
|
21
|
+
failures = run_infos.select { |x| x.failure != nil }
|
22
|
+
errors = run_infos.select { |x| x.error != nil }
|
23
|
+
skipped = run_infos.select { |x| x.skipped? }
|
24
|
+
|
25
|
+
xml_str += '<testsuite package="' + subject.desc + '" id="' + suite_id.to_s + '" name="' + subject.desc + '" timestamp="' + datetime + '" tests="' + run_infos.count.to_s + '" failures="' + failures.count.to_s + '" errors="' + errors.count.to_s + '" skipped="' + skipped.count.to_s + '">'
|
26
|
+
suite_id += 1
|
27
|
+
|
28
|
+
run_infos.each do |run_info|
|
29
|
+
xml_str += '<testcase classname="' + run_info.spec.file.to_s + '" name="' + run_info.spec.desc + '" timestamp="' + run_info.started.to_s + '" time="' + ('%.3f' % run_info.duration) + '">'
|
30
|
+
|
31
|
+
if run_info.failure and !run_info.failure.cause
|
32
|
+
failure_message = "Expected #{run_info.failure.expectation}"
|
33
|
+
failure_message += " with #{run_info.data}" if run_info.data
|
34
|
+
|
35
|
+
if run_info.failure.message
|
36
|
+
failure_message += " but it failed with #{run_info.failure.message}"
|
37
|
+
else
|
38
|
+
failure_message += " but it failed"
|
39
|
+
end
|
40
|
+
|
41
|
+
xml_str += '<failure message="' + failure_message.gsub('"', '`') + '"></failure>'
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
if run_info.error or (run_info.failure and run_info.failure.cause)
|
46
|
+
error = run_info.error || run_info.failure.cause
|
47
|
+
|
48
|
+
type = error.class.name
|
49
|
+
failure_message = error.message
|
50
|
+
text = error.backtrace.join "\n"
|
51
|
+
|
52
|
+
xml_str += '<error message="' + failure_message.gsub('"', '`') + '" type="' + type + '">'
|
53
|
+
xml_str += '<![CDATA[' + text + ']]>'
|
54
|
+
xml_str += '</error>'
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
if run_info.log.count > 0 or run_info.properties.count > 0 or run_info.data
|
59
|
+
xml_str += '<system-out>'
|
60
|
+
|
61
|
+
if run_info.properties.count > 0
|
62
|
+
run_info.properties.each do |key, val|
|
63
|
+
xml_str += "#{key}: #{val}\n"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
if run_info.data
|
68
|
+
data_str = run_info.data
|
69
|
+
data_str = run_info.data.inspect unless run_info.data.is_a? String or run_info.data.is_a? Integer
|
70
|
+
xml_str += "data: #{data_str}\n"
|
71
|
+
end
|
72
|
+
|
73
|
+
if run_info.log.count > 0
|
74
|
+
messages = run_info.log.map { |x| "[#{x[0].strftime('%F %T')}] #{x[1]}" }
|
75
|
+
xml_str += messages.join("\n")
|
76
|
+
end
|
77
|
+
|
78
|
+
xml_str += '</system-out>'
|
79
|
+
end
|
80
|
+
|
81
|
+
xml_str += '</testcase>'
|
82
|
+
end
|
83
|
+
|
84
|
+
xml_str += '</testsuite>'
|
85
|
+
end
|
86
|
+
|
87
|
+
xml_str += '</testsuites>'
|
88
|
+
|
89
|
+
Dir.mkdir @config['out_path'] if not Dir.exist? @config['out_path']
|
90
|
+
|
91
|
+
file_path = File.join(@config['out_path'], "spectre-junit_#{timestamp}.xml")
|
92
|
+
|
93
|
+
File.open(file_path, 'w') do |file|
|
94
|
+
file.write(xml_str)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Spectre
|
4
|
+
module Resources
|
5
|
+
class ResourceCollection
|
6
|
+
def initialize
|
7
|
+
@items = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def add name, path
|
11
|
+
@items[name] = path
|
12
|
+
end
|
13
|
+
|
14
|
+
def [] name
|
15
|
+
raise "Resource with name '#{name}' does not exist" if not @items.has_key? name
|
16
|
+
@items[name]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class << self
|
21
|
+
@@resources = ResourceCollection.new
|
22
|
+
|
23
|
+
def resources
|
24
|
+
@@resources
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
Spectre.register do |config|
|
29
|
+
return if !config.has_key? 'resource_paths'
|
30
|
+
|
31
|
+
config['resource_paths'].each do |resource_path|
|
32
|
+
resource_files = Dir.glob File.join(resource_path, '**/*')
|
33
|
+
|
34
|
+
resource_files.each do |file|
|
35
|
+
file.slice! resource_path
|
36
|
+
file = file[1..-1]
|
37
|
+
@@resources.add file, File.expand_path(File.join resource_path, file)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
@@resources.freeze
|
42
|
+
end
|
43
|
+
|
44
|
+
Spectre.delegate :resources, to: Resources
|
45
|
+
end
|
46
|
+
end
|
data/lib/spectre/ssh.rb
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
|
5
|
+
module Spectre
|
6
|
+
module SSH
|
7
|
+
@@cfg = {}
|
8
|
+
|
9
|
+
class SSHConnection < DslClass
|
10
|
+
def initialize host, username, opts, logger
|
11
|
+
opts[:non_interactive] = true
|
12
|
+
|
13
|
+
@__logger = logger
|
14
|
+
@__host = host
|
15
|
+
@__username = username
|
16
|
+
@__opts = opts
|
17
|
+
@__session = nil
|
18
|
+
@__exit_code = nil
|
19
|
+
@__output = ''
|
20
|
+
end
|
21
|
+
|
22
|
+
def file_exists path
|
23
|
+
exec "ls #{path}"
|
24
|
+
exit_code == 0
|
25
|
+
end
|
26
|
+
|
27
|
+
def owner_of path
|
28
|
+
exec "stat -c %U #{path}"
|
29
|
+
output.chomp
|
30
|
+
end
|
31
|
+
|
32
|
+
def connect!
|
33
|
+
return unless @__session == nil or @__session.closed?
|
34
|
+
@__session = Net::SSH.start(@__host, @__username, @__opts)
|
35
|
+
end
|
36
|
+
|
37
|
+
def close
|
38
|
+
return unless @__session and not @__session.closed?
|
39
|
+
@__session.close
|
40
|
+
end
|
41
|
+
|
42
|
+
def can_connect?
|
43
|
+
@__output = nil
|
44
|
+
|
45
|
+
begin
|
46
|
+
connect!
|
47
|
+
@__session.open_channel.close
|
48
|
+
@__output = "successfully connected to #{@__host} with user #{@__username}"
|
49
|
+
@__exit_code = 0
|
50
|
+
return true
|
51
|
+
rescue Exception => e
|
52
|
+
@__logger.error e.message
|
53
|
+
@__output = "unable to connect to #{@__host} with user #{@__username}"
|
54
|
+
@__exit_code = 1
|
55
|
+
end
|
56
|
+
|
57
|
+
return false
|
58
|
+
end
|
59
|
+
|
60
|
+
def exec command
|
61
|
+
connect!
|
62
|
+
|
63
|
+
log_str = "#{@__session.options[:user]}@#{@__session.host} -p #{@__session.options[:port]} #{command}"
|
64
|
+
|
65
|
+
@channel = @__session.open_channel do |channel|
|
66
|
+
channel.exec(command) do |ch, success|
|
67
|
+
abort "could not execute #{command} on #{@__session.host}" unless success
|
68
|
+
|
69
|
+
@__output = ''
|
70
|
+
|
71
|
+
channel.on_data do |ch, data|
|
72
|
+
@__output += data
|
73
|
+
end
|
74
|
+
|
75
|
+
channel.on_extended_data do |ch,type,data|
|
76
|
+
@__output += data
|
77
|
+
end
|
78
|
+
|
79
|
+
channel.on_request('exit-status') do |ch, data|
|
80
|
+
@__exit_code = data.read_long
|
81
|
+
end
|
82
|
+
|
83
|
+
# channel.on_request('exit-signal') do |ch, data|
|
84
|
+
# exit_code = data.read_long
|
85
|
+
# end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
@channel.wait
|
91
|
+
@__session.loop
|
92
|
+
|
93
|
+
log_str += "\n" + @__output
|
94
|
+
@__logger.info log_str
|
95
|
+
end
|
96
|
+
|
97
|
+
def output
|
98
|
+
@__output
|
99
|
+
end
|
100
|
+
|
101
|
+
def exit_code
|
102
|
+
@__exit_code
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
class << self
|
108
|
+
def ssh name, config = {}, &block
|
109
|
+
raise "SSH connection '#{name}' not configured" unless @@cfg.has_key?(name) or config.count > 0
|
110
|
+
|
111
|
+
cfg = @@cfg[name] || {}
|
112
|
+
|
113
|
+
host = cfg['host'] || name
|
114
|
+
username = config[:username] || cfg['username']
|
115
|
+
password = config[:password] || cfg['password']
|
116
|
+
|
117
|
+
opts = {}
|
118
|
+
opts[:password] = password
|
119
|
+
opts[:port] = config[:port] || cfg['port'] || 22
|
120
|
+
opts[:keys] = [cfg['key']] if cfg.has_key? 'key'
|
121
|
+
opts[:passphrase] = cfg['passphrase'] if cfg.has_key? 'passphrase'
|
122
|
+
|
123
|
+
opts[:auth_methods] = []
|
124
|
+
opts[:auth_methods].push 'publickey' if opts[:keys]
|
125
|
+
opts[:auth_methods].push 'password' if opts[:password]
|
126
|
+
|
127
|
+
ssh_con = SSHConnection.new(host, username, opts, @@logger)
|
128
|
+
|
129
|
+
begin
|
130
|
+
ssh_con.instance_eval &block
|
131
|
+
ensure
|
132
|
+
ssh_con.close
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
Spectre.register do |config|
|
138
|
+
@@logger = ::Logger.new config['log_file'], progname: 'spectre/ssh'
|
139
|
+
|
140
|
+
if config.has_key? 'ssh'
|
141
|
+
config['ssh'].each do |name, cfg|
|
142
|
+
@@cfg[name] = cfg
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
Spectre.delegate :ssh, to: self
|
148
|
+
end
|
149
|
+
end
|