cem_win_spec 0.1.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/.rspec +3 -0
- data/.rubocop.yml +534 -0
- data/CODEOWNERS +2 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +115 -0
- data/LICENSE.txt +21 -0
- data/README.md +33 -0
- data/Rakefile +12 -0
- data/cem_win_spec.gemspec +37 -0
- data/exe/cem-win-spec +52 -0
- data/lib/cem_win_spec/fixture_cache.rb +121 -0
- data/lib/cem_win_spec/iap_tunnel.rb +174 -0
- data/lib/cem_win_spec/logging/formatter.rb +97 -0
- data/lib/cem_win_spec/logging.rb +170 -0
- data/lib/cem_win_spec/module_archive_builder.rb +84 -0
- data/lib/cem_win_spec/rake_tasks.rb +138 -0
- data/lib/cem_win_spec/remote_command.rb +47 -0
- data/lib/cem_win_spec/rspec_test_cmds.rb +51 -0
- data/lib/cem_win_spec/test_runner.rb +144 -0
- data/lib/cem_win_spec/version.rb +5 -0
- data/lib/cem_win_spec/win_exec/base_exec.rb +42 -0
- data/lib/cem_win_spec/win_exec/connection_opts.rb +169 -0
- data/lib/cem_win_spec/win_exec/local_exec.rb +74 -0
- data/lib/cem_win_spec/win_exec/output.rb +104 -0
- data/lib/cem_win_spec/win_exec/winrm_exec.rb +89 -0
- data/lib/cem_win_spec/win_exec.rb +234 -0
- data/lib/cem_win_spec.rb +79 -0
- data/sig/cem_win_spec.rbs +4 -0
- metadata +159 -0
data/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# CemWinSpec
|
|
2
|
+
|
|
3
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/cem_win_spec`. To experiment with that code, run `bin/console` for an interactive prompt.
|
|
4
|
+
|
|
5
|
+
TODO: Delete this and the text above, and describe your gem
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
10
|
+
|
|
11
|
+
$ bundle add cem_win_spec
|
|
12
|
+
|
|
13
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
14
|
+
|
|
15
|
+
$ gem install cem_win_spec
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
TODO: Write usage instructions here
|
|
20
|
+
|
|
21
|
+
## Development
|
|
22
|
+
|
|
23
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
24
|
+
|
|
25
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
26
|
+
|
|
27
|
+
## Contributing
|
|
28
|
+
|
|
29
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/cem_win_spec.
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/cem_win_spec/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "cem_win_spec"
|
|
7
|
+
spec.version = CemWinSpec::VERSION
|
|
8
|
+
spec.authors = ["Heston Snodgrass"]
|
|
9
|
+
spec.email = ["hsnodgrass@users.noreply.github.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Write a short summary, because RubyGems requires one."
|
|
12
|
+
spec.description = "Write a longer description or delete this line."
|
|
13
|
+
spec.homepage = "https://github.com/hsnodgrass/cem_win_spec"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = ">= 2.7.0"
|
|
16
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
17
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/CHANGELOG.md"
|
|
19
|
+
|
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
|
21
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
22
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
23
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
24
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
spec.bindir = "exe"
|
|
28
|
+
spec.executables = ["cem-win-spec"]
|
|
29
|
+
spec.require_paths = ["lib"]
|
|
30
|
+
|
|
31
|
+
spec.add_dependency "winrm", "~> 2.3"
|
|
32
|
+
spec.add_dependency "winrm-fs", "~> 1.3"
|
|
33
|
+
spec.add_dependency "tty-spinner", "~> 0.9"
|
|
34
|
+
spec.add_dependency "puppet_forge", "~> 4.1"
|
|
35
|
+
spec.add_dependency "parallel_tests", "~> 3.4"
|
|
36
|
+
spec.add_development_dependency "pry"
|
|
37
|
+
end
|
data/exe/cem-win-spec
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'optparse'
|
|
5
|
+
require 'cem_win_spec'
|
|
6
|
+
|
|
7
|
+
# This is a wrapper script for the cem_win_spec gem. It is used to run the
|
|
8
|
+
# cem_win_spec gem from the command line. It is installed as part of the gem
|
|
9
|
+
# and is not intended to be run directly from the source code.
|
|
10
|
+
|
|
11
|
+
# Parse command line options
|
|
12
|
+
options = {}
|
|
13
|
+
parser = OptionParser.new do |opts|
|
|
14
|
+
opts.banner = 'Usage: cem-win-spec [options]'
|
|
15
|
+
|
|
16
|
+
opts.on('-v', '--version', 'Print version and exit') do
|
|
17
|
+
puts "cem-win-spec version #{CemWinSpec::VERSION}"
|
|
18
|
+
exit 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
opts.on('-q', '--quiet', 'Suppress output') do
|
|
22
|
+
options[:quiet] = true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
opts.on('-d', '--debug', 'Enable debug output on the console') do
|
|
26
|
+
options[:debug] = true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
opts.on('-V', '--verbose', 'Enable verbose output on the console') do
|
|
30
|
+
options[:verbose] = true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
opts.on('-l', '--log-level [LEVEL]', 'Set log level (debug, info, warn, error, fatal)') do |level|
|
|
34
|
+
options[:log_level] = level
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
opts.on('-L', '--log-file [FILE]', 'Log output to file') do |file|
|
|
38
|
+
options[:log_file] = file
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
opts.on('-f', '--log-format [FORMAT]', 'Set log format(file, text, json, github_action') do |log_format|
|
|
42
|
+
options[:log_format] = log_format
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
opts.on('-h', '--help', 'Print this help') do
|
|
46
|
+
puts opts
|
|
47
|
+
exit 0
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
parser.parse!
|
|
51
|
+
|
|
52
|
+
CemWinSpec.run_tests(options)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'pathname'
|
|
7
|
+
require 'puppet_forge'
|
|
8
|
+
require 'yaml'
|
|
9
|
+
|
|
10
|
+
module CemWinSpec
|
|
11
|
+
# Class for managing cached fixtures
|
|
12
|
+
# Fixture caching is used to speed up test runs by reusing the same
|
|
13
|
+
# Puppet modules between test runs instead of downloading them each time.
|
|
14
|
+
# The cache works by creating a YAML file that maps module names and
|
|
15
|
+
class FixtureCache
|
|
16
|
+
CACHE_MANIFEST = 'cache_manifest.yaml'
|
|
17
|
+
|
|
18
|
+
attr_reader :cache_dir, :cache_entries
|
|
19
|
+
|
|
20
|
+
def initialize(cache_dir = 'C:\\ProgramData\\cem_win_spec')
|
|
21
|
+
raise 'FixtureCache must be ran on Windows' unless Gem.win_platform?
|
|
22
|
+
|
|
23
|
+
@cache_dir = cache_dir
|
|
24
|
+
@cache_entries = setup_and_load_cache
|
|
25
|
+
@dependencies = dependencies_from_metadata
|
|
26
|
+
setup!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def copy_fixtures_to(fixtures_dir)
|
|
30
|
+
@cache_entries.each do |_, path|
|
|
31
|
+
FileUtils.cp_r(path, fixtures_dir)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def setup!
|
|
38
|
+
@dependencies.each do |name, data|
|
|
39
|
+
next if cached?(data[:checksum])
|
|
40
|
+
|
|
41
|
+
puts "Downloading #{name} #{data[:version]}..."
|
|
42
|
+
download_and_cache(name, data[:release_slug], data[:checksum])
|
|
43
|
+
end
|
|
44
|
+
ensure
|
|
45
|
+
save_cache_entries
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def dependencies_from_metadata
|
|
49
|
+
raise "File metadata.json not found in current directory #{Dir.pwd}" unless File.exist?('metadata.json')
|
|
50
|
+
|
|
51
|
+
metadata = JSON.parse(File.read('metadata.json'))
|
|
52
|
+
return {} unless metadata.key?('dependencies')
|
|
53
|
+
|
|
54
|
+
metadata['dependencies'].each_with_object({}) do |dep, hsh|
|
|
55
|
+
mod_name = dep['name'].tr('/', '-')
|
|
56
|
+
latest_valid_release = PuppetForge::Module.find(mod_name).releases.find do |r|
|
|
57
|
+
dep_version_req_satisfied?(dep['version_requirement'], r.version)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
hsh[mod_name] = {
|
|
61
|
+
checksum: module_checksum(mod_name, dep['version_requirement'], latest_valid_release.version),
|
|
62
|
+
version_req: dep['version_requirement'],
|
|
63
|
+
version: latest_valid_release.version,
|
|
64
|
+
release_slug: latest_valid_release.slug,
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def dep_version_req_satisfied?(version_req, version)
|
|
70
|
+
reqs = version_req.match(%r{^([<>=!]+ \S+)\s*([<>=!]+ \S+)})
|
|
71
|
+
raise "Invalid version requirement #{version_req}" unless reqs.to_a.length > 1
|
|
72
|
+
|
|
73
|
+
reqs.to_a[1..-1].all? { |req| version_req_satisfied?(req, version) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def version_req_satisfied?(version_req, version)
|
|
77
|
+
Gem::Requirement.create(version_req).satisfied_by?(Gem::Version.new(version))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def module_checksum(name, version_req, version)
|
|
81
|
+
Digest::SHA256.hexdigest("#{name}#{version_req}#{version}")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def manifest
|
|
85
|
+
File.join(cache_dir, CACHE_MANIFEST)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def setup_and_load_cache
|
|
89
|
+
unless Dir.exist?(cache_dir)
|
|
90
|
+
puts "Creating cache directory #{cache_dir}..."
|
|
91
|
+
FileUtils.mkdir_p(cache_dir)
|
|
92
|
+
end
|
|
93
|
+
unless File.exist?(manifest)
|
|
94
|
+
puts "Creating cache manifest #{manifest}..."
|
|
95
|
+
File.write(manifest, {}.to_yaml)
|
|
96
|
+
end
|
|
97
|
+
YAML.load_file(manifest)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def cached?(checksum)
|
|
101
|
+
cache_entries.key?(checksum)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def download_and_cache(release_slug, checksum)
|
|
105
|
+
release = PuppetForge::Release.find(release_slug)
|
|
106
|
+
module_cache_dir = File.join(cache_dir, checksum)
|
|
107
|
+
::FileUtils.mkdir_p(File.join(cache_dir, checksum))
|
|
108
|
+
tarball_path = File.join(cache_dir, checksum, "#{release_slug}.tar.gz")
|
|
109
|
+
release.download(Pathname(tarball_path))
|
|
110
|
+
release.verify(Pathname(tarball_path))
|
|
111
|
+
PuppetForge::Unpacker.unpack(tarball_path,
|
|
112
|
+
File.join(module_cache_dir, name),
|
|
113
|
+
File.join(module_cache_dir, 'temp'))
|
|
114
|
+
cache_entries[checksum] = File.join(module_cache_dir, name)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def save_cache_entries
|
|
118
|
+
File.write(manifest, cache_entries.to_yaml)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'io/console'
|
|
4
|
+
require_relative 'logging'
|
|
5
|
+
|
|
6
|
+
module CemWinSpec
|
|
7
|
+
# This class is used to create a tunnel to a GCP instance
|
|
8
|
+
class IapTunnel
|
|
9
|
+
include CemWinSpec::Logging
|
|
10
|
+
|
|
11
|
+
# We don't go all the way to 65_535 because gcloud shits the bed
|
|
12
|
+
# on MacOS when you assign port 65_535 to a tunnel for some reason.
|
|
13
|
+
EPHEMERAL_PORT_RANGE = (49_152..65_534).to_a.freeze
|
|
14
|
+
|
|
15
|
+
attr_reader :pid, :port, :project_id, :zone, :instance_name
|
|
16
|
+
|
|
17
|
+
def initialize(project_id = nil, zone = nil, instance_name = nil)
|
|
18
|
+
@project_id = project_id || get_project_id
|
|
19
|
+
@zone = zone || get_zone
|
|
20
|
+
@instance_name = instance_name || get_instance_name
|
|
21
|
+
@port = find_available_port # Get a random port from the ephemeral port range
|
|
22
|
+
@pid = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def running?
|
|
26
|
+
!@pid.nil?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def with
|
|
30
|
+
return unless block_given?
|
|
31
|
+
|
|
32
|
+
start
|
|
33
|
+
yield port
|
|
34
|
+
ensure
|
|
35
|
+
stop
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def start
|
|
39
|
+
return if running?
|
|
40
|
+
|
|
41
|
+
logger.info 'Starting IAP tunnel...'
|
|
42
|
+
logger.debug "Running command: #{tunnel_cmd}"
|
|
43
|
+
@pid = spawn(tunnel_cmd)
|
|
44
|
+
sleep(5)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# This method stops the IAP tunnel
|
|
48
|
+
# @param wait [Boolean] Whether to wait for the tunnel to stop before returning
|
|
49
|
+
# @param log [Boolean] Whether to log messages. This must be set to false if you are
|
|
50
|
+
# calling this method from within a trap block, otherwise the logger will throw an exception.
|
|
51
|
+
def stop(wait: true, log: true)
|
|
52
|
+
return unless running?
|
|
53
|
+
|
|
54
|
+
logger.info 'Stopping IAP tunnel...' if log
|
|
55
|
+
logger.debug "Killing PID: #{@pid}" if log
|
|
56
|
+
Process.kill('TERM', @pid)
|
|
57
|
+
Process.waitpid(@pid) if wait
|
|
58
|
+
@pid = nil
|
|
59
|
+
logger.info 'IAP tunnel stopped' if log
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def tunnel_cmd
|
|
65
|
+
raise 'Port must be set' unless port
|
|
66
|
+
[
|
|
67
|
+
'gcloud',
|
|
68
|
+
'compute',
|
|
69
|
+
'start-iap-tunnel',
|
|
70
|
+
instance_name,
|
|
71
|
+
'5986',
|
|
72
|
+
"--local-host-port=localhost:#{port}",
|
|
73
|
+
"--zone=#{zone}",
|
|
74
|
+
"--project=#{project_id}",
|
|
75
|
+
].join(' ')
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# This function gets the GCP project ID
|
|
79
|
+
def get_project_id
|
|
80
|
+
# Get the project ID from the environment
|
|
81
|
+
project_id = ENV['GCP_PROJECT_ID'] || `gcloud config get-value project`.chomp
|
|
82
|
+
|
|
83
|
+
# If the project ID is not set, prompt the user for it
|
|
84
|
+
if project_id.nil? && ENV['CI']
|
|
85
|
+
raise 'Please set the GCP_PROJECT_ID environment variable or ensure that gcloud is configured'
|
|
86
|
+
elsif project_id.nil?
|
|
87
|
+
puts 'Please enter your GCP project ID:'
|
|
88
|
+
project_id = $stdin.cooked(&:gets).chomp
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
raise 'GCP project ID cannot be empty' if project_id.empty?
|
|
92
|
+
|
|
93
|
+
logger.debug 'GCP project ID is set'
|
|
94
|
+
# Return the project ID
|
|
95
|
+
project_id
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# This function gets the GCP zone
|
|
99
|
+
def get_zone
|
|
100
|
+
return @zone if @zone
|
|
101
|
+
|
|
102
|
+
# Get the zone from the environment
|
|
103
|
+
zone = ENV['GCP_ZONE'] || `gcloud config get-value compute/zone`.chomp
|
|
104
|
+
|
|
105
|
+
# If the zone is not set, prompt the user for it
|
|
106
|
+
if zone.nil? && ENV['CI']
|
|
107
|
+
raise 'Please set the GCP_ZONE environment variable or ensure that gcloud is configured'
|
|
108
|
+
elsif zone.nil?
|
|
109
|
+
puts 'Please enter your GCP zone:'
|
|
110
|
+
zone = $stdin.cooked(&:gets).chomp
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
raise 'GCP zone cannot be empty' if zone.empty?
|
|
114
|
+
|
|
115
|
+
logger.debug 'GCP zone is set'
|
|
116
|
+
# Return the zone
|
|
117
|
+
zone
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# This function gets the GCP instance name
|
|
121
|
+
def get_instance_name
|
|
122
|
+
# Get the instance name from the environment
|
|
123
|
+
instance_name = ENV['GCP_INSTANCE_NAME']
|
|
124
|
+
|
|
125
|
+
# If the instance name is not set, prompt the user for it
|
|
126
|
+
if instance_name.nil? && ENV['CI']
|
|
127
|
+
raise 'Please set the GCP_INSTANCE_NAME environment variable'
|
|
128
|
+
elsif instance_name.nil?
|
|
129
|
+
puts 'Please enter your GCP instance name:'
|
|
130
|
+
instance_name = $stdin.cooked(&:gets).chomp
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
raise 'GCP instance name cannot be empty' if instance_name.empty?
|
|
134
|
+
|
|
135
|
+
logger.debug 'GCP instance name is set'
|
|
136
|
+
# Return the instance name
|
|
137
|
+
instance_name
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# This function ensures that gcloud is installed, usable, and authenticated
|
|
141
|
+
def gcloud_setup
|
|
142
|
+
# Ensure that gcloud is installed
|
|
143
|
+
unless system('gcloud --version')
|
|
144
|
+
raise 'Please install Google Cloud SDK'
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Ensure that gcloud is authenticated
|
|
148
|
+
unless system('gcloud auth list --format=json | grep ACTIVE')
|
|
149
|
+
raise 'Please run `gcloud auth login`'
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# This function finds an available local port to use for the IAP tunnel
|
|
154
|
+
def find_available_port
|
|
155
|
+
port_range = EPHEMERAL_PORT_RANGE.dup.shuffle
|
|
156
|
+
port_range.each do |prt|
|
|
157
|
+
return prt if local_port_open?(prt)
|
|
158
|
+
end
|
|
159
|
+
raise 'No available ports found'
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# This function checks if a local port is open
|
|
163
|
+
def local_port_open?(prt)
|
|
164
|
+
require 'socket'
|
|
165
|
+
socket = Socket.new(Socket::Constants::AF_INET, Socket::Constants::SOCK_STREAM, 0)
|
|
166
|
+
socket.bind(Socket.pack_sockaddr_in(prt, '0.0.0.0'))
|
|
167
|
+
true
|
|
168
|
+
rescue Errno::EADDRINUSE, Errno::CONNREFUSED
|
|
169
|
+
false
|
|
170
|
+
ensure
|
|
171
|
+
socket&.close
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CemWinSpec
|
|
4
|
+
module Logging
|
|
5
|
+
module Formatter
|
|
6
|
+
class << self
|
|
7
|
+
def for(log_format)
|
|
8
|
+
log_format = log_format.to_s.downcase.to_sym if log_format.respond_to?(:to_s)
|
|
9
|
+
all.find { |f| f.log_format == log_format }.get
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def all
|
|
15
|
+
@all ||= [FileFormatter.new, JSONFormatter.new, TextFormatter.new, GithubActionFormatter.new]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class FileFormatter
|
|
20
|
+
attr_reader :log_format
|
|
21
|
+
|
|
22
|
+
def initialize
|
|
23
|
+
@log_format = :file
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def get
|
|
27
|
+
proc do |severity, datetime, progname, msg|
|
|
28
|
+
format(severity, datetime, progname, msg)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def format(severity, datetime, progname, msg)
|
|
35
|
+
"[#{datetime}] | #{progname} | #{severity} | #{msg}\n"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class JSONFormatter < FileFormatter
|
|
40
|
+
def initialize
|
|
41
|
+
super
|
|
42
|
+
@log_format = :json
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def format(severity, datetime, progname, msg)
|
|
48
|
+
require 'json'
|
|
49
|
+
{
|
|
50
|
+
timestamp: datetime,
|
|
51
|
+
progname: progname,
|
|
52
|
+
severity: severity,
|
|
53
|
+
message: msg,
|
|
54
|
+
}.to_json + "\n"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class TextFormatter < FileFormatter
|
|
59
|
+
def initialize
|
|
60
|
+
super
|
|
61
|
+
@log_format = :text
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def format(severity, _datetime, _progname, msg)
|
|
67
|
+
severity == 'INFO' ? "#{msg}\n" : "#{severity}: #{msg}\n"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
class GithubActionFormatter < FileFormatter
|
|
72
|
+
SEV_MAP = {
|
|
73
|
+
'DEBUG' => '::debug',
|
|
74
|
+
'INFO' => '::notice',
|
|
75
|
+
'WARN' => '::warning',
|
|
76
|
+
'ERROR' => '::error',
|
|
77
|
+
'FATAL' => '::error',
|
|
78
|
+
}.freeze
|
|
79
|
+
|
|
80
|
+
def initialize
|
|
81
|
+
super
|
|
82
|
+
@log_format = :github_action
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def format(severity, _datetime, _progname, msg)
|
|
88
|
+
if severity == 'DEBUG'
|
|
89
|
+
"#{SEV_MAP[severity]}::{#{msg}}\n"
|
|
90
|
+
else
|
|
91
|
+
"#{SEV_MAP[severity]} #{msg}\n"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
require_relative 'logging/formatter'
|
|
5
|
+
|
|
6
|
+
module CemWinSpec
|
|
7
|
+
# Logging for CemWinSpec
|
|
8
|
+
module Logging
|
|
9
|
+
LEVEL_MAP = {
|
|
10
|
+
'debug' => Logger::DEBUG,
|
|
11
|
+
'info' => Logger::INFO,
|
|
12
|
+
'warn' => Logger::WARN,
|
|
13
|
+
'error' => Logger::ERROR,
|
|
14
|
+
'fatal' => Logger::FATAL,
|
|
15
|
+
'unknown' => Logger::UNKNOWN,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# Delegator class for when you want to log to multiple devices
|
|
19
|
+
# at the same time, such as STDOUT and a file.
|
|
20
|
+
# @param loggers [::Logger] one or more instances of Logger
|
|
21
|
+
class MultiLogger
|
|
22
|
+
def initialize(*loggers)
|
|
23
|
+
@loggers = loggers
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def method_missing(m, *args, &block)
|
|
27
|
+
if @loggers.all? { |l| l.respond_to?(m) }
|
|
28
|
+
@loggers.map { |l| l.send(m, *args, &block) }
|
|
29
|
+
else
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def respond_to_missing?(m, include_private = false)
|
|
35
|
+
@loggers.all? { |l| l.respond_to?(m) } || super
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class << self
|
|
40
|
+
def log_setup!(options = {})
|
|
41
|
+
console_l_level = if options[:debug]
|
|
42
|
+
new_log_level('debug')
|
|
43
|
+
else
|
|
44
|
+
new_log_level('info')
|
|
45
|
+
end
|
|
46
|
+
console_l_formatter = if ENV['CI'] || ENV['GITHUB_ACTIONS']
|
|
47
|
+
new_log_formatter(options[:log_format] || 'github_action')
|
|
48
|
+
else
|
|
49
|
+
new_log_formatter(options[:log_format] || 'text')
|
|
50
|
+
end
|
|
51
|
+
file_l_level = new_log_level(options[:log_level] || 'debug')
|
|
52
|
+
file_l_formatter = new_log_formatter(options[:log_format] || 'file')
|
|
53
|
+
loggers = []
|
|
54
|
+
if options[:log_file]
|
|
55
|
+
loggers << ::Logger.new(
|
|
56
|
+
options[:log_file],
|
|
57
|
+
level: file_l_level,
|
|
58
|
+
formatter: file_l_formatter,
|
|
59
|
+
progname: 'CemWinSpec',
|
|
60
|
+
datetime_format: '%Y%m%dT%H%M%S%z',
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
unless options[:quiet]
|
|
64
|
+
loggers << ::Logger.new(
|
|
65
|
+
$stdout,
|
|
66
|
+
level: console_l_level,
|
|
67
|
+
formatter: console_l_formatter,
|
|
68
|
+
progname: 'CemWinSpec',
|
|
69
|
+
datetime_format: '%Y%m%dT%H%M%S%z',
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
@logger = MultiLogger.new(*loggers)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Exposes a logger instance. Will either use the currently set logger or
|
|
76
|
+
# create a new one.
|
|
77
|
+
# @return [Logger]
|
|
78
|
+
def logger
|
|
79
|
+
raise 'Logger not set up! Call CemWinSpec::Logging.log_setup! first.' unless @logger
|
|
80
|
+
|
|
81
|
+
@logger
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Shortcut method for logger.level
|
|
85
|
+
# @return [Logger::Severity]
|
|
86
|
+
def current_log_level
|
|
87
|
+
logger.level
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Gets a log level from a string and returns the corresponding
|
|
91
|
+
# Logger::Severity constant.
|
|
92
|
+
# @param level [String] the log level to get
|
|
93
|
+
def new_log_level(level)
|
|
94
|
+
raise ArgumentError, 'Log level not recognized' unless LEVEL_MAP[level.to_s.downcase]
|
|
95
|
+
|
|
96
|
+
LEVEL_MAP[level.downcase]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Shows the current log format style if set, or the default if not.
|
|
100
|
+
# @return [Symbol] the current log format style
|
|
101
|
+
def current_log_format
|
|
102
|
+
@current_log_format ||= :text
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Sets the current log format style and returns a proc to be passed to
|
|
106
|
+
# Logger#formatter=
|
|
107
|
+
# @param f [Symbol] the log format style to set
|
|
108
|
+
# @return [Proc] the proc to be passed to Logger#formatter=
|
|
109
|
+
def new_log_formatter(f)
|
|
110
|
+
Formatter.for(f)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Provides class method wrappers for logging methods
|
|
115
|
+
def self.included(base)
|
|
116
|
+
class << base
|
|
117
|
+
def log_setup!(options = {})
|
|
118
|
+
CemWinSpec::Logging.log_setup!(options)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def logger
|
|
122
|
+
CemWinSpec::Logging.logger
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def current_log_level
|
|
126
|
+
CemWinSpec::Logging.current_log_level
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def new_log_level(level)
|
|
130
|
+
CemWinSpec::Logging.new_log_level(level)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def current_log_format
|
|
134
|
+
CemWinSpec::Logging.current_log_format
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def new_log_formatter(f)
|
|
138
|
+
CemWinSpec::Logging.new_log_formatter(f)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def log_setup!(options = {})
|
|
144
|
+
CemWinSpec::Logging.log_setup!(options)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Exposes the logger instance
|
|
148
|
+
def logger
|
|
149
|
+
CemWinSpec::Logging.logger
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Exposes the current log level
|
|
153
|
+
def current_log_level
|
|
154
|
+
CemWinSpec::Logging.current_log_level
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Exposes setting the log level
|
|
158
|
+
def new_log_level(level)
|
|
159
|
+
CemWinSpec::Logging.new_log_level(level)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def current_log_format
|
|
163
|
+
CemWinSpec::Logging.current_log_format
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def new_log_formatter(f)
|
|
167
|
+
CemWinSpec::Logging.new_log_formatter(f)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|