pacsea/logic/pkgbuild_checks.rs
1//! PKGBUILD static check state tied to the selected package.
2
3use crate::state::AppState;
4use crate::state::app_state::PkgbuildCheckStatus;
5
6/// What: Drop PKGBUILD check results when they belong to a different package than the current selection.
7///
8/// Inputs:
9/// - `app`: Application state holding check status and findings.
10/// - `selected_package`: Name of the package now highlighted in the results list.
11///
12/// Output:
13/// - Mutates `app` to idle check state with empty findings when the last check target does not match.
14///
15/// Details:
16/// - Compares `selected_package` to [`AppState::pkgb_check_last_package_name`], which is set when a
17/// run starts and when results arrive. Keeps results when the user is still on the same package.
18pub fn clear_stale_pkgbuild_checks_for_selection(app: &mut AppState, selected_package: &str) {
19 let still_valid = app
20 .pkgb_check_last_package_name
21 .as_deref()
22 .is_some_and(|pkg| pkg == selected_package);
23 if still_valid {
24 return;
25 }
26 reset_pkgbuild_check_ui_state(app);
27}
28
29/// What: Reset all PKGBUILD check UI fields to an idle, empty state.
30///
31/// Inputs:
32/// - `app`: Application state.
33///
34/// Output:
35/// - Clears findings, raw output, errors, and scroll positions for the check panels.
36///
37/// Details:
38/// - Used when switching packages or when discarding stale worker responses.
39fn reset_pkgbuild_check_ui_state(app: &mut AppState) {
40 app.pkgb_check_status = PkgbuildCheckStatus::Idle;
41 app.pkgb_check_findings.clear();
42 app.pkgb_check_raw_results.clear();
43 app.pkgb_check_missing_tools.clear();
44 app.pkgb_check_last_error = None;
45 app.pkgb_check_last_package_name = None;
46 app.pkgb_check_last_run_at = None;
47 app.pkgb_check_scroll = 0;
48 app.pkgb_check_raw_scroll = 0;
49 app.pkgb_check_show_raw_output = false;
50}
51
52/// What: Whether a [`crate::state::PkgbuildCheckResponse`] should update the UI for the current row.
53///
54/// Inputs:
55/// - `app`: Application state.
56/// - `response_package`: Package name carried in the worker response.
57///
58/// Output:
59/// - `true` if the response matches the focused or selected package (same rule as PKGBUILD fetch).
60///
61/// Details:
62/// - Prevents late results from repopulating the panel after the user moved to another package.
63#[must_use]
64pub fn pkgbuild_check_response_matches_selection(app: &AppState, response_package: &str) -> bool {
65 app.details_focus.as_deref() == Some(response_package)
66 || app
67 .results
68 .get(app.selected)
69 .is_some_and(|item| item.name == response_package)
70}
71
72#[cfg(test)]
73#[allow(clippy::field_reassign_with_default)] // Test setup mutates a default `AppState` field by field.
74mod tests {
75 use super::*;
76 use crate::state::app_state::{PkgbuildCheckFinding, PkgbuildCheckSeverity, PkgbuildCheckTool};
77 use crate::state::{PackageItem, Source};
78
79 fn aur_item(name: &str) -> PackageItem {
80 PackageItem {
81 name: name.to_string(),
82 version: "1".to_string(),
83 description: String::new(),
84 source: Source::Aur,
85 popularity: None,
86 out_of_date: None,
87 orphaned: false,
88 }
89 }
90
91 #[test]
92 /// What: Stale check results clear when the selection package name changes.
93 ///
94 /// Inputs:
95 /// - `AppState` with completed checks recorded for `pkg-a`.
96 /// - `selected_package` of `pkg-b`.
97 ///
98 /// Output:
99 /// - Check status is idle and findings are empty.
100 ///
101 /// Details:
102 /// - Mirrors navigating from one result row to another after running checks.
103 fn clear_stale_drops_results_for_other_package() {
104 let mut app = AppState::default();
105 app.pkgb_check_status = PkgbuildCheckStatus::Complete;
106 app.pkgb_check_last_package_name = Some("pkg-a".to_string());
107 app.pkgb_check_findings.push(PkgbuildCheckFinding {
108 tool: PkgbuildCheckTool::Shellcheck,
109 severity: PkgbuildCheckSeverity::Warning,
110 line: Some(1),
111 message: "old".to_string(),
112 });
113
114 clear_stale_pkgbuild_checks_for_selection(&mut app, "pkg-b");
115
116 assert_eq!(app.pkgb_check_status, PkgbuildCheckStatus::Idle);
117 assert!(app.pkgb_check_findings.is_empty());
118 assert!(app.pkgb_check_last_package_name.is_none());
119 }
120
121 #[test]
122 /// What: Results are retained when the selection still matches the check target.
123 ///
124 /// Inputs:
125 /// - `AppState` with checks for `pkg-a` and selection `pkg-a`.
126 ///
127 /// Output:
128 /// - Findings and complete status unchanged.
129 ///
130 /// Details:
131 /// - Ensures we do not clear on no-op navigation callbacks.
132 fn clear_stale_keeps_results_for_same_package() {
133 let mut app = AppState::default();
134 app.pkgb_check_status = PkgbuildCheckStatus::Complete;
135 app.pkgb_check_last_package_name = Some("pkg-a".to_string());
136 app.pkgb_check_findings.push(PkgbuildCheckFinding {
137 tool: PkgbuildCheckTool::Namcap,
138 severity: PkgbuildCheckSeverity::Info,
139 line: None,
140 message: "keep".to_string(),
141 });
142
143 clear_stale_pkgbuild_checks_for_selection(&mut app, "pkg-a");
144
145 assert_eq!(app.pkgb_check_status, PkgbuildCheckStatus::Complete);
146 assert_eq!(app.pkgb_check_findings.len(), 1);
147 }
148
149 #[test]
150 /// What: Response matches when the selected results row names the same package.
151 ///
152 /// Inputs:
153 /// - `AppState` with `results` and `selected` pointing at `foo`.
154 ///
155 /// Output:
156 /// - `pkgbuild_check_response_matches_selection` is true for `foo`.
157 ///
158 /// Details:
159 /// - Covers the worker completion path when `details_focus` is unset but the row matches.
160 fn response_matches_selected_row() {
161 let mut app = AppState::default();
162 app.results = vec![aur_item("foo")];
163 app.selected = 0;
164
165 assert!(pkgbuild_check_response_matches_selection(&app, "foo"));
166 assert!(!pkgbuild_check_response_matches_selection(&app, "bar"));
167 }
168
169 #[test]
170 /// What: Response matches when `details_focus` agrees even if row index were wrong.
171 ///
172 /// Inputs:
173 /// - `AppState` with `details_focus` set to `bar`.
174 ///
175 /// Output:
176 /// - Matcher returns true for `bar`.
177 ///
178 /// Details:
179 /// - Aligns with `handle_pkgbuild_result` gating.
180 fn response_matches_details_focus() {
181 let mut app = AppState::default();
182 app.details_focus = Some("bar".to_string());
183
184 assert!(pkgbuild_check_response_matches_selection(&app, "bar"));
185 }
186}