Presubmit.Assertions (presubmit v0.2.2)

Copy Markdown View Source

ExUnit assertion verbs over a Presubmit.Commit.

Every verb takes the commit first and fails with a message that names the offending paths, modules, or lines and says what would satisfy it. Path arguments accept any Presubmit.Pattern: a Regex, an exact path string, or a list of either.

Files

Lines

Elixir source

Library-aware verbs live in Presubmit.Assertions.Phoenix, .Ecto, .OTP, .ExUnit, .Mix, and .Changelog; use Presubmit imports them all.

Message

Shape

Summary

Functions

Asserts at least one file matching pattern was added.

Asserts every body line that wrapping could shorten is at most max characters long.

Asserts that every added file matching regex has a counterpart in the after tree, named by substituting the captures into template.

Asserts that if any path matching trigger was touched, at least one path matching opts[:then] was touched too.

Asserts path exists after the commit.

Asserts that files matching pattern which existed before the commit are unchanged: not modified, deleted, or renamed away. Additions are fine.

Asserts that path sorts last (byte-wise) among every after-tree path matching opts[:among].

Asserts the commit adds at most max lines.

Asserts the commit changes at most max files.

Asserts the full raw message matches regex.

Asserts at least one file matching pattern was modified in place.

Asserts every module the commit adds in files matching pattern declares a @moduledoc (@moduledoc false counts as a deliberate choice). The default, :shipped, is the code the project ships (Presubmit.MixFile.shipped_source/1), so test support compiled only under MIX_ENV=test is exempt.

Asserts that a commit containing renames is a pure move.

Asserts that the after-tree file at path references module by name.

Asserts every API function the commit removes from the code the project shipped was marked @deprecated before the commit, so consumers had a release to migrate. Functions marked @doc false are not API, and test support compiled only under MIX_ENV=test was never shipped; both may be removed freely.

Asserts at least one file matching pattern was removed.

Asserts the subject names a scope that agrees with the paths touched.

Asserts every public function the commit adds in files matching pattern has a @spec. The default, :shipped, is the code the project ships (Presubmit.MixFile.shipped_source/1), so test support compiled only under MIX_ENV=test is exempt.

Asserts the subject line matches regex.

Asserts the subject line is non-empty and at most max characters long. Characters are graphemes, so — or é count once, not once per byte.

Asserts the commit touches at least one path matching pattern.

Asserts the message carries at least one key trailer; with regex, that at least one of its values matches.

Asserts no file matching pattern was added.

Asserts no line added by the commit matches regex.

Asserts path does not exist after the commit.

Asserts no file matching pattern was modified.

Asserts no file matching pattern was removed.

Asserts the subject line does not match regex.

Asserts the commit touches no path matching pattern.

Asserts the message carries no key trailer.

Types

pattern()

@type pattern() :: Presubmit.Pattern.t()

Functions

assert_added(commit, pattern)

@spec assert_added(Presubmit.Commit.t(), pattern()) :: :ok

Asserts at least one file matching pattern was added.

assert_body_line_length(commit, max)

@spec assert_body_line_length(Presubmit.Commit.t(), pos_integer()) :: :ok

Asserts every body line that wrapping could shorten is at most max characters long.

Characters are graphemes, as in assert_subject_length/2: a grapheme is what a terminal or editor draws in one column, so é written as e plus a combining accent counts once, as it displays. Trailing whitespace is not counted; git's default cleanup strips it.

The body is what Presubmit.Message.parse/1 puts between the subject and the trailer block, so trailers (Co-Authored-By: …, a long session URL) are never measured. Within the body a line is exempt when wrapping it could not shorten it:

  • it starts with whitespace: an indented code block, command output, or a hanging indent the author laid out by hand;
  • it starts with >: quoted text, kept as the quoted source wrote it;
  • it is shaped like a trailer, Key: value with a one-word value: a Link: or session URL left outside the trailer block is data, and wrapping it would break it;
  • its last word is longer than max on its own and the text before that word fits: a long URL or path after a lead-in such as See. Wrapping would leave the word over the limit on its own line anyway.

A line over the limit by a word or two of prose is not exempt: wrapping moves those words to the next line. Nor is a long paragraph that contains a URL: the prose around it wraps.

Each failure names the line by its number in the message, with its length and the start of its text.

assert_counterpart(commit, regex, template)

@spec assert_counterpart(Presubmit.Commit.t(), Regex.t(), String.t()) :: :ok

Asserts that every added file matching regex has a counterpart in the after tree, named by substituting the captures into template.

Examples

assert_counterpart(commit, ~r{^lib/(.+)\.ex$}, "test/\\1_test.exs")

assert_coupled(commit, trigger, list)

@spec assert_coupled(Presubmit.Commit.t(), pattern(), [{:then, pattern()}]) :: :ok

Asserts that if any path matching trigger was touched, at least one path matching opts[:then] was touched too.

This is the "if you changed X you must also change Y" rule on paths alone. Prefer an after-tree invariant when one exists — a coupling on the diff passes when Y was touched for an unrelated reason.

Examples

assert_coupled(commit, "mix.exs", then: "mix.lock")

assert_exists(commit, path)

@spec assert_exists(Presubmit.Commit.t(), String.t()) :: :ok

Asserts path exists after the commit.

assert_immutable(commit, pattern)

@spec assert_immutable(Presubmit.Commit.t(), pattern()) :: :ok

Asserts that files matching pattern which existed before the commit are unchanged: not modified, deleted, or renamed away. Additions are fine.

assert_last_by_name(commit, path, list)

@spec assert_last_by_name(Presubmit.Commit.t(), String.t(), [{:among, pattern()}]) ::
  :ok

Asserts that path sorts last (byte-wise) among every after-tree path matching opts[:among].

For Ecto migrations prefer Presubmit.Assertions.Ecto.assert_migrations_ordered/1, which finds migrations by their use rather than by path.

assert_max_additions(commit, max)

@spec assert_max_additions(Presubmit.Commit.t(), non_neg_integer()) :: :ok

Asserts the commit adds at most max lines.

assert_max_files(commit, max)

@spec assert_max_files(Presubmit.Commit.t(), non_neg_integer()) :: :ok

Asserts the commit changes at most max files.

assert_message(commit, regex)

@spec assert_message(Presubmit.Commit.t(), Regex.t()) :: :ok

Asserts the full raw message matches regex.

assert_modified(commit, pattern)

@spec assert_modified(Presubmit.Commit.t(), pattern()) :: :ok

Asserts at least one file matching pattern was modified in place.

assert_moduledoc(commit, pattern \\ :shipped)

@spec assert_moduledoc(Presubmit.Commit.t(), Presubmit.Query.source_pattern()) :: :ok

Asserts every module the commit adds in files matching pattern declares a @moduledoc (@moduledoc false counts as a deliberate choice). The default, :shipped, is the code the project ships (Presubmit.MixFile.shipped_source/1), so test support compiled only under MIX_ENV=test is exempt.

assert_pure_move(commit)

@spec assert_pure_move(Presubmit.Commit.t()) :: :ok

Asserts that a commit containing renames is a pure move.

Moving a module means renaming its file, its defmodule line, and every reference to it — in callers, tests, and docs; moving any file means updating references to its path. All of that is allowed; anything else is not. The check compares the before and after sides under the rename map of both module names and file paths: Elixir files must define the same functions with the same bodies, and other files must be textually identical once old names and paths are rewritten to new ones.

Commits with no renames pass trivially, so this can be applied to every commit to enforce "moves are their own commit".

assert_references(commit, path, module)

@spec assert_references(Presubmit.Commit.t(), String.t(), module()) :: :ok

Asserts that the after-tree file at path references module by name.

References are resolved through the file's aliases, so alias Demo.Accounts followed by Accounts.User counts as a reference to Demo.Accounts.User. For Phoenix routers use Presubmit.Assertions.Phoenix.assert_routed/1, which understands scope aliasing.

assert_removals_deprecated(commit)

@spec assert_removals_deprecated(Presubmit.Commit.t()) :: :ok

Asserts every API function the commit removes from the code the project shipped was marked @deprecated before the commit, so consumers had a release to migrate. Functions marked @doc false are not API, and test support compiled only under MIX_ENV=test was never shipped; both may be removed freely.

assert_removed(commit, pattern)

@spec assert_removed(Presubmit.Commit.t(), pattern()) :: :ok

Asserts at least one file matching pattern was removed.

assert_scope_matches_paths(commit, regex, path_pattern)

@spec assert_scope_matches_paths(Presubmit.Commit.t(), Regex.t(), (String.t() ->
                                                               pattern())) ::
  :ok

Asserts the subject names a scope that agrees with the paths touched.

regex must capture the scope from the subject; path_pattern is a function from the captured scope to a pattern every touched path must match.

Examples

assert_scope_matches_paths(commit, ~r/^\[(\w+)\]/, fn scope -> ~r{^#{scope}/} end)

assert_specs(commit, pattern \\ :shipped)

@spec assert_specs(Presubmit.Commit.t(), Presubmit.Query.source_pattern()) :: :ok

Asserts every public function the commit adds in files matching pattern has a @spec. The default, :shipped, is the code the project ships (Presubmit.MixFile.shipped_source/1), so test support compiled only under MIX_ENV=test is exempt.

Macros and @impl callbacks are exempt: callbacks take their contract from the behaviour, and macros are not conventionally spec'd. A spec for a head's full arity covers every arity its default arguments generate.

assert_subject(commit, regex)

@spec assert_subject(Presubmit.Commit.t(), Regex.t()) :: :ok

Asserts the subject line matches regex.

assert_subject_length(commit, max)

@spec assert_subject_length(Presubmit.Commit.t(), pos_integer()) :: :ok

Asserts the subject line is non-empty and at most max characters long. Characters are graphemes, so — or é count once, not once per byte.

assert_touched(commit, pattern)

@spec assert_touched(Presubmit.Commit.t(), pattern()) :: :ok

Asserts the commit touches at least one path matching pattern.

assert_trailer(commit, key, regex \\ nil)

@spec assert_trailer(Presubmit.Commit.t(), String.t(), Regex.t() | nil) :: :ok

Asserts the message carries at least one key trailer; with regex, that at least one of its values matches.

refute_added(commit, pattern)

@spec refute_added(Presubmit.Commit.t(), pattern()) :: :ok

Asserts no file matching pattern was added.

refute_added_lines(commit, regex, opts \\ [])

@spec refute_added_lines(Presubmit.Commit.t(), Regex.t(), keyword()) :: :ok

Asserts no line added by the commit matches regex.

Restrict the files considered with in: pattern. Failures list each offending path:line with the line's text.

Examples

refute_added_lines(commit, ~r/IO\.inspect|dbg\(/, in: ~r{^lib/})

refute_exists(commit, path)

@spec refute_exists(Presubmit.Commit.t(), String.t()) :: :ok

Asserts path does not exist after the commit.

refute_modified(commit, pattern)

@spec refute_modified(Presubmit.Commit.t(), pattern()) :: :ok

Asserts no file matching pattern was modified.

refute_removed(commit, pattern)

@spec refute_removed(Presubmit.Commit.t(), pattern()) :: :ok

Asserts no file matching pattern was removed.

refute_subject(commit, regex)

@spec refute_subject(Presubmit.Commit.t(), Regex.t()) :: :ok

Asserts the subject line does not match regex.

refute_touched(commit, pattern)

@spec refute_touched(Presubmit.Commit.t(), pattern()) :: :ok

Asserts the commit touches no path matching pattern.

refute_trailer(commit, key)

@spec refute_trailer(Presubmit.Commit.t(), String.t()) :: :ok

Asserts the message carries no key trailer.