diff --git a/doc/xml/release/2025/2.56.0.xml b/doc/xml/release/2025/2.56.0.xml
index c4fbc777d..e1a49ead5 100644
--- a/doc/xml/release/2025/2.56.0.xml
+++ b/doc/xml/release/2025/2.56.0.xml
@@ -13,6 +13,19 @@
Fix defaults in command-line help.
+
+
+
+
+
+
+
+
+
+
+ Improve the predictability of floating point numbers formatted as strings.
+
+
diff --git a/src/command/archive/get/get.c b/src/command/archive/get/get.c
index 0bbebd182..612d50be8 100644
--- a/src/command/archive/get/get.c
+++ b/src/command/archive/get/get.c
@@ -793,7 +793,9 @@ cmdArchiveGet(void)
ArchiveTimeoutError,
"unable to get WAL file '%s' from the archive asynchronously after %s second(s)\n"
"HINT: check '%s' for errors.",
- strZ(walSegment), strZ(strNewDbl((double)cfgOptionInt64(cfgOptArchiveTimeout) / MSEC_PER_SEC)),
+ strZ(walSegment),
+ strZ(
+ strNewDivP((uint64_t)cfgOptionInt64(cfgOptArchiveTimeout), MSEC_PER_SEC, .precision = 3, .trim = true)),
strZ(cfgLoadLogFileName(cfgCmdRoleAsync)));
}
// Else report that the WAL segment could not be found
diff --git a/src/command/backup/backup.c b/src/command/backup/backup.c
index bb773e0e5..b5f43733e 100644
--- a/src/command/backup/backup.c
+++ b/src/command/backup/backup.c
@@ -1456,11 +1456,10 @@ backupJobResult(
if (copySize != file.sizeOriginal)
strCatFmt(logProgress, "%s->", strZ(strSizeFormat(file.sizeOriginal)));
- // Store percentComplete as an integer
- percentComplete = sizeTotal == 0 ? 10000 : (unsigned int)(((double)*sizeProgress / (double)sizeTotal) * 10000);
+ // Store percentComplete as an integer (used to update progress in the lock file)
+ percentComplete = cvtPctToUInt(*sizeProgress, sizeTotal);
- strCatFmt(
- logProgress, "%s, %u.%02u%%", strZ(strSizeFormat(copySize)), percentComplete / 100, percentComplete % 100);
+ strCatFmt(logProgress, "%s, %s", strZ(strSizeFormat(copySize)), strZ(strNewPct(*sizeProgress, sizeTotal)));
// Format log checksum
const String *const logChecksum =
diff --git a/src/command/info/info.c b/src/command/info/info.c
index 0a1e7279f..7e9f116d5 100644
--- a/src/command/info/info.c
+++ b/src/command/info/info.c
@@ -251,7 +251,7 @@ stanzaStatus(const int code, const InfoStanzaRepo *const stanzaData, const Varia
{
kvPut(
backupLockKv, STATUS_KEY_LOCK_BACKUP_PERCENT_COMPLETE_VAR,
- VARUINT((unsigned int)(((double)stanzaData->sizeComplete / (double)stanzaData->size) * 10000)));
+ VARUINT(cvtPctToUInt(stanzaData->sizeComplete, stanzaData->size)));
}
}
diff --git a/src/command/restore/restore.c b/src/command/restore/restore.c
index ef3789138..86b410f8b 100644
--- a/src/command/restore/restore.c
+++ b/src/command/restore/restore.c
@@ -2138,7 +2138,7 @@ restoreJobResult(
// Add size and percent complete
sizeRestored += file.size;
- strCatFmt(log, "%s, %.2lf%%)", strZ(strSizeFormat(file.size)), (double)sizeRestored * 100.00 / (double)sizeTotal);
+ strCatFmt(log, "%s, %s)", strZ(strSizeFormat(file.size)), strZ(strNewPct(sizeRestored, sizeTotal)));
// If not zero-length add the checksum
if (file.size != 0 && !zeroed)
diff --git a/src/common/debug.h b/src/common/debug.h
index 9b85e6ab4..ccc0a4ab7 100644
--- a/src/common/debug.h
+++ b/src/common/debug.h
@@ -200,11 +200,6 @@ FN_EXTERN size_t typeToLog(const char *typeName, char *buffer, size_t bufferSize
#define FUNCTION_LOG_CHARPY_FORMAT(value, buffer, bufferSize) \
ptrToLog(value, "char *[]", buffer, bufferSize)
-#define FUNCTION_LOG_DOUBLE_TYPE \
- double
-#define FUNCTION_LOG_DOUBLE_FORMAT(value, buffer, bufferSize) \
- cvtDoubleToZ(value, buffer, bufferSize)
-
#define FUNCTION_LOG_INT_TYPE \
int
#define FUNCTION_LOG_INT_FORMAT(value, buffer, bufferSize) \
diff --git a/src/common/type/convert.c b/src/common/type/convert.c
index fbdb77339..13c9a1f33 100644
--- a/src/common/type/convert.c
+++ b/src/common/type/convert.c
@@ -118,48 +118,245 @@ cvtBoolToConstZ(bool value)
return value ? TRUE_Z : FALSE_Z;
}
-/**********************************************************************************************************************************/
-FN_EXTERN size_t
-cvtDoubleToZ(const double value, char *const buffer, const size_t bufferSize)
+/***********************************************************************************************************************************
+Round an integer contained in a string
+***********************************************************************************************************************************/
+static size_t
+cvtRound(size_t result, char *const buffer, const size_t bufferSize)
{
FUNCTION_TEST_BEGIN();
- FUNCTION_TEST_PARAM(DOUBLE, value);
+ FUNCTION_TEST_PARAM(SIZE, result);
FUNCTION_TEST_PARAM_P(CHARDATA, buffer);
FUNCTION_TEST_PARAM(SIZE, bufferSize);
FUNCTION_TEST_END();
ASSERT(buffer != NULL);
+ ASSERT(bufferSize > 0);
- // Convert to a string
- const size_t result = (size_t)snprintf(buffer, bufferSize, "%lf", value);
+ for (int roundIdx = (int)result - 2; roundIdx >= 0; roundIdx--)
+ {
+ // Round when the rounding digit is >= 5 (current digit needs to be incremented) or the current digit > 9 (prior carry
+ // overflowed and needs to be carried again)
+ if (((roundIdx == (int)result - 2) && buffer[roundIdx + 1] >= '5') || buffer[roundIdx] > '9')
+ {
+ // Carry to the prior digit
+ if (buffer[roundIdx] >= '9')
+ {
+ // If this is the first digit then add a new prior digit to carry to. Since it will start as zero we can just
+ // set it to one.
+ if (roundIdx == 0)
+ {
+ if (result + 1 >= bufferSize)
+ THROW(AssertError, "buffer overflow");
+
+ memmove(buffer + 1, buffer, ++result);
+ buffer[1] = '0';
+ buffer[0] = '1';
+ }
+ // Else set current digit to zero and carry to prior digit. An overflow is handled on the next iteration.
+ else
+ {
+ buffer[roundIdx] = '0';
+ buffer[roundIdx - 1]++;
+ }
+ }
+ // Else increment current digit
+ else
+ buffer[roundIdx]++;
+ }
+ // Else stop rounding
+ else
+ break;
+ }
+
+ // Remove rightmost digit used to start rounding
+ result--;
+ buffer[result] = '\0';
+
+ // Return string length
+ FUNCTION_TEST_RETURN(SIZE, result);
+}
+
+/***********************************************************************************************************************************
+Separate the fractional part of an integer container in a string
+***********************************************************************************************************************************/
+static size_t
+cvtFraction(size_t result, const unsigned int precision, const bool trim, char *const buffer, const size_t bufferSize)
+{
+ FUNCTION_TEST_BEGIN();
+ FUNCTION_TEST_PARAM(SIZE, result);
+ FUNCTION_TEST_PARAM(UINT, precision);
+ FUNCTION_TEST_PARAM(BOOL, trim);
+ FUNCTION_TEST_PARAM_P(CHARDATA, buffer);
+ FUNCTION_TEST_PARAM(SIZE, bufferSize);
+ FUNCTION_TEST_END();
+
+ ASSERT(buffer != NULL);
+ ASSERT(bufferSize > 0);
+
+ // Add decimal point
+ if (result + 1 >= bufferSize)
+ THROW(AssertError, "buffer overflow");
+
+ memmove(buffer + result - precision + 1, buffer + result - precision, precision + 1);
+ buffer[result - precision] = '.';
+ buffer[++result] = '\0';
+
+ // Strip off any final 0s and the decimal point if there are no non-zero digits after it
+ if (trim)
+ {
+ char *end = buffer + result - 1;
+
+ while (*end == '0' || *end == '.')
+ {
+ // It should not be possible to go past the beginning because a decimal point is always written
+ ASSERT(end > buffer);
+
+ end--;
+
+ if (*(end + 1) == '.')
+ break;
+ }
+
+ // Zero terminate the string
+ end[1] = 0;
+
+ // Calculate length
+ result = (size_t)(end - buffer + 1);
+ }
+
+ // Return string length
+ FUNCTION_TEST_RETURN(SIZE, result);
+}
+
+/**********************************************************************************************************************************/
+FN_EXTERN unsigned int
+cvtPctToUInt(const uint64_t dividend, const uint64_t divisor)
+{
+ FUNCTION_TEST_BEGIN();
+ FUNCTION_TEST_PARAM(UINT64, dividend);
+ FUNCTION_TEST_PARAM(UINT64, divisor);
+ FUNCTION_TEST_END();
+
+ ASSERT(dividend <= divisor);
+
+ // If 100% then return a fixed value to avoid any rounding throwing off the result
+ if (dividend == divisor)
+ FUNCTION_TEST_RETURN(UINT, 10000);
+
+ // Calculate percentage
+ char buffer[CVT_PCT_BUFFER_SIZE];
+
+ size_t size = (size_t)snprintf(buffer, sizeof(buffer), "%04" PRIu64, (uint64_t)((double)dividend / (double)divisor * 100000));
+
+ // Round
+ cvtRound(size, buffer, sizeof(buffer));
+
+ FUNCTION_TEST_RETURN(UINT, cvtZToUInt(buffer));
+}
+
+/**********************************************************************************************************************************/
+FN_EXTERN size_t
+cvtPctToZ(const uint64_t dividend, const uint64_t divisor, char *const buffer, const size_t bufferSize)
+{
+ FUNCTION_TEST_BEGIN();
+ FUNCTION_TEST_PARAM(UINT64, dividend);
+ FUNCTION_TEST_PARAM(UINT64, divisor);
+ FUNCTION_TEST_PARAM_P(CHARDATA, buffer);
+ FUNCTION_TEST_PARAM(SIZE, bufferSize);
+ FUNCTION_TEST_END();
+
+ ASSERT(buffer != NULL);
+ ASSERT(bufferSize > 0);
+ ASSERT(dividend <= divisor);
+
+ // Calculate percentage as an integer
+ size_t result = (size_t)snprintf(buffer, bufferSize, "%03u", cvtPctToUInt(dividend, divisor));
if (result >= bufferSize)
THROW(AssertError, "buffer overflow");
- // Any formatted double should be at least 8 characters, i.e. 0.000000
- ASSERT(strlen(buffer) >= 8);
- // Any formatted double should have a decimal point
- ASSERT(strchr(buffer, '.') != NULL);
+ // Separate fractional part
+ result = cvtFraction(result, 2, false, buffer, bufferSize);
- // Strip off any final 0s and the decimal point if there are no non-zero digits after it
- char *end = buffer + strlen(buffer) - 1;
+ // Add percent sign
+ if (result + 1 >= bufferSize)
+ THROW(AssertError, "buffer overflow");
- while (*end == '0' || *end == '.')
+ buffer[result++] = '%';
+ buffer[result] = '\0';
+
+ // Return string length
+ FUNCTION_TEST_RETURN(SIZE, result);
+}
+
+/**********************************************************************************************************************************/
+FN_EXTERN size_t
+cvtDivToZ(
+ const uint64_t dividend, const uint64_t divisor, const unsigned int precision, const bool trim, char *const buffer,
+ const size_t bufferSize)
+{
+ FUNCTION_TEST_BEGIN();
+ FUNCTION_TEST_PARAM(UINT64, dividend);
+ FUNCTION_TEST_PARAM(UINT64, divisor);
+ FUNCTION_TEST_PARAM(UINT, precision);
+ FUNCTION_TEST_PARAM(BOOL, trim);
+ FUNCTION_TEST_PARAM_P(CHARDATA, buffer);
+ FUNCTION_TEST_PARAM(SIZE, bufferSize);
+ FUNCTION_TEST_END();
+
+ ASSERT(buffer != NULL);
+ ASSERT(bufferSize > 0);
+
+ // Determine multiplier for precision digits
+ unsigned int multiplier = 1;
+
+ switch (precision)
{
- // It should not be possible to go past the beginning because format "%lf" will always write a decimal point
- ASSERT(end > buffer);
+ case 0:
+ break;
- end--;
+ case 1:
+ multiplier = 10;
+ break;
- if (*(end + 1) == '.')
+ case 2:
+ multiplier = 100;
+ break;
+
+ default:
+ CHECK_FMT(AssertError, precision <= 3, "precision %u is invalid", precision);
+ multiplier = 1000;
break;
}
- // Zero terminate the string
- end[1] = 0;
+ CHECK_FMT(AssertError, dividend <= UINT64_MAX / multiplier, "dividend %" PRIu64 " is too large", dividend);
+
+ // If possible add a digit for rounding
+ bool round = false;
+
+ if (dividend <= UINT64_MAX / (multiplier * 10))
+ {
+ round = true;
+ multiplier *= 10;
+ }
+
+ // Convert to string
+ size_t result = (size_t)snprintf(buffer, bufferSize, "%0*" PRIu64, (int)precision, dividend * multiplier / divisor);
+
+ if (result >= bufferSize)
+ THROW(AssertError, "buffer overflow");
+
+ // Round
+ if (round)
+ result = cvtRound(result, buffer, bufferSize);
+
+ // Separate fractional part
+ if (precision > 0)
+ result = cvtFraction(result, precision, trim, buffer, bufferSize);
// Return string length
- FUNCTION_TEST_RETURN(SIZE, (size_t)(end - buffer + 1));
+ FUNCTION_TEST_RETURN(SIZE, result);
}
/**********************************************************************************************************************************/
diff --git a/src/common/type/convert.h b/src/common/type/convert.h
index 074878d43..140f7c947 100644
--- a/src/common/type/convert.h
+++ b/src/common/type/convert.h
@@ -19,7 +19,8 @@ Required buffer sizes
***********************************************************************************************************************************/
#define CVT_BOOL_BUFFER_SIZE 6
#define CVT_BASE10_BUFFER_SIZE 64
-#define CVT_DOUBLE_BUFFER_SIZE 318
+#define CVT_DIV_BUFFER_SIZE 68
+#define CVT_PCT_BUFFER_SIZE 8
#define CVT_VARINT128_BUFFER_SIZE 10
/***********************************************************************************************************************************
@@ -35,8 +36,14 @@ cvtCharToZ(const char value, char *const buffer, const size_t bufferSize)
return (size_t)snprintf(buffer, bufferSize, "%c", value);
}
-// Convert double to zero-terminated string
-FN_EXTERN size_t cvtDoubleToZ(double value, char *buffer, size_t bufferSize);
+// Convert percentage to an integer. This is useful for exporting a percentage in a way that does not incur rounding.
+FN_EXTERN unsigned int cvtPctToUInt(uint64_t dividend, uint64_t divisor);
+
+// Convert percentage to zero-terminated string. This is more reproducible than formatting the results of floating point division.
+FN_EXTERN size_t cvtPctToZ(uint64_t dividend, uint64_t divisor, char *buffer, size_t bufferSize);
+
+// Convert division to zero-terminated string. This is more reproducible than formatting the results of floating point division.
+FN_EXTERN size_t cvtDivToZ(uint64_t dividend, uint64_t divisor, unsigned int precision, bool trim, char *buffer, size_t bufferSize);
// Convert int to zero-terminated string and vice versa
FN_EXTERN size_t cvtIntToZ(const int value, char *buffer, size_t bufferSize);
diff --git a/src/common/type/string.c b/src/common/type/string.c
index 70bab3b50..9f62651a1 100644
--- a/src/common/type/string.c
+++ b/src/common/type/string.c
@@ -162,17 +162,36 @@ strNewZ(const char *const string)
/**********************************************************************************************************************************/
FN_EXTERN String *
-strNewDbl(const double value)
+strNewDiv(const uint64_t dividend, const uint64_t divisor, StrNewDivParam param)
{
FUNCTION_TEST_BEGIN();
- FUNCTION_TEST_PARAM(DOUBLE, value);
+ FUNCTION_TEST_PARAM(UINT64, dividend);
+ FUNCTION_TEST_PARAM(UINT64, divisor);
+ FUNCTION_TEST_PARAM(UINT, param.precision);
+ FUNCTION_TEST_PARAM(BOOL, param.trim);
FUNCTION_TEST_END();
- char working[CVT_DOUBLE_BUFFER_SIZE];
+ char working[CVT_DIV_BUFFER_SIZE];
- cvtDoubleToZ(value, working, sizeof(working));
+ size_t resultSize = cvtDivToZ(dividend, divisor, param.precision, param.trim, working, sizeof(working));
- FUNCTION_TEST_RETURN(STRING, strNewZ(working));
+ FUNCTION_TEST_RETURN(STRING, strNewZN(working, resultSize));
+}
+
+/**********************************************************************************************************************************/
+FN_EXTERN String *
+strNewPct(const uint64_t dividend, const uint64_t divisor)
+{
+ FUNCTION_TEST_BEGIN();
+ FUNCTION_TEST_PARAM(UINT64, dividend);
+ FUNCTION_TEST_PARAM(UINT64, divisor);
+ FUNCTION_TEST_END();
+
+ char working[CVT_PCT_BUFFER_SIZE];
+
+ size_t resultSize = cvtPctToZ(dividend, divisor, working, sizeof(working));
+
+ FUNCTION_TEST_RETURN(STRING, strNewZN(working, resultSize));
}
/**********************************************************************************************************************************/
@@ -1052,26 +1071,31 @@ strSizeFormat(const uint64_t size)
if (size < 1024)
result = strNewFmt("%" PRIu64 "B", size);
- else if (size < (1024 * 1024))
- {
- if ((uint64_t)((double)size / 102.4) % 10 != 0)
- result = strNewFmt("%.1lfKB", (double)size / 1024);
- else
- result = strNewFmt("%" PRIu64 "KB", size / 1024);
- }
- else if (size < (1024 * 1024 * 1024))
- {
- if ((uint64_t)((double)size / (1024 * 102.4)) % 10 != 0)
- result = strNewFmt("%.1lfMB", (double)size / (1024 * 1024));
- else
- result = strNewFmt("%" PRIu64 "MB", size / (1024 * 1024));
- }
else
{
- if ((uint64_t)((double)size / (1024 * 1024 * 102.4)) % 10 != 0)
- result = strNewFmt("%.1lfGB", (double)size / (1024 * 1024 * 1024));
- else
- result = strNewFmt("%" PRIu64 "GB", size / (1024 * 1024 * 1024));
+ char working[CVT_DIV_BUFFER_SIZE];
+ uint64_t divisor = 1024 * 1024 * 1024;
+ unsigned int precision = 1;
+ const char *suffix = "GB";
+
+ if (size < (1024 * 1024))
+ {
+ divisor = 1024;
+ suffix = "KB";
+ }
+ else if (size < (1024 * 1024 * 1024))
+ {
+ divisor = 1024 * 1024;
+ suffix = "MB";
+ }
+
+ // Skip precision when it would cause overflow
+ if (size > UINT64_MAX / 10)
+ precision = 0;
+
+ // Format size
+ cvtDivToZ(size, divisor, precision, true, working, sizeof(working));
+ result = strNewFmt("%s%s", working, suffix);
}
FUNCTION_TEST_RETURN(STRING, result);
diff --git a/src/common/type/string.h b/src/common/type/string.h
index 1f3769c64..d87a1c00f 100644
--- a/src/common/type/string.h
+++ b/src/common/type/string.h
@@ -66,8 +66,21 @@ FN_EXTERN String *strNewZN(const char *string, size_t size);
// will be copied but only the data before the NULL character will be used as a string.
FN_EXTERN String *strNewBuf(const Buffer *buffer);
-// Create a new fixed length string by converting the double value
-FN_EXTERN String *strNewDbl(double value);
+// Create a new fixed length string by performing division
+typedef struct StrNewDivParam
+{
+ VAR_PARAM_HEADER;
+ unsigned int precision; // Digits of precision after the decimal
+ bool trim; // Trim trailing zeros and decimal point?
+} StrNewDivParam;
+
+#define strNewDivP(dividend, divisor, ...) \
+ strNewDiv(dividend, divisor, (StrNewDivParam){VAR_PARAM_INIT, __VA_ARGS__})
+
+FN_EXTERN String *strNewDiv(uint64_t dividend, uint64_t divisor, StrNewDivParam param);
+
+// Create a new fixed length string containing a percentage
+FN_EXTERN String *strNewPct(uint64_t dividend, uint64_t divisor);
// Create a new fixed length string by converting a timestamp
typedef struct StrNewTimeParam
diff --git a/src/config/config.c b/src/config/config.c
index 25cf81e17..237cd34db 100644
--- a/src/config/config.c
+++ b/src/config/config.c
@@ -448,7 +448,7 @@ cfgOptionDisplayVar(const Variant *const value, const ConfigOptionType optionTyp
}
else if (optionType == cfgOptTypeTime)
{
- FUNCTION_TEST_RETURN_CONST(STRING, strNewDbl((double)varInt64(value) / MSEC_PER_SEC));
+ FUNCTION_TEST_RETURN_CONST(STRING, strNewDivP(varUInt64Force(value), MSEC_PER_SEC, .precision = 3, .trim = true));
}
else if (optionType == cfgOptTypeStringId)
{
diff --git a/test/define.yaml b/test/define.yaml
index ef20b109f..1431efddd 100644
--- a/test/define.yaml
+++ b/test/define.yaml
@@ -102,7 +102,7 @@ unit:
# ----------------------------------------------------------------------------------------------------------------------------
- name: type-convert
- total: 12
+ total: 13
coverage:
- common/type/convert
diff --git a/test/src/command/test/coverage.c b/test/src/command/test/coverage.c
index fc23b8b2c..5aece66fc 100644
--- a/test/src/command/test/coverage.c
+++ b/test/src/command/test/coverage.c
@@ -816,14 +816,11 @@ testCvgSummaryValue(const unsigned int hit, const unsigned int total)
// Else render value
else
{
- strCatFmt(result, "%u/%u (", hit, total);
-
- if (hit == total)
- strCatZ(result, "100.0");
- else
- strCatFmt(result, "%.2f", (float)hit * 100 / (float)total);
-
- strCatZ(result, "%)");
+ MEM_CONTEXT_TEMP_BEGIN()
+ {
+ strCatFmt(result, "%u/%u (%s)", hit, total, strZ(strNewPct(hit, total)));
+ }
+ MEM_CONTEXT_TEMP_END();
}
FUNCTION_LOG_RETURN(STRING, result);
diff --git a/test/src/module/command/backupTest.c b/test/src/module/command/backupTest.c
index b0e4285e1..245326b20 100644
--- a/test/src/module/command/backupTest.c
+++ b/test/src/module/command/backupTest.c
@@ -1944,7 +1944,7 @@ testRun(void)
"P00 WARN: no prior backup exists, incr backup has been changed to full\n"
"P00 WARN: --no-online passed and " PG_FILE_POSTMTRPID " exists but --force was passed so backup will continue though"
" it looks like " PG_NAME " is running and the backup will probably not be consistent\n"
- "P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (8KB, 99.86%%) checksum %s\n"
+ "P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (8KB, 99.87%%) checksum %s\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/postgresql.conf (11B, 100.00%%) checksum"
" e3db315c260e79211b7b52587123b7aa060f30ab\n"
"P00 INFO: new backup label = [FULL-1]\n"
@@ -3268,7 +3268,7 @@ testRun(void)
"P01 DETAIL: backup file " TEST_PATH "/pg1/postgresql.auto.conf (bundle 1/32, 12B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/postgresql.conf (bundle 1/64, 11B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/PG_VERSION (bundle 1/95, 2B, [PCT]) checksum [SHA1]\n"
- "P01 DETAIL: backup file " TEST_PATH "/pg1/bigish.dat (bundle 2/0, 8.0KB, [PCT]) checksum [SHA1]\n"
+ "P01 DETAIL: backup file " TEST_PATH "/pg1/bigish.dat (bundle 2/0, 8KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (bundle 3/0, 8KB, [PCT]) checksum [SHA1]\n"
"P00 INFO: execute non-exclusive backup stop and wait for all WAL segments to archive\n"
"P00 INFO: backup stop archive = 0000000105DB8EB000000001, lsn = 5db8eb0/180000\n"
@@ -3569,7 +3569,7 @@ testRun(void)
"P01 DETAIL: checksum resumed file " TEST_PATH "/pg1/block-incr-grow (24KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/PG_VERSION (bundle 1/0, 2B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/normal-same (bundle 1/2, 4B, [PCT]) checksum [SHA1]\n"
- "P01 DETAIL: backup file " TEST_PATH "/pg1/grow-to-block-incr (bundle 1/6, 16.0KB, [PCT]) checksum [SHA1]\n"
+ "P01 DETAIL: backup file " TEST_PATH "/pg1/grow-to-block-incr (bundle 1/6, 16KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (bundle 1/16389, 8KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-shrink-block (bundle 1/24581, 16KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-shrink-below (bundle 1/40987, 16KB, [PCT]) checksum [SHA1]\n"
@@ -3704,7 +3704,7 @@ testRun(void)
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (bundle 1/16411, 8KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-shrink-block (bundle 1/24603, 8KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-shrink-below (bundle 1/24620, 8B, [PCT]) checksum [SHA1]\n"
- "P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-shrink (bundle 1/24628, 16.0KB, [PCT]) checksum [SHA1]\n"
+ "P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-shrink (bundle 1/24628, 16KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: match file from prior backup " TEST_PATH "/pg1/block-incr-same (16KB, [PCT]) checksum [SHA1]\n"
"P00 DETAIL: reference pg_data/PG_VERSION to 20191103-165320F\n"
"P00 DETAIL: reference pg_data/block-incr-same to 20191103-165320F\n"
diff --git a/test/src/module/command/infoTest.c b/test/src/module/command/infoTest.c
index 36957953f..c94a5443b 100644
--- a/test/src/module/command/infoTest.c
+++ b/test/src/module/command/infoTest.c
@@ -607,13 +607,13 @@ testRun(void)
" timestamp start/stop: 2018-11-16 15:47:56+00 / 2018-11-16 15:48:09+00\n"
" wal start/stop: n/a\n"
" database size: 25.7MB, database backup size: 25.7MB\n"
- " repo1: backup set size: 3MB, backup size: 3KB\n"
+ " repo1: backup set size: 3MB, backup size: 3.1KB\n"
"\n"
" full backup: 20201116-154900F\n"
" timestamp start/stop: 2020-11-16 15:47:56+00 / 2020-11-16 15:48:00+00\n"
" wal start/stop: 000000030000000000000001 / 000000030000000000000001\n"
" database size: 25.7MB, database backup size: 25.7MB\n"
- " repo1: backup set size: 3MB, backup size: 3KB\n",
+ " repo1: backup set size: 3MB, backup size: 3.1KB\n",
"text - single stanza, valid backup, no priors, no archives in latest DB, backup/expire lock detected");
// Notify child to release lock
@@ -1530,7 +1530,7 @@ testRun(void)
TEST_RESULT_STR_Z(
infoRender(),
"stanza: stanza1\n"
- " status: ok (backup/expire running - 65.27% complete)\n"
+ " status: ok (backup/expire running - 65.28% complete)\n"
" cipher: mixed\n"
" repo1: none\n"
" repo2: aes-256-cbc\n"
diff --git a/test/src/module/command/manifestTest.c b/test/src/module/command/manifestTest.c
index c3571c8e6..dd46701ce 100644
--- a/test/src/module/command/manifestTest.c
+++ b/test/src/module/command/manifestTest.c
@@ -179,7 +179,7 @@ testRun(void)
"\n"
"file list:\n"
" - pg_data/base/1/2\n"
- " size: 64KB, repo 64KB\n"
+ " size: 64KB, repo 64.1KB\n"
" checksum: 1adc95bebe9eea8c112d40cd04ab7a8d75c4f961\n"
" bundle: 1\n"
" block: size 8KB, map size 58B, checksum size 6B\n"
@@ -274,7 +274,7 @@ testRun(void)
"\n"
"file list:\n"
" - pg_data/base/1/2\n"
- " size: 96KB, repo 64KB\n"
+ " size: 96KB, repo 64.1KB\n"
" checksum: d4976e362696a43fb09e7d4e780d7d9352a2ec2e\n"
" bundle: 1\n"
" block: size 8KB, map size 99B, checksum size 6B\n"
@@ -379,7 +379,7 @@ testRun(void)
"\n"
"file list:\n"
" - pg_data/base/1/2\n"
- " size: 96KB, repo 64KB\n"
+ " size: 96KB, repo 64.1KB\n"
" checksum: d4976e362696a43fb09e7d4e780d7d9352a2ec2e\n"
" bundle: 1\n"
" block: size 8KB, map size 99B, checksum size 6B\n"
@@ -473,7 +473,7 @@ testRun(void)
"\n"
"file list:\n"
" - pg_data/base/1/2\n"
- " size: 96KB, repo 64KB\n"
+ " size: 96KB, repo 64.1KB\n"
" checksum: d4976e362696a43fb09e7d4e780d7d9352a2ec2e\n"
" bundle: 1\n"
" block: size 8KB, map size 99B, checksum size 6B\n"
diff --git a/test/src/module/command/restoreTest.c b/test/src/module/command/restoreTest.c
index 3a6247bf4..7e03f2182 100644
--- a/test/src/module/command/restoreTest.c
+++ b/test/src/module/command/restoreTest.c
@@ -2503,7 +2503,7 @@ testRun(void)
// Replace percent complete and restore size since they can cause a lot of churn when files are added/removed
hrnLogReplaceAdd(", [0-9]{1,3}\\.[0-9]{2}%\\)", "[0-9]+\\.[0-9]+%", "PCT", false);
- hrnLogReplaceAdd(" restore size = [0-9]+[A-Z]+", "[^ ]+$", "SIZE", false);
+ hrnLogReplaceAdd(" restore size = [0-9]+(\\.[0-9]+){0,1}[A-Z]+", "[^ ]+$", "SIZE", false);
argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
diff --git a/test/src/module/command/verifyTest.c b/test/src/module/command/verifyTest.c
index d6ae8cb08..bf7979b67 100644
--- a/test/src/module/command/verifyTest.c
+++ b/test/src/module/command/verifyTest.c
@@ -2337,7 +2337,7 @@ testRun(void)
"P00 INFO: backup start archive = 0000000105D95D3000000000, lsn = 5d95d30/0\n"
"P00 INFO: check archive for prior segment 0000000105D95D2F000000FF\n"
"P00 DETAIL: store zero-length file " TEST_PATH "/pg1/postgresql.auto.conf\n"
- "P01 DETAIL: backup file " TEST_PATH "/pg1/base/1/2 (bundle 1/0, 256KB, 96.96%) checksum [SHA1]\n"
+ "P01 DETAIL: backup file " TEST_PATH "/pg1/base/1/2 (bundle 1/0, 256KB, 96.97%) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (bundle 1/237, 8KB, 100.00%) checksum [SHA1]\n"
"P00 DETAIL: reference pg_data/PG_VERSION to 20191002-070640F\n"
"P00 DETAIL: reference pg_data/base/1/44 to 20191002-070640F\n"
diff --git a/test/src/module/common/debugOnTest.c b/test/src/module/common/debugOnTest.c
index e9fda3fc4..26eafae85 100644
--- a/test/src/module/common/debugOnTest.c
+++ b/test/src/module/common/debugOnTest.c
@@ -25,7 +25,7 @@ testFunction2(void)
static int
testFunction1(
- int paramInt, bool paramBool, bool *paramBoolP, bool **paramBoolPP, void *paramVoidP, double paramDouble, mode_t paramMode)
+ int paramInt, bool paramBool, bool *paramBoolP, bool **paramBoolPP, void *paramVoidP, mode_t paramMode)
{
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(INT, paramInt);
@@ -33,7 +33,6 @@ testFunction1(
FUNCTION_LOG_PARAM_P(BOOL, paramBoolP);
FUNCTION_LOG_PARAM_PP(BOOL, paramBoolPP);
FUNCTION_LOG_PARAM_P(VOID, paramVoidP);
- FUNCTION_LOG_PARAM(DOUBLE, paramDouble);
FUNCTION_LOG_PARAM(MODE, paramMode);
FUNCTION_LOG_END();
@@ -185,11 +184,11 @@ testRun(void)
if (testBegin("FUNCTION_DEBUG() and FUNCTION_LOG_RETURN()"))
{
harnessLogLevelSet(logLevelTrace);
- testFunction1(99, false, NULL, NULL, NULL, 1.17, 0755);
+ testFunction1(99, false, NULL, NULL, NULL, 0755);
TEST_RESULT_LOG(
"P00 DEBUG: " TEST_PGB_PATH "/test/src/module/common/debugOnTest::testFunction1: (paramInt: 99, paramBool: false,"
- " paramBoolP: null, paramBoolPP: null, paramVoidP: null, paramDouble: 1.17, paramMode: 0755)\n"
+ " paramBoolP: null, paramBoolPP: null, paramVoidP: null, paramMode: 0755)\n"
"P00 TRACE: " TEST_PGB_PATH "/test/src/module/common/debugOnTest::testFunction2: (void)\n"
"P00 TRACE: " TEST_PGB_PATH "/test/src/module/common/debugOnTest::testFunction2: => void\n"
"P00 DEBUG: " TEST_PGB_PATH "/test/src/module/common/debugOnTest::testFunction1: => 1");
@@ -200,11 +199,11 @@ testRun(void)
bool **testBoolPP = &testBoolP;
void *testVoidP = NULL;
- testFunction1(99, false, testBoolP, testBoolPP, testVoidP, 1.17, 0755);
+ testFunction1(99, false, testBoolP, testBoolPP, testVoidP, 0755);
TEST_RESULT_LOG(
"P00 DEBUG: " TEST_PGB_PATH "/test/src/module/common/debugOnTest::testFunction1: (paramInt: 99, paramBool: false,"
- " paramBoolP: *true, paramBoolPP: **true, paramVoidP: null, paramDouble: 1.17, paramMode: 0755)\n"
+ " paramBoolP: *true, paramBoolPP: **true, paramVoidP: null, paramMode: 0755)\n"
"P00 TRACE: " TEST_PGB_PATH "/test/src/module/common/debugOnTest::testFunction2: (void)\n"
"P00 TRACE: " TEST_PGB_PATH "/test/src/module/common/debugOnTest::testFunction2: => void\n"
"P00 DEBUG: " TEST_PGB_PATH "/test/src/module/common/debugOnTest::testFunction1: => 1");
@@ -212,18 +211,18 @@ testRun(void)
testBoolP = NULL;
testVoidP = (void *)1;
- testFunction1(99, false, testBoolP, testBoolPP, testVoidP, 1.17, 0755);
+ testFunction1(99, false, testBoolP, testBoolPP, testVoidP, 0755);
TEST_RESULT_LOG(
"P00 DEBUG: " TEST_PGB_PATH "/test/src/module/common/debugOnTest::testFunction1: (paramInt: 99, paramBool: false,"
- " paramBoolP: null, paramBoolPP: *null, paramVoidP: *void, paramDouble: 1.17, paramMode: 0755)\n"
+ " paramBoolP: null, paramBoolPP: *null, paramVoidP: *void, paramMode: 0755)\n"
"P00 TRACE: " TEST_PGB_PATH "/test/src/module/common/debugOnTest::testFunction2: (void)\n"
"P00 TRACE: " TEST_PGB_PATH "/test/src/module/common/debugOnTest::testFunction2: => void\n"
"P00 DEBUG: " TEST_PGB_PATH "/test/src/module/common/debugOnTest::testFunction1: => 1");
// -------------------------------------------------------------------------------------------------------------------------
harnessLogLevelReset();
- testFunction1(55, true, NULL, NULL, NULL, 0.99, 0755);
+ testFunction1(55, true, NULL, NULL, NULL, 0755);
TEST_RESULT_LOG("");
}
diff --git a/test/src/module/common/typeConvertTest.c b/test/src/module/common/typeConvertTest.c
index ba16505d3..ef2a58e09 100644
--- a/test/src/module/common/typeConvertTest.c
+++ b/test/src/module/common/typeConvertTest.c
@@ -34,20 +34,74 @@ testRun(void)
}
// *****************************************************************************************************************************
- if (testBegin("cvtDoubleToZ()"))
+ if (testBegin("cvtDivToZ()"))
{
- char buffer[STACK_TRACE_PARAM_MAX];
+ char buffer[CVT_DIV_BUFFER_SIZE];
- TEST_ERROR(cvtDoubleToZ(999.1234, buffer, 4), AssertError, "buffer overflow");
+ TEST_ERROR(cvtDivToZ(100, 2, 0, false, buffer, 2), AssertError, "buffer overflow");
+ TEST_ERROR(cvtDivToZ(999999, 100, 1, false, buffer, 7), AssertError, "buffer overflow");
+ TEST_ERROR(cvtDivToZ(UINT64_MAX / 100, 100, 2, false, buffer, 19), AssertError, "buffer overflow");
- TEST_RESULT_UINT(cvtDoubleToZ(999.1234, buffer, STACK_TRACE_PARAM_MAX), 8, "convert double to string");
- TEST_RESULT_Z(buffer, "999.1234", " check buffer");
+ TEST_RESULT_UINT(cvtDivToZ(100, 2, 0, false, buffer, sizeof(buffer)), 2, "no precision");
+ TEST_RESULT_Z(buffer, "50", " check buffer");
- TEST_RESULT_UINT(cvtDoubleToZ(999999999.123456, buffer, STACK_TRACE_PARAM_MAX), 16, "convert double to string");
- TEST_RESULT_Z(buffer, "999999999.123456", " check buffer");
+ TEST_RESULT_UINT(cvtDivToZ(100, 2, 3, false, buffer, sizeof(buffer)), 6, "all zero precision");
+ TEST_RESULT_Z(buffer, "50.000", " check buffer");
- TEST_RESULT_UINT(cvtDoubleToZ(999.0, buffer, STACK_TRACE_PARAM_MAX), 3, "convert double to string");
- TEST_RESULT_Z(buffer, "999", " check buffer");
+ TEST_RESULT_UINT(cvtDivToZ(100, 2, 3, true, buffer, sizeof(buffer)), 2, "all zero precision trimmed");
+ TEST_RESULT_Z(buffer, "50", " check buffer");
+
+ TEST_RESULT_UINT(cvtDivToZ(110, 100, 2, true, buffer, sizeof(buffer)), 3, "precision trimmed");
+ TEST_RESULT_Z(buffer, "1.1", " check buffer");
+
+ TEST_RESULT_UINT(cvtDivToZ(5, 4, 1, true, buffer, sizeof(buffer)), 3, "precision rounded");
+ TEST_RESULT_Z(buffer, "1.3", " check buffer");
+
+ TEST_RESULT_UINT(cvtDivToZ(5, 4, 3, true, buffer, sizeof(buffer)), 4, "precision exact");
+ TEST_RESULT_Z(buffer, "1.25", " check buffer");
+
+ TEST_RESULT_UINT(cvtDivToZ(100, 3, 3, false, buffer, sizeof(buffer)), 6, "precision rounded");
+ TEST_RESULT_Z(buffer, "33.333", " check buffer");
+
+ TEST_RESULT_UINT(cvtDivToZ(UINT64_MAX, 3, 0, false, buffer, sizeof(buffer)), 19, "no rounding for max allowed");
+ TEST_RESULT_Z(buffer, "6148914691236517205", " check buffer");
+
+ TEST_RESULT_UINT(cvtDivToZ(1999, 10, 0, false, buffer, sizeof(buffer)), 3, "round up");
+ TEST_RESULT_Z(buffer, "200", " check buffer");
+
+ TEST_RESULT_UINT(cvtDivToZ(999999, 100, 1, false, buffer, sizeof(buffer)), 7, "round up");
+ TEST_RESULT_Z(buffer, "10000.0", " check buffer");
+
+ TEST_RESULT_UINT(cvtDivToZ(9199, 10, 0, false, buffer, sizeof(buffer)), 3, "partial round up");
+ TEST_RESULT_Z(buffer, "920", " check buffer");
+
+ TEST_RESULT_UINT(cvtDivToZ(5555, 100, 1, false, buffer, sizeof(buffer)), 4, "partial round up");
+ TEST_RESULT_Z(buffer, "55.6", " check buffer");
+ }
+
+ // *****************************************************************************************************************************
+ if (testBegin("cvtPctToZ()"))
+ {
+ char buffer[CVT_PCT_BUFFER_SIZE];
+
+ TEST_ERROR(cvtPctToZ(2, 100, buffer, 2), AssertError, "buffer overflow");
+ TEST_ERROR(cvtPctToZ(100, 100, buffer, 2), AssertError, "buffer overflow");
+ TEST_ERROR(cvtPctToZ(2, 100, buffer, 5), AssertError, "buffer overflow");
+
+ TEST_RESULT_UINT(cvtPctToZ(467, 467, buffer, sizeof(buffer)), 7, "100%");
+ TEST_RESULT_Z(buffer, "100.00%", " check buffer");
+
+ TEST_RESULT_UINT(cvtPctToZ(111, 10000, buffer, sizeof(buffer)), 5, "100%");
+ TEST_RESULT_Z(buffer, "1.11%", " check buffer");
+
+ TEST_RESULT_UINT(cvtPctToZ(2, 3, buffer, sizeof(buffer)), 6, "66.67%");
+ TEST_RESULT_Z(buffer, "66.67%", " check buffer");
+
+ TEST_RESULT_UINT(cvtPctToZ(1, 10000, buffer, sizeof(buffer)), 5, "0.01%");
+ TEST_RESULT_Z(buffer, "0.01%", " check buffer");
+
+ TEST_RESULT_UINT(cvtPctToZ(0, 1, buffer, sizeof(buffer)), 5, "0.00%");
+ TEST_RESULT_Z(buffer, "0.00%", " check buffer");
}
// *****************************************************************************************************************************
diff --git a/test/src/module/common/typeStringTest.c b/test/src/module/common/typeStringTest.c
index beb92262b..90583c39b 100644
--- a/test/src/module/common/typeStringTest.c
+++ b/test/src/module/common/typeStringTest.c
@@ -65,9 +65,16 @@ testRun(void)
TEST_RESULT_STR_Z(strNewBuf(buffer), "12345678", "new string from buffer");
// -------------------------------------------------------------------------------------------------------------------------
- TEST_TITLE("new from double");
+ TEST_TITLE("new from division");
- TEST_RESULT_STR_Z(strNewDbl(999.1), "999.1", "new");
+ TEST_RESULT_STR_Z(strNewDivP(9991, 10, .precision = 2), "999.10", "new div");
+ TEST_RESULT_STR_Z(strNewDivP(9991, 10, .precision = 2, .trim = true), "999.1", "new div");
+ TEST_RESULT_STR_Z(strNewDivP(9991, 10, .precision = 0), "999", "new div");
+
+ // -------------------------------------------------------------------------------------------------------------------------
+ TEST_TITLE("new from percentage");
+
+ TEST_RESULT_STR_Z(strNewPct(2, 3), "66.67%", "new pct");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("new from time");
@@ -347,6 +354,7 @@ testRun(void)
TEST_RESULT_STR_Z(strSizeFormat(20162900), "19.2MB", "19.2 MB");
TEST_RESULT_STR_Z(strSizeFormat(1073741824), "1GB", "1 GB");
TEST_RESULT_STR_Z(strSizeFormat(1073741824 + 107374183), "1.1GB", "1.1 GB");
+ TEST_RESULT_STR_Z(strSizeFormat(UINT64_MAX / 10), "1717986918.3GB", "uint64 max / 10");
TEST_RESULT_STR_Z(strSizeFormat(UINT64_MAX), "17179869183GB", "uint64 max");
}
diff --git a/test/src/module/test/coverageTest.c b/test/src/module/test/coverageTest.c
index 9a7724825..4a07f371f 100644
--- a/test/src/module/test/coverageTest.c
+++ b/test/src/module/test/coverageTest.c
@@ -432,9 +432,9 @@ testRun(void)
"\n"
"\n"
" common/error/sub\n"
- " 1/1 (100.0%)\n"
+ " 1/1 (100.00%)\n"
" ---\n"
- " 1/1 (100.0%)\n"
+ " 1/1 (100.00%)\n"
"\n"
"\n"
"\n"