use super::*;
use dictator_decree_abi::{Capability, Decree, DecreeMetadata, Diagnostics};
use dictator_decree_abi::{Diagnostic, Span};
struct MockDecree {
name: &'static str,
exts: Vec<String>,
filenames: Vec<String>,
skip: Vec<String>,
rule: &'static str,
}
impl MockDecree {
fn simple(name: &'static str, exts: Vec<String>, rule: &'static str) -> Self {
Self {
name,
exts,
filenames: vec![],
skip: vec![],
rule,
}
}
}
impl Decree for MockDecree {
fn name(&self) -> &str {
self.name
}
fn lint(&self, _path: &str, _source: &str) -> Diagnostics {
vec![Diagnostic {
rule: self.rule.to_string(),
message: format!("hit {}", self.name),
span: Span::new(0, 0),
enforced: false,
}]
}
fn metadata(&self) -> DecreeMetadata {
DecreeMetadata {
abi_version: "1".into(),
decree_version: "1".into(),
description: String::new(),
persona: "The Dictator".into(),
dectauthors: None,
supported_extensions: self.exts.clone(),
supported_filenames: self.filenames.clone(),
skip_filenames: self.skip.clone(),
file_scope_rules: vec![],
capabilities: vec![Capability::Lint],
}
}
}
#[test]
fn classified_files_get_one_diag_and_no_inspection() {
let universal: BoxDecree = Box::new(MockDecree::simple("supreme", vec![], "supreme/hit"));
let mut regime = Regime::new();
regime.add_decree(universal);
let secret = Source {
path: Utf8Path::new("config/master.key"),
text: "27c79b585e4a73d4aa7645fe6c880533",
};
let diags = regime.enforce(&[secret]).unwrap();
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].rule, classified::CLASSIFIED_RULE);
assert!(
!diags[0].enforced,
"classified files must never be auto-fixed"
);
let public = Source {
path: Utf8Path::new("main.rs"),
text: "fn main() {}",
};
let diags = regime.enforce(&[public]).unwrap();
assert_eq!(diags[0].rule, "supreme/hit");
}
#[test]
fn watched_extensions_unites_declared_sets() {
let decree_a: BoxDecree = Box::new(MockDecree::simple(
"a",
vec!["rs".into(), "Rb".into()],
"a/hit",
));
let decree_b: BoxDecree = Box::new(MockDecree::simple("b", vec!["ts".into()], "b/hit"));
let mut regime = Regime::new();
regime.add_decree(decree_a);
regime.add_decree(decree_b);
let exts = regime.watched_extensions().unwrap();
assert!(exts.contains("rs"));
assert!(exts.contains("rb"));
assert!(exts.contains("ts"));
assert_eq!(exts.len(), 3);
}
#[test]
fn watched_extensions_none_when_only_universal() {
let sup: BoxDecree = Box::new(MockDecree::simple("supreme", vec![], "supreme/hit"));
let mut regime = Regime::new();
regime.add_decree(sup);
assert!(regime.watched_extensions().is_none());
}
#[test]
fn enforce_skips_supreme_when_language_specific_matches() {
let supreme: BoxDecree = Box::new(MockDecree::simple("supreme", vec![], "supreme/hit"));
let ruby: BoxDecree = Box::new(MockDecree::simple("ruby", vec!["rb".into()], "ruby/hit"));
let mut regime = Regime::new();
regime.add_decree(supreme);
regime.add_decree(ruby);
let path = Utf8Path::new("test.rb");
let sources = [Source { path, text: "x" }];
let diags = regime.enforce(&sources).unwrap();
assert!(diags.iter().any(|d| d.rule == "ruby/hit"));
assert!(!diags.iter().any(|d| d.rule == "supreme/hit"));
}
#[test]
fn enforce_runs_supreme_when_language_specific_does_not_match() {
let supreme: BoxDecree = Box::new(MockDecree::simple("supreme", vec![], "supreme/hit"));
let ruby: BoxDecree = Box::new(MockDecree::simple("ruby", vec!["rb".into()], "ruby/hit"));
let mut regime = Regime::new();
regime.add_decree(supreme);
regime.add_decree(ruby);
let path = Utf8Path::new("test.txt");
let sources = [Source { path, text: "x" }];
let diags = regime.enforce(&sources).unwrap();
assert!(diags.iter().any(|d| d.rule == "supreme/hit"));
assert!(!diags.iter().any(|d| d.rule == "ruby/hit"));
}
#[test]
fn enforce_ignores_configured_rules_by_filename() {
let supreme: BoxDecree = Box::new(MockDecree::simple(
"supreme",
vec![],
"supreme/tab-character",
));
let mut settings = DecreeSettings::default();
settings.ignore.insert(
"tab-character".to_string(),
crate::config::RuleIgnore {
filenames: vec!["Makefile".to_string()],
extensions: vec![],
},
);
let mut config = DictateConfig::default();
config.decree.insert("supreme".to_string(), settings);
let mut regime = Regime::new();
regime.set_rule_ignores_from_config(Some(&config));
regime.add_decree(supreme);
let path = Utf8Path::new("Makefile");
let sources = [Source { path, text: "x" }];
let diags = regime.enforce(&sources).unwrap();
assert!(diags.is_empty(), "rule should be ignored for Makefile");
}
#[test]
fn enforce_ignores_configured_rules_by_extension() {
let supreme: BoxDecree = Box::new(MockDecree::simple(
"supreme",
vec![],
"supreme/tab-character",
));
let mut settings = DecreeSettings::default();
settings.ignore.insert(
"tab-character".to_string(),
crate::config::RuleIgnore {
filenames: vec![],
extensions: vec!["md".to_string(), "MDX".to_string()],
},
);
let mut config = DictateConfig::default();
config.decree.insert("supreme".to_string(), settings);
let mut regime = Regime::new();
regime.set_rule_ignores_from_config(Some(&config));
regime.add_decree(supreme);
let path = Utf8Path::new("README.md");
let sources = [Source { path, text: "x" }];
let diags = regime.enforce(&sources).unwrap();
assert!(diags.is_empty(), "rule should be ignored for .md");
let path = Utf8Path::new("doc.mdx");
let sources = [Source { path, text: "x" }];
let diags = regime.enforce(&sources).unwrap();
assert!(diags.is_empty(), "rule should be ignored for .mdx");
}
#[test]
fn enforce_does_not_ignore_unconfigured_rules() {
let supreme: BoxDecree = Box::new(MockDecree::simple(
"supreme",
vec![],
"supreme/trailing-whitespace",
));
let mut settings = DecreeSettings::default();
settings.ignore.insert(
"tab-character".to_string(),
crate::config::RuleIgnore {
filenames: vec!["Makefile".to_string()],
extensions: vec!["md".to_string()],
},
);
let mut config = DictateConfig::default();
config.decree.insert("supreme".to_string(), settings);
let mut regime = Regime::new();
regime.set_rule_ignores_from_config(Some(&config));
regime.add_decree(supreme);
let path = Utf8Path::new("README.md");
let sources = [Source { path, text: "x" }];
let diags = regime.enforce(&sources).unwrap();
assert!(
diags
.iter()
.any(|d| d.rule == "supreme/trailing-whitespace"),
"unconfigured rules should still be reported"
);
}
#[test]
fn enforce_does_not_shadow_supreme_for_non_language_decree() {
let supreme: BoxDecree = Box::new(MockDecree::simple("supreme", vec![], "supreme/hit"));
let frontmatter: BoxDecree = Box::new(MockDecree::simple(
"frontmatter",
vec!["md".into()],
"frontmatter/hit",
));
let mut regime = Regime::new();
regime.add_decree(supreme);
regime.add_decree(frontmatter);
let path = Utf8Path::new("README.md");
let sources = [Source { path, text: "x" }];
let diags = regime.enforce(&sources).unwrap();
assert!(diags.iter().any(|d| d.rule == "supreme/hit"));
assert!(diags.iter().any(|d| d.rule == "frontmatter/hit"));
}
#[test]
fn enforce_golang_shadows_supreme_for_go_files() {
let supreme: BoxDecree = Box::new(MockDecree::simple("supreme", vec![], "supreme/hit"));
let golang: BoxDecree = Box::new(MockDecree::simple(
"golang",
vec!["go".into()],
"golang/hit",
));
let mut regime = Regime::new();
regime.add_decree(supreme);
regime.add_decree(golang);
let path = Utf8Path::new("main.go");
let sources = [Source {
path,
text: "package main",
}];
let diags = regime.enforce(&sources).unwrap();
assert!(
diags.iter().any(|d| d.rule == "golang/hit"),
"golang should run on .go files"
);
assert!(
!diags.iter().any(|d| d.rule == "supreme/hit"),
"supreme should be shadowed by golang"
);
}
#[test]
fn enforce_supreme_runs_on_go_files_when_golang_not_loaded() {
let supreme: BoxDecree = Box::new(MockDecree::simple("supreme", vec![], "supreme/hit"));
let mut regime = Regime::new();
regime.add_decree(supreme);
let path = Utf8Path::new("main.go");
let sources = [Source {
path,
text: "package main",
}];
let diags = regime.enforce(&sources).unwrap();
assert!(
diags.iter().any(|d| d.rule == "supreme/hit"),
"supreme should run when no golang decree loaded"
);
}
#[test]
fn enforce_all_shadowers_work() {
for (name, ext, rule) in [
("ruby", "rb", "ruby/hit"),
("typescript", "ts", "typescript/hit"),
("golang", "go", "golang/hit"),
("rust", "rs", "rust/hit"),
("python", "py", "python/hit"),
] {
let supreme: BoxDecree = Box::new(MockDecree::simple("supreme", vec![], "supreme/hit"));
let lang: BoxDecree = Box::new(MockDecree::simple(name, vec![ext.into()], rule));
let mut regime = Regime::new();
regime.add_decree(supreme);
regime.add_decree(lang);
let path_str = format!("test.{ext}");
let path = Utf8Path::new(&path_str);
let sources = [Source { path, text: "x" }];
let diags = regime.enforce(&sources).unwrap();
assert!(
diags.iter().any(|d| d.rule == rule),
"{name} should run on .{ext} files"
);
assert!(
!diags.iter().any(|d| d.rule == "supreme/hit"),
"supreme should be shadowed by {name} on .{ext} files"
);
}
}
#[test]
fn enforce_matches_by_filename() {
let ruby: BoxDecree = Box::new(MockDecree {
name: "ruby",
exts: vec!["rb".into()],
filenames: vec!["Gemfile".into(), "Rakefile".into()],
skip: vec![],
rule: "ruby/hit",
});
let mut regime = Regime::new();
regime.add_decree(ruby);
let path = Utf8Path::new("Gemfile");
let sources = [Source { path, text: "x" }];
let diags = regime.enforce(&sources).unwrap();
assert!(
diags.iter().any(|d| d.rule == "ruby/hit"),
"ruby should match Gemfile by filename"
);
}
#[test]
fn enforce_skips_skip_filenames() {
let ruby: BoxDecree = Box::new(MockDecree {
name: "ruby",
exts: vec!["rb".into()],
filenames: vec!["Gemfile".into()],
skip: vec!["Gemfile.lock".into()],
rule: "ruby/hit",
});
let mut regime = Regime::new();
regime.add_decree(ruby);
let path = Utf8Path::new("Gemfile.lock");
let sources = [Source { path, text: "x" }];
let diags = regime.enforce(&sources).unwrap();
assert!(
diags.is_empty(),
"Gemfile.lock should be skipped (owned but not linted)"
);
}
#[test]
fn enforce_skip_filenames_prevents_supreme() {
let supreme: BoxDecree = Box::new(MockDecree::simple("supreme", vec![], "supreme/hit"));
let ruby: BoxDecree = Box::new(MockDecree {
name: "ruby",
exts: vec!["rb".into()],
filenames: vec!["Gemfile".into()],
skip: vec!["Gemfile.lock".into()],
rule: "ruby/hit",
});
let mut regime = Regime::new();
regime.add_decree(supreme);
regime.add_decree(ruby);
let path = Utf8Path::new("Gemfile.lock");
let sources = [Source { path, text: "x" }];
let diags = regime.enforce(&sources).unwrap();
assert!(
diags.iter().any(|d| d.rule == "supreme/hit"),
"supreme lints files not in its skip list"
);
}
#[test]
fn enforce_filename_shadows_supreme() {
let supreme: BoxDecree = Box::new(MockDecree::simple("supreme", vec![], "supreme/hit"));
let golang: BoxDecree = Box::new(MockDecree {
name: "golang",
exts: vec!["go".into()],
filenames: vec!["go.mod".into()],
skip: vec!["go.sum".into()],
rule: "golang/hit",
});
let mut regime = Regime::new();
regime.add_decree(supreme);
regime.add_decree(golang);
let path = Utf8Path::new("go.mod");
let sources = [Source { path, text: "x" }];
let diags = regime.enforce(&sources).unwrap();
assert!(
diags.iter().any(|d| d.rule == "golang/hit"),
"golang should match go.mod"
);
assert!(
!diags.iter().any(|d| d.rule == "supreme/hit"),
"supreme should be shadowed by golang for go.mod"
);
}
#[test]
fn enforce_golang_skips_go_sum() {
let golang: BoxDecree = Box::new(MockDecree {
name: "golang",
exts: vec!["go".into()],
filenames: vec!["go.mod".into()],
skip: vec!["go.sum".into()],
rule: "golang/hit",
});
let mut regime = Regime::new();
regime.add_decree(golang);
let path = Utf8Path::new("go.sum");
let sources = [Source { path, text: "x" }];
let diags = regime.enforce(&sources).unwrap();
assert!(diags.is_empty(), "go.sum should be skipped by golang");
}