Skip to content

Commit

Permalink
Detect and warn about incorrectly used ignore-comments (#325)
Browse files Browse the repository at this point in the history
* Detect and warn about incorrectly used ignore-comments

Resolves #197.

* Keep existing ignoring behavior

* Improve formatting

* Test more ignore-related warnings

* Remove warning in the case of ignore-next-line at the EOF

* Test the warning output

* Add a changelog entry

* Adjust test descriptions

---------

Co-authored-by: Roman <[email protected]>
  • Loading branch information
RKushnir and RKushnir authored May 30, 2024
1 parent d09ee95 commit a530769
Show file tree
Hide file tree
Showing 26 changed files with 365 additions and 88 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
Unreleased
------
#### 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.

0.18.1
------
#### Changes
Expand Down
3 changes: 2 additions & 1 deletion lib/excoveralls/circle.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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, %{}))
Expand All @@ -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]
Expand Down
3 changes: 2 additions & 1 deletion lib/excoveralls/drone.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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, %{}))
Expand All @@ -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]
Expand Down
3 changes: 2 additions & 1 deletion lib/excoveralls/github.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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, %{}))
Expand All @@ -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()
Expand Down
3 changes: 2 additions & 1 deletion lib/excoveralls/gitlab.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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, %{}))
Expand All @@ -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]
Expand Down
186 changes: 151 additions & 35 deletions lib/excoveralls/ignore.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion lib/excoveralls/json.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule ExCoveralls.Json do
@moduledoc """
Generate JSON output for results.
"""
alias ExCoveralls.Stats

@file_name "excoveralls.json"

Expand All @@ -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

Expand Down
7 changes: 7 additions & 0 deletions lib/excoveralls/local.ex
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,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
Expand Down Expand Up @@ -92,6 +93,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)
Expand Down
3 changes: 2 additions & 1 deletion lib/excoveralls/post.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion lib/excoveralls/semaphore.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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, %{}))
Expand All @@ -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]
Expand Down
8 changes: 8 additions & 0 deletions lib/excoveralls/stats.ex
Original file line number Diff line number Diff line change
Expand Up @@ -223,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.
"""
Expand Down
5 changes: 3 additions & 2 deletions lib/excoveralls/travis.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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, %{}))
Expand All @@ -18,15 +19,15 @@ 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
def generate_json(stats, _options) 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
Expand Down
Loading

0 comments on commit a530769

Please sign in to comment.