test-kitchen 4.1.2 → 4.1.4
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 +4 -4
- data/Gemfile +3 -10
- data/Rakefile +45 -0
- data/lib/kitchen/base64_stream.rb +2 -2
- data/lib/kitchen/command/doctor.rb +4 -2
- data/lib/kitchen/command.rb +9 -7
- data/lib/kitchen/configurable.rb +1 -1
- data/lib/kitchen/data_munger.rb +36 -5
- data/lib/kitchen/errors.rb +1 -1
- data/lib/kitchen/generator/init.rb +0 -1
- data/lib/kitchen/instance.rb +9 -4
- data/lib/kitchen/lazy_hash.rb +35 -0
- data/lib/kitchen/logger.rb +65 -24
- data/lib/kitchen/platform_filter.rb +4 -2
- data/lib/kitchen/provisioner/base.rb +5 -1
- data/lib/kitchen/provisioner/shell.rb +1 -1
- data/lib/kitchen/shell_out.rb +2 -0
- data/lib/kitchen/transport/exec.rb +3 -3
- data/lib/kitchen/transport/winrm.rb +1 -1
- data/lib/kitchen/util.rb +52 -7
- data/lib/kitchen/verifier/base.rb +5 -1
- data/lib/kitchen/verifier/busser.rb +2 -2
- data/lib/kitchen/verifier/shell.rb +33 -12
- data/lib/kitchen/version.rb +1 -1
- data/lib/kitchen.rb +16 -4
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c56fc990001b04aa4573652682c08b02ea00508480f76beb1db8ed72e8ade07f
|
|
4
|
+
data.tar.gz: 575f723e4cd3105fdd6e3ec4961f3bcfaa08ab8d2d38bc434dab095c3e9f193c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f629caaf011634872054a8e6fae1643758a50a97c98d28fd4967181d4a5e6f68719965894cdb007d571e9a71ae45eb41cf1de8d9639d917b2b9cab99540d5c3e
|
|
7
|
+
data.tar.gz: e10c5f823385e29b6d3c08fe9f7059d160e815ef02ebca55e81e5dd526bcbb081727b6f3aacdc4f46bcb0b0928f562014fcc0475a4c3fe95c88e6d4e67d898ab
|
data/Gemfile
CHANGED
|
@@ -12,19 +12,12 @@ group :test do
|
|
|
12
12
|
gem "maruku", "~> 0.7"
|
|
13
13
|
gem "minitest", "~> 6.0", "< 6.1"
|
|
14
14
|
gem "mocha", "~> 3.0"
|
|
15
|
+
gem "simplecov", "~> 0.22", require: false
|
|
15
16
|
gem "yard", "~> 0.9"
|
|
16
17
|
end
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
gem "cinc-auditor-bin", source: "https://rubygems.cinc.sh"
|
|
20
|
-
gem "kitchen-cinc"
|
|
21
|
-
gem "kitchen-cinc-auditor",
|
|
22
|
-
git: "https://github.com/test-kitchen/kitchen-cinc-auditor.git",
|
|
23
|
-
ref: "3d0b89eaa13f12da08a8761970e39c0f564c24c6"
|
|
24
|
-
gem "kitchen-dokken"
|
|
25
|
-
gem "kitchen-vagrant"
|
|
26
|
-
end
|
|
19
|
+
# Acceptance-suite gems live in Gemfile.integration.
|
|
27
20
|
|
|
28
21
|
group :linting do
|
|
29
|
-
gem "cookstyle", "~>
|
|
22
|
+
gem "cookstyle", "~> 9.0"
|
|
30
23
|
end
|
data/Rakefile
CHANGED
|
@@ -7,6 +7,48 @@ Rake::TestTask.new(:unit) do |t|
|
|
|
7
7
|
t.verbose = true
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
+
namespace :unit do
|
|
11
|
+
desc "Run each spec file in its own process to catch cross-file coupling"
|
|
12
|
+
task :isolated do
|
|
13
|
+
require "open3"
|
|
14
|
+
|
|
15
|
+
specs = FileList["spec/**/*_spec.rb"].sort
|
|
16
|
+
# Spec files are independent, and each one spends most of its time in
|
|
17
|
+
# process startup, so run a few at a time.
|
|
18
|
+
workers = (ENV["ISOLATED_WORKERS"] || 4).to_i
|
|
19
|
+
failed = []
|
|
20
|
+
|
|
21
|
+
specs.each_slice(workers) do |batch|
|
|
22
|
+
batch.map do |spec|
|
|
23
|
+
Thread.new do
|
|
24
|
+
out, status = Open3.capture2e(
|
|
25
|
+
Gem.ruby, "-Ilib", "-Ispec", spec
|
|
26
|
+
)
|
|
27
|
+
[spec, status.success?, out]
|
|
28
|
+
end
|
|
29
|
+
end.map(&:value).each do |spec, ok, out|
|
|
30
|
+
puts(ok ? " ok #{spec}" : " FAIL #{spec}\n#{out}")
|
|
31
|
+
failed << spec unless ok
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
unless failed.empty?
|
|
36
|
+
abort "\n#{failed.length} of #{specs.length} spec files fail on their " \
|
|
37
|
+
"own:\n #{failed.join("\n ")}\n\n" \
|
|
38
|
+
"These pass in `rake unit` only because another spec file loads " \
|
|
39
|
+
"something they need. Add the missing require."
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
puts "\nAll #{specs.length} spec files pass in isolation."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
desc "Run the unit tests with coverage reporting"
|
|
46
|
+
task :coverage do
|
|
47
|
+
ENV["COVERAGE"] = "1"
|
|
48
|
+
Rake::Task[:unit].invoke
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
10
52
|
begin
|
|
11
53
|
require "cucumber"
|
|
12
54
|
require "cucumber/rake/task"
|
|
@@ -20,6 +62,9 @@ end
|
|
|
20
62
|
desc "Run all test suites"
|
|
21
63
|
task test: %i{unit features}
|
|
22
64
|
|
|
65
|
+
desc "Run the checks that must pass before a change is merged"
|
|
66
|
+
task verify: %i{unit unit:isolated style}
|
|
67
|
+
|
|
23
68
|
desc "Display LOC stats"
|
|
24
69
|
task :stats do
|
|
25
70
|
puts "\n## Production Code Stats"
|
|
@@ -28,7 +28,7 @@ module Kitchen
|
|
|
28
28
|
# @param io_in [#read] input stream
|
|
29
29
|
# @param io_out [#write] output stream
|
|
30
30
|
def self.strict_encode(io_in, io_out)
|
|
31
|
-
buffer = ""
|
|
31
|
+
buffer = +"" # mutable: IO#read writes into this buffer
|
|
32
32
|
io_out.write([buffer].pack("m0")) while io_in.read(3 * 1000, buffer)
|
|
33
33
|
buffer = nil # rubocop:disable Lint/UselessAssignment
|
|
34
34
|
end
|
|
@@ -40,7 +40,7 @@ module Kitchen
|
|
|
40
40
|
# @param io_in [#read] input stream
|
|
41
41
|
# @param io_out [#write] output stream
|
|
42
42
|
def self.strict_decode(io_in, io_out)
|
|
43
|
-
buffer = ""
|
|
43
|
+
buffer = +"" # mutable: IO#read writes into this buffer
|
|
44
44
|
io_out.write(buffer.unpack("m0").first) while io_in.read(3 * 1000, buffer)
|
|
45
45
|
buffer = nil # rubocop:disable Lint/UselessAssignment
|
|
46
46
|
end
|
|
@@ -28,10 +28,12 @@ module Kitchen
|
|
|
28
28
|
end
|
|
29
29
|
# By default only doctor the first instance to avoid output spam.
|
|
30
30
|
results = [results.first] unless options[:all]
|
|
31
|
-
|
|
31
|
+
# Doctor every instance before deciding the outcome. #any? would
|
|
32
|
+
# stop at the first instance reporting a problem and skip the rest.
|
|
33
|
+
failed = results.map do |instance|
|
|
32
34
|
debug "Doctor on #{instance.name}."
|
|
33
35
|
instance.doctor_action
|
|
34
|
-
end
|
|
36
|
+
end.any?
|
|
35
37
|
exit(1) if failed
|
|
36
38
|
end
|
|
37
39
|
end
|
data/lib/kitchen/command.rb
CHANGED
|
@@ -195,18 +195,20 @@ module Kitchen
|
|
|
195
195
|
end
|
|
196
196
|
|
|
197
197
|
# Determines how many instances may be acted on at once, clamped to the
|
|
198
|
-
# number of instances available.
|
|
198
|
+
# number of instances available and to at least one worker. Without the
|
|
199
|
+
# lower bound a zero or negative --concurrency would start no worker
|
|
200
|
+
# threads at all, so every instance would be skipped and the run would
|
|
201
|
+
# still report success.
|
|
199
202
|
#
|
|
200
203
|
# @param instances [Array<Instance>] the instances to be acted on
|
|
201
204
|
# @return [Integer] the number of worker threads to start
|
|
202
205
|
# @api private
|
|
203
206
|
def concurrency_setting(instances)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
concurrency
|
|
207
|
+
return 1 unless options[:concurrency]
|
|
208
|
+
|
|
209
|
+
concurrency = options[:concurrency]
|
|
210
|
+
concurrency = instances.size if concurrency > instances.size
|
|
211
|
+
[concurrency, 1].max
|
|
210
212
|
end
|
|
211
213
|
|
|
212
214
|
# Performs an action on a single instance, recording any instance or
|
data/lib/kitchen/configurable.rb
CHANGED
|
@@ -247,7 +247,7 @@ module Kitchen
|
|
|
247
247
|
@deprecated_config = deprecated_attributes.delete_if { |attr, obj| !provided_config.key?(attr) || obj.nil? }
|
|
248
248
|
|
|
249
249
|
unless deprecated_config.empty?
|
|
250
|
-
warning = Util.outdent
|
|
250
|
+
warning = Util.outdent(<<-MSG)
|
|
251
251
|
Deprecated configuration detected:
|
|
252
252
|
#{deprecated_config.keys.join("\n")}
|
|
253
253
|
Run 'kitchen doctor' for details.
|
data/lib/kitchen/data_munger.rb
CHANGED
|
@@ -1037,9 +1037,7 @@ module Kitchen
|
|
|
1037
1037
|
# Hash if not found
|
|
1038
1038
|
# @api private
|
|
1039
1039
|
def platform_data_for(name)
|
|
1040
|
-
|
|
1041
|
-
platform.fetch(:name, nil) == name
|
|
1042
|
-
end
|
|
1040
|
+
platform_index[name] || {}
|
|
1043
1041
|
end
|
|
1044
1042
|
|
|
1045
1043
|
# Destructively sets a base kitchen config key/value pair at the root of
|
|
@@ -1068,8 +1066,41 @@ module Kitchen
|
|
|
1068
1066
|
# Hash if not found
|
|
1069
1067
|
# @api private
|
|
1070
1068
|
def suite_data_for(name)
|
|
1071
|
-
|
|
1072
|
-
|
|
1069
|
+
suite_index[name] || {}
|
|
1070
|
+
end
|
|
1071
|
+
|
|
1072
|
+
# Name-keyed index of the platform data, built once on first use.
|
|
1073
|
+
#
|
|
1074
|
+
# Every plugin lookup for every instance resolves a platform and a suite by
|
|
1075
|
+
# name, so the linear scans this replaces made building a project's
|
|
1076
|
+
# instances quadratic in its size.
|
|
1077
|
+
#
|
|
1078
|
+
# @return [Hash{String => Hash}] platform name to platform data
|
|
1079
|
+
# @api private
|
|
1080
|
+
def platform_index
|
|
1081
|
+
@platform_index ||= index_by_name(data.fetch(:platforms, []))
|
|
1082
|
+
end
|
|
1083
|
+
|
|
1084
|
+
# Name-keyed index of the suite data, built once on first use.
|
|
1085
|
+
#
|
|
1086
|
+
# @return [Hash{String => Hash}] suite name to suite data
|
|
1087
|
+
# @api private
|
|
1088
|
+
def suite_index
|
|
1089
|
+
@suite_index ||= index_by_name(data.fetch(:suites, []))
|
|
1090
|
+
end
|
|
1091
|
+
|
|
1092
|
+
# Indexes a collection of named data hashes by their :name value.
|
|
1093
|
+
#
|
|
1094
|
+
# An earlier entry wins over a later duplicate, matching the first match
|
|
1095
|
+
# that a linear search would have returned.
|
|
1096
|
+
#
|
|
1097
|
+
# @param collection [Enumerable<Hash>] platform or suite data
|
|
1098
|
+
# @return [Hash{String => Hash}] name to entry
|
|
1099
|
+
# @api private
|
|
1100
|
+
def index_by_name(collection)
|
|
1101
|
+
collection.each_with_object({}) do |entry, index|
|
|
1102
|
+
key = entry.fetch(:name, nil)
|
|
1103
|
+
index[key] = entry unless index.key?(key)
|
|
1073
1104
|
end
|
|
1074
1105
|
end
|
|
1075
1106
|
end
|
data/lib/kitchen/errors.rb
CHANGED
data/lib/kitchen/instance.rb
CHANGED
|
@@ -20,6 +20,8 @@ require "fileutils" unless defined?(FileUtils)
|
|
|
20
20
|
require "securerandom" unless defined?(SecureRandom)
|
|
21
21
|
require "time" unless defined?(Time.zone_offset)
|
|
22
22
|
|
|
23
|
+
require_relative "logging"
|
|
24
|
+
|
|
23
25
|
module Kitchen
|
|
24
26
|
# An instance of a suite running on a platform. A created instance may be a
|
|
25
27
|
# local virtual machine, cloud instance, container, or even a bare metal
|
|
@@ -240,9 +242,12 @@ module Kitchen
|
|
|
240
242
|
#
|
|
241
243
|
def doctor_action
|
|
242
244
|
banner "The doctor is in"
|
|
243
|
-
|
|
245
|
+
# Each plugin reports its own problems as it finds them, so run all of
|
|
246
|
+
# them and reduce afterwards. #any? would stop at the first plugin with
|
|
247
|
+
# something to say and hide the rest.
|
|
248
|
+
[driver, provisioner, transport, verifier].map do |obj|
|
|
244
249
|
obj.doctor(state_file.read)
|
|
245
|
-
end
|
|
250
|
+
end.any?
|
|
246
251
|
end
|
|
247
252
|
|
|
248
253
|
# Returns a Hash of configuration and other useful diagnostic information.
|
|
@@ -769,12 +774,12 @@ module Kitchen
|
|
|
769
774
|
# reported transitioned state into the desired transitioned state.
|
|
770
775
|
#
|
|
771
776
|
# @param last [String,Symbol,nil] the last known transitioned state of
|
|
772
|
-
# the Instance,
|
|
777
|
+
# the Instance, or `nil` for unknown or no history
|
|
773
778
|
# @param desired [String,Symbol] the desired transitioned state for the
|
|
774
779
|
# Instance
|
|
775
780
|
# @return [Array<Symbol>] an Array of transition actions to perform
|
|
776
781
|
# @api private
|
|
777
|
-
def self.actions(last
|
|
782
|
+
def self.actions(last, desired)
|
|
778
783
|
last_index = index(last)
|
|
779
784
|
desired_index = index(desired)
|
|
780
785
|
|
data/lib/kitchen/lazy_hash.rb
CHANGED
|
@@ -95,6 +95,41 @@ module Kitchen
|
|
|
95
95
|
end
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
+
# Sets the raw value for the given key. Values are stored unrendered, so a
|
|
99
|
+
# callable assigned here is still evaluated lazily on read.
|
|
100
|
+
#
|
|
101
|
+
# SimpleDelegator would forward this through #method_missing; defining it
|
|
102
|
+
# here keeps the hot path off that dispatch, which every plugin walks once
|
|
103
|
+
# per default attribute while applying its defaults.
|
|
104
|
+
#
|
|
105
|
+
# @param key [Object] hash key
|
|
106
|
+
# @param value [Object] the value to store
|
|
107
|
+
# @return [Object] the stored value
|
|
108
|
+
def []=(key, value)
|
|
109
|
+
__getobj__[key] = value
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Returns whether the given key is present, without rendering its value.
|
|
113
|
+
#
|
|
114
|
+
# Defined for the same reason as {#[]=}: to bypass SimpleDelegator's
|
|
115
|
+
# #method_missing on a hot path.
|
|
116
|
+
#
|
|
117
|
+
# @param key [Object] hash key
|
|
118
|
+
# @return [Boolean] whether the key is present
|
|
119
|
+
def key?(key)
|
|
120
|
+
__getobj__.key?(key)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Returns the keys of the underlying hash.
|
|
124
|
+
#
|
|
125
|
+
# Defined for the same reason as {#[]=}: to bypass SimpleDelegator's
|
|
126
|
+
# #method_missing on a hot path.
|
|
127
|
+
#
|
|
128
|
+
# @return [Array] the hash keys
|
|
129
|
+
def keys
|
|
130
|
+
__getobj__.keys
|
|
131
|
+
end
|
|
132
|
+
|
|
98
133
|
# Returns a new Hash with all keys and rendered values of the LazyHash.
|
|
99
134
|
#
|
|
100
135
|
# @return [Hash] a new hash
|
data/lib/kitchen/logger.rb
CHANGED
|
@@ -312,8 +312,10 @@ module Kitchen
|
|
|
312
312
|
|
|
313
313
|
# @return [Hash] metadata added to structured log events
|
|
314
314
|
def metadata
|
|
315
|
-
|
|
316
|
-
|
|
315
|
+
# Merge in place into the fresh dup rather than allocating a new hash per
|
|
316
|
+
# stack entry; this runs once per structured log event.
|
|
317
|
+
metadata_stack.each_with_object(@base_metadata.dup) do |values, result|
|
|
318
|
+
result.merge!(values)
|
|
317
319
|
end
|
|
318
320
|
end
|
|
319
321
|
|
|
@@ -449,15 +451,20 @@ module Kitchen
|
|
|
449
451
|
|
|
450
452
|
private
|
|
451
453
|
|
|
454
|
+
# Builds the formatter used for console output.
|
|
455
|
+
#
|
|
456
|
+
# The colour is fixed for the life of the logger, so both escape
|
|
457
|
+
# sequences are resolved once here rather than on every line. An
|
|
458
|
+
# uncolourised logger, and one whose colour did not resolve, share the
|
|
459
|
+
# same uncoloured formatter.
|
|
452
460
|
def stdout_formatter(color, colorize)
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
461
|
+
escape = colorize ? Color.escape(color) : ""
|
|
462
|
+
|
|
463
|
+
if escape.empty?
|
|
464
|
+
proc { |_severity, _datetime, _progname, msg| "#{msg}\n" }
|
|
457
465
|
else
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
end
|
|
466
|
+
reset = Color.escape(:reset)
|
|
467
|
+
proc { |_severity, _datetime, _progname, msg| "#{escape}#{msg}#{reset}\n" }
|
|
461
468
|
end
|
|
462
469
|
end
|
|
463
470
|
|
|
@@ -496,22 +503,50 @@ module Kitchen
|
|
|
496
503
|
# @return [void]
|
|
497
504
|
# @api private
|
|
498
505
|
def <<(msg)
|
|
499
|
-
@buffer
|
|
506
|
+
@buffer << msg
|
|
500
507
|
flush_lines
|
|
501
508
|
end
|
|
502
509
|
|
|
503
510
|
private
|
|
504
511
|
|
|
512
|
+
# Emits every complete line in the buffer, then drops the emitted bytes
|
|
513
|
+
# in a single pass. Scanning from a moving offset instead of deleting
|
|
514
|
+
# each line as it is found keeps a chunk of n lines linear rather than
|
|
515
|
+
# quadratic, which matters because one SSH or WinRM read can carry
|
|
516
|
+
# thousands of lines.
|
|
505
517
|
def flush_lines
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
@buffer[
|
|
518
|
+
start = 0
|
|
519
|
+
while (i = @buffer.index("\n", start))
|
|
520
|
+
@line_handler.call(@buffer[start, i - start].chomp)
|
|
521
|
+
start = i + 1
|
|
509
522
|
end
|
|
523
|
+
@buffer.slice!(0, start) unless start == 0
|
|
510
524
|
end
|
|
511
525
|
end
|
|
512
526
|
|
|
513
527
|
# Rewrites already-prefixed stream lines into the matching logger call.
|
|
514
528
|
class StreamLineFormatter
|
|
529
|
+
# Width of every stream prefix in {PREFIXES}. All prefixes are the same
|
|
530
|
+
# width so that a line's severity is a fixed-offset lookup.
|
|
531
|
+
#
|
|
532
|
+
# @return [Integer] the prefix width in characters
|
|
533
|
+
# @api private
|
|
534
|
+
PREFIX_WIDTH = 7
|
|
535
|
+
|
|
536
|
+
# Maps each stream prefix to the severity it denotes and whether the line
|
|
537
|
+
# must be replayed verbatim on sinks that cannot accept stream events.
|
|
538
|
+
#
|
|
539
|
+
# @return [Hash{String => Array}] prefix to [severity, structured] pairs
|
|
540
|
+
# @api private
|
|
541
|
+
PREFIXES = {
|
|
542
|
+
"-----> " => [:banner, false],
|
|
543
|
+
"D " => [:debug, true],
|
|
544
|
+
"$$$$$$ " => [:warn, true],
|
|
545
|
+
">>>>>> " => [:error, false],
|
|
546
|
+
"!!!!!! " => [:fatal, true],
|
|
547
|
+
" " => [:info, false],
|
|
548
|
+
}.freeze
|
|
549
|
+
|
|
515
550
|
def initialize(logger)
|
|
516
551
|
@logger = logger
|
|
517
552
|
end
|
|
@@ -519,18 +554,22 @@ module Kitchen
|
|
|
519
554
|
# Routes an already-prefixed stream line to the logger call matching its
|
|
520
555
|
# prefix, stripping the prefix before logging.
|
|
521
556
|
#
|
|
557
|
+
# A hash lookup on the fixed-width prefix replaces a chain of anchored
|
|
558
|
+
# regexp matches, so an unprefixed line costs one lookup rather than six
|
|
559
|
+
# failed matches.
|
|
560
|
+
#
|
|
522
561
|
# @param line [String] a single line of prefixed stream output
|
|
523
562
|
# @return [void]
|
|
524
563
|
# @api private
|
|
525
564
|
def format(line)
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
565
|
+
severity, structured = PREFIXES[line[0, PREFIX_WIDTH]]
|
|
566
|
+
|
|
567
|
+
if severity.nil?
|
|
568
|
+
log_line(:info, line)
|
|
569
|
+
elsif structured
|
|
570
|
+
structured_line(severity, line[PREFIX_WIDTH..], line)
|
|
571
|
+
else
|
|
572
|
+
log_line(severity, line[PREFIX_WIDTH..])
|
|
534
573
|
end
|
|
535
574
|
end
|
|
536
575
|
|
|
@@ -808,9 +847,11 @@ module Kitchen
|
|
|
808
847
|
message: message.to_s,
|
|
809
848
|
progname:,
|
|
810
849
|
sequence: next_sequence
|
|
811
|
-
)
|
|
812
|
-
|
|
813
|
-
|
|
850
|
+
)
|
|
851
|
+
event.compact!
|
|
852
|
+
# One writev rather than two writes: the log device is opened with
|
|
853
|
+
# sync = true, so each call is a syscall.
|
|
854
|
+
@logdev.write(JSON.generate(event), "\n")
|
|
814
855
|
end
|
|
815
856
|
true
|
|
816
857
|
end
|
|
@@ -32,9 +32,10 @@ module Kitchen
|
|
|
32
32
|
# A string "looks-like" a regexp if it starts with / and ends with / + Regexp options i or x
|
|
33
33
|
#
|
|
34
34
|
# @return [Array] filters with regexp-like string converted to PlatformRegexpFilter
|
|
35
|
+
# @raise [ArgumentError] if a filter is neither a String nor a Regexp
|
|
35
36
|
def self.convert(filters)
|
|
36
37
|
::Kernel.Array(filters).map do |filter|
|
|
37
|
-
if (match = filter.match(REGEXP_LIKE_PATTERN))
|
|
38
|
+
if filter.is_a?(::String) && (match = filter.match(REGEXP_LIKE_PATTERN))
|
|
38
39
|
options = match["options"].include?("i") ? ::Regexp::IGNORECASE : 0
|
|
39
40
|
options |= ::Regexp::EXTENDED if match["options"].include?("x")
|
|
40
41
|
filter = ::Regexp.new(match["pattern"], options)
|
|
@@ -51,7 +52,8 @@ module Kitchen
|
|
|
51
52
|
# @param [Regexp,String] value of the filter
|
|
52
53
|
def initialize(value)
|
|
53
54
|
unless value.is_a?(::Regexp) || value.is_a?(::String)
|
|
54
|
-
raise ::ArgumentError, "PlatformFilter#new requires value to be a String or a Regexp"
|
|
55
|
+
raise ::ArgumentError, "PlatformFilter#new requires value to be a String or a Regexp, " \
|
|
56
|
+
"got #{value.inspect} (#{value.class})"
|
|
55
57
|
end
|
|
56
58
|
|
|
57
59
|
@value = value
|
|
@@ -230,7 +230,11 @@ module Kitchen
|
|
|
230
230
|
# will persist after the process terminates. In other words, cleanup is
|
|
231
231
|
# explicit. This method is safe to call multiple times.
|
|
232
232
|
def cleanup_sandbox
|
|
233
|
-
|
|
233
|
+
# Check the ivar, not #sandbox_path, which raises rather than
|
|
234
|
+
# returning nil when the sandbox was never created. This runs from
|
|
235
|
+
# an ensure block, so raising here would mask the error that kept
|
|
236
|
+
# #create_sandbox from finishing.
|
|
237
|
+
return if @sandbox_path.nil?
|
|
234
238
|
|
|
235
239
|
debug("Cleaning up local sandbox in #{sandbox_path}")
|
|
236
240
|
@install_script_path = nil
|
data/lib/kitchen/shell_out.rb
CHANGED
|
@@ -134,11 +134,11 @@ module Kitchen
|
|
|
134
134
|
# @return [Hash] hash of connection options
|
|
135
135
|
# @api private
|
|
136
136
|
def connection_options(data)
|
|
137
|
-
|
|
137
|
+
{
|
|
138
138
|
instance_name: instance.name,
|
|
139
|
-
kitchen_root: Dir.pwd,
|
|
139
|
+
kitchen_root: data[:kitchen_root] || Dir.pwd,
|
|
140
|
+
logger:,
|
|
140
141
|
}
|
|
141
|
-
opts
|
|
142
142
|
end
|
|
143
143
|
end
|
|
144
144
|
end
|
|
@@ -247,7 +247,7 @@ module Kitchen
|
|
|
247
247
|
# Mac system
|
|
248
248
|
# @api private
|
|
249
249
|
def create_rdp_doc(opts = {})
|
|
250
|
-
content = Util.outdent
|
|
250
|
+
content = Util.outdent(<<-RDP)
|
|
251
251
|
full address:s:#{URI.parse(options[:endpoint]).host}:#{rdp_port}
|
|
252
252
|
prompt for credentials:i:1
|
|
253
253
|
username:s:#{options[:user]}
|
data/lib/kitchen/util.rb
CHANGED
|
@@ -24,6 +24,17 @@ module Kitchen
|
|
|
24
24
|
#
|
|
25
25
|
# @author Fletcher Nichol <fnichol@nichol.ca>
|
|
26
26
|
module Util
|
|
27
|
+
class << self
|
|
28
|
+
# @return [Mutex] a mutex used to serialize the process-global Dir.chdir
|
|
29
|
+
attr_accessor :mutex_chdir
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Dir.chdir mutates process-global state, so every call has to be
|
|
33
|
+
# serialized. The mutex is owned here rather than in lib/kitchen.rb
|
|
34
|
+
# because Util is its only consumer and has to keep working when
|
|
35
|
+
# "kitchen/util" is required on its own.
|
|
36
|
+
@mutex_chdir = Mutex.new
|
|
37
|
+
|
|
27
38
|
# Returns the standard library Logger level constants for a given symbol
|
|
28
39
|
# representation.
|
|
29
40
|
#
|
|
@@ -88,17 +99,35 @@ module Kitchen
|
|
|
88
99
|
end
|
|
89
100
|
end
|
|
90
101
|
|
|
102
|
+
# The text substituted for a masked value.
|
|
103
|
+
MASK = "******".freeze
|
|
104
|
+
|
|
105
|
+
# A double-quoted string value, allowing for backslash escapes so that a
|
|
106
|
+
# quote inside the value does not end the match early.
|
|
107
|
+
QUOTED_VALUE = /"(?:\\.|[^"\\])*"/.source
|
|
108
|
+
|
|
91
109
|
# Returns a string with masked values for specified parameters.
|
|
92
110
|
#
|
|
111
|
+
# Hashes are commonly interpolated into these strings via Hash#inspect,
|
|
112
|
+
# whose rendering has changed over time, so every spelling a key/value
|
|
113
|
+
# pair has had is matched:
|
|
114
|
+
#
|
|
115
|
+
# :key=>"value" symbol key, Ruby <= 3.3
|
|
116
|
+
# "key"=>"value" string key, Ruby <= 3.3
|
|
117
|
+
# key: "value" symbol key, Ruby >= 3.4
|
|
118
|
+
# "key" => "value" string key, Ruby >= 3.4
|
|
119
|
+
#
|
|
93
120
|
# @param string_to_mask [String] the object whose string representation is parsed
|
|
94
121
|
# @param keys [Array] the list of keys whose values should be masked
|
|
95
|
-
# @return [String]
|
|
122
|
+
# @return [String] a new string with the values of the given keys masked
|
|
96
123
|
def self.mask_values(string_to_mask, keys)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
124
|
+
keys.reduce(string_to_mask.to_s) do |masked, key|
|
|
125
|
+
key = ::Regexp.escape(key.to_s)
|
|
126
|
+
masked.gsub(
|
|
127
|
+
/(?<assignment>(?::#{key}|"#{key}")\s*=>\s*|(?<!\w)#{key}:\s*)#{QUOTED_VALUE}/,
|
|
128
|
+
%{\\k<assignment>"#{MASK}"}
|
|
129
|
+
)
|
|
100
130
|
end
|
|
101
|
-
masked_string
|
|
102
131
|
end
|
|
103
132
|
|
|
104
133
|
# Returns a formatted string representing a duration in seconds.
|
|
@@ -146,6 +175,22 @@ module Kitchen
|
|
|
146
175
|
string.gsub!(/^ {#{string.index(/[^ ]/)}}/, "")
|
|
147
176
|
end
|
|
148
177
|
|
|
178
|
+
# Returns a copy of a string with each line outdented by the indentation
|
|
179
|
+
# level of its first line.
|
|
180
|
+
#
|
|
181
|
+
# Prefer this over {.outdent!} when the input may be a string literal:
|
|
182
|
+
# Ruby 4 freezes literals, and mutating one raises a FrozenError.
|
|
183
|
+
#
|
|
184
|
+
# @param string [String] the string to outdent
|
|
185
|
+
# @return [String] a new, outdented string
|
|
186
|
+
# @example
|
|
187
|
+
#
|
|
188
|
+
# string = " a\n b\n c\n"
|
|
189
|
+
# Util.outdent(string) # => "a\n b\nc\n"
|
|
190
|
+
def self.outdent(string)
|
|
191
|
+
string.gsub(/^ {#{string.index(/[^ ]/)}}/, "")
|
|
192
|
+
end
|
|
193
|
+
|
|
149
194
|
# Returns a set of Bourne Shell (AKA /bin/sh) compatible helper
|
|
150
195
|
# functions. This function is usually called inline in a string that
|
|
151
196
|
# will be executed remotely on a test instance.
|
|
@@ -178,7 +223,7 @@ module Kitchen
|
|
|
178
223
|
# the directory does not exist
|
|
179
224
|
return [] unless Dir.exist?(path)
|
|
180
225
|
|
|
181
|
-
|
|
226
|
+
mutex_chdir.synchronize do
|
|
182
227
|
Dir.chdir(path) do
|
|
183
228
|
glob_pattern = if recurse
|
|
184
229
|
"**/*"
|
|
@@ -214,7 +259,7 @@ module Kitchen
|
|
|
214
259
|
def self.safe_glob(path, pattern, *flags)
|
|
215
260
|
return [] unless Dir.exist?(path)
|
|
216
261
|
|
|
217
|
-
|
|
262
|
+
mutex_chdir.synchronize do
|
|
218
263
|
Dir.chdir(path) do
|
|
219
264
|
Dir.glob(pattern, *flags).map { |f| File.join(path, f) }
|
|
220
265
|
end
|
|
@@ -115,7 +115,11 @@ module Kitchen
|
|
|
115
115
|
# will persist after the process terminates. In other words, cleanup is
|
|
116
116
|
# explicit. This method is safe to call multiple times.
|
|
117
117
|
def cleanup_sandbox
|
|
118
|
-
|
|
118
|
+
# Check the ivar, not #sandbox_path, which raises rather than
|
|
119
|
+
# returning nil when the sandbox was never created. This runs from
|
|
120
|
+
# an ensure block, so raising here would mask the error that kept
|
|
121
|
+
# #create_sandbox from finishing.
|
|
122
|
+
return if @sandbox_path.nil?
|
|
119
123
|
|
|
120
124
|
debug("Cleaning up local sandbox in #{sandbox_path}")
|
|
121
125
|
FileUtils.rmtree(sandbox_path)
|
|
@@ -72,7 +72,7 @@ module Kitchen
|
|
|
72
72
|
cmd = sudo(config[:busser_bin]).dup
|
|
73
73
|
.tap { |str| str.insert(0, "& ") if powershell_shell? }
|
|
74
74
|
|
|
75
|
-
prefix_command(wrap_shell_code(Util.outdent
|
|
75
|
+
prefix_command(wrap_shell_code(Util.outdent(<<-CMD)))
|
|
76
76
|
#{busser_env}
|
|
77
77
|
|
|
78
78
|
#{cmd} suite cleanup
|
|
@@ -95,7 +95,7 @@ module Kitchen
|
|
|
95
95
|
cmd = sudo(config[:busser_bin]).dup
|
|
96
96
|
.tap { |str| str.insert(0, "& ") if powershell_shell? }
|
|
97
97
|
|
|
98
|
-
prefix_command(wrap_shell_code(Util.outdent
|
|
98
|
+
prefix_command(wrap_shell_code(Util.outdent(<<-CMD)))
|
|
99
99
|
#{busser_env}
|
|
100
100
|
|
|
101
101
|
#{cmd} test #{plugins.join(" ").gsub!("busser-", "")}
|
|
@@ -39,13 +39,12 @@ module Kitchen
|
|
|
39
39
|
def call(state)
|
|
40
40
|
info("[#{name}] Verify on instance #{instance.name} with state=#{state}")
|
|
41
41
|
sleep_if_set
|
|
42
|
-
merge_state_to_env(state)
|
|
43
42
|
if config[:remote_exec]
|
|
44
43
|
instance.transport.connection(state) do |conn|
|
|
45
44
|
conn.execute(config[:command])
|
|
46
45
|
end
|
|
47
46
|
else
|
|
48
|
-
shellout
|
|
47
|
+
shellout(state)
|
|
49
48
|
end
|
|
50
49
|
debug("[#{name}] Verify completed.")
|
|
51
50
|
end
|
|
@@ -72,8 +71,8 @@ module Kitchen
|
|
|
72
71
|
end
|
|
73
72
|
end
|
|
74
73
|
|
|
75
|
-
def shellout
|
|
76
|
-
cmd = Mixlib::ShellOut.new(config[:command],
|
|
74
|
+
def shellout(state = {})
|
|
75
|
+
cmd = Mixlib::ShellOut.new(config[:command], shellout_opts(state))
|
|
77
76
|
cmd.live_stream = config[:live_stream]
|
|
78
77
|
cmd.run_command
|
|
79
78
|
begin
|
|
@@ -83,16 +82,38 @@ module Kitchen
|
|
|
83
82
|
end
|
|
84
83
|
end
|
|
85
84
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
85
|
+
# Returns the options to run the command with: whatever the user
|
|
86
|
+
# configured, with the KITCHEN_* variables merged into their environment
|
|
87
|
+
# rather than replacing it.
|
|
88
|
+
#
|
|
89
|
+
# This builds a new Hash instead of writing back to config. Nested config
|
|
90
|
+
# hashes are shared between every instance, so mutating :shellout_opts
|
|
91
|
+
# would leak one instance's environment into the others.
|
|
92
|
+
#
|
|
93
|
+
# @param state [Hash] mutable instance state
|
|
94
|
+
# @return [Hash] options for Mixlib::ShellOut
|
|
95
|
+
# @api private
|
|
96
|
+
def shellout_opts(state)
|
|
97
|
+
opts = config[:shellout_opts].to_h
|
|
98
|
+
opts.merge(
|
|
99
|
+
environment: opts.fetch(:environment, {}).to_h.merge(kitchen_environment(state))
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# @param state [Hash] mutable instance state
|
|
104
|
+
# @return [Hash] the KITCHEN_* environment variables for this instance
|
|
105
|
+
# @api private
|
|
106
|
+
def kitchen_environment(state)
|
|
107
|
+
env = {
|
|
108
|
+
"KITCHEN_INSTANCE" => instance.name,
|
|
109
|
+
"KITCHEN_PLATFORM" => instance.platform.name,
|
|
110
|
+
"KITCHEN_SUITE" => instance.suite.name,
|
|
111
|
+
}
|
|
112
|
+
env["KITCHEN_USERNAME"] = instance.transport[:username] if instance.respond_to?(:transport)
|
|
92
113
|
state.each_pair do |key, value|
|
|
93
|
-
|
|
114
|
+
env["KITCHEN_" + key.to_s.upcase] = value.to_s
|
|
94
115
|
end
|
|
95
|
-
|
|
116
|
+
env
|
|
96
117
|
end
|
|
97
118
|
end
|
|
98
119
|
end
|
data/lib/kitchen/version.rb
CHANGED
data/lib/kitchen.rb
CHANGED
|
@@ -57,8 +57,23 @@ module Kitchen
|
|
|
57
57
|
# @return [Mutex] a common mutex for global coordination
|
|
58
58
|
attr_accessor :mutex
|
|
59
59
|
|
|
60
|
+
# Returns the mutex used for Dir.chdir coordination.
|
|
61
|
+
#
|
|
60
62
|
# @return [Mutex] a mutex used for Dir.chdir coordination
|
|
61
|
-
|
|
63
|
+
# @deprecated Use {Kitchen::Util.mutex_chdir}, which owns the mutex and is
|
|
64
|
+
# available without loading all of Test Kitchen.
|
|
65
|
+
def mutex_chdir
|
|
66
|
+
Kitchen::Util.mutex_chdir
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Sets the mutex used for Dir.chdir coordination.
|
|
70
|
+
#
|
|
71
|
+
# @param mutex [Mutex] the mutex to use for Dir.chdir coordination
|
|
72
|
+
# @return [Mutex] the newly set mutex
|
|
73
|
+
# @deprecated Use {Kitchen::Util.mutex_chdir=}.
|
|
74
|
+
def mutex_chdir=(mutex)
|
|
75
|
+
Kitchen::Util.mutex_chdir = mutex
|
|
76
|
+
end
|
|
62
77
|
|
|
63
78
|
# @return [String] identifier for the current Kitchen process invocation
|
|
64
79
|
attr_writer :run_id
|
|
@@ -164,6 +179,3 @@ Kitchen.logger = Kitchen.default_logger
|
|
|
164
179
|
|
|
165
180
|
# Set up a collection of instance crash exceptions for error reporting
|
|
166
181
|
Kitchen.mutex = Mutex.new
|
|
167
|
-
|
|
168
|
-
# Initialize the mutex for Dir.chdir coordination
|
|
169
|
-
Kitchen.mutex_chdir = Mutex.new
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: test-kitchen
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.1.
|
|
4
|
+
version: 4.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Fletcher Nichol
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bcrypt_pbkdf
|