cem_win_spec 0.1.1 → 0.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cdb91d3bd9c4220238d0712938226a34f036c4313370f0ac70b818402d67051d
4
- data.tar.gz: 6e1b4a03b910513b5d4e985f5643394834f0ff38e7af9c03ffebe24a179d1f61
3
+ metadata.gz: c59006dbfceccc0e0581630b0091722aa31a5644440bb906572c8429a654c37a
4
+ data.tar.gz: 91d3e8e17c961d90e90dd5895376418279062ace8ee3b3327d24a511b7287a98
5
5
  SHA512:
6
- metadata.gz: d36cc7f04c91d80fd494caecbe1ee33b39a71d3cde95cf71f4381ba12c232f0f4ed02b91886a89a9e4d2253d1b8ca9bc58bb8348e69d8eda97c7e5d5da22399c
7
- data.tar.gz: c3fc479c502495358843257577391242f7c7003abee9fcb73e7dfa2e456adcd9bbbd448b864b9df347096d519c6ca4ca9ce6c9a7be71582acca448710bb88807
6
+ metadata.gz: cddc71efc46c8a5a0872ee5670b6bdcff1b77904190e18e87f3fa728f2bdb8c533bef7382e8df918337b87811fc21ac04a4b0bd0f358774911a78279f9709968
7
+ data.tar.gz: d52050dd876973fadb74a801b198e88f56341ee125a96a6407594376d6155a3a02a7037576ec10f775346660a7d5fd34ee75e26966ef7011889c45ad968a9e02
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cem_win_spec (0.1.1)
4
+ cem_win_spec (0.1.3)
5
5
  parallel_tests (~> 3.4)
6
6
  puppet_forge (~> 4.1)
7
7
  tty-spinner (~> 0.9)
@@ -15,21 +15,22 @@ GEM
15
15
  builder (3.2.4)
16
16
  coderay (1.1.3)
17
17
  diff-lcs (1.5.0)
18
- erubi (1.10.0)
19
- faraday (2.3.0)
20
- faraday-net_http (~> 2.0)
18
+ erubi (1.12.0)
19
+ faraday (2.7.5)
20
+ faraday-net_http (>= 2.0, < 3.1)
21
21
  ruby2_keywords (>= 0.0.4)
22
22
  faraday-follow_redirects (0.3.0)
23
23
  faraday (>= 1, < 3)
24
- faraday-net_http (2.0.3)
24
+ faraday-net_http (3.0.2)
25
25
  ffi (1.15.5)
26
26
  gssapi (1.3.1)
27
27
  ffi (>= 1.0.1)
28
- gyoku (1.3.1)
28
+ gyoku (1.4.0)
29
29
  builder (>= 2.1.2)
30
+ rexml (~> 3.0)
30
31
  httpclient (2.8.3)
31
32
  little-plugger (1.1.4)
32
- logging (2.3.0)
33
+ logging (2.3.1)
33
34
  little-plugger (~> 1.1)
34
35
  multi_json (~> 1.14)
35
36
  method_source (1.0.0)
@@ -81,7 +82,7 @@ GEM
81
82
  ruby2_keywords (0.0.5)
82
83
  rubyntlm (0.6.3)
83
84
  rubyzip (2.3.2)
84
- semantic_puppet (1.0.4)
85
+ semantic_puppet (1.1.0)
85
86
  tty-cursor (0.7.1)
86
87
  tty-spinner (0.9.3)
87
88
  tty-cursor (~> 0.7)
@@ -103,6 +104,7 @@ GEM
103
104
 
104
105
  PLATFORMS
105
106
  x86_64-darwin-19
107
+ x86_64-darwin-20
106
108
 
107
109
  DEPENDENCIES
108
110
  cem_win_spec!
data/exe/cem-win-spec CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  require 'optparse'
5
5
  require 'cem_win_spec'
6
+ require 'cem_win_spec/version'
6
7
 
7
8
  # This is a wrapper script for the cem_win_spec gem. It is used to run the
8
9
  # cem_win_spec gem from the command line. It is installed as part of the gem
@@ -18,6 +19,26 @@ parser = OptionParser.new do |opts|
18
19
  exit 0
19
20
  end
20
21
 
22
+ opts.on('-p', '--puppet-version [VERSION]', 'Puppet version to test against') do |version|
23
+ unless %w[7 8].include?(version)
24
+ warn "Unknown Puppet version: #{version}"
25
+ warn 'Valid versions are: 7, 8'
26
+ puts opts
27
+ exit 1
28
+ end
29
+ options[:puppet_version] = version
30
+ end
31
+
32
+ opts.on('-r', '--ruby-version [VERSION]', 'Ruby version to test against') do |version|
33
+ unless %w[2 3].include?(version)
34
+ warn "Unknown Ruby version: #{version}"
35
+ warn 'Valid versions are: 2, 3'
36
+ puts opts
37
+ exit 1
38
+ end
39
+ options[:ruby_version] = version
40
+ end
41
+
21
42
  opts.on('-o', '--operation [OPERATION]', 'Operation to perform (spec, clean_fixture_cache)') do |operation|
22
43
  unless %w[spec clean_fixture_cache].include?(operation)
23
44
  warn "Unknown operation: #{operation}"
@@ -4,6 +4,8 @@ require 'io/console'
4
4
  require_relative 'logging'
5
5
 
6
6
  module CemWinSpec
7
+ class IapTunnelStartError < StandardError; end
8
+
7
9
  # This class is used to create a tunnel to a GCP instance
8
10
  class IapTunnel
9
11
  include CemWinSpec::Logging
@@ -23,7 +25,9 @@ module CemWinSpec
23
25
  end
24
26
 
25
27
  def running?
26
- !@pid.nil?
28
+ !@pid.nil? && Process.getpgid(@pid)
29
+ rescue Errno::ESRCH
30
+ false
27
31
  end
28
32
 
29
33
  def with
@@ -41,7 +45,12 @@ module CemWinSpec
41
45
  logger.info 'Starting IAP tunnel...'
42
46
  logger.debug "Running command: #{tunnel_cmd}"
43
47
  @pid = spawn(tunnel_cmd)
44
- sleep(5)
48
+ sleep(3) # Give the tunnel a few seconds to start
49
+ Process.getpgid(@pid) # Check if the process starts successfully
50
+ logger.info "IAP tunnel started on port #{port}"
51
+ rescue Errno::ESRCH
52
+ @pid = nil
53
+ raise IapTunnelStartError, "Failed to start IAP tunnel on port #{port}: #{$ERROR_INFO}"
45
54
  end
46
55
 
47
56
  # This method stops the IAP tunnel
@@ -6,24 +6,25 @@ module CemWinSpec
6
6
  class RspecTestCmds
7
7
  DEFAULT_PATTERN = 'spec/{classes,defines}/**/*_spec.rb'
8
8
 
9
- def initialize(pattern: DEFAULT_PATTERN)
9
+ def initialize(use_bundler: true, pattern: DEFAULT_PATTERN)
10
+ @use_bundler = use_bundler
10
11
  @pattern = pattern
11
12
  end
12
13
 
13
14
  def cmd_standalone(*args)
14
- "bundle exec rake 'cem:spec_standalone#{rake_args(nil, *args)}'"
15
+ prefix "rake 'cem:spec_standalone#{rake_args(nil, *args)}'"
15
16
  end
16
17
 
17
18
  def cmd_parallel(*args)
18
- "bundle exec rake 'cem:parallel_spec_standalone#{rake_args(*args)}'"
19
+ prefix "rake 'cem:parallel_spec_standalone#{rake_args(*args)}'"
19
20
  end
20
21
 
21
22
  def cmd_chunked(*files)
22
- "bundle exec rake 'cem:parallel_spec_files#{rake_args(files.join(' '))}'"
23
+ prefix "rake 'cem:parallel_spec_files#{rake_args(files.join(' '))}'"
23
24
  end
24
25
 
25
26
  def prep_cmd
26
- 'bundle exec rake cem:win_spec_prep'
27
+ prefix 'rake cem:win_spec_prep'
27
28
  end
28
29
 
29
30
  def cmds(*args)
@@ -31,7 +32,7 @@ module CemWinSpec
31
32
  end
32
33
 
33
34
  def cleanup_cmd
34
- 'bundle exec rake cem:win_spec_clean'
35
+ prefix 'rake cem:win_spec_clean'
35
36
  end
36
37
 
37
38
  def spec_files
@@ -40,12 +41,16 @@ module CemWinSpec
40
41
 
41
42
  private
42
43
 
44
+ def prefix(cmd)
45
+ @use_bundler ? "bundle exec #{cmd}" : cmd
46
+ end
47
+
43
48
  def rake_args(*args)
44
49
  args.empty? ? '' : "[#{args.join(',')}]"
45
50
  end
46
51
 
47
52
  def rspec_cmd(file, *args)
48
- "bundle exec rake 'cem:spec_standalone#{rake_args(file, *args)}'"
53
+ prefix "rake 'cem:spec_standalone#{rake_args(file, *args)}'"
49
54
  end
50
55
  end
51
56
  end
@@ -38,7 +38,7 @@ module CemWinSpec
38
38
 
39
39
  def enable_long_paths(**opts)
40
40
  new_command('Enable long paths', **opts) do
41
- remote_exec('Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1')
41
+ remote_run('Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1')
42
42
  end
43
43
  end
44
44
 
@@ -68,7 +68,7 @@ module CemWinSpec
68
68
  module_archive_build { |a| remote_upload(a, remote_working_dir) }
69
69
  module_dir = "#{remote_working_dir}\\#{File.basename(module_archive_path, '.tar.gz')}"
70
70
  logger.debug "Module uploaded to #{module_dir}.tar.gz, extracting..."
71
- remote_exec("tar -xzf #{module_dir}.tar.gz -C #{remote_working_dir}")
71
+ remote_run("tar -xzf #{module_dir}.tar.gz -C #{remote_working_dir}")
72
72
  logger.debug "Module extracted to #{module_dir}"
73
73
  module_dir
74
74
  end
@@ -76,33 +76,33 @@ module CemWinSpec
76
76
 
77
77
  def setup_ruby(**opts)
78
78
  @setup_ruby ||= new_command('Set up ruby', **opts) do
79
- remote_exec('bundle config disable_platform_warnings true')
80
- remote_exec('bundle install')
79
+ remote_run('bundle config disable_platform_warnings true')
80
+ remote_run('bundle install')
81
81
  end
82
82
  end
83
83
 
84
84
  def rspec_prep(**opts)
85
85
  @rspec_prep ||= new_command('Prepare rspec tests', **opts) do
86
- remote_exec('bundle exec rake cem_win_spec:prep --trace')
86
+ remote_run('bundle exec rake cem_win_spec:prep --trace')
87
87
  end
88
88
  end
89
89
 
90
90
  def rspec_tests(**opts)
91
91
  @rspec_tests ||= new_command('Run rspec tests', **opts) do
92
- remote_exec(rspec_cmd_standalone('false', 'progress', 'true'))
92
+ remote_run(rspec_cmd_standalone('false', 'progress', 'true'))
93
93
  end
94
94
  end
95
95
 
96
96
  def rspec_tests_parallel(**opts)
97
97
  @rspec_tests_parallel ||= new_command('Run rspec tests in parallel', **opts) do
98
- remote_exec('bundle exec rake cem_win_spec:parallel_spec')
98
+ remote_run('bundle exec rake cem_win_spec:parallel_spec')
99
99
  end
100
100
  end
101
101
 
102
102
  def clean_up
103
103
  @clean_up ||= new_command('Cleanup') do |working_dir|
104
104
  if remote_available?
105
- remote_exec(cleanup_cmd, quiet: true)
105
+ remote_run(cleanup_cmd, quiet: true)
106
106
  else
107
107
  logger.warn 'Cleanup not available'
108
108
  end
@@ -111,7 +111,7 @@ module CemWinSpec
111
111
 
112
112
  def clean_fixture_cache(**opts)
113
113
  @clean_cache ||= new_command('Clean fixture cache', **opts) do
114
- remote_exec('bundle exec rake cem_win_spec:clean_fixture_cache')
114
+ remote_run('bundle exec rake cem_win_spec:clean_fixture_cache')
115
115
  end
116
116
  end
117
117
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CemWinSpec
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../logging'
4
+
5
+ module CemWinSpec
6
+ module WinExec
7
+ class BaseCmd
8
+ include CemWinSpec::Logging
9
+
10
+ COMMAND_SEPARATOR = '; '
11
+ PUPPET_VER_TO_RUBY_VER = {
12
+ '7' => '278',
13
+ '8' => '322',
14
+ }.freeze
15
+
16
+ attr_accessor :working_dir, :env_vars, :puppet_version
17
+
18
+ def initialize(working_dir = nil, puppet_version: nil, **env_vars)
19
+ @working_dir = working_dir
20
+ self.puppet_version = puppet_version
21
+ self.env_vars = env_vars
22
+ end
23
+
24
+ # Sets the puppet version that will be used to execute the command
25
+ # @param value [String, nil] the puppet version
26
+ def puppet_version=(value)
27
+ if value.nil?
28
+ @puppet_version = nil
29
+ return
30
+ end
31
+
32
+ ver = value.to_s
33
+ raise ArgumentError, 'puppet_version only supports major versions 7 and 8' unless ver.match?(/^(7|8).*/)
34
+ unless ver.match?(/^(7|8)(\.\d+)?\.\d+$/)
35
+ ver = "#{ver}.0"
36
+ end
37
+
38
+ @puppet_version = ver
39
+ end
40
+
41
+ # Sets the environment variables that will be used to execute the command
42
+ # @param value [Hash] the environment variables
43
+ def env_vars=(value)
44
+ raise ArgumentError, 'env_vars must be a hash' unless value.is_a?(Hash)
45
+
46
+ value['PUPPET_GEM_VERSION'] = "~> #{puppet_version}" if puppet_version
47
+ value['FACTER_GEM_VERSION'] = 'https://github.com/puppetlabs/facter#main' if puppet_version
48
+ @env_vars = value
49
+ end
50
+
51
+ def available?
52
+ raise NotImplementedError
53
+ end
54
+
55
+ def run(cmd, *_args, **_kwargs)
56
+ raise NotImplementedError
57
+ end
58
+
59
+ # Returns the ruby version that will be used to execute the command
60
+ # The ruby version is determined by the puppet version that is set
61
+ # @return [String, nil] the ruby version or nil if the puppet version is not set
62
+ def ruby_version
63
+ return nil unless puppet_version
64
+
65
+ PUPPET_VER_TO_RUBY_VER[puppet_version.split('.')[0]]
66
+ end
67
+
68
+ def command(cmd)
69
+ cmd = [cmd]
70
+ cmd.unshift(change_ruby_version_cmd) if ruby_version # executes third
71
+ env_vars.each { |k, v| cmd.unshift(set_env_var_cmd(k, v)) } if env_vars.any? # executes second
72
+ cmd.unshift(change_working_dir_cmd(working_dir)) if working_dir # executes first
73
+ cmd.join(COMMAND_SEPARATOR)
74
+ end
75
+
76
+ private
77
+
78
+ def log_command(cmd)
79
+ cmd = command(cmd)
80
+ logger.debug "Executing command:\n#{cmd.split(%r{\n|\r\n|;\s*}).map { |c| " #> #{c}" }.join("\n")}"
81
+ end
82
+
83
+ def change_ruby_version_cmd
84
+ "uru #{ruby_version}"
85
+ end
86
+
87
+ def set_env_var_cmd(key, value)
88
+ "set #{key}=\"#{value}\""
89
+ end
90
+
91
+ def change_working_dir_cmd(dir)
92
+ "cd #{dir}"
93
+ end
94
+ end
95
+ end
96
+ end
@@ -1,18 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'open3'
4
- require_relative 'base_exec'
4
+ require_relative 'base_cmd'
5
5
 
6
6
  module CemWinSpec
7
7
  module WinExec
8
8
  # Class for executing local shell commands
9
- class LocalExec < BaseExec
9
+ class LocalCmd < BaseCmd
10
10
  attr_reader :thread_results
11
11
 
12
- def initialize(working_dir = nil)
12
+ def initialize(working_dir = nil, puppet_version: nil, **env_vars)
13
13
  @ran_in_thread = false
14
14
  @thread_results = {}
15
- super(working_dir)
15
+ super(working_dir, puppet_version: puppet_version, **env_vars)
16
16
  end
17
17
 
18
18
  def available?
@@ -35,9 +35,9 @@ module CemWinSpec
35
35
  # This is useful for running commands that should not block the current process
36
36
  # and that you don't need stdout/stderr from.
37
37
  # @param cmd [String] The command to execute
38
- def bg_exec(cmd, *_args, **_kwargs)
39
- puts_cmd(cmd)
40
- Process.detach(spawn(cd_working_dir(cmd)))
38
+ def bg_run(cmd, *_args, **_kwargs)
39
+ log_command(cmd)
40
+ Process.detach(spawn(command(cmd)))
41
41
  end
42
42
 
43
43
  # Execute a command in a new thread. This works by creating a new thread in a thread group
@@ -48,20 +48,20 @@ module CemWinSpec
48
48
  # Process::Status object.
49
49
  # @param cmd [String] The command to execute
50
50
  # @return [Array] An array of [:threaded, cmd]
51
- def thread_exec(cmd, *_args, **_kwargs)
51
+ def thread_run(cmd, *_args, **_kwargs)
52
52
  @ran_in_thread = true
53
53
  th = Thread.new do
54
- puts_cmd(cmd)
55
- so, se, st = Open3.capture3(cd_working_dir(cmd))
54
+ log_command(cmd)
55
+ so, se, st = Open3.capture3(command(cmd))
56
56
  @thread_results[cmd] = [so, se, st]
57
57
  end
58
58
  thread_group.add th
59
59
  :threaded
60
60
  end
61
61
 
62
- def exec(cmd, *_args, **_kwargs)
63
- puts_cmd(cmd)
64
- Open3.capture3(cd_working_dir(cmd))
62
+ def run(cmd, *_args, **_kwargs)
63
+ log_command(cmd)
64
+ Open3.capture3(command(cmd))
65
65
  end
66
66
 
67
67
  private
@@ -2,32 +2,32 @@
2
2
 
3
3
  require 'winrm'
4
4
  require 'winrm-fs'
5
- require_relative 'base_exec'
5
+ require_relative 'base_cmd'
6
6
 
7
7
  module CemWinSpec
8
8
  module WinExec
9
9
  # Class for executing PowerShell commands over WinRM
10
- class WinRMExec < BaseExec
10
+ class WinRMCmd < BaseCmd
11
11
  attr_reader :conn_opts
12
12
 
13
- def initialize(conn_opts, working_dir = nil, quiet: false)
13
+ def initialize(conn_opts, working_dir = nil, quiet: false, puppet_version: nil, **env_vars)
14
14
  @conn_opts = conn_opts
15
15
  @quiet = quiet
16
16
  @conn = new_conn(@conn_opts.to_h)
17
- super(working_dir)
17
+ super(working_dir, puppet_version: puppet_version, **env_vars)
18
18
  end
19
19
 
20
20
  def available?
21
21
  @available
22
22
  end
23
23
 
24
- def exec(cmd, *_args, **_kwargs)
25
- puts_cmd(cmd)
24
+ def run(cmd, *_args, **_kwargs)
25
+ log_command(cmd)
26
26
  shell = nil
27
27
  output = nil
28
28
  begin
29
29
  shell = conn.shell(:powershell)
30
- output = shell.run(cd_working_dir(cmd))
30
+ output = shell.run(command(cmd))
31
31
  rescue WinRM::WinRMAuthorizationError => e
32
32
  @available = false
33
33
  raise e
@@ -77,10 +77,19 @@ module CemWinSpec
77
77
  def new_conn(hash_opts)
78
78
  logger.debug "Creating connection to #{hash_opts[:endpoint]}"
79
79
  new_conn = WinRM::Connection.new(hash_opts)
80
- new_conn.logger = logger
80
+ new_conn.logger = new_conn_logger
81
81
  new_conn
82
82
  end
83
83
 
84
+ def new_conn_logger
85
+ conn_logger = logger.dup
86
+ conn_logger.level = Logger::ERROR if @quiet
87
+ conn_logger.level = Logger::DEBUG if ENV['WINRM_DEBUG'] == 'true'
88
+ conn_logger.level = Logger::INFO if conn_logger.level == Logger::DEBUG && ENV.fetch('WINRM_DEBUG', 'false') != 'true'
89
+ logger.debug "WinRM connection logger level set to #{conn_logger.level}"
90
+ conn_logger
91
+ end
92
+
84
93
  def file_manager
85
94
  @file_manager ||= WinRM::FS::FileManager.new(conn)
86
95
  end
@@ -49,7 +49,7 @@ module CemWinSpec
49
49
  end
50
50
 
51
51
  def pass
52
- @pass ||= Digest::SHA256.hexdigest(pt_pass) unless pt_pass.nil?
52
+ Digest::SHA256.hexdigest(pt_pass) unless pt_pass.nil?
53
53
  end
54
54
 
55
55
  def endpoint
@@ -57,7 +57,7 @@ module CemWinSpec
57
57
  end
58
58
 
59
59
  def opts
60
- @opts ||= new_opts(@kwargs)
60
+ @opts ||= create_new_opts(@kwargs)
61
61
  end
62
62
 
63
63
  def ==(other)
@@ -75,14 +75,14 @@ module CemWinSpec
75
75
  end
76
76
 
77
77
  def digest
78
- Digest::SHA256.hexdigest([host, port, user, @pass, opts.values].join(':'))
78
+ Digest::SHA256.hexdigest([host, port, user, pt_pass, opts.values].join(':'))
79
79
  end
80
80
 
81
81
  # Merge new options into existing options and return a new ConnectionOpts object
82
82
  # @param [Hash] new_opts
83
83
  # @return [ConnectionOpts]
84
84
  def merge(**new_opts)
85
- self.class.new(opts_merge(**new_opts))
85
+ self.class.new(**opts_merge(**new_opts))
86
86
  end
87
87
 
88
88
  def merge!(**new_opts)
@@ -139,7 +139,7 @@ module CemWinSpec
139
139
  password
140
140
  end
141
141
 
142
- def new_opts(nopts = {})
142
+ def create_new_opts(nopts = {})
143
143
  logger.debug 'Creating new connection options opts'
144
144
  new_opts_h = CONN_DEFAULTS.dup.merge(nopts)
145
145
  new_opts_h[:transport] = new_opts_h[:transport].to_sym
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../logging'
4
+ require_relative 'output'
5
+
6
+ module CemWinSpec
7
+ module WinExec
8
+ # Class for executing PowerShell commands over WinRM
9
+ class Exec
10
+ include CemWinSpec::Logging
11
+
12
+ HALTING_ERRORS = [
13
+ IapTunnelStartError,
14
+ ].freeze
15
+
16
+ attr_reader :title, :result, :reuse_tunnel, :ignore_exitcode
17
+
18
+ def initialize(title: 'Command', l_cmd: nil, r_cmd: nil, iap_tunnel: nil, ma_builder: nil, r_test_cmds: nil, &block)
19
+ add_title(title)
20
+ add_local_cmd(l_cmd) unless l_cmd.nil?
21
+ add_remote_cmd(r_cmd) unless r_cmd.nil?
22
+ add_iap_tunnel(iap_tunnel) unless iap_tunnel.nil?
23
+ add_ma_builder(ma_builder) unless ma_builder.nil?
24
+ add_rspec_test_cmds(r_test_cmds) unless r_test_cmds.nil?
25
+ add_command_block(&block) unless block.nil?
26
+ @reuse_tunnel = true
27
+ @ignore_exitcode = false
28
+ @spinner = nil
29
+ @result = nil
30
+ end
31
+
32
+ def add_title(value)
33
+ raise ArgumentError, 'title must be a string' unless value.is_a?(String)
34
+
35
+ @title = value
36
+ end
37
+
38
+ def add_local_cmd(value)
39
+ raise ArgumentError, 'local_exec must implement the #run method' unless value.respond_to?(:run)
40
+
41
+ @local_cmd = value
42
+ end
43
+
44
+ def add_remote_cmd(value)
45
+ raise ArgumentError, 'winrm_exec must implement the #run method' unless value.respond_to?(:run)
46
+
47
+ @remote_cmd = value
48
+ end
49
+
50
+ def add_iap_tunnel(value)
51
+ unless value.respond_to?(:start) && value.respond_to?(:stop) && value.respond_to?(:with)
52
+ raise ArgumentError, 'iap_tunnel must implement the #start, #stop, and #with methods'
53
+ end
54
+
55
+ @iap_tunnel = value
56
+ end
57
+
58
+ def add_ma_builder(value)
59
+ raise ArgumentError, 'ma_builder must implement the #build method' unless value.respond_to?(:build)
60
+
61
+ @ma_builder = value
62
+ end
63
+
64
+ def add_rspec_test_cmds(value)
65
+ unless value.respond_to?(:cmd_standalone) && value.respond_to?(:cmd_parallel) &&
66
+ value.respond_to?(:prep_cmd) && value.respond_to?(:cleanup_cmd)
67
+ raise ArgumentError, 'rspec_test_cmds must implement the #cmd_standalone, #cmd_parallel, #prep_cmd, and #cleanup_cmd methods'
68
+ end
69
+
70
+ @rspec_test_cmds = value
71
+ end
72
+
73
+ def add_command_block(&block)
74
+ raise ArgumentError, 'block must be a Proc' unless block.is_a?(Proc)
75
+
76
+ @block = block
77
+ end
78
+
79
+ def reuse_tunnel=(value)
80
+ raise ArgumentError, 'reuse_tunnel must be a boolean' unless [true, false].include?(value)
81
+
82
+ @reuse_tunnel = value
83
+ end
84
+ alias reuse_tunnel? reuse_tunnel
85
+
86
+ def ignore_exitcode=(value)
87
+ raise ArgumentError, 'ignore_exitcode must be a boolean' unless [true, false].include?(value)
88
+
89
+ @ignore_exitcode = value
90
+ end
91
+ alias ignore_exitcode? ignore_exitcode
92
+
93
+ def success?
94
+ @result.success? if @result.is_a?(Output)
95
+
96
+ false
97
+ end
98
+
99
+ def spinner=(value)
100
+ raise ArgumentError, 'spinner must implement #auto_spin and #stop methods' unless value.respond_to?(:auto_spin) && value.respond_to?(:stop)
101
+
102
+ @spinner = value
103
+ end
104
+
105
+ # Proxy method calls for methods that don't exist here to various other objects.
106
+ # This allows doing things like: `win_exec.remote_exec('Get-Process')` and
107
+ # calling the `exec` method on the `winrm_exec` object. This is done by
108
+ # checking method name prefixes and calling the corresponding method on the
109
+ # appropriate object. The prefix is removed from the method name before
110
+ # calling the method on the object. The supported prefixes are:
111
+ # local_ - calls the method on the @local_cmd object
112
+ # remote_ - calls the method on the @remote_cmd object
113
+ # rspec_ - calls the method on the @rspec_test_cmds object
114
+ # module_archive_ - calls the method on the @ma_builder object
115
+ def method_missing(method, *args, **kwargs, &block)
116
+ if method.to_s.start_with?('local_') # proxy to local_exec
117
+ method = method.to_s.sub('local_', '').to_sym
118
+ @local_cmd.send(method, *args, **kwargs, &block)
119
+ elsif method.to_s.start_with?('remote_') # proxy to remote_exec
120
+ method = method.to_s.sub('remote_', '').to_sym
121
+ @remote_cmd.send(method, *args, **kwargs, &block)
122
+ elsif method.to_s.start_with?('rspec_') # proxy to rspec_test_cmds
123
+ method = method.to_s.sub('rspec_', '').to_sym
124
+ @rspec_test_cmds.send(method, *args, **kwargs, &block)
125
+ elsif method.to_s.start_with?('module_archive_') # proxy to ma_builder
126
+ method = method.to_s.sub('module_archive_', '').to_sym
127
+ @ma_builder.send(method, *args, **kwargs, &block)
128
+ else
129
+ super
130
+ end
131
+ end
132
+
133
+ # Proxy respond_to? for methods that don't exist here to various other objects.
134
+ # This allows doing things like: `win_exec.respond_to?(:remote_exec)` and
135
+ # checking if the `exec` method exists on the `winrm_exec` object. This is done by
136
+ # checking method name prefixes and calling the corresponding method on the
137
+ # appropriate object. The prefix is removed from the method name before
138
+ # calling the method on the object. The supported prefixes are:
139
+ # local_ - calls the method on the @local_cmd object
140
+ # remote_ - calls the method on the @remote_cmd object
141
+ # rspec_ - calls the method on the @rspec_test_cmds object
142
+ # module_archive_ - calls the method on the @ma_builder object
143
+ def respond_to_missing?(method, include_private = false)
144
+ if method.to_s.start_with?('local_')
145
+ @local_cmd.respond_to?(method.to_s.sub('local_', '').to_sym, include_private)
146
+ elsif method.to_s.start_with?('remote_')
147
+ @remote_cmd.respond_to?(method.to_s.sub('remote_', '').to_sym, include_private)
148
+ elsif method.to_s.start_with?('rspec_')
149
+ @rspec_test_cmds.respond_to?(method.to_s.sub('rspec_', '').to_sym, include_private)
150
+ elsif method.to_s.start_with?('module_archive_')
151
+ @ma_builder.respond_to?(method.to_s.sub('module_archive_', '').to_sym, include_private)
152
+ else
153
+ super
154
+ end
155
+ end
156
+
157
+ def run(*args, **kwargs)
158
+ validate_instance_variables
159
+ logger.info "Running #{@title}"
160
+ @spinner&.auto_spin
161
+ result = run_with_tunnel { run_in_current_scope(*args, **kwargs) }
162
+ @spinner&.stop
163
+ @result = Output.new(result)
164
+ return if @result.pending_threaded?
165
+
166
+ @result.puts_combined
167
+ unless @result.success? || ignore_exitcode?
168
+ raise "Command failed with exit code #{@result.exitcode}: #{@result.stdout}\n#{@result.stderr}"
169
+ end
170
+ @result
171
+ rescue StandardError => e
172
+ raise if HALTING_ERRORS.include?(e.class)
173
+
174
+ logger.error "Error running #{@title}: #{e.message[0..100]}"
175
+ @result = Output.new(e)
176
+ @result
177
+ end
178
+
179
+ private
180
+
181
+ def run_with_tunnel(&block)
182
+ if reuse_tunnel?
183
+ @iap_tunnel.start # ensure tunnel is running
184
+ block.call
185
+ else
186
+ @iap_tunnel.stop # ensure tunnel is stopped
187
+ @iap_tunnel.with do # start tunnel for this block
188
+ block.call
189
+ end
190
+ end
191
+ end
192
+
193
+ def validate_instance_variables
194
+ %i[@title @local_cmd @remote_cmd @iap_tunnel @ma_builder @rspec_test_cmds @block].each do |var|
195
+ raise ArgumentError, "#{var} must be set" if instance_variable_get(var).nil?
196
+ end
197
+ end
198
+
199
+ # Runs the block in the current scope. This allows the block to access
200
+ # instance variables and methods from the current scope.
201
+ def run_in_current_scope(*args, **kwargs)
202
+ instance_exec(*args, **kwargs, &@block) # self is the "instance"
203
+ end
204
+
205
+ def wrap_spinner
206
+ @spinner.auto_spin if @spinner
207
+ yield if block_given?
208
+ ensure
209
+ @spinner.stop if @spinner
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require_relative '../logging'
5
+ require_relative 'connection_opts'
6
+ require_relative 'cmd/local_cmd'
7
+ require_relative 'cmd/winrm_cmd'
8
+
9
+ module CemWinSpec
10
+ module WinExec
11
+ # Factory class for creating a WinExec object
12
+ class Factory
13
+ include CemWinSpec::Logging
14
+
15
+ attr_reader :current_local_exec, :current_remote_cmd, :current_conn_opts
16
+
17
+ def initialize(iap_tunnel, ma_builder, rspec_test_cmds, **opts)
18
+ @iap_tunnel = iap_tunnel
19
+ @ma_builder = ma_builder
20
+ @rspec_test_cmds = rspec_test_cmds
21
+ @current_local_cmd = LocalCmd.new(**opts)
22
+ @current_remote_cmd = nil
23
+ @current_conn_opts = nil
24
+ @init_opts = opts
25
+ end
26
+
27
+ # Build a WinExec object
28
+ # @param title [String] Title of the WinExec object
29
+ # @param merge [Boolean] Merge the current connection options with the new options, if applicable
30
+ # @param host [String] Hostname or IP address of the remote host
31
+ # @param port [Integer] Port of the remote host
32
+ # @param user [String] Username for the remote host
33
+ # @param pass [String] Password for the remote host
34
+ # @param opts [Hash] Additional options for the WinRM connection
35
+ # @return [Exec] An Exec object
36
+ def build(title, merge: true, user: nil, pass: nil, working_dir: nil, **opts, &block)
37
+ logger.debug "Building Wexec object for #{title}"
38
+ build_conn_opts(merge: merge, user: user, pass: pass, **opts)
39
+ logger.debug 'Created ConnectionOpts'
40
+ wexec = Exec.new
41
+ wexec.add_title title
42
+ wexec.add_local_cmd @current_local_cmd
43
+ wexec.add_remote_cmd get_remote_cmd(working_dir, **@init_opts.merge(opts))
44
+ wexec.add_iap_tunnel @iap_tunnel
45
+ wexec.add_ma_builder @ma_builder
46
+ wexec.add_rspec_test_cmds @rspec_test_cmds
47
+ wexec.add_command_block(&block)
48
+ wexec.reuse_tunnel = opts[:reuse_tunnel] if opts.key?(:reuse_tunnel)
49
+ wexec.ignore_exitcode = opts[:ignore_exitcode] if opts.key?(:ignore_exitcode)
50
+ wexec.spinner = opts[:spinner] if opts.key?(:spinner)
51
+ logger.debug 'Created Wexec'
52
+ wexec
53
+ end
54
+
55
+ def local_threaded_results
56
+ logger.info 'Checking for deferred results...'
57
+ @current_local_exec.join_threads
58
+ @current_local_exec.threaded_results.each do |cmd, results|
59
+ logger.info "Deferred results for #{cmd}:"
60
+ Output.new(results).puts_combined
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def build_conn_opts(merge: true, user: nil, pass: nil, **opts)
67
+ if @current_conn_opts.nil?
68
+ logger.debug 'Creating new ConnectionOpts object'
69
+ @current_conn_opts = ConnectionOpts.new(new_host: 'localhost', new_port: @iap_tunnel.port, user: user, pass: pass, **opts)
70
+ return @current_conn_opts
71
+ end
72
+
73
+ if need_new_conn_opts?(new_host: 'localhost', new_port: @iap_tunnel.port, user: user, pass: pass, **opts)
74
+ if merge
75
+ logger.debug 'Merging ConnectionOpts with new options'
76
+ @current_conn_opts = @current_conn_opts.merge(new_host: 'localhost', new_port: @iap_tunnel.port, user: user, pass: pass, **opts)
77
+ else
78
+ logger.debug 'Creating new ConnectionOpts object with new options'
79
+ @current_conn_opts = ConnectionOpts.new(new_host: 'localhost', new_port: @iap_tunnel.port, user: user, pass: pass, **opts)
80
+ end
81
+ else
82
+ logger.debug 'Returning existing ConnectionOpts object'
83
+ end
84
+ @current_conn_opts
85
+ end
86
+
87
+ def need_new_conn_opts?(new_host: nil, new_port: nil, user: nil, pass: nil, **opts)
88
+ if new_host && new_host != @current_conn_opts.host
89
+ logger.debug "New host #{new_host} does not match current host #{@current_conn_opts.host}"
90
+ return true
91
+ end
92
+ if new_port && new_port != @current_conn_opts.port
93
+ logger.debug "New port #{new_port} does not match current port #{@current_conn_opts.port}"
94
+ return true
95
+ end
96
+ if user && user != @current_conn_opts.user
97
+ logger.debug "New user #{user} does not match current user #{@current_conn_opts.user}"
98
+ return true
99
+ end
100
+ if pass && Digest::SHA256.hexdigest(pass) != @current_conn_opts.pass
101
+ logger.debug 'New password does not match current password'
102
+ return true
103
+ end
104
+ if !opts.empty? && opts != @current_conn_opts.opts
105
+ logger.debug "New extra options #{opts} do not match current extra options #{@current_conn_opts.opts}"
106
+ return true
107
+ end
108
+
109
+ false
110
+ end
111
+
112
+ def get_remote_cmd(working_dir = nil, **opts)
113
+ if @current_winrm_cmd.nil? || @current_winrm_cmd.conn_opts.digest != @current_conn_opts.digest
114
+ logger.debug 'Creating new WinRMExec object'
115
+ @current_remote_cmd = WinRMCmd.new(@current_conn_opts, **opts)
116
+ end
117
+ logger.debug "Setting working directory to #{working_dir}" unless working_dir.nil?
118
+ @current_remote_cmd.working_dir = working_dir
119
+ logger.debug 'Returning WinRMExec object'
120
+ @current_remote_cmd
121
+ end
122
+ end
123
+ end
124
+ end
@@ -1,234 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'digest'
4
- require_relative 'iap_tunnel'
5
- require_relative 'logging'
6
- require_relative 'module_archive_builder'
7
- require_relative 'win_exec/connection_opts'
8
- require_relative 'win_exec/local_exec'
9
- require_relative 'win_exec/winrm_exec'
10
- require_relative 'win_exec/output'
11
-
12
3
  module CemWinSpec
13
4
  module WinExec
14
- # Class for executing PowerShell commands over WinRM
15
- class Exec
16
- include CemWinSpec::Logging
17
-
18
- attr_reader :title, :result, :reuse_tunnel, :ignore_exitcode
19
-
20
- def initialize(title, local_exec, winrm_exec, iap_tunnel, ma_builder, rspec_test_cmds, &block)
21
- @title = title
22
- @local_exec = local_exec
23
- @winrm_exec = winrm_exec
24
- @iap_tunnel = iap_tunnel
25
- @ma_builder = ma_builder
26
- @rspec_test_cmds = rspec_test_cmds
27
- @block = block
28
- @reuse_tunnel = true
29
- @ignore_exitcode = false
30
- @spinner = nil
31
- @result = nil
32
- end
33
-
34
- def reuse_tunnel=(value)
35
- raise ArgumentError, 'reuse_tunnel must be a boolean' unless [true, false].include?(value)
36
-
37
- @reuse_tunnel = value
38
- end
39
- alias reuse_tunnel? reuse_tunnel
40
-
41
- def ignore_exitcode=(value)
42
- raise ArgumentError, 'ignore_exitcode must be a boolean' unless [true, false].include?(value)
43
-
44
- @ignore_exitcode = value
45
- end
46
- alias ignore_exitcode? ignore_exitcode
47
-
48
- def success?
49
- @result.success? if @result.is_a?(Output)
50
-
51
- false
52
- end
53
-
54
- def spinner=(value)
55
- raise ArgumentError, 'spinner must implement #auto_spin and #stop methods' unless value.respond_to?(:auto_spin) && value.respond_to?(:stop)
56
-
57
- @spinner = value
58
- end
59
-
60
- # Proxy method calls for methods that don't exist here to various other objects.
61
- # This allows doing things like: `win_exec.remote_exec('Get-Process')` and
62
- # calling the `exec` method on the `winrm_exec` object. This is done by
63
- # checking method name prefixes and calling the corresponding method on the
64
- # appropriate object. The prefix is removed from the method name before
65
- # calling the method on the object. The supported prefixes are:
66
- # local_ - calls the method on the @local_exec object
67
- # remote_ - calls the method on the @winrm_exec object
68
- # rspec_ - calls the method on the @rspec_test_cmds object
69
- # module_archive_ - calls the method on the @ma_builder object
70
- def method_missing(method, *args, **kwargs, &block)
71
- if method.to_s.start_with?('local_') # proxy to local_exec
72
- method = method.to_s.sub('local_', '').to_sym
73
- @local_exec.send(method, *args, **kwargs, &block)
74
- elsif method.to_s.start_with?('remote_') # proxy to winrm_exec
75
- method = method.to_s.sub('remote_', '').to_sym
76
- @winrm_exec.send(method, *args, **kwargs, &block)
77
- elsif method.to_s.start_with?('rspec_') # proxy to rspec_test_cmds
78
- method = method.to_s.sub('rspec_', '').to_sym
79
- @rspec_test_cmds.send(method, *args, **kwargs, &block)
80
- elsif method.to_s.start_with?('module_archive_') # proxy to ma_builder
81
- method = method.to_s.sub('module_archive_', '').to_sym
82
- @ma_builder.send(method, *args, **kwargs, &block)
83
- else
84
- super
85
- end
86
- end
87
-
88
- # Proxy respond_to? for methods that don't exist here to various other objects.
89
- # This allows doing things like: `win_exec.respond_to?(:remote_exec)` and
90
- # checking if the `exec` method exists on the `winrm_exec` object. This is done by
91
- # checking method name prefixes and calling the corresponding method on the
92
- # appropriate object. The prefix is removed from the method name before
93
- # calling the method on the object. The supported prefixes are:
94
- # local_ - calls the method on the @local_exec object
95
- # remote_ - calls the method on the @winrm_exec object
96
- # rspec_ - calls the method on the @rspec_test_cmds object
97
- # module_archive_ - calls the method on the @ma_builder object
98
- def respond_to_missing?(method, include_private = false)
99
- if method.to_s.start_with?('local_')
100
- @local_exec.respond_to?(method.to_s.sub('local_', '').to_sym, include_private)
101
- elsif method.to_s.start_with?('remote_')
102
- @winrm_exec.respond_to?(method.to_s.sub('remote_', '').to_sym, include_private)
103
- elsif method.to_s.start_with?('rspec_')
104
- @rspec_test_cmds.respond_to?(method.to_s.sub('rspec_', '').to_sym, include_private)
105
- elsif method.to_s.start_with?('module_archive_')
106
- @ma_builder.respond_to?(method.to_s.sub('module_archive_', '').to_sym, include_private)
107
- else
108
- super
109
- end
110
- end
111
-
112
- def run(*args, **kwargs)
113
- logger.info "Running #{@title}"
114
- @spinner&.auto_spin
115
- result = if reuse_tunnel?
116
- @iap_tunnel.start # ensure tunnel is running
117
- run_in_current_scope(*args, **kwargs)
118
- else
119
- @iap_tunnel.stop # ensure tunnel is stopped
120
- @iap_tunnel.with do # start tunnel for this block
121
- run_in_current_scope(*args, **kwargs)
122
- end
123
- end
124
- @spinner&.stop
125
- @result = Output.new(result)
126
- return if @result.pending_threaded?
127
-
128
- @result.puts_combined
129
- unless @result.success? || ignore_exitcode?
130
- raise "Command failed with exit code #{@result.exitcode}: #{@result.stdout}\n#{@result.stderr}"
131
- end
132
- @result
133
- rescue StandardError => e
134
- logger.error "Error running #{@title}: #{e.message[0..100]}"
135
- @result = Output.new(e)
136
- @result
137
- end
138
-
139
- private
140
-
141
- def run_in_current_scope(*args, **kwargs)
142
- instance_exec(*args, **kwargs, &@block)
143
- end
144
-
145
- def wrap_spinner
146
- @spinner.auto_spin if @spinner
147
- yield if block_given?
148
- ensure
149
- @spinner.stop if @spinner
150
- end
151
- end
152
-
153
- # Factory class for creating a WinExec object
154
- class Factory
155
- include CemWinSpec::Logging
156
-
157
- attr_reader :current_local_exec, :current_winrm_exec, :current_conn_opts
158
-
159
- def initialize(iap_tunnel, ma_builder, rspec_test_cmds)
160
- @iap_tunnel = iap_tunnel
161
- @ma_builder = ma_builder
162
- @rspec_test_cmds = rspec_test_cmds
163
- @current_local_exec = LocalExec.new
164
- @current_winrm_exec = nil
165
- @current_conn_opts = nil
166
- end
167
-
168
- # Build a WinExec object
169
- # @param title [String] Title of the WinExec object
170
- # @param merge [Boolean] Merge the current connection options with the new options, if applicable
171
- # @param host [String] Hostname or IP address of the remote host
172
- # @param port [Integer] Port of the remote host
173
- # @param user [String] Username for the remote host
174
- # @param pass [String] Password for the remote host
175
- # @param opts [Hash] Additional options for the WinRM connection
176
- # @return [Exec] An Exec object
177
- def build(title, merge: true, user: nil, pass: nil, working_dir: nil, **opts, &block)
178
- logger.debug "Building Wexec object for #{title}"
179
- build_conn_opts(merge: merge, user: user, pass: pass, **opts)
180
- logger.debug 'Created ConnectionOpts'
181
- wexec = Exec.new(title, @current_local_exec, get_winrm_exec(working_dir), @iap_tunnel, @ma_builder, @rspec_test_cmds, &block)
182
- wexec.reuse_tunnel = opts[:reuse_tunnel] if opts.key?(:reuse_tunnel)
183
- wexec.ignore_exitcode = opts[:ignore_exitcode] if opts.key?(:ignore_exitcode)
184
- wexec.spinner = opts[:spinner] if opts.key?(:spinner)
185
- logger.debug 'Created Wexec'
186
- wexec
187
- end
188
-
189
- def local_threaded_results
190
- logger.info 'Checking for deferred results...'
191
- @current_local_exec.join_threads
192
- @current_local_exec.threaded_results.each do |cmd, results|
193
- logger.info "Deferred results for #{cmd}:"
194
- Output.new(results).puts_combined
195
- end
196
- end
197
-
198
- private
199
-
200
- def build_conn_opts(merge: true, user: nil, pass: nil, **opts)
201
- if @current_conn_opts.nil?
202
- logger.debug 'Creating new ConnectionOpts object'
203
- @current_conn_opts = ConnectionOpts.new(new_host: 'localhost', new_port: @iap_tunnel.port, user: user, pass: pass, **opts)
204
- return @current_conn_opts
205
- end
206
- opts_digest = Digest::SHA256.hexdigest(['localhost', @iap_tunnel.port, user, pass, opts].join(':'))
207
- if opts_digest != @current_conn_opts.digest
208
- if merge
209
- logger.debug 'Merging ConnectionOpts with new options'
210
- @current_conn_opts = @current_conn_opts.merge(new_host: 'localhost', new_port: @iap_tunnel.port, user: user, pass: pass, **opts)
211
- else
212
- logger.debug 'Creating new ConnectionOpts object with new options'
213
- @current_conn_opts = ConnectionOpts.new(new_host: 'localhost', new_port: @iap_tunnel.port, user: user, pass: pass, **opts)
214
- end
215
- else
216
- logger.debug 'Returning existing ConnectionOpts object'
217
- end
218
- @current_conn_opts
219
- end
220
-
221
-
222
- def get_winrm_exec(working_dir = nil)
223
- if @current_winrm_exec.nil? || @current_winrm_exec.conn_opts.digest != @current_conn_opts.digest
224
- logger.debug 'Creating new WinRMExec object'
225
- @current_winrm_exec = WinRMExec.new(@current_conn_opts)
226
- end
227
- logger.debug "Setting working directory to #{working_dir}" unless working_dir.nil?
228
- @current_winrm_exec.working_dir = working_dir
229
- logger.debug 'Returning WinRMExec object'
230
- @current_winrm_exec
231
- end
232
- end
5
+ require_relative 'win_exec/exec'
6
+ require_relative 'win_exec/factory'
233
7
  end
234
8
  end
data/lib/cem_win_spec.rb CHANGED
@@ -45,14 +45,14 @@ module CemWinSpec
45
45
  check_output!(sre_out, runner)
46
46
  module_dir = sre_out.stdout.chomp
47
47
  logger.debug "Module dir: #{module_dir}"
48
- srr_out = setup_remote_ruby(runner, module_dir, spinner)
48
+ srr_out = setup_remote_ruby(runner, module_dir, spinner, **options)
49
49
  check_output!(srr_out, runner)
50
50
  case operation
51
51
  when :spec
52
- spec_out = run_spec(runner, module_dir, spinner)
52
+ spec_out = run_spec(runner, module_dir, spinner, **options)
53
53
  check_output!(spec_out, runner)
54
54
  when :clean_fixture_cache
55
- clean_fixture_cache_out = clean_fixture_cache(runner, module_dir, spinner)
55
+ clean_fixture_cache_out = clean_fixture_cache(runner, module_dir, spinner, **options)
56
56
  check_output!(clean_fixture_cache_out, runner)
57
57
  else
58
58
  raise ArgumentError, "Unknown operation: #{operation}"
@@ -71,32 +71,34 @@ module CemWinSpec
71
71
  working_dir_out = runner.create_working_dir.run
72
72
  working_dir = working_dir_out.stdout.chomp
73
73
  logger.debug "Working dir: #{working_dir}"
74
- runner.upload_module(working_dir: working_dir).run
74
+ runner.upload_module(operation_timeout: 300, receive_timeout: 310, working_dir: working_dir).run
75
75
  end
76
76
 
77
- def self.setup_remote_ruby(runner, module_dir, spinner)
77
+ def self.setup_remote_ruby(runner, module_dir, spinner, **opts)
78
78
  runner.setup_ruby(operation_timeout: 300,
79
79
  receive_timeout: 310,
80
80
  working_dir: module_dir,
81
81
  reuse_tunnel: false,
82
- spinner: spinner).run
82
+ spinner: spinner,
83
+ **opts).run
83
84
  end
84
85
 
85
86
  # Runs RSpec tests
86
- def self.run_spec(runner, module_dir, spinner)
87
+ def self.run_spec(runner, module_dir, spinner, **opts)
87
88
  #runner.rspec_prep(working_dir: module_dir, reuse_tunnel: false, spinner: spinner).run
88
89
  runner.rspec_tests_parallel(operation_timeout: 300,
89
90
  receive_timeout: 310,
90
91
  working_dir: module_dir,
91
92
  ignore_exitcode: true,
92
93
  reuse_tunnel: false,
93
- spinner: spinner).run
94
+ spinner: spinner,
95
+ **opts).run
94
96
  end
95
97
 
96
98
  # Clean the remote fixture cache
97
99
  # @param options [Hash] Options for the test runner
98
- def self.clean_fixture_cache(runner, module_dir, spinner)
99
- runner.clean_fixture_cache(working_dir: module_dir, spinner: spinner).run
100
+ def self.clean_fixture_cache(runner, module_dir, spinner, **opts)
101
+ runner.clean_fixture_cache(working_dir: module_dir, spinner: spinner, **opts).run
100
102
  end
101
103
 
102
104
  def self.check_output!(output, runner = nil)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cem_win_spec
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Heston Snodgrass
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-03-13 00:00:00.000000000 Z
11
+ date: 2023-07-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: winrm
@@ -119,16 +119,17 @@ files:
119
119
  - lib/cem_win_spec/logging/formatter.rb
120
120
  - lib/cem_win_spec/module_archive_builder.rb
121
121
  - lib/cem_win_spec/rake_tasks.rb
122
- - lib/cem_win_spec/remote_command.rb
123
122
  - lib/cem_win_spec/rspec_test_cmds.rb
124
123
  - lib/cem_win_spec/test_runner.rb
125
124
  - lib/cem_win_spec/version.rb
126
125
  - lib/cem_win_spec/win_exec.rb
127
- - lib/cem_win_spec/win_exec/base_exec.rb
126
+ - lib/cem_win_spec/win_exec/cmd/base_cmd.rb
127
+ - lib/cem_win_spec/win_exec/cmd/local_cmd.rb
128
+ - lib/cem_win_spec/win_exec/cmd/winrm_cmd.rb
128
129
  - lib/cem_win_spec/win_exec/connection_opts.rb
129
- - lib/cem_win_spec/win_exec/local_exec.rb
130
+ - lib/cem_win_spec/win_exec/exec.rb
131
+ - lib/cem_win_spec/win_exec/factory.rb
130
132
  - lib/cem_win_spec/win_exec/output.rb
131
- - lib/cem_win_spec/win_exec/winrm_exec.rb
132
133
  - sig/cem_win_spec.rbs
133
134
  homepage: https://github.com/hsnodgrass/cem_win_spec
134
135
  licenses:
@@ -152,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
152
153
  - !ruby/object:Gem::Version
153
154
  version: '0'
154
155
  requirements: []
155
- rubygems_version: 3.1.4
156
+ rubygems_version: 3.4.6
156
157
  signing_key:
157
158
  specification_version: 4
158
159
  summary: Write a short summary, because RubyGems requires one.
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'iap_tunnel'
4
- require_relative 'win_exec'
5
-
6
- module CemWinSpec
7
- # Class for running a command on a remote Windows host
8
- class RemoteCommand
9
- attr_reader :title, :result
10
-
11
- def initialize(title, iap_tunnel: nil, reuse_tunnel: true, winrm_opts: {}, &block)
12
- @title = title
13
- @iap_tunnel = iap_tunnel || IapTunnel.new
14
- @reuse_tunnel = reuse_tunnel
15
- @winrm_opts = winrm_opts
16
- @block = block
17
- @win_exec = WinExec.new('localhost', @iap_tunnel.port, winrm_opts: winrm_opts)
18
- @result = nil
19
- end
20
-
21
- def ran?
22
- !@result.nil?
23
- end
24
-
25
- def success?
26
- @result.is_a? WinRM::Output
27
- end
28
-
29
- def run
30
- puts "Running #{@title}"
31
- @result = if @reuse_tunnel
32
- @iap_tunnel.start # ensure tunnel is running
33
- @block.call @win_exec
34
- else
35
- @iap_tunnel.stop # ensure tunnel is stopped
36
- @iap_tunnel.with do # start tunnel for this block
37
- @block.call @win_exec
38
- end
39
- end
40
- @result
41
- rescue StandardError => e
42
- puts "Error running #{@title}: #{e}"
43
- @result = e
44
- raise @result
45
- end
46
- end
47
- end
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../logging'
4
-
5
- module CemWinSpec
6
- module WinExec
7
- class BaseExec
8
- include CemWinSpec::Logging
9
-
10
- attr_accessor :working_dir
11
-
12
- def initialize(working_dir = nil)
13
- @working_dir = working_dir
14
- end
15
-
16
- def available?
17
- raise NotImplementedError
18
- end
19
-
20
- def exec(cmd, *_args, **_kwargs)
21
- raise NotImplementedError
22
- end
23
-
24
- private
25
-
26
- def cd_working_dir(cmd)
27
- return cmd if working_dir.nil?
28
-
29
- "cd #{working_dir}; #{cmd}"
30
- end
31
-
32
- def cmd_prefix_msg
33
- prefix = 'Executing command'
34
- working_dir.nil? ? "#{prefix}:\n" : "#{prefix} in #{working_dir}:\n"
35
- end
36
-
37
- def puts_cmd(cmd)
38
- logger.debug "#{cmd_prefix_msg}#{cmd.split(%r{\n|\r\n|;\s*}).map { |c| " #> #{c}" }.join("\n")}"
39
- end
40
- end
41
- end
42
- end