Current section
Files
Jump to
Current section
Files
lib/workspace/config.ex
defmodule Workspace.Config do
@test_coverage_schema [
threshold: [
type: :non_neg_integer,
doc: """
The overall coverage threshold for the workspace. If the overall test coverage is below this
value then the `workspace.test.coverage` command is considered failed. Notice
that the overall test coverage percentage is calculated only on the enabled projects.
""",
default: 90
],
warning_threshold: [
type: :non_neg_integer,
doc: """
If set it specifies an overall warning threshold under which a warning will be
raised. If not set it is implied to be the mid value between `threshold` and `100`.
"""
],
exporters: [
type: :keyword_list,
doc: """
Definition of exporters to be used. Each defined exporter must be an anonymous
function taking as input the `workspace` and the `coverage_stats`. For more
details check the `Mix.Tasks.Workspace.Test.Coverage` task.
"""
],
allow_failure: [
type: {:list, :atom},
doc: """
A list of projects for which the test coverage is allowed to fail without affecting
the overall status.
""",
default: []
]
]
@options_schema NimbleOptions.new!(
ignore_projects: [
type: {:list, :atom},
doc: """
A list of project modules to be ignored. If set these projects will
not be considered workspace projects when initializing a `Workspace`
with the current config.
""",
default: []
],
ignore_paths: [
type: {:list, :string},
doc: """
List of paths relative to the workspace root to be ignored from
parsing for projects detection.
""",
default: []
],
checks: [
type: {:list, :keyword_list},
doc: """
List of checks configured for the workspace. For more details check
`Workspace.Check`
""",
default: []
],
groups_for_checks: [
type: :non_empty_keyword_list,
doc: """
Allows you to configure the look and feel of the checks group headings.
It expects a keyword list, where the key is the group identifier and the
value is the configuration for this specific group heading, for example:
```elixir
groups_for_checks: [
package: [
title: "-> Package checks",
style: [:red]
],
docs: [
title: " 📚 Documentation checks",
style: [:yellow_background, :white]
]
]
```
Each group style definition is expected to be a keyword list with the
following options:
""",
keys: [
*: [
type: :non_empty_keyword_list,
keys: [
title: [
type: :string,
doc:
"The title of the check group, if not set defaults to the group name"
],
style: [
type: {:list, :atom},
doc: """
The ANSI style for the group heading. You can use it to modify the look
of this specific header. For example in order to make the background blue
and the text red you can set the style to:
```elixir
style: [:blue_background, :red]
```
For more details check the `IO.ANSI` docs.
"""
]
]
]
]
],
test_coverage: [
type: :keyword_list,
doc: """
Test coverage configuration for the workspace. Notice that this is
independent of the `test_coverage` configuration per project. It is
applied in the aggregate coverage and except thresholds you can
also configure coverage exporters.
""",
keys: @test_coverage_schema,
default: []
]
)
@moduledoc """
The workspace configuration.
The workspace configuration is specified usually in a `.workspace.exs` file in the
root of your workspace. This is the place to:
* Configure your workspace checks (`Workspace.Check`)
* Specify paths or projects to be ignored during the workspace graph generation
* Define overall workspace coverage thresholds
* Define any other configuration option may be needed by a third party plugin.
It is expected to be a keyword list with an arbitrary set of configuration options.
## Options
The following configuration options are supported by default:
#{NimbleOptions.docs(@options_schema)}
A simple example of a `.workspace.exs` follows:
```elixir
[
ignore_paths: ["deps", "artifacts", "cover"],
checks: [
# Add your checks here
[
module: Workspace.Checks.ValidateProject,
description: "all projects must have a description set",
opts: [
validate: fn project ->
case project.config[:description] do
nil -> {:error, "no :description set"}
description when is_binary(description) -> {:ok, ""}
other -> {:error, "description must be binary, got: \#{inspect(other)}"}
end
end
]
]
],
test_coverage: [
allow_failure: [:foo],
threshold: 98,
warning_threshold: 99,
exporters: [
lcov: fn workspace, coverage_stats ->
Workspace.Coverage.LCOV.export(workspace, coverage_stats,
output_path: "artifacts/coverage"
)
end
]
],
my_awesome_plugin_options: [
x: 1,
y: 2
]
]
```
> #### Extra Options {: .info}
>
> Notice that the validation will **not fail** if any extra configuration option
> is present. This way various plugins or mix tasks may define their own options
> that can be read from this configuration.
"""
@doc """
Loads the workspace config from the given path.
An error tuple will be returned if the config is invalid.
"""
@spec load(config_file :: String.t()) :: {:ok, keyword()} | {:error, binary()}
def load(config_file) do
config_file = Path.expand(config_file)
with {:ok, config_file} <- Workspace.Helpers.ensure_file_exists(config_file),
{config, _bindings} <- Code.eval_file(config_file) do
{:ok, config}
end
end
@doc """
Validates that the given `config` is a valid `Workspace` config.
A `config` is valid if:
- it follows the workspace config schema
- every check configuration is valid according to it's own schema. For more details
check `Workspace.Check.validate/1`.
Returns either `{:ok, config}` with the updated `config` is it is valid, or
`{:error, message}` in case of errors.
Notice that only the default options are validated, since the config may be used by
plugins for setting custom options.
"""
@spec validate(config :: keyword()) :: {:ok, keyword()} | {:error, binary()}
def validate(config) do
with {:ok, config} <- validate_config(config) do
validate_checks(config)
end
end
defp validate_config(config) when is_list(config) do
default_options_config = Keyword.take(config, Keyword.keys(@options_schema.schema))
# we only validate default options since extra options may be added by
# plugins
case NimbleOptions.validate(default_options_config, @options_schema) do
{:ok, default_options_config} -> {:ok, Keyword.merge(config, default_options_config)}
{:error, %NimbleOptions.ValidationError{message: message}} -> {:error, message}
end
end
defp validate_config(config) do
{:error, "expected workspace config to be a keyword list, got: #{inspect(config)}"}
end
defp validate_checks(config) do
checks = config[:checks]
with {:ok, checks} <- validate_checks(checks, [], []),
:ok <- ensure_unique_check_ids(checks) do
{:ok, Keyword.put(config, :checks, checks)}
end
end
defp validate_checks([], checks, []), do: {:ok, :lists.reverse(checks)}
defp validate_checks([], _checks, errors) do
errors = :lists.reverse(errors) |> Enum.join("\n")
{:error, "failed to validate checks:\n #{errors}"}
end
defp validate_checks([check | rest], acc, errors) do
case Workspace.Check.validate(check) do
{:ok, check} ->
validate_checks(rest, [check | acc], errors)
{:error, message} ->
validate_checks(rest, acc, [message | errors])
end
end
defp ensure_unique_check_ids(checks) do
# TODO: remove filter once id required
duplicates =
checks
|> Enum.map(fn check -> check[:id] end)
|> Enum.reject(&is_nil/1)
|> Enum.group_by(& &1)
|> Enum.filter(&match?({_id, [_, _ | _]}, &1))
case duplicates do
[] ->
:ok
duplicates ->
{:error,
"check ids must be unique, the following have duplicates: #{inspect(Keyword.keys(duplicates))}"}
end
end
@doc """
Same as `validate/1` but raises an `ArgumentError` exception in case of failure.
In case of success the validated configuration keyword list is returned.
"""
@spec validate!(config :: keyword()) :: keyword()
def validate!(config) do
case validate(config) do
{:ok, config} -> config
{:error, message} -> raise ArgumentError, message: message
end
end
end