feat: load fixtures from pytest entry_points#78
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the fixture scanner to discover and scan pytest plugins dynamically via pytest11 entry points (instead of relying on a hardcoded plugin-name list), aligning plugin discovery with pytest’s standard mechanism and addressing #75.
Changes:
- Add parsing of
entry_points.txtto extract[pytest11]plugin registrations. - Resolve discovered plugin module paths to files under
site-packagesand scan them for fixtures. - Add unit/integration tests covering entry point parsing, module resolution, and discovery behavior.
Comments suppressed due to low confidence (1)
src/fixtures/scanner.rs:598
std::fs::read_dir(site_packages).into_iter().flatten()silently turns aread_direrror (e.g., permission denied) into an empty iteration, which makes plugin discovery fail without any warning. Consider explicitly handling theErr(e)fromread_dirand logging (and/or surfacing) the failure.
// Iterate over ALL dist-info directories and check for pytest11 entry points
for entry in std::fs::read_dir(site_packages).into_iter().flatten() {
let entry = match entry {
Ok(e) => e,
Err(_) => continue,
};
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| self.scan_single_plugin_file(&path); | ||
| scanned_count += 1; |
There was a problem hiding this comment.
load_plugin_from_entry_point only scans the resolved module file (often __init__.py when the entry point points at a package). Since the analyzer doesn’t follow imports, this will miss fixtures defined in submodules that the entry-point module imports (a common plugin layout). Consider scanning the owning package directory (e.g., the directory containing __init__.py) with scan_plugin_directory, or adding an import-following pass for plugin modules.
| self.scan_single_plugin_file(&path); | |
| scanned_count += 1; | |
| // If the resolved path is a package's __init__.py, scan the whole package directory | |
| let scanned = if path.is_file() { | |
| let is_init = path | |
| .file_name() | |
| .and_then(|name| name.to_str()) | |
| .map(|name| name == "__init__.py") | |
| .unwrap_or(false); | |
| if is_init { | |
| if let Some(package_dir) = path.parent() { | |
| info!( | |
| "Scanning pytest plugin package directory for {}: {:?}", | |
| entry.name, package_dir | |
| ); | |
| self.scan_plugin_directory(package_dir); | |
| true | |
| } else { | |
| // Fallback: scan the __init__.py file itself | |
| self.scan_single_plugin_file(&path); | |
| true | |
| } | |
| } else { | |
| self.scan_single_plugin_file(&path); | |
| true | |
| } | |
| } else if path.is_dir() { | |
| info!( | |
| "Scanning pytest plugin directory for {}: {:?}", | |
| entry.name, path | |
| ); | |
| self.scan_plugin_directory(&path); | |
| true | |
| } else { | |
| debug!( | |
| "Resolved module path for plugin {} is neither file nor directory: {:?}", | |
| entry.name, path | |
| ); | |
| false | |
| }; | |
| if scanned { | |
| scanned_count += 1; | |
| } |
| // Only process .dist-info directories | ||
| if !filename.ends_with(".dist-info") { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
Plugin discovery currently only considers *.dist-info metadata directories. Some environments still install distributions with *.egg-info/entry_points.txt (including some non-editable/legacy installs), which would cause pytest plugins to be missed. If supporting those environments matters, consider also checking *.egg-info for entry points (or at least documenting this limitation).
| /// Parse entry_points.txt content and extract pytest11 entries. | ||
| /// Returns empty vec if no pytest11 section or file is malformed. |
There was a problem hiding this comment.
The doc comment says this returns an empty vec when the file is malformed, but the implementation currently just skips lines that don’t match name = value and returns any successfully parsed entries. Either tighten the parsing to treat malformed [pytest11] content as an error (and return empty), or adjust the comment to match the current behavior.
| /// Parse entry_points.txt content and extract pytest11 entries. | |
| /// Returns empty vec if no pytest11 section or file is malformed. | |
| /// Parse `entry_points.txt` content and extract pytest11 entries. | |
| /// | |
| /// Returns all successfully parsed entries from the `[pytest11]` section. | |
| /// Returns an empty vec if there is no `[pytest11]` section or no valid | |
| /// `name = value` lines within that section. Malformed lines are ignored. |
| // Try as a single-file module at top level (for single-part module paths) | ||
| if parts.len() == 1 { | ||
| let single_file = site_packages.join(format!("{}.py", parts[0])); | ||
| if single_file.exists() { | ||
| return Some(single_file); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
The parts.len() == 1 fallback is redundant: for single-part module paths the earlier path.with_extension("py") check already looks for site_packages/<module>.py. Removing this branch would simplify the resolver and avoid extra filesystem calls.
| // Try as a single-file module at top level (for single-part module paths) | |
| if parts.len() == 1 { | |
| let single_file = site_packages.join(format!("{}.py", parts[0])); | |
| if single_file.exists() { | |
| return Some(single_file); | |
| } | |
| } |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #78 +/- ##
==========================================
+ Coverage 50.55% 52.62% +2.07%
==========================================
Files 26 26
Lines 2702 2757 +55
==========================================
+ Hits 1366 1451 +85
+ Misses 1336 1306 -30 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Fixed #75.