refactor(cpn): introduce BoundedString<N> for ModelData string fields (#7438)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Raphael Coeffic
2026-07-10 13:50:53 +10:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 68873e2955
commit 9326a202b2
21 changed files with 490 additions and 440 deletions
+4
View File
@@ -119,6 +119,10 @@ jobs:
with:
os: 'linux'
- name: Run companion unit tests
shell: bash
run: cmake --build build/native --target tests-companion --parallel
build-macos:
name: macOS Companion
runs-on: macos-15
+92
View File
@@ -0,0 +1,92 @@
/*
* Copyright (C) EdgeTX
*
* Based on code named
* opentx - https://github.com/opentx/opentx
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#pragma once
#include <string>
#include <string_view>
#include <QString>
// A self-limiting string for Companion's data-model fields.
//
// N is the maximum number of bytes the value may hold, EXCLUDING the implicit
// NUL of the legacy `char[]` fields it replaces. So a legacy `char foo[16+1]`
// (16 usable chars) maps to `BoundedString<16>`.
//
// Assignment always truncates to N bytes, mirroring the byte-level truncation
// the YAML char-array reader does today (yaml_ops.h: `str.copy(value, N-1)`).
// Truncation is byte-based on purpose: it must match the radio firmware (and the
// legacy code path) exactly, including the case where a multi-byte UTF-8
// sequence is split at the boundary. Do not "improve" it to be codepoint-aware.
template <size_t N>
class BoundedString
{
static_assert(N > 0, "BoundedString capacity must be greater than zero");
std::string s_;
public:
static constexpr size_t capacity() { return N; }
BoundedString() = default;
BoundedString(const BoundedString&) = default;
BoundedString& operator=(const BoundedString&) = default;
BoundedString(std::string_view v) { assign(v); }
BoundedString(const char* v) { assign(v ? std::string_view(v) : std::string_view()); }
BoundedString(const QString& v) { assign(v); }
BoundedString& operator=(std::string_view v) { assign(v); return *this; }
BoundedString& operator=(const char* v) { assign(v ? std::string_view(v) : std::string_view()); return *this; }
BoundedString& operator=(const QString& v) { assign(v); return *this; }
void assign(std::string_view v) { s_.assign(v.substr(0, N)); }
// NOTE: QString is serialised as UTF-8 here, matching the YAML layer. Some
// legacy UI call sites stored Latin-1 via QString::toLatin1(); Phase 1 must
// confirm the encoding at each migrated boundary before relying on this.
void assign(const QString& v) { assign(v.toStdString()); }
void clear() { s_.clear(); }
bool empty() const { return s_.empty(); }
size_t size() const { return s_.size(); }
size_t length() const { return s_.size(); }
const std::string& str() const { return s_; }
const char* c_str() const { return s_.c_str(); }
QString toQString() const { return QString::fromStdString(s_); }
// Implicit conversion so a migrated field still drops into APIs taking a
// std::string (and, via QString's std::string ctor, much of the Qt UI).
operator const std::string&() const { return s_; }
// Explicit comparison overloads (std::string's own operator== is a
// non-member template, so the implicit conversion above can't drive it).
// The const char* overload binds legacy char[]/literals via array-to-pointer
// (a standard conversion), so it wins cleanly over the others rather than
// tying with them and producing an ambiguity.
bool operator==(const BoundedString& o) const { return s_ == o.s_; }
bool operator!=(const BoundedString& o) const { return s_ != o.s_; }
bool operator==(const std::string& o) const { return s_ == o; }
bool operator!=(const std::string& o) const { return s_ != o; }
bool operator==(const char* o) const { return o ? s_ == o : s_.empty(); }
bool operator!=(const char* o) const { return o ? s_ != o : !s_.empty(); }
};
@@ -46,9 +46,9 @@
void YamlValidateLabelsNames(ModelData& model, Board::Type board)
{
YamlValidateName(model.name, board);
model.name = YamlValidateName(model.name.toQString(), board).toLatin1().constData();
QStringList lst = QString(model.labels).split(',', Qt::SkipEmptyParts);
QStringList lst = model.labels.toQString().split(',', Qt::SkipEmptyParts);
for (int i = lst.count() - 1; i >= 0; i--) {
YamlValidateLabel(lst[i]);
@@ -56,7 +56,7 @@ void YamlValidateLabelsNames(ModelData& model, Board::Type board)
lst.removeAt(i);
}
strcpy(model.labels, QString(lst.join(',')).toLatin1().data());
model.labels = QString(lst.join(',')).toLatin1().constData();
for (int i = 0; i < CPN_MAX_CURVES; i++) {
YamlValidateName(model.curves[i].name, board);
@@ -418,7 +418,7 @@ struct YamlSwitchWarningState {
std::stringstream ss(src_str);
while (!ss.eof()) {
auto c = ss.get();
if (c < 'A' && c > 'Z') {
if (c < 'A' || c > 'Z') {
ss.ignore();
continue;
}
@@ -1324,12 +1324,12 @@ bool convert<ModelData>::decode(const Node& node, ModelData& rhs)
if (node["semver"]) {
node["semver"] >> rhs.semver;
if (SemanticVersion().isValid(rhs.semver)) {
modelSettingsVersion = SemanticVersion(QString(rhs.semver));
if (SemanticVersion().isValid(rhs.semver.toQString())) {
modelSettingsVersion = SemanticVersion(rhs.semver.toQString());
}
else {
qDebug() << "Invalid settings version:" << rhs.semver;
memset(rhs.semver, 0, sizeof(rhs.semver));
qDebug() << "Invalid settings version:" << rhs.semver.c_str();
rhs.semver.clear();
}
}
@@ -1355,7 +1355,7 @@ bool convert<ModelData>::decode(const Node& node, ModelData& rhs)
<< "Companion" << SemanticVersion(VERSION).toString();
} else {
QString prmpt = QCoreApplication::translate("YamlModelSettings", "Warning: '%1' has settings version %2 that is not supported by Companion %3!\n\nModel settings may be corrupted if you continue.");
prmpt = prmpt.arg(rhs.name).arg(modelSettingsVersion.toString()).arg(SemanticVersion(VERSION).toString());
prmpt = prmpt.arg(rhs.name.toQString()).arg(modelSettingsVersion.toString()).arg(SemanticVersion(VERSION).toString());
QMessageBox msgBox;
msgBox.setWindowTitle(QCoreApplication::translate("YamlModelSettings", "Read Model Settings"));
msgBox.setText(prmpt);
+10 -6
View File
@@ -72,18 +72,22 @@ void YamlValidateLabel(QString &input)
delete lv;
}
void YamlValidateName(char *input, Board::Type board)
QString YamlValidateName(const QString &input, Board::Type board)
{
NameValidator *nv = new NameValidator(board);
NameValidator nv(board);
QString in(input);
if (!nv->isValid(in)) {
nv->fixup(in);
if (!nv.isValid(in)) {
nv.fixup(in);
in = in.trimmed();
}
strcpy(input, in.toLatin1().data());
delete nv;
return in;
}
void YamlValidateName(char *input, Board::Type board)
{
strcpy(input, YamlValidateName(QString(input), board).toLatin1().data());
}
namespace YAML {
+26
View File
@@ -25,6 +25,7 @@
#include "helpers.h"
#include "boards.h"
#include "semanticversion.h"
#include "boundedstring.h"
#include <algorithm>
#include <QString>
@@ -97,6 +98,17 @@ void operator >> (const YAML::Node& node, char (&value)[N])
}
}
// Mirrors the char[] reader above: a scalar is assigned and truncated to the
// field capacity. BoundedString<N> truncates to N bytes, matching the legacy
// char[N+1] field (which keeps N usable bytes plus the NUL).
template <size_t N>
void operator >> (const YAML::Node& node, BoundedString<N>& value)
{
if (node && node.IsScalar()) {
value = node.as<std::string>();
}
}
template <typename T, size_t N>
void operator>>(const YAML::Node& node, T (&value)[N])
{
@@ -127,10 +139,24 @@ void operator>>(const YAML::Node& node, T (&value)[N])
}
void YamlValidateName(char *input, Board::Type board);
QString YamlValidateName(const QString &input, Board::Type board);
void YamlValidateLabel(QString &input);
namespace YAML {
// Encode/decode for BoundedString<N> so `node["x"] = field` and `node.as<>()`
// behave like the legacy char[] fields. Decode truncates to the capacity.
template <size_t N>
struct convert<BoundedString<N>> {
static Node encode(const BoundedString<N>& rhs) { return Node(rhs.str()); }
static bool decode(const Node& node, BoundedString<N>& rhs)
{
if (node && node.IsScalar())
rhs = node.as<std::string>();
return true;
}
};
std::string LookupValue(const YamlLookupTable& lut, const int& value);
Node operator << (const YamlLookupTable& lut, const int& value);
+11 -11
View File
@@ -50,11 +50,11 @@ ModelData & ModelData::operator=(const ModelData & src)
void ModelData::copy(const ModelData & src)
{
memcpy(&semver, &src.semver, sizeof(semver));
semver = src.semver;
used = src.used;
memcpy(&name, &src.name, sizeof(name));
memcpy(&filename, &src.filename, sizeof(filename));
memcpy(&labels, &src.labels, sizeof(labels));
name = src.name;
filename = src.filename;
labels = src.labels;
modelIndex = src.modelIndex;
modelUpdated = src.modelUpdated;
modelErrors = src.modelErrors;
@@ -230,11 +230,11 @@ void ModelData::clear()
// memset(reinterpret_cast<void *>(this), 0, sizeof(ModelData));
// as struct contains complex data types eg std::string
memset(&semver, 0, sizeof(semver));
semver.clear();
used = false;
memset(&name, 0, sizeof(name));
memset(&filename, 0, sizeof(filename));
memset(&labels, 0, sizeof(labels));
name.clear();
filename.clear();
labels.clear();
modelIndex = -1; // an invalid index, this is managed by the TreeView data model
modelUpdated = false;
modelErrors = false;
@@ -419,7 +419,7 @@ void ModelData::setDefaultValues(unsigned int id, const GeneralSettings & settin
{
clear();
used = true;
sprintf(name, "MODEL%02d", id + 1);
name = QString::asprintf("MODEL%02d", id + 1);
for (int i = 0; i < CPN_MAX_MODULES; i++) {
moduleData[i].modelId = id + 1;
@@ -592,7 +592,7 @@ void ModelData::convert(RadioDataConversionState & cstate)
{
// Here we can add explicit conversions when moving from one board to another
QString origin = QString(name);
QString origin = name.toQString();
if (origin.isEmpty())
origin = QString::number(cstate.modelIdx+1);
cstate.setOrigin(tr("Model: ") % origin);
@@ -2180,7 +2180,7 @@ const Board::SwitchType ModelData::getSwitchType(int sw, const GeneralSettings &
QString ModelData::getChecklistFilename() const
{
return QString(name).replace(" ", "_").append(".txt").toLower();
return name.toQString().replace(" ", "_").append(".txt").toLower();
}
void ModelData::gvarClear(const int index, bool updateRefs)
+8 -4
View File
@@ -21,6 +21,7 @@
#pragma once
#include "boundedstring.h"
#include "constants.h"
#include "curvedata.h"
#include "customfunctiondata.h"
@@ -89,6 +90,9 @@ enum TrainerMode {
};
#define MODEL_NAME_LEN 15
#define MODEL_FILENAME_LEN 16 // must match radio LEN_MODEL_FILENAME (dataconstants.h)
#define MODEL_SEMVER_LEN 8
#define MODEL_LABELS_LEN 99 // CSV of labels; was char labels[100]
#define INPUT_NAME_LEN 4
#define CPN_MAX_BITMAP_LEN 14
@@ -129,11 +133,11 @@ class ModelData {
ModelData();
ModelData(const ModelData & src);
char semver[8 + 1];
BoundedString<MODEL_SEMVER_LEN> semver;
bool used;
char name[MODEL_NAME_LEN + 1];
char filename[16+1];
char labels[100];
BoundedString<MODEL_NAME_LEN> name;
BoundedString<MODEL_FILENAME_LEN> filename;
BoundedString<MODEL_LABELS_LEN> labels;
int modelIndex; // Companion only, temporary index position managed by data model.
bool modelUpdated; // Companion only, used to highlight if changed in models list
bool modelErrors; // Companion only, used to highlight if data errors in models list
+21 -21
View File
@@ -34,14 +34,14 @@ void RadioData::setCurrentModel(unsigned int index)
{
generalSettings.currModelIndex = index;
if (index < models.size()) {
strcpy(generalSettings.currModelFilename, models[index].filename);
strcpy(generalSettings.currModelFilename, models[index].filename.c_str());
}
}
void RadioData::fixModelFilename(unsigned int index)
{
ModelData & model = models[index];
QString filename(model.filename);
QString filename = model.filename.toQString();
const bool hasSDCard = Boards::getCapability(getCurrentFirmware()->getBoard(), Board::HasSDCard);
bool ok = hasSDCard ? filename.endsWith(".yml") : filename.endsWith(".bin");
if (ok) {
@@ -51,14 +51,14 @@ void RadioData::fixModelFilename(unsigned int index)
}
if (ok) {
for (unsigned i=0; i<index; i++) {
if (strcmp(models[i].filename, model.filename) == 0) {
if (models[i].filename == model.filename) {
ok = false;
break;
}
}
}
if (!ok) {
sprintf(model.filename, "model%d.%s", index + 1, hasSDCard ? "yml" : "bin");
model.filename = QString::asprintf("model%d.%s", index + 1, hasSDCard ? "yml" : "bin");
}
}
@@ -72,14 +72,14 @@ void RadioData::fixModelFilenames()
QString RadioData::getNextModelFilename()
{
char filename[sizeof(ModelData::filename)];
int8_t index = 0;
bool found = true;
QString filename;
while (found) {
sprintf(filename, "model%d.yml", ++index);
filename = QString::asprintf("model%d.yml", ++index);
found = false;
for (unsigned int i = 0; i < models.size(); i++) {
if (strcmp(filename, models[i].filename) == 0) {
if (models[i].filename == filename.toStdString()) {
found = true;
break;
}
@@ -135,12 +135,12 @@ bool RadioData::deleteLabel(QString label)
// Remove labels in the models
for(auto& model : models) {
QStringList modelLabels = fromCSV(QString::fromLatin1(model.labels));
QStringList modelLabels = fromCSV(QString::fromLatin1(model.labels.c_str()));
if (modelLabels.indexOf(label) >= 0) {
deleted = true;
modelLabels.removeAll(label);
}
strcpy(model.labels, toCSV(modelLabels).toLatin1().data());
model.labels = toCSV(modelLabels).toLatin1().constData();
}
// Remove the label from the global list
@@ -171,8 +171,8 @@ bool RadioData::renameLabel(QString from, QString to)
// Check that rename is possible (Rename won't cause too long of a string)
for(auto& model : models) {
if (RadioData::fromCSV(model.labels).indexOf(from) != -1) {
if ((int)strlen(model.labels) + lengthdiff > (int)sizeof(model.labels) - 1) {
if (RadioData::fromCSV(model.labels.toQString()).indexOf(from) != -1) {
if ((int)model.labels.size() + lengthdiff > (int)model.labels.capacity()) {
success = false;
throw std::length_error(model.name);
break;
@@ -181,13 +181,13 @@ bool RadioData::renameLabel(QString from, QString to)
}
if (success) {
for(auto& model : models) {
QStringList modelLabels = QString(model.labels).split(',', Qt::SkipEmptyParts);
QStringList modelLabels = model.labels.toQString().split(',', Qt::SkipEmptyParts);
int ind = modelLabels.indexOf(csvFrom);
if (ind != -1) {
modelLabels.replace(ind, csvTo);
QString outputcsv = QString(modelLabels.join(','));
if (outputcsv.toLatin1().size() < (int)sizeof(model.labels)) {
strcpy(model.labels, outputcsv.toLatin1().data());
if (outputcsv.toLatin1().size() <= (int)model.labels.capacity()) {
model.labels = outputcsv.toLatin1().constData();
} else { // Shouldn't ever get here, from check above
success = false;
throw std::length_error(model.name);
@@ -228,13 +228,13 @@ bool RadioData::addLabelToModel(int index, QString label)
if ((unsigned int)index >= models.size()) return false;
label = escapeCSV(label);
char *modelLabelCsv = models[index].labels;
auto & modelLabelCsv = models[index].labels;
// Make sure it will fit
if (strlen(modelLabelCsv) + label.size() + 1 < sizeof(models[index].labels)-1) {
QStringList modelLabels = QString::fromLatin1(modelLabelCsv).split(',', Qt::SkipEmptyParts);
if (modelLabelCsv.size() + (size_t)label.size() + 1 < modelLabelCsv.capacity()) {
QStringList modelLabels = QString::fromLatin1(modelLabelCsv.c_str()).split(',', Qt::SkipEmptyParts);
if (modelLabels.indexOf(label) == -1) {
modelLabels.append(label);
strcpy(models[index].labels, QString(modelLabels.join(',')).toLatin1().data());
modelLabelCsv = QString(modelLabels.join(',')).toLatin1().constData();
return true;
}
}
@@ -246,10 +246,10 @@ bool RadioData::removeLabelFromModel(int index, QString label)
{
if ((unsigned int)index >= models.size()) return false;
QStringList lbls = fromCSV(QString::fromLatin1(models[index].labels));
QStringList lbls = fromCSV(QString::fromLatin1(models[index].labels.c_str()));
if (lbls.indexOf(label) >= 0) {
lbls.removeAll(label);
strcpy(models[index].labels, toCSV(lbls).toLatin1().data());
models[index].labels = toCSV(lbls).toLatin1().constData();
return true;
}
return false;
@@ -258,7 +258,7 @@ bool RadioData::removeLabelFromModel(int index, QString label)
void RadioData::addLabelsFromModels()
{
for(const auto &model: models) {
QStringList labels = QString(model.labels).split(',', Qt::SkipEmptyParts);
QStringList labels = model.labels.toQString().split(',', Qt::SkipEmptyParts);
foreach(QString label, labels) {
addLabel(label);
}
+1 -1
View File
@@ -129,7 +129,7 @@ QVariant LabelsModel::data(const QModelIndex &index, int role) const
} else if (role == Qt::CheckStateRole) {
if (index.column() == 0 && selectedModel >= 0 &&
selectedModel < (int)radioData->models.size()) {
QStringList modelLabels = QString(radioData->models.at(selectedModel).labels).split(',',Qt::SkipEmptyParts);
QStringList modelLabels = radioData->models.at(selectedModel).labels.toQString().split(',',Qt::SkipEmptyParts);
label = RadioData::escapeCSV(label);
return modelLabels.indexOf(label)==-1?Qt::Unchecked:Qt::Checked;
} else if (index.column() == 0 && selectedModel == -1) {
+5 -5
View File
@@ -1033,7 +1033,7 @@ void MdiChild::pasteModelData(const QMimeData * mimeData, const QModelIndex row,
// We don't want to create an index value conflict so use an invalid one (it will get updated after we're done here)
// this is esp. important because otherwise we may delete this model during a move operation (eg. after a cut)
radioData.models[modelIdx].modelIndex = -modelIdx;
strcpy(radioData.models[modelIdx].filename, radioData.getNextModelFilename().toStdString().c_str());
radioData.models[modelIdx].filename = radioData.getNextModelFilename();
lastSelectedModel = modelIdx; // after refresh the last pasted model will be selected
modified = true;
setModelModified(modelIdx, false); // avoid unnecessary refreshes
@@ -1259,7 +1259,7 @@ void MdiChild::openModelEditWindow(int row)
gStopwatch.report("ModelEdit creation");
ModelEdit * t = new ModelEdit(this, radioData, (row), firmware);
gStopwatch.report("ModelEdit created");
t->setWindowTitle(tr("Editing model %1: ").arg(row+1) + QString(model.name) + QString(" (%1)").arg(userFriendlyCurrentFile()));
t->setWindowTitle(tr("Editing model %1: ").arg(row+1) + model.name.toQString() + QString(" (%1)").arg(userFriendlyCurrentFile()));
connect(t, &ModelEdit::modified, this, &MdiChild::setCurrentModelModified);
connect(t, &ModelEdit::closed, this, &MdiChild::onModelEditClosed);
gStopwatch.report("STARTING MODEL EDIT");
@@ -1922,7 +1922,7 @@ unsigned MdiChild::exportModels(const QVector<int> modelIndices)
if (idx < 0 || idx >= (int)radioData.models.size())
continue;
const QString path(QDir::toNativeSeparators(g.profile[g.id()].sdPath() + "/TEMPLATES/" + QString(radioData.models[idx].name) + ".yml"));
const QString path(QDir::toNativeSeparators(g.profile[g.id()].sdPath() + "/TEMPLATES/" + radioData.models[idx].name.toQString() + ".yml"));
qDebug() << path;
QString filename;
@@ -2000,7 +2000,7 @@ QStringList MdiChild::modelErrorsList()
void MdiChild::modelShowErrors()
{
ModelData &mdl = radioData.models[getCurrentModel()];
QMessageBox::critical(this, QString("%1").arg(mdl.name), mdl.errorsList().join("\n"));
QMessageBox::critical(this, QString("%1").arg(mdl.name.toQString()), mdl.errorsList().join("\n"));
}
void MdiChild::onModelEditClosed(int id)
@@ -2111,7 +2111,7 @@ void MdiChild::modelImport()
if (ok) {
// We don't want to create an index value conflict so use an invalid one (it will get updated after we're done here)
radioData.models[modelIdx].modelIndex = -modelIdx;
strcpy(radioData.models[modelIdx].filename, radioData.getNextModelFilename().toStdString().c_str());
radioData.models[modelIdx].filename = radioData.getNextModelFilename();
lastSelectedModel = modelIdx; // after refresh the last pasted model will be selected
setModelModified(modelIdx, false); // avoid unnecessary refreshes
radioData.addLabelsFromModels();
+3 -3
View File
@@ -357,9 +357,9 @@ void SetupPanel::on_throttleTrimSwitch_currentIndexChanged(int index)
void SetupPanel::on_name_editingFinished()
{
if (QString(model->name) != ui->name->text()) {
if (model->name.toQString() != ui->name->text()) {
int length = ui->name->maxLength();
strncpy(model->name, ui->name->text().toLatin1(), length);
model->name = ui->name->text().left(length).toLatin1().constData();
emit modified();
}
}
@@ -410,7 +410,7 @@ void SetupPanel::populateThrottleTrimSwitchCB()
void SetupPanel::update()
{
ui->name->setText(model->name);
ui->name->setText(model->name.toQString());
ui->throttleReverse->setChecked(model->throttleReversed);
ui->throttleSource->updateValue();
populateThrottleTrimSwitchCB();
+3 -3
View File
@@ -715,8 +715,8 @@ void ModelsListModel::refresh()
if (!model.isEmpty() && current) {
QString modelName;
if (strlen(model.name) > 0) {
modelName = model.name;
if (!model.name.empty()) {
modelName = model.name.toQString();
}
else {
/*: Translators: do NOT use accents here, this is a default model name. */
@@ -746,7 +746,7 @@ void ModelsListModel::refresh()
current->setData(currentColumn++, rxs);
}
if (hasLabels) {
QStringList labels = RadioData::fromCSV(QString::fromUtf8(model.labels));
QStringList labels = RadioData::fromCSV(QString::fromUtf8(model.labels.c_str()));
current->setData(currentColumn++, labels.join(QChar(0x2022)));
}
}
+1 -1
View File
@@ -123,7 +123,7 @@ void CompareDialog::compare()
for (int i=0; i < modelsList.size(); ++i) {
multimodelprinter->setModel(i, &modelsList[i].model, &modelsList[i].gs);
QString name(modelsList.at(i).model.name);
QString name(modelsList.at(i).model.name.toQString());
if (name.isEmpty())
name = tr("Unnamed Model %1").arg(i+1);
+1 -1
View File
@@ -310,7 +310,7 @@ QString MultiModelPrinter::printSetup()
MultiColumns columns(modelPrinterMap.size());
columns.appendSectionTableStart();
ROWLABELCOMPARECELL(tr("Name"), 20, model->name, 80);
ROWLABELCOMPARECELL(tr("Name"), 20, model->name.toQString(), 80);
if (firmware->getCapability(HasModelImage)) {
ROWLABELCOMPARECELL(tr("Model Image"), 0, model->bitmap, 0);
}
+1 -1
View File
@@ -37,7 +37,7 @@ PrintDialog::PrintDialog(QWidget *parent, Firmware * firmware, GeneralSettings &
{
ui->setupUi(this);
setWindowIcon(CompanionIcon("print.png"));
setWindowTitle(model.name);
setWindowTitle(model.name.toQString());
multiModelPrinter.setModel(0, &model, &generalSettings);
ui->textEdit->setHtml(multiModelPrinter.print(ui->textEdit->document()));
if (!printfilename.isEmpty()) {
+2 -2
View File
@@ -178,9 +178,9 @@ bool LabelsStorageFormat::load(RadioData & radioData)
}
model.modelIndex = modelIdx;
strncpy(model.filename, mc.filename.c_str(), sizeof(model.filename)-1);
model.filename = mc.filename;
if (hasLabels && !strncmp(radioData.generalSettings.currModelFilename, model.filename, sizeof(model.filename)))
if (hasLabels && model.filename == radioData.generalSettings.currModelFilename)
radioData.generalSettings.currModelIndex = modelIdx;
model.used = true;
+161
View File
@@ -0,0 +1,161 @@
/*
* Copyright (C) EdgeTX
*
* Based on code named
* opentx - https://github.com/opentx/opentx
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#include "gtests.h"
#include <string>
#include <vector>
#include "firmwares/boundedstring.h"
#include "firmwares/edgetx/yaml_ops.h"
// A representative spread of inputs, including boundary and over-length cases
// and a multi-byte UTF-8 sequence that straddles the truncation point.
static const std::vector<std::string> kInputs = {
"",
"model1.yml",
"0123456789012345", // 16 bytes: exactly the filename capacity
"01234567890123456789", // over the filename capacity
std::string(160, 'X'), // far over any capacity
"caf\xC3\xA9", // "café" (UTF-8), 5 bytes
};
// --- Construction & assignment -------------------------------------------
TEST(BoundedString, AssignTruncatesToCapacity)
{
BoundedString<16> bs;
bs = std::string(40, 'A');
EXPECT_EQ(16u, bs.size());
EXPECT_EQ(std::string(16, 'A'), bs.str());
}
TEST(BoundedString, ShortValueIsUntouched)
{
BoundedString<16> bs("model1.yml");
EXPECT_EQ("model1.yml", bs.str());
EXPECT_FALSE(bs.empty());
}
TEST(BoundedString, AssignFromVariousSources)
{
BoundedString<16> a("c-string");
BoundedString<16> b(std::string("std-string"));
BoundedString<16> c(QString("qstring"));
EXPECT_EQ("c-string", a.str());
EXPECT_EQ("std-string", b.str());
EXPECT_EQ("qstring", c.str());
EXPECT_EQ(QString("qstring"), c.toQString());
}
TEST(BoundedString, NullCStringIsEmpty)
{
const char* p = nullptr;
BoundedString<16> bs(p);
EXPECT_TRUE(bs.empty());
}
TEST(BoundedString, Comparison)
{
BoundedString<16> a("abc");
BoundedString<16> b("abc");
BoundedString<16> c("xyz");
EXPECT_TRUE(a == b);
EXPECT_TRUE(a != c);
EXPECT_TRUE(a == std::string("abc"));
EXPECT_TRUE(a != std::string("abcd"));
EXPECT_TRUE(a == "abc"); // const char*
}
// --- YAML round-trip ------------------------------------------------------
TEST(BoundedString, YamlRoundTrip)
{
for (const auto& in : kInputs) {
BoundedString<16> src(in); // truncates on construction
YAML::Node node;
node["v"] = src;
BoundedString<16> dst;
node["v"] >> dst;
EXPECT_EQ(src.str(), dst.str()) << "round-trip mismatch for input: " << in;
}
}
TEST(BoundedString, YamlDecodeTruncatesOverlongScalar)
{
// A malicious/oversized scalar must be clamped on decode, not overflow.
YAML::Node node;
node["v"] = std::string(200, 'Z');
BoundedString<16> dst;
node["v"] >> dst;
EXPECT_EQ(16u, dst.size());
}
// --- Byte-for-byte equivalence with the legacy char[N+1] path -------------
// This is the gate that protects radio YAML compatibility: BoundedString<N>
// must produce identical bytes to a legacy `char[N+1]` field on both encode
// and decode, for every input.
TEST(BoundedString, MatchesLegacyCharArrayOnDecode)
{
constexpr size_t CAP = 16;
for (const auto& in : kInputs) {
YAML::Node node;
node["v"] = in;
char legacy[CAP + 1] = {0};
node["v"] >> legacy;
BoundedString<CAP> modern;
node["v"] >> modern;
EXPECT_EQ(std::string(legacy), modern.str())
<< "decode diverged from legacy char[] for input: " << in;
}
}
TEST(BoundedString, MatchesLegacyCharArrayOnEncode)
{
constexpr size_t CAP = 16;
for (const auto& in : kInputs) {
char legacy[CAP + 1] = {0};
{
// seed the legacy buffer the same way decode would
YAML::Node seed;
seed["v"] = in;
seed["v"] >> legacy;
}
BoundedString<CAP> modern(in);
YAML::Node a, b;
a["v"] = legacy;
b["v"] = modern;
YAML::Emitter ea, eb;
ea << a;
eb << b;
EXPECT_EQ(std::string(ea.c_str()), std::string(eb.c_str()))
<< "encode diverged from legacy char[] for input: " << in;
}
}
-366
View File
@@ -1,366 +0,0 @@
#include "gtests.h"
#include "location.h"
#include "storage/etx.h"
#include "storage/storage.h"
#include "firmwares/opentx/opentxinterface.h"
#include "firmwares/customfunctiondata.h"
TEST(Conversions, ConversionX9DPFrom22)
{
RadioData radioData;
Storage store = Storage(RADIO_TESTS_PATH "/eeprom_22_x9d+.bin");
ASSERT_EQ(true, store.load(radioData));
const GeneralSettings& settings = radioData.generalSettings;
EXPECT_EQ(RawSwitch(SWITCH_TYPE_TELEMETRY, 1), settings.customFn[0].swtch);
EXPECT_EQ(FuncLogs, settings.customFn[0].func);
EXPECT_EQ(20, settings.customFn[0].param);
EXPECT_STREQ("Tes", settings.switchName[0]);
EXPECT_EQ(Board::SWITCH_3POS, settings.switchConfig[0]);
const ModelData& model = radioData.models[0];
EXPECT_STREQ("Test", model.name);
EXPECT_EQ(TimerData::TIMERMODE_ON, model.timers[0].mode);
EXPECT_EQ(RawSwitch(SWITCH_TYPE_SWITCH, SWITCH_SA0), model.timers[0].swtch);
EXPECT_EQ(80, model.mixData[0].weight);
EXPECT_EQ(900, model.limitData[0].max); // -100
EXPECT_EQ(80, model.expoData[0].weight);
EXPECT_EQ(CurveReference::CURVE_REF_CUSTOM, model.expoData[0].curve.type);
EXPECT_EQ(1, model.expoData[0].curve.value);
EXPECT_EQ(CurveReference::CURVE_REF_EXPO, model.expoData[1].curve.type);
EXPECT_EQ(20, model.expoData[1].curve.value);
EXPECT_EQ(HELI_SWASH_TYPE_120X, model.swashRingData.type);
EXPECT_EQ(10, model.flightModeData[0].gvars[0]);
EXPECT_STREQ("Tes", model.gvarData[0].name);
EXPECT_EQ(PULSES_PXX_XJT_X16, model.moduleData[0].protocol);
EXPECT_EQ(0, model.moduleData[0].subType);
EXPECT_EQ(PULSES_PXX_R9M, model.moduleData[1].protocol);
EXPECT_EQ(MODULE_SUBTYPE_R9M_FCC, model.moduleData[1].subType);
EXPECT_STREQ("Thr", model.inputNames[0]);
EXPECT_STREQ("Tes", model.sensorData[0].label);
EXPECT_EQ(10, model.sensorData[0].id);
EXPECT_EQ(10, model.sensorData[0].instance);
EXPECT_EQ(RawSource(SOURCE_TYPE_TELEMETRY,0).toValue(), model.logicalSw[0].val1);
}
TEST(Conversions, ConversionX9DPFrom23)
{
RadioData radioData;
Storage store = Storage(RADIO_TESTS_PATH "/eeprom_23_x9d+.bin");
ASSERT_EQ(true, store.load(radioData));
const GeneralSettings& settings = radioData.generalSettings;
EXPECT_EQ(20, settings.speakerVolume);
EXPECT_EQ(RawSwitch(SWITCH_TYPE_TELEMETRY, 1), settings.customFn[0].swtch);
EXPECT_EQ(FuncLogs, settings.customFn[0].func);
EXPECT_EQ(20, settings.customFn[0].param);
EXPECT_STREQ("Tes", settings.switchName[0]);
EXPECT_EQ(Board::SWITCH_3POS, settings.switchConfig[0]);
const ModelData & model1 = radioData.models[0];
EXPECT_STREQ("Test", model1.name);
EXPECT_EQ(TimerData::TIMERMODE_ON, model1.timers[0].mode);
EXPECT_EQ(RawSwitch(SWITCH_TYPE_SWITCH, SWITCH_SA0), model1.timers[0].swtch);
EXPECT_EQ(80, model1.mixData[0].weight);
EXPECT_EQ(900, model1.limitData[0].max); // -100
EXPECT_EQ(80, model1.expoData[0].weight);
// EXPECT_EQ(HELI_SWASH_TYPE_120X, model1.swashRingData.type);
EXPECT_EQ(10, model1.flightModeData[0].gvars[0]);
EXPECT_STREQ("Tes", model1.gvarData[0].name);
EXPECT_EQ(PULSES_ACCST_ISRM_D16, model1.moduleData[0].protocol);
EXPECT_EQ(PULSES_PXX_R9M, model1.moduleData[1].protocol);
EXPECT_EQ(MODULE_SUBTYPE_R9M_FCC, model1.moduleData[1].subType);
EXPECT_STREQ("Rud", model1.inputNames[0]);
EXPECT_STREQ("Tes", model1.sensorData[0].label);
EXPECT_EQ(10, model1.sensorData[0].id);
EXPECT_EQ(11, model1.sensorData[0].instance);
EXPECT_EQ(RawSource(SOURCE_TYPE_TELEMETRY,0).toValue(), model1.logicalSw[0].val1);
const ModelData & model2 = radioData.models[1];
EXPECT_STREQ("Test", model2.name);
EXPECT_EQ(TimerData::TIMERMODE_ON, model2.timers[0].mode);
EXPECT_EQ(RawSwitch(SWITCH_TYPE_SWITCH, SWITCH_SA0), model2.timers[0].swtch);
EXPECT_EQ(80, model2.mixData[0].weight);
EXPECT_EQ(900, model2.limitData[0].max); // -100
EXPECT_EQ(80, model2.expoData[0].weight);
// EXPECT_EQ(HELI_SWASH_TYPE_120X, model2.swashRingData.type);
EXPECT_EQ(10, model2.flightModeData[0].gvars[0]);
EXPECT_STREQ("Tes", model2.gvarData[0].name);
EXPECT_EQ(PULSES_ACCESS_ISRM, model2.moduleData[0].protocol);
EXPECT_EQ(0, model2.moduleData[0].subType);
EXPECT_EQ(PULSES_PXX_R9M, model2.moduleData[1].protocol);
EXPECT_EQ(MODULE_SUBTYPE_R9M_FCC, model2.moduleData[1].subType);
EXPECT_STREQ("Rud", model2.inputNames[0]);
EXPECT_STREQ("Tes", model2.sensorData[0].label);
EXPECT_EQ(10, model2.sensorData[0].id);
EXPECT_EQ(11, model2.sensorData[0].instance);
EXPECT_EQ(RawSource(SOURCE_TYPE_TELEMETRY,0).toValue(), model2.logicalSw[0].val1);
}
TEST(Conversions, ConversionX7From22)
{
RadioData radioData;
Storage store = Storage(RADIO_TESTS_PATH "/eeprom_22_x7.bin");
ASSERT_EQ(true, store.load(radioData));
const GeneralSettings& settings = radioData.generalSettings;
EXPECT_EQ(RawSwitch(SWITCH_TYPE_TELEMETRY, 1), settings.customFn[0].swtch);
EXPECT_EQ(FuncLogs, settings.customFn[0].func);
EXPECT_EQ(20, settings.customFn[0].param);
EXPECT_STREQ("Tes", settings.switchName[0]);
EXPECT_EQ(Board::SWITCH_3POS, settings.switchConfig[0]);
const ModelData& model = radioData.models[0];
EXPECT_STREQ("Test", model.name);
EXPECT_EQ(PULSES_PXX_R9M, model.moduleData[1].protocol);
EXPECT_EQ(MODULE_SUBTYPE_R9M_EU, model.moduleData[1].subType);
EXPECT_EQ(80, model.mixData[0].weight);
EXPECT_EQ(80, model.expoData[0].weight);
EXPECT_EQ(RawSource(SOURCE_TYPE_PPM,0), model.mixData[4].srcRaw);
EXPECT_EQ(RawSwitch(SWITCH_TYPE_TELEMETRY, 1), model.mixData[4].swtch);
EXPECT_EQ(HELI_SWASH_TYPE_120X, model.swashRingData.type);
EXPECT_STREQ("Thr", model.inputNames[0]);
EXPECT_STREQ("Tes", model.sensorData[0].label);
EXPECT_EQ(10, model.sensorData[0].id);
EXPECT_EQ(10, model.sensorData[0].instance);
EXPECT_EQ(900, model.limitData[0].max); // -100
EXPECT_EQ(10, model.flightModeData[0].gvars[0]);
EXPECT_STREQ("FMtest", model.flightModeData[1].name);
EXPECT_EQ(RawSwitch(SWITCH_TYPE_VIRTUAL,1), model.flightModeData[1].swtch);
EXPECT_STREQ("Tes", model.gvarData[0].name);
EXPECT_EQ(LS_FN_VPOS, model.logicalSw[0].func);
EXPECT_EQ(RawSource(SOURCE_TYPE_PPM,0).toValue(), model.logicalSw[0].val1);
EXPECT_EQ(0, model.logicalSw[0].val2);
const FrSkyScreenData& screen = model.frsky.screens[0];
EXPECT_EQ(TELEMETRY_SCREEN_NUMBERS, screen.type);
EXPECT_EQ(RawSource(SOURCE_TYPE_PPM,0), screen.body.lines[0].source[0]);
EXPECT_EQ(RawSource(SOURCE_TYPE_SPECIAL,4), screen.body.lines[0].source[1]);
}
TEST(Conversions, ConversionXLiteFrom22)
{
RadioData radioData;
Storage store = Storage(RADIO_TESTS_PATH "/eeprom_22_xlite.bin");
ASSERT_EQ(true, store.load(radioData));
const GeneralSettings& settings = radioData.generalSettings;
EXPECT_EQ(RawSwitch(SWITCH_TYPE_TELEMETRY, 1), settings.customFn[0].swtch);
EXPECT_EQ(FuncLogs, settings.customFn[0].func);
EXPECT_EQ(20, settings.customFn[0].param);
EXPECT_STREQ("Tes", settings.switchName[0]);
EXPECT_EQ(Board::SWITCH_3POS, settings.switchConfig[0]);
const ModelData& model = radioData.models[0];
EXPECT_STREQ("Test", model.name);
EXPECT_EQ(PULSES_PXX_R9M, model.moduleData[1].protocol);
EXPECT_EQ(MODULE_SUBTYPE_R9M_EU, model.moduleData[1].subType);
EXPECT_EQ(80, model.mixData[0].weight);
EXPECT_EQ(80, model.expoData[0].weight);
EXPECT_EQ(RawSource(SOURCE_TYPE_PPM,0), model.mixData[4].srcRaw);
EXPECT_EQ(RawSwitch(SWITCH_TYPE_TELEMETRY, 1), model.mixData[4].swtch);
EXPECT_EQ(HELI_SWASH_TYPE_120X, model.swashRingData.type);
EXPECT_STREQ("Thr", model.inputNames[0]);
EXPECT_STREQ("Tes", model.sensorData[0].label);
EXPECT_EQ(10, model.sensorData[0].id);
EXPECT_EQ(9, model.sensorData[0].instance);
EXPECT_EQ(900, model.limitData[0].max); // -100
EXPECT_EQ(LS_FN_VPOS, model.logicalSw[0].func);
EXPECT_EQ(RawSource(SOURCE_TYPE_PPM,0).toValue(), model.logicalSw[0].val1);
EXPECT_EQ(0, model.logicalSw[0].val2);
const FrSkyScreenData& screen = model.frsky.screens[0];
EXPECT_EQ(TELEMETRY_SCREEN_NUMBERS, screen.type);
EXPECT_EQ(RawSource(SOURCE_TYPE_PPM,0), screen.body.lines[0].source[0]);
EXPECT_EQ(RawSource(SOURCE_TYPE_SPECIAL,4), screen.body.lines[0].source[1]);
}
bool loadFile(QByteArray & filedata, const QString & filename)
{
QFile file(filename);
if (!file.open(QFile::ReadOnly)) {
qDebug() << QString("Error opening file %1:\n%2.").arg(filename).arg(file.errorString());
return false;
}
filedata = file.readAll();
qDebug() << "File" << filename << "read, size:" << filedata.size();
return true;
}
TEST(Conversions, ConversionX10From22)
{
QByteArray byteBuffer;
#define USE_ETX
#if defined(USE_ETX)
EtxFormat etx(RADIO_TESTS_PATH "/model_22_x10.etx");
RadioData radio;
EXPECT_EQ(true, etx.load(radio));
const GeneralSettings& settings = radio.generalSettings;
const ModelData& model = radio.models[0];
#else
ASSERT_EQ(true, loadFile(byteBuffer, RADIO_TESTS_PATH "/model_22_x10/RADIO/radio.bin"));
GeneralSettings settings;
EXPECT_NE(nullptr, loadRadioSettingsFromByteArray(settings, byteBuffer));
#endif
EXPECT_EQ(100, settings.calibSpanNeg[9]);
EXPECT_EQ(500, settings.calibMid[9]);
EXPECT_EQ(900, settings.calibSpanPos[9]);
EXPECT_EQ(200, settings.calibSpanNeg[10]);
EXPECT_EQ(400, settings.calibMid[10]);
EXPECT_EQ(600, settings.calibSpanPos[10]);
EXPECT_EQ(-23, settings.vBatMin);
EXPECT_EQ(20, settings.speakerVolume);
EXPECT_STREQ("en", settings.ttsLanguage);
EXPECT_STREQ("model1.bin", settings.currModelFilename);
EXPECT_EQ(RawSwitch(SWITCH_TYPE_TELEMETRY, 1), settings.customFn[0].swtch);
EXPECT_EQ(FuncLogs, settings.customFn[0].func);
EXPECT_EQ(20, settings.customFn[0].param);
EXPECT_EQ(RawSwitch(SWITCH_TYPE_ON), settings.customFn[1].swtch);
EXPECT_EQ(FuncVolume, settings.customFn[1].func);
EXPECT_EQ(RawSource(SOURCE_TYPE_INPUT, 4+5+1).toValue(), settings.customFn[1].param); // RS
EXPECT_STREQ("Tes", settings.switchName[0]);
EXPECT_EQ(Board::SWITCH_3POS, settings.switchConfig[0]);
EXPECT_STREQ("BT_X10", settings.bluetoothName);
#if !defined(USE_ETX)
byteBuffer.clear();
ASSERT_EQ(true, loadFile(byteBuffer, RADIO_TESTS_PATH "/model_22_x10/MODELS/model1.bin"));
ModelData model;
ASSERT_NE(nullptr, loadModelFromByteArray(model, byteBuffer));
#endif
EXPECT_STREQ("Test", model.name);
EXPECT_EQ(0, model.noGlobalFunctions);
EXPECT_EQ(0, model.beepANACenter);
EXPECT_EQ(80, model.mixData[0].weight);
EXPECT_EQ(RawSource(SOURCE_TYPE_MAX), model.mixData[2].srcRaw); // MAX
EXPECT_EQ(RawSource(SOURCE_TYPE_INPUT, 4+5), model.mixData[3].srcRaw); // LS
EXPECT_EQ(RawSource(SOURCE_TYPE_PPM, 0), model.mixData[5].srcRaw);
EXPECT_EQ(RawSwitch(SWITCH_TYPE_TELEMETRY, 1), model.mixData[5].swtch);
EXPECT_EQ(900, model.limitData[0].max); // -100
EXPECT_EQ(80, model.expoData[0].weight);
EXPECT_EQ(LS_FN_VPOS, model.logicalSw[0].func);
EXPECT_EQ(RawSource(SOURCE_TYPE_PPM,0).toValue(), model.logicalSw[0].val1); // TR1
EXPECT_EQ(0, model.logicalSw[0].val2);
EXPECT_EQ(RawSource(SOURCE_TYPE_TELEMETRY, 19*3).toValue(), model.logicalSw[1].val1); // TELE:20
EXPECT_EQ(20, model.logicalSw[1].val2);
EXPECT_EQ(RawSwitch(SWITCH_TYPE_VIRTUAL,1).toValue(), model.logicalSw[1].andsw);
EXPECT_EQ(HELI_SWASH_TYPE_120X, model.swashRingData.type);
EXPECT_STREQ("Tes", model.flightModeData[0].name);
EXPECT_EQ(10, model.flightModeData[0].gvars[0]);
EXPECT_STREQ("Tes", model.gvarData[0].name);
EXPECT_EQ(PULSES_PXX_XJT_D8, model.moduleData[0].protocol);
EXPECT_EQ(PULSES_PXX_R9M, model.moduleData[1].protocol);
EXPECT_EQ(MODULE_SUBTYPE_R9M_EU, model.moduleData[1].subType);
EXPECT_STREQ("Rud", model.inputNames[0]);
EXPECT_STREQ("Tes", model.sensorData[0].label);
EXPECT_EQ(10, model.sensorData[0].id);
EXPECT_EQ(10, model.sensorData[0].instance);
EXPECT_EQ(5 + 2 + 3, model.thrTraceSrc); // CH3
}
TEST(Conversions, ConversionX12SFrom22)
{
QByteArray byteBuffer;
#define USE_ETX
#if defined(USE_ETX)
EtxFormat etx(RADIO_TESTS_PATH "/model_22_x12s.etx");
RadioData radio;
EXPECT_EQ(true, etx.load(radio));
const GeneralSettings& settings = radio.generalSettings;
const ModelData& model = radio.models[0];
#else
ASSERT_EQ(true, loadFile(byteBuffer, RADIO_TESTS_PATH "/model_22_x12s/RADIO/radio.bin"));
GeneralSettings settings;
EXPECT_NE(nullptr, loadRadioSettingsFromByteArray(settings, byteBuffer));
#endif
EXPECT_EQ(-30, settings.vBatMin);
EXPECT_EQ(20, settings.speakerVolume);
EXPECT_STREQ("en", settings.ttsLanguage);
EXPECT_STREQ("model1.bin", settings.currModelFilename);
EXPECT_EQ(RawSwitch(SWITCH_TYPE_TELEMETRY, 1), settings.customFn[0].swtch);
EXPECT_EQ(FuncLogs, settings.customFn[0].func);
EXPECT_EQ(20, settings.customFn[0].param);
EXPECT_EQ(RawSwitch(SWITCH_TYPE_ON), settings.customFn[1].swtch);
EXPECT_EQ(FuncVolume, settings.customFn[1].func);
EXPECT_EQ(RawSource(SOURCE_TYPE_INPUT, 4+5+1).toValue(), settings.customFn[1].param); // RS
EXPECT_STREQ("Tes", settings.switchName[0]);
EXPECT_EQ(Board::SWITCH_3POS, settings.switchConfig[0]);
EXPECT_STREQ("BT", settings.bluetoothName);
#if !defined(USE_ETX)
byteBuffer.clear();
ASSERT_EQ(true, loadFile(byteBuffer, RADIO_TESTS_PATH "/model_22_x10/MODELS/model1.bin"));
ModelData model;
ASSERT_NE(nullptr, loadModelFromByteArray(model, byteBuffer));
#endif
EXPECT_STREQ("Test", model.name);
EXPECT_EQ(0, model.noGlobalFunctions);
EXPECT_EQ(0, model.beepANACenter);
EXPECT_EQ(80, model.mixData[0].weight);
EXPECT_EQ(RawSource(SOURCE_TYPE_MAX), model.mixData[2].srcRaw); // MAX
EXPECT_EQ(RawSource(SOURCE_TYPE_INPUT, 4+5), model.mixData[3].srcRaw); // LS
EXPECT_EQ(RawSource(SOURCE_TYPE_PPM, 0), model.mixData[5].srcRaw);
EXPECT_EQ(RawSwitch(SWITCH_TYPE_TELEMETRY, 1), model.mixData[5].swtch);
EXPECT_EQ(900, model.limitData[0].max); // -100
EXPECT_EQ(80, model.expoData[0].weight);
EXPECT_EQ(LS_FN_VPOS, model.logicalSw[0].func);
EXPECT_EQ(RawSource(SOURCE_TYPE_PPM,0).toValue(), model.logicalSw[0].val1); // TR1
EXPECT_EQ(0, model.logicalSw[0].val2);
EXPECT_EQ(RawSource(SOURCE_TYPE_TELEMETRY, 19*3).toValue(), model.logicalSw[1].val1); // TELE:20
EXPECT_EQ(20, model.logicalSw[1].val2);
EXPECT_EQ(RawSwitch(SWITCH_TYPE_VIRTUAL,1).toValue(), model.logicalSw[1].andsw);
EXPECT_EQ(HELI_SWASH_TYPE_120X, model.swashRingData.type);
EXPECT_STREQ("Test", model.flightModeData[0].name);
EXPECT_EQ(10, model.flightModeData[0].gvars[0]);
EXPECT_STREQ("Tes", model.gvarData[0].name);
EXPECT_EQ(PULSES_PXX_R9M, model.moduleData[1].protocol);
EXPECT_EQ(MODULE_SUBTYPE_R9M_EU, model.moduleData[1].subType);
EXPECT_STREQ("Rud", model.inputNames[0]);
EXPECT_STREQ("Tes", model.sensorData[0].label);
EXPECT_EQ(10, model.sensorData[0].id);
EXPECT_EQ(10, model.sensorData[0].instance);
EXPECT_EQ(5 + 2 + 3, model.thrTraceSrc); // CH3
}
+2
View File
@@ -29,6 +29,7 @@
#include "storage.h"
#include "opentxinterface.h"
#include "customdebug.h"
#include "firmwares/boardfactories.h"
using ::testing::TestEventListener;
@@ -130,6 +131,7 @@ int main(int argc, char **argv)
QCoreApplication app(argc, argv);
CustomDebug::setFilterRules();
gBoardFactories = new BoardFactories();
registerStorageFactories();
registerOpenTxFirmwares();
@@ -0,0 +1,126 @@
/*
* Copyright (C) EdgeTX
*
* Based on code named
* opentx - https://github.com/opentx/opentx
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
// Model-level YAML round-trip corpus test.
//
// Gates the byte-compatibility of the BoundedString<N> migration: the
// migrated ModelData header fields (name, labels) must survive Companion's
// real YAML (de)serialisation unchanged, and the serialisation must be a
// byte-stable fixed point. Truncation to the field capacity must match the
// radio (and the legacy char[] behaviour) exactly.
//
// Note: ModelData::filename is the on-storage file name and is NOT part of a
// single model's YAML (the ScriptData "file" key is unrelated), so it is not
// exercised here -- its byte equivalence is covered by boundedstring_test.
#include "gtests.h"
#include <QByteArray>
#include <string>
#include <vector>
#include "firmwares/eeprominterface.h"
#include "firmwares/edgetx/edgetxinterface.h"
namespace {
// Round-trip a model through the real Companion YAML serialisation and return
// the reloaded copy; hands the serialised bytes back to the caller.
ModelData roundTrip(const ModelData& in, QByteArray& yamlOut)
{
writeModelToYaml(in, yamlOut);
ModelData out;
out.clear();
loadModelFromYaml(out, yamlOut);
return out;
}
class ModelYamlRoundTrip : public ::testing::Test
{
protected:
void SetUp() override
{
Firmware::setCurrentVariant(Firmware::getFirmwareForFlavour("tx16s"));
ASSERT_NE(getCurrentFirmware(), nullptr)
<< "tx16s firmware must be registered for this test";
}
};
} // namespace
// Valid, in-limit header fields survive a round-trip unchanged, and writing
// the reloaded model reproduces identical bytes (a deterministic fixed point).
TEST_F(ModelYamlRoundTrip, HeaderFieldsSurviveAndAreStable)
{
struct Case { const char* name; const char* labels; };
const std::vector<Case> corpus = {
{"MyModel", "fav,race"},
{"ABCDEFGHIJKLMNO", "alpha,bravo"}, // name exactly at the 15-char limit
{"", ""}, // empty model
{"Plane01", "fav"},
{"Heli3D", "race,fav,park,3d,heli,test"},
};
for (const auto& c : corpus) {
ModelData m;
m.clear();
m.used = true;
m.name = c.name;
m.labels = c.labels;
QByteArray y1;
ModelData m2 = roundTrip(m, y1);
EXPECT_EQ(m.name.str(), m2.name.str()) << "name lost for: " << c.name;
EXPECT_EQ(m.labels.str(), m2.labels.str()) << "labels lost for: " << c.name;
// Fixed point at the field level: reloading the re-serialised model
// yields the same field bytes. (We deliberately do NOT compare the whole
// YAML document -- it round-trips unrelated non-string fields that have
// pre-existing non-idempotencies, e.g. varioData.centerSilent.)
QByteArray y2;
ModelData m3 = roundTrip(m2, y2);
EXPECT_EQ(m2.name.str(), m3.name.str());
EXPECT_EQ(m2.labels.str(), m3.labels.str());
}
}
// An over-length name is truncated to the field capacity on assignment (no
// overflow), and that truncated value is exactly what round-trips.
TEST_F(ModelYamlRoundTrip, OverlongNameTruncatesAndRoundTrips)
{
ModelData m;
m.clear();
m.used = true;
m.name = std::string(40, 'N'); // capacity is MODEL_NAME_LEN (15)
ASSERT_EQ(m.name.size(), (size_t)MODEL_NAME_LEN);
QByteArray y1;
ModelData m2 = roundTrip(m, y1);
EXPECT_EQ(m.name.str(), m2.name.str());
// The truncated value is a fixed point: no further loss or growth.
QByteArray y2;
ModelData m3 = roundTrip(m2, y2);
EXPECT_EQ(m2.name.str(), m3.name.str());
}
+3 -6
View File
@@ -46,8 +46,7 @@ WizMix::WizMix(const GeneralSettings & settings, unsigned int modelId, const Mod
vehicle(NOVEHICLE)
{
memset(name, 0, sizeof(name));
memcpy(name, originalModelData.name, sizeof(name) - 1);
name[sizeof(name) - 1] = '\0';
strncpy(name, originalModelData.name.c_str(), sizeof(name) - 1);
}
void WizMix::maxMixSwitch(char *name, MixData &mix, int channel, int sw, int weight)
@@ -87,7 +86,7 @@ WizMix::operator ModelData()
int throttleChannel = -1;
ModelData model;
model.labels[0] = '\0';
model.labels.clear();
model.used = true;
model.moduleData[0].modelId = modelId;
model.setDefaultInputs(settings);
@@ -96,9 +95,7 @@ WizMix::operator ModelData()
int mixIndex = 0;
int timerIndex = 0;
memset(model.name, 0, sizeof(model.name));
memcpy(model.name, name, sizeof(model.name) - 1);
model.name[sizeof(model.name) - 1] = '\0';
model.name = name;
// Add the channel mixes
for (int i=0; i<WIZ_MAX_CHANNELS; i++ )