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
assert_added/2,refute_added/2,assert_modified/2,refute_modified/2,assert_removed/2,refute_removed/2,assert_touched/2,refute_touched/2assert_immutable/2— files matching the pattern are never edited, deleted, or renamed awayassert_coupled/3— if anything matching the trigger changed, something matchingthen:did tooassert_counterpart/3— each added file has a sibling derived from its pathassert_exists/2,refute_exists/2— after-tree existenceassert_last_by_name/3— an added file sorts last among its peers
Lines
refute_added_lines/3— no added line matches a regex (debug calls, secrets, merge markers)
Elixir source
assert_references/3— a file's AST names a module, aliases resolvedassert_specs/2— every newly added public function has a@specassert_moduledoc/2— every newly added module has a@moduledocassert_removals_deprecated/1— removed public functions were@deprecatedfirstassert_pure_move/1— a commit containing renames contains nothing else
Library-aware verbs live in Presubmit.Assertions.Phoenix, .Ecto,
.OTP, .ExUnit, .Mix, and .Changelog; use Presubmit imports
them all.
Message
assert_subject/2,refute_subject/2,assert_message/2assert_subject_length/2— at mostmaxcharactersassert_body_line_length/2— every wrappable body line at mostmaxcharactersassert_trailer/2,assert_trailer/3,refute_trailer/2assert_scope_matches_paths/3— the subject's scope agrees with the paths touched
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
@type pattern() :: Presubmit.Pattern.t()
Functions
@spec assert_added(Presubmit.Commit.t(), pattern()) :: :ok
Asserts at least one file matching pattern was added.
@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: valuewith a one-word value: aLink:or session URL left outside the trailer block is data, and wrapping it would break it; - its last word is longer than
maxon its own and the text before that word fits: a long URL or path after a lead-in such asSee. 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.
@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")
@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")
@spec assert_exists(Presubmit.Commit.t(), String.t()) :: :ok
Asserts path exists after the commit.
@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.
@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.
@spec assert_max_additions(Presubmit.Commit.t(), non_neg_integer()) :: :ok
Asserts the commit adds at most max lines.
@spec assert_max_files(Presubmit.Commit.t(), non_neg_integer()) :: :ok
Asserts the commit changes at most max files.
@spec assert_message(Presubmit.Commit.t(), Regex.t()) :: :ok
Asserts the full raw message matches regex.
@spec assert_modified(Presubmit.Commit.t(), pattern()) :: :ok
Asserts at least one file matching pattern was modified in place.
@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.
@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".
@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.
@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.
@spec assert_removed(Presubmit.Commit.t(), pattern()) :: :ok
Asserts at least one file matching pattern was removed.
@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)
@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.
@spec assert_subject(Presubmit.Commit.t(), Regex.t()) :: :ok
Asserts the subject line matches regex.
@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.
@spec assert_touched(Presubmit.Commit.t(), pattern()) :: :ok
Asserts the commit touches at least one path matching pattern.
@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.
@spec refute_added(Presubmit.Commit.t(), pattern()) :: :ok
Asserts no file matching pattern was added.
@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/})
@spec refute_exists(Presubmit.Commit.t(), String.t()) :: :ok
Asserts path does not exist after the commit.
@spec refute_modified(Presubmit.Commit.t(), pattern()) :: :ok
Asserts no file matching pattern was modified.
@spec refute_removed(Presubmit.Commit.t(), pattern()) :: :ok
Asserts no file matching pattern was removed.
@spec refute_subject(Presubmit.Commit.t(), Regex.t()) :: :ok
Asserts the subject line does not match regex.
@spec refute_touched(Presubmit.Commit.t(), pattern()) :: :ok
Asserts the commit touches no path matching pattern.
@spec refute_trailer(Presubmit.Commit.t(), String.t()) :: :ok
Asserts the message carries no key trailer.