KogenCredo.TestModuleInfo (KogenCredo v0.1.0)

View Source

Reads ExUnit test modules out of an AST, for the async-safety checks.

For every defmodule that directly uses ExUnit.Case (or a configured case template) it reports:

  • :mode: :async or :serial, from the literal async: option; :unknown when the option is not a literal boolean.
  • :group: the literal group: option, :dynamic when it is not a literal atom, nil when absent.
  • :serial_tags: atoms from @moduletag serial: atom; non_literal_reason/0 stands for serial: true, serial: nil or any value that is not a literal atom.
  • :accesses: global-state touches found anywhere in the module body (nested defmodules are scanned separately).
  • :teardowns: calls that destroy or stop named, app-wide state.

Case templates

Modules that use anything other than ExUnit.Case are invisible unless the template is declared. Declare templates as a map or keyword list from module name to behaviour:

%{"MyApp.DataCase" => :inherit, "MyApp.AsyncCase" => :async, "MyApp.SerialCase" => :serial}
  • :inherit: the template forwards its options to use ExUnit.Case, so the async: and group: given at the use site apply (default serial).
  • :async / :serial: the template forces that mode regardless of options.

An undeclared template yields no module at all (the checks stay quiet) rather than a guess. The declaration is not verified against the template source.

Limits

Everything is syntactic. Only literal resource names are tracked (atoms, strings and module attributes holding them). Calls in helper modules, macros or quote blocks are not followed. Scope is lexical: a nested defmodule is a module of its own (a nested test module has its own mode and group; a nested helper module's calls are not charged to the enclosing test module). Pipes are expanded before classification, so "p" |> File.write!(data) is a write to "p".

Summary

Functions

Module attributes with a literal binary or atom value.

Like literal/2, but module aliases also count (expanded name as a string).

Resolves a node to a literal binary/atom, through attributes and Path.join.

Marker found in serial_tags when the serial: value is not a literal reason atom.

Normalises a template declaration (map or keyword/tuple list) to a string-keyed map.

Parses test modules from ast. Options: :case_templates (see moduledoc).

Parses source text; invalid source yields [].

Types

access()

@type access() :: %{
  kind: atom(),
  key: String.t() | atom(),
  access: :read | :write,
  line: pos_integer(),
  call: String.t()
}

t()

@type t() :: %KogenCredo.TestModuleInfo{
  accesses: term(),
  created_tables: term(),
  group: term(),
  line: term(),
  mode: term(),
  name: term(),
  serial_tags: term(),
  teardowns: term(),
  template: term()
}

Functions

attrs(stmts)

@spec attrs([Macro.t()]) :: %{required(atom()) => term()}

Module attributes with a literal binary or atom value.

global_name(node, attrs, aliases)

@spec global_name(Macro.t(), map(), KogenCredo.SafetySupport.aliases()) ::
  {:ok, String.t()} | :error

Like literal/2, but module aliases also count (expanded name as a string).

literal(value, attrs)

@spec literal(Macro.t(), map()) :: {:ok, String.t() | atom()} | :error

Resolves a node to a literal binary/atom, through attributes and Path.join.

non_literal_reason()

@spec non_literal_reason() :: atom()

Marker found in serial_tags when the serial: value is not a literal reason atom.

normalize_templates(config)

@spec normalize_templates(map() | list()) :: %{
  required(String.t()) => :async | :serial | :inherit
}

Normalises a template declaration (map or keyword/tuple list) to a string-keyed map.

parse(ast, opts \\ [])

@spec parse(
  Macro.t(),
  keyword()
) :: [t()]

Parses test modules from ast. Options: :case_templates (see moduledoc).

parse_source(source, opts \\ [])

@spec parse_source(
  String.t(),
  keyword()
) :: [t()]

Parses source text; invalid source yields [].