@@ -38,7 +38,7 @@ jobs:
|
|||||||
python3 \
|
python3 \
|
||||||
python3-venv
|
python3-venv
|
||||||
python3 -m venv build/build_common/ci_python
|
python3 -m venv build/build_common/ci_python
|
||||||
build/build_common/ci_python/bin/python -m pip install --no-cache-dir conan==2.26.2
|
build/build_common/ci_python/bin/python -m pip install --no-cache-dir conan==2.26.2 -r tools/docker/format-requirements.txt
|
||||||
echo "$PWD/build/build_common/ci_python/bin" >> "$GITHUB_PATH"
|
echo "$PWD/build/build_common/ci_python/bin" >> "$GITHUB_PATH"
|
||||||
- name: Restore Conan cache
|
- name: Restore Conan cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -50,6 +50,10 @@ jobs:
|
|||||||
run: ./tools/configure.py --target x86_64_gcc12_release
|
run: ./tools/configure.py --target x86_64_gcc12_release
|
||||||
- name: Test tooling
|
- name: Test tooling
|
||||||
run: python3 -m unittest discover -s tests -p 'test_*.py'
|
run: python3 -m unittest discover -s tests -p 'test_*.py'
|
||||||
|
- name: Check code style
|
||||||
|
run: |
|
||||||
|
python3 -m pip install --no-cache-dir -r tools/docker/format-requirements.txt
|
||||||
|
python3 tools/format.py --check
|
||||||
- name: Build native release
|
- name: Build native release
|
||||||
run: ./tools/build.py --target x86_64_gcc12_release
|
run: ./tools/build.py --target x86_64_gcc12_release
|
||||||
- name: Test native release
|
- name: Test native release
|
||||||
|
|||||||
@@ -47,14 +47,26 @@ Product repos can add typed extra CMake compile databases under `tooling.lsp.com
|
|||||||
{"CompileFlags": {"CompilationDatabase": "build/native"}},
|
{"CompileFlags": {"CompilationDatabase": "build/native"}},
|
||||||
{"If": {"PathMatch": "src/firmware/.*"}, "CompileFlags": {"CompilationDatabase": "build/firmware"}}
|
{"If": {"PathMatch": "src/firmware/.*"}, "CompileFlags": {"CompilationDatabase": "build/firmware"}}
|
||||||
],
|
],
|
||||||
"clang_format": {"DisableFormat": true},
|
"clang_format": {"BasedOnStyle": "LLVM", "IndentWidth": 2, "ColumnLimit": 0, "SortIncludes": "Never"},
|
||||||
"pyright": {"include": ["tools", "conanfile.py"]}
|
"pyright": {"include": ["tools", "conanfile.py"]}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Each supplied entry replaces that file's entire configuration. Omitted entries use Modulus defaults: the default target's build directory for clangd, `-Wall`/`-Wextra`, disabled clang-tidy and formatting, and Python paths for the bootstrap, recipe, and exported tools. Entries use the editors' own configuration keys. LLVM files use JSON-compatible YAML, with separate documents for clangd fragments.
|
Each supplied entry replaces that file's entire configuration. Omitted entries use Modulus defaults: the default target's build directory for clangd, `-Wall`/`-Wextra`, disabled clang-tidy, the compact formatting policy below, and Python paths for the bootstrap, recipe, and exported tools. Entries use the editors' own configuration keys. LLVM files use JSON-compatible YAML, with separate documents for clangd fragments.
|
||||||
|
|
||||||
|
### Shared Code Style
|
||||||
|
|
||||||
|
Run `python3 tools/format.py` in Modulus, or `./modulus.py format` in a consumer. Optional repository-relative paths restrict the operation. `format --check` reports layout differences and prohibited aliases without writing files; CI runs this check. `format --stdin-filename /absolute/buffer.cpp` reads stdin and writes only formatted text, for editor integrations.
|
||||||
|
|
||||||
|
The build image pins clang-format 19.1.7, autopep8 2.3.2, and the syntax parser dependencies in `tools/docker/format-requirements.txt`. Python uses two-space indentation for both blocks and continuations, including multiline tuples, lists, dictionaries, imports, and calls. Continuations indent two spaces from their opening line rather than aligning to a column; closing brackets align with the opening line. Line-length and aggressive semantic fixes are disabled. C++ has no column limit, uses braces, and preserves include order and groups. The shared syntax-aware pass joins simple signatures/calls, removes blank lines inside functions and class/struct bodies, separates definitions outside classes by one blank line, and pads namespace interiors without indenting their bodies. Class members, including access labels, defaulted/deleted constructors, inline methods, and nested types, have no intervening blank lines. Blank lines within literal and comment contents are preserved. Comments, multiline strings, structured literals, and preprocessor content are not joined. Python formatting verifies AST equivalence before writing. C++ formatting supplements clang-format using concrete syntax boundaries, including common test macros; it is not a C++ type checker.
|
||||||
|
|
||||||
|
Use full type/module names rather than C++ type/namespace aliases, Python type aliases, or renamed imports. The existing `std::*_t` portability definitions in `include/modulus/types.hpp` are the sole local exception; third-party SDK definitions are not rewritten. Formatting changes layout only, so alias diagnostics require explicit code changes.
|
||||||
|
|
||||||
|
Discovery includes Git-tracked and nonignored untracked C/C++ and Python sources. Generated/build/vendor directories and symlinks are excluded. Add project-relative glob patterns with `tooling.format.exclude`; C++ settings remain configurable through `tooling.editor.clang_format`. A batch validates every input before it writes any output.
|
||||||
|
|
||||||
|
Neovim uses Conform for C++ and Python in registered Modulus projects, retaining clangd and Pyright for language features. LazyVim's manual command, format-on-save handling, and global/buffer toggles remain in control. Other projects retain their formatter selections. There is no editor-triggered download, image build, host formatter fallback, or compilation-database requirement. Missing/stale images fail on stderr without replacing the buffer. Configure first to prepare the image. `MODULUS_DIRECT=1` is for provisioned CI environments, which install the same pinned requirements; it is not set by the editor.
|
||||||
|
|
||||||
The authored `cmake/` directory belongs in Git. `CMakeFiles/`, compiler caches, and generated sources belong in build directories. Modulus rejects project/source-root output directories before clearing configure or LSP caches. Conan may still use its own isolated build folders. Existing build caches are retained; they are not source files.
|
The authored `cmake/` directory belongs in Git. `CMakeFiles/`, compiler caches, and generated sources belong in build directories. Modulus rejects project/source-root output directories before clearing configure or LSP caches. Conan may still use its own isolated build folders. Existing build caches are retained; they are not source files.
|
||||||
|
|
||||||
|
|||||||
+1
-21
@@ -8,8 +8,6 @@ import conan.tools.files
|
|||||||
|
|
||||||
import modulus.config
|
import modulus.config
|
||||||
|
|
||||||
config = modulus.config
|
|
||||||
|
|
||||||
class modulus_recipe(conan.ConanFile):
|
class modulus_recipe(conan.ConanFile):
|
||||||
name = "modulus"
|
name = "modulus"
|
||||||
version = "latest"
|
version = "latest"
|
||||||
@@ -83,26 +81,18 @@ class modulus_recipe(conan.ConanFile):
|
|||||||
"tools/docker/*",
|
"tools/docker/*",
|
||||||
"README.md",
|
"README.md",
|
||||||
)
|
)
|
||||||
|
|
||||||
def config_options(self):
|
def config_options(self):
|
||||||
settings = self.settings
|
settings = self.settings
|
||||||
options = self.options
|
options = self.options
|
||||||
if settings.os == "Windows":
|
if settings.os == "Windows":
|
||||||
del options.fPIC
|
del options.fPIC
|
||||||
|
|
||||||
def configure(self):
|
def configure(self):
|
||||||
options = self.options
|
options = self.options
|
||||||
if options.shared:
|
if options.shared:
|
||||||
del options.fPIC
|
del options.fPIC
|
||||||
|
|
||||||
def requirements(self):
|
def requirements(self):
|
||||||
config = modulus.config.load_recipe_config(self)
|
config = modulus.config.load_recipe_config(self)
|
||||||
self.requires(
|
self.requires(modulus.config.boost_ref(config), transitive_headers=True, transitive_libs=True, options=modulus.config.boost_requirement_options(config))
|
||||||
modulus.config.boost_ref(config),
|
|
||||||
transitive_headers=True,
|
|
||||||
transitive_libs=True,
|
|
||||||
options=modulus.config.boost_requirement_options(config),
|
|
||||||
)
|
|
||||||
if self.options.with_openssl:
|
if self.options.with_openssl:
|
||||||
self.requires(
|
self.requires(
|
||||||
"openssl/4.0.1",
|
"openssl/4.0.1",
|
||||||
@@ -127,7 +117,6 @@ class modulus_recipe(conan.ConanFile):
|
|||||||
transitive_headers=True,
|
transitive_headers=True,
|
||||||
transitive_libs=True,
|
transitive_libs=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def generate(self):
|
def generate(self):
|
||||||
options = self.options
|
options = self.options
|
||||||
deps = conan.tools.cmake.CMakeDeps(self)
|
deps = conan.tools.cmake.CMakeDeps(self)
|
||||||
@@ -142,7 +131,6 @@ class modulus_recipe(conan.ConanFile):
|
|||||||
toolchain.variables["MODULUS_WITH_RAYLIB"] = bool(options.with_raylib)
|
toolchain.variables["MODULUS_WITH_RAYLIB"] = bool(options.with_raylib)
|
||||||
toolchain.variables["MODULUS_WITH_MINIAUDIO"] = bool(options.with_miniaudio)
|
toolchain.variables["MODULUS_WITH_MINIAUDIO"] = bool(options.with_miniaudio)
|
||||||
toolchain.generate()
|
toolchain.generate()
|
||||||
|
|
||||||
def build(self):
|
def build(self):
|
||||||
options = self.options
|
options = self.options
|
||||||
cmake = conan.tools.cmake.CMake(self)
|
cmake = conan.tools.cmake.CMake(self)
|
||||||
@@ -150,17 +138,14 @@ class modulus_recipe(conan.ConanFile):
|
|||||||
cmake.build()
|
cmake.build()
|
||||||
if options.with_tests and conan.tools.build.can_run(self):
|
if options.with_tests and conan.tools.build.can_run(self):
|
||||||
cmake.test(target="check")
|
cmake.test(target="check")
|
||||||
|
|
||||||
def package_id(self):
|
def package_id(self):
|
||||||
self.info.options.rm_safe("with_tests")
|
self.info.options.rm_safe("with_tests")
|
||||||
|
|
||||||
def package(self):
|
def package(self):
|
||||||
cmake = conan.tools.cmake.CMake(self)
|
cmake = conan.tools.cmake.CMake(self)
|
||||||
cmake.install()
|
cmake.install()
|
||||||
package = pathlib.Path(self.package_folder)
|
package = pathlib.Path(self.package_folder)
|
||||||
conan.tools.files.copy(self, "*.py", src=pathlib.Path(self.recipe_folder) / "modulus", dst=package / "modulus", keep_path=True)
|
conan.tools.files.copy(self, "*.py", src=pathlib.Path(self.recipe_folder) / "modulus", dst=package / "modulus", keep_path=True)
|
||||||
conan.tools.files.copy(self, "*", src=pathlib.Path(self.source_folder) / "tools", dst=package / "tools", keep_path=True)
|
conan.tools.files.copy(self, "*", src=pathlib.Path(self.source_folder) / "tools", dst=package / "tools", keep_path=True)
|
||||||
|
|
||||||
def package_info(self):
|
def package_info(self):
|
||||||
cpp_info = self.cpp_info
|
cpp_info = self.cpp_info
|
||||||
cpp_info.set_property("cmake_file_name", "modulus")
|
cpp_info.set_property("cmake_file_name", "modulus")
|
||||||
@@ -173,18 +158,14 @@ class modulus_recipe(conan.ConanFile):
|
|||||||
components["modulus"].set_property("cmake_target_name", "modulus::modulus")
|
components["modulus"].set_property("cmake_target_name", "modulus::modulus")
|
||||||
components["modulus"].libs = ["modulus"]
|
components["modulus"].libs = ["modulus"]
|
||||||
components["modulus"].requires = ["boost::json"]
|
components["modulus"].requires = ["boost::json"]
|
||||||
|
|
||||||
components["boost_json"].set_property("cmake_target_name", "modulus::boost_json")
|
components["boost_json"].set_property("cmake_target_name", "modulus::boost_json")
|
||||||
components["boost_json"].requires = ["boost::json"]
|
components["boost_json"].requires = ["boost::json"]
|
||||||
|
|
||||||
if self.options.with_openssl:
|
if self.options.with_openssl:
|
||||||
components["openssl"].set_property("cmake_target_name", "modulus::openssl")
|
components["openssl"].set_property("cmake_target_name", "modulus::openssl")
|
||||||
components["openssl"].requires = ["openssl::ssl", "openssl::crypto"]
|
components["openssl"].requires = ["openssl::ssl", "openssl::crypto"]
|
||||||
|
|
||||||
if self.options.with_sqlite:
|
if self.options.with_sqlite:
|
||||||
components["sqlite"].set_property("cmake_target_name", "modulus::sqlite")
|
components["sqlite"].set_property("cmake_target_name", "modulus::sqlite")
|
||||||
components["sqlite"].requires = ["sqlite3::sqlite"]
|
components["sqlite"].requires = ["sqlite3::sqlite"]
|
||||||
|
|
||||||
if self.options.with_raylib:
|
if self.options.with_raylib:
|
||||||
components["raylib"].set_property("cmake_target_name", "modulus::raylib")
|
components["raylib"].set_property("cmake_target_name", "modulus::raylib")
|
||||||
components["raylib"].requires = ["raylib::raylib"]
|
components["raylib"].requires = ["raylib::raylib"]
|
||||||
@@ -197,7 +178,6 @@ class modulus_recipe(conan.ConanFile):
|
|||||||
"atomic",
|
"atomic",
|
||||||
"dl",
|
"dl",
|
||||||
]
|
]
|
||||||
|
|
||||||
if self.options.with_miniaudio:
|
if self.options.with_miniaudio:
|
||||||
components["miniaudio"].set_property("cmake_target_name", "modulus::miniaudio")
|
components["miniaudio"].set_property("cmake_target_name", "modulus::miniaudio")
|
||||||
components["miniaudio"].requires = ["miniaudio::miniaudio"]
|
components["miniaudio"].requires = ["miniaudio::miniaudio"]
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <modulus/error.hpp>
|
#include <modulus/error.hpp>
|
||||||
#include <modulus/log.hpp>
|
#include <modulus/log.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace modulus {
|
namespace modulus {
|
||||||
|
|
||||||
struct logging_config {
|
struct logging_config {
|
||||||
@@ -19,14 +20,14 @@ struct config {
|
|||||||
modulus::error_config errors{};
|
modulus::error_config errors{};
|
||||||
};
|
};
|
||||||
|
|
||||||
auto parse_logging_config(boost::json::value const& value) -> std::expected<modulus::logging_config, modulus::error>;
|
auto parse_logging_config(boost::json::value const &value) -> std::expected<modulus::logging_config, modulus::error>;
|
||||||
auto parse_error_config(boost::json::value const& value) -> std::expected<modulus::error_config, modulus::error>;
|
auto parse_error_config(boost::json::value const &value) -> std::expected<modulus::error_config, modulus::error>;
|
||||||
auto parse_config(boost::json::value const& value) -> std::expected<modulus::config, modulus::error>;
|
auto parse_config(boost::json::value const &value) -> std::expected<modulus::config, modulus::error>;
|
||||||
auto load_config(std::string const& path) -> std::expected<modulus::config, modulus::error>;
|
auto load_config(std::string const &path) -> std::expected<modulus::config, modulus::error>;
|
||||||
auto to_json(modulus::logging_config const& config) -> boost::json::value;
|
auto to_json(modulus::logging_config const &config) -> boost::json::value;
|
||||||
auto to_json(modulus::error_config const& config) -> boost::json::value;
|
auto to_json(modulus::error_config const &config) -> boost::json::value;
|
||||||
auto to_json(modulus::config const& config) -> boost::json::value;
|
auto to_json(modulus::config const &config) -> boost::json::value;
|
||||||
auto configure_logger(modulus::logger& logger, modulus::logging_config const& config) -> void;
|
auto configure_logger(modulus::logger &logger, modulus::logging_config const &config) -> void;
|
||||||
auto configure(modulus::config const& config) -> void;
|
auto configure(modulus::config const &config) -> void;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <modulus/source_location.hpp>
|
#include <modulus/source_location.hpp>
|
||||||
#include <modulus/types.hpp>
|
#include <modulus/types.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace modulus {
|
namespace modulus {
|
||||||
|
|
||||||
struct error_config {
|
struct error_config {
|
||||||
@@ -19,12 +20,12 @@ struct error_config {
|
|||||||
class error {
|
class error {
|
||||||
public:
|
public:
|
||||||
error() = default;
|
error() = default;
|
||||||
error(std::string category, std::int32_t code, std::string message, modulus::source_location location = modulus::source_location( __builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION()));
|
error(std::string category, std::int32_t code, std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION()));
|
||||||
auto category() const noexcept -> std::string_view;
|
auto category() const noexcept -> std::string_view;
|
||||||
auto code() const noexcept -> std::int32_t;
|
auto code() const noexcept -> std::int32_t;
|
||||||
auto message() const noexcept -> std::string_view;
|
auto message() const noexcept -> std::string_view;
|
||||||
auto location() const noexcept -> const modulus::source_location&;
|
auto location() const noexcept -> const modulus::source_location &;
|
||||||
auto describe(modulus::error_config const& config) const -> std::string;
|
auto describe(modulus::error_config const &config) const -> std::string;
|
||||||
auto describe() const -> std::string;
|
auto describe() const -> std::string;
|
||||||
private:
|
private:
|
||||||
std::string category_{"generic"};
|
std::string category_{"generic"};
|
||||||
@@ -33,7 +34,7 @@ private:
|
|||||||
modulus::source_location location_{modulus::source_location::unknown()};
|
modulus::source_location location_{modulus::source_location::unknown()};
|
||||||
};
|
};
|
||||||
|
|
||||||
auto make_error(std::string category, std::int32_t code, std::string message, modulus::source_location location = modulus::source_location( __builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> modulus::error;
|
auto make_error(std::string category, std::int32_t code, std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> modulus::error;
|
||||||
auto set_default_error_config(modulus::error_config config) -> void;
|
auto set_default_error_config(modulus::error_config config) -> void;
|
||||||
auto default_error_config() -> modulus::error_config;
|
auto default_error_config() -> modulus::error_config;
|
||||||
|
|
||||||
|
|||||||
@@ -30,22 +30,22 @@ struct enum_mapping {
|
|||||||
|
|
||||||
auto invalid(modulus::json_config::options config, std::string message) -> modulus::error;
|
auto invalid(modulus::json_config::options config, std::string message) -> modulus::error;
|
||||||
auto not_found(modulus::json_config::options config, std::string message) -> modulus::error;
|
auto not_found(modulus::json_config::options config, std::string message) -> modulus::error;
|
||||||
auto load_value(std::string const& path, modulus::json_config::options config = {}) -> std::expected<boost::json::value, modulus::error>;
|
auto load_value(std::string const &path, modulus::json_config::options config = {}) -> std::expected<boost::json::value, modulus::error>;
|
||||||
auto require_object(boost::json::value const& value, std::string_view context, modulus::json_config::options config = {}) -> std::expected<boost::json::object const*, modulus::error>;
|
auto require_object(boost::json::value const &value, std::string_view context, modulus::json_config::options config = {}) -> std::expected<boost::json::object const *, modulus::error>;
|
||||||
auto reject_unknown_fields(boost::json::object const& object, std::initializer_list<std::string_view> allowed, std::string_view context, modulus::json_config::options config = {}) -> std::expected<void, modulus::error>;
|
auto reject_unknown_fields(boost::json::object const &object, std::initializer_list<std::string_view> allowed, std::string_view context, modulus::json_config::options config = {}) -> std::expected<void, modulus::error>;
|
||||||
auto optional_bool_field(boost::json::object const& object, std::string_view name, std::bool_t fallback, modulus::json_config::options config = {}) -> std::expected<std::bool_t, modulus::error>;
|
auto optional_bool_field(boost::json::object const &object, std::string_view name, std::bool_t fallback, modulus::json_config::options config = {}) -> std::expected<std::bool_t, modulus::error>;
|
||||||
auto optional_string_field(boost::json::object const& object, std::string_view name, std::string fallback, modulus::json_config::options config = {}) -> std::expected<std::string, modulus::error>;
|
auto optional_string_field(boost::json::object const &object, std::string_view name, std::string fallback, modulus::json_config::options config = {}) -> std::expected<std::string, modulus::error>;
|
||||||
auto required_string_field(boost::json::object const& object, std::string_view name, std::string_view context, modulus::json_config::options config = {}) -> std::expected<std::string, modulus::error>;
|
auto required_string_field(boost::json::object const &object, std::string_view name, std::string_view context, modulus::json_config::options config = {}) -> std::expected<std::string, modulus::error>;
|
||||||
auto optional_uint64_field(boost::json::object const& object, std::string_view name, std::uint64_t fallback, std::uint64_t max_value, modulus::json_config::options config = {}) -> std::expected<std::uint64_t, modulus::error>;
|
auto optional_uint64_field(boost::json::object const &object, std::string_view name, std::uint64_t fallback, std::uint64_t max_value, modulus::json_config::options config = {}) -> std::expected<std::uint64_t, modulus::error>;
|
||||||
auto positive_uint64_field(boost::json::object const& object, std::string_view name, std::uint64_t fallback, std::uint64_t max_value, modulus::json_config::options config = {}) -> std::expected<std::uint64_t, modulus::error>;
|
auto positive_uint64_field(boost::json::object const &object, std::string_view name, std::uint64_t fallback, std::uint64_t max_value, modulus::json_config::options config = {}) -> std::expected<std::uint64_t, modulus::error>;
|
||||||
auto optional_int32_field(boost::json::object const& object, std::string_view name, std::int32_t fallback, std::int32_t min_value, std::int32_t max_value, modulus::json_config::options config = {}) -> std::expected<std::int32_t, modulus::error>;
|
auto optional_int32_field(boost::json::object const &object, std::string_view name, std::int32_t fallback, std::int32_t min_value, std::int32_t max_value, modulus::json_config::options config = {}) -> std::expected<std::int32_t, modulus::error>;
|
||||||
auto positive_milliseconds_field(boost::json::object const& object, std::string_view name, std::chrono::milliseconds fallback, std::uint64_t max_value = static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max()), modulus::json_config::options config = {}) -> std::expected<std::chrono::milliseconds, modulus::error>;
|
auto positive_milliseconds_field(boost::json::object const &object, std::string_view name, std::chrono::milliseconds fallback, std::uint64_t max_value = static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max()), modulus::json_config::options config = {}) -> std::expected<std::chrono::milliseconds, modulus::error>;
|
||||||
|
|
||||||
template <typename ENUM>
|
template <typename ENUM>
|
||||||
auto parse_enum(std::string_view value, std::initializer_list<modulus::json_config::enum_mapping<ENUM>> mappings, std::string_view message, modulus::json_config::options config = {}) -> std::expected<ENUM, modulus::error>;
|
auto parse_enum(std::string_view value, std::initializer_list<modulus::json_config::enum_mapping<ENUM>> mappings, std::string_view message, modulus::json_config::options config = {}) -> std::expected<ENUM, modulus::error>;
|
||||||
|
|
||||||
template <typename ENUM>
|
template <typename ENUM>
|
||||||
auto optional_enum_field(boost::json::object const& object, std::string_view name, ENUM fallback, std::initializer_list<modulus::json_config::enum_mapping<ENUM>> mappings, std::string_view message, modulus::json_config::options config = {}) -> std::expected<ENUM, modulus::error>;
|
auto optional_enum_field(boost::json::object const &object, std::string_view name, ENUM fallback, std::initializer_list<modulus::json_config::enum_mapping<ENUM>> mappings, std::string_view message, modulus::json_config::options config = {}) -> std::expected<ENUM, modulus::error>;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ auto modulus::json_config::parse_enum(std::string_view value, std::initializer_l
|
|||||||
}
|
}
|
||||||
|
|
||||||
template <typename ENUM>
|
template <typename ENUM>
|
||||||
auto modulus::json_config::optional_enum_field(boost::json::object const& object, std::string_view name, ENUM fallback, std::initializer_list<modulus::json_config::enum_mapping<ENUM>> mappings, std::string_view message, modulus::json_config::options config) -> std::expected<ENUM, modulus::error> {
|
auto modulus::json_config::optional_enum_field(boost::json::object const &object, std::string_view name, ENUM fallback, std::initializer_list<modulus::json_config::enum_mapping<ENUM>> mappings, std::string_view message, modulus::json_config::options config) -> std::expected<ENUM, modulus::error> {
|
||||||
auto found = object.find(name);
|
auto found = object.find(name);
|
||||||
if (found == object.end()) {
|
if (found == object.end()) {
|
||||||
return fallback;
|
return fallback;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include <modulus/source_location.hpp>
|
#include <modulus/source_location.hpp>
|
||||||
#include <modulus/types.hpp>
|
#include <modulus/types.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace modulus {
|
namespace modulus {
|
||||||
|
|
||||||
enum class log_level : std::int32_t {
|
enum class log_level : std::int32_t {
|
||||||
@@ -41,20 +42,20 @@ public:
|
|||||||
auto set_level(modulus::log_level level) -> void;
|
auto set_level(modulus::log_level level) -> void;
|
||||||
auto level() const -> modulus::log_level;
|
auto level() const -> modulus::log_level;
|
||||||
auto enabled(modulus::log_level level) const -> std::bool_t;
|
auto enabled(modulus::log_level level) const -> std::bool_t;
|
||||||
auto set_sink(std::function<void(const modulus::log_record&)> sink) -> void;
|
auto set_sink(std::function<void(const modulus::log_record &)> sink) -> void;
|
||||||
auto write(modulus::log_record record) const -> void;
|
auto write(modulus::log_record record) const -> void;
|
||||||
auto log(modulus::log_level level, std::string message, modulus::source_location location = modulus::source_location( __builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) const -> void;
|
auto log(modulus::log_level level, std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) const -> void;
|
||||||
private:
|
private:
|
||||||
mutable std::mutex mutex_;
|
mutable std::mutex mutex_;
|
||||||
mutable std::mutex sink_mutex_;
|
mutable std::mutex sink_mutex_;
|
||||||
modulus::log_level level_{modulus::log_level::INFO};
|
modulus::log_level level_{modulus::log_level::INFO};
|
||||||
std::function<void(const modulus::log_record&)> sink_{};
|
std::function<void(const modulus::log_record &)> sink_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
auto default_logger() -> modulus::logger&;
|
auto default_logger() -> modulus::logger &;
|
||||||
auto set_default_log_level(modulus::log_level level) -> void;
|
auto set_default_log_level(modulus::log_level level) -> void;
|
||||||
auto is_log_enabled(modulus::log_level level) -> std::bool_t;
|
auto is_log_enabled(modulus::log_level level) -> std::bool_t;
|
||||||
auto log(modulus::log_level level, std::string message, modulus::source_location location = modulus::source_location( __builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> void;
|
auto log(modulus::log_level level, std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> void;
|
||||||
auto log_trace(std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> void;
|
auto log_trace(std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> void;
|
||||||
auto log_debug(std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> void;
|
auto log_debug(std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> void;
|
||||||
auto log_info(std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> void;
|
auto log_info(std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> void;
|
||||||
@@ -63,12 +64,12 @@ auto log_error(std::string message, modulus::source_location location = modulus:
|
|||||||
auto log_critical(std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> void;
|
auto log_critical(std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> void;
|
||||||
|
|
||||||
template <typename MESSAGE>
|
template <typename MESSAGE>
|
||||||
auto log_lazy(modulus::log_level level, MESSAGE&& message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> void;
|
auto log_lazy(modulus::log_level level, MESSAGE &&message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> void;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename MESSAGE>
|
template <typename MESSAGE>
|
||||||
auto modulus::log_lazy(modulus::log_level level, MESSAGE&& message, modulus::source_location location) -> void {
|
auto modulus::log_lazy(modulus::log_level level, MESSAGE &&message, modulus::source_location location) -> void {
|
||||||
assert(static_cast<std::int32_t>(level) >= static_cast<std::int32_t>(modulus::log_level::TRACE));
|
assert(static_cast<std::int32_t>(level) >= static_cast<std::int32_t>(modulus::log_level::TRACE));
|
||||||
assert(static_cast<std::int32_t>(level) <= static_cast<std::int32_t>(modulus::log_level::OFF));
|
assert(static_cast<std::int32_t>(level) <= static_cast<std::int32_t>(modulus::log_level::OFF));
|
||||||
assert(location.file_name() != nullptr);
|
assert(location.file_name() != nullptr);
|
||||||
|
|||||||
@@ -7,10 +7,11 @@
|
|||||||
|
|
||||||
#include <modulus/error.hpp>
|
#include <modulus/error.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace modulus {
|
namespace modulus {
|
||||||
|
|
||||||
template <typename VALUE>
|
template <typename VALUE>
|
||||||
auto success(VALUE&& value) -> std::expected<std::decay_t<VALUE>, modulus::error>;
|
auto success(VALUE &&value) -> std::expected<std::decay_t<VALUE>, modulus::error>;
|
||||||
|
|
||||||
template <typename VALUE>
|
template <typename VALUE>
|
||||||
auto fail(modulus::error value) -> std::expected<VALUE, modulus::error>;
|
auto fail(modulus::error value) -> std::expected<VALUE, modulus::error>;
|
||||||
@@ -18,7 +19,7 @@ auto fail(modulus::error value) -> std::expected<VALUE, modulus::error>;
|
|||||||
}
|
}
|
||||||
|
|
||||||
template <typename VALUE>
|
template <typename VALUE>
|
||||||
auto modulus::success(VALUE&& value) -> std::expected<std::decay_t<VALUE>, modulus::error> {
|
auto modulus::success(VALUE &&value) -> std::expected<std::decay_t<VALUE>, modulus::error> {
|
||||||
return std::forward<VALUE>(value);
|
return std::forward<VALUE>(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,31 +5,33 @@
|
|||||||
|
|
||||||
#include <modulus/types.hpp>
|
#include <modulus/types.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace modulus {
|
namespace modulus {
|
||||||
|
|
||||||
class source_location {
|
class source_location {
|
||||||
public:
|
public:
|
||||||
constexpr source_location() noexcept = default;
|
constexpr source_location() noexcept = default;
|
||||||
constexpr source_location(std::char_t const* file, std::uint_least32_t line, std::char_t const* function) noexcept;
|
constexpr source_location(std::char_t const *file, std::uint_least32_t line, std::char_t const *function) noexcept;
|
||||||
static consteval auto current(std::char_t const* file = __builtin_FILE(), std::uint_least32_t line = __builtin_LINE(), std::char_t const* function = __builtin_FUNCTION()) noexcept -> modulus::source_location;
|
static consteval auto current(std::char_t const *file = __builtin_FILE(), std::uint_least32_t line = __builtin_LINE(), std::char_t const *function = __builtin_FUNCTION()) noexcept -> modulus::source_location;
|
||||||
static constexpr auto unknown() noexcept -> modulus::source_location;
|
static constexpr auto unknown() noexcept -> modulus::source_location;
|
||||||
constexpr auto file_name() const noexcept -> std::char_t const*;
|
constexpr auto file_name() const noexcept -> std::char_t const *;
|
||||||
constexpr auto line() const noexcept -> std::uint_least32_t;
|
constexpr auto line() const noexcept -> std::uint_least32_t;
|
||||||
constexpr auto function_name() const noexcept -> std::char_t const*;
|
constexpr auto function_name() const noexcept -> std::char_t const *;
|
||||||
private:
|
private:
|
||||||
std::char_t const* file_{"<unknown>"};
|
std::char_t const *file_{"<unknown>"};
|
||||||
std::uint_least32_t line_{};
|
std::uint_least32_t line_{};
|
||||||
std::char_t const* function_{"<unknown>"};
|
std::char_t const *function_{"<unknown>"};
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr modulus::source_location::source_location(std::char_t const* file, std::uint_least32_t line, std::char_t const* function) noexcept : file_(file), line_(line), function_(function) {
|
constexpr modulus::source_location::source_location(std::char_t const *file, std::uint_least32_t line, std::char_t const *function) noexcept
|
||||||
|
: file_(file), line_(line), function_(function) {
|
||||||
assert(file != nullptr);
|
assert(file != nullptr);
|
||||||
assert(function != nullptr);
|
assert(function != nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
consteval auto modulus::source_location::current(std::char_t const* file, std::uint_least32_t line, std::char_t const* function) noexcept -> modulus::source_location {
|
consteval auto modulus::source_location::current(std::char_t const *file, std::uint_least32_t line, std::char_t const *function) noexcept -> modulus::source_location {
|
||||||
assert(file != nullptr);
|
assert(file != nullptr);
|
||||||
assert(function != nullptr);
|
assert(function != nullptr);
|
||||||
return modulus::source_location(file, line, function);
|
return modulus::source_location(file, line, function);
|
||||||
@@ -39,7 +41,7 @@ constexpr auto modulus::source_location::unknown() noexcept -> modulus::source_l
|
|||||||
return modulus::source_location();
|
return modulus::source_location();
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr auto modulus::source_location::file_name() const noexcept -> std::char_t const* {
|
constexpr auto modulus::source_location::file_name() const noexcept -> std::char_t const * {
|
||||||
assert(file_ != nullptr);
|
assert(file_ != nullptr);
|
||||||
return file_;
|
return file_;
|
||||||
}
|
}
|
||||||
@@ -49,7 +51,7 @@ constexpr auto modulus::source_location::line() const noexcept -> std::uint_leas
|
|||||||
return line_;
|
return line_;
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr auto modulus::source_location::function_name() const noexcept -> std::char_t const* {
|
constexpr auto modulus::source_location::function_name() const noexcept -> std::char_t const * {
|
||||||
assert(function_ != nullptr);
|
assert(function_ != nullptr);
|
||||||
return function_;
|
return function_;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
#if defined(__has_include)
|
#if defined(__has_include)
|
||||||
# if __has_include(<stdfloat>)
|
#if __has_include(<stdfloat>)
|
||||||
# include <stdfloat>
|
#include <stdfloat>
|
||||||
# endif
|
|
||||||
#endif
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
namespace std {
|
namespace std {
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
|
|
||||||
__all__ = []
|
__all__ = []
|
||||||
|
|||||||
+12
-7
@@ -1,4 +1,3 @@
|
|||||||
"""Prepare container headers separately from the stdout-clean LSP transport."""
|
|
||||||
import fcntl
|
import fcntl
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
@@ -22,7 +21,11 @@ def settings(context):
|
|||||||
return {**modulus.config.clangd_defaults, **context.lsp_settings.get("clangd", {})}
|
return {**modulus.config.clangd_defaults, **context.lsp_settings.get("clangd", {})}
|
||||||
|
|
||||||
def recipe_digest(context):
|
def recipe_digest(context):
|
||||||
return hashlib.sha256(context.dockerfile.read_bytes()).hexdigest()
|
digest = hashlib.sha256(context.dockerfile.read_bytes())
|
||||||
|
requirements = context.dockerfile.parent / "format-requirements.txt"
|
||||||
|
if requirements.is_file():
|
||||||
|
digest.update(requirements.read_bytes())
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
def current_image(context, architecture=None):
|
def current_image(context, architecture=None):
|
||||||
try:
|
try:
|
||||||
@@ -106,7 +109,7 @@ def prepare(context):
|
|||||||
probe = context.tools_dir / "clangd_headers.py"
|
probe = context.tools_dir / "clangd_headers.py"
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[*isolated_run(image), "--volume", f"{probe}:/modulus-headers.py:ro", image,
|
[*isolated_run(image), "--volume", f"{probe}:/modulus-headers.py:ro", image,
|
||||||
"python3", "/modulus-headers.py", json.dumps(settings(context))],
|
"python3", "/modulus-headers.py", json.dumps(settings(context))],
|
||||||
stdout=subprocess.PIPE, text=True, check=True,
|
stdout=subprocess.PIPE, text=True, check=True,
|
||||||
)
|
)
|
||||||
roots = json.loads(result.stdout)
|
roots = json.loads(result.stdout)
|
||||||
@@ -114,8 +117,8 @@ def prepare(context):
|
|||||||
stage = pathlib.Path(temporary) / "cache"
|
stage = pathlib.Path(temporary) / "cache"
|
||||||
stage.mkdir()
|
stage.mkdir()
|
||||||
command = [*isolated_run(image), image, "tar", "--dereference", "--hard-dereference",
|
command = [*isolated_run(image), image, "tar", "--dereference", "--hard-dereference",
|
||||||
"--exclude=*/.git", "--exclude=*/__pycache__",
|
"--exclude=*/.git", "--exclude=*/__pycache__",
|
||||||
"-C", "/", "-cf", "-", *[root.lstrip("/") for root in roots]]
|
"-C", "/", "-cf", "-", *[root.lstrip("/") for root in roots]]
|
||||||
with subprocess.Popen(command, stdout=subprocess.PIPE) as process:
|
with subprocess.Popen(command, stdout=subprocess.PIPE) as process:
|
||||||
try:
|
try:
|
||||||
extract_headers(process.stdout, stage / "rootfs", roots)
|
extract_headers(process.stdout, stage / "rootfs", roots)
|
||||||
@@ -144,7 +147,7 @@ def path_mappings(cache, roots):
|
|||||||
def server_arguments(context, argv):
|
def server_arguments(context, argv):
|
||||||
config = settings(context)
|
config = settings(context)
|
||||||
return [config["executable"], *config["arguments"], *argv,
|
return [config["executable"], *config["arguments"], *argv,
|
||||||
"--query-driver=" + ",".join(config["query_drivers"])]
|
"--query-driver=" + ",".join(config["query_drivers"])]
|
||||||
|
|
||||||
def launch_command(context, argv, image, cache, roots, name):
|
def launch_command(context, argv, image, cache, roots, name):
|
||||||
root = context.repo_root
|
root = context.repo_root
|
||||||
@@ -184,8 +187,10 @@ def launch(context, argv):
|
|||||||
|
|
||||||
def run_server(command, name):
|
def run_server(command, name):
|
||||||
# Inherit stdio verbatim; never send build logs or a terminal over the LSP pipe.
|
# Inherit stdio verbatim; never send build logs or a terminal over the LSP pipe.
|
||||||
|
|
||||||
def interrupted(signum, frame):
|
def interrupted(signum, frame):
|
||||||
raise SystemExit(128 + signum)
|
raise SystemExit(128 + signum)
|
||||||
|
|
||||||
previous = {sig: signal.signal(sig, interrupted) for sig in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP)}
|
previous = {sig: signal.signal(sig, interrupted) for sig in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP)}
|
||||||
process = None
|
process = None
|
||||||
try:
|
try:
|
||||||
@@ -197,7 +202,7 @@ def run_server(command, name):
|
|||||||
if process is not None:
|
if process is not None:
|
||||||
try:
|
try:
|
||||||
subprocess.run(["docker", "rm", "--force", name], stdout=subprocess.DEVNULL,
|
subprocess.run(["docker", "rm", "--force", name], stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.DEVNULL, check=False, timeout=15)
|
stderr=subprocess.DEVNULL, check=False, timeout=15)
|
||||||
except (OSError, subprocess.TimeoutExpired) as error:
|
except (OSError, subprocess.TimeoutExpired) as error:
|
||||||
print(f"Modulus clangd cleanup: {error}", file=sys.stderr)
|
print(f"Modulus clangd cleanup: {error}", file=sys.stderr)
|
||||||
try:
|
try:
|
||||||
|
|||||||
+6
-6
@@ -12,6 +12,7 @@ import urllib.parse
|
|||||||
import modulus.config
|
import modulus.config
|
||||||
import modulus.export
|
import modulus.export
|
||||||
import modulus.clangd
|
import modulus.clangd
|
||||||
|
import modulus.formatting
|
||||||
|
|
||||||
build_root = pathlib.Path("build")
|
build_root = pathlib.Path("build")
|
||||||
common_dir = build_root / "build_common"
|
common_dir = build_root / "build_common"
|
||||||
@@ -30,6 +31,8 @@ def main(command):
|
|||||||
repo_root = find_repo_root(pathlib.Path.cwd())
|
repo_root = find_repo_root(pathlib.Path.cwd())
|
||||||
config = modulus.config.load_repo_config(repo_root)
|
config = modulus.config.load_repo_config(repo_root)
|
||||||
context = build_context(repo_root, config)
|
context = build_context(repo_root, config)
|
||||||
|
if command == "format":
|
||||||
|
return modulus.formatting.launch(context, sys.argv[1:])
|
||||||
if command == "clangd":
|
if command == "clangd":
|
||||||
return modulus.clangd.launch(context, sys.argv[1:])
|
return modulus.clangd.launch(context, sys.argv[1:])
|
||||||
if command in {"configure", "lsp"}:
|
if command in {"configure", "lsp"}:
|
||||||
@@ -79,7 +82,6 @@ class build_context:
|
|||||||
self.docker_marker = pathlib.Path(str(docker["marker"]))
|
self.docker_marker = pathlib.Path(str(docker["marker"]))
|
||||||
self.upload_env = str(self.tooling["upload_env"])
|
self.upload_env = str(self.tooling["upload_env"])
|
||||||
self.package_upload_env = str(self.artifact["upload_env"])
|
self.package_upload_env = str(self.artifact["upload_env"])
|
||||||
|
|
||||||
def _targets(self):
|
def _targets(self):
|
||||||
result = {}
|
result = {}
|
||||||
targets = self.tooling["targets"]
|
targets = self.tooling["targets"]
|
||||||
@@ -94,14 +96,12 @@ class build_context:
|
|||||||
cmake_definitions=dict(target.get("cmake_definitions", {})),
|
cmake_definitions=dict(target.get("cmake_definitions", {})),
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def target(self, name):
|
def target(self, name):
|
||||||
try:
|
try:
|
||||||
return self.targets[name]
|
return self.targets[name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
valid = ", ".join(sorted(self.targets))
|
valid = ", ".join(sorted(self.targets))
|
||||||
raise SystemExit(f"unknown target '{name}'. Valid targets: {valid}") from None
|
raise SystemExit(f"unknown target '{name}'. Valid targets: {valid}") from None
|
||||||
|
|
||||||
def selected_targets(self, names, all_targets):
|
def selected_targets(self, names, all_targets):
|
||||||
if all_targets:
|
if all_targets:
|
||||||
return list(self.targets.values())
|
return list(self.targets.values())
|
||||||
@@ -165,7 +165,7 @@ def require_conan_credentials(operation):
|
|||||||
return None
|
return None
|
||||||
return user, token
|
return user, token
|
||||||
|
|
||||||
def run(context, command, cwd=None, env_overrides=None, env_unset=None, display_command=None,):
|
def run(context, command, cwd=None, env_overrides=None, env_unset=None, display_command=None):
|
||||||
visible = display_command if display_command is not None else command
|
visible = display_command if display_command is not None else command
|
||||||
print("+", " ".join(shlex.quote(str(part)) for part in visible))
|
print("+", " ".join(shlex.quote(str(part)) for part in visible))
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
@@ -287,6 +287,8 @@ def build_image(context):
|
|||||||
context.docker_image,
|
context.docker_image,
|
||||||
"--label",
|
"--label",
|
||||||
f"{modulus.clangd.image_label}={modulus.clangd.recipe_digest(context)}",
|
f"{modulus.clangd.image_label}={modulus.clangd.recipe_digest(context)}",
|
||||||
|
"--build-arg",
|
||||||
|
f"MODULUS_TOOLS={context.tools_rel}",
|
||||||
".",
|
".",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -658,7 +660,6 @@ def lsp(context, argv):
|
|||||||
parser.add_argument("--update", action="store_true", help="Check Conan remotes for newer dependency revisions.")
|
parser.add_argument("--update", action="store_true", help="Check Conan remotes for newer dependency revisions.")
|
||||||
parser.add_argument("--no-remote", action="store_true", help="Do not query Conan remotes for Modulus build-target compile databases.")
|
parser.add_argument("--no-remote", action="store_true", help="Do not query Conan remotes for Modulus build-target compile databases.")
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
if not args.no_build_targets:
|
if not args.no_build_targets:
|
||||||
context.conan_cache_dir.mkdir(parents=True, exist_ok=True)
|
context.conan_cache_dir.mkdir(parents=True, exist_ok=True)
|
||||||
if not args.no_remote:
|
if not args.no_remote:
|
||||||
@@ -671,7 +672,6 @@ def lsp(context, argv):
|
|||||||
status = configure_one(context, target, args.update, no_remote=args.no_remote)
|
status = configure_one(context, target, args.update, no_remote=args.no_remote)
|
||||||
if status != 0:
|
if status != 0:
|
||||||
return status
|
return status
|
||||||
|
|
||||||
database_names = args.database or [str(database["name"]) for database in lsp_compile_databases(context)]
|
database_names = args.database or [str(database["name"]) for database in lsp_compile_databases(context)]
|
||||||
for name in database_names:
|
for name in database_names:
|
||||||
status = configure_lsp_database(context, database_map[name])
|
status = configure_lsp_database(context, database_map[name])
|
||||||
|
|||||||
+9
-1
@@ -4,6 +4,8 @@ import os
|
|||||||
import re
|
import re
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
|
import modulus.format_layout
|
||||||
|
|
||||||
class config_error(RuntimeError):
|
class config_error(RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -85,6 +87,7 @@ tooling_keys = {
|
|||||||
"remote",
|
"remote",
|
||||||
"run",
|
"run",
|
||||||
"lsp",
|
"lsp",
|
||||||
|
"format",
|
||||||
}
|
}
|
||||||
run_keys = {
|
run_keys = {
|
||||||
"enabled",
|
"enabled",
|
||||||
@@ -224,6 +227,11 @@ def normalize(data):
|
|||||||
)
|
)
|
||||||
if not isinstance(tooling, dict):
|
if not isinstance(tooling, dict):
|
||||||
raise config_error("modulus.json tooling must be an object")
|
raise config_error("modulus.json tooling must be an object")
|
||||||
|
formatting = tooling.setdefault("format", {})
|
||||||
|
if not isinstance(formatting, dict):
|
||||||
|
raise config_error("modulus.json tooling.format must be an object")
|
||||||
|
validate_keys(formatting, {"exclude"}, "tooling.format")
|
||||||
|
validate_string_list(formatting.setdefault("exclude", []), "tooling.format.exclude")
|
||||||
tooling.setdefault("default_target", "x86_64_gcc12_debug")
|
tooling.setdefault("default_target", "x86_64_gcc12_debug")
|
||||||
tooling.setdefault("targets", default_targets())
|
tooling.setdefault("targets", default_targets())
|
||||||
tooling.setdefault(
|
tooling.setdefault(
|
||||||
@@ -285,7 +293,7 @@ def default_editor_config(tooling):
|
|||||||
},
|
},
|
||||||
"Diagnostics": {"ClangTidy": {"Remove": ["*"]}},
|
"Diagnostics": {"ClangTidy": {"Remove": ["*"]}},
|
||||||
}],
|
}],
|
||||||
"clang_format": {"DisableFormat": True},
|
"clang_format": copy.deepcopy(modulus.format_layout.clang_style),
|
||||||
"pyright": {
|
"pyright": {
|
||||||
"include": ["modulus.py", "conanfile.py", "build/build_tools"],
|
"include": ["modulus.py", "conanfile.py", "build/build_tools"],
|
||||||
"extraPaths": ["build/build_common/modulus", "."],
|
"extraPaths": ["build/build_common/modulus", "."],
|
||||||
|
|||||||
@@ -0,0 +1,301 @@
|
|||||||
|
"""Syntax-aware whitespace rules supplementing clang-format and autopep8."""
|
||||||
|
import ast
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import tokenize
|
||||||
|
|
||||||
|
clang_style = {
|
||||||
|
"BasedOnStyle": "LLVM",
|
||||||
|
"IndentWidth": 2,
|
||||||
|
"ContinuationIndentWidth": 2,
|
||||||
|
"ColumnLimit": 0,
|
||||||
|
"NamespaceIndentation": "None",
|
||||||
|
"BreakBeforeBraces": "Attach",
|
||||||
|
"AllowShortFunctionsOnASingleLine": "Empty",
|
||||||
|
"AllowShortIfStatementsOnASingleLine": "Never",
|
||||||
|
"AllowShortLoopsOnASingleLine": False,
|
||||||
|
"InsertBraces": True,
|
||||||
|
"SeparateDefinitionBlocks": "Always",
|
||||||
|
"MaxEmptyLinesToKeep": 1,
|
||||||
|
"KeepEmptyLinesAtTheStartOfBlocks": False,
|
||||||
|
"SortIncludes": "Never",
|
||||||
|
"IncludeBlocks": "Preserve",
|
||||||
|
"SortUsingDeclarations": "Never",
|
||||||
|
"BreakStringLiterals": False,
|
||||||
|
"ReflowComments": False,
|
||||||
|
"FixNamespaceComments": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def descendants(node):
|
||||||
|
yield node
|
||||||
|
for child in node.children:
|
||||||
|
yield from descendants(child)
|
||||||
|
|
||||||
|
def cpp_tree(source):
|
||||||
|
import tree_sitter
|
||||||
|
import tree_sitter_cpp
|
||||||
|
parser = tree_sitter.Parser(tree_sitter.Language(tree_sitter_cpp.language()))
|
||||||
|
return parser.parse(source).root_node
|
||||||
|
|
||||||
|
def protected_cpp(node):
|
||||||
|
return node.type in {"string_literal", "raw_string_literal", "char_literal", "comment", "preproc_def", "preproc_function_def", "preproc_call", "preproc_include", "preproc_arg"}
|
||||||
|
|
||||||
|
def apply_edits(source, edits):
|
||||||
|
disjoint = []
|
||||||
|
end = 0
|
||||||
|
for start, stop, replacement in sorted(edits, key=lambda edit: (edit[0], -edit[1])):
|
||||||
|
if start >= end:
|
||||||
|
disjoint.append((start, stop, replacement))
|
||||||
|
end = stop
|
||||||
|
for start, stop, replacement in reversed(disjoint):
|
||||||
|
source = source[:start] + replacement + source[stop:]
|
||||||
|
return source
|
||||||
|
|
||||||
|
def join_cpp(source):
|
||||||
|
edits = []
|
||||||
|
root = cpp_tree(source)
|
||||||
|
for node in descendants(root):
|
||||||
|
if node.type not in {"parameter_list", "argument_list", "template_argument_list", "template_parameter_list"}:
|
||||||
|
continue
|
||||||
|
children = list(descendants(node))
|
||||||
|
if node.has_error or any(child.type in {"comment", "lambda_expression", "initializer_list"} or (protected_cpp(child) and child.start_point.row != child.end_point.row) for child in children):
|
||||||
|
continue
|
||||||
|
parent = node.parent
|
||||||
|
if any(protected_cpp(ancestor) for ancestor in ancestors(parent)):
|
||||||
|
continue
|
||||||
|
value = source[node.start_byte:node.end_byte]
|
||||||
|
if b"\n" in value:
|
||||||
|
edits.append((node.start_byte, node.end_byte, re.sub(rb"[ \t]*\r?\n[ \t]*", b" ", value)))
|
||||||
|
return apply_edits(source, edits)
|
||||||
|
|
||||||
|
def ancestors(node):
|
||||||
|
while node is not None:
|
||||||
|
yield node
|
||||||
|
node = node.parent
|
||||||
|
|
||||||
|
def space_lines(source, compact, units, padding, protected, class_interiors=()):
|
||||||
|
lines = source.splitlines(keepends=True)
|
||||||
|
boundaries = set(padding)
|
||||||
|
for first, last in units:
|
||||||
|
if first > 0:
|
||||||
|
boundaries.add(first)
|
||||||
|
boundaries.add(last + 1)
|
||||||
|
# Class/struct members stay together even when a member is itself a definition.
|
||||||
|
boundaries.difference_update(class_interiors)
|
||||||
|
compact = compact.union(class_interiors)
|
||||||
|
output = []
|
||||||
|
pending = []
|
||||||
|
previous = None
|
||||||
|
for row, line in enumerate(lines):
|
||||||
|
if not line.strip() and row not in protected:
|
||||||
|
pending.append(row)
|
||||||
|
continue
|
||||||
|
if previous is not None:
|
||||||
|
gap = range(previous + 1, row + 1)
|
||||||
|
wanted = any(boundary in boundaries for boundary in gap)
|
||||||
|
inside = any(position in compact for position in gap)
|
||||||
|
if wanted or (pending and not inside):
|
||||||
|
output.append("\n")
|
||||||
|
output.append(line)
|
||||||
|
previous = row
|
||||||
|
pending = []
|
||||||
|
return "".join(output).rstrip("\n") + "\n"
|
||||||
|
|
||||||
|
def space_cpp(source):
|
||||||
|
root = cpp_tree(source.encode())
|
||||||
|
compact, protected, padding = set(), set(), set()
|
||||||
|
class_interiors = set()
|
||||||
|
units = []
|
||||||
|
for node in descendants(root):
|
||||||
|
first, last = node.start_point.row, node.end_point.row
|
||||||
|
if protected_cpp(node):
|
||||||
|
protected.update(range(first, last + 1))
|
||||||
|
if node.type == "compound_statement" and not node.has_error:
|
||||||
|
compact.update(range(first + 1, last + 1))
|
||||||
|
if node.type == "field_declaration_list" and len(node.children) >= 2:
|
||||||
|
opening, closing = node.children[0], node.children[-1]
|
||||||
|
if opening.type == "{" and closing.type == "}" and not opening.is_missing and not closing.is_missing:
|
||||||
|
class_interiors.update(range(first + 1, last + 1))
|
||||||
|
if node.type == "namespace_definition":
|
||||||
|
body = node.child_by_field_name("body")
|
||||||
|
if body and body.end_point.row > body.start_point.row + 1:
|
||||||
|
padding.update((body.start_point.row + 1, body.end_point.row))
|
||||||
|
if node.type in {"function_definition", "class_specifier", "struct_specifier", "enum_specifier", "namespace_definition"} and not node.has_error:
|
||||||
|
unit = node
|
||||||
|
while unit.parent and unit.parent.type in {"template_declaration", "declaration", "type_definition"}:
|
||||||
|
unit = unit.parent
|
||||||
|
before = source.splitlines()[unit.start_point.row][:unit.start_point.column]
|
||||||
|
if not before.strip():
|
||||||
|
units.append((unit.start_point.row, unit.end_point.row))
|
||||||
|
# Unit padding belongs between siblings, never between a block brace and a function.
|
||||||
|
lines = source.splitlines()
|
||||||
|
units = [(first, last) for first, last in units if first not in protected and last not in protected]
|
||||||
|
boundaries = set(padding)
|
||||||
|
for first, last in units:
|
||||||
|
previous = next((line.strip() for line in reversed(lines[:first]) if line.strip()), "")
|
||||||
|
following = next((line.strip() for line in lines[last + 1:] if line.strip()), "")
|
||||||
|
if previous and not previous.endswith("{"):
|
||||||
|
boundaries.add(first)
|
||||||
|
if following and not following.startswith("}"):
|
||||||
|
boundaries.add(last + 1)
|
||||||
|
return space_lines(source, compact, [], boundaries, protected, class_interiors)
|
||||||
|
|
||||||
|
def python_tokens(source):
|
||||||
|
return list(tokenize.generate_tokens(io.StringIO(source).readline))
|
||||||
|
|
||||||
|
def python_literal_lines(tree):
|
||||||
|
rows = set()
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, (ast.JoinedStr, ast.Constant)) and hasattr(node, "end_lineno"):
|
||||||
|
if isinstance(node, ast.JoinedStr) or isinstance(node.value, (str, bytes)):
|
||||||
|
rows.update(range(node.lineno - 1, node.end_lineno))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def indent_python(source):
|
||||||
|
lines = source.splitlines(keepends=True)
|
||||||
|
indents = [len(line) - len(line.lstrip(" \t")) for line in lines]
|
||||||
|
tokens = python_tokens(source)
|
||||||
|
stack, handled = [], set()
|
||||||
|
statement_indent = None
|
||||||
|
literal_starts = []
|
||||||
|
for index, token in enumerate(tokens):
|
||||||
|
# Python 3.12+ tokenizes f-string expressions separately; their contents stay intact.
|
||||||
|
if token.type == getattr(tokenize, "FSTRING_END", None):
|
||||||
|
handled.update(range(literal_starts.pop(), token.end[0]))
|
||||||
|
continue
|
||||||
|
if literal_starts:
|
||||||
|
if token.type == getattr(tokenize, "FSTRING_START", None):
|
||||||
|
literal_starts.append(token.start[0])
|
||||||
|
continue
|
||||||
|
if token.type in {tokenize.INDENT, tokenize.DEDENT, tokenize.NL, tokenize.ENDMARKER}:
|
||||||
|
continue
|
||||||
|
if token.type == tokenize.NEWLINE:
|
||||||
|
statement_indent = None
|
||||||
|
continue
|
||||||
|
if token.type == tokenize.STRING:
|
||||||
|
handled.update(range(token.start[0], token.end[0]))
|
||||||
|
row = token.start[0] - 1
|
||||||
|
if row not in handled:
|
||||||
|
indent = indents[row]
|
||||||
|
if stack:
|
||||||
|
# Inline nested brackets share their opening line's indentation.
|
||||||
|
indent = stack[-1] + 2
|
||||||
|
closing = 0
|
||||||
|
for following_index in range(index, len(tokens)):
|
||||||
|
following = tokens[following_index]
|
||||||
|
if following.start[0] != token.start[0] or following.type != tokenize.OP or following.string not in ")]}":
|
||||||
|
break
|
||||||
|
closing += 1
|
||||||
|
if closing:
|
||||||
|
indent = stack[-closing]
|
||||||
|
elif statement_indent is not None:
|
||||||
|
indent = statement_indent + 2
|
||||||
|
lines[row] = " " * indent + lines[row].lstrip(" \t")
|
||||||
|
indents[row] = indent
|
||||||
|
handled.add(row)
|
||||||
|
if statement_indent is None and token.type != tokenize.COMMENT:
|
||||||
|
statement_indent = indents[row]
|
||||||
|
if token.type == getattr(tokenize, "FSTRING_START", None):
|
||||||
|
literal_starts.append(token.start[0])
|
||||||
|
if token.type == tokenize.OP:
|
||||||
|
if token.string in "([{":
|
||||||
|
stack.append(indents[row])
|
||||||
|
elif token.string in ")]}":
|
||||||
|
stack.pop()
|
||||||
|
return "".join(lines)
|
||||||
|
|
||||||
|
def join_python(source):
|
||||||
|
tokens = python_tokens(source)
|
||||||
|
literals = python_literal_lines(ast.parse(source))
|
||||||
|
lines = source.splitlines(keepends=True)
|
||||||
|
offsets = [0]
|
||||||
|
for line in lines:
|
||||||
|
offsets.append(offsets[-1] + len(line))
|
||||||
|
stack, edits = [], []
|
||||||
|
for index, token in enumerate(tokens):
|
||||||
|
if token.type != tokenize.OP:
|
||||||
|
continue
|
||||||
|
if token.string in "([{":
|
||||||
|
stack.append(index)
|
||||||
|
elif token.string in ")]}":
|
||||||
|
start = stack.pop()
|
||||||
|
opening = tokens[start]
|
||||||
|
prior = tokens[start - 1] if start else None
|
||||||
|
# Only calls and signatures. Structured literals and multiline strings stay intact.
|
||||||
|
if opening.string != "(" or not prior or prior.type != tokenize.NAME or prior.string == "import":
|
||||||
|
continue
|
||||||
|
content = tokens[start + 1:index]
|
||||||
|
if any(row in literals for row in range(opening.start[0], token.end[0] - 1)):
|
||||||
|
continue
|
||||||
|
if any(item.type == tokenize.COMMENT or (item.type == tokenize.STRING and item.start[0] != item.end[0]) or item.string in {"[", "{", "lambda", "for"} for item in content):
|
||||||
|
continue
|
||||||
|
first = offsets[opening.start[0] - 1] + opening.start[1]
|
||||||
|
last = offsets[token.end[0] - 1] + token.end[1]
|
||||||
|
value = re.sub(r"[ \t]*\r?\n[ \t]*", " ", source[first:last])
|
||||||
|
value = re.sub(r",[ \t]*\)$", ")", value)
|
||||||
|
if value != source[first:last]:
|
||||||
|
edits.append((first, last, value))
|
||||||
|
return apply_edits(source, edits)
|
||||||
|
|
||||||
|
def space_python(source):
|
||||||
|
tree = ast.parse(source)
|
||||||
|
compact, protected = set(), python_literal_lines(tree)
|
||||||
|
class_interiors = set()
|
||||||
|
units = []
|
||||||
|
for token in python_tokens(source):
|
||||||
|
if token.type in {tokenize.STRING, tokenize.COMMENT}:
|
||||||
|
protected.update(range(token.start[0] - 1, token.end[0]))
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.ClassDef):
|
||||||
|
class_interiors.update(range(node.lineno, node.end_lineno))
|
||||||
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||||
|
compact.update(range(node.lineno, node.end_lineno))
|
||||||
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
||||||
|
first = min([node.lineno, *[decorator.lineno for decorator in node.decorator_list]]) - 1
|
||||||
|
last = node.end_lineno - 1
|
||||||
|
units.append((first, last))
|
||||||
|
return space_lines(source, compact, units, set(), protected, class_interiors)
|
||||||
|
|
||||||
|
def format_source(source, suffix, style=None):
|
||||||
|
if not source.strip():
|
||||||
|
return source
|
||||||
|
if suffix == ".py":
|
||||||
|
import autopep8
|
||||||
|
before = ast.dump(ast.parse(source))
|
||||||
|
result = autopep8.fix_code(join_python(source), options={"indent_size": 2, "ignore": ["E501", "E3", "E4", "W50", "W6"], "aggressive": 0})
|
||||||
|
result = space_python(indent_python(result))
|
||||||
|
if ast.dump(ast.parse(result)) != before:
|
||||||
|
raise ValueError("formatter changed Python syntax")
|
||||||
|
return result
|
||||||
|
result = subprocess.run(["clang-format", "--style=" + json.dumps(style or clang_style)], input=join_cpp(source.encode()).decode(), text=True, capture_output=True, check=True)
|
||||||
|
return space_cpp(result.stdout)
|
||||||
|
|
||||||
|
def alias_diagnostics(source, suffix, portability=False):
|
||||||
|
if suffix != ".py":
|
||||||
|
root = cpp_tree(source.encode())
|
||||||
|
allowed = {"bool_t", "char_t", "uchar_t", "float32_t", "float64_t"}
|
||||||
|
for node in descendants(root):
|
||||||
|
if node.type in {"alias_declaration", "namespace_alias_definition", "type_definition"}:
|
||||||
|
name = node.child_by_field_name("name")
|
||||||
|
if portability and name and name.text.decode() in allowed and any(parent.type == "namespace_definition" and parent.child_by_field_name("name").text == b"std" for parent in ancestors(node.parent)):
|
||||||
|
continue
|
||||||
|
yield node.start_point.row + 1, "use the original type or namespace, not an alias"
|
||||||
|
return
|
||||||
|
tree = ast.parse(source)
|
||||||
|
modules = {alias.name for node in ast.walk(tree) if isinstance(node, ast.Import) for alias in node.names}
|
||||||
|
modules.update(name.split(".")[0] for name in list(modules))
|
||||||
|
typing_names = {alias.name for node in ast.walk(tree) if isinstance(node, ast.ImportFrom) and node.module in {"typing", "typing_extensions"} for alias in node.names}
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, (ast.Import, ast.ImportFrom)) and any(alias.asname for alias in node.names):
|
||||||
|
yield node.lineno, "use imports without renaming"
|
||||||
|
if isinstance(node, getattr(ast, "TypeAlias", ())):
|
||||||
|
yield node.lineno, "write the type annotation directly"
|
||||||
|
if isinstance(node, (ast.Assign, ast.AnnAssign)) and node.value is not None:
|
||||||
|
value = node.value
|
||||||
|
explicit = isinstance(node, ast.AnnAssign) and ast.unparse(node.annotation).split(".")[-1] == "TypeAlias"
|
||||||
|
type_expression = isinstance(value, ast.Subscript) and (ast.unparse(value.value) in typing_names | {"list", "dict", "tuple", "set", "frozenset", "type"} or ast.unparse(value.value).startswith(("typing.", "typing_extensions.")))
|
||||||
|
module_reference = isinstance(value, (ast.Name, ast.Attribute)) and ast.unparse(value) in modules
|
||||||
|
if explicit or type_expression or module_reference:
|
||||||
|
yield node.lineno, "use the original module or inline type annotation"
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
"""Shared batch/check and stdout-clean editor formatting transport."""
|
||||||
|
import argparse
|
||||||
|
import fnmatch
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import modulus.clangd
|
||||||
|
import modulus.format_layout
|
||||||
|
|
||||||
|
suffixes = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".py"}
|
||||||
|
excluded_directories = {".git", ".venv", "venv", "build", "dist", "generated", "CMakeFiles", "__pycache__", ".cache", ".mypy_cache", ".pytest_cache", ".tox", "_deps", "vendor", "third_party", "third-party", "external", "node_modules"}
|
||||||
|
|
||||||
|
def eligible(root, path, excludes):
|
||||||
|
relative = path.relative_to(root)
|
||||||
|
if path.suffix not in suffixes or any(part in excluded_directories for part in relative.parts):
|
||||||
|
return False
|
||||||
|
if any(fnmatch.fnmatch(relative.as_posix(), pattern) for pattern in excludes):
|
||||||
|
return False
|
||||||
|
return path.resolve().is_relative_to(root.resolve()) and not any(parent.is_symlink() for parent in [path, *path.parents] if parent != root)
|
||||||
|
|
||||||
|
def source_files(root, requested, excludes):
|
||||||
|
result = subprocess.run(["git", "-C", str(root), "ls-files", "--cached", "--others", "--exclude-standard", "-z"], capture_output=True, check=True)
|
||||||
|
candidates = {root / os.fsdecode(name) for name in result.stdout.split(b"\0") if name}
|
||||||
|
selected = [pathlib.Path(name) for name in requested]
|
||||||
|
if any(path.is_absolute() or ".." in path.parts for path in selected):
|
||||||
|
raise ValueError("format paths must be relative to the repository")
|
||||||
|
return sorted(path for path in candidates if path.is_file() and eligible(root, path, excludes) and (not selected or any(path == root / name or path.is_relative_to(root / name) for name in selected)))
|
||||||
|
|
||||||
|
def local_format(context, args):
|
||||||
|
root = context.repo_root
|
||||||
|
settings = context.tooling.get("format", {})
|
||||||
|
excludes = settings.get("exclude", [])
|
||||||
|
style = context.tooling["editor"]["clang_format"]
|
||||||
|
if args.stdin_filename:
|
||||||
|
path = pathlib.Path(args.stdin_filename)
|
||||||
|
path = path if path.is_absolute() else root / path
|
||||||
|
if not path.is_relative_to(root) or not eligible(root, path, excludes):
|
||||||
|
raise ValueError("buffer is outside the project's formattable sources")
|
||||||
|
source = sys.stdin.read()
|
||||||
|
sys.stdout.write(modulus.format_layout.format_source(source, path.suffix, style))
|
||||||
|
return 0
|
||||||
|
changed = []
|
||||||
|
diagnostics = []
|
||||||
|
# Validate and format every input before changing any files.
|
||||||
|
for path in source_files(root, args.paths, excludes):
|
||||||
|
source = path.read_text(encoding="utf-8")
|
||||||
|
try:
|
||||||
|
result = modulus.format_layout.format_source(source, path.suffix, style)
|
||||||
|
except (ValueError, SyntaxError) as error:
|
||||||
|
raise ValueError(f"{path.relative_to(root)}: {error}") from error
|
||||||
|
if source != result:
|
||||||
|
changed.append((path, result))
|
||||||
|
if args.check:
|
||||||
|
portability = context.package["name"] == "modulus" and path.relative_to(root).as_posix() == "include/modulus/types.hpp"
|
||||||
|
diagnostics.extend(f"{path.relative_to(root)}:{line}: {message}" for line, message in modulus.format_layout.alias_diagnostics(source, path.suffix, portability))
|
||||||
|
if args.check:
|
||||||
|
for path, _ in changed:
|
||||||
|
print(f"{path.relative_to(root)}: formatting differs", file=sys.stderr)
|
||||||
|
for message in diagnostics:
|
||||||
|
print(message, file=sys.stderr)
|
||||||
|
return int(bool(changed or diagnostics))
|
||||||
|
for path, result in changed:
|
||||||
|
path.write_text(result, encoding="utf-8")
|
||||||
|
print(f"Formatted {len(changed)} files.", file=sys.stderr)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def launch(context, argv):
|
||||||
|
parser = argparse.ArgumentParser(description="Format project C/C++ and Python with the shared Modulus policy.")
|
||||||
|
parser.add_argument("--check", action="store_true", help="check layout and prohibited aliases without writing files")
|
||||||
|
parser.add_argument("--stdin-filename", help="format stdin using this buffer's path; output only formatted text")
|
||||||
|
parser.add_argument("paths", nargs="*", help="optional repository-relative files or directories")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
if args.stdin_filename and (args.check or args.paths):
|
||||||
|
parser.error("--stdin-filename cannot be combined with --check or paths")
|
||||||
|
try:
|
||||||
|
if context.docker_marker.exists() or os.environ.get("MODULUS_DIRECT") == "1":
|
||||||
|
return local_format(context, args)
|
||||||
|
image = modulus.clangd.current_image(context)
|
||||||
|
if image is None:
|
||||||
|
raise RuntimeError("missing or stale build image; run configure first")
|
||||||
|
root = context.repo_root
|
||||||
|
mode = "ro" if args.check or args.stdin_filename else "rw"
|
||||||
|
command = [*modulus.clangd.isolated_run(image), "--interactive", "--volume", f"{root}:{root}:{mode}", "--env", "PYTHONDONTWRITEBYTECODE=1", "--workdir", str(root), image, "python3", str(context.tools_dir / "format.py"), *argv]
|
||||||
|
return subprocess.run(command, check=False).returncode
|
||||||
|
except (OSError, ValueError, SyntaxError, ImportError, RuntimeError, subprocess.SubprocessError) as error:
|
||||||
|
detail = error.stderr if isinstance(error, subprocess.CalledProcessError) and error.stderr else str(error)
|
||||||
|
print(f"Modulus format: {detail}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
@@ -134,6 +134,13 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"format": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"exclude": { "type": "array", "items": { "type": "string" } }
|
||||||
|
}
|
||||||
|
},
|
||||||
"default_target": {
|
"default_target": {
|
||||||
"$ref": "#/$defs/cmake_name"
|
"$ref": "#/$defs/cmake_name"
|
||||||
},
|
},
|
||||||
|
|||||||
+24
-21
@@ -17,6 +17,7 @@
|
|||||||
#include <modulus/source_location.hpp>
|
#include <modulus/source_location.hpp>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
auto config_error(std::string message) -> modulus::error {
|
auto config_error(std::string message) -> modulus::error {
|
||||||
@@ -74,7 +75,7 @@ auto log_level_name(modulus::log_level level) -> std::string_view {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::parse_logging_config(boost::json::value const& value) -> std::expected<modulus::logging_config, modulus::error> {
|
auto modulus::parse_logging_config(boost::json::value const &value) -> std::expected<modulus::logging_config, modulus::error> {
|
||||||
auto object = modulus::json_config::require_object(value, "logging config");
|
auto object = modulus::json_config::require_object(value, "logging config");
|
||||||
if (!object) {
|
if (!object) {
|
||||||
return modulus::fail<modulus::logging_config>(object.error());
|
return modulus::fail<modulus::logging_config>(object.error());
|
||||||
@@ -99,17 +100,18 @@ auto modulus::parse_logging_config(boost::json::value const& value) -> std::expe
|
|||||||
return modulus::success(config);
|
return modulus::success(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::parse_error_config(boost::json::value const& value) -> std::expected<modulus::error_config, modulus::error> {
|
auto modulus::parse_error_config(boost::json::value const &value) -> std::expected<modulus::error_config, modulus::error> {
|
||||||
auto object = modulus::json_config::require_object(value, "error config");
|
auto object = modulus::json_config::require_object(value, "error config");
|
||||||
if (!object) {
|
if (!object) {
|
||||||
return modulus::fail<modulus::error_config>(object.error());
|
return modulus::fail<modulus::error_config>(object.error());
|
||||||
}
|
}
|
||||||
auto unknown = modulus::json_config::reject_unknown_fields(**object, {
|
auto unknown = modulus::json_config::reject_unknown_fields(**object, {
|
||||||
"include_category",
|
"include_category",
|
||||||
"include_code",
|
"include_code",
|
||||||
"include_message",
|
"include_message",
|
||||||
"include_location",
|
"include_location",
|
||||||
}, "error config");
|
},
|
||||||
|
"error config");
|
||||||
if (!unknown) {
|
if (!unknown) {
|
||||||
return modulus::fail<modulus::error_config>(unknown.error());
|
return modulus::fail<modulus::error_config>(unknown.error());
|
||||||
}
|
}
|
||||||
@@ -137,15 +139,16 @@ auto modulus::parse_error_config(boost::json::value const& value) -> std::expect
|
|||||||
return modulus::success(config);
|
return modulus::success(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::parse_config(boost::json::value const& value) -> std::expected<modulus::config, modulus::error> {
|
auto modulus::parse_config(boost::json::value const &value) -> std::expected<modulus::config, modulus::error> {
|
||||||
auto object = modulus::json_config::require_object(value, "config root");
|
auto object = modulus::json_config::require_object(value, "config root");
|
||||||
if (!object) {
|
if (!object) {
|
||||||
return modulus::fail<modulus::config>(object.error());
|
return modulus::fail<modulus::config>(object.error());
|
||||||
}
|
}
|
||||||
auto unknown = modulus::json_config::reject_unknown_fields(**object, {
|
auto unknown = modulus::json_config::reject_unknown_fields(**object, {
|
||||||
"logging",
|
"logging",
|
||||||
"errors",
|
"errors",
|
||||||
}, "config root");
|
},
|
||||||
|
"config root");
|
||||||
if (!unknown) {
|
if (!unknown) {
|
||||||
return modulus::fail<modulus::config>(unknown.error());
|
return modulus::fail<modulus::config>(unknown.error());
|
||||||
}
|
}
|
||||||
@@ -167,19 +170,19 @@ auto modulus::parse_config(boost::json::value const& value) -> std::expected<mod
|
|||||||
return modulus::success(config);
|
return modulus::success(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::load_config(std::string const& path) -> std::expected<modulus::config, modulus::error> {
|
auto modulus::load_config(std::string const &path) -> std::expected<modulus::config, modulus::error> {
|
||||||
auto value = modulus::json_config::load_value(path, {
|
auto value = modulus::json_config::load_value(path, {
|
||||||
.category = "modulus.config",
|
.category = "modulus.config",
|
||||||
.invalid_argument_code = 1,
|
.invalid_argument_code = 1,
|
||||||
.not_found_code = 1,
|
.not_found_code = 1,
|
||||||
});
|
});
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return modulus::fail<modulus::config>(value.error());
|
return modulus::fail<modulus::config>(value.error());
|
||||||
}
|
}
|
||||||
return modulus::parse_config(*value);
|
return modulus::parse_config(*value);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::to_json(modulus::logging_config const& config) -> boost::json::value {
|
auto modulus::to_json(modulus::logging_config const &config) -> boost::json::value {
|
||||||
return boost::json::object{
|
return boost::json::object{
|
||||||
{
|
{
|
||||||
"level",
|
"level",
|
||||||
@@ -188,7 +191,7 @@ auto modulus::to_json(modulus::logging_config const& config) -> boost::json::val
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::to_json(modulus::error_config const& config) -> boost::json::value {
|
auto modulus::to_json(modulus::error_config const &config) -> boost::json::value {
|
||||||
return boost::json::object{
|
return boost::json::object{
|
||||||
{
|
{
|
||||||
"include_category",
|
"include_category",
|
||||||
@@ -209,7 +212,7 @@ auto modulus::to_json(modulus::error_config const& config) -> boost::json::value
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::to_json(modulus::config const& config) -> boost::json::value {
|
auto modulus::to_json(modulus::config const &config) -> boost::json::value {
|
||||||
return boost::json::object{
|
return boost::json::object{
|
||||||
{
|
{
|
||||||
"logging",
|
"logging",
|
||||||
@@ -222,12 +225,12 @@ auto modulus::to_json(modulus::config const& config) -> boost::json::value {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::configure_logger(modulus::logger& logger, modulus::logging_config const& config) -> void {
|
auto modulus::configure_logger(modulus::logger &logger, modulus::logging_config const &config) -> void {
|
||||||
logger.set_level(config.level);
|
logger.set_level(config.level);
|
||||||
modulus::log_debug("modulus logger configured");
|
modulus::log_debug("modulus logger configured");
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::configure(modulus::config const& config) -> void {
|
auto modulus::configure(modulus::config const &config) -> void {
|
||||||
modulus::configure_logger(modulus::default_logger(), config.logging);
|
modulus::configure_logger(modulus::default_logger(), config.logging);
|
||||||
modulus::set_default_error_config(config.errors);
|
modulus::set_default_error_config(config.errors);
|
||||||
modulus::log_debug("modulus configuration applied");
|
modulus::log_debug("modulus configuration applied");
|
||||||
|
|||||||
+7
-5
@@ -6,21 +6,23 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
auto config_mutex() -> std::mutex& {
|
auto config_mutex() -> std::mutex & {
|
||||||
static auto mutex = std::mutex{};
|
static auto mutex = std::mutex{};
|
||||||
return mutex;
|
return mutex;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto config_storage() -> modulus::error_config& {
|
auto config_storage() -> modulus::error_config & {
|
||||||
static auto config = modulus::error_config{};
|
static auto config = modulus::error_config{};
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
modulus::error::error(std::string category, std::int32_t code, std::string message, modulus::source_location location) : category_(std::move(category)), code_(code), message_(std::move(message)), location_(location) {
|
modulus::error::error(std::string category, std::int32_t code, std::string message, modulus::source_location location)
|
||||||
|
: category_(std::move(category)), code_(code), message_(std::move(message)), location_(location) {
|
||||||
assert(!category_.empty());
|
assert(!category_.empty());
|
||||||
assert(location_.file_name() != nullptr);
|
assert(location_.file_name() != nullptr);
|
||||||
assert(location_.function_name() != nullptr);
|
assert(location_.function_name() != nullptr);
|
||||||
@@ -41,12 +43,12 @@ auto modulus::error::message() const noexcept -> std::string_view {
|
|||||||
return message_;
|
return message_;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::error::location() const noexcept -> const modulus::source_location& {
|
auto modulus::error::location() const noexcept -> const modulus::source_location & {
|
||||||
assert(location_.file_name() != nullptr);
|
assert(location_.file_name() != nullptr);
|
||||||
return location_;
|
return location_;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::error::describe(modulus::error_config const& config) const -> std::string {
|
auto modulus::error::describe(modulus::error_config const &config) const -> std::string {
|
||||||
assert(!category_.empty());
|
assert(!category_.empty());
|
||||||
assert(location_.file_name() != nullptr);
|
assert(location_.file_name() != nullptr);
|
||||||
auto out = std::ostringstream{};
|
auto out = std::ostringstream{};
|
||||||
|
|||||||
+13
-12
@@ -17,6 +17,7 @@
|
|||||||
#include <modulus/error.hpp>
|
#include <modulus/error.hpp>
|
||||||
#include <modulus/types.hpp>
|
#include <modulus/types.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
auto make_config_error(modulus::json_config::options config, std::int32_t code, std::string message) -> modulus::error {
|
auto make_config_error(modulus::json_config::options config, std::int32_t code, std::string message) -> modulus::error {
|
||||||
@@ -35,7 +36,7 @@ auto key_allowed(std::string_view key, std::initializer_list<std::string_view> a
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto integer_value(boost::json::value const& value, std::string_view name, modulus::json_config::options config) -> std::expected<std::int64_t, modulus::error> {
|
auto integer_value(boost::json::value const &value, std::string_view name, modulus::json_config::options config) -> std::expected<std::int64_t, modulus::error> {
|
||||||
if (!value.is_number() || value.is_double()) {
|
if (!value.is_number() || value.is_double()) {
|
||||||
return std::unexpected(modulus::json_config::invalid(config, std::string{name} + " must be an integer"));
|
return std::unexpected(modulus::json_config::invalid(config, std::string{name} + " must be an integer"));
|
||||||
}
|
}
|
||||||
@@ -58,7 +59,7 @@ auto modulus::json_config::not_found(modulus::json_config::options config, std::
|
|||||||
return make_config_error(config, config.not_found_code, std::move(message));
|
return make_config_error(config, config.not_found_code, std::move(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::json_config::load_value(std::string const& path, modulus::json_config::options config) -> std::expected<boost::json::value, modulus::error> {
|
auto modulus::json_config::load_value(std::string const &path, modulus::json_config::options config) -> std::expected<boost::json::value, modulus::error> {
|
||||||
if (path.empty()) {
|
if (path.empty()) {
|
||||||
return std::unexpected(modulus::json_config::invalid(config, "config path must not be empty"));
|
return std::unexpected(modulus::json_config::invalid(config, "config path must not be empty"));
|
||||||
}
|
}
|
||||||
@@ -76,7 +77,7 @@ auto modulus::json_config::load_value(std::string const& path, modulus::json_con
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::json_config::require_object(boost::json::value const& value, std::string_view context, modulus::json_config::options config) -> std::expected<boost::json::object const*, modulus::error> {
|
auto modulus::json_config::require_object(boost::json::value const &value, std::string_view context, modulus::json_config::options config) -> std::expected<boost::json::object const *, modulus::error> {
|
||||||
assert(context.data() != nullptr);
|
assert(context.data() != nullptr);
|
||||||
if (!value.is_object()) {
|
if (!value.is_object()) {
|
||||||
return std::unexpected(modulus::json_config::invalid(config, std::string{context} + " must be an object"));
|
return std::unexpected(modulus::json_config::invalid(config, std::string{context} + " must be an object"));
|
||||||
@@ -84,9 +85,9 @@ auto modulus::json_config::require_object(boost::json::value const& value, std::
|
|||||||
return &value.as_object();
|
return &value.as_object();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::json_config::reject_unknown_fields(boost::json::object const& object, std::initializer_list<std::string_view> allowed, std::string_view context, modulus::json_config::options config) -> std::expected<void, modulus::error> {
|
auto modulus::json_config::reject_unknown_fields(boost::json::object const &object, std::initializer_list<std::string_view> allowed, std::string_view context, modulus::json_config::options config) -> std::expected<void, modulus::error> {
|
||||||
assert(context.data() != nullptr);
|
assert(context.data() != nullptr);
|
||||||
for (auto const& field : object) {
|
for (auto const &field : object) {
|
||||||
auto key = std::string_view{
|
auto key = std::string_view{
|
||||||
field.key().data(),
|
field.key().data(),
|
||||||
field.key().size(),
|
field.key().size(),
|
||||||
@@ -98,7 +99,7 @@ auto modulus::json_config::reject_unknown_fields(boost::json::object const& obje
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::json_config::optional_bool_field(boost::json::object const& object, std::string_view name, std::bool_t fallback, modulus::json_config::options config) -> std::expected<std::bool_t, modulus::error> {
|
auto modulus::json_config::optional_bool_field(boost::json::object const &object, std::string_view name, std::bool_t fallback, modulus::json_config::options config) -> std::expected<std::bool_t, modulus::error> {
|
||||||
assert(name.data() != nullptr);
|
assert(name.data() != nullptr);
|
||||||
auto found = object.find(name);
|
auto found = object.find(name);
|
||||||
if (found == object.end()) {
|
if (found == object.end()) {
|
||||||
@@ -110,7 +111,7 @@ auto modulus::json_config::optional_bool_field(boost::json::object const& object
|
|||||||
return found->value().as_bool();
|
return found->value().as_bool();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::json_config::optional_string_field(boost::json::object const& object, std::string_view name, std::string fallback, modulus::json_config::options config) -> std::expected<std::string, modulus::error> {
|
auto modulus::json_config::optional_string_field(boost::json::object const &object, std::string_view name, std::string fallback, modulus::json_config::options config) -> std::expected<std::string, modulus::error> {
|
||||||
assert(name.data() != nullptr);
|
assert(name.data() != nullptr);
|
||||||
auto found = object.find(name);
|
auto found = object.find(name);
|
||||||
if (found == object.end()) {
|
if (found == object.end()) {
|
||||||
@@ -122,7 +123,7 @@ auto modulus::json_config::optional_string_field(boost::json::object const& obje
|
|||||||
return std::string{found->value().as_string()};
|
return std::string{found->value().as_string()};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::json_config::required_string_field(boost::json::object const& object, std::string_view name, std::string_view context, modulus::json_config::options config) -> std::expected<std::string, modulus::error> {
|
auto modulus::json_config::required_string_field(boost::json::object const &object, std::string_view name, std::string_view context, modulus::json_config::options config) -> std::expected<std::string, modulus::error> {
|
||||||
assert(name.data() != nullptr);
|
assert(name.data() != nullptr);
|
||||||
assert(context.data() != nullptr);
|
assert(context.data() != nullptr);
|
||||||
auto found = object.find(name);
|
auto found = object.find(name);
|
||||||
@@ -136,7 +137,7 @@ auto modulus::json_config::required_string_field(boost::json::object const& obje
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::json_config::optional_uint64_field(boost::json::object const& object, std::string_view name, std::uint64_t fallback, std::uint64_t max_value, modulus::json_config::options config) -> std::expected<std::uint64_t, modulus::error> {
|
auto modulus::json_config::optional_uint64_field(boost::json::object const &object, std::string_view name, std::uint64_t fallback, std::uint64_t max_value, modulus::json_config::options config) -> std::expected<std::uint64_t, modulus::error> {
|
||||||
assert(name.data() != nullptr);
|
assert(name.data() != nullptr);
|
||||||
auto found = object.find(name);
|
auto found = object.find(name);
|
||||||
if (found == object.end()) {
|
if (found == object.end()) {
|
||||||
@@ -155,7 +156,7 @@ auto modulus::json_config::optional_uint64_field(boost::json::object const& obje
|
|||||||
return number;
|
return number;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::json_config::positive_uint64_field(boost::json::object const& object, std::string_view name, std::uint64_t fallback, std::uint64_t max_value, modulus::json_config::options config) -> std::expected<std::uint64_t, modulus::error> {
|
auto modulus::json_config::positive_uint64_field(boost::json::object const &object, std::string_view name, std::uint64_t fallback, std::uint64_t max_value, modulus::json_config::options config) -> std::expected<std::uint64_t, modulus::error> {
|
||||||
auto parsed = modulus::json_config::optional_uint64_field(object, name, fallback, max_value, config);
|
auto parsed = modulus::json_config::optional_uint64_field(object, name, fallback, max_value, config);
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
return parsed;
|
return parsed;
|
||||||
@@ -166,7 +167,7 @@ auto modulus::json_config::positive_uint64_field(boost::json::object const& obje
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::json_config::optional_int32_field(boost::json::object const& object, std::string_view name, std::int32_t fallback, std::int32_t min_value, std::int32_t max_value, modulus::json_config::options config) -> std::expected<std::int32_t, modulus::error> {
|
auto modulus::json_config::optional_int32_field(boost::json::object const &object, std::string_view name, std::int32_t fallback, std::int32_t min_value, std::int32_t max_value, modulus::json_config::options config) -> std::expected<std::int32_t, modulus::error> {
|
||||||
assert(name.data() != nullptr);
|
assert(name.data() != nullptr);
|
||||||
auto found = object.find(name);
|
auto found = object.find(name);
|
||||||
if (found == object.end()) {
|
if (found == object.end()) {
|
||||||
@@ -182,7 +183,7 @@ auto modulus::json_config::optional_int32_field(boost::json::object const& objec
|
|||||||
return static_cast<std::int32_t>(*number);
|
return static_cast<std::int32_t>(*number);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::json_config::positive_milliseconds_field(boost::json::object const& object, std::string_view name, std::chrono::milliseconds fallback, std::uint64_t max_value, modulus::json_config::options config) -> std::expected<std::chrono::milliseconds, modulus::error> {
|
auto modulus::json_config::positive_milliseconds_field(boost::json::object const &object, std::string_view name, std::chrono::milliseconds fallback, std::uint64_t max_value, modulus::json_config::options config) -> std::expected<std::chrono::milliseconds, modulus::error> {
|
||||||
auto parsed = modulus::json_config::positive_uint64_field(object, name, static_cast<std::uint64_t>(fallback.count()), max_value, config);
|
auto parsed = modulus::json_config::positive_uint64_field(object, name, static_cast<std::uint64_t>(fallback.count()), max_value, config);
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
return std::unexpected(parsed.error());
|
return std::unexpected(parsed.error());
|
||||||
|
|||||||
+22
-13
@@ -7,6 +7,7 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
#include <modulus/types.hpp>
|
#include <modulus/types.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
auto is_valid_log_level(modulus::log_level level) noexcept -> std::bool_t {
|
auto is_valid_log_level(modulus::log_level level) noexcept -> std::bool_t {
|
||||||
@@ -19,13 +20,13 @@ auto should_log(modulus::log_level record_level, modulus::log_level configured_l
|
|||||||
return configured_level != modulus::log_level::OFF && static_cast<std::int32_t>(record_level) >= static_cast<std::int32_t>(configured_level);
|
return configured_level != modulus::log_level::OFF && static_cast<std::int32_t>(record_level) >= static_cast<std::int32_t>(configured_level);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto format_log_record(std::ostream& stream, const modulus::log_record& record) -> void {
|
auto format_log_record(std::ostream &stream, const modulus::log_record &record) -> void {
|
||||||
assert(is_valid_log_level(record.level));
|
assert(is_valid_log_level(record.level));
|
||||||
assert(record.location.file_name() != nullptr);
|
assert(record.location.file_name() != nullptr);
|
||||||
stream << '[' << modulus::to_string(record.level) << "] " << record.message << " (" << record.location.file_name() << ':' << record.location.line() << ")\n";
|
stream << '[' << modulus::to_string(record.level) << "] " << record.message << " (" << record.location.file_name() << ':' << record.location.line() << ")\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
auto stderr_sink(const modulus::log_record& record) -> void {
|
auto stderr_sink(const modulus::log_record &record) -> void {
|
||||||
assert(is_valid_log_level(record.level));
|
assert(is_valid_log_level(record.level));
|
||||||
assert(record.location.file_name() != nullptr);
|
assert(record.location.file_name() != nullptr);
|
||||||
static auto mutex = std::mutex{};
|
static auto mutex = std::mutex{};
|
||||||
@@ -38,18 +39,26 @@ auto stderr_sink(const modulus::log_record& record) -> void {
|
|||||||
auto modulus::to_string(modulus::log_level level) noexcept -> std::string_view {
|
auto modulus::to_string(modulus::log_level level) noexcept -> std::string_view {
|
||||||
assert(is_valid_log_level(level));
|
assert(is_valid_log_level(level));
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case modulus::log_level::TRACE: return "trace";
|
case modulus::log_level::TRACE:
|
||||||
case modulus::log_level::DEBUG: return "debug";
|
return "trace";
|
||||||
case modulus::log_level::INFO: return "info";
|
case modulus::log_level::DEBUG:
|
||||||
case modulus::log_level::WARN: return "warn";
|
return "debug";
|
||||||
case modulus::log_level::ERROR: return "error";
|
case modulus::log_level::INFO:
|
||||||
case modulus::log_level::CRITICAL: return "critical";
|
return "info";
|
||||||
case modulus::log_level::OFF: return "off";
|
case modulus::log_level::WARN:
|
||||||
|
return "warn";
|
||||||
|
case modulus::log_level::ERROR:
|
||||||
|
return "error";
|
||||||
|
case modulus::log_level::CRITICAL:
|
||||||
|
return "critical";
|
||||||
|
case modulus::log_level::OFF:
|
||||||
|
return "off";
|
||||||
}
|
}
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
modulus::logger::logger() : sink_(stderr_sink) {}
|
modulus::logger::logger()
|
||||||
|
: sink_(stderr_sink) {}
|
||||||
|
|
||||||
auto modulus::logger::set_level(modulus::log_level level) -> void {
|
auto modulus::logger::set_level(modulus::log_level level) -> void {
|
||||||
assert(is_valid_log_level(level));
|
assert(is_valid_log_level(level));
|
||||||
@@ -69,7 +78,7 @@ auto modulus::logger::enabled(modulus::log_level level) const -> std::bool_t {
|
|||||||
return should_log(level, level_) && static_cast<std::bool_t>(sink_);
|
return should_log(level, level_) && static_cast<std::bool_t>(sink_);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::logger::set_sink(std::function<void(const modulus::log_record&)> sink) -> void {
|
auto modulus::logger::set_sink(std::function<void(const modulus::log_record &)> sink) -> void {
|
||||||
auto lock = std::lock_guard{mutex_};
|
auto lock = std::lock_guard{mutex_};
|
||||||
sink_ = std::move(sink);
|
sink_ = std::move(sink);
|
||||||
}
|
}
|
||||||
@@ -78,7 +87,7 @@ auto modulus::logger::write(modulus::log_record record) const -> void {
|
|||||||
assert(is_valid_log_level(record.level));
|
assert(is_valid_log_level(record.level));
|
||||||
assert(record.location.file_name() != nullptr);
|
assert(record.location.file_name() != nullptr);
|
||||||
assert(record.location.function_name() != nullptr);
|
assert(record.location.function_name() != nullptr);
|
||||||
auto sink = std::function<void(const modulus::log_record&)>{};
|
auto sink = std::function<void(const modulus::log_record &)>{};
|
||||||
{
|
{
|
||||||
auto lock = std::lock_guard{mutex_};
|
auto lock = std::lock_guard{mutex_};
|
||||||
if (!should_log(record.level, level_) || !sink_) {
|
if (!should_log(record.level, level_) || !sink_) {
|
||||||
@@ -102,7 +111,7 @@ auto modulus::logger::log(modulus::log_level level, std::string message, modulus
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
auto modulus::default_logger() -> modulus::logger& {
|
auto modulus::default_logger() -> modulus::logger & {
|
||||||
static auto instance = modulus::logger{};
|
static auto instance = modulus::logger{};
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
local root = assert(vim.env.MODULUS_FORMAT_REPO)
|
||||||
|
vim.opt.rtp:append(assert(vim.env.MODULUS_CONFORM_PATH))
|
||||||
|
local conform = require("conform")
|
||||||
|
conform.setup({ formatters_by_ft = { cpp = { "clang_format" }, _ = { "original_fallback" } } })
|
||||||
|
vim.lsp.config("clangd", { cmd = { "clangd" }, filetypes = { "c", "cpp" } })
|
||||||
|
vim.lsp.enable = function() end
|
||||||
|
dofile(assert(vim.env.MODULUS_FORMAT_HELPER)).setup(root)
|
||||||
|
local function buffer(name, ft, lines)
|
||||||
|
local buf = vim.api.nvim_create_buf(true, false)
|
||||||
|
vim.api.nvim_set_current_buf(buf)
|
||||||
|
vim.api.nvim_buf_set_name(buf, name)
|
||||||
|
vim.bo[buf].filetype = ft
|
||||||
|
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||||
|
return buf
|
||||||
|
end
|
||||||
|
local function content(buf)
|
||||||
|
return table.concat(vim.api.nvim_buf_get_lines(buf, 0, -1, false), "\n")
|
||||||
|
end
|
||||||
|
local function check()
|
||||||
|
local outside = buffer("/tmp/not-modulus.cpp", "cpp", { "int x;" })
|
||||||
|
assert(vim.deep_equal(conform.list_formatters_for_buffer(outside), { "clang_format" }))
|
||||||
|
local python_outside = buffer("/tmp/not_modulus.py", "python", { "pass" })
|
||||||
|
assert(vim.deep_equal(conform.list_formatters_for_buffer(python_outside), { "original_fallback" }))
|
||||||
|
local cpp = buffer(root .. "/tests/modulus-format-probe.cpp", "cpp", { "void example(){", "", "int a=1;", "", "a++;", "}" })
|
||||||
|
assert(vim.deep_equal(conform.list_formatters_for_buffer(cpp), { "modulus" }))
|
||||||
|
local failure
|
||||||
|
conform.format({ bufnr = cpp, timeout_ms = 10000 }, function(err) failure = err end)
|
||||||
|
assert(not failure, vim.inspect(failure))
|
||||||
|
assert(content(cpp) == "void example() {\n int a = 1;\n a++;\n}", content(cpp))
|
||||||
|
local py = buffer(root .. "/tests/modulus_format_probe.py", "python", { "def example( x ): return x+1" })
|
||||||
|
vim.g.autoformat = false
|
||||||
|
vim.api.nvim_exec_autocmds("BufWritePre", { group = "ModulusFormat", buffer = py })
|
||||||
|
assert(content(py) == "def example( x ): return x+1")
|
||||||
|
vim.g.autoformat = true
|
||||||
|
vim.b[py].autoformat = false
|
||||||
|
vim.api.nvim_exec_autocmds("BufWritePre", { group = "ModulusFormat", buffer = py })
|
||||||
|
assert(content(py) == "def example( x ): return x+1")
|
||||||
|
vim.b[py].autoformat = true
|
||||||
|
vim.api.nvim_exec_autocmds("BufWritePre", { group = "ModulusFormat", buffer = py })
|
||||||
|
assert(content(py) == "def example(x): return x + 1", content(py))
|
||||||
|
vim.api.nvim_buf_set_lines(py, 0, -1, false, { "patterns = (", ' ".git",', ' ".tmp",', ")" })
|
||||||
|
vim.api.nvim_exec_autocmds("BufWritePre", { group = "ModulusFormat", buffer = py })
|
||||||
|
assert(content(py) == 'patterns = (\n ".git",\n ".tmp",\n)', content(py))
|
||||||
|
vim.api.nvim_buf_set_lines(py, 0, -1, false, { "def broken(" })
|
||||||
|
conform.format({ bufnr = py, timeout_ms = 10000, quiet = true }, function(err) failure = err end)
|
||||||
|
assert(failure, "invalid Python unexpectedly succeeded")
|
||||||
|
assert(content(py) == "def broken(", "failed formatter replaced the buffer")
|
||||||
|
end
|
||||||
|
local ok, err = pcall(check)
|
||||||
|
if not ok then
|
||||||
|
print("MODULUS_FORMAT_FAILURE " .. tostring(err))
|
||||||
|
vim.cmd("cquit 1")
|
||||||
|
end
|
||||||
|
print("MODULUS_FORMAT_OK")
|
||||||
|
vim.cmd("qa!")
|
||||||
+10
-9
@@ -15,11 +15,12 @@
|
|||||||
|
|
||||||
#include <modulus.hpp>
|
#include <modulus.hpp>
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
struct stderr_capture {
|
struct stderr_capture {
|
||||||
std::ostringstream stream{};
|
std::ostringstream stream{};
|
||||||
std::streambuf* previous{std::cerr.rdbuf(stream.rdbuf())};
|
std::streambuf *previous{std::cerr.rdbuf(stream.rdbuf())};
|
||||||
~stderr_capture();
|
~stderr_capture();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -71,7 +72,7 @@ BOOST_AUTO_TEST_CASE(modulus_logger_filters_and_reports_caller_location) {
|
|||||||
auto records = std::vector<modulus::log_record>{};
|
auto records = std::vector<modulus::log_record>{};
|
||||||
auto logger = modulus::logger{};
|
auto logger = modulus::logger{};
|
||||||
logger.set_level(modulus::log_level::DEBUG);
|
logger.set_level(modulus::log_level::DEBUG);
|
||||||
logger.set_sink([&](const modulus::log_record& record) {
|
logger.set_sink([&](const modulus::log_record &record) {
|
||||||
records.push_back(record);
|
records.push_back(record);
|
||||||
});
|
});
|
||||||
logger.log(modulus::log_level::TRACE, "hidden");
|
logger.log(modulus::log_level::TRACE, "hidden");
|
||||||
@@ -85,7 +86,7 @@ BOOST_AUTO_TEST_CASE(modulus_logger_serializes_custom_sink_writes) {
|
|||||||
auto records = std::vector<modulus::log_record>{};
|
auto records = std::vector<modulus::log_record>{};
|
||||||
auto logger = modulus::logger{};
|
auto logger = modulus::logger{};
|
||||||
logger.set_level(modulus::log_level::INFO);
|
logger.set_level(modulus::log_level::INFO);
|
||||||
logger.set_sink([&](const modulus::log_record& record) {
|
logger.set_sink([&](const modulus::log_record &record) {
|
||||||
records.push_back(record);
|
records.push_back(record);
|
||||||
});
|
});
|
||||||
auto threads = std::vector<std::thread>{};
|
auto threads = std::vector<std::thread>{};
|
||||||
@@ -96,12 +97,12 @@ BOOST_AUTO_TEST_CASE(modulus_logger_serializes_custom_sink_writes) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (auto& thread : threads) {
|
for (auto &thread : threads) {
|
||||||
thread.join();
|
thread.join();
|
||||||
}
|
}
|
||||||
BOOST_TEST(records.size() == std::size_t{200});
|
BOOST_TEST(records.size() == std::size_t{200});
|
||||||
auto messages = std::set<std::string>{};
|
auto messages = std::set<std::string>{};
|
||||||
for (const auto& record : records) {
|
for (const auto &record : records) {
|
||||||
messages.insert(record.message);
|
messages.insert(record.message);
|
||||||
}
|
}
|
||||||
BOOST_TEST(messages.size() == std::size_t{200});
|
BOOST_TEST(messages.size() == std::size_t{200});
|
||||||
@@ -111,10 +112,10 @@ BOOST_AUTO_TEST_CASE(modulus_logger_tolerates_concurrent_configuration_changes)
|
|||||||
auto records = std::vector<modulus::log_record>{};
|
auto records = std::vector<modulus::log_record>{};
|
||||||
auto logger = modulus::logger{};
|
auto logger = modulus::logger{};
|
||||||
logger.set_level(modulus::log_level::INFO);
|
logger.set_level(modulus::log_level::INFO);
|
||||||
logger.set_sink([&](const modulus::log_record& record) {
|
logger.set_sink([&](const modulus::log_record &record) {
|
||||||
records.push_back(record);
|
records.push_back(record);
|
||||||
});
|
});
|
||||||
auto sink = [&](const modulus::log_record& record) -> void {
|
auto sink = [&](const modulus::log_record &record) -> void {
|
||||||
records.push_back(record);
|
records.push_back(record);
|
||||||
};
|
};
|
||||||
auto mutator = std::thread{[&]() -> void {
|
auto mutator = std::thread{[&]() -> void {
|
||||||
@@ -132,7 +133,7 @@ BOOST_AUTO_TEST_CASE(modulus_logger_tolerates_concurrent_configuration_changes)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (auto& thread : threads) {
|
for (auto &thread : threads) {
|
||||||
thread.join();
|
thread.join();
|
||||||
}
|
}
|
||||||
mutator.join();
|
mutator.join();
|
||||||
@@ -159,7 +160,7 @@ BOOST_AUTO_TEST_CASE(modulus_default_logger_serializes_stderr_lines) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (auto& thread : threads) {
|
for (auto &thread : threads) {
|
||||||
thread.join();
|
thread.join();
|
||||||
}
|
}
|
||||||
auto captured = std::istringstream{capture.stream.str()};
|
auto captured = std::istringstream{capture.stream.str()};
|
||||||
|
|||||||
+57
-68
@@ -9,80 +9,75 @@ import tempfile
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import modulus.clangd as clangd
|
import modulus.clangd
|
||||||
import modulus.cli as cli
|
import modulus.cli
|
||||||
import modulus.config as config
|
import modulus.config
|
||||||
import modulus.export as export
|
import modulus.export
|
||||||
|
|
||||||
class clangd_test(unittest.TestCase):
|
class clangd_test(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
temporary = tempfile.TemporaryDirectory()
|
temporary = tempfile.TemporaryDirectory()
|
||||||
self.addCleanup(temporary.cleanup)
|
self.addCleanup(temporary.cleanup)
|
||||||
self.root = pathlib.Path(temporary.name)
|
self.root = pathlib.Path(temporary.name)
|
||||||
self.context = cli.build_context(self.root, config.normalize({"package": {"name": "sample"}}))
|
self.context = modulus.cli.build_context(self.root, modulus.config.normalize({"package": {"name": "sample"}}))
|
||||||
|
self.context.docker_marker = self.root / "not-in-container"
|
||||||
self.context.dockerfile.parent.mkdir(parents=True)
|
self.context.dockerfile.parent.mkdir(parents=True)
|
||||||
self.context.dockerfile.write_text("FROM sample\n")
|
self.context.dockerfile.write_text("FROM sample\n")
|
||||||
self.image = "sha256:" + "1" * 64
|
self.image = "sha256:" + "1" * 64
|
||||||
|
|
||||||
def test_defaults_and_custom_settings(self):
|
def test_defaults_and_custom_settings(self):
|
||||||
self.assertEqual(clangd.settings(self.context)["executable"], "/usr/bin/clangd-19")
|
self.assertEqual(modulus.clangd.settings(self.context)["executable"], "/usr/bin/clangd-19")
|
||||||
self.assertIn("/usr/bin/g++-12", clangd.settings(self.context)["query_drivers"])
|
self.assertIn("/usr/bin/g++-12", modulus.clangd.settings(self.context)["query_drivers"])
|
||||||
custom = {"executable": "/opt/bin/clangd", "arguments": [], "query_drivers": ["/opt/bin/g++"], "header_roots": ["/opt/sdk"]}
|
custom = {"executable": "/opt/bin/clangd", "arguments": [], "query_drivers": ["/opt/bin/g++"], "header_roots": ["/opt/sdk"]}
|
||||||
result = config.normalize({"package": {"name": "sample"}, "tooling": {"lsp": {"clangd": custom}}})
|
result = modulus.config.normalize({"package": {"name": "sample"}, "tooling": {"lsp": {"clangd": custom}}})
|
||||||
self.assertEqual(result["tooling"]["lsp"]["clangd"], custom)
|
self.assertEqual(result["tooling"]["lsp"]["clangd"], custom)
|
||||||
|
|
||||||
def test_config_rejects_unsafe_or_unknown_settings(self):
|
def test_config_rejects_unsafe_or_unknown_settings(self):
|
||||||
for invalid in ([], {"unknown": True}, {"executable": "clangd"}, {"arguments": "--background-index"},
|
for invalid in ([], {"unknown": True}, {"executable": "clangd"}, {"arguments": "--background-index"},
|
||||||
{"query_drivers": ["/usr/bin/*"]}, {"header_roots": ["/"]},
|
{"query_drivers": ["/usr/bin/*"]}, {"header_roots": ["/"]},
|
||||||
{"header_roots": ["/usr/../etc"]}, {"query_drivers": ["/bin/g++,/bin/gcc"]},
|
{"header_roots": ["/usr/../etc"]}, {"query_drivers": ["/bin/g++,/bin/gcc"]},
|
||||||
{"arguments": ["--query-driver=*"]}, {"arguments": ["--path-mappings=/=/"]}):
|
{"arguments": ["--query-driver=*"]}, {"arguments": ["--path-mappings=/=/"]}):
|
||||||
with self.subTest(invalid=invalid), self.assertRaises(config.config_error):
|
with self.subTest(invalid=invalid), self.assertRaises(modulus.config.config_error):
|
||||||
config.normalize({"package": {"name": "sample"}, "tooling": {"lsp": {"clangd": invalid}}})
|
modulus.config.normalize({"package": {"name": "sample"}, "tooling": {"lsp": {"clangd": invalid}}})
|
||||||
|
|
||||||
def test_image_fingerprint_and_architecture(self):
|
def test_image_fingerprint_and_architecture(self):
|
||||||
image = {"Id": self.image, "Architecture": "amd64", "Config": {"Labels": {clangd.image_label: clangd.recipe_digest(self.context)}}}
|
image = {"Id": self.image, "Architecture": "amd64", "Config": {"Labels": {modulus.clangd.image_label: modulus.clangd.recipe_digest(self.context)}}}
|
||||||
with patch.object(subprocess, "run", return_value=subprocess.CompletedProcess([], 0, json.dumps([image]))):
|
with patch.object(subprocess, "run", return_value=subprocess.CompletedProcess([], 0, json.dumps([image]))):
|
||||||
self.assertEqual(clangd.current_image(self.context), self.image)
|
self.assertEqual(modulus.clangd.current_image(self.context), self.image)
|
||||||
self.assertIsNone(clangd.current_image(self.context, "arm64"))
|
self.assertIsNone(modulus.clangd.current_image(self.context, "arm64"))
|
||||||
self.context.dockerfile.write_text("FROM changed\n")
|
self.context.dockerfile.write_text("FROM changed\n")
|
||||||
self.assertIsNone(clangd.current_image(self.context))
|
self.assertIsNone(modulus.clangd.current_image(self.context))
|
||||||
|
|
||||||
def test_cache_is_shared_by_image_and_header_settings_not_repo(self):
|
def test_cache_is_shared_by_image_and_header_settings_not_repo(self):
|
||||||
cache = clangd.cache_directory(self.context, self.image)
|
cache = modulus.clangd.cache_directory(self.context, self.image)
|
||||||
other = cli.build_context(self.root / "other", self.context.config)
|
other = modulus.cli.build_context(self.root / "other", self.context.config)
|
||||||
self.assertEqual(cache, clangd.cache_directory(other, self.image))
|
self.assertEqual(cache, modulus.clangd.cache_directory(other, self.image))
|
||||||
self.context.lsp_settings["clangd"]["arguments"].append("--log=error")
|
self.context.lsp_settings["clangd"]["arguments"].append("--log=error")
|
||||||
self.assertEqual(cache, clangd.cache_directory(self.context, self.image))
|
self.assertEqual(cache, modulus.clangd.cache_directory(self.context, self.image))
|
||||||
self.context.lsp_settings["clangd"]["header_roots"].append("/opt/sdk")
|
self.context.lsp_settings["clangd"]["header_roots"].append("/opt/sdk")
|
||||||
self.assertNotEqual(cache, clangd.cache_directory(self.context, self.image))
|
self.assertNotEqual(cache, modulus.clangd.cache_directory(self.context, self.image))
|
||||||
self.assertNotEqual(cache, clangd.cache_directory(other, "sha256:" + "2" * 64))
|
self.assertNotEqual(cache, modulus.clangd.cache_directory(other, "sha256:" + "2" * 64))
|
||||||
|
|
||||||
def test_stdio_launch_has_no_tty_socket_credentials_or_host_home(self):
|
def test_stdio_launch_has_no_tty_socket_credentials_or_host_home(self):
|
||||||
command = clangd.launch_command(self.context, [], self.image, self.root / "cache", ["/usr/include"], "test-server")
|
command = modulus.clangd.launch_command(self.context, [], self.image, self.root / "cache", ["/usr/include"], "test-server")
|
||||||
self.assertIn("--interactive", command)
|
self.assertIn("--interactive", command)
|
||||||
self.assertIn("--read-only", command)
|
self.assertIn("--read-only", command)
|
||||||
self.assertIn(f"{self.root}:{self.root}:ro", command)
|
self.assertIn(f"{self.root}:{self.root}:ro", command)
|
||||||
self.assertIn(f"{self.context.build_root}:{self.context.build_root}:rw", command)
|
self.assertIn(f"{self.context.build_root}:{self.context.build_root}:rw", command)
|
||||||
self.assertIn("--network", command)
|
self.assertIn("--network", command)
|
||||||
self.assertNotIn("-t", command)
|
self.assertNotIn("-t", command)
|
||||||
for unwanted in ("--tty", "--volumes-from", "CONAN_TOKEN", "/var/run/docker.sock", str(pathlib.Path.home()) + ":"):
|
for unwanted in ("--tty", "--volumes-from", "CONAN_TOKEN", "/var/run/docker.sock"):
|
||||||
self.assertNotIn(unwanted, " ".join(command))
|
self.assertNotIn(unwanted, " ".join(command))
|
||||||
|
volumes = [command[index + 1] for index, argument in enumerate(command) if argument == "--volume"]
|
||||||
|
self.assertFalse(any(volume.startswith(str(pathlib.Path.home()) + ":") for volume in volumes))
|
||||||
self.assertIn("--query-driver=/usr/bin/gcc-12,/usr/bin/g++-12,/usr/bin/aarch64-linux-gnu-gcc,/usr/bin/aarch64-linux-gnu-g++", command)
|
self.assertIn("--query-driver=/usr/bin/gcc-12,/usr/bin/g++-12,/usr/bin/aarch64-linux-gnu-gcc,/usr/bin/aarch64-linux-gnu-g++", command)
|
||||||
|
|
||||||
def test_startup_failure_does_not_prepare_and_keeps_stdout_clean(self):
|
def test_startup_failure_does_not_prepare_and_keeps_stdout_clean(self):
|
||||||
output, errors = io.StringIO(), io.StringIO()
|
output, errors = io.StringIO(), io.StringIO()
|
||||||
with patch.object(clangd, "current_image", return_value=None), patch.object(clangd, "prepare") as prepare, \
|
with patch.object(modulus.clangd, "current_image", return_value=None), patch.object(modulus.clangd, "prepare") as prepare, \
|
||||||
patch.object(cli, "ensure_build_image") as build, contextlib.redirect_stdout(output), contextlib.redirect_stderr(errors):
|
patch.object(modulus.cli, "ensure_build_image") as build, contextlib.redirect_stdout(output), contextlib.redirect_stderr(errors):
|
||||||
self.assertEqual(clangd.launch(self.context, []), 1)
|
self.assertEqual(modulus.clangd.launch(self.context, []), 1)
|
||||||
prepare.assert_not_called()
|
prepare.assert_not_called()
|
||||||
build.assert_not_called()
|
build.assert_not_called()
|
||||||
self.assertEqual(output.getvalue(), "")
|
self.assertEqual(output.getvalue(), "")
|
||||||
self.assertIn("configure", errors.getvalue())
|
self.assertIn("configure", errors.getvalue())
|
||||||
|
|
||||||
def test_path_mapping_uses_client_to_server_direction_and_specificity(self):
|
def test_path_mapping_uses_client_to_server_direction_and_specificity(self):
|
||||||
self.assertEqual(clangd.path_mappings(pathlib.Path("/cache"), ["/usr/include", "/opt/sdk"]),
|
self.assertEqual(modulus.clangd.path_mappings(pathlib.Path("/cache"), ["/usr/include", "/opt/sdk"]),
|
||||||
"--path-mappings=/cache/rootfs/usr/include=/usr/include,/cache/rootfs/opt/sdk=/opt/sdk")
|
"--path-mappings=/cache/rootfs/usr/include=/usr/include,/cache/rootfs/opt/sdk=/opt/sdk")
|
||||||
|
|
||||||
def archive(self, name, kind=tarfile.REGTYPE):
|
def archive(self, name, kind=tarfile.REGTYPE):
|
||||||
stream = io.BytesIO()
|
stream = io.BytesIO()
|
||||||
with tarfile.open(fileobj=stream, mode="w") as archive:
|
with tarfile.open(fileobj=stream, mode="w") as archive:
|
||||||
@@ -92,69 +87,63 @@ class clangd_test(unittest.TestCase):
|
|||||||
archive.addfile(member)
|
archive.addfile(member)
|
||||||
stream.seek(0)
|
stream.seek(0)
|
||||||
return stream
|
return stream
|
||||||
|
|
||||||
def test_header_extraction_rejects_escape_and_links(self):
|
def test_header_extraction_rejects_escape_and_links(self):
|
||||||
for name, kind in (("../bad", tarfile.REGTYPE), ("/usr/include/bad", tarfile.REGTYPE),
|
for name, kind in (("../bad", tarfile.REGTYPE), ("/usr/include/bad", tarfile.REGTYPE),
|
||||||
("usr/include/../bad", tarfile.REGTYPE), ("etc/passwd", tarfile.REGTYPE),
|
("usr/include/../bad", tarfile.REGTYPE), ("etc/passwd", tarfile.REGTYPE),
|
||||||
("usr/include/link", tarfile.SYMTYPE)):
|
("usr/include/link", tarfile.SYMTYPE)):
|
||||||
with self.subTest(name=name), self.assertRaises(ValueError):
|
with self.subTest(name=name), self.assertRaises(ValueError):
|
||||||
clangd.extract_headers(self.archive(name, kind), self.root, ["/usr/include"])
|
modulus.clangd.extract_headers(self.archive(name, kind), self.root, ["/usr/include"])
|
||||||
clangd.extract_headers(self.archive("usr/include/header.h"), self.root, ["/usr/include"])
|
modulus.clangd.extract_headers(self.archive("usr/include/header.h"), self.root, ["/usr/include"])
|
||||||
self.assertEqual((self.root / "usr/include/header.h").stat().st_mode & 0o777, 0o444)
|
self.assertEqual((self.root / "usr/include/header.h").stat().st_mode & 0o777, 0o444)
|
||||||
|
|
||||||
def test_completed_cache_never_invokes_container_again(self):
|
def test_completed_cache_never_invokes_container_again(self):
|
||||||
cache = self.root / "cache"
|
cache = self.root / "cache"
|
||||||
(cache / "rootfs/usr/include").mkdir(parents=True)
|
(cache / "rootfs/usr/include").mkdir(parents=True)
|
||||||
(cache / "manifest.json").write_text(json.dumps({"version": clangd.cache_version, "roots": ["/usr/include"]}))
|
(cache / "manifest.json").write_text(json.dumps({"version": modulus.clangd.cache_version, "roots": ["/usr/include"]}))
|
||||||
with patch.object(clangd, "cache_directory", return_value=cache), patch.object(clangd, "current_image", return_value=self.image), \
|
with patch.object(modulus.clangd, "cache_directory", return_value=cache), patch.object(modulus.clangd, "current_image", return_value=self.image), \
|
||||||
patch.object(subprocess, "run") as run:
|
patch.object(subprocess, "run") as run:
|
||||||
self.assertEqual(clangd.prepare(self.context), 0)
|
self.assertEqual(modulus.clangd.prepare(self.context), 0)
|
||||||
run.assert_not_called()
|
run.assert_not_called()
|
||||||
|
|
||||||
def test_failed_extraction_never_publishes_partial_cache(self):
|
def test_failed_extraction_never_publishes_partial_cache(self):
|
||||||
cache = self.root / "image/cache"
|
cache = self.root / "image/cache"
|
||||||
process = MagicMock()
|
process = MagicMock()
|
||||||
process.__enter__.return_value = process
|
process.__enter__.return_value = process
|
||||||
process.stdout = self.archive("../escape")
|
process.stdout = self.archive("../escape")
|
||||||
with patch.object(clangd, "cache_directory", return_value=cache), patch.object(clangd, "current_image", return_value=self.image), \
|
with patch.object(modulus.clangd, "cache_directory", return_value=cache), patch.object(modulus.clangd, "current_image", return_value=self.image), \
|
||||||
patch.object(subprocess, "run", return_value=subprocess.CompletedProcess([], 0, '["/usr/include"]')), \
|
patch.object(subprocess, "run", return_value=subprocess.CompletedProcess([], 0, '["/usr/include"]')), \
|
||||||
patch.object(subprocess, "Popen", return_value=process), contextlib.redirect_stderr(io.StringIO()):
|
patch.object(subprocess, "Popen", return_value=process), contextlib.redirect_stderr(io.StringIO()):
|
||||||
self.assertEqual(clangd.prepare(self.context), 1)
|
self.assertEqual(modulus.clangd.prepare(self.context), 1)
|
||||||
self.assertFalse(cache.exists())
|
self.assertFalse(cache.exists())
|
||||||
self.assertEqual(list(cache.parent.glob(".headers-*")), [])
|
self.assertEqual(list(cache.parent.glob(".headers-*")), [])
|
||||||
|
|
||||||
def test_cleanup_always_waits_for_child_even_if_docker_removal_times_out(self):
|
def test_cleanup_always_waits_for_child_even_if_docker_removal_times_out(self):
|
||||||
process = MagicMock()
|
process = MagicMock()
|
||||||
process.wait.side_effect = [0, subprocess.TimeoutExpired("docker", 5), 0]
|
process.wait.side_effect = [0, subprocess.TimeoutExpired("docker", 5), 0]
|
||||||
with patch.object(subprocess, "Popen", return_value=process), \
|
with patch.object(subprocess, "Popen", return_value=process), \
|
||||||
patch.object(subprocess, "run", side_effect=subprocess.TimeoutExpired("docker rm", 15)), \
|
patch.object(subprocess, "run", side_effect=subprocess.TimeoutExpired("docker rm", 15)), \
|
||||||
contextlib.redirect_stderr(io.StringIO()):
|
contextlib.redirect_stderr(io.StringIO()):
|
||||||
self.assertEqual(clangd.run_server(["docker"], "test-server"), 0)
|
self.assertEqual(modulus.clangd.run_server(["docker"], "test-server"), 0)
|
||||||
process.kill.assert_called_once()
|
process.kill.assert_called_once()
|
||||||
self.assertEqual(process.wait.call_count, 3)
|
self.assertEqual(process.wait.call_count, 3)
|
||||||
|
|
||||||
def test_nvim_generation_is_stable_opt_out_and_respects_ownership(self):
|
def test_nvim_generation_is_stable_opt_out_and_respects_ownership(self):
|
||||||
export.sync_nvim_file(self.root, {})
|
modulus.export.sync_nvim_file(self.root, {})
|
||||||
path = self.root / ".nvim.lua"
|
path = self.root / ".nvim.lua"
|
||||||
before = path.stat().st_mtime_ns
|
before = path.stat().st_mtime_ns
|
||||||
export.sync_nvim_file(self.root, {})
|
modulus.export.sync_nvim_file(self.root, {})
|
||||||
self.assertEqual(before, path.stat().st_mtime_ns)
|
self.assertEqual(before, path.stat().st_mtime_ns)
|
||||||
export.sync_nvim_file(self.root, {"enabled": False})
|
modulus.export.sync_nvim_file(self.root, {"enabled": False})
|
||||||
self.assertFalse(path.exists())
|
self.assertFalse(path.exists())
|
||||||
path.write_text("-- user settings\n")
|
path.write_text("-- user settings\n")
|
||||||
with contextlib.redirect_stderr(io.StringIO()):
|
with contextlib.redirect_stderr(io.StringIO()):
|
||||||
export.sync_nvim_file(self.root, {})
|
modulus.export.sync_nvim_file(self.root, {})
|
||||||
export.sync_nvim_file(self.root, {"enabled": False})
|
modulus.export.sync_nvim_file(self.root, {"enabled": False})
|
||||||
self.assertEqual(path.read_text(), "-- user settings\n")
|
self.assertEqual(path.read_text(), "-- user settings\n")
|
||||||
|
|
||||||
def test_lsp_prepares_after_successful_database_refresh_only(self):
|
def test_lsp_prepares_after_successful_database_refresh_only(self):
|
||||||
with patch.object(cli, "in_docker", return_value=False), patch.object(cli, "docker_run", return_value=0) as run, \
|
with patch.object(modulus.cli, "in_docker", return_value=False), patch.object(modulus.cli, "docker_run", return_value=0) as run, \
|
||||||
patch.object(clangd, "prepare", return_value=0) as prepare:
|
patch.object(modulus.clangd, "prepare", return_value=0) as prepare:
|
||||||
self.assertEqual(cli.lsp(self.context, []), 0)
|
self.assertEqual(modulus.cli.lsp(self.context, []), 0)
|
||||||
prepare.assert_called_once_with(self.context)
|
prepare.assert_called_once_with(self.context)
|
||||||
prepare.reset_mock()
|
prepare.reset_mock()
|
||||||
run.return_value = 1
|
run.return_value = 1
|
||||||
self.assertEqual(cli.lsp(self.context, []), 1)
|
self.assertEqual(modulus.cli.lsp(self.context, []), 1)
|
||||||
prepare.assert_not_called()
|
prepare.assert_not_called()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ class lsp_session:
|
|||||||
self.reader.start()
|
self.reader.start()
|
||||||
self.next_id = 0
|
self.next_id = 0
|
||||||
self.pending = []
|
self.pending = []
|
||||||
|
|
||||||
def read_messages(self):
|
def read_messages(self):
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
@@ -38,12 +37,10 @@ class lsp_session:
|
|||||||
self.messages.put(json.loads(self.process.stdout.read(int(headers["content-length"]))))
|
self.messages.put(json.loads(self.process.stdout.read(int(headers["content-length"]))))
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
self.messages.put(error)
|
self.messages.put(error)
|
||||||
|
|
||||||
def send(self, message):
|
def send(self, message):
|
||||||
body = json.dumps({"jsonrpc": "2.0", **message}).encode()
|
body = json.dumps({"jsonrpc": "2.0", **message}).encode()
|
||||||
self.process.stdin.write(f"Content-Length: {len(body)}\r\n\r\n".encode() + body)
|
self.process.stdin.write(f"Content-Length: {len(body)}\r\n\r\n".encode() + body)
|
||||||
self.process.stdin.flush()
|
self.process.stdin.flush()
|
||||||
|
|
||||||
def wait(self, predicate, timeout=90):
|
def wait(self, predicate, timeout=90):
|
||||||
deadline = time.monotonic() + timeout
|
deadline = time.monotonic() + timeout
|
||||||
while True:
|
while True:
|
||||||
@@ -58,7 +55,6 @@ class lsp_session:
|
|||||||
self.log.seek(0)
|
self.log.seek(0)
|
||||||
raise AssertionError(f"{message}\n{self.log.read().decode(errors='replace')}")
|
raise AssertionError(f"{message}\n{self.log.read().decode(errors='replace')}")
|
||||||
self.pending.append(message)
|
self.pending.append(message)
|
||||||
|
|
||||||
def request(self, method, params):
|
def request(self, method, params):
|
||||||
self.next_id += 1
|
self.next_id += 1
|
||||||
self.send({"id": self.next_id, "method": method, "params": params})
|
self.send({"id": self.next_id, "method": method, "params": params})
|
||||||
@@ -66,20 +62,17 @@ class lsp_session:
|
|||||||
if "error" in response:
|
if "error" in response:
|
||||||
raise AssertionError(response["error"])
|
raise AssertionError(response["error"])
|
||||||
return response.get("result")
|
return response.get("result")
|
||||||
|
|
||||||
def initialize(self, root):
|
def initialize(self, root):
|
||||||
result = self.request("initialize", {"processId": os.getpid(), "rootUri": root.as_uri(),
|
result = self.request("initialize", {"processId": os.getpid(), "rootUri": root.as_uri(),
|
||||||
"capabilities": {"textDocument": {"publishDiagnostics": {"versionSupport": True}}}})
|
"capabilities": {"textDocument": {"publishDiagnostics": {"versionSupport": True}}}})
|
||||||
self.send({"method": "initialized", "params": {}})
|
self.send({"method": "initialized", "params": {}})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def open(self, path, text):
|
def open(self, path, text):
|
||||||
uri = path.as_uri()
|
uri = path.as_uri()
|
||||||
self.send({"method": "textDocument/didOpen", "params": {"textDocument": {
|
self.send({"method": "textDocument/didOpen", "params": {"textDocument": {
|
||||||
"uri": uri, "languageId": "cpp", "version": 1, "text": text}}})
|
"uri": uri, "languageId": "cpp", "version": 1, "text": text}}})
|
||||||
return self.wait(lambda message: message.get("method") == "textDocument/publishDiagnostics"
|
return self.wait(lambda message: message.get("method") == "textDocument/publishDiagnostics"
|
||||||
and message["params"]["uri"] == uri)["params"]["diagnostics"]
|
and message["params"]["uri"] == uri)["params"]["diagnostics"]
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
if self.process.poll() is None:
|
if self.process.poll() is None:
|
||||||
self.process.terminate()
|
self.process.terminate()
|
||||||
@@ -93,26 +86,24 @@ class lsp_session:
|
|||||||
class container_clangd_test(unittest.TestCase):
|
class container_clangd_test(unittest.TestCase):
|
||||||
def roots(self):
|
def roots(self):
|
||||||
return [pathlib.Path(root).resolve() for root in os.environ["MODULUS_LSP_TEST_ROOTS"].split(os.pathsep)]
|
return [pathlib.Path(root).resolve() for root in os.environ["MODULUS_LSP_TEST_ROOTS"].split(os.pathsep)]
|
||||||
|
|
||||||
@unittest.skipUnless(shutil.which("nvim"), "Neovim is not installed")
|
@unittest.skipUnless(shutil.which("nvim"), "Neovim is not installed")
|
||||||
def test_neovim_attachment_and_readonly_navigation(self):
|
def test_neovim_attachment_and_readonly_navigation(self):
|
||||||
script = pathlib.Path(__file__).with_name("nvim_clangd.lua")
|
script = pathlib.Path(__file__).with_name("nvim_clangd.lua")
|
||||||
for root in self.roots():
|
for root in self.roots():
|
||||||
database = json.loads((root / "build/build_x86_debug/compile_commands.json").read_text())
|
database = json.loads((root / "build/build_x86_debug/compile_commands.json").read_text())
|
||||||
source = next(pathlib.Path(entry["file"]) for entry in database
|
source = next(pathlib.Path(entry["file"]) for entry in database
|
||||||
if pathlib.Path(entry["file"]).is_relative_to(root / "src"))
|
if pathlib.Path(entry["file"]).is_relative_to(root / "src"))
|
||||||
sources = [(source, "cstdint")]
|
sources = [(source, "cstdint")]
|
||||||
if (root / "src/vamp_pico/main.cpp").exists():
|
if (root / "src/vamp_pico/main.cpp").exists():
|
||||||
sources.append((root / "src/vamp_pico/main.cpp", "pico/stdlib.h"))
|
sources.append((root / "src/vamp_pico/main.cpp", "pico/stdlib.h"))
|
||||||
for source, header in sources:
|
for source, header in sources:
|
||||||
with self.subTest(repo=root.name, source=str(source)):
|
with self.subTest(repo=root.name, source=str(source)):
|
||||||
env = {**os.environ, "MODULUS_LSP_REPO": str(root), "MODULUS_LSP_SOURCE": str(source.relative_to(root)),
|
env = {**os.environ, "MODULUS_LSP_REPO": str(root), "MODULUS_LSP_SOURCE": str(source.relative_to(root)),
|
||||||
"MODULUS_LSP_HEADER": header}
|
"MODULUS_LSP_HEADER": header}
|
||||||
result = subprocess.run(["nvim", "--headless", "-i", "NONE", "-u", str(script)], cwd=root,
|
result = subprocess.run(["nvim", "--headless", "-i", "NONE", "-u", str(script)], cwd=root,
|
||||||
env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=180)
|
env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=180)
|
||||||
self.assertEqual(result.returncode, 0, result.stdout)
|
self.assertEqual(result.returncode, 0, result.stdout)
|
||||||
self.assertIn("MODULUS_NVIM_OK", result.stdout)
|
self.assertIn("MODULUS_NVIM_OK", result.stdout)
|
||||||
|
|
||||||
def test_native_and_pico_diagnostics_and_local_header_definitions(self):
|
def test_native_and_pico_diagnostics_and_local_header_definitions(self):
|
||||||
for root in self.roots():
|
for root in self.roots():
|
||||||
with self.subTest(repo=root.name):
|
with self.subTest(repo=root.name):
|
||||||
@@ -122,7 +113,7 @@ class container_clangd_test(unittest.TestCase):
|
|||||||
self.assertIn("19.", initialized["serverInfo"]["version"])
|
self.assertIn("19.", initialized["serverInfo"]["version"])
|
||||||
database = json.loads((root / "build/build_x86_debug/compile_commands.json").read_text())
|
database = json.loads((root / "build/build_x86_debug/compile_commands.json").read_text())
|
||||||
source = next(pathlib.Path(entry["file"]) for entry in database
|
source = next(pathlib.Path(entry["file"]) for entry in database
|
||||||
if pathlib.Path(entry["file"]).is_relative_to(root / "src"))
|
if pathlib.Path(entry["file"]).is_relative_to(root / "src"))
|
||||||
sources = [(source, "cstdint")]
|
sources = [(source, "cstdint")]
|
||||||
if (root / "src/vamp_pico").is_dir():
|
if (root / "src/vamp_pico").is_dir():
|
||||||
sources.extend((path, "pico/stdlib.h") for path in sorted((root / "src/vamp_pico").glob("*.cpp")))
|
sources.extend((path, "pico/stdlib.h") for path in sorted((root / "src/vamp_pico").glob("*.cpp")))
|
||||||
@@ -145,7 +136,6 @@ class container_clangd_test(unittest.TestCase):
|
|||||||
self.assertEqual(session.process.wait(timeout=25), 0)
|
self.assertEqual(session.process.wait(timeout=25), 0)
|
||||||
finally:
|
finally:
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
def test_eof_and_termination_remove_container(self):
|
def test_eof_and_termination_remove_container(self):
|
||||||
def containers():
|
def containers():
|
||||||
return set(subprocess.check_output(["docker", "ps", "-q", "--filter", "name=modulus-clangd-"], text=True).split())
|
return set(subprocess.check_output(["docker", "ps", "-q", "--filter", "name=modulus-clangd-"], text=True).split())
|
||||||
|
|||||||
+2
-51
@@ -59,7 +59,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
return modulus.cli.build_context(repo_root or pathlib.Path(os.getcwd()), config)
|
return modulus.cli.build_context(repo_root or pathlib.Path(os.getcwd()), config)
|
||||||
|
|
||||||
def test_cli_import_does_not_import_conan_recipe_module(self):
|
def test_cli_import_does_not_import_conan_recipe_module(self):
|
||||||
repo = pathlib.Path(__file__).resolve().parents[1]
|
repo = pathlib.Path(__file__).resolve().parents[1]
|
||||||
command = [
|
command = [
|
||||||
@@ -69,7 +68,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
]
|
]
|
||||||
completed = subprocess.run(command, cwd=repo, check=False)
|
completed = subprocess.run(command, cwd=repo, check=False)
|
||||||
self.assertEqual(completed.returncode, 0)
|
self.assertEqual(completed.returncode, 0)
|
||||||
|
|
||||||
def test_project_root_build_is_rejected(self):
|
def test_project_root_build_is_rejected(self):
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
root = pathlib.Path(directory)
|
root = pathlib.Path(directory)
|
||||||
@@ -77,7 +75,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
context.config["tooling"]["targets"]["native"]["build_dir"] = "."
|
context.config["tooling"]["targets"]["native"]["build_dir"] = "."
|
||||||
with self.assertRaisesRegex(modulus.config.config_error, "project and source roots"):
|
with self.assertRaisesRegex(modulus.config.config_error, "project and source roots"):
|
||||||
modulus.cli.build_context(root, context.config)
|
modulus.cli.build_context(root, context.config)
|
||||||
|
|
||||||
def test_configure_and_lsp_refresh_project_editor_settings(self):
|
def test_configure_and_lsp_refresh_project_editor_settings(self):
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
root = pathlib.Path(directory)
|
root = pathlib.Path(directory)
|
||||||
@@ -97,7 +94,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
before = {name: (root / name).stat().st_mtime_ns for name in modulus.export.editor_files.values()}
|
before = {name: (root / name).stat().st_mtime_ns for name in modulus.export.editor_files.values()}
|
||||||
self.assertEqual(modulus.cli.main(command), 0)
|
self.assertEqual(modulus.cli.main(command), 0)
|
||||||
self.assertEqual(before, {name: (root / name).stat().st_mtime_ns for name in before})
|
self.assertEqual(before, {name: (root / name).stat().st_mtime_ns for name in before})
|
||||||
|
|
||||||
def test_lsp_rejects_source_root_before_cache_cleanup(self):
|
def test_lsp_rejects_source_root_before_cache_cleanup(self):
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
root = pathlib.Path(directory)
|
root = pathlib.Path(directory)
|
||||||
@@ -112,7 +108,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
modulus.cli.configure_lsp_database(context, {"source_dir": "subproject", "build_dir": build_dir})
|
modulus.cli.configure_lsp_database(context, {"source_dir": "subproject", "build_dir": build_dir})
|
||||||
clear.assert_not_called()
|
clear.assert_not_called()
|
||||||
self.assertEqual(cache.read_text(encoding="utf-8"), "keep")
|
self.assertEqual(cache.read_text(encoding="utf-8"), "keep")
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "upload_all_packages", return_value=37)
|
@unittest.mock.patch.object(modulus.cli, "upload_all_packages", return_value=37)
|
||||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||||
def test_failed_upload_does_not_force_by_default(self, configure, upload):
|
def test_failed_upload_does_not_force_by_default(self, configure, upload):
|
||||||
@@ -122,7 +117,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
self.assertEqual(status, 37)
|
self.assertEqual(status, 37)
|
||||||
configure.assert_called_once_with(context, authenticate=True)
|
configure.assert_called_once_with(context, authenticate=True)
|
||||||
upload.assert_called_once_with(context, "origin", force=False)
|
upload.assert_called_once_with(context, "origin", force=False)
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "upload_all_packages")
|
@unittest.mock.patch.object(modulus.cli, "upload_all_packages")
|
||||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote")
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote")
|
||||||
def test_replace_requires_guard_env(self, configure, upload):
|
def test_replace_requires_guard_env(self, configure, upload):
|
||||||
@@ -131,7 +125,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
self.assertEqual(status, 1)
|
self.assertEqual(status, 1)
|
||||||
configure.assert_not_called()
|
configure.assert_not_called()
|
||||||
upload.assert_not_called()
|
upload.assert_not_called()
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "upload_all_packages", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "upload_all_packages", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||||
def test_replace_forces_upload_when_guarded(self, configure, upload):
|
def test_replace_forces_upload_when_guarded(self, configure, upload):
|
||||||
@@ -147,7 +140,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
self.assertEqual(status, 0)
|
self.assertEqual(status, 0)
|
||||||
configure.assert_called_once_with(context, authenticate=True)
|
configure.assert_called_once_with(context, authenticate=True)
|
||||||
upload.assert_called_once_with(context, "origin", force=True)
|
upload.assert_called_once_with(context, "origin", force=True)
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
def test_upload_defaults_to_latest_reference(self, run):
|
def test_upload_defaults_to_latest_reference(self, run):
|
||||||
with unittest.mock.patch.dict(os.environ, {}, clear=True):
|
with unittest.mock.patch.dict(os.environ, {}, clear=True):
|
||||||
@@ -155,13 +147,11 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
self.assertEqual(status, 0)
|
self.assertEqual(status, 0)
|
||||||
self.assertIn("sample/latest:*", run.call_args.args[1])
|
self.assertIn("sample/latest:*", run.call_args.args[1])
|
||||||
self.assertNotIn("--force", run.call_args.args[1])
|
self.assertNotIn("--force", run.call_args.args[1])
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
def test_replace_upload_uses_conan_force(self, run):
|
def test_replace_upload_uses_conan_force(self, run):
|
||||||
status = modulus.cli.upload_all_packages(self.context(), "origin", force=True)
|
status = modulus.cli.upload_all_packages(self.context(), "origin", force=True)
|
||||||
self.assertEqual(status, 0)
|
self.assertEqual(status, 0)
|
||||||
self.assertIn("--force", run.call_args.args[1])
|
self.assertIn("--force", run.call_args.args[1])
|
||||||
|
|
||||||
def test_checked_in_ci_overwrites_latest_on_master(self):
|
def test_checked_in_ci_overwrites_latest_on_master(self):
|
||||||
workflow = (pathlib.Path(__file__).resolve().parents[1] / ".gitea" / "workflows" / "ci.yml").read_text(encoding="utf-8")
|
workflow = (pathlib.Path(__file__).resolve().parents[1] / ".gitea" / "workflows" / "ci.yml").read_text(encoding="utf-8")
|
||||||
self.assertIn("gitea.ref == 'refs/heads/master'", workflow)
|
self.assertIn("gitea.ref == 'refs/heads/master'", workflow)
|
||||||
@@ -178,7 +168,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
self.assertNotIn("--volume \"${PWD}:${PWD}\"", workflow)
|
self.assertNotIn("--volume \"${PWD}:${PWD}\"", workflow)
|
||||||
self.assertNotIn("CONAN_PUBLIC_REMOTE_URL", workflow)
|
self.assertNotIn("CONAN_PUBLIC_REMOTE_URL", workflow)
|
||||||
self.assertNotIn("docs.py", workflow)
|
self.assertNotIn("docs.py", workflow)
|
||||||
|
|
||||||
def test_removed_public_commands_are_rejected(self):
|
def test_removed_public_commands_are_rejected(self):
|
||||||
with tempfile.TemporaryDirectory() as repo_dir:
|
with tempfile.TemporaryDirectory() as repo_dir:
|
||||||
repo = pathlib.Path(repo_dir)
|
repo = pathlib.Path(repo_dir)
|
||||||
@@ -194,7 +183,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
modulus.cli.main(command)
|
modulus.cli.main(command)
|
||||||
finally:
|
finally:
|
||||||
os.chdir(previous_cwd)
|
os.chdir(previous_cwd)
|
||||||
|
|
||||||
def test_docker_passthrough_includes_replace_env(self):
|
def test_docker_passthrough_includes_replace_env(self):
|
||||||
passthrough = modulus.cli.docker_passthrough_env(self.context())
|
passthrough = modulus.cli.docker_passthrough_env(self.context())
|
||||||
self.assertIn("MODULUS_ALLOW_PACKAGE_REPLACE", passthrough)
|
self.assertIn("MODULUS_ALLOW_PACKAGE_REPLACE", passthrough)
|
||||||
@@ -205,7 +193,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
self.assertNotIn("MODULUS_EXPORT_RUNTIME", passthrough)
|
self.assertNotIn("MODULUS_EXPORT_RUNTIME", passthrough)
|
||||||
self.assertNotIn("GITHUB_TOKEN", passthrough)
|
self.assertNotIn("GITHUB_TOKEN", passthrough)
|
||||||
self.assertNotIn("GITEA_TOKEN", passthrough)
|
self.assertNotIn("GITEA_TOKEN", passthrough)
|
||||||
|
|
||||||
def test_run_ports_default_to_empty_list(self):
|
def test_run_ports_default_to_empty_list(self):
|
||||||
config = modulus.config.normalize({
|
config = modulus.config.normalize({
|
||||||
"package": {
|
"package": {
|
||||||
@@ -213,7 +200,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
self.assertEqual(config["tooling"]["run"]["ports"], [])
|
self.assertEqual(config["tooling"]["run"]["ports"], [])
|
||||||
|
|
||||||
def test_run_accepts_docker_publish_mapping(self):
|
def test_run_accepts_docker_publish_mapping(self):
|
||||||
config = modulus.config.normalize({
|
config = modulus.config.normalize({
|
||||||
"package": {
|
"package": {
|
||||||
@@ -226,7 +212,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
self.assertEqual(config["tooling"]["run"]["ports"], ["127.0.0.1:8000:8000/tcp"])
|
self.assertEqual(config["tooling"]["run"]["ports"], ["127.0.0.1:8000:8000/tcp"])
|
||||||
|
|
||||||
def test_run_rejects_invalid_docker_publish_mapping(self):
|
def test_run_rejects_invalid_docker_publish_mapping(self):
|
||||||
with self.assertRaisesRegex(modulus.config.config_error, "tooling.run.ports\\[0\\]"):
|
with self.assertRaisesRegex(modulus.config.config_error, "tooling.run.ports\\[0\\]"):
|
||||||
modulus.config.normalize({
|
modulus.config.normalize({
|
||||||
@@ -239,13 +224,11 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
||||||
def test_run_is_disabled_by_default(self, docker):
|
def test_run_is_disabled_by_default(self, docker):
|
||||||
status = modulus.cli.product_run(self.context(), [])
|
status = modulus.cli.product_run(self.context(), [])
|
||||||
self.assertEqual(status, 1)
|
self.assertEqual(status, 1)
|
||||||
docker.assert_not_called()
|
docker.assert_not_called()
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
||||||
def test_run_executes_configured_product_executable(self, docker, run):
|
def test_run_executes_configured_product_executable(self, docker, run):
|
||||||
@@ -284,7 +267,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(run.call_args.kwargs["env_overrides"], {"SAMPLE_WEB_ROOT": "build/web"})
|
self.assertEqual(run.call_args.kwargs["env_overrides"], {"SAMPLE_WEB_ROOT": "build/web"})
|
||||||
docker.assert_called_once()
|
docker.assert_called_once()
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "docker_run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "docker_run", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=False)
|
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=False)
|
||||||
def test_run_publishes_configured_ports_when_wrapped_by_docker(self, docker, docker_run):
|
def test_run_publishes_configured_ports_when_wrapped_by_docker(self, docker, docker_run):
|
||||||
@@ -308,7 +290,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
docker.assert_called_once()
|
docker.assert_called_once()
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
def test_build_image_honors_docker_platform_env(self, run):
|
def test_build_image_honors_docker_platform_env(self, run):
|
||||||
context = self.context()
|
context = self.context()
|
||||||
@@ -334,10 +315,11 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
"sample-build:dev",
|
"sample-build:dev",
|
||||||
"--label",
|
"--label",
|
||||||
f"{modulus.cli.modulus.clangd.image_label}={modulus.cli.modulus.clangd.recipe_digest(context)}",
|
f"{modulus.cli.modulus.clangd.image_label}={modulus.cli.modulus.clangd.recipe_digest(context)}",
|
||||||
|
"--build-arg",
|
||||||
|
"MODULUS_TOOLS=tools",
|
||||||
".",
|
".",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "ensure_build_image", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "ensure_build_image", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "in_container", return_value=False)
|
@unittest.mock.patch.object(modulus.cli, "in_container", return_value=False)
|
||||||
@@ -356,11 +338,9 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
self.assertIn("linux/arm64", run.call_args.args[1])
|
self.assertIn("linux/arm64", run.call_args.args[1])
|
||||||
ensure_image.assert_called_once_with(context)
|
ensure_image.assert_called_once_with(context)
|
||||||
in_container.assert_called_once()
|
in_container.assert_called_once()
|
||||||
|
|
||||||
def test_conan_remote_url_uses_config_by_default(self):
|
def test_conan_remote_url_uses_config_by_default(self):
|
||||||
with unittest.mock.patch.dict(os.environ, {}, clear=True):
|
with unittest.mock.patch.dict(os.environ, {}, clear=True):
|
||||||
self.assertEqual(modulus.cli.conan_remote_url(self.context()), "https://example.test/conan")
|
self.assertEqual(modulus.cli.conan_remote_url(self.context()), "https://example.test/conan")
|
||||||
|
|
||||||
def test_conan_remote_url_uses_config_in_ci(self):
|
def test_conan_remote_url_uses_config_in_ci(self):
|
||||||
with unittest.mock.patch.dict(
|
with unittest.mock.patch.dict(
|
||||||
os.environ,
|
os.environ,
|
||||||
@@ -370,7 +350,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
clear=True,
|
clear=True,
|
||||||
):
|
):
|
||||||
self.assertEqual(modulus.cli.conan_remote_url(self.context()), "https://example.test/conan")
|
self.assertEqual(modulus.cli.conan_remote_url(self.context()), "https://example.test/conan")
|
||||||
|
|
||||||
def test_conan_credentials_only_uses_conan_environment(self):
|
def test_conan_credentials_only_uses_conan_environment(self):
|
||||||
with unittest.mock.patch.dict(
|
with unittest.mock.patch.dict(
|
||||||
os.environ,
|
os.environ,
|
||||||
@@ -384,7 +363,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
user, token = modulus.cli.conan_credentials()
|
user, token = modulus.cli.conan_credentials()
|
||||||
self.assertEqual(user, "dev")
|
self.assertEqual(user, "dev")
|
||||||
self.assertEqual(token, "secret")
|
self.assertEqual(token, "secret")
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
def test_configure_remote_skips_login_without_conan_credentials(self, run):
|
def test_configure_remote_skips_login_without_conan_credentials(self, run):
|
||||||
with unittest.mock.patch.dict(os.environ, {}, clear=True):
|
with unittest.mock.patch.dict(os.environ, {}, clear=True):
|
||||||
@@ -402,14 +380,12 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
"https://example.test/conan",
|
"https://example.test/conan",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
def test_configure_remote_rejects_partial_conan_credentials(self, run):
|
def test_configure_remote_rejects_partial_conan_credentials(self, run):
|
||||||
with unittest.mock.patch.dict(os.environ, {"CONAN_USER": "dev"}, clear=True):
|
with unittest.mock.patch.dict(os.environ, {"CONAN_USER": "dev"}, clear=True):
|
||||||
status = modulus.cli.configure_conan_remote(self.context())
|
status = modulus.cli.configure_conan_remote(self.context())
|
||||||
self.assertEqual(status, 1)
|
self.assertEqual(status, 1)
|
||||||
run.assert_not_called()
|
run.assert_not_called()
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
def test_configure_remote_skips_login_until_authentication_is_requested(self, run):
|
def test_configure_remote_skips_login_until_authentication_is_requested(self, run):
|
||||||
context = self.context()
|
context = self.context()
|
||||||
@@ -435,7 +411,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
"https://example.test/conan",
|
"https://example.test/conan",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
def test_configure_remote_logs_in_with_hidden_token_when_authenticating(self, run):
|
def test_configure_remote_logs_in_with_hidden_token_when_authenticating(self, run):
|
||||||
context = self.context()
|
context = self.context()
|
||||||
@@ -484,7 +459,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
"<hidden>",
|
"<hidden>",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def multi_target_context(self):
|
def multi_target_context(self):
|
||||||
config = {
|
config = {
|
||||||
"package": {
|
"package": {
|
||||||
@@ -537,7 +511,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
return modulus.cli.build_context(pathlib.Path(os.getcwd()), config)
|
return modulus.cli.build_context(pathlib.Path(os.getcwd()), config)
|
||||||
|
|
||||||
def test_selected_targets_default_to_default_target_only(self):
|
def test_selected_targets_default_to_default_target_only(self):
|
||||||
context = self.multi_target_context()
|
context = self.multi_target_context()
|
||||||
selected = context.selected_targets(
|
selected = context.selected_targets(
|
||||||
@@ -545,7 +518,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
False,
|
False,
|
||||||
)
|
)
|
||||||
self.assertEqual([target.name for target in selected], ["debug"])
|
self.assertEqual([target.name for target in selected], ["debug"])
|
||||||
|
|
||||||
def test_selected_targets_all_returns_every_target(self):
|
def test_selected_targets_all_returns_every_target(self):
|
||||||
context = self.multi_target_context()
|
context = self.multi_target_context()
|
||||||
selected = context.selected_targets(
|
selected = context.selected_targets(
|
||||||
@@ -553,7 +525,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
self.assertEqual([target.name for target in selected], ["debug", "release"])
|
self.assertEqual([target.name for target in selected], ["debug", "release"])
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
||||||
def test_build_can_target_one_cmake_target(self, docker, run):
|
def test_build_can_target_one_cmake_target(self, docker, run):
|
||||||
@@ -586,7 +557,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
docker.assert_called_once()
|
docker.assert_called_once()
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||||
@@ -609,7 +579,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
docker.assert_called_once()
|
docker.assert_called_once()
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||||
@@ -635,7 +604,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
docker.assert_called_once()
|
docker.assert_called_once()
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||||
@@ -664,7 +632,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
docker.assert_called_once()
|
docker.assert_called_once()
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=37)
|
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=37)
|
||||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||||
@@ -678,7 +645,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
configure_one.assert_called_once_with(context, context.default_target, update=False)
|
configure_one.assert_called_once_with(context, context.default_target, update=False)
|
||||||
run.assert_not_called()
|
run.assert_not_called()
|
||||||
docker.assert_called_once()
|
docker.assert_called_once()
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
||||||
@@ -697,7 +663,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
configure_remote.assert_not_called()
|
configure_remote.assert_not_called()
|
||||||
configure_one.assert_called_once_with(context, context.default_target, False, no_remote=True)
|
configure_one.assert_called_once_with(context, context.default_target, False, no_remote=True)
|
||||||
docker.assert_called_once()
|
docker.assert_called_once()
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "upload_packages", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "upload_packages", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "create_package", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "create_package", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||||
@@ -724,7 +689,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
create_package.assert_called_once_with(context, context.default_target, False, no_remote=True)
|
create_package.assert_called_once_with(context, context.default_target, False, no_remote=True)
|
||||||
upload.assert_not_called()
|
upload.assert_not_called()
|
||||||
docker.assert_called_once()
|
docker.assert_called_once()
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "upload_packages", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "upload_packages", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "create_package", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "create_package", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||||
@@ -752,7 +716,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
create_package.assert_called_once_with(context, context.default_target, False, no_remote=False)
|
create_package.assert_called_once_with(context, context.default_target, False, no_remote=False)
|
||||||
upload.assert_called_once_with(context, replace=False)
|
upload.assert_called_once_with(context, replace=False)
|
||||||
docker.assert_called_once()
|
docker.assert_called_once()
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "upload_packages", return_value=1)
|
@unittest.mock.patch.object(modulus.cli, "upload_packages", return_value=1)
|
||||||
@unittest.mock.patch.object(modulus.cli, "create_package", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "create_package", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||||
@@ -778,7 +741,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
create_package.assert_called_once_with(context, context.default_target, False, no_remote=False)
|
create_package.assert_called_once_with(context, context.default_target, False, no_remote=False)
|
||||||
upload.assert_called_once_with(context, replace=False)
|
upload.assert_called_once_with(context, replace=False)
|
||||||
docker.assert_called_once()
|
docker.assert_called_once()
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "create_package", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "create_package", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
||||||
@@ -803,7 +765,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
create_package.assert_not_called()
|
create_package.assert_not_called()
|
||||||
run.assert_not_called()
|
run.assert_not_called()
|
||||||
docker.assert_called_once()
|
docker.assert_called_once()
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "clear_conan_deps")
|
@unittest.mock.patch.object(modulus.cli, "clear_conan_deps")
|
||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
def test_configure_one_passes_no_remote_to_conan_install(self, run, clear):
|
def test_configure_one_passes_no_remote_to_conan_install(self, run, clear):
|
||||||
@@ -819,7 +780,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
self.assertIn(f"-DCMAKE_TOOLCHAIN_FILE={context.default_target.build_dir / 'conan' / 'conan_toolchain.cmake'}", run.call_args_list[1].args[1])
|
self.assertIn(f"-DCMAKE_TOOLCHAIN_FILE={context.default_target.build_dir / 'conan' / 'conan_toolchain.cmake'}", run.call_args_list[1].args[1])
|
||||||
self.assertIn("-DCMAKE_BUILD_TYPE=Release", run.call_args_list[1].args[1])
|
self.assertIn("-DCMAKE_BUILD_TYPE=Release", run.call_args_list[1].args[1])
|
||||||
clear.assert_called_once()
|
clear.assert_called_once()
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "clear_conan_deps")
|
@unittest.mock.patch.object(modulus.cli, "clear_conan_deps")
|
||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
def test_configure_one_passes_target_cmake_definitions(self, run, clear):
|
def test_configure_one_passes_target_cmake_definitions(self, run, clear):
|
||||||
@@ -836,7 +796,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
self.assertIn("-DSAMPLE_MODE=dev", cmake_command)
|
self.assertIn("-DSAMPLE_MODE=dev", cmake_command)
|
||||||
self.assertIn("-DSAMPLE_LIMIT=2", cmake_command)
|
self.assertIn("-DSAMPLE_LIMIT=2", cmake_command)
|
||||||
clear.assert_called_once()
|
clear.assert_called_once()
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "configure_lsp_database", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "configure_lsp_database", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
|
||||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||||
@@ -864,7 +823,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
configure_one.assert_called_once_with(context, context.default_target, False, no_remote=False)
|
configure_one.assert_called_once_with(context, context.default_target, False, no_remote=False)
|
||||||
configure_lsp.assert_called_once_with(context, database)
|
configure_lsp.assert_called_once_with(context, database)
|
||||||
docker.assert_called_once()
|
docker.assert_called_once()
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
def test_configure_lsp_database_passes_cmake_definitions_and_unsets_env(self, run):
|
def test_configure_lsp_database_passes_cmake_definitions_and_unsets_env(self, run):
|
||||||
with tempfile.TemporaryDirectory() as repo_dir:
|
with tempfile.TemporaryDirectory() as repo_dir:
|
||||||
@@ -891,7 +849,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
self.assertIn("-DVAMP_PICO_FIRMWARE_PROJECT=ON", command)
|
self.assertIn("-DVAMP_PICO_FIRMWARE_PROJECT=ON", command)
|
||||||
self.assertEqual(run.call_args.kwargs["env_overrides"], {"PICO_SDK_PATH": "/opt/pico-sdk"})
|
self.assertEqual(run.call_args.kwargs["env_overrides"], {"PICO_SDK_PATH": "/opt/pico-sdk"})
|
||||||
self.assertEqual(run.call_args.kwargs["env_unset"], ["CC", "CXX"])
|
self.assertEqual(run.call_args.kwargs["env_unset"], ["CC", "CXX"])
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "clear_conan_deps")
|
@unittest.mock.patch.object(modulus.cli, "clear_conan_deps")
|
||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
def test_configure_one_allows_public_repos_to_build_missing_dependencies(self, run, clear):
|
def test_configure_one_allows_public_repos_to_build_missing_dependencies(self, run, clear):
|
||||||
@@ -901,7 +858,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
conan_command = run.call_args_list[0].args[1]
|
conan_command = run.call_args_list[0].args[1]
|
||||||
self.assertIn("--build=missing", conan_command)
|
self.assertIn("--build=missing", conan_command)
|
||||||
clear.assert_called_once()
|
clear.assert_called_once()
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
def test_create_package_passes_no_remote_to_conan_create(self, run):
|
def test_create_package_passes_no_remote_to_conan_create(self, run):
|
||||||
with tempfile.TemporaryDirectory() as repo_dir:
|
with tempfile.TemporaryDirectory() as repo_dir:
|
||||||
@@ -912,7 +868,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
self.assertIn("--build=sample/*", run.call_args.args[1])
|
self.assertIn("--build=sample/*", run.call_args.args[1])
|
||||||
self.assertIn("--build=missing", run.call_args.args[1])
|
self.assertIn("--build=missing", run.call_args.args[1])
|
||||||
self.assertIn("--no-remote", run.call_args.args[1])
|
self.assertIn("--no-remote", run.call_args.args[1])
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
def test_create_package_allows_public_repos_to_build_missing_dependencies(self, run):
|
def test_create_package_allows_public_repos_to_build_missing_dependencies(self, run):
|
||||||
with tempfile.TemporaryDirectory() as repo_dir:
|
with tempfile.TemporaryDirectory() as repo_dir:
|
||||||
@@ -921,7 +876,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
self.assertEqual(status, 0)
|
self.assertEqual(status, 0)
|
||||||
self.assertIn("--build=sample/*", run.call_args.args[1])
|
self.assertIn("--build=sample/*", run.call_args.args[1])
|
||||||
self.assertIn("--build=missing", run.call_args.args[1])
|
self.assertIn("--build=missing", run.call_args.args[1])
|
||||||
|
|
||||||
def test_create_artifact_package_installs_archives_and_checksums_tree(self):
|
def test_create_artifact_package_installs_archives_and_checksums_tree(self):
|
||||||
with tempfile.TemporaryDirectory() as repo_dir:
|
with tempfile.TemporaryDirectory() as repo_dir:
|
||||||
repo = pathlib.Path(repo_dir)
|
repo = pathlib.Path(repo_dir)
|
||||||
@@ -937,14 +891,12 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
"with_appliance_adapters": True,
|
"with_appliance_adapters": True,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
def fake_run(run_context, command, **kwargs):
|
def fake_run(run_context, command, **kwargs):
|
||||||
if command[:2] == ["cmake", "--install"]:
|
if command[:2] == ["cmake", "--install"]:
|
||||||
install_root = modulus.cli.package_artifact_build_root(context, context.default_target) / "stage" / "usr"
|
install_root = modulus.cli.package_artifact_build_root(context, context.default_target) / "stage" / "usr"
|
||||||
(install_root / "bin").mkdir(parents=True)
|
(install_root / "bin").mkdir(parents=True)
|
||||||
(install_root / "bin" / "sample").write_text("binary\n", encoding="utf-8")
|
(install_root / "bin" / "sample").write_text("binary\n", encoding="utf-8")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
with unittest.mock.patch.object(modulus.cli, "run", side_effect=fake_run) as run:
|
with unittest.mock.patch.object(modulus.cli, "run", side_effect=fake_run) as run:
|
||||||
status, artifacts = modulus.cli.create_artifact_package(
|
status, artifacts = modulus.cli.create_artifact_package(
|
||||||
context,
|
context,
|
||||||
@@ -971,7 +923,6 @@ class upload_packages_test(unittest.TestCase):
|
|||||||
cmake_command = run.call_args_list[1].args[1]
|
cmake_command = run.call_args_list[1].args[1]
|
||||||
self.assertIn("-DSAMPLE_WITH_WEB=ON", cmake_command)
|
self.assertIn("-DSAMPLE_WITH_WEB=ON", cmake_command)
|
||||||
self.assertIn(f"-DCMAKE_INSTALL_PREFIX={repo / 'build' / 'build_common' / 'artifacts' / 'native' / 'stage' / 'usr'}", cmake_command)
|
self.assertIn(f"-DCMAKE_INSTALL_PREFIX={repo / 'build' / 'build_common' / 'artifacts' / 'native' / 'stage' / 'usr'}", cmake_command)
|
||||||
|
|
||||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||||
def test_generic_artifact_upload_uses_package_registry_credentials(self, run):
|
def test_generic_artifact_upload_uses_package_registry_credentials(self, run):
|
||||||
context = self.context()
|
context = self.context()
|
||||||
|
|||||||
+8
-20
@@ -23,10 +23,8 @@ class modulus_export_test(unittest.TestCase):
|
|||||||
"name": "sample",
|
"name": "sample",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def write_config(self, root, config):
|
def write_config(self, root, config):
|
||||||
(root / "modulus.json").write_text(json.dumps(config), encoding="utf-8")
|
(root / "modulus.json").write_text(json.dumps(config), encoding="utf-8")
|
||||||
|
|
||||||
def write_package(self, package):
|
def write_package(self, package):
|
||||||
for directory in (
|
for directory in (
|
||||||
"modulus",
|
"modulus",
|
||||||
@@ -43,16 +41,19 @@ class modulus_export_test(unittest.TestCase):
|
|||||||
(package / "modulus" / "cli.py").write_text("", encoding="utf-8")
|
(package / "modulus" / "cli.py").write_text("", encoding="utf-8")
|
||||||
(package / "modulus" / "config.py").write_text("", encoding="utf-8")
|
(package / "modulus" / "config.py").write_text("", encoding="utf-8")
|
||||||
(package / "modulus" / "export.py").write_text("", encoding="utf-8")
|
(package / "modulus" / "export.py").write_text("", encoding="utf-8")
|
||||||
|
(package / "modulus" / "formatting.py").write_text("", encoding="utf-8")
|
||||||
|
(package / "modulus" / "format_layout.py").write_text("", encoding="utf-8")
|
||||||
(package / "tools" / "configure.py").write_text("#!/usr/bin/env python3\n", encoding="utf-8")
|
(package / "tools" / "configure.py").write_text("#!/usr/bin/env python3\n", encoding="utf-8")
|
||||||
(package / "tools" / "lsp.py").write_text("#!/usr/bin/env python3\n", encoding="utf-8")
|
(package / "tools" / "lsp.py").write_text("#!/usr/bin/env python3\n", encoding="utf-8")
|
||||||
(package / "tools" / "run.py").write_text("#!/usr/bin/env python3\n", encoding="utf-8")
|
(package / "tools" / "run.py").write_text("#!/usr/bin/env python3\n", encoding="utf-8")
|
||||||
|
(package / "tools" / "format.py").write_text("#!/usr/bin/env python3\n", encoding="utf-8")
|
||||||
|
(package / "tools" / "docker" / "format-requirements.txt").write_text("autopep8==2.3.2\n", encoding="utf-8")
|
||||||
(package / "tools" / "docker" / "Dockerfile").write_text("FROM scratch\n", encoding="utf-8")
|
(package / "tools" / "docker" / "Dockerfile").write_text("FROM scratch\n", encoding="utf-8")
|
||||||
(package / "include" / "modulus" / "types.hpp").write_text("", encoding="utf-8")
|
(package / "include" / "modulus" / "types.hpp").write_text("", encoding="utf-8")
|
||||||
(package / "tests" / "test.cpp").write_text("", encoding="utf-8")
|
(package / "tests" / "test.cpp").write_text("", encoding="utf-8")
|
||||||
(package / "lib" / "cmake" / "modulus" / "modulus-config.cmake").write_text("", encoding="utf-8")
|
(package / "lib" / "cmake" / "modulus" / "modulus-config.cmake").write_text("", encoding="utf-8")
|
||||||
(package / "share" / "modulus" / "duplicate.py").write_text("", encoding="utf-8")
|
(package / "share" / "modulus" / "duplicate.py").write_text("", encoding="utf-8")
|
||||||
(package / "bin" / "modulus-tool").write_text("", encoding="utf-8")
|
(package / "bin" / "modulus-tool").write_text("", encoding="utf-8")
|
||||||
|
|
||||||
def test_boost_requirement_options_enable_configured_components(self):
|
def test_boost_requirement_options_enable_configured_components(self):
|
||||||
config = modulus.config.normalize(self.config())
|
config = modulus.config.normalize(self.config())
|
||||||
boost_options = modulus.config.boost_requirement_options(config)
|
boost_options = modulus.config.boost_requirement_options(config)
|
||||||
@@ -61,7 +62,6 @@ class modulus_export_test(unittest.TestCase):
|
|||||||
self.assertIs(boost_options["without_container"], False)
|
self.assertIs(boost_options["without_container"], False)
|
||||||
self.assertIs(boost_options["without_charconv"], False)
|
self.assertIs(boost_options["without_charconv"], False)
|
||||||
self.assertIs(boost_options["without_atomic"], True)
|
self.assertIs(boost_options["without_atomic"], True)
|
||||||
|
|
||||||
def test_default_targets_include_architecture_and_build_type_matrix(self):
|
def test_default_targets_include_architecture_and_build_type_matrix(self):
|
||||||
config = modulus.config.normalize(self.config())
|
config = modulus.config.normalize(self.config())
|
||||||
targets = config["tooling"]["targets"]
|
targets = config["tooling"]["targets"]
|
||||||
@@ -82,7 +82,6 @@ class modulus_export_test(unittest.TestCase):
|
|||||||
self.assertFalse(config["tooling"]["run"]["enabled"])
|
self.assertFalse(config["tooling"]["run"]["enabled"])
|
||||||
self.assertFalse(config["tooling"]["lsp"]["enabled"])
|
self.assertFalse(config["tooling"]["lsp"]["enabled"])
|
||||||
self.assertEqual(config["tooling"]["lsp"]["compile_databases"], [])
|
self.assertEqual(config["tooling"]["lsp"]["compile_databases"], [])
|
||||||
|
|
||||||
def test_product_artifact_and_run_config_are_validated(self):
|
def test_product_artifact_and_run_config_are_validated(self):
|
||||||
config = self.config()
|
config = self.config()
|
||||||
config["package"]["artifact"] = {
|
config["package"]["artifact"] = {
|
||||||
@@ -111,11 +110,9 @@ class modulus_export_test(unittest.TestCase):
|
|||||||
self.assertEqual(normalized["package"]["artifact"]["install_prefix"], "usr")
|
self.assertEqual(normalized["package"]["artifact"]["install_prefix"], "usr")
|
||||||
self.assertEqual(normalized["package"]["artifact"]["registry_url"], "https://git.thyme-labs.com/api/packages/khodo/generic")
|
self.assertEqual(normalized["package"]["artifact"]["registry_url"], "https://git.thyme-labs.com/api/packages/khodo/generic")
|
||||||
self.assertEqual(normalized["tooling"]["run"]["args"], [])
|
self.assertEqual(normalized["tooling"]["run"]["args"], [])
|
||||||
|
|
||||||
config["package"]["artifact"]["targets"] = ["missing"]
|
config["package"]["artifact"]["targets"] = ["missing"]
|
||||||
with self.assertRaisesRegex(modulus.config.config_error, "unknown target"):
|
with self.assertRaisesRegex(modulus.config.config_error, "unknown target"):
|
||||||
modulus.config.normalize(config)
|
modulus.config.normalize(config)
|
||||||
|
|
||||||
def test_target_cmake_definitions_are_validated_and_defaulted(self):
|
def test_target_cmake_definitions_are_validated_and_defaulted(self):
|
||||||
config = self.config()
|
config = self.config()
|
||||||
config["tooling"] = {
|
config["tooling"] = {
|
||||||
@@ -148,7 +145,6 @@ class modulus_export_test(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(normalized["tooling"]["targets"]["release"]["cmake_definitions"], {})
|
self.assertEqual(normalized["tooling"]["targets"]["release"]["cmake_definitions"], {})
|
||||||
|
|
||||||
config["tooling"]["targets"]["native"]["cmake_definitions"] = {
|
config["tooling"]["targets"]["native"]["cmake_definitions"] = {
|
||||||
"SAMPLE_BAD": [
|
"SAMPLE_BAD": [
|
||||||
"invalid",
|
"invalid",
|
||||||
@@ -156,7 +152,6 @@ class modulus_export_test(unittest.TestCase):
|
|||||||
}
|
}
|
||||||
with self.assertRaisesRegex(modulus.config.config_error, "tooling.targets.native.cmake_definitions"):
|
with self.assertRaisesRegex(modulus.config.config_error, "tooling.targets.native.cmake_definitions"):
|
||||||
modulus.config.normalize(config)
|
modulus.config.normalize(config)
|
||||||
|
|
||||||
def test_lsp_compile_databases_are_validated_and_defaulted(self):
|
def test_lsp_compile_databases_are_validated_and_defaulted(self):
|
||||||
config = self.config()
|
config = self.config()
|
||||||
config["tooling"] = {
|
config["tooling"] = {
|
||||||
@@ -183,11 +178,9 @@ class modulus_export_test(unittest.TestCase):
|
|||||||
self.assertEqual(database["generator"], "Ninja")
|
self.assertEqual(database["generator"], "Ninja")
|
||||||
self.assertEqual(database["env"], {})
|
self.assertEqual(database["env"], {})
|
||||||
self.assertEqual(database["unset_env"], ["CC", "CXX"])
|
self.assertEqual(database["unset_env"], ["CC", "CXX"])
|
||||||
|
|
||||||
config["tooling"]["lsp"]["compile_databases"][0]["unset_env"] = ["bad-name"]
|
config["tooling"]["lsp"]["compile_databases"][0]["unset_env"] = ["bad-name"]
|
||||||
with self.assertRaisesRegex(modulus.config.config_error, "tooling.lsp.compile_databases\\[0\\].unset_env"):
|
with self.assertRaisesRegex(modulus.config.config_error, "tooling.lsp.compile_databases\\[0\\].unset_env"):
|
||||||
modulus.config.normalize(config)
|
modulus.config.normalize(config)
|
||||||
|
|
||||||
def test_checked_in_profile_overrides_exported_default_profile(self):
|
def test_checked_in_profile_overrides_exported_default_profile(self):
|
||||||
with tempfile.TemporaryDirectory() as repo_dir:
|
with tempfile.TemporaryDirectory() as repo_dir:
|
||||||
repo = pathlib.Path(repo_dir)
|
repo = pathlib.Path(repo_dir)
|
||||||
@@ -197,14 +190,12 @@ class modulus_export_test(unittest.TestCase):
|
|||||||
profile.write_text("[settings]\n", encoding="utf-8")
|
profile.write_text("[settings]\n", encoding="utf-8")
|
||||||
config = modulus.config.load_repo_config(repo)
|
config = modulus.config.load_repo_config(repo)
|
||||||
self.assertEqual(config["tooling"]["targets"]["x86_64_gcc12_release"]["profile"], "conan/profiles/x86_64_gcc12_release")
|
self.assertEqual(config["tooling"]["targets"]["x86_64_gcc12_release"]["profile"], "conan/profiles/x86_64_gcc12_release")
|
||||||
|
|
||||||
def test_docker_defaults_use_shared_modulus_marker(self):
|
def test_docker_defaults_use_shared_modulus_marker(self):
|
||||||
config = modulus.config.normalize(self.config())
|
config = modulus.config.normalize(self.config())
|
||||||
self.assertEqual(config["tooling"]["docker"]["image"], "modulus-build:dev")
|
self.assertEqual(config["tooling"]["docker"]["image"], "modulus-build:dev")
|
||||||
self.assertEqual(config["tooling"]["docker"]["marker"], "/opt/modulus-build-image")
|
self.assertEqual(config["tooling"]["docker"]["marker"], "/opt/modulus-build-image")
|
||||||
self.assertNotIn("apt_architectures", config["tooling"]["docker"])
|
self.assertNotIn("apt_architectures", config["tooling"]["docker"])
|
||||||
self.assertNotIn("apt_packages", config["tooling"]["docker"])
|
self.assertNotIn("apt_packages", config["tooling"]["docker"])
|
||||||
|
|
||||||
def test_export_project_copies_runtime_and_exported_tools(self):
|
def test_export_project_copies_runtime_and_exported_tools(self):
|
||||||
with tempfile.TemporaryDirectory() as repo_dir, tempfile.TemporaryDirectory() as package_dir:
|
with tempfile.TemporaryDirectory() as repo_dir, tempfile.TemporaryDirectory() as package_dir:
|
||||||
repo = pathlib.Path(repo_dir)
|
repo = pathlib.Path(repo_dir)
|
||||||
@@ -226,6 +217,10 @@ class modulus_export_test(unittest.TestCase):
|
|||||||
self.assertTrue((runtime / "modulus" / "cli.py").is_file())
|
self.assertTrue((runtime / "modulus" / "cli.py").is_file())
|
||||||
self.assertTrue((runtime / "modulus" / "config.py").is_file())
|
self.assertTrue((runtime / "modulus" / "config.py").is_file())
|
||||||
self.assertTrue((runtime / "modulus" / "export.py").is_file())
|
self.assertTrue((runtime / "modulus" / "export.py").is_file())
|
||||||
|
self.assertTrue((runtime / "modulus" / "formatting.py").is_file())
|
||||||
|
self.assertTrue((runtime / "modulus" / "format_layout.py").is_file())
|
||||||
|
self.assertTrue((repo / "build/build_tools/format.py").is_file())
|
||||||
|
self.assertTrue((repo / "build/build_tools/docker/format-requirements.txt").is_file())
|
||||||
self.assertFalse((runtime / "CMakeLists.txt").exists())
|
self.assertFalse((runtime / "CMakeLists.txt").exists())
|
||||||
self.assertFalse((runtime / "README.md").exists())
|
self.assertFalse((runtime / "README.md").exists())
|
||||||
self.assertFalse((runtime / "conanfile.py").exists())
|
self.assertFalse((runtime / "conanfile.py").exists())
|
||||||
@@ -239,7 +234,6 @@ class modulus_export_test(unittest.TestCase):
|
|||||||
self.assertFalse((runtime / "lib").exists())
|
self.assertFalse((runtime / "lib").exists())
|
||||||
self.assertFalse((runtime / "share").exists())
|
self.assertFalse((runtime / "share").exists())
|
||||||
self.assertFalse((runtime / "bin").exists())
|
self.assertFalse((runtime / "bin").exists())
|
||||||
|
|
||||||
def test_editor_defaults_use_configured_target(self):
|
def test_editor_defaults_use_configured_target(self):
|
||||||
config = modulus.config.normalize(self.config())
|
config = modulus.config.normalize(self.config())
|
||||||
config["tooling"].pop("editor")
|
config["tooling"].pop("editor")
|
||||||
@@ -250,7 +244,6 @@ class modulus_export_test(unittest.TestCase):
|
|||||||
self.assertEqual(editor["clangd"][0]["Diagnostics"]["ClangTidy"]["Remove"], ["*"])
|
self.assertEqual(editor["clangd"][0]["Diagnostics"]["ClangTidy"]["Remove"], ["*"])
|
||||||
self.assertIn("conanfile.py", editor["pyright"]["include"])
|
self.assertIn("conanfile.py", editor["pyright"]["include"])
|
||||||
self.assertNotIn("package/conanfile.py", editor["pyright"]["include"])
|
self.assertNotIn("package/conanfile.py", editor["pyright"]["include"])
|
||||||
|
|
||||||
def test_editor_replacements_and_refresh_are_idempotent(self):
|
def test_editor_replacements_and_refresh_are_idempotent(self):
|
||||||
config = self.config()
|
config = self.config()
|
||||||
editor = {
|
editor = {
|
||||||
@@ -277,7 +270,6 @@ class modulus_export_test(unittest.TestCase):
|
|||||||
before = {path.name: path.stat().st_mtime_ns for path in root.iterdir()}
|
before = {path.name: path.stat().st_mtime_ns for path in root.iterdir()}
|
||||||
modulus.export.sync_editor_files(root, normalized)
|
modulus.export.sync_editor_files(root, normalized)
|
||||||
self.assertEqual(before, {path.name: path.stat().st_mtime_ns for path in root.iterdir()})
|
self.assertEqual(before, {path.name: path.stat().st_mtime_ns for path in root.iterdir()})
|
||||||
|
|
||||||
def test_editor_config_rejects_invalid_shapes(self):
|
def test_editor_config_rejects_invalid_shapes(self):
|
||||||
for editor in (None, [], {"unknown": {}}, {"clangd": {}}, {"clangd": []}, {"clangd": ["bad"]}, {"pyright": []}, {"clang_format": None}, {"pyright": {"value": float("nan")}}):
|
for editor in (None, [], {"unknown": {}}, {"clangd": {}}, {"clangd": []}, {"clangd": ["bad"]}, {"pyright": []}, {"clang_format": None}, {"pyright": {"value": float("nan")}}):
|
||||||
with self.subTest(editor=editor):
|
with self.subTest(editor=editor):
|
||||||
@@ -285,7 +277,6 @@ class modulus_export_test(unittest.TestCase):
|
|||||||
config["tooling"] = {"editor": editor}
|
config["tooling"] = {"editor": editor}
|
||||||
with self.assertRaises(modulus.config.config_error):
|
with self.assertRaises(modulus.config.config_error):
|
||||||
modulus.config.normalize(config)
|
modulus.config.normalize(config)
|
||||||
|
|
||||||
def test_export_requires_packaged_tools(self):
|
def test_export_requires_packaged_tools(self):
|
||||||
with tempfile.TemporaryDirectory() as repo_dir, tempfile.TemporaryDirectory() as package_dir:
|
with tempfile.TemporaryDirectory() as repo_dir, tempfile.TemporaryDirectory() as package_dir:
|
||||||
repo = pathlib.Path(repo_dir)
|
repo = pathlib.Path(repo_dir)
|
||||||
@@ -295,7 +286,6 @@ class modulus_export_test(unittest.TestCase):
|
|||||||
(package / "modulus" / "cli.py").write_text("", encoding="utf-8")
|
(package / "modulus" / "cli.py").write_text("", encoding="utf-8")
|
||||||
with self.assertRaisesRegex(RuntimeError, "missing tools directory"):
|
with self.assertRaisesRegex(RuntimeError, "missing tools directory"):
|
||||||
modulus.export.sync_project(repo, package)
|
modulus.export.sync_project(repo, package)
|
||||||
|
|
||||||
def test_normalize_rejects_invalid_cmake_values(self):
|
def test_normalize_rejects_invalid_cmake_values(self):
|
||||||
config = self.config()
|
config = self.config()
|
||||||
config["package"]["name"] = "BadName"
|
config["package"]["name"] = "BadName"
|
||||||
@@ -305,13 +295,11 @@ class modulus_export_test(unittest.TestCase):
|
|||||||
config["docs"] = {}
|
config["docs"] = {}
|
||||||
with self.assertRaisesRegex(modulus.config.config_error, "unknown keys: docs"):
|
with self.assertRaisesRegex(modulus.config.config_error, "unknown keys: docs"):
|
||||||
modulus.config.normalize(config)
|
modulus.config.normalize(config)
|
||||||
|
|
||||||
def test_unknown_top_level_config_keys_are_rejected(self):
|
def test_unknown_top_level_config_keys_are_rejected(self):
|
||||||
config = self.config()
|
config = self.config()
|
||||||
config["old_config"] = {}
|
config["old_config"] = {}
|
||||||
with self.assertRaisesRegex(modulus.config.config_error, "unknown keys: old_config"):
|
with self.assertRaisesRegex(modulus.config.config_error, "unknown keys: old_config"):
|
||||||
modulus.config.normalize(config)
|
modulus.config.normalize(config)
|
||||||
|
|
||||||
def test_removed_setup_generation_fields_are_rejected(self):
|
def test_removed_setup_generation_fields_are_rejected(self):
|
||||||
config = self.config()
|
config = self.config()
|
||||||
config["schema_version"] = 1
|
config["schema_version"] = 1
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import ast
|
||||||
|
import contextlib
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import modulus.cli
|
||||||
|
import modulus.config
|
||||||
|
import modulus.export
|
||||||
|
import modulus.format_layout
|
||||||
|
import modulus.formatting
|
||||||
|
|
||||||
|
class formatting_test(unittest.TestCase):
|
||||||
|
def formatted(self, source, suffix):
|
||||||
|
result = modulus.format_layout.format_source(source, suffix)
|
||||||
|
self.assertEqual(modulus.format_layout.format_source(result, suffix), result)
|
||||||
|
return result
|
||||||
|
def test_cpp_units_namespaces_and_include_groups(self):
|
||||||
|
source = '#include <vector>\n#include <string>\n\n#include "local.hpp"\n\nnamespace sample {\nint one() {\n\nint x=1;\n\nx++;\nreturn x;\n}\nint two() {return 2;}\n}\n'
|
||||||
|
result = self.formatted(source, ".cpp")
|
||||||
|
self.assertTrue(result.startswith(source.split("namespace")[0]))
|
||||||
|
self.assertIn("namespace sample {\n\nint one()", result)
|
||||||
|
self.assertIn("int x = 1;\n x++;\n return x;", result)
|
||||||
|
self.assertIn("}\n\nint two()", result)
|
||||||
|
self.assertTrue(result.endswith("}\n\n}\n"))
|
||||||
|
def test_cpp_no_column_limit_and_simple_lists(self):
|
||||||
|
name = "argument_" + "a" * 180
|
||||||
|
result = self.formatted(f"void function(\nint {name},\nint b) {{\n call(\n{name},\nb);\n}}\n", ".cpp")
|
||||||
|
self.assertIn(f"void function(int {name}, int b)", result)
|
||||||
|
self.assertIn(f"call({name}, b);", result)
|
||||||
|
def test_cpp_class_and_struct_members_have_no_blank_lines(self):
|
||||||
|
for kind in ("class", "struct"):
|
||||||
|
with self.subTest(kind=kind):
|
||||||
|
source = f'{kind} source_location {{\npublic:\n\n source_location() = default;\n\n source_location(source_location const&) = delete;\n\n int line() const;\n\n int value() const {{\n\n return line_;\n\n }}\n\n struct nested {{\n\n int value;\n\n }};\n\nprivate:\n\n int line_;\n\n}};\n\nstruct next {{ int value; }};\n'
|
||||||
|
result = self.formatted(source, ".hpp")
|
||||||
|
body = result.split(f"{kind} source_location {{\n", 1)[1].split("\n};", 1)[0]
|
||||||
|
self.assertNotIn("\n\n", body)
|
||||||
|
self.assertIn("};\n\nstruct next", result)
|
||||||
|
def test_cpp_class_keeps_literal_and_comment_blank_lines(self):
|
||||||
|
literal = 'R"raw(first\n\nsecond)raw"'
|
||||||
|
comment = '/* first\n\nsecond */'
|
||||||
|
source = f'struct example {{\n\n const char* text = {literal};\n\n {comment}\n\n int value;\n}};\n'
|
||||||
|
result = self.formatted(source, ".hpp")
|
||||||
|
self.assertIn(literal, result)
|
||||||
|
self.assertIn(comment, result)
|
||||||
|
self.assertNotIn("\n\n", result.replace(literal, "literal").replace(comment, "comment"))
|
||||||
|
def test_cpp_literals_macros_templates_and_lambdas(self):
|
||||||
|
literal = 'R"raw(first\n\n second\n)raw"'
|
||||||
|
source = f'template<class T>\nvoid test(T v) {{\n const char* s = {literal};\n\n visit(v, [](auto a) {{\n\nreturn a;\n}});\n}}\nBOOST_AUTO_TEST_CASE(example) {{\n\nCHECK(true);\n\n}}\n'
|
||||||
|
result = self.formatted(source, ".cpp")
|
||||||
|
self.assertIn(literal, result)
|
||||||
|
self.assertIn("BOOST_AUTO_TEST_CASE(example) {\n CHECK(true);\n}", result)
|
||||||
|
self.assertIn("[](auto a) {\n", result)
|
||||||
|
def test_python_ast_literals_decorators_and_nested_units(self):
|
||||||
|
source = 'import os\n\nimport pathlib\n\n@decorator\ndef outer(\n first,\n second,\n):\n text = """first\n\n second\n"""\n\n first += 1\n def nested():\n return 1\n return call(\n first,\n second,\n )\n'
|
||||||
|
result = self.formatted(source, ".py")
|
||||||
|
self.assertEqual(ast.dump(ast.parse(source)), ast.dump(ast.parse(result)))
|
||||||
|
self.assertIn('"""first\n\n second\n"""', result)
|
||||||
|
self.assertIn("@decorator\ndef outer(first, second):", result)
|
||||||
|
self.assertIn("first += 1\n\n def nested()", result)
|
||||||
|
self.assertIn("return 1\n\n return call(first, second)", result)
|
||||||
|
self.assertTrue(result.startswith("import os\n\nimport pathlib\n"))
|
||||||
|
def test_python_no_column_limit_and_structured_literals(self):
|
||||||
|
name = "a" * 250
|
||||||
|
source = f"def test({name},\n other):\n return call({name},\nother)\n"
|
||||||
|
result = self.formatted(source, ".py")
|
||||||
|
self.assertIn(f"def test({name}, other):", result)
|
||||||
|
data = 'def test():\n return call({\n "a": 1,\n "b": 2,\n })\n'
|
||||||
|
self.assertIn('{\n', self.formatted(data, ".py"))
|
||||||
|
def test_python_classes_keep_members_together(self):
|
||||||
|
source = 'class Example:\n\n """first\n\nsecond"""\n\n field = 1\n\n @staticmethod\n def value():\n\n return 1\n\n class Nested:\n\n field = 2\n\n async def other(self):\n\n return 2\n\nclass Next:\n\n field = 3\n'
|
||||||
|
result = self.formatted(source, ".py")
|
||||||
|
self.assertEqual(ast.dump(ast.parse(source)), ast.dump(ast.parse(result)))
|
||||||
|
self.assertIn('"""first\n\nsecond"""', result)
|
||||||
|
self.assertNotIn("\n\n", result.split("\n\nclass Next", 1)[0].replace('"""first\n\nsecond"""', 'docstring'))
|
||||||
|
self.assertIn("return 2\n\nclass Next:\n field = 3", result)
|
||||||
|
def test_python_tuple_continuations_use_two_spaces(self):
|
||||||
|
source = 'runtime_ignore_patterns = (\n ".git",\n ".tmp",\n "__pycache__",\n)\n'
|
||||||
|
expected = 'runtime_ignore_patterns = (\n ".git",\n ".tmp",\n "__pycache__",\n)\n'
|
||||||
|
self.assertEqual(self.formatted(source, ".py"), expected)
|
||||||
|
def test_python_nested_collections_use_two_spaces_per_opening_line(self):
|
||||||
|
source = 'def example():\n result = build({\n "items": [\n (\n 1,\n 2,\n ),\n ],\n "names": {\n "a",\n "b",\n },\n })\n return result\n'
|
||||||
|
expected = 'def example():\n result = build({\n "items": [\n (\n 1,\n 2,\n ),\n ],\n "names": {\n "a",\n "b",\n },\n })\n return result\n'
|
||||||
|
self.assertEqual(self.formatted(source, ".py"), expected)
|
||||||
|
self.assertEqual(self.formatted("values = [[\n 1,\n 2,\n]]\n", ".py"), "values = [[\n 1,\n 2,\n]]\n")
|
||||||
|
def test_python_continued_imports_headers_and_comments(self):
|
||||||
|
source = 'from somewhere import (\n first,\n second,\n)\n\n@decorator(\n first, # comment\n second,\n)\ndef example(\n first, # comment\n second,\n):\n if (\n first # condition\n and second\n ):\n return first\n'
|
||||||
|
expected = 'from somewhere import (\n first,\n second,\n)\n\n@decorator(\n first, # comment\n second,\n)\ndef example(\n first, # comment\n second,\n):\n if (\n first # condition\n and second\n ):\n return first\n'
|
||||||
|
self.assertEqual(self.formatted(source, ".py"), expected)
|
||||||
|
def test_python_explicit_continuations_use_two_spaces(self):
|
||||||
|
source = 'def example():\n with first() as one, \\\n second() as two:\n return one + \\\n two\n'
|
||||||
|
expected = 'def example():\n with first() as one, \\\n second() as two:\n return one + \\\n two\n'
|
||||||
|
self.assertEqual(self.formatted(source, ".py"), expected)
|
||||||
|
def test_python_continuations_preserve_multiline_literal_contents(self):
|
||||||
|
source = 'def example():\n return [\n # first value\n f"{call({1: 2})}",\n """first\n second\n\n third""",\n f"""start {name}\n {value}\n\n end""",\n "next",\n ]\n'
|
||||||
|
expected = 'def example():\n return [\n # first value\n f"{call({1: 2})}",\n """first\n second\n\n third""",\n f"""start {name}\n {value}\n\n end""",\n "next",\n ]\n'
|
||||||
|
self.assertEqual(self.formatted(source, ".py"), expected)
|
||||||
|
def test_python_adjacent_literals_are_continuations_not_literal_contents(self):
|
||||||
|
source = 'def example():\n text = (\n "first"\n "second"\n f"{value}"\n f"{other}"\n )\n return text\n'
|
||||||
|
expected = 'def example():\n text = (\n "first"\n "second"\n f"{value}"\n f"{other}"\n )\n return text\n'
|
||||||
|
self.assertEqual(self.formatted(source, ".py"), expected)
|
||||||
|
def test_aliases_and_portability_exception(self):
|
||||||
|
cpp = "namespace std { using bool_t = decltype(true); }\nusing integer = int;\nnamespace fs = std::filesystem;\ntypedef int count;\n"
|
||||||
|
self.assertEqual(len(list(modulus.format_layout.alias_diagnostics(cpp, ".hpp"))), 4)
|
||||||
|
self.assertEqual(len(list(modulus.format_layout.alias_diagnostics(cpp, ".hpp", True))), 3)
|
||||||
|
python = "import pathlib as p\nfrom typing import Literal\nRole = Literal['a', 'b']\n"
|
||||||
|
self.assertEqual(len(list(modulus.format_layout.alias_diagnostics(python, ".py"))), 2)
|
||||||
|
def test_stdin_failure_has_no_output_and_never_builds_image(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = pathlib.Path(directory)
|
||||||
|
context = modulus.cli.build_context(root, modulus.config.normalize({"package": {"name": "sample"}}))
|
||||||
|
context.docker_marker = root / "not-in-container"
|
||||||
|
output, errors = io.StringIO(), io.StringIO()
|
||||||
|
with patch.dict(os.environ, {"MODULUS_DIRECT": "1"}), patch("sys.stdin", io.StringIO("def broken(")), contextlib.redirect_stdout(output), contextlib.redirect_stderr(errors):
|
||||||
|
self.assertEqual(modulus.formatting.launch(context, ["--stdin-filename", "test.py"]), 1)
|
||||||
|
self.assertEqual(output.getvalue(), "")
|
||||||
|
with patch.dict(os.environ, {"MODULUS_DIRECT": "0"}), patch.object(modulus.clangd, "current_image", return_value=None), patch.object(modulus.cli, "ensure_build_image") as build, contextlib.redirect_stderr(errors):
|
||||||
|
self.assertEqual(modulus.formatting.launch(context, ["--check"]), 1)
|
||||||
|
build.assert_not_called()
|
||||||
|
def test_discovery_excludes_generated_and_symlinks(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = pathlib.Path(directory)
|
||||||
|
subprocess.run(["git", "init", "-q", str(root)], check=True)
|
||||||
|
for name in ["keep.cpp", "skip.cpp", "build/generated.cpp", "vendor/library.cpp"]:
|
||||||
|
path = root / name
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.touch()
|
||||||
|
(root / "link.cpp").symlink_to(root / "keep.cpp")
|
||||||
|
self.assertEqual(modulus.formatting.source_files(root, [], ["skip.cpp"]), [root / "keep.cpp"])
|
||||||
|
def test_nested_lists_join_in_one_pass_without_changing_string_spaces(self):
|
||||||
|
cpp = 'void example() { outer(\n inner(\n 1,\n 2),\n "a b"); }\n'
|
||||||
|
self.assertIn('outer(inner(1, 2), "a b");', self.formatted(cpp, ".cpp"))
|
||||||
|
python = 'def example():\n return outer(\n inner(\n 1,\n 2),\n "a b")\n'
|
||||||
|
self.assertIn('outer(inner(1, 2), "a b")', self.formatted(python, ".py"))
|
||||||
|
def test_multiline_fstrings_preserve_blank_lines_on_new_python_tokenizers(self):
|
||||||
|
source = 'def example(name):\n return f"""first {name}\n\nsecond\n\nthird"""\n'
|
||||||
|
self.assertEqual(self.formatted(source, ".py"), source)
|
||||||
|
def test_alias_check_distinguishes_module_aliases_from_saved_callbacks(self):
|
||||||
|
source = 'import modulus.cli\nsaved = modulus.cli.docker_run\nconfig = modulus.cli\nItems = list[int]\n'
|
||||||
|
self.assertEqual([line for line, _ in modulus.format_layout.alias_diagnostics(source, ".py")], [3, 4])
|
||||||
|
def test_exported_stdin_formatter_needs_no_compile_database(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = pathlib.Path(directory)
|
||||||
|
(root / "modulus.json").write_text(json.dumps({"package": {"name": "sample"}}))
|
||||||
|
modulus.export.sync_project(root, pathlib.Path(__file__).resolve().parent.parent)
|
||||||
|
result = subprocess.run([sys.executable, str(root / "build/build_tools/format.py"), "--stdin-filename", "sample.py"], cwd=root, env=dict(os.environ, MODULUS_DIRECT="1", PYTHONDONTWRITEBYTECODE="1"), input="def sample():\n values = (\n 1,\n 2,\n )\n return values\n", text=True, capture_output=True)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertEqual(result.stdout, "def sample():\n values = (\n 1,\n 2,\n )\n return values\n")
|
||||||
|
self.assertEqual(result.stderr, "")
|
||||||
|
@unittest.skipUnless(os.environ.get("MODULUS_FORMAT_REPO") and os.environ.get("MODULUS_CONFORM_PATH"), "set MODULUS_FORMAT_REPO and MODULUS_CONFORM_PATH for the Neovim integration test")
|
||||||
|
def test_neovim_container_formatting(self):
|
||||||
|
root = pathlib.Path(__file__).resolve().parent.parent
|
||||||
|
env = dict(os.environ, MODULUS_FORMAT_HELPER=str(root / "tools/nvim/modulus.lua"))
|
||||||
|
result = subprocess.run(["nvim", "--headless", "-u", "NONE", "-l", str(root / "tests/nvim_format.lua")], env=env, text=True, capture_output=True, timeout=60)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||||
|
self.assertIn("MODULUS_FORMAT_OK", result.stdout + result.stderr)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -11,7 +11,7 @@ def header_roots(config):
|
|||||||
for driver in config["query_drivers"]:
|
for driver in config["query_drivers"]:
|
||||||
for language in ("c", "c++"):
|
for language in ("c", "c++"):
|
||||||
result = subprocess.run([driver, "-E", "-v", "-x", language, "-"], input="",
|
result = subprocess.run([driver, "-E", "-v", "-x", language, "-"], input="",
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True)
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True)
|
||||||
listing = False
|
listing = False
|
||||||
for line in result.stderr.splitlines():
|
for line in result.stderr.splitlines():
|
||||||
if line == "#include <...> search starts here:":
|
if line == "#include <...> search starts here:":
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ ENV PATH=/opt/modulus-python/bin:${PATH}
|
|||||||
RUN python -m pip install --no-cache-dir \
|
RUN python -m pip install --no-cache-dir \
|
||||||
conan==2.26.2
|
conan==2.26.2
|
||||||
|
|
||||||
|
# The path is shared by source builds and exported consumer tooling.
|
||||||
|
ARG MODULUS_TOOLS=tools
|
||||||
|
COPY ${MODULUS_TOOLS}/docker/format-requirements.txt /tmp/modulus-format-requirements.txt
|
||||||
|
RUN python -m pip install --no-cache-dir -r /tmp/modulus-format-requirements.txt
|
||||||
|
|
||||||
WORKDIR /workspace
|
WORKDIR /workspace
|
||||||
|
|
||||||
RUN mkdir -p /opt && touch /opt/modulus-build-image
|
RUN mkdir -p /opt && touch /opt/modulus-build-image
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
clang-format==19.1.7
|
||||||
|
autopep8==2.3.2
|
||||||
|
pycodestyle==2.14.0
|
||||||
|
tree-sitter==0.25.2
|
||||||
|
tree-sitter-cpp==0.23.4
|
||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import modulus_tool
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
modulus_tool.run("format")
|
||||||
+67
-1
@@ -4,6 +4,71 @@ local function within(path, root)
|
|||||||
return path == root or path:sub(1, #root + 1) == root .. "/"
|
return path == root or path:sub(1, #root + 1) == root .. "/"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function setup_formatting(state)
|
||||||
|
if state.formatting then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local ok, conform = pcall(require, "conform")
|
||||||
|
if not ok then
|
||||||
|
vim.notify("Modulus formatting requires conform.nvim", vim.log.levels.WARN)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
state.formatting = true
|
||||||
|
local function project(buf)
|
||||||
|
local ft = vim.bo[buf].filetype
|
||||||
|
if ft ~= "c" and ft ~= "cpp" and ft ~= "python" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return state.project(vim.api.nvim_buf_get_name(buf))
|
||||||
|
end
|
||||||
|
local fallback = conform.formatters_by_ft["_"]
|
||||||
|
for _, ft in ipairs({ "c", "cpp", "python", "*" }) do
|
||||||
|
local original = conform.formatters_by_ft[ft]
|
||||||
|
if ft ~= "*" and (not original or (type(original) == "table" and vim.tbl_isempty(original))) then
|
||||||
|
original = fallback
|
||||||
|
end
|
||||||
|
conform.formatters_by_ft[ft] = function(buf)
|
||||||
|
if project(buf) then
|
||||||
|
if ft == "*" then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
return { "modulus", lsp_format = "never", timeout_ms = 10000 }
|
||||||
|
end
|
||||||
|
if type(original) == "function" then
|
||||||
|
return original(buf) or {}
|
||||||
|
end
|
||||||
|
return original or {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
conform.formatters.modulus = {
|
||||||
|
command = "python3",
|
||||||
|
env = { MODULUS_DIRECT = "0" },
|
||||||
|
args = function(_, context)
|
||||||
|
local root = assert(project(context.buf))
|
||||||
|
local args = vim.uv.fs_stat(root .. "/modulus/cli.py")
|
||||||
|
and { root .. "/tools/format.py" }
|
||||||
|
or { root .. "/modulus.py", "format" }
|
||||||
|
vim.list_extend(args, { "--stdin-filename", context.filename })
|
||||||
|
return args
|
||||||
|
end,
|
||||||
|
cwd = function(_, context) return project(context.buf) end,
|
||||||
|
stdin = true,
|
||||||
|
}
|
||||||
|
-- LazyVim already owns save/manual formatting and its global/buffer toggles.
|
||||||
|
local has_save, save_hooks = pcall(vim.api.nvim_get_autocmds, { event = "BufWritePre", group = "Conform" })
|
||||||
|
if not rawget(_G, "LazyVim") and (not has_save or #save_hooks == 0) then
|
||||||
|
vim.api.nvim_create_autocmd("BufWritePre", {
|
||||||
|
group = vim.api.nvim_create_augroup("ModulusFormat", { clear = true }),
|
||||||
|
callback = function(event)
|
||||||
|
if project(event.buf) and vim.g.autoformat ~= false and vim.b[event.buf].autoformat ~= false
|
||||||
|
and not vim.g.disable_autoformat and not vim.b[event.buf].disable_autoformat then
|
||||||
|
conform.format({ bufnr = event.buf, lsp_format = "never", timeout_ms = 10000 })
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function M.setup(root)
|
function M.setup(root)
|
||||||
if vim.fn.has("nvim-0.11") ~= 1 then
|
if vim.fn.has("nvim-0.11") ~= 1 then
|
||||||
vim.notify("Modulus container clangd requires Neovim 0.11+", vim.log.levels.ERROR)
|
vim.notify("Modulus container clangd requires Neovim 0.11+", vim.log.levels.ERROR)
|
||||||
@@ -13,7 +78,7 @@ function M.setup(root)
|
|||||||
-- Finish LazyVim's server setup before changing only the command and root routing.
|
-- Finish LazyVim's server setup before changing only the command and root routing.
|
||||||
local has_lazy, lazy = pcall(require, "lazy")
|
local has_lazy, lazy = pcall(require, "lazy")
|
||||||
if has_lazy then
|
if has_lazy then
|
||||||
lazy.load({ plugins = { "nvim-lspconfig" } })
|
lazy.load({ plugins = { "nvim-lspconfig", "conform.nvim" } })
|
||||||
end
|
end
|
||||||
local state = rawget(_G, "modulus_clangd")
|
local state = rawget(_G, "modulus_clangd")
|
||||||
if not state then
|
if not state then
|
||||||
@@ -85,6 +150,7 @@ function M.setup(root)
|
|||||||
command = { "python3", root .. "/modulus.py", "clangd" }
|
command = { "python3", root .. "/modulus.py", "clangd" }
|
||||||
end
|
end
|
||||||
state.roots[root] = command
|
state.roots[root] = command
|
||||||
|
setup_formatting(state)
|
||||||
for _, client in ipairs(vim.lsp.get_clients({ name = "clangd" })) do
|
for _, client in ipairs(vim.lsp.get_clients({ name = "clangd" })) do
|
||||||
if not client.config._modulus_container and client.config.root_dir and within(client.config.root_dir, root) then
|
if not client.config._modulus_container and client.config.root_dir and within(client.config.root_dir, root) then
|
||||||
client:stop()
|
client:stop()
|
||||||
|
|||||||
Reference in New Issue
Block a user