Skip to content

refactor: improve test helpers and code quality#75

Merged
leszek3737 merged 2 commits into
mainfrom
fa5
Jun 12, 2026
Merged

refactor: improve test helpers and code quality#75
leszek3737 merged 2 commits into
mainfrom
fa5

Conversation

@leszek3737

Copy link
Copy Markdown
Owner
  • Consolidate test setup with helper functions (state_with_panels, make_history_picker, panel_with_files) to reduce duplication
  • Add set_hidden/set_executable methods to Cha for clearer mutation API
  • Deduplicate uid/gid cache logic with NameMapCache struct
  • Extract path_matches_any helper in watcher_sync
  • Restore debug_log for canonicalize failures in config loading
  • Replace unreachable! with error dialog fallback in render_dialog_map
  • Add documentation comments and TODO markers for future improvements
  • Improve test assertions with descriptive messages
  • Remove unused directory() helper and redundant MENU* constants

- Consolidate test setup with helper functions (state_with_panels,
  make_history_picker, panel_with_files) to reduce duplication
- Add set_hidden/set_executable methods to Cha for clearer mutation API
- Deduplicate uid/gid cache logic with NameMapCache struct
- Extract path_matches_any helper in watcher_sync
- Restore debug_log for canonicalize failures in config loading
- Replace unreachable! with error dialog fallback in render_dialog_map
- Add documentation comments and TODO markers for future improvements
- Improve test assertions with descriptive messages
- Remove unused _directory() helper and redundant MENU_* constants

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @leszek3737, your pull request is larger than the review limit of 150000 diff characters

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces various refactorings, documentation updates, and test cleanups across the codebase, notably consolidating menu constants into a single MENUS array, optimizing path matching in the watcher, and introducing set_hidden and set_executable APIs in Cha. Feedback on these changes highlights a regression in test coverage in src/app/keymap.rs due to the simplification of help message validation, and points out a redundant API in src/fs/cha.rs where both with_* and set_* mutators are kept.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/app/keymap.rs
Comment on lines 776 to 780
#[test]
fn build_help_message_lines_are_not_empty() {
fn build_help_message_is_non_empty() {
let msg = build_help_message();
for (i, line) in msg.lines().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
assert!(
trimmed.len() > 2,
"Line {i} is suspiciously short: {:?}",
line
);
}
assert!(!msg.is_empty(), "Help message must not be empty");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The test build_help_message_lines_are_not_empty was replaced with build_help_message_is_non_empty. While the new test is simpler, it's also significantly less thorough. The previous test verified that individual lines in the help message were not empty or suspiciously short, which could catch formatting bugs. The new test only confirms that the help message as a whole is not empty. This is a regression in test coverage.

I suggest restoring the more detailed line-by-line checks to ensure the help message is always formatted correctly.

    #[test]
    fn build_help_message_is_valid() {
        let msg = build_help_message();
        assert!(!msg.is_empty(), "Help message must not be empty");
        for (i, line) in msg.lines().enumerate() {
            let trimmed = line.trim();
            if trimmed.is_empty() {
                continue;
            }
            assert!(
                trimmed.len() > 2,
                "Line {i} is suspiciously short: {:?}",
                line
            );
        }
    }

Comment thread src/fs/cha.rs Outdated
Comment on lines 379 to 391
pub fn with_hidden(&mut self, hidden: bool) -> &mut Self {
self.kind.set(ChaKind::HIDDEN, hidden);
self
}

pub fn with_executable(mut self, executable: bool) -> Self {
pub fn with_executable(&mut self, executable: bool) -> &mut Self {
if executable {
self.mode = ChaMode::new(self.mode.mode_u32() | 0o111);
} else {
self.mode = ChaMode::new(self.mode.mode_u32() & !0o111);
}
self
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The new set_hidden and set_executable methods are a great addition for a clearer mutation API. However, the existing with_hidden and with_executable methods have been kept and modified to return &mut Self. This creates a redundant API where, for example, with_hidden and set_hidden have identical implementations apart from the return type.

To improve API clarity and reduce duplication, I suggest removing the with_* variants and exclusively using the new set_* methods throughout the codebase.

@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown

Greptile Summary

This PR is a broad refactor across 42 files: consolidating test setup with shared helper functions, extracting path_matches_any in the watcher sync, deduplicating uid/gid cache logic with NameMapCache, adding set_hidden/set_executable mutation methods to Cha, removing the redundant MENU_TITLES/MENU_ITEMS/MENU_ACTIONS constants, and improving test assertions throughout.

  • apply_panel (config.rs): The fallback path for canonicalize failures is now correctly applied with canonical_path = None instead of the old behaviour of setting canonical to an uncanonicalized path; however, the "canonicalize succeeded but not a dir" case no longer emits a debug log.
  • watcher_sync.rs: path_matches_any cleanly unifies two duplicated comparison sequences, but full_refresh_panel becomes a trivial one-line wrapper around the renamed refresh_panel_from_disk without updating call sites.
  • cha.rs / reader.rs: set_hidden/set_executable are the new canonical setters; the old with_hidden/with_executable remain public but have no callers, and os_str_to_arc contains a dead fallback in its ASCII branch.

Confidence Score: 4/5

Safe to merge — all changes are refactors and test improvements with no new feature risk; the watcher-sync path-matching logic is semantically equivalent to the old code, and the config panel-loading change correctly fixes the stale canonical_path bug.

The functional logic is sound throughout and test coverage is improved. The remaining rough edges — leftover dead public API, an unnecessary delegation wrapper, an unreachable fallback branch, and a lost debug-log path — do not affect runtime correctness but leave the codebase slightly untidier than the refactor intends.

src/fs/cha.rs (dead public methods), src/app/watcher_sync.rs (trivial wrapper), src/fs/reader.rs (dead branch in os_str_to_arc), src/app/config.rs (missing debug log for canonical-not-dir case)

Important Files Changed

Filename Overview
src/app/config.rs Refactors apply_panel to explicitly handle canonicalize failure with raw-path fallback; fixes a bug where canonical_path was set to an uncanonicalized path, but loses the debug log for the "canonicalize succeeded / not a dir" case.
src/app/watcher_sync.rs Extracts path_matches_any helper shared by event_is_panel_dir_cached and path_parent_matches_cached; renames full_refresh_panel to refresh_panel_from_disk but leaves full_refresh_panel as a trivial delegating wrapper with call sites unchanged.
src/fs/cha.rs Adds set_hidden/set_executable void-return mutators; changes with_hidden/with_executable to &mut self returning &mut Self, leaving them as public dead code with no remaining callers. Removes len() getter in favour of direct field access.
src/fs/reader.rs Deduplicates uid/gid cache with NameMapCache struct and get_or_insert_name helper; adds os_str_to_arc with an unreachable fallback in its ASCII branch. Switches tests from a manual temp-dir helper to tempfile::TempDir.
src/tests/helpers.rs Improves TestEntry builder: size() renamed to file() with explicit EntryKind enum, adds symlink() flag, and replaces hardcoded /tmp/ paths with std::env::temp_dir(). dummy_tree_entries gains three optional parameters that no current caller sets to non-None.
src/tests/dialogs.rs Adds helper no_viewer_state() to remove repeated type annotations; introduces _assert_dialog_contains which is unused dead code. Adds stronger post-condition assertions to several dialog tests.
src/menu.rs Removes redundant MENU_TITLES, MENU_ITEMS, MENU_ACTIONS constants; updates the compile-time length-consistency assert to use const {} block syntax.
src/ui/menu.rs All three removed constants replaced with direct MENUS[i] field access; clean, no functional changes.
src/tests/compare.rs Introduces state_with_panels and extract_confirm_details local helpers, upgrades assertions with descriptive messages, and adds a new mixed-types/symlinks test case.
src/tests/misc.rs Refactors IsolatedEnv to hold Option fields; adds _with_home (unused/underscore-prefixed) and empty/xdg constructors; adds three new config-load tests.
src/app/types/file_entry.rs Switches FileEntryBuilder::is_hidden/is_executable to use set_hidden/set_executable; changes size() / cha.len() call to direct field access cha.len.
src/render_dialog_map.rs Adds explanatory // Per-frame alloc; low cost for short strings. comments only; no functional changes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A([apply_panel called]) --> B{fs::canonicalize succeeds?}
    B -- Yes --> C{resolved.is_dir?}
    B -- No --> D[debug_log: canonicalize failed\nfalling back to raw path]
    C -- Yes --> E[set_path + set_canonical_path]
    C -- No --> F[silent drop\n⚠ no debug log]
    D --> G{raw path.is_dir?}
    G -- Yes --> H[set_path\nset_canonical_path = None]
    G -- No --> I[debug_log: path ignored]

    style F fill:#ffcccc,stroke:#cc0000
    style E fill:#ccffcc,stroke:#009900
    style H fill:#ccffcc,stroke:#009900
    style I fill:#ffffcc,stroke:#cccc00
Loading

Comments Outside Diff (1)

  1. src/app/config.rs, line 262-287 (link)

    P2 Missing debug log when canonical path is not a directory

    When canonicalize succeeds but the resolved path is not a directory (e.g., the user configured a file path as a panel path in config.toml), the new code silently does nothing. Previously, crate::debug_log!("configured panel path ignored: {}", path.display()) was emitted for this case. Without that log, a user whose panel path silently stops being applied after a config edit has no diagnostic trail to follow.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "refactor: improve test helpers and code ..." | Re-trigger Greptile

@opencode-agent

Copy link
Copy Markdown

The review is complete. The critical item is the flush_pending reordering bug in main.rs:275-283 — it should be fixed before merge. The remaining items are lower priority but worth addressing.

New%20session%20-%202026-06-12T17%3A21%3A55.387Z
opencode session  |  github run

Comment thread src/app/watcher_sync.rs Outdated
Comment on lines +530 to +532
fn full_refresh_panel(panel: &mut PanelState) {
refresh_panel_from_disk(panel);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 full_refresh_panel is now a trivial wrapper

The renamed function refresh_panel_from_disk contains all the logic, while full_refresh_panel just delegates to it. All call sites — full_refresh_panels at lines 329/332 and the tests — still call full_refresh_panel. The rename added indirection without consolidating the call sites, leaving an empty wrapper that will confuse future readers about which name is canonical.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread src/fs/reader.rs
Comment on lines +56 to +65
fn get_or_insert_name(
cache: &mut NameMapCache,
id: u32,
lookup: impl FnOnce(u32) -> Option<Arc<str>>,
) -> Arc<str> {
if let Some(name) = cache.map.get(&id) {
return name.clone();
}
if cache.map.len() >= CACHE_MAX_SIZE
&& let Some(old) = cache.order.pop_front()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Dead fallback branch in os_str_to_arc

Inside the bytes.is_ascii() branch, String::from_utf8(bytes.to_vec()) is called — but this can never fail, because ASCII is always valid UTF-8. The map_or_else error fallback || Arc::from(s.to_string_lossy().into_owned()) is therefore unreachable dead code. A direct std::str::from_utf8(bytes) (which can use unwrap safely here) or merging both branches into a single s.to_string_lossy().into_owned() call would be simpler and equally correct.

Comment thread src/fs/cha.rs
Comment on lines 376 to +397
&& self.gid == other.gid
}

pub fn with_hidden(mut self, hidden: bool) -> Self {
pub fn with_hidden(&mut self, hidden: bool) -> &mut Self {
self.kind.set(ChaKind::HIDDEN, hidden);
self
}

pub fn with_executable(mut self, executable: bool) -> Self {
pub fn with_executable(&mut self, executable: bool) -> &mut Self {
if executable {
self.mode = ChaMode::new(self.mode.mode_u32() | 0o111);
} else {
self.mode = ChaMode::new(self.mode.mode_u32() & !0o111);
}
self
}

pub fn set_hidden(&mut self, hidden: bool) {
self.kind.set(ChaKind::HIDDEN, hidden);
}

pub fn set_executable(&mut self, executable: bool) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 with_hidden / with_executable are now dead public API

Both methods were converted to &mut self returning &mut Self, and all callers were migrated to set_hidden / set_executable. No callers remain for with_hidden or with_executable. Since they are pub, they silently widen the API surface with methods that can only be called for their side-effects, not in a builder chain. They should either be removed or deprecated.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread src/tests/helpers.rs
Comment on lines 151 to 177
);
}

pub fn dummy_tree_entries(count: usize) -> Vec<TreeEntry> {
pub fn dummy_tree_entries(
count: usize,
depth: Option<usize>,
expanded: Option<bool>,
read_error: Option<bool>,
) -> Vec<TreeEntry> {
let depth = depth.unwrap_or(0);
let expanded = expanded.unwrap_or(false);
let read_error = read_error.unwrap_or(false);
(0..count)
.map(|i| {
let name = format!("entry-{i}");
let name_width = unicode_width::UnicodeWidthStr::width(name.as_str());
TreeEntry {
path: PathBuf::from(format!("/tmp/{i}")),
depth: 0,
path: std::env::temp_dir().join(format!("{i}")),
depth,
is_dir: false,
expanded: false,
expanded,
name,
name_width,
read_error: false,
read_error,
}
})
.collect()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 dummy_tree_entries gains three optional params that are always None at every call site

All call sites in the codebase pass (count, None, None, None). The added flexibility is not exercised anywhere, making the signature more verbose without benefit. Consider keeping the simple arity for the common case and introducing a dummy_tree_entries_with variant (or a builder/struct) for callers that actually need non-default values.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread src/tests/dialogs.rs Outdated
Comment on lines +13 to +25
None
}

fn _assert_dialog_contains(terminal: &mut Terminal<TestBackend>, state: &AppState, text: &str) {
terminal
.draw(|f| render::render_ui(f, state, None, None))
.unwrap();
let buf = terminal.backend().buffer();
let rendered = buffer_to_string(buf);
assert!(
rendered.contains(text),
"expected dialog to contain '{text}', got:\n{rendered}"
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 _assert_dialog_contains is unused dead code

The helper is added with a leading underscore to suppress the "unused" warning, but no test in this module calls it. Unused helpers in test files accumulate silently and make it harder to understand which utilities are actually exercised. If this is scaffolding for a planned test, a // TODO comment explaining what test it supports would clarify intent; otherwise it should be removed.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

- Remove trivial full_refresh_panel wrapper, call refresh_panel_from_disk directly
- Simplify os_str_to_arc: ASCII is always valid UTF-8, remove dead fallback branch
- Remove unused with_hidden/with_executable methods (replaced by set_* variants)
- Restore thorough line-by-line validation in build_help_message test
- Remove unused _assert_dialog_contains helper from dialogs tests
- Simplify dummy_tree_entries signature: remove optional params always passed as None
@leszek3737
leszek3737 merged commit 1257cca into main Jun 12, 2026
7 checks passed
@leszek3737
leszek3737 deleted the fa5 branch June 12, 2026 17:37
@opencode-agent

Copy link
Copy Markdown

PR Review: refactor: improve test helpers and code quality

CI status: GREENcargo fmt, cargo clippy --locked --all-targets -- -D warnings, and cargo test --locked (231 tests) all pass cleanly.


Critical Issues

None found. The earlier comment about a flush_pending reordering "bug" in main.rs:275-283 is not a bug — the move is intentional and correct. Previously, flush_pending() ran unconditionally every loop iteration even when no watcher events were queued. Now it only runs inside if poll_watcher_events(...) (which already drained the channel), meaning flush_pending() processes debounced events that arrived during that poll cycle. This is a minor optimization that avoids a wasted lock acquisition on idle loops. The ordering dependency (flush after poll) is preserved within the dirty = true block.


Medium Priority Issues

  1. Dead public API: with_hidden / with_executable removed (src/fs/cha.rs:376-391)
    The old builder-pattern methods were fully replaced by set_hidden/set_executable. The PR correctly removed them — no callers remain. Good.

  2. Missing debug log: canonicalize succeeded but not a directory (src/app/config.rs:269-273)
    When fs::canonicalize succeeds but the resolved path is not a directory, the code silently does nothing. Previously a debug_log! was emitted. This diagnostic gap could confuse users whose panel path silently stops loading after a config edit. Consider adding:

    if c.is_dir() { ... } else {
        crate::debug_log!("configured panel path is not a directory: {}", c.display());
    }
  3. Trivial wrapper: full_refresh_panel renamed but not removed (src/app/watcher_sync.rs:492)
    The function was renamed to refresh_panel_from_disk and made pub(crate), with call sites in full_refresh_panels updated. No dead wrapper remains. Clean.

  4. Dead code in os_str_to_arc ASCII branch (src/fs/reader.rs:65-71)
    The ASCII branch uses std::str::from_utf8(bytes).unwrap() with an #[allow(clippy::unwrap_used)] — this is correct since ASCII is always valid UTF-8. The earlier review flagged a dead fallback that was present in the first commit but the second commit fixed it. Current code is clean.

  5. _assert_dialog_contains unused helper (src/tests/dialogs.rs)
    No such function exists in the final code. Clean.

  6. dummy_tree_entries unused optional params (src/tests/helpers.rs:169)
    The function signature only takes count — no unused optional parameters in the final code. Clean.


Low Priority / Observations

  • render.rs refactor: The if/else if chain for AppMode::Viewing/DirectoryTree was replaced with a match block. This is stylistically cleaner and functionally equivalent.

  • Menu constants removal (src/menu.rs): MENU_TITLES, MENU_ITEMS, MENU_ACTIONS were replaced with direct MENUS[i] field access. The compile-time assertion uses const {} block syntax (Rust 2024 edition). All call sites updated.

  • lib.rs new exports: CompareReport, apply_compare_to_panels, compare_entries, chmod, create_directory, rename_entry, cycle_sort_mode, sort_entries were added to the public API. These appear to support the test consolidation.

  • panel_ops.rsrestore_panel_selection split: The restore_selection_for helper avoids duplicating the loop for entries and unfiltered_entries. Also, restore_panel_cursor now uses saturating_sub(1) instead of a conditional — handles the empty-list case correctly.

  • mime.rscategory_from_ext path traversal fix: Now extracts file_name from the input via Path::new(name).file_name() before looking up the extension. This prevents a path like "foo/bar.txt" from incorrectly matching "bar" as the extension.

  • text_input.rs: is_whitespace_grapheme was lifted from an associated function to a module-level function, and delete_grapheme_at was extracted from the inline drain logic. Both are clean refactorings.

  • watcher.rs: entry().or_insert(...) replaced with entry().and_modify(...).or_insert_with(...) to avoid silently dropping coalesced events when a debounce entry already exists. This is a correctness improvement.

  • Test coverage improvements: New tests for config loading (invalid TOML, missing XDG/HOME), compare with mixed types/symlinks, history picker edge cases (beyond-length index, empty list navigation), non-printable keys, and watcher skip_debounce behavior.

  • TODO comments: Many // TODO: markers were added throughout. These are useful for future work but the volume is high (~20+). Consider tracking these as issues rather than in-code markers to avoid comment rot.


Summary

This is a well-structured refactoring PR. The core changes — test helper consolidation, NameMapCache deduplication, path_matches_any extraction, Cha mutation API, and menu constant removal — are all clean improvements. CI passes fully. The one actionable item is restoring the debug log for the canonicalize-succeeded-but-not-a-directory case in config.rs, which would help with future diagnostics. Otherwise, this is ready to merge.

New%20session%20-%202026-06-12T17%3A34%3A29.839Z
opencode session  |  github run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant