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.
@@ -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
@@ -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