use std::{
collections::{hash_map::IntoIter, HashMap, HashSet},
path::PathBuf,
sync::atomic::Ordering,
};
use crate::{
references::ReferencesError,
wiki::{
ref_list::{ProjectLine, RefCntKind, RefListEntry},
req::{Req, ReqId},
Wiki, WikiReq,
},
};
use super::ReferencesMap;
#[derive(Debug)]
pub struct ReferenceChanges {
new_cnt_map: HashMap<ReqId, RefCntKind>,
implicits_cnt_map: HashMap<ReqId, RefCntKind>,
file_changes: HashMap<PathBuf, Vec<Req>>,
proj_line: ProjectLine,
}
impl ReferenceChanges {
pub fn new(
proj_line: ProjectLine,
wiki: &Wiki,
ref_map: &ReferencesMap,
) -> Result<Self, ReferencesError> {
let mut changes = ReferenceChanges {
new_cnt_map: HashMap::new(),
implicits_cnt_map: HashMap::new(),
file_changes: HashMap::new(),
proj_line,
};
changes.update_cnts(wiki, ref_map)?;
Ok(changes)
}
pub fn cnt_changes(self) -> IntoIter<ReqId, RefCntKind> {
self.new_cnt_map.into_iter()
}
pub fn ordered_file_changes(&self) -> Vec<(&PathBuf, Vec<Req>)> {
let mut ordered_file_changes = Vec::with_capacity(self.file_changes.len());
for (filepath, changes) in self.file_changes.iter() {
let mut ordered_changes = changes.clone();
ordered_changes.sort_by(|a, b| a.line_nr.cmp(&b.line_nr));
for req in ordered_changes.iter_mut() {
let req_id = &req.head.id;
let new_cnt_kind = self
.new_cnt_map
.get(req_id)
.expect("Changed requirement had no new counter in `new_cnt_map`.")
.to_owned();
match req.ref_list.iter_mut().find(|entry| {
entry.proj_line.branch_name == self.proj_line.branch_name
&& entry.proj_line.repo_name == self.proj_line.repo_name
}) {
Some(entry) => entry.ref_cnt = new_cnt_kind,
None => req.ref_list.push(RefListEntry {
proj_line: self.proj_line.clone(),
ref_cnt: new_cnt_kind,
is_manual: false,
is_deprecated: false,
}),
}
}
ordered_file_changes.push((filepath, ordered_changes));
}
ordered_file_changes
}
fn update_cnts(&mut self, wiki: &Wiki, ref_map: &ReferencesMap) -> Result<(), ReferencesError> {
let flat_wiki = wiki.flatten();
for req in flat_wiki {
let req_id = req.req_id().clone();
let new_direct_cnt = ref_map
.map
.get(&req_id)
.map(|atomic_cnt| atomic_cnt.load(Ordering::Relaxed))
.unwrap_or_default();
let new_cnt_kind = match wiki.sub_reqs(&req_id) {
Some(sub_reqs) => {
let sub_cnt = self.sub_ref_cnts(sub_reqs, wiki);
if new_direct_cnt == 0 && sub_cnt == 0 {
RefCntKind::Untraced
} else {
RefCntKind::HighLvl {
direct_cnt: new_direct_cnt,
sub_cnt,
}
}
}
None => {
if new_direct_cnt == 0 {
RefCntKind::Untraced
} else {
RefCntKind::LowLvl {
cnt: new_direct_cnt,
}
}
}
};
match req {
WikiReq::Explicit { req: explicit_req } => {
let kind_changed = match wiki.req_ref_entry(
&req_id,
self.proj_line.repo_name.as_ref().map(|r| r.as_str()),
&self.proj_line.branch_name,
) {
Some(req_entry) => {
if req_entry.is_deprecated && new_cnt_kind != RefCntKind::Untraced {
let err = logid::err!(ReferencesError::DeprecatedReqReferenced {
req_id: req_id.clone(),
repo_name: self
.proj_line
.repo_name
.as_ref()
.map(|s| s.to_string()),
branch_name: self.proj_line.branch_name.to_string()
});
if crate::globals::early_exit() {
return err;
} else {
continue;
}
}
req_entry.ref_cnt != new_cnt_kind
}
None => new_cnt_kind != RefCntKind::Untraced,
};
if kind_changed {
self.new_cnt_map.insert(req_id, new_cnt_kind);
self.file_changes
.entry(explicit_req.filepath.clone())
.or_insert(Vec::new())
.push(explicit_req);
}
}
WikiReq::Implicit {
req_id: implicit_req_id,
} => {
self.implicits_cnt_map.insert(implicit_req_id, new_cnt_kind);
}
}
}
Ok(())
}
fn sub_ref_cnts(&mut self, sub_reqs: &HashSet<ReqId>, wiki: &Wiki) -> usize {
let mut sub_cnt = 0;
for sub_req in sub_reqs {
let opt_ref_entry = wiki.req_ref_entry(
sub_req,
self.proj_line.repo_name.as_ref().map(|r| r.as_str()),
&self.proj_line.branch_name,
);
let mut sub_cnt_kind = self.new_cnt_map.get(sub_req).copied().unwrap_or_else(|| {
match opt_ref_entry {
Some(entry) => entry.ref_cnt,
None => {
if wiki.is_implicit(sub_req) {
*self
.implicits_cnt_map
.get(sub_req)
.expect("Implicit requirement not in implicit cnt-map.")
} else {
RefCntKind::Untraced
}
}
}
});
if opt_ref_entry.map_or_else(|| false, |entry| entry.is_manual) {
sub_cnt_kind = match sub_cnt_kind {
RefCntKind::HighLvl {
direct_cnt,
sub_cnt,
} => RefCntKind::HighLvl {
direct_cnt: direct_cnt + 1,
sub_cnt,
},
RefCntKind::LowLvl { cnt } => RefCntKind::LowLvl { cnt: cnt + 1 },
RefCntKind::Untraced => RefCntKind::LowLvl { cnt: 1 },
};
}
sub_cnt += match sub_cnt_kind {
RefCntKind::HighLvl {
direct_cnt,
sub_cnt,
} => direct_cnt + sub_cnt,
RefCntKind::LowLvl { cnt } => cnt,
RefCntKind::Untraced => 0,
}
}
sub_cnt
}
}
#[cfg(test)]
mod test {
use super::*;
fn setup_wiki() -> Wiki {
let filename = "test_wiki";
let content = r#"
# ref_req: Some Title
**References:**
- in branch main: 2
## ref_req.test: Some Title
**References:**
- in branch main: 1
"#;
Wiki::try_from((PathBuf::from(filename), content)).unwrap()
}
fn setup_references(wiki: &Wiki) -> ReferencesMap {
let filename = "test_file";
let content = "[req:ref_req][req:ref_req.test]";
let ref_map = ReferencesMap::with(&mut wiki.req_ids());
ref_map.trace(&PathBuf::from(filename), content).unwrap();
ref_map
}
#[test]
fn high_lvl_cnt_changed_low_lvl_unchanged() {
let wiki = setup_wiki();
let ref_map = setup_references(&wiki);
let branch_name = String::from("main");
let changes =
ReferenceChanges::new(ProjectLine::new(None, branch_name, None), &wiki, &ref_map)
.unwrap();
assert_eq!(
changes.new_cnt_map.len(),
1,
"More than one reference counter changed."
);
let new_cnt = changes
.new_cnt_map
.get("ref_req")
.expect("High-level requirement did not change.");
assert_eq!(
new_cnt,
&RefCntKind::HighLvl {
direct_cnt: 1,
sub_cnt: 1
},
);
}
fn setup_partial_referenced_wiki() -> Wiki {
let filename = "test_wiki";
let content = r#"
# ref_req: Some Title
**References:**
- in branch main: 2 (1 direct)
## ref_req.test: Some Title
"#;
Wiki::try_from((PathBuf::from(filename), content)).unwrap()
}
#[test]
fn branch_link_updated_for_new_ref_entries() {
let wiki = setup_partial_referenced_wiki();
let ref_map = setup_references(&wiki);
let branch_name = String::from("main");
let branch_link = String::from("https://github.com/mhatzl/mantra/tree/main");
let changes = ReferenceChanges::new(
ProjectLine::new(None, branch_name, Some(branch_link.clone())),
&wiki,
&ref_map,
)
.unwrap();
assert_eq!(
changes.new_cnt_map.len(),
1,
"More than one reference counter changed."
);
let file_changes = changes.ordered_file_changes();
let test_file_changes = &file_changes[0].1;
assert_eq!(
test_file_changes.len(),
1,
"More than one requirement reference changed."
);
let low_lvl_req = &test_file_changes[0];
assert_eq!(
low_lvl_req.head.id, "ref_req.test",
"Wrong requirement Id changed."
);
assert_eq!(
low_lvl_req.ref_list.len(),
1,
"More than one ref entry created."
);
let ref_entry = &low_lvl_req.ref_list[0];
assert_eq!(
ref_entry.proj_line.branch_link,
Some(branch_link.into()),
"Branch link was not added to new ref entry."
);
}
fn setup_deprecated_wiki() -> Wiki {
let filename = "test_wiki";
let content = r#"
# ref_req: Some Title
**References:**
- in branch main: deprecated
"#;
Wiki::try_from((PathBuf::from(filename), content)).unwrap()
}
fn setup_deprecated_references(wiki: &Wiki) -> ReferencesMap {
let filename = "test_file";
let content = "[req:ref_req]";
let ref_map = ReferencesMap::with(&mut wiki.req_ids());
ref_map.trace(&PathBuf::from(filename), content).unwrap();
ref_map
}
#[test]
fn deprecated_req_referenced() {
let wiki = setup_deprecated_wiki();
let ref_map = setup_deprecated_references(&wiki);
let branch_name = String::from("main");
let branch_link = String::from("https://github.com/mhatzl/mantra/tree/main");
let changes = ReferenceChanges::new(
ProjectLine::new(None, branch_name, Some(branch_link.clone())),
&wiki,
&ref_map,
);
assert!(
changes.is_err(),
"Referencing deprecated requirement did not result in error."
);
}
fn setup_manual_verified_wiki() -> Wiki {
let filename = "test_wiki";
let content = r#"
# ref_req: Some Title
**References:**
- in branch main: 1
## ref_req.test: Some Title
**References:**
- in branch main: manual
"#;
Wiki::try_from((PathBuf::from(filename), content)).unwrap()
}
#[test]
fn manual_flag_for_low_lvl_ref_entry() {
let wiki = setup_manual_verified_wiki();
let ref_map = setup_references(&wiki);
let branch_name = String::from("main");
let branch_link = String::from("https://github.com/mhatzl/mantra/tree/main");
let changes = ReferenceChanges::new(
ProjectLine::new(None, branch_name, Some(branch_link.clone())),
&wiki,
&ref_map,
)
.unwrap();
let file_changes = changes.ordered_file_changes();
let test_file_changes = &file_changes[0].1;
let high_lvl_ref_entry = &test_file_changes[0].ref_list[0];
assert_eq!(
high_lvl_ref_entry.to_string(),
"- in branch main: 3 (1 direct)",
"*manual* flag not correctly counted in parent requirement."
);
let low_lvl_ref_entry = &test_file_changes[1].ref_list[0];
assert_eq!(
low_lvl_ref_entry.to_string(),
"- in branch main: manual + 1",
"*manual* flag + reference not correctly displayed."
);
}
}