spectre-core 1.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|