diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5d8d102f..9123df3a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,22 +8,27 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - otp: ['20.3'] - elixir: ['1.4', '1.5', '1.6', '1.7'] + include: + - otp: '21.3' + elixir: '1.11' + - otp: '26.0' + elixir: '1.15' + - otp: '27.0' + elixir: '1.17' env: MIX_ENV: test GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: erlef/setup-beam@v1 with: otp-version: ${{ matrix.otp }} elixir-version: ${{ matrix.elixir }} - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: deps - key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} + key: ${{ runner.os }}-${{ matrix.elixir }}-otp${{ matrix.otp }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} restore-keys: | - ${{ runner.os }}-mix- + ${{ runner.os }}-${{ matrix.elixir }}-otp${{ matrix.otp }}-mix- - run: mix deps.get - run: mix coveralls.github diff --git a/CHANGELOG.md b/CHANGELOG.md index 917064c6..33b0d585 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,49 @@ +0.18.3 +------ +#### Changes +- Avoid warning messages for Cobertura + - Update Range to use function syntax (#332) + +0.18.2 +------ +#### Enhancements +- Print warnings about incorrectly used ignore-markers (#325), such as start-marker +without a corresponding stop-marker, or two start-markers without a stop-marker in-between etc. + +#### Changes +- Fix Elixir 1.17 single-quoted string warning (#327) + +0.18.1 +------ +#### Changes +- Use explicit steps to remove 1.16 deprecation warning in Cobertura (#322). + +0.18.0 +------ +#### Changes +- Always floor coverage instead of rounding (#310). + - **Note:** If you want to keep the previous rounding behavior, please check the `floor_coverage` option. + - https://github.com/parroty/excoveralls#coverage-options + +0.17.1 +------ +#### Enhancements +- Accept custom http options (#319). + +0.17.0 +------ +#### Changes +- Replace hackney with httpc (#311). +- Update Elixir requirement to 1.11+ (#316). +- Fix lcov 2.0 source file handling (#315). +- Import .coverdata after test run and improve documentation (#309). + - Fixes around `--import-cover` option. + +0.16.1 +------ +#### Changes +- Cobertura now handles defprotocol and defimpl definitions (#306). + 0.16.0 ------ #### Enhancements diff --git a/README.md b/README.md index 92d2b9b8..843f1559 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,8 @@ def project do coveralls: :test, "coveralls.detail": :test, "coveralls.post": :test, - "coveralls.html": :test + "coveralls.html": :test, + "coveralls.cobertura": :test ] # if you want to use espec, # test_coverage: [tool: ExCoveralls, test_task: "espec"] @@ -55,7 +56,7 @@ end defp deps do [ - {:excoveralls, "~> 0.10", only: :test}, + {:excoveralls, "~> 0.18", only: :test}, ] end ``` @@ -88,16 +89,20 @@ end - [[mix coveralls.html] Show coverage as HTML report](#mix-coverallshtml-show-coverage-as-html-report) - [[mix coveralls.json] Show coverage as JSON report](#mix-coverallsjson-show-coverage-as-json-report) - [[mix coveralls.xml] Show coverage as XML report](#mix-coverallsxml-show-coverage-as-xml-report) + - [[mix coveralls.cobertura] Show coverage as Cobertura report](#mix-coverallscobertura-show-coverage-as-cobertura-report) - [[mix coveralls.lcov] Show coverage as lcov report (Experimental)](#mix-coverallslcov-show-coverage-as-lcov-report-experimental) - [coveralls.json](#coverallsjson) - - [Stop Words](#stop-words) - - [Exclude Files](#exclude-files) - - [Terminal Report Output](#terminal-report-output) - - [Coverage Options](#coverage-options) + - [Stop Words](#stop-words) + - [Exclude Files](#exclude-files) + - [Terminal Report Output](#terminal-report-output) + - [Coverage Options](#coverage-options) + - [Other Considerations](#other-considerations) - [Ignore Lines](#ignore-lines) + - [Silence OTP Cover Warnings](#silence-otp-cover-warnings) + - [Merging Coverage Results](#merging-coverage-results) - [Notes](#notes) - [Todo](#todo) - - [License](#license) +- [License](#license) ### [mix coveralls] Show coverage Run the `MIX_ENV=test mix coveralls` command to show coverage information on localhost. @@ -145,7 +150,8 @@ Usage: mix coveralls and your git repo resides in "app", then the root path should be: "/home/runs/app/" (from coveralls.io) --flagname Job flag name which will be shown in the Coveralls UI - --import_cover Directory from where '.coverdata' files should be imported and their results added to the report + --import-cover Directory from where '.coverdata' files should be imported and their results added to the report. + Coverdata is imported after tests are run. Usage: mix coveralls.detail [--filter file-name-pattern] Used to display coverage with detail @@ -362,6 +368,13 @@ Output to the shell is the same as running the command `mix coveralls` (to suppr Output reports are written to `cover/excoveralls.xml` by default, however, the path can be specified by overwriting the `"output_dir"` coverage option. +### [mix coveralls.cobertura] Show coverage as Cobertura report +This task displays coverage information at the source-code level formatted as a [Cobertura](https://cobertura.github.io/cobertura/) document. +The report follows a format supported by [Gitlab](https://docs.gitlab.com/ee/ci/testing/test_coverage_visualization.html) code coverage visualization. +Output to the shell is the same as running the command `mix coveralls` (to suppress this output, add `"print_summary": false` to your project's `coveralls.json` file). In a similar manner to `mix coveralls.detail`, reported source code can be filtered by specifying arguments using the `--filter` flag. + +Output reports are written to `cover/cobertura.xml` by default, however, the path can be specified by overwriting the `"output_dir"` coverage option. + ### [mix coveralls.lcov] Show coverage as lcov report (Experimental) This task displays coverage information at the line level formatted as a lcov. The report follows a format supported by several code coverage services like VSCode extension(`ryanluker.vscode-coverage-gutters`). @@ -432,6 +445,9 @@ to `false`: - When used in conjunction with `minimum_coverage`, overall project coverage is checked first before individual file coverages are checked. - `html_filter_full_covered` - A boolean, when `true` files with 100% coverage are not shown in the HTML report. Default to `false`. +- `floor_coverage` + - A boolean, when `false` coverage values are ceiled instead of floored, this means that a project with some lines + that are not covered can still have a total 100% coverage. Default to `true`. Example configuration file: @@ -461,6 +477,8 @@ Example configuration file: } ``` +## Other Considerations + ### Ignore Lines Use comments `coveralls-ignore-start` and `coveralls-ignore-stop` to ignore certain lines from code coverage calculation. @@ -501,6 +519,68 @@ imported_info(_Text,_Module,_Imported) -> ok. ``` +### Merging Coverage Results + +ExCoveralls can include `.coverdata` files in the result of the current test run through the `--import-cover` flag. This can be used to include coverage data from partitioned tests or integration tests that may run in a subprocess, for instance. + +Coverage data is generated when running `mix test --cover`, optionally with the `--export-coverage` flag to specify an output name. + +```shell +$ mix test --only integration --cover --export-coverage integration-coverage +Excluding tags: [:test] +Including tags: [:integration] +... test run omitted ... +# Coverage data written to cover/integration-coverage.coverdata + +# Report coverage, do not run integration tests +$ mix coveralls --exclude integration +Excluding tags: [:integration] +... test run omitted ... + +---------------- +COV FILE LINES RELEVANT MISSED +... +[TOTAL] 80.2% # <-- This result does not include coverage from integration tests +---------------- + +# Report coverage, do not run integration tests, but include previously written coverdata +$ mix coveralls --exclude integration --import-cover cover +Excluding tags: [:integration] +... test run omitted ... + +---------------- +COV FILE LINES RELEVANT MISSED +... +[TOTAL] 95.3% # <-- This result now includes coverage from integration tests +---------------- +``` + +Coverage data is imported after tests are run. + +See the `mix test` [Coverage documentation](https://hexdocs.pm/mix/Mix.Tasks.Test.html#module-coverage) for more information on `.coverdata`. + +### Configuring HTTP Options in ExCoveralls + +You can customize the HTTP options used by [`:httpc`](https://www.erlang.org/doc/man/httpc.html) when posting results. The example below shows how to specify a custom `cacertfile`: + +```elixir +config :excoveralls, + http_options: [ + timeout: 10_000, + ssl: [ + # Refer to the secure coding guide: + # https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/inets + verify: :verify_peer, + depth: 2, + customize_hostname_check: [ + match_fun: :public_key.pkix_verify_hostname_match_fun(:https) + ], + cacertfile: to_charlist(System.fetch_env!("TEST_COVERAGE_CACERTFILE")) + ] +``` + +By default, ExCoveralls uses the `cacertfile` from [`castore`](https://hexdocs.pm/castore/api-reference.html) when the dependency is installed. If it's not available and you're running Erlang `25` or later, the system will attempt to use the OS certificates via [`:public_key.cacerts_load/0`](https://www.erlang.org/doc/man/public_key.html#cacerts_load-0). + ### Notes - If mock library is used, it will show some warnings during execution. - https://github.com/eproxus/meck/pull/17 @@ -512,6 +592,6 @@ imported_info(_Text,_Module,_Imported) -> - It might not work well on projects which handle multiple project (Mix.Project) files. - Needs improvement on file-path handling. -## License +# License This source code is licensed under the MIT license. Copyright (c) 2013-present, parroty. diff --git a/lib/conf/coveralls.json b/lib/conf/coveralls.json index f6cfa931..0d1c485a 100644 --- a/lib/conf/coveralls.json +++ b/lib/conf/coveralls.json @@ -16,7 +16,8 @@ "coverage_options": { "treat_no_relevant_lines_as_covered": false, "output_dir": "cover/", - "minimum_coverage": 0 + "minimum_coverage": 0, + "floor_coverage": true }, "terminal_options": { diff --git a/lib/excoveralls.ex b/lib/excoveralls.ex index df80fa6f..38f74c47 100644 --- a/lib/excoveralls.ex +++ b/lib/excoveralls.ex @@ -41,13 +41,14 @@ defmodule ExCoveralls do def start(compile_path, opts) do Cover.compile(compile_path) - options = ConfServer.get() - if options[:import_cover] do - Cover.import(options[:import_cover]) - end - fn() -> - execute(ConfServer.get, compile_path, opts) + options = ConfServer.get() + + if options[:import_cover] do + Cover.import(options[:import_cover]) + end + + execute(options, compile_path, opts) end end diff --git a/lib/excoveralls/circle.ex b/lib/excoveralls/circle.ex index 627f6dc4..6c4bffa3 100644 --- a/lib/excoveralls/circle.ex +++ b/lib/excoveralls/circle.ex @@ -3,6 +3,7 @@ defmodule ExCoveralls.Circle do Handles circle-ci integration with coveralls. """ alias ExCoveralls.Poster + alias ExCoveralls.Stats def execute(stats, options) do json = generate_json(stats, Enum.into(options, %{})) @@ -20,7 +21,7 @@ defmodule ExCoveralls.Circle do service_number: get_number(), service_job_id: get_job_id(), service_pull_request: get_pull_request(), - source_files: stats, + source_files: Stats.serialize(stats), git: generate_git_info(), parallel: options[:parallel], flag_name: options[:flagname] diff --git a/lib/excoveralls/cobertura.ex b/lib/excoveralls/cobertura.ex index 91fe3af0..1d1d43ce 100644 --- a/lib/excoveralls/cobertura.ex +++ b/lib/excoveralls/cobertura.ex @@ -156,14 +156,12 @@ defmodule ExCoveralls.Cobertura do end defp module_name(source) do - case Regex.run(~r/^defmodule\s+(.*)\s+do$/m, source, capture: :all_but_first) do - [module] -> - module - - _ -> - [module] = Regex.run(~r/^-module\((.*)\)\.$/m, source, capture: :all_but_first) - module - end + with nil <- Regex.run(~r/^def(?:module|protocol|impl)\s+(.*)\s+do$/m, source, capture: :all_but_first), + nil <- Regex.run(~r/^-module\((.*)\)\.$/m, source, capture: :all_but_first) do + "UNKNOWN_MODULE" + else + [module] -> module + end end defp package_name(path, c_paths) do @@ -172,7 +170,7 @@ defmodule ExCoveralls.Cobertura do c_paths |> Enum.find_value(package_name, fn c_path -> if String.starts_with?(package_name, c_path) do - String.slice(package_name, (String.length(c_path) + 1)..-1) + String.slice(package_name, get_slice_range_for_package_name(c_path)) else false end @@ -182,6 +180,14 @@ defmodule ExCoveralls.Cobertura do |> to_charlist() end + # TODO: Remove when we require Elixir 1.12 as minimum and inline it with range syntax + if Version.match?(System.version(), ">= 1.12.0") do + # We use Range.new/3 because using x..y//step would give a syntax error on Elixir < 1.12 + defp get_slice_range_for_package_name(c_path), do: Range.new(String.length(c_path) + 1, -1, 1) + else + defp get_slice_range_for_package_name(c_path), do: Range.new(String.length(c_path) + 1, -1) + end + defp rate(valid_lines) when length(valid_lines) == 0, do: 0.0 defp rate(valid_lines) do diff --git a/lib/excoveralls/cover.ex b/lib/excoveralls/cover.ex index f444eaa1..a0d35502 100644 --- a/lib/excoveralls/cover.ex +++ b/lib/excoveralls/cover.ex @@ -14,7 +14,9 @@ defmodule ExCoveralls.Cover do {:ok, string_io} = StringIO.open("") Process.group_leader(pid, string_io) - :cover.compile_beam_directory(compile_path |> string_to_charlist) + compile_path + |> String.to_charlist() + |> :cover.compile_beam_directory() end @doc """ @@ -59,12 +61,6 @@ defmodule ExCoveralls.Cover do :cover.analyse(module, :calls, :line) end - if Version.compare(System.version, "1.3.0") == :lt do - defp string_to_charlist(string), do: String.to_char_list(string) - else - defp string_to_charlist(string), do: String.to_charlist(string) - end - defp file_exist?(module, path) do if File.exists?(path) do true diff --git a/lib/excoveralls/drone.ex b/lib/excoveralls/drone.ex index 6dbd45f6..610c1ea3 100644 --- a/lib/excoveralls/drone.ex +++ b/lib/excoveralls/drone.ex @@ -3,6 +3,7 @@ defmodule ExCoveralls.Drone do Handles drone-ci integration with coveralls. """ alias ExCoveralls.Poster + alias ExCoveralls.Stats def execute(stats, options) do json = generate_json(stats, Enum.into(options, %{})) @@ -20,7 +21,7 @@ defmodule ExCoveralls.Drone do service_number: get_build_num(), service_job_id: get_build_num(), service_pull_request: get_pull_request(), - source_files: stats, + source_files: Stats.serialize(stats), git: generate_git_info(), parallel: options[:parallel], flag_name: options[:flagname] diff --git a/lib/excoveralls/github.ex b/lib/excoveralls/github.ex index f5a04fb3..4c84b4ae 100644 --- a/lib/excoveralls/github.ex +++ b/lib/excoveralls/github.ex @@ -3,6 +3,7 @@ defmodule ExCoveralls.Github do Handles GitHub Actions integration with coveralls. """ alias ExCoveralls.Poster + alias ExCoveralls.Stats def execute(stats, options) do json = generate_json(stats, Enum.into(options, %{})) @@ -20,7 +21,7 @@ defmodule ExCoveralls.Github do %{ repo_token: get_env("GITHUB_TOKEN"), service_name: "github", - source_files: stats, + source_files: Stats.serialize(stats), parallel: options[:parallel], flag_name: options[:flagname], git: git_info() diff --git a/lib/excoveralls/gitlab.ex b/lib/excoveralls/gitlab.ex index b801d970..0dda681e 100644 --- a/lib/excoveralls/gitlab.ex +++ b/lib/excoveralls/gitlab.ex @@ -3,6 +3,7 @@ defmodule ExCoveralls.Gitlab do Handles gitlab-ci integration with coveralls. """ alias ExCoveralls.Poster + alias ExCoveralls.Stats def execute(stats, options) do json = generate_json(stats, Enum.into(options, %{})) @@ -23,7 +24,7 @@ defmodule ExCoveralls.Gitlab do service_number: get_number(), service_job_id: get_job_id(), service_pull_request: get_pull_request(), - source_files: stats, + source_files: Stats.serialize(stats), git: generate_git_info(), parallel: options[:parallel], flag_name: options[:flagname] diff --git a/lib/excoveralls/ignore.ex b/lib/excoveralls/ignore.ex index 4d6465bd..3663a75f 100644 --- a/lib/excoveralls/ignore.ex +++ b/lib/excoveralls/ignore.ex @@ -10,51 +10,167 @@ defmodule ExCoveralls.Ignore do Enum.map(info, &do_filter/1) end + defmodule State do + defstruct ignore_mode: :no_ignore, + coverage: [], + coverage_buffer: [], + warnings: [], + last_marker_index: nil + end + defp do_filter(%{name: name, source: source, coverage: coverage}) do - lines = String.split(source, "\n") - list = Enum.zip(lines, coverage) - |> Enum.map_reduce(:no_ignore, &check_and_swap/2) - |> elem(0) - |> List.zip - |> Enum.map(&Tuple.to_list(&1)) + source_lines = String.split(source, "\n") - [source, coverage] = parse_filter_list(list) - %{name: name, source: source, coverage: coverage} - end + processing_result = + Enum.zip(source_lines, coverage) + |> Enum.with_index() + |> Enum.reduce(%State{}, &process_line/2) + |> process_end_of_file() - defp check_and_swap({line, coverage}, ignore) do - { - coverage_for_line({line, coverage}, ignore), - ignore_next?(line, ignore) - } + updated_coverage = processing_result.coverage |> List.flatten() |> Enum.reverse() + warnings = Enum.sort_by(processing_result.warnings, &elem(&1, 0)) + %{name: name, source: source, coverage: updated_coverage, warnings: warnings} end - defp parse_filter_list([]), do: ["", []] - defp parse_filter_list([lines, coverage]), do: [Enum.join(lines, "\n"), coverage] - - defp coverage_for_line({line, coverage}, ignore) do - if ignore == :no_ignore do - {line, coverage} - else - {line, nil} + defp process_line({{source_line, coverage_line}, index}, state) do + case detect_ignore_marker(source_line) do + :none -> process_regular_line(coverage_line, index, state) + :start -> process_start_marker(coverage_line, index, state) + :stop -> process_stop_marker(coverage_line, index, state) + :next_line -> process_next_line_marker(coverage_line, index, state) end end - defp ignore_next?(line, ignore) do + defp detect_ignore_marker(line) do case Regex.run(~r/coveralls-ignore-(start|stop|next-line)/, line, capture: :all_but_first) do - ["start"] -> :ignore_block - ["stop"] -> :no_ignore - ["next-line"] -> - case ignore do - :ignore_block -> ignore - _sth -> :ignore_line - end - _sth -> - case ignore do - :ignore_line -> :no_ignore - _sth -> ignore - end + ["start"] -> :start + ["stop"] -> :stop + ["next-line"] -> :next_line + _sth -> :none end end + defp process_regular_line( + coverage_line, + _index, + state = %{ignore_mode: :no_ignore, coverage_buffer: []} + ) do + %{state | coverage: [coverage_line | state.coverage]} + end + + defp process_regular_line(_coverage_line, _index, state = %{ignore_mode: :ignore_line}) do + %{state | ignore_mode: :no_ignore, coverage: [nil | state.coverage]} + end + + defp process_regular_line(_coverage_line, _index, state = %{ignore_mode: :ignore_block}) do + %{state | coverage: [nil | state.coverage]} + end + + defp process_start_marker( + _coverage_line, + index, + state = %{ignore_mode: :no_ignore} + ) do + %{ + state + | ignore_mode: :ignore_block, + coverage: [nil | state.coverage], + last_marker_index: index + } + end + + defp process_start_marker(_coverage_line, index, state = %{ignore_mode: :ignore_block}) do + warning = {index, "unexpected ignore-start or missing previous ignore-stop"} + + %{ + state + | coverage: [nil | state.coverage], + warnings: [warning | state.warnings], + last_marker_index: index + } + end + + defp process_start_marker(_coverage_line, index, state = %{ignore_mode: :ignore_line}) do + warning = {state.last_marker_index, "redundant ignore-next-line right before an ignore-start"} + + %{ + state + | ignore_mode: :ignore_block, + coverage: [nil | state.coverage], + warnings: [warning | state.warnings], + last_marker_index: index + } + end + + defp process_stop_marker(_coverage_line, index, state = %{ignore_mode: :ignore_block}) do + %{ + state + | ignore_mode: :no_ignore, + coverage: [nil | state.coverage], + last_marker_index: index + } + end + + defp process_stop_marker(_coverage_line, index, state) do + warning = {index, "unexpected ignore-stop or missing previous ignore-start"} + + %{ + state + | ignore_mode: :no_ignore, + coverage: [nil | state.coverage], + warnings: [warning | state.warnings], + last_marker_index: index + } + end + + defp process_next_line_marker( + _coverage_line, + index, + state = %{ignore_mode: :no_ignore} + ) do + %{ + state + | ignore_mode: :ignore_line, + coverage: [nil | state.coverage], + last_marker_index: index + } + end + + defp process_next_line_marker( + _coverage_line, + index, + state = %{ignore_mode: :ignore_block} + ) do + warning = {index, "redundant ignore-next-line inside ignore block"} + + %{ + state + | coverage: [nil | state.coverage], + warnings: [warning | state.warnings] + } + end + + defp process_next_line_marker( + _coverage_line, + index, + state = %{ignore_mode: :ignore_line} + ) do + warning = {index, "duplicated ignore-next-line"} + + %{ + state + | coverage: [nil | state.coverage], + warnings: [warning | state.warnings], + last_marker_index: index + } + end + + defp process_end_of_file(state = %{ignore_mode: :ignore_block}) do + warning = + {state.last_marker_index, "ignore-start without a corresponding ignore-stop"} + + %{state | warnings: [warning | state.warnings]} + end + + defp process_end_of_file(state), do: state end diff --git a/lib/excoveralls/json.ex b/lib/excoveralls/json.ex index 6b8bc542..633bc046 100644 --- a/lib/excoveralls/json.ex +++ b/lib/excoveralls/json.ex @@ -2,6 +2,7 @@ defmodule ExCoveralls.Json do @moduledoc """ Generate JSON output for results. """ + alias ExCoveralls.Stats @file_name "excoveralls.json" @@ -16,7 +17,7 @@ defmodule ExCoveralls.Json do def generate_json(stats, _options) do Jason.encode!(%{ - source_files: stats + source_files: Stats.serialize(stats) }) end diff --git a/lib/excoveralls/lcov.ex b/lib/excoveralls/lcov.ex index f5310932..fd78c8d0 100644 --- a/lib/excoveralls/lcov.ex +++ b/lib/excoveralls/lcov.ex @@ -33,9 +33,10 @@ defmodule ExCoveralls.Lcov do lf = foundlines |> Enum.count() lh = foundlines |> Enum.filter(fn v -> v > 0 end) |> Enum.count() + sf = Path.expand(stat.name, ".") lines = - ["TN:", "SF:" <> stat.name] ++ + ["TN:", "SF:" <> sf] ++ da ++ [ "LF:" <> Integer.to_string(lf), diff --git a/lib/excoveralls/local.ex b/lib/excoveralls/local.ex index d18fe32b..83b69c18 100644 --- a/lib/excoveralls/local.ex +++ b/lib/excoveralls/local.ex @@ -2,7 +2,7 @@ defmodule ExCoveralls.Local do @moduledoc """ Locally displays the result to screen. """ - + defmodule Count do @moduledoc """ Stores count information for calculating coverage values. @@ -44,6 +44,7 @@ defmodule ExCoveralls.Local do enabled = ExCoveralls.Settings.get_print_summary if enabled and not ExCoveralls.ConfServer.summary_printed?() do coverage(stats, options) |> IO.puts() + warnings(stats) |> IO.write() ExCoveralls.ConfServer.summary_printed() end end @@ -90,6 +91,12 @@ defmodule ExCoveralls.Local do end end + def warnings(stats) do + for stat <- stats, {line_num, message} <- stat[:warnings], into: "" do + print_string("\e[33mwarning:\e[m ~s\n ~s:~b\n", [message, stat[:name], line_num + 1]) + end + end + defp sort(count_info, options) do if options[:sort] do sort_order = parse_sort_options(options) @@ -98,7 +105,7 @@ defmodule ExCoveralls.Local do Enum.map(count_info, fn original -> [stat, count] = original %{ - "cov" => get_coverage(count), + "cov" => ExCoveralls.Stats.get_coverage(count.relevant, count.covered), "file" => stat[:name], "lines" => count.lines, "relevant" => count.relevant, @@ -141,16 +148,16 @@ defmodule ExCoveralls.Local do end defp format_info([stat, count]) do - coverage = get_coverage(count) + coverage = ExCoveralls.Stats.get_coverage(count.relevant, count.covered) file_width = ExCoveralls.Settings.get_file_col_width - print_string("~5.1f% ~-#{file_width}s ~8w ~8w ~8w", + print_string("~5w% ~-#{file_width}s ~8w ~8w ~8w", [coverage, stat[:name], count.lines, count.relevant, count.relevant - count.covered]) end defp format_total(info) do totals = Enum.reduce(info, %Count{}, fn([_, count], acc) -> append(count, acc) end) - coverage = get_coverage(totals) - print_string("[TOTAL] ~5.1f%", [coverage]) + coverage = ExCoveralls.Stats.get_coverage(totals.relevant, totals.covered) + print_string("[TOTAL] ~5w%", [coverage]) end defp append(a, b) do @@ -161,21 +168,6 @@ defmodule ExCoveralls.Local do } end - defp get_coverage(count) do - case count.relevant do - 0 -> default_coverage_value() - _ -> (count.covered / count.relevant) * 100 - end - end - - defp default_coverage_value do - options = ExCoveralls.Settings.get_coverage_options - case Map.fetch(options, "treat_no_relevant_lines_as_covered") do - {:ok, false} -> 0.0 - _ -> 100.0 - end - end - @doc """ Calculate count information from the coverage stats. """ diff --git a/lib/excoveralls/post.ex b/lib/excoveralls/post.ex index 0f546f8f..e5982e58 100644 --- a/lib/excoveralls/post.ex +++ b/lib/excoveralls/post.ex @@ -3,6 +3,7 @@ defmodule ExCoveralls.Post do Handles general-purpose CI integration with coveralls. """ alias ExCoveralls.Poster + alias ExCoveralls.Stats def execute(stats, options) do json = generate_json(stats, options) @@ -17,7 +18,7 @@ defmodule ExCoveralls.Post do repo_token: options[:token], service_name: options[:service_name], service_number: options[:service_number], - source_files: source_info, + source_files: Stats.serialize(source_info), parallel: options[:parallel], flag_name: options[:flagname], git: generate_git_info(options) diff --git a/lib/excoveralls/poster.ex b/lib/excoveralls/poster.ex index fca0a6ec..39e87514 100644 --- a/lib/excoveralls/poster.ex +++ b/lib/excoveralls/poster.ex @@ -5,15 +5,10 @@ defmodule ExCoveralls.Poster do @file_name "excoveralls.post.json.gz" @doc """ - Create a temporarily json file and post it to server using hackney library. - Then, remove the file after it's completed. + Compresses the given `json` and posts it to the coveralls server. """ def execute(json, options \\ []) do - File.write!(@file_name, json |> :zlib.gzip()) - response = send_file(@file_name, options) - File.rm!(@file_name) - - case response do + case json |> :zlib.gzip() |> upload_zipped_json(options) do {:ok, message} -> IO.puts(message) @@ -22,44 +17,135 @@ defmodule ExCoveralls.Poster do end end - defp send_file(file_name, options) do - Application.ensure_all_started(:hackney) + defp upload_zipped_json(content, options) do + Application.ensure_all_started(:ssl) + Application.ensure_all_started(:inets) + endpoint = options[:endpoint] || "https://coveralls.io" - response = - :hackney.request( - :post, - "#{endpoint}/api/v1/jobs", - [], - {:multipart, - [ - {:file, file_name, {"form-data", [{"name", "json_file"}, {"filename", file_name}]}, - [{"Content-Type", "gzip/json"}]} - ]}, - [{:recv_timeout, 10_000}] - ) - - case response do - {:ok, status_code, _, _} when status_code in 200..299 -> + multipart_boundary = + "---------------------------" <> Base.encode16(:crypto.strong_rand_bytes(8), case: :lower) + + body = + [ + "--#{multipart_boundary}", + "content-length: #{byte_size(content)}", + "content-disposition: form-data; name=json_file; filename=#{@file_name}", + "content-type: gzip/json", + "", + content, + "--#{multipart_boundary}--" + ] + |> Enum.join("\r\n") + + headers = [ + {~c"Host", String.to_charlist(URI.parse(endpoint).host)}, + {~c"User-Agent", ~c"excoveralls"}, + {~c"Content-Length", String.to_charlist(Integer.to_string(byte_size(body)))}, + {~c"Accept", ~c"*/*"} + ] + + # All header names and values MUST be charlists in older OTP versions. In newer versions, + # binaries are fine. This is hard to debug because httpc simply *hangs* on older OTP + # versions if you use a binary value. + if Enum.any?(headers, fn {_, val} -> not is_list(val) end) do + raise "all header names and values must be charlists" + end + + request = { + String.to_charlist(endpoint) ++ ~c"/api/v1/jobs", + headers, + _content_type = ~c"multipart/form-data; boundary=#{multipart_boundary}", + body + } + + http_options = + case Application.get_env(:excoveralls, :http_options) do + [_ | _] = options -> + options + + _ -> + [ + timeout: 10_000, + ssl: + [ + verify: :verify_peer, + depth: 2, + customize_hostname_check: [ + match_fun: :public_key.pkix_verify_hostname_match_fun(:https) + ] + # https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/inets + ] ++ cacert_option() + ] + end + + case :httpc.request(:post, request, http_options, sync: true, body_format: :binary) do + {:ok, {{_protocol, status_code, _status_message}, _headers, _body}} + when status_code in 200..299 -> {:ok, "Successfully uploaded the report to '#{endpoint}'."} - {:ok, 500 = _status_code, _, _client} -> - {:ok, "API endpoint `#{endpoint}` is not available and return internal server error! Ignoring upload"} - {:ok, 405 = _status_code, _, _client} -> + {:ok, {{_protocol, 500, _status_message}, _headers, _body}} -> + {:ok, + "API endpoint `#{endpoint}` is not available and return internal server error! Ignoring upload"} + + {:ok, {{_protocol, 405, _status_message}, _headers, _body}} -> {:ok, "API endpoint `#{endpoint}` is not available due to maintenance! Ignoring upload"} - {:ok, status_code, _, client} -> - {:ok, body} = :hackney.body(client) + {:ok, {{_protocol, status_code, _status_message}, _headers, body}} -> {:error, - "Failed to upload the report to '#{endpoint}' (reason: status_code = #{status_code}, body = #{ - body - })."} + "Failed to upload the report to '#{endpoint}' (reason: status_code = #{status_code}, body = #{body})."} - {:error, reason} when reason in [:timeout, :connect_timeout] -> - {:ok, "Unable to upload the report to '#{endpoint}' due to a timeout. Not failing the build."} + {:error, reason} when reason in [:timeout, :connect_timeout] -> + {:ok, + "Unable to upload the report to '#{endpoint}' due to a timeout. Not failing the build."} {:error, reason} -> {:error, "Failed to upload the report to '#{endpoint}' (reason: #{inspect(reason)})."} end end + + # TODO: remove this once we depend on an Elixir version that requires OTP 25+. + if System.otp_release() >= "25" do + defp cacert_option do + if Code.ensure_loaded?(CAStore) do + [cacertfile: String.to_charlist(CAStore.file_path())] + else + case :public_key.cacerts_load() do + :ok -> + [cacerts: :public_key.cacerts_get()] + + {:error, reason} -> + raise ExCoveralls.ReportUploadError, + message: """ + Failed to load OS certificates. We tried to use OS certificates because we + couldn't find the :castore library. If you want to use :castore, please add + + {:castore, "~> 1.0"} + + to your dependencies. Otherwise, make sure you can load OS certificates by + running :public_key.cacerts_load() and checking the result. The error we + got was: + + #{inspect(reason)} + """ + end + end + end + else + defp cacert_option do + if Code.ensure_loaded?(CAStore) do + [cacertfile: String.to_charlist(CAStore.file_path())] + else + raise ExCoveralls.ReportUploadError, + message: """ + Failed to use any SSL certificates. We didn't find the :castore library, + and we couldn't use OS certificates because that requires OTP 25 or later. + If you want to use :castore, please add + + {:castore, "~> 1.0"} + + """ + end + end + end end diff --git a/lib/excoveralls/semaphore.ex b/lib/excoveralls/semaphore.ex index 5a91fc06..024c1721 100644 --- a/lib/excoveralls/semaphore.ex +++ b/lib/excoveralls/semaphore.ex @@ -3,6 +3,7 @@ defmodule ExCoveralls.Semaphore do Handles semaphore-ci integration with coveralls. """ alias ExCoveralls.Poster + alias ExCoveralls.Stats def execute(stats, options) do json = generate_json(stats, Enum.into(options, %{})) @@ -20,7 +21,7 @@ defmodule ExCoveralls.Semaphore do service_number: get_build_num(), service_job_id: get_build_num(), service_pull_request: get_pull_request(), - source_files: stats, + source_files: Stats.serialize(stats), git: generate_git_info(), parallel: options[:parallel], flag_name: options[:flagname] diff --git a/lib/excoveralls/settings.ex b/lib/excoveralls/settings.ex index c30f26e6..46a54ae0 100644 --- a/lib/excoveralls/settings.ex +++ b/lib/excoveralls/settings.ex @@ -30,12 +30,12 @@ defmodule ExCoveralls.Settings do Get default coverage value for lines marked as not relevant. """ def default_coverage_value do - case Map.fetch(get_coverage_options(), "treat_no_relevant_lines_as_covered") do - {:ok, true} -> 100.0 - _ -> 0.0 - end + get_coverage_options() |> default_coverage_value() end + def default_coverage_value(%{"treat_no_relevant_lines_as_covered" => true}), do: 100.0 + def default_coverage_value(_), do: 0.0 + @doc """ Get terminal output options from the json file. """ diff --git a/lib/excoveralls/stats.ex b/lib/excoveralls/stats.ex index 0d8a5b50..c7119551 100644 --- a/lib/excoveralls/stats.ex +++ b/lib/excoveralls/stats.ex @@ -198,16 +198,17 @@ defmodule ExCoveralls.Stats do {s+sloc, h+hits, m+misses} end - defp get_coverage(relevant, covered) do - value = case relevant do - 0 -> Settings.default_coverage_value - _ -> (covered / relevant) * 100 + def get_coverage(relevant, covered) do + coverage_options = Settings.get_coverage_options() + + approximate_fn = case coverage_options do + %{"floor_coverage" => false} -> &Float.round(&1, 1) + _ -> &Float.floor(&1, 1) end - if value == trunc(value) do - trunc(value) - else - Float.round(value, 1) + case relevant do + 0 -> Settings.default_coverage_value(coverage_options) + _ -> approximate_fn.((covered / relevant) * 100) end end @@ -222,6 +223,14 @@ defmodule ExCoveralls.Stats do %Line{coverage: Enum.at(coverage, i) , source: line} end + @doc """ + Converts coverage stats to a map, which can be serialized to JSON + for posting it to Coveralls or writing to excoveralls.json. + """ + def serialize(stats) do + Enum.map(stats, &Map.take(&1, [:name, :source, :coverage])) + end + @doc """ Exit the process with a status of 1 if coverage is below the minimum. """ diff --git a/lib/excoveralls/task/util.ex b/lib/excoveralls/task/util.ex index 229274bd..ddfe43d4 100644 --- a/lib/excoveralls/task/util.ex +++ b/lib/excoveralls/task/util.ex @@ -27,7 +27,8 @@ Usage: mix coveralls and your git repo resides in "app", then the root path should be: "/home/runs/app/" (from coveralls.io) --flagname Job flag name which will be shown in the Coveralls UI - --import_cover Directory from where '.coverdata' files should be imported and their results added to the report + --import-cover Directory from where '.coverdata' files should be imported and their results added to the report. + Coverdata is imported after tests are run. Usage: mix coveralls.detail [--filter file-name-pattern] Used to display coverage with detail diff --git a/lib/excoveralls/travis.ex b/lib/excoveralls/travis.ex index 966b02ca..00e54b22 100644 --- a/lib/excoveralls/travis.ex +++ b/lib/excoveralls/travis.ex @@ -3,6 +3,7 @@ defmodule ExCoveralls.Travis do Handles travis-ci integration with coveralls. """ alias ExCoveralls.Poster + alias ExCoveralls.Stats def execute(stats, options) do json = generate_json(stats, Enum.into(options, %{})) @@ -18,7 +19,7 @@ defmodule ExCoveralls.Travis do service_job_id: get_job_id(), service_name: "travis-pro", repo_token: get_repo_token(), - source_files: stats, + source_files: Stats.serialize(stats), git: generate_git_info() }) end @@ -26,7 +27,7 @@ defmodule ExCoveralls.Travis do Jason.encode!(%{ service_job_id: get_job_id(), service_name: "travis-ci", - source_files: stats, + source_files: Stats.serialize(stats), git: generate_git_info() }) end diff --git a/lib/templates/html/htmlcov/coverage.html.eex b/lib/templates/html/htmlcov/coverage.html.eex index b501429d..a21b708f 100644 --- a/lib/templates/html/htmlcov/coverage.html.eex +++ b/lib/templates/html/htmlcov/coverage.html.eex @@ -70,7 +70,7 @@ <%= number %> - <%= ExCoveralls.Html.View.safe(line.source || ' ') %> + <%= ExCoveralls.Html.View.safe(line.source || " ") %> <% end %> <% end %> diff --git a/mix.exs b/mix.exs index 7d40f00f..27a6ad54 100644 --- a/mix.exs +++ b/mix.exs @@ -6,8 +6,8 @@ defmodule ExCoveralls.Mixfile do def project do [ app: :excoveralls, - version: "0.16.0", - elixir: "~> 1.3", + version: "0.18.3", + elixir: "~> 1.11", elixirc_paths: elixirc_paths(Mix.env()), deps: deps(), docs: docs(), @@ -30,16 +30,17 @@ defmodule ExCoveralls.Mixfile do end def application do - [extra_applications: [:eex, :tools, :xmerl]] + [extra_applications: [:eex, :tools, :xmerl, :inets, :ssl, :public_key]] end defp elixirc_paths(:test), do: ["lib", "test/fixtures/test_missing.ex"] defp elixirc_paths(_), do: ["lib"] - def deps do + defp deps do [ + {:castore, "~> 1.0", optional: true}, {:jason, "~> 1.0"}, - {:hackney, "~> 1.16"}, + {:bypass, "~> 2.1.0", only: :test}, {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, {:meck, "~> 0.8", only: :test}, {:mock, "~> 0.3.6", only: :test}, diff --git a/mix.lock b/mix.lock index 6b7d7a46..73ff8823 100644 --- a/mix.lock +++ b/mix.lock @@ -1,21 +1,23 @@ %{ - "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"}, - "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, + "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, + "castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"}, + "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, + "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, + "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, "earmark_parser": {:hex, :earmark_parser, "1.4.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"}, "ex_doc": {:hex, :ex_doc, "0.23.0", "a069bc9b0bf8efe323ecde8c0d62afc13d308b1fa3d228b65bca5cf8703a529d", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f5e2c4702468b2fd11b10d39416ddadd2fcdd173ba2a0285ebd92c39827a5a16"}, - "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, - "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"}, "jason": {:hex, :jason, "1.1.1", "d3ccb840dfb06f2f90a6d335b536dd074db748b3e7f5b11ab61d239506585eb2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "639645cfac325e34938167b272bae0791fea3a34cf32c29525abf1d323ed4c18"}, "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, "makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"}, "meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm", "d34f013c156db51ad57cc556891b9720e6a1c1df5fe2e15af999c84d6cebeb1a"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, + "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mock": {:hex, :mock, "0.3.6", "e810a91fabc7adf63ab5fdbec5d9d3b492413b8cda5131a2a8aa34b4185eb9b4", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "bcf1d0a6826fb5aee01bae3d74474669a3fa8b2df274d094af54a25266a1ebd2"}, "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, - "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, + "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, + "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "sax_map": {:hex, :sax_map, "1.0.1", "51a9382d741504c34d49118fb36d691c303d042e1da88f8edae8ebe75fe74435", [:mix], [{:saxy, "~> 1.0", [hex: :saxy, repo: "hexpm", optional: false]}], "hexpm", "a7c57c25d23bfc3ce93cf93400dcfb447fe463d27ee8c6913545161e78dc487a"}, "saxy": {:hex, :saxy, "0.10.0", "38879f46a595862c22114792c71379355ecfcfa0f713b1cfcc59e1d4127f1f55", [:mix], [], "hexpm", "da130ed576e9f53d1a986ec5bd2fa72c1599501ede7d7a2dceb81acf53bf9790"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, } diff --git a/test/circle_test.exs b/test/circle_test.exs index 8c40471d..a29ddc29 100644 --- a/test/circle_test.exs +++ b/test/circle_test.exs @@ -7,7 +7,8 @@ defmodule ExCoveralls.CircleTest do @counts [0, 1, nil, nil] @source_info [%{name: "test/fixtures/test.ex", source: @content, - coverage: @counts + coverage: @counts, + warnings: [] }] setup do diff --git a/test/cobertura_test.exs b/test/cobertura_test.exs index 92d01a64..9176a2e8 100644 --- a/test/cobertura_test.exs +++ b/test/cobertura_test.exs @@ -9,7 +9,11 @@ defmodule ExCoveralls.CoberturaTest do @content "defmodule Test do\n def test do\n end\nend\n" @counts [0, 1, nil, nil] - @source_info [%{name: "test/fixtures/test.ex", source: @content, coverage: @counts}] + @source_info [%{name: "test/fixtures/test.ex", + source: @content, + coverage: @counts, + warnings: [] + }] @stats_result "" <> "----------------\n" <> @@ -198,4 +202,52 @@ defmodule ExCoveralls.CoberturaTest do assert String.ends_with?(source1, "/lib") assert String.ends_with?(source2, "/test/fixtures") end + + test_with_mock "generate cobertura file with defprotocol", _, ExCoveralls.Settings, [], + get_coverage_options: fn -> %{"output_dir" => @test_output_dir} end, + get_file_col_width: fn -> 40 end, + get_print_summary: fn -> true end, + get_print_files: fn -> true end do + content = "defprotocol TestProtocol do\n def test(value)\nend\n" + counts = [0, 1, nil, nil] + source_info = [%{name: "test/fixtures/test_protocol.ex", + source: content, + coverage: counts, + warnings: [] + }] + + stats_result = + "" <> + "----------------\n" <> + "COV FILE LINES RELEVANT MISSED\n" <> + " 50.0% test/fixtures/test_protocol.ex 4 2 1\n" <> + "[TOTAL] 50.0%\n" <> + "----------------\n" + + assert capture_io(fn -> Cobertura.execute(source_info) end) =~ stats_result + end + + test_with_mock "generate cobertura file with defimpl", _, ExCoveralls.Settings, [], + get_coverage_options: fn -> %{"output_dir" => @test_output_dir} end, + get_file_col_width: fn -> 40 end, + get_print_summary: fn -> true end, + get_print_files: fn -> true end do + content = "defimpl TestProtocol, for: Integer do\n def test(value), do: \"integer!\" \nend\n" + counts = [0, 1, nil, nil] + source_info = [%{name: "test/fixtures/test_impl.ex", + source: content, + coverage: counts, + warnings: [] + }] + + stats_result = + "" <> + "----------------\n" <> + "COV FILE LINES RELEVANT MISSED\n" <> + " 50.0% test/fixtures/test_impl.ex 4 2 1\n" <> + "[TOTAL] 50.0%\n" <> + "----------------\n" + + assert capture_io(fn -> Cobertura.execute(source_info) end) =~ stats_result + end end diff --git a/test/drone_test.exs b/test/drone_test.exs index 37cbe7e1..0d790041 100644 --- a/test/drone_test.exs +++ b/test/drone_test.exs @@ -7,7 +7,8 @@ defmodule ExCoveralls.DroneTest do @counts [0, 1, nil, nil] @source_info [%{name: "test/fixtures/test.ex", source: @content, - coverage: @counts + coverage: @counts, + warnings: [] }] setup do diff --git a/test/github_test.exs b/test/github_test.exs index 429ccf6e..94b86270 100644 --- a/test/github_test.exs +++ b/test/github_test.exs @@ -5,7 +5,12 @@ defmodule ExCoveralls.GithubTest do @content "defmodule Test do\n def test do\n end\nend\n" @counts [0, 1, nil, nil] - @source_info [%{name: "test/fixtures/test.ex", source: @content, coverage: @counts}] + @source_info [%{name: "test/fixtures/test.ex", + source: @content, + coverage: @counts, + warnings: [] + }] + setup do # No additional context github_event_path = System.get_env("GITHUB_EVENT_PATH") diff --git a/test/gitlab_test.exs b/test/gitlab_test.exs index 5d819725..674ca74b 100644 --- a/test/gitlab_test.exs +++ b/test/gitlab_test.exs @@ -5,7 +5,11 @@ defmodule ExCoveralls.GitlabTest do @content "defmodule Test do\n def test do\n end\nend\n" @counts [0, 1, nil, nil] - @source_info [%{name: "test/fixtures/test.ex", source: @content, coverage: @counts}] + @source_info [%{name: "test/fixtures/test.ex", + source: @content, + coverage: @counts, + warnings: [] + }] setup do # Capture existing values diff --git a/test/html_test.exs b/test/html_test.exs index b69552a5..9cdf8aef 100644 --- a/test/html_test.exs +++ b/test/html_test.exs @@ -5,7 +5,7 @@ defmodule ExCoveralls.HtmlTest do alias ExCoveralls.Html @file_name "excoveralls.html" - @file_size 20375 + @file_size 20381 @test_output_dir "cover_test/" @test_template_path "lib/templates/html/htmlcov/" @@ -13,7 +13,8 @@ defmodule ExCoveralls.HtmlTest do @counts [0, 1, nil, nil] @source_info [%{name: "test/fixtures/test.ex", source: @content, - coverage: @counts + coverage: @counts, + warnings: [] }] @stats_result "" <> @@ -35,7 +36,7 @@ defmodule ExCoveralls.HtmlTest do File.rm!(path) File.rmdir!(@test_output_dir) end - + ExCoveralls.ConfServer.clear() end @@ -68,4 +69,39 @@ defmodule ExCoveralls.HtmlTest do assert(size == @file_size) end + @tag :skip + test_with_mock "Exit status code is 1 when actual coverage does not reach the minimum", + ExCoveralls.Settings, [ + get_coverage_options: fn -> coverage_options(100) end, + get_file_col_width: fn -> 40 end, + get_print_summary: fn -> true end, + get_print_files: fn -> true end + ] do + output = capture_io(fn -> + assert catch_exit(Html.execute(@source_info)) == {:shutdown, 1} + end) + assert String.contains?(output, "FAILED: Expected minimum coverage of 100%, got 50.0%.") + end + + @tag :skip + test_with_mock "Exit status code is 0 when actual coverage reaches the minimum", + ExCoveralls.Settings, [ + get_coverage_options: fn -> coverage_options(49.9) end, + get_file_col_width: fn -> 40 end, + get_print_summary: fn -> true end, + get_print_files: fn -> true end + ] do + assert capture_io(fn -> + Html.execute(@source_info) + end) =~ @stats_result + end + + defp coverage_options(minimum_coverage) do + %{ + "minimum_coverage" => minimum_coverage, + "output_dir" => @test_output_dir, + "template_path" => @test_template_path + } + end + end diff --git a/test/ignore_test.exs b/test/ignore_test.exs index c661f5bb..f3587309 100644 --- a/test/ignore_test.exs +++ b/test/ignore_test.exs @@ -2,7 +2,7 @@ defmodule ExCoveralls.IgnoreTest do use ExUnit.Case alias ExCoveralls.Ignore - @block_content """ + @content """ defmodule Test do def test do end @@ -12,13 +12,20 @@ defmodule ExCoveralls.IgnoreTest do #coveralls-ignore-stop end """ - @block_counts [0, 0, 0, nil, 0, 0, nil, 0, 0] - @block_source_info [%{name: "test/fixtures/test.ex", - source: @block_content, - coverage: @block_counts + @counts [0, 0, 0, nil, 0, 0, nil, 0, 0] + @source_info [%{name: "test/fixtures/test.ex", + source: @content, + coverage: @counts }] - @single_line_content """ + test "filter ignored lines with start/stop block returns valid list" do + info = Ignore.filter(@source_info) |> Enum.at(0) + assert(info[:source] == @content) + assert(info[:coverage] == [0, 0, 0, nil, nil, nil, nil, 0, 0]) + assert(info[:warnings] == []) + end + + @content """ defmodule Test do def test do end @@ -29,13 +36,20 @@ defmodule ExCoveralls.IgnoreTest do end end """ - @single_line_counts [0, 0, 0, nil, 0, 0, 0, 0, 0, 0] - @single_line_source_info [%{name: "test/fixtures/test.ex", - source: @single_line_content, - coverage: @single_line_counts + @counts [0, 0, 0, nil, 0, 0, 0, 0, 0, 0] + @source_info [%{name: "test/fixtures/test.ex", + source: @content, + coverage: @counts }] - @mixed_content """ + test "filter ignored lines with next-line returns valid list" do + info = Ignore.filter(@source_info) |> Enum.at(0) + assert(info[:source] == @content) + assert(info[:coverage] == [0, 0, 0, nil, nil, 0, 0, 0, 0, 0]) + assert(info[:warnings] == []) + end + + @content """ defmodule Test do def test do end @@ -48,27 +62,89 @@ defmodule ExCoveralls.IgnoreTest do end end """ - @mixed_counts [0, 0, 0, nil, 0, nil, 0, nil, 0, 0, 0, 0] - @mixed_source_info [%{name: "test/fixtures/test.ex", - source: @mixed_content, - coverage: @mixed_counts + @counts [0, 0, 0, nil, 0, nil, 0, nil, 0, 0, 0, 0] + @source_info [%{name: "test/fixtures/test.ex", + source: @content, + coverage: @counts }] - test "filter ignored lines with start/stop block returns valid list" do - info = Ignore.filter(@block_source_info) |> Enum.at(0) - assert(info[:source] == @block_content) - assert(info[:coverage] == [0, 0, 0, nil, nil, nil, nil, 0, 0]) + test "next-line marker inside start/stop block produces warning" do + info = Ignore.filter(@source_info) |> Enum.at(0) + assert(info[:source] == @content) + assert(info[:coverage] == [0, 0, 0, nil, nil, nil, nil, nil, 0, 0, 0, 0]) + assert(info[:warnings] == [{5, "redundant ignore-next-line inside ignore block"}]) end - test "filter ignored lines with next-line returns valid list" do - info = Ignore.filter(@single_line_source_info) |> Enum.at(0) - assert(info[:source] == @single_line_content) - assert(info[:coverage] == [0, 0, 0, nil, nil, 0, 0, 0, 0, 0]) + @content """ + defmodule Test do + #coveralls-ignore-next-line + #coveralls-ignore-next-line end + """ + @counts [0, nil, nil, 0, 0] + @source_info [%{name: "test/fixtures/test.ex", + source: @content, + coverage: @counts + }] - test "filter ignored lines with next-line inside start/stop block returns valid list" do - info = Ignore.filter(@mixed_source_info) |> Enum.at(0) - assert(info[:source] == @mixed_content) - assert(info[:coverage] == [0, 0, 0, nil, nil, nil, nil, nil, 0, 0, 0, 0]) + test "next-line marker right after another next-line marker produces warning" do + info = Ignore.filter(@source_info) |> Enum.at(0) + assert(info[:source] == @content) + assert(info[:coverage] == [0, nil, nil, nil, 0]) + assert(info[:warnings] == [{2, "duplicated ignore-next-line"}]) + end + + @content """ + defmodule Test do + def test do + end + #coveralls-ignore-start + def test do + end + #coveralls-ignore-stop + def test_not_ignored do + end + #coveralls-ignore-start + def test_missing_stop + end + end + """ + @counts [0, 0, 0, nil, 0, 0, nil, 0, 0, nil, 0, 0, 0, 0] + @source_info [%{name: "test/fixtures/test.ex", + source: @content, + coverage: @counts + }] + + test "start marker without a stop marker produces warning" do + info = Ignore.filter(@source_info) |> Enum.at(0) + assert(info[:source] == @content) + assert(info[:coverage] == [0, 0, 0, nil, nil, nil, nil, 0, 0, nil, nil, nil, nil, nil]) + assert(info[:warnings] == [{9, "ignore-start without a corresponding ignore-stop"}]) + end + + @content """ + defmodule Test do + def test do + end + #coveralls-ignore-start + def test do + end + #coveralls-ignore-start + def test_ignore + end + #coveralls-ignore-stop + end + """ + @counts [0, 0, 0, nil, 0, 0, nil, 0, 0, nil, 0, 0] + @source_info [%{name: "test/fixtures/test.ex", + source: @content, + coverage: @counts + }] + + test "start marker followed by another start marker produces warning" do + info = Ignore.filter(@source_info) |> Enum.at(0) + assert(info[:source] == @content) + assert(info[:coverage] == [0, 0, 0, nil, nil, nil, nil, nil, nil, nil, 0, 0]) + assert(info[:warnings] == [{6, "unexpected ignore-start or missing previous ignore-stop"}]) end end diff --git a/test/json_test.exs b/test/json_test.exs index b129281d..92d6f169 100644 --- a/test/json_test.exs +++ b/test/json_test.exs @@ -12,7 +12,8 @@ defmodule ExCoveralls.JsonTest do @counts [0, 1, nil, nil] @source_info [%{name: "test/fixtures/test.ex", source: @content, - coverage: @counts + coverage: @counts, + warnings: [] }] @stats_result "" <> @@ -34,7 +35,7 @@ defmodule ExCoveralls.JsonTest do File.rm!(path) File.rmdir!(@test_output_dir) end - + ExCoveralls.ConfServer.clear() end @@ -53,7 +54,16 @@ defmodule ExCoveralls.JsonTest do Json.execute(@source_info) end) =~ @stats_result - assert(File.read!(report) =~ ~s({"source_files":[{"coverage":[0,1,null,null],"name":"test/fixtures/test.ex","source":"defmodule Test do\\n def test do\\n end\\nend\\n"}]})) + assert( + %{ + "source_files" => [ + %{ + "coverage" => [0, 1, nil, nil], + "name" => "test/fixtures/test.ex", + "source" => "defmodule Test do\n def test do\n end\nend\n" + } + ] + } = Jason.decode!(File.read!(report))) %{size: size} = File.stat! report assert(size == @file_size) end @@ -63,7 +73,16 @@ defmodule ExCoveralls.JsonTest do Json.execute(@source_info, [output_dir: @test_output_dir]) end) =~ @stats_result - assert(File.read!(report) =~ ~s({"source_files":[{"coverage":[0,1,null,null],"name":"test/fixtures/test.ex","source":"defmodule Test do\\n def test do\\n end\\nend\\n"}]})) + assert( + %{ + "source_files" => [ + %{ + "coverage" => [0, 1, nil, nil], + "name" => "test/fixtures/test.ex", + "source" => "defmodule Test do\n def test do\n end\nend\n" + } + ] + } = Jason.decode!(File.read!(report))) %{size: size} = File.stat! report assert(size == @file_size) end diff --git a/test/lcov_test.exs b/test/lcov_test.exs index 9146b8f8..ac52896b 100644 --- a/test/lcov_test.exs +++ b/test/lcov_test.exs @@ -5,14 +5,15 @@ defmodule ExCoveralls.LcovTest do alias ExCoveralls.Lcov @file_name "lcov.info" - @file_size 67 @test_output_dir "cover_test/" @content "defmodule Test do\n def test do\n end\nend\n" @counts [0, 1, nil, nil] - @source_info [%{name: "test/fixtures/test.ex", + @test_file_name "test/fixtures/test.ex" + @source_info [%{name: @test_file_name, source: @content, - coverage: @counts + coverage: @counts, + warnings: [] }] @stats_result "" <> @@ -53,9 +54,9 @@ defmodule ExCoveralls.LcovTest do Lcov.execute(@source_info) end) =~ @stats_result - assert(File.read!(report) =~ ~s(TN:\nSF:test/fixtures/test.ex\nDA:1,0\nDA:2,1\nLF:2\nLH:1\nend_of_record\n)) - %{size: size} = File.stat! report - assert(size == @file_size) + source_file = Path.expand(@test_file_name, ".") + + assert(File.read!(report) =~ ~s(TN:\nSF:#{source_file}\nDA:1,0\nDA:2,1\nLF:2\nLH:1\nend_of_record\n)) end test "generate json file with output_dir parameter", %{report: report} do @@ -63,8 +64,8 @@ defmodule ExCoveralls.LcovTest do Lcov.execute(@source_info, [output_dir: @test_output_dir]) end) =~ @stats_result - assert(File.read!(report) =~ ~s(TN:\nSF:test/fixtures/test.ex\nDA:1,0\nDA:2,1\nLF:2\nLH:1\nend_of_record\n)) - %{size: size} = File.stat! report - assert(size == @file_size) + source_file = Path.expand(@test_file_name, ".") + + assert(File.read!(report) =~ ~s(TN:\nSF:#{source_file}\nDA:1,0\nDA:2,1\nLF:2\nLH:1\nend_of_record\n)) end end diff --git a/test/local_test.exs b/test/local_test.exs index 29f7e7e1..121ee5a2 100644 --- a/test/local_test.exs +++ b/test/local_test.exs @@ -8,21 +8,32 @@ defmodule ExCoveralls.LocalTest do @counts [0, 1, nil, nil] @source_info [%{name: "test/fixtures/test.ex", source: @content, - coverage: @counts + coverage: @counts, + warnings: [] }] @invalid_counts [0, 1, nil, "invalid"] @invalid_source_info [%{name: "test/fixtures/test.ex", source: @content, - coverage: @invalid_counts + coverage: @invalid_counts, + warnings: [] }] @empty_counts [nil, nil, nil, nil] @empty_source_info [%{name: "test/fixtures/test.ex", source: @content, - coverage: @empty_counts + coverage: @empty_counts, + warnings: [] }] + @warning_source_info [%{ + name: "test/fixtures/test.ex", + source: @content, + coverage: @counts, + warnings: [{2, "this is a test"}, {4, "another test"}] + }] + + @stats_result "" <> "----------------\n" <> "COV FILE LINES RELEVANT MISSED\n" <> @@ -37,7 +48,13 @@ defmodule ExCoveralls.LocalTest do "\e[31mdefmodule Test do\e[m\n\e[32m def test do\e[m\n" <> " end\n" <> "end" - + + @warning_result "" <> + "\n\e[33mwarning:\e[m this is a test\n" <> + " test/fixtures/test.ex:3\n" <> + "\e[33mwarning:\e[m another test\n" <> + " test/fixtures/test.ex:5\n" + setup do ExCoveralls.ConfServer.clear() on_exit(fn -> ExCoveralls.ConfServer.clear() end) @@ -74,12 +91,18 @@ defmodule ExCoveralls.LocalTest do end end + test "display warnings" do + assert capture_io(fn -> + Local.execute(@warning_source_info) + end) =~ @warning_result + end + test "Empty (no relevant lines) file is calculated as 0.0%" do - assert String.contains?(Local.coverage(@empty_source_info), "[TOTAL] 100.0%") + assert String.contains?(Local.coverage(@empty_source_info), "[TOTAL] 0.0%") end test_with_mock "Empty (no relevant lines) file with treat_no_relevant_lines_as_covered=true option is calculated as 100.0%", - ExCoveralls.Settings, [ + ExCoveralls.Settings, [:passthrough], [ get_coverage_options: fn -> %{"treat_no_relevant_lines_as_covered" => true} end, get_file_col_width: fn -> 40 end, get_print_files: fn -> true end @@ -88,7 +111,7 @@ defmodule ExCoveralls.LocalTest do end test_with_mock "Empty (no relevant lines) file with treat_no_relevant_lines_as_covered=false option is calculated as 0.0%", - ExCoveralls.Settings, [ + ExCoveralls.Settings, [:passthrough], [ get_coverage_options: fn -> %{"treat_no_relevant_lines_as_covered" => false} end, get_file_col_width: fn -> 40 end, get_print_files: fn -> true end @@ -96,6 +119,33 @@ defmodule ExCoveralls.LocalTest do assert String.contains?(Local.coverage(@empty_source_info), "[TOTAL] 0.0%") end + @tag :skip + test_with_mock "Exit status code is 1 when actual coverage does not reach the minimum", + ExCoveralls.Settings, [ + get_coverage_options: fn -> %{"minimum_coverage" => 100} end, + get_file_col_width: fn -> 40 end, + get_print_summary: fn -> true end, + get_print_files: fn -> true end + ] do + output = capture_io(fn -> + assert catch_exit(Local.execute(@source_info)) == {:shutdown, 1} + end) + assert String.contains?(output, "FAILED: Expected minimum coverage of 100%, got 50.0%.") + end + + @tag :skip + test_with_mock "Exit status code is 0 when actual coverage reaches the minimum", + ExCoveralls.Settings, [ + get_coverage_options: fn -> %{"minimum_coverage" => 49.9} end, + get_file_col_width: fn -> 40 end, + get_print_summary: fn -> true end, + get_print_files: fn -> true end + ] do + assert capture_io(fn -> + Local.execute(@source_info) + end) =~ @stats_result + end + test_with_mock "No output if print_summary is false", ExCoveralls.Settings, [ get_coverage_options: fn -> %{"minimum_coverage" => 49.9} end, diff --git a/test/post_test.exs b/test/post_test.exs index 810ca75a..85b5274f 100644 --- a/test/post_test.exs +++ b/test/post_test.exs @@ -7,7 +7,8 @@ defmodule ExCoveralls.PostTest do @counts [0, 1, nil, nil] @source_info [%{name: "test/fixtures/test.ex", source: @content, - coverage: @counts + coverage: @counts, + warnings: [] }] test_with_mock "execute", ExCoveralls.Poster, [execute: fn(_, _) -> "result" end] do @@ -34,15 +35,23 @@ defmodule ExCoveralls.PostTest do flagname: "arbitrary_value" ]) - assert json == - "{\"flag_name\":\"arbitrary_value\",\"git\":{\"branch\":\"\",\"head\":{\"committer_name\":\"\",\"id\":\"\",\"message\":\"\"}}," <> - "\"parallel\":null," <> - "\"repo_token\":\"1234567890\"," <> - "\"service_name\":\"local\"," <> - "\"service_number\":\"build_num_1\"," <> - "\"source_files\":" <> - "[{\"coverage\":[0,1,null,null]," <> - "\"name\":\"test/fixtures/test.ex\"," <> - "\"source\":\"defmodule Test do\\n def test do\\n end\\nend\\n\"}]}" + assert Jason.decode!(json) == %{ + "flag_name" => "arbitrary_value", + "git" => %{ + "branch" => "", + "head" => %{"committer_name" => "", "id" => "", "message" => ""} + }, + "parallel" => nil, + "repo_token" => "1234567890", + "service_name" => "local", + "service_number" => "build_num_1", + "source_files" => [ + %{ + "coverage" => [0, 1, nil, nil], + "name" => "test/fixtures/test.ex", + "source" => "defmodule Test do\n def test do\n end\nend\n" + } + ] + } end end diff --git a/test/poster_test.exs b/test/poster_test.exs index ad3952e2..a808fd5a 100644 --- a/test/poster_test.exs +++ b/test/poster_test.exs @@ -1,36 +1,73 @@ defmodule PosterTest do use ExUnit.Case - import Mock import ExUnit.CaptureIO - test_with_mock "post json", :hackney, [request: fn(_, _, _, _, _) -> {:ok, 200, "", ""} end] do + setup do + bypass = Bypass.open() + %{bypass: bypass, endpoint: "http://localhost:#{bypass.port}"} + end + + test "successfully posting JSON", %{bypass: bypass, endpoint: endpoint} do + Bypass.expect(bypass, fn conn -> + assert conn.method == "POST" + assert {"host", "localhost"} in conn.req_headers + Plug.Conn.resp(conn, 200, "") + end) + assert capture_io(fn -> - ExCoveralls.Poster.execute("json") - end) =~ ~r/Successfully uploaded/ + ExCoveralls.Poster.execute("{}", endpoint: endpoint) + end) =~ "Successfully uploaded" end - test_with_mock "post json fails", :hackney, [request: fn(_, _, _, _, _) -> {:error, "failed"} end] do + test "post JSON fails", %{bypass: bypass, endpoint: endpoint} do + Bypass.down(bypass) + assert_raise ExCoveralls.ReportUploadError, fn -> - ExCoveralls.Poster.execute("json") + ExCoveralls.Poster.execute("{}", endpoint: endpoint) end end - test_with_mock "post json timeout", :hackney, [request: fn(_, _, _, _, _) -> {:error, :timeout} end, - request: fn(_, _, _, _, _) -> {:error, :connect_timeout} end] do - assert capture_io(fn -> - assert ExCoveralls.Poster.execute("json") == :ok - end) =~ ~r/timeout/ - end + test "post JSON fails due internal server error", %{bypass: bypass, endpoint: endpoint} do + Bypass.expect(bypass, fn conn -> + assert conn.method == "POST" + Plug.Conn.resp(conn, 500, "") + end) - test_with_mock "post json fails due internal server error", :hackney, [request: fn(_, _, _, _, _) -> {:ok, 500, "", ""} end] do assert capture_io(fn -> - assert ExCoveralls.Poster.execute("json") == :ok + assert ExCoveralls.Poster.execute("{}", endpoint: endpoint) == :ok end) =~ ~r/internal server error/ end - test_with_mock "post json fails due to maintenance", :hackney, [request: fn(_, _, _, _, _) -> {:ok, 405, "", ""} end] do + test "post JSON fails due to maintenance", %{bypass: bypass, endpoint: endpoint} do + Bypass.expect(bypass, fn conn -> + assert conn.method == "POST" + Plug.Conn.resp(conn, 405, "") + end) + assert capture_io(fn -> - assert ExCoveralls.Poster.execute("json") == :ok + assert ExCoveralls.Poster.execute("{}", endpoint: endpoint) == :ok end) =~ ~r/maintenance/ end + + test "passes custom http options when configured", %{bypass: bypass, endpoint: endpoint} do + Application.put_env(:excoveralls, :http_options, autoredirect: false) + + on_exit(fn -> + Application.delete_env(:excoveralls, :http_options) + end) + + Bypass.expect_once(bypass, "POST", "/api/v1/jobs", fn conn -> + conn + |> Plug.Conn.put_resp_header("location", Path.join(endpoint, "redirected")) + |> Plug.Conn.resp(302, "") + end) + + assert_raise( + ExCoveralls.ReportUploadError, + "Failed to upload the report to '#{endpoint}' (reason: status_code = 302, body = ).", + fn -> + ExCoveralls.Poster.execute("{}", endpoint: endpoint) == :ok + end + ) + end end diff --git a/test/semaphore_test.exs b/test/semaphore_test.exs index 161674af..7ff49177 100644 --- a/test/semaphore_test.exs +++ b/test/semaphore_test.exs @@ -5,7 +5,11 @@ defmodule ExCoveralls.SemaphoreTest do @content "defmodule Test do\n def test do\n end\nend\n" @counts [0, 1, nil, nil] - @source_info [%{name: "test/fixtures/test.ex", source: @content, coverage: @counts}] + @source_info [%{name: "test/fixtures/test.ex", + source: @content, + coverage: @counts, + warnings: [] + }] setup do # Capture existing values diff --git a/test/settings_test.exs b/test/settings_test.exs index 07c3197c..66e5869f 100644 --- a/test/settings_test.exs +++ b/test/settings_test.exs @@ -15,7 +15,8 @@ defmodule Excoveralls.SettingsTest do test "returns default file path" do assert(Settings.Files.default_file - |> Path.relative_to(File.cwd!) == "lib/excoveralls/../conf/coveralls.json") + |> Path.expand() + |> Path.relative_to(File.cwd!) == "lib/conf/coveralls.json") end test "returns custom file path" do diff --git a/test/stats_test.exs b/test/stats_test.exs index 3a53455f..98f999df 100644 --- a/test/stats_test.exs +++ b/test/stats_test.exs @@ -125,7 +125,7 @@ defmodule ExCoveralls.StatsTest do end test_with_mock "Empty (no relevant lines) file with treat_no_relevant_lines_as_covered option is calculated as 100.0%", - ExCoveralls.Settings, [default_coverage_value: fn -> 100 end] do + ExCoveralls.Settings, [:passthrough], [default_coverage_value: fn _ -> 100 end] do results = Stats.source(@empty_source_info) assert(results.coverage == 100) @@ -133,7 +133,7 @@ defmodule ExCoveralls.StatsTest do test "coverage stats are rounded to one decimal place" do results = Stats.source(@fractional_source_info) - assert(results.coverage == 66.7) + assert(results.coverage == 66.6) end describe "update_stats/2" do diff --git a/test/travis_test.exs b/test/travis_test.exs index caf40686..269db90d 100644 --- a/test/travis_test.exs +++ b/test/travis_test.exs @@ -7,7 +7,8 @@ defmodule ExCoveralls.TravisTest do @counts [0, 1, nil, nil] @source_info [%{name: "test/fixtures/test.ex", source: @content, - coverage: @counts + coverage: @counts, + warnings: [] }] test_with_mock "execute", ExCoveralls.Poster, [execute: fn(_) -> "result" end] do diff --git a/test/xml_test.exs b/test/xml_test.exs index ad0e6019..279ebe5e 100644 --- a/test/xml_test.exs +++ b/test/xml_test.exs @@ -11,7 +11,8 @@ defmodule ExCoveralls.XmlTest do @counts [0, 1, nil, nil] @source_info [%{name: "test/fixtures/test.ex", source: @content, - coverage: @counts + coverage: @counts, + warnings: [] }] @stats_result "" <>