lib/covered/source.rb in covered-0.1.0 vs lib/covered/source.rb in covered-0.3.0

- old
+ new

@@ -16,41 +16,101 @@ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +require_relative 'eval' +require_relative 'wrapper' + +require 'thread' + module Covered - class Source - def initialize(files) - @files = files - @paths = {} + # The source map, loads the source file, parses the AST to generate which lines contain executable code. + class Source < Wrapper + EXECUTABLE = /NODE_(.?CALL|.VAR|.ASGN|DEFN)/.freeze + + # Deviate from the standard policy above, because all the files are already loaded, so we skip NODE_FCALL. + DOGFOOD = /NODE_([V]?CALL|.VAR|.ASGN|DEFN)/.freeze + + # Ruby trace points don't trigger for argument execution. + # Constants are loaded when the file loads, so they are less interesting. + IGNORE = /NODE_(ARGS|CDECL)/.freeze + + def initialize(output, executable: EXECUTABLE, ignore: IGNORE) + super(output) + @paths = {} @mutex = Mutex.new + + @executable = executable + @ignore = ignore end - # [String -> Source] + def enable + super + + Eval::enable(self) + end + + def disable + Eval::disable(self) + + super + end + attr :paths - def map(string, binding = nil, filename = nil, lineno = 1) + def intercept_eval(string, binding = nil, filename = nil, lineno = 1) return unless filename # TODO replace with Concurrent::Map @mutex.synchronize do @paths[filename] = string end end - def mark(path, lineno) - @files.mark(path, lineno) + def executable?(node) + node.type =~ @executable end + def ignore?(node) + # NODE_ARGS Ruby doesn't report execution of arguments in :line tracepoint. + node.type =~ @ignore + end + + def expand(node, lines) + # puts "#{node.first_lineno}: #{node.inspect}" + + lines[node.first_lineno] ||= 0 if executable?(node) + + node.children.each do |child| + next if child.nil? or ignore?(child) + + expand(child, lines) + end + end + + def parse(path) + # puts "Parse #{path}" + + if source = @paths[path] + RubyVM::AST.parse(source) + elsif File.exist?(path) + RubyVM::AST.parse_file(path) + else + warn "Couldn't parse #{path}, file doesn't exist?" + end + end + def each(&block) - @files.each do |path, lines| - if source = @paths[path] - yield path, RubyVM::AST.parse(source), lines - else - yield path, RubyVM::AST.parse_file(path), lines + @output.each do |path, lines| + lines = lines.dup + + if top = parse(path) + expand(top, lines) end + + yield path, lines end end end end