From 6ff7f2772b96f16997cb664aeb2ddbea5d09af6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Thu, 17 Sep 2026 20:11:25 +0800 Subject: [PATCH] Tests: adds more UTs --- CMakeLists.txt | 64 ++++++++++++ tests/endian-bigendian.c | 44 ++++++++ tests/endian.c | 96 +++++++++++++++++ tests/frequency.c | 90 ++++++++++++++++ tests/parsing.c | 190 ++++++++++++++++++++++++++++++++++ tests/percent.c | 216 +++++++++++++++++++++++++++++++++++++++ tests/properties.c | 138 +++++++++++++++++++++++++ tests/size.c | 142 +++++++++++++++++++++++++ tests/strutil.c | 164 ++++++++++++++++++++++++++++- tests/temps.c | 177 ++++++++++++++++++++++++++++++++ 10 files changed, 1320 insertions(+), 1 deletion(-) create mode 100644 tests/endian-bigendian.c create mode 100644 tests/endian.c create mode 100644 tests/frequency.c create mode 100644 tests/parsing.c create mode 100644 tests/percent.c create mode 100644 tests/properties.c create mode 100644 tests/size.c create mode 100644 tests/temps.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 9cf4b05cc..597c752e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2239,6 +2239,62 @@ if (BUILD_TESTS) PRIVATE libfastfetch ) + add_executable(fastfetch-test-parsing + tests/parsing.c + ) + target_link_libraries(fastfetch-test-parsing + PRIVATE libfastfetch + ) + + add_executable(fastfetch-test-properties + tests/properties.c + ) + target_link_libraries(fastfetch-test-properties + PRIVATE libfastfetch + ) + + add_executable(fastfetch-test-size + tests/size.c + ) + target_link_libraries(fastfetch-test-size + PRIVATE libfastfetch + ) + + add_executable(fastfetch-test-frequency + tests/frequency.c + ) + target_link_libraries(fastfetch-test-frequency + PRIVATE libfastfetch + ) + + add_executable(fastfetch-test-percent + tests/percent.c + ) + target_link_libraries(fastfetch-test-percent + PRIVATE libfastfetch + ) + + add_executable(fastfetch-test-temps + tests/temps.c + ) + target_link_libraries(fastfetch-test-temps + PRIVATE libfastfetch + ) + + add_executable(fastfetch-test-endian + tests/endian.c + ) + target_link_libraries(fastfetch-test-endian + PRIVATE libfastfetch + ) + + add_executable(fastfetch-test-endian-bigendian + tests/endian-bigendian.c + ) + target_link_libraries(fastfetch-test-endian-bigendian + PRIVATE libfastfetch + ) + enable_testing() add_test(NAME test-strbuf COMMAND fastfetch-test-strbuf) add_test(NAME test-list COMMAND fastfetch-test-list) @@ -2248,6 +2304,14 @@ if (BUILD_TESTS) add_test(NAME test-strutil COMMAND fastfetch-test-strutil) add_test(NAME test-networking COMMAND fastfetch-test-networking) add_test(NAME test-base64 COMMAND fastfetch-test-base64) + add_test(NAME test-parsing COMMAND fastfetch-test-parsing) + add_test(NAME test-properties COMMAND fastfetch-test-properties) + add_test(NAME test-size COMMAND fastfetch-test-size) + add_test(NAME test-frequency COMMAND fastfetch-test-frequency) + add_test(NAME test-percent COMMAND fastfetch-test-percent) + add_test(NAME test-temps COMMAND fastfetch-test-temps) + add_test(NAME test-endian COMMAND fastfetch-test-endian) + add_test(NAME test-endian-bigendian COMMAND fastfetch-test-endian-bigendian) endif() ################## diff --git a/tests/endian-bigendian.c b/tests/endian-bigendian.c new file mode 100644 index 000000000..39056bedf --- /dev/null +++ b/tests/endian-bigendian.c @@ -0,0 +1,44 @@ +// This translation unit pretends to be compiled on a big endian host, so that the endianness +// detection in `common/endian.h` can be exercised on a little endian machine. +// +// The selection is a preprocessor decision, so it can be pinned with `_Static_assert` instead of by +// running anything -- which matters, because the alternative would be to observe a byte swap at run +// time on hardware that really is little endian, and that cannot work. +// +// The case this guards: GCC does not define `__BIG_ENDIAN__` at all, it only documents +// `__BYTE_ORDER__` and the `__ORDER_*_ENDIAN__` values. A header that tests `#if __BIG_ENDIAN__` +// therefore evaluates it as 0 on s390x and silently picks the little endian branch, byte swapping +// values that were already big endian. `__BIG_ENDIAN__` is undefined here for exactly that reason: +// a header that only looks at its presence, or at its value, fails the assertions below. +// +// System headers are pulled in before the macros are faked so that they are not affected. + +#include +#include + +#include "common/textModifier.h" + +// What a big endian compiler reports ... +#undef __BYTE_ORDER__ +#define __BYTE_ORDER__ __ORDER_BIG_ENDIAN__ +// ... and what GCC does not report on such a machine, even though it is one. +#undef __BIG_ENDIAN__ + +#include "common/endian.h" + +// On a big endian host the byte order of the machine and the byte order of the data agree, so +// reading a big endian value is a no-op ... +_Static_assert(FF_READ_BE((uint16_t) 0x1122) == (uint16_t) 0x1122, "FF_READ_BE must be the identity on a big endian host"); +_Static_assert(FF_READ_BE(0x11223344u) == 0x11223344u, "FF_READ_BE must be the identity on a big endian host"); +_Static_assert(FF_READ_BE(0x1122334455667788ull) == 0x1122334455667788ull, "FF_READ_BE must be the identity on a big endian host"); + +// ... and reading a little endian one swaps. +_Static_assert(FF_READ_LE((uint16_t) 0x1122) == (uint16_t) 0x2211, "FF_READ_LE must swap on a big endian host"); +_Static_assert(FF_READ_LE(0x11223344u) == 0x44332211u, "FF_READ_LE must swap on a big endian host"); +_Static_assert(FF_READ_LE(0x1122334455667788ull) == 0x8877665544332211ull, "FF_READ_LE must swap on a big endian host"); + +int main(void) { + // Everything this test asserts is checked at compile time, so reaching this line is the result. + puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); + return 0; +} diff --git a/tests/endian.c b/tests/endian.c new file mode 100644 index 000000000..29294b418 --- /dev/null +++ b/tests/endian.c @@ -0,0 +1,96 @@ +#include "common/endian.h" +#include "common/textModifier.h" + +#include +#include +#include +#include + +static void verify(bool expression, const char* expressionStr, int lineNo) { + if (expression) { + return; + } + + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] %s\n" FASTFETCH_TEXT_MODIFIER_RESET, lineNo, expressionStr); + exit(1); +} + +#define VERIFY(expression) verify((expression), #expression, __LINE__) + +// The macros take a value, so the only way to test them is to lay out the bytes of a known number +// in memory and read them back through the macro. The expectations below do not depend on the +// endianness of the machine running the test: +// +// * copying the little endian byte sequence of 0x11223344 (44 33 22 11) into a `uint32_t` yields +// 0x11223344 on a little endian machine and 0x44332211 on a big endian one. `FF_READ_LE` has to +// turn both of them into 0x11223344, which means it is the identity on one machine and a byte +// swap on the other. +// * `FF_READ_BE` is the mirror image. +// +// On a little endian machine this only pins that the two macros are the identity / swap pair; the +// selection itself (a compiler that does not define `__BIG_ENDIAN__` at all must still be detected +// as big endian from `__BYTE_ORDER__`) can only be exercised by a big endian build, or by a cross +// target such as `clang --target=s390x-linux-gnu`. +static void verify16(void) { + const uint8_t littleEndianBytes[2] = { 0x22, 0x11 }; + const uint8_t bigEndianBytes[2] = { 0x11, 0x22 }; + + uint16_t value; + + memcpy(&value, littleEndianBytes, sizeof(value)); + VERIFY(FF_READ_LE(value) == (uint16_t) 0x1122); + + memcpy(&value, bigEndianBytes, sizeof(value)); + VERIFY(FF_READ_BE(value) == (uint16_t) 0x1122); +} + +static void verify32(void) { + const uint8_t littleEndianBytes[4] = { 0x44, 0x33, 0x22, 0x11 }; + const uint8_t bigEndianBytes[4] = { 0x11, 0x22, 0x33, 0x44 }; + + uint32_t value; + + memcpy(&value, littleEndianBytes, sizeof(value)); + VERIFY(FF_READ_LE(value) == 0x11223344u); + + memcpy(&value, bigEndianBytes, sizeof(value)); + VERIFY(FF_READ_BE(value) == 0x11223344u); +} + +static void verify64(void) { + const uint8_t littleEndianBytes[8] = { 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11 }; + const uint8_t bigEndianBytes[8] = { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88 }; + + uint64_t value; + + memcpy(&value, littleEndianBytes, sizeof(value)); + VERIFY(FF_READ_LE(value) == 0x1122334455667788ull); + + memcpy(&value, bigEndianBytes, sizeof(value)); + VERIFY(FF_READ_BE(value) == 0x1122334455667788ull); +} + +int main(void) { + verify16(); + verify32(); + verify64(); + + // A byte swap has to stay inside the width of the value it was given. A value whose bytes are + // all equal reads the same through either macro, on either kind of machine. + { + uint16_t narrow = 0x0F0Fu; + VERIFY(FF_READ_LE(narrow) == (uint16_t) 0x0F0F); + VERIFY(FF_READ_BE(narrow) == (uint16_t) 0x0F0F); + + uint32_t wide = 0x0F0F0F0Fu; + VERIFY(FF_READ_LE(wide) == 0x0F0F0F0Fu); + VERIFY(FF_READ_BE(wide) == 0x0F0F0F0Fu); + + uint64_t widest = 0x0F0F0F0F0F0F0F0Full; + VERIFY(FF_READ_LE(widest) == 0x0F0F0F0F0F0F0F0Full); + VERIFY(FF_READ_BE(widest) == 0x0F0F0F0F0F0F0F0Full); + } + + puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); + return 0; +} diff --git a/tests/frequency.c b/tests/frequency.c new file mode 100644 index 000000000..60f5a3c9c --- /dev/null +++ b/tests/frequency.c @@ -0,0 +1,90 @@ +#include "common/frequency.h" +#include "common/textModifier.h" +#include "fastfetch.h" + +#include +#include +#include + +static void verify(uint32_t mhz, bool expectedResult, const char* expected, int lineNo) { + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate(); + const bool returned = ffFreqAppendNum(mhz, &result); + + if (returned != expectedResult || !ffStrbufEqualS(&result, expected)) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffFreqAppendNum(%u): expected %s \"%s\", got %s \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, mhz, expectedResult ? "true" : "false", expected, returned ? "true" : "false", result.chars); + exit(1); + } +} + +#define VERIFY_FREQ(mhz, expectedResult, expected) verify((mhz), (expectedResult), (expected), __LINE__) + +int main(void) { + // Use the real defaults rather than a hand built config, so a changed default is caught here too + ffOptionsInitDisplay(&instance.config.display); + FFOptionsDisplay* options = &instance.config.display; + + // An unknown frequency prints nothing at all + { + VERIFY_FREQ(0, false, ""); + } + + // With `freqNdigits >= 0` the value is printed in GHz, with that many digits after the point + { + VERIFY_FREQ(1000, true, "1.00 GHz"); + VERIFY_FREQ(3600, true, "3.60 GHz"); + VERIFY_FREQ(5000, true, "5.00 GHz"); + VERIFY_FREQ(100, true, "0.10 GHz"); + VERIFY_FREQ(1, true, "0.00 GHz"); // rounds down to zero, but is still reported + VERIFY_FREQ(4294000, true, "4294.00 GHz"); // the whole `uint32_t` range is usable + } + + // `freqNdigits = 0` rounds to whole GHz + { + options->freqNdigits = 0; + VERIFY_FREQ(1000, true, "1 GHz"); + VERIFY_FREQ(3600, true, "4 GHz"); // rounded, not truncated + VERIFY_FREQ(1400, true, "1 GHz"); + VERIFY_FREQ(0, false, ""); + options->freqNdigits = 2; + } + + // A negative `freqNdigits` selects the integer MHz form instead + { + options->freqNdigits = -1; + VERIFY_FREQ(3600, true, "3600 MHz"); + VERIFY_FREQ(1000, true, "1000 MHz"); + VERIFY_FREQ(1, true, "1 MHz"); + VERIFY_FREQ(0, false, ""); + options->freqNdigits = 2; + } + + // Only `never` removes the space between the number and the unit + { + options->freqSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_NEVER; + VERIFY_FREQ(3600, true, "3.60GHz"); + options->freqNdigits = -1; + VERIFY_FREQ(3600, true, "3600MHz"); + options->freqNdigits = 2; + + options->freqSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_ALWAYS; + VERIFY_FREQ(3600, true, "3.60 GHz"); + + options->freqSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_DEFAULT; + VERIFY_FREQ(3600, true, "3.60 GHz"); + } + + // The result is appended to the buffer instead of replacing it + { + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateS("CPU: "); + if (!ffFreqAppendNum(3600, &result) || !ffStrbufEqualS(&result, "CPU: 3.60 GHz")) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffFreqAppendNum did not append: got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, __LINE__, result.chars); + exit(1); + } + } + + ffOptionsDestroyDisplay(options); + + puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); + return 0; +} diff --git a/tests/parsing.c b/tests/parsing.c new file mode 100644 index 000000000..dbf56723c --- /dev/null +++ b/tests/parsing.c @@ -0,0 +1,190 @@ +#include "common/parsing.h" +#include "common/textModifier.h" + +#include +#include +#include + +static void verify(bool expression, const char* expressionStr, int lineNo) { + if (expression) { + return; + } + + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] %s\n" FASTFETCH_TEXT_MODIFIER_RESET, lineNo, expressionStr); + exit(1); +} + +#define VERIFY(expression) verify((expression), #expression, __LINE__) + +static void verifyVersionCompare(uint32_t major1, uint32_t minor1, uint32_t patch1, uint32_t major2, uint32_t minor2, uint32_t patch2, int8_t expected, int lineNo) { + const FFVersion first = { major1, minor1, patch1 }; + const FFVersion second = { major2, minor2, patch2 }; + + const int8_t forward = ffVersionCompare(&first, &second); + const int8_t backward = ffVersionCompare(&second, &first); + + // Only the sign is meaningful to callers, but the comparison is documented to be antisymmetric, + // so a caller that negates the result to get "greater" must not be surprised. + if (forward != expected || backward != -expected) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffVersionCompare(%u.%u.%u, %u.%u.%u): expected %d/%d, got %d/%d\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, major1, minor1, patch1, major2, minor2, patch2, expected, -expected, forward, backward); + exit(1); + } +} + +#define VERIFY_COMPARE(...) verifyVersionCompare(__VA_ARGS__, __LINE__) + +static void verifyPretty(uint32_t major, uint32_t minor, uint32_t patch, const char* expected, int lineNo) { + const FFVersion version = { major, minor, patch }; + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate(); + ffVersionToPretty(&version, &result); + + if (!ffStrbufEqualS(&result, expected)) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffVersionToPretty(%u.%u.%u): expected \"%s\", got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, major, minor, patch, expected, result.chars); + exit(1); + } +} + +#define VERIFY_PRETTY(...) verifyPretty(__VA_ARGS__, __LINE__) + +static void verifySemver(const char* major, const char* minor, const char* patch, const char* expected, int lineNo) { + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY majorBuf = ffStrbufCreateS(major); + FF_STRBUF_AUTO_DESTROY minorBuf = ffStrbufCreateS(minor); + FF_STRBUF_AUTO_DESTROY patchBuf = ffStrbufCreateS(patch); + + ffParseSemver(&result, &majorBuf, &minorBuf, &patchBuf); + + if (!ffStrbufEqualS(&result, expected)) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffParseSemver(\"%s\", \"%s\", \"%s\"): expected \"%s\", got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, major, minor, patch, expected, result.chars); + exit(1); + } +} + +#define VERIFY_SEMVER(...) verifySemver(__VA_ARGS__, __LINE__) + +static void verifyGtk(const char* gtk2, const char* gtk3, const char* gtk4, const char* expected, int lineNo) { + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY gtk2Buf = ffStrbufCreateS(gtk2); + FF_STRBUF_AUTO_DESTROY gtk3Buf = ffStrbufCreateS(gtk3); + FF_STRBUF_AUTO_DESTROY gtk4Buf = ffStrbufCreateS(gtk4); + + ffParseGTK(&result, >k2Buf, >k3Buf, >k4Buf); + + if (!ffStrbufEqualS(&result, expected)) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffParseGTK(\"%s\", \"%s\", \"%s\"): expected \"%s\", got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, gtk2, gtk3, gtk4, expected, result.chars); + exit(1); + } +} + +#define VERIFY_GTK(...) verifyGtk(__VA_ARGS__, __LINE__) + +int main(void) { + // ffVersionCompare: the major field decides first, then minor, then patch. Equal versions + // compare equal even when they come from different sources. + { + VERIFY_COMPARE(0, 0, 0, 0, 0, 0, 0); + VERIFY_COMPARE(1, 2, 3, 1, 2, 3, 0); + VERIFY_COMPARE(UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX, 0); + + VERIFY_COMPARE(2, 0, 0, 1, 99, 99, 1); + VERIFY_COMPARE(1, 99, 99, 2, 0, 0, -1); + + VERIFY_COMPARE(1, 2, 0, 1, 1, 99, 1); + VERIFY_COMPARE(1, 1, 99, 1, 2, 0, -1); + + VERIFY_COMPARE(1, 2, 3, 1, 2, 2, 1); + VERIFY_COMPARE(1, 2, 2, 1, 2, 3, -1); + + // A larger patch must not be able to outvote a smaller minor + VERIFY_COMPARE(1, 2, 0, 1, 1, UINT32_MAX, 1); + VERIFY_COMPARE(1, 1, UINT32_MAX, 1, 2, 0, -1); + + // ... nor a larger minor a smaller major + VERIFY_COMPARE(2, 0, 0, 1, UINT32_MAX, UINT32_MAX, 1); + VERIFY_COMPARE(1, UINT32_MAX, UINT32_MAX, 2, 0, 0, -1); + } + + // ffVersionToPretty: a zero version prints as nothing at all, and a field is only printed when + // it or a later field is non-zero. + { + VERIFY_PRETTY(0, 0, 0, ""); + VERIFY_PRETTY(1, 0, 0, "1"); + VERIFY_PRETTY(0, 1, 0, "0.1"); + VERIFY_PRETTY(0, 0, 1, "0.0.1"); + VERIFY_PRETTY(1, 2, 0, "1.2"); + VERIFY_PRETTY(1, 0, 3, "1.0.3"); + VERIFY_PRETTY(1, 2, 3, "1.2.3"); + VERIFY_PRETTY(10, 20, 30, "10.20.30"); + VERIFY_PRETTY(UINT32_MAX, 0, 0, "4294967295"); + VERIFY_PRETTY(0, 0, UINT32_MAX, "0.0.4294967295"); + } + + // ffParseSemver: joins whichever parts were detected. A missing major is reported as 1, which is + // how "2.3" from a source that only reports the last two components stays unambiguous. + { + VERIFY_SEMVER("1", "2", "3", "1.2.3"); + VERIFY_SEMVER("1", "2", "", "1.2"); + VERIFY_SEMVER("1", "", "", "1"); + VERIFY_SEMVER("1", "", "3", "1.0.3"); + VERIFY_SEMVER("", "2", "3", "1.2.3"); + VERIFY_SEMVER("", "2", "", "1.2"); + VERIFY_SEMVER("", "", "3", "1.0.3"); + VERIFY_SEMVER("", "", "", ""); + + // A zero part is a value, not a missing part + VERIFY_SEMVER("0", "0", "0", "0.0.0"); + VERIFY_SEMVER("0", "", "", "0"); + } + + // ffParseSemver appends instead of replacing, so a caller can prefix its own text + { + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateS("v"); + FF_STRBUF_AUTO_DESTROY majorBuf = ffStrbufCreateS("1"); + FF_STRBUF_AUTO_DESTROY minorBuf = ffStrbufCreateS("2"); + FF_STRBUF_AUTO_DESTROY patchBuf = ffStrbufCreateS("3"); + ffParseSemver(&result, &majorBuf, &minorBuf, &patchBuf); + VERIFY(ffStrbufEqualS(&result, "v1.2.3")); + } + + // ffParseGTK: the three versions are folded into one line, and every combination of present and + // equal versions has its own suffix. + { + // All three present + VERIFY_GTK("1.0", "1.0", "1.0", "1.0 [GTK2/3/4]"); + VERIFY_GTK("2.0", "2.0", "3.0", "2.0 [GTK2/3], 3.0 [GTK4]"); + VERIFY_GTK("2.0", "3.0", "3.0", "2.0 [GTK2], 3.0 [GTK3/4]"); + VERIFY_GTK("2.0", "3.0", "4.0", "2.0 [GTK2], 3.0 [GTK3], 4.0 [GTK4]"); + + // GTK2 and GTK4 equal while GTK3 differs is not special cased: it falls back to the plain + // three-way listing, so the same version is printed twice with different suffixes. + VERIFY_GTK("2.0", "3.0", "2.0", "2.0 [GTK2], 3.0 [GTK3], 2.0 [GTK4]"); + + // The comparison is case insensitive, but the value printed is the one from the buffer that + // the branch selected -- here the third one, not the first. + VERIFY_GTK("ABC", "abc", "aBc", "aBc [GTK2/3/4]"); + VERIFY_GTK("2.0", "2.0", "3.0", "2.0 [GTK2/3], 3.0 [GTK4]"); + + // Two present + VERIFY_GTK("1.0", "1.0", "", "1.0 [GTK2/3]"); + VERIFY_GTK("2.0", "3.0", "", "2.0 [GTK2], 3.0 [GTK3]"); + VERIFY_GTK("1.0", "", "1.0", "1.0 [GTK2/4]"); + VERIFY_GTK("2.0", "", "4.0", "2.0 [GTK2], 4.0 [GTK4]"); + VERIFY_GTK("", "1.0", "1.0", "1.0 [GTK3/4]"); + VERIFY_GTK("", "3.0", "4.0", "3.0 [GTK3], 4.0 [GTK4]"); + + // One present + VERIFY_GTK("2.0", "", "", "2.0 [GTK2]"); + VERIFY_GTK("", "3.0", "", "3.0 [GTK3]"); + VERIFY_GTK("", "", "4.0", "4.0 [GTK4]"); + + // None present + VERIFY_GTK("", "", "", ""); + } + + puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); + return 0; +} diff --git a/tests/percent.c b/tests/percent.c new file mode 100644 index 000000000..e697a854d --- /dev/null +++ b/tests/percent.c @@ -0,0 +1,216 @@ +#include "common/option.h" +#include "common/percent.h" +#include "common/textModifier.h" +#include "fastfetch.h" + +#include +#include +#include + +static FFModuleArgs module; + +static void verifyNum(double percent, FFPercentageModuleConfig config, bool parentheses, const char* expected, int lineNo) { + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate(); + ffPercentAppendNum(&result, percent, config, parentheses, &module); + + if (!ffStrbufEqualS(&result, expected)) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffPercentAppendNum(%f, {%u, %u, %u}, %s): expected \"%s\", got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, percent, config.green, config.yellow, (unsigned) config.type, parentheses ? "true" : "false", expected, result.chars); + exit(1); + } +} + +#define VERIFY_NUM(percent, config, parentheses, expected) verifyNum((percent), (config), (parentheses), (expected), __LINE__) + +static void verifyBar(double percent, FFPercentageModuleConfig config, const char* expected, int lineNo) { + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate(); + ffPercentAppendBar(&result, percent, config, &module); + + if (!ffStrbufEqualS(&result, expected)) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffPercentAppendBar(%f, {%u, %u, %u}): expected \"%s\", got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, percent, config.green, config.yellow, (unsigned) config.type, expected, result.chars); + exit(1); + } +} + +#define VERIFY_BAR(percent, config, expected) verifyBar((percent), (config), (expected), __LINE__) + +int main(void) { + // Use the real defaults rather than a hand built config, so a changed default is caught here too + ffOptionsInitDisplay(&instance.config.display); + FFOptionsDisplay* options = &instance.config.display; + ffOptionInitModuleArg(&module, ""); + + // Pin the threshold colors: the yellow and red defaults depend on whether the terminal is + // detected as a light theme, which is not something a test should depend on. + ffStrbufSetS(&options->percentColorGreen, "32"); + ffStrbufSetS(&options->percentColorYellow, "33"); + ffStrbufSetS(&options->percentColorRed, "31"); + + // The plain number. `pipe` disables the color, which is what the first group of cases checks. + { + options->pipe = true; + const FFPercentageModuleConfig config = { .green = 50, .yellow = 80, .type = FF_PERCENTAGE_TYPE_NUM_BIT | FF_PERCENTAGE_TYPE_NUM_COLOR_BIT }; + + VERIFY_NUM(0, config, false, "0%"); + VERIFY_NUM(50, config, false, "50%"); + VERIFY_NUM(100, config, false, "100%"); + VERIFY_NUM(99.9, config, false, "100%"); // the default is zero digits, so this rounds + + // An unset percentage prints as a bare dash, without a percent sign + VERIFY_NUM(-DBL_MAX, config, false, "-"); + + // Parentheses wrap the whole thing, color included + VERIFY_NUM(0, config, true, "(0%)"); + VERIFY_NUM(-DBL_MAX, config, true, "(-)"); + } + + // percentNdigits + { + options->pipe = true; + options->percentNdigits = 2; + const FFPercentageModuleConfig config = { .green = 50, .yellow = 80, .type = FF_PERCENTAGE_TYPE_NUM_BIT }; + + VERIFY_NUM(0, config, false, "0.00%"); + VERIFY_NUM(50, config, false, "50.00%"); + VERIFY_NUM(33.333, config, false, "33.33%"); + VERIFY_NUM(100, config, false, "100.00%"); + VERIFY_NUM(-DBL_MAX, config, false, "-"); + + options->percentNdigits = 0; + } + + // percentWidth pads on the left, and the percent sign is not part of the padded field + { + options->pipe = true; + options->percentWidth = 5; + const FFPercentageModuleConfig config = { .green = 50, .yellow = 80, .type = FF_PERCENTAGE_TYPE_NUM_BIT }; + + VERIFY_NUM(0, config, false, " 0%"); + VERIFY_NUM(50, config, false, " 50%"); + VERIFY_NUM(100, config, false, " 100%"); + + options->percentWidth = 0; + } + + // Only `always` adds a space before the percent sign + { + options->pipe = true; + const FFPercentageModuleConfig config = { .green = 50, .yellow = 80, .type = FF_PERCENTAGE_TYPE_NUM_BIT }; + + options->percentSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_ALWAYS; + VERIFY_NUM(50, config, false, "50 %"); + + options->percentSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_NEVER; + VERIFY_NUM(50, config, false, "50%"); + + options->percentSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_DEFAULT; + VERIFY_NUM(50, config, false, "50%"); + } + + // The color is picked by comparing against the thresholds, and each threshold belongs to the + // lower band: exactly `green` is green, exactly `yellow` is yellow. + { + options->pipe = false; + const FFPercentageModuleConfig config = { .green = 50, .yellow = 80, .type = FF_PERCENTAGE_TYPE_NUM_COLOR_BIT }; + + VERIFY_NUM(0, config, false, "\e[32m0%\e[m"); + VERIFY_NUM(50, config, false, "\e[32m50%\e[m"); + VERIFY_NUM(51, config, false, "\e[33m51%\e[m"); + VERIFY_NUM(79, config, false, "\e[33m79%\e[m"); + VERIFY_NUM(80, config, false, "\e[33m80%\e[m"); + VERIFY_NUM(81, config, false, "\e[31m81%\e[m"); + VERIFY_NUM(100, config, false, "\e[31m100%\e[m"); + + VERIFY_NUM(-DBL_MAX, config, false, "\e[90m-\e[m"); + VERIFY_NUM(-DBL_MAX, config, true, "(\e[90m-\e[m)"); + } + + // When `green > yellow` the bands are inverted, so a low value is the alarming one + { + options->pipe = false; + const FFPercentageModuleConfig config = { .green = 80, .yellow = 50, .type = FF_PERCENTAGE_TYPE_NUM_COLOR_BIT }; + + VERIFY_NUM(0, config, false, "\e[31m0%\e[m"); + VERIFY_NUM(49, config, false, "\e[31m49%\e[m"); + VERIFY_NUM(50, config, false, "\e[33m50%\e[m"); + VERIFY_NUM(79, config, false, "\e[33m79%\e[m"); + VERIFY_NUM(80, config, false, "\e[32m80%\e[m"); + VERIFY_NUM(100, config, false, "\e[32m100%\e[m"); + } + + // Without the color flag the number is never colored, even when colors are enabled + { + options->pipe = false; + const FFPercentageModuleConfig config = { .green = 50, .yellow = 80, .type = FF_PERCENTAGE_TYPE_NUM_BIT }; + + VERIFY_NUM(0, config, false, "0%"); + VERIFY_NUM(100, config, false, "100%"); + } + + // A zero `type` means "use the global default" instead of "no flags" + { + options->pipe = false; + FFPercentageModuleConfig config = { .green = 50, .yellow = 80, .type = 0 }; + + options->percentType = FF_PERCENTAGE_TYPE_NUM_BIT; + VERIFY_NUM(100, config, false, "100%"); + + options->percentType = FF_PERCENTAGE_TYPE_NUM_BIT | FF_PERCENTAGE_TYPE_NUM_COLOR_BIT; + VERIFY_NUM(100, config, false, "\e[31m100%\e[m"); + } + + // The bar fills `percent` of the width, rounding to the nearest block. Colors are left to the + // `pipe` free cases above; the block characters and the borders are what is checked here. + { + options->pipe = true; + const FFPercentageModuleConfig config = { .green = 50, .yellow = 80, .type = 0 }; + + options->barWidth = 10; + VERIFY_BAR(0, config, "[ ---------- ]"); + VERIFY_BAR(50, config, "[ ■■■■■----- ]"); + VERIFY_BAR(100, config, "[ ■■■■■■■■■■ ]"); + + // An unset percentage fills the bar with the "total" character, same as zero + VERIFY_BAR(-DBL_MAX, config, "[ ---------- ]"); + + // The half block is rounded up at exactly 0.5 + VERIFY_BAR(4, config, "[ ---------- ]"); + VERIFY_BAR(5, config, "[ ■--------- ]"); + VERIFY_BAR(14, config, "[ ■--------- ]"); + VERIFY_BAR(15, config, "[ ■■-------- ]"); + + options->barWidth = 4; + VERIFY_BAR(0, config, "[ ---- ]"); + VERIFY_BAR(25, config, "[ ■--- ]"); + VERIFY_BAR(50, config, "[ ■■-- ]"); + VERIFY_BAR(100, config, "[ ■■■■ ]"); + + // The borders are configurable and may be removed entirely + ffStrbufSetS(&options->barBorderLeft, ""); + ffStrbufSetS(&options->barBorderRight, ""); + VERIFY_BAR(50, config, "■■--"); + + ffStrbufSetS(&options->barBorderLeft, "<"); + ffStrbufSetS(&options->barBorderRight, ">"); + VERIFY_BAR(50, config, "<■■-->"); + + ffStrbufSetS(&options->barBorderLeft, "[ "); + ffStrbufSetS(&options->barBorderRight, " ]"); + + // The bar characters themselves are configurable + ffStrbufSetS(&options->barCharElapsed, "#"); + ffStrbufSetS(&options->barCharTotal, "."); + VERIFY_BAR(50, config, "[ ##.. ]"); + ffStrbufSetS(&options->barCharElapsed, "■"); + ffStrbufSetS(&options->barCharTotal, "-"); + + options->barWidth = 10; + } + + ffOptionDestroyModuleArg(&module); + ffOptionsDestroyDisplay(options); + + puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); + return 0; +} diff --git a/tests/properties.c b/tests/properties.c new file mode 100644 index 000000000..e8f95ec05 --- /dev/null +++ b/tests/properties.c @@ -0,0 +1,138 @@ +#include "common/properties.h" +#include "common/textModifier.h" + +#include +#include + +static void verify(bool expression, const char* expressionStr, int lineNo) { + if (expression) { + return; + } + + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] %s\n" FASTFETCH_TEXT_MODIFIER_RESET, lineNo, expressionStr); + exit(1); +} + +#define VERIFY(expression) verify((expression), #expression, __LINE__) + +static void verifyLine(const char* line, const char* start, bool expectedResult, const char* expectedValue, int lineNo) { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + const char* cursor = line; + const bool result = ffParsePropLinePointer(&cursor, start, &buffer); + + if (result != expectedResult || (result && !ffStrbufEqualS(&buffer, expectedValue))) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffParsePropLine(\"%s\", \"%s\"): expected %s \"%s\", got %s \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, line, start, expectedResult ? "true" : "false", expectedResult ? expectedValue : "", + result ? "true" : "false", result ? buffer.chars : ""); + exit(1); + } +} + +#define VERIFY_LINE(...) verifyLine(__VA_ARGS__, __LINE__) + +static void verifyLines(const char* lines, const char* start, bool expectedResult, const char* expectedValue, int lineNo) { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + const bool result = ffParsePropLines(lines, start, &buffer); + + if (result != expectedResult || (result && !ffStrbufEqualS(&buffer, expectedValue))) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffParsePropLines(\"%s\", \"%s\"): expected %s \"%s\", got %s \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, lines, start, expectedResult ? "true" : "false", expectedResult ? expectedValue : "", + result ? "true" : "false", result ? buffer.chars : ""); + exit(1); + } +} + +#define VERIFY_LINES(...) verifyLines(__VA_ARGS__, __LINE__) + +int main(void) { + // The plain case: `key: value`, with the value running to the end of the line. + { + VERIFY_LINE("Name: foo", "Name:", true, "foo"); + VERIFY_LINE("Name: foo\n", "Name:", true, "foo"); + VERIFY_LINE("Name: foo\nBar: baz", "Name:", true, "foo"); // stops at the newline + VERIFY_LINE("Name:foo", "Name:", true, "foo"); + VERIFY_LINE("Name: ", "Name:", true, ""); // found, but the value is empty + VERIFY_LINE("Name:", "Name:", true, ""); + VERIFY_LINE("Name: a:b", "Name:", true, "a:b"); // only the first colon is the separator + VERIFY_LINE("Name: foo bar", "Name:", true, "foo bar"); // spaces inside the value are kept + } + + // Any amount of whitespace in the format matches any amount of whitespace in the line, including + // none -- the two sides are independent. + { + VERIFY_LINE("Name: foo", "Name:", true, "foo"); + VERIFY_LINE("Name:foo", "Name: ", true, "foo"); + VERIFY_LINE("Name: foo", "Name: ", true, "foo"); + VERIFY_LINE("Name:\tfoo", "Name: ", true, "foo"); + VERIFY_LINE("Name: foo", "Name:\t", true, "foo"); + VERIFY_LINE(" \t Name: foo", "Name:", true, "foo"); // leading whitespace of the line is skipped + } + + // A mismatch anywhere in the key makes the whole line fail, and the key must match in full. + { + VERIFY_LINE("Name: foo", "Value:", false, ""); + VERIFY_LINE("name: foo", "Name:", true, "foo"); // the key is compared case insensitively + VERIFY_LINE("NAME: foo", "name:", true, "foo"); + VERIFY_LINE("nAmE: foo", "NAME:", true, "foo"); + VERIFY_LINE("NameX: foo", "Name:", false, ""); + VERIFY_LINE("Name :foo", "Name:", false, ""); + VERIFY_LINE("Nam", "Name:", false, ""); // the line ends before the key does + VERIFY_LINE("", "Name:", false, ""); + VERIFY_LINE("\n", "Name:", false, ""); + VERIFY_LINE("Name", "Name:", false, ""); + } + + // Trailing spaces are trimmed, but nothing else is: a trailing tab survives. + { + VERIFY_LINE("Name: foo ", "Name:", true, "foo"); + VERIFY_LINE("Name: foo\t", "Name:", true, "foo\t"); + VERIFY_LINE("Name: foo \n", "Name:", true, "foo"); + } + + // A quoted value ends at its closing quote, so trailing text is not part of the value. + { + VERIFY_LINE("Name: \"foo bar\"", "Name:", true, "foo bar"); + VERIFY_LINE("Name: 'foo bar'", "Name:", true, "foo bar"); + VERIFY_LINE("Name: \"foo\" trailing", "Name:", true, "foo"); + VERIFY_LINE("Name: \"\"", "Name:", true, ""); + VERIFY_LINE("Name: \"foo", "Name:", true, "foo"); // unterminated quote: the line end closes it + + // The trim still applies, and it only removes the right hand side + VERIFY_LINE("Name: \" foo \"", "Name:", true, " foo"); + } + + // A key ending in `>` switches the parser to XML mode, where the value ends at `<` instead of at + // the end of the line. + { + VERIFY_LINE("foo", "", true, "foo"); + VERIFY_LINE("", "", true, ""); + VERIFY_LINE("foo", "", true, "foo"); // no closing tag + VERIFY_LINE("foo bar", "", true, "foo bar"); + VERIFY_LINE(" foo", "", true, "foo"); + VERIFY_LINE("foo\n", "", true, "foo"); + } + + // ffParsePropLines walks the lines until the key is found, and the first match wins -- including + // when that match has an empty value. + { + VERIFY_LINES("A: 1\nName: foo\nB: 2", "Name:", true, "foo"); + VERIFY_LINES("A: 1\nName: foo", "Name:", true, "foo"); + VERIFY_LINES("Name: foo", "Name:", true, "foo"); + VERIFY_LINES("\n\nName: foo", "Name:", true, "foo"); + VERIFY_LINES("Name: foo\nName: bar", "Name:", true, "foo"); + VERIFY_LINES("Name:\nName: bar", "Name:", true, ""); + VERIFY_LINES("A: 1\nB: 2", "Name:", false, ""); + VERIFY_LINES("A: 1\nB: 2\n", "Name:", false, ""); + VERIFY_LINES("", "Name:", false, ""); + } + + // The value is appended to the buffer, so a caller can collect several keys into one string. + { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreateS("pre"); + VERIFY(ffParsePropLines("Name: foo", "Name:", &buffer)); + VERIFY(ffStrbufEqualS(&buffer, "prefoo")); + } + + puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); + return 0; +} diff --git a/tests/size.c b/tests/size.c new file mode 100644 index 000000000..0288829e1 --- /dev/null +++ b/tests/size.c @@ -0,0 +1,142 @@ +#include "common/size.h" +#include "common/textModifier.h" +#include "fastfetch.h" + +#include +#include +#include + +static void verify(uint64_t bytes, const char* expected, int lineNo) { + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate(); + ffSizeAppendNum(bytes, &result); + + if (!ffStrbufEqualS(&result, expected)) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffSizeAppendNum(%llu): expected \"%s\", got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, (unsigned long long) bytes, expected, result.chars); + exit(1); + } +} + +#define VERIFY_SIZE(bytes, expected) verify((bytes), (expected), __LINE__) + +int main(void) { + // Use the real defaults rather than a hand built config, so a changed default is caught here too + ffOptionsInitDisplay(&instance.config.display); + FFOptionsDisplay* options = &instance.config.display; + + // IEC (the default): 1024 based, with the `KiB` style suffixes + { + VERIFY_SIZE(0, "0 B"); + VERIFY_SIZE(1, "1 B"); + VERIFY_SIZE(999, "999 B"); + VERIFY_SIZE(1023, "1023 B"); // one byte short of the first prefix + VERIFY_SIZE(1024, "1.00 KiB"); + VERIFY_SIZE(1025, "1.00 KiB"); // below the rounding precision + VERIFY_SIZE(1536, "1.50 KiB"); + VERIFY_SIZE(1024ULL * 1024, "1.00 MiB"); + VERIFY_SIZE(1024ULL * 1024 * 1024, "1.00 GiB"); + VERIFY_SIZE(1024ULL * 1024 * 1024 * 1024, "1.00 TiB"); + VERIFY_SIZE(1024ULL * 1024 * 1024 * 1024 * 1024, "1.00 PiB"); + VERIFY_SIZE(1024ULL * 1024 * 1024 * 1024 * 1024 * 1024, "1.00 EiB"); + + // The largest 64 bit value is 16 EiB; `ZiB` and `YiB` are listed but can never be reached + // through an IEC conversion of a `uint64_t`. + VERIFY_SIZE(UINT64_MAX, "16.00 EiB"); + } + + // SI: 1000 based, with the `kB` style suffixes + { + options->sizeBinaryPrefix = FF_SIZE_BINARY_PREFIX_TYPE_SI; + + VERIFY_SIZE(0, "0 B"); + VERIFY_SIZE(999, "999 B"); + VERIFY_SIZE(1000, "1.00 kB"); + VERIFY_SIZE(1500, "1.50 kB"); + VERIFY_SIZE(1000000, "1.00 MB"); + VERIFY_SIZE(1000000000ULL, "1.00 GB"); + VERIFY_SIZE(1000000000000ULL, "1.00 TB"); + VERIFY_SIZE(1000000000000000ULL, "1.00 PB"); + VERIFY_SIZE(1000000000000000000ULL, "1.00 EB"); + VERIFY_SIZE(UINT64_MAX, "18.45 EB"); + } + + // JEDEC: 1024 based, but with the `KB` style suffixes. The suffix list stops at `TB`, so the + // loop has to stop there as well instead of reading past the end. + { + options->sizeBinaryPrefix = FF_SIZE_BINARY_PREFIX_TYPE_JEDEC; + + VERIFY_SIZE(1023, "1023 B"); + VERIFY_SIZE(1024, "1.00 KB"); + VERIFY_SIZE(1024ULL * 1024, "1.00 MB"); + VERIFY_SIZE(1024ULL * 1024 * 1024, "1.00 GB"); + VERIFY_SIZE(1024ULL * 1024 * 1024 * 1024, "1.00 TB"); + VERIFY_SIZE(1024ULL * 1024 * 1024 * 1024 * 1024, "1024.00 TB"); // no `PB` in JEDEC + VERIFY_SIZE(UINT64_MAX, "16777216.00 TB"); + } + + options->sizeBinaryPrefix = FF_SIZE_BINARY_PREFIX_TYPE_IEC; + + // sizeNdigits: the number of digits after the decimal point. It applies only once a prefix is + // used -- a plain byte count is always printed as an integer. + { + options->sizeNdigits = 0; + VERIFY_SIZE(1024, "1 KiB"); + VERIFY_SIZE(1536, "2 KiB"); // rounded, not truncated + VERIFY_SIZE(512, "512 B"); + VERIFY_SIZE(1023, "1023 B"); + + options->sizeNdigits = 4; + VERIFY_SIZE(1024, "1.0000 KiB"); + VERIFY_SIZE(1536, "1.5000 KiB"); + VERIFY_SIZE(512, "512 B"); // still an integer + VERIFY_SIZE(0, "0 B"); + + options->sizeNdigits = 2; + } + + // sizeMaxPrefix: the index of the largest suffix that may be used. `0` disables the conversion + // entirely. + { + options->sizeMaxPrefix = 0; + VERIFY_SIZE(0, "0 B"); + VERIFY_SIZE(1024, "1024 B"); + VERIFY_SIZE(UINT64_MAX, "18446744073709551615 B"); + + options->sizeMaxPrefix = 1; + VERIFY_SIZE(1024, "1.00 KiB"); + VERIFY_SIZE(1024ULL * 1024, "1024.00 KiB"); + VERIFY_SIZE(1024ULL * 1024 * 1024, "1048576.00 KiB"); + + options->sizeMaxPrefix = 8; + } + + // sizeSpaceBeforeUnit: only `never` removes the space, `default` and `always` both keep it + { + options->sizeSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_NEVER; + VERIFY_SIZE(0, "0B"); + VERIFY_SIZE(1024, "1.00KiB"); + VERIFY_SIZE(1536, "1.50KiB"); + + options->sizeSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_ALWAYS; + VERIFY_SIZE(0, "0 B"); + VERIFY_SIZE(1024, "1.00 KiB"); + + options->sizeSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_DEFAULT; + VERIFY_SIZE(1024, "1.00 KiB"); + } + + // The result is appended to the buffer instead of replacing it + { + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateS("Size: "); + ffSizeAppendNum(1024, &result); + if (!ffStrbufEqualS(&result, "Size: 1.00 KiB")) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffSizeAppendNum did not append: got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, __LINE__, result.chars); + exit(1); + } + } + + ffOptionsDestroyDisplay(options); + + puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); + return 0; +} diff --git a/tests/strutil.c b/tests/strutil.c index 3d065eee6..1b06ff5b7 100644 --- a/tests/strutil.c +++ b/tests/strutil.c @@ -17,6 +17,168 @@ static void verify(bool expression, const char* expressionStr, int lineNo) { #define VERIFY(expression) verify((expression), #expression, __LINE__) int main(void) { + // The plain C string helpers. These have nothing to do with wcwidth, so they run in every build. + { + VERIFY(!ffStrSet(nullptr)); + VERIFY(!ffStrSet("")); + VERIFY(!ffStrSet(" ")); + VERIFY(!ffStrSet(" \t \n")); + VERIFY(ffStrSet("a")); + VERIFY(ffStrSet(" a")); + VERIFY(ffStrSet("a ")); + } + + { + VERIFY(ffStrStartsWith("", "")); + VERIFY(ffStrStartsWith("abc", "")); + VERIFY(ffStrStartsWith("abc", "a")); + VERIFY(ffStrStartsWith("abc", "abc")); + VERIFY(!ffStrStartsWith("abc", "abcd")); // the needle is longer than the haystack + VERIFY(!ffStrStartsWith("abc", "b")); + VERIFY(!ffStrStartsWith("", "a")); + VERIFY(!ffStrStartsWith("Abc", "abc")); + + VERIFY(ffStrStartsWithIgnCase("Abc", "abc")); + VERIFY(ffStrStartsWithIgnCase("ABC", "abc")); + VERIFY(ffStrStartsWithIgnCase("abc", "")); + VERIFY(!ffStrStartsWithIgnCase("Abc", "abd")); + } + + { + VERIFY(ffStrEndsWith("", "")); + VERIFY(ffStrEndsWith("abc", "")); + VERIFY(ffStrEndsWith("abc", "c")); + VERIFY(ffStrEndsWith("abc", "abc")); + VERIFY(!ffStrEndsWith("abc", "abcd")); // the needle is longer than the haystack + VERIFY(!ffStrEndsWith("abc", "b")); + VERIFY(!ffStrEndsWith("", "a")); + VERIFY(!ffStrEndsWith("abc", "Abc")); + + VERIFY(ffStrEndsWithIgnCase("abc", "BC")); + VERIFY(ffStrEndsWithIgnCase("abc", "")); + VERIFY(!ffStrEndsWithIgnCase("abc", "BD")); + } + + { + VERIFY(ffStrEquals("", "")); + VERIFY(ffStrEquals("abc", "abc")); + VERIFY(!ffStrEquals("abc", "abcd")); + VERIFY(!ffStrEquals("abc", "ABC")); + + VERIFY(ffStrEqualsIgnCase("abc", "ABC")); + VERIFY(ffStrEqualsIgnCase("", "")); + VERIFY(!ffStrEqualsIgnCase("abc", "abd")); + + VERIFY(ffStrContains("abc", "")); + VERIFY(ffStrContains("abc", "b")); + VERIFY(ffStrContains("abc", "abc")); + VERIFY(!ffStrContains("abc", "d")); + + VERIFY(ffStrContainsIgnCase("abc", "B")); + VERIFY(!ffStrContainsIgnCase("abc", "D")); + // `strcasestr` reports an empty needle as a match, but the `StrStrIA` it is aliased to on + // Windows reports no match, so on Windows this disagrees with `ffStrContains`. +#ifndef _WIN32 + VERIFY(ffStrContainsIgnCase("abc", "")); +#endif + + VERIFY(ffStrContainsC("abc", 'b')); + VERIFY(!ffStrContainsC("abc", 'd')); + VERIFY(ffStrContainsC("abc", '\0')); // `strchr` reports the terminator as a match + } + + { + VERIFY(ffCharIsEnglishAlphabet('a')); + VERIFY(ffCharIsEnglishAlphabet('z')); + VERIFY(ffCharIsEnglishAlphabet('A')); + VERIFY(ffCharIsEnglishAlphabet('Z')); + VERIFY(!ffCharIsEnglishAlphabet('0')); + VERIFY(!ffCharIsEnglishAlphabet('_')); + VERIFY(!ffCharIsEnglishAlphabet(' ')); + + VERIFY(ffCharIsDigit('0')); + VERIFY(ffCharIsDigit('9')); + VERIFY(!ffCharIsDigit('/')); // one below '0' + VERIFY(!ffCharIsDigit(':')); // one above '9' + VERIFY(!ffCharIsDigit('a')); + + VERIFY(ffCharIsHexDigit('0')); + VERIFY(ffCharIsHexDigit('9')); + VERIFY(ffCharIsHexDigit('a')); + VERIFY(ffCharIsHexDigit('F')); + VERIFY(!ffCharIsHexDigit('g')); + VERIFY(!ffCharIsHexDigit('G')); + VERIFY(!ffCharIsHexDigit('/')); + VERIFY(!ffCharIsHexDigit('@')); + } + + { + VERIFY(ffHexCharToInt('0') == 0); + VERIFY(ffHexCharToInt('9') == 9); + VERIFY(ffHexCharToInt('a') == 10); + VERIFY(ffHexCharToInt('f') == 15); + VERIFY(ffHexCharToInt('A') == 10); + VERIFY(ffHexCharToInt('F') == 15); + VERIFY(ffHexCharToInt('g') == -1); + VERIFY(ffHexCharToInt('G') == -1); + VERIFY(ffHexCharToInt('/') == -1); // one below '0' + VERIFY(ffHexCharToInt(':') == -1); // one above '9' + VERIFY(ffHexCharToInt('@') == -1); // one below 'A' + VERIFY(ffHexCharToInt('`') == -1); // one below 'a' + VERIFY(ffHexCharToInt(' ') == -1); + } + + // ffStrCopy never writes more than `dstBufSiz` bytes and always terminates. It returns a pointer + // to the end of the copy, so consecutive calls can be chained. + { + char buffer[8]; + + memset(buffer, 'X', sizeof(buffer)); + char* end = ffStrCopy(buffer, "abc", sizeof(buffer)); + VERIFY(ffStrEquals(buffer, "abc")); + VERIFY(end == buffer + 3); + + memset(buffer, 'X', sizeof(buffer)); + end = ffStrCopy(buffer, "abcdef", sizeof(buffer)); + VERIFY(ffStrEquals(buffer, "abcdef")); + VERIFY(end == buffer + 6); + + // Filling the buffer exactly still leaves room for the terminator + char exact[4]; + memset(exact, 'X', sizeof(exact)); + end = ffStrCopy(exact, "abc", sizeof(exact)); + VERIFY(ffStrEquals(exact, "abc")); + VERIFY(end == exact + 3); + + // One byte short: the copy is truncated to `dstBufSiz - 1` bytes + char truncated[3]; + memset(truncated, 'X', sizeof(truncated)); + end = ffStrCopy(truncated, "abcdef", sizeof(truncated)); + VERIFY(ffStrEquals(truncated, "ab")); + VERIFY(end == truncated + 2); + + // A one byte buffer holds nothing but the terminator + char single[1]; + single[0] = 'X'; + end = ffStrCopy(single, "abc", sizeof(single)); + VERIFY(single[0] == '\0'); + VERIFY(end == single); + + // A zero sized buffer is left completely alone + char untouched[1] = { 'X' }; + end = ffStrCopy(untouched, "abc", 0); + VERIFY(untouched[0] == 'X'); + VERIFY(end == untouched); + + VERIFY(ffStrCopy(nullptr, "abc", 8) == nullptr); + } + + // FF_STR stringifies its argument + { + VERIFY(ffStrEquals(FF_STR(1), "1")); + VERIFY(ffStrEquals(FF_STR(123), "123")); + } + #if FF_ENABLE_WCWIDTH { uint8_t width = 255; @@ -102,7 +264,7 @@ int main(void) { puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); #else - puts("\033[33mTests skipped because wcwidth support is disabled." FASTFETCH_TEXT_MODIFIER_RESET); + puts("\033[33mwcwidth tests skipped because wcwidth support is disabled." FASTFETCH_TEXT_MODIFIER_RESET); #endif return 0; } diff --git a/tests/temps.c b/tests/temps.c new file mode 100644 index 000000000..84978bff4 --- /dev/null +++ b/tests/temps.c @@ -0,0 +1,177 @@ +#include "common/option.h" +#include "common/temps.h" +#include "common/textModifier.h" +#include "fastfetch.h" + +#include +#include +#include + +static FFModuleArgs module; + +static void verify(double celsius, FFColorRangeConfig config, const char* expected, int lineNo) { + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate(); + ffTempsAppendNum(celsius, &result, config, &module); + + if (!ffStrbufEqualS(&result, expected)) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffTempsAppendNum(%f, {%u, %u}): expected \"%s\", got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, celsius, config.green, config.yellow, expected, result.chars); + exit(1); + } +} + +#define VERIFY_TEMP(celsius, config, expected) verify((celsius), (config), (expected), __LINE__) + +int main(void) { + // Use the real defaults rather than a hand built config, so a changed default is caught here too + ffOptionsInitDisplay(&instance.config.display); + FFOptionsDisplay* options = &instance.config.display; + ffOptionInitModuleArg(&module, ""); + + // Pin the threshold colors: the yellow and red defaults depend on whether the terminal is + // detected as a light theme, which is not something a test should depend on. + ffStrbufSetS(&options->tempColorGreen, "32"); + ffStrbufSetS(&options->tempColorYellow, "33"); + ffStrbufSetS(&options->tempColorRed, "31"); + + const FFColorRangeConfig config = { .green = 50, .yellow = 80 }; + + // An unknown temperature prints nothing at all, not even the unit + { + options->pipe = true; + VERIFY_TEMP(-DBL_MAX, config, ""); + } + + // The default unit is Celsius, with one digit after the point + { + options->pipe = true; + VERIFY_TEMP(0, config, "0.0°C"); + VERIFY_TEMP(25.5, config, "25.5°C"); + VERIFY_TEMP(-10, config, "-10.0°C"); + VERIFY_TEMP(-273.15, config, "-273.1°C"); + } + + // tempNdigits + { + options->pipe = true; + options->tempNdigits = 0; + VERIFY_TEMP(25.4, config, "25°C"); + VERIFY_TEMP(25.6, config, "26°C"); + VERIFY_TEMP(0, config, "0°C"); + + options->tempNdigits = 2; + VERIFY_TEMP(25.5, config, "25.50°C"); + + options->tempNdigits = 1; + } + + // Celsius and Fahrenheit only take a space before the unit when it is forced on, while Kelvin + // takes one by default. That asymmetry is deliberate and easy to break. + { + options->pipe = true; + options->tempNdigits = 2; // so that 25.5 + 273.15 prints exactly + + options->tempSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_DEFAULT; + VERIFY_TEMP(25.5, config, "25.50°C"); + options->tempUnit = FF_TEMPERATURE_UNIT_KELVIN; + VERIFY_TEMP(25.5, config, "298.65 K"); + options->tempUnit = FF_TEMPERATURE_UNIT_DEFAULT; + + options->tempSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_NEVER; + VERIFY_TEMP(25.5, config, "25.50°C"); + options->tempUnit = FF_TEMPERATURE_UNIT_KELVIN; + VERIFY_TEMP(25.5, config, "298.65K"); + options->tempUnit = FF_TEMPERATURE_UNIT_DEFAULT; + + options->tempSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_ALWAYS; + VERIFY_TEMP(25.5, config, "25.50 °C"); + options->tempUnit = FF_TEMPERATURE_UNIT_KELVIN; + VERIFY_TEMP(25.5, config, "298.65 K"); + options->tempUnit = FF_TEMPERATURE_UNIT_DEFAULT; + + options->tempSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_DEFAULT; + options->tempNdigits = 1; + } + + // Fahrenheit and Kelvin are conversions of the Celsius value that the thresholds were compared + // against, so the unit changes what is printed but never which band is picked. + { + options->pipe = true; + + options->tempUnit = FF_TEMPERATURE_UNIT_FAHRENHEIT; + VERIFY_TEMP(0, config, "32.0°F"); + VERIFY_TEMP(100, config, "212.0°F"); + VERIFY_TEMP(37, config, "98.6°F"); + VERIFY_TEMP(-40, config, "-40.0°F"); // the one point where the two scales meet + + options->tempNdigits = 2; + options->tempUnit = FF_TEMPERATURE_UNIT_KELVIN; + VERIFY_TEMP(0, config, "273.15 K"); + VERIFY_TEMP(100, config, "373.15 K"); + VERIFY_TEMP(-273.15, config, "0.00 K"); + options->tempNdigits = 1; + + options->tempUnit = FF_TEMPERATURE_UNIT_DEFAULT; + } + + // Colors, with the same band rules as the percentage module: the threshold itself belongs to the + // lower band, and the reset sequence is always emitted when colors are on. + { + options->pipe = false; + + VERIFY_TEMP(0, config, "\e[32m0.0°C\e[m"); + VERIFY_TEMP(50, config, "\e[32m50.0°C\e[m"); + VERIFY_TEMP(51, config, "\e[33m51.0°C\e[m"); + VERIFY_TEMP(80, config, "\e[33m80.0°C\e[m"); + VERIFY_TEMP(81, config, "\e[31m81.0°C\e[m"); + VERIFY_TEMP(100, config, "\e[31m100.0°C\e[m"); + + // The color is chosen from the Celsius value, so a display unit that is not Celsius does not + // move the threshold + options->tempUnit = FF_TEMPERATURE_UNIT_FAHRENHEIT; + VERIFY_TEMP(81, config, "\e[31m177.8°F\e[m"); + options->tempUnit = FF_TEMPERATURE_UNIT_DEFAULT; + + // An unknown temperature has no color at all + VERIFY_TEMP(-DBL_MAX, config, ""); + + // When `green > yellow` the bands are inverted, so a low value is the alarming one + const FFColorRangeConfig inverted = { .green = 80, .yellow = 50 }; + VERIFY_TEMP(0, inverted, "\e[31m0.0°C\e[m"); + VERIFY_TEMP(49, inverted, "\e[31m49.0°C\e[m"); + VERIFY_TEMP(50, inverted, "\e[33m50.0°C\e[m"); + VERIFY_TEMP(79, inverted, "\e[33m79.0°C\e[m"); + VERIFY_TEMP(80, inverted, "\e[32m80.0°C\e[m"); + VERIFY_TEMP(100, inverted, "\e[32m100.0°C\e[m"); + } + + // The color is restored to the module's output color, so the rest of the line keeps its own + { + options->pipe = false; + ffStrbufSetS(&module.outputColor, "94"); + VERIFY_TEMP(0, config, "\e[32m0.0°C\e[m\e[94m"); + ffStrbufClear(&module.outputColor); + + // ... and it falls back to the global output color when the module has none + ffStrbufSetS(&options->colorOutput, "95"); + VERIFY_TEMP(0, config, "\e[32m0.0°C\e[m\e[95m"); + ffStrbufClear(&options->colorOutput); + } + + // The result is appended to the buffer instead of replacing it + { + options->pipe = true; + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateS("Temp: "); + ffTempsAppendNum(25.5, &result, config, &module); + if (!ffStrbufEqualS(&result, "Temp: 25.5°C")) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffTempsAppendNum did not append: got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, __LINE__, result.chars); + exit(1); + } + } + + ffOptionDestroyModuleArg(&module); + ffOptionsDestroyDisplay(options); + + puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); + return 0; +}