diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e35ee3f..6c2e34b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -5,9 +5,11 @@ on: push: branches: - "master" + - "develop" pull_request: branches: - "master" + - "develop" # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -27,17 +29,17 @@ jobs: # # To add more build types (Release, Debug, RelWithDebInfo, etc.) customize the build_type list. matrix: - os: [ ubuntu-latest, windows-latest ] + os: [ ubuntu-24.04, windows-latest ] build_type: [ Release ] c_compiler: [ gcc, clang, cl ] include: - os: windows-latest c_compiler: cl cpp_compiler: cl - - os: ubuntu-latest + - os: ubuntu-24.04 c_compiler: gcc cpp_compiler: g++ - - os: ubuntu-latest + - os: ubuntu-24.04 c_compiler: clang cpp_compiler: clang++ exclude: @@ -45,29 +47,29 @@ jobs: c_compiler: gcc - os: windows-latest c_compiler: clang - - os: ubuntu-latest + - os: ubuntu-24.04 c_compiler: cl steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: 🔧 Install GCC uses: egor-tensin/setup-gcc@v1.3 - if: matrix.os == 'ubuntu-latest' && matrix.c_compiler == 'gcc' + if: matrix.os == 'ubuntu-24.04' && matrix.c_compiler == 'gcc' with: version: 13 - name: 🔧 Install Clang uses: egor-tensin/setup-clang@v1.4 - if: matrix.os == 'ubuntu-latest' && matrix.c_compiler == 'clang' + if: matrix.os == 'ubuntu-24.04' && matrix.c_compiler == 'clang' with: version: 16 - name: 🔧 Setup python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: '3.10' - cache: pip + python-version: '3.13' + cache: 'pip' - name: ☁️ Install required python packages run: pip install -r requirements.txt @@ -76,27 +78,25 @@ jobs: # Turn repeated input strings (such as the build output directory) into step outputs. These step outputs can be used throughout the workflow file. id: strings shell: bash - run: echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" + run: | + echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" + if [ "${{ matrix.os }}" == "windows-latest" ]; then + echo "preset-name=conan-default" >> "$GITHUB_OUTPUT" + else + echo "preset-name=conan-release" >> "$GITHUB_OUTPUT" + fi - name: 🐸 Create default Conan profile run: conan profile detect - name: ☁️ Get dependencies - run: conan install ${{ github.workspace }} --build=missing --output-folder=build --settings compiler.cppstd=20 + run: conan install ${{ github.workspace }} --build=missing -s compiler.cppstd=20 -o testing=True - name: 🛠️ Configure CMake - run: > - cmake -B ${{ steps.strings.outputs.build-output-dir }} - -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} - -DCMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }} - -DCMAKE_C_COMPILER=${{ matrix.c_compiler }} - -DBUILD_TESTING=ON - --toolchain=conan_toolchain.cmake - -S ${{ github.workspace }} + run: cmake --preset ${{ steps.strings.outputs.preset-name }} - name: 🔨 Build project - run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} --parallel + run: cmake --build --preset conan-release --parallel - name: 🏃 Run test suite - working-directory: build - run: ctest --build-config ${{ matrix.build_type }} + run: ctest --preset conan-release diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml new file mode 100644 index 0000000..4841c68 --- /dev/null +++ b/.github/workflows/coverage.yaml @@ -0,0 +1,61 @@ +name: Coverage + +on: + # Triggers the workflow on push or pull request events but only for the "master" branch + push: + branches: + - "master" + - "develop" + pull_request: + branches: + - "master" + - "develop" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: 🔧 Install GCC + uses: egor-tensin/setup-gcc@v1.3 + with: + version: 13 + + - name: 🔧 Setup python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + cache: pip + + - name: ☁️ Install required packages + run: | + sudo apt-get install -y lcov + pip install -r requirements.txt + + - name: 🐸 Create default Conan profile + run: conan profile detect + + - name: ☁️ Get dependencies + run: conan install ${{ github.workspace }} --build=missing -s compiler.cppstd=20 -o testing=True -o coverage=True + + - name: 🛠️ Configure CMake + run: cmake --preset conan-release + + - name: 🔨 Build project + run: cmake --build --preset conan-release --parallel + + - name: 🏃 Run test suite + run: ctest --preset conan-release + + - name: 📊 Generate coverage reports with lcov + run: | + lcov --directory . --capture --output-file coverage.info --gcov-tool gcov-13 + lcov --remove coverage.info '/usr/*' --remove coverage.info '**/.conan*' --remove coverage.info '**/test*' --remove coverage.info '**/test_package*' --output-file coverage.info + lcov --list coverage.info + + - name: ☂️ Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index d785328..6b22081 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ /*build* /doc /include/cppIni/cppini_export.h +test_package/build +python CMakeUserPresets.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 772e787..8d10e36 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,12 +1,14 @@ cmake_minimum_required(VERSION 3.24) -project(cppIni LANGUAGES CXX VERSION 0.1.0) +project(cppIni LANGUAGES CXX VERSION 0.2.0) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -option(BUILD_TESTING ON "Build test files") -option(BUILD_SHARED_LIBS ON "Build shared library files") +option(BUILD_TESTING "Build test files" OFF) +option(BUILD_SHARED_LIBS "Build shared library files" ON) +option(CODE_COVERAGE "Enable coverage reporting" OFF) +include(cmake/CodeCoverage.cmake) add_subdirectory(src) if(BUILD_TESTING) diff --git a/README.md b/README.md index d13d668..5023ecf 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ # cppIni - A C++20 library for reading and writing INI files +Branch | Status | Coverage +--- |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| --- +`master` | [![Build](https://github.com/Master92/cppIni/actions/workflows/build.yaml/badge.svg?branch=master)](https://github.com/Master92/cppIni/actions/workflows/build.yaml) | [![codecov](https://codecov.io/gh/Master92/cppIni/branch/master/graph/badge.svg?token=V66BUECAMV)](https://codecov.io/gh/Master92/cppIni) +`develop` | [![Build](https://github.com/Master92/cppIni/actions/workflows/build.yaml/badge.svg?branch=develop)](https://github.com/Master92/cppIni/actions/workflows/build.yaml) | [![codecov](https://codecov.io/gh/Master92/cppIni/branch/develop/graph/badge.svg?token=V66BUECAMV)](https://codecov.io/gh/Master92/cppIni/tree/develop) + [![Release](https://img.shields.io/github/v/tag/Master92/cppIni?label=release)](https://github.com/Master92/cppIni/releases) -[![Build](https://img.shields.io/github/actions/workflow/status/Master92/cppIni/build.yaml?logo=github)](https://github.com/Master92/cppIni/actions/workflows/build.yaml) ![License](https://img.shields.io/github/license/Master92/cppIni) ![GitHub stars](https://img.shields.io/github/stars/Master92/cppIni?label=%E2%AD%90%20Stars) diff --git a/cmake/CodeCoverage.cmake b/cmake/CodeCoverage.cmake new file mode 100644 index 0000000..65545ed --- /dev/null +++ b/cmake/CodeCoverage.cmake @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.24) + +# Code coverage configuration +add_library(coverage_config INTERFACE) +if(CODE_COVERAGE AND CMAKE_CXX_COMPILER_ID MATCHES "GNU") + message("Enabling code coverage") + target_compile_options(coverage_config INTERFACE + -O0 # no optimization + -g # generate debug info + --coverage # sets all required flags + ) + target_link_options(coverage_config INTERFACE --coverage) +endif() +install(TARGETS coverage_config EXPORT ${PROJECT_NAME}-targets) diff --git a/conanfile.py b/conanfile.py new file mode 100644 index 0000000..8eeedd7 --- /dev/null +++ b/conanfile.py @@ -0,0 +1,105 @@ +# cppIni - A C++20 library for reading and writing INI files +# Copyright (C) 2023-2024 Nils Hofmann +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from conan import ConanFile +from conan.tools.build import check_min_cppstd +from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout, CMakeDeps + + +class cppiniRecipe(ConanFile): + name = "cppini" + version = "0.2.0" + package_type = "library" + + # Optional metadata + license = "GPL-3.0-or-later" + author = "Nils Hofmann " + url = "https://github.com/Master92/cppIni" + description = "A C++20 library for reading and writing INI files" + topics = ("c++20", "configuration", "ini") + + # Binary configuration + settings = "os", "compiler", "build_type", "arch" + options = { + "shared": [True, False], + "fPIC": [True, False], + "testing": [True, False], + "coverage": [True, False] + } + default_options = { + "shared": False, + "fPIC": True, + "testing": True, + "coverage": False + } + + # Sources are located in the same place as this recipe, copy them to the recipe + exports_sources = "CMakeLists.txt", "src/*", "include/*", "cmake/*", "tests/*" + + def build_requirements(self): + self.build_requires("cmake/[>=3.24]") + if self.options.testing: + self.test_requires("doctest/[>=2.4]") + + def config_options(self): + if self.settings.os == "Windows": + self.options.rm_safe("fPIC") + + def configure(self): + if self.options.shared: + self.options.rm_safe("fPIC") + + def layout(self): + cmake_layout(self) + + def validate(self): + if self.settings.compiler.cppstd: + check_min_cppstd(self, "20") + + def generate(self): + deps = CMakeDeps(self) + deps.generate() + tc = CMakeToolchain(self) + tc.variables["BUILD_SHARED_LIBS"] = self.options.shared + tc.variables["BUILD_TESTING"] = self.options.testing + tc.variables["CODE_COVERAGE"] = self.options.coverage + tc.generate() + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + cmake.test() + + def package(self): + cmake = CMake(self) + cmake.install() + + def package_info(self): + self.cpp_info.libs = ["cppini"] diff --git a/conanfile.txt b/conanfile.txt deleted file mode 100644 index cb01832..0000000 --- a/conanfile.txt +++ /dev/null @@ -1,6 +0,0 @@ -[requires] -doctest/[>=2.4] - -[generators] -CMakeDeps -CMakeToolchain \ No newline at end of file diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..9bda608 --- /dev/null +++ b/docs/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +cppini-community@googlegroups.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. \ No newline at end of file diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..6e977bd --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,52 @@ +# Welcome to the Contributing Guide for the cppIni project! + +We are very happy that you are interested in contributing to cppIni. + +This document will provide you with all the information you need to get started. + +## Issues + +If you find a bug, please open an issue on the GitHub issue tracker. Please make sure that there is no existing issue +for your problem/feature request. If there is an existing issue, please add a comment to the existing issue instead of +creating a new one. +If you are providing a bug report, please make sure to include the following information: + +- The version of cppIni you are using +- The compiler you are using +- The operating system you are using +- A minimal example that reproduces the bug +- A description of the expected behavior and the actual behavior + +Feature requests are no bug reports, therefore please don't open an issue for a feature request. Instead, please open a +pull request with your feature. Feature requests that are opened as issues will be closed without further notice. + +## Pull Requests + +If you want to contribute code to cppIni, please open a pull request on GitHub. Please make sure that there is no +existing pull request for your changes. If there is an existing pull request, please add a comment to the existing pull +request instead of creating a new one. Pull requests have to be reviewed by at least one maintainer before they can be +merged. On top of that, all CI builds have to pass. + +Please make sure that your pull request follows the following guidelines: + +- All pull request have to be based on the `develop` branch +- The pull request has a meaningful title and description +- The pull request is linked to an issue (if applicable) +- The code is documented according to the [Doxygen Style Guide](https://www.doxygen.nl/manual/docblocks.html) +- The code is tested +- There is no merge commit from develop to the pull request's branch + - If you want to update your branch, please rebase it on the current develop branch prior to opening the pull + request + - If there is a merge conflict due to another pull request being merged in the meantime, merge commits are permitted + +If your code is not ready to be reviewed yet, please open it as a draft pull request. + +## Code of Conduct + +Please note that this project is released with a Contributor Covenant Code of Conduct. By participating in this project +you agree to abide by its terms. See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for more information. + +## License + +By contributing to cppIni, you agree that your contributions will be licensed under its GPLv3 license. +See [LICENSE](LICENSE) for more information. diff --git a/include/cppIni/File.h b/include/cppIni/File.h index fb08cdf..76565f8 100644 --- a/include/cppIni/File.h +++ b/include/cppIni/File.h @@ -60,6 +60,12 @@ class CPPINI_EXPORT File { std::vector m_sections{}; }; +/// \details Calls findEntry() and returns the value of the Entry if it exists. +/// Otherwise, returns a default-constructed value. +/// \arg section The fully qualified title of the Section to search in. +/// \arg name The name of the Entry to search for. +/// \tparam T The type of the value to return. +/// \returns The value of the Entry if it exists, otherwise a default-constructed value. template auto File::get(std::string_view section, std::string_view name) const -> T { diff --git a/include/cppIni/cppIni_c.h b/include/cppIni/cppIni_c.h new file mode 100644 index 0000000..dbe9a82 --- /dev/null +++ b/include/cppIni/cppIni_c.h @@ -0,0 +1,44 @@ +// cppIni - A C++20 library for reading and writing INI files +// Copyright (C) 2023-2024 Nils Hofmann +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void* pFile; +/// \file cppIni_c.h +/// \brief The C API for cppIni +/// +/// \details +/// This file contains the C API for cppIni. It is a very thin wrapper around +/// the C++ API. It is intended for use with languages that do not support +/// C++. + +CPPINI_EXPORT pFile cppIni_open(const char* filename); ///< Opens a file +CPPINI_EXPORT void cppIni_close(pFile* file); ///< Closes a file +CPPINI_EXPORT void cppIni_set(pFile file, const char* section, const char* key, const char* value); ///< Sets a value +CPPINI_EXPORT const char* cppIni_gets(pFile file, const char* section, const char* key, char* out, size_t outSize); ///< Gets a string +CPPINI_EXPORT int cppIni_geti(pFile file, const char* section, const char* key); ///< Gets an integer +CPPINI_EXPORT float cppIni_getf(pFile file, const char* section, const char* key); ///< Gets a float + +#ifdef __cplusplus +} +#endif diff --git a/src/CInterface.cpp b/src/CInterface.cpp new file mode 100644 index 0000000..8d6db42 --- /dev/null +++ b/src/CInterface.cpp @@ -0,0 +1,84 @@ +// cppIni - A C++20 library for reading and writing INI files +// Copyright (C) 2023-2024 Nils Hofmann +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include + +#include +#include + +/// Opens a file for reading and writing. Throws an exception if the file cannot +/// be opened. +/// +/// \param[in] filename The name of the file to open +/// \return A pointer to a File object +pFile cppIni_open(const char* filename) +{ + return new File(filename); +} + +/// Closes a file that was opened with cppIni_open(). +/// \param[in] file A pointer to a File object +void cppIni_close(pFile* file) +{ + delete static_cast(*file); +} + +/// \param[in] file A pointer to a File object +/// \param[in] section The name of the section to add +/// \param[in] key The name of the key to add +/// \param[in] value The value to add +void cppIni_set(pFile file, const char* section, const char* key, const char* value) +{ + static_cast(file)->set(section, key, value); +} + +/// \param[in] file A pointer to a File object +/// \param[in] section The name of the section to get +/// \param[in] key The name of the key to get +/// \param[out] out A buffer to store the value in +/// \param[in] outSize The size of the buffer +/// \return A pointer to the buffer +const char* cppIni_gets(pFile file, const char* section, const char* key, char* out, size_t outSize) +{ + const auto value = static_cast(file)->get(section, key); + + if (value.empty()) { + return out; + } + + std::strncpy(out, value.data(), outSize); + return out; +} + +/// \see cppIni_gets +/// \param[in] file A pointer to a File object +/// \param[in] section The name of the section to get +/// \param[in] key The name of the key to get +/// \return The value of the key +int cppIni_geti(pFile file, const char* section, const char* key) +{ + return static_cast(file)->get(section, key); +} + +/// \see cppIni_gets +/// \param[in] file A pointer to a File object +/// \param[in] section The name of the section to get +/// \param[in] key The name of the key to get +/// \return The value of the key +float cppIni_getf(pFile file, const char* section, const char* key) +{ + return static_cast(file)->get(section, key); +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 268dd6c..eecc4e7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.24) set(CMAKE_DEBUG_POSTFIX d) set(SOURCES + CInterface.cpp Entry.cpp File.cpp Section.cpp @@ -10,6 +11,7 @@ set(SOURCES set(API_HEADERS cppIni.h + cppIni_c.h Entry.h File.h Section.h @@ -21,6 +23,7 @@ set(PRIVATE_HEADERS ) add_library(${PROJECT_NAME} ${SOURCES} ${API_HEADERS} ${PRIVATE_HEADERS}) +target_link_libraries(${PROJECT_NAME} PUBLIC coverage_config) include(GenerateExportHeader) string(TOLOWER ${PROJECT_NAME} PROJECT_NAME_LOWER) @@ -35,3 +38,24 @@ target_include_directories(${PROJECT_NAME} PRIVATE $ ) + +## Install targets +include(GNUInstallDirs) +set(INSTALL_CONFIGDIR ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}) + +install(EXPORT ${PROJECT_NAME}-targets + FILE ${PROJECT_NAME}Targets.cmake + NAMESPACE ${PROJECT_NAME}:: + DESTINATION ${INSTALL_CONFIGDIR} +) + +install(FILES ${API_HEADERS} ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/${PROJECT_NAME_LOWER}_export.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME} +) + +install(TARGETS ${PROJECT_NAME} + EXPORT ${PROJECT_NAME}-targets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) diff --git a/test_package/CMakeLists.txt b/test_package/CMakeLists.txt new file mode 100644 index 0000000..b981585 --- /dev/null +++ b/test_package/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.15) +project(PackageTest CXX) + +find_package(cppIni CONFIG REQUIRED) + +add_executable(example src/example.cpp) +target_link_libraries(example cppini::cppini) diff --git a/test_package/conanfile.py b/test_package/conanfile.py new file mode 100644 index 0000000..e3cea02 --- /dev/null +++ b/test_package/conanfile.py @@ -0,0 +1,26 @@ +import os + +from conan import ConanFile +from conan.tools.cmake import CMake, cmake_layout +from conan.tools.build import can_run + + +class cppiniTestConan(ConanFile): + settings = "os", "compiler", "build_type", "arch" + generators = "CMakeDeps", "CMakeToolchain" + + def requirements(self): + self.requires(self.tested_reference_str) + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + + def layout(self): + cmake_layout(self) + + def test(self): + if can_run(self): + cmd = os.path.join(self.cpp.build.bindir, "example") + self.run(cmd, env="conanrun") diff --git a/test_package/src/example.cpp b/test_package/src/example.cpp new file mode 100644 index 0000000..6ebd2d7 --- /dev/null +++ b/test_package/src/example.cpp @@ -0,0 +1,26 @@ +/* + * cppIni - A C++20 library for reading and writing INI files + * Copyright (C) 2023 Nils Hofmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +int main() +{ + Section s{"Section1"}; + + return EXIT_SUCCESS; +} diff --git a/tests/CInterfaceTest.cpp b/tests/CInterfaceTest.cpp new file mode 100644 index 0000000..a979e59 --- /dev/null +++ b/tests/CInterfaceTest.cpp @@ -0,0 +1,99 @@ +// cppIni - A C++20 library for reading and writing INI files +// Copyright (C) 2023-2024 Nils Hofmann +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include +#include +#include + +#include +#include + +static const std::string fileName = std::format("{}{}", WORKING_DIR, "/res/test.ini");; + +TEST_SUITE_BEGIN("CInterface"); + +TEST_CASE("Construction of File object") +{ + void* file; + CHECK_NOTHROW(file = cppIni_open(fileName.c_str())); + CHECK_NE(file, nullptr); + CHECK_NOTHROW(cppIni_close(&file)); +} + +struct ScopeGuard +{ + ScopeGuard(pFile file) : file(file){}; + ~ScopeGuard(){ cppIni_close(&file); }; + pFile file; +}; + +TEST_CASE("Read a string entry") +{ + void* file = cppIni_open(fileName.c_str()); + ScopeGuard guard{file}; + + std::array buffer{0}; + cppIni_gets(file, "Section1", "Entry1", buffer.data(), buffer.size()); + + CHECK_EQ(std::string_view{buffer.data()}, "Value1"); +} + +TEST_CASE("Try to read a non-existing entry") +{ + void* file = cppIni_open(fileName.c_str()); + ScopeGuard guard{file}; + + std::array buffer{0}; + CHECK_EQ(cppIni_gets(file, "Section1", "NonExistingEntry", buffer.data(), buffer.size()), buffer.data()); + CHECK_EQ(buffer[0], '\0'); +} + +TEST_CASE("Change a value") +{ + constexpr auto tempFileName = "tmp.ini"; + std::filesystem::copy_file(fileName, tempFileName); + + { + constexpr auto newValue = 1337; + void* file = cppIni_open(tempFileName); + ScopeGuard guard{file}; + + const auto previousValue = cppIni_geti(file, "Section1", "IntEntry"); + CHECK_NE(previousValue, newValue); + cppIni_set(file, "Section1", "IntEntry", std::to_string(newValue).c_str()); + CHECK_EQ(cppIni_geti(file, "Section1", "IntEntry"), newValue); + } + + std::filesystem::remove(tempFileName); +} + +TEST_CASE("Read an integer entry") +{ + void* file = cppIni_open(fileName.c_str()); + ScopeGuard guard{file}; + + CHECK_EQ(cppIni_geti(file, "Section1", "IntEntry"), 42); +} + +TEST_CASE("Read a floating point value entry") +{ + void* file = cppIni_open(fileName.c_str()); + ScopeGuard guard{file}; + + CHECK_LT(std::abs(cppIni_getf(file, "Section1.Subsection1", "DoubleEntry") - 3.1415), 0.001); +} + +TEST_SUITE_END(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ab235f1..f086d2d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,6 +6,7 @@ set(TEST_SOURCES EntryTest.cpp FileTest.cpp SectionTest.cpp + CInterfaceTest.cpp ) add_executable(${PROJECT_NAME}_tests ${TEST_SOURCES}) @@ -29,5 +30,13 @@ if(NOT DOCTEST_CMAKE) message(FATAL_ERROR "Could not find doctest.cmake") else() include(${DOCTEST_CMAKE}) - doctest_discover_tests(${PROJECT_NAME}_tests WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/../src) + set(LIBRARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/../src) + # Fix path for MSVC is located in the respective build type subdirectory + if(MSVC) + set(DOCTEST_CMAKE_DIR "${LIBRARY_DIR}/$") + else() + set(DOCTEST_CMAKE_DIR "${LIBRARY_DIR}") + endif() + + doctest_discover_tests(${PROJECT_NAME}_tests WORKING_DIRECTORY ${DOCTEST_CMAKE_DIR}) endif() diff --git a/tests/EntryTest.cpp b/tests/EntryTest.cpp index 4290a7d..7291e74 100644 --- a/tests/EntryTest.cpp +++ b/tests/EntryTest.cpp @@ -30,6 +30,7 @@ TEST_CASE("Entry construction") Entry meaningful("Blubb", 42); CHECK_EQ(meaningful.key(), "Blubb"); + CHECK_EQ(meaningful.key(), meaningful.fqKey()); CHECK_EQ(meaningful.value(), 42); const int a = 69; diff --git a/tests/FileTest.cpp b/tests/FileTest.cpp index dcc1bb6..966b1bc 100644 --- a/tests/FileTest.cpp +++ b/tests/FileTest.cpp @@ -76,6 +76,18 @@ TEST_CASE("Open file from static method") CHECK_EQ(f, f2); } +TEST_CASE("Get the value of an entry") +{ + const auto f = File{fileName}; + CHECK_EQ(f.get("Section1", "Entry1"), "Value1"sv); +} + +TEST_CASE("Get the value of an entry that doesn't exist") +{ + const auto f = File{fileName}; + CHECK_EQ(f.get("Section1", "Entry2"), int()); +} + TEST_CASE("Create a section") { auto f = File{fileName}; @@ -104,6 +116,28 @@ TEST_CASE("Call findSection to get an existing Section") CHECK_EQ(section->findEntry("Entry1")->value(), "Value1"sv); } +TEST_CASE("Call findSection to get a non-existing Section") +{ + const auto f = File{fileName}; + const auto section = f.findSection("Section2"); + CHECK_EQ(section, nullptr); +} + +TEST_CASE("Call findEntry to get an existing Entry") +{ + const auto f = File{fileName}; + const auto entry = f.findEntry("Section1.Entry1"); + REQUIRE(entry); + CHECK_EQ(entry->value(), "Value1"sv); +} + +TEST_CASE("Call findEntry with a non-existing section") +{ + const auto f = File{fileName}; + const auto entry = f.findEntry("Section2.Entry1"); + CHECK_EQ(entry, nullptr); +} + TEST_CASE("Equality operator") { const auto f = File{fileName};