diff --git a/Makefile b/Makefile index 62c9c6b..b3161a9 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,8 @@ PREFIX ?= /usr/local SHARD_BIN ?= ../../bin build: bin/crystal-coverage -bin/crystal-coverage: - $(SHARDS_BIN) build $(CRFLAGS) +bin/crystal-coverage: $(shell find src -type f -name '*.cr') + $(SHARDS_BIN) --without-development build $(CRFLAGS) clean: rm -f .bin/crystal-coverage .bin/crystal-coverage.dwarf install: build @@ -16,4 +16,4 @@ bin: build cp ./bin/crystal-coverage $(SHARD_BIN) # test: build # $(CRYSTAL_BIN) spec -# ./bin/crystal-coverage +# ./bin/crystal-coverage diff --git a/README.md b/README.md index 552261c..826fe6b 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Wait for the binary to compile. The binary will be build in `bin/crystal-coverag ## Usage ``` -crystal-coverage spec/myfile_spec1.cr spec/myfile_spec2.cr +bin/crystal-coverage ``` Coverage file will be recreated after your software run on `coverage/` folder. @@ -60,7 +60,7 @@ software is executed without release flag. To test in `--release` mode, you can do: ``` -crystal-coverage src/main.cr -p | crystal eval --release +bin/crystal-coverage -p | crystal eval --release ``` ## How does it works? diff --git a/shard.lock b/shard.lock index 48428b1..1e8513e 100644 --- a/shard.lock +++ b/shard.lock @@ -1,6 +1,6 @@ -version: 1.0 +version: 2.0 shards: ameba: - github: veelenga/ameba - version: 0.10.0 + git: https://github.com/crystal-ameba/ameba.git + version: 0.14.3 diff --git a/shard.yml b/shard.yml index edab69a..8a1bc3c 100644 --- a/shard.yml +++ b/shard.yml @@ -11,11 +11,13 @@ targets: crystal-coverage: main: src/coverage/cli.cr +crystal: ">= 1.0.0, < 2.0.0" + scripts: postinstall: make bin development_dependencies: ameba: - github: veelenga/ameba + github: crystal-ameba/ameba license: MIT diff --git a/src/coverage/inject/cli.cr b/src/coverage/inject/cli.cr index 9951535..ca4cad2 100644 --- a/src/coverage/inject/cli.cr +++ b/src/coverage/inject/cli.cr @@ -1,4 +1,5 @@ require "option_parser" + # require "tempfile" module Coverage @@ -8,11 +9,15 @@ module Coverage filenames = [] of String print_only = false - OptionParser.parse! do |parser| - parser.banner = "Usage: crystal-cover [options] " + OptionParser.parse do |parser| + parser.banner = "Usage: crystal-coverage [options] " parser.on("-o FORMAT", "--output-format=FORMAT", "The output format used (default: HtmlReport): HtmlReport, Coveralls ") { |f| output_format = f } parser.on("-p", "--print-only", "output the generated source code") { |_p| print_only = true } parser.on("--use-require=REQUIRE", "change the require of cover library in runtime") { |r| Coverage::SourceFile.use_require = r } + parser.on("-h", "--help", "Show this help") do + puts parser + exit + end parser.unknown_args do |args| args.each do filenames << ARGV.shift @@ -20,12 +25,12 @@ module Coverage end end - raise "You must choose a file to compile" unless filenames.any? + filenames = Dir["spec/**/*_spec.cr"] unless filenames.any? Coverage::SourceFile.outputter = "Coverage::Outputter::#{output_format.camelcase}" first = true - output = String::Builder.new(capacity: 2**18) + output = String::Builder.new filenames.each do |f| v = Coverage::SourceFile.new(path: f, source: ::File.read(f)) output << v.to_covered_source diff --git a/src/coverage/inject/source_file.cr b/src/coverage/inject/source_file.cr index b15fc92..1644512 100644 --- a/src/coverage/inject/source_file.cr +++ b/src/coverage/inject/source_file.cr @@ -6,25 +6,6 @@ require "./extensions" require "./macro_utils" class Coverage::SourceFile < Crystal::Visitor - # List of keywords which are trouble with variable - # name. Some keywoards are not and won't be present in this - # list. - # Since this can break the code replacing the variable by a underscored - # version of it, and I'm not sure about this list, we will need to add/remove - # stuff to not break the code. - CRYSTAL_KEYWORDS = %w( - abstract do if nil? self unless - alias else of sizeof until - as elsif include struct when - as? end instance_sizeof pointerof super while - asm ensure is_a? private then with - begin enum lib protected true yield - break extend macro require - case false module rescue typeof - class for next return uninitialized - def fun nil select union - ) - class_getter file_list = [] of Coverage::SourceFile class_getter already_covered_file_name = Set(String).new class_getter! project_path : String @@ -74,14 +55,16 @@ class Coverage::SourceFile < Crystal::Visitor # Inject in AST tree if required. def process unless @astree - @astree = Crystal::Parser.parse(self.source) + parser = Crystal::Parser.new(self.source) + parser.filename = File.expand_path(path, ".") + @astree = parser.parse astree.accept(self) end end def to_covered_source if @enriched_source.nil? - io = String::Builder.new(capacity: 32_768) + io = String::Builder.new # call process to enrich AST before # injection of cover head dependencies @@ -103,7 +86,7 @@ class Coverage::SourceFile < Crystal::Visitor file_list = @@require_expanders[expansion_id] if file_list.any? - io = String::Builder.new(capacity: (2 ** 20)) + io = String::Builder.new file_list.each do |file| io << "#" << "require of `" << file.path io << "` from `" << self.path << ":#{file.required_at}" << "`" << "\n" @@ -120,7 +103,7 @@ class Coverage::SourceFile < Crystal::Visitor end private def inject_location(file = @path, line = 0, column = 0) - %(#) + %(#) end def self.prelude_operations @@ -139,17 +122,20 @@ class Coverage::SourceFile < Crystal::Visitor end def self.final_operations - "\n::Coverage.get_results(#{@@outputter}.new)" + "\n Spec.after_suite { ::Coverage.get_results(#{@@outputter}.new) }" end # Inject line tracer for easy debugging. # add `;` after the Coverage instrumentation - # to avoid some with macros + # to avoid some with macros. Be careful to only insert + # `;` if there is something else on the same line, or else + # it breaks parsing with expressions inside expressions. private def inject_line_traces(output) output.gsub(/\:\:Coverage\[([0-9]+),[ ]*([0-9]+)\](.*)/) do |_str, match| [ "::Coverage[", match[1], - ", ", match[2], "]; ", + ", ", match[2], "]", + match[3].empty? ? " " : "; ", match[3], inject_location(@path, @lines[match[2].to_i] - 1), ].join("") @@ -168,7 +154,7 @@ class Coverage::SourceFile < Crystal::Visitor n = Crystal::Call.new(Crystal::Global.new("::Coverage"), "[]", [Crystal::NumberLiteral.new(@id), - Crystal::NumberLiteral.new(lidx)].unsafe_as(Array(Crystal::ASTNode))) + Crystal::NumberLiteral.new(lidx)] of Crystal::ASTNode) n else node @@ -177,9 +163,9 @@ class Coverage::SourceFile < Crystal::Visitor private def force_inject_cover(node : Crystal::ASTNode, location = nil) location ||= node.location - return node if @already_covered_locations.includes?(location) + return node if @already_covered_locations.includes?(location) || @path.starts_with? "spec/" already_covered_locations << location - Crystal::Expressions.from([inject_coverage_tracker(node), node].unsafe_as(Array(Crystal::ASTNode))) + Crystal::Expressions.from([inject_coverage_tracker(node), node] of Crystal::ASTNode) end def inject_cover(node : Crystal::ASTNode) @@ -259,26 +245,11 @@ class Coverage::SourceFile < Crystal::Visitor end def visit(node : Crystal::Arg) - name = node.name - if CRYSTAL_KEYWORDS.includes?(name) - node.external_name = node.name = "_#{name}" - end - true end # Placeholder for bug #XXX def visit(node : Crystal::Assign) - target = node.target - value = node.value - - if target.is_a?(Crystal::InstanceVar) && - value.is_a?(Crystal::Var) - if CRYSTAL_KEYWORDS.includes?(value.name) - value.name = "_#{value.name}" - end - end - true end @@ -312,7 +283,7 @@ class Coverage::SourceFile < Crystal::Visitor propagate_location_in_macro(node, node.location.not_nil!) node.then = force_inject_cover(node.then) - node.else = force_inject_cover(node.else) + node.else = force_inject_cover(node.else) unless node.cond == Crystal::BoolLiteral.new(true) true end diff --git a/src/coverage/runtime/outputters/html_report.cr b/src/coverage/runtime/outputters/html_report.cr index a90fc2d..4a705d8 100644 --- a/src/coverage/runtime/outputters/html_report.cr +++ b/src/coverage/runtime/outputters/html_report.cr @@ -1,6 +1,7 @@ require "ecr" require "file_utils" require "html" +require "../coverage" class Coverage::Outputter::HtmlReport < Coverage::Outputter struct CoverageReport @@ -22,7 +23,7 @@ class Coverage::Outputter::HtmlReport < Coverage::Outputter end def percent_coverage_str - "#{(100*percent_coverage).round(2)}%" + "#{"%.2f" % (100*percent_coverage)}%" end end @@ -71,9 +72,7 @@ class Coverage::Outputter::HtmlReport < Coverage::Outputter end def output(files : Array(Coverage::File)) - puts "Generating coverage report, please wait..." - - system("rm -r coverage/") + system("rm -rf coverage/") sum_lines = 0 sum_covered = 0 @@ -107,14 +106,12 @@ class Coverage::Outputter::HtmlReport < Coverage::Outputter sum_covered += cr.covered_lines cr + end.select do |cr| + cr.relevant_lines > 0 end # puts percent covered - if sum_lines == 0 - puts "100% covered" - else - puts (100.0*(sum_covered / sum_lines.to_f)).round(2).to_s + "% covered" - end + print "\nLines #{sum_lines == 0 ? 100 : "%.2f" % (100 * sum_covered / sum_lines)}% covered" # Generate the code FileUtils.mkdir_p("coverage") diff --git a/template/cover.html.ecr b/template/cover.html.ecr index 212ba3f..7db4a15 100644 --- a/template/cover.html.ecr +++ b/template/cover.html.ecr @@ -94,15 +94,11 @@
- - - + - - - +
Hitted linesRelevant linesPercentageLines
<%=@file.relevant_lines%><%=@file.covered_lines%><%=@file.percent_coverage_str%><%=@file.covered_lines%> / <%=@file.relevant_lines%> (<%=@file.percent_coverage_str%>)
diff --git a/template/summary.html.ecr b/template/summary.html.ecr index f3bac36..c242e46 100644 --- a/template/summary.html.ecr +++ b/template/summary.html.ecr @@ -32,26 +32,22 @@ - - - + <%- @covered_files.each do |file| -%> - - - + + + <%- end -%> - - - +
FileRelevant linesCovered linesPercentage coveredLines
<%=file.filename%><%=file.relevant_lines%><%=file.covered_lines%><%=file.percent_coverage_str%><%=file.covered_lines%> / <%=file.relevant_lines%> (<%=file.percent_coverage_str%>)
TOTAL: <%= total_relevant %><%= total_covered %><%= total_percentage %><%= total_covered %> / <%= total_relevant %> (<%= total_percentage %>)
- \ No newline at end of file +