Initial commit

This commit is contained in:
2026-09-12 10:47:02 -05:00
commit 4b3ad9b6f8
6 changed files with 1039 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
+3
View File
@@ -0,0 +1,3 @@
.chinook/
.environ/
.vscode/
+32
View File
@@ -0,0 +1,32 @@
name: version
gpid: malunal
semv: 1.0.0
requires:
- remote: git@git.erasit.com:malunal/microtest
branch: v1.0.0
- remote: git@git.erasit.com:malunal/strview
branch: v1.0.0
- remote: git@git.erasit.com:malunal/types
branch: v1.0.0
targets:
- name: malunal.version
type: archive
deps:
- malunal.strview
- malunal.types
srcs:
- ./sources/version.c
tests:
- name: malunal.version.tests
type: program
deps:
- malunal.microtest
- malunal.version
srcs:
- ./tests/version.c
exports:
- malunal.version
+94
View File
@@ -0,0 +1,94 @@
/**
* @file version.h
* @brief Contains the structure and functions for a semantic version.
* @author John Christman (sorakatadzuma@gmail.com)
* @copyright Malunal Studios, LLC.
*/
#include "malunal/strview.h"
#include "malunal/types/error.h"
#ifndef MALUNAL_VERSION_HEADER
#define MALUNAL_VERSION_HEADER
/**
* @brief Imports the @c version_t error domain for error checking.
* @details The @c version_t error domain is specific for versions to indicate
* an issue with the parsing of a semantic version string into the
* @c version_t object.
*/
extern
const error_domain_t
ERROR_DOMAIN_VERSION_T;
/**
* @brief Contains information for a semantic version.
* @details Allows a semantic version to be parsed from some string and its
* content stored in this structure.
*/
define_struct(version) {
malunal_size_t major;
malunal_size_t minor;
malunal_size_t patch;
strview_t preview;
strview_t build;
};
/**
* @brief Defines a set of error codes for @c version_t.
* @details
*/
typedef enum {
VERSION_ERROR_NULL_INPUT,
VERSION_ERROR_INVALID_NUMBER,
VERSION_ERROR_UNEXPECTED_CHAR,
VERSION_ERROR_MISSING_PART,
VERSION_ERROR_LEADING_ZERO,
VERSION_ERROR_VALUE_OVERFLOW,
} version_error_t;
/**
* @brief Parses a version string into a @c version_t object.
* @param verstr The string containing the semantic version to be parsed.
* @param outver A pointer to the version to populate with the result.
* @returns An error if the semantic version string could not be parsed into the
* @c version_t object.
*/
error_t
parse_version(
strview_iptr_t verstr,
version_mptr_t outver
);
/**
* @brief Compares one @c version_t to another.
* @param self The @c version_t to compare against.
* @param other The @c version_t to compare with.
* @retval -1 if self is less than other.
* @retval 0 if self is equal to other.
* @retval 1 if self is more than other.
*/
malunal_int32_t
version_compare(
version_iptr_t self,
version_iptr_t other
);
/**
* @brief Converts the provided @c version_t to a string.
* @param self A pointer to the version object to convert to a string.
* @param buffer A pointer to where to write the semantic version string.
* @param length The length of the buffer which will receive the string.
* @returns The length of the version written or -1 if the buffer was too small.
*/
malunal_int32_t
version_string(
version_iptr_t self,
malunal_str_t buffer,
malunal_size_t length
);
#endif /* MALUNAL_VERSION_HEADER */
+446
View File
@@ -0,0 +1,446 @@
#include <ctype.h>
#include <errno.h>
#include <memory.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "malunal/version.h"
static
malunal_cstr_t
describe(malunal_int32_t code) {
switch (code) {
case VERSION_ERROR_NULL_INPUT:
return "Version input or output pointer was null.";
case VERSION_ERROR_INVALID_NUMBER:
return "Expected a numeric version component but found none, or trailing "
"characters remained after it.";
case VERSION_ERROR_UNEXPECTED_CHAR:
return "An unexpected character was encountered while parsing the version.";
case VERSION_ERROR_MISSING_PART:
return "The version string was missing one of its required parts.";
case VERSION_ERROR_LEADING_ZERO:
return "A numeric version component had a disallowed leading zero.";
case VERSION_ERROR_VALUE_OVERFLOW:
return "A numeric version component was too large to represent.";
}
return "Unknown version error";
}
const error_domain_t ERROR_DOMAIN_VERSION_T = {
.describe = &describe,
.name = "malunal.version.error"
};
static
error_t
parse_core_number(
strview_mptr_t verstr,
malunal_size_t* outval
) {
if (!isdigit((unsigned char)*verstr->beg))
return (error_t) {
.domain = &ERROR_DOMAIN_VERSION_T,
.code = VERSION_ERROR_INVALID_NUMBER
};
malunal_cstr_t start = verstr->beg;
while (isdigit((unsigned char)*verstr->beg))
verstr->beg++;
if (*verstr->beg != '.')
return (error_t) {
.domain = &ERROR_DOMAIN_VERSION_T,
.code = !(
strview_length(verstr) == 0 ||
*verstr->beg == '-' ||
*verstr->beg == '+' ||
*verstr->beg == '.'
) ? VERSION_ERROR_UNEXPECTED_CHAR
: VERSION_ERROR_MISSING_PART
};
if (verstr->beg - start > 1 && start[0] == '0')
return (error_t) {
.domain = &ERROR_DOMAIN_VERSION_T,
.code = VERSION_ERROR_LEADING_ZERO
};
strview_t walked = { start, verstr->beg };
malunal_int64_t value = 0;
error_t result = strview_to_int64(&walked, &value);
if (result.domain == &ERROR_DOMAIN_STRVIEW_T) {
if (result.code == STRVIEW_ERROR_OUT_OF_RANGE)
return (error_t) {
.domain = &ERROR_DOMAIN_VERSION_T,
.code = VERSION_ERROR_VALUE_OVERFLOW
};
if (result.code == STRVIEW_ERROR_UNCONSUMED)
return (error_t) {
.domain = &ERROR_DOMAIN_VERSION_T,
.code = VERSION_ERROR_INVALID_NUMBER
};
}
*outval = (malunal_size_t)value;
return NO_ERROR;
}
static
error_t
parse_patch_number(
strview_mptr_t verstr,
malunal_size_t* outval
) {
if (!isdigit((unsigned char)*verstr->beg))
return (error_t) {
.domain = &ERROR_DOMAIN_VERSION_T,
.code = VERSION_ERROR_INVALID_NUMBER
};
malunal_cstr_t start = verstr->beg;
while (isdigit((unsigned char)*verstr->beg))
verstr->beg++;
if (
strview_length(verstr) != 0 &&
*verstr->beg != '-' &&
*verstr->beg != '+'
) return (error_t) {
.domain = &ERROR_DOMAIN_VERSION_T,
.code = VERSION_ERROR_UNEXPECTED_CHAR
};
if (verstr->beg - start > 1 && start[0] == '0')
return (error_t) {
.domain = &ERROR_DOMAIN_VERSION_T,
.code = VERSION_ERROR_LEADING_ZERO
};
strview_t walked = { start, verstr->beg };
malunal_int64_t value = 0;
error_t result = strview_to_int64(&walked, &value);
if (result.domain == &ERROR_DOMAIN_STRVIEW_T) {
if (result.code == STRVIEW_ERROR_OUT_OF_RANGE)
return (error_t) {
.domain = &ERROR_DOMAIN_VERSION_T,
.code = VERSION_ERROR_VALUE_OVERFLOW
};
if (result.code == STRVIEW_ERROR_UNCONSUMED)
return (error_t) {
.domain = &ERROR_DOMAIN_VERSION_T,
.code = VERSION_ERROR_INVALID_NUMBER
};
}
*outval = (malunal_size_t)value;
return NO_ERROR;
}
static
error_t
parse_preview_string(
strview_mptr_t verstr,
strview_mptr_t outstr
) {
if (*verstr->beg != '-')
return NO_ERROR;
malunal_cstr_t start = ++verstr->beg;
if (strview_length(verstr) == 0 || *verstr->beg == '+')
return (error_t) {
.domain = &ERROR_DOMAIN_VERSION_T,
.code = VERSION_ERROR_MISSING_PART
};
while (strview_length(verstr) && *verstr->beg != '+')
verstr->beg++;
*outstr = (strview_t) {
.beg = start,
.end = verstr->beg
};
return NO_ERROR;
}
static
error_t
parse_build_string(
strview_mptr_t verstr,
strview_mptr_t outstr
) {
if (*verstr->beg != '+')
return NO_ERROR;
malunal_cstr_t start = ++verstr->beg;
if (strview_length(verstr) == 0)
return (error_t) {
.domain = &ERROR_DOMAIN_VERSION_T,
.code = VERSION_ERROR_MISSING_PART
};
while (strview_length(verstr) != 0)
verstr->beg++;
*outstr = (strview_t) {
.beg = start,
.end = verstr->beg
};
return NO_ERROR;
}
error_t
parse_version(
strview_iptr_t verstr,
version_mptr_t out
) {
if (verstr == null)
return (error_t) {
.domain = &ERROR_DOMAIN_VERSION_T,
.code = VERSION_ERROR_NULL_INPUT
};
if (out == null)
return (error_t) {
.domain = &ERROR_DOMAIN_VERSION_T,
.code = VERSION_ERROR_NULL_INPUT
};
memset(out, 0, sizeof(version_t));
if (verstr->beg == null)
return NO_ERROR;
error_t result;
strview_t tempstr = {
.beg = verstr->beg,
.end = verstr->end
};
result = parse_core_number(&tempstr, &out->major);
if (result.domain != null)
return result;
tempstr.beg++;
result = parse_core_number(&tempstr, &out->minor);
if (result.domain != null)
return result;
tempstr.beg++;
result = parse_patch_number(&tempstr, &out->patch);
if (result.domain != null)
return result;
result = parse_preview_string(&tempstr, &out->preview);
if (result.domain != null)
return result;
result = parse_build_string(&tempstr, &out->build);
if (result.domain != null)
return result;
if (strview_length(&tempstr) != 0)
return (error_t) {
.domain = &ERROR_DOMAIN_VERSION_T,
.code = VERSION_ERROR_UNEXPECTED_CHAR
};
return NO_ERROR;
}
static
malunal_bool_t
identifier_is_numeric(strview_iptr_t id) {
if (strview_length(id) == 0)
return false;
malunal_cstr_t p;
for (p = id->beg; p < id->end; p++)
if (!isdigit(*p))
return false;
return true;
}
static
malunal_int32_t
compare_numeric_identifier(
strview_iptr_t a,
strview_iptr_t b
) {
malunal_size_t alen = strview_length(a);
malunal_size_t blen = strview_length(b);
malunal_cstr_t ap = a->beg;
malunal_cstr_t bp = b->beg;
// Ignore leading zeros so two differently-padded numbers compare fairly.
while (alen > 1 && *ap == '0') { ap++; alen--; }
while (blen > 1 && *bp == '0') { bp++; blen--; }
if (alen != blen) return alen < blen ? -1 : 1;
if (alen == 0) return 0;
malunal_int32_t cmp = strncmp(ap, bp, alen);
return cmp < 0 ? -1 : (cmp > 0 ? 1 : 0);
}
static
malunal_int32_t
compare_alphanumeric_identifier(
strview_iptr_t a,
strview_iptr_t b
) {
malunal_size_t alen = strview_length(a);
malunal_size_t blen = strview_length(b);
malunal_size_t minlen = alen < blen ? alen : blen;
malunal_int32_t cmp = minlen > 0
? strncmp(a->beg, b->beg, (size_t)minlen)
: 0;
if (cmp != 0)
return cmp < 0 ? -1 : 1;
return alen != blen
? alen < blen ? -1 : 1
: 0;
}
static
malunal_int32_t
compare_preview_identifier(
strview_iptr_t a,
strview_iptr_t b
) {
malunal_bool_t anum = identifier_is_numeric(a);
malunal_bool_t bnum = identifier_is_numeric(b);
if (anum && bnum)
return compare_numeric_identifier(a, b);
// Per semver, numeric identifiers always have lower precedence than
// alphanumeric ones.
return anum == bnum
? compare_alphanumeric_identifier(a, b)
: anum ? -1 : 1;
}
static
malunal_int32_t
compare_preview(
strview_iptr_t self,
strview_iptr_t other
) {
strview_t sremain = *self;
strview_t oremain = *other;
for (;;) {
malunal_bool_t sdone = strview_length(&sremain) == 0;
malunal_bool_t odone = strview_length(&oremain) == 0;
if (sdone && odone)
return 0;
// A pre-release with fewer identifiers has lower precedence, as long as
// all the preceding identifiers matched.
if (sdone) return -1;
if (odone) return 1;
malunal_cstr_t send = sremain.beg;
while (send < sremain.end && *send != '.')
send++;
strview_t sid = { sremain.beg, send };
malunal_cstr_t oend = oremain.beg;
while (oend < oremain.end && *oend != '.')
oend++;
strview_t oid = { oremain.beg, oend };
malunal_int32_t cmp = compare_preview_identifier(&sid, &oid);
if (cmp != 0)
return cmp;
sremain.beg = send < sremain.end ? send + 1 : send;
oremain.beg = oend < oremain.end ? oend + 1 : oend;
}
}
malunal_int32_t
version_compare(
version_iptr_t self,
version_iptr_t other
) {
if (self == other) return 0;
if (self == null) return -1;
if (other == null) return 1;
if (self->major != other->major) return self->major < other->major ? -1 : 1;
if (self->minor != other->minor) return self->minor < other->minor ? -1 : 1;
if (self->patch != other->patch) return self->patch < other->patch ? -1 : 1;
malunal_bool_t self_has_preview = strview_length(&self->preview) != 0;
malunal_bool_t other_has_preview = strview_length(&other->preview) != 0;
if (self_has_preview != other_has_preview)
return self_has_preview ? -1 : 1;
return self_has_preview
? compare_preview(&self->preview, &other->preview)
: 0;
}
malunal_int32_t
version_string(
version_iptr_t self,
malunal_str_t buffer,
malunal_size_t length
) {
if (self == null || buffer == null || length == 0)
return -1;
malunal_size_t written = snprintf(
buffer,
length,
"%llu.%llu.%llu",
self->major,
self->minor,
self->patch
);
if (written < 0 || written >= length)
return -1;
malunal_size_t preview_len =
strview_length(&self->preview);
if (preview_len > 0) {
malunal_int32_t n = snprintf(
buffer + written,
length - written,
"-%.*s",
preview_len,
self->preview.beg
);
if (n < 0 || (written + n) >= length)
return -1;
written += n;
}
malunal_size_t build_len =
strview_length(&self->build);
if (build_len > 0) {
malunal_int32_t n = snprintf(
buffer + written,
length - written,
"+%.*s",
build_len,
self->build.beg
);
if (n < 0 || (written + n) >= length)
return -1;
written += n;
}
return written;
}
+455
View File
@@ -0,0 +1,455 @@
#include "malunal/microtest.h"
#include "malunal/version.h"
/* -------------------------------------------------------------------------
* parse_version(): successful parses
* ---------------------------------------------------------------------- */
MICROTEST(parse_version, basic_triple_succeeds) {
strview_t view = strview_from_cstr("1.2.3");
version_t ver;
error_t err = parse_version(&view, &ver);
MICROTEST_EXPECT_NULL(err.domain);
MICROTEST_EXPECT_EQ(ver.major, (malunal_size_t)1);
MICROTEST_EXPECT_EQ(ver.minor, (malunal_size_t)2);
MICROTEST_EXPECT_EQ(ver.patch, (malunal_size_t)3);
MICROTEST_EXPECT_EQ(strview_length(&ver.preview), (malunal_size_t)0);
MICROTEST_EXPECT_EQ(strview_length(&ver.build), (malunal_size_t)0);
}
MICROTEST(parse_version, all_zero_succeeds) {
strview_t view = strview_from_cstr("0.0.0");
version_t ver;
error_t err = parse_version(&view, &ver);
MICROTEST_EXPECT_NULL(err.domain);
MICROTEST_EXPECT_EQ(ver.major, (malunal_size_t)0);
MICROTEST_EXPECT_EQ(ver.minor, (malunal_size_t)0);
MICROTEST_EXPECT_EQ(ver.patch, (malunal_size_t)0);
}
MICROTEST(parse_version, multi_digit_components_succeed) {
strview_t view = strview_from_cstr("123.456.789");
version_t ver;
error_t err = parse_version(&view, &ver);
MICROTEST_EXPECT_NULL(err.domain);
MICROTEST_EXPECT_EQ(ver.major, (malunal_size_t)123);
MICROTEST_EXPECT_EQ(ver.minor, (malunal_size_t)456);
MICROTEST_EXPECT_EQ(ver.patch, (malunal_size_t)789);
}
MICROTEST(parse_version, preview_only_succeeds) {
strview_t view = strview_from_cstr("1.2.3-alpha");
version_t ver;
error_t err = parse_version(&view, &ver);
MICROTEST_EXPECT_NULL(err.domain);
MICROTEST_EXPECT_EQ(strview_length(&ver.preview), (malunal_size_t)5);
MICROTEST_EXPECT_TRUE(memcmp(ver.preview.beg, "alpha", 5) == 0);
MICROTEST_EXPECT_EQ(strview_length(&ver.build), (malunal_size_t)0);
}
MICROTEST(parse_version, dotted_preview_succeeds) {
strview_t view = strview_from_cstr("1.2.3-alpha.1");
version_t ver;
error_t err = parse_version(&view, &ver);
MICROTEST_EXPECT_NULL(err.domain);
MICROTEST_EXPECT_EQ(strview_length(&ver.preview), (malunal_size_t)7);
MICROTEST_EXPECT_TRUE(memcmp(ver.preview.beg, "alpha.1", 7) == 0);
}
MICROTEST(parse_version, build_only_succeeds) {
strview_t view = strview_from_cstr("1.2.3+build.5");
version_t ver;
error_t err = parse_version(&view, &ver);
MICROTEST_EXPECT_NULL(err.domain);
MICROTEST_EXPECT_EQ(strview_length(&ver.preview), (malunal_size_t)0);
MICROTEST_EXPECT_EQ(strview_length(&ver.build), (malunal_size_t)7);
MICROTEST_EXPECT_TRUE(memcmp(ver.build.beg, "build.5", 7) == 0);
}
MICROTEST(parse_version, preview_and_build_succeed) {
strview_t view = strview_from_cstr("1.2.3-alpha.1+build.5");
version_t ver;
error_t err = parse_version(&view, &ver);
MICROTEST_EXPECT_NULL(err.domain);
MICROTEST_EXPECT_EQ(ver.major, (malunal_size_t)1);
MICROTEST_EXPECT_EQ(ver.minor, (malunal_size_t)2);
MICROTEST_EXPECT_EQ(ver.patch, (malunal_size_t)3);
MICROTEST_EXPECT_TRUE(memcmp(ver.preview.beg, "alpha.1", 7) == 0);
MICROTEST_EXPECT_TRUE(memcmp(ver.build.beg, "build.5", 7) == 0);
}
MICROTEST(parse_version, single_digit_zero_component_is_not_leading_zero) {
/* A lone "0" is a valid component; the leading-zero rule only forbids
* *multiple* digits starting with zero (e.g. "01"). */
strview_t view = strview_from_cstr("0.2.3");
version_t ver;
error_t err = parse_version(&view, &ver);
MICROTEST_EXPECT_NULL(err.domain);
MICROTEST_EXPECT_EQ(ver.major, (malunal_size_t)0);
}
/* -------------------------------------------------------------------------
* parse_version(): error cases
* ---------------------------------------------------------------------- */
MICROTEST(parse_version, null_verstr_reports_null_input) {
version_t ver;
error_t err = parse_version(null, &ver);
MICROTEST_EXPECT_NOT_NULL(err.domain);
MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_NULL_INPUT);
}
MICROTEST(parse_version, null_out_reports_null_input) {
strview_t view = strview_from_cstr("1.2.3");
error_t err = parse_version(&view, null);
MICROTEST_EXPECT_NOT_NULL(err.domain);
MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_NULL_INPUT);
}
MICROTEST(parse_version, null_backing_data_succeeds_as_zero_version) {
strview_t view = { null, null };
version_t ver;
error_t err = parse_version(&view, &ver);
MICROTEST_EXPECT_NULL(err.domain);
MICROTEST_EXPECT_EQ(ver.major, (malunal_size_t)0);
MICROTEST_EXPECT_EQ(ver.minor, (malunal_size_t)0);
MICROTEST_EXPECT_EQ(ver.patch, (malunal_size_t)0);
}
MICROTEST(parse_version, leading_zero_in_major_fails) {
strview_t view = strview_from_cstr("01.2.3");
version_t ver;
error_t err = parse_version(&view, &ver);
MICROTEST_EXPECT_NOT_NULL(err.domain);
MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_LEADING_ZERO);
}
MICROTEST(parse_version, leading_zero_in_minor_fails) {
strview_t view = strview_from_cstr("1.02.3");
version_t ver;
error_t err = parse_version(&view, &ver);
MICROTEST_EXPECT_NOT_NULL(err.domain);
MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_LEADING_ZERO);
}
MICROTEST(parse_version, leading_zero_in_patch_fails) {
strview_t view = strview_from_cstr("1.2.03");
version_t ver;
error_t err = parse_version(&view, &ver);
MICROTEST_EXPECT_NOT_NULL(err.domain);
MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_LEADING_ZERO);
}
MICROTEST(parse_version, missing_minor_and_patch_fails) {
strview_t view = strview_from_cstr("1.2");
version_t ver;
error_t err = parse_version(&view, &ver);
MICROTEST_EXPECT_NOT_NULL(err.domain);
MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_MISSING_PART);
}
MICROTEST(parse_version, missing_patch_digits_fails) {
strview_t view = strview_from_cstr("1.2.");
version_t ver;
error_t err = parse_version(&view, &ver);
MICROTEST_EXPECT_NOT_NULL(err.domain);
MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_INVALID_NUMBER);
}
MICROTEST(parse_version, fourth_numeric_component_fails) {
strview_t view = strview_from_cstr("1.2.3.4");
version_t ver;
error_t err = parse_version(&view, &ver);
MICROTEST_EXPECT_NOT_NULL(err.domain);
MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_UNEXPECTED_CHAR);
}
MICROTEST(parse_version, empty_preview_fails) {
strview_t view = strview_from_cstr("1.2.3-");
version_t ver;
error_t err = parse_version(&view, &ver);
MICROTEST_EXPECT_NOT_NULL(err.domain);
MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_MISSING_PART);
}
MICROTEST(parse_version, empty_build_fails) {
strview_t view = strview_from_cstr("1.2.3+");
version_t ver;
error_t err = parse_version(&view, &ver);
MICROTEST_EXPECT_NOT_NULL(err.domain);
MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_MISSING_PART);
}
MICROTEST(parse_version, negative_major_fails) {
strview_t view = strview_from_cstr("-1.2.3");
version_t ver;
error_t err = parse_version(&view, &ver);
MICROTEST_EXPECT_NOT_NULL(err.domain);
MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_INVALID_NUMBER);
}
MICROTEST(parse_version, non_numeric_components_fail) {
strview_t view = strview_from_cstr("a.b.c");
version_t ver;
error_t err = parse_version(&view, &ver);
MICROTEST_EXPECT_NOT_NULL(err.domain);
MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_INVALID_NUMBER);
}
MICROTEST(parse_version, overflowing_major_fails) {
strview_t view = strview_from_cstr("99999999999999999999.2.3");
version_t ver;
error_t err = parse_version(&view, &ver);
MICROTEST_EXPECT_NOT_NULL(err.domain);
MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_VALUE_OVERFLOW);
}
MICROTEST(parse_version, overflowing_patch_fails) {
strview_t view = strview_from_cstr("1.2.99999999999999999999");
version_t ver;
error_t err = parse_version(&view, &ver);
MICROTEST_EXPECT_NOT_NULL(err.domain);
MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_VALUE_OVERFLOW);
}
/* -------------------------------------------------------------------------
* version_compare(): core number precedence
* ---------------------------------------------------------------------- */
static
version_t
must_parse(malunal_cstr_t s) {
strview_t view = strview_from_cstr(s);
version_t ver;
parse_version(&view, &ver);
return ver;
}
MICROTEST(version_compare, identical_versions_are_equal) {
version_t a = must_parse("1.2.3");
version_t b = must_parse("1.2.3");
MICROTEST_EXPECT_EQ(version_compare(&a, &b), 0);
}
MICROTEST(version_compare, is_reflexive) {
version_t a = must_parse("1.2.3");
MICROTEST_EXPECT_EQ(version_compare(&a, &a), 0);
}
MICROTEST(version_compare, greater_major_wins) {
version_t a = must_parse("2.0.0");
version_t b = must_parse("1.9.9");
MICROTEST_EXPECT_GT(version_compare(&a, &b), 0);
MICROTEST_EXPECT_LT(version_compare(&b, &a), 0);
}
MICROTEST(version_compare, greater_minor_wins_when_major_equal) {
version_t a = must_parse("1.3.0");
version_t b = must_parse("1.2.9");
MICROTEST_EXPECT_GT(version_compare(&a, &b), 0);
}
MICROTEST(version_compare, greater_patch_wins_when_major_minor_equal) {
version_t a = must_parse("1.2.4");
version_t b = must_parse("1.2.3");
MICROTEST_EXPECT_GT(version_compare(&a, &b), 0);
}
MICROTEST(version_compare, numeric_comparison_not_lexical) {
/* 1.2.10 > 1.2.9 numerically, even though "10" < "9" lexically. */
version_t a = must_parse("1.2.10");
version_t b = must_parse("1.2.9");
MICROTEST_EXPECT_GT(version_compare(&a, &b), 0);
}
/* -------------------------------------------------------------------------
* version_compare(): pre-release precedence
* ---------------------------------------------------------------------- */
MICROTEST(version_compare, preview_has_lower_precedence_than_release) {
version_t preview = must_parse("1.0.0-alpha");
version_t release = must_parse("1.0.0");
MICROTEST_EXPECT_LT(version_compare(&preview, &release), 0);
MICROTEST_EXPECT_GT(version_compare(&release, &preview), 0);
}
MICROTEST(version_compare, more_preview_identifiers_has_higher_precedence) {
version_t shorter = must_parse("1.0.0-alpha");
version_t longer = must_parse("1.0.0-alpha.1");
MICROTEST_EXPECT_LT(version_compare(&shorter, &longer), 0);
}
MICROTEST(version_compare, numeric_identifier_has_lower_precedence_than_alphanumeric) {
version_t numeric = must_parse("1.0.0-alpha.1");
version_t alpha = must_parse("1.0.0-alpha.beta");
MICROTEST_EXPECT_LT(version_compare(&numeric, &alpha), 0);
}
MICROTEST(version_compare, numeric_preview_identifiers_compare_numerically) {
/* "10" > "2" numerically, even though "10" < "2" lexically. */
version_t ten = must_parse("1.0.0-rc.10");
version_t two = must_parse("1.0.0-rc.2");
MICROTEST_EXPECT_GT(version_compare(&ten, &two), 0);
}
MICROTEST(version_compare, alphanumeric_preview_identifiers_compare_lexically) {
version_t a = must_parse("1.0.0-alpha");
version_t b = must_parse("1.0.0-beta");
MICROTEST_EXPECT_LT(version_compare(&a, &b), 0);
}
MICROTEST(version_compare, identical_previews_are_equal) {
version_t a = must_parse("1.0.0-alpha.1");
version_t b = must_parse("1.0.0-alpha.1");
MICROTEST_EXPECT_EQ(version_compare(&a, &b), 0);
}
/* -------------------------------------------------------------------------
* version_compare(): build metadata is ignored
* ---------------------------------------------------------------------- */
MICROTEST(version_compare, differing_build_metadata_is_ignored) {
version_t a = must_parse("1.0.0+build.1");
version_t b = must_parse("1.0.0+build.2");
MICROTEST_EXPECT_EQ(version_compare(&a, &b), 0);
}
MICROTEST(version_compare, build_metadata_ignored_with_preview_present) {
version_t a = must_parse("1.0.0-alpha+build.1");
version_t b = must_parse("1.0.0-alpha+build.2");
MICROTEST_EXPECT_EQ(version_compare(&a, &b), 0);
}
/* -------------------------------------------------------------------------
* version_string()
* ---------------------------------------------------------------------- */
MICROTEST(version_string, basic_triple_round_trips) {
version_t ver = must_parse("1.2.3");
malunal_char_t buffer[32];
malunal_int32_t n = version_string(&ver, buffer, sizeof(buffer));
MICROTEST_EXPECT_GT(n, 0);
MICROTEST_EXPECT_TRUE(strcmp(buffer, "1.2.3") == 0);
}
MICROTEST(version_string, includes_preview) {
version_t ver = must_parse("1.2.3-alpha.1");
malunal_char_t buffer[32];
malunal_int32_t n = version_string(&ver, buffer, sizeof(buffer));
MICROTEST_EXPECT_GT(n, 0);
MICROTEST_EXPECT_TRUE(strcmp(buffer, "1.2.3-alpha.1") == 0);
}
MICROTEST(version_string, includes_build) {
version_t ver = must_parse("1.2.3+build.5");
malunal_char_t buffer[32];
malunal_int32_t n = version_string(&ver, buffer, sizeof(buffer));
MICROTEST_EXPECT_GT(n, 0);
MICROTEST_EXPECT_TRUE(strcmp(buffer, "1.2.3+build.5") == 0);
}
MICROTEST(version_string, includes_preview_and_build) {
version_t ver = must_parse("1.2.3-alpha.1+build.5");
malunal_char_t buffer[32];
malunal_int32_t n = version_string(&ver, buffer, sizeof(buffer));
MICROTEST_EXPECT_GT(n, 0);
MICROTEST_EXPECT_TRUE(strcmp(buffer, "1.2.3-alpha.1+build.5") == 0);
}
MICROTEST(version_string, too_small_buffer_fails) {
version_t ver = must_parse("1.2.3-alpha.1+build.5");
malunal_char_t buffer[5];
malunal_int32_t n = version_string(&ver, buffer, sizeof(buffer));
MICROTEST_EXPECT_EQ(n, -1);
}
MICROTEST(version_string, exact_size_buffer_succeeds) {
/* "1.2.3" is 5 characters plus a null terminator. */
version_t ver = must_parse("1.2.3");
malunal_char_t buffer[6];
malunal_int32_t n = version_string(&ver, buffer, sizeof(buffer));
MICROTEST_EXPECT_EQ(n, 5);
MICROTEST_EXPECT_TRUE(strcmp(buffer, "1.2.3") == 0);
}
MICROTEST(version_string, one_byte_too_small_fails) {
version_t ver = must_parse("1.2.3");
malunal_char_t buffer[5];
malunal_int32_t n = version_string(&ver, buffer, sizeof(buffer));
MICROTEST_EXPECT_EQ(n, -1);
}
MICROTEST(version_string, null_self_fails) {
malunal_char_t buffer[32];
malunal_int32_t n = version_string(null, buffer, sizeof(buffer));
MICROTEST_EXPECT_EQ(n, -1);
}
MICROTEST(version_string, null_buffer_fails) {
version_t ver = must_parse("1.2.3");
malunal_int32_t n = version_string(&ver, null, 32);
MICROTEST_EXPECT_EQ(n, -1);
}
MICROTEST(version_string, zero_length_fails) {
version_t ver = must_parse("1.2.3");
malunal_char_t buffer[32];
malunal_int32_t n = version_string(&ver, buffer, 0);
MICROTEST_EXPECT_EQ(n, -1);
}
MICROTEST_MAIN()