diff --git a/Dockerfile b/Dockerfile index e899ed6..d847eef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,8 @@ FROM ruby:2.6.5-alpine +RUN apk add --update nodejs + COPY lib /action/lib +COPY bin /action/bin CMD ["ruby", "/action/lib/index.rb"] diff --git a/README.md b/README.md index 45cf830..8dd7b20 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,26 @@ A GitHub Action that check minimum coverage percentage! ### Usage -#### [Simplecov](https://github.com/colszowka/simplecov) +#### Lcov ```yml -- uses: devmasx/coverage-check-action@v1.0.1 +- uses: devmasx/coverage-check-action@v1.1.0 with: - result_path: coverage/.last_run.json + type: lcov + result_path: coverage/example.lcov + min_coverage: 90 token: ${{secrets.GITHUB_TOKEN}} +``` + +#### [Simplecov](https://github.com/colszowka/simplecov) + +```yml +- uses: devmasx/coverage-check-action@v1.1.0 + with: type: simplecov + result_path: coverage/.last_run.json min_coverage: 90 + token: ${{secrets.GITHUB_TOKEN}} ``` ## Screenshots diff --git a/action.yml b/action.yml index 846fefa..f9bf9fd 100644 --- a/action.yml +++ b/action.yml @@ -6,9 +6,9 @@ branding: color: "green" inputs: type: - description: "simplecov | jest" + description: "lcov | simplecov" required: true - default: "simplecov" + default: "lcov" token: description: "Github token for create checks" required: true diff --git a/bin/lcov-parse.js b/bin/lcov-parse.js new file mode 100644 index 0000000..80d0e52 --- /dev/null +++ b/bin/lcov-parse.js @@ -0,0 +1,129 @@ +/* +Copyright (c) 2012, Yahoo! Inc. All rights reserved. +Code licensed under the BSD License: +http://yuilibrary.com/license/ +*/ + +var fs = require('fs'), + path = require('path'); + +/* istanbul ignore next */ +var exists = fs.exists || path.exists; + +var walkFile = function (str, cb) { + var data = [], item; + + ['end_of_record'].concat(str.split('\n')).forEach(function (line) { + line = line.trim(); + var allparts = line.split(':'), + parts = [allparts.shift(), allparts.join(':')], + lines, fn; + + switch (parts[0].toUpperCase()) { + case 'TN': + item.title = parts[1].trim(); + break; + case 'SF': + item.file = parts.slice(1).join(':').trim(); + break; + case 'FNF': + item.functions.found = Number(parts[1].trim()); + break; + case 'FNH': + item.functions.hit = Number(parts[1].trim()); + break; + case 'LF': + item.lines.found = Number(parts[1].trim()); + break; + case 'LH': + item.lines.hit = Number(parts[1].trim()); + break; + case 'DA': + lines = parts[1].split(','); + item.lines.details.push({ + line: Number(lines[0]), + hit: Number(lines[1]) + }); + break; + case 'FN': + fn = parts[1].split(','); + item.functions.details.push({ + name: fn[1], + line: Number(fn[0]) + }); + break; + case 'FNDA': + fn = parts[1].split(','); + item.functions.details.some(function (i, k) { + if (i.name === fn[1] && i.hit === undefined) { + item.functions.details[k].hit = Number(fn[0]); + return true; + } + }); + break; + case 'BRDA': + fn = parts[1].split(','); + item.branches.details.push({ + line: Number(fn[0]), + block: Number(fn[1]), + branch: Number(fn[2]), + taken: ((fn[3] === '-') ? 0 : Number(fn[3])) + }); + break; + case 'BRF': + item.branches.found = Number(parts[1]); + break; + case 'BRH': + item.branches.hit = Number(parts[1]); + break; + } + + if (line.indexOf('end_of_record') > -1) { + data.push(item); + item = { + lines: { + found: 0, + hit: 0, + details: [] + }, + functions: { + hit: 0, + found: 0, + details: [] + }, + branches: { + hit: 0, + found: 0, + details: [] + } + }; + } + }); + + data.shift(); + + if (data.length) { + cb(null, data); + } else { + cb('Failed to parse string'); + } +}; + +var parse = function (file, cb) { + exists(file, function (x) { + if (!x) { + return walkFile(file, cb); + } + fs.readFile(file, 'utf8', function (err, str) { + walkFile(str, cb); + }); + }); + +}; + +var path = process.argv[2] +parse(path, function (err, data) { + console.log(JSON.stringify(data, null, 2)) +}); + +// console.log(process.argv[2]) diff --git a/lib/coverage_report.rb b/lib/coverage_report.rb index 18dd021..8752daa 100644 --- a/lib/coverage_report.rb +++ b/lib/coverage_report.rb @@ -1,33 +1,46 @@ # frozen_string_literal: true class CoverageReport - def self.generate(type, report_path, data) - if type == 'simplecov' - simplecov(report_path, data) - elsif type == 'jest' - jest(report_path, data) - else - raise 'InvalidCoverageReportType' + class << self + def generate(type, report_path, data) + if type == 'simplecov' + simplecov(report_path, data) + elsif type == 'lcov' + lcov(report_path, data) + else + raise 'InvalidCoverageReportType' + end end - end - def self.simplecov(report_path, data) - report = read_json(report_path) - minumum_percent = data[:min] - covered_percent = report.dig('result', 'covered_percent') - { 'lines' => { 'covered_percent' => covered_percent, 'minumum_percent' => minumum_percent } } - end + def simplecov(report_path, data) + report = read_json(report_path) + minumum_percent = data[:min] + covered_percent = report.dig('result', 'covered_percent') + { 'lines' => { 'covered_percent' => covered_percent, 'minumum_percent' => minumum_percent } } + end - def self.jest(report_path, data) - report = read_json(report_path) - minumum_percent = data[:min] - covered_percent = report.dig('result', 'covered_percent') - { 'lines' => { 'covered_percent' => covered_percent, 'minumum_percent' => minumum_percent } } - end + def lcov(report_path, data) + lcov_result = execute_lcov_parse(report_path) + minumum_percent = data[:min] + { 'lines' => { 'covered_percent' => lcov_covered_percent(lcov_result), 'minumum_percent' => minumum_percent } } + end - private + private - def self.read_json(path) - JSON.parse(File.read(path)) + def lcov_covered_percent(lcov_result) + lines = lcov_result.map { |r| r['lines']['details'] }.flatten + total_lines = lines.count.to_f.round(2) + covered_lines = lines.select { |r| r['hit'] >= 1 }.count.to_f + ((covered_lines / total_lines) * 100).round(2) + end + + def execute_lcov_parse(report_path) + bin_path = "#{File.dirname(__FILE__)}/../bin" + JSON.parse(`node #{bin_path}/lcov-parse.js #{report_path}`) + end + + def read_json(path) + JSON.parse(File.read(path)) + end end end diff --git a/spec/coverage_report_spec.rb b/spec/coverage_report_spec.rb index b1f7d05..b5b60d4 100644 --- a/spec/coverage_report_spec.rb +++ b/spec/coverage_report_spec.rb @@ -7,4 +7,9 @@ result = CoverageReport.simplecov('./spec/fixtures/simplecov.json', min: 80) expect(result['lines']['covered_percent']).to eq(80.5) end + + it '.lcov' do + result = CoverageReport.lcov('./spec/fixtures/example.lcov', min: 80) + expect(result['lines']['covered_percent']).to eq(85.61) + end end diff --git a/spec/fixtures/example.lcov b/spec/fixtures/example.lcov new file mode 100644 index 0000000..f2f795a --- /dev/null +++ b/spec/fixtures/example.lcov @@ -0,0 +1,193 @@ +SF:./app/controllers/application_controller.rb +DA:1,1 +DA:2,1 +end_of_record +SF:./app/controllers/posts_controller.rb +DA:1,1 +DA:2,1 +DA:6,1 +DA:7,1 +DA:12,1 +DA:16,1 +DA:17,1 +DA:21,1 +DA:26,1 +DA:27,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:42,1 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:48,0 +DA:49,0 +DA:56,1 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:64,1 +DA:66,1 +DA:67,0 +DA:71,1 +DA:72,0 +end_of_record +SF:./app/helpers/application_helper.rb +DA:1,1 +end_of_record +SF:./app/helpers/posts_helper.rb +DA:1,1 +end_of_record +SF:./app/models/application_record.rb +DA:1,1 +DA:2,1 +end_of_record +SF:./app/models/post.rb +DA:1,1 +end_of_record +SF:./config/application.rb +DA:1,1 +DA:3,1 +DA:7,1 +DA:9,1 +DA:10,1 +DA:12,1 +end_of_record +SF:./config/boot.rb +DA:1,1 +DA:3,1 +DA:4,1 +end_of_record +SF:./config/environment.rb +DA:2,1 +DA:5,1 +end_of_record +SF:./config/environments/test.rb +DA:1,1 +DA:8,1 +DA:13,1 +DA:16,1 +DA:17,1 +DA:22,1 +DA:23,1 +DA:26,1 +DA:29,1 +DA:32,1 +DA:34,1 +DA:39,1 +DA:42,1 +end_of_record +SF:./config/initializers/application_controller_renderer.rb + +end_of_record +SF:./config/initializers/assets.rb +DA:4,1 +DA:9,1 +end_of_record +SF:./config/initializers/backtrace_silencers.rb + +end_of_record +SF:./config/initializers/content_security_policy.rb + +end_of_record +SF:./config/initializers/cookies_serializer.rb +DA:5,1 +end_of_record +SF:./config/initializers/filter_parameter_logging.rb +DA:4,1 +end_of_record +SF:./config/initializers/inflections.rb + +end_of_record +SF:./config/initializers/mime_types.rb + +end_of_record +SF:./config/initializers/wrap_parameters.rb +DA:7,1 +DA:8,2 +end_of_record +SF:./config/routes.rb +DA:1,1 +DA:3,1 +DA:4,1 +DA:5,1 +DA:6,1 +end_of_record +SF:./spec/helpers/posts_helper_spec.rb +DA:1,1 +DA:13,1 +DA:14,1 +end_of_record +SF:./spec/models/post_spec.rb +DA:1,1 +DA:3,1 +DA:4,1 +end_of_record +SF:./spec/requests/posts_spec.rb +DA:1,1 +DA:3,1 +DA:4,1 +DA:5,1 +DA:6,1 +DA:7,1 +end_of_record +SF:./spec/routing/posts_routing_spec.rb +DA:1,1 +DA:3,1 +DA:4,1 +DA:5,1 +DA:6,1 +DA:9,1 +DA:10,1 +DA:13,1 +DA:14,1 +DA:17,1 +DA:18,1 +DA:22,1 +DA:23,1 +DA:26,1 +DA:27,1 +DA:30,1 +DA:31,1 +DA:34,1 +DA:35,1 +end_of_record +SF:./spec/views/posts/edit.html.erb_spec.rb +DA:1,1 +DA:3,1 +DA:4,1 +DA:5,1 +DA:8,1 +DA:9,1 +DA:11,1 +end_of_record +SF:./spec/views/posts/index.html.erb_spec.rb +DA:1,1 +DA:3,1 +DA:4,1 +DA:5,1 +DA:11,1 +DA:12,1 +end_of_record +SF:./spec/views/posts/new.html.erb_spec.rb +DA:1,1 +DA:3,1 +DA:4,1 +DA:5,1 +DA:8,1 +DA:9,1 +DA:11,1 +end_of_record +SF:./spec/views/posts/show.html.erb_spec.rb +DA:1,1 +DA:3,1 +DA:4,1 +DA:5,1 +DA:8,1 +DA:9,1 +end_of_record