diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..3e848b4 --- /dev/null +++ b/.clang-format @@ -0,0 +1,11 @@ +BasedOnStyle: LLVM +BreakBeforeBraces: Custom +BraceWrapping: + AfterFunction: true +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 1 +BreakConstructorInitializers: BeforeComma +ColumnLimit: 100 +IndentWidth: 4 +AlwaysBreakTemplateDeclarations: Yes + diff --git a/.cmake-format.json b/.cmake-format.json new file mode 100644 index 0000000..ea6b420 --- /dev/null +++ b/.cmake-format.json @@ -0,0 +1,9 @@ +{ + "line_width": 100, + "tab_size": 4, + "max_pargs_hwrap": 4, + "max_subargs_per_line": 4, + "separate_ctrl_name_with_space": false, + "separate_fn_name_with_space": false, + "dangle_parens": false +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7ca00be --- /dev/null +++ b/.editorconfig @@ -0,0 +1,25 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{c,cc,cpp,cxx,h,hh,hpp,hxx,ipp,tpp}] +indent_style = space +indent_size = 4 +tab_width = 4 +max_line_length = 100 + +[CMakeLists.txt] +indent_style = tab +tab_width = 4 + +[*.cmake] +indent_style = tab +tab_width = 4 + +[*.{json,jsonc}] +indent_style = space +indent_size = 2 diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml new file mode 100644 index 0000000..f1678a2 --- /dev/null +++ b/.github/workflows/ci-build.yml @@ -0,0 +1,41 @@ +name: ci-build + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install --yes \ + clang-format \ + libgtest-dev \ + ninja-build \ + pipx + pipx install cmakelang + pipx install cmakelint + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + - name: Check C++ formatting + run: ./scripts/lint.sh cpp-format + + - name: Check CMake formatting + run: ./scripts/lint.sh cmake-format + + - name: Lint CMake files + run: ./scripts/lint.sh cmake-lint + + - name: Configure, build, and test + working-directory: ctprintf + run: cmake --workflow --preset debug-workflow diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..51b351d --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,15 @@ +{ + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/ctprintf/include" + ], + "cppStandard": "c++20", + "intelliSenseMode": "linux-gcc-x64", + "compilerPath": "/usr/bin/c++", + "cStandard": "gnu17" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..563496d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "ms-vscode.cpptools", + "ms-vscode.cmake-tools", + "cheshirekow.cmake-format", + "sonarsource.sonarlint-vscode" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..77034ab --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,28 @@ +{ + "cmake.useCMakePresets": "always", + "cmake.options.statusBarVisibility": "compact", + "cmake.options.advanced": { + "ctest": { + "statusBarVisibility": "hidden" + }, + "testPreset": { + "statusBarVisibility": "hidden" + } + }, + "cmake.sourceDirectory": "${workspaceFolder}/ctprintf", + "cmake.configureOnOpen": false, + "cmake.configureOnEdit": false, + "cmake.loadCompileCommands": false, + "cmake.skipConfigureIfCachePresent": true, + "cmake.parallelJobs": 0, + "cmake.copyCompileCommands": "${workspaceFolder}/ctprintf/build/debug/compile_commands.json", + "sonarlint.pathToCompileCommands": "${workspaceFolder}/ctprintf/build/debug/compile_commands.json", + "[cmake]": { + "editor.defaultFormatter": "cheshirekow.cmake-format", + "editor.formatOnSave": true, + "editor.tabSize": 4, + "editor.insertSpaces": true + }, + "cmake-format.configFile": "${workspaceFolder}/.cmake-format.json", + "cmake-format.executable": "cmake-format" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..9c95a99 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,84 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "configure", + "type": "cmake", + "command": "configure", + "preset": "${input:buildType}", + "problemMatcher": [] + }, + { + "label": "build", + "type": "cmake", + "command": "build", + "targets": [ + "all" + ], + "preset": "${input:buildType}", + "group": "build", + "problemMatcher": "$gcc", + "dependsOn": [ + "configure" + ] + }, + { + "label": "clean", + "type": "cmake", + "command": "clean", + "preset": "${input:buildType}", + "problemMatcher": [] + }, + { + "label": "Run CMake workflow", + "type": "shell", + "command": "cmake", + "args": [ + "--workflow", + "--fresh", + "--preset", + "${input:buildType}-workflow" + ], + "options": { + "cwd": "${workspaceFolder}/ctprintf" + }, + "problemMatcher": [], + "group": "test", + "presentation": { + "reveal": "always", + "echo": true, + "focus": true, + "panel": "dedicated", + "clear": true + } + }, + { + "label": "Lint", + "type": "shell", + "command": "./scripts/lint.sh", + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [], + "presentation": { + "reveal": "always", + "echo": true, + "focus": true, + "panel": "dedicated", + "clear": true + } + } + ], + "inputs": [ + { + "id": "buildType", + "type": "pickString", + "description": "Select the CMake workflow preset", + "options": [ + "debug", + "release" + ], + "default": "debug" + }, + ] +} diff --git a/ctprintf/CMakeLists.txt b/ctprintf/CMakeLists.txt new file mode 100644 index 0000000..7e811a7 --- /dev/null +++ b/ctprintf/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.25) +project(ctprintf LANGUAGES CXX) + +option(ENABLE_TESTING "Build and enable tests" OFF) + +add_library(ctprintf INTERFACE) + +add_library(ctprintf::ctprintf ALIAS ctprintf) + +target_sources( + ctprintf + INTERFACE include/ctprintf/format.hpp + include/ctprintf/output.hpp + include/ctprintf/types.hpp + include/ctprintf/detail/formatter.hpp + include/ctprintf/detail/parser.hpp) + +target_include_directories(ctprintf INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include) + +target_compile_features(ctprintf INTERFACE cxx_std_20) + +if(ENABLE_TESTING) + include(CTest) + enable_testing() + add_subdirectory(tests) +endif() # ENABLE_TESTING diff --git a/ctprintf/CMakePresets.json b/ctprintf/CMakePresets.json new file mode 100644 index 0000000..143991a --- /dev/null +++ b/ctprintf/CMakePresets.json @@ -0,0 +1,96 @@ +{ + "version": 6, + "cmakeMinimumRequired": { + "major": 3, + "minor": 25, + "patch": 0 + }, + "configurePresets": [ + { + "name": "debug", + "displayName": "Debug", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "ENABLE_TESTING": "ON", + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "release", + "displayName": "Release", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "ENABLE_TESTING": "ON", + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + } + ], + "buildPresets": [ + { + "name": "debug", + "configurePreset": "debug" + }, + { + "name": "release", + "configurePreset": "release" + } + ], + "testPresets": [ + { + "name": "debug", + "configurePreset": "debug", + "output": { + "outputOnFailure": true + } + }, + { + "name": "release", + "configurePreset": "release", + "output": { + "outputOnFailure": true + } + } + ], + "workflowPresets": [ + { + "name": "debug-workflow", + "displayName": "Debug: configure, build, and test", + "steps": [ + { + "type": "configure", + "name": "debug" + }, + { + "type": "build", + "name": "debug" + }, + { + "type": "test", + "name": "debug" + } + ] + }, + { + "name": "release-workflow", + "displayName": "Release: configure, build, and test", + "steps": [ + { + "type": "configure", + "name": "release" + }, + { + "type": "build", + "name": "release" + }, + { + "type": "test", + "name": "release" + } + ] + } + ] +} diff --git a/ctprintf/include/ctprintf/detail/formatter.hpp b/ctprintf/include/ctprintf/detail/formatter.hpp new file mode 100644 index 0000000..b2dd408 --- /dev/null +++ b/ctprintf/include/ctprintf/detail/formatter.hpp @@ -0,0 +1,219 @@ +#pragma once + +#include "ctprintf/detail/parser.hpp" +#include "ctprintf/output.hpp" +#include "ctprintf/types.hpp" + +#include +#include +#include +#include + +namespace ctprintf::detail { + +template +void put_repeat(O &output, char character, int count) +{ + for (int index = 0; index < count; ++index) + output.put(character); +} + +template +void write_string(O &output, const char *value, const format_spec &spec) +{ + if (value == nullptr) + value = "(null)"; + + std::size_t length = 0; + while (value[length] != '\0') + ++length; + + const int padding = + spec.width > static_cast(length) ? spec.width - static_cast(length) : 0; + if (!spec.left) + put_repeat(output, ' ', padding); + for (std::size_t index = 0; index < length; ++index) + output.put(value[index]); + if (spec.left) + put_repeat(output, ' ', padding); +} + +constexpr unsigned int base_of(conversion type) +{ + switch (type) { + case conversion::octal: + return 8; + case conversion::hexadecimal: + case conversion::hexadecimal_upper: + case conversion::pointer: + return 16; + default: + return 10; + } +} + +constexpr char sign_of(bool negative, const format_spec &spec) +{ + if (negative) + return '-'; + if (spec.type != conversion::signed_decimal) + return '\0'; + if (spec.plus) + return '+'; + return spec.space ? ' ' : '\0'; +} + +struct number_prefix { + std::array chars{}; + int length = 0; +}; + +constexpr number_prefix prefix_of(const format_spec &spec, bool nonzero, char first_digit) +{ + number_prefix prefix; + const bool is_hexadecimal = + spec.type == conversion::hexadecimal || spec.type == conversion::hexadecimal_upper; + if (spec.alternate && spec.type == conversion::octal && first_digit != '0') + prefix.chars[prefix.length++] = '0'; + if (spec.type == conversion::pointer || (spec.alternate && nonzero && is_hexadecimal)) { + prefix.chars[prefix.length++] = '0'; + prefix.chars[prefix.length++] = spec.type == conversion::hexadecimal_upper ? 'X' : 'x'; + } + return prefix; +} + +template +void write_character(O &output, char value, const format_spec &spec) +{ + const int padding = spec.width > 1 ? spec.width - 1 : 0; + if (!spec.left) + put_repeat(output, ' ', padding); + output.put(value); + if (spec.left) + put_repeat(output, ' ', padding); +} + +template +void write_integer(O &output, T value, const format_spec &spec) +{ + using value_type = std::remove_cvref_t; + using unsigned_type = std::make_unsigned_t; + + auto magnitude = static_cast(value); + bool negative = false; + if constexpr (std::is_signed_v) { + if (spec.type == conversion::signed_decimal && value < 0) { + negative = true; + magnitude = unsigned_type{0} - magnitude; + } + } + + const unsigned int base = base_of(spec.type); + + const char *digits = + spec.type == conversion::hexadecimal_upper ? "0123456789ABCDEF" : "0123456789abcdef"; + const bool nonzero = magnitude != 0; + std::array buffer{}; + int digit_count = 0; + do { + buffer[digit_count++] = digits[magnitude % base]; + magnitude /= base; + } while (magnitude != 0); + + const char sign = sign_of(negative, spec); + const number_prefix prefix = prefix_of(spec, nonzero, buffer[digit_count - 1]); + + const int sign_length = sign == '\0' ? 0 : 1; + const int content_length = sign_length + prefix.length + digit_count; + const int padding = spec.width > content_length ? spec.width - content_length : 0; + const bool zero_padding = spec.zero && !spec.left; + + if (!spec.left && !zero_padding) + put_repeat(output, ' ', padding); + if (sign != '\0') + output.put(sign); + for (int index = 0; index < prefix.length; ++index) + output.put(prefix.chars[index]); + if (zero_padding) + put_repeat(output, '0', padding); + while (digit_count > 0) + output.put(buffer[--digit_count]); + if (spec.left) + put_repeat(output, ' ', padding); +} + +template +void write_value(O &output, const format_spec &spec, const T &value) +{ + using value_type = std::remove_cvref_t; + + if constexpr (std::is_integral_v) { + if constexpr (std::is_same_v) { + write_character(output, static_cast(value), spec); + } else if (spec.type == conversion::character) { + write_character(output, static_cast(value), spec); + } else { + write_integer(output, value, spec); + } + } + + if constexpr (std::is_convertible_v) { + if (spec.type == conversion::string) + write_string(output, value, spec); + } + + if constexpr (std::is_pointer_v || std::is_same_v) { + if (spec.type == conversion::pointer) { + std::uintptr_t address = 0; + if constexpr (std::is_pointer_v) + address = reinterpret_cast(value); + + format_spec pointer_spec = spec; + pointer_spec.type = conversion::pointer; + pointer_spec.alternate = true; + write_integer(output, address, pointer_spec); + } + } +} + +template +bool write_literals_until_specifier(O &output, const char *&cursor) +{ + while (*cursor != '\0') { + if (*cursor != '%') { + output.put(*cursor++); + continue; + } + + ++cursor; + if (*cursor == '%') { + output.put('%'); + ++cursor; + continue; + } + + return true; + } + + return false; +} + +template +void write_formatted_arguments(O &output, const char *&cursor) +{ + while (write_literals_until_specifier(output, cursor)) + static_cast(parse_spec(cursor)); +} + +template +void write_formatted_arguments(O &output, const char *&cursor, const First &first, Rest &&...rest) +{ + if (!write_literals_until_specifier(output, cursor)) + return; + + const parsed_spec parsed = parse_spec(cursor); + write_value(output, parsed.spec, first); + write_formatted_arguments(output, cursor, static_cast(rest)...); +} + +} // namespace ctprintf::detail diff --git a/ctprintf/include/ctprintf/detail/parser.hpp b/ctprintf/include/ctprintf/detail/parser.hpp new file mode 100644 index 0000000..e398010 --- /dev/null +++ b/ctprintf/include/ctprintf/detail/parser.hpp @@ -0,0 +1,207 @@ +#pragma once + +#include "ctprintf/types.hpp" + +#include +#include +#include + +namespace ctprintf::detail { + +constexpr conversion parse_conversion(char character) +{ + switch (character) { + case 'c': + return conversion::character; + case 's': + return conversion::string; + case 'd': + case 'i': + return conversion::signed_decimal; + case 'u': + return conversion::unsigned_decimal; + case 'o': + return conversion::octal; + case 'x': + return conversion::hexadecimal; + case 'X': + return conversion::hexadecimal_upper; + case 'p': + return conversion::pointer; + default: + return conversion::character; + } +} + +constexpr bool is_conversion(char character) +{ + switch (character) { + case 'c': + case 's': + case 'd': + case 'i': + case 'u': + case 'o': + case 'x': + case 'X': + case 'p': + return true; + default: + return false; + } +} + +struct parsed_spec { + format_spec spec{}; + bool valid = true; +}; + +constexpr parsed_spec parse_spec(const char *&cursor) +{ + parsed_spec parsed{}; + format_spec &result = parsed.spec; + + bool parsing_flags = true; + while (parsing_flags) { + switch (*cursor) { + case '-': + result.left = true; + ++cursor; + break; + case '+': + result.plus = true; + ++cursor; + break; + case ' ': + result.space = true; + ++cursor; + break; + case '#': + result.alternate = true; + ++cursor; + break; + case '0': + result.zero = true; + ++cursor; + break; + default: + parsing_flags = false; + break; + } + } + + if (*cursor >= '0' && *cursor <= '9') { + result.width = 0; + while (*cursor >= '0' && *cursor <= '9') { + if (const int digit = *cursor - '0'; result.width > (INT_MAX - digit) / 10) { + parsed.valid = false; + result.width = INT_MAX; + } else if (result.width != INT_MAX) { + result.width = result.width * 10 + digit; + } + ++cursor; + } + } + + if (*cursor == '\0') { + parsed.valid = false; + return parsed; + } + + if (!is_conversion(*cursor)) + parsed.valid = false; + + result.type = parse_conversion(*cursor); + ++cursor; + return parsed; +} + +void invalid_format(); + +template +constexpr bool argument_matches(conversion type) +{ + using value_type = std::remove_cvref_t; + + switch (type) { + case conversion::signed_decimal: + return std::is_integral_v && std::is_signed_v; + case conversion::unsigned_decimal: + case conversion::octal: + case conversion::hexadecimal: + case conversion::hexadecimal_upper: + return std::is_integral_v && !std::is_same_v && + !std::is_signed_v; + case conversion::character: + return std::is_integral_v; + case conversion::string: + return std::is_convertible_v; + case conversion::pointer: + return (std::is_pointer_v && + (std::is_object_v> || + std::is_void_v>)) || + std::is_same_v; + case conversion::percent: + return false; + } + + return false; +} + +template +consteval void validate_format(const char *format) +{ + constexpr std::size_t argument_count = sizeof...(Args); + std::size_t argument_index = 0; + + while (*format != '\0') { + if (*format++ != '%') + continue; + + if (*format == '%') { + ++format; + continue; + } + + const parsed_spec parsed = parse_spec(format); + if (!parsed.valid) + invalid_format(); + const format_spec &spec = parsed.spec; + if constexpr (argument_count == 0) { + invalid_format(); + } else { + constexpr std::array matches{ + &argument_matches...}; + if (argument_index >= argument_count) + invalid_format(); + if (!matches[argument_index](spec.type)) + invalid_format(); + } + ++argument_index; + } + + if (argument_index != argument_count) + invalid_format(); +} + +} // namespace ctprintf::detail + +namespace ctprintf { + +template +class format_string { + public: + template + consteval format_string(const char (&text)[N]) + : text_(text) + { + detail::validate_format(text); + } + + [[nodiscard]] constexpr const char *data() const { return text_; } + + private: + const char *text_; +}; + +} // namespace ctprintf diff --git a/ctprintf/include/ctprintf/format.hpp b/ctprintf/include/ctprintf/format.hpp new file mode 100644 index 0000000..4136f2e --- /dev/null +++ b/ctprintf/include/ctprintf/format.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "ctprintf/detail/formatter.hpp" +#include "ctprintf/detail/parser.hpp" + +#include + +namespace ctprintf { + +template +using format_text = format_string...>; + +template +void format(O &output, format_text format_text, Args &&...args) +{ + const char *cursor = format_text.data(); + if constexpr (sizeof...(Args) == 0) + detail::write_formatted_arguments(output, cursor); + else + detail::write_formatted_arguments(output, cursor, static_cast(args)...); +} + +} // namespace ctprintf diff --git a/ctprintf/include/ctprintf/output.hpp b/ctprintf/include/ctprintf/output.hpp new file mode 100644 index 0000000..233b793 --- /dev/null +++ b/ctprintf/include/ctprintf/output.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace ctprintf { + +// clang-format off +template +concept Output = requires(T &output, char character) { + output.put(character); +}; +// clang-format on + +} // namespace ctprintf diff --git a/ctprintf/include/ctprintf/types.hpp b/ctprintf/include/ctprintf/types.hpp new file mode 100644 index 0000000..43d8486 --- /dev/null +++ b/ctprintf/include/ctprintf/types.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include + +namespace ctprintf { + +template +struct fixed_string { + std::array data{}; + + constexpr explicit fixed_string(const char (&text)[N]) + { + for (std::size_t index = 0; index < N; ++index) + data[index] = text[index]; + } + + [[nodiscard]] constexpr std::size_t size() const { return N - 1; } + + [[nodiscard]] constexpr char operator[](std::size_t index) const { return data[index]; } +}; + +enum class conversion { + character, + string, + signed_decimal, + unsigned_decimal, + octal, + hexadecimal, + hexadecimal_upper, + pointer, + percent +}; + +struct format_spec { + conversion type{}; + bool left = false; + bool plus = false; + bool space = false; + bool alternate = false; + bool zero = false; + int width = -1; + int precision = -1; +}; + +} // namespace ctprintf diff --git a/ctprintf/tests/CMakeLists.txt b/ctprintf/tests/CMakeLists.txt new file mode 100644 index 0000000..6a26ddb --- /dev/null +++ b/ctprintf/tests/CMakeLists.txt @@ -0,0 +1,16 @@ +find_package(GTest CONFIG REQUIRED) +include(GoogleTest) +try_compile( + CTPRINTF_ACCEPTS_INVALID_FORMAT ${CMAKE_CURRENT_BINARY_DIR}/compile_fail/type_mismatch + ${CMAKE_CURRENT_SOURCE_DIR}/compile_fail/type_mismatch.cpp + CMAKE_FLAGS -DCMAKE_CXX_STANDARD=20 + OUTPUT_VARIABLE CTPRINTF_INVALID_FORMAT_OUTPUT) +if(CTPRINTF_ACCEPTS_INVALID_FORMAT) + message(FATAL_ERROR "ctprintf accepted an argument with the wrong format type") +endif() + +add_executable(ctprintf_tests format_integer.cpp format_string.cpp) + +target_link_libraries(ctprintf_tests PRIVATE ctprintf::ctprintf GTest::gtest_main) + +gtest_discover_tests(ctprintf_tests) diff --git a/ctprintf/tests/compile_fail/type_mismatch.cpp b/ctprintf/tests/compile_fail/type_mismatch.cpp new file mode 100644 index 0000000..bd77f52 --- /dev/null +++ b/ctprintf/tests/compile_fail/type_mismatch.cpp @@ -0,0 +1,14 @@ +#include "../../include/ctprintf/format.hpp" + +struct Output { + void put(char) const + { + // This fixture only satisfies ctprintf::Output; it is never executed. + } +}; + +int main() +{ + Output output; + ctprintf::format(output, "%08x", "not an integer"); +} diff --git a/ctprintf/tests/format_integer.cpp b/ctprintf/tests/format_integer.cpp new file mode 100644 index 0000000..29297b3 --- /dev/null +++ b/ctprintf/tests/format_integer.cpp @@ -0,0 +1,57 @@ +#include + +#include + +#include + +#include + +namespace { + +struct StringOutput { + std::string buffer; + + void put(char character) { buffer.push_back(character); } +}; + +} // namespace + +TEST(FormatIntegerTest, FormatsZeroPaddedHexadecimal) +{ + StringOutput output; + ctprintf::format(output, "PC=%08x", std::uint32_t{0x2a}); + + EXPECT_EQ(output.buffer, "PC=0000002a"); +} + +TEST(FormatIntegerTest, FormatsAlternateAndLeftAlignedHexadecimal) +{ + StringOutput output; + ctprintf::format(output, "%-#8X", std::uint32_t{0x2a}); + + EXPECT_EQ(output.buffer, "0X2A "); +} + +TEST(FormatIntegerTest, FormatsOctalAndEscapedPercent) +{ + StringOutput output; + ctprintf::format(output, "load=%% %o", 8U); + + EXPECT_EQ(output.buffer, "load=% 10"); +} + +TEST(FormatIntegerTest, FormatsSignedDecimalAndFlags) +{ + StringOutput output; + ctprintf::format(output, "%+06d", -42); + + EXPECT_EQ(output.buffer, "-00042"); +} + +TEST(FormatIntegerTest, FormatsUnsignedDecimal) +{ + StringOutput output; + ctprintf::format(output, "%u", std::uint32_t{4294967295}); + + EXPECT_EQ(output.buffer, "4294967295"); +} diff --git a/ctprintf/tests/format_string.cpp b/ctprintf/tests/format_string.cpp new file mode 100644 index 0000000..e01e6f4 --- /dev/null +++ b/ctprintf/tests/format_string.cpp @@ -0,0 +1,73 @@ +#include + +#include + +#include + +namespace { + +struct StringOutput { + std::string buffer; + + void put(char character) { buffer.push_back(character); } +}; + +} // namespace + +TEST(FormatStringTest, FormatsStringCharacterAndLiteralPercent) +{ + StringOutput output; + ctprintf::format(output, "Hello, %-6s %c %%", "world", '!'); + + EXPECT_EQ(output.buffer, "Hello, world ! %"); +} + +TEST(FormatStringTest, FormatsBooleanAsCharacter) +{ + StringOutput output; + ctprintf::format(output, "%c", true); + + EXPECT_EQ(output.buffer, std::string(1, '\x01')); +} + +TEST(FormatStringTest, WritesLiteralWithoutArguments) +{ + StringOutput output; + ctprintf::format(output, "ready %%"); + + EXPECT_EQ(output.buffer, "ready %"); +} + +TEST(FormatStringTest, FormatsNullString) +{ + StringOutput output; + const char *value = nullptr; + ctprintf::format(output, "%s", value); + + EXPECT_EQ(output.buffer, "(null)"); +} + +TEST(FormatStringTest, FormatsObjectPointer) +{ + StringOutput output; + int value = 0; + ctprintf::format(output, "%p", &value); + + EXPECT_EQ(output.buffer.substr(0, 2), "0x"); +} + +TEST(FormatStringTest, FormatsNullPointer) +{ + StringOutput output; + ctprintf::format(output, "%p", nullptr); + + EXPECT_EQ(output.buffer, "0x0"); +} + +TEST(FormatStringTest, FormatsMultipleArguments) +{ + StringOutput output; + ctprintf::format(output, "%d:%s:%c:%u", -42, "answer", '!', 42U); + + EXPECT_EQ(output.buffer, "-42:answer:!:42"); +} diff --git a/scripts/lint.sh b/scripts/lint.sh new file mode 100755 index 0000000..c162ef2 --- /dev/null +++ b/scripts/lint.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repository_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$repository_root" + +check_cpp_format() { + find ctprintf \ + -path ctprintf/build -prune -o \ + -type f \( -name '*.cpp' -o -name '*.hpp' \) -print0 | + xargs --null --no-run-if-empty \ + clang-format \ + --dry-run \ + --Werror \ + --style=file:.clang-format +} + +check_cmake_format() { + find ctprintf \ + -path ctprintf/build -prune -o \ + -type f -name 'CMakeLists.txt' -print0 | + xargs --null --no-run-if-empty \ + cmake-format \ + --check \ + --config-file .cmake-format.json +} + +check_cmake_lint() { + find ctprintf \ + -path ctprintf/build -prune -o \ + -type f -name 'CMakeLists.txt' -print0 | + xargs --null --no-run-if-empty \ + cmakelint --linelength=100 +} + +usage() { + echo "Usage: $0 [all|cpp-format|cmake-format|cmake-lint]" >&2 +} + +case "${1:-all}" in + all) + check_cpp_format + check_cmake_format + check_cmake_lint + ;; + cpp-format) + check_cpp_format + ;; + cmake-format) + check_cmake_format + ;; + cmake-lint) + check_cmake_lint + ;; + *) + usage + exit 2 + ;; +esac