diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index deaaffc..8990e5c 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -22,90 +22,83 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Elixir - uses: erlef/setup-beam@v1.11 - id: setup + # - name: Elixir + # uses: erlef/setup-beam@v1.11 + # id: setup + # with: + # otp-version: ${{ env.OTP_VERSION_SPEC }} + # elixir-version: ${{ env.ELIXIR_VERSION_SPEC }} + # env: + # ImageOS: ubuntu20 + + # - name: Restore Deps Cache + # uses: actions/cache/restore@v3 + # id: deps-cache + # with: + # path: deps + # key: deps-${{ runner.os }}-${{ env.OTP_VERSION_SPEC }}-${{ env.ELIXIR_VERSION_SPEC }}-${{ hashFiles('mix.lock') }} + + # - name: Restore Build Cache + # uses: actions/cache/restore@v3 + # id: build-cache + # with: + # path: _build + # key: build-${{ runner.os }}-${{ env.OTP_VERSION_SPEC }}-${{ env.ELIXIR_VERSION_SPEC }}-${{ hashFiles('mix.lock') }} + + # - name: Restore PLT cache + # uses: actions/cache@v2 + # id: plt-cache + # with: + # key: plt-${{ runner.os }}-${{ env.OTP_VERSION_SPEC }}-${{ env.ELIXIR_VERSION_SPEC }} + # path: priv/plts + + # - 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 + + # - name: Check Formatting + # run: mix format --check-formatted + + # - name: Run Credo + # run: mix credo --strict + + # - name: Run Tests + # run: mix test --cover + + - name: Coverage Reporter + uses: peek-travel/coverage-reporter@partitioned-lcov-results + id: coverage-reporter + if: github.event_name == 'pull_request' with: - otp-version: ${{ env.OTP_VERSION_SPEC }} - elixir-version: ${{ env.ELIXIR_VERSION_SPEC }} - env: - ImageOS: ubuntu20 - - - name: Restore Deps Cache - uses: actions/cache/restore@v3 - id: deps-cache - with: - path: deps - key: deps-${{ runner.os }}-${{ env.OTP_VERSION_SPEC }}-${{ env.ELIXIR_VERSION_SPEC }}-${{ hashFiles('mix.lock') }} - - - name: Restore Build Cache - uses: actions/cache/restore@v3 - id: build-cache - with: - path: _build - key: build-${{ runner.os }}-${{ env.OTP_VERSION_SPEC }}-${{ env.ELIXIR_VERSION_SPEC }}-${{ hashFiles('mix.lock') }} - - - name: Restore PLT cache - uses: actions/cache@v2 - id: plt-cache - with: - key: plt-${{ runner.os }}-${{ env.OTP_VERSION_SPEC }}-${{ env.ELIXIR_VERSION_SPEC }} - path: priv/plts - - - 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 - - - name: Check Formatting - run: mix format --check-formatted - - - name: Run Credo - run: mix credo --strict - - - name: Run Tests - run: mix test --cover --export-coverage default - - - name: Run Coverage Reporter - run: | - mix coverage_reporter \ - --github-token ${{ secrets.GITHUB_TOKEN }} \ - --pull-number ${{ github.event.number }} \ - --repository ${{ github.repository }} \ - --head-branch ${{ github.head_ref }} \ - --commit-sha ${{ github.sha }} - - - name: Create Coverage Report Artifact - uses: actions/upload-artifact@v3 - with: - name: code-coverage - path: cover/reports/coverage_report.txt - - - name: Create PLTs - if: steps.plt-cache.outputs.cache-hit != 'true' - run: MIX_ENV=dev mix dialyzer --plt - - - name: Run dialyzer - run: MIX_ENV=dev mix dialyzer --format github - - - name: Save Build Cache - uses: actions/cache/save@v3 - with: - path: _build - key: build-${{ runner.os }}-${{ env.OTP_VERSION_SPEC }}-${{ env.ELIXIR_VERSION_SPEC }}-${{ hashFiles('mix.lock') }} - - - name: Save Deps Cache - uses: actions/cache/save@v3 - with: - path: deps - key: deps-${{ runner.os }}-${{ env.OTP_VERSION_SPEC }}-${{ env.ELIXIR_VERSION_SPEC }}-${{ hashFiles('mix.lock') }} - - - name: Save PLT cache - id: plt-cache-save - uses: actions/cache/save@v3 - with: - path: priv/plts - key: plt-${{ runner.os }}-${{ env.OTP_VERSION_SPEC }}-${{ env.ELIXIR_VERSION_SPEC }} + lcov_path: cover/lcov.info + coverage_threshold: 80 + + # - name: Create PLTs + # if: steps.plt-cache.outputs.cache-hit != 'true' + # run: MIX_ENV=dev mix dialyzer --plt + + # - name: Run dialyzer + # run: MIX_ENV=dev mix dialyzer --format github + + # - name: Save Build Cache + # uses: actions/cache/save@v3 + # with: + # path: _build + # key: build-${{ runner.os }}-${{ env.OTP_VERSION_SPEC }}-${{ env.ELIXIR_VERSION_SPEC }}-${{ hashFiles('mix.lock') }} + + # - name: Save Deps Cache + # uses: actions/cache/save@v3 + # with: + # path: deps + # key: deps-${{ runner.os }}-${{ env.OTP_VERSION_SPEC }}-${{ env.ELIXIR_VERSION_SPEC }}-${{ hashFiles('mix.lock') }} + + # - name: Save PLT cache + # id: plt-cache-save + # uses: actions/cache/save@v3 + # with: + # path: priv/plts + # key: plt-${{ runner.os }}-${{ env.OTP_VERSION_SPEC }}-${{ env.ELIXIR_VERSION_SPEC }} diff --git a/.tool-versions b/.tool-versions index af11155..af12053 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -erlang 26.0 -elixir 1.14.5-otp-26 +elixir 1.15.7-otp-26 +erlang 26.1 diff --git a/cover/lcov.info b/cover/lcov.info new file mode 100644 index 0000000..f53ac5e --- /dev/null +++ b/cover/lcov.info @@ -0,0 +1,467 @@ +TN:Elixir.Brian +SF:lib/brian.ex +FNF:0 +FNH:0 +LF:0 +LH:0 +end_of_record +TN:Elixir.Brian.Application +SF:lib/brian/application.ex +FNDA:0,config_change/3 +FNDA:1,start/2 +FNF:2 +FNH:1 +DA:10,1 +DA:24,1 +DA:25,1 +DA:32,0 +LF:4 +LH:3 +end_of_record +TN:Elixir.Brian.Wakatime +SF:lib/brian/wakatime.ex +FNDA:1,fetch_activity/0 +FNDA:1,impl/0 +FNF:2 +FNH:2 +DA:6,1 +DA:9,1 +LF:2 +LH:2 +end_of_record +TN:Elixir.Brian.Wakatime.External +SF:lib/brian/wakatime.ex +FNDA:0,fetch_activity/0 +FNDA:0,parse_activity/1 +FNF:2 +FNH:0 +DA:14,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:32,0 +DA:34,0 +DA:35,0 +LF:8 +LH:0 +end_of_record +TN:Elixir.BrianWeb +SF:lib/brian_web.ex +FNDA:3,MACRO-__using__/2 +FNDA:0,channel/0 +FNDA:0,controller/0 +FNDA:0,html/0 +FNDA:0,html_helpers/0 +FNDA:0,live_component/0 +FNDA:0,live_view/0 +FNDA:0,router/0 +FNDA:3,static_paths/0 +FNDA:3,verified_routes/0 +FNF:10 +FNH:3 +DA:20,3 +DA:109,3 +LF:2 +LH:2 +end_of_record +TN:Elixir.BrianWeb.CodeCoverageLive +SF:lib/brian_web/live/code_coverage_live.ex +FNDA:0,__components__/0 +FNDA:0,__live__/0 +FNDA:0,__phoenix_component_verify__/1 +FNDA:0,__phoenix_verify_routes__/1 +FNDA:0,render/1 +FNF:5 +FNH:0 +DA:1,0 +DA:5,0 +DA:22,0 +DA:28,0 +DA:41,0 +DA:60,0 +DA:94,0 +DA:111,0 +LF:8 +LH:0 +end_of_record +TN:Elixir.BrianWeb.ConnCase +SF:test/support/conn_case.ex +FNDA:3,MACRO-__using__/2 +FNDA:8,__ex_unit__/2 +FNDA:5,__ex_unit_setup_0/1 +FNF:3 +FNH:3 +DA:1,5 +DA:20,3 +DA:35,5 +LF:3 +LH:3 +end_of_record +TN:Elixir.BrianWeb.CoreComponents +SF:lib/brian_web/components/core_components.ex +FNDA:0,__components__/0 +FNDA:0,__phoenix_component_verify__/1 +FNDA:0,back/1 +FNDA:0,back (overridable 1)/1 +FNDA:0,button/1 +FNDA:0,button (overridable 1)/1 +FNDA:0,error/1 +FNDA:0,error (overridable 1)/1 +FNDA:6,flash/1 +FNDA:6,flash (overridable 1)/1 +FNDA:2,flash_group/1 +FNDA:2,flash_group (overridable 1)/1 +FNDA:0,header/1 +FNDA:0,header (overridable 1)/1 +FNDA:2,hide/1 +FNDA:5,hide/2 +FNDA:1,hide_modal/1 +FNDA:1,hide_modal/2 +FNDA:7,icon/1 +FNDA:7,icon (overridable 1)/1 +FNDA:0,input/1 +FNDA:0,input (overridable 1)/1 +FNDA:0,label/1 +FNDA:0,label (overridable 1)/1 +FNDA:0,list/1 +FNDA:0,list (overridable 1)/1 +FNDA:2,makeup/1 +FNDA:2,makeup (overridable 1)/1 +FNDA:1,modal/1 +FNDA:1,modal (overridable 1)/1 +FNDA:2,show/1 +FNDA:3,show/2 +FNDA:1,show_modal/1 +FNDA:1,show_modal/2 +FNDA:0,simple_form/1 +FNDA:0,simple_form (overridable 1)/1 +FNDA:0,table/1 +FNDA:0,table (overridable 1)/1 +FNDA:0,translate_error/1 +FNDA:0,translate_errors/2 +FNF:40 +FNH:18 +DA:1,7 +DA:46,1 +DA:47,1 +DA:48,1 +DA:49,1 +DA:50,1 +DA:51,1 +DA:54,1 +DA:57,1 +DA:58,1 +DA:65,1 +DA:66,1 +DA:67,1 +DA:69,1 +DA:73,1 +DA:74,1 +DA:79,1 +DA:82,1 +DA:83,1 +DA:110,6 +DA:111,2 +DA:112,6 +DA:113,2 +DA:114,2 +DA:118,2 +DA:119,2 +DA:121,2 +DA:123,2 +DA:124,2 +DA:125,2 +DA:126,2 +DA:130,2 +DA:146,2 +DA:147,2 +DA:148,2 +DA:149,2 +DA:157,2 +DA:186,0 +DA:187,0 +DA:189,0 +DA:190,0 +DA:213,0 +DA:214,0 +DA:215,0 +DA:219,0 +DA:221,0 +DA:223,0 +DA:278,0 +DA:279,0 +DA:280,0 +DA:281,0 +DA:282,0 +DA:286,0 +DA:288,0 +DA:289,0 +DA:291,0 +DA:292,0 +DA:294,0 +DA:295,0 +DA:297,0 +DA:299,0 +DA:301,0 +DA:303,0 +DA:309,0 +DA:310,0 +DA:311,0 +DA:312,0 +DA:313,0 +DA:314,0 +DA:316,0 +DA:317,0 +DA:319,0 +DA:320,0 +DA:322,0 +DA:328,0 +DA:329,0 +DA:330,0 +DA:331,0 +DA:332,0 +DA:333,0 +DA:337,0 +DA:338,0 +DA:340,0 +DA:341,0 +DA:342,0 +DA:349,0 +DA:350,0 +DA:351,0 +DA:352,0 +DA:353,0 +DA:354,0 +DA:355,0 +DA:356,0 +DA:360,0 +DA:361,0 +DA:363,0 +DA:365,0 +DA:377,0 +DA:378,0 +DA:379,0 +DA:390,0 +DA:392,0 +DA:393,0 +DA:408,0 +DA:409,0 +DA:412,0 +DA:414,0 +DA:415,0 +DA:418,0 +DA:449,0 +DA:450,0 +DA:451,0 +DA:454,0 +DA:459,0 +DA:463,0 +DA:464,0 +DA:465,0 +DA:468,0 +DA:470,0 +DA:471,0 +DA:472,0 +DA:476,0 +DA:477,0 +DA:481,0 +DA:485,0 +DA:488,0 +DA:514,0 +DA:517,0 +DA:518,0 +DA:537,0 +DA:539,0 +DA:540,0 +DA:541,0 +DA:569,7 +DA:570,7 +DA:576,2 +DA:577,3 +DA:585,2 +DA:586,5 +DA:595,1 +DA:597,1 +DA:599,1 +DA:602,1 +DA:604,1 +DA:607,1 +DA:610,1 +DA:613,1 +DA:614,1 +DA:616,1 +DA:632,0 +DA:633,0 +DA:641,0 +DA:648,2 +DA:649,2 +LF:154 +LH:55 +end_of_record +TN:Elixir.BrianWeb.Endpoint +SF:lib/brian_web/endpoint.ex +FNDA:2,__sockets__/0 +FNDA:0,broadcast/3 +FNDA:0,broadcast!/3 +FNDA:0,broadcast_from/4 +FNDA:0,broadcast_from!/4 +FNDA:1,call/2 +FNDA:1,call (overridable 2)/2 +FNDA:1,do_socket_dispatch/2 +FNDA:1,init/1 +FNDA:1,init/2 +FNDA:0,local_broadcast/3 +FNDA:0,local_broadcast_from/4 +FNDA:1,plug_builder_call/2 +FNDA:0,pubsub_server!/0 +FNDA:1,socket_dispatch/2 +FNDA:0,subscribe/1 +FNDA:0,subscribe/2 +FNDA:0,unsubscribe/1 +FNF:18 +FNH:8 +DA:1,1 +DA:2,1 +LF:2 +LH:2 +end_of_record +TN:Elixir.BrianWeb.ErrorHTML +SF:lib/brian_web/controllers/error_html.ex +FNDA:0,__components__/0 +FNDA:0,__phoenix_verify_routes__/1 +FNDA:2,render/2 +FNF:3 +FNH:1 +DA:1,0 +DA:17,2 +LF:2 +LH:1 +end_of_record +TN:Elixir.BrianWeb.ErrorJSON +SF:lib/brian_web/controllers/error_json.ex +FNDA:2,render/2 +FNF:1 +FNH:1 +DA:13,2 +LF:1 +LH:1 +end_of_record +TN:Elixir.BrianWeb.HomeLive +SF:lib/brian_web/live/home_live.ex +FNDA:0,__components__/0 +FNDA:9,__live__/0 +FNDA:0,__phoenix_component_verify__/1 +FNDA:0,__phoenix_verify_routes__/1 +FNDA:1,color/1 +FNDA:1,handle_info/2 +FNDA:2,mount/3 +FNDA:3,render/1 +FNF:8 +FNH:5 +DA:1,9 +DA:4,2 +DA:5,2 +DA:10,3 +DA:12,2 +DA:23,3 +DA:24,1 +DA:30,1 +DA:33,1 +DA:97,1 +DA:98,1 +DA:101,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:115,0 +DA:116,1 +DA:117,0 +LF:24 +LH:12 +end_of_record +TN:Elixir.BrianWeb.Layouts +SF:lib/brian_web/components/layouts.ex +FNDA:0,__components__/0 +FNDA:0,__mix_recompile__?/0 +FNDA:0,__phoenix_component_verify__/1 +FNDA:0,__phoenix_verify_routes__/1 +FNF:4 +FNH:0 +DA:1,0 +LF:1 +LH:0 +end_of_record +TN:Elixir.BrianWeb.PageController +SF:lib/brian_web/controllers/page_controller.ex +FNDA:0,__phoenix_verify_routes__/1 +FNDA:0,action/2 +FNDA:0,action (overridable 2)/2 +FNDA:0,call/2 +FNDA:0,home/2 +FNDA:0,init/1 +FNDA:0,phoenix_controller_pipeline/2 +FNF:7 +FNH:0 +DA:1,0 +DA:2,0 +DA:7,0 +LF:3 +LH:0 +end_of_record +TN:Elixir.BrianWeb.PageHTML +SF:lib/brian_web/controllers/page_html.ex +FNDA:0,__components__/0 +FNDA:0,__mix_recompile__?/0 +FNDA:0,__phoenix_component_verify__/1 +FNDA:0,__phoenix_verify_routes__/1 +FNF:4 +FNH:0 +DA:1,0 +LF:1 +LH:0 +end_of_record +TN:Elixir.BrianWeb.Router +SF:lib/brian_web/router.ex +FNDA:0,__checks__/0 +FNDA:0,__forward__/1 +FNDA:0,__helpers__/0 +FNDA:1,__match_route__/3 +FNDA:1,__pipe_through0__/1 +FNDA:0,__routes__/0 +FNDA:1,__verify_route__/1 +FNDA:0,api/2 +FNDA:1,browser/2 +FNDA:1,prepare/1 +FNF:10 +FNH:5 +DA:1,1 +DA:4,1 +DA:13,0 +DA:20,2 +DA:21,0 +LF:5 +LH:3 +end_of_record +TN:Elixir.BrianWeb.Telemetry +SF:lib/brian_web/telemetry.ex +FNDA:1,init/1 +FNDA:0,metrics/0 +FNDA:1,periodic_measurements/0 +FNDA:1,start_link/1 +FNF:4 +FNH:3 +DA:7,1 +DA:12,1 +DA:20,1 +DA:23,0 +DA:63,1 +LF:5 +LH:4 +end_of_record diff --git a/lib/brian_web/live/home_live.ex b/lib/brian_web/live/home_live.ex index dcd4ce9..cdf3a69 100644 --- a/lib/brian_web/live/home_live.ex +++ b/lib/brian_web/live/home_live.ex @@ -81,7 +81,10 @@ defmodule BrianWeb.HomeLive do
  • - + Get to Know Brian Berlin: Dad, Amateur Car Racer, and Software Engineer
  • diff --git a/lib/mix/tasks/coverage_reporter.ex b/lib/mix/tasks/coverage_reporter.ex deleted file mode 100644 index fd278c2..0000000 --- a/lib/mix/tasks/coverage_reporter.ex +++ /dev/null @@ -1,451 +0,0 @@ -defmodule Mix.Tasks.CoverageReporter do - @shortdoc "List of changed files in a pull github_request" - @moduledoc false - use Mix.Task - - @switches [ - github_token: :string, - pull_number: :integer, - repository: :string, - head_branch: :string, - commit_sha: :string - ] - - def run(args) do - Mix.Task.run("compile") - - {opts, _, _} = OptionParser.parse(args, strict: @switches) - - pull_number = Keyword.get(opts, :pull_number) - repository = Keyword.get(opts, :repository) - github_token = Keyword.get(opts, :github_token) - head_branch = Keyword.get(opts, :head_branch) - - %{ - changed_apps: changed_apps, - changed_files: changed_files - } = get_changed_files(repository, pull_number, github_token) - - changed_modules = changed_files |> Map.values() |> List.flatten() - - setup_cover(changed_apps) - - generate_lcov_file(changed_modules) - - {total, module_results} = get_coverage_reports(changed_modules) - - summary = create_summary(total, module_results) - - coverage_reports = create_coverage_reports(changed_modules) - - text = Enum.join([summary, coverage_reports], "\n\n") - - params = %{ - name: "Code Coverage Report", - head_sha: head_branch, - status: "completed", - conclusion: get_conclusion(total), - output: %{ - title: "Code Coverage Report", - summary: "Below is a summary of code coverages.", - text: String.slice(text, 0..65_500), - annotations: create_annotations(changed_files) - } - } - - github_request(:post, "#{repository}/check-runs", github_token, params) - - opts - |> Keyword.put(:summary, summary) - |> create_or_update_review_comment() - - Enum.each(changed_files, &create_pull_request_annotations(&1, opts)) - - File.mkdir_p!("cover/reports") - File.write!("cover/reports/coverage_report.txt", text) - end - - defp create_annotations(changed_files) do - changed_files - |> Enum.filter(fn {_, changed_modules} -> length(changed_modules) > 0 end) - |> Enum.flat_map(fn {changed_file, changed_modules} -> - {:result, results, _fail} = - changed_modules - |> Enum.map(&:"Elixir.#{&1}") - |> :cover.analyse(:coverage, :line) - - results - |> Enum.filter(fn {{_mod, line}, {_cov, _not_cov}} -> line > 1 end) - |> Enum.chunk_by(fn {{_mod, _line}, {cov, _not_cov}} -> cov == 1 end) - |> Enum.reject(fn [{{_mod, _line}, {cov, _not_cov}} | _] -> cov == 1 end) - |> Enum.map(fn lines -> - {{_mod, first_line}, _} = List.first(lines) - {{_mod, last_line}, _} = List.last(lines) - - %{ - title: "Code Coverage", - message: "Lines #{first_line}-#{last_line} not covered by tests.", - start_line: first_line, - end_line: last_line, - annotation_level: "warning", - path: changed_file - } - end) - end) - end - - defp create_pull_request_annotations({path, changed_modules}, opts) do - pull_number = Keyword.get(opts, :pull_number) - repository = Keyword.get(opts, :repository) - github_token = Keyword.get(opts, :github_token) - commit_sha = Keyword.get(opts, :commit_sha) - - body = - Enum.reduce(changed_modules, "", fn changed_module, acc -> - case File.read("cover/reports/#{changed_module}.txt") do - {:ok, coverage_report} -> - """ - #{acc} -
    - #{changed_module} - - ``` - #{coverage_report} - ``` -
    - """ - - _ -> - acc - end - end) - - params = %{ - path: path, - subject_type: "file", - body: body, - commit_id: commit_sha - } - - github_request(:post, "#{repository}/pulls/#{pull_number}/comments", github_token, params) - end - - defp create_or_update_review_comment(opts) do - %{ - repository: repository, - pull_number: pull_number, - github_token: github_token, - summary: summary - } = Map.new(opts) - - {:ok, {{_, 200, ~c"OK"}, _headers, body}} = - github_request(:get, "#{repository}/pulls/#{pull_number}/reviews?per_page=100", github_token) - - review = - body - |> to_string() - |> Jason.decode!() - |> Enum.find(&(&1["body"] =~ "Code Coverage Report")) - - if is_nil(review) do - github_request(:post, "#{repository}/pulls/#{pull_number}/reviews", github_token, %{ - body: summary, - event: "COMMENT" - }) - else - github_request(:put, "#{repository}/pulls/#{pull_number}/reviews/#{review["id"]}", github_token, %{ - body: summary - }) - end - end - - defp generate_lcov_file(changed_modules) do - changed_modules = Enum.map(changed_modules, &:"Elixir.#{&1}") - covered_modules = MapSet.intersection(MapSet.new(:cover.modules()), MapSet.new(changed_modules)) - - lcov = - covered_modules - |> Enum.sort() - |> Enum.map(fn mod -> - path = - mod.module_info(:compile)[:source] - |> to_string() - |> Path.relative_to(File.cwd!()) - - {:ok, fun_data} = :cover.analyse(mod, :calls, :function) - {functions_coverage, %{fnf: fnf, fnh: fnh}} = LcovEx.Stats.function_coverage_data(fun_data) - {:ok, lines_data} = :cover.analyse(mod, :calls, :line) - {lines_coverage, %{lf: lf, lh: lh}} = LcovEx.Stats.line_coverage_data(lines_data) - LcovEx.Formatter.format_lcov(mod, path, functions_coverage, fnf, fnh, lines_coverage, lf, lh) - end) - - File.mkdir_p!("cover") - File.write!("cover/lcov.info", lcov, [:write]) - end - - defp create_summary(total, module_results) do - Enum.join( - [ - "[Code Coverage Report](#report)", - "", - "| Percentage | Module |", - "|-----------|--------------------------|", - create_module_results(module_results), - "-----------|--------------------------", - display({total, "Total"}) - ], - "\n" - ) - end - - defp create_module_results([]), do: "| No Changes | |" - - defp create_module_results(module_results) do - Enum.map_join(module_results, "\n", &display(&1)) - end - - defp create_coverage_reports(changed_modules) do - File.mkdir_p!("cover/reports") - - Mix.shell().info("Writing Coverage Files") - - for mod <- changed_modules do - Mix.shell().info(" - #{mod}") - :cover.analyse_to_file(:"Elixir.#{mod}", ~c"cover/reports/#{mod}.txt") - end - - Mix.shell().info("") - - Enum.reduce(changed_modules, "", fn changed_module, acc -> - case File.read("cover/reports/#{changed_module}.txt") do - {:ok, coverage_report} -> - """ - #{acc} - - ---------------------------------------- - - [#{changed_module}](##{changed_module}) - - ``` - #{coverage_report} - ``` - """ - - _ -> - acc - end - end) - end - - defp get_conclusion(total) do - if total >= 90 do - "success" - else - "neutral" - end - end - - defp display({percentage, name}) do - "| #{format_number(percentage, 9)}% | #{format_name(name)} |" - end - - defp format_number(number, length), do: :io_lib.format("~#{length}.2f", [number]) - - defp format_name(name) when is_binary(name), do: name - defp format_name(mod) when is_atom(mod), do: inspect(mod) - - defp get_changed_files(repository, pull_number, github_token) do - {:ok, {{_, 200, ~c"OK"}, _headers, body}} = - github_request(:get, "#{repository}/pulls/#{pull_number}/files", github_token) - - body - |> to_string() - |> Jason.decode!() - |> do_get_changed_files() - end - - defp do_get_changed_files(changed_files) do - changed_files = - changed_files - |> Enum.reject(&String.equivalent?(&1["status"], "removed")) - |> Enum.map(& &1["filename"]) - |> Enum.filter(&String.ends_with?(&1, ".ex")) - - changed_apps = - changed_files - |> Enum.map(&Regex.scan(~r/apps\/([a-z_]+)\/.+/, &1, capture: :all_but_first)) - |> List.flatten() - |> Enum.uniq() - - %{ - changed_apps: changed_apps, - changed_files: - Map.new(changed_files, fn file -> - {file, get_modules(file)} - end) - } - end - - defp get_modules(file_path) do - app_dir = File.cwd!() - absolute_file_path = Path.join([app_dir, file_path]) - - if File.exists?(absolute_file_path) do - {:ok, contents} = File.read(absolute_file_path) - - ~r{defmodule \s+ (\S+) }x - |> Regex.scan(contents, capture: :all_but_first) - |> List.flatten() - |> Enum.reject(&String.equivalent?(&1, "\\s+")) - else - [] - end - end - - defp get_coverage_reports(changed_modules) do - {:result, results, _fail} = :cover.analyse(:coverage, :line) - changed_modules = Enum.map(changed_modules, &:"Elixir.#{&1}") - table = :ets.new(__MODULE__, [:set, :private]) - - for {{module, line}, cov} <- results, module in changed_modules, line != 0 do - case cov do - {1, 0} -> - :ets.insert(table, {{module, line}, true}) - - {0, 1} -> - :ets.insert_new(table, {{module, line}, false}) - end - end - - percentage = fn - 0, 0 -> 100.0 - covered, not_covered -> covered / (covered + not_covered) * 100 - end - - module_results = - for module <- changed_modules do - covered = :ets.select_count(table, [{{{module, :_}, true}, [], [true]}]) - not_covered = :ets.select_count(table, [{{{module, :_}, false}, [], [true]}]) - {percentage.(covered, not_covered), module} - end - - covered = :ets.select_count(table, [{{{:_, :_}, true}, [], [true]}]) - not_covered = :ets.select_count(table, [{{{:_, :_}, false}, [], [true]}]) - total = percentage.(covered, not_covered) - - {total, module_results} - end - - defp github_request(method, path, github_token, params \\ %{}) do - :inets.start() - :ssl.start() - - url = ~c"https://api.github.com/repos/#{path}" - - headers = [ - {~c"Authorization", ~c"Bearer #{github_token}"}, - {~c"Accept", ~c"application/vnd.github+json"}, - {~c"X-GitHub-Api-Version", ~c"2022-11-28"}, - {~c"User-Agent", ~c"CoverageReporter"} - ] - - request = - case method do - :get -> - {url, headers} - - :post -> - {url, headers, ~c"application/json", Jason.encode!(params)} - - :put -> - {url, headers, ~c"application/json", Jason.encode!(params)} - end - - ssl = [ - verify: :verify_peer, - cacerts: :public_key.cacerts_get(), - customize_hostname_check: [ - match_fun: :public_key.pkix_verify_hostname_match_fun(:https) - ] - ] - - :httpc.request(method, request, [ssl: ssl], []) - end - - defp setup_cover(changed_apps) do - _ = :cover.stop() - {:ok, pid} = :cover.start() - - config = Mix.Project.config() - - compile_paths = apps_paths(config) - - changed_paths = - Enum.filter(compile_paths, &Enum.any?(changed_apps, fn app -> String.contains?(&1, "lib/#{app}") end)) - - {:ok, string_io} = StringIO.open("") - - Process.group_leader(pid, string_io) - - Mix.shell().info("Cover Compiling") - - for compile_path <- changed_paths do - Mix.shell().info(" - #{compile_path}") - - case :cover.compile_beam(beams(compile_path)) do - results when is_list(results) -> - :ok - - {:error, reason} -> - Mix.raise( - "Failed to cover compile directory #{inspect(Path.relative_to_cwd(compile_path))} " <> - "with reason: #{inspect(reason)}" - ) - end - end - - Mix.shell().info("") - - for entry <- Path.wildcard("**/*.coverdata") do - entry - |> String.to_charlist() - |> :cover.import() - end - - pid - end - - defp apps_paths(config) do - if apps_paths = Mix.Project.apps_paths(config) do - build_path = Mix.Project.build_path(config) - - Enum.map(apps_paths, fn {app, _} -> - Path.join([build_path, "lib", Atom.to_string(app), "ebin"]) - end) - else - [Mix.Project.compile_path(config)] - end - end - - # Pick beams from the compile_path but if by any chance it is a protocol, - # gets its path from the code server (which will most likely point to - # the consolidation directory as long as it is enabled). - defp beams(dir) do - consolidation_dir = Mix.Project.consolidation_path() - - consolidated = - case File.ls(consolidation_dir) do - {:ok, files} -> files - _ -> [] - end - - for file <- File.ls!(dir), Path.extname(file) == ".beam" do - with true <- file in consolidated, - [_ | _] = path <- :code.which(file |> Path.rootname() |> String.to_atom()) do - path - else - _ -> String.to_charlist(Path.join(dir, file)) - end - end - end -end diff --git a/mix.exs b/mix.exs index acd3193..4a9f5e7 100644 --- a/mix.exs +++ b/mix.exs @@ -10,11 +10,11 @@ defmodule Brian.MixProject do start_permanent: Mix.env() == :prod, aliases: aliases(), deps: deps(), - dialyzer: [ - plt_file: {:no_warn, "priv/plts/dialyzer.plt"} - ], - preferred_cli_env: [ - lcov: :test + dialyzer: [plt_file: {:no_warn, "priv/plts/dialyzer.plt"}], + preferred_cli_env: [lcov: :test], + test_coverage: [ + tool: LcovEx, + ignore_paths: ["config/*", "test/support/*"] ] ] end