mod utils;
use moon_common::Id;
use moon_config::{
DependencyScope, GlobPath, Input, LanguageType, LayerType, OwnersPaths, PortablePath,
ProjectConfig, ProjectDependencyConfig, ProjectDependsOn, ProjectToolchainEntry, TaskArgs,
ToolchainPluginConfig,
};
use moon_config_loader::ConfigLoader;
use proto_core::UnresolvedVersionSpec;
use rustc_hash::FxHashMap;
use schematic::schema::IndexMap;
use serde_json::Value;
use std::collections::BTreeMap;
use std::path::Path;
use utils::*;
fn load_config_from_root(root: &Path, source: &str) -> miette::Result<ProjectConfig> {
ConfigLoader::new(root.join(".moon")).load_project_config_from_source(root, source)
}
mod project_config {
use super::*;
#[test]
#[should_panic(
expected = "unknown field `unknown`, expected one of `$schema`, `dependsOn`, `deps`, `docker`, `env`, `fileGroups`, `id`, `language`, `layer`, `owners`, `project`, `stack`, `tags`, `tasks`, `toolchains`, `workspace`"
)]
fn error_unknown_field() {
test_load_config("moon.yml", "unknown: 123", |path| {
load_config_from_root(path, ".")
});
}
#[test]
fn loads_defaults() {
let config = test_load_config("moon.yml", "{}", |path| load_config_from_root(path, "."));
assert_eq!(config.language, LanguageType::Unknown);
assert_eq!(config.layer, LayerType::Unknown);
}
#[test]
fn can_use_references() {
let config = test_load_config(
"moon.yml",
r"
tasks:
build: &webpack
command: 'webpack'
inputs:
- 'src/**/*'
start:
<<: *webpack
args: 'serve'
",
|path| load_config_from_root(path, "."),
);
let build = config.tasks.get("build").unwrap();
assert_eq!(build.command, TaskArgs::String("webpack".to_owned()));
assert_eq!(build.args, TaskArgs::Noop);
assert_eq!(
build.inputs,
Some(vec![Input::Glob(stub_glob_input("src/**/*"))])
);
let start = config.tasks.get("start").unwrap();
assert_eq!(start.command, TaskArgs::String("webpack".to_owned()));
assert_eq!(start.args, TaskArgs::String("serve".to_owned()));
assert_eq!(
start.inputs,
Some(vec![Input::Glob(stub_glob_input("src/**/*"))])
);
}
mod depends_on {
use super::*;
#[test]
fn supports_list_of_strings() {
let config = test_load_config("moon.yml", "dependsOn: ['a', 'b', 'c']", |path| {
load_config_from_root(path, ".")
});
assert_eq!(
config.depends_on,
vec![
ProjectDependsOn::String(Id::raw("a")),
ProjectDependsOn::String(Id::raw("b")),
ProjectDependsOn::String(Id::raw("c"))
]
);
}
#[test]
fn supports_list_of_objects() {
let config = test_load_config(
"moon.yml",
r"
dependsOn:
- id: 'a'
scope: 'development'
- id: 'b'
scope: 'production'",
|path| load_config_from_root(path, "."),
);
assert_eq!(
config.depends_on,
vec![
ProjectDependsOn::Object(ProjectDependencyConfig {
id: Id::raw("a"),
scope: DependencyScope::Development,
..ProjectDependencyConfig::default()
}),
ProjectDependsOn::Object(ProjectDependencyConfig {
id: Id::raw("b"),
scope: DependencyScope::Production,
..ProjectDependencyConfig::default()
})
]
);
}
#[test]
fn supports_list_of_strings_and_objects() {
let config = test_load_config(
"moon.yml",
r"
dependsOn:
- 'a'
- id: 'b'
scope: 'production'",
|path| load_config_from_root(path, "."),
);
assert_eq!(
config.depends_on,
vec![
ProjectDependsOn::String(Id::raw("a")),
ProjectDependsOn::Object(ProjectDependencyConfig {
id: Id::raw("b"),
scope: DependencyScope::Production,
..ProjectDependencyConfig::default()
})
]
);
}
#[test]
#[should_panic(expected = "failed to parse as any variant of PartialProjectDependsOn")]
fn errors_on_invalid_object_scope() {
test_load_config(
"moon.yml",
r"
dependsOn:
- id: 'a'
scope: 'invalid'
",
|path| load_config_from_root(path, "."),
);
}
}
mod file_groups {
use super::*;
#[test]
fn groups_into_correct_enums() {
let config = test_load_config(
"moon.yml",
r"
fileGroups:
files:
- /ws/relative
- proj/relative
globs:
- /ws/**/*
- /!ws/**/*
- proj/**/*
- '!proj/**/*'
",
|path| load_config_from_root(path, "."),
);
assert_eq!(
config.file_groups,
FxHashMap::from_iter([
(
Id::raw("files"),
vec![
Input::File(stub_file_input("/ws/relative")),
Input::File(stub_file_input("proj/relative"))
]
),
(
Id::raw("globs"),
vec![
Input::Glob(stub_glob_input("/ws/**/*")),
Input::Glob(stub_glob_input("!/ws/**/*")),
Input::Glob(stub_glob_input("proj/**/*")),
Input::Glob(stub_glob_input("!proj/**/*")),
]
),
])
);
}
}
mod language {
use super::*;
#[test]
fn supports_variant() {
let config = test_load_config("moon.yml", "language: rust", |path| {
load_config_from_root(path, ".")
});
assert_eq!(config.language, LanguageType::Rust);
}
#[test]
fn unsupported_variant_becomes_other() {
let config = test_load_config("moon.yml", "language: groovy", |path| {
load_config_from_root(path, ".")
});
assert_eq!(config.language, LanguageType::Other(Id::raw("groovy")));
}
}
mod owners {
use super::*;
#[test]
fn loads_defaults() {
let config = test_load_config("moon.yml", "owners: {}", |path| {
load_config_from_root(path, ".")
});
assert_eq!(config.owners.custom_groups, FxHashMap::default());
assert_eq!(config.owners.default_owner, None);
assert!(!config.owners.optional);
assert_eq!(config.owners.paths, OwnersPaths::List(vec![]));
assert_eq!(config.owners.required_approvals, None);
}
#[test]
fn can_set_values() {
let config = test_load_config(
"moon.yml",
r"
owners:
customGroups:
foo: [a, b, c]
bar: [x, y, z]
defaultOwner: x
optional: true
requiredApprovals: 2
",
|path| load_config_from_root(path, "."),
);
assert_eq!(
config.owners.custom_groups,
FxHashMap::from_iter([
("foo".into(), vec!["a".into(), "b".into(), "c".into()]),
("bar".into(), vec!["x".into(), "y".into(), "z".into()]),
])
);
assert_eq!(config.owners.default_owner, Some("x".to_string()));
assert!(config.owners.optional);
assert_eq!(config.owners.required_approvals, Some(2));
}
#[test]
fn can_set_paths_as_list() {
let config = test_load_config(
"moon.yml",
r"
owners:
defaultOwner: x
paths:
- file.txt
- dir/**/*
",
|path| load_config_from_root(path, "."),
);
assert_eq!(
config.owners.paths,
OwnersPaths::List(vec![
GlobPath::parse("file.txt").unwrap(),
GlobPath::parse("dir/**/*").unwrap()
])
);
}
#[test]
fn can_set_paths_as_map() {
let config = test_load_config(
"moon.yml",
r"
owners:
paths:
'file.txt': [a, b]
'dir/**/*': [c, d]
",
|path| load_config_from_root(path, "."),
);
assert_eq!(
config.owners.paths,
OwnersPaths::Map(IndexMap::from_iter([
(
GlobPath::parse("file.txt").unwrap(),
vec!["a".into(), "b".into()]
),
(
GlobPath::parse("dir/**/*").unwrap(),
vec!["c".into(), "d".into()]
),
]))
);
}
#[test]
#[should_panic(expected = "a default owner is required when defining a list of paths")]
fn errors_on_paths_list_empty_owner() {
test_load_config(
"moon.yml",
r"
owners:
paths:
- file.txt
- dir/**/*
",
|path| load_config_from_root(path, "."),
);
}
#[test]
#[should_panic(
expected = "a default owner is required when defining an empty list of owners"
)]
fn errors_on_paths_map_empty_owner() {
test_load_config(
"moon.yml",
r"
owners:
paths:
'file.txt': []
",
|path| load_config_from_root(path, "."),
);
}
}
mod project {
use super::*;
use serde_json::Value;
#[test]
fn can_set_only_description() {
let config = test_load_config(
"moon.yml",
r"
project:
description: 'Text'
",
|path| load_config_from_root(path, "."),
);
let meta = config.project.unwrap();
assert_eq!(meta.description.unwrap(), "Text");
}
#[test]
fn can_set_all() {
let config = test_load_config(
"moon.yml",
r"
project:
title: Name
description: Description
owner: team
maintainers: [a, b, c]
channel: '#abc'
",
|path| load_config_from_root(path, "."),
);
let meta = config.project.unwrap();
assert_eq!(meta.title.unwrap(), "Name");
assert_eq!(meta.description.unwrap(), "Description");
assert_eq!(meta.owner.unwrap(), "team");
assert_eq!(meta.maintainers, vec!["a", "b", "c"]);
assert_eq!(meta.channel.unwrap(), "#abc");
}
#[test]
fn can_set_custom_fields() {
let config = test_load_config(
"moon.yml",
r"
project:
description: 'Test'
bool: true
string: 'abc'
",
|path| load_config_from_root(path, "."),
);
let meta = config.project.unwrap();
assert_eq!(
meta.metadata,
FxHashMap::from_iter([
("bool".into(), Value::Bool(true)),
("string".into(), Value::String("abc".into())),
])
);
}
#[test]
#[should_panic(expected = "must start with a `#`")]
fn errors_if_channel_no_hash() {
test_load_config(
"moon.yml",
r"
project:
description: Description
channel: abc
",
|path| load_config_from_root(path, "."),
);
}
}
mod tags {
use super::*;
#[test]
fn can_set_tags() {
let config = test_load_config(
"moon.yml",
r"
tags:
- normal
- camelCase
- kebab-case
- snake_case
- dot.case
- slash/case
",
|path| load_config_from_root(path, "."),
);
assert_eq!(
config.tags,
vec![
Id::raw("normal"),
Id::raw("camelCase"),
Id::raw("kebab-case"),
Id::raw("snake_case"),
Id::raw("dot.case"),
Id::raw("slash/case")
]
);
}
#[test]
#[should_panic(expected = "Invalid identifier format for `foo bar`")]
fn errors_on_invalid_format() {
test_load_config("moon.yml", "tags: ['foo bar']", |path| {
load_config_from_root(path, ".")
});
}
}
mod tasks {
use super::*;
#[test]
fn supports_id_patterns() {
let config = test_load_config(
"moon.yml",
r"
tasks:
normal:
command: 'a'
kebab-case:
command: 'b'
camelCase:
command: 'c'
snake_case:
command: 'd'
dot.case:
command: 'e'
slash/case:
command: 'f'
",
|path| load_config_from_root(path, "."),
);
assert!(config.tasks.contains_key("normal"));
assert!(config.tasks.contains_key("kebab-case"));
assert!(config.tasks.contains_key("camelCase"));
assert!(config.tasks.contains_key("snake_case"));
assert!(config.tasks.contains_key("dot.case"));
assert!(config.tasks.contains_key("slash/case"));
}
#[test]
fn can_extend_siblings() {
let config = test_load_config(
"moon.yml",
r"
tasks:
base:
command: 'base'
extender:
extends: 'base'
args: '--more'
",
|path| load_config_from_root(path, "."),
);
assert!(config.tasks.contains_key("base"));
assert!(config.tasks.contains_key("extender"));
}
}
mod toolchain {
use super::*;
#[test]
fn can_set_settings() {
let config = test_load_config(
"moon.yml",
r"
toolchains:
node:
version: '18.0.0'
typescript:
routeOutDirToCache: true
",
|path| load_config_from_root(path, "."),
);
assert!(config.toolchains.plugins.contains_key("node"));
assert!(config.toolchains.plugins.contains_key("typescript"));
if let ProjectToolchainEntry::Object(node) =
config.toolchains.plugins.get("node").unwrap()
{
assert_eq!(
node.version,
Some(UnresolvedVersionSpec::parse("18.0.0").unwrap())
);
}
if let ProjectToolchainEntry::Object(ts) =
config.toolchains.plugins.get("typescript").unwrap()
{
assert_eq!(
ts.config.get("routeOutDirToCache").unwrap(),
&Value::Bool(true),
);
}
}
#[test]
fn can_disable_with_null() {
let config = test_load_config(
"moon.yml",
r"
toolchains:
example: null
",
|path| load_config_from_root(path, "."),
);
assert_eq!(
config.toolchains.plugins.get("example").unwrap(),
&ProjectToolchainEntry::Disabled
);
}
#[test]
fn can_disable_with_false() {
let config = test_load_config(
"moon.yml",
r"
toolchains:
example: false
",
|path| load_config_from_root(path, "."),
);
assert_eq!(
config.toolchains.plugins.get("example").unwrap(),
&ProjectToolchainEntry::Enabled(false)
);
}
#[test]
fn can_enable_with_true() {
let config = test_load_config(
"moon.yml",
r"
toolchains:
example: true
",
|path| load_config_from_root(path, "."),
);
assert_eq!(
config.toolchains.plugins.get("example").unwrap(),
&ProjectToolchainEntry::Enabled(true)
);
}
#[test]
fn can_set_customg_config() {
let config = test_load_config(
"moon.yml",
r"
toolchains:
example:
version: '1.2.3'
custom: true
",
|path| load_config_from_root(path, "."),
);
assert_eq!(
config.toolchains.plugins.get("example").unwrap(),
&ProjectToolchainEntry::Object(ToolchainPluginConfig {
version: Some(UnresolvedVersionSpec::parse("1.2.3").unwrap()),
config: BTreeMap::from_iter([("custom".into(), serde_json::Value::Bool(true))]),
..Default::default()
})
);
}
}
mod workspace {
use super::*;
#[test]
fn can_set_settings() {
let config = test_load_config(
"moon.yml",
r"
workspace:
inheritedTasks:
exclude: [a]
include: [b]
rename:
c: d
",
|path| load_config_from_root(path, "."),
);
assert_eq!(config.workspace.inherited_tasks.exclude, vec![Id::raw("a")]);
assert_eq!(
config.workspace.inherited_tasks.include,
Some(vec![Id::raw("b")])
);
assert_eq!(
config.workspace.inherited_tasks.rename,
FxHashMap::from_iter([(Id::raw("c"), Id::raw("d"))])
);
}
}
#[test]
fn supports_hcl() {
load_project_config_in_format("hcl");
}
#[test]
fn supports_pkl() {
load_project_config_in_format("pkl");
}
#[test]
fn supports_toml() {
load_project_config_in_format("toml");
}
}