diff --git a/src/fprime/util/cli.py b/src/fprime/util/cli.py index 633bde20..71337254 100644 --- a/src/fprime/util/cli.py +++ b/src/fprime/util/cli.py @@ -61,7 +61,7 @@ def skip_build_loading(parsed): """Determines if the build load step should be skipped. Commands that do not require a build object should manually be added here by the developer. """ - if parsed.command == "version-check": + if parsed.command in ["version-check", "format"]: return True return False @@ -73,7 +73,6 @@ def skip_build_cache_validation(parsed): if parsed.command in [ "purge", "info", - "format", ]: return True if parsed.command == "new" and parsed.new_deployment: diff --git a/src/fprime/util/code_formatter.py b/src/fprime/util/code_formatter.py index bcdfad44..aec11d3a 100644 --- a/src/fprime/util/code_formatter.py +++ b/src/fprime/util/code_formatter.py @@ -10,7 +10,7 @@ import shutil import subprocess from pathlib import Path -from typing import Dict, List, Tuple +from typing import Dict, List, Optional, Tuple from fprime.fbuild.target import ExecutableAction, TargetScope @@ -32,7 +32,7 @@ class ClangFormatter(ExecutableAction): """Class encapsulating the clang-format logic for fprime-util""" - def __init__(self, executable: str, style_file: "Path", options: Dict): + def __init__(self, executable: str, style_file: "Optional[Path]", options: Dict): super().__init__(TargetScope.LOCAL) self.executable = executable self.style_file = style_file @@ -99,12 +99,13 @@ def execute( args (Tuple[Dict[str, str], List[str]]): extra arguments to supply to the utility """ combined_env = os.environ.copy() - combined_env.update(builder.settings.get("environment", {})) + if builder is not None: + combined_env.update(builder.settings.get("environment", {})) if len(self._files_to_format) == 0: print("[INFO] No files were formatted.") return 0 - if not self.style_file.is_file(): + if self.style_file is not None and not self.style_file.is_file(): print( f"[ERROR] No .clang-format file found in {self.style_file.parent}. " "Override location with --pass-through --style=file:." @@ -129,7 +130,13 @@ def execute( print(f"[INFO] {self.executable}") print("[INFO] Clang format arguments:") print(f"[INFO] {clang_args[1:]}") - print("[INFO] Clang format style file:") - print(f"[INFO] {self.style_file}") + if self.style_file is not None: + print("[INFO] Clang format style file:") + print(f"[INFO] {self.style_file}") + else: + print( + "[INFO] Clang format style file: discovered by clang-format " + "(--style=file)" + ) status = subprocess.run(clang_args, env=combined_env) return status.returncode diff --git a/src/fprime/util/commands.py b/src/fprime/util/commands.py index 3564a55f..2c877a30 100644 --- a/src/fprime/util/commands.py +++ b/src/fprime/util/commands.py @@ -164,7 +164,7 @@ def run_code_format( """Runs code formatting using clang-format Args: - build: used to retrieve .clang-format file + build: unused; format runs without a build cache (may be None) parsed: parsed input arguments __: unused cmake_args ___: unused make_args @@ -177,9 +177,12 @@ def run_code_format( "validate_extensions": not parsed.force, "check": parsed.check, } + # No explicit style file: clang-format is invoked with --style=file, which + # discovers the nearest .clang-format from each input file's directory + # (projects and libraries provide their own). clang_formatter = ClangFormatter( "clang-format", - build.settings.get("framework_path", Path(".")) / ".clang-format", + None, options, ) if not clang_formatter.is_supported(): diff --git a/test/fprime/util/test_code_formatter.py b/test/fprime/util/test_code_formatter.py index 1a4fa109..f4afe90d 100644 --- a/test/fprime/util/test_code_formatter.py +++ b/test/fprime/util/test_code_formatter.py @@ -2,6 +2,7 @@ Tests for fprime.util.code_formatter """ +import argparse from pathlib import Path import shutil @@ -9,6 +10,7 @@ import pytest from fprime.util.code_formatter import ClangFormatter +from fprime.util.commands import run_code_format def test_init(): @@ -143,3 +145,56 @@ def test_execute_check_pass(tmp_path, mock_build, style_file): result = formatter.execute(mock_build, tmp_path, ({}, [])) assert result == 0 + + +def test_execute_no_build(tmp_path, style_file): + """Test that execute works when no build object is provided (library case)""" + malformed_src = DATA_DIR / "malformed.cpp" + malformed_dst = tmp_path / "malformed.cpp" + shutil.copy(malformed_src, malformed_dst) + + options = {"backup": False, "verbose": False, "quiet": True, "check": False} + formatter = ClangFormatter("clang-format", style_file, options) + formatter.stage_file(malformed_dst) + + # build=None must not raise (libraries run format without a build cache) + result = formatter.execute(None, tmp_path, ({}, [])) + assert result == 0 + + +def test_run_code_format_in_library(tmp_path, monkeypatch): + """End-to-end: format a library directory with no settings.ini, using its own style file""" + if shutil.which("clang-format") is None: + pytest.skip("clang-format executable not available") + + library_root = tmp_path / "fprime-zephyr" + svc_dir = library_root / "Svc" + svc_dir.mkdir(parents=True) + (library_root / ".clang-format").write_text("BasedOnStyle: LLVM\n") + + malformed = svc_dir / "Component.cpp" + shutil.copy(DATA_DIR / "malformed.cpp", malformed) + + monkeypatch.chdir(library_root) + parsed = argparse.Namespace( + root=None, + path=Path.cwd(), + quiet=True, + verbose=False, + backup=False, + force=False, + check=False, + allow_extension=[], + stdin=False, + files=[], + dirs=[Path("./Svc")], + exclude=[], + pass_through=[], + ) + + # build is None: libraries run format without a build cache + result = run_code_format(None, parsed, {}, {}, []) + assert result == 0 + + well_formed_content = (DATA_DIR / "well-formed.cpp").read_text() + assert malformed.read_text() == well_formed_content