Fix Database migration version handling (#1356)

This commit is contained in:
LordGrey 2021-10-16 13:54:18 +02:00 committed by GitHub
parent ac1dad77e3
commit 532ac7e330
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 267 additions and 171 deletions

View File

@ -27,10 +27,15 @@ jobs:
with: with:
submodules: true submodules: true
- name: Extract major.minor.patch from file .version
run: |
tr -d '\n' < .version > temp && mv temp .version
VERSION=$(tr -d '\n' < .version)
echo "MAJOR_MINOR_PATCH=$(echo "${VERSION%.*}.$((${VERSION##*.}))")" >> $GITHUB_ENV
- name: Build package - name: Build package
shell: bash shell: bash
run: | run: |
tr -d '\n' < .version > temp && mv temp .version
mkdir -p "${GITHUB_WORKSPACE}/deploy" mkdir -p "${GITHUB_WORKSPACE}/deploy"
docker run --rm \ docker run --rm \
-v "${GITHUB_WORKSPACE}/deploy:/deploy" \ -v "${GITHUB_WORKSPACE}/deploy:/deploy" \
@ -40,9 +45,8 @@ jobs:
mkdir -p debian/source && echo '3.0 (quilt)' > debian/source/format && \ mkdir -p debian/source && echo '3.0 (quilt)' > debian/source/format && \
dch --create --distribution $(echo ${{ matrix.distribution }} | tr '[:upper:]' '[:lower:]') --package 'hyperion' -v '$(cat .version)~$(echo ${{ matrix.distribution }} | tr '[:upper:]' '[:lower:]')' '${{ github.event.commits[0].message }}' && \ dch --create --distribution $(echo ${{ matrix.distribution }} | tr '[:upper:]' '[:lower:]') --package 'hyperion' -v '$(cat .version)~$(echo ${{ matrix.distribution }} | tr '[:upper:]' '[:lower:]')' '${{ github.event.commits[0].message }}' && \
cp -fr LICENSE debian/copyright && \ cp -fr LICENSE debian/copyright && \
sed 's/@BUILD_DEPENDS@/${{ matrix.build-depends }}/g; s/@DEPENDS@/${{ matrix.package-depends }}/g; s/@ARCHITECTURE@/${{ matrix.architecture }}/g' debian/control.in > debian/control && \ sed 's/@BUILD_DEPENDS@/${{ matrix.build-depends }}/g; s/@DEPENDS@/${{ matrix.package-depends }}/g; s/@ARCHITECTURE@/${{ matrix.architecture }}/g; s/@STANDARDS_VERSION@/${{ env.MAJOR_MINOR_PATCH }}/g' debian/control.in > debian/control && \
tar cf ../hyperion_2.0.0.orig.tar . && \ tar -cJf ../hyperion_$(cat .version)~$(echo ${{ matrix.distribution }} | tr '[:upper:]' '[:lower:]').orig.tar.xz . && \
xz -9 ../hyperion_2.0.0.orig.tar && \
debuild --no-lintian -uc -us && \ debuild --no-lintian -uc -us && \
cp ../hyperion_* /deploy" cp ../hyperion_* /deploy"

View File

@ -54,6 +54,7 @@ jobs:
id: vars id: vars
run: | run: |
VERSION="$(tr -d '\n' < .version)+nightly$(date '+%Y%m%d')$(git rev-parse --short HEAD)" VERSION="$(tr -d '\n' < .version)+nightly$(date '+%Y%m%d')$(git rev-parse --short HEAD)"
echo "MAJOR_MINOR_PATCH=$(echo "${VERSION%.*}.$((${VERSION##*.}))")" >> $GITHUB_ENV
echo "$VERSION" > .version echo "$VERSION" > .version
- name: Build package - name: Build package
@ -68,9 +69,8 @@ jobs:
mkdir -p debian/source && echo '3.0 (quilt)' > debian/source/format && \ mkdir -p debian/source && echo '3.0 (quilt)' > debian/source/format && \
dch --create --distribution $(echo ${{ matrix.distribution }} | tr '[:upper:]' '[:lower:]') --package 'hyperion' -v '$(cat .version)~$(echo ${{ matrix.distribution }} | tr '[:upper:]' '[:lower:]')' '${{ github.event.commits[0].message }}' && \ dch --create --distribution $(echo ${{ matrix.distribution }} | tr '[:upper:]' '[:lower:]') --package 'hyperion' -v '$(cat .version)~$(echo ${{ matrix.distribution }} | tr '[:upper:]' '[:lower:]')' '${{ github.event.commits[0].message }}' && \
cp -fr LICENSE debian/copyright && \ cp -fr LICENSE debian/copyright && \
sed 's/@BUILD_DEPENDS@/${{ matrix.build-depends }}/g; s/@DEPENDS@/${{ matrix.package-depends }}/g; s/@ARCHITECTURE@/${{ matrix.architecture }}/g' debian/control.in > debian/control && \ sed 's/@BUILD_DEPENDS@/${{ matrix.build-depends }}/g; s/@DEPENDS@/${{ matrix.package-depends }}/g; s/@ARCHITECTURE@/${{ matrix.architecture }}/g; s/@STANDARDS_VERSION@/${{ env.MAJOR_MINOR_PATCH }}/g' debian/control.in > debian/control && \
tar cf ../hyperion_2.0.0.orig.tar . && \ tar -cJf ../hyperion_$(cat .version)~$(echo ${{ matrix.distribution }} | tr '[:upper:]' '[:lower:]').orig.tar.xz . && \
xz -9 ../hyperion_2.0.0.orig.tar && \
debuild --no-lintian -uc -us && \ debuild --no-lintian -uc -us && \
cp ../hyperion_* /deploy" cp ../hyperion_* /deploy"

2
debian/control.in vendored
View File

@ -2,7 +2,7 @@ Source: hyperion
Section: devel Section: devel
Priority: optional Priority: optional
Build-Depends: @BUILD_DEPENDS@ Build-Depends: @BUILD_DEPENDS@
Standards-Version: 2.0.0 Standards-Version: @STANDARDS_VERSION@
Maintainer: Hyperion Project <admin@hyperion-project.org> Maintainer: Hyperion Project <admin@hyperion-project.org>
Homepage: https://hyperion-project.org/ Homepage: https://hyperion-project.org/

3
debian/rules vendored
View File

@ -14,6 +14,7 @@ binary-indep:
binary-arch: binary-arch:
cd $(BUILDDIR); cmake -P cmake_install.cmake cd $(BUILDDIR); cmake -P cmake_install.cmake
rm -rf debian/tmp/usr/include debian/tmp/usr/lib debian/tmp/usr/bin/flatc
mkdir debian/tmp/DEBIAN mkdir debian/tmp/DEBIAN
cp cmake/package-scripts/postinst debian/tmp/DEBIAN cp cmake/package-scripts/postinst debian/tmp/DEBIAN
chmod 0775 debian/tmp/DEBIAN/postinst chmod 0775 debian/tmp/DEBIAN/postinst
@ -23,7 +24,7 @@ binary-arch:
chmod 0775 debian/tmp/DEBIAN/prerm chmod 0775 debian/tmp/DEBIAN/prerm
dpkg-gencontrol -phyperion dpkg-gencontrol -phyperion
dpkg --build debian/tmp .. dpkg --build debian/tmp ..
rm -rf debian/tmp rm -rf debian/tmp $(BUILDDIR)
clean: clean:
rm -rf $(BUILDDIR) rm -rf $(BUILDDIR)

View File

@ -406,12 +406,12 @@ namespace semver {
friend bool operator== (version &lft, version &rgt) friend bool operator== (version &lft, version &rgt)
{ {
return lft.getVersion().compare(rgt.getVersion()) == 0; return !(lft != rgt);
} }
friend bool operator!= (version &lft, version &rgt) friend bool operator!= (version &lft, version &rgt)
{ {
return !(lft == rgt); return (lft > rgt) || (lft < rgt);
} }
friend bool operator> (version &lft, version &rgt) friend bool operator> (version &lft, version &rgt)

View File

@ -255,6 +255,21 @@ bool SettingsManager::saveSettings(QJsonObject config, bool correct)
return rc; return rc;
} }
inline QString fixVersion (const QString& version)
{
QString newVersion;
//Try fixing version number, remove dot separated pre-release identifiers not supported
QRegularExpression regEx("(\\d+\\.\\d+\\.\\d+-?[a-zA-Z-\\d]*\\.?[\\d]*)", QRegularExpression::CaseInsensitiveOption | QRegularExpression::MultilineOption);
QRegularExpressionMatch match;
match = regEx.match(version);
if (match.hasMatch())
{
newVersion = match.captured(1);
}
return newVersion;
}
bool SettingsManager::resolveConfigVersion(QJsonObject& config) bool SettingsManager::resolveConfigVersion(QJsonObject& config)
{ {
bool isValid = false; bool isValid = false;
@ -267,6 +282,14 @@ bool SettingsManager::resolveConfigVersion(QJsonObject& config)
if ( !configVersion.isEmpty() ) if ( !configVersion.isEmpty() )
{ {
isValid = _configVersion.setVersion(configVersion.toStdString()); isValid = _configVersion.setVersion(configVersion.toStdString());
if (!isValid)
{
isValid = _configVersion.setVersion( fixVersion(configVersion).toStdString() );
if (isValid)
{
Info(_log, "Invalid config version [%s] fixed. Updated to [%s]", QSTRING_CSTR(configVersion), _configVersion.getVersion().c_str());
}
}
} }
else else
{ {
@ -277,6 +300,14 @@ bool SettingsManager::resolveConfigVersion(QJsonObject& config)
if ( !previousVersion.isEmpty() && isValid ) if ( !previousVersion.isEmpty() && isValid )
{ {
isValid = _previousVersion.setVersion(previousVersion.toStdString()); isValid = _previousVersion.setVersion(previousVersion.toStdString());
if (!isValid)
{
isValid = _previousVersion.setVersion( fixVersion(previousVersion).toStdString() );
if (isValid)
{
Info(_log, "Invalid previous version [%s] fixed. Updated to [%s]", QSTRING_CSTR(previousVersion), _previousVersion.getVersion().c_str());
}
}
} }
else else
{ {
@ -291,199 +322,206 @@ bool SettingsManager::handleConfigUpgrade(QJsonObject& config)
{ {
bool migrated = false; bool migrated = false;
resolveConfigVersion(config); //Only migrate, if valid versions are available
if ( !resolveConfigVersion(config) )
//Do only migrate, if configuration is not up to date
if (_previousVersion < _configVersion)
{ {
//Migration steps for versions <= alpha 9 Warning(_log, "Invalid version information found in configuration. No database migration executed.");
semver::version targetVersion {"2.0.0-alpha.9"}; }
if (_previousVersion <= targetVersion ) else
{
//Do only migrate, if configuration is not up to date
if (_previousVersion < _configVersion)
{ {
Info(_log, "Instance [%u]: Migrate LED Layout from current version [%s] to version [%s] or later", _instance, _previousVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); //Migration steps for versions <= alpha 9
semver::version targetVersion {"2.0.0-alpha.9"};
// LED LAYOUT UPGRADE if (_previousVersion <= targetVersion )
// from { hscan: { minimum: 0.2, maximum: 0.3 }, vscan: { minimum: 0.2, maximum: 0.3 } }
// from { h: { min: 0.2, max: 0.3 }, v: { min: 0.2, max: 0.3 } }
// to { hmin: 0.2, hmax: 0.3, vmin: 0.2, vmax: 0.3}
if(config.contains("leds"))
{ {
const QJsonArray ledarr = config["leds"].toArray(); Info(_log, "Instance [%u]: Migrate LED Layout from current version [%s] to version [%s] or later", _instance, _previousVersion.getVersion().c_str(), targetVersion.getVersion().c_str());
const QJsonObject led = ledarr[0].toObject();
if(led.contains("hscan") || led.contains("h")) // LED LAYOUT UPGRADE
// from { hscan: { minimum: 0.2, maximum: 0.3 }, vscan: { minimum: 0.2, maximum: 0.3 } }
// from { h: { min: 0.2, max: 0.3 }, v: { min: 0.2, max: 0.3 } }
// to { hmin: 0.2, hmax: 0.3, vmin: 0.2, vmax: 0.3}
if(config.contains("leds"))
{ {
const bool whscan = led.contains("hscan"); const QJsonArray ledarr = config["leds"].toArray();
QJsonArray newLedarr; const QJsonObject led = ledarr[0].toObject();
for(const auto & entry : ledarr) if(led.contains("hscan") || led.contains("h"))
{ {
const QJsonObject led = entry.toObject(); const bool whscan = led.contains("hscan");
QJsonObject hscan; QJsonArray newLedarr;
QJsonObject vscan;
QJsonValue hmin;
QJsonValue hmax;
QJsonValue vmin;
QJsonValue vmax;
QJsonObject nL;
if(whscan) for(const auto & entry : ledarr)
{ {
hscan = led["hscan"].toObject(); const QJsonObject led = entry.toObject();
vscan = led["vscan"].toObject(); QJsonObject hscan;
hmin = hscan["minimum"]; QJsonObject vscan;
hmax = hscan["maximum"]; QJsonValue hmin;
vmin = vscan["minimum"]; QJsonValue hmax;
vmax = vscan["maximum"]; QJsonValue vmin;
} QJsonValue vmax;
else QJsonObject nL;
{
hscan = led["h"].toObject(); if(whscan)
vscan = led["v"].toObject(); {
hmin = hscan["min"]; hscan = led["hscan"].toObject();
hmax = hscan["max"]; vscan = led["vscan"].toObject();
vmin = vscan["min"]; hmin = hscan["minimum"];
vmax = vscan["max"]; hmax = hscan["maximum"];
} vmin = vscan["minimum"];
// append to led object vmax = vscan["maximum"];
nL["hmin"] = hmin; }
nL["hmax"] = hmax; else
nL["vmin"] = vmin; {
nL["vmax"] = vmax; hscan = led["h"].toObject();
newLedarr.append(nL); vscan = led["v"].toObject();
} hmin = hscan["min"];
// replace hmax = hscan["max"];
config["leds"] = newLedarr; vmin = vscan["min"];
migrated = true; vmax = vscan["max"];
Info(_log,"Instance [%u]: LED Layout migrated", _instance); }
} // append to led object
} nL["hmin"] = hmin;
nL["hmax"] = hmax;
if(config.contains("ledConfig")) nL["vmin"] = vmin;
{ nL["vmax"] = vmax;
QJsonObject oldLedConfig = config["ledConfig"].toObject(); newLedarr.append(nL);
if ( !oldLedConfig.contains("classic"))
{
QJsonObject newLedConfig;
newLedConfig.insert("classic", oldLedConfig );
QJsonObject defaultMatrixConfig {{"ledshoriz", 1}
,{"ledsvert", 1}
,{"cabling","snake"}
,{"start","top-left"}
};
newLedConfig.insert("matrix", defaultMatrixConfig );
config["ledConfig"] = newLedConfig;
migrated = true;
Info(_log,"Instance [%u]: LED-Config migrated", _instance);
}
}
// LED Hardware count is leading for versions after alpha 9
// Setting Hardware LED count to number of LEDs configured via layout, if layout number is greater than number of hardware LEDs
if (config.contains("device"))
{
QJsonObject newDeviceConfig = config["device"].toObject();
if (newDeviceConfig.contains("hardwareLedCount"))
{
int hwLedcount = newDeviceConfig["hardwareLedCount"].toInt();
if (config.contains("leds"))
{
const QJsonArray ledarr = config["leds"].toArray();
int layoutLedCount = ledarr.size();
if (hwLedcount < layoutLedCount )
{
Warning(_log, "Instance [%u]: HwLedCount/Layout mismatch! Setting Hardware LED count to number of LEDs configured via layout", _instance);
hwLedcount = layoutLedCount;
newDeviceConfig["hardwareLedCount"] = hwLedcount;
migrated = true;
} }
// replace
config["leds"] = newLedarr;
migrated = true;
Info(_log,"Instance [%u]: LED Layout migrated", _instance);
} }
} }
if (newDeviceConfig.contains("type")) if(config.contains("ledConfig"))
{ {
QString type = newDeviceConfig["type"].toString(); QJsonObject oldLedConfig = config["ledConfig"].toObject();
if (type == "atmoorb" || type == "fadecandy" || type == "philipshue" ) if ( !oldLedConfig.contains("classic"))
{ {
if (newDeviceConfig.contains("output")) QJsonObject newLedConfig;
{ newLedConfig.insert("classic", oldLedConfig );
newDeviceConfig["host"] = newDeviceConfig["output"].toString(); QJsonObject defaultMatrixConfig {{"ledshoriz", 1}
newDeviceConfig.remove("output"); ,{"ledsvert", 1}
migrated = true; ,{"cabling","snake"}
} ,{"start","top-left"}
};
newLedConfig.insert("matrix", defaultMatrixConfig );
config["ledConfig"] = newLedConfig;
migrated = true;
Info(_log,"Instance [%u]: LED-Config migrated", _instance);
} }
} }
if (migrated) // LED Hardware count is leading for versions after alpha 9
// Setting Hardware LED count to number of LEDs configured via layout, if layout number is greater than number of hardware LEDs
if (config.contains("device"))
{ {
config["device"] = newDeviceConfig; QJsonObject newDeviceConfig = config["device"].toObject();
Debug(_log, "LED-Device records migrated");
}
}
if (config.contains("grabberV4L2")) if (newDeviceConfig.contains("hardwareLedCount"))
{ {
QJsonObject newGrabberV4L2Config = config["grabberV4L2"].toObject(); int hwLedcount = newDeviceConfig["hardwareLedCount"].toInt();
if (config.contains("leds"))
{
const QJsonArray ledarr = config["leds"].toArray();
int layoutLedCount = ledarr.size();
if (newGrabberV4L2Config.contains("encoding_format")) if (hwLedcount < layoutLedCount )
{ {
newGrabberV4L2Config.remove("encoding_format"); Warning(_log, "Instance [%u]: HwLedCount/Layout mismatch! Setting Hardware LED count to number of LEDs configured via layout", _instance);
newGrabberV4L2Config["grabberV4L2"] = newGrabberV4L2Config; hwLedcount = layoutLedCount;
migrated = true; newDeviceConfig["hardwareLedCount"] = hwLedcount;
migrated = true;
}
}
}
if (newDeviceConfig.contains("type"))
{
QString type = newDeviceConfig["type"].toString();
if (type == "atmoorb" || type == "fadecandy" || type == "philipshue" )
{
if (newDeviceConfig.contains("output"))
{
newDeviceConfig["host"] = newDeviceConfig["output"].toString();
newDeviceConfig.remove("output");
migrated = true;
}
}
}
if (migrated)
{
config["device"] = newDeviceConfig;
Debug(_log, "LED-Device records migrated");
}
} }
//Add new element enable if (config.contains("grabberV4L2"))
if (!newGrabberV4L2Config.contains("enable"))
{ {
newGrabberV4L2Config["enable"] = false; QJsonObject newGrabberV4L2Config = config["grabberV4L2"].toObject();
migrated = true;
}
config["grabberV4L2"] = newGrabberV4L2Config;
Debug(_log, "GrabberV4L2 records migrated");
}
if (config.contains("framegrabber")) if (newGrabberV4L2Config.contains("encoding_format"))
{ {
QJsonObject newFramegrabberConfig = config["framegrabber"].toObject(); newGrabberV4L2Config.remove("encoding_format");
newGrabberV4L2Config["grabberV4L2"] = newGrabberV4L2Config;
migrated = true;
}
//Align element namings with grabberV4L2 //Add new element enable
//Rename element type -> device if (!newGrabberV4L2Config.contains("enable"))
if (newFramegrabberConfig.contains("type")) {
{ newGrabberV4L2Config["enable"] = false;
newFramegrabberConfig["device"] = newFramegrabberConfig["type"].toString(); migrated = true;
newFramegrabberConfig.remove("type"); }
migrated = true; config["grabberV4L2"] = newGrabberV4L2Config;
} Debug(_log, "GrabberV4L2 records migrated");
//Rename element frequency_Hz -> fps
if (newFramegrabberConfig.contains("frequency_Hz"))
{
newFramegrabberConfig["fps"] = newFramegrabberConfig["frequency_Hz"].toInt(25);
newFramegrabberConfig.remove("frequency_Hz");
migrated = true;
} }
//Rename element display -> input if (config.contains("framegrabber"))
if (newFramegrabberConfig.contains("display"))
{ {
newFramegrabberConfig["input"] = newFramegrabberConfig["display"]; QJsonObject newFramegrabberConfig = config["framegrabber"].toObject();
newFramegrabberConfig.remove("display");
migrated = true;
}
//Add new element enable //Align element namings with grabberV4L2
if (!newFramegrabberConfig.contains("enable")) //Rename element type -> device
{ if (newFramegrabberConfig.contains("type"))
newFramegrabberConfig["enable"] = false; {
migrated = true; newFramegrabberConfig["device"] = newFramegrabberConfig["type"].toString();
} newFramegrabberConfig.remove("type");
migrated = true;
}
//Rename element frequency_Hz -> fps
if (newFramegrabberConfig.contains("frequency_Hz"))
{
newFramegrabberConfig["fps"] = newFramegrabberConfig["frequency_Hz"].toInt(25);
newFramegrabberConfig.remove("frequency_Hz");
migrated = true;
}
config["framegrabber"] = newFramegrabberConfig; //Rename element display -> input
Debug(_log, "Framegrabber records migrated"); if (newFramegrabberConfig.contains("display"))
{
newFramegrabberConfig["input"] = newFramegrabberConfig["display"];
newFramegrabberConfig.remove("display");
migrated = true;
}
//Add new element enable
if (!newFramegrabberConfig.contains("enable"))
{
newFramegrabberConfig["enable"] = false;
migrated = true;
}
config["framegrabber"] = newFramegrabberConfig;
Debug(_log, "Framegrabber records migrated");
}
} }
} }
} }
return migrated; return migrated;
} }

View File

@ -36,6 +36,9 @@ if(ENABLE_X11)
target_link_libraries(test_x11performance ${X11_LIBRARIES} Qt5::Widgets) target_link_libraries(test_x11performance ${X11_LIBRARIES} Qt5::Widgets)
endif(ENABLE_X11) endif(ENABLE_X11)
add_executable(test_versions TestVersions.cpp)
target_link_libraries(test_versions Qt5::Core)
######### These tests are broken. May they fix someone ########## ######### These tests are broken. May they fix someone ##########
# add_executable(test_image2ledsmap TestImage2LedsMap.cpp) # add_executable(test_image2ledsmap TestImage2LedsMap.cpp)

50
test/TestVersions.cpp Normal file
View File

@ -0,0 +1,50 @@
// STL includes
#include <iostream>
// QT includes
#include <QString>
#include <utils/version.hpp>
using namespace semver;
void test(const QString& a, const QString& b, char exp)
{
semver::version verA (a.toStdString());
semver::version verB (b.toStdString());
std::cout << "[" << a.toStdString() << "] : " << (verA.isValid() ? "" : "not ") << "valid" << std::endl;
std::cout << "[" << b.toStdString() << "] : " << (verB.isValid() ? "" : "not ") << "valid" << std::endl;
if ( verA.isValid() && verB.isValid())
{
std::cout << "[" << a.toStdString() << "] < [" << b.toStdString() << "]: " << (verA < verB) << " " << ((exp == '<') ? ((verA < verB) ? "OK" : "NOK") : "") << std::endl;
std::cout << "[" << a.toStdString() << "] > [" << b.toStdString() << "]: " << (verA > verB) << " " << ((exp == '>') ? ((verA > verB) ? "OK" : "NOK") : "") << std::endl;
std::cout << "[" << a.toStdString() << "] = [" << b.toStdString() << "]: " << (verA == verB) << " " << ((exp == '=') ? ((verA == verB) ? "OK" : "NOK") : "") << std::endl;
}
std::cout << "" << std::endl;
};
int main()
{
test ("2.12.0", "2.12.0", '=');
test ("2.0.0-alpha.11", "2.12.0", '<');
test ("2.11.1", "2.12.0", '<');
test ("2.12.0", "2.12.1", '<');
test ("2.12.0+PR4711", "2.12.0+PR4712", '=');
test ("2.12.0+nighly20211012ac1dad7", "2.12.0+nighly20211012ac1dad8", '=');
test ("2.0.0+nighly20211012ac1dad7", "2.0.12+nighly20211012ac1dad8", '<');
test ("2.0.0-alpha.11+nighly20211012ac1dad7", "2.0.0-alpha.11+nighly20211012ac1dad8", '=');
test ("2.0.0-alpha.11+PR1354", "2.0.0-alpha.11+PR1355", '=');
test ("2.0.0-alpha.11+nighly20211012ac1dad7", "2.0.12+nighly20211012ac1dad8", '<');
test ("2.0.0-alpha.11+nighly20211012ac1dad7", "2.0.12", '<');
test ("2.0.0-alpha-11", "2.12.0", '<');
test ("2.0.0-alpha.10.1", "2.0.0-alpha.10", '>');
return 0;
}