/* * 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. */ // Custom-screen and topbar-widget layout data (CustomScreenData / // TopBarPersistentData) is stored per-process in static globals rather than // inside ModelData itself (see ModelData::getScreenData()/getTopbarData()), // because that data can be large and only the currently loaded model's copy // needs to be resident. Any code that reads or writes a *full* ModelData // buffer -- as opposed to the header-only PartialModel -- for a model other // than the one currently loaded therefore risks reading/writing those // globals on behalf of the wrong model. // // Label management (renaming/adding/removing labels) routinely touches // models other than the currently loaded one. These tests exercise that // label-management path through its public ModelMap API and assert the // currently loaded model's screen/topbar data is left alone, regardless of // what label edits are made to other models on disk. #include "gtests.h" #include "location.h" #include #include "storage/modelslist.h" #include "storage/sdcard_common.h" #include "storage/sdcard_yaml.h" #include "storage/yaml/yaml_datastructs.h" #if defined(COLORLCD) namespace fs = std::filesystem; class ModelMapFsTest : public ::testing::Test { protected: fs::path scratchDir; void SetUp() override { scratchDir = fs::temp_directory_path() / fs::path("edgetx-gtest-modelslist-labels"); std::error_code ec; fs::remove_all(scratchDir, ec); fs::create_directories(scratchDir / "MODELS", ec); ASSERT_FALSE(ec) << "could not create scratch MODELS directory"; simuFatfsSetPaths(scratchDir.string().c_str(), nullptr); modelslist.clear(); modelslabels.clear(); memclear(&g_model, sizeof(g_model)); } void TearDown() override { modelslist.clear(); modelslabels.clear(); simuFatfsSetPaths(TESTS_PATH, nullptr); std::error_code ec; fs::remove_all(scratchDir, ec); } // Writes a standalone model YAML file to /MODELS/ // with the given label list and screen/topbar marker values. This must be // called *before* the active model's own screen/topbar markers are set on // g_model: since getScreenData()/getTopbarData() are shared globals, using // them here to build this "other" model's fixture is itself writing // through the same storage g_model uses, exactly like the code under test // does. The test bodies set the active model's markers afterwards, which // simulates that model actually being loaded, and is what the assertions // check survives the label edit untouched. void writeFixtureModel(const char* filename, const char* labels, const char* screenLayoutId, const char* widgetName) { ModelData model; memclear(&model, sizeof(model)); // A real model always has a name; an entirely-default header (no name, // no labels, zeroed modelId) is omitted from the written YAML altogether // as a compaction optimisation, which would leave this fixture without // a "header:" section at all. Giving it a name keeps the fixture // realistic and avoids that unrelated edge case. strAppend(model.header.name, filename, LEN_MODEL_NAME); strAppend(model.header.labels, labels, LABELS_LENGTH - 1); model.getScreenData(0)->LayoutId = screenLayoutId; model.getTopbarData()->zones[0].widgetName = widgetName; char path[256]; getModelPath(path, filename); ASSERT_EQ(writeFileYaml(path, get_modeldata_nodes(), (uint8_t*)&model, 0), (const char*)nullptr) << "failed writing fixture model " << filename; } }; TEST_F(ModelMapFsTest, RenamingLabelOnOtherModelLeavesActiveScreenDataUntouched) { // Seed a second, non-active model on disk with a label and screen/topbar // data distinct from the active model's. writeFixtureModel("model0002.yml", "Foo", "OtherLayout", "OtherWidget"); ModelCell* other = modelslist.addModel("model0002.yml", false); ASSERT_NE(other, nullptr); ASSERT_FALSE(modelslabels.addLabelToModel("Foo", other, false)); // Now "load" the active model: register its cell and mark it current, and // set its screen/topbar data to its own values. ModelCell* active = modelslist.addModel("model0001.yml", false); ASSERT_NE(active, nullptr); modelslist.setCurrentModel(active); g_model.getScreenData(0)->LayoutId = "ActiveLayout"; g_model.getTopbarData()->zones[0].widgetName = "ActiveWidget"; // Rename a label that only exists on the other, non-active model. modelslabels.renameLabel("Foo", "Bar"); // The active model's screen/topbar data must be unaffected by editing an // unrelated model's labels. EXPECT_STREQ(g_model.getScreenData(0)->LayoutId.c_str(), "ActiveLayout"); EXPECT_STREQ(g_model.getTopbarData()->zones[0].widgetName.c_str(), "ActiveWidget"); // The other model's file on disk should reflect the renamed label. Read // back only the header (PartialModel), not a full ModelData, so this // verification step doesn't itself touch the shared screen/topbar globals. PartialModel partial; memclear(&partial, sizeof(partial)); readModelYaml("model0002.yml", (uint8_t*)&partial, sizeof(PartialModel)); EXPECT_STREQ(partial.header.labels, "Bar"); } TEST_F(ModelMapFsTest, AddingLabelToOtherModelWithFileUpdateLeavesActiveScreenDataUntouched) { writeFixtureModel("model0002.yml", "", "OtherLayout", "OtherWidget"); ModelCell* other = modelslist.addModel("model0002.yml", false); ASSERT_NE(other, nullptr); ModelCell* active = modelslist.addModel("model0001.yml", false); ASSERT_NE(active, nullptr); modelslist.setCurrentModel(active); g_model.getScreenData(0)->LayoutId = "ActiveLayout"; g_model.getTopbarData()->zones[0].widgetName = "ActiveWidget"; // update=true drives ModelMap::updateModelFile(), the second call site // that reads/writes a non-active model's file on disk. EXPECT_FALSE(modelslabels.addLabelToModel("Baz", other, true)); EXPECT_STREQ(g_model.getScreenData(0)->LayoutId.c_str(), "ActiveLayout"); EXPECT_STREQ(g_model.getTopbarData()->zones[0].widgetName.c_str(), "ActiveWidget"); PartialModel partial; memclear(&partial, sizeof(partial)); readModelYaml("model0002.yml", (uint8_t*)&partial, sizeof(PartialModel)); EXPECT_STREQ(partial.header.labels, "Baz"); } #endif // defined(COLORLCD)