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, 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>,
}
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,
}
}
}
#[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,
}
#[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,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum DiagnosticCategory {
InitialDiff,
SubmoduleDiff,
SkippedSameSha,
ShallowClone,
PatternLoad,
SymlinkDetection,
WorkflowApi,
AncestorRecovery,
}
#[derive(Debug, Clone)]
pub struct GroupResult {
pub key: InternedString,
pub matched_indices: Vec<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum GroupDeployAction {
Deploy,
Skip,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum GroupDeployReason {
NewChange,
PreviousFailure,
BothNewAndFailed,
}
#[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,
}
#[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 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,
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,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[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,
};
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,
};
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,
};
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"
);
}
}