diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 99196b3..07fb8a9 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -35,35 +35,31 @@ jobs: - name: Checkout code uses: actions/checkout@v6 - - name: Install mise + - name: Install Dependencies run: | curl https://mise.run | sh mise settings experimental=true + mise trust + mise install - - name: Trust workspace - run: mise trust + - name: Configure + run: mise exec -- make configure - - name: Install dependencies - run: mise exec -- mise install + - name: Build + run: mise exec -- make build - - name: Configure project - run: mise exec -- make configure + - name: Test + run: mise exec -- make test - - name: Run formatting checks + - name: Formatting run: mise exec -- make format - - name: Run lint + - name: Linting run: mise exec -- make lint - - name: Run static analysis + - name: Static Analysis run: mise exec -- make check - - name: Build project - run: mise exec -- make build - - - name: Run tests - run: mise exec -- make test - test-amd64: needs: test runs-on: ubuntu-latest diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index cbd449f..231d558 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -2,8 +2,12 @@ "configurations": [ { "name": "Mac", - "compileCommands": "${workspaceFolder}/build/compile_commands.json", + "includePath": ["${workspaceFolder}/**"], + "macFrameworkPath": [ + "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks" + ], "compilerPath": "/usr/bin/clang", + "compileCommands": "${workspaceFolder}/build/compile_commands.json", "cStandard": "c11", "intelliSenseMode": "macos-clang-arm64" } diff --git a/.vscode/settings.json b/.vscode/settings.json index f0b5828..d66d7a0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,12 +19,15 @@ }, "cSpell.words": [ "armv", + "BINDIR", "CMSIS", "cppcheck", "ctest", "Dryrun", "eabi", "endforeach", + "endfunction", + "INCLUDEDIR", "libnewlib", "noninteractive", "tinyclib", diff --git a/CMakeLists.txt b/CMakeLists.txt index 45352a7..0d13a25 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,24 +3,27 @@ cmake_minimum_required(VERSION 3.25) project(tinyclib VERSION 0.3.1 LANGUAGES C) # Settings and options -option(TL_BUILD_TESTS "Build tinyclib tests" ${PROJECT_IS_TOP_LEVEL}) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin) +option(TL_BUILD_TESTS "Build tinyclib tests" ${PROJECT_IS_TOP_LEVEL}) + +function(tl_configure_c_target target) + set_property(TARGET ${target} PROPERTY C_STANDARD 11) + set_property(TARGET ${target} PROPERTY C_STANDARD_REQUIRED ON) + set_property(TARGET ${target} PROPERTY C_EXTENSIONS OFF) +endfunction() # Dependencies +include(CTest) include(FetchContent) + # Sources file(GLOB SOURCES CONFIGURE_DEPENDS src/*.c) - # Add the library (BUILD_SHARED_LIBS is handled by CMake) add_library(tinyclib ${SOURCES}) - -# Set the C standard -set_property(TARGET tinyclib PROPERTY C_STANDARD 11) -set_property(TARGET tinyclib PROPERTY C_STANDARD_REQUIRED ON) -set_property(TARGET tinyclib PROPERTY C_EXTENSIONS OFF) +tl_configure_c_target(tinyclib) # Set include directories for build and install target_include_directories( @@ -29,6 +32,14 @@ target_include_directories( $ $ ) +set_target_properties(tinyclib PROPERTIES POSITION_INDEPENDENT_CODE ON) # support shared libraries +add_library(tinyclib::tinyclib ALIAS tinyclib) # add a namespace alias for modern linking + +# Tests +if(TL_BUILD_TESTS) + add_subdirectory(third_party/unity) + add_subdirectory(tests) +endif() # Install targets and headers include(GNUInstallDirs) @@ -71,38 +82,3 @@ install( NAMESPACE tinyclib:: DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/tinyclib ) - -# Add a namespace alias for modern linking -add_library(tinyclib::tinyclib ALIAS tinyclib) - -# Support shared libraries -set_target_properties(tinyclib PROPERTIES POSITION_INDEPENDENT_CODE ON) - -# Build tests -if(TL_BUILD_TESTS) - include(CTest) - include(FetchContent) - set(TEST_TARGETS - tl_app_test - tl_config_test - tl_debug_test - tl_error_test - tl_flag_test - tl_test_test - ) - - # FetchContent for Unity testing framework - FetchContent_Declare( - Unity - GIT_REPOSITORY https://github.com/ThrowTheSwitch/Unity.git - GIT_TAG v2.6.1 - ) - FetchContent_MakeAvailable(Unity) - - enable_testing() - foreach(t IN LISTS TEST_TARGETS) - add_executable(${t} tests/unit/${t}.c) - target_link_libraries(${t} unity tinyclib) - add_test(NAME ${t} COMMAND ${t}) - endforeach() -endif() diff --git a/Makefile b/Makefile index 71eff7c..f4f0ba5 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,5 @@ .DEFAULT_GOAL := help -BUILD_DIR := build CLANG_FORMAT := $(shell if command -v clang-format >/dev/null 2>&1; then echo clang-format; fi) CLANG_TIDY := $(shell if command -v clang-tidy >/dev/null 2>&1; then echo clang-tidy; fi) CPPCHECK := $(shell if command -v cppcheck >/dev/null 2>&1; then echo cppcheck; fi) @@ -8,12 +7,16 @@ CLANG_TIDY_EXTRA_ARGS := $(shell if [ "$$(uname)" = "Darwin" ]; then echo "--ext SRC_FILES := src/*.c include/*.h TEST_FILES := tests/unit/*.c -EXAMPLE_FILES := $(wildcard examples/*/*.c) -ALL_FILES := $(SRC_FILES) $(TEST_FILES) $(EXAMPLE_FILES) +CMD_FILES := $(wildcard cmd/*/*.c cmd/*/*.h) +ALL_FILES := $(SRC_FILES) $(TEST_FILES) $(CMD_FILES) PRESET ?= default +JOBS ?= 4 +PROJECT_DIR := $(CURDIR) +BUILD_ROOT := build +PRESET_BUILD_DIR := $(if $(filter default,$(PRESET)),$(BUILD_ROOT),$(BUILD_ROOT)/$(PRESET)) -.PHONY: help install build clean clean-bin test format lint check check-all fix +.PHONY: help configure build clean clean-bin clean-cache test format lint check check-all fix help: ## Show available make targets @echo "Usage: make " @@ -21,24 +24,30 @@ help: ## Show available make targets @echo "Targets:" @awk 'BEGIN {FS = ":.*## "} /^[a-zA-Z_-]+:.*## / {printf " %-15s %s\n", $$1, $$2}' $(MAKEFILE_LIST) -configure: ## Configure cmake +configure: ## Configure cmake (use PRESET=release for release mode) cmake --preset $(PRESET) -build: ## Build the project - @test -d "$(BUILD_DIR)" || cmake --preset $(PRESET) - cmake --build --preset $(PRESET) +build: ## Build the project (use PRESET=release for release mode) + cmake --preset $(PRESET) + cmake --build --preset $(PRESET) -j $(JOBS) + +clean: ## Remove build directories + @test -n "$(PROJECT_DIR)" && [ "$(PROJECT_DIR)" != "/" ] + rm -rf "$(PROJECT_DIR)/$(BUILD_ROOT)" -clean: ## Remove build directory - @test -n "$(CURDIR)" && [ "$(CURDIR)" != "/" ] - rm -rf "$(CURDIR)/$(BUILD_DIR)" +clean-bin: ## Remove built binaries for the selected preset + @test -n "$(PROJECT_DIR)" && [ "$(PROJECT_DIR)" != "/" ] + @test -d "$(PROJECT_DIR)/$(PRESET_BUILD_DIR)/cmd/bin" || exit 0 + find "$(PROJECT_DIR)/$(PRESET_BUILD_DIR)/cmd/bin" -mindepth 1 -delete -clean-bin: ## Remove built binaries from the build directory - @test -n "$(CURDIR)" && [ "$(CURDIR)" != "/" ] - @test -d "$(CURDIR)/$(BUILD_DIR)/bin" || exit 0 - find "$(CURDIR)/$(BUILD_DIR)/bin" -mindepth 1 -delete +clean-cache: ## Remove CMake cache for the selected preset + @test -n "$(PROJECT_DIR)" && [ "$(PROJECT_DIR)" != "/" ] + rm -f "$(PROJECT_DIR)/$(PRESET_BUILD_DIR)/CMakeCache.txt" test: ## Run tests - ctest --preset default + @test -f "$(PROJECT_DIR)/$(PRESET_BUILD_DIR)/CMakeCache.txt" || cmake --preset $(PRESET) + cmake --build --preset $(PRESET) -j $(JOBS) + ctest --preset $(PRESET) format: ## Check code formatting @test -n "$(CLANG_FORMAT)" || { echo "error: clang-format not found"; exit 1; } @@ -46,14 +55,16 @@ format: ## Check code formatting lint: ## Check code linting @test -n "$(CLANG_TIDY)" || { echo "error: clang-tidy not found"; exit 1; } - $(CLANG_TIDY) -p $(BUILD_DIR) $(CLANG_TIDY_EXTRA_ARGS) \ - --header-filter="^$(CURDIR)/(src|include|tests)/" src/*.c tests/unit/*.c + @test -f "$(PRESET_BUILD_DIR)/compile_commands.json" || cmake --preset default + $(CLANG_TIDY) --config-file=$(PROJECT_DIR)/.clang-tidy -p $(PRESET_BUILD_DIR) $(CLANG_TIDY_EXTRA_ARGS) \ + --header-filter="^$(PROJECT_DIR)/(src|include|tests)/" src/*.c tests/unit/*.c check: ## Static analysis @test -n "$(CPPCHECK)" || { echo "error: cppcheck not found"; exit 1; } + @test -f "$(PRESET_BUILD_DIR)/compile_commands.json" || cmake --preset default $(CPPCHECK) --enable=warning,style,performance,portability --error-exitcode=1 \ - --check-level=exhaustive --project=$(BUILD_DIR)/compile_commands.json \ - --suppress=missingIncludeSystem -i$(BUILD_DIR) + --check-level=exhaustive --project=$(PRESET_BUILD_DIR)/compile_commands.json \ + --suppress=missingIncludeSystem -i$(PRESET_BUILD_DIR) check-all: test format lint check ## Run all checks diff --git a/include/tl_flag.h b/include/tl_flag.h index e463d5a..e752d0d 100644 --- a/include/tl_flag.h +++ b/include/tl_flag.h @@ -123,6 +123,15 @@ size_t tl_count_flag(const char *flag); */ const char *tl_get_flag_at(const char *flag, size_t index); +/** + * @brief Looks up a specific positional argument by value. + * + * @param value The positional value to look up. + * + * @return true if the positional is found, false otherwise. + */ +bool tl_lookup_positional(const char *value); + /** * @brief Returns the number of positional arguments. * diff --git a/src/tl_flag.c b/src/tl_flag.c index 9165f46..2cc9038 100644 --- a/src/tl_flag.c +++ b/src/tl_flag.c @@ -291,6 +291,18 @@ const char *tl_get_flag_at(const char *flag, size_t index) { return NULL; } +bool tl_lookup_positional(const char *value) { + if (!value) { + return false; + } + for (size_t i = 0; i < positional_count; i++) { + if (strcmp(positionals[i], value) == 0) { + return true; + } + } + return false; +} + size_t tl_count_positional(void) { return positional_count; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..805f892 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,20 @@ +set(TEST_BIN_DIR "${PROJECT_BINARY_DIR}/tests/bin") + +set(UNIT_TEST_TARGETS + app_test + config_test + debug_test + error_test + flag_test + test_test +) + +foreach(test_target IN LISTS UNIT_TEST_TARGETS) + string(REGEX REPLACE "_test$" "" test_name "${test_target}") + set(test_binary "tl_${test_name}") + add_executable(${test_binary} unit/${test_target}.c) + target_link_libraries(${test_binary} PRIVATE tinyclib unity) + target_include_directories(${test_binary} PRIVATE ${PROJECT_SOURCE_DIR}/src) + set_target_properties(${test_binary} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${TEST_BIN_DIR}) + add_test(NAME ${test_binary} COMMAND ${test_binary}) +endforeach() diff --git a/tests/unit/tl_app_test.c b/tests/unit/app_test.c similarity index 100% rename from tests/unit/tl_app_test.c rename to tests/unit/app_test.c diff --git a/tests/unit/tl_config_test.c b/tests/unit/config_test.c similarity index 100% rename from tests/unit/tl_config_test.c rename to tests/unit/config_test.c diff --git a/tests/unit/tl_debug_test.c b/tests/unit/debug_test.c similarity index 100% rename from tests/unit/tl_debug_test.c rename to tests/unit/debug_test.c diff --git a/tests/unit/tl_error_test.c b/tests/unit/error_test.c similarity index 100% rename from tests/unit/tl_error_test.c rename to tests/unit/error_test.c diff --git a/tests/unit/tl_flag_test.c b/tests/unit/flag_test.c similarity index 92% rename from tests/unit/tl_flag_test.c rename to tests/unit/flag_test.c index 31dbf71..112f4de 100644 --- a/tests/unit/tl_flag_test.c +++ b/tests/unit/flag_test.c @@ -468,6 +468,42 @@ static void test_tl_parse_args_null_argv(void) { TEST_ASSERT_FALSE(tl_lookup_flag("--anything")); } +static void test_tl_lookup_positional(void) { + char *argv[] = {"program", "serve", "-f", "file"}; + tl_parse_args(4, argv); + TEST_ASSERT_TRUE(tl_lookup_positional("serve")); + TEST_ASSERT_FALSE(tl_lookup_positional("missing")); +} + +static void test_tl_lookup_positional_multiple(void) { + char *argv[] = {"program", "remote", "add", "origin", "--verbose"}; + tl_parse_args(5, argv); + TEST_ASSERT_TRUE(tl_lookup_positional("remote")); + TEST_ASSERT_TRUE(tl_lookup_positional("add")); + TEST_ASSERT_TRUE(tl_lookup_positional("origin")); + TEST_ASSERT_FALSE(tl_lookup_positional("--verbose")); +} + +static void test_tl_lookup_positional_after_terminator(void) { + char *argv[] = {"program", "cmd", "--", "--foo", "bar"}; + tl_parse_args(5, argv); + TEST_ASSERT_TRUE(tl_lookup_positional("cmd")); + TEST_ASSERT_TRUE(tl_lookup_positional("--foo")); + TEST_ASSERT_TRUE(tl_lookup_positional("bar")); +} + +static void test_tl_lookup_positional_none(void) { + char *argv[] = {"program", "--flag"}; + tl_parse_args(2, argv); + TEST_ASSERT_FALSE(tl_lookup_positional("anything")); +} + +static void test_tl_lookup_positional_null(void) { + char *argv[] = {"program", "cmd"}; + tl_parse_args(2, argv); + TEST_ASSERT_FALSE(tl_lookup_positional(NULL)); +} + int main(void) { UNITY_BEGIN(); @@ -528,6 +564,11 @@ int main(void) { RUN_TEST(test_tl_parse_line_double_backslash); RUN_TEST(test_tl_negative_number_value); RUN_TEST(test_tl_parse_args_null_argv); + RUN_TEST(test_tl_lookup_positional); + RUN_TEST(test_tl_lookup_positional_multiple); + RUN_TEST(test_tl_lookup_positional_after_terminator); + RUN_TEST(test_tl_lookup_positional_none); + RUN_TEST(test_tl_lookup_positional_null); return UNITY_END(); } diff --git a/tests/unit/tl_test_test.c b/tests/unit/test_test.c similarity index 100% rename from tests/unit/tl_test_test.c rename to tests/unit/test_test.c diff --git a/third_party/unity/CMakeLists.txt b/third_party/unity/CMakeLists.txt new file mode 100644 index 0000000..9fbacec --- /dev/null +++ b/third_party/unity/CMakeLists.txt @@ -0,0 +1,8 @@ +include(FetchContent) + +FetchContent_Declare( + Unity + GIT_REPOSITORY https://github.com/ThrowTheSwitch/Unity.git + GIT_TAG cbcd08fa7de711053a3deec6339ee89cad5d2697 # v2.6.1 +) +FetchContent_MakeAvailable(Unity)