feat: standalone ReleaseUpdater library v1.1.0

Promoted from DIYStickyNotes/lib/ReleaseUpdater (byte-identical src) into a
reusable Gitea library. Adds LICENSE, .gitignore, library.properties,
keywords.txt and carries the ReleaseUpdaterCore native unit test for
downstream reuse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-15 14:02:41 +02:00
co-authored by Claude Opus 4.8
commit 59add9eac7
13 changed files with 1043 additions and 0 deletions
@@ -0,0 +1,129 @@
/** @file Native unit tests for ReleaseUpdaterCore (version + release/asset parsing). */
#include <unity.h>
#include "ReleaseUpdaterCore.hpp"
#include <cstring>
static void test_version_parse_basic(void) {
RuVersion v;
TEST_ASSERT_TRUE(ruVersionParse("v1.2.3", v));
TEST_ASSERT_EQUAL_INT(1, v.major); TEST_ASSERT_EQUAL_INT(2, v.minor); TEST_ASSERT_EQUAL_INT(3, v.patch);
TEST_ASSERT_TRUE(ruVersionParse("1.2.3", v));
TEST_ASSERT_EQUAL_INT(3, v.patch);
TEST_ASSERT_TRUE(ruVersionParse("v10.0.1", v));
TEST_ASSERT_EQUAL_INT(10, v.major);
}
static void test_version_parse_short(void) {
RuVersion v;
TEST_ASSERT_TRUE(ruVersionParse("1.2", v));
TEST_ASSERT_EQUAL_INT(1, v.major); TEST_ASSERT_EQUAL_INT(2, v.minor); TEST_ASSERT_EQUAL_INT(0, v.patch);
TEST_ASSERT_TRUE(ruVersionParse("v1", v));
TEST_ASSERT_EQUAL_INT(1, v.major); TEST_ASSERT_EQUAL_INT(0, v.minor); TEST_ASSERT_EQUAL_INT(0, v.patch);
}
static void test_version_parse_reject(void) {
RuVersion v;
TEST_ASSERT_FALSE(ruVersionParse("", v));
TEST_ASSERT_FALSE(ruVersionParse("x", v));
TEST_ASSERT_FALSE(ruVersionParse("1.x.0", v));
TEST_ASSERT_FALSE(ruVersionParse("v.1.2", v));
}
static void test_version_parse_suffix_tolerated(void) {
RuVersion v;
// git-describe style build suffix after patch is ignored.
TEST_ASSERT_TRUE(ruVersionParse("v1.2.3-5-gabcdef", v));
TEST_ASSERT_EQUAL_INT(1, v.major); TEST_ASSERT_EQUAL_INT(2, v.minor); TEST_ASSERT_EQUAL_INT(3, v.patch);
// dotted trailing content after patch is also ignored (treated as build suffix).
TEST_ASSERT_TRUE(ruVersionParse("1.2.3.99", v));
TEST_ASSERT_EQUAL_INT(1, v.major); TEST_ASSERT_EQUAL_INT(2, v.minor); TEST_ASSERT_EQUAL_INT(3, v.patch);
}
static void test_version_compare(void) {
RuVersion a{1,2,3}, b{1,2,3};
TEST_ASSERT_EQUAL_INT(0, ruVersionCompare(a, b));
TEST_ASSERT_EQUAL_INT(1, ruVersionCompare(RuVersion{1,3,0}, RuVersion{1,2,9}));
TEST_ASSERT_EQUAL_INT(-1, ruVersionCompare(RuVersion{1,2,3}, RuVersion{2,0,0}));
TEST_ASSERT_EQUAL_INT(1, ruVersionCompare(RuVersion{1,2,4}, RuVersion{1,2,3}));
}
// Minimal GitHub/Gitea "releases/latest" shape (both share tag_name + assets[].{name,browser_download_url}).
static const char* SAMPLE_JSON =
"{\"tag_name\":\"v1.4.0\",\"assets\":["
"{\"name\":\"DIYStickyNotes-sc01plus-firmware-1.4.0.bin\",\"browser_download_url\":\"https://h/a/sc.bin\"},"
"{\"name\":\"DIYStickyNotes-JC-jc3248w535-firmware-1.4.0.bin\",\"browser_download_url\":\"https://h/a/jc.bin\"}"
"]}";
static void test_parse_selects_sc01(void) {
RuRelease r;
TEST_ASSERT_TRUE(ruParseLatestRelease(SAMPLE_JSON, "sc01plus", r));
TEST_ASSERT_EQUAL_STRING("v1.4.0", r.tag);
TEST_ASSERT_EQUAL_INT(1, r.version.major); TEST_ASSERT_EQUAL_INT(4, r.version.minor);
TEST_ASSERT_EQUAL_STRING("https://h/a/sc.bin", r.assetUrl);
}
static void test_parse_selects_jc_not_sc(void) {
RuRelease r;
// jc token must NOT cross-match the sc01 asset, and sc01 token must NOT match the jc asset.
TEST_ASSERT_TRUE(ruParseLatestRelease(SAMPLE_JSON, "jc3248w535", r));
TEST_ASSERT_EQUAL_STRING("https://h/a/jc.bin", r.assetUrl);
}
static void test_parse_no_matching_asset(void) {
RuRelease r;
TEST_ASSERT_TRUE(ruParseLatestRelease(SAMPLE_JSON, "nosuchboard", r)); // parse ok...
TEST_ASSERT_EQUAL_STRING("", r.assetUrl); // ...but no asset
}
// A same-token asset with a non-.bin extension listed BEFORE the real firmware must be skipped
// (locks the `&& ruEndsWithBin(name)` filter against a regression).
static const char* SAMPLE_JSON_NONBIN_FIRST =
"{\"tag_name\":\"v1.4.0\",\"assets\":["
"{\"name\":\"DIYStickyNotes-sc01plus-firmware-1.4.0.sha256\",\"browser_download_url\":\"https://h/a/sc.sha256\"},"
"{\"name\":\"DIYStickyNotes-sc01plus-firmware-1.4.0.bin\",\"browser_download_url\":\"https://h/a/sc.bin\"}"
"]}";
static void test_parse_skips_non_bin_asset(void) {
RuRelease r;
TEST_ASSERT_TRUE(ruParseLatestRelease(SAMPLE_JSON_NONBIN_FIRST, "sc01plus", r));
TEST_ASSERT_EQUAL_STRING("https://h/a/sc.bin", r.assetUrl); // .sha256 skipped, real .bin chosen
}
// A board-token ".bin" asset that is NOT the app image (missing the "-firmware-" segment, e.g. a
// LittleFS/factory/combined image) listed BEFORE the real firmware asset must be skipped (locks the
// "{token}-firmware-" needle against a regression that could flash a non-app binary to the app
// partition and brick the device).
static const char* SAMPLE_JSON_NONFIRMWARE_FIRST =
"{\"tag_name\":\"v1.4.0\",\"assets\":["
"{\"name\":\"DIYStickyNotes-sc01plus-littlefs-1.4.0.bin\",\"browser_download_url\":\"https://h/a/lfs.bin\"},"
"{\"name\":\"DIYStickyNotes-sc01plus-firmware-1.4.0.bin\",\"browser_download_url\":\"https://h/a/sc.bin\"}"
"]}";
static void test_parse_skips_non_firmware_bin_asset(void) {
RuRelease r;
TEST_ASSERT_TRUE(ruParseLatestRelease(SAMPLE_JSON_NONFIRMWARE_FIRST, "sc01plus", r));
TEST_ASSERT_EQUAL_STRING("https://h/a/sc.bin", r.assetUrl); // littlefs .bin skipped, real firmware chosen
}
static void test_parse_malformed(void) {
RuRelease r;
TEST_ASSERT_FALSE(ruParseLatestRelease("not json", "sc01plus", r));
TEST_ASSERT_FALSE(ruParseLatestRelease("{\"assets\":[]}", "sc01plus", r)); // missing tag_name
TEST_ASSERT_FALSE(ruParseLatestRelease("{\"tag_name\":\"\",\"assets\":[]}", "sc01plus", r)); // empty tag_name
}
int main(int, char**) {
UNITY_BEGIN();
RUN_TEST(test_version_parse_basic);
RUN_TEST(test_version_parse_short);
RUN_TEST(test_version_parse_reject);
RUN_TEST(test_version_parse_suffix_tolerated);
RUN_TEST(test_version_compare);
RUN_TEST(test_parse_selects_sc01);
RUN_TEST(test_parse_selects_jc_not_sc);
RUN_TEST(test_parse_no_matching_asset);
RUN_TEST(test_parse_skips_non_bin_asset);
RUN_TEST(test_parse_skips_non_firmware_bin_asset);
RUN_TEST(test_parse_malformed);
return UNITY_END();
}