use std::collections::{BTreeMap, HashMap, HashSet};
use std::path::{Path, PathBuf};
use anyhow::Result;
use rayon::prelude::*;
use crate::file::{File, OnChangeBlock};
use crate::git::Repo;
use crate::{ThenChange, ThenChangeTarget};
#[derive(Debug)]
pub struct Parser {
root_path: PathBuf,
files: BTreeMap<PathBuf, File>,
num_blocks: usize,
}
impl Parser {
fn validate_block_target(
&self,
path: &Path,
block: &OnChangeBlock,
target: &ThenChangeTarget,
blocks: &HashMap<(&Path, &str), &OnChangeBlock>,
) -> Result<()> {
match target {
ThenChangeTarget::File(file) => {
if !self.files.contains_key(file) {
return Err(anyhow::anyhow!(
r#"block "{}" at "{}:{}" has non-existent ThenChange target "{}""#,
block.name(),
path.display(),
block.end_line(),
file.display(),
));
}
}
ThenChangeTarget::Block {
block: target_block,
file,
} => {
let file = file.as_deref().unwrap_or(path);
let block_key = (file, target_block.as_str());
if !blocks.contains_key(&block_key) {
return Err(anyhow::anyhow!(
r#"block "{}" at "{}:{}" has non-existent ThenChange target "{}:{}""#,
block.name(),
path.display(),
block.end_line(),
file.display(),
target_block,
));
}
}
}
Ok(())
}
fn on_change_blocks(&self) -> HashMap<(&Path, &str), &OnChangeBlock> {
let mut blocks = HashMap::with_capacity(self.num_blocks);
for (path, file) in self.files.iter() {
for block in file.blocks.iter() {
if !block.is_targetable() {
continue;
}
blocks.insert((path.as_path(), block.name()), block);
}
}
blocks
}
fn validate(&self) -> Result<()> {
let blocks = self.on_change_blocks();
for (path, file) in &self.files {
for block in &file.blocks {
match block.then_change() {
ThenChange::NoTarget => {}
ThenChange::Targets(targets) => {
for t in targets {
Self::validate_block_target(&self, path, block, t, &blocks)?;
}
}
ThenChange::Unset => {
return Err(anyhow::anyhow!(
r#"block "{}" in file "{}" has an unset OnChange target (line {})"#,
block.name(),
path.display(),
block.end_line(),
));
}
}
}
}
Ok(())
}
fn validate_root_path<P: AsRef<Path>>(root_path: P) -> Result<()> {
let root_path = root_path.as_ref();
if !root_path.exists() {
Err(anyhow::anyhow!(
"root path {} does not exist",
root_path.display(),
))
} else if !root_path.is_dir() {
Err(anyhow::anyhow!(
"root path {} is not a directory",
root_path.display(),
))
} else {
Ok(())
}
}
fn from_files_internal<P: AsRef<Path>, Q: AsRef<Path>>(
paths: impl Iterator<Item = P>,
root_path: Q,
file_callback: impl Fn(PathBuf, &Path) -> Result<Option<(File, HashSet<PathBuf>)>>,
) -> Result<Self> {
let root_path = root_path.as_ref().canonicalize()?;
let mut files = BTreeMap::new();
Self::validate_root_path(&root_path)?;
let mut file_stack: Vec<PathBuf> = paths
.map(|p| {
let path = p.as_ref();
path.to_owned()
})
.collect();
for path in &file_stack {
let path = root_path.join(path);
if !path.exists() {
return Err(anyhow::anyhow!(
"file with path \"{}\" does not exist",
path.display(),
));
} else if !path.is_file() {
return Err(anyhow::anyhow!("path \"{}\" is not a file", path.display(),));
}
}
let s = std::time::Instant::now();
while let Some(path) = file_stack.pop() {
if let Some((file, files_to_parse)) = file_callback(path.clone(), &root_path)? {
files.insert(path, file);
for file_path in files_to_parse {
if !files.contains_key(&file_path) {
file_stack.push(file_path);
}
}
}
}
let mut num_blocks = 0;
for file in files.values() {
num_blocks += file.blocks.len();
}
log::info!(
"Parsed {} files ({} blocks) in {:?}",
files.len(),
num_blocks,
s.elapsed()
);
Ok(Self {
root_path: root_path.to_owned(),
files,
num_blocks,
})
}
pub fn from_files<P: AsRef<Path>, Q: AsRef<Path>>(
paths: impl Iterator<Item = P>,
root_path: Q,
) -> Result<Self> {
let parser = Self::from_files_internal(paths, root_path, |path, root_path| {
File::parse(path, root_path, None)
})?;
parser.validate()?;
Ok(parser)
}
pub fn from_directory<P: AsRef<Path>>(path: P, ignore: bool) -> Result<Self> {
let root_path = path.as_ref().canonicalize()?;
let mut files = BTreeMap::new();
Self::validate_root_path(&root_path)?;
let s = std::time::Instant::now();
let dir_walker = ignore::WalkBuilder::new(&root_path)
.ignore(ignore)
.git_global(ignore)
.git_ignore(ignore)
.git_exclude(ignore)
.parents(ignore)
.build();
let paths: Vec<PathBuf> = dir_walker
.filter_map(|e| {
let path = e.as_ref().unwrap().path().to_owned();
if !path.is_file() {
None
} else {
Some(path.strip_prefix(&root_path).unwrap().to_owned())
}
})
.collect();
log::info!("Walked {} file paths in {:?}", paths.len(), s.elapsed());
let s = std::time::Instant::now();
let file_items: Vec<_> = paths
.par_iter()
.filter_map(|p| {
if let Some((f, _)) = File::parse(p.to_owned(), &root_path, None).unwrap() {
Some(f)
} else {
None
}
})
.collect();
for f in file_items {
files.insert(f.path.clone(), f);
}
let mut num_blocks = 0;
for (_, f) in &files {
num_blocks += f.blocks.len();
}
log::info!(
"Parsed {} files ({} blocks) in {:?}",
paths.len(),
num_blocks,
s.elapsed()
);
let s = std::time::Instant::now();
let parser = Self {
root_path: root_path.to_owned(),
files,
num_blocks,
};
parser.validate()?;
log::info!("Validated {} blocks in {:?}", num_blocks, s.elapsed());
Ok(parser)
}
pub fn on_change_blocks_in_file<P: AsRef<Path>>(
&self,
path: P,
) -> Option<impl Iterator<Item = &OnChangeBlock>> {
self.files.get(path.as_ref()).map(|file| file.blocks.iter())
}
pub fn get_block_in_file<P: AsRef<Path>>(
&self,
path: P,
block_name: &str,
) -> Option<&OnChangeBlock> {
self.files
.get(path.as_ref())
.and_then(|f| f.blocks.iter().find(|b| b.name() == block_name))
}
pub fn paths(&self) -> impl Iterator<Item = &Path> {
self.files.keys().map(|p| p.as_path())
}
pub fn root_path(&self) -> &Path {
&self.root_path
}
pub fn num_blocks(&self) -> usize {
self.num_blocks
}
}
#[derive(Debug)]
pub struct OnChangeViolation<'a> {
root_path: &'a Path,
block: &'a OnChangeBlock,
target_file: &'a Path,
target_block_name: Option<&'a str>,
}
impl<'a> ToString for OnChangeViolation<'a> {
fn to_string(&self) -> String {
if let Some(target_block_name) = self.target_block_name {
format!(
r#"block "{}" in {} (due to block "{}" at {}:{})"#,
target_block_name,
self.root_path.join(&self.target_file).display(),
self.block.name(),
self.root_path.join(&self.block.file()).display(),
self.block.start_line(),
)
} else {
format!(
r#"file "{}" (due to block "{}" at {}:{})"#,
self.root_path.join(&self.target_file).display(),
self.block.name(),
self.root_path.join(&self.block.file()).display(),
self.block.start_line(),
)
}
}
}
impl Parser {
pub fn from_git_repo<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
let s = std::time::Instant::now();
#[cfg(feature = "git")]
let (staged_files, staged_hunks) = {
let repo = git2::Repository::open(path)?;
(repo.get_staged_files()?, repo.get_staged_hunks()?)
};
#[cfg(not(feature = "git"))]
let (staged_files, staged_hunks) = {
let cli = crate::git::cli::Cli { repo_path: path };
(cli.get_staged_files()?, cli.get_staged_hunks()?)
};
log::info!("Got staged files and hunks in {:?}", s.elapsed());
Self::from_files_internal(staged_files.iter(), path, |path, root_path| {
let hunks = staged_hunks.get(&path).map(|v| v.as_slice());
if let Some(hunks) = hunks {
File::parse(path, root_path, Some(hunks))
} else {
Ok(None)
}
})
}
fn validate_changed_files_and_blocks<'a, 'b>(
&'a self,
files_changed: HashSet<&Path>,
blocks_changed: Vec<&'a OnChangeBlock>,
targetable_blocks_changed: HashSet<(&Path, &'a str)>,
) -> Vec<OnChangeViolation<'a>>
where
'a: 'b,
{
let mut violations = Vec::new();
for block in blocks_changed {
let blocks_to_check = block.get_then_change_targets_as_keys();
for (on_change_file, on_change_block) in blocks_to_check {
if let Some(on_change_block) = on_change_block {
if !targetable_blocks_changed.contains(&(on_change_file, on_change_block)) {
violations.push(OnChangeViolation {
root_path: &self.root_path,
block,
target_file: on_change_file,
target_block_name: Some(on_change_block),
});
}
} else if !files_changed.contains(on_change_file) {
violations.push(OnChangeViolation {
root_path: &self.root_path,
block,
target_file: on_change_file,
target_block_name: None,
});
}
}
}
violations
}
pub fn validate_git_repo(&self) -> Result<Vec<OnChangeViolation<'_>>> {
let path = self.root_path.as_path();
if self.files.len() == 0 {
return Ok(Vec::new());
}
#[cfg(feature = "git")]
let staged_files = {
let repo = git2::Repository::open(path)?;
repo.get_staged_files()?
};
#[cfg(not(feature = "git"))]
let staged_files = {
let cli = crate::git::cli::Cli { repo_path: path };
cli.get_staged_files()?
};
let s = std::time::Instant::now();
let files_changed: HashSet<&Path> =
HashSet::from_iter(staged_files.iter().map(|p| p.as_path()));
let mut blocks_changed: Vec<&OnChangeBlock> = Vec::new();
let mut targetable_blocks_changed: HashSet<(&Path, &str)> = HashSet::new();
for path in &staged_files {
let changed_blocks: Vec<&OnChangeBlock> =
if let Some(blocks) = self.on_change_blocks_in_file(path) {
blocks.collect()
} else {
continue;
};
for block in changed_blocks {
blocks_changed.push(block);
if block.is_targetable() {
targetable_blocks_changed.insert((&path, block.name()));
}
}
}
log::info!("Found changed blocks in {:?}", s.elapsed());
let s = std::time::Instant::now();
let violations = self.validate_changed_files_and_blocks(
files_changed,
blocks_changed,
targetable_blocks_changed,
);
log::info!("Validated changed files and blocks in {:?}", s.elapsed());
Ok(violations)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::test_helpers::*;
use indoc::indoc;
fn parse_and_validate(path: &Path, num_violations: usize) {
let p = Parser::from_git_repo(path).unwrap();
assert_eq!(p.validate_git_repo().unwrap().len(), num_violations);
}
#[test]
fn test_from_directory() {
let files = &[
(
"f1.txt",
"LINT.OnChange(default)\n
abdbbda\nadadd\n
LINT.ThenChange(f2.txt:other)",
),
(
"f2.txt",
"LINT.OnChange(other)\n
LINT.ThenChange(f1.txt:default)",
),
];
let d = TestDir::from_files(files);
Parser::from_directory(d.path(), false).unwrap();
}
#[test]
fn test_from_files() {
let files = &[
(
"f1.txt",
"LINT.OnChange()\n
abdbbda\n
adadd\n
LINT.ThenChange(f2.txt:other)",
),
(
"f2.txt",
"LINT.OnChange(other)\n
LINT.ThenChange()",
),
];
let d = TestDir::from_files(files);
let file_names = files.iter().map(|f| f.0);
Parser::from_files(file_names, d.path()).unwrap();
}
#[test]
fn test_from_files_file_target() {
let files = &[
(
"f1.txt",
"LINT.OnChange()\n
abdbbda\nadadd\n
LINT.ThenChange(f2.txt:other)",
),
(
"f2.txt",
"LINT.OnChange(other)\n
LINT.ThenChange()",
),
(
"f3.txt",
"LINT.OnChange(this)\n
LINT.ThenChange(f1.txt)",
),
];
let d = TestDir::from_files(files);
let file_names = files.iter().map(|f| f.0);
Parser::from_files(file_names, d.path()).unwrap();
}
#[test]
fn test_from_files_file_target_parsed() {
let files = &[
(
"f1.txt",
"LINT.OnChange()\n
abdbbda\nadadd\n
LINT.ThenChange(f2.txt:other)",
),
(
"f2.txt",
"LINT.OnChange(other)\n
LINT.ThenChange()",
),
(
"f3.txt",
"LINT.OnChange(this)\n
LINT.ThenChange(f1.txt)",
),
];
let d = TestDir::from_files(files);
Parser::from_files(std::iter::once("f3.txt"), d.path()).unwrap();
}
#[test]
fn test_from_files_relative_target() {
let files = &[
(
"abc/f1.txt",
"LINT.OnChange()\n
abdbbda\nadadd\n
LINT.ThenChange(../f2.txt:other)",
),
(
"f2.txt",
"LINT.OnChange(other)\n
LINT.ThenChange()",
),
];
let d = TestDir::from_files(files);
let file_names = files.iter().map(|f| f.0);
Parser::from_files(file_names, d.path()).unwrap();
}
#[test]
fn test_from_files_invalid_block_target_file_path() {
let files = &[
(
"f1.txt",
"LINT.OnChange(default)\n
abdbbda\nadadd\n
LINT.ThenChange(f3.txt:default)",
),
(
"f2.txt",
"LINT.OnChange(default)\n
LINT.ThenChange(f1.txt:default)",
),
];
let d = TestDir::from_files(files);
let file_names = files.iter().map(|f| f.0);
let res = Parser::from_files(file_names, d.path());
assert!(res.is_err());
let err = res.unwrap_err().to_string();
assert_eq!(
err,
r#"ThenChange target file "f3.txt" at f1.txt:6 does not exist"#
);
}
#[test]
fn test_from_files_invalid_block_target() {
let files = &[
(
"f1.txt",
"LINT.OnChange(default)\n
abdbbda\nadadd\n
LINT.ThenChange(f2.txt:invalid)",
),
(
"f2.txt",
"LINT.OnChange(default)\n
LINT.ThenChange(f1.txt:default)",
),
];
let d = TestDir::from_files(files);
let file_names = files.iter().map(|f| f.0);
let res = Parser::from_files(file_names, d.path());
assert!(res.is_err());
let err = res.unwrap_err().to_string();
assert_eq!(
err,
r#"block "default" at "f1.txt:6" has non-existent ThenChange target "f2.txt:invalid""#,
);
}
#[test]
fn test_from_files_duplicate_block_in_file() {
let files = &[(
"f1.txt",
"LINT.OnChange(default)\n
abdbbda\nadadd\n
LINT.ThenChange(:other)
LINT.OnChange(default)\n
abdbbda\n
LINT.ThenChange(:other)
LINT.OnChange(other)\n
abdbbda\nadadd\n
LINT.ThenChange(:default)",
)];
let d = TestDir::from_files(files);
let file_names = files.iter().map(|f| f.0);
let res = Parser::from_files(file_names, d.path());
assert!(res.is_err());
let err = res.unwrap_err().to_string();
assert_eq!(
err,
r#"duplicate block name "default" found on f1.txt:1 and f1.txt:7"#,
);
}
#[test]
fn test_from_files_nested_on_change() {
let files = &[(
"f1.txt",
"LINT.OnChange(default)\n
abdbbda\nadadd\n
LINT.OnChange(other)\n
LINT.ThenChange(:other)\n
LINT.ThenChange()",
)];
let d = TestDir::from_files(files);
let file_names = files.iter().map(|f| f.0);
Parser::from_files(file_names, d.path()).unwrap();
}
#[test]
fn test_from_files_nested_on_change_unbalanced() {
let files = &[(
"f1.txt",
"LINT.OnChange(default)\n
abdbbda\nadadd\n
LINT.OnChange(other)\n
LINT.ThenChange(:other)",
)];
let d = TestDir::from_files(files);
let file_names = files.iter().map(|f| f.0);
let res = Parser::from_files(file_names, d.path());
assert!(res.is_err());
let err = res.unwrap_err().to_string();
assert!(err.contains(
"while looking for ThenChange for block \"default\" which started on line 1"
));
}
#[test]
fn test_from_files_eof_in_block() {
let files = &[("f1.txt", "LINT.OnChange(default)\nabdbbda\nadadd\n")];
let d = TestDir::from_files(files);
let file_names = files.iter().map(|f| f.0);
let res = Parser::from_files(file_names, d.path());
assert!(res.is_err());
let err = res.unwrap_err().to_string();
assert!(err.contains("reached end of file"));
}
#[test]
fn test_from_directory_with_code() {
let files = &[
(
"f1.html",
indoc! {"
<html>
<body>
<!-- LINT.OnChange(html) -->
<h1>Hello</h1>
<!-- LINT.ThenChange(f2.cpp:cpp) -->
<p>abc</p>
<!-- LINT.OnChange(other-html) -->
<p>def</p>
<!-- LINT.ThenChange() -->
</body>
</html>
"},
),
(
"f2.cpp",
indoc! {r#"
class A {
A() {
// LINT.OnChange()
int a = 10;
// LINT.ThenChange(abc/f3.py:python)
}
}
int main() {
// LINT.OnChange(cpp)
printf("Hello, world!\n);
// LINT.ThenChange(f1.html:html)
}
"#},
),
(
"abc/f3.py",
indoc! {r#"
class A:
def __init__(self):
# LINT.OnChange(python)
self.v = 10
# LINT.ThenChange(../f1.html)
if __name__ == "__main__":
print(A())
"#},
),
];
let d = TestDir::from_files(files);
Parser::from_directory(d.path(), true).unwrap();
}
#[test]
fn test_from_git_repo() {
let files = &[
(
"f1.txt",
"LINT.OnChange(default)\n
abdbbda\nadadd\n
LINT.ThenChange(f2.txt:default)\n",
),
(
"f2.txt",
"LINT.OnChange(default)\n
LINT.ThenChange(f1.txt:default)\n",
),
(
"f3.txt",
"LINT.OnChange(this)\n
LINT.ThenChange(f1.txt)\n",
),
];
let d = GitRepo::from_files(files);
d.write_and_add_files(&[(
"f1.txt",
"LINT.OnChange(default)\n
adadd\n
LINT.ThenChange(f2.txt:default)\n",
)]);
parse_and_validate(d.path(), 1);
d.write_and_add_files(&[(
"f2.txt",
"LINT.OnChange(default)\n
adadd\n
LINT.ThenChange(f1.txt:default)\n",
)]);
parse_and_validate(d.path(), 0);
d.write_and_add_files(&[(
"f3.txt",
"LINT.OnChange(this)\n
abcde\n
LINT.ThenChange(f1.txt)\n",
)]);
parse_and_validate(d.path(), 0);
}
#[test]
fn test_from_git_repo_relative_path_priority() {
let files = &[
(
"f1.txt",
"LINT.OnChange(default)\n
abdbbda\nadadd\n
LINT.ThenChange(f2.txt:default)\n",
),
(
"f2.txt",
"LINT.OnChange(default)\n
LINT.ThenChange(abc/f1.txt:default)\n",
),
(
"abc/f1.txt",
"LINT.OnChange(default)\n
abdbbda\nadadd\n
LINT.ThenChange(f2.txt:default)\n",
),
(
"abc/f2.txt",
"LINT.OnChange(default)\n
LINT.ThenChange(f1.txt:default)\n",
),
];
let d = GitRepo::from_files(files);
d.write_and_add_files(&[
(
"abc/f1.txt",
"LINT.OnChange(default)\n
adadd\n
LINT.ThenChange(f2.txt:default)\n",
),
(
"f2.txt",
"LINT.OnChange(default)\n
adadd\n
LINT.ThenChange(abc/f1.txt:default)\n",
),
]);
parse_and_validate(d.path(), 1);
d.write_and_add_files(&[(
"abc/f2.txt",
"LINT.OnChange(default)\n
abc\n
LINT.ThenChange(f1.txt:default)\n",
)]);
parse_and_validate(d.path(), 0);
}
#[test]
fn test_from_git_repo_multiple_blocks_in_file() {
let files = &[
(
"f1.txt",
indoc! {"
LINT.OnChange(default)\n
abdbbda\nadadd\n
LINT.ThenChange(f2.txt:default)\n
some\ntext\t\there\n
LINT.OnChange()\n
abdbbda\nadadd\n
LINT.ThenChange(f2.txt:other)\n
"},
),
(
"f2.txt",
indoc! {"
LINT.OnChange(default)\n
LINT.ThenChange(f1.txt:default)\n
LINT.OnChange(other)\n
LINT.ThenChange(f1.txt:default)\n
"},
),
];
let d = GitRepo::from_files(files);
d.write_and_add_files(&[(
"f1.txt",
indoc! {"
LINT.OnChange(default)\n
abdbbda\nadadd\n
LINT.ThenChange(f2.txt:default)\n
LINT.OnChange()\n
abdbbda\nadadd\n
LINT.ThenChange(f2.txt:other)\n
"},
)]);
parse_and_validate(d.path(), 0);
d.write_and_add_files(&[(
"f1.txt",
indoc! {"
LINT.OnChange(default)\n
abdbbda\n
LINT.ThenChange(f2.txt:default)\n
LINT.OnChange()\n
abdbbda\n
LINT.ThenChange(f2.txt:other)\n
"},
)]);
parse_and_validate(d.path(), 2);
d.write_and_add_files(&[(
"f2.txt",
indoc! {"
LINT.OnChange(default)\n
abba\n
LINT.ThenChange(f1.txt:default)\n
LINT.OnChange(other)\n
LINT.ThenChange(f1.txt:default)\n
"},
)]);
parse_and_validate(d.path(), 1);
d.write_and_add_files(&[(
"f2.txt",
indoc! {"
LINT.OnChange(default)\n
abba\n
LINT.ThenChange(f1.txt:default)\n
LINT.OnChange(other)\n
abba\n
LINT.ThenChange(f1.txt:default)\n
"},
)]);
parse_and_validate(d.path(), 0);
}
#[test]
fn test_from_git_repo_multiple_targets() {
let files = &[
(
"f1.txt",
"LINT.OnChange(default)\n
abdbbda\nadadd\n
LINT.ThenChange(f2.txt:potato)\n",
),
(
"f2.txt",
"LINT.OnChange(potato)\n
LINT.ThenChange(f1.txt:default)\n",
),
(
"f3.txt",
"LINT.OnChange()\n
LINT.ThenChange(f1.txt:default, f2.txt:potato, f4.txt:something)\n",
),
(
"f4.txt",
"LINT.OnChange(something)\n
LINT.ThenChange(f1.txt:default, f2.txt:potato)\n",
),
];
let d = GitRepo::from_files(files);
d.write_and_add_files(&[(
"f3.txt",
"LINT.OnChange()\n
hello,there!\n
LINT.ThenChange(f1.txt:default, f2.txt:potato, f4.txt:something)\n",
)]);
parse_and_validate(d.path(), 3);
d.write_and_add_files(&[
(
"f1.txt",
"LINT.OnChange(default)\n
adadd\n
LINT.ThenChange(f2.txt:potato)\n",
),
(
"f2.txt",
"LINT.OnChange(potato)\n
adadd\n
LINT.ThenChange(f1.txt:default)\n",
),
(
"f4.txt",
"LINT.OnChange(something)\n
newlinehere\n\n
LINT.ThenChange(f1.txt:default, f2.txt:potato)\n",
),
]);
parse_and_validate(d.path(), 0);
}
#[test]
fn test_from_git_repo_multiple_target_files() {
let files = &[
(
"f1.txt",
"LINT.OnChange(default)\n
abdbbda\nadadd\n
LINT.ThenChange(f2.txt)\n",
),
(
"f2.txt",
"LINT.OnChange(potato)\n
LINT.ThenChange(f1.txt)\n",
),
(
"f3.txt",
"LINT.OnChange()\n
LINT.ThenChange(f1.txt, f2.txt, f4.txt)\n",
),
(
"f4.txt",
"LINT.OnChange(something)\n
LINT.ThenChange(f1.txt, f2.txt)\n",
),
];
let d = GitRepo::from_files(files);
d.write_and_add_files(&[(
"f3.txt",
"LINT.OnChange()\n
hello,there!\n
LINT.ThenChange(f1.txt, f2.txt, f4.txt)\n",
)]);
parse_and_validate(d.path(), 3);
d.write_and_add_files(&[
(
"f1.txt",
"LINT.OnChange(default)\n
adadd\n
LINT.ThenChange(f2.txt)\n",
),
(
"f2.txt",
"LINT.OnChange(potato)\n
adadd\n
LINT.ThenChange(f1.txt)\n",
),
(
"f4.txt",
"LINT.OnChange(something)\n
newlinehere\n\n
LINT.ThenChange(f1.txt, f2.txt)\n",
),
]);
parse_and_validate(d.path(), 0);
}
#[test]
fn test_from_git_repo_nested_blocks() {
let files = &[
(
"f1.txt",
"LINT.OnChange(outer)
\nabdbbda\nadadd
\n
LINT.OnChange(inner)\n
bbbb\n
LINT.ThenChange(f2.txt:first)\n
\n
LINT.ThenChange()\n",
),
(
"f2.txt",
"LINT.OnChange(first)\n
LINT.ThenChange(f1.txt:inner)\n
LINT.OnChange(second)\n
LINT.ThenChange()\n",
),
];
let d = GitRepo::from_files(files);
d.write_and_add_files(&[
(
"f1.txt",
"LINT.OnChange(outer)
\nabdbbda\nadadd
\n
LINT.OnChange(inner)\n
LINT.ThenChange(f2.txt:first)\n
\n
LINT.ThenChange()\n",
),
(
"f2.txt",
"LINT.OnChange(first)\naaaa\nLINT.ThenChange(f1.txt:inner)\n
LINT.OnChange(second)\nLINT.ThenChange()\n",
),
]);
parse_and_validate(d.path(), 0);
}
#[test]
fn test_from_git_repo_single_line_block() {
let files = &[
(
"f1.txt",
"LINT.OnChange(default)\n aaa LINT.ThenChange(f2.txt:first)\n",
),
(
"f2.txt",
indoc! {"
LINT.OnChange(first)\n
LINT.ThenChange(f1.txt:default)\n
"},
),
];
let d = GitRepo::from_files(files);
d.write_and_add_files(&[(
"f1.txt",
"LINT.OnChange(default)\n aa LINT.ThenChange(f2.txt:first)\n",
)]);
parse_and_validate(d.path(), 1);
d.write_and_add_files(&[(
"f2.txt",
indoc! {"
LINT.OnChange(first)\n
aaaaaa
LINT.ThenChange(f1.txt:default)\n
"},
)]);
parse_and_validate(d.path(), 0);
}
#[test]
fn test_validate_empty_git_repo() {
let d = GitRepo::new();
parse_and_validate(d.path(), 0);
}
#[test]
fn test_from_git_repo_unstaged_block_parsed() {
let files = &[
(
"f1.txt",
indoc! {"
LINT.OnChange(outer)
LINT.ThenChange(f2.txt:first)\n
"},
),
(
"f2.txt",
indoc! {"
LINT.OnChange(first)\n
LINT.ThenChange(f3.txt:second)\n
"},
),
(
"f3.txt",
indoc! {"
LINT.OnChange(second)\n
LINT.ThenChange()\n
"},
),
];
let d = GitRepo::from_files(files);
d.write_and_add_files(&[(
"f1.txt",
indoc! {"
LINT.OnChange(outer)
HELLO
LINT.ThenChange(f2.txt:first)\n
"},
)]);
parse_and_validate(d.path(), 1);
}
}