cover(3erl)

πŸ“¦
version 25.3.2.8 Β· 3 versions
, in debian bookworm, ubuntu noble

πŸ’– NAME πŸ’–

cover β€” A Coverage Analysis Tool for Erlang (3erl, bestie, yes this is the Erlang module definition page πŸ’…)


🌸 DESCRIPTION 🌸

Okay so the cover module gives you a whole set of functions for coverage analysis of Erlang programs, counting how many times each executable line of code is executed when a program is run. It’s giving β€œwatch the code actually live its life” energy ✨

An executable line contains an Erlang expression such as a matching or a function call. A blank line or a line containing a comment, function head or pattern in a case- or receive statement is not executable. Very important distinction, bestie πŸŽ€

Coverage analysis can be used to verify test cases, making sure all relevant code is covered, and may also be helpful when looking for bottlenecks in the code. So it’s both β€œdid my tests touch this?” and β€œwhy is this slow?” icon behavior πŸ‘‘

Before any analysis can take place, the involved modules must 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. No cap: your original source stays untouched 🫢

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 must then be selected as the main node, and all Cover commands must be executed from this node. The error reason not_main_node is returned if an interface function is called on one of the remote nodes. Rude, but fair 😭

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. Teamwork makes the coverage dream work 🌈

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. It’s giving β€œwe never forgot you” energy πŸ₯Ή


✨ EXPORTS ✨

πŸ› οΈ start()

start() -> {ok, pid()} | {error, Reason}

Types:

Reason = {already_started, pid()} | term()

Starts the Cover server which owns the Cover internal database. This function is called automatically by the other functions in the module. So like, it’s already doing the most behind the scenes πŸ’…

πŸ› οΈ local_only()

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

Only support running Cover on the local node. This function must be called before any modules have been compiled or any nodes added. When running in this mode, modules will be Cover compiled in a more efficient way, but the resulting code will only work on the same node they were compiled on. Local icon behavior, but read the warning bestie πŸŽ€

πŸ› οΈ start(Nodes)

start(Nodes) ->
         {ok, StartedNodes} |
         {error, not_main_node} |
         {error, local_only}

Types:

Nodes = node() | [node()]
StartedNodes = [node()]

Starts a Cover server on the each of given nodes, and loads all cover compiled modules. This call will fail if cover:local_only/0 has been called. So if you went local-only, this is not the moment 🫠

πŸ› οΈ compile(...) / compile_module(...)

compile(ModFiles) -> Result | [Result]

compile(ModFiles, Options) -> Result | [Result]

compile_module(ModFiles) -> Result | [Result]

compile_module(ModFiles, Options) -> Result | [Result]

Types:

ModFiles = mod_files()
Options = [option()]
Result = compile_result()
mod_files() = mod_file() | [mod_file()]
mod_file() = (Module :: module()) | (File :: file:filename())
option() =
    {i, Dir :: file:filename()} |
    {d, Macro :: atom()} |
    {d, Macro :: atom(), Value :: term()} |
    export_all
  See compile:file/2.
compile_result() =
    {ok, Module :: module()} |
    {error, file:filename()} |
    {error, not_main_node}

Compiles a module for Cover analysis. The module is given by its module name Module or by its file name File. The .erl extension may be omitted. If the module is located in another directory, the path has to be specified. Bestie, path matters 😌

Options is a list of compiler options which defaults to []. Only options defining include file directories and macros are passed to compile:file/2, everything else is ignored. So not every compiler option gets invited to the party πŸŽ‰

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. Very β€œI’m telling you right now” of it πŸ’€

If a list of ModFiles is given as input, a list of Result will be returned. The order of the returned list is undefined. Don’t trust the order, trust the coverage ✨

Note that the internal database is (re-)initiated during the compilation, meaning any previously collected coverage data for the module will be lost. So like, previous data goes poof. Be aware 🫢

πŸ› οΈ compile_directory(...)

compile_directory() -> [Result] | {error, Reason}

compile_directory(Dir) -> [Result] | {error, Reason}

compile_directory(Dir, Options) -> [Result] | {error, Reason}

Types:

Dir = file:filename()
Options = [option()]
Reason = file_error()
Result = compile_result()
option() =
    {i, Dir :: file:filename()} |
    {d, Macro :: atom()} |
    {d, Macro :: atom(), Value :: term()} |
    export_all
  See compile:file/2.
file_error() = eacces | enoent
compile_result() =
    {ok, Module :: module()} |
    {error, file:filename()} |
    {error, not_main_node}

Compiles all modules (.erl files) in a directory Dir for Cover analysis the same way as compile_module/1,2 and returns a list with the return values. Whole directory getting the Cover treatment, periodt πŸ’…

Dir defaults to the current working directory.

The function returns {error, eacces} if the directory is not readable or {error, enoent} if the directory does not exist. Permission issues and missing directories are not invited to the function πŸ’”

πŸ› οΈ compile_beam(...)

compile_beam(ModFiles) -> Result | [Result]

Types:

ModFiles = beam_mod_files()
Result = compile_beam_result()
beam_mod_files() = beam_mod_file() | [beam_mod_file()]
beam_mod_file() =
    (Module :: module()) | (BeamFile :: file:filename())
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

Does the same as compile/1,2, but uses an existing .beam file as base, that is, the module is not compiled from source. Thus compile_beam/1 is faster than compile/1,2. Speedy little icon ⚑

Note that the existing .beam file must contain abstract code, that is, it must have been compiled with the debug_info option. If not, 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. No abstract code, no coverage bestie 😭

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/1,2, there is no way to find the correct .beam file, so the error reason {already_cover_compiled, no_beam_found, Module} is returned. It’s giving β€œwe tried to find it, we really did” πŸ’€

{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. Again, order is a mystery, coverage is the main character ✨

πŸ› οΈ compile_beam_directory(...)

compile_beam_directory() -> [Result] | {error, Reason}

compile_beam_directory(Dir) -> [Result] | {error, Reason}

Types:

Dir = file:filename()
Reason = file_error()
Result = compile_beam_result()
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
file_error() = eacces | enoent

Compiles all modules (.beam files) in a directory Dir for Cover analysis the same way as compile_beam/1 and returns a list with the return values. Beam files, assemble πŸ¦‹

Dir defaults to the current working directory.

The function returns {error, eacces} if the directory is not readable or {error, enoent} if the directory does not exist. πŸ’…


Okay so next up is the analyse squad β€” there are literally eight of them and they all do coverage math. Here's the full lineup, no cap:

       analyse() ->
                  {result, analyse_ok(), analyse_fail()} |
                  {error, not_main_node}

       analyse(Analysis) ->
                  {result, analyse_ok(), analyse_fail()} |
                  {error, not_main_node}

       analyse(Level) ->
                  {result, analyse_ok(), analyse_fail()} |
                  {error, not_main_node}

       analyse(Modules) ->
                  OneResult |
                  {result, analyse_ok(), analyse_fail()} |
                  {error, not_main_node}

       analyse(Analysis, Level) ->
                  {result, analyse_ok(), analyse_fail()} |
                  {error, not_main_node}

       analyse(Modules, Analysis) ->
                  OneResult |
                  {result, analyse_ok(), analyse_fail()} |
                  {error, not_main_node}

       analyse(Modules, Level) ->
                  OneResult |
                  {result, analyse_ok(), analyse_fail()} |
                  {error, not_main_node}

       analyse(Modules, Analysis, Level) ->
                  OneResult |
                  {result, analyse_ok(), analyse_fail()} |
                  {error, not_main_node}

Types:

                 Analysis = analysis()
                 Level = level()
                 Modules = modules()
                 OneResult = one_result()
                 analysis() = coverage | calls
                 level() = line | clause | function | module
                 modules() = module() | [module()]
                 one_result() =
                     {ok, {Module :: module(), Value :: analyse_value()}} |
                     {ok, [{Item :: analyse_item(), Value :: analyse_value()}]} |
                     {error, {not_cover_compiled, module()}}
                 analyse_fail() = [{not_cover_compiled, module()}]
                 analyse_ok() =
                     [{Module :: module(), Value :: analyse_value()}] |
                     [{Item :: analyse_item(), Value :: analyse_value()}]
                 analyse_value() =
                     {Cov :: integer() >= 0, NotCov :: integer() >= 0} |
                     (Calls :: integer() >= 0)
                 analyse_item() =
                     (Line :: {M :: module(), N :: integer() >= 0}) |
                     (Clause ::
                          {M :: module(),
                           F :: atom(),
                           A :: arity(),
                           C :: integer() >= 0}) |
                     (Function :: {M :: module(), F :: atom(), A :: arity()})

This whole fam performs analysis on one or more Cover-compiled modules, using Analysis and Level (peep the types above) by digging through the contents of the internal database. πŸ•΅οΈβ€β™€οΈ

Analysis defaults to coverage and Level defaults to function. Slay, sensible defaults.

If Modules is just one atom (a single module), you get back OneResult. Otherwise you get {result, Ok, Fail}. Easy peasy.

If you don't pass Modules at all, it analyses all modules that have data in the cover data table. Note that this includes both Cover-compiled modules and imported modules. Don't forget the imports, bestie.

If a given module isn't Cover compiled, you'll see the error reason {not_cover_compiled, Module}. Rude but fair.


Next: analyse_to_file. It makes a copy of the source file and tells you how many times each executable line ran. Here are the signatures:

       analyse_to_file() ->
                          {result,
                           analyse_file_ok(),
                           analyse_file_fail()} |
                          {error, not_main_node}

       analyse_to_file(Modules) ->
                          Answer |
                          {result,
                           analyse_file_ok(),
                           analyse_file_fail()} |
                          {error, not_main_node}

       analyse_to_file(Options) ->
                          {result,
                           analyse_file_ok(),
                           analyse_file_fail()} |
                          {error, not_main_node}

       analyse_to_file(Modules, Options) ->
                          Answer |
                          {result,
                           analyse_file_ok(),
                           analyse_file_fail()} |
                          {error, not_main_node}

Types:

                 Modules = modules()
                 Options = [analyse_option()]
                 Answer = analyse_answer()
                 modules() = module() | [module()]
                 analyse_option() =
                     html |
                     {outfile, OutFile :: file:filename()} |
                     {outdir, OutDir :: file:filename()}
                 analyse_answer() =
                     {ok, OutFile :: file:filename()} | {error, analyse_rsn()}
                 analyse_file_ok() = [OutFile :: file:filename()]
                 analyse_file_fail() = [analyse_rsn()]
                 analyse_rsn() =
                     {not_cover_compiled, Module :: module()} |
                     {file, File :: file:filename(), Reason :: term()} |
                     {no_source_code_found, Module :: module()}

This one makes copies of the source file for the given modules, and for each executable line it specifies how many times it has been executed. It's giving "receipts for your code" energy. 🧾✨

The output file OutFile defaults to Module.COVER.out, or Module.COVER.html if the option html was used.

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

If Modules is not given, all modules that have data in the cover data table are analysed. Again: includes both Cover-compiled and imported modules, no cap.

If a module isn't Cover compiled, you get 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.

Now the source-lookup tea β˜•: 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 the same directory as the .beam file, in ../src relative to that directory, or using the source path in Module:module_info(compile). When using the latter, two paths are examined: first the one constructed by joining ../src and the tail of the compiled path below a trailing src component, then the compiled path itself. If no source code is found, this is indicated by the error reason {no_source_code_found, Module}.


Now async_analyse_to_file β€” it's the same as above but asynchronous. Here are the signatures:

       async_analyse_to_file(Module) -> pid()

       async_analyse_to_file(Module, OutFile) -> pid()

       async_analyse_to_file(Module, Options) -> pid()

       async_analyse_to_file(Module, OutFile, Options) -> pid()

Types:

                 Module = module()
                 OutFile = file:filename()
                 Options = [Option]
                 Option = html
                 analyse_rsn() =
                     {not_cover_compiled, Module :: module()} |
                     {file, File :: file:filename(), Reason :: term()} |
                     {no_source_code_found, Module :: module()}

This function works exactly the same way as analyse_to_file except that it is asynchronous instead of synchronous. 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. πŸ’₯


modules() β€” returns a list with all modules that are currently Cover compiled.

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

imported_modules() β€” returns a list with all modules for which there are imported data.

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

imported() β€” returns a list with all imported files.

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

which_nodes() β€” returns a list with all nodes that are part of the coverage analysis. Note that the current node is not returned. This node is always part of the analysis. πŸ‘‘

       which_nodes() -> [node()]

is_compiled(Module) β€” tells you if a module is Cover compiled:

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

Types:

                 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. Slay.


reset() and reset(Module) β€” wipe the coverage data:

       reset() -> ok | {error, not_main_node}

       reset(Module) ->
                ok |
                {error, not_main_node} |
                {error, {not_cover_compiled, Module}}

Types:

                 Module = module()

Resets all coverage data for a Cover-compiled module Module in the Cover database on all nodes. If the argument is omitted, the coverage data will be reset for all modules known by Cover.

If Module is not Cover compiled, the function returns {error, {not_cover_compiled, Module}}.


export(File) and export(File, Module) β€” save that coverage data:

       export(File) -> ok | {error, Reason}

       export(File, Module) -> ok | {error, Reason}

Types:

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

Exports the current coverage data for Module to the file ExportFile. It is recommended to name the ExportFile with the extension .coverdata, since other filenames cannot be read by the web-based interface to cover. So use .coverdata or the web UI will ghost you. πŸ‘»

If Module is not given, 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.


import(ExportFile) β€” pull that data back in:

       import(ExportFile) -> ok | {error, Reason}

Types:

                 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 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. Math girl summer. πŸ“Š

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. (Chaotic but valid. πŸ’…)

See also export/1,2.

stop() -> ok | {error, not_main_node}

Okay so this one is the big red button πŸ›‘ β€” it stops the Cover server and unloads all Cover compiled code. That's the whole entire job. You get ok back when it goes through, or {error, not_main_node} if the Cover server situation isn't giving main-character main-node energy. No cap, that's it.

stop(Nodes) -> ok | {error, not_main_node}

Types:

Nodes = node() | [node()]

Same shutdown slay, but now with a guest list πŸ’Œ β€” you hand it one node or a whole list of them, and it stops the Cover server and unloads all Cover compiled code on the given nodes. And here's the iconic bit: data stored in the Cover database on the remote nodes is fetched and stored on the main node first. So like, nothing gets ghosted on the way out. πŸŽ€

Returns ok, or {error, not_main_node} β€” same vibe check as before.

flush(Nodes) -> ok | {error, not_main_node}

Types:

Nodes = node() | [node()]

This one is just the data heist, no shutdown πŸ’… β€” fetch data from the Cover database on the remote nodes and stored on the main node. Like stop/1 but without the unaliving of the server. Returns ok, or {error, not_main_node} when the main node situation isn't cooperating.

Heads up bestie: stop/0, stop/1 and flush/1 all share that {error, not_main_node} return β€” so if you see it, that's the error you're getting, fr fr. πŸ™ˆ

🌈 SEE ALSO 🌈


And that's a wrap on the whole page, bestie! 🫢 This little masterpiece comes to you from Ericsson AB, shipped as part of tools 3.5.3, and it's the cover(3erl) manual page β€” go forth and get that coverage. πŸ‘‘βœ¨