bashcov 1.2.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -3
  3. data/Gemfile +2 -0
  4. data/Guardfile +2 -0
  5. data/LICENSE.txt +1 -1
  6. data/README.md +60 -5
  7. data/Rakefile +2 -0
  8. data/bashcov.gemspec +4 -3
  9. data/bin/bashcov +3 -2
  10. data/lib/bashcov/bash_info.rb +32 -0
  11. data/lib/bashcov/errors.rb +16 -0
  12. data/lib/bashcov/field_stream.rb +77 -0
  13. data/lib/bashcov/lexer.rb +13 -7
  14. data/lib/bashcov/line.rb +4 -2
  15. data/lib/bashcov/runner.rb +96 -21
  16. data/lib/bashcov/version.rb +4 -1
  17. data/lib/bashcov/xtrace.rb +150 -35
  18. data/lib/bashcov.rb +64 -31
  19. metadata +9 -58
  20. data/.gitignore +0 -19
  21. data/.rspec +0 -4
  22. data/.rubocop.yml +0 -27
  23. data/.travis.yml +0 -21
  24. data/spec/bashcov/lexer_spec.rb +0 -11
  25. data/spec/bashcov/runner_spec.rb +0 -98
  26. data/spec/bashcov_spec.rb +0 -82
  27. data/spec/spec_helper.rb +0 -25
  28. data/spec/support/common.rb +0 -7
  29. data/spec/support/test_app.rb +0 -35
  30. data/spec/test_app/.simplecov +0 -2
  31. data/spec/test_app/README.md +0 -1
  32. data/spec/test_app/never_called.sh +0 -4
  33. data/spec/test_app/scripts/README.md +0 -1
  34. data/spec/test_app/scripts/case.sh +0 -15
  35. data/spec/test_app/scripts/delete.sh +0 -9
  36. data/spec/test_app/scripts/executable +0 -4
  37. data/spec/test_app/scripts/exit_non_zero.sh +0 -3
  38. data/spec/test_app/scripts/function.sh +0 -20
  39. data/spec/test_app/scripts/long_line.sh +0 -7
  40. data/spec/test_app/scripts/multiline.sh +0 -24
  41. data/spec/test_app/scripts/nested/simple.sh +0 -13
  42. data/spec/test_app/scripts/one_liner.sh +0 -9
  43. data/spec/test_app/scripts/simple.sh +0 -13
  44. data/spec/test_app/scripts/source.sh +0 -6
  45. data/spec/test_app/scripts/sourced.txt +0 -4
  46. data/spec/test_app/scripts/unicode.sh +0 -4
  47. data/spec/test_app/test_suite.sh +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a3fa863e6848203b79a712e22697741dbe3f40e8
4
- data.tar.gz: 5a145fc486a1d5ddf0bc09db6d0e0cba65e82bb3
3
+ metadata.gz: f2fc246b18fc2cbae42869d11c95270f5a5343b4
4
+ data.tar.gz: a82eae685fd50447bc27e5fe73eff1ff48513f1d
5
5
  SHA512:
6
- metadata.gz: 3d6dec21c6e9619988ebbc8b811a87fbbcdbc5b73d07405f7ba50e281e81ea8371e45bca945425acaf433fa4eba673f7f68fd84bb08cba7c546e6d6590952854
7
- data.tar.gz: 34257113bfc309bb1c7e374c2f23170685c0f5ca59c6b718354a7d0f61dca898ed3979f008c199c35b921fe8a12c804d5ef19aeeacf4b3e9d45df2c172c0d12c
6
+ metadata.gz: 1e417c499432768996d30fac933516aa2baa52ac4087eec92d0a36c74b8a8df74d55331a3548e4c8e1f3cef9c80e65c6f2deac5d7d9d3629fedf518a92dff85e
7
+ data.tar.gz: e2f657b4097abcc4973ba66d81ec26e00ef29c881d761f0b02f3b5c1f467d3a6d2c0a11075c4c62115859aacfff9ff78e340043cf51ff0d337f2fa77d0d2fcf9
data/CHANGELOG.md CHANGED
@@ -1,7 +1,20 @@
1
- ## Unreleased ([changes](https://github.com/infertux/bashcov/compare/v1.2.1...master))
1
+ ## Unreleased ([changes](https://github.com/infertux/bashcov/compare/v1.3.0...master))
2
2
 
3
3
  * TBD
4
4
 
5
+ ## v1.3.0, 2016-02-10 ([changes](https://github.com/infertux/bashcov/compare/v1.2.1...v1.3.0))
6
+
7
+ * [FEATURE] Upgrade SimpleCov dependency to 0.11
8
+ * [FEATURE] Add support for Ruby 2.3 and drop 1.9
9
+ * [FEATURE] Add ability to pass `--bash-path` and `--root` as arguments
10
+ * [FEATURE] Add basic support for Bash versions prior to 4.1 (no `BASH_XTRACEFD`)
11
+ * [FEATURE] Handle `pushd` & `popd` commands
12
+ * [BUGFIX] Fix potential bug with long paths under Bash 4.2 as it truncates `PS4` to 128 characters
13
+ * [BUGFIX] Fail gracefully if a Bash script unsets `LINENO`
14
+ * [BUGFIX] Refactor parser to not use subshells in `PS4` as it causes erroneous extra hits as well as being slow (classes `FieldStream` & `Xtrace`)
15
+ Big kudos to @BaxterStockman for his awesome work on PR #16
16
+ See https://github.com/infertux/bashcov/#some-gory-details
17
+
5
18
  ## v1.2.1, 2015-05-05 ([changes](https://github.com/infertux/bashcov/compare/v1.2.0...v1.2.1))
6
19
 
7
20
  * [BUGFIX] Preserve original exit status when exiting Bashcov
@@ -18,8 +31,8 @@
18
31
 
19
32
  ## v1.0.1, 2013-03-21 ([changes](https://github.com/infertux/bashcov/compare/v1.0.0...v1.0.1))
20
33
 
21
- * [BUGFIX] Allow to add SimpleCov filters.
22
- * [BUGFIX] Lines containing only `elif` should be ignored.
34
+ * [BUGFIX] Allow to add SimpleCov filters
35
+ * [BUGFIX] Lines containing only `elif` should be ignored
23
36
 
24
37
  ## v1.0.0, 2013-03-16 ([changes](https://github.com/infertux/bashcov/compare/v0.0.9...v1.0.0))
25
38
 
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
 
3
5
  # gem 'simplecov', path: '../simplecov'
data/Guardfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # More info at https://github.com/guard/guard#readme
2
4
 
3
5
  guard "rspec", cli: "--tag ~speed:slow" do
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012-2015 Cédric Félizard
1
+ Copyright (c) 2012-2016 Cédric Félizard
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -1,4 +1,10 @@
1
- # Bashcov [![Build Status](https://secure.travis-ci.org/infertux/bashcov.png?branch=master)](https://travis-ci.org/infertux/bashcov) [![Dependency Status](https://gemnasium.com/infertux/bashcov.png)](https://gemnasium.com/infertux/bashcov) [![Code Climate](https://codeclimate.com/github/infertux/bashcov.png)](https://codeclimate.com/github/infertux/bashcov) [![Coverage Status](https://coveralls.io/repos/infertux/bashcov/badge.png?branch=master)](https://coveralls.io/r/infertux/bashcov) [![Gem Version](http://img.shields.io/gem/v/bashcov.svg)](https://rubygems.org/gems/bashcov)
1
+ # Bashcov
2
+
3
+ [![Build Status](https://secure.travis-ci.org/infertux/bashcov.png?branch=master)](https://travis-ci.org/infertux/bashcov)
4
+ [![Dependency Status](https://gemnasium.com/infertux/bashcov.png)](https://gemnasium.com/infertux/bashcov)
5
+ [![Code Climate](https://codeclimate.com/github/infertux/bashcov.png)](https://codeclimate.com/github/infertux/bashcov)
6
+ [![Coverage Status](https://coveralls.io/repos/infertux/bashcov/badge.png?branch=master)](https://coveralls.io/r/infertux/bashcov)
7
+ [![Gem Version](https://img.shields.io/gem/v/bashcov.svg)](https://rubygems.org/gems/bashcov)
2
8
 
3
9
  **Code coverage for Bash**
4
10
 
@@ -20,11 +26,12 @@
20
26
  [Dependencies]: https://gemnasium.com/infertux/bashcov "Bashcov dependencies on Gemnasium"
21
27
  [Bashcov]: https://github.com/infertux/bashcov
22
28
  [SimpleCov]: https://github.com/colszowka/simplecov "Bashcov is backed by SimpleCov to generate awesome coverage report"
29
+ [Test app demo]: https://infertux.github.com/bashcov/test_app/ "Coverage for the bundled test application"
23
30
 
24
31
  You should check out these coverage examples - it's worth a thousand words:
25
32
 
26
- - [Test app demo](http://infertux.github.com/bashcov/test_app/ "Coverage for the bundled test application")
27
- - [RVM demo](http://infertux.github.com/bashcov/rvm/ "Coverage for RVM")
33
+ - [Test app demo]
34
+ - [RVM demo](https://infertux.github.com/bashcov/rvm/ "Coverage for RVM")
28
35
 
29
36
  ## Installation
30
37
 
@@ -48,8 +55,56 @@ Open `./coverage/index.html` to browse the coverage report.
48
55
 
49
56
  ### SimpleCov integration
50
57
 
51
- You can take great advantage of [SimpleCov] by adding a `.simplecov` file in your project's root (like [this](https://github.com/infertux/bashcov/blob/master/spec/test_app/.simplecov)).
52
- See [SimpleCov README](https://github.com/colszowka/simplecov#readme) for more information.
58
+ You can take great advantage of [SimpleCov] by adding a `.simplecov` file in
59
+ your project's root (like [this](https://github.com/infertux/bashcov/blob/master/spec/test_app/.simplecov)).
60
+ See [SimpleCov README](https://github.com/colszowka/simplecov#readme) for more
61
+ information.
62
+
63
+ ### Some gory details
64
+
65
+ Figuring out where an executing Bash script lives in the file system can be
66
+ surprisingly difficult. Bash offers a fair amount of [introspection into its
67
+ internals](https://www.gnu.org/software/bash/manual/html_node/Bash-Variables.html),
68
+ but the location of the current script has to be inferred from the limited
69
+ information available through `BASH_SOURCE`, `PWD`, and `OLDPWD` (and
70
+ potentially `DIRSTACK` if you are using `pushd`/`popd`). For this purpose,
71
+ Bashcov puts Bash in debug mode and sets up a `PS4` that expands the values of
72
+ these variables, reading them on each command that Bash executes. But, given
73
+ that:
74
+
75
+ * `BASH_SOURCE` is only an absolute path if the script was invoked using an
76
+ absolute path,
77
+ * the builtins `cd`, `pushd`, and `popd` alter `PWD` and `OLDPWD`, and
78
+ * none of these variables are read-only and can therefore be `unset` or
79
+ otherwise altered,
80
+
81
+ it can be easy to lose track of where we are.
82
+
83
+ _"Wait a minute, what about `pwd`, `readlink`, and so on?"_ That would be great,
84
+ except that subshells executed as part of expanding the `PS4` can cause Bash to
85
+ report [extra executions](https://github.com/infertux/bashcov/commit/4130874e30a05b7ab6ea66fb96a19acaa973c178)
86
+ for [certain lines](https://github.com/infertux/bashcov/pull/16). Also,
87
+ subshells are slow as the `PS4` is expanded on each and every command when Bash
88
+ is in debug mode.
89
+
90
+ To deal with these limitations, Bashcov uses the expedient of maintaining two
91
+ stacks that track changes to `PWD` and `OLDPWD`. To determine the full path to
92
+ the executing script, Bashcov iterates in reverse over the `PWD` stack, testing
93
+ for the first `$PWD/$BASH_SOURCE` combination that refers to an existing file.
94
+ This heuristic is susceptible to false positives -- under certain combinations
95
+ of directory structure, script invocation paths, and working directory changes,
96
+ it may yield a path that doesn't refer to the currently-running script.
97
+ However, it performs well under the various working directory changes performed
98
+ in the [test app demo] and avoids the spurious extra hits caused by using
99
+ subshells in the `PS4`.
100
+
101
+ One final note on innards: Bash 4.3 fixed a bug in which `PS4` expansion is
102
+ truncated to a maximum of 128 characters. On platforms whose Bash version
103
+ suffers from this bug, Bashcov uses the ASCII record separator character to
104
+ delimit the `PS4` fields, whereas it uses a long random string on Bash 4.3 and
105
+ above. When the field delimiter appears in the path of a script under test or
106
+ in a command the script executes, Bashcov won't correctly parse the `PS4` and
107
+ will abort early with incomplete coverage results.
53
108
 
54
109
  ## Contributing
55
110
 
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
4
 
3
5
  require "rspec/core/rake_task"
data/bashcov.gemspec CHANGED
@@ -1,4 +1,5 @@
1
- # -*- encoding: utf-8 -*-
1
+ # frozen_string_literal: true
2
+
2
3
  lib = File.expand_path("../lib", __FILE__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require "bashcov/version"
@@ -13,12 +14,12 @@ Gem::Specification.new do |gem|
13
14
  gem.homepage = "https://github.com/infertux/bashcov"
14
15
  gem.license = "MIT"
15
16
 
16
- gem.files = `git ls-files`.split($/)
17
+ gem.files = `git ls-files -z`.split("\x0").reject { |f| f.start_with?(".") || f.match(%r{\A(test|spec|features)/}) }
17
18
  gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
18
19
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
20
  gem.require_paths = ["lib"]
20
21
 
21
- gem.add_dependency "simplecov", "~> 0.10.0"
22
+ gem.add_dependency "simplecov", "~> 0.11"
22
23
 
23
24
  gem.add_development_dependency "rake"
24
25
  gem.add_development_dependency "rspec", "~> 3"
data/bin/bashcov CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  lib = File.expand_path("../../lib", __FILE__)
4
5
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
@@ -7,13 +8,13 @@ require "bashcov"
7
8
 
8
9
  Bashcov.parse_options! ARGV
9
10
 
10
- runner = Bashcov::Runner.new Bashcov.options.command
11
+ runner = Bashcov::Runner.new Bashcov.command
11
12
  status = runner.run
12
13
  coverage = runner.result
13
14
 
14
15
  require "simplecov"
15
16
 
16
- SimpleCov.command_name Bashcov.name
17
+ SimpleCov.command_name Bashcov.fullname
17
18
  SimpleCov.root Bashcov.root_directory
18
19
  SimpleCov::Result.new(coverage).format!
19
20
 
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bashcov
4
+ # Module exposing information concerning the installed Bash version
5
+ # @note methods do not cache results because +bash_path+ can change at
6
+ # runtime
7
+ # @note receiver is expected to implement +bash_path+
8
+ module BashInfo
9
+ # @return [Array<String>] An array representing the components of
10
+ # +BASH_VERSINFO+
11
+ def bash_versinfo
12
+ `#{bash_path} -c 'echo "${BASH_VERSINFO[@]}"'`.chomp.split
13
+ end
14
+
15
+ # @return [Boolean] Whether Bash supports +BASH_XTRACEFD+
16
+ def bash_xtracefd?
17
+ bash_versinfo[0..1].join.to_i >= 41
18
+ end
19
+
20
+ # @param [Integer] length the number of bytes to test; default 128
21
+ # @return [Boolean] whether Bash supports a +PS4+ of at least a given
22
+ # number of bytes
23
+ # @see https://tiswww.case.edu/php/chet/bash/CHANGES
24
+ # @note Item +i.+ under the +bash-4.2-release+ to +bash-4.3-alpha+ change
25
+ # list notes that version 4.2 truncates +PS4+ if it is greater than 128
26
+ # bytes.
27
+ def truncated_ps4?(length = 128)
28
+ ps4 = SecureRandom.base64(length)
29
+ !`PS4=#{ps4} #{bash_path} 2>&1 1>&- -xc 'echo hello'`.start_with?(ps4)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bashcov
4
+ # Signals an error parsing Bash's debugging output.
5
+ class XtraceError < ::RuntimeError
6
+ # Will contain the coverages parsed prior to the error
7
+ attr_reader :files
8
+
9
+ # @param [#to_s] message An error message
10
+ # @param [Hash] files A hash containing coverage information
11
+ def initialize(message, files)
12
+ @files = files
13
+ super(message)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bashcov
4
+ # Classes for streaming token-delimited fields
5
+ class FieldStream
6
+ attr_accessor :read
7
+
8
+ # @param [IO] read an IO object opened for reading
9
+ def initialize(read = nil)
10
+ @read = read
11
+ end
12
+
13
+ # A convenience wrapper around +each_line(delimiter)+ that also does
14
+ # +chomp(delimiter)+ on the yielded line.
15
+ # @param [String, nil] delimiter the field separator for the stream
16
+ # @return [void]
17
+ # @yieldparam [String] field each +chomp+ed line
18
+ def each_field(delimiter)
19
+ return enum_for(__method__, delimiter) unless block_given?
20
+
21
+ read.each_line(delimiter) do |line|
22
+ yield line.chomp(delimiter)
23
+ end
24
+ end
25
+
26
+ # Yields fields extracted from a input stream
27
+ # @param [String, nil] delimiter the field separator
28
+ # @param [Integer] field_count the number of fields to extract
29
+ # @param [Regexp] start_match a +Regexp+ that, when matched against the
30
+ # input stream, signifies the beginning of the next series of fields to
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.
36
+ def each(delimiter, field_count, start_match)
37
+ unless block_given?
38
+ return enum_for(__method__, delimiter, field_count, start_match)
39
+ end
40
+
41
+ # Whether the current field is the start-of-fields match
42
+ matched_start = nil
43
+
44
+ # The number of fields processed since passing the last start-of-fields
45
+ # match
46
+ seen_fields = 0
47
+
48
+ fields = each_field(delimiter)
49
+
50
+ # Close over +field_count+ and +seen_fields+ to yield empty strings to
51
+ # the caller when we've already hit the next start-of-fields match
52
+ yield_remaining = -> { (field_count - seen_fields).times { yield "" } }
53
+
54
+ # Advance until the first start-of-fields match
55
+ loop { break if fields.next =~ start_match }
56
+
57
+ fields.each do |field|
58
+ # If the current field is the start-of-fields match...
59
+ if field =~ start_match
60
+ # Fill out any remaining (unparseable) fields with empty strings
61
+ yield_remaining.call
62
+
63
+ matched_start = nil
64
+ seen_fields = 0
65
+ elsif seen_fields < field_count
66
+ yield field
67
+ seen_fields += 1
68
+ end
69
+ end
70
+
71
+ # One last filling-out of empty fields if we're at the end of the stream
72
+ yield_remaining.call
73
+
74
+ read.close unless read.closed?
75
+ end
76
+ end
77
+ end
data/lib/bashcov/lexer.rb CHANGED
@@ -1,24 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bashcov/line"
4
+
1
5
  module Bashcov
2
6
  # Simple lexer which analyzes Bash files in order to get information for
3
7
  # coverage
4
8
  class Lexer
5
9
  # Lines starting with one of these tokens are irrelevant for coverage
6
- IGNORE_START_WITH = %w(# function)
10
+ IGNORE_START_WITH = %w(# function).freeze
7
11
 
8
12
  # Lines ending with one of these tokens are irrelevant for coverage
9
- IGNORE_END_WITH = %w|(|
13
+ IGNORE_END_WITH = %w|(|.freeze
10
14
 
11
15
  # Lines containing only one of these keywords are irrelevant for coverage
12
- IGNORE_IS = %w(esac if then else elif fi while do done { } ;;)
16
+ IGNORE_IS = %w(esac if then else elif fi while do done { } ;;).freeze
13
17
 
14
18
  # @param [String] filename File to analyze
15
19
  # @param [Hash] coverage Coverage with executed lines marked
16
20
  # @raise [ArgumentError] if the given +filename+ is invalid.
17
21
  def initialize(filename, coverage)
18
- @filename = File.expand_path(filename)
22
+ @filename = filename
19
23
  @coverage = coverage
20
24
 
21
- raise ArgumentError, "#{@filename} is not a file" unless File.file?(@filename)
25
+ unless File.file?(@filename)
26
+ raise ArgumentError, "#{@filename} is not a file"
27
+ end
22
28
  end
23
29
 
24
30
  # Yields uncovered relevant lines.
@@ -27,7 +33,7 @@ module Bashcov
27
33
  def uncovered_relevant_lines
28
34
  lineno = 0
29
35
  File.open(@filename, "rb").each_line do |line|
30
- if @coverage[lineno] == Bashcov::Line::IGNORED && revelant?(line)
36
+ if @coverage[lineno] == Bashcov::Line::IGNORED && relevant?(line)
31
37
  yield lineno
32
38
  end
33
39
  lineno += 1
@@ -36,7 +42,7 @@ module Bashcov
36
42
 
37
43
  private
38
44
 
39
- def revelant?(line)
45
+ def relevant?(line)
40
46
  line.strip!
41
47
 
42
48
  !line.empty? and
data/lib/bashcov/line.rb CHANGED
@@ -1,12 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Bashcov
2
4
  # {Line} represents a line of code.
3
5
  module Line
4
6
  # Uncovered line
5
- # @see http://www.ruby-doc.org/stdlib-1.9.3/libdoc/coverage/rdoc/Coverage.html
7
+ # @see http://ruby-doc.org/stdlib-2.3.0/libdoc/coverage/rdoc/Coverage.html
6
8
  UNCOVERED = 0
7
9
 
8
10
  # Ignored line
9
- # @see http://www.ruby-doc.org/stdlib-1.9.3/libdoc/coverage/rdoc/Coverage.html
11
+ # @see http://ruby-doc.org/stdlib-2.3.0/libdoc/coverage/rdoc/Coverage.html
10
12
  IGNORED = nil
11
13
  end
12
14
  end
@@ -1,3 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bashcov/errors"
4
+ require "bashcov/field_stream"
5
+ require "bashcov/lexer"
6
+ require "bashcov/xtrace"
7
+
1
8
  module Bashcov
2
9
  # Runs a given command with xtrace enabled then computes code coverage.
3
10
  class Runner
@@ -10,21 +17,44 @@ module Bashcov
10
17
  # @note Binds Bashcov +stdin+ to the program being executed.
11
18
  # @return [Process::Status] Status of the executed command
12
19
  def run
13
- inject_xtrace_flag!
20
+ # Clear out previous run
21
+ @result = nil
22
+
23
+ field_stream = FieldStream.new
24
+ @xtrace = Xtrace.new(field_stream)
14
25
 
15
- @xtrace = Xtrace.new
16
26
  fd = @xtrace.file_descriptor
17
- options = { :in => :in, fd => fd } # bind FDs to the child process
18
- options.merge!(out: "/dev/null", err: "/dev/null") if Bashcov.options.mute
19
- env = { "BASH_XTRACEFD" => fd.to_s, "PS4" => Xtrace::PS4 }
27
+ env = { "PS4" => Xtrace.ps4 }
28
+ options = { in: :in }
29
+
30
+ if Bashcov.options.mute
31
+ options[:out] = "/dev/null"
32
+ options[:err] = "/dev/null"
33
+ end
34
+
35
+ run_xtrace(fd, env, options) do
36
+ command_pid = Process.spawn env, *@command, options # spawn the command
20
37
 
21
- command_pid = Process.spawn env, @command, options # spawn the command
22
- xtrace_thread = Thread.new { @xtrace.read } # start processing the xtrace output
38
+ begin
39
+ # start processing the xtrace output
40
+ xtrace_thread = Thread.new { @xtrace.read }
23
41
 
24
- Process.wait command_pid
25
- @xtrace.close
42
+ Process.wait command_pid
26
43
 
27
- @coverage = xtrace_thread.value # wait for the thread to return
44
+ @xtrace.close
45
+
46
+ @coverage = xtrace_thread.value # wait for the thread to return
47
+ rescue XtraceError => e
48
+ write_warning <<-WARNING
49
+ encountered an error parsing Bash's output (error was:
50
+ #{e.message}). This can occur if your script or its path contains
51
+ the sequence #{Xtrace.delimiter.inspect}, or if your script unsets
52
+ LINENO. Aborting early; coverage report will be incomplete.
53
+ WARNING
54
+
55
+ @coverage = e.files
56
+ end
57
+ end
28
58
 
29
59
  $?
30
60
  end
@@ -37,48 +67,93 @@ module Bashcov
37
67
  expunge_invalid_files!
38
68
  mark_relevant_lines!
39
69
 
40
- @coverage
70
+ convert_coverage
41
71
  end
42
72
  end
43
73
 
44
74
  private
45
75
 
76
+ def write_warning(message)
77
+ warn format "%s: warning: %s", Bashcov.program_name,
78
+ message.gsub(/^\s+/, "").lines.map(&:chomp).join(" ")
79
+ end
80
+
81
+ def run_xtrace(fd, env, options)
82
+ # Older versions of Bash (< 4.1) don't have the BASH_XTRACEFD variable
83
+ if Bashcov.bash_xtracefd?
84
+ options[fd] = fd # bind FDs to the child process
85
+
86
+ env["BASH_XTRACEFD"] = fd.to_s
87
+ else
88
+ # Send subprocess standard error to @xtrace.file_descriptor
89
+ options[:err] = fd
90
+
91
+ # Don't bother issuing warning if we're silencing output anyway
92
+ unless Bashcov.mute
93
+ write_warning <<-WARNING
94
+ you are using a version of Bash that does not support
95
+ BASH_XTRACEFD. All xtrace output will print to standard error, and
96
+ your script's output on standard error will not be printed to the
97
+ console.
98
+ WARNING
99
+ end
100
+ end
101
+
102
+ inject_xtrace_flag! do
103
+ yield
104
+ end
105
+ end
106
+
46
107
  # @note +SHELLOPTS+ must be exported so we use Ruby's {ENV} variable
47
- # @return [void]
108
+ # @yield [void] adds "xtrace" to +SHELLOPTS+ and then runs the provided
109
+ # block
110
+ # @return [Object, ...] the value returned by the calling block
48
111
  def inject_xtrace_flag!
49
- existing_flags = (ENV["SHELLOPTS"] || "").split(":")
112
+ existing_flags_s = ENV["SHELLOPTS"]
113
+ existing_flags = (existing_flags_s || "").split(":")
50
114
  ENV["SHELLOPTS"] = (existing_flags | ["xtrace"]).join(":")
115
+
116
+ yield
117
+ ensure
118
+ ENV["SHELLOPTS"] = existing_flags_s
51
119
  end
52
120
 
53
121
  # Add files which have not been executed at all (i.e. with no coverage)
54
122
  # @return [void]
55
123
  def find_bash_files!
56
- return if Bashcov.options.skip_uncovered
124
+ return if Bashcov.skip_uncovered
57
125
 
58
- Dir["#{Bashcov.root_directory}/**/*.sh"].each do |file|
59
- @coverage[file] ||= [] # empty coverage array
126
+ Pathname.glob("#{Bashcov.root_directory}/**/*.sh").each do |filename|
127
+ @coverage[filename] = [] unless @coverage.include?(filename)
60
128
  end
61
129
  end
62
130
 
63
131
  # @return [void]
64
132
  def expunge_invalid_files!
65
- @coverage.each_key do |file|
66
- next if File.file? file
133
+ @coverage.each_key do |filename|
134
+ next if filename.file?
67
135
 
68
- @coverage.delete file
69
- warn "Warning: #{file} was executed but has been deleted since then - it won't be reported in coverage."
136
+ @coverage.delete filename
137
+ write_warning <<-WARNING
138
+ #{filename} was executed but has been deleted since then - it won't
139
+ be reported in coverage.
140
+ WARNING
70
141
  end
71
142
  end
72
143
 
73
144
  # @see Lexer
74
145
  # @return [void]
75
146
  def mark_relevant_lines!
76
- @coverage.each do |filename, coverage|
147
+ @coverage.each_pair do |filename, coverage|
77
148
  lexer = Lexer.new(filename, coverage)
78
149
  lexer.uncovered_relevant_lines do |lineno|
79
150
  @coverage[filename][lineno] = Bashcov::Line::UNCOVERED
80
151
  end
81
152
  end
82
153
  end
154
+
155
+ def convert_coverage
156
+ Hash[@coverage.map { |filename, coverage| [filename.to_s, coverage] }]
157
+ end
83
158
  end
84
159
  end
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # :nodoc:
2
4
  module Bashcov
3
- VERSION = "1.2.1".freeze
5
+ # Current Bashcov version
6
+ VERSION = "1.3.0"
4
7
  end