bashcov 1.2.1 → 1.3.0

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.
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