beaker 6.8.1 → 6.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 569f6e836d92183b8d192a6865f0db91f711fe52235c37a4d3faaf0dbde2daec
4
- data.tar.gz: 49dd48391cb708a5dd9eb17272c8a84df1658b1263664c185caa8da81592b213
3
+ metadata.gz: f34a28e10b3448ee6294cdfd53903ec145d6c81d28b95dc7370a30936922e80c
4
+ data.tar.gz: 7e7ba934516ed374c96c97357047d56693dddbd23a45be3a6b088b5dcdbae52a
5
5
  SHA512:
6
- metadata.gz: a0f60c37aa1325936de181703c981bab351560035cb42b50b9c0789ce39463c655848c7856d4387dac64a4d7dbf1be6b16102447017aec25fb3befe0a7af3b78
7
- data.tar.gz: b26dc87dbff51e7437594c17a0dfac7dd0287a7e91a5f03070dbb7783f2e32a26d832ce7048a32713aec9d04824aa4b2bc1780a0625b71c1d1dd9cd0ead43142
6
+ metadata.gz: b7a2345337cec50941ba9aa8c20a8f4cfab3bd0c84e1bb74f0e8c506ff29f894568562c423aa815fcaaf16b423dbde3150117a579c3463481802b91d3bb159eb
7
+ data.tar.gz: ba81525b5f025ad793d7175b694a2a95702af799282ddcec8f060e3c4e0badd5ded71a905b2239c7230d4161056c0a9a69511fd7e318eb675605bcfde0aa3aa5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [6.9.0](https://github.com/voxpupuli/beaker/tree/6.9.0) (2026-08-14)
4
+
5
+ [Full Changelog](https://github.com/voxpupuli/beaker/compare/6.8.1...6.9.0)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - \(maint\) Add Ubuntu 26.04 \(resolute\) codename mapping [\#2024](https://github.com/voxpupuli/beaker/pull/2024) ([bastelfreak](https://github.com/bastelfreak))
10
+
11
+ **Fixed bugs:**
12
+
13
+ - Ensure correct encoding for logging and fix performance bugs [\#2027](https://github.com/voxpupuli/beaker/pull/2027) ([bastelfreak](https://github.com/bastelfreak))
14
+ - Don't lose a whole junit report to one bad byte [\#2026](https://github.com/voxpupuli/beaker/pull/2026) ([bastelfreak](https://github.com/bastelfreak))
15
+ - Stop escape\_invalid\_xml\_chars allocating per character [\#2025](https://github.com/voxpupuli/beaker/pull/2025) ([bastelfreak](https://github.com/bastelfreak))
16
+
17
+ **Closed issues:**
18
+
19
+ - The tmpfile method in pswindows needs to implement the use of the extension parameter [\#1955](https://github.com/voxpupuli/beaker/issues/1955)
20
+
3
21
  ## [6.8.1](https://github.com/voxpupuli/beaker/tree/6.8.1) (2025-06-16)
4
22
 
5
23
  [Full Changelog](https://github.com/voxpupuli/beaker/compare/6.8.0...6.8.1)
data/Rakefile CHANGED
@@ -191,8 +191,8 @@ begin
191
191
  config.project = 'beaker'
192
192
  gem_version = Gem::Specification.load("#{config.project}.gemspec").version
193
193
  config.future_release = gem_version
194
- config.release_branch = 'master'
195
- config.exclude_tags_regex = /\A4\./
194
+ config.release_branch = '6.x'
195
+ config.exclude_tags_regex = /^v?[4,7]\./
196
196
  config.since_tag = '5.7.0'
197
197
  end
198
198
  rescue LoadError
data/lib/beaker/logger.rb CHANGED
@@ -197,7 +197,7 @@ module Beaker
197
197
  else
198
198
  # Remove invalid and undefined UTF-8 character encodings
199
199
  string = string.to_s.dup.force_encoding('UTF-8')
200
- return string.to_s.chars.select { |i| i.valid_encoding? }.join
200
+ string.scrub('')
201
201
  end
202
202
  end
203
203
 
@@ -120,21 +120,34 @@ module Beaker
120
120
  self.escape_invalid_xml_chars(Logger.strip_color_codes(string))
121
121
  end
122
122
 
123
+ # Every character {is_valid_xml} rejects, as one pattern. Note that this
124
+ # tracks that method rather than the specification it cites: #xD is absent
125
+ # from both, and both take the last range as 0x100000 rather than the
126
+ # 0x10000 the specification gives. Changing either would change the
127
+ # contents of every junit report, so they are left alone here and the specs
128
+ # walk the whole range to keep the two in step.
129
+ INVALID_XML_CHARS = /[^\u{9}\u{A}\u{20}-\u{D7FF}\u{E000}-\u{FFFD}\u{100000}-\u{10FFFF}]/
130
+
123
131
  # Escape invalid XML UTF-8 codes from provided string, see http://www.w3.org/TR/xml/#charsets for valid
124
132
  # character specification
125
133
  # @param [String] string The string to remove invalid codes from
126
134
  # @return [String] Properly escaped string
127
135
  def self.escape_invalid_xml_chars string
128
- escaped_string = ""
129
- string.chars.each do |i|
130
- char_as_codestring = i.unpack("U*").join
131
- escaped_string << if self.is_valid_xml(char_as_codestring.to_i)
132
- i
133
- else
134
- "\\#{char_as_codestring}"
135
- end
136
- end
137
- escaped_string
136
+ # Bytes that are not valid in their own encoding cannot be represented in
137
+ # xml at all, and would make the gsub below raise. Drop them, the same
138
+ # way Logger#convert does further upstream. Raising matters more here
139
+ # than it might look: TestSuiteResult calls #format_cdata from inside a
140
+ # blanket rescue that abandons the whole junit document, so one stray
141
+ # byte in one sublog loses every test result for the run.
142
+ string = string.dup.force_encoding(Encoding::UTF_8) unless string.encoding == Encoding::UTF_8
143
+ string = string.scrub('') unless string.valid_encoding?
144
+
145
+ # This walked the string a character at a time, unpacking each one into
146
+ # an array and joining it back into a string just to read its codepoint.
147
+ # That is around four objects per character, and it runs over the whole
148
+ # sublog of every test case at the end of a run: 28 million objects, and
149
+ # 98MB of permanently grown ruby heap, for a 6MB suite.
150
+ string.gsub(INVALID_XML_CHARS) { |char| "\\#{char.ord}" }
138
151
  end
139
152
 
140
153
  # Determine if the provided number falls in the range of accepted xml unicode values
@@ -11,7 +11,8 @@ module Beaker
11
11
  "bookworm" => "12",
12
12
  "bullseye" => "11",
13
13
  "buster" => "10", },
14
- :ubuntu => { "noble" => "2404",
14
+ :ubuntu => { "resolute" => "2604",
15
+ "noble" => "2404",
15
16
  "jammy" => "2204",
16
17
  "focal" => "2004",
17
18
  "bionic" => "1804", },
data/lib/beaker/result.rb CHANGED
@@ -31,10 +31,24 @@ module Beaker
31
31
  return string.gsub(/\r\n?/, "\n")
32
32
  end
33
33
 
34
- def convert string
35
- # Remove invalid and undefined UTF-8 character encodings
36
- string.to_s.force_encoding('UTF-8')
37
- string.to_s.chars.select { |i| i.valid_encoding? }.join
34
+ # This method behaves differently when given a string instance, subclass
35
+ # or something else. The latter two cases should be deprecated.
36
+ def convert(string)
37
+ # guard against nil, which never worked on accident
38
+ raise TypeError, "Beaker::Result#convert cannot convert nil to a String" if string.nil?
39
+
40
+ unless string.instance_of?(String)
41
+ # For a string subclass, `string.to_s` returns a copy, so calling
42
+ # `force_encoding` on it was meaningless.
43
+ return string.to_s.scrub('')
44
+ end
45
+
46
+ # Data collected from the connection is binary (ASCII-8BIT). In that
47
+ # encoding, every byte is a valid character, so we tag the string as UTF-8
48
+ string.force_encoding('UTF-8')
49
+
50
+ # And remove invalid and undefined UTF-8 character encodings.
51
+ string.scrub('')
38
52
  end
39
53
 
40
54
  def log(logger)
@@ -1,5 +1,5 @@
1
1
  module Beaker
2
2
  module Version
3
- STRING = '6.8.1'
3
+ STRING = '6.9.0'
4
4
  end
5
5
  end
@@ -32,6 +32,42 @@ module Beaker
32
32
  testing_string = 'pants man, pants!'
33
33
  expect(described_class.escape_invalid_xml_chars(testing_string)).to be === testing_string
34
34
  end
35
+
36
+ it 'escapes carriage returns, which is_valid_xml does not accept' do
37
+ expect(described_class.escape_invalid_xml_chars("a\rb")).to be === 'a\13b'
38
+ end
39
+
40
+ it 'agrees with is_valid_xml on every codepoint' do
41
+ # INVALID_XML_CHARS has to track is_valid_xml rather than the spec that
42
+ # method cites, so walk the whole range instead of trusting either.
43
+ codepoints = (0..0xD7FF).to_a + (0xE000..0x10FFFF).to_a # surrogates cannot be in a String
44
+ codepoints.each_slice(50_000) do |slice|
45
+ string = slice.map { |c| c.chr(Encoding::UTF_8) }.join
46
+ expected = slice.map { |c| described_class.is_valid_xml(c) ? c.chr(Encoding::UTF_8) : "\\#{c}" }.join
47
+ expect(described_class.escape_invalid_xml_chars(string)).to eq(expected)
48
+ end
49
+ end
50
+
51
+ it 'drops invalid encodings rather than raising' do
52
+ # TestSuiteResult calls format_cdata from inside a blanket rescue that
53
+ # abandons the whole junit document, so an exception raised here would
54
+ # cost the run its entire test report.
55
+ expect(described_class.escape_invalid_xml_chars("ok\xFFbad".b)).to eq('okbad')
56
+ end
57
+
58
+ it 'accepts a frozen string' do
59
+ expect(described_class.escape_invalid_xml_chars('pants'.freeze)).to eq('pants')
60
+ end
61
+ end
62
+
63
+ describe '#format_cdata' do
64
+ it 'strips colour codes and escapes what is left' do
65
+ expect(described_class.format_cdata("\e[00;33mpants\e[00;00m")).to eq('pants')
66
+ end
67
+
68
+ it 'survives invalid encodings' do
69
+ expect(described_class.format_cdata("\e[00;33mok\xFFbad".b)).to eq('okbad')
70
+ end
35
71
  end
36
72
 
37
73
  describe '#copy_stylesheet_into_xml_dir' do
@@ -24,6 +24,24 @@ module Beaker
24
24
  valid_utf8.freeze
25
25
  expect(logger.convert(valid_utf8)).to be === valid_utf8
26
26
  end
27
+
28
+ it 'leaves the string it was given alone' do
29
+ expect(logger.convert(invalid_utf8)).not_to equal(invalid_utf8)
30
+ expect(invalid_utf8.bytes).to include(0xAD)
31
+ end
32
+
33
+ it 'converts each item of an array' do
34
+ expect(logger.convert([valid_utf8, invalid_utf8])).to eq([valid_utf8, valid_utf8])
35
+ end
36
+
37
+ it 'converts arrays of arrays' do
38
+ expect(logger.convert([[invalid_utf8], valid_utf8])).to eq([[valid_utf8], valid_utf8])
39
+ end
40
+
41
+ it 'converts anything that can describe itself' do
42
+ expect(logger.convert(42)).to eq('42')
43
+ expect(logger.convert(nil)).to eq('')
44
+ end
27
45
  end
28
46
 
29
47
  describe '#generate_dated_log_folder' do
@@ -78,6 +78,11 @@ module Beaker
78
78
  expect(platform.with_version_codename).to be === 'debian-bullseye-xxx'
79
79
  end
80
80
 
81
+ it "can convert ubuntu-2604-xxx to ubuntu-resolute-xxx" do
82
+ @name = 'ubuntu-2604-xxx'
83
+ expect(platform.with_version_codename).to be === 'ubuntu-resolute-xxx'
84
+ end
85
+
81
86
  it "can convert ubuntu-2404-xxx to ubuntu-noble-xxx" do
82
87
  @name = 'ubuntu-2404-xxx'
83
88
  expect(platform.with_version_codename).to be === 'ubuntu-noble-xxx'
@@ -122,6 +127,11 @@ module Beaker
122
127
  expect(platform.with_version_number).to be === 'ubuntu-2204-xxx'
123
128
  end
124
129
 
130
+ it "can convert ubuntu-resolute-xxx to ubuntu-2604-xxx" do
131
+ @name = 'ubuntu-resolute-xxx'
132
+ expect(platform.with_version_number).to be === 'ubuntu-2604-xxx'
133
+ end
134
+
125
135
  %w[centos redhat].each do |p|
126
136
  it "leaves #{p}-7-xxx alone" do
127
137
  @name = "#{p}-7-xxx"
@@ -0,0 +1,189 @@
1
+ require 'spec_helper'
2
+
3
+ module Beaker
4
+ describe Result do
5
+ subject(:result) { described_class.new(host, cmd) }
6
+
7
+ let(:host) { 'my_host' }
8
+ let(:cmd) { 'ls -la' }
9
+
10
+ # Command output reaches a Result the way net-ssh hands it over: as chunks
11
+ # of binary data, appended as they arrive.
12
+ def binary(string)
13
+ string.dup.force_encoding('ASCII-8BIT')
14
+ end
15
+
16
+ describe '#initialize' do
17
+ it 'starts with empty streams and no exit code' do
18
+ expect(result.stdout).to eq('')
19
+ expect(result.stderr).to eq('')
20
+ expect(result.output).to eq('')
21
+ expect(result.exit_code).to be_nil
22
+ end
23
+ end
24
+
25
+ describe '#convert' do
26
+ let(:valid_utf8) { "modules\n├── jimmy-appleseed (v1.1.0)\n└── jimmy-crakorn (v0.4.0)\n" }
27
+
28
+ it 'preserves valid utf-8' do
29
+ expect(result.convert(valid_utf8.dup)).to eq(valid_utf8)
30
+ end
31
+
32
+ it 'strips invalid utf-8 byte sequences' do
33
+ expect(result.convert(binary("ok\xFFbad"))).to eq('okbad')
34
+ end
35
+
36
+ it 'strips a truncated multi-byte sequence' do
37
+ expect(result.convert(binary("ok\xE3\x81"))).to eq('ok')
38
+ end
39
+
40
+ it 'returns utf-8 whatever encoding it was handed' do
41
+ expect(result.convert(binary('plain')).encoding).to eq(Encoding::UTF_8)
42
+ end
43
+
44
+ it 'modifies the argument encoding in place' do
45
+ string = binary('plain')
46
+ result.convert(string)
47
+ expect(string.encoding).to eq(Encoding::UTF_8)
48
+ end
49
+
50
+ it 'returns a new string rather than scrubbing the one it was given' do
51
+ string = binary("ok\xFFbad")
52
+ expect(result.convert(string)).not_to equal(string)
53
+ expect(string.bytes).to include(0xFF)
54
+ end
55
+
56
+ context 'when handed String subclass' do
57
+ let(:string_subclass) { Class.new(String) }
58
+
59
+ it 'accepts String subclass' do
60
+ expect(result.convert(string_subclass.new('plain'))).to eq('plain')
61
+ end
62
+
63
+ it 'leaves invalid encodings in place, as it always has' do
64
+ subclass = string_subclass.new(binary("ok\xFFbad"))
65
+ expect(result.convert(subclass).bytes).to include(0xFF)
66
+ end
67
+
68
+ it 'converts anything else that can describe itself' do
69
+ expect(result.convert(5)).to eq('5')
70
+ end
71
+
72
+ it 'raises TypeError on nil' do
73
+ expect { result.convert(nil) }.to raise_error(TypeError, /cannot convert nil to a String/)
74
+ end
75
+ end
76
+ end
77
+
78
+ describe '#normalize_line_endings' do
79
+ it 'converts crlf to lf' do
80
+ expect(result.normalize_line_endings("a\r\nb")).to eq("a\nb")
81
+ end
82
+
83
+ it 'converts a lone cr to lf' do
84
+ expect(result.normalize_line_endings("a\rb")).to eq("a\nb")
85
+ end
86
+
87
+ it 'leaves lf alone' do
88
+ expect(result.normalize_line_endings("a\nb")).to eq("a\nb")
89
+ end
90
+ end
91
+
92
+ describe '#finalize!' do
93
+ before do
94
+ result.stdout << binary("out\xFF\r\n")
95
+ result.stderr << binary("err\xFF\r\n")
96
+ result.output << binary("out\xFF\r\nerr\xFF\r\n")
97
+ result.finalize!
98
+ end
99
+
100
+ it 'scrubs and normalizes every stream' do
101
+ expect(result.stdout).to eq("out\n")
102
+ expect(result.stderr).to eq("err\n")
103
+ expect(result.output).to eq("out\nerr\n")
104
+ end
105
+
106
+ it 'keeps the unscrubbed bytes in the raw_ attributes' do
107
+ expect(result.raw_stdout.bytes).to include(0xFF)
108
+ expect(result.raw_stderr.bytes).to include(0xFF)
109
+ expect(result.raw_output.bytes).to include(0xFF)
110
+ end
111
+
112
+ it 'leaves the raw_ line endings alone' do
113
+ expect(result.raw_stdout).to include("\r\n")
114
+ end
115
+ end
116
+
117
+ describe '#formatted_output' do
118
+ before do
119
+ result.output << (1..15).map { |i| "line #{i}" }.join("\n")
120
+ end
121
+
122
+ it 'returns the last ten lines by default, tab indented' do
123
+ expect(result.formatted_output).to eq((6..15).map { |i| "\tline #{i}" }.join("\n"))
124
+ end
125
+
126
+ it 'honours a limit' do
127
+ expect(result.formatted_output(2)).to eq("\tline 14\n\tline 15")
128
+ end
129
+ end
130
+
131
+ describe '#exit_code_in?' do
132
+ it 'is true when the exit code is in the range' do
133
+ result.exit_code = 2
134
+ expect(result.exit_code_in?([0, 2])).to be true
135
+ end
136
+
137
+ it 'is false when it is not' do
138
+ result.exit_code = 1
139
+ expect(result.exit_code_in?([0, 2])).to be false
140
+ end
141
+ end
142
+
143
+ describe '#success?' do
144
+ it 'is true only for an exit code of zero' do
145
+ result.exit_code = 0
146
+ expect(result).to be_success
147
+ end
148
+
149
+ it 'is false for a non-zero exit code' do
150
+ result.exit_code = 1
151
+ expect(result).not_to be_success
152
+ end
153
+
154
+ it 'is false when no exit code was collected' do
155
+ expect(result).not_to be_success
156
+ end
157
+ end
158
+
159
+ describe '#log' do
160
+ let(:logger) { instance_double(Beaker::Logger) }
161
+
162
+ it 'reports a non-zero exit code' do
163
+ result.exit_code = 1
164
+ expect(logger).to receive(:debug).with('Exited: 1')
165
+ result.log(logger)
166
+ end
167
+
168
+ it 'says nothing for a successful command' do
169
+ result.exit_code = 0
170
+ expect(logger).not_to receive(:debug)
171
+ result.log(logger)
172
+ end
173
+
174
+ it 'says nothing when no exit code was collected' do
175
+ expect(logger).not_to receive(:debug)
176
+ result.log(logger)
177
+ end
178
+ end
179
+ end
180
+
181
+ describe NullResult do
182
+ subject(:result) { described_class.new('my_host', 'ls -la') }
183
+
184
+ it 'succeeds without having run anything' do
185
+ expect(result.exit_code).to eq(0)
186
+ expect(result).to be_success
187
+ end
188
+ end
189
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: beaker
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.8.1
4
+ version: 6.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet
@@ -659,6 +659,7 @@ files:
659
659
  - spec/beaker/options/validator_spec.rb
660
660
  - spec/beaker/perf_spec.rb
661
661
  - spec/beaker/platform_spec.rb
662
+ - spec/beaker/result_spec.rb
662
663
  - spec/beaker/shared/error_handler_spec.rb
663
664
  - spec/beaker/shared/fog_credentials_spec.rb
664
665
  - spec/beaker/shared/host_manager_spec.rb
@@ -692,7 +693,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
692
693
  - !ruby/object:Gem::Version
693
694
  version: '0'
694
695
  requirements: []
695
- rubygems_version: 3.6.7
696
+ rubygems_version: 4.0.16
696
697
  specification_version: 4
697
698
  summary: Let's test Puppet!
698
699
  test_files: []