diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index bec9932..82785aa 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -9,6 +9,7 @@ on: env: OTP_VERSION: 26.1.1 ELIXIR_VERSION: 1.15.6-otp-26 + MIX_ENV: test jobs: test: @@ -18,7 +19,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - + - name: Elixir uses: erlef/setup-beam@v1 with: @@ -31,7 +32,7 @@ jobs: with: path: _build key: build-${{ runner.os }}-${{ env.OTP_VERSION }}-${{ env.ELIXIR_VERSION }}-${{ hashFiles('mix.lock') }} - + - name: Deps Cache uses: actions/cache/restore@v3 id: deps-cache @@ -42,7 +43,7 @@ jobs: - name: Install Mix Dependencies if: steps.deps-cache.outputs.cache-hit != 'true' run: mix deps.get - + - name: Compile if: steps.build-cache.outputs.cache-hit != 'true' run: mix compile @@ -54,17 +55,17 @@ jobs: run: mix credo --strict - name: Run Tests - run: mix lcov - + run: mix test --cover + - name: Coverage Reporter uses: peek-travel/coverage-reporter@main id: coverage-reporter if: github.event_name == 'pull_request' continue-on-error: true - with: + with: lcov_path: cover/lcov.info coverage_threshold: 90 - + - name: Restore PLT cache uses: actions/cache@v2 id: plt-cache diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a205ae3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,39 @@ +name: Main + +on: + push: + tags: + - "v*.*.*" +env: + MIX_ENV: prod + ELIXIR_VERSION: 1.15.6-otp-26 + OTP_VERSION: 26.1.1 + +jobs: + build: + runs-on: ubuntu-latest + steps: + + - name: Checkout + uses: actions/checkout@v4 + + - name: Elixir + uses: erlef/setup-beam@v1 + with: + otp-version: ${{ env.OTP_VERSION }} + elixir-version: ${{ env.ELIXIR_VERSION }} + + - name: Install Mix Dependencies + run: mix deps.get + + - name: Escript Build + run: mix escript.build + + - name: Release + uses: softprops/action-gh-release@v1 + with: + files: | + coverage_reporter + Dockerfile + entrypoint.sh + action.yml diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..d77e0b2 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +elixir 1.15.7-otp-26 +erlang 26.1.1 diff --git a/Dockerfile b/Dockerfile index be614de..b89933d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,7 @@ -ARG ELIXIR_VERSION=1.14.4 -ARG OTP_VERSION=25.3.1 -ARG DEBIAN_VERSION=bullseye-20230227-slim +FROM hexpm/elixir:1.15.7-erlang-26.1.1-ubuntu-jammy-20230126 -ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" -ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}" +COPY coverage_reporter / +COPY entrypoint.sh / +COPY deps/castore/priv/cacerts.pem / -FROM ${BUILDER_IMAGE} as builder - -RUN apt-get update -y && apt-get install -y build-essential git \ - && apt-get clean && rm -f /var/lib/apt/lists/*_* - -WORKDIR /app -COPY mix.exs mix.lock entrypoint.sh ./ -COPY lib ./lib - -ENTRYPOINT ["/app/entrypoint.sh"] \ No newline at end of file +ENTRYPOINT ["/entrypoint.sh"] diff --git a/entrypoint.sh b/entrypoint.sh index 99fca32..db1a7a9 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -3,10 +3,6 @@ set -e set -o pipefail echo ">>> Running command" echo "" -# bash -c "set -e; set -o pipefail; $1" -cd /app -mix local.hex --force -mix local.rebar --force -mix deps.get -mix compile -mix run --no-mix-exs -e "CoverageReporter.run()" \ No newline at end of file + + +/coverage_reporter diff --git a/lib/coverage_reporter.ex b/lib/coverage_reporter.ex index 12f7f67..8fcac3b 100644 --- a/lib/coverage_reporter.ex +++ b/lib/coverage_reporter.ex @@ -20,9 +20,8 @@ defmodule CoverageReporter do However, The LCOV files produced by excoveralls only include SF, DA, LF, LH, and end_of_record lines. """ - def run(opts \\ []) do + def main(opts) do config = get_config(opts) - %{pull_number: pull_number, head_branch: head_branch, repository: repository} = config changed_files = get_changed_files(config) {total, module_results} = get_coverage_from_lcov_files(config) @@ -96,14 +95,15 @@ defmodule CoverageReporter do table = :ets.new(__MODULE__, [:set, :private]) module_results = - Enum.flat_map(lcov_paths, fn path -> + Enum.reduce(lcov_paths, %{}, fn path, acc -> path |> File.stream!() |> Stream.map(&String.trim(&1)) |> Stream.chunk_by(&(&1 == "end_of_record")) |> Stream.reject(&(&1 == ["end_of_record"])) - |> Stream.map(fn record -> process_lcov_record(table, record) end) + |> Enum.reduce(acc, fn record, acc -> process_lcov_record(table, record, acc) end) end) + |> Map.values() covered = :ets.select_count(table, [{{{:_, :_}, true}, [], [true]}]) not_covered = :ets.select_count(table, [{{{:_, :_}, false}, [], [true]}]) @@ -112,7 +112,7 @@ defmodule CoverageReporter do {total, module_results} end - defp process_lcov_record(table, record) do + defp process_lcov_record(table, record, acc) do "SF:" <> path = Enum.find(record, &String.starts_with?(&1, "SF:")) coverage_by_line = @@ -124,19 +124,22 @@ defmodule CoverageReporter do |> String.split(",") |> Enum.map(&String.to_integer(&1)) + covered = :ets.select_count(table, [{{{path, line_number}, true}, [], [true]}]) insert_line_coverage(table, count, path, line_number) - {line_number, count} + {line_number, count + covered} end) covered = :ets.select_count(table, [{{{path, :_}, true}, [], [true]}]) not_covered = :ets.select_count(table, [{{{path, :_}, false}, [], [true]}]) - {percentage(covered, not_covered), path, coverage_by_line} + Map.put(acc, path, {percentage(covered, not_covered), path, coverage_by_line}) end defp insert_line_coverage(table, count, path, line_number) do - if count == 0 do - :ets.insert(table, {{path, line_number}, false}) + covered_count = :ets.select_count(table, [{{{path, line_number}, true}, [], [true]}]) + + if count == 0 and covered_count == 0 do + :ets.insert_new(table, {{path, line_number}, false}) else Enum.each(1..count, fn _ -> :ets.insert(table, {{path, line_number}, true}) end) end @@ -262,12 +265,12 @@ defmodule CoverageReporter do |> Enum.reduce(_groups = [], &add_line_to_groups/2) |> Enum.reduce( _annotations = [], - &create_annotations(&1, &2, changed_lines, file, source_code) + &do_create_annotations(&1, &2, changed_lines, file, source_code) ) end) end - defp create_annotations(line_number_group, annotations, changed_lines, file, source_code) do + defp do_create_annotations(line_number_group, annotations, changed_lines, file, source_code) do end_line = List.first(line_number_group) start_line = List.last(line_number_group) @@ -400,7 +403,13 @@ defmodule CoverageReporter do {:user_agent, "CoverageReporter"} ] - options = Keyword.merge(opts, base_url: github_api_url, headers: headers) + options = + Keyword.merge(opts, + base_url: github_api_url, + headers: headers, + connect_options: [transport_opts: [cacertfile: "/cacerts.pem"]] + ) + request = Req.new(options) case Req.request(request) do diff --git a/mix.exs b/mix.exs index 750c934..6d14875 100644 --- a/mix.exs +++ b/mix.exs @@ -1,13 +1,16 @@ defmodule CoverageReporter.MixProject do use Mix.Project + @app :coverage_reporter + def project do [ - app: :coverage_reporter, + app: @app, version: "0.1.0", elixir: "~> 1.14", - start_permanent: Mix.env() == :prod, - deps: deps() + deps: deps(), + test_coverage: [tool: LcovEx], + escript: [main_module: CoverageReporter] ] end diff --git a/test/coverage_reporter_test.exs b/test/coverage_reporter_test.exs index 4ac8260..e7bd233 100644 --- a/test/coverage_reporter_test.exs +++ b/test/coverage_reporter_test.exs @@ -7,7 +7,7 @@ defmodule CoverageReporterTest do config = [ coverage_threshold: "80", - input_lcov_path: "lcov.info", + input_lcov_path: "*-lcov.info", github_ref: "refs/pull/1/merge", input_github_token: "github-token", github_workspace: workspace, @@ -25,7 +25,8 @@ defmodule CoverageReporterTest do setup_changes( bypass, config, - path: "path/to/file", + lcov_path: "1-lcov.info", + file_path: "path/to/file", status: "added", changed_lines: [1, 2, 3, 4, 5, 6, 7, 8], patch: "@@ -0,0 +1,8 @@\n+one\n+two\n+three\n+four\n+five\n+six\n+seven\n+eight\n", @@ -47,18 +48,60 @@ defmodule CoverageReporterTest do } ] } - }} = CoverageReporter.run(config) + }} = CoverageReporter.main(config) assert summary =~ "87.5%" end + test "with a partitioned lcov files", ctx do + %{bypass: bypass, config: config} = ctx + + setup_changes( + bypass, + config, + lcov_path: "1-lcov.info", + file_path: "path/to/file", + status: "added", + changed_lines: [1, 2, 3, 4, 5, 6, 7, 8], + patch: "@@ -0,0 +1,8 @@\n+one\n+two\n+three\n+four\n+five\n+six\n+seven\n+eight\n", + lcov: + "TN:\nSF:path/to/file\nDA:1,1\nDA:2,1\nDA:3,1\nDA:4,0\nDA:5,1\nDA:6,1\nDA:7,1\nDA:8,1\nend_of_record", + source_code: "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight" + ) + + setup_changes( + bypass, + config, + lcov_path: "2-lcov.info", + file_path: "path/to/file", + status: "added", + changed_lines: [1, 2, 3, 4, 5, 6, 7, 8], + patch: "@@ -0,0 +1,8 @@\n+one\n+two\n+three\n+four\n+five\n+six\n+seven\n+eight\n", + lcov: + "TN:\nSF:path/to/file\nDA:1,1\nDA:2,1\nDA:3,1\nDA:4,1\nDA:5,1\nDA:6,1\nDA:7,1\nDA:8,1\nend_of_record", + source_code: "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight" + ) + + assert {:ok, + %{ + conclusion: "success", + output: %{ + summary: summary, + annotations: [] + } + }} = CoverageReporter.main(config) + + assert summary =~ "100.0%" + end + test "with a two uncovered lines", ctx do %{bypass: bypass, config: config} = ctx setup_changes( bypass, config, - path: "path/to/file", + lcov_path: "1-lcov.info", + file_path: "path/to/file", status: "added", changed_lines: [1, 2, 3, 4, 5, 6, 7, 8], patch: "@@ -0,0 +1,8 @@\n+one\n+two\n+three\n+four\n+five\n+six\n+seven\n+eight\n", @@ -81,7 +124,7 @@ defmodule CoverageReporterTest do ] } }} = - CoverageReporter.run(config) + CoverageReporter.main(config) assert summary =~ "75.0%" end @@ -92,7 +135,8 @@ defmodule CoverageReporterTest do setup_changes( bypass, config, - path: "path/to/file", + lcov_path: "1-lcov.info", + file_path: "path/to/file", status: "added", changed_lines: [1, 2, 3, 4, 5, 6, 7, 8], patch: "@@ -0,0 +1,8 @@\n+one\n+two\n+three\n+four\n+five\n+six\n+seven\n+eight\n", @@ -114,7 +158,7 @@ defmodule CoverageReporterTest do } ] } - }} = CoverageReporter.run(config) + }} = CoverageReporter.main(config) assert summary =~ "75.0%" end @@ -125,7 +169,8 @@ defmodule CoverageReporterTest do setup_changes( bypass, config, - path: "path/to/file", + lcov_path: "1-lcov.info", + file_path: "path/to/file", status: "added", changed_lines: [1, 2, 3, 4, 5, 6, 7, 8], patch: "@@ -0,0 +1,8 @@\n+one\n+two\n+three\n+four\n+five\n+six\n+seven\n+eight\n", @@ -152,7 +197,7 @@ defmodule CoverageReporterTest do } ] } - }} = CoverageReporter.run(config) + }} = CoverageReporter.main(config) assert summary =~ "75.0%" end @@ -163,7 +208,8 @@ defmodule CoverageReporterTest do setup_changes( bypass, config, - path: "path/to/file", + lcov_path: "1-lcov.info", + file_path: "path/to/file", status: "added", changed_lines: [], patch: "", @@ -177,7 +223,7 @@ defmodule CoverageReporterTest do output: %{ annotations: [] } - }} = CoverageReporter.run(config) + }} = CoverageReporter.main(config) end test "without annotations", ctx do @@ -186,7 +232,8 @@ defmodule CoverageReporterTest do setup_changes( bypass, config, - path: "path/to/file", + lcov_path: "1-lcov.info", + file_path: "path/to/file", status: "added", changed_lines: [1], patch: "@@ -0,0 +1,1 @@\n+one", @@ -202,7 +249,7 @@ defmodule CoverageReporterTest do summary: summary, annotations: [] } - }} = CoverageReporter.run(config) + }} = CoverageReporter.main(config) assert summary =~ "100.0%" end @@ -213,7 +260,8 @@ defmodule CoverageReporterTest do setup_changes( bypass, config, - path: "path/to/file", + lcov_path: "1-lcov.info", + file_path: "path/to/file", status: "added", changed_lines: [1], patch: "@@ -1,1 +1,1 @@\n-one\n+two\n ", @@ -229,7 +277,7 @@ defmodule CoverageReporterTest do summary: summary, annotations: [] } - }} = CoverageReporter.run(config) + }} = CoverageReporter.main(config) assert summary =~ "100.0%" end @@ -242,7 +290,8 @@ defmodule CoverageReporterTest do setup_changes( bypass, config, - path: "path/to/file", + lcov_path: "1-lcov.info", + file_path: "path/to/file", status: "added", changed_lines: [1, 2, 3], patch: "@@ -0,0 +1,8 @@\n+one\n+two\n+three", @@ -250,7 +299,7 @@ defmodule CoverageReporterTest do source_code: "one\ntwo\nthree" ) - assert {:ok, %{output: %{summary: summary}}} = CoverageReporter.run(config) + assert {:ok, %{output: %{summary: summary}}} = CoverageReporter.main(config) assert summary =~ " path/to/file " end @@ -258,7 +307,8 @@ defmodule CoverageReporterTest do defp setup_changes(bypass, config, opts) do changed_lines = Keyword.fetch!(opts, :changed_lines) patch = Keyword.fetch!(opts, :patch) - path = Keyword.fetch!(opts, :path) + file_path = Keyword.fetch!(opts, :file_path) + lcov_path = Keyword.fetch!(opts, :lcov_path) status = Keyword.get(opts, :status, "added") lcov = Keyword.fetch!(opts, :lcov) source_code = Keyword.fetch!(opts, :source_code) @@ -271,7 +321,7 @@ defmodule CoverageReporterTest do %{ status: status, changed_lines: changed_lines, - filename: path, + filename: file_path, patch: patch } ]) @@ -306,17 +356,17 @@ defmodule CoverageReporterTest do ) end - File.write!(config[:github_workspace] <> "/" <> config[:input_lcov_path], lcov) + File.write!(config[:github_workspace] <> "/" <> lcov_path, lcov) directory = - String.split(path, "/") + String.split(file_path, "/") |> Enum.reverse() |> Enum.drop(1) |> Enum.reverse() |> Enum.join("/") File.mkdir_p!(config[:github_workspace] <> "/" <> directory) - File.write!(config[:github_workspace] <> "/" <> path, source_code) + File.write!(config[:github_workspace] <> "/" <> file_path, source_code) end defp json(conn, status, data) do