Skip to content

test: improve coverage with focused seam tests#111

Merged
seapagan merged 5 commits intomainfrom
test/improve-coverage
Apr 4, 2026
Merged

test: improve coverage with focused seam tests#111
seapagan merged 5 commits intomainfrom
test/improve-coverage

Conversation

@seapagan
Copy link
Copy Markdown
Owner

@seapagan seapagan commented Apr 3, 2026

This improves test coverage for the new feature work while continuing the shift toward behavior-focused checks at the app, settings, and render seams instead of broad smoke-style coverage.

The branch moves the relevant inline src tests into tests/, adds focused seam coverage for the file and gitignore helpers, and keeps the extracted helper boundaries tighter by using crate-local tests and pub(crate) visibility where possible instead of widening the public API just for test access.

The result is broader and more accurate production-code coverage, with tests centered on module behavior and boundary contracts rather than incidental implementation details.

Summary by CodeRabbit

  • Tests

    • Moved many unit tests into a dedicated test suite and added broad integration/unit coverage across CLI, file handling, rendering, gitignore, icons and configuration.
  • Refactor

    • Internal restructuring of listing, formatting and rendering logic to centralise table building and symlink/size formatting for more consistent output.
  • Bug Fixes

    • Improved robustness around missing paths, broken symlinks, gitignore resolution and malformed configs; better fallback formatting for version/metadata.

seapagan added 2 commits April 3, 2026 19:55
Signed-off-by: Grant Ramsay <seapagan@gmail.com>
Signed-off-by: Grant Ramsay <seapagan@gmail.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 926a7756-6c2f-4568-b48e-56b342494bf8

📥 Commits

Reviewing files that changed from the base of the PR and between 2fa69f6 and ccfe765.

📒 Files selected for processing (4)
  • src/utils/gitignore.rs
  • tests/crate/file.rs
  • tests/crate/gitignore.rs
  • tests/main.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/main.rs
  • tests/crate/gitignore.rs
  • tests/crate/file.rs
  • src/utils/gitignore.rs

📝 Walkthrough

Walkthrough

Refactors internal helpers into crate-visible functions, extracts many helpers from source modules, removes inline #[cfg(test)] unit modules, and relocates tests into a comprehensive external test suite under tests/ and tests/crate/.

Changes

Cohort / File(s) Summary
App & matching
src/app.rs
Promoted patterns_from_args to pub(crate), added pub(crate) fn collect_matches(...) and refactored run_multi to delegate match aggregation. Removed inline tests.
CLI metadata
src/cli.rs
Added pub(crate) fn format_version_info(...) and simplified version_info() to delegate formatting. Removed inline tests.
Settings & config
src/settings.rs
Added pub(crate) fn config_path_from_home(...), made load_config_from_path(...) pub(crate), and delegated config_path() to the new helper. Removed inline tests.
Structs (tests removed)
src/structs.rs
Deleted the module-local test suite; no production API changes.
File utilities
src/utils/file.rs
Introduced DirectoryEntryData, collect_visible_file_names, append_file_info_for_names, format_symlink_display_name, format_path_error, made sanitize_for_terminal pub(crate), refactored collection/filtering/sorting and directory recursion. Removed inline tests.
Formatting & fuzzy time (tests removed)
src/utils/format.rs, src/utils/fuzzy_time.rs
Removed test modules; production formatting and fuzzy-time APIs unchanged.
Gitignore helpers
src/utils/gitignore.rs
Made several helpers pub(crate) (collect_gitignore_files, parse_gitdir_file, parse_commondir), added matcher_ignores_path and test-only find_git_paths_parts, changed build behavior to return empty matcher on build errors, and removed inline tests.
Icons
src/utils/icons.rs
Promoted has_extension to pub(crate) and removed inline tests.
Render/table building
src/utils/render.rs
Moved table construction to pub(crate) builders (build_long_format_table, build_short_format_table) and added terminal_width_or_default; display_*_format now delegates to builders. Removed inline tests.
Library test wiring
src/lib.rs
Added #[cfg(test)] declarations linking external test modules (app_tests, cli_tests, file_tests, gitignore_tests, icons_tests, render_tests, settings_tests).
External tests added
tests/crate/*.rs, tests/*.rs
Added comprehensive external test suite files (unit and integration tests) covering app/CLI, file utilities, gitignore, icons, renderers, settings, formatting and fuzzy-time behaviour, and seam tests.

Sequence Diagram(s)

(Not applicable; changes are refactors and test reorganisation without new multi-component sequential flows.)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • feat: dim gitignored entries #110 — modifies gitignore integration and file-listing code; shares overlap with the gitignore and file-collection helpers changed here.

Poem

🐇 I hopped through code, nibbling helpers anew,

Tests moved to meadows where tempdirs grew,
Crate-visible carrots, tidy and bright,
Helpers now leap in the moonlit night,
Thump-thump, I celebrate this tidy delight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarises the main objective of the changeset: moving tests from source files to dedicated test files and improving test coverage through focused integration tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/improve-coverage

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codacy-production
Copy link
Copy Markdown

codacy-production bot commented Apr 3, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics -95 complexity

Metric Results
Complexity -95

View in Codacy

TIP This summary will be updated as you push new changes. Give us feedback

@seapagan seapagan changed the title test: shift seam coverage out of src test: improve coverage with focused seam tests Apr 3, 2026
@seapagan seapagan self-assigned this Apr 3, 2026
@seapagan seapagan marked this pull request as ready for review April 4, 2026 06:27
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (5)
tests/format.rs (2)

48-67: Consider removing duplicated assertions in test_format_size_extreme.

Lines 50-51, 56-57, 59-60, and 62-63 duplicate assertions already covered in test_format_size. The only unique assertion in this test is the PB case (lines 53-54) and the boundary case at 1 MB - 1 (lines 65-66). Consider renaming or trimming this test to focus on truly extreme or boundary values.

♻️ Suggested refactor to remove duplicates
 #[test]
 fn test_format_size_extreme() {
-    let (size, unit) = human_readable_format(1024 * 1024 * 1024 * 1024);
-    assert_eq!(format!("{:.1} {}", size, unit), "1.0 TB");
-
     let (size, unit) = human_readable_format(1024 * 1024 * 1024 * 1024 * 1024);
     assert_eq!(format!("{:.1} {}", size, unit), "1.0 PB");
 
-    let (size, unit) = human_readable_format(1024);
-    assert_eq!(format!("{:.1} {}", size, unit), "1.0 KB");
-
-    let (size, unit) = human_readable_format(1024 * 1024);
-    assert_eq!(format!("{:.1} {}", size, unit), "1.0 MB");
-
-    let (size, unit) = human_readable_format(1023);
-    assert_eq!(format!("{:.1} {}", size, unit), "1023.0 B");
-
     let (size, unit) = human_readable_format(1024 * 1024 - 1);
     assert_eq!(format!("{:.1} {}", size, unit), "1024.0 KB");
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/format.rs` around lines 48 - 67, The test test_format_size_extreme
duplicates many assertions from test_format_size; trim it to only the truly
extreme/boundary cases by removing the redundant checks for 1 KB, 1 MB, and 1023
B and keeping the PB case (human_readable_format(1024^5)) and the boundary case
just below 1 MB (human_readable_format(1024 * 1024 - 1)); optionally rename the
test to test_format_size_boundaries or test_format_size_extremes to reflect its
focused purpose and update assertions to only validate those remaining cases
using the human_readable_format function and its returned (size, unit) tuple.

69-81: Minor overlap between test_format_mode and test_format_mode_permissions.

Both tests assert mode_to_rwx(0o777) returns "rwxrwxrwx". Consider consolidating or ensuring each test has a distinct purpose.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/format.rs` around lines 69 - 81, Two tests, test_format_mode and
test_format_mode_permissions, both assert mode_to_rwx(0o777) == "rwxrwxrwx"
which is redundant; update one of the tests so each has a distinct purpose —
either remove the duplicated assertion from test_format_mode or replace it in
test_format_mode_permissions with a different meaningful case (e.g., a mixed
permission like 0o711 or 0o600) to cover additional behavior, keeping references
to mode_to_rwx, test_format_mode, and test_format_mode_permissions to locate the
assertions.
tests/crate/icons.rs (1)

13-33: Test relies on Cargo.toml existing in working directory.

Line 15 uses fs::metadata("Cargo.toml") to obtain metadata for testing icon selection. This works for cargo test but could be fragile if tests are run from a different directory. Consider using tempfile to create a test file with known metadata for better isolation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/crate/icons.rs` around lines 13 - 33, The test currently depends on an
external Cargo.toml via fs::metadata("Cargo.toml") which is brittle; modify
test_get_item_icon_uses_known_file_types_and_names to create an isolated
tempfile::TempDir, create files named "test.unknown", "test.rs", "Cargo.toml",
and "nested/config/.gitignore" inside that temp dir (using
std::fs::create_dir_all and File::create), obtain each file's metadata and pass
Path::new relative to that tempdir to get_item_icon, then assert icons as
before; this keeps the test independent of the working directory and uses the
same function names (get_item_icon) and test name to locate the change.
tests/crate/render.rs (1)

56-75: Fuzzy time test looks correct but verify boundary behaviour.

The test creates a timestamp exactly 2 hours ago and expects "2 hours ago". This should be deterministic, but if the fuzzy time implementation has boundary conditions (e.g., 2 hours and 1 second showing "2 hours ago" vs "3 hours ago"), minor timing variations during test execution could cause flakiness. Consider using a more robust time offset (e.g., 2.5 hours) to avoid potential boundary issues.

♻️ Suggested safer time offset
     let info = test_file_info(
         "aged.txt",
         None,
         128,
         SystemTime::now()
-            .checked_sub(Duration::from_secs(2 * 60 * 60))
+            .checked_sub(Duration::from_secs(2 * 60 * 60 + 30 * 60)) // 2.5 hours
             .unwrap(),
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/crate/render.rs` around lines 56 - 75, The test
test_build_long_format_table_uses_fuzzy_time_when_requested uses an exact 2-hour
offset which risks flakiness at boundary conditions; change the timestamp
subtraction in the call to test_file_info to use a safer offset (e.g., 2.5
hours) by replacing Duration::from_secs(2 * 60 * 60) with a duration
representing 2.5 hours (such as Duration::from_secs(2 * 60 * 60 + 30 * 60)) so
Params { fuzzy_time: true } and the assertions still validate "2 hours ago"
without timing boundary issues.
tests/crate/settings.rs (1)

14-21: Unix-specific path assumption in test.

Line 18 uses /tmp/home which assumes a Unix filesystem. If Windows support is needed, consider using PathBuf::from("C:\\tmp\\home") conditionally or a platform-agnostic approach. However, this appears acceptable given the crate's Unix-focused nature (permissions, symlinks, etc.).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/crate/settings.rs` around lines 14 - 21, The test uses a hard-coded
Unix-style path string; update test_config_path_from_home to construct the input
and expected PathBufs using PathBuf and join (e.g., let base =
PathBuf::from("/tmp/home") or use tempfile::tempdir() for a temp dir, then let
expected = base.join(".config").join("lsplus").join("config.toml")) so the
assertion against config_path_from_home(Some(base)) is platform-agnostic; modify
the test function name test_config_path_from_home_handles_some_and_none and
references to use these PathBuf joins instead of a literal "/tmp/home" string.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/utils/gitignore.rs`:
- Around line 48-53: Replace the panic-on-error behavior when parsing repository
ignore files by handling builder.build() failures gracefully: instead of calling
builder.build().expect(...), catch the error and return None or an empty matcher
for gitignore_matcher so directory listing continues; apply the same
non-panicking pattern used by build_git_global_matcher for the other parser (the
expect at the info/exclude parsing referenced by build_git_exclude_matcher).
Locate the builder.build() call that produces gitignore_matcher and the other
expect around the info/exclude parsing, replace expects with error handling that
logs/debugs the parse error and yields a no-op matcher or None to preserve
existing behavior rather than crashing.

In `@tests/crate/file.rs`:
- Around line 1-21: The file imports std::os::unix::fs::PermissionsExt
unconditionally while tests like
test_create_file_info_handles_executables_and_large_files and
test_create_file_info_handles_regular_files_symlinks_and_special_cases call
Unix-only APIs (fs::Permissions::from_mode and std::os::unix::fs::symlink),
causing Windows compilation failures; fix by gating the Unix-only import and/or
tests with #[cfg(unix)] — either mark the PermissionsExt import with
#[cfg(unix)] and add #[cfg(unix)] on the two test functions (or wrap the whole
file with #[cfg(unix)] if all tests are Unix-only) so the Unix-specific symbols
are only compiled on Unix platforms.

In `@tests/main.rs`:
- Around line 9-23: The test uses a 0o000 directory (blocked_dir) to force a
failure, which is flaky on elevated runners; change the test to use a
deterministic failure input or skip when privileged: either (a) create a regular
temp file (e.g., temp_file.path()) and pass that path to
Command::cargo_bin("lsp") instead of blocked_dir so the binary errors
deterministically when a directory is expected, or (b) detect elevated
privileges at test start (e.g., via geteuid() == 0 using nix or users crate) and
early-return/skip the test; update the assertions accordingly and remove the
permission toggling on blocked_dir.

---

Nitpick comments:
In `@tests/crate/icons.rs`:
- Around line 13-33: The test currently depends on an external Cargo.toml via
fs::metadata("Cargo.toml") which is brittle; modify
test_get_item_icon_uses_known_file_types_and_names to create an isolated
tempfile::TempDir, create files named "test.unknown", "test.rs", "Cargo.toml",
and "nested/config/.gitignore" inside that temp dir (using
std::fs::create_dir_all and File::create), obtain each file's metadata and pass
Path::new relative to that tempdir to get_item_icon, then assert icons as
before; this keeps the test independent of the working directory and uses the
same function names (get_item_icon) and test name to locate the change.

In `@tests/crate/render.rs`:
- Around line 56-75: The test
test_build_long_format_table_uses_fuzzy_time_when_requested uses an exact 2-hour
offset which risks flakiness at boundary conditions; change the timestamp
subtraction in the call to test_file_info to use a safer offset (e.g., 2.5
hours) by replacing Duration::from_secs(2 * 60 * 60) with a duration
representing 2.5 hours (such as Duration::from_secs(2 * 60 * 60 + 30 * 60)) so
Params { fuzzy_time: true } and the assertions still validate "2 hours ago"
without timing boundary issues.

In `@tests/crate/settings.rs`:
- Around line 14-21: The test uses a hard-coded Unix-style path string; update
test_config_path_from_home to construct the input and expected PathBufs using
PathBuf and join (e.g., let base = PathBuf::from("/tmp/home") or use
tempfile::tempdir() for a temp dir, then let expected =
base.join(".config").join("lsplus").join("config.toml")) so the assertion
against config_path_from_home(Some(base)) is platform-agnostic; modify the test
function name test_config_path_from_home_handles_some_and_none and references to
use these PathBuf joins instead of a literal "/tmp/home" string.

In `@tests/format.rs`:
- Around line 48-67: The test test_format_size_extreme duplicates many
assertions from test_format_size; trim it to only the truly extreme/boundary
cases by removing the redundant checks for 1 KB, 1 MB, and 1023 B and keeping
the PB case (human_readable_format(1024^5)) and the boundary case just below 1
MB (human_readable_format(1024 * 1024 - 1)); optionally rename the test to
test_format_size_boundaries or test_format_size_extremes to reflect its focused
purpose and update assertions to only validate those remaining cases using the
human_readable_format function and its returned (size, unit) tuple.
- Around line 69-81: Two tests, test_format_mode and
test_format_mode_permissions, both assert mode_to_rwx(0o777) == "rwxrwxrwx"
which is redundant; update one of the tests so each has a distinct purpose —
either remove the duplicated assertion from test_format_mode or replace it in
test_format_mode_permissions with a different meaningful case (e.g., a mixed
permission like 0o711 or 0o600) to cover additional behavior, keeping references
to mode_to_rwx, test_format_mode, and test_format_mode_permissions to locate the
assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 51c6996c-e6ca-4e0f-aa96-3e8ab122fe88

📥 Commits

Reviewing files that changed from the base of the PR and between 152b828 and 2fa69f6.

📒 Files selected for processing (23)
  • src/app.rs
  • src/cli.rs
  • src/lib.rs
  • src/settings.rs
  • src/structs.rs
  • src/utils/file.rs
  • src/utils/format.rs
  • src/utils/fuzzy_time.rs
  • src/utils/gitignore.rs
  • src/utils/icons.rs
  • src/utils/render.rs
  • tests/crate/app.rs
  • tests/crate/cli.rs
  • tests/crate/file.rs
  • tests/crate/gitignore.rs
  • tests/crate/icons.rs
  • tests/crate/render.rs
  • tests/crate/settings.rs
  • tests/format.rs
  • tests/fuzzy_time.rs
  • tests/main.rs
  • tests/seams.rs
  • tests/structs.rs
💤 Files with no reviewable changes (3)
  • src/utils/format.rs
  • src/structs.rs
  • src/utils/fuzzy_time.rs

Comment on lines +9 to +23
let temp_dir = tempdir().unwrap();
let blocked_dir = temp_dir.path().join("blocked");
fs::create_dir(&blocked_dir).unwrap();
fs::set_permissions(&blocked_dir, fs::Permissions::from_mode(0o000))
.unwrap();

let mut cmd = Command::cargo_bin("lsp").unwrap();
cmd.arg(&blocked_dir)
.assert()
.failure()
.stderr(contains("Error:"));

fs::set_permissions(&blocked_dir, fs::Permissions::from_mode(0o755))
.unwrap();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This failure path is runner-dependent.

A 0o000 directory only guarantees an error for an unprivileged process. On root-capable CI runners the binary can still read it, so this assertion becomes flaky. Please switch to a deterministic error input, or explicitly skip when running with elevated privileges.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/main.rs` around lines 9 - 23, The test uses a 0o000 directory
(blocked_dir) to force a failure, which is flaky on elevated runners; change the
test to use a deterministic failure input or skip when privileged: either (a)
create a regular temp file (e.g., temp_file.path()) and pass that path to
Command::cargo_bin("lsp") instead of blocked_dir so the binary errors
deterministically when a directory is expected, or (b) detect elevated
privileges at test start (e.g., via geteuid() == 0 using nix or users crate) and
early-return/skip the test; update the assertions accordingly and remove the
permission toggling on blocked_dir.

seapagan added 3 commits April 4, 2026 07:47
Signed-off-by: Grant Ramsay <seapagan@gmail.com>
Signed-off-by: Grant Ramsay <seapagan@gmail.com>
Signed-off-by: Grant Ramsay <seapagan@gmail.com>
@seapagan seapagan merged commit 59f7d27 into main Apr 4, 2026
5 checks passed
@seapagan seapagan deleted the test/improve-coverage branch April 4, 2026 07:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant