diff --git a/.claude/settings.json b/.claude/settings.json index cb6980dc..b5c02acf 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -2,23 +2,27 @@ "permissions": { "allow": [ "Bash(codespell)", - "Bash(pre-commit run *)", - "Bash(git stash *)", - "Bash(xenon *)", - "Bash(radon *)", - "Bash(isort --diff dfetch*)", - "Bash(black --check dfetch*)", - "Bash(pylint dfetch*)", - "Bash(ruff check dfetch*)", - "Bash(mypy dfetch*)", - "Bash(python -m mypy dfetch*)", - "Bash(python -m pytest tests/*)", - "Bash(pip show *)", - "Bash(doc8 doc*)", - "Bash(pydocstyle dfetch*)", - "Bash(bandit *)", + "Bash(bandit -r dfetch)", + "Bash(black --check dfetch)", + "Bash(doc8 doc:*)", + "Bash(git fetch origin:*)", + "Bash(git stash:*)", + "Bash(git update-index --refresh)", + "Bash(isort --diff dfetch)", + "Bash(lint-imports)", + "Bash(mypy dfetch:*)", + "Bash(pip show:*)", + "Bash(pre-commit run:*)", + "Bash(pydocstyle dfetch:*)", + "Bash(pylint dfetch:*)", "Bash(pyroma --directory --min=10 .)", - "Bash(xargs pyupgrade *)", + "Bash(pytest tests/test_sbom_reporter.py -q)", + "Bash(python -m behave *)", + "Bash(python -m pytest tests/*)", + "Bash(radon *)", + "Bash(ruff check:*)", + "Bash(xenon *)", + "Bash(xargs pyupgrade:*)", "Bash(lint-imports)", "Bash(pip install *)", "Bash(pytest *)", diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 393dadde..c516042c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,6 +19,7 @@ Release 0.14.0 (unreleased) * Edit manifest in-place when freezing inside a git or SVN superproject, preserving comments and layout (#1063) * Add new ``remove`` command to remove projects from manifest and disk (#26) * Fix "unsafe symlink target" error for archives containing relative ``..`` symlinks (#1122) +* Print runtime errors (e.g. ``svn not available on system``) directly in context of the failing subproject instead of collecting and showing them at the end (#1096) * Fix ``dfetch add`` crashing with a ``ValueError`` when the remote URL has a trailing slash (#1137) * Fix unhelpful error message when a metadata file is malformed (#1145) * Fix arbitrary file write via malicious tar/zip symlink (#1152) diff --git a/dfetch/commands/check.py b/dfetch/commands/check.py index 58b5d062..eb177f85 100644 --- a/dfetch/commands/check.py +++ b/dfetch/commands/check.py @@ -46,7 +46,7 @@ from dfetch.reporting.check.sarif_reporter import SarifReporter from dfetch.reporting.check.stdout_reporter import CheckStdoutReporter from dfetch.util.github_version_check import newer_version_available -from dfetch.util.util import catch_runtime_exceptions, in_directory +from dfetch.util.util import in_directory logger = get_logger(__name__) @@ -100,6 +100,7 @@ def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None def __call__(self, args: argparse.Namespace) -> None: """Perform the check.""" if not os.environ.get("CI"): + logger.debug("Checking for a newer dfetch version") newer = newer_version_available() if newer: logger.print_newer_version_notice(newer) @@ -107,23 +108,27 @@ def __call__(self, args: argparse.Namespace) -> None: reporters = self._get_reporters(args, superproject.manifest) with in_directory(superproject.root_directory): - exceptions: list[str] = [] + had_errors: bool = False for project in superproject.manifest.selected_projects(args.projects): - with catch_runtime_exceptions(exceptions) as exceptions: + try: dfetch.project.create_sub_project(project).check_for_update( reporters, files_to_ignore=superproject.ignored_files(project.destination), ) - - if not args.no_recommendations and os.path.isdir(project.destination): - with in_directory(project.destination): - check_sub_manifests(superproject.manifest, project) + if not args.no_recommendations and os.path.isdir( + project.destination + ): + with in_directory(project.destination): + check_sub_manifests(superproject.manifest, project) + except RuntimeError as exc: + logger.print_error_line(project.name, str(exc)) + had_errors = True for reporter in reporters: reporter.dump_to_file() - if exceptions: - raise RuntimeError("\n".join(exceptions)) + if had_errors: + raise RuntimeError() @staticmethod def _get_reporters( diff --git a/dfetch/commands/diff.py b/dfetch/commands/diff.py index 2e529ef2..e2e9a92d 100644 --- a/dfetch/commands/diff.py +++ b/dfetch/commands/diff.py @@ -58,11 +58,12 @@ import pathlib import dfetch.commands.command +import dfetch.manifest.project from dfetch.log import get_logger from dfetch.project import create_super_project from dfetch.project.metadata import Metadata -from dfetch.project.superproject import NoVcsSuperProject, RevisionRange -from dfetch.util.util import catch_runtime_exceptions, in_directory +from dfetch.project.superproject import NoVcsSuperProject, RevisionRange, SuperProject +from dfetch.util.util import in_directory logger = get_logger(__name__) @@ -115,51 +116,62 @@ def __call__(self, args: argparse.Namespace) -> None: ) with in_directory(superproject.root_directory): - exceptions: list[str] = [] projects = superproject.manifest.selected_projects(args.projects) if not projects: raise RuntimeError( f"No (such) project found! {', '.join(args.projects)}" ) + had_errors: bool = False for project in projects: - with catch_runtime_exceptions(exceptions) as exceptions: - if not os.path.exists(project.destination): - raise RuntimeError( - "You cannot generate a diff of a project that was never fetched" - ) - subproject = superproject.get_sub_project(project) - - if not subproject: - raise RuntimeError("No subproject!") - - old_rev = old_rev or superproject.get_file_revision( - subproject.metadata_path - ) - if not old_rev: - raise RuntimeError( - "When not providing any revisions, dfetch starts from" - f" the last revision to {Metadata.FILENAME} in {subproject.local_path}." - " Please either commit this, or specify a revision to start from with --revs" - ) - patch = superproject.diff( - project.destination, - revisions=RevisionRange(old_rev, new_rev), - ignore=(Metadata.FILENAME,), - ) - - msg = self._rev_msg(old_rev, new_rev) - if patch: - patch_path = pathlib.Path(f"{project.name}.patch") - logger.print_info_line( - project.name, - f"Generating patch {patch_path} {msg} in {superproject.root_directory}", - ) - patch_path.write_text(patch, encoding="UTF-8") - else: - logger.print_info_line(project.name, f"No diffs found {msg}") - - if exceptions: - raise RuntimeError("\n".join(exceptions)) + try: + self._diff_project(superproject, project, old_rev, new_rev) + except RuntimeError as exc: + logger.print_error_line(project.name, str(exc)) + had_errors = True + + if had_errors: + raise RuntimeError() + + def _diff_project( + self, + superproject: SuperProject, + project: dfetch.manifest.project.ProjectEntry, + old_rev: str, + new_rev: str, + ) -> None: + """Generate a diff patch for a single project.""" + if not os.path.exists(project.destination): + raise RuntimeError( + "You cannot generate a diff of a project that was never fetched" + ) + subproject = superproject.get_sub_project(project) + + if not subproject: + raise RuntimeError("No subproject!") + + old_rev = old_rev or superproject.get_file_revision(subproject.metadata_path) + if not old_rev: + raise RuntimeError( + "When not providing any revisions, dfetch starts from" + f" the last revision to {Metadata.FILENAME} in {subproject.local_path}." + " Please either commit this, or specify a revision to start from with --revs" + ) + patch = superproject.diff( + project.destination, + revisions=RevisionRange(old_rev, new_rev), + ignore=(Metadata.FILENAME,), + ) + + msg = self._rev_msg(old_rev, new_rev) + if patch: + patch_path = pathlib.Path(f"{project.name}.patch") + logger.print_info_line( + project.name, + f"Generating patch {patch_path} {msg} in {superproject.root_directory}", + ) + patch_path.write_text(patch, encoding="UTF-8") + else: + logger.print_info_line(project.name, f"No diffs found {msg}") @staticmethod def _parse_revs(revs_arg: str) -> tuple[str, str]: diff --git a/dfetch/commands/environment.py b/dfetch/commands/environment.py index d81fea00..84c9753b 100644 --- a/dfetch/commands/environment.py +++ b/dfetch/commands/environment.py @@ -37,6 +37,7 @@ def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None def __call__(self, _: argparse.Namespace) -> None: """Perform listing the environment.""" logger.print_report_line("dfetch", __version__) + logger.debug("Checking for a newer dfetch version") newer = newer_version_available() if newer: logger.print_newer_version_notice(newer) diff --git a/dfetch/commands/format_patch.py b/dfetch/commands/format_patch.py index 4bc34148..ac7c9c11 100644 --- a/dfetch/commands/format_patch.py +++ b/dfetch/commands/format_patch.py @@ -38,7 +38,6 @@ from dfetch.project.subproject import SubProject from dfetch.project.svnsubproject import SvnSubProject from dfetch.util.util import ( - catch_runtime_exceptions, check_no_path_traversal, in_directory, ) @@ -84,7 +83,7 @@ def __call__(self, args: argparse.Namespace) -> None: """Perform the format patch.""" superproject = create_super_project() - exceptions: list[str] = [] + had_errors: bool = False output_dir_path = pathlib.Path(args.output_directory).resolve() @@ -94,7 +93,7 @@ def __call__(self, args: argparse.Namespace) -> None: with in_directory(superproject.root_directory): for project in superproject.manifest.selected_projects(args.projects): - with catch_runtime_exceptions(exceptions) as exceptions: + try: subproject = dfetch.project.create_sub_project(project) # Check if the project has a patch, maybe suggest creating one? @@ -139,9 +138,12 @@ def __call__(self, args: argparse.Namespace) -> None: project.name, f"formatted patch written to {output_patch_file.relative_to(os.getcwd())}", ) + except RuntimeError as exc: + logger.print_error_line(project.name, str(exc)) + had_errors = True - if exceptions: - raise RuntimeError("\n".join(exceptions)) + if had_errors: + raise RuntimeError() def _determine_target_patch_type(subproject: SubProject) -> PatchType: diff --git a/dfetch/commands/freeze.py b/dfetch/commands/freeze.py index 5e4cb21b..515e2cbc 100644 --- a/dfetch/commands/freeze.py +++ b/dfetch/commands/freeze.py @@ -68,7 +68,7 @@ from dfetch.log import get_logger from dfetch.project import create_super_project from dfetch.project.superproject import NoVcsSuperProject -from dfetch.util.util import catch_runtime_exceptions, in_directory +from dfetch.util.util import in_directory logger = get_logger(__name__) @@ -99,8 +99,8 @@ def __call__(self, args: argparse.Namespace) -> None: superproject = create_super_project() make_backup = isinstance(superproject, NoVcsSuperProject) - exceptions: list[str] = [] manifest_updated = False + had_errors = False with in_directory(superproject.root_directory): manifest_path = superproject.manifest.path @@ -110,7 +110,7 @@ def __call__(self, args: argparse.Namespace) -> None: shutil.copyfile(manifest_path, manifest_path + ".backup") for project in projects_to_freeze: - with catch_runtime_exceptions(exceptions) as exceptions: + try: sub_project = dfetch.project.create_sub_project(project) on_disk_version = sub_project.on_disk_version() @@ -133,7 +133,13 @@ def __call__(self, args: argparse.Namespace) -> None: ) superproject.manifest.update_project_version(project) manifest_updated = True + except RuntimeError as exc: + logger.print_error_line(project.name, str(exc)) + had_errors = True if manifest_updated: superproject.manifest.dump() logger.info(f"Updated manifest ({manifest_path}) in {os.getcwd()}") + + if had_errors: + raise RuntimeError() diff --git a/dfetch/commands/update.py b/dfetch/commands/update.py index 294700a0..6a6b010c 100644 --- a/dfetch/commands/update.py +++ b/dfetch/commands/update.py @@ -28,7 +28,6 @@ from dfetch.log import get_logger from dfetch.project import create_super_project from dfetch.util.util import ( - catch_runtime_exceptions, check_no_path_traversal, in_directory, ) @@ -78,20 +77,25 @@ def __call__(self, args: argparse.Namespace) -> None: """Perform the update.""" superproject = create_super_project() - exceptions: list[str] = [] + had_errors: bool = False destinations: list[str] = [ os.path.realpath(project.destination) for project in superproject.manifest.projects ] with in_directory(superproject.root_directory): for project in superproject.manifest.selected_projects(args.projects): - with catch_runtime_exceptions(exceptions) as exceptions: + try: self._check_destination(project, destinations) - destination = project.destination + except RuntimeError: + had_errors = True + continue - def _ignored(dst: str = destination) -> list[str]: - return list(superproject.ignored_files(dst)) + destination = project.destination + def _ignored(dst: str = destination) -> list[str]: + return list(superproject.ignored_files(dst)) + + try: dfetch.project.create_sub_project(project).update( force=args.force, ignored_files_callback=_ignored, @@ -103,9 +107,12 @@ def _ignored(dst: str = destination) -> list[str]: ): with in_directory(project.destination): check_sub_manifests(superproject.manifest, project) + except RuntimeError as exc: + logger.print_error_line(project.name, str(exc)) + had_errors = True - if exceptions: - raise RuntimeError("\n".join(exceptions)) + if had_errors: + raise RuntimeError() @staticmethod def _check_destination( diff --git a/dfetch/commands/update_patch.py b/dfetch/commands/update_patch.py index 5f4a9f06..15dd2818 100644 --- a/dfetch/commands/update_patch.py +++ b/dfetch/commands/update_patch.py @@ -40,9 +40,8 @@ from dfetch.project import create_super_project from dfetch.project.gitsuperproject import GitSuperProject from dfetch.project.metadata import Metadata -from dfetch.project.superproject import NoVcsSuperProject, RevisionRange +from dfetch.project.superproject import NoVcsSuperProject, RevisionRange, SuperProject from dfetch.util.util import ( - catch_runtime_exceptions, check_no_path_traversal, in_directory, ) @@ -76,7 +75,7 @@ def __call__(self, args: argparse.Namespace) -> None: """Perform the update patch.""" superproject = create_super_project() - exceptions: list[str] = [] + had_errors: bool = False if isinstance(superproject, NoVcsSuperProject): raise RuntimeError( @@ -88,74 +87,85 @@ def __call__(self, args: argparse.Namespace) -> None: with in_directory(superproject.root_directory): for project in superproject.manifest.selected_projects(args.projects): - with catch_runtime_exceptions(exceptions) as exceptions: - subproject = dfetch.project.create_sub_project(project) - destination = project.destination - - def _ignored(dst: str = destination) -> list[str]: - return list(superproject.ignored_files(dst)) - - # Check if the project has a patch, maybe suggest creating one? - if not subproject.patch: - logger.print_warning_line( - project.name, - f'skipped - there is no patch file, use "dfetch diff {project.name}"' - " to generate one instead", - ) - continue - - # Check if the project was ever fetched - on_disk_version = subproject.on_disk_version() - if not on_disk_version: - logger.print_warning_line( - project.name, - f'skipped - the project was never fetched before, use "dfetch update {project.name}"', - ) - continue - - # Make sure no uncommitted changes (don't care about ignored files) - if superproject.has_local_changes_in_dir(subproject.local_path): - logger.print_warning_line( - project.name, - f"skipped - Uncommitted changes in {subproject.local_path}", - ) - continue - - # force update to fetched version from metadata without applying patch - subproject.update( - force=True, - ignored_files_callback=_ignored, - patch_count=len(subproject.patch) - 1, - eol_preferences_callback=superproject.eol_preferences, - ) - - # generate reverse patch - patch_text = superproject.diff( - subproject.local_path, - revisions=RevisionRange("", ""), - ignore=(Metadata.FILENAME,), - reverse=True, - ) - - # Select patch to overwrite & make backup - if not self._update_patch( - subproject.patch[-1], - superproject.root_directory, - project.name, - patch_text, - ): - continue - - # force update again to fetched version from metadata but with applying patch - subproject.update( - force=True, - ignored_files_callback=_ignored, - patch_count=-1, - eol_preferences_callback=superproject.eol_preferences, - ) - - if exceptions: - raise RuntimeError("\n".join(exceptions)) + try: + self._process_project(superproject, project) + except RuntimeError as exc: + logger.print_error_line(project.name, str(exc)) + had_errors = True + + if had_errors: + raise RuntimeError() + + def _process_project( + self, + superproject: SuperProject, + project: dfetch.manifest.project.ProjectEntry, + ) -> None: + """Perform the patch update for a single project.""" + subproject = dfetch.project.create_sub_project(project) + destination = project.destination + + def _ignored(dst: str = destination) -> list[str]: + return list(superproject.ignored_files(dst)) + + # Check if the project has a patch, maybe suggest creating one? + if not subproject.patch: + logger.print_warning_line( + project.name, + f'skipped - there is no patch file, use "dfetch diff {project.name}"' + " to generate one instead", + ) + return + + # Check if the project was ever fetched + on_disk_version = subproject.on_disk_version() + if not on_disk_version: + logger.print_warning_line( + project.name, + f'skipped - the project was never fetched before, use "dfetch update {project.name}"', + ) + return + + # Make sure no uncommitted changes (don't care about ignored files) + if superproject.has_local_changes_in_dir(subproject.local_path): + logger.print_warning_line( + project.name, + f"skipped - Uncommitted changes in {subproject.local_path}", + ) + return + + # force update to fetched version from metadata without applying patch + subproject.update( + force=True, + ignored_files_callback=_ignored, + patch_count=len(subproject.patch) - 1, + eol_preferences_callback=superproject.eol_preferences, + ) + + # generate reverse patch + patch_text = superproject.diff( + subproject.local_path, + revisions=RevisionRange("", ""), + ignore=(Metadata.FILENAME,), + reverse=True, + ) + + # Select patch to overwrite & make backup + if not self._update_patch( + subproject.patch[-1], + superproject.root_directory, + project.name, + patch_text, + ): + return + + # force update again to fetched version from metadata but with applying patch + subproject.update( + force=True, + ignored_files_callback=_ignored, + patch_count=-1, + eol_preferences_callback=superproject.eol_preferences, + ) def _update_patch( self, diff --git a/dfetch/log.py b/dfetch/log.py index 64862608..d9ae1998 100644 --- a/dfetch/log.py +++ b/dfetch/log.py @@ -86,6 +86,7 @@ class DLogger(logging.Logger): """Logging class extended with specific log items for dfetch.""" _printed_projects: set[str] = set() + _active_status: Status | None = None def print_report_line(self, name: str, info: str) -> None: """Print a line for a report.""" @@ -116,6 +117,16 @@ def print_warning_line(self, name: str, info: str) -> None: line = markup_escape(info).replace("\n", "\n ") self.info(f" [bold bright_yellow]> {line}[/bold bright_yellow]") + def print_error_line(self, name: str, info: str) -> None: + """Print an error line: green name, red value.""" + if name not in DLogger._printed_projects: + safe_name = markup_escape(name) + self.info(f" [bold][bright_green]{safe_name}:[/bright_green][/bold]") + DLogger._printed_projects.add(name) + + line = markup_escape(info).replace("\n", "\n ") + self.info(f" [bold bright_red]> {line}[/bold bright_red]") + def print_overview(self, name: str, title: str, info: dict[str, Any]) -> None: """Print an overview of fields.""" self.print_info_line(name, title) @@ -209,11 +220,20 @@ def status( self.info(f" [bold][bright_green]{safe_name}:[/bright_green][/bold]") DLogger._printed_projects.add(name) - return Status( + active = Status( f"[bold bright_blue]> {markup_escape(message)}[/bold bright_blue]", spinner=spinner, console=rich_console, ) + DLogger._active_status = active + return active + + def update_status(self, message: str) -> None: + """Update the text of the currently active spinner, if any.""" + if DLogger._active_status is not None: + DLogger._active_status.update( + f"[bold bright_blue]> {markup_escape(message)}[/bold bright_blue]" + ) @classmethod def reset_projects(cls) -> None: diff --git a/dfetch/project/svnsubproject.py b/dfetch/project/svnsubproject.py index e7e1bf48..eafbe194 100644 --- a/dfetch/project/svnsubproject.py +++ b/dfetch/project/svnsubproject.py @@ -167,6 +167,7 @@ def _fetch_impl( def _fetch_externals(self, complete_path: str, revision: str) -> list[Dependency]: """Detect and log SVN externals that were exported with the project.""" + logger.update_status("Indexing externals") vcs_deps = [] for external in SvnRepo.externals_from_url(complete_path, revision): path_display = "./" + external.path.lstrip("./") diff --git a/dfetch/util/util.py b/dfetch/util/util.py index 018ffa12..51c7bb86 100644 --- a/dfetch/util/util.py +++ b/dfetch/util/util.py @@ -188,18 +188,6 @@ def in_directory(path: str | Path) -> Generator[str, None, None]: os.chdir(pwd) -@contextmanager -def catch_runtime_exceptions( - exc_list: list[str] | None = None, -) -> Generator[list[str], None, None]: - """Catch all runtime errors and add it to list of strings.""" - exc_list = exc_list or [] - try: - yield exc_list - except RuntimeError as exc: - exc_list += [str(exc)] - - @contextmanager def prefix_runtime_exceptions( prefix: str, @@ -376,10 +364,11 @@ def glob_within_root(pattern: str, root: Path) -> tuple[list[str], list[str]]: A ``(safe, escaped)`` tuple where *safe* contains sorted paths that resolve inside *root* and *escaped* contains those that do not. """ + root_resolved = root.resolve() safe: list[str] = [] escaped: list[str] = [] for p in sorted(glob.glob(pattern)): - (safe if Path(p).resolve().is_relative_to(root) else escaped).append(p) + (safe if Path(p).resolve().is_relative_to(root_resolved) else escaped).append(p) return safe, escaped diff --git a/dfetch/vcs/svn.py b/dfetch/vcs/svn.py index de7deed1..a8be2972 100644 --- a/dfetch/vcs/svn.py +++ b/dfetch/vcs/svn.py @@ -317,6 +317,11 @@ def externals(self) -> list[External]: def externals_from_url(url: str, revision: str = "") -> list[External]: """Get list of externals from a remote SVN URL.""" extra = ["--revision", revision] if revision else [] + rev_suffix = f"@{revision}" if revision else "" + logger.debug( + f"Scanning '{url}{rev_suffix}' recursively for svn:externals " + f"(may be slow for large repositories)" + ) output = _run_svn(["propget", "svn:externals", "-R"] + extra + [url], url=url) repo_root = SvnRepo.get_info_from_target(url)["Repository Root"] normalized = SvnRepo._normalize_url_prefix(output, url) diff --git a/doc/_ext/scenario_directive.py b/doc/_ext/scenario_directive.py index e04bc02d..deffeb5d 100644 --- a/doc/_ext/scenario_directive.py +++ b/doc/_ext/scenario_directive.py @@ -495,23 +495,22 @@ def process_scenario_appendix(app, doctree: nodes.document, _fromdocname: str) - return if app.builder.name not in ("latex", "rinoh"): - note = nodes.note() - para = nodes.paragraph() - para += nodes.Text( - "In the HTML edition, feature examples appear as expandable " - "blocks directly within each guide section. " - "In the PDF edition they are collected here, grouped by command." - ) - note += para for placeholder in placeholders: + note = nodes.note() + para = nodes.paragraph() + para += nodes.Text( + "In the HTML edition, feature examples appear as expandable " + "blocks directly within each guide section. " + "In the PDF edition they are collected here, grouped by command." + ) + note += para placeholder.replace_self([note]) return entries = getattr(app.env, "scenario_appendix_entries", {}) - appendix_nodes = _build_appendix_nodes(entries) if entries else [] for placeholder in placeholders: - placeholder.replace_self(appendix_nodes) + placeholder.replace_self(_build_appendix_nodes(entries) if entries else []) def purge_scenario_appendix(_app, env, docname: str) -> None: diff --git a/features/check-git-repo.feature b/features/check-git-repo.feature index e7408683..e3439b7c 100644 --- a/features/check-git-repo.feature +++ b/features/check-git-repo.feature @@ -181,9 +181,10 @@ Feature: Checking dependencies from a git repository Then the output shows """ Dfetch (0.13.0) - >>>git ls-remote --heads https://giiiiiidhub.com/i-do-not-exist/broken<<< failed! - 'https://giiiiiidhub.com/i-do-not-exist/broken' is not a valid URL or unreachable: - fatal: unable to access 'https://giiiiiidhub.com/i-do-not-exist/broken/': Could not resolve host: giiiiiidhub.com + non-existent-url: + > >>>git ls-remote --heads https://giiiiiidhub.com/i-do-not-exist/broken<<< failed! + 'https://giiiiiidhub.com/i-do-not-exist/broken' is not a valid URL or unreachable: + fatal: unable to access 'https://giiiiiidhub.com/i-do-not-exist/broken/': Could not resolve host: giiiiiidhub.com """ Scenario: A non-existent tag, branch or revision is reported diff --git a/features/check-svn-repo.feature b/features/check-svn-repo.feature index e6b588f1..0924f298 100644 --- a/features/check-svn-repo.feature +++ b/features/check-svn-repo.feature @@ -138,10 +138,11 @@ Feature: Checking dependencies from a svn repository Then the output shows """ Dfetch (0.13.0) - >>>svn --non-interactive info https://giiiiiidhub.com/i-do-not-exist/broken/trunk<<< failed! - 'https://giiiiiidhub.com/i-do-not-exist/broken/trunk' is not a valid URL or unreachable: - svn: E170013: Unable to connect to a repository at URL 'https://giiiiiidhub.com/i-do-not-exist/broken/trunk' - svn: E670002: Name or service not known + non-existent-url: + > >>>svn --non-interactive info https://giiiiiidhub.com/i-do-not-exist/broken/trunk<<< failed! + 'https://giiiiiidhub.com/i-do-not-exist/broken/trunk' is not a valid URL or unreachable: + svn: E170013: Unable to connect to a repository at URL 'https://giiiiiidhub.com/i-do-not-exist/broken/trunk' + svn: E670002: Name or service not known """ Scenario: A non-existent tag in svn repo diff --git a/features/fetch-archive.feature b/features/fetch-archive.feature index 98e3c3e0..44343f4a 100644 --- a/features/fetch-archive.feature +++ b/features/fetch-archive.feature @@ -119,7 +119,8 @@ Feature: Fetching dependencies from an archive (tar/zip) Then the output shows """ Dfetch (0.13.0) - Hash mismatch for SomeProject! sha256 expected 0000000000000000000000000000000000000000000000000000000000000000 + SomeProject: + > Hash mismatch for SomeProject! sha256 expected 0000000000000000000000000000000000000000000000000000000000000000 """ Scenario: Specific directory from archive can be fetched diff --git a/features/fetch-checks-destination.feature b/features/fetch-checks-destination.feature index 999590cb..c1279ba3 100644 --- a/features/fetch-checks-destination.feature +++ b/features/fetch-checks-destination.feature @@ -24,7 +24,6 @@ Feature: Fetch checks destinations Dfetch (0.13.0) ext/test-repo-tag: > Skipping, path "." is not allowed as destination. - Destination must be in a valid subfolder. "." is not valid! """ Scenario: Path traversal is not allowed @@ -46,5 +45,4 @@ Feature: Fetch checks destinations Dfetch (0.13.0) ext/test-repo-tag: > Skipping, path "../../some-higher-folder" is outside manifest directory tree. - Destination must be in the manifests folder or a subfolder. "../../some-higher-folder" is outside this tree! """ diff --git a/features/patch-after-fetch-git.feature b/features/patch-after-fetch-git.feature index 53f3a8d6..e030d8ff 100644 --- a/features/patch-after-fetch-git.feature +++ b/features/patch-after-fetch-git.feature @@ -75,7 +75,7 @@ Feature: Patch after fetching from git repo source/target file does not exist: --- b'README1.md' +++ b'README1.md' - Applying patch "diff.patch" failed + > Applying patch "diff.patch" failed """ Scenario: Multiple patch files are applied after fetching diff --git a/features/patch-after-fetch-svn.feature b/features/patch-after-fetch-svn.feature index 6e7a39ca..f637162a 100644 --- a/features/patch-after-fetch-svn.feature +++ b/features/patch-after-fetch-svn.feature @@ -76,5 +76,5 @@ Feature: Patch after fetching from svn repo source/target file does not exist: --- b'build-deb2.sh' +++ b'build-deb2.sh' - Applying patch "diff.patch" failed + > Applying patch "diff.patch" failed """ diff --git a/features/updated-project-has-dependencies.feature b/features/updated-project-has-dependencies.feature index 081b0574..19a9b00e 100644 --- a/features/updated-project-has-dependencies.feature +++ b/features/updated-project-has-dependencies.feature @@ -92,12 +92,10 @@ Feature: Updated project has dependencies Dfetch (0.13.0) SomeProject: > Fetched v1 - SomeProject/dfetch.yaml: Schema validation failed: - - "very-invalid-manifest\n" - ^ (line: 1) - - found arbitrary text + > SomeProject/dfetch.yaml: Schema validation failed: + "very-invalid-manifest\n" + ^ (line: 1) + found arbitrary text """ And 'MyProject' looks like: """ diff --git a/tests/test_fuzzing.py b/tests/test_fuzzing.py index ce4bd451..777e9e1d 100644 --- a/tests/test_fuzzing.py +++ b/tests/test_fuzzing.py @@ -54,13 +54,17 @@ ) # VERSION = Int() | Float() | Str(): generate unquoted ints, unquoted floats, -# and arbitrary safe strings to cover all three schema branches. -# Empty strings are excluded: Float()'s validator crashes on them instead of -# raising YAMLValidationError, so the OrValidator cannot fall through to Str(). +# and float-like strings. Arbitrary strings cannot be used: strictyaml's Float +# validator calls float() directly and raises ValueError (not +# YAMLValidationError) for non-float strings, so the OrValidator cannot fall +# through to Str(). Float-like strings are safe: as_document serializes them +# via Float() (float("1.5") succeeds), while yaml.dump quotes them as YAML +# string scalars ('1.5'), exercising the Str() branch during parsing in +# test_manifest_can_be_created, test_check, and test_update. SAFE_VERSION = st.one_of( st.integers(), st.floats(allow_nan=False, allow_infinity=False), - SAFE_TEXT.filter(lambda s: s != ""), + st.floats(allow_nan=False, allow_infinity=False).map(str), )