diff --git a/tools/test_build_system/conftest.py b/tools/test_build_system/conftest.py index 0fb2ec88673..ad789dcb82d 100644 --- a/tools/test_build_system/conftest.py +++ b/tools/test_build_system/conftest.py @@ -67,10 +67,13 @@ def _create_idf_copy_via_worktree(path_from: Path, path_to: Path) -> str: src_submodule = path_from / submodule_rel_path dst_submodule = path_to / submodule_rel_path - # Only copy if source submodule exists and has content - if src_submodule.exists() and any(src_submodule.iterdir()): + # Only copy if the source submodule is a populated directory. A gitlink + # file or empty dir means the source checkout did not materialize it. + if src_submodule.is_dir() and any(src_submodule.iterdir()): logging.debug(f'copying submodule {submodule_rel_path}') - # Worktree submodule paths may be gitlink files; rmtree() does not remove those. + # Worktree submodule paths are often gitlink files; rmtree() cannot + # remove those (even with ignore_errors=True), and copytree() then + # leaves a file where CMake expects a directory (e.g. mbedtls/include). if dst_submodule.is_file() or dst_submodule.is_symlink(): dst_submodule.unlink() elif dst_submodule.exists(): diff --git a/tools/test_build_system/test_build_system_helpers/idf_utils.py b/tools/test_build_system/test_build_system_helpers/idf_utils.py index e0f222ee4d3..357c0c5faa9 100644 --- a/tools/test_build_system/test_build_system_helpers/idf_utils.py +++ b/tools/test_build_system/test_build_system_helpers/idf_utils.py @@ -19,6 +19,55 @@ EnvDict = dict[str, str] IdfPyFunc = typing.Callable[..., subprocess.CompletedProcess] +_LOG_ERROR_MARKERS = ( + 'CMake Error', + 'FAILED:', + 'fatal error', + 'ninja: build stopped', + 'HINT:', +) + + +def _clip_log_output(text: str | None, max_lines: int = 80, max_line_len: int = 400) -> str: + """Last ``max_lines`` of process output for logging, plus failure lines. + + pytest.ini enables ``log_cli``, so ``logging.error(full_stdout)`` after a + failed build is one record. On Windows CI that live-log can stall for hours + even when the line count is small: CMake's ``-- Component paths:`` line is + a single multi-KB (sometimes multi-MB) string. + """ + if not text: + return '' + lines = text.splitlines() + + def _short(line: str) -> str: + if len(line) <= max_line_len: + return line + return line[:max_line_len] + f'... [{len(line) - max_line_len} chars omitted]' + + omitted = max(0, len(lines) - max_lines) + tail_start = len(lines) - max_lines if omitted else 0 + tail = lines[tail_start:] + + failures: list[str] = [] + for idx, line in enumerate(lines): + if idx >= tail_start: + break + if any(marker in line for marker in _LOG_ERROR_MARKERS): + failures.append(line) + if len(failures) >= 40: + break + + parts: list[str] = [] + if failures: + parts.append('[... failure lines ...]') + parts.extend(_short(line) for line in failures) + if omitted: + parts.append(f'[... {omitted} lines omitted ...]') + parts.extend(_short(line) for line in tail) + return '\n'.join(parts) + + def normalize_output(text: str) -> str: """Collapse all whitespace runs to a single space. @@ -108,8 +157,8 @@ def run_idf_py( except subprocess.CalledProcessError as e: logging.error('The following idf.py command has failed: {}'.format(' '.join(cmd))) logging.error(f'Working directory: {workdir}') - logging.error(f'Stdout: {e.stdout}') - logging.error(f'Stderr: {e.stderr}') + logging.error(f'Stdout: {_clip_log_output(e.stdout)}') + logging.error(f'Stderr: {_clip_log_output(e.stderr)}') raise @@ -153,8 +202,8 @@ def run_cmake( except subprocess.CalledProcessError as e: logging.error('The following cmake command has failed: {}'.format(' '.join(cmd))) logging.error(f'Working directory: {workdir}') - logging.error(f'Stdout: {e.stdout}') - logging.error(f'Stderr: {e.stderr}') + logging.error(f'Stdout: {_clip_log_output(e.stdout)}') + logging.error(f'Stderr: {_clip_log_output(e.stderr)}') raise