class Bashcov::Xtrace
This class manages xtrace
output.
@see Runner
Constants
- DEPTH_CHAR
- String
-
Character that will be used to indicate the nesting level of
+xtrace+d instructions
- FIELDS
- Array<String>
-
A collection of Bash internal variables to expand in the
{PS4}
- PREFIX
- String
-
Prefix used in
PS4
to identify relevant output
- PS4_START_REGEXP
Regexp to match the beginning of the {.ps4}. {DEPTH_CHAR} will be repeated in proportion to the level of Bash call nesting.
Attributes
Public Class Methods
- String
-
A randomly-generated UUID used for delimiting the fields of
the PS4
.
# File lib/bashcov/xtrace.rb, line 29 def delimiter @delimiter ||= SecureRandom.uuid end
@return [String] a {delimiter}-separated String
suitable for use as
+PS4+
# File lib/bashcov/xtrace.rb, line 43 def make_ps4(*fields) fields.reduce(DEPTH_CHAR + PREFIX) do |memo, field| memo + delimiter + field end + delimiter end
Creates a pipe for xtrace output. @see stackoverflow.com/questions/6977562/pipe-vs-temporary-file
# File lib/bashcov/xtrace.rb, line 56 def initialize(field_stream) @field_stream = field_stream @read, @write = IO.pipe # Tracks coverage for each file under test @files ||= {} # Stacks for updating working directory changes @pwd_stack ||= [] @oldpwd_stack ||= [] end
@return [String] PS4
variable used for xtrace output. Expands to
internal Bash variables +BASH_SOURCE+, +PWD+, +OLDPWD+, and +LINENO+, delimited by {delimiter}.
@see www.gnu.org/software/bash/manual/bashref.html#index-PS4
# File lib/bashcov/xtrace.rb, line 37 def ps4 @ps4 ||= make_ps4(*FIELDS) end
Public Instance Methods
Closes the pipe for writing. @return [void]
# File lib/bashcov/xtrace.rb, line 76 def close @write.close end
@return [Fixnum] File descriptor of the write end of the pipe
# File lib/bashcov/xtrace.rb, line 70 def file_descriptor @write.fileno end
Read fields extracted from Bash’s debugging output @return [Hash<Pathname, Array<Integer, nil>>] A hash mapping Bash scripts
to Simplecov-style coverage stats
# File lib/bashcov/xtrace.rb, line 83 def read @field_stream.read = @read field_count = FIELDS.length fields = @field_stream.each( self.class.delimiter, field_count, PS4_START_REGEXP ) # +take(field_count)+ would be more natural here, but doesn't seem to # play nicely with +Enumerator+s backed by +IO+ objects. loop do break if (hit = (1..field_count).map { fields.next }).empty? parse_hit!(*hit) end @read.close unless @read.closed? @files end
Private Instance Methods
Scans entries in the PWD
stack, checking whether +entry/$BASH_SOURCE+ refers to an existing file. Scans the stack in reverse on the assumption that more-recent entries are more plausible candidates for base directories from which BASH_SOURCE
can be reached. @param [Pathname] bash_source expanded BASH_SOURCE
@return [Pathname] the resolved path to bash_source
, if it exists;
otherwise, +bash_source+ cleaned of redundant slashes and dots
# File lib/bashcov/xtrace.rb, line 154 def find_script(bash_source) script = @pwd_stack.reverse.map { |wd| wd + bash_source }.find(&:file?) return bash_source.cleanpath if script.nil? begin script.realpath rescue Errno::ENOENT # catch race condition if the file has been deleted bash_source.cleanpath end end
Parses the expanded {ps4} fields and updates the coverage-tracking {@files} hash @overload parse_hit!(lineno, bash_source, pwd, oldpwd)
@param [String] lineno expanded +LINENO+ @param [Pathname] bash_source expanded +BASH_SOURCE+ @param [Pathname] pwd expanded +PWD+ @param [Pathname] oldpwd expanded +OLDPWD+
@return [void] @raise [XtraceError] when lineno
is not composed solely of digits,
indicating that something has gone wrong with parsing the +PS4+ fields
# File lib/bashcov/xtrace.rb, line 116 def parse_hit!(lineno, *paths) # If +LINENO+ isn't a series of digits, something has gone wrong. Add # +@files+ to the exception in order to propagate the existing coverage # data back to the {Bashcov::Runner} instance. if /\A\d+\z/.match?(lineno) lineno = lineno.to_i elsif lineno == "${LINENO-}" # the variable doesn't expand on line misses so we can safely ignore it return else raise XtraceError.new( "expected integer for LINENO, got #{lineno.inspect}", @files ) end # The next three fields will be $BASH_SOURCE, $PWD, $OLDPWD, and $LINENO bash_source, pwd, oldpwd = paths.map { |p| Pathname.new(p) } update_wd_stacks!(pwd, oldpwd) script = find_script(bash_source) # For one-liners, +LINENO+ == 0. Do this to avoid an +IndexError+; # one-liners will be culled from the coverage results later on. index = (lineno > 1 ? lineno - 1 : 0) @files[script] ||= [] @files[script][index] ||= 0 @files[script][index] += 1 end
Updates the stacks that track the history of values for PWD
and OLDPWD
@param [Pathname] pwd expanded PWD
@param [Pathname] oldpwd expanded OLDPWD
@return [void]
# File lib/bashcov/xtrace.rb, line 171 def update_wd_stacks!(pwd, oldpwd) @pwd_stack[0] ||= pwd @oldpwd_stack[0] ||= oldpwd unless oldpwd.to_s.empty? # We haven't changed working directories; short-circuit. return if pwd == @pwd_stack[-1] # If the current +pwd+ is identical to the top of the +@oldpwd_stack+ and # the current +oldpwd+ is identical to the second-to-top entry, then a # previous cd/pushd has been undone. if pwd == @oldpwd_stack[-1] && oldpwd == @oldpwd_stack[-2] @pwd_stack.pop @oldpwd_stack.pop else # New cd/pushd @pwd_stack << pwd @oldpwd_stack << oldpwd end end