bashcov 4.0.1 → 4.0.2

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: f0747d5303e019390aac25da832f145d0fe434ca0990ce68b11124423f6cf1f7
4
- data.tar.gz: daed283089aa39da826552a98178c59916e215e30552113ebf094103388cae5b
3
+ metadata.gz: e79dc939af127bef6f99cd953e085033f167a5221e332a133951e476f9b63f6e
4
+ data.tar.gz: 0aaf4dfc8062e7a14efa1b7ccfd1f2dc8ac7e86b53f79c31500019cfae3ab97d
5
5
  SHA512:
6
- metadata.gz: 8e04f898757e9fd035eac7f5da81e5de7a93a2d492f3039a6c97c2b88610fcce1f86af7c3ddc734ae941f6cfcebe05361dfe442826f7dcf792d0d6cb9a88b542
7
- data.tar.gz: 9446b060debec76ebb08765cb7c16b8c16bb1d17b50d85da3543631ab2c5c9c32671033bdfd9bd811cda0d735c454a2a2e803f30ad812aebb7ea253e7e8cc2d2
6
+ metadata.gz: f04d9cf50ddd7a95843676be55deb850201dea08ac7ef33bc6e7adfc17befe7e8ef14cc79a7899a2e529e058beea1930895344dfba915f6c3b8063f0faca5d49
7
+ data.tar.gz: 43ff857b123ca2c5fa85bb03dc707ea4a52c0601bfc3eab1fc94863136298a12614315b2b369569c74071e97ab88525599780bbaa344a862d43a0b285c0da004
data/CHANGELOG.md CHANGED
@@ -1,7 +1,12 @@
1
- ## Unreleased ([changes](https://github.com/infertux/bashcov/compare/v4.0.1...master))
1
+ ## Unreleased ([changes](https://github.com/infertux/bashcov/compare/v4.0.2...master))
2
2
 
3
3
  * TBD
4
4
 
5
+ ## v4.0.2, 2026-11-04 ([changes](https://github.com/infertux/bashcov/compare/v4.0.1...v4.0.2))
6
+
7
+ * [BUGFIX] Keep xtrace writes blocking so coverage records are not dropped (fixes #113)
8
+ * [BUGFIX] Skip malformed xtrace records instead of aborting coverage (fixes #112)
9
+
5
10
  ## v4.0.1, 2026-10-26 ([changes](https://github.com/infertux/bashcov/compare/v4.0.0...v4.0.1))
6
11
 
7
12
  * [BUGFIX] Fix coverage of multiline strings on Bash < 5.3 (fixes #108)
data/Gemfile.nix.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- bashcov (4.0.1)
4
+ bashcov (4.0.2)
5
5
  simplecov (~> 1.1)
6
6
 
7
7
  GEM
data/flake.nix CHANGED
@@ -206,7 +206,7 @@
206
206
  pname = "bashcov";
207
207
  gemdir = self;
208
208
  lockfile = ./Gemfile.nix.lock;
209
- version = "4.0.1";
209
+ version = "4.0.2";
210
210
  postBuild = ''
211
211
  for gem in $out/${ruby.gemPath}/bundler/gems/*; do
212
212
  ln -sfrT "$gem" $out/${ruby.gemPath}/gems/"''${gem##*/}"
data/gemset.nix CHANGED
@@ -38,7 +38,7 @@
38
38
  path = ./.;
39
39
  type = "path";
40
40
  };
41
- version = "4.0.1";
41
+ version = "4.0.2";
42
42
  };
43
43
  benchmark = {
44
44
  groups = ["development"];
@@ -29,17 +29,19 @@ module Bashcov
29
29
  # @param [Regexp] start_match a +Regexp+ that, when matched against the
30
30
  # input stream, signifies the beginning of the next series of fields to
31
31
  # yield
32
- # @yieldparam [String] field each field extracted from the stream. If
33
- # +start_match+ is matched with fewer than +field_count+ fields yielded
34
- # since the last match, yields empty strings until +field_count+ is
35
- # reached.
32
+ # @yieldparam [String] field each field extracted from the stream. Exactly
33
+ # +field_count+ fields are yielded for each +start_match+ match: if fewer
34
+ # fields follow the match, yields empty strings until +field_count+ is
35
+ # reached; if more follow (e.g. a traced command that contains the
36
+ # delimiter), the extra fields are dropped so the next series of fields
37
+ # stays aligned.
36
38
  def each(delimiter, field_count, start_match, &block)
37
39
  return enum_for(__method__, delimiter, field_count, start_match) unless block_given?
38
40
 
39
41
  chunked = each_field(delimiter).chunk(&chunk_matches(start_match))
40
42
 
41
43
  yield_fields = lambda do |(_, chunk)|
42
- chunk.each(&block)
44
+ chunk.first(field_count).each(&block)
43
45
  (field_count - chunk.size).times { yield "" }
44
46
  end
45
47
 
@@ -57,25 +57,16 @@ module Bashcov
57
57
  with_xtrace_flag do
58
58
  command_pid = Process.spawn env, *@command, options # spawn the command
59
59
 
60
- begin
61
- # start processing the xtrace output
62
- xtrace_thread = Thread.new { @xtrace.read }
60
+ # start processing the xtrace output
61
+ xtrace_thread = Thread.new { @xtrace.read }
63
62
 
64
- Process.wait command_pid
63
+ Process.wait command_pid
65
64
 
66
- @xtrace.close
65
+ @xtrace.close
67
66
 
68
- @coverage = xtrace_thread.value # wait for the thread to return
69
- rescue XtraceError => e
70
- write_warning <<-WARNING
71
- encountered an error parsing Bash's output (error was:
72
- #{e.message}). This can occur if your script or its path contains
73
- the sequence #{Xtrace.delimiter.inspect}, or if your script unsets
74
- LINENO. Aborting early; coverage report will be incomplete.
75
- WARNING
67
+ @coverage = xtrace_thread.value # wait for the thread to return
76
68
 
77
- @coverage = e.files
78
- end
69
+ warn_skipped_records
79
70
  end
80
71
 
81
72
  temp_file&.unlink
@@ -97,6 +88,19 @@ module Bashcov
97
88
 
98
89
  private
99
90
 
91
+ def warn_skipped_records
92
+ return if @xtrace.skipped_count.zero?
93
+
94
+ write_warning <<-WARNING
95
+ skipped #{@xtrace.skipped_count} malformed line(s) in Bash's output
96
+ (first error was: #{@xtrace.first_skip_reason}). This can occur if your
97
+ script prints its own PS4 (e.g. with `declare -p` or `set`), if your
98
+ script or its path contains the sequence #{Xtrace.delimiter.inspect},
99
+ or if your script unsets LINENO. Coverage of the affected lines may be
100
+ incomplete.
101
+ WARNING
102
+ end
103
+
100
104
  def write_warning(message)
101
105
  warn [
102
106
  Bashcov.program_name,
@@ -3,5 +3,5 @@
3
3
  # :nodoc:
4
4
  module Bashcov
5
5
  # Current Bashcov version
6
- VERSION = "4.0.1"
6
+ VERSION = "4.0.2"
7
7
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "io/nonblock"
3
4
  require "pathname"
4
5
  require "securerandom"
5
6
 
@@ -57,6 +58,8 @@ module Bashcov
57
58
  @field_stream = field_stream
58
59
 
59
60
  @read, @write = IO.pipe
61
+ # Keep Bash's xtrace writes blocking so records are not dropped when the pipe fills.
62
+ @write.nonblock = false
60
63
 
61
64
  # Tracks coverage for each file under test
62
65
  @files ||= {}
@@ -64,22 +67,35 @@ module Bashcov
64
67
  # Stacks for updating working directory changes
65
68
  @pwd_stack ||= []
66
69
  @oldpwd_stack ||= []
70
+
71
+ @skipped_count = 0
67
72
  end
68
73
 
74
+ # [Integer] Number of malformed +PS4+ records skipped by {#read}
75
+ attr_reader :skipped_count
76
+
77
+ # [String, nil] Why the first malformed +PS4+ record was skipped
78
+ attr_reader :first_skip_reason
79
+
69
80
  # @return [Fixnum] File descriptor of the write end of the pipe
70
81
  def file_descriptor
71
82
  @write.fileno
72
83
  end
73
84
 
74
- # Closes the pipe for writing.
85
+ # Closes the parent's write end after the command exits, also ending any
86
+ # error-recovery drain without waiting for inherited background writers.
75
87
  # @return [void]
76
88
  def close
77
89
  @write.close
90
+ @read.close if @draining
78
91
  end
79
92
 
80
93
  # Read fields extracted from Bash's debugging output
81
94
  # @return [Hash<Pathname, Array<Integer, nil>>] A hash mapping Bash scripts
82
95
  # to Simplecov-style coverage stats
96
+ # @raise [StandardError] any error while parsing, raised only after
97
+ # draining until the command exits or the pipe reaches EOF. Malformed
98
+ # records are skipped (see {#skipped_count}) rather than raised.
83
99
  def read
84
100
  @field_stream.read = @read
85
101
 
@@ -96,36 +112,44 @@ module Bashcov
96
112
  parse_hit!(*hit)
97
113
  end
98
114
 
99
- @read.close unless @read.closed?
100
-
101
115
  @files
116
+ rescue StandardError
117
+ # Keep draining so Bash's blocking xtrace writes cannot fill the pipe and
118
+ # hang the run, or hit a closed pipe and die of SIGPIPE. #close ends the
119
+ # drain once the command exits, so background writers are not waited on.
120
+ @draining = true
121
+ begin
122
+ nil while !@write.closed? && @read.read(65_536)
123
+ rescue IOError
124
+ raise unless @write.closed? # Only ignore closure after the command exits.
125
+ end
126
+ raise
127
+ ensure
128
+ @read.close unless @read.closed?
102
129
  end
103
130
 
104
131
  private
105
132
 
106
133
  # Parses the expanded {ps4} fields and updates the coverage-tracking
107
- # {@files} hash
134
+ # {@files} hash. A record whose +LINENO+ is not composed solely of digits
135
+ # is skipped: it is usually text that merely looks like the start of a
136
+ # {ps4}, such as the output of +declare -p+ or +set+ being traced, and
137
+ # {FieldStream#each} resynchronizes on the next start of a {ps4}.
108
138
  # @overload parse_hit!(lineno, bash_source, pwd, oldpwd)
109
139
  # @param [String] lineno expanded +LINENO+
110
140
  # @param [Pathname] bash_source expanded +BASH_SOURCE+
111
141
  # @param [Pathname] pwd expanded +PWD+
112
142
  # @param [Pathname] oldpwd expanded +OLDPWD+
113
143
  # @return [void]
114
- # @raise [XtraceError] when +lineno+ is not composed solely of digits,
115
- # indicating that something has gone wrong with parsing the +PS4+ fields
116
144
  def parse_hit!(lineno, *paths)
117
- # if +LINENO+ isn't a series of digits, add +@files+ to the exception in
118
- # order to propagate the existing coverage data back to the
119
- # {Bashcov::Runner} instance
120
145
  if /\A\d+\z/.match?(lineno)
121
146
  lineno = lineno.to_i
122
147
  elsif lineno == "${LINENO-}"
123
148
  # the variable doesn't expand on line misses so we can safely ignore it
124
149
  return
125
150
  else
126
- raise XtraceError.new(
127
- "expected integer for LINENO, got #{lineno.inspect}", @files,
128
- )
151
+ skip_record!("expected integer for LINENO, got #{lineno.inspect}")
152
+ return
129
153
  end
130
154
 
131
155
  bash_source, pwd, oldpwd = paths.map { |p| Pathname.new(p) }
@@ -143,6 +167,14 @@ module Bashcov
143
167
  @files[script][index] += 1
144
168
  end
145
169
 
170
+ # Records that a malformed record was skipped
171
+ # @param [String] reason why the record was skipped
172
+ # @return [void]
173
+ def skip_record!(reason)
174
+ @skipped_count += 1
175
+ @first_skip_reason = reason if @skipped_count == 1
176
+ end
177
+
146
178
  # Scans entries in the +PWD+ stack, checking whether +entry/$BASH_SOURCE+
147
179
  # refers to an existing file. Scans the stack in reverse on the assumption
148
180
  # that more-recent entries are more plausible candidates for base
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bashcov
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.1
4
+ version: 4.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cédric Félizard
@@ -268,7 +268,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
268
268
  - !ruby/object:Gem::Version
269
269
  version: '0'
270
270
  requirements: []
271
- rubygems_version: 4.0.22
271
+ rubygems_version: 4.0.17
272
272
  specification_version: 4
273
273
  summary: Code coverage tool for Bash
274
274
  test_files: []