From 45cc47a85967e0b59f3b46a5f8289175cfe6cb07 Mon Sep 17 00:00:00 2001 From: irisz64 Date: Thu, 26 Jun 2025 23:33:30 +0200 Subject: [PATCH] Squashed 'external/mINI/' content from commit 52b66e987c git-subtree-dir: external/mINI git-subtree-split: 52b66e987cb56171dc91d96115cdf094b6e4d7a0 --- .gitignore | 8 + CHANGELOG.md | 63 ++ CMakeLists.txt | 14 + LICENSE | 19 + README.md | 333 +++++++++ icon.png | Bin 0 -> 1634 bytes src/mini/ini.h | 780 +++++++++++++++++++++ tests/CMakeLists.txt | 23 + tests/build.bat | 7 + tests/build.sh | 6 + tests/building.txt | 36 + tests/clean.bat | 3 + tests/clean.sh | 3 + tests/lest/LICENSE.txt | 23 + tests/lest/lest.hpp | 1485 ++++++++++++++++++++++++++++++++++++++++ tests/run.bat | 7 + tests/run.sh | 6 + tests/runalltest.sh | 13 + tests/testcasesens.cpp | 100 +++ tests/testcopy.cpp | 77 +++ tests/testgenerate.cpp | 287 ++++++++ tests/testhuge.cpp | 59 ++ tests/testpath.cpp | 312 +++++++++ tests/testread.cpp | 457 +++++++++++++ tests/testutf8.cpp | 145 ++++ tests/testwrite.cpp | 747 ++++++++++++++++++++ 26 files changed, 5013 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 icon.png create mode 100644 src/mini/ini.h create mode 100644 tests/CMakeLists.txt create mode 100644 tests/build.bat create mode 100755 tests/build.sh create mode 100644 tests/building.txt create mode 100644 tests/clean.bat create mode 100755 tests/clean.sh create mode 100644 tests/lest/LICENSE.txt create mode 100644 tests/lest/lest.hpp create mode 100644 tests/run.bat create mode 100755 tests/run.sh create mode 100755 tests/runalltest.sh create mode 100644 tests/testcasesens.cpp create mode 100644 tests/testcopy.cpp create mode 100644 tests/testgenerate.cpp create mode 100644 tests/testhuge.cpp create mode 100644 tests/testpath.cpp create mode 100644 tests/testread.cpp create mode 100644 tests/testutf8.cpp create mode 100644 tests/testwrite.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..1a928e6e --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*~ +*.swp +*.swo +*.vim +/tests/*.ini +/tests/*.test +/tests/*.exe +/tests/build \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..aa7cf96f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,63 @@ +## 0.9.18 (March 30, 2025) +- `FEATURE` Replaces string paths with std::filesystem::path. ([pull #42](https://github.com/metayeti/mINI/pull/42)) + +## 0.9.17 (September 30, 2024) +- `FEATURE` Adds `CMakeLists.txt`. ([pull #38](https://github.com/metayeti/mINI/pull/38)) + +## 0.9.16 (August 13, 2024) +- `BUGFIX` Fixes a serious regression bug where removing a section would break the file in random ways due to the assignment operator introduced in the previous version. This version removes the assignment operator until it can be implemented in a way that is compliant with expected behavior. ([issue #36](https://github.com/metayeti/mINI/issues/36)) + +## 0.9.15 (January 11, 2024) +- `BUGFIX` Fixes G++ warnings and implements a copy assignment operator for mINI::INIMap. ([pull #28](https://github.com/metayeti/mINI/pull/28)) + +## 0.9.14 (May 27, 2022) +- `BUGFIX` Fixes C4310 warning. ([issue #19](https://github.com/metayeti/mINI/issues/19)) + +## 0.9.13 (April 25, 2022) +- `BUGFIX` Writer now understands UTF-8 BOM-encoded files. ([issue #7](https://github.com/metayeti/mINI/issues/17)) +- `BUGFIX` Fixes a bug introduced in 0.9.12 where reader would break when reading empty files. + +## 0.9.12 (April 24, 2022) +- `BUGFIX` Fixes parser breaking for UTF-8 BOM-encoded files. ([issue #7](https://github.com/metayeti/mINI/issues/17)) + +## 0.9.11 (October 6, 2021) +- `BUGFIX` Fixes various compiler warnings. + +## 0.9.10 (March 4, 2021) +- `BUGFIX` Change delimiter constants to `const char* const` to prevent unnecessary allocations. ([issue #5](https://github.com/metayeti/mINI/issues/5)) + +## 0.9.9 (February 22, 2021) +- `BUGFIX` Adds missing cctype header. ([pull #4](https://github.com/metayeti/mINI/pull/4)) + +## 0.9.8 (February 14, 2021) +- `BUGFIX` Avoid C4244 warning. ([pull #2](https://github.com/metayeti/mINI/pull/2)) + +## 0.9.7 (August 14, 2018) +- `FEATURE` Adds case sensitivity toggle via a macro definition. + +## 0.9.6 (May 30, 2018) +- `BUGFIX` Changed how files are written / generated. Proper line endings are selected depending on the system. +- `FEATURE` Support UTF-8 encoding. + +## 0.9.5 (May 28, 2018) +- `BUGFIX` Fixes a bug where writer would skip escaped `=` sequences for new sections. + +## 0.9.4 (May 28, 2018) +- `BUGFIX / FEATURE` Equals (`=`) characters within key names are now allowed. When writing or generating a file, key values containing the `=` characters will be escaped with the `\=` sequence. Upon reading the file back, the escape sequences will again be converted back to `=`. Values do not use escape sequences and may contain `=` characters. +- `BUGFIX` Square bracket characters (`[` and `]`) are now valid within section names. +- `BUGFIX` Trailing comments on section lines are now parsed properly. Fixes a bug where a trailing comment containing the `]` character would break the parser. +- `BUGFIX` Values being written or generated are now stripped of leading and trailing whitespace to conform to the specified format. + +## 0.9.3 (May 24, 2018) +- `BUGFIX` Fixes inconsistent behavior with empty section and key names where read would ignore empty names and write would allow them. +- `FEATURE` Empty key and section names are now allowed. + +## 0.9.2 (May 24, 2018) +- `BUGFIX` Fixes the multiple definition bug [issue #1](/../../issues/1) +- `BUGFIX` Fixes a bug where a `write()` call to an empty file would begin writing at line 2 instead of 1 due to a reader bug. + +## 0.9.1 (May 20, 2018) +- `BUGFIX` Fixed a bug where the writer would skip writing new keys and values following an empty section. + +## 0.9.0 (May 20, 2018) +- Release v0.9.0 diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..edad736a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.5) +project(mINI CXX) +set(CMAKE_CXX_STANDARD 17) + +# Check GCC version and add -lstdc++fs if necessary +if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + execute_process(COMMAND ${CMAKE_CXX_COMPILER} -dumpversion OUTPUT_VARIABLE GCC_VERSION) + if (GCC_VERSION VERSION_LESS 9.1) + link_libraries("-lstdc++fs") + endif() +endif() + +add_library(mINI INTERFACE) +target_include_directories(mINI INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/src") diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..6c63398c --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +The MIT License (MIT) +Copyright (c) 2018 Danijel Durakovic + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..8408c6b6 --- /dev/null +++ b/README.md @@ -0,0 +1,333 @@ +# mINI + +v0.9.18 + +## Info + +This is a tiny, header only C++ library for manipulating INI files. + +It conforms to the following format: +- section and key names are case insensitive by default +- whitespace around sections, keys and values is ignored +- empty section and key names are allowed +- keys that do not belong to a section are ignored +- comments are lines where the first non-whitespace character is a semicolon (`;`) +- trailing comments are allowed on section lines, but not key/value lines +- every entry exists on a single line and multiline is not supported + + +```INI +; comment +[section] +key = value +``` + + +Files are read on demand in one go, after which the data is kept in memory and is ready to be manipulated. Files are closed after read or write operations. This utility supports lazy writing, which only writes changes and updates and preserves custom formatting and comments. A lazy write invoked by a `write()` call will read the output file, find which changes have been made, and update the file accordingly. If you only need to generate files, use `generate()` instead. + +Section and key order is preserved on read and write operations. Iterating through data will take the same order as the original file or the order in which keys were added to the structure. + +This library operates with the `std::string` type to hold values and relies on your host environment for encoding. It should play nicely with UTF-8 but your mileage may vary. + +## Installation + +This is a header-only library. To install it, just copy everything in `/src/` into your own project's source code folder, or use a custom location and just make sure your compiler sees the additional include directory. Then include the file somewhere in your code: + +```C++ +#include "mini/ini.h" +``` + +You're good to go! + +## Basic examples + +### Reading / writing + +Start with an INI file named `myfile.ini`: +```INI +; amounts of fruits +[fruits] +apples=20 +oranges=30 +``` + +Our code: +```C++ +// first, create a file instance +mINI::INIFile file("myfile.ini"); + +// next, create a structure that will hold data +mINI::INIStructure ini; + +// now we can read the file +file.read(ini); + +// read a value +std::string& amountOfApples = ini["fruits"]["apples"]; + +// update a value +ini["fruits"]["oranges"] = "50"; + +// add a new entry +ini["fruits"]["bananas"] = "100"; + +// write updates to file +file.write(ini); +``` + +After running the code, our INI file now looks like this: +```INI +; amounts of fruits +[fruits] +apples=20 +oranges=50 +bananas=100 +``` + +### Generating a file + +```C++ +// create a file instance +mINI::INIFile file("myfile.ini"); + +// create a data structure +mINI::INIStructure ini; + +// populate the structure +ini["things"]["chairs"] = "20"; +ini["things"]["balloons"] = "100"; + +// generate an INI file (overwrites any previous file) +file.generate(ini); +``` + +## Manipulating files + +The `INIFile` class holds the filename and exposes functions for reading, writing and generating INI files. It does not keep the file open but merely provides an abstraction you can use to access physical files. + +To create a file instance: +```C++ +mINI::INIFile file("myfile.ini"); +``` + +You will also need a structure you can operate on: +```C++ +mINI::INIStructure ini; +``` + +To read from a file: +```C++ +bool readSuccess = file.read(ini); +``` + +To write back to a file while preserving comments and custom formatting: +```C++ +bool writeSuccess = file.write(ini); +``` + +You can set the second parameter to `write()` to `true` if you want the file to be written with pretty-print. Pretty-print adds spaces between key-value pairs and blank lines between sections in the output file: +```C++ +bool writeSuccess = file.write(ini, true); +``` + +A `write()` call will attempt to preserve any custom formatting the original INI file uses and will only use pretty-print for creation of new keys and sections. + +To generate a file: +```C++ +file.generate(ini); +``` + +Note that `generate()` will overwrite any custom formatting and comments from the original file! + +You can use pretty-print with `generate()` as well: +```C++ +file.generate(ini, true); +``` + +Example output for a generated INI file *without* pretty-print: +```INI +[section1] +key1=value1 +key2=value2 +[section2] +key1=value1 +``` + +Example output for a generated INI file *with* pretty-print: +```INI +[section1] +key1 = value1 +key2 = value2 + +[section2] +key1 = value1 +``` + +## Manipulating data + +### Reading data + +There are two ways to read data from the INI structure. You can either use the `[]` operator or the `get()` function: + +```C++ +// read value - if key or section don't exist, they will be created +// returns reference to real value +std::string& value = ini["section"]["key"]; + +// read value safely - if key or section don't exist they will NOT be created +// returns a copy +std::string value = ini.get("section").get("key"); +``` + +The difference between `[]` and `get()` operations is that `[]` returns a reference to **real** data (that you may modify) and creates a new item automatically if one does not already exist, whereas `get()` returns a **copy** of the data and doesn't create new items in the structure. Use `has()` before doing any operations with `[]` if you wish to avoid altering the structure. + +You may combine usage of `[]` and `get()`. + +Section and key names are case insensitive and are stripped of leading and trailing whitespace. `ini["section"]` is the same as `ini["SECTION"]` is the same as `ini[" sEcTiOn "]` and so on, and same for keys. Generated files always use lower case for section and key names. Writing to an existing file will preserve letter cases of the original file whenever those keys or sections already exists. + +### Updating data + +To set or update a value: +```C++ +ini["section"]["key"] = "value"; +``` + +Note that when writing to a file, values will be stripped of leading and trailing whitespace . For example, the following value will be converted to just `"c"` when reading back from a file: `ini["a"]["b"] = " c ";` + +You can set multiple values at once by using `set()`: +```C++ +ini["section"].set({ + {"key1", "value1"}, + {"key2", "value2"} +}); +``` + +To create an empty section, simply do: +```C++ +ini["section"]; +``` + +Similarly, to create an empty key: +```C++ +ini["section"]["key"]; +``` + +To remove a single key from a section: +```C++ +bool removeSuccess = ini["section"].remove("key"); +``` + +To remove a section: +```C++ +bool removeSuccess = ini.remove("section"); +``` + +To remove all keys from a section: +```C++ +ini["section"].clear(); +``` + +To remove all data in structure: +```C++ +ini.clear(); +``` + +### Other functions + +To check if a section is present: +```C++ +bool hasSection = ini.has("section"); +``` + +To check if a key within a section is present: +```C++ +bool hasKey = ini["section"].has("key"); +``` + +To get the number of keys in a section: +```C++ +size_t n_keys = ini["section"].size(); +``` + +To get the number of sections in the structure: +```C++ +size_t n_sections = ini.size(); +``` + +### Nitty-gritty + +Keep in mind that `[]` will always create a new item if the item does not already exist! You can use `has()` to check if an item exists before performing further operations. Remember that `get()` will return a copy of data, so you should **not** be doing removes or updates to data with it! + +Usage of the `[]` operator shouldn't be a problem in most real-world cases where you're doing lookups on known keys and you may not care if empty keys or sections get created. However - if you have a situation where you do not want new items to be added to the structure, either use `get()` to retreive items, or if you don't want to be working with copies of data, use `has()` before using the `[]` operator if you want to be on the safe side. + +Short example that demonstrates safe manipulation of data: +```C++ +if (ini.has("section")) +{ + // we have section, we can access it safely without creating a new one + auto& collection = ini["section"]; + if (collection.has("key")) + { + // we have key, we can access it safely without creating a new one + auto& value = collection["key"]; + } +} +``` + +## Iteration + +You can traverse the structure in order of insertion. The following example loops through the structure and displays results in a familiar format: +```C++ +for (auto const& it : ini) +{ + auto const& section = it.first; + auto const& collection = it.second; + std::cout << "[" << section << "]" << std::endl; + for (auto const& it2 : collection) + { + auto const& key = it2.first; + auto const& value = it2.second; + std::cout << key << "=" << value << std::endl; + } +} +``` + +`it.first` is always `std::string` type. + +`it.second` is an object which is either a `mINI::INIMap` type on the first level or `std::string` type on the second level. + +The API only exposes a `const_iterator`, so you can't use iterators to manipulate data directly. You can however access the structure as normal while iterating: + +```C++ +// change all values in the structure to "banana" +for (auto const& it : ini) +{ + auto const& section = it.first; + auto const& collection = it.second; + for (auto const& it2 : collection) + { + auto const& key = it2.first; + ini[section][key] = "banana"; // O(1) because hashmaps + } +} +``` + +## Case sensitivity + +If you wish to make the library not ignore letter case, add the directive `#define MINI_CASE_SENSITIVE` **before** including the library: +```C++ +#define MINI_CASE_SENSITIVE +#include "mini/ini.h" +``` + +This will affect reading and writing from files and access to the structure. + +## Thanks + +- [lest](https://github.com/martinmoene/lest) - testing framework + +## License + +Copyright © 2018 Danijel Durakovic + +Licensed under the terms of the [MIT license](LICENSE) diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..850432e443f656347dcce27391e2518208152aca GIT binary patch literal 1634 zcmeAS@N?(olHy`uVBq!ia0y~yU^oE69Bd2>3_*8t*cliYTQZ%U13aCb6$*;-(=u~X z85lGs)=sqbIP4&EG(LFgB3;=x3Xdi%w2ExedK9rLf`xnO+6$|iPkt4iWul^~*VmMN z@L>PZRn6U-*YT}uQux96=+&boFO*dO>1%H3`1oLl{QGyd@9r@=tv)8QSf5Q})yn0< zg@)7PMQhhfI0-Pi7^)gNd#8zQE|87?{^7${y+fPtod0Xd)Vk;(L%)Yi@~k|ee1Y?M zv!tHaJ`z~%!TPB9k>^}Jk3T0$Z$30XmpYqup5&#?FDoAjY>ar9xU)UdHM}Fe<1nl1 z(i+9*1&yD@(wAOKm|Oix{8#rYueR4F$0r;Rn&jcw(!;iD#X*;ru&E-WUe-BCX{IP#dqugCh{W7aEgn5gc1Nj3V@B@r*x2Y-9^Y4Y>jXKK#&%b#02 z$BrTLRn|{73kw#tQ&aZ7`}D5y-o1aemtVeT=lfRl)cXSOJ_ZH`wj^(N7lwTxxOJ_= zYz77f&H|6fVg?3oVGw3ym^DX&fq{X&#M9T6{W&*3ucf+o0e3tD153Q8i(^Q|t+#g# z^KM6ouwJn6@GX5AlO}$raStQ&y~f=kX{kP{p`NPi9U9CN-aG8}c$sIldX`_Nk%P+);JqsCwzgyoA+PUw!Di`C`|*Ma!=Lz8CFxmW6>41RHkMNWAWR z|L&d4U%|E4U*CJ{d-=<@vfqFI>D*QnW?1f^zyN{)z5INP_OExXE3e+NEc@Nt37ieV z3IYruxS~z0!~E8^w|DE-VF)i0(=hs8c7o-ArT_;62!9YT# z*_Zt7Z_9SydwcrnB^Cx@1%b6)JuN~-+uak{bj&-puAzB`i1}f-#v4g z8W^tFSZ=z^%*b$`u}*BR_xxX%OIQ4zaq6`(;{lcj{`L#&|CZ0cp$4`Ct{~~^Z zIimvu1STXzEtuJPYu@zf)34m$xo@AJZQaI*FM0K~wWbI)d|Mn^@7}w&Om6?poGzR9 zPoJi~`ndaU-jv6mfBd+y`|q=7%gTQrKHSU+4nI&Bs%A9yiEOoSuI{ZVm$ zx9u*aoUGq7_kKE!FupOfvF^mZt2?hu4G8Qu62F+=(z;($Y>V^7!tQA?roR9GHYr`K zZ9z6|Rd%D--K#sNtPq{)Z9OHo+Dmr+m%6$=_A}tl3R$~LOrzRO%w-!;3U zX63w|y7&M2XWn^lgIAh5sWJF9LSkxx)YYvHH+M_AFA`JA3S9Ipd5!i$rNG;h@6LZy zcw(L4sck{;_;^8q&&0sw@O2HV)x?!sZIdN`b!}Mj)({?-E9Qy4XwT>kQ2BlOQ{>I9 zdN;RvCcpL9yt(Z-JeU?#MJ<@<`u5b7sFiR2xrE>F&3!u=DYG3rdcl7~)LOpU+qbvb zo@U+^qZiy?UsoshrE>FH?<-!sd*@FK%C7SP=S)!UTxDLteZ}PL!Ng|~QP2!4z@YKO Z`X`Hif<^hUS_TFN22WQ%mvv4FO#pqkwfz79 literal 0 HcmV?d00001 diff --git a/src/mini/ini.h b/src/mini/ini.h new file mode 100644 index 00000000..ce60414f --- /dev/null +++ b/src/mini/ini.h @@ -0,0 +1,780 @@ +/* + * The MIT License (MIT) + * Copyright (c) 2018 Danijel Durakovic + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +/////////////////////////////////////////////////////////////////////////////// +// +// /mINI/ v0.9.18 +// An INI file reader and writer for the modern age. +// +/////////////////////////////////////////////////////////////////////////////// +// +// A tiny utility library for manipulating INI files with a straightforward +// API and a minimal footprint. It conforms to the (somewhat) standard INI +// format - sections and keys are case insensitive and all leading and +// trailing whitespace is ignored. Comments are lines that begin with a +// semicolon. Trailing comments are allowed on section lines. +// +// Files are read on demand, upon which data is kept in memory and the file +// is closed. This utility supports lazy writing, which only writes changes +// and updates to a file and preserves custom formatting and comments. A lazy +// write invoked by a write() call will read the output file, find what +// changes have been made and update the file accordingly. If you only need to +// generate files, use generate() instead. Section and key order is preserved +// on read, write and insert. +// +/////////////////////////////////////////////////////////////////////////////// +// +// /* BASIC USAGE EXAMPLE: */ +// +// /* read from file */ +// mINI::INIFile file("myfile.ini"); +// mINI::INIStructure ini; +// file.read(ini); +// +// /* read value; gets a reference to actual value in the structure. +// if key or section don't exist, a new empty value will be created */ +// std::string& value = ini["section"]["key"]; +// +// /* read value safely; gets a copy of value in the structure. +// does not alter the structure */ +// std::string value = ini.get("section").get("key"); +// +// /* set or update values */ +// ini["section"]["key"] = "value"; +// +// /* set multiple values */ +// ini["section2"].set({ +// {"key1", "value1"}, +// {"key2", "value2"} +// }); +// +// /* write updates back to file, preserving comments and formatting */ +// file.write(ini); +// +// /* or generate a file (overwrites the original) */ +// file.generate(ini); +// +/////////////////////////////////////////////////////////////////////////////// +// +// Long live the INI file!!! +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef MINI_INI_H_ +#define MINI_INI_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mINI +{ + namespace INIStringUtil + { + const char* const whitespaceDelimiters = " \t\n\r\f\v"; + inline void trim(std::string& str) + { + str.erase(str.find_last_not_of(whitespaceDelimiters) + 1); + str.erase(0, str.find_first_not_of(whitespaceDelimiters)); + } +#ifndef MINI_CASE_SENSITIVE + inline void toLower(std::string& str) + { + std::transform(str.begin(), str.end(), str.begin(), [](const char c) { + return static_cast(std::tolower(c)); + }); + } +#endif + inline void replace(std::string& str, std::string const& a, std::string const& b) + { + if (!a.empty()) + { + std::size_t pos = 0; + while ((pos = str.find(a, pos)) != std::string::npos) + { + str.replace(pos, a.size(), b); + pos += b.size(); + } + } + } +#ifdef _WIN32 + const char* const endl = "\r\n"; +#else + const char* const endl = "\n"; +#endif + } + + template + class INIMap + { + private: + using T_DataIndexMap = std::unordered_map; + using T_DataItem = std::pair; + using T_DataContainer = std::vector; + using T_MultiArgs = typename std::vector>; + + T_DataIndexMap dataIndexMap; + T_DataContainer data; + + std::size_t setEmpty(std::string& key) + { + const std::size_t index = data.size(); + dataIndexMap[key] = index; + data.emplace_back(key, T()); + return index; + } + + public: + using const_iterator = typename T_DataContainer::const_iterator; + + INIMap() = default; + + INIMap(INIMap const& other) : dataIndexMap(other.dataIndexMap), data(other.data) + { + } + + T& operator[](std::string key) + { + INIStringUtil::trim(key); +#ifndef MINI_CASE_SENSITIVE + INIStringUtil::toLower(key); +#endif + auto it = dataIndexMap.find(key); + const bool hasIt = (it != dataIndexMap.end()); + const std::size_t index = (hasIt) ? it->second : setEmpty(key); + return data[index].second; + } + [[nodiscard]] T get(std::string key) const + { + INIStringUtil::trim(key); +#ifndef MINI_CASE_SENSITIVE + INIStringUtil::toLower(key); +#endif + auto it = dataIndexMap.find(key); + if (it == dataIndexMap.end()) + { + return T(); + } + return T(data[it->second].second); + } + [[nodiscard]] bool has(std::string key) const + { + INIStringUtil::trim(key); +#ifndef MINI_CASE_SENSITIVE + INIStringUtil::toLower(key); +#endif + return (dataIndexMap.count(key) == 1); + } + void set(std::string key, T obj) + { + INIStringUtil::trim(key); +#ifndef MINI_CASE_SENSITIVE + INIStringUtil::toLower(key); +#endif + auto it = dataIndexMap.find(key); + if (it != dataIndexMap.end()) + { + data[it->second].second = obj; + } + else + { + dataIndexMap[key] = data.size(); + data.emplace_back(key, obj); + } + } + void set(T_MultiArgs const& multiArgs) + { + for (auto const& it : multiArgs) + { + auto const& key = it.first; + auto const& obj = it.second; + set(key, obj); + } + } + bool remove(std::string key) + { + INIStringUtil::trim(key); +#ifndef MINI_CASE_SENSITIVE + INIStringUtil::toLower(key); +#endif + auto it = dataIndexMap.find(key); + if (it != dataIndexMap.end()) + { + std::size_t index = it->second; + data.erase(data.begin() + index); + dataIndexMap.erase(it); + for (auto& it2 : dataIndexMap) + { + auto& vi = it2.second; + if (vi > index) + { + vi--; + } + } + return true; + } + return false; + } + void clear() + { + data.clear(); + dataIndexMap.clear(); + } + [[nodiscard]] std::size_t size() const + { + return data.size(); + } + [[nodiscard]] const_iterator begin() const { return data.begin(); } + [[nodiscard]] const_iterator end() const { return data.end(); } + }; + + using INIStructure = INIMap>; + + namespace INIParser + { + using T_ParseValues = std::pair; + + enum class PDataType : char + { + PDATA_NONE, + PDATA_COMMENT, + PDATA_SECTION, + PDATA_KEYVALUE, + PDATA_UNKNOWN + }; + + inline PDataType parseLine(std::string line, T_ParseValues& parseData) + { + parseData.first.clear(); + parseData.second.clear(); + INIStringUtil::trim(line); + if (line.empty()) + { + return PDataType::PDATA_NONE; + } + const char firstCharacter = line[0]; + if (firstCharacter == ';') + { + return PDataType::PDATA_COMMENT; + } + if (firstCharacter == '[') + { + auto commentAt = line.find_first_of(';'); + if (commentAt != std::string::npos) + { + line = line.substr(0, commentAt); + } + auto closingBracketAt = line.find_last_of(']'); + if (closingBracketAt != std::string::npos) + { + auto section = line.substr(1, closingBracketAt - 1); + INIStringUtil::trim(section); + parseData.first = section; + return PDataType::PDATA_SECTION; + } + } + auto lineNorm = line; + INIStringUtil::replace(lineNorm, "\\=", " "); + auto equalsAt = lineNorm.find_first_of('='); + if (equalsAt != std::string::npos) + { + auto key = line.substr(0, equalsAt); + INIStringUtil::trim(key); + INIStringUtil::replace(key, "\\=", "="); + auto value = line.substr(equalsAt + 1); + INIStringUtil::trim(value); + parseData.first = key; + parseData.second = value; + return PDataType::PDATA_KEYVALUE; + } + return PDataType::PDATA_UNKNOWN; + } + } + + class INIReader + { + public: + using T_LineData = std::vector; + using T_LineDataPtr = std::shared_ptr; + + bool isBOM = false; + + private: + std::ifstream fileReadStream; + T_LineDataPtr lineData; + + T_LineData readFile() + { + fileReadStream.seekg(0, std::ios::end); + const std::size_t fileSize = static_cast(fileReadStream.tellg()); + fileReadStream.seekg(0, std::ios::beg); + if (fileSize >= 3) { + const char header[3] = { + static_cast(fileReadStream.get()), + static_cast(fileReadStream.get()), + static_cast(fileReadStream.get()) + }; + isBOM = ( + header[0] == static_cast(0xEF) && + header[1] == static_cast(0xBB) && + header[2] == static_cast(0xBF) + ); + } + else { + isBOM = false; + } + std::string fileContents; + fileContents.resize(fileSize); + fileReadStream.seekg(isBOM ? 3 : 0, std::ios::beg); + fileReadStream.read(fileContents.data(), fileSize); + fileReadStream.close(); + T_LineData output; + if (fileSize == 0) + { + return output; + } + std::string buffer; + buffer.reserve(50); + for (std::size_t i = 0; i < fileSize; ++i) + { + const char& c = fileContents[i]; + if (c == '\n') + { + output.emplace_back(buffer); + buffer.clear(); + continue; + } + if (c != '\0' && c != '\r') + { + buffer += c; + } + } + output.emplace_back(buffer); + return output; + } + + public: + INIReader(std::filesystem::path const& filename, bool keepLineData = false) + { + fileReadStream.open(filename, std::ios::in | std::ios::binary); + if (keepLineData) + { + lineData = std::make_shared(); + } + } + ~INIReader() = default; + + bool operator>>(INIStructure& data) + { + if (!fileReadStream.is_open()) + { + return false; + } + const T_LineData fileLines = readFile(); + std::string section; + bool inSection = false; + INIParser::T_ParseValues parseData; + for (auto const& line : fileLines) + { + auto parseResult = INIParser::parseLine(line, parseData); + if (parseResult == INIParser::PDataType::PDATA_SECTION) + { + inSection = true; + data[section = parseData.first]; + } + else if (inSection && parseResult == INIParser::PDataType::PDATA_KEYVALUE) + { + auto const& key = parseData.first; + auto const& value = parseData.second; + data[section][key] = value; + } + if (lineData && parseResult != INIParser::PDataType::PDATA_UNKNOWN) + { + if (parseResult == INIParser::PDataType::PDATA_KEYVALUE && !inSection) + { + continue; + } + lineData->emplace_back(line); + } + } + return true; + } + T_LineDataPtr getLines() + { + return lineData; + } + }; + + class INIGenerator + { + private: + std::ofstream fileWriteStream; + + public: + bool prettyPrint = false; + + INIGenerator(std::filesystem::path const& filename) + { + fileWriteStream.open(filename, std::ios::out | std::ios::binary); + } + ~INIGenerator() = default; + + bool operator<<(INIStructure const& data) + { + if (!fileWriteStream.is_open()) + { + return false; + } + if (data.size() == 0U) + { + return true; + } + auto it = data.begin(); + for (;;) + { + auto const& section = it->first; + auto const& collection = it->second; + fileWriteStream + << "[" + << section + << "]"; + if (collection.size() != 0U) + { + fileWriteStream << INIStringUtil::endl; + auto it2 = collection.begin(); + for (;;) + { + auto key = it2->first; + INIStringUtil::replace(key, "=", "\\="); + auto value = it2->second; + INIStringUtil::trim(value); + fileWriteStream + << key + << ((prettyPrint) ? " = " : "=") + << value; + if (++it2 == collection.end()) + { + break; + } + fileWriteStream << INIStringUtil::endl; + } + } + if (++it == data.end()) + { + break; + } + fileWriteStream << INIStringUtil::endl; + if (prettyPrint) + { + fileWriteStream << INIStringUtil::endl; + } + } + return true; + } + }; + + class INIWriter + { + private: + using T_LineData = std::vector; + using T_LineDataPtr = std::shared_ptr; + + std::filesystem::path filename; + + T_LineData getLazyOutput(T_LineDataPtr const& lineData, INIStructure& data, INIStructure& original) const + { + T_LineData output; + INIParser::T_ParseValues parseData; + std::string sectionCurrent; + bool parsingSection = false; + bool continueToNextSection = false; + bool discardNextEmpty = false; + bool writeNewKeys = false; + std::size_t lastKeyLine = 0; + for (auto line = lineData->begin(); line != lineData->end(); ++line) + { + if (!writeNewKeys) + { + auto parseResult = INIParser::parseLine(*line, parseData); + if (parseResult == INIParser::PDataType::PDATA_SECTION) + { + if (parsingSection) + { + writeNewKeys = true; + parsingSection = false; + --line; + continue; + } + sectionCurrent = parseData.first; + if (data.has(sectionCurrent)) + { + parsingSection = true; + continueToNextSection = false; + discardNextEmpty = false; + output.emplace_back(*line); + lastKeyLine = output.size(); + } + else + { + continueToNextSection = true; + discardNextEmpty = true; + continue; + } + } + else if (parseResult == INIParser::PDataType::PDATA_KEYVALUE) + { + if (continueToNextSection) + { + continue; + } + if (data.has(sectionCurrent)) + { + auto& collection = data[sectionCurrent]; + auto const& key = parseData.first; + auto const& value = parseData.second; + if (collection.has(key)) + { + auto outputValue = collection[key]; + if (value == outputValue) + { + output.emplace_back(*line); + } + else + { + INIStringUtil::trim(outputValue); + auto lineNorm = *line; + INIStringUtil::replace(lineNorm, "\\=", " "); + auto equalsAt = lineNorm.find_first_of('='); + auto valueAt = lineNorm.find_first_not_of( + INIStringUtil::whitespaceDelimiters, + equalsAt + 1 + ); + std::string outputLine = line->substr(0, valueAt); + if (prettyPrint && equalsAt + 1 == valueAt) + { + outputLine += " "; + } + outputLine += outputValue; + output.emplace_back(outputLine); + } + lastKeyLine = output.size(); + } + } + } + else + { + if (discardNextEmpty && line->empty()) + { + discardNextEmpty = false; + } + else if (parseResult != INIParser::PDataType::PDATA_UNKNOWN) + { + output.emplace_back(*line); + } + } + } + if (writeNewKeys || std::next(line) == lineData->end()) + { + T_LineData linesToAdd; + if (data.has(sectionCurrent) && original.has(sectionCurrent)) + { + auto const& collection = data[sectionCurrent]; + auto const& collectionOriginal = original[sectionCurrent]; + for (auto const& it : collection) + { + auto key = it.first; + if (collectionOriginal.has(key)) + { + continue; + } + auto value = it.second; + INIStringUtil::replace(key, "=", "\\="); + INIStringUtil::trim(value); + linesToAdd.emplace_back( + key + ((prettyPrint) ? " = " : "=") + value + ); + } + } + if (!linesToAdd.empty()) + { + output.insert( + output.begin() + lastKeyLine, + linesToAdd.begin(), + linesToAdd.end() + ); + } + if (writeNewKeys) + { + writeNewKeys = false; + --line; + } + } + } + for (auto const& it : data) + { + auto const& section = it.first; + if (original.has(section)) + { + continue; + } + if (prettyPrint && !output.empty() && !output.back().empty()) + { + output.emplace_back(); + } + output.emplace_back("[" + section + "]"); + auto const& collection = it.second; + for (auto const& it2 : collection) + { + auto key = it2.first; + auto value = it2.second; + INIStringUtil::replace(key, "=", "\\="); + INIStringUtil::trim(value); + output.emplace_back( + key + ((prettyPrint) ? " = " : "=") + value + ); + } + } + return output; + } + + public: + bool prettyPrint = false; + + INIWriter(std::filesystem::path filename) + : filename(std::move(filename)) + { + } + ~INIWriter() = default; + + bool operator<<(INIStructure& data) + { + if (!std::filesystem::exists(filename)) + { + INIGenerator generator(filename); + generator.prettyPrint = prettyPrint; + return generator << data; + } + INIStructure originalData; + T_LineDataPtr lineData; + bool readSuccess = false; + bool fileIsBOM = false; + { + INIReader reader(filename, true); + readSuccess = reader >> originalData; + if (readSuccess) + { + lineData = reader.getLines(); + fileIsBOM = reader.isBOM; + } + } + if (!readSuccess) + { + return false; + } + T_LineData output = getLazyOutput(lineData, data, originalData); + std::ofstream fileWriteStream(filename, std::ios::out | std::ios::binary); + if (fileWriteStream.is_open()) + { + if (fileIsBOM) { + const char utf8_BOM[3] = { + static_cast(0xEF), + static_cast(0xBB), + static_cast(0xBF) + }; + fileWriteStream.write(utf8_BOM, 3); + } + if (!output.empty()) + { + auto line = output.begin(); + for (;;) + { + fileWriteStream << *line; + if (++line == output.end()) + { + break; + } + fileWriteStream << INIStringUtil::endl; + } + } + return true; + } + return false; + } + }; + + class INIFile + { + private: + std::filesystem::path filename; + + public: + INIFile(std::filesystem::path filename) + : filename(std::move(filename)) + { } + + ~INIFile() = default; + + bool read(INIStructure& data) const + { + if (data.size() != 0U) + { + data.clear(); + } + if (filename.empty()) + { + return false; + } + INIReader reader(filename); + return reader >> data; + } + [[nodiscard]] bool generate(INIStructure const& data, bool pretty = false) const + { + if (filename.empty()) + { + return false; + } + INIGenerator generator(filename); + generator.prettyPrint = pretty; + return generator << data; + } + bool write(INIStructure& data, bool pretty = false) const + { + if (filename.empty()) + { + return false; + } + INIWriter writer(filename); + writer.prettyPrint = pretty; + return writer << data; + } + }; +} + +#endif // MINI_INI_H_ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 00000000..55e3a2bb --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.5) +project(mINI_test CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_EXECUTABLE_SUFFIX ".test") + +# Check GCC version and add -lstdc++fs if necessary +if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + execute_process(COMMAND ${CMAKE_CXX_COMPILER} -dumpversion OUTPUT_VARIABLE GCC_VERSION) + if (GCC_VERSION VERSION_LESS 9.1) + link_libraries("-lstdc++fs") + endif() +endif() + +# Collect source files +file(GLOB SRC_FILES "test*.cpp") + +include_directories("lest" "../src") + +# Filter to build each test exetuable +foreach(_source IN ITEMS ${SRC_FILES}) + get_filename_component(_source_name "${_source}" NAME_WE) + add_executable(${_source_name} ${_source}) +endforeach() diff --git a/tests/build.bat b/tests/build.bat new file mode 100644 index 00000000..277b414b --- /dev/null +++ b/tests/build.bat @@ -0,0 +1,7 @@ +@echo off +IF %1.==. GOTO ERR +g++ -Wall -Wextra -std=c++17 -I./lest -I./../src -lstdc++fs -o %1.exe %1.cpp +GOTO END +:ERR +echo Use: %0 [test name] +:END diff --git a/tests/build.sh b/tests/build.sh new file mode 100755 index 00000000..44f40577 --- /dev/null +++ b/tests/build.sh @@ -0,0 +1,6 @@ +#!/bin/bash +if [[ $# -eq 0 ]] ; then + echo Use: $0 [test name] + exit 1 +fi +g++ -Wall -Wextra -std=c++17 -I./lest -I./../src -lstdc++fs -o $1.test $1.cpp diff --git a/tests/building.txt b/tests/building.txt new file mode 100644 index 00000000..2c92c994 --- /dev/null +++ b/tests/building.txt @@ -0,0 +1,36 @@ +To build a test, use: + + build [testname] + +Examples: + + build testread + build testwrite + +------------------------------------------------------------ + +To run a test, use: + + run [testname] + +Examples: + + run testread + run testwrite + +------------------------------------------------------------ + +To cleanup all test files: + + clean + +------------------------------------------------------------ + +Also can use cmake to build all tests: + + cmake . -B "build" + cmake --build "build" -j + +And can run all test, use: + + ./runalltest.sh \ No newline at end of file diff --git a/tests/clean.bat b/tests/clean.bat new file mode 100644 index 00000000..e8a3acff --- /dev/null +++ b/tests/clean.bat @@ -0,0 +1,3 @@ +@echo off +del *.ini +del *.exe \ No newline at end of file diff --git a/tests/clean.sh b/tests/clean.sh new file mode 100755 index 00000000..47097389 --- /dev/null +++ b/tests/clean.sh @@ -0,0 +1,3 @@ +#!/bin/bash +rm *.ini +rm *.test \ No newline at end of file diff --git a/tests/lest/LICENSE.txt b/tests/lest/LICENSE.txt new file mode 100644 index 00000000..36b7cd93 --- /dev/null +++ b/tests/lest/LICENSE.txt @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/tests/lest/lest.hpp b/tests/lest/lest.hpp new file mode 100644 index 00000000..3c964194 --- /dev/null +++ b/tests/lest/lest.hpp @@ -0,0 +1,1485 @@ +// Copyright 2013-2018 by Martin Moene +// +// lest is based on ideas by Kevlin Henney, see video at +// http://skillsmatter.com/podcast/agile-testing/kevlin-henney-rethinking-unit-testing-in-c-plus-plus +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +#ifndef LEST_LEST_HPP_INCLUDED +#define LEST_LEST_HPP_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define lest_MAJOR 1 +#define lest_MINOR 35 +#define lest_PATCH 2 + +#define lest_VERSION lest_STRINGIFY(lest_MAJOR) "." lest_STRINGIFY(lest_MINOR) "." lest_STRINGIFY(lest_PATCH) + +#ifndef lest_FEATURE_AUTO_REGISTER +# define lest_FEATURE_AUTO_REGISTER 0 +#endif + +#ifndef lest_FEATURE_COLOURISE +# define lest_FEATURE_COLOURISE 0 +#endif + +#ifndef lest_FEATURE_LITERAL_SUFFIX +# define lest_FEATURE_LITERAL_SUFFIX 0 +#endif + +#ifndef lest_FEATURE_REGEX_SEARCH +# define lest_FEATURE_REGEX_SEARCH 0 +#endif + +#ifndef lest_FEATURE_TIME_PRECISION +# define lest_FEATURE_TIME_PRECISION 0 +#endif + +#ifndef lest_FEATURE_WSTRING +# define lest_FEATURE_WSTRING 1 +#endif + +#ifdef lest_FEATURE_RTTI +# define lest__cpp_rtti lest_FEATURE_RTTI +#elif defined(__cpp_rtti) +# define lest__cpp_rtti __cpp_rtti +#elif defined(__GXX_RTTI) || defined (_CPPRTTI) +# define lest__cpp_rtti 1 +#else +# define lest__cpp_rtti 0 +#endif + +#if lest_FEATURE_REGEX_SEARCH +# include +#endif + +// Stringify: + +#define lest_STRINGIFY( x ) lest_STRINGIFY_( x ) +#define lest_STRINGIFY_( x ) #x + +// Compiler warning suppression: + +#if defined (__clang__) +# pragma clang diagnostic ignored "-Waggregate-return" +# pragma clang diagnostic ignored "-Woverloaded-shift-op-parentheses" +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-comparison" +#elif defined (__GNUC__) +# pragma GCC diagnostic ignored "-Waggregate-return" +# pragma GCC diagnostic push +#endif + +// Suppress shadow and unused-value warning for sections: + +#if defined (__clang__) +# define lest_SUPPRESS_WSHADOW _Pragma( "clang diagnostic push" ) \ + _Pragma( "clang diagnostic ignored \"-Wshadow\"" ) +# define lest_SUPPRESS_WUNUSED _Pragma( "clang diagnostic push" ) \ + _Pragma( "clang diagnostic ignored \"-Wunused-value\"" ) +# define lest_RESTORE_WARNINGS _Pragma( "clang diagnostic pop" ) + +#elif defined (__GNUC__) +# define lest_SUPPRESS_WSHADOW _Pragma( "GCC diagnostic push" ) \ + _Pragma( "GCC diagnostic ignored \"-Wshadow\"" ) +# define lest_SUPPRESS_WUNUSED _Pragma( "GCC diagnostic push" ) \ + _Pragma( "GCC diagnostic ignored \"-Wunused-value\"" ) +# define lest_RESTORE_WARNINGS _Pragma( "GCC diagnostic pop" ) +#else +# define lest_SUPPRESS_WSHADOW /*empty*/ +# define lest_SUPPRESS_WUNUSED /*empty*/ +# define lest_RESTORE_WARNINGS /*empty*/ +#endif + +// C++ language version detection (C++23 is speculative): +// Note: VC14.0/1900 (VS2015) lacks too much from C++14. + +#ifndef lest_CPLUSPLUS +# if defined(_MSVC_LANG ) && !defined(__clang__) +# define lest_CPLUSPLUS (_MSC_VER == 1900 ? 201103L : _MSVC_LANG ) +# else +# define lest_CPLUSPLUS __cplusplus +# endif +#endif + +#define lest_CPP98_OR_GREATER ( lest_CPLUSPLUS >= 199711L ) +#define lest_CPP11_OR_GREATER ( lest_CPLUSPLUS >= 201103L ) +#define lest_CPP14_OR_GREATER ( lest_CPLUSPLUS >= 201402L ) +#define lest_CPP17_OR_GREATER ( lest_CPLUSPLUS >= 201703L ) +#define lest_CPP20_OR_GREATER ( lest_CPLUSPLUS >= 202002L ) +#define lest_CPP23_OR_GREATER ( lest_CPLUSPLUS >= 202300L ) + +#if ! defined( lest_NO_SHORT_MACRO_NAMES ) && ! defined( lest_NO_SHORT_ASSERTION_NAMES ) +# define MODULE lest_MODULE + +# if ! lest_FEATURE_AUTO_REGISTER +# define CASE lest_CASE +# define CASE_ON lest_CASE_ON +# define SCENARIO lest_SCENARIO +# endif + +# define SETUP lest_SETUP +# define SECTION lest_SECTION + +# define EXPECT lest_EXPECT +# define EXPECT_NOT lest_EXPECT_NOT +# define EXPECT_NO_THROW lest_EXPECT_NO_THROW +# define EXPECT_THROWS lest_EXPECT_THROWS +# define EXPECT_THROWS_AS lest_EXPECT_THROWS_AS + +# define GIVEN lest_GIVEN +# define WHEN lest_WHEN +# define THEN lest_THEN +# define AND_WHEN lest_AND_WHEN +# define AND_THEN lest_AND_THEN +#endif + +#if lest_FEATURE_AUTO_REGISTER +#define lest_SCENARIO( specification, sketch ) lest_CASE( specification, lest::text("Scenario: ") + sketch ) +#else +#define lest_SCENARIO( sketch ) lest_CASE( lest::text("Scenario: ") + sketch ) +#endif +#define lest_GIVEN( context ) lest_SETUP( lest::text(" Given: ") + context ) +#define lest_WHEN( story ) lest_SECTION( lest::text(" When: ") + story ) +#define lest_THEN( story ) lest_SECTION( lest::text(" Then: ") + story ) +#define lest_AND_WHEN( story ) lest_SECTION( lest::text("And then: ") + story ) +#define lest_AND_THEN( story ) lest_SECTION( lest::text("And then: ") + story ) + +#if lest_FEATURE_AUTO_REGISTER + +# define lest_CASE( specification, proposition ) \ + static void lest_FUNCTION( lest::env & ); \ + namespace { lest::add_test lest_REGISTRAR( specification, lest::test( proposition, lest_FUNCTION ) ); } \ + static void lest_FUNCTION( lest::env & lest_env ) + +#else // lest_FEATURE_AUTO_REGISTER + +# define lest_CASE( proposition ) \ + proposition, []( lest::env & lest_env ) + +# define lest_CASE_ON( proposition, ... ) \ + proposition, [__VA_ARGS__]( lest::env & lest_env ) + +# define lest_MODULE( specification, module ) \ + namespace { lest::add_module _( specification, module ); } + +#endif //lest_FEATURE_AUTO_REGISTER + +#define lest_SETUP( context ) \ + for ( int lest__section = 0, lest__count = 1; lest__section < lest__count; lest__count -= 0==lest__section++ ) \ + for ( lest::ctx lest__ctx_setup( lest_env, context ); lest__ctx_setup; ) + +#define lest_SECTION( proposition ) \ + lest_SUPPRESS_WSHADOW \ + static int lest_UNIQUE( id ) = 0; \ + if ( lest::guard( lest_UNIQUE( id ), lest__section, lest__count ) ) \ + for ( int lest__section = 0, lest__count = 1; lest__section < lest__count; lest__count -= 0==lest__section++ ) \ + for ( lest::ctx lest__ctx_section( lest_env, proposition ); lest__ctx_section; ) \ + lest_RESTORE_WARNINGS + +#define lest_EXPECT( expr ) \ + do { \ + try \ + { \ + if ( lest::result score = lest_DECOMPOSE( expr ) ) \ + throw lest::failure{ lest_LOCATION, #expr, score.decomposition }; \ + else if ( lest_env.pass() ) \ + lest::report( lest_env.os, lest::passing{ lest_LOCATION, #expr, score.decomposition, lest_env.zen() }, lest_env.context() ); \ + } \ + catch(...) \ + { \ + lest::inform( lest_LOCATION, #expr ); \ + } \ + } while ( lest::is_false() ) + +#define lest_EXPECT_NOT( expr ) \ + do { \ + try \ + { \ + if ( lest::result score = lest_DECOMPOSE( expr ) ) \ + { \ + if ( lest_env.pass() ) \ + lest::report( lest_env.os, lest::passing{ lest_LOCATION, lest::not_expr( #expr ), lest::not_expr( score.decomposition ), lest_env.zen() }, lest_env.context() ); \ + } \ + else \ + throw lest::failure{ lest_LOCATION, lest::not_expr( #expr ), lest::not_expr( score.decomposition ) }; \ + } \ + catch(...) \ + { \ + lest::inform( lest_LOCATION, lest::not_expr( #expr ) ); \ + } \ + } while ( lest::is_false() ) + +#define lest_EXPECT_NO_THROW( expr ) \ + do \ + { \ + try \ + { \ + lest_SUPPRESS_WUNUSED \ + expr; \ + lest_RESTORE_WARNINGS \ + } \ + catch (...) \ + { \ + lest::inform( lest_LOCATION, #expr ); \ + } \ + if ( lest_env.pass() ) \ + lest::report( lest_env.os, lest::got_none( lest_LOCATION, #expr ), lest_env.context() ); \ + } while ( lest::is_false() ) + +#define lest_EXPECT_THROWS( expr ) \ + do \ + { \ + try \ + { \ + lest_SUPPRESS_WUNUSED \ + expr; \ + lest_RESTORE_WARNINGS \ + } \ + catch (...) \ + { \ + if ( lest_env.pass() ) \ + lest::report( lest_env.os, lest::got{ lest_LOCATION, #expr }, lest_env.context() ); \ + break; \ + } \ + throw lest::expected{ lest_LOCATION, #expr }; \ + } \ + while ( lest::is_false() ) + +#define lest_EXPECT_THROWS_AS( expr, excpt ) \ + do \ + { \ + try \ + { \ + lest_SUPPRESS_WUNUSED \ + expr; \ + lest_RESTORE_WARNINGS \ + } \ + catch ( excpt & ) \ + { \ + if ( lest_env.pass() ) \ + lest::report( lest_env.os, lest::got{ lest_LOCATION, #expr, lest::of_type( #excpt ) }, lest_env.context() ); \ + break; \ + } \ + catch (...) {} \ + throw lest::expected{ lest_LOCATION, #expr, lest::of_type( #excpt ) }; \ + } \ + while ( lest::is_false() ) + +#define lest_UNIQUE( name ) lest_UNIQUE2( name, __LINE__ ) +#define lest_UNIQUE2( name, line ) lest_UNIQUE3( name, line ) +#define lest_UNIQUE3( name, line ) name ## line + +#define lest_DECOMPOSE( expr ) ( lest::expression_decomposer() << expr ) + +#define lest_FUNCTION lest_UNIQUE(__lest_function__ ) +#define lest_REGISTRAR lest_UNIQUE(__lest_registrar__ ) + +#define lest_LOCATION lest::location{__FILE__, __LINE__} + +namespace lest { + +const int exit_max_value = 255; + +using text = std::string; +using texts = std::vector; + +struct env; + +struct test +{ + text name; + std::function behaviour; + +#if lest_FEATURE_AUTO_REGISTER + test( text name_, std::function behaviour_ ) + : name( name_), behaviour( behaviour_) {} +#endif +}; + +using tests = std::vector; + +#if lest_FEATURE_AUTO_REGISTER + +struct add_test +{ + add_test( tests & specification, test const & test_case ) + { + specification.push_back( test_case ); + } +}; + +#else + +struct add_module +{ + template< std::size_t N > + add_module( tests & specification, test const (&module)[N] ) + { + specification.insert( specification.end(), std::begin( module ), std::end( module ) ); + } +}; + +#endif + +struct result +{ + const bool passed; + const text decomposition; + + template< typename T > + result( T const & passed_, text decomposition_) + : passed( !!passed_), decomposition( decomposition_) {} + + explicit operator bool() { return ! passed; } +}; + +struct location +{ + const text file; + const int line; + + location( text file_, int line_) + : file( file_), line( line_) {} +}; + +struct comment +{ + const text info; + + comment( text info_) : info( info_) {} + explicit operator bool() { return ! info.empty(); } +}; + +struct message : std::runtime_error +{ + const text kind; + const location where; + const comment note; + + ~message() throw() {} // GCC 4.6 + + message( text kind_, location where_, text expr_, text note_ = "" ) + : std::runtime_error( expr_), kind( kind_), where( where_), note( note_) {} +}; + +struct failure : message +{ + failure( location where_, text expr_, text decomposition_) + : message{ "failed", where_, expr_ + " for " + decomposition_ } {} +}; + +struct success : message +{ +// using message::message; // VC is lagging here + + success( text kind_, location where_, text expr_, text note_ = "" ) + : message( kind_, where_, expr_, note_ ) {} +}; + +struct passing : success +{ + passing( location where_, text expr_, text decomposition_, bool zen ) + : success( "passed", where_, expr_ + (zen ? "":" for " + decomposition_) ) {} +}; + +struct got_none : success +{ + got_none( location where_, text expr_ ) + : success( "passed: got no exception", where_, expr_ ) {} +}; + +struct got : success +{ + got( location where_, text expr_) + : success( "passed: got exception", where_, expr_) {} + + got( location where_, text expr_, text excpt_) + : success( "passed: got exception " + excpt_, where_, expr_) {} +}; + +struct expected : message +{ + expected( location where_, text expr_, text excpt_ = "" ) + : message{ "failed: didn't get exception", where_, expr_, excpt_ } {} +}; + +struct unexpected : message +{ + unexpected( location where_, text expr_, text note_ = "" ) + : message{ "failed: got unexpected exception", where_, expr_, note_ } {} +}; + +struct guard +{ + int & id; + int const & section; + + guard( int & id_, int const & section_, int & count ) + : id( id_), section( section_) + { + if ( section == 0 ) + id = count++ - 1; + } + operator bool() { return id == section; } +}; + +class approx +{ +public: + explicit approx ( double magnitude ) + : epsilon_ { std::numeric_limits::epsilon() * 100 } + , scale_ { 1.0 } + , magnitude_{ magnitude } {} + + approx( approx const & other ) = default; + + static approx custom() { return approx( 0 ); } + + approx operator()( double new_magnitude ) + { + approx appr( new_magnitude ); + appr.epsilon( epsilon_ ); + appr.scale ( scale_ ); + return appr; + } + + double magnitude() const { return magnitude_; } + + approx & epsilon( double epsilon ) { epsilon_ = epsilon; return *this; } + approx & scale ( double scale ) { scale_ = scale; return *this; } + + friend bool operator == ( double lhs, approx const & rhs ) + { + // Thanks to Richard Harris for his help refining this formula. + return std::abs( lhs - rhs.magnitude_ ) < rhs.epsilon_ * ( rhs.scale_ + (std::min)( std::abs( lhs ), std::abs( rhs.magnitude_ ) ) ); + } + + friend bool operator == ( approx const & lhs, double rhs ) { return operator==( rhs, lhs ); } + friend bool operator != ( double lhs, approx const & rhs ) { return !operator==( lhs, rhs ); } + friend bool operator != ( approx const & lhs, double rhs ) { return !operator==( rhs, lhs ); } + + friend bool operator <= ( double lhs, approx const & rhs ) { return lhs < rhs.magnitude_ || lhs == rhs; } + friend bool operator <= ( approx const & lhs, double rhs ) { return lhs.magnitude_ < rhs || lhs == rhs; } + friend bool operator >= ( double lhs, approx const & rhs ) { return lhs > rhs.magnitude_ || lhs == rhs; } + friend bool operator >= ( approx const & lhs, double rhs ) { return lhs.magnitude_ > rhs || lhs == rhs; } + +private: + double epsilon_; + double scale_; + double magnitude_; +}; + +inline bool is_false( ) { return false; } +inline bool is_true ( bool flag ) { return flag; } + +inline text not_expr( text message ) +{ + return "! ( " + message + " )"; +} + +inline text with_message( text message ) +{ + return "with message \"" + message + "\""; +} + +inline text of_type( text type ) +{ + return "of type " + type; +} + +inline void inform( location where, text expr ) +{ + try + { + throw; + } + catch( message const & ) + { + throw; + } + catch( std::exception const & e ) + { + throw unexpected{ where, expr, with_message( e.what() ) }; \ + } + catch(...) + { + throw unexpected{ where, expr, "of unknown type" }; \ + } +} + +// Expression decomposition: + +template< typename T > +auto make_value_string( T const & value ) -> std::string; + +template< typename T > +auto make_memory_string( T const & item ) -> std::string; + +#if lest_FEATURE_LITERAL_SUFFIX +inline char const * sfx( char const * txt ) { return txt; } +#else +inline char const * sfx( char const * ) { return ""; } +#endif + +inline std::string transformed( char chr ) +{ + struct Tr { char chr; char const * str; } table[] = + { + {'\\', "\\\\" }, + {'\r', "\\r" }, {'\f', "\\f" }, + {'\n', "\\n" }, {'\t', "\\t" }, + }; + + for ( auto tr : table ) + { + if ( chr == tr.chr ) + return tr.str; + } + + // The cast below helps suppress warnings ("-Wtype-limits" on GCC) on architectures where `char` is unsigned. + auto unprintable = [](char c){ return 0 <= static_cast(c) && c < ' '; }; + + auto to_hex_string = [](char c) + { + std::ostringstream os; + os << "\\x" << std::hex << std::setw(2) << std::setfill('0') << static_cast( static_cast(c) ); + return os.str(); + }; + + return unprintable( chr ) ? to_hex_string( chr ) : std::string( 1, chr ); +} + +inline std::string make_tran_string( std::string const & txt ) { std::ostringstream os; for(auto c:txt) os << transformed(c); return os.str(); } +inline std::string make_strg_string( std::string const & txt ) { return "\"" + make_tran_string( txt ) + "\"" ; } +inline std::string make_char_string( char chr ) { return "\'" + make_tran_string( std::string( 1, chr ) ) + "\'" ; } + +inline std::string to_string( std::nullptr_t ) { return "nullptr"; } +inline std::string to_string( std::string const & txt ) { return make_strg_string( txt ); } +#if lest_FEATURE_WSTRING +inline std::string to_string( std::wstring const & txt ) ; +#endif + +inline std::string to_string( char const * const txt ) { return txt ? make_strg_string( txt ) : "{null string}"; } +inline std::string to_string( char * const txt ) { return txt ? make_strg_string( txt ) : "{null string}"; } +#if lest_FEATURE_WSTRING +inline std::string to_string( wchar_t const * const txt ) { return txt ? to_string( std::wstring( txt ) ) : "{null string}"; } +inline std::string to_string( wchar_t * const txt ) { return txt ? to_string( std::wstring( txt ) ) : "{null string}"; } +#endif + +inline std::string to_string( bool flag ) { return flag ? "true" : "false"; } + +inline std::string to_string( signed short value ) { return make_value_string( value ) ; } +inline std::string to_string( unsigned short value ) { return make_value_string( value ) + sfx("u" ); } +inline std::string to_string( signed int value ) { return make_value_string( value ) ; } +inline std::string to_string( unsigned int value ) { return make_value_string( value ) + sfx("u" ); } +inline std::string to_string( signed long value ) { return make_value_string( value ) + sfx("l" ); } +inline std::string to_string( unsigned long value ) { return make_value_string( value ) + sfx("ul" ); } +inline std::string to_string( signed long long value ) { return make_value_string( value ) + sfx("ll" ); } +inline std::string to_string( unsigned long long value ) { return make_value_string( value ) + sfx("ull"); } +inline std::string to_string( double value ) { return make_value_string( value ) ; } +inline std::string to_string( float value ) { return make_value_string( value ) + sfx("f" ); } + +inline std::string to_string( signed char chr ) { return make_char_string( static_cast( chr ) ); } +inline std::string to_string( unsigned char chr ) { return make_char_string( static_cast( chr ) ); } +inline std::string to_string( char chr ) { return make_char_string( chr ); } + +template< typename T > +struct is_streamable +{ + template< typename U > + static auto test( int ) -> decltype( std::declval() << std::declval(), std::true_type() ); + + template< typename > + static auto test( ... ) -> std::false_type; + +#ifdef _MSC_VER + enum { value = std::is_same< decltype( test(0) ), std::true_type >::value }; +#else + static constexpr bool value = std::is_same< decltype( test(0) ), std::true_type >::value; +#endif +}; + +template< typename T > +struct is_container +{ + template< typename U > + static auto test( int ) -> decltype( std::declval().begin() == std::declval().end(), std::true_type() ); + + template< typename > + static auto test( ... ) -> std::false_type; + +#ifdef _MSC_VER + enum { value = std::is_same< decltype( test(0) ), std::true_type >::value }; +#else + static constexpr bool value = std::is_same< decltype( test(0) ), std::true_type >::value; +#endif +}; + +template< typename T, typename R > +using ForEnum = typename std::enable_if< std::is_enum::value, R>::type; + +template< typename T, typename R > +using ForNonEnum = typename std::enable_if< ! std::is_enum::value, R>::type; + +template< typename T, typename R > +using ForStreamable = typename std::enable_if< is_streamable::value, R>::type; + +template< typename T, typename R > +using ForNonStreamable = typename std::enable_if< ! is_streamable::value, R>::type; + +template< typename T, typename R > +using ForContainer = typename std::enable_if< is_container::value, R>::type; + +template< typename T, typename R > +using ForNonContainerNonPointer = typename std::enable_if< ! (is_container::value || std::is_pointer::value), R>::type; + +template< typename T > +auto make_enum_string( T const & item ) -> ForNonEnum +{ +#if lest__cpp_rtti + return text("[type: ") + typeid(T).name() + "]: " + make_memory_string( item ); +#else + return text("[type: (no RTTI)]: ") + make_memory_string( item ); +#endif +} + +template< typename T > +auto make_enum_string( T const & item ) -> ForEnum +{ + return to_string( static_cast::type>( item ) ); +} + +template< typename T > +auto make_string( T const & item ) -> ForNonStreamable +{ + return make_enum_string( item ); +} + +template< typename T > +auto make_string( T const & item ) -> ForStreamable +{ + std::ostringstream os; os << item; return os.str(); +} + +template +auto make_string( std::pair const & pair ) -> std::string +{ + std::ostringstream oss; + oss << "{ " << to_string( pair.first ) << ", " << to_string( pair.second ) << " }"; + return oss.str(); +} + +template< typename TU, std::size_t N > +struct make_tuple_string +{ + static std::string make( TU const & tuple ) + { + std::ostringstream os; + os << to_string( std::get( tuple ) ) << ( N < std::tuple_size::value ? ", ": " "); + return make_tuple_string::make( tuple ) + os.str(); + } +}; + +template< typename TU > +struct make_tuple_string +{ + static std::string make( TU const & ) { return ""; } +}; + +template< typename ...TS > +auto make_string( std::tuple const & tuple ) -> std::string +{ + return "{ " + make_tuple_string, sizeof...(TS)>::make( tuple ) + "}"; +} + +template< typename T > +inline std::string make_string( T const * ptr ) +{ + // Note showbase affects the behavior of /integer/ output; + std::ostringstream os; + os << std::internal << std::hex << std::showbase << std::setw( 2 + 2 * sizeof(T*) ) << std::setfill('0') << reinterpret_cast( ptr ); + return os.str(); +} + +template< typename C, typename R > +inline std::string make_string( R C::* ptr ) +{ + std::ostringstream os; + os << std::internal << std::hex << std::showbase << std::setw( 2 + 2 * sizeof(R C::* ) ) << std::setfill('0') << ptr; + return os.str(); +} + +template< typename T > +auto to_string( T const * ptr ) -> std::string +{ + return ! ptr ? "nullptr" : make_string( ptr ); +} + +template +auto to_string( R C::* ptr ) -> std::string +{ + return ! ptr ? "nullptr" : make_string( ptr ); +} + +template< typename T > +auto to_string( T const & item ) -> ForNonContainerNonPointer +{ + return make_string( item ); +} + +template< typename C > +auto to_string( C const & cont ) -> ForContainer +{ + std::ostringstream os; + os << "{ "; + for ( auto & x : cont ) + { + os << to_string( x ) << ", "; + } + os << "}"; + return os.str(); +} + +#if lest_FEATURE_WSTRING +inline +auto to_string( std::wstring const & txt ) -> std::string +{ + std::string result; result.reserve( txt.size() ); + + for( auto & chr : txt ) + { + result += chr <= 0xff ? static_cast( chr ) : '?'; + } + return to_string( result ); +} +#endif + +template< typename T > +auto make_value_string( T const & value ) -> std::string +{ + std::ostringstream os; os << value; return os.str(); +} + +inline +auto make_memory_string( void const * item, std::size_t size ) -> std::string +{ + // reverse order for little endian architectures: + + auto is_little_endian = [] + { + union U { int i = 1; char c[ sizeof(int) ]; }; + + return 1 != U{}.c[ sizeof(int) - 1 ]; + }; + + int i = 0, end = static_cast( size ), inc = 1; + + if ( is_little_endian() ) { i = end - 1; end = inc = -1; } + + unsigned char const * bytes = static_cast( item ); + + std::ostringstream os; + os << "0x" << std::setfill( '0' ) << std::hex; + for ( ; i != end; i += inc ) + { + os << std::setw(2) << static_cast( bytes[i] ) << " "; + } + return os.str(); +} + +template< typename T > +auto make_memory_string( T const & item ) -> std::string +{ + return make_memory_string( &item, sizeof item ); +} + +inline +auto to_string( approx const & appr ) -> std::string +{ + return to_string( appr.magnitude() ); +} + +template< typename L, typename R > +auto to_string( L const & lhs, std::string op, R const & rhs ) -> std::string +{ + std::ostringstream os; os << to_string( lhs ) << " " << op << " " << to_string( rhs ); return os.str(); +} + +template< typename L > +struct expression_lhs +{ + const L lhs; + + expression_lhs( L lhs_) : lhs( lhs_) {} + + operator result() { return result{ !!lhs, to_string( lhs ) }; } + + template< typename R > result operator==( R const & rhs ) { return result{ lhs == rhs, to_string( lhs, "==", rhs ) }; } + template< typename R > result operator!=( R const & rhs ) { return result{ lhs != rhs, to_string( lhs, "!=", rhs ) }; } + template< typename R > result operator< ( R const & rhs ) { return result{ lhs < rhs, to_string( lhs, "<" , rhs ) }; } + template< typename R > result operator<=( R const & rhs ) { return result{ lhs <= rhs, to_string( lhs, "<=", rhs ) }; } + template< typename R > result operator> ( R const & rhs ) { return result{ lhs > rhs, to_string( lhs, ">" , rhs ) }; } + template< typename R > result operator>=( R const & rhs ) { return result{ lhs >= rhs, to_string( lhs, ">=", rhs ) }; } +}; + +struct expression_decomposer +{ + template + expression_lhs operator<< ( L const & operand ) + { + return expression_lhs( operand ); + } +}; + +// Reporter: + +#if lest_FEATURE_COLOURISE + +inline text red ( text words ) { return "\033[1;31m" + words + "\033[0m"; } +inline text green( text words ) { return "\033[1;32m" + words + "\033[0m"; } +inline text gray ( text words ) { return "\033[1;30m" + words + "\033[0m"; } + +inline bool starts_with( text words, text with ) +{ + return 0 == words.find( with ); +} + +inline text replace( text words, text from, text to ) +{ + size_t pos = words.find( from ); + return pos == std::string::npos ? words : words.replace( pos, from.length(), to ); +} + +inline text colour( text words ) +{ + if ( starts_with( words, "failed" ) ) return replace( words, "failed", red ( "failed" ) ); + else if ( starts_with( words, "passed" ) ) return replace( words, "passed", green( "passed" ) ); + + return replace( words, "for", gray( "for" ) ); +} + +inline bool is_cout( std::ostream & os ) { return &os == &std::cout; } + +struct colourise +{ + const text words; + + colourise( text words ) + : words( words ) {} + + // only colourise for std::cout, not for a stringstream as used in tests: + + std::ostream & operator()( std::ostream & os ) const + { + return is_cout( os ) ? os << colour( words ) : os << words; + } +}; + +inline std::ostream & operator<<( std::ostream & os, colourise words ) { return words( os ); } +#else +inline text colourise( text words ) { return words; } +#endif + +inline text pluralise( text word, int n ) +{ + return n == 1 ? word : word + "s"; +} + +inline std::ostream & operator<<( std::ostream & os, comment note ) +{ + return os << (note ? " " + note.info : "" ); +} + +inline std::ostream & operator<<( std::ostream & os, location where ) +{ +#ifdef __GNUG__ + return os << where.file << ":" << where.line; +#else + return os << where.file << "(" << where.line << ")"; +#endif +} + +inline void report( std::ostream & os, message const & e, text test ) +{ + os << e.where << ": " << colourise( e.kind ) << e.note << ": " << test << ": " << colourise( e.what() ) << std::endl; +} + +// Test runner: + +#if lest_FEATURE_REGEX_SEARCH + inline bool search( text re, text line ) + { + return std::regex_search( line, std::regex( re ) ); + } +#else + inline bool search( text part, text line ) + { + auto case_insensitive_equal = []( char a, char b ) + { + return tolower( a ) == tolower( b ); + }; + + return std::search( + line.begin(), line.end(), + part.begin(), part.end(), case_insensitive_equal ) != line.end(); + } +#endif + +inline bool match( texts whats, text line ) +{ + for ( auto & what : whats ) + { + if ( search( what, line ) ) + return true; + } + return false; +} + +inline bool select( text name, texts include ) +{ + auto none = []( texts args ) { return args.size() == 0; }; + +#if lest_FEATURE_REGEX_SEARCH + auto hidden = []( text arg ){ return match( { "\\[\\..*", "\\[hide\\]" }, arg ); }; +#else + auto hidden = []( text arg ){ return match( { "[.", "[hide]" }, arg ); }; +#endif + + if ( none( include ) ) + { + return ! hidden( name ); + } + + bool any = false; + for ( auto pos = include.rbegin(); pos != include.rend(); ++pos ) + { + auto & part = *pos; + + if ( part == "@" || part == "*" ) + return true; + + if ( search( part, name ) ) + return true; + + if ( '!' == part[0] ) + { + any = true; + if ( search( part.substr(1), name ) ) + return false; + } + else + { + any = false; + } + } + return any && ! hidden( name ); +} + +inline int indefinite( int repeat ) { return repeat == -1; } + +using seed_t = std::mt19937::result_type; + +struct options +{ + bool help = false; + bool abort = false; + bool count = false; + bool list = false; + bool tags = false; + bool time = false; + bool pass = false; + bool zen = false; + bool lexical = false; + bool random = false; + bool verbose = false; + bool version = false; + int repeat = 1; + seed_t seed = 0; +}; + +struct env +{ + std::ostream & os; + options opt; + text testing; + std::vector< text > ctx; + + env( std::ostream & out, options option ) + : os( out ), opt( option ), testing(), ctx() {} + + env & operator()( text test ) + { + clear(); testing = test; return *this; + } + + bool abort() { return opt.abort; } + bool pass() { return opt.pass; } + bool zen() { return opt.zen; } + + void clear() { ctx.clear(); } + void pop() { ctx.pop_back(); } + void push( text proposition ) { ctx.emplace_back( proposition ); } + + text context() { return testing + sections(); } + + text sections() + { + if ( ! opt.verbose ) + return ""; + + text msg; + for( auto section : ctx ) + { + msg += "\n " + section; + } + return msg; + } +}; + +struct ctx +{ + env & environment; + bool once; + + ctx( env & environment_, text proposition_ ) + : environment( environment_), once( true ) + { + environment.push( proposition_); + } + + ~ctx() + { +#if lest_CPP17_OR_GREATER + if ( std::uncaught_exceptions() == 0 ) +#else + if ( ! std::uncaught_exception() ) +#endif + { + environment.pop(); + } + } + + explicit operator bool() { bool result = once; once = false; return result; } +}; + +struct action +{ + std::ostream & os; + + action( std::ostream & out ) : os( out ) {} + + action( action const & ) = delete; + void operator=( action const & ) = delete; + + operator int() { return 0; } + bool abort() { return false; } + action & operator()( test ) { return *this; } +}; + +struct print : action +{ + print( std::ostream & out ) : action( out ) {} + + print & operator()( test testing ) + { + os << testing.name << "\n"; return *this; + } +}; + +inline texts tags( text name, texts result = {} ) +{ + auto none = std::string::npos; + auto lb = name.find_first_of( "[" ); + auto rb = name.find_first_of( "]" ); + + if ( lb == none || rb == none ) + return result; + + result.emplace_back( name.substr( lb, rb - lb + 1 ) ); + + return tags( name.substr( rb + 1 ), result ); +} + +struct ptags : action +{ + std::set result; + + ptags( std::ostream & out ) : action( out ), result() {} + + ptags & operator()( test testing ) + { + for ( auto & tag : tags( testing.name ) ) + result.insert( tag ); + + return *this; + } + + ~ptags() + { + std::copy( result.begin(), result.end(), std::ostream_iterator( os, "\n" ) ); + } +}; + +struct count : action +{ + int n = 0; + + count( std::ostream & out ) : action( out ) {} + + count & operator()( test ) { ++n; return *this; } + + ~count() + { + os << n << " selected " << pluralise("test", n) << "\n"; + } +}; + +struct timer +{ + using time = std::chrono::high_resolution_clock; + + time::time_point start = time::now(); + + double elapsed_seconds() const + { + return 1e-6 * static_cast( std::chrono::duration_cast< std::chrono::microseconds >( time::now() - start ).count() ); + } +}; + +struct times : action +{ + env output; + int selected = 0; + int failures = 0; + + timer total; + + times( std::ostream & out, options option ) + : action( out ), output( out, option ), total() + { + os << std::setfill(' ') << std::fixed << std::setprecision( lest_FEATURE_TIME_PRECISION ); + } + + operator int() { return failures; } + + bool abort() { return output.abort() && failures > 0; } + + times & operator()( test testing ) + { + timer t; + + try + { + testing.behaviour( output( testing.name ) ); + } + catch( message const & ) + { + ++failures; + } + + os << std::setw(3) << ( 1000 * t.elapsed_seconds() ) << " ms: " << testing.name << "\n"; + + return *this; + } + + ~times() + { + os << "Elapsed time: " << std::setprecision(1) << total.elapsed_seconds() << " s\n"; + } +}; + +struct confirm : action +{ + env output; + int selected = 0; + int failures = 0; + + confirm( std::ostream & out, options option ) + : action( out ), output( out, option ) {} + + operator int() { return failures; } + + bool abort() { return output.abort() && failures > 0; } + + confirm & operator()( test testing ) + { + try + { + ++selected; testing.behaviour( output( testing.name ) ); + } + catch( message const & e ) + { + ++failures; report( os, e, output.context() ); + } + return *this; + } + + ~confirm() + { + if ( failures > 0 ) + { + os << failures << " out of " << selected << " selected " << pluralise("test", selected) << " " << colourise( "failed.\n" ); + } + else if ( output.pass() ) + { + os << "All " << selected << " selected " << pluralise("test", selected) << " " << colourise( "passed.\n" ); + } + } +}; + +template< typename Action > +bool abort( Action & perform ) +{ + return perform.abort(); +} + +template< typename Action > +Action && for_test( tests specification, texts in, Action && perform, int n = 1 ) +{ + for ( int i = 0; indefinite( n ) || i < n; ++i ) + { + for ( auto & testing : specification ) + { + if ( select( testing.name, in ) ) + if ( abort( perform( testing ) ) ) + return std::move( perform ); + } + } + return std::move( perform ); +} + +inline void sort( tests & specification ) +{ + auto test_less = []( test const & a, test const & b ) { return a.name < b.name; }; + std::sort( specification.begin(), specification.end(), test_less ); +} + +inline void shuffle( tests & specification, options option ) +{ + std::shuffle( specification.begin(), specification.end(), std::mt19937( option.seed ) ); +} + +// workaround MinGW bug, http://stackoverflow.com/a/16132279: + +inline int stoi( text num ) +{ + return static_cast( std::strtol( num.c_str(), nullptr, 10 ) ); +} + +inline bool is_number( text arg ) +{ + return std::all_of( arg.begin(), arg.end(), ::isdigit ); +} + +inline seed_t seed( text opt, text arg ) +{ + if ( is_number( arg ) ) + return static_cast( lest::stoi( arg ) ); + + if ( arg == "time" ) + return static_cast( std::chrono::high_resolution_clock::now().time_since_epoch().count() ); + + throw std::runtime_error( "expecting 'time' or positive number with option '" + opt + "', got '" + arg + "' (try option --help)" ); +} + +inline int repeat( text opt, text arg ) +{ + const int num = lest::stoi( arg ); + + if ( indefinite( num ) || num >= 0 ) + return num; + + throw std::runtime_error( "expecting '-1' or positive number with option '" + opt + "', got '" + arg + "' (try option --help)" ); +} + +inline auto split_option( text arg ) -> std::tuple +{ + auto pos = arg.rfind( '=' ); + + return pos == text::npos + ? std::make_tuple( arg, "" ) + : std::make_tuple( arg.substr( 0, pos ), arg.substr( pos + 1 ) ); +} + +inline auto split_arguments( texts args ) -> std::tuple +{ + options option; texts in; + + bool in_options = true; + + for ( auto & arg : args ) + { + if ( in_options ) + { + text opt, val; + std::tie( opt, val ) = split_option( arg ); + + if ( opt[0] != '-' ) { in_options = false; } + else if ( opt == "--" ) { in_options = false; continue; } + else if ( opt == "-h" || "--help" == opt ) { option.help = true; continue; } + else if ( opt == "-a" || "--abort" == opt ) { option.abort = true; continue; } + else if ( opt == "-c" || "--count" == opt ) { option.count = true; continue; } + else if ( opt == "-g" || "--list-tags" == opt ) { option.tags = true; continue; } + else if ( opt == "-l" || "--list-tests" == opt ) { option.list = true; continue; } + else if ( opt == "-t" || "--time" == opt ) { option.time = true; continue; } + else if ( opt == "-p" || "--pass" == opt ) { option.pass = true; continue; } + else if ( opt == "-z" || "--pass-zen" == opt ) { option.zen = true; continue; } + else if ( opt == "-v" || "--verbose" == opt ) { option.verbose = true; continue; } + else if ( "--version" == opt ) { option.version = true; continue; } + else if ( opt == "--order" && "declared" == val ) { /* by definition */ ; continue; } + else if ( opt == "--order" && "lexical" == val ) { option.lexical = true; continue; } + else if ( opt == "--order" && "random" == val ) { option.random = true; continue; } + else if ( opt == "--random-seed" ) { option.seed = seed ( "--random-seed", val ); continue; } + else if ( opt == "--repeat" ) { option.repeat = repeat( "--repeat" , val ); continue; } + else throw std::runtime_error( "unrecognised option '" + arg + "' (try option --help)" ); + } + in.push_back( arg ); + } + option.pass = option.pass || option.zen; + + return std::make_tuple( option, in ); +} + +inline int usage( std::ostream & os ) +{ + os << + "\nUsage: test [options] [test-spec ...]\n" + "\n" + "Options:\n" + " -h, --help this help message\n" + " -a, --abort abort at first failure\n" + " -c, --count count selected tests\n" + " -g, --list-tags list tags of selected tests\n" + " -l, --list-tests list selected tests\n" + " -p, --pass also report passing tests\n" + " -z, --pass-zen ... without expansion\n" + " -t, --time list duration of selected tests\n" + " -v, --verbose also report passing or failing sections\n" + " --order=declared use source code test order (default)\n" + " --order=lexical use lexical sort test order\n" + " --order=random use random test order\n" + " --random-seed=n use n for random generator seed\n" + " --random-seed=time use time for random generator seed\n" + " --repeat=n repeat selected tests n times (-1: indefinite)\n" + " --version report lest version and compiler used\n" + " -- end options\n" + "\n" + "Test specification:\n" + " \"@\", \"*\" all tests, unless excluded\n" + " empty all tests, unless tagged [hide] or [.optional-name]\n" +#if lest_FEATURE_REGEX_SEARCH + " \"re\" select tests that match regular expression\n" + " \"!re\" omit tests that match regular expression\n" +#else + " \"text\" select tests that contain text (case insensitive)\n" + " \"!text\" omit tests that contain text (case insensitive)\n" +#endif + ; + return 0; +} + +inline text compiler() +{ + std::ostringstream os; +#if defined (__clang__ ) + os << "clang " << __clang_version__; +#elif defined (__GNUC__ ) + os << "gcc " << __GNUC__ << "." << __GNUC_MINOR__ << "." << __GNUC_PATCHLEVEL__; +#elif defined ( _MSC_VER ) + os << "MSVC " << (_MSC_VER / 100 - 5 - (_MSC_VER < 1900)) << " (" << _MSC_VER << ")"; +#else + os << "[compiler]"; +#endif + return os.str(); +} + +inline int version( std::ostream & os ) +{ + os << "lest version " << lest_VERSION << "\n" + << "Compiled with " << compiler() << " on " << __DATE__ << " at " << __TIME__ << ".\n" + << "For more information, see https://github.com/martinmoene/lest.\n"; + return 0; +} + +inline int run( tests specification, texts arguments, std::ostream & os = std::cout ) +{ + try + { + options option; texts in; + std::tie( option, in ) = split_arguments( arguments ); + + if ( option.lexical ) { sort( specification ); } + if ( option.random ) { shuffle( specification, option ); } + + if ( option.help ) { return usage ( os ); } + if ( option.version ) { return version ( os ); } + if ( option.count ) { return for_test( specification, in, count( os ) ); } + if ( option.list ) { return for_test( specification, in, print( os ) ); } + if ( option.tags ) { return for_test( specification, in, ptags( os ) ); } + if ( option.time ) { return for_test( specification, in, times( os, option ) ); } + + return for_test( specification, in, confirm( os, option ), option.repeat ); + } + catch ( std::exception const & e ) + { + os << "Error: " << e.what() << "\n"; + return 1; + } +} + +inline int run( tests specification, int argc, char * argv[], std::ostream & os = std::cout ) +{ + return run( specification, texts( argv + 1, argv + argc ), os ); +} + +template< std::size_t N > +int run( test const (&specification)[N], texts arguments, std::ostream & os = std::cout ) +{ + std::cout.sync_with_stdio( false ); + return (std::min)( run( tests( specification, specification + N ), arguments, os ), exit_max_value ); +} + +template< std::size_t N > +int run( test const (&specification)[N], std::ostream & os = std::cout ) +{ + return run( tests( specification, specification + N ), {}, os ); +} + +template< std::size_t N > +int run( test const (&specification)[N], int argc, char * argv[], std::ostream & os = std::cout ) +{ + return run( tests( specification, specification + N ), texts( argv + 1, argv + argc ), os ); +} + +} // namespace lest + +#if defined (__clang__) +# pragma clang diagnostic pop +#elif defined (__GNUC__) +# pragma GCC diagnostic pop +#endif + +#endif // LEST_LEST_HPP_INCLUDED diff --git a/tests/run.bat b/tests/run.bat new file mode 100644 index 00000000..fe7a064a --- /dev/null +++ b/tests/run.bat @@ -0,0 +1,7 @@ +@echo off +IF %1.==. GOTO ERR +%1.exe -p +GOTO END +:ERR +echo Use: %0 [test name] +:END \ No newline at end of file diff --git a/tests/run.sh b/tests/run.sh new file mode 100755 index 00000000..0a227e83 --- /dev/null +++ b/tests/run.sh @@ -0,0 +1,6 @@ +#!/bin/bash +if [[ $# -eq 0 ]] ; then + echo Use: $0 [test name] + exit 1 +fi +./$1.test -p \ No newline at end of file diff --git a/tests/runalltest.sh b/tests/runalltest.sh new file mode 100755 index 00000000..31fb5a27 --- /dev/null +++ b/tests/runalltest.sh @@ -0,0 +1,13 @@ +#!/bin/bash +cd build/ + +rm -f *.ini + +for test_file in *.test; do + ./$test_file -p > /dev/null + if [[ $? -ne 0 ]]; then + echo "Test $test_file failed." + exit 1 + fi + echo "Test $test_file passed." +done diff --git a/tests/testcasesens.cpp b/tests/testcasesens.cpp new file mode 100644 index 00000000..ab6eec25 --- /dev/null +++ b/tests/testcasesens.cpp @@ -0,0 +1,100 @@ +#include +#include +#include +#include +#include +#include "lest.hpp" +#define MINI_CASE_SENSITIVE +#include "mini/ini.h" + +using T_LineData = std::vector; +using T_INIFileData = std::pair; + +// +// helper functions +// +bool writeTestFile(T_INIFileData const& testData) +{ + std::string const& filename = testData.first; + T_LineData const& lines = testData.second; + std::ofstream fileWriteStream(filename); + if (fileWriteStream.is_open()) + { + if (lines.size()) + { + auto it = lines.begin(); + for (;;) + { + fileWriteStream << *it; + if (++it == lines.end()) + { + break; + } + fileWriteStream << std::endl; + } + } + return true; + } + return false; +} + +void outputData(mINI::INIStructure const& ini) +{ + for (auto const& it : ini) + { + auto const& section = it.first; + auto const& collection = it.second; + std::cout << "[" << section << "]" << std::endl; + for (auto const& it2 : collection) + { + auto const& key = it2.first; + auto const& value = it2.second; + std::cout << key << "=" << value << std::endl; + } + std::cout << std::endl; + } +} + +// +// test data +// +const T_INIFileData testDataBasic = { + // filename + "data01.ini", + // test data + { + "[Fruit]", + "Bananas=1", + "APPLES=2", + } +}; + +const lest::test mINI_tests[] = { + CASE("Test: Read case sensitive") + { + // read a basic INI file and check if values are read correctly + auto const& filename = testDataBasic.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + std::cout << filename << std::endl; + outputData(ini); + EXPECT(ini["Fruit"]["Bananas"] == "1"); + EXPECT(ini["Fruit"]["APPLES"] == "2"); + EXPECT(ini["fruit"]["bananas"] == ""); + EXPECT(ini["fruit"]["apples"] == ""); + }, +}; + +int main(int argc, char** argv) +{ + // write test files + writeTestFile(testDataBasic); + + // run tests + if (int failures = lest::run(mINI_tests, argc, argv)) + { + return failures; + } + return std::cout << std::endl << "All tests passed!" << std::endl, EXIT_SUCCESS; +} \ No newline at end of file diff --git a/tests/testcopy.cpp b/tests/testcopy.cpp new file mode 100644 index 00000000..560da02c --- /dev/null +++ b/tests/testcopy.cpp @@ -0,0 +1,77 @@ +#include +#include "lest.hpp" +#include "mini/ini.h" + +bool compareData(mINI::INIStructure a, mINI::INIStructure b) +{ + for (auto const& it : a) + { + auto const& section = it.first; + auto const& collection = it.second; + if (collection.size() != b[section].size()) { + return false; + } + for (auto const& it2 : collection) + { + auto const& key = it2.first; + auto const& value = it2.second; + if (value != b[section][key]) { + return false; + } + } + std::cout << std::endl; + } + return a.size() == b.size(); +} + +void outputData(mINI::INIStructure const& ini) +{ + for (auto const& it : ini) + { + auto const& section = it.first; + auto const& collection = it.second; + std::cout << "[" << section << "]" << std::endl; + for (auto const& it2 : collection) + { + auto const& key = it2.first; + auto const& value = it2.second; + std::cout << key << "=" << value << std::endl; + } + std::cout << std::endl; + } +} + +const lest::test mINI_tests[] = { + CASE("TEST: Copy semantics") + { + mINI::INIStructure iniA; + + iniA["a"].set({ + { "x", "1" }, + { "y", "2" }, + { "z", "3" } + }); + + iniA["b"].set({ + { "q", "100" }, + { "w", "100" }, + { "e", "100" } + }); + + mINI::INIStructure iniB(iniA); + EXPECT(compareData(iniA, iniB)); + + mINI::INIStructure iniC = iniA; + EXPECT(compareData(iniA, iniC)); + } +}; + +int main(int argc, char** argv) +{ + // run tests + if (int failures = lest::run(mINI_tests, argc, argv)) + { + return failures; + } + return std::cout << std::endl << "All tests passed!" << std::endl, EXIT_SUCCESS; +} \ No newline at end of file diff --git a/tests/testgenerate.cpp b/tests/testgenerate.cpp new file mode 100644 index 00000000..3014e79c --- /dev/null +++ b/tests/testgenerate.cpp @@ -0,0 +1,287 @@ +#include +#include +#include +#include +#include +#include "lest.hpp" +#include "mini/ini.h" + +using T_LineData = std::vector; +using T_INIFileData = std::pair; + +// +// helper functions +// +bool verifyData(T_INIFileData const& testData) +{ + // compares file contents to expected data + std::string line; + std::string const& filename = testData.first; + T_LineData const& linesExpected = testData.second; + size_t lineCount = 0; + size_t lineCountExpected = linesExpected.size(); + std::ifstream fileReadStream(filename); + if (fileReadStream.is_open()) + { + while (std::getline(fileReadStream, line)) + { + if (fileReadStream.bad()) + { + return false; + } + if (lineCount >= lineCountExpected) + { + std::cout << "Line count exceeds expected!" << std::endl; + return false; + } + std::string const& lineExpected = linesExpected[lineCount++]; + if (line != lineExpected) + { + std::cout << "Line " << lineCount << " does not match expected!" << std::endl; + std::cout << "Expected: " << lineExpected << std::endl; + std::cout << "Is: " << line << std::endl; + return false; + } + } + if (lineCount < lineCountExpected) + { + std::cout << "Line count falls behind expected!" << std::endl; + } + return lineCount == lineCountExpected; + } + return false; +} + +// +// test data +// +const T_INIFileData testDataBasic = { + // filename + "data01.ini", + // expected result + { + "[section]", + "key1=value1", + "key2=value2" + } +}; + +const T_INIFileData testDataManySections = { + // filename + "data02.ini", + // expected result + { + "[section1]", + "key1=value1", + "key2=value2", + "key3=value3", + "[section2]", + "key1=value1", + "key2=value2", + "[section3]", + "key1=value1" + } +}; + +const T_INIFileData testDataEmptySection = { + // filename + "data03.ini", + // expected result + { + "[empty]" + } +}; + +const T_INIFileData testDataManyEmptySections = { + // filename + "data04.ini", + // expected result + { + "[empty1]", + "[empty2]", + "[empty3]" + } +}; + +const T_INIFileData testDataEmpty = { + // filename + "data05.ini", + // expected result + {} +}; + +const T_INIFileData testDataPrettyPrint = { + // filename + "data06.ini", + // expected result + { + "[section1]", + "key1 = value1", + "key2 = value2", + "", + "[section2]", + "key1 = value1" + } +}; + +const T_INIFileData testDataEmptyNames = { + // filename + "data07.ini", + // expected result + { + "[]", + "key=value", + "[section]", + "=value", + "[section2]", + "=" + } +}; + +const T_INIFileData testDataMalformed1 = { + // filename + "data08.ini", + // test data + { + "[[name1]", + "key=value", + "[name2]]", + "key=value", + "[[name3]]", + "key=value" + } +}; + +const T_INIFileData testDataMalformed2 = { + // filename + "data09.ini", + // test data + { + "[name]", + "\\===", // key: "=" value: "=" + "a\\= \\===b", // key: "a= =" value: "=b" + "c\\= \\===d" // key: "c= =" value: "=d" + } +}; + +// +// test cases +// +const lest::test mINI_tests[] = { + CASE("Test: Basic generate") + { + // create a very basic INI file and verify resulting file has correct data + std::string const& filename = testDataBasic.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + ini["section"]["key1"] = "value1"; + ini["section"]["key2"] = "value2"; + EXPECT(file.generate(ini) == true); + EXPECT(verifyData(testDataBasic) == true); + }, + CASE("Test: Generate many sections") + { + std::string const& filename = testDataManySections.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + ini["section1"].set({ + {"key1", "value1"}, + {"key2", "value2"}, + {"key3", "value3"} + }); + ini["section2"].set({ + {"key1", "value1"}, + {"key2", "value2"} + }); + ini["section3"]["key1"] = "value1"; + EXPECT(file.generate(ini) == true); + EXPECT(verifyData(testDataManySections)); + }, + CASE("Test: Generate empty section") + { + std::string const& filename = testDataEmptySection.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + ini["empty"]; + EXPECT(file.generate(ini) == true); + EXPECT(verifyData(testDataEmptySection)); + }, + CASE("Test: Generate many empty sections") + { + std::string const& filename = testDataManyEmptySections.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + ini["empty1"]; + ini["empty2"]; + ini["empty3"]; + EXPECT(file.generate(ini) == true); + EXPECT(verifyData(testDataManyEmptySections)); + }, + CASE("Test: Generate empty file") + { + std::string const& filename = testDataEmpty.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.generate(ini) == true); + EXPECT(verifyData(testDataEmpty)); + }, + CASE("Test: Generate with pretty-print") + { + std::string const& filename = testDataPrettyPrint.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + ini["section1"].set({ + {"key1", "value1"}, + {"key2", "value2"}, + }); + ini["section2"]["key1"] = "value1"; + EXPECT(file.generate(ini, true) == true); + EXPECT(verifyData(testDataPrettyPrint)); + }, + CASE("Test: Generate empty section and key names") + { + std::string const& filename = testDataEmptyNames.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + ini[""]["key"] = "value"; + ini["section"][""] = "value"; + ini["section2"][""] = ""; + EXPECT(file.generate(ini) == true); + EXPECT(verifyData(testDataEmptyNames)); + }, + CASE("Test: Generate malformed section names") + { + std::string const& filename = testDataMalformed1.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + ini["[name1"]["key"] = "value"; + ini["name2]"]["key"] = "value"; + ini["[name3]"]["key"] = "value"; + EXPECT(file.generate(ini) == true); + EXPECT(verifyData(testDataMalformed1)); + }, + CASE("Test: Generate malformed key names") + { + std::string const& filename = testDataMalformed2.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + ini["name"].set({ + {"=", "="}, + {"a= =", "=b"}, + {" c= = ", " =d "} + }); + EXPECT(file.generate(ini) == true); + EXPECT(verifyData(testDataMalformed2)); + } +}; + +int main(int argc, char** argv) +{ + // run tests + if (int failures = lest::run(mINI_tests, argc, argv)) + { + return failures; + } + return std::cout << std::endl << "All tests passed!" << std::endl, EXIT_SUCCESS; +} + diff --git a/tests/testhuge.cpp b/tests/testhuge.cpp new file mode 100644 index 00000000..f6c16471 --- /dev/null +++ b/tests/testhuge.cpp @@ -0,0 +1,59 @@ +/* use testhuge -t to time tests */ + +#include +#include "lest.hpp" +#include "mini/ini.h" + +const std::string filename = "data_huge.ini"; + +const size_t N_sections = 20; +const size_t N_items_per_section = 500; + +const bool generate_pretty = false; + +const lest::test mINI_tests[] = { + CASE("TEST: Generate a huge file") + { + std::cout << "Writing file..." << std::endl; + mINI::INIFile file(filename); + mINI::INIStructure ini; + // generate data + for (unsigned int i = 1; i <= N_sections; ++i) + { + std::string section = "section" + std::to_string(i); + for (unsigned int j = 1; j <= N_items_per_section; ++j) + { + std::string j_str = std::to_string(j); + std::string key = "key" + j_str; + std::string value = "value" + j_str; + ini[section][key] = value; + } + } + // generate file + EXPECT(file.generate(ini) == true); + }, + CASE("TEST: Read a huge file") + { + // this testcase relies on the previous test passing + std::cout << "Reading file..." << std::endl; + mINI::INIFile file(filename); + mINI::INIStructure ini; + // read from file + EXPECT(file.read(ini) == true); + EXPECT(ini.size() == N_sections); + for (auto& it : ini) + { + EXPECT(it.second.size() == N_items_per_section); + } + } +}; + +int main(int argc, char** argv) +{ + // run tests + if (int failures = lest::run(mINI_tests, argc, argv)) + { + return failures; + } + return std::cout << std::endl << "All tests passed!" << std::endl, EXIT_SUCCESS; +} diff --git a/tests/testpath.cpp b/tests/testpath.cpp new file mode 100644 index 00000000..3823b617 --- /dev/null +++ b/tests/testpath.cpp @@ -0,0 +1,312 @@ +#include +#include +#include +#include +#include +#include +#include "lest.hpp" +#include "mini/ini.h" + +using T_LineData = std::vector; +using T_INIFileData = std::tuple; + +// +// helper functions +// +bool writeTestFile(T_INIFileData const& testData) +{ + auto const& filename = std::get<0>(testData); + T_LineData const& lines = std::get<1>(testData); + std::ofstream fileWriteStream(filename); + if (fileWriteStream.is_open()) + { + if (lines.size()) + { + auto it = lines.begin(); + for (;;) + { + fileWriteStream << *it; + if (++it == lines.end()) + { + break; + } + fileWriteStream << std::endl; + } + } + return true; + } + return false; +} + +bool verifyData(T_INIFileData const& testData) +{ + // compares file contents to expected data + std::string line; + auto const& filename = std::get<0>(testData); + T_LineData const& linesExpected = std::get<2>(testData); + size_t lineCount = 0; + size_t lineCountExpected = linesExpected.size(); + std::ifstream fileReadStream(filename); + if (fileReadStream.is_open()) + { + while (std::getline(fileReadStream, line)) + { + if (fileReadStream.bad()) + { + return false; + } + if (lineCount >= lineCountExpected) + { + std::cout << "Line count exceeds expected!" << std::endl; + return false; + } + std::string const& lineExpected = linesExpected[lineCount++]; + if (line != lineExpected) + { + std::cout << "Line " << lineCount << " does not match expected!" << std::endl; + std::cout << "Expected: " << lineExpected << std::endl; + std::cout << "Is: " << line << std::endl; + return false; + } + } + if (lineCount < lineCountExpected) + { + std::cout << "Line count falls behind expected!" << std::endl; + } + return lineCount == lineCountExpected; + } + return false; +} + +void outputData(mINI::INIStructure const& ini) +{ + for (auto const& it : ini) + { + auto const& section = it.first; + auto const& collection = it.second; + std::cout << "[" << section << "]" << std::endl; + for (auto const& it2 : collection) + { + auto const& key = it2.first; + auto const& value = it2.second; + std::cout << key << "=" << value << std::endl; + } + std::cout << std::endl; + } +} + +// +// test data +// +const T_INIFileData testDataStdString { + // filename + "data_stdstring.ini", + // original data + { + "[fruit]", + "bananas=1", + "apples=2", + "grapes=3", + }, + // expected result + { + "[fruit]", + "bananas=2", + "apples=3", + "grapes=4", + } +}; + +const T_INIFileData testDataUTF16JP { + // filename + u"data_u16テスト.ini", + // original data + { + "[fruit]", + "bananas=1", + "apples=2", + "grapes=3", + }, + // expected result + { + "[fruit]", + "bananas=2", + "apples=3", + "grapes=4", + } +}; + +const T_INIFileData testDataUTF16TC { + // filename + u"data_u16測試.ini", + // original data + { + "[fruit]", + "bananas=1", + "apples=2", + "grapes=3", + }, + // expected result + { + "[fruit]", + "bananas=2", + "apples=3", + "grapes=4", + } +}; + +const T_INIFileData testDataUTF32JP { + // filename + U"data_u32テスト.ini", + // original data + { + "[fruit]", + "bananas=1", + "apples=2", + "grapes=3", + }, + // expected result + { + "[fruit]", + "bananas=2", + "apples=3", + "grapes=4", + } +}; + +const T_INIFileData testDataUTF32TC { + // filename + U"data_u32測試.ini", + // original data + { + "[fruit]", + "bananas=1", + "apples=2", + "grapes=3", + }, + // expected result + { + "[fruit]", + "bananas=2", + "apples=3", + "grapes=4", + } +}; + +// +// test cases +// +const lest::test mINI_tests[] = { + CASE("TEST: std::string read/write") + { + // read a INI file with std::string and check if values are read correctly + auto const& filename = std::get<0>(testDataStdString); + mINI::INIFile file(filename.string()); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + EXPECT(ini["fruit"]["bananas"] == "1"); + EXPECT(ini["fruit"]["apples"] == "2"); + EXPECT(ini["fruit"]["grapes"] == "3"); + // update data + ini["fruit"]["bananas"] = "2"; + ini["fruit"]["apples"] = "3"; + ini["fruit"]["grapes"] = "4"; + // write to file + EXPECT(file.write(ini) == true); + // verify data + EXPECT(verifyData(testDataStdString)); + }, + CASE("TEST: utf-16 jp read/write") + { + // read a INI file with utf-16 jp path and check if values are read correctly + auto const& filename = std::get<0>(testDataUTF16JP); + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + EXPECT(ini["fruit"]["bananas"] == "1"); + EXPECT(ini["fruit"]["apples"] == "2"); + EXPECT(ini["fruit"]["grapes"] == "3"); + // update data + ini["fruit"]["bananas"] = "2"; + ini["fruit"]["apples"] = "3"; + ini["fruit"]["grapes"] = "4"; + // write to file + EXPECT(file.write(ini) == true); + // verify data + EXPECT(verifyData(testDataUTF16JP)); + }, + CASE("TEST: utf-16 tc read/write") + { + // read a INI file with utf-16 tc path and check if values are read correctly + auto const& filename = std::get<0>(testDataUTF16TC); + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + EXPECT(ini["fruit"]["bananas"] == "1"); + EXPECT(ini["fruit"]["apples"] == "2"); + EXPECT(ini["fruit"]["grapes"] == "3"); + // update data + ini["fruit"]["bananas"] = "2"; + ini["fruit"]["apples"] = "3"; + ini["fruit"]["grapes"] = "4"; + // write to file + EXPECT(file.write(ini) == true); + // verify data + EXPECT(verifyData(testDataUTF16TC)); + }, + CASE("TEST: utf-32 jp read/write") + { + // read a INI file with utf-32 jp path and check if values are read correctly + auto const& filename = std::get<0>(testDataUTF32JP); + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + EXPECT(ini["fruit"]["bananas"] == "1"); + EXPECT(ini["fruit"]["apples"] == "2"); + EXPECT(ini["fruit"]["grapes"] == "3"); + // update data + ini["fruit"]["bananas"] = "2"; + ini["fruit"]["apples"] = "3"; + ini["fruit"]["grapes"] = "4"; + // write to file + EXPECT(file.write(ini) == true); + // verify data + EXPECT(verifyData(testDataUTF32JP)); + }, + CASE("TEST: utf-32 tc read/write") + { + // read a INI file with utf-32 tc path and check if values are read correctly + auto const& filename = std::get<0>(testDataUTF32TC); + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + EXPECT(ini["fruit"]["bananas"] == "1"); + EXPECT(ini["fruit"]["apples"] == "2"); + EXPECT(ini["fruit"]["grapes"] == "3"); + // update data + ini["fruit"]["bananas"] = "2"; + ini["fruit"]["apples"] = "3"; + ini["fruit"]["grapes"] = "4"; + // write to file + EXPECT(file.write(ini) == true); + // verify data + EXPECT(verifyData(testDataUTF32TC)); + }, +}; + +int main(int argc, char** argv) +{ + // write test files + writeTestFile(testDataStdString); + writeTestFile(testDataUTF16JP); + writeTestFile(testDataUTF16TC); + writeTestFile(testDataUTF32JP); + writeTestFile(testDataUTF32TC); + + // run tests + if (int failures = lest::run(mINI_tests, argc, argv)) + { + return failures; + } + return std::cout << std::endl << "All tests passed!" << std::endl, EXIT_SUCCESS; +} diff --git a/tests/testread.cpp b/tests/testread.cpp new file mode 100644 index 00000000..16413b5d --- /dev/null +++ b/tests/testread.cpp @@ -0,0 +1,457 @@ +#include +#include +#include +#include +#include +#include "lest.hpp" +#include "mini/ini.h" + +using T_LineData = std::vector; +using T_INIFileData = std::pair; + +// +// helper functions +// +bool writeTestFile(T_INIFileData const& testData) +{ + std::string const& filename = testData.first; + T_LineData const& lines = testData.second; + std::ofstream fileWriteStream(filename); + if (fileWriteStream.is_open()) + { + if (lines.size()) + { + auto it = lines.begin(); + for (;;) + { + fileWriteStream << *it; + if (++it == lines.end()) + { + break; + } + fileWriteStream << std::endl; + } + } + return true; + } + return false; +} + +void outputData(mINI::INIStructure const& ini) +{ + for (auto const& it : ini) + { + auto const& section = it.first; + auto const& collection = it.second; + std::cout << "[" << section << "]" << std::endl; + for (auto const& it2 : collection) + { + auto const& key = it2.first; + auto const& value = it2.second; + std::cout << key << "=" << value << std::endl; + } + std::cout << std::endl; + } +} + +// +// test data +// +const T_INIFileData testDataBasic = { + // filename + "data01.ini", + // test data + { + "[fruit]", + "bananas=1", + "apples=2", + "grapes=3", + "[veggies]", + "lettuce=scarce", + "onions=sufficient", + "potatoes=plentiful", + "[section with spaces]", + "key with spaces = value with spaces" + } +}; + +const T_INIFileData testDataWellFormed = { + // filename + "data02.ini", + // test data + { + "; this is a comment", + "[first section]", + "someKey = 1", + "anotherKey = 2", + "", + "; this is another comment", + "[second section]", + "humans = 16", + "orcs = 8", + "barbarians = 20", + "", + "; this is yet another comment", + "[third section]", + "spiders=25", + "bugs=0", + "ants=100", + "flies=5" + } +}; + +const T_INIFileData testDataNotWellFormed = { + // filename + "data03.ini", + // test data + { + "GARBAGE", + "; this is a comment", + " ; abcd ", + "[first section] ;test comment", + "someKey= 1", + "GARBAGE", + "anotherKey =2", + "", + "; this is another comment", + "GARBAGE", + "GARBAGE", + " [second section]", + "GARBAGE", + "hUmAnS=16", + "GARBAGE", + " orcs = 8 ", + " barbarians = 20 ", + "", + " GARBAGE", + "; this is yet another comment", + "[ third section ] ;;; test comment[]]] ", + "spiders = 25", + "bugs =0 ", + "ants= 100", + "GARBAGE ", + "GARBAGE ", + "FLIES = 5", + "GARBAGE " + } +}; + +const T_INIFileData testDataEmpty = { + // filename + "data04.ini", + // test data + {} +}; + +const T_INIFileData testDataEdgeCase1 = { + // filename + "data05.ini", + // test data + { + "ignored1=value1", + "ignored2=value2" + } +}; + +const T_INIFileData testDataEdgeCase2 = { + // filename + "data06.ini", + // test data + { + "ignored1=value1", + "ignored2=value2", + "[data]", + "proper1=a", + "proper2=b" + } +}; + +const T_INIFileData testDataEdgeCase3 = { + // filename + "data07.ini", + // test data + { + "[empty]" + } +}; + +const T_INIFileData testDataEdgeCase4 = { + // filename + "data08.ini", + // test data + { + "[empty1]", + "[empty2]", + "[empty3]", + "[empty4]", + "[empty5]" + } +}; + +const T_INIFileData testDataEdgeCase5 = { + // filename + "data09.ini", + // test data + { + "[data]", + "valueA=1", // expected: ignored + "valueA=2" // expected: not ignored + } +}; + +const T_INIFileData testDataEdgeCase6 = { + // filename + "data10.ini", + // test data + { + "[data]", // expected: ignored + "valueA=10", // expected: ignored + "valueB=20", // expected: not ignored + "[data]", + "valueA=30" // expected: not ignored + } +}; + +const T_INIFileData testDataEdgeCase7 = { + // filename + "data11.ini", + // test data + { + "[]", + "key1=value1", + "[a]", + "key2=value2" + } +}; + +const T_INIFileData testDataEdgeCase8 = { + // filename + "data12.ini", + // test data + { + "[a]", + "=1" + } +}; + +const T_INIFileData testDataMalformed1 = { + // filename + "data13.ini", + // test data + { + "[[name1]", + "key=value", + "[name2]]", + "key=value", + "[[name3]]", + "key=value" + } +}; + +const T_INIFileData testDataMalformed2 = { + // filename + "data14.ini", + // test data + { + "[name]", + "key\\==value", + "\\===", // expected key: "=" expected value: "=" + "a\\= \\===b", // expected key: "a= =" expected value: "=b" + " c\\= \\= = =d " // expected key: "c= =" expected value: "=d" + } +}; + +// +// test cases +// +const lest::test mINI_tests[] = { + CASE("Test: Basic read") + { + // read a basic INI file and check if values are read correctly + auto const& filename = testDataBasic.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + std::cout << filename << std::endl; + outputData(ini); + EXPECT(ini["fruit"]["bananas"] == "1"); + EXPECT(ini["fruit"]["apples"] == "2"); + EXPECT(ini["fruit"]["grapes"] == "3"); + EXPECT(ini["veggies"]["lettuce"] == "scarce"); + EXPECT(ini["veggies"]["onions"] == "sufficient"); + EXPECT(ini["veggies"]["potatoes"] == "plentiful"); + EXPECT(ini["section with spaces"]["key with spaces"] == "value with spaces"); + }, + CASE("Test: Read and compare") + { + // read two INI files with differing form and check if all read values match + auto const& filename1 = testDataWellFormed.first; + auto const& filename2 = testDataNotWellFormed.first; + mINI::INIFile file1(filename1); + mINI::INIFile file2(filename2); + mINI::INIStructure ini1, ini2; + EXPECT(file1.read(ini1) == true); + EXPECT(file2.read(ini2) == true); + std::cout << filename1 << std::endl; + outputData(ini1); + std::cout << filename2 << std::endl; + outputData(ini2); + // compare sizes + EXPECT(ini1.size() == ini2.size()); + EXPECT(ini1.get("first section").size() == ini2.get("first section").size()); + EXPECT(ini1.get("second section").size() == ini2.get("second section").size()); + EXPECT(ini1.get("third section").size() == ini2.get("third section").size()); + // compare data + EXPECT(ini1.get("first section").get("somekey") == ini2.get("first section").get("somekey")); + EXPECT(ini1["first section"]["anotherkey"] == ini2["first section"]["anotherkey"]); + EXPECT(ini1["second section"]["humans"] == ini2["second section"]["humans"]); + EXPECT(ini1["second section"]["orcs"] == ini2["second section"]["orcs"]); + EXPECT(ini1["second section"]["barbarians"] == ini2["second section"]["barbarians"]); + EXPECT(ini1["third section"]["spiders"] == ini2["third section"]["spiders"]); + EXPECT(ini1["third section"]["bugs"] == ini2["third section"]["bugs"]); + EXPECT(ini1["third section"]["ants"] == ini2["third section"]["ants"]); + EXPECT(ini1["third section"]["flies"] == ini2["third section"]["flies"]); + }, + CASE("Test: Read empty") + { + auto const& filename = testDataEmpty.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + EXPECT(ini.size() == 0u); + }, + CASE("Test: Edge case 1") + { + auto const& filename = testDataEdgeCase1.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + EXPECT(ini.size() == 0u); + }, + CASE("Test: Edge case 2") + { + auto const& filename = testDataEdgeCase2.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + EXPECT(ini.size() == 1u); + EXPECT(ini.get("data").size() == 2u); + EXPECT(ini["data"]["proper1"] == "a"); + EXPECT(ini["data"]["proper2"] == "b"); + }, + CASE("Test: Edge case 3") + { + auto const& filename = testDataEdgeCase3.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + EXPECT(ini.size() == 1u); + EXPECT(ini.get("empty").size() == 0u); + }, + CASE("Test: Edge case 4") + { + auto const& filename = testDataEdgeCase4.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + EXPECT(ini.size() == 5u); + EXPECT(ini.get("empty1").size() == 0u); + EXPECT(ini.get("empty2").size() == 0u); + EXPECT(ini.get("empty3").size() == 0u); + EXPECT(ini.get("empty4").size() == 0u); + EXPECT(ini.get("empty5").size() == 0u); + }, + CASE("Test: Edge case 5") + { + auto const& filename = testDataEdgeCase5.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + EXPECT(ini.size() == 1u); + EXPECT(ini.get("data").size() == 1u); + EXPECT(ini["data"]["valueA"] == "2"); + }, + CASE("Test: Edge case 6") + { + auto const& filename = testDataEdgeCase6.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + EXPECT(ini.size() == 1u); + EXPECT(ini.get("data").size() == 2u); + EXPECT(ini["data"]["valueA"] == "30"); + EXPECT(ini["data"]["valueB"] == "20"); + }, + CASE("Test: Edge case 7") + { + auto const& filename = testDataEdgeCase7.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + EXPECT(ini.size() == 2u); + EXPECT(ini.get("").size() == 1u); + EXPECT(ini.get("").get("key1") == "value1"); + EXPECT(ini.get("a").size() == 1u); + EXPECT(ini["a"]["key2"] == "value2"); + }, + CASE("Test: Edge case 8") + { + auto const& filename = testDataEdgeCase8.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + EXPECT(ini.size() == 1u); + EXPECT(ini.get("a").size() == 1u); + EXPECT(ini["a"][""] == "1"); + }, + CASE("Test: Malformed case 1") + { + // read INI with malformed section names + auto const& filename = testDataMalformed1.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + EXPECT(ini.size() == 3u); + EXPECT(ini.get("[name1").size() == 1u); + EXPECT(ini.get("name2]").size() == 1u); + EXPECT(ini.get("[name3]").size() == 1u); + }, + CASE("Test: Malformed case 2") + { + // read INI with malformed key names + auto const& filename = testDataMalformed2.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + EXPECT(ini.get("name").get("key=") == "value"); + EXPECT(ini.get("name").get("=") == "="); + EXPECT(ini.get("name").get("a= =") == "=b"); + EXPECT(ini.get("name").get("c= =") == "=d"); + } +}; + +int main(int argc, char** argv) +{ + // write test files + writeTestFile(testDataBasic); + writeTestFile(testDataWellFormed); + writeTestFile(testDataNotWellFormed); + writeTestFile(testDataEmpty); + writeTestFile(testDataEdgeCase1); + writeTestFile(testDataEdgeCase2); + writeTestFile(testDataEdgeCase3); + writeTestFile(testDataEdgeCase4); + writeTestFile(testDataEdgeCase5); + writeTestFile(testDataEdgeCase6); + writeTestFile(testDataEdgeCase7); + writeTestFile(testDataEdgeCase8); + writeTestFile(testDataMalformed1); + writeTestFile(testDataMalformed2); + + // run tests + if (int failures = lest::run(mINI_tests, argc, argv)) + { + return failures; + } + return std::cout << std::endl << "All tests passed!" << std::endl, EXIT_SUCCESS; +} diff --git a/tests/testutf8.cpp b/tests/testutf8.cpp new file mode 100644 index 00000000..bac7a35c --- /dev/null +++ b/tests/testutf8.cpp @@ -0,0 +1,145 @@ +#include +#include +#include +#include +#include +#include "lest.hpp" +#include "mini/ini.h" + +using T_LineData = std::vector; +using T_INIFileData = std::pair; + +// +// helper functions +// +void outputData(mINI::INIStructure const& ini) +{ + for (auto const& it : ini) + { + auto const& section = it.first; + auto const& collection = it.second; + std::cout << "[" << section << "]" << std::endl; + for (auto const& it2 : collection) + { + auto const& key = it2.first; + auto const& value = it2.second; + std::cout << key << "=" << value << std::endl; + } + std::cout << std::endl; + } +} + +bool writeTestFile(T_INIFileData const& testData) +{ + std::string const& filename = testData.first; + T_LineData const& lines = testData.second; + std::ofstream fileWriteStream(filename); + if (fileWriteStream.is_open()) + { + const char utf8_BOM[3] = { + static_cast(0xEF), + static_cast(0xBB), + static_cast(0xBF) + }; + fileWriteStream.write(utf8_BOM, 3); + + if (lines.size()) + { + auto it = lines.begin(); + for (;;) + { + fileWriteStream << *it; + if (++it == lines.end()) + { + break; + } + fileWriteStream << std::endl; + } + } + return true; + } + return false; +} + +// +// test data +// +const T_INIFileData testDataUTF8BOM = { + // filename + "utf8bom.ini", + // test data + { + "[section]", + "key=value", + "key2=value2", + "[section2]", + "key=value" + } +}; + +// +// test cases +// +const lest::test mINI_tests[] = { + CASE("Test: Write and read back utf-8 values") + { + mINI::INIFile file("data01.ini"); + mINI::INIStructure ini; + ini["section"]["key"] = "€"; + ini["section"]["€"] = "value"; + ini["€"]["key"] = "value"; + ini["section"]["key2"] = "𐍈"; + ini["section"]["𐍈"] = "value"; + ini["𐍈"]["key"] = "value"; + ini["section"]["key3"] = "你好"; + ini["section"]["你好"] = "value"; + ini["你好"]["key"] = "value"; + EXPECT(file.write(ini) == true); + ini.clear(); + EXPECT(file.read(ini) == true); + outputData(ini); + EXPECT(ini["section"]["key"] == "€"); + EXPECT(ini["section"]["key2"] == "𐍈"); + EXPECT(ini["section"]["€"] == "value"); + EXPECT(ini["€"]["key"] == "value"); + EXPECT(ini["section"]["𐍈"] == "value"); + EXPECT(ini["𐍈"]["key"] == "value"); + EXPECT(ini["section"]["key3"] == "你好"); + EXPECT(ini["section"]["你好"] == "value"); + EXPECT(ini["你好"]["key"] == "value"); + }, + CASE("Test: UTF-8 BOM encoded file") + { + const auto& filename = testDataUTF8BOM.first; + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + EXPECT(ini["section"]["key"] == "value"); + EXPECT(ini["section"]["key2"] == "value2"); + EXPECT(ini["section2"]["key"] == "value"); + // update + ini["section"]["key"] = "something else"; + // write + EXPECT(file.write(ini) == true); + // expect BOM encoding + mINI::INIReader testReader(filename); + mINI::INIStructure testStructure; + testReader >> testStructure; + EXPECT(testReader.isBOM == true); + // verify data + EXPECT(testStructure["section"]["key"] == "something else"); + } +}; + +int main(int argc, char** argv) +{ + // write test files + writeTestFile(testDataUTF8BOM); + // run tests + if (int failures = lest::run(mINI_tests, argc, argv)) + { + return failures; + } + return std::cout << std::endl << "All tests passed!" << std::endl, EXIT_SUCCESS; +} + diff --git a/tests/testwrite.cpp b/tests/testwrite.cpp new file mode 100644 index 00000000..b4b0f0f7 --- /dev/null +++ b/tests/testwrite.cpp @@ -0,0 +1,747 @@ +#include +#include +#include +#include +#include +#include "lest.hpp" +#include "mini/ini.h" + +using T_LineData = std::vector; +using T_INIFileData = std::tuple; + +// +// helper functions +// +bool writeTestFile(T_INIFileData const& testData) +{ + std::string const& filename = std::get<0>(testData); + T_LineData const& lines = std::get<1>(testData); + std::ofstream fileWriteStream(filename); + if (fileWriteStream.is_open()) + { + if (lines.size()) + { + auto it = lines.begin(); + for (;;) + { + fileWriteStream << *it; + if (++it == lines.end()) + { + break; + } + fileWriteStream << std::endl; + } + } + return true; + } + return false; +} + +bool verifyData(T_INIFileData const& testData) +{ + // compares file contents to expected data + std::string line; + std::string const& filename = std::get<0>(testData); + T_LineData const& linesExpected = std::get<2>(testData); + size_t lineCount = 0; + size_t lineCountExpected = linesExpected.size(); + std::ifstream fileReadStream(filename); + if (fileReadStream.is_open()) + { + while (std::getline(fileReadStream, line)) + { + if (fileReadStream.bad()) + { + return false; + } + if (lineCount >= lineCountExpected) + { + std::cout << "Line count exceeds expected!" << std::endl; + return false; + } + std::string const& lineExpected = linesExpected[lineCount++]; + if (line != lineExpected) + { + std::cout << "Line " << lineCount << " does not match expected!" << std::endl; + std::cout << "Expected: " << lineExpected << std::endl; + std::cout << "Is: " << line << std::endl; + return false; + } + } + if (lineCount < lineCountExpected) + { + std::cout << "Line count falls behind expected!" << std::endl; + } + return lineCount == lineCountExpected; + } + return false; +} + +void outputData(mINI::INIStructure const& ini) +{ + for (auto const& it : ini) + { + auto const& section = it.first; + auto const& collection = it.second; + std::cout << "[" << section << "]" << std::endl; + for (auto const& it2 : collection) + { + auto const& key = it2.first; + auto const& value = it2.second; + std::cout << key << "=" << value << std::endl; + } + std::cout << std::endl; + } +} + +// +// test data +// +const T_INIFileData testDataBasic { + // filename + "data01.ini", + // original data + { + ";some comment", + "[some section]", + "some key=1", + "other key=2" + }, + // expected result + { + ";some comment", + "[some section]", + "some key=2", + "other key=2", + "yet another key=3" + } +}; + +const T_INIFileData testDataWithGarbage { + // filename + "data02.ini", + // original data + { + "", + " GARBAGE ; ALSO GARBAGE ", + ";;;;;;;;;;;;;;;comment comment", + ";;;;", + ";;;; ", + " ;", + " ;; ;;xxxx ", + "ignored key = ignored value", + "ignored=ignored", + "GARBAGE", + "", + "ignored key2=ignored key2", + "GARBAGE ;;;;;;;;;;;;;;;;;;;;;", + "[section] ; trailing comment", + "", + "GARBAGE", + ";;", + "a=1", + "b = 2", + "c =3", + "d= 4", + "e = 5", + "f =6", + "", + "@#%$@(*(!@*@GARBAGE#!@GARBAGE%$@#GARBAGE%@&*%@$", + "GARBAGE", + "[other section] ;also a comment", + "GARBAGE", + "dinosaurs= 16", + "GARBAGE", + "birds= 123456", + "giraffes= 22", + "GARBAGE", + "[extra section];also a comment", + " aaa = 1", + " bbb=2", + " ccc = 3", + "GARBAGE", + "", + "" + }, + // expected result + { + "", + ";;;;;;;;;;;;;;;comment comment", + ";;;;", + ";;;; ", + " ;", + " ;; ;;xxxx ", + "", + "[section] ; trailing comment", + "", + ";;", + "a=2", + "b = 3", + "c =4", + "d= 5", + "e = 6", + "f =7", + "g=8", + "", + "[other section] ;also a comment", + "birds= 123456", + "giraffes= 22", + "[extra section];also a comment", + " aaa = 2", + " bbb=3", + " ccc = 4", + "ddd=5", + "", + "", + "[new section]", + "test=something" + } +}; + +const T_INIFileData testDataRemEntry { + // filename + "data04.ini", + // original data + { + "[section]", + "data1=A", + "data2=B" + }, + // expected result + { + "[section]", + "data2=B" + } +}; + +const T_INIFileData testDataRemSection { + // filename + "data05.ini", + // original data + { + "[section]", + "data1=A", + "data2=B" + }, + // expected result + { + } +}; + +const T_INIFileData testDataDuplicateKeys { + // filename + "data06.ini", + // original data + { + "[section]", + "data=A", + "data=B", + "[section]", + "data=C" + }, + // expected result + { + "[section]", + "data=D", + "data=D", + "[section]", + "data=D" + } +}; + +const T_INIFileData testDataDuplicateSections { + // filename + "data07.ini", + // original data + { + "[section]", + "[section]", + "[section]" + }, + // expected result + { + "[section]", + "data=A", + "[section]", + "data=A", + "[section]", + "data=A" + } +}; + +const T_INIFileData testDataPrettyPrint { + // filename + "data08.ini", + // oiriginal data + { + "[section1]", + "value1=A", + "value2=B", + "[section2]", + "value1=A" + }, + // expected result + { + "[section1]", + "value1=A", + "value2=B", + "value3 = C", + "[section2]", + "value1=A", + "value2 = B", + "", + "[section3]", + "value1 = A", + "value2 = B" + } +}; + +const T_INIFileData testDataEmptyFile { + // filename + "data09.ini", + // original data + {}, + // expected results + { + "[section]", + "key=value" + } +}; + +const T_INIFileData testDataEmptySection { + // filename + "data10.ini", + // original data + { + "[section]" + }, + // expected result + { + "[section]", + "key=value" + } +}; + +const T_INIFileData testDataManyEmptySections { + // filename + "data11.ini", + // original data + { + "[section1]", + "[section2]", + "[section3]", + "[section4]", + "[section5]" + }, + // expected result + { + "[section1]", + "[section2]", + "[section3]", + "key=value", + "[section4]", + "[section5]" + } +}; + +const T_INIFileData testDataEmptyNames { + // filename + "data12.ini", + // original data + { + "[]", + "key=value", + "key2=value2", + "[section]", + "=value" + }, + // expected result + { + "[]", + "key=", + "=value3", + "[section]", + "=value2", + "[section2]", + "=" + } +}; + +const T_INIFileData testDataMalformed1 { + // filename + "data13.ini", + // original data + { + "[[name1]", + "key=value", + "[name2]]", + "key=value", + "[[name3]]", + "key=value" + }, + // expected result + { + "[[name1]", + "key=value1", + "[name2]]", + "key=value2", + "[[name3]]", + "key=value3" + } +}; + +const T_INIFileData testDataMalformed2 { + // filename + "data14.ini", + // original data + { + "[name]", + "\\===", // key: "=" value: "=" + "a\\= \\===b", // key: "a= =" value: "=b" + "c\\= \\===d" // key: "c= =" value: "=d" + }, + // expected result + { + "[name]", + "\\====", // key: "=" value: "==" + "a\\= \\===bb", // key: "a= =" value: "=bb" + "e\\===f=", // key: "e=" value: "=f=", + "[other]", + "\\===" + } +}; + +const T_INIFileData testDataConsecutiveWrites { + // filename + "data15.ini", + // original data + { + "[ Food ]", + "Cheese = 32", + "Ice Cream = 64", + "Bananas = 128", + "", + "[ Things ]", + "Scissors = AAA", + "Wooden Box = BBB", + "Speakers = CCC" + }, + // expected result + { + "[ Food ]", + "Cheese = AAA", + "Ice Cream = BBB", + "Bananas = CCC", + "soup=DDD", + "", + "[ Things ]", + "Scissors = 32", + "Wooden Box = 64", + "Speakers = 128", + "book=256" + } +}; + +const T_INIFileData testDataEmptyValues { + // filename + "data16.ini", + // original data + { + "[section]", + "key=value" + }, + // expected result + { + "[section]", + "key=" + } +}; + +// +// test cases +// +const lest::test mINI_tests[] = { + CASE("TEST: Basic write") + { + // do some basic modifications to an INI file + // then compare resulting file to expected data + std::string const& filename = std::get<0>(testDataBasic); + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + ini["some section"]["some key"] = "2"; + ini["some section"]["yet another key"] = "3"; + EXPECT(file.write(ini) == true); + EXPECT(verifyData(testDataBasic)); + }, + CASE("TEST: Garbage data") + { + auto const& filename = std::get<0>(testDataWithGarbage); + // read from file + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + // update data + ini["section"].set({ + {"a", "2"}, + {"b", "3"}, + {"c", "4"}, + {"d", "5"}, + {"e", "6"}, + {"f", "7"}, + {"g", "8"} + }); + ini["other section"].remove("dinosaurs"); // sorry, dinosaurs + ini["extra section"].set({ + {"aaa", "2"}, + {"bbb", "3"}, + {"ccc", "4"}, + {"ddd", "5"} + }); + ini["new section"]["test"] = "something"; + // write to file + EXPECT(file.write(ini) == true); + // verify data + EXPECT(verifyData(testDataWithGarbage)); + }, + CASE("Test: Remove entry") + { + auto const& filename = std::get<0>(testDataRemEntry); + // read from file + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + // update data + ini["section"].remove("data1"); + // write to file + EXPECT(file.write(ini) == true); + // verify data + EXPECT(verifyData(testDataRemEntry)); + }, + CASE("Test: Remove section") + { + auto const& filename = std::get<0>(testDataRemSection); + // read from file + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + // update data + ini.remove("section"); + // write to file + EXPECT(file.write(ini) == true); + // verify data + EXPECT(verifyData(testDataRemSection)); + }, + CASE("Test: Duplicate keys") + { + auto const& filename = std::get<0>(testDataDuplicateKeys); + // read from file + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + // update data + ini["section"]["data"] = "D"; + // write to file + EXPECT(file.write(ini) == true); + // verify data + EXPECT(verifyData(testDataDuplicateKeys)); + }, + CASE("Test: Duplicate sections") + { + auto const& filename = std::get<0>(testDataDuplicateSections); + // read from file + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + // update data + ini["section"]["data"] = "A"; + // write to file + EXPECT(file.write(ini) == true); + // verify data + EXPECT(verifyData(testDataDuplicateSections)); + }, + CASE("Test: Pretty print") + { + auto const& filename = std::get<0>(testDataPrettyPrint); + // read from file + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + // update data + ini["section1"]["value3"] = "C"; + ini["section2"]["value2"] = "B"; + ini["section3"].set({ + {"value1", "A"}, + {"value2", "B"} + }); + // write to file + EXPECT(file.write(ini, true) == true); + // verify data + EXPECT(verifyData(testDataPrettyPrint)); + }, + CASE("Test: Write to empty file") + { + auto const& filename = std::get<0>(testDataEmptyFile); + // read from file + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + // update data + ini["section"]["key"] = "value"; + // write to file + EXPECT(file.write(ini) == true); + // verify data + EXPECT(verifyData(testDataEmptyFile)); + }, + CASE("Test: Write to empty section") + { + auto const& filename = std::get<0>(testDataEmptySection); + // read from file + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + // update data + ini["section"]["key"] = "value"; + // write to file + EXPECT(file.write(ini) == true); + // verify data + EXPECT(verifyData(testDataEmptySection)); + }, + CASE("Test: Write to a single empty section among many") + { + auto const& filename = std::get<0>(testDataManyEmptySections); + // read from file + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + // update data + ini["section3"]["key"] = "value"; + // write to file + EXPECT(file.write(ini) == true); + // verify data + EXPECT(verifyData(testDataManyEmptySections)); + }, + CASE("Test: Write with empty section and key names") + { + auto const& filename = std::get<0>(testDataEmptyNames); + // read from file + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + // update data + ini[""]["key"] = ""; + EXPECT(ini[""].remove("key2") == true); + ini[""][""] = "value3"; + ini["section"][""] = "value2"; + ini["section2"][""] = ""; + // write to file + EXPECT(file.write(ini) == true); + // verify data + EXPECT(verifyData(testDataEmptyNames)); + }, + CASE("Test: Write malformed section names") + { + auto const& filename = std::get<0>(testDataMalformed1); + // read from file + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + // update data + ini["[name1"]["key"] = "value1"; + ini["name2]"]["key"] = "value2"; + ini["[name3]"]["key"] = "value3"; + // write to file + EXPECT(file.write(ini) == true); + // verify data + EXPECT(verifyData(testDataMalformed1)); + }, + CASE("Test: Write malformed key names") + { + auto const& filename = std::get<0>(testDataMalformed2); + // read from file + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + // update data + ini["name"].set({ + {"=", " == "}, + {"a= =", "=bb"}, + {"e=", " =f= "} + }); + ini["name"].remove("c= ="); + ini["other"]["="] = "="; + // write to file + EXPECT(file.write(ini) == true); + // verify data + EXPECT(verifyData(testDataMalformed2)); + }, + CASE("Test: Consecutive writes") + { + auto const& filename = std::get<0>(testDataConsecutiveWrites); + // read from file + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + // update data + ini["food"].set({ + {"cheese", "AAA"}, + {"ice cream", "BBB"}, + {"bananas", "CCC"}, + {"soup", "DDD"} + }); + ini["things"].set({ + {"scissors", "32"}, + {"wooden box", "64"}, + {" speakers ", " 128 "}, + {" book ", " 256 "} + }); + // write to file multiple times + for (unsigned int i = 0; i < 10; ++i) + { + EXPECT(file.write(ini) == true); + } + // verify data + EXPECT(verifyData(testDataConsecutiveWrites)); + }, + CASE("Test: Empty values") + { + auto const& filename = std::get<0>(testDataEmptyValues); + // read from file + mINI::INIFile file(filename); + mINI::INIStructure ini; + EXPECT(file.read(ini) == true); + // update data + ini["section"]["key"].clear(); + // write to file + EXPECT(file.write(ini) == true); + // verify data + EXPECT(verifyData(testDataEmptyValues)); + } +}; + +int main(int argc, char** argv) +{ + // write test files + writeTestFile(testDataBasic); + writeTestFile(testDataWithGarbage); + writeTestFile(testDataRemEntry); + writeTestFile(testDataRemSection); + writeTestFile(testDataDuplicateKeys); + writeTestFile(testDataDuplicateSections); + writeTestFile(testDataPrettyPrint); + writeTestFile(testDataEmptyFile); + writeTestFile(testDataEmptySection); + writeTestFile(testDataManyEmptySections); + writeTestFile(testDataEmptyNames); + writeTestFile(testDataMalformed1); + writeTestFile(testDataMalformed2); + writeTestFile(testDataConsecutiveWrites); + writeTestFile(testDataEmptyValues); + + // run tests + if (int failures = lest::run(mINI_tests, argc, argv)) + { + return failures; + } + return std::cout << std::endl << "All tests passed!" << std::endl, EXIT_SUCCESS; +}