Skip to main content

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}