use super::{mkid, mkrepo, revs};
use crate::messages::{RevSet, StoreRef, queries::RevsResult};
use crate::worker::{WorkerSession, queries};
use anyhow::Result;
use assert_matches::assert_matches;
use std::collections::HashSet;
#[tokio::test]
async fn log_all() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let all_rows = queries::query_log(&ws, "all()", 100)?;
assert_eq!(24, all_rows.rows.len());
assert!(!all_rows.has_more);
Ok(())
}
#[tokio::test]
async fn log_paged() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let page_rows = queries::query_log(&ws, "all()", 6)?;
assert_eq!(6, page_rows.rows.len());
assert!(page_rows.has_more);
Ok(())
}
#[tokio::test]
async fn log_subset() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let several_rows = queries::query_log(&ws, "bookmarks()", 100)?;
assert_eq!(4, several_rows.rows.len());
Ok(())
}
#[tokio::test]
async fn log_mutable() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let single_row = queries::query_log(&ws, "wnpusytq", 100)?
.rows
.pop()
.unwrap();
assert!(!single_row.revision.is_immutable);
Ok(())
}
#[tokio::test]
async fn log_immutable() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let single_row = queries::query_log(&ws, "ywknyuol", 100)?
.rows
.pop()
.unwrap();
assert!(single_row.revision.is_immutable);
Ok(())
}
#[tokio::test]
async fn revision() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let header = queries::query_revision(&ws, &revs::main_bookmark())?.expect("revision exists");
assert_matches!(
header.refs.as_slice(),
[StoreRef::LocalBookmark { bookmark_name, .. }] if bookmark_name == "main"
);
Ok(())
}
#[tokio::test]
async fn revision_with_conflict() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let id = revs::conflict_bookmark();
let result = queries::query_revisions(
&ws,
RevSet {
from: id.clone(),
to: id,
},
)
.await?;
let RevsResult::Detail {
headers, conflicts, ..
} = result
else {
panic!("Expected RevsResult::Detail");
};
let header = headers.last().expect("at least one header");
assert!(
header.has_conflict,
"Expected header.has_conflict to be true"
);
assert!(!conflicts.is_empty(), "Expected conflicts to be non-empty");
let conflict_lines: String = conflicts
.iter()
.flat_map(|c| &c.hunk.lines.lines)
.cloned()
.collect::<Vec<_>>()
.join("\n");
assert!(
conflict_lines.contains("<<<<<<<") && conflict_lines.contains(">>>>>>>"),
"Expected conflict markers in conflict hunks, got: {conflict_lines}"
);
Ok(())
}
#[tokio::test]
async fn conflicted_paths_are_not_duplicated() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let result = queries::query_revisions(
&ws,
RevSet {
from: revs::hunk_source(),
to: revs::inherited_conflict(),
},
)
.await?;
let RevsResult::Detail {
changes, conflicts, ..
} = result
else {
panic!("Expected RevsResult::Detail");
};
let conflict_paths: HashSet<String> = conflicts
.into_iter()
.map(|conflict| conflict.path.repo_path)
.collect();
let duplicated_paths: Vec<String> = changes
.into_iter()
.map(|change| change.path.repo_path)
.filter(|path| conflict_paths.contains(path))
.collect();
assert!(
duplicated_paths.is_empty(),
"Expected conflicted paths to appear once, duplicates: {:?}",
duplicated_paths
);
Ok(())
}
#[tokio::test]
async fn revision_without_conflict() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let id = revs::main_bookmark();
let result = queries::query_revisions(
&ws,
RevSet {
from: id.clone(),
to: id,
},
)
.await?;
let RevsResult::Detail {
headers, conflicts, ..
} = result
else {
panic!("Expected RevsResult::Detail");
};
let header = headers.last().expect("at least one header");
assert!(
!header.has_conflict,
"Expected header.has_conflict to be false"
);
assert!(
conflicts.is_empty(),
"Expected conflicts to be empty for non-conflicted revision"
);
Ok(())
}
#[tokio::test]
async fn revision_resolves_conflict() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let id = revs::resolve_conflict();
let result = queries::query_revisions(
&ws,
RevSet {
from: id.clone(),
to: id,
},
)
.await?;
let RevsResult::Detail {
headers,
changes,
conflicts,
..
} = result
else {
panic!("Expected RevsResult::Detail");
};
let header = headers.last().expect("at least one header");
assert!(
!header.has_conflict,
"Expected header.has_conflict to be false for resolved commit"
);
assert!(
conflicts.is_empty(),
"Expected conflicts to be empty for resolved commit, got {} conflicts",
conflicts.len()
);
assert!(!changes.is_empty(), "Expected at least one change");
let all_lines: String = changes
.iter()
.flat_map(|c| &c.hunks)
.flat_map(|h| &h.lines.lines)
.cloned()
.collect::<Vec<_>>()
.join("\n");
assert!(
all_lines.contains("-<<<<<<<") || all_lines.contains("->>>>>>>"),
"Expected removed conflict markers in diff (showing resolution), got:\n{all_lines}"
);
Ok(())
}
#[tokio::test]
async fn revisions_nonexistent_range_returns_not_found() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let nonexistent_set = RevSet {
from: mkid("aaaaaaaa", "0000000000000000000000000000000000000000"),
to: mkid("bbbbbbbb", "1111111111111111111111111111111111111111"),
};
let result = queries::query_revisions(&ws, nonexistent_set).await?;
assert_matches!(
result,
RevsResult::NotFound { .. },
"Querying non-existent range should return NotFound"
);
Ok(())
}
#[tokio::test]
async fn merge_introduces_conflict() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let parent_id = revs::hunk_source(); let parent_result = queries::query_revisions(
&ws,
RevSet {
from: parent_id.clone(),
to: parent_id,
},
)
.await?;
let RevsResult::Detail {
headers: parent_headers,
conflicts: parent_conflicts,
..
} = parent_result
else {
panic!("Expected RevsResult::Detail for parent");
};
let parent_header = parent_headers.last().expect("at least one header");
assert!(
!parent_header.has_conflict,
"Parent commit should not have conflict"
);
assert!(
parent_conflicts.is_empty(),
"Parent commit should have empty conflicts list"
);
let merge_id = revs::conflict_bookmark();
let merge_result = queries::query_revisions(
&ws,
RevSet {
from: merge_id.clone(),
to: merge_id,
},
)
.await?;
let RevsResult::Detail {
headers: merge_headers,
changes,
conflicts,
..
} = merge_result
else {
panic!("Expected RevsResult::Detail for merge");
};
let merge_header = merge_headers.last().expect("at least one header");
assert!(
merge_header.has_conflict,
"Merge commit should have conflict"
);
assert!(
!conflicts.is_empty(),
"Merge commit should have non-empty conflicts"
);
let all_change_lines: String = changes
.iter()
.flat_map(|c| &c.hunks)
.flat_map(|h| &h.lines.lines)
.cloned()
.collect::<Vec<_>>()
.join("\n");
let has_conflict_in_before = all_change_lines.contains("-<<<<<<<")
|| all_change_lines.contains("->>>>>>>")
|| all_change_lines.contains("-+++++++")
|| all_change_lines.contains("--------");
assert!(
!has_conflict_in_before,
"Parent trees should not have conflict markers in diff:\n{all_change_lines}"
);
Ok(())
}
#[tokio::test]
async fn inherited_conflict_persists() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let child_id = revs::inherited_conflict();
let result = queries::query_revisions(
&ws,
RevSet {
from: child_id.clone(),
to: child_id,
},
)
.await?;
let RevsResult::Detail {
headers,
changes,
conflicts,
..
} = result
else {
panic!("Expected RevsResult::Detail");
};
let header = headers.last().expect("at least one header");
assert!(
header.has_conflict,
"Expected header.has_conflict to be true for inherited conflict"
);
assert!(
!conflicts.is_empty(),
"Expected conflicts to be non-empty for inherited conflict"
);
let conflict_lines: String = conflicts
.iter()
.flat_map(|c| &c.hunk.lines.lines)
.cloned()
.collect::<Vec<_>>()
.join("\n");
assert!(
conflict_lines.contains("<<<<<<<") && conflict_lines.contains(">>>>>>>"),
"Expected conflict markers in inherited conflict, got: {conflict_lines}"
);
let has_unrelated_change = changes
.iter()
.any(|c| c.path.repo_path.ends_with("unrelated.txt"));
assert!(
has_unrelated_change,
"Expected change for unrelated.txt in inherited conflict commit"
);
Ok(())
}
#[tokio::test]
async fn range_through_inherited_conflict() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let result = queries::query_revisions(
&ws,
RevSet {
from: revs::conflict_bookmark(),
to: revs::inherited_conflict(),
},
)
.await?;
let RevsResult::Detail {
headers, conflicts, ..
} = result
else {
panic!("Expected RevsResult::Detail");
};
assert_eq!(headers.len(), 2, "Expected 2 headers in range");
assert!(
headers.iter().all(|h| h.has_conflict),
"All commits in range should have conflicts"
);
assert!(
!conflicts.is_empty(),
"Range ending in conflicted commit should have conflicts"
);
Ok(())
}
mod conflict_chain_ranges {
use super::*;
#[tokio::test]
async fn range_ends_in_conflict() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let result = queries::query_revisions(
&ws,
RevSet {
from: revs::resolve_conflict(),
to: revs::chain_conflict(),
},
)
.await?;
let RevsResult::Detail {
headers, conflicts, ..
} = result
else {
panic!("Expected RevsResult::Detail");
};
let final_header = headers.first().expect("at least one header");
assert!(
final_header.has_conflict,
"Final commit in range should have conflict"
);
assert!(
!conflicts.is_empty(),
"Range ending in conflicted commit should have non-empty conflicts"
);
let conflict_paths: Vec<_> = conflicts.iter().map(|c| &c.path.repo_path).collect();
assert!(
conflict_paths.iter().any(|p| p.contains("conflict_chain")),
"Expected conflict in conflict_chain.txt, got: {:?}",
conflict_paths
);
Ok(())
}
#[tokio::test]
async fn range_conflict_to_resolved() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let result = queries::query_revisions(
&ws,
RevSet {
from: revs::chain_conflict(),
to: revs::chain_resolved(),
},
)
.await?;
let RevsResult::Detail {
headers, conflicts, ..
} = result
else {
panic!("Expected RevsResult::Detail");
};
assert_eq!(headers.len(), 2, "Should have 2 headers in range");
let oldest = headers.last().expect("at least one header");
assert!(oldest.has_conflict, "Oldest commit should have conflict");
let newest = headers.first().expect("at least one header");
assert!(
!newest.has_conflict,
"Newest commit should not have conflict"
);
assert!(
conflicts.is_empty(),
"Range ending in resolved commit should have empty conflicts"
);
Ok(())
}
#[tokio::test]
async fn range_through_multiple_conflict_states() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let result = queries::query_revisions(
&ws,
RevSet {
from: revs::conflict_bookmark(),
to: revs::chain_resolved(),
},
)
.await?;
let RevsResult::Detail {
headers, conflicts, ..
} = result
else {
panic!("Expected RevsResult::Detail");
};
assert!(
headers.len() >= 4,
"Expected at least 4 headers, got {}",
headers.len()
);
assert!(
conflicts.is_empty(),
"Range ending in fully resolved commit should have empty conflicts"
);
let conflict_states: Vec<bool> = headers.iter().map(|h| h.has_conflict).collect();
let has_conflicted = conflict_states.iter().any(|&c| c);
let has_resolved = conflict_states.iter().any(|&c| !c);
assert!(
has_conflicted && has_resolved,
"Range should have both conflicted and resolved commits: {:?}",
conflict_states
);
Ok(())
}
#[tokio::test]
async fn single_chain_conflict() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let id = revs::chain_conflict();
let result = queries::query_revisions(
&ws,
RevSet {
from: id.clone(),
to: id,
},
)
.await?;
let RevsResult::Detail {
headers, conflicts, ..
} = result
else {
panic!("Expected RevsResult::Detail");
};
let header = headers.last().expect("at least one header");
assert!(header.has_conflict, "chain_conflict should have conflict");
assert!(
!conflicts.is_empty(),
"chain_conflict should have non-empty conflicts"
);
Ok(())
}
#[tokio::test]
async fn single_chain_resolved() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let id = revs::chain_resolved();
let result = queries::query_revisions(
&ws,
RevSet {
from: id.clone(),
to: id,
},
)
.await?;
let RevsResult::Detail {
headers, conflicts, ..
} = result
else {
panic!("Expected RevsResult::Detail");
};
let header = headers.last().expect("at least one header");
assert!(
!header.has_conflict,
"chain_resolved should not have conflict"
);
assert!(
conflicts.is_empty(),
"chain_resolved should have empty conflicts"
);
Ok(())
}
}
#[tokio::test]
async fn remotes_all() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let remotes = queries::query_remotes(&ws, None)?;
assert_eq!(2, remotes.len());
assert!(remotes.contains(&String::from("origin")));
assert!(remotes.contains(&String::from("second")));
Ok(())
}
#[tokio::test]
async fn remotes_tracking_bookmark() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let remotes = queries::query_remotes(&ws, Some(String::from("main")))?;
assert_eq!(1, remotes.len());
assert!(remotes.contains(&String::from("origin")));
Ok(())
}
mod revisions_immutability {
use super::*;
use crate::messages::{RevSet, queries::RevsResult};
fn mkset(from: crate::messages::RevId, to: crate::messages::RevId) -> RevSet {
RevSet { from, to }
}
fn get_immutability(result: &RevsResult) -> Vec<bool> {
match result {
RevsResult::Detail { headers, .. } => headers.iter().map(|h| h.is_immutable).collect(),
RevsResult::NotFound { .. } => panic!("Expected Detail, got NotFound"),
}
}
#[tokio::test]
async fn single_revision_immutable() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let set = mkset(revs::immutable_bookmark(), revs::immutable_bookmark());
let result = queries::query_revisions(&ws, set).await?;
let flags = get_immutability(&result);
assert_eq!(
flags,
vec![true],
"Single immutable revision should be marked immutable"
);
Ok(())
}
#[tokio::test]
async fn single_revision_mutable() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let set = mkset(revs::main_bookmark(), revs::main_bookmark());
let result = queries::query_revisions(&ws, set).await?;
let flags = get_immutability(&result);
assert_eq!(
flags,
vec![false],
"Single mutable revision should be marked mutable"
);
Ok(())
}
#[tokio::test]
async fn sequence_all_immutable() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let set = mkset(revs::immutable_grandparent(), revs::immutable_bookmark());
let result = queries::query_revisions(&ws, set).await?;
let flags = get_immutability(&result);
assert_eq!(flags.len(), 3, "Should have 3 revisions in range");
assert!(
flags.iter().all(|&f| f),
"All revisions in immutable range should be immutable: {:?}",
flags
);
Ok(())
}
#[tokio::test]
async fn sequence_all_mutable() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let set = mkset(revs::main_bookmark(), revs::working_copy());
let result = queries::query_revisions(&ws, set).await?;
let flags = get_immutability(&result);
assert_eq!(flags.len(), 2, "Should have 2 revisions in range");
assert!(
flags.iter().all(|&f| !f),
"All revisions in mutable range should be mutable: {:?}",
flags
);
Ok(())
}
#[tokio::test]
async fn sequence_oldest_immutable_newest_mutable() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let set = mkset(revs::immutable_bookmark(), revs::main_bookmark());
let result = queries::query_revisions(&ws, set).await?;
let flags = get_immutability(&result);
assert_eq!(flags.len(), 2, "Should have 2 revisions in range");
assert_eq!(
flags,
vec![false, true],
"Oldest should be immutable, newest should be mutable"
);
Ok(())
}
#[tokio::test]
async fn sequence_mixed_immutability_longer() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let set = mkset(revs::immutable_parent(), revs::working_copy());
let result = queries::query_revisions(&ws, set).await?;
let flags = get_immutability(&result);
assert_eq!(flags.len(), 4, "Should have 4 revisions in range");
assert_eq!(
flags,
vec![false, false, true, true],
"First two should be immutable, last two should be mutable"
);
Ok(())
}
}