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 +4 -4
- data/CHANGELOG.md +6 -1
- data/Gemfile.nix.lock +1 -1
- data/flake.nix +1 -1
- data/gemset.nix +1 -1
- data/lib/bashcov/field_stream.rb +7 -5
- data/lib/bashcov/runner.rb +19 -15
- data/lib/bashcov/version.rb +1 -1
- data/lib/bashcov/xtrace.rb +44 -12
- 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: e79dc939af127bef6f99cd953e085033f167a5221e332a133951e476f9b63f6e
|
|
4
|
+
data.tar.gz: 0aaf4dfc8062e7a14efa1b7ccfd1f2dc8ac7e86b53f79c31500019cfae3ab97d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
+
## 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
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.
|
|
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
data/lib/bashcov/field_stream.rb
CHANGED
|
@@ -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.
|
|
33
|
-
# +
|
|
34
|
-
#
|
|
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
|
|
data/lib/bashcov/runner.rb
CHANGED
|
@@ -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
|
-
|
|
61
|
-
|
|
62
|
-
xtrace_thread = Thread.new { @xtrace.read }
|
|
60
|
+
# start processing the xtrace output
|
|
61
|
+
xtrace_thread = Thread.new { @xtrace.read }
|
|
63
62
|
|
|
64
|
-
|
|
63
|
+
Process.wait command_pid
|
|
65
64
|
|
|
66
|
-
|
|
65
|
+
@xtrace.close
|
|
67
66
|
|
|
68
|
-
|
|
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
|
-
|
|
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,
|
data/lib/bashcov/version.rb
CHANGED
data/lib/bashcov/xtrace.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
127
|
-
|
|
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.
|
|
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.
|
|
271
|
+
rubygems_version: 4.0.17
|
|
272
272
|
specification_version: 4
|
|
273
273
|
summary: Code coverage tool for Bash
|
|
274
274
|
test_files: []
|