-
-
Notifications
You must be signed in to change notification settings - Fork 21
PS4 with subshells causes erroneous extra hits to be registered #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ee5e5d0
d810cca
bd85f6c
63476f2
4989134
67e0807
b082e58
460147c
d53db13
d5f13b3
e224937
c071c0e
bbf0e67
e56d0d5
073ddeb
a529aa2
f893e0c
1c7e70c
27b3fe0
47911cc
2c27260
2539e7a
0ff7d0c
9060ed2
e5a2f94
0705146
5856369
1bfe652
5ad4dfc
2037468
9cb87ca
c45e674
949dcb8
bf8af04
00df6f0
e060601
9eef89d
4612a79
33d32e4
4811632
0d5864b
ff27e24
8b9fc66
66e02ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ doc/ | |
lib/bundler/man | ||
pkg | ||
rdoc | ||
.rubocop-* | ||
spec/reports | ||
test/tmp | ||
test/version_tmp | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,27 +1,6 @@ | ||
Metrics/AbcSize: | ||
Enabled: false | ||
|
||
Metrics/MethodLength: | ||
Enabled: false | ||
|
||
Metrics/LineLength: | ||
Max: 120 | ||
Exclude: [spec/**/*] | ||
|
||
Style/AccessModifierIndentation: | ||
EnforcedStyle: outdent | ||
|
||
Style/AndOr: | ||
EnforcedStyle: conditionals | ||
|
||
Style/SignalException: | ||
EnforcedStyle: only_raise | ||
inherit_from: | ||
- https://gist.githubusercontent.com/infertux/cdd2ccc6e0a0cd94f458/raw | ||
|
||
Style/SpecialGlobalVars: | ||
Enabled: false | ||
|
||
Style/StringLiterals: | ||
EnforcedStyle: double_quotes | ||
|
||
Style/TrailingComma: | ||
Enabled: false |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,48 +1,71 @@ | ||
require "optparse" | ||
require "ostruct" | ||
require "bashcov/version" | ||
require "bashcov/lexer" | ||
require "bashcov/line" | ||
require "pathname" | ||
|
||
require "bashcov/bash_info" | ||
require "bashcov/runner" | ||
require "bashcov/xtrace" | ||
require "bashcov/version" | ||
|
||
# Bashcov default module | ||
# @note Keep it short! | ||
module Bashcov | ||
class << self | ||
# @return [OpenStruct] Bashcov settings | ||
attr_reader :options | ||
|
||
# @return [String] The project's root directory | ||
def root_directory | ||
@root_directory ||= Dir.pwd | ||
end | ||
extend Bashcov::BashInfo | ||
|
||
# Sets default options overriding any existing ones. | ||
# @return [void] | ||
def set_default_options! | ||
@options ||= OpenStruct.new | ||
@options.skip_uncovered = false | ||
@options.mute = false | ||
class << self | ||
# @return [OpenStruct] The +OpenStruct+ object representing Bashcov's | ||
# execution environment | ||
def options | ||
set_default_options! unless defined?(@options) | ||
@options | ||
end | ||
|
||
# Parses the given CLI arguments and sets +options+. | ||
# @param [Array] args list of arguments | ||
# @raise [SystemExit] if invalid arguments are given | ||
# @return [void] | ||
def parse_options!(args) | ||
option_parser.parse!(args) | ||
begin | ||
option_parser.parse!(args) | ||
rescue OptionParser::ParseError, Errno::ENOENT => e | ||
abort "#{option_parser.program_name}: #{e.message}" | ||
end | ||
|
||
if args.empty? | ||
abort("You must give exactly one command to execute.") | ||
else | ||
@options.command = args.join(" ") | ||
options.command = args.unshift(bash_path) | ||
end | ||
end | ||
|
||
# @return [String] Program name | ||
def program_name | ||
"bashcov" | ||
end | ||
|
||
# @return [String] Program name including version for easy consistent output | ||
def name | ||
"bashcov v#{VERSION}" | ||
# @note +fullname+ instead of name to avoid clashing with +Module.name+ | ||
def fullname | ||
"#{program_name} v#{VERSION}" | ||
end | ||
|
||
# Wipe the current options and reset default values | ||
def set_default_options! | ||
@options = OpenStruct.new | ||
|
||
@options.root_directory = Dir.getwd | ||
@options.skip_uncovered = false | ||
@options.bash_path = "/bin/bash" | ||
@options.mute = false | ||
end | ||
|
||
# Passes off +respond_to?+ to {options} for missing methods | ||
def respond_to_missing?(*args) | ||
options.respond_to?(*args) | ||
end | ||
|
||
# Dispatches missing methods to {options} | ||
def method_missing(method_name, *args, &block) | ||
options.send(method_name, *args, &block) | ||
end | ||
|
||
private | ||
|
@@ -60,17 +83,25 @@ def help(program_name) | |
|
||
def option_parser | ||
OptionParser.new do |opts| | ||
opts.program_name = "bashcov" | ||
opts.program_name = program_name | ||
opts.version = Bashcov::VERSION | ||
opts.banner = help opts.program_name | ||
|
||
opts.separator "\nSpecific options:" | ||
|
||
opts.on("-s", "--skip-uncovered", "Do not report uncovered files") do |s| | ||
@options.skip_uncovered = s | ||
options.skip_uncovered = s | ||
end | ||
opts.on("-m", "--mute", "Do not print script output") do |m| | ||
@options.mute = m | ||
options.mute = m | ||
end | ||
opts.on("--bash-path PATH", "Path to Bash executable") do |p| | ||
raise Errno::ENOENT, p unless File.file? p | ||
options.bash_path = p | ||
end | ||
opts.on("--root PATH", "Project root directory") do |d| | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When would we need to set this by hand? Is there any usecase when we can't rely on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I found this a useful shortcut when making changes to Bashcov and testing them against shell scripts in other directories. But that's a pretty slender use case! TBH the reason I added this was because I needed to make the project root configurable in order to properly test the temporary scripts created by the test suite, and I figured that there'd be little harm in exposing the setting as a command line option. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good, in fact I just remembered this feature was also suggested in #13 so let's keep it! |
||
raise Errno::ENOENT, d unless File.directory? d | ||
options.root_directory = d | ||
end | ||
|
||
opts.separator "\nCommon options:" | ||
|
@@ -86,6 +117,3 @@ def option_parser | |
end | ||
end | ||
end | ||
|
||
# Make sure default options are set | ||
Bashcov.set_default_options! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
module Bashcov | ||
# Module exposing information concerning the installed Bash version | ||
# @note methods do not cache results because +bash_path+ can change at | ||
# runtime | ||
# @note receiver is expected to implement +bash_path+ | ||
module BashInfo | ||
# @return [Array<String>] An array representing the components of | ||
# +BASH_VERSINFO+ | ||
def bash_versinfo | ||
`#{bash_path} -c 'echo "${BASH_VERSINFO[@]}"'`.chomp.split | ||
end | ||
|
||
# @return [Boolean] Whether Bash supports +BASH_XTRACEFD+ | ||
def bash_xtracefd? | ||
bash_versinfo[0..1].join.to_i >= 41 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm tempted to NOT support very old versions of Bash. 4.1 was shipped up to Debian Squeeze which was removed from security support this month. In this case, it doesn't add many There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Supporting older Bash versions is definitely a drag! My main motivation here is supporting CentOS 6.7, which ships with |
||
end | ||
|
||
# @param [Integer] length the number of bytes to test; default 128 | ||
# @return [Boolean] whether Bash supports a +PS4+ of at least a given | ||
# number of bytes | ||
# @see https://tiswww.case.edu/php/chet/bash/CHANGES | ||
# @note Item +i.+ under the +bash-4.2-release+ to +bash-4.3-alpha+ change | ||
# list notes that version 4.2 truncates +PS4+ if it is greater than 128 | ||
# bytes. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Along the lines of my comment above, I don't think it's worth adding a lot of complexity to the codebase to work around one-off Bash bugs. Since you've already worked on it and Bash 4.2 is still widely used, it's okay to keep it of course. I'm just talking in general :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree in full -- so vehemently so that I considered seeing whether it would be possible to modify the Travis build to install Bash 4.3 :). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I was thinking we should be able to do something like that with Travis build matrix by maybe listing Bash versions we wanna test against through environment variables. No need to do that now though I reckon. |
||
def truncated_ps4?(length = 128) | ||
ps4 = SecureRandom.base64(length) | ||
!`PS4=#{ps4} #{bash_path} 2>&1 1>&- -xc 'echo hello'`.start_with?(ps4) | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
module Bashcov | ||
# Signals an error parsing Bash's debugging output. | ||
class XtraceError < ::RuntimeError | ||
# Will contain the coverages parsed prior to the error | ||
attr_reader :files | ||
|
||
# @param [#to_s] message An error message | ||
# @param [Hash] files A hash containing coverage information | ||
def initialize(message, files) | ||
@files = files | ||
super(message) | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
module Bashcov | ||
# Classes for streaming token-delimited fields | ||
class FieldStream | ||
attr_accessor :read | ||
|
||
# @param [IO] read an IO object opened for reading | ||
def initialize(read = nil) | ||
@read = read | ||
end | ||
|
||
# A convenience wrapper around +each_line(sep)+ that also does | ||
# +chomp(sep)+ on the yielded line. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick: should be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice catch; thanks! |
||
# @param [String, nil] delim the field separator for the stream | ||
# @return [void] | ||
# @yieldparam [String] field each +chomp+ed line | ||
def each_field(delim) | ||
return enum_for(__method__, delim) unless block_given? | ||
|
||
read.each_line(delim) do |line| | ||
yield line.chomp(delim) | ||
end | ||
end | ||
|
||
# Yields fields extracted from a input stream | ||
# @param [String, nil] delim the field separator | ||
# @param [Integer] field_count the number of fields to extract | ||
# @param [Regexp] start_match a +Regexp+ that, when matched against the | ||
# input stream, signifies the beginning of the next series of fields to | ||
# yield | ||
# @yieldparam [String] field each field extracted from the stream. If | ||
# +start_match+ is matched with fewer than +field_count+ fields yielded | ||
# since the last match, yields empty strings until +field_count+ is | ||
# reached. | ||
def each(delim, field_count, start_match) | ||
return enum_for(__method__, delim, field_count, start_match) unless block_given? | ||
|
||
# Whether the current field is the start-of-fields match | ||
matched_start = nil | ||
|
||
# The number of fields processed since passing the last start-of-fields | ||
# match | ||
seen_fields = 0 | ||
|
||
fields = each_field(delim) | ||
|
||
# Close over +field_count+ and +seen_fields+ to yield empty strings to | ||
# the caller when we've already hit the next start-of-fields match | ||
yield_remaining = -> { (field_count - seen_fields).times { yield "" } } | ||
|
||
# Advance until the first start-of-fields match | ||
loop { break if fields.next =~ start_match } | ||
|
||
fields.each do |field| | ||
# If the current field is the start-of-fields match... | ||
if field =~ start_match | ||
# Fill out any remaining (unparseable) fields with empty strings | ||
yield_remaining.call | ||
|
||
matched_start = nil | ||
seen_fields = 0 | ||
elsif seen_fields < field_count | ||
yield field | ||
seen_fields += 1 | ||
end | ||
end | ||
|
||
# One last filling-out of empty fields if we're at the end of the stream | ||
yield_remaining.call | ||
|
||
read.close unless read.closed? | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty sure
respond_to_missing?
andmethod_missing
should be private.