103 """Return (testpaths, python_files) declared in ``pytest.ini``."""
104 func =
"_read_pytest_ini"
105 path = root /
"pytest.ini"
106 if not path.is_file():
108 parser = configparser.ConfigParser()
110 parser.read(path, encoding=
"utf-8")
111 except (OSError, configparser.Error)
as exc:
112 _dbg(
"OC_TESTING", func, f
"unreadable pytest.ini: {exc}", 2)
114 if not parser.has_section(
"pytest"):
116 testpaths = parser.get(
"pytest",
"testpaths", fallback=
"").split()
117 python_files = parser.get(
"pytest",
"python_files", fallback=
None)
118 return testpaths, python_files
122 """ADR-T-12 discovery: walk upward from *cwd* for a package config.
124 Returns ``(result, None)`` or ``(None, error)`` where *error* is the
125 ``CWD_RESOLUTION_FAILED`` entry with the pinned message.
127 func =
"walk_for_package"
128 current = Path(cwd).resolve()
129 candidates = [current, *current.parents[:_WALK_LEVELS]]
130 root: Path |
None =
None
131 config_file: Path |
None =
None
132 for candidate
in candidates:
133 for name
in _CONFIG_NAMES:
134 if (candidate / name).is_file():
136 config_file = candidate / name
140 if root
is None or config_file
is None:
141 _dbg(
"OC_TESTING", func, f
"resolution failed from {cwd}", 2)
142 return None, make_error(
143 "configuration",
"CWD_RESOLUTION_FAILED",
"fatal",
144 _CWD_RESOLUTION_MESSAGE,
150 ini_options = pyproject.get(
"tool", {}).get(
"pytest", {}).get(
152 declared = ini_options.get(
"testpaths", [])
153 if isinstance(declared, str):
154 declared = declared.split()
155 testpaths = [str(p)
for p
in declared]
156 if python_files
is None:
157 pf = ini_options.get(
"python_files")
158 if isinstance(pf, list):
159 python_files =
" ".join(pf)
160 elif isinstance(pf, str):
163 oct_test = pyproject.get(
"tool", {}).get(
"oct", {}).get(
"test", {})
164 package_id = str(oct_test.get(
"package_id")
or root.name)
165 profile_pin = oct_test.get(
"profile")
167 roots = [root / p
for p
in testpaths]
if testpaths
else [root]
170 config_file=config_file,
171 package_id=package_id,
174 python_files=python_files
or "test_*.py",
175 profile_pin=str(profile_pin)
if profile_pin
else None,
177 _dbg(
"OC_TESTING", func,
178 f
"root={root} id={package_id} roots={len(roots)}", 3)
188 declared: list[Path],
189 python_files: str =
"test_*.py",
191 """§10 scan: directories under *root* holding test files outside the
194 The nested-package boundary rule: a non-root directory carrying its
195 own package config is a different package and is pruned, unless a
196 declared root lies at-or-under it.
198 func =
"scan_undeclared_roots"
199 root = Path(root).resolve()
200 declared_resolved = [Path(d).resolve()
for d
in declared]
201 patterns = python_files.split()
202 findings: set[Path] = set()
204 def _under(path: Path, ancestor: Path) -> bool:
205 return path == ancestor
or ancestor
in path.parents
207 for dirpath, dirnames, filenames
in os.walk(root):
208 current = Path(dirpath)
210 for name
in sorted(dirnames):
211 child = current / name
212 if name.startswith(
".")
or name
in _SKIP_DIR_NAMES:
214 if child != root
and any(
215 (child / marker).is_file()
for marker
in _BOUNDARY_NAMES
216 )
and not any(_under(d, child)
for d
in declared_resolved):
217 _dbg(
"OC_TESTING", func, f
"boundary pruned: {child}", 4)
222 if any(_under(current, d)
for d
in declared_resolved):
225 findings.add(current)
227 result = sorted(findings)
228 _dbg(
"OC_TESTING", func, f
"{len(result)} undeclared root(s)", 3)