use std::borrow::Cow;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct InternedString(pub(crate) u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum ChangeType {
Added = b'A',
Copied = b'C',
Deleted = b'D',
Modified = b'M',
Renamed = b'R',
TypeChanged = b'T',
Unmerged = b'U',
Unknown = b'X',
}
impl ChangeType {
#[inline]
pub const fn from_byte(b: u8) -> Option<Self> {
match b {
b'A' => Some(Self::Added),
b'C' => Some(Self::Copied),
b'D' => Some(Self::Deleted),
b'M' => Some(Self::Modified),
b'R' => Some(Self::Renamed),
b'T' => Some(Self::TypeChanged),
b'U' => Some(Self::Unmerged),
b'X' => Some(Self::Unknown),
_ => None,
}
}
#[inline]
pub const fn as_byte(&self) -> u8 {
match self {
Self::Added => b'A',
Self::Copied => b'C',
Self::Deleted => b'D',
Self::Modified => b'M',
Self::Renamed => b'R',
Self::TypeChanged => b'T',
Self::Unmerged => b'U',
Self::Unknown => b'X',
}
}
#[inline]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Added => "added",
Self::Copied => "copied",
Self::Deleted => "deleted",
Self::Modified => "modified",
Self::Renamed => "renamed",
Self::TypeChanged => "type_changed",
Self::Unmerged => "unmerged",
Self::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone)]
pub struct ChangedFile {
pub path: InternedString,
pub change_type: ChangeType,
pub previous_path: Option<InternedString>,
pub is_symlink: bool,
pub submodule_depth: u8,
pub origin: FileOrigin,
}
#[derive(Debug, Default)]
pub struct DiffResult {
pub files: Vec<ChangedFile>,
pub additions: u32,
pub deletions: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VanishedFile {
pub path: InternedString,
pub last_seen_sha: InternedString,
}
#[derive(Debug, Default)]
pub struct ProcessedResult {
pub all_files: Vec<ChangedFile>,
pub filtered_indices: Vec<u32>,
pub unmatched_indices: Vec<u32>,
pub pattern_applied: bool,
pub group_results: Vec<GroupResult>,
pub additions: u32,
pub deletions: u32,
pub diagnostics: Vec<Diagnostic>,
pub workflow_result: Option<WorkflowCheckResult>,
pub ci_decision: Option<CiDecision>,
pub vanished_files: Vec<VanishedFile>,
pub base_sha: Option<InternedString>,
pub head_sha: Option<InternedString>,
}
impl ProcessedResult {
pub fn matched_files(&self) -> Vec<&ChangedFile> {
self.filtered_indices
.iter()
.map(|&i| &self.all_files[i as usize])
.collect()
}
pub fn other_files(&self) -> Vec<&ChangedFile> {
self.unmatched_indices
.iter()
.map(|&i| &self.all_files[i as usize])
.collect()
}
pub fn from_unfiltered(diff: DiffResult) -> Self {
let n = diff.files.len() as u32;
Self {
filtered_indices: (0..n).collect(),
unmatched_indices: Vec::new(),
pattern_applied: false,
all_files: diff.files,
group_results: Vec::new(),
additions: diff.additions,
deletions: diff.deletions,
diagnostics: Vec::new(),
workflow_result: None,
ci_decision: None,
vanished_files: Vec::new(),
base_sha: None,
head_sha: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum WorkflowStatus {
Queued,
InProgress,
Completed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum WorkflowConclusion {
Success,
Failure,
Cancelled,
Skipped,
TimedOut,
Neutral,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FailureTrackingLevel {
#[default]
Run,
Job,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct FileOrigin {
pub in_current_changes: bool,
pub in_previous_failure: bool,
pub in_previous_success: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct WorkflowRun {
pub id: u64,
pub name: InternedString,
pub status: WorkflowStatus,
pub conclusion: Option<WorkflowConclusion>,
pub branch: InternedString,
pub head_sha: InternedString,
pub created_at: i64,
}
#[derive(Debug, Clone, Copy)]
pub struct WorkflowJob {
pub id: u64,
pub name: InternedString,
pub status: WorkflowStatus,
pub conclusion: Option<WorkflowConclusion>,
pub run_id: u64,
pub started_at: i64,
pub completed_at: i64,
}
#[derive(Debug)]
pub struct WorkflowFailure {
pub run: WorkflowRun,
pub files: Vec<InternedString>,
pub failed_jobs: Vec<WorkflowJob>,
}
#[derive(Debug)]
pub struct WorkflowSuccess {
pub run: WorkflowRun,
pub jobs: Vec<WorkflowJob>,
pub files: Vec<InternedString>,
}
#[derive(Debug, Default)]
pub struct WorkflowCheckResult {
pub blocking_runs: Vec<WorkflowRun>,
pub failures: Vec<WorkflowFailure>,
pub successes: Vec<WorkflowSuccess>,
pub waited: bool,
pub wait_time_ms: u64,
pub blocked_groups: std::collections::HashMap<InternedString, Vec<u64>>,
}
#[derive(Debug, Default)]
pub struct CiDecision {
pub files_to_rebuild: Vec<InternedString>,
pub files_to_skip: Vec<InternedString>,
pub failed_jobs: Vec<InternedString>,
pub successful_jobs: Vec<InternedString>,
pub rebuild_reasons: Vec<RebuildReason>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum RebuildReasonKind {
NewChange,
PreviousFailure,
BothNewAndFailed,
}
impl RebuildReasonKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::NewChange => "new_change",
Self::PreviousFailure => "previous_failure",
Self::BothNewAndFailed => "both_new_and_failed",
}
}
}
#[derive(Debug, Clone)]
pub struct RebuildReason {
pub file: InternedString,
pub kind: RebuildReasonKind,
pub failed_run_id: Option<u64>,
pub failed_job_name: Option<InternedString>,
}
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub severity: DiagnosticSeverity,
pub category: DiagnosticCategory,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum DiagnosticSeverity {
Warning,
SoftError,
}
impl DiagnosticSeverity {
pub const fn as_str(self) -> &'static str {
match self {
Self::Warning => "warning",
Self::SoftError => "soft_error",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum DiagnosticCategory {
InitialDiff,
SubmoduleDiff,
SkippedSameSha,
ShallowClone,
PatternLoad,
SymlinkDetection,
WorkflowApi,
AncestorRecovery,
VanishedDetection,
}
impl DiagnosticCategory {
pub const fn as_str(self) -> &'static str {
match self {
Self::InitialDiff => "initial_diff",
Self::SubmoduleDiff => "submodule_diff",
Self::SkippedSameSha => "skipped_same_sha",
Self::ShallowClone => "shallow_clone",
Self::PatternLoad => "pattern_load",
Self::SymlinkDetection => "symlink_detection",
Self::WorkflowApi => "workflow_api",
Self::AncestorRecovery => "ancestor_recovery",
Self::VanishedDetection => "vanished_detection",
}
}
}
#[derive(Debug, Clone)]
pub struct GroupResult {
pub key: InternedString,
pub matched_indices: Vec<u32>,
pub vanished_indices: Vec<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum GroupDeployAction {
Deploy,
Skip,
Destroy,
}
impl GroupDeployAction {
pub const fn as_str(self) -> &'static str {
match self {
Self::Deploy => "deploy",
Self::Skip => "skip",
Self::Destroy => "destroy",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum GroupDeployReason {
NewChange,
PreviousFailure,
BothNewAndFailed,
Vanished,
EndpointDeleted,
}
impl GroupDeployReason {
pub const fn as_str(self) -> &'static str {
match self {
Self::NewChange => "new_change",
Self::PreviousFailure => "previous_failure",
Self::BothNewAndFailed => "both_new_and_failed",
Self::Vanished => "vanished",
Self::EndpointDeleted => "deleted",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GroupByKey {
#[default]
Name,
Path,
Hash,
}
impl GroupByKey {
pub fn parse(s: &str) -> Self {
match s.to_ascii_lowercase().as_str() {
"path" => Self::Path,
"hash" => Self::Hash,
_ => Self::Name,
}
}
}
#[derive(Debug, Clone)]
pub struct GroupDeployDecision {
pub key: InternedString,
pub action: GroupDeployAction,
pub reason: Option<GroupDeployReason>,
pub files_to_rebuild: Vec<InternedString>,
pub files_to_skip: Vec<InternedString>,
pub total_files: u32,
pub concurrency_blocked: bool,
pub concurrency_blocked_by: u32,
pub vanished_files: Vec<VanishedFile>,
pub reconstruct_sha: Option<InternedString>,
}
#[derive(Clone)]
pub struct InputConfig<'a> {
pub base_sha: Option<Cow<'a, str>>,
pub sha: Option<Cow<'a, str>>,
pub since: Option<Cow<'a, str>>,
pub until: Option<Cow<'a, str>>,
pub files: Option<Vec<Cow<'a, str>>>,
pub files_separator: Cow<'a, str>,
pub files_ignore: Option<Vec<Cow<'a, str>>>,
pub files_ignore_separator: Cow<'a, str>,
pub files_yaml: Option<Cow<'a, str>>,
pub files_yaml_from_source_file: Option<Cow<'a, str>>,
pub files_from_source_file: Option<Cow<'a, str>>,
pub files_from_source_file_separator: Cow<'a, str>,
pub diff_filter: Cow<'a, str>,
pub include_all_old_new_renamed_files: bool,
pub old_new_separator: Cow<'a, str>,
pub old_new_files_separator: Cow<'a, str>,
pub dir_names: bool,
pub dir_names_max_depth: Option<u32>,
pub quotepath: bool,
pub path_separator: Cow<'a, str>,
pub dir_names_exclude_current_dir: bool,
pub dir_names_include_files: Option<Vec<Cow<'a, str>>>,
pub dir_names_deleted_files_include_only_deleted_dirs: bool,
pub include_submodules: bool,
pub submodule_filter: Option<Cow<'a, str>>,
pub fetch_depth: u32,
pub fetch_additional_submodule_history: bool,
pub json: bool,
pub escape_json: bool,
pub safe_output: bool,
pub output_dir: Option<Cow<'a, str>>,
pub detect_vanished: bool,
pub vanished_max_commits: u32,
pub deleted_to_destroy: bool,
pub skip_initial_fetch: bool,
pub use_rest_api: bool,
pub api_url: Option<Cow<'a, str>>,
pub token: Option<Cow<'a, str>>,
pub write_output_files: bool,
pub negation_patterns_first: bool,
pub match_gitignore_files: bool,
pub recover_deleted_files: bool,
pub exclude_symlinks: bool,
pub tags_pattern: Option<Cow<'a, str>>,
pub tags_ignore_pattern: Option<Cow<'a, str>>,
pub fail_on_initial_diff_error: bool,
pub fail_on_submodule_diff_error: bool,
pub skip_same_sha: bool,
pub output_renamed_as_deleted_added: bool,
pub use_posix_path_separator: bool,
pub track_workflow_failures: bool,
pub workflow_lookback_commits: u32,
pub wait_for_active_workflows: bool,
pub workflow_max_wait_seconds: u32,
pub include_failed_files: bool,
pub failure_tracking_level: FailureTrackingLevel,
pub workflow_success_lookback: u32,
pub skip_successful_files: bool,
pub workflow_name_filter: Option<Cow<'a, str>>,
pub files_group_by: Option<Cow<'a, str>>,
pub files_group_by_key: Option<Cow<'a, str>>,
pub files_ancestor_lookup_depth: u32,
pub deploy_matrix_include_reason: bool,
pub deploy_matrix_include_concurrency: bool,
}
impl<'a> std::fmt::Debug for InputConfig<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InputConfig")
.field("base_sha", &self.base_sha)
.field("sha", &self.sha)
.field("since", &self.since)
.field("until", &self.until)
.field("files", &self.files)
.field("files_separator", &self.files_separator)
.field("files_ignore", &self.files_ignore)
.field("files_yaml", &self.files_yaml)
.field("files_from_source_file", &self.files_from_source_file)
.field("diff_filter", &self.diff_filter)
.field("json", &self.json)
.field("safe_output", &self.safe_output)
.field("use_rest_api", &self.use_rest_api)
.field("api_url", &self.api_url)
.field("token", &self.token.as_ref().map(|_| "<redacted>"))
.field("track_workflow_failures", &self.track_workflow_failures)
.field("files_group_by", &self.files_group_by)
.field("files_group_by_key", &self.files_group_by_key)
.field(
"files_ancestor_lookup_depth",
&self.files_ancestor_lookup_depth,
)
.field(
"deploy_matrix_include_reason",
&self.deploy_matrix_include_reason,
)
.field(
"deploy_matrix_include_concurrency",
&self.deploy_matrix_include_concurrency,
)
.finish_non_exhaustive()
}
}
impl<'a> Default for InputConfig<'a> {
fn default() -> Self {
Self {
base_sha: None,
sha: None,
since: None,
until: None,
files: None,
files_separator: Cow::Borrowed("\n"),
files_ignore: None,
files_ignore_separator: Cow::Borrowed("\n"),
files_yaml: None,
files_yaml_from_source_file: None,
files_from_source_file: None,
files_from_source_file_separator: Cow::Borrowed("\n"),
diff_filter: Cow::Borrowed("ACDMRTUX"),
include_all_old_new_renamed_files: false,
old_new_separator: Cow::Borrowed(" "),
old_new_files_separator: Cow::Borrowed("\n"),
dir_names: false,
dir_names_max_depth: None,
quotepath: true,
path_separator: if cfg!(windows) {
Cow::Borrowed("\\")
} else {
Cow::Borrowed("/")
},
dir_names_exclude_current_dir: false,
dir_names_include_files: None,
dir_names_deleted_files_include_only_deleted_dirs: false,
include_submodules: false,
submodule_filter: None,
fetch_depth: 0,
fetch_additional_submodule_history: false,
json: false,
escape_json: true,
safe_output: true,
output_dir: None,
detect_vanished: false,
vanished_max_commits: 500,
deleted_to_destroy: false,
skip_initial_fetch: false,
use_rest_api: false,
api_url: None,
token: None,
write_output_files: false,
negation_patterns_first: true,
match_gitignore_files: false,
recover_deleted_files: false,
exclude_symlinks: false,
tags_pattern: None,
tags_ignore_pattern: None,
fail_on_initial_diff_error: true,
fail_on_submodule_diff_error: false,
skip_same_sha: false,
output_renamed_as_deleted_added: false,
use_posix_path_separator: false,
track_workflow_failures: false,
workflow_lookback_commits: 5,
wait_for_active_workflows: true,
workflow_max_wait_seconds: 300,
include_failed_files: true,
failure_tracking_level: FailureTrackingLevel::Run,
workflow_success_lookback: 5,
skip_successful_files: true,
workflow_name_filter: None,
files_group_by: None,
files_group_by_key: None,
files_ancestor_lookup_depth: 0,
deploy_matrix_include_reason: false,
deploy_matrix_include_concurrency: false,
}
}
}
impl<'a> InputConfig<'a> {
pub fn github_actions_defaults() -> Self {
Self {
safe_output: true,
json: true,
escape_json: true,
use_posix_path_separator: true,
skip_initial_fetch: true,
..Self::default()
}
}
pub fn with_base_sha(mut self, v: Option<&'a str>) -> Self {
if v.is_some() {
self.base_sha = v.map(Cow::Borrowed);
}
self
}
pub fn with_sha(mut self, v: Option<&'a str>) -> Self {
if v.is_some() {
self.sha = v.map(Cow::Borrowed);
}
self
}
pub fn with_files(mut self, v: Option<Vec<&'a str>>) -> Self {
if let Some(files) = v {
self.files = Some(files.into_iter().map(Cow::Borrowed).collect());
}
self
}
pub fn with_files_ignore(mut self, v: Option<Vec<&'a str>>) -> Self {
if let Some(files) = v {
self.files_ignore = Some(files.into_iter().map(Cow::Borrowed).collect());
}
self
}
pub fn with_files_group_by(mut self, v: Option<&'a str>) -> Self {
if v.is_some() {
self.files_group_by = v.map(Cow::Borrowed);
}
self
}
pub fn with_files_group_by_key(mut self, v: &'a str) -> Self {
self.files_group_by_key = Some(Cow::Borrowed(v));
self
}
pub fn with_files_ancestor_lookup_depth(mut self, v: u32) -> Self {
self.files_ancestor_lookup_depth = v;
self
}
pub fn with_track_workflow_failures(mut self, v: bool) -> Self {
self.track_workflow_failures = v;
self
}
pub fn with_failure_tracking_level_str(mut self, v: &str) -> Self {
self.failure_tracking_level = match v {
"job" | "Job" => FailureTrackingLevel::Job,
_ => FailureTrackingLevel::Run,
};
self
}
pub fn with_wait_for_active_workflows(mut self, v: bool) -> Self {
self.wait_for_active_workflows = v;
self
}
pub fn with_workflow_max_wait_seconds(mut self, v: u32) -> Self {
self.workflow_max_wait_seconds = v;
self
}
pub fn with_workflow_name_filter(mut self, v: Option<&'a str>) -> Self {
if v.is_some() {
self.workflow_name_filter = v.map(Cow::Borrowed);
}
self
}
pub fn with_deploy_matrix_include_reason(mut self, v: bool) -> Self {
self.deploy_matrix_include_reason = v;
self
}
pub fn with_deploy_matrix_include_concurrency(mut self, v: bool) -> Self {
self.deploy_matrix_include_concurrency = v;
self
}
pub fn with_token(mut self, v: Option<&'a str>) -> Self {
if v.is_some() {
self.token = v.map(Cow::Borrowed);
}
self
}
pub fn with_detect_vanished(mut self, v: bool) -> Self {
self.detect_vanished = v;
self
}
pub fn with_vanished_max_commits(mut self, v: u32) -> Self {
self.vanished_max_commits = v;
self
}
pub fn with_deleted_to_destroy(mut self, v: bool) -> Self {
self.deleted_to_destroy = v;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_matches_struct_literal() {
let built = InputConfig::github_actions_defaults()
.with_base_sha(Some("abc"))
.with_sha(Some("def"))
.with_files(Some(vec!["stacks/**"]))
.with_files_ignore(Some(vec!["docs/**"]))
.with_files_group_by(Some("stacks/{group}/**"))
.with_files_group_by_key("path")
.with_files_ancestor_lookup_depth(2)
.with_track_workflow_failures(true)
.with_failure_tracking_level_str("job")
.with_wait_for_active_workflows(true)
.with_workflow_max_wait_seconds(60)
.with_workflow_name_filter(Some("CI*"))
.with_deploy_matrix_include_reason(true)
.with_deploy_matrix_include_concurrency(true)
.with_token(Some("t"));
let literal = InputConfig {
base_sha: Some(Cow::Borrowed("abc")),
sha: Some(Cow::Borrowed("def")),
files: Some(vec![Cow::Borrowed("stacks/**")]),
files_ignore: Some(vec![Cow::Borrowed("docs/**")]),
files_group_by: Some(Cow::Borrowed("stacks/{group}/**")),
files_group_by_key: Some(Cow::Borrowed("path")),
files_ancestor_lookup_depth: 2,
track_workflow_failures: true,
failure_tracking_level: FailureTrackingLevel::Job,
wait_for_active_workflows: true,
workflow_max_wait_seconds: 60,
workflow_name_filter: Some(Cow::Borrowed("CI*")),
deploy_matrix_include_reason: true,
deploy_matrix_include_concurrency: true,
token: Some(Cow::Borrowed("t")),
safe_output: true,
json: true,
escape_json: true,
use_posix_path_separator: true,
skip_initial_fetch: true,
..Default::default()
};
assert_eq!(built.base_sha, literal.base_sha);
assert_eq!(built.sha, literal.sha);
assert_eq!(built.files, literal.files);
assert_eq!(built.files_ignore, literal.files_ignore);
assert_eq!(built.files_group_by, literal.files_group_by);
assert_eq!(built.files_group_by_key, literal.files_group_by_key);
assert_eq!(
built.files_ancestor_lookup_depth,
literal.files_ancestor_lookup_depth
);
assert_eq!(
built.track_workflow_failures,
literal.track_workflow_failures
);
assert_eq!(built.failure_tracking_level, literal.failure_tracking_level);
assert_eq!(
built.wait_for_active_workflows,
literal.wait_for_active_workflows
);
assert_eq!(
built.workflow_max_wait_seconds,
literal.workflow_max_wait_seconds
);
assert_eq!(built.workflow_name_filter, literal.workflow_name_filter);
assert_eq!(built.token, literal.token);
assert!(built.safe_output && built.json && built.escape_json);
assert!(built.use_posix_path_separator && built.skip_initial_fetch);
}
#[test]
fn test_builder_none_leaves_defaults() {
let built = InputConfig::github_actions_defaults()
.with_base_sha(None)
.with_files(None)
.with_files_group_by(None)
.with_workflow_name_filter(None)
.with_token(None);
assert!(built.base_sha.is_none());
assert!(built.files.is_none());
assert!(built.files_group_by.is_none());
assert!(built.workflow_name_filter.is_none());
assert!(built.token.is_none());
}
#[test]
fn test_change_type_from_byte() {
assert_eq!(ChangeType::from_byte(b'A'), Some(ChangeType::Added));
assert_eq!(ChangeType::from_byte(b'M'), Some(ChangeType::Modified));
assert_eq!(ChangeType::from_byte(b'D'), Some(ChangeType::Deleted));
assert_eq!(ChangeType::from_byte(b'R'), Some(ChangeType::Renamed));
assert_eq!(ChangeType::from_byte(b'Z'), None);
}
#[test]
fn test_change_type_as_str() {
assert_eq!(ChangeType::Added.as_str(), "added");
assert_eq!(ChangeType::Modified.as_str(), "modified");
assert_eq!(ChangeType::Deleted.as_str(), "deleted");
}
#[test]
fn test_change_type_as_byte() {
assert_eq!(ChangeType::Added.as_byte(), b'A');
assert_eq!(ChangeType::Modified.as_byte(), b'M');
assert_eq!(ChangeType::Deleted.as_byte(), b'D');
assert_eq!(ChangeType::Renamed.as_byte(), b'R');
}
#[test]
fn test_change_type_roundtrip() {
let types = [
ChangeType::Added,
ChangeType::Copied,
ChangeType::Deleted,
ChangeType::Modified,
ChangeType::Renamed,
ChangeType::TypeChanged,
ChangeType::Unmerged,
ChangeType::Unknown,
];
for change_type in &types {
let byte = change_type.as_byte();
let parsed = ChangeType::from_byte(byte);
assert_eq!(parsed, Some(*change_type));
}
}
#[test]
fn test_input_config_default() {
let config = InputConfig::default();
assert_eq!(config.diff_filter, "ACDMRTUX");
assert_eq!(config.files_separator, "\n");
assert!(!config.json);
assert!(config.quotepath);
}
#[test]
fn test_file_origin_default() {
let origin = FileOrigin::default();
assert!(!origin.in_current_changes);
assert!(!origin.in_previous_failure);
assert!(!origin.in_previous_success);
}
#[test]
fn test_processed_result_from_unfiltered() {
let diff = DiffResult {
files: vec![
ChangedFile {
path: InternedString(0),
change_type: ChangeType::Added,
previous_path: None,
is_symlink: false,
submodule_depth: 0,
origin: FileOrigin::default(),
},
ChangedFile {
path: InternedString(1),
change_type: ChangeType::Modified,
previous_path: None,
is_symlink: false,
submodule_depth: 0,
origin: FileOrigin::default(),
},
],
additions: 10,
deletions: 5,
};
let result = ProcessedResult::from_unfiltered(diff);
assert_eq!(result.all_files.len(), 2);
assert_eq!(result.filtered_indices, vec![0, 1]);
assert!(result.unmatched_indices.is_empty());
assert!(!result.pattern_applied);
assert_eq!(result.additions, 10);
assert_eq!(result.deletions, 5);
}
#[test]
fn test_processed_result_accessors() {
let result = ProcessedResult {
all_files: vec![
ChangedFile {
path: InternedString(0),
change_type: ChangeType::Added,
previous_path: None,
is_symlink: false,
submodule_depth: 0,
origin: FileOrigin::default(),
},
ChangedFile {
path: InternedString(1),
change_type: ChangeType::Modified,
previous_path: None,
is_symlink: false,
submodule_depth: 0,
origin: FileOrigin::default(),
},
ChangedFile {
path: InternedString(2),
change_type: ChangeType::Deleted,
previous_path: None,
is_symlink: false,
submodule_depth: 0,
origin: FileOrigin::default(),
},
],
filtered_indices: vec![0, 2],
unmatched_indices: vec![1],
pattern_applied: true,
group_results: Vec::new(),
additions: 0,
deletions: 0,
diagnostics: Vec::new(),
workflow_result: None,
ci_decision: None,
vanished_files: Vec::new(),
base_sha: None,
head_sha: None,
};
assert_eq!(result.matched_files().len(), 2);
assert_eq!(result.other_files().len(), 1);
assert_eq!(result.matched_files()[0].change_type, ChangeType::Added);
assert_eq!(result.matched_files()[1].change_type, ChangeType::Deleted);
assert_eq!(result.other_files()[0].change_type, ChangeType::Modified);
}
#[test]
fn test_ci_decision_default() {
let decision = CiDecision::default();
assert!(decision.files_to_rebuild.is_empty());
assert!(decision.files_to_skip.is_empty());
assert!(decision.failed_jobs.is_empty());
assert!(decision.successful_jobs.is_empty());
assert!(decision.rebuild_reasons.is_empty());
}
#[test]
fn test_failure_tracking_level_default() {
let level = FailureTrackingLevel::default();
assert_eq!(level, FailureTrackingLevel::Run);
}
#[test]
fn test_failure_tracking_level_equality() {
assert_eq!(FailureTrackingLevel::Run, FailureTrackingLevel::Run);
assert_eq!(FailureTrackingLevel::Job, FailureTrackingLevel::Job);
assert_ne!(FailureTrackingLevel::Run, FailureTrackingLevel::Job);
}
#[test]
fn test_failure_tracking_level_copy() {
let level = FailureTrackingLevel::Job;
let copy = level; assert_eq!(level, copy);
}
#[test]
fn test_input_config_failure_tracking_level() {
let config = InputConfig::default();
assert_eq!(config.failure_tracking_level, FailureTrackingLevel::Run);
let config_job = InputConfig {
failure_tracking_level: FailureTrackingLevel::Job,
..Default::default()
};
assert_eq!(config_job.failure_tracking_level, FailureTrackingLevel::Job);
}
#[test]
fn test_workflow_failure_with_jobs() {
let failure = WorkflowFailure {
run: WorkflowRun {
id: 1,
name: InternedString(0),
status: WorkflowStatus::Completed,
conclusion: Some(WorkflowConclusion::Failure),
branch: InternedString(1),
head_sha: InternedString(2),
created_at: 1000,
},
files: vec![InternedString(3)],
failed_jobs: vec![WorkflowJob {
id: 10,
name: InternedString(4),
status: WorkflowStatus::Completed,
conclusion: Some(WorkflowConclusion::Failure),
run_id: 1,
started_at: 100,
completed_at: 200,
}],
};
assert_eq!(failure.run.id, 1);
assert_eq!(failure.files.len(), 1);
assert_eq!(failure.failed_jobs.len(), 1);
assert_eq!(failure.failed_jobs[0].id, 10);
}
#[test]
fn test_workflow_success_with_jobs() {
let success = WorkflowSuccess {
run: WorkflowRun {
id: 2,
name: InternedString(0),
status: WorkflowStatus::Completed,
conclusion: Some(WorkflowConclusion::Success),
branch: InternedString(1),
head_sha: InternedString(2),
created_at: 2000,
},
jobs: vec![WorkflowJob {
id: 20,
name: InternedString(5),
status: WorkflowStatus::Completed,
conclusion: Some(WorkflowConclusion::Success),
run_id: 2,
started_at: 300,
completed_at: 400,
}],
files: vec![InternedString(3), InternedString(4)],
};
assert_eq!(success.run.id, 2);
assert_eq!(success.files.len(), 2);
assert_eq!(success.jobs.len(), 1);
assert_eq!(success.jobs[0].id, 20);
}
#[test]
fn test_workflow_status_variants() {
assert_eq!(WorkflowStatus::Queued, WorkflowStatus::Queued);
assert_eq!(WorkflowStatus::InProgress, WorkflowStatus::InProgress);
assert_eq!(WorkflowStatus::Completed, WorkflowStatus::Completed);
assert_ne!(WorkflowStatus::Queued, WorkflowStatus::Completed);
}
#[test]
fn test_workflow_conclusion_variants() {
assert_eq!(WorkflowConclusion::Success, WorkflowConclusion::Success);
assert_eq!(WorkflowConclusion::Failure, WorkflowConclusion::Failure);
assert_eq!(WorkflowConclusion::Cancelled, WorkflowConclusion::Cancelled);
assert_eq!(WorkflowConclusion::Skipped, WorkflowConclusion::Skipped);
assert_eq!(WorkflowConclusion::TimedOut, WorkflowConclusion::TimedOut);
assert_eq!(WorkflowConclusion::Neutral, WorkflowConclusion::Neutral);
assert_ne!(WorkflowConclusion::Success, WorkflowConclusion::Failure);
}
#[test]
fn test_group_deploy_action_equality() {
assert_eq!(GroupDeployAction::Deploy, GroupDeployAction::Deploy);
assert_eq!(GroupDeployAction::Skip, GroupDeployAction::Skip);
assert_ne!(GroupDeployAction::Deploy, GroupDeployAction::Skip);
}
#[test]
fn test_group_deploy_reason_equality() {
assert_eq!(GroupDeployReason::NewChange, GroupDeployReason::NewChange);
assert_eq!(
GroupDeployReason::PreviousFailure,
GroupDeployReason::PreviousFailure
);
assert_eq!(
GroupDeployReason::BothNewAndFailed,
GroupDeployReason::BothNewAndFailed
);
assert_ne!(
GroupDeployReason::NewChange,
GroupDeployReason::PreviousFailure
);
}
#[test]
fn test_group_deploy_decision_deploy() {
use crate::interner::StringInterner;
let interner = StringInterner::new();
let prod_key = interner.intern("prod");
let decision = GroupDeployDecision {
key: prod_key,
action: GroupDeployAction::Deploy,
reason: Some(GroupDeployReason::NewChange),
files_to_rebuild: vec![InternedString(0), InternedString(1)],
files_to_skip: vec![],
total_files: 2,
concurrency_blocked: false,
concurrency_blocked_by: 0,
vanished_files: Vec::new(),
reconstruct_sha: None,
};
assert_eq!(decision.action, GroupDeployAction::Deploy);
assert_eq!(decision.reason, Some(GroupDeployReason::NewChange));
assert_eq!(decision.files_to_rebuild.len(), 2);
assert_eq!(decision.total_files, 2);
assert!(!decision.concurrency_blocked);
assert_eq!(decision.concurrency_blocked_by, 0);
}
#[test]
fn test_group_deploy_decision_skip() {
use crate::interner::StringInterner;
let interner = StringInterner::new();
let staging_key = interner.intern("staging");
let decision = GroupDeployDecision {
key: staging_key,
action: GroupDeployAction::Skip,
reason: None,
files_to_rebuild: vec![],
files_to_skip: vec![InternedString(0)],
total_files: 1,
concurrency_blocked: false,
concurrency_blocked_by: 0,
vanished_files: Vec::new(),
reconstruct_sha: None,
};
assert_eq!(decision.action, GroupDeployAction::Skip);
assert!(decision.reason.is_none());
assert!(decision.files_to_rebuild.is_empty());
assert_eq!(decision.files_to_skip.len(), 1);
}
#[test]
fn test_workflow_run_is_copy() {
let r = WorkflowRun {
id: 1,
name: InternedString(0),
status: WorkflowStatus::Completed,
conclusion: Some(WorkflowConclusion::Success),
branch: InternedString(1),
head_sha: InternedString(2),
created_at: 1000,
};
let r2 = r; let _ = r; assert_eq!(r2.id, 1);
}
#[test]
fn test_workflow_job_is_copy() {
let j = WorkflowJob {
id: 10,
name: InternedString(0),
status: WorkflowStatus::Completed,
conclusion: Some(WorkflowConclusion::Failure),
run_id: 1,
started_at: 100,
completed_at: 200,
};
let j2 = j; let _ = j; assert_eq!(j2.id, 10);
}
#[test]
fn test_group_by_key_parse() {
assert_eq!(GroupByKey::parse("name"), GroupByKey::Name);
assert_eq!(GroupByKey::parse("path"), GroupByKey::Path);
assert_eq!(GroupByKey::parse("hash"), GroupByKey::Hash);
assert_eq!(GroupByKey::parse("PATH"), GroupByKey::Path);
assert_eq!(GroupByKey::parse("HASH"), GroupByKey::Hash);
assert_eq!(GroupByKey::parse("unknown"), GroupByKey::Name); }
#[test]
fn test_group_by_key_default() {
assert_eq!(GroupByKey::default(), GroupByKey::Name);
}
#[test]
fn test_input_config_debug_redacts_token() {
let config = InputConfig {
token: Some(std::borrow::Cow::Borrowed("ghp_SuperSecretToken12345")),
..Default::default()
};
let debug_output = format!("{:?}", config);
assert!(
!debug_output.contains("ghp_SuperSecretToken12345"),
"Debug output must not contain the actual token value"
);
assert!(
debug_output.contains("<redacted>"),
"Debug output should show <redacted> for the token field"
);
}
#[test]
fn test_input_config_debug_no_token_shows_none() {
let config = InputConfig::default();
let debug_output = format!("{:?}", config);
assert!(
debug_output.contains("token: None"),
"Debug output should show None when no token is set"
);
assert!(
!debug_output.contains("<redacted>"),
"Debug output should not show <redacted> when no token is set"
);
}
}