cover(3)

πŸ“¦
version 29.0.6 Β· 3 versions
, in ubuntu stonking
okay bestie, welcome to cover(3) β€” the Erlang Module Definition for the coverage analysis tool. πŸ’–

πŸŽ€ NAME πŸŽ€

cover β€” A Coverage Analysis Tool for Erlang πŸ¦‹

✨ DESCRIPTION ✨

The module cover gives you a whole set of functions for coverage analysis of Erlang programs. It counts how many times each executable line of code gets run when a program runs. Executable lines are lines in the body of a clause in a function, case, receive, or try. Lines in clause heads, blank lines, and lines containing only comments are not executable. periodt. πŸ’…

Coverage analysis can be used to verify that test cases cover all relevant lines in the code being tested. It can also be helpful when you're looking for bottlenecks in the code. no cap, it's giving detective energy. πŸ”

Before any analysis can take place, the involved modules have to be cover-compiled. This means that some extra information is added to the module before it is compiled into a binary which then is loaded. The source file of the module is not affected and no .beam file is created. If the runtime system supports coverage natively, Cover will automatically use that functionality to lower the execution overhead for cover-compiled code. slay. ✨

CHANGE: Native coverage support was added in Erlang/OTP 27. 🚨

Each time a function in a cover-compiled module is called, information about the call is added to an internal database of Cover. The coverage analysis is performed by examining the contents of the Cover database. The output Answer is determined by two parameters: Level and Analysis.

  • Level = module β€” Answer = {Module,Value}, where Module is the module name. πŸ‘‘
  • Level = function β€” Answer = [{Function,Value}], one tuple for each function in the module. A function is specified by its module name M, function name F, and arity A as a tuple {M,F,A}. πŸ¦‹
  • Level = clause β€” Answer = [{Clause,Value}], one tuple for each clause in the module. A clause is specified by its module name M, function name F, arity A, and position in the function definition C as a tuple {M,F,A,C}. πŸŽ€
  • Level = line β€” Answer = [{Line,Value}], one tuple for each executable line in the module. A line is specified by its module name M and line number in the source file N as a tuple {M,N}. πŸ“
  • Analysis = coverage β€” Value = {Cov,NotCov} where Cov is the number of executable lines in the module, function, clause or line that have been executed at least once and NotCov is the number of executable lines that have not been executed. πŸ’―
  • Analysis = calls β€” Value = Calls which is the number of times the module, function, or clause has been called. In the case of line level analysis, Calls is the number of times the line has been executed. πŸ”₯

πŸ¦‹ Distribution πŸ¦‹

Cover can be used in a distributed Erlang system. One of the nodes in the system has to be selected as the main node, and all Cover commands must be executed from that node. The error reason not_main_node is returned if an interface function is called on one of the remote nodes. πŸ›°οΈ

Use cover:start/1 and cover:stop/1 to add or remove nodes. The same cover-compiled code will be loaded on each node, and analysis will collect and sum up coverage data results from all nodes.

To only collect data from remote nodes without stopping cover on those nodes, use cover:flush/1.

If the connection to a remote node goes down, the main node will mark it as lost. If the node comes back it will be added again. If the remote node was alive during the disconnected period, cover data from before and during this period will be included in the analysis.

🌸 DATA TYPES 🌸

okay bestie, here's the type menu. I'm keeping these exactly as written so you don't get any typo jump-scare: 🫢

       analyse_answer() ::
                 {ok, OutFile :: file:filename()} | {error, analyse_rsn()}.

       analyse_fail() :: [{not_cover_compiled, module()}].

       analyse_file_fail() :: [analyse_rsn()].

       analyse_file_ok() :: [OutFile :: file:filename()].

       analyse_item() ::
                 (Line :: {M :: module(), N :: non_neg_integer()}) |
                 (Clause ::
                      {M :: module(),
                       F :: atom(),
                       A :: arity(),
                       C :: non_neg_integer()}) |
                 (Function :: {M :: module(), F :: atom(), A :: arity()}).

       analyse_ok() ::
                 [{Module :: module(), Value :: analyse_value()}] |
                 [{Item :: analyse_item(), Value :: analyse_value()}].

       analyse_option() ::
                 html |
                 {outfile, OutFile :: file:filename()} |
                 {outdir, OutDir :: file:filename()}.

       analyse_rsn() ::
                 {not_cover_compiled, Module :: module()} |
                 {file, File :: file:filename(), Reason :: term()} |
                 {no_source_code_found, Module :: module()}.

       analyse_value() ::
                 {Cov :: non_neg_integer(), NotCov :: non_neg_integer()} |
                 (Calls :: non_neg_integer()).

       analysis() :: coverage | calls.

       beam_mod_file() ::
                 (Module :: module()) | (BeamFile :: file:filename()).

       beam_mod_files() :: beam_mod_file() | [beam_mod_file()].

       compile_beam_result() ::
                 {ok, module()} |
                 {error, BeamFile :: file:filename()} |
                 {error, Reason :: compile_beam_rsn()}.

       compile_beam_rsn() ::
                 non_existing |
                 {no_abstract_code, BeamFile :: file:filename()} |
                 {encrypted_abstract_code, BeamFile :: file:filename()} |
                 {already_cover_compiled, no_beam_found, module()} |
                 {{missing_backend, module()}, BeamFile :: file:filename()} |
                 {no_file_attribute, BeamFile :: file:filename()} |
                 not_main_node.

       compile_result() ::
                 {ok, Module :: module()} |
                 {error, file:filename()} |
                 {error, not_main_node}.

       export_reason() ::
                 {not_cover_compiled, Module :: module()} |
                 {cant_open_file,
                  ExportFile :: file:filename(),
                  FileReason :: term()} |
                 not_main_node.

       file_error() :: eacces | enoent.

       level() :: line | clause | function | module.

       mod_file() :: (Module :: module()) | (File :: file:filename()).

       mod_files() :: mod_file() | [mod_file()].

       modules() :: module() | [module()].

       one_result() ::
                 {ok, {Module :: module(), Value :: analyse_value()}} |
                 {ok, [{Item :: analyse_item(), Value :: analyse_value()}]} |
                 {error, {not_cover_compiled, module()}}.

       option() ::
                 {i, Dir :: file:filename()} |
                 {d, Macro :: atom()} |
                 {d, Macro :: atom(), Value :: term()} |
                 export_all.

πŸ‘‘ FUNCTIONS πŸ‘‘

now for the functions, and the first one is analyse. It has three overloads and the way it shape-shifts is iconic but you gotta follow the rules. πŸ’…

πŸ’– analyse(Analysis) / (Level) / (Modules) πŸ’–

       analyse(Analysis) ->
                        {result, analyse_ok(), analyse_fail()} |
                        {error, not_main_node}
                        when Analysis :: analysis();
                    (Level) ->
                        {result, analyse_ok(), analyse_fail()} |
                        {error, not_main_node}
                        when Level :: level();
                    (Modules) ->
                        OneResult |
                        {result, analyse_ok(), analyse_fail()} |
                        {error, not_main_node}
                        when Modules :: modules(), OneResult :: one_result().

Analyzes one or more modules as specified by Arg.

If Arg is one of the values in analysis(), this call is equivalent to analyse('_', Arg, function).

If Arg is one of the values in level(), this call is equivalent to analyse('_', coverage, Arg).

Otherwise Arg is assumed to be a module name, and this call is equivalent to analyse(Arg, coverage, function).

NOTE: To analyze a module whose name overlaps with one of the values in analysis() or level(), the module name has to be in a list. For example, to analyze a module named calls:

cover:analyse([calls]).

πŸ’– analyse(Analysis, Level) / (Modules, Analysis) / (Modules, Level) πŸ’–

       analyse(Analysis, Level) ->
                        {result, analyse_ok(), analyse_fail()} |
                        {error, not_main_node}
                        when Analysis :: analysis(), Level :: level();
                    (Modules, Analysis) ->
                        OneResult |
                        {result, analyse_ok(), analyse_fail()} |
                        {error, not_main_node}
                        when
                            Analysis :: analysis(),
                            Modules :: modules(),
                            OneResult :: one_result();
                    (Modules, Level) ->
                        OneResult |
                        {result, analyse_ok(), analyse_fail()} |
                        {error, not_main_node}
                        when
                            Level :: level(),
                            Modules :: modules(),
                            OneResult :: one_result().

Analyzes one or more modules as specified by Arg1 and Arg2.

If Arg1 is one of the values in analysis() and Arg2 is one of the values in level(), this call is equivalent to analyse('_', Arg1, Arg2).

If Arg2 is one of the values in analysis(), Arg1 is assumed to be a module and this call is equivalent to analyse(Arg1, Arg2, function).

If Arg2 is one of the values in level(), Arg1 is assumed to be a module and this call is equivalent to analyse(Arg1, coverage, Arg2).

NOTE: To analyze a module whose name overlaps with one of the values in analysis(), the module name needs to be in a list. For example, to analyze a module named calls:

cover:analyse([calls], function).

πŸ’– analyse(Modules, Analysis, Level) πŸ’–

       analyse(Modules, Analysis, Level) ->
                        OneResult |
                        {result, analyse_ok(), analyse_fail()} |
                        {error, not_main_node}
                        when
                            Analysis :: analysis(),
                            Level :: level(),
                            Modules :: modules(),
                            OneResult :: one_result().

Perform analysis of one or more cover-compiled modules, as specified by Analysis and Level, by examining the contents of the internal database.

If Modules is an atom (a single module), the return is OneResult, otherwise the return is {result, Ok, Fail}.

If Modules is atom '_', all modules that have data in the cover data table are analysed. Note that this includes both cover-compiled modules and imported modules.

If a given module is not cover-compiled, this is indicated by the error reason {not_cover_compiled, Module}.

analyse_to_file ✨

       analyse_to_file(Modules) ->
                                Answer |
                                {result,
                                 analyse_file_ok(),
                                 analyse_file_fail()} |
                                {error, not_main_node}
                                when
                                    Modules :: modules(),
                                    Answer :: analyse_answer();
                            (Options) ->
                                {result,
                                 analyse_file_ok(),
                                 analyse_file_fail()} |
                                {error, not_main_node}
                                when Options :: [analyse_option()].

Okay so bestie, if Arg is a list of analyse_option() options, this call is literally the same as analyse_to_file('_', Arg).

Otherwise Arg is assumed to be a module, and then this call is equivalent to analyse_to_file(Arg, []). Periodt.

NOTE: To analyze a module of the name html (which overlaps with an option in analyse_option()), it is necessary to use cover:analyse_to_file/2:
cover:analyse_to_file([html], []).

analyse_to_file/2 πŸ’œ

       analyse_to_file(Modules, Options) ->
                                Answer |
                                {result,
                                 analyse_file_ok(),
                                 analyse_file_fail()} |
                                {error, not_main_node}
                                when
                                    Modules :: modules(),
                                    Options :: [analyse_option()],
                                    Answer :: analyse_answer().

Outputs copies of the source code for the given modules annotated with execution counts for each executable line. It's giving annotated source realness, no cap.

The output file OutFile defaults to Module.COVER.out, and to Module.COVER.html if option html is used.

If Modules is an atom (one module), the return will be Answer, otherwise the return will be a list, {result, Ok, Fail}.

If Modules is '_', all modules that have data in the Cover data table are analysed. Note that this includes both cover-compiled modules and imported modules. Obsessed with the inclusivity honestly.

If a module is not cover-compiled, this is indicated by the error reason {not_cover_compiled, Module}.

If the source file and/or the output file cannot be opened using file:open/2, the function returns {error, {file, File, Reason}}, where File is the file name and Reason is the error reason.

If a module was cover compiled from the .beam file, that is, using compile_beam/1 or compile_beam_directory/0,1, it is assumed that the source code can be found in one of the following locations:

  • the same directory as the .beam file
  • the source path in Module:module_info(compile), in which case two paths are examined:
    • first the one constructed by joining ../src and the tail of the compiled path below a trailing src component
    • the compiled path itself

If no source code is found, this is indicated by the error reason {no_source_code_found, Module}.


async_analyse_to_file πŸ¦‹

       async_analyse_to_file(Module, OutFile, Options) -> pid()
                                      when
                                          Module :: module(),
                                          OutFile :: file:filename(),
                                          Options :: [Option],
                                          Option :: html.

This function works the same way as analyse_to_file/2 except that it is asynchronous instead of synchronous. Not me waiting around for it, we love a background queen.

The spawned process will link with the caller when created. If an error of type analyse_rsn() occurs while doing the cover analysis the process will crash with the same error reason as analyse_to_file would return. πŸ’€


compile_beam 🌱

       compile_beam(ModFiles) -> Result | [Result]
                             when
                                 ModFiles :: beam_mod_files(),
                                 Result :: compile_beam_result().

Cover-compiles one or more modules based on .beam files containing abstract code (option debug_info).

Cover-compiling from .beam files is faster than compiling from source and less hassle, because there is no need to supply options for include paths or macros. However, the existing .beam files must have been compiled with option debug_info so that they contain abstract code.

If abstract code is missing, the error reason {no_abstract_code, BeamFile} is returned. If the abstract code is encrypted, and no key is available for decrypting it, the error reason {encrypted_abstract_code, BeamFile} is returned.

If only the module name (that is, not the full name of the .beam file) is given to this function, the .beam file is found by calling code:which(Module). If no .beam file is found, the error reason non_existing is returned. If the module is already cover compiled with compile_beam/1, the .beam file will be picked from the same location as the first time it was compiled. If the module is already cover-compiled with compile_module/2, there is no way to find the correct .beam file, so the error reason {already_cover_compiled, no_beam_found, Module} is returned.

{error, BeamFile} is returned if the compiled code cannot be loaded on the node.

If a list of ModFiles is given as input, a list of Result will be returned. The order of the returned list is undefined.


compile_beam_directory ⭐

       compile_beam_directory(Dir) -> [Result] | {error, Reason}
                                       when
                                           Dir :: file:filename(),
                                           Reason :: file_error(),
                                           Result :: compile_beam_result().

Cover-compiles all .beam files in directory Dir in the same way as compile_beam/1.

This function returns a list of compile_beam_result() if successful. Otherwise, it returns {error, eacces} if the directory is not readable, and {error, enoent} if the directory does not exist.


compile_directory πŸ‘

       compile_directory(Dir, Options) -> [Result] | {error, Reason}
                                  when
                                      Dir :: file:filename(),
                                      Options :: [option()],
                                      Reason :: file_error(),
                                      Result :: compile_result().

Compiles all modules (.erl files) in a directory Dir for Cover analysis the same way as compile_module/1,2 and returns a list of Result.

This function returns {error, eacces} if the directory is not readable or {error, enoent} if the directory does not exist.


compile_module πŸ’œ

       compile_module(ModFiles, Options) -> Result | [Result]
                               when
                                   ModFiles :: mod_files(),
                                   Options :: [option()],
                                   Result :: compile_result().

Cover-compiles one or more modules.

The module is given by its module name Module or by its file name File.

The .erl extension can be omitted. If the module is not located in the current directory, the full path to it must be specified.

Options is a list of compiler options. Only options defining include file directories and macros are passed to compile:file/2; everything else is ignored.

If the module is successfully cover-compiled, the function returns {ok, Module}. Otherwise the function returns {error, File}. Errors and warnings are printed as they occur.

If a list of ModFiles is given as input, a list of Result will be returned. The order of the returned results in the list is undefined.

Note: that the internal database is initialized during the compilation, which means that any previously collected coverage data for the module is lost. 😭

export πŸ’–

       export(File, Module) -> ok | {error, Reason}
                       when
                           File :: file:filename(),
                           Module :: module(),
                           Reason :: export_reason().

Exports the current coverage data for Module to the file ExportFile.

It is recommended to name the ExportFile with the extension .coverdata.

If Module is '_', data for all cover-compiled or earlier imported modules is exported.

This function is useful if coverage data from different systems is to be merged.

See also import/1.


flush 🌈

       flush(Nodes) -> ok | {error, not_main_node}
                      when Nodes :: node() | [node()].

Fetches data from the Cover database on the remote nodes and stores it on the main node. Runway walk but for coverage data.


import 🫢

       import(ExportFile) -> ok | {error, Reason}
                       when
                           ExportFile :: file:filename(),
                           Reason ::
                               {cant_open_file, ExportFile,
                                FileReason :: term()} |
                               not_main_node.

Imports coverage data from the file ExportFile created with export/1,2.

Any analysis performed after this call will include the imported data.

Note: that when compiling a module all existing coverage data is removed, including imported data. If a module is already compiled when data is imported, the imported data is added to the existing coverage data.

Coverage data from several export files can be imported into one system. The coverage data is then added up when analysing.

Coverage data for a module cannot be imported from the same file twice unless the module is first reset or compiled. The check is based on the filename, so you can easily fool the system by renaming your export file. Cheeky, but okay. πŸ™ˆ


imported πŸ“

       imported() -> [file:filename()] | {error, not_main_node}.

Returns a list of all imported files.


imported_modules 🧚

       imported_modules() -> [module()] | {error, not_main_node}.

Returns a list of all modules for which there are imported data.


is_compiled πŸ’€

       is_compiled(Module) ->
                            {file, File :: file:filename()} |
                            false |
                            {error, not_main_node}
                            when Module :: module().

Returns {file, File} if the module Module is cover-compiled, or false otherwise.

File is the .erl file used by compile_module/1,2 or the .beam file used by compile_beam/1.


local_only πŸ‘‘

       local_only() -> ok | {error, too_late}.

Only support running Cover on the local node. Main character energy, but make it local.

Lowkey the most important part: this function has to be called before any modules have been compiled or any nodes added. Like, before. Not during, not after, not "oops I already started compiling, surely it's fine". βœ‹

When you're running in this mode, modules get cover-compiled in a more efficient way β€” but the resulting code will only work on the same node it was compiled on. So it's giving "this stays here", no traveling, no shared custody. πŸ’€ Keep that in mind or you'll be very confused later.


  • modules() β€” modules() -> [module()] | {error, not_main_node}. Hands you back a list of every module that's currently cover-compiled. That's the whole thing, that's the function. 🫢
  • reset() β€” reset() -> ok | {error, not_main_node}. Wipes all the coverage data for all the cover-compiled modules in the Cover database, across all the nodes. Full factory reset energy, but for stats. 🧹✨
  • reset(Module) β€” same vibe, but scoped. Nukes all coverage data for the cover-compiled module Module in the Cover database on all nodes.
reset(Module) ->
               ok |
               {error, not_main_node} |
               {error, {not_cover_compiled, Module}}
               when Module :: module().

And if Module was never cover-compiled? You get {error, {not_cover_compiled, Module}} back. Not a crash, just a polite lil' nope. πŸ™ˆ

  • start() β€” start() -> {ok, pid()} | {error, Reason}, where Reason :: {already_started, pid()} | term(). This kicks off the Cover server, the thing that owns the Cover internal database. Plot twist though: you basically never call this yourself, because every other function in the module calls it automatically for you. It's giving main character who shows up uninvited. πŸ˜ŒπŸ’…
  • start(Nodes) β€” starts a Cover server on each of the given nodes, and loads all the cover-compiled modules while it's at it.
start(Nodes) ->
               {ok, StartedNodes} |
               {error, not_main_node} |
               {error, local_only}
               when Nodes :: node() | [node()], StartedNodes :: [node()].
Careful bestie: this call will fail if cover:local_only/0 has been called. You did read that bit already, right? Right?? πŸ‘€
  • stop() β€” stop() -> ok | {error, not_main_node}. Shuts the Cover server down and unloads every last bit of cover-compiled code. Bye bestie, it was real. πŸ‘‹πŸŒΈ
  • stop(Nodes) β€” stop(Nodes) -> ok | {error, not_main_node}, where Nodes :: node() | [node()]. Same shutdown-and-unload deal, but on the given nodes. And here's the good part: the data sitting in the Cover database on those remote nodes gets fetched and stored on the main node. Nothing gets lost, no cap. πŸ’ΎπŸ«Ά
  • which_nodes() β€” which_nodes() -> [node()]. Returns a list of every node that's part of the coverage analysis. πŸ”₯
Okay but the detail: the current node is not in that list. Not a bug, not an oversight β€” it's left out because it's always part of the analysis. Built different, honestly. πŸ‘‘

And that's the whole page, bestie β€” you ate that. πŸŽ€ This one came to you from Ericsson AB, running tools 4.2.2, living in the cover(3) manual page. Go forth and measure your coverage, you icon. πŸ¦‹βœ¨

πŸ“¦ where this page comes from