File indexing completed on 2024-05-19 05:41:57
0001 // ct_lvtcgn_codegendialog.t.cpp -*-C++-*- 0002 0003 /* 0004 // Copyright 2023 Codethink Ltd <codethink@codethink.co.uk> 0005 // SPDX-License-Identifier: Apache-2.0 0006 // 0007 // Licensed under the Apache License, Version 2.0 (the "License"); 0008 // you may not use this file except in compliance with the License. 0009 // You may obtain a copy of the License at 0010 // 0011 // http://www.apache.org/licenses/LICENSE-2.0 0012 // 0013 // Unless required by applicable law or agreed to in writing, software 0014 // distributed under the License is distributed on an "AS IS" BASIS, 0015 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 0016 // See the License for the specific language governing permissions and 0017 // limitations under the License. 0018 */ 0019 0020 #include <ct_lvtcgn_codegendialog.h> 0021 #include <ct_lvtcgn_generatecode.h> 0022 #include <ct_lvtcgn_testutils.h> 0023 0024 #include <ct_lvttst_fixture_qt.h> 0025 #include <ct_lvttst_tmpdir.h> 0026 0027 #include <QDebug> 0028 #include <QLineEdit> 0029 #include <QPushButton> 0030 #include <QStandardItemModel> 0031 #include <QTreeView> 0032 #include <catch2-local-includes.h> 0033 0034 #pragma push_macro("slots") 0035 #undef slots 0036 #include <pybind11/embed.h> 0037 #include <pybind11/pybind11.h> 0038 #pragma pop_macro("slots") 0039 0040 namespace py = pybind11; 0041 struct PyDefaultGilReleasedContext { 0042 py::scoped_interpreter pyInterp; 0043 py::gil_scoped_release pyGilDefaultReleased; 0044 }; 0045 0046 static const std::string TMPDIR_NAME = "tmp_ct_lvtcgn_codegendialog"; 0047 0048 using namespace Codethink::lvtcgn::gui; 0049 using namespace Codethink::lvtcgn::mdl; 0050 0051 class MockedCodeGenerationDialogDetails : public CodeGenerationDialog::Detail { 0052 public: 0053 QString mockedOutputDir; 0054 QString mockedExecutablePath; 0055 int handleOutputDirEmptyCallCount = 0; 0056 int codeGenerationIterationCallCount = 0; 0057 int handleCodeGenerationErrorCallCount = 0; 0058 0059 protected: 0060 QString getExistingDirectory(QDialog& dialog, QString const& defaultPath = "") override 0061 { 0062 (void) dialog; 0063 return mockedOutputDir; 0064 } 0065 0066 void handleOutputDirEmpty(Ui::CodeGenerationDialogUi& ui) override 0067 { 0068 handleOutputDirEmptyCallCount += 1; 0069 } 0070 0071 void codeGenerationIterationCallback(Ui::CodeGenerationDialogUi& ui, 0072 const CodeGeneration::CodeGenerationStep& step) override 0073 { 0074 (void) step; 0075 codeGenerationIterationCallCount += 1; 0076 } 0077 0078 void handleCodeGenerationError(Ui::CodeGenerationDialogUi& ui, 0079 Codethink::lvtcgn::mdl::CodeGenerationError const& error) override 0080 { 0081 qDebug() << QString::fromStdString(error.message); 0082 handleCodeGenerationErrorCallCount += 1; 0083 } 0084 0085 [[nodiscard]] QString executablePath() const override 0086 { 0087 return mockedExecutablePath; 0088 } 0089 }; 0090 0091 TEST_CASE_METHOD(QTApplicationFixture, "codegen dialog test") 0092 { 0093 PyDefaultGilReleasedContext _pyDefaultGilReleasedContext; 0094 0095 auto tmpDir = TmpDir{TMPDIR_NAME}; 0096 auto outputDir = tmpDir.createDir("output"); 0097 (void) tmpDir.createDir("python/codegeneration/testingscript/"); 0098 0099 auto contentProvider = FakeContentProvider{}; 0100 auto dialogDetailPtr = std::make_unique<MockedCodeGenerationDialogDetails>(); 0101 auto& dialogDetail = *dialogDetailPtr; 0102 dialogDetail.mockedExecutablePath = QString::fromStdString(tmpDir.path().string()); 0103 auto dialog = CodeGenerationDialog{contentProvider, std::move(dialogDetailPtr)}; 0104 0105 auto *physicalEntitiesTree = dialog.findChild<QTreeView *>("physicalEntitiesTree"); 0106 auto *treeModel = qobject_cast<QStandardItemModel *>(physicalEntitiesTree->model()); 0107 auto getTextFrom = [&treeModel](QModelIndex const& i) { 0108 return treeModel->itemFromIndex(i)->text().toStdString(); 0109 }; 0110 auto isChecked = [&treeModel](QModelIndex const& i) { 0111 return treeModel->itemFromIndex(i)->checkState() == Qt::Checked; 0112 }; 0113 0114 REQUIRE(treeModel->rowCount() == 3); 0115 REQUIRE(getTextFrom(treeModel->index(0, 0)) == "somepkg_a"); 0116 REQUIRE(getTextFrom(treeModel->index(1, 0)) == "somepkg_b"); 0117 REQUIRE(getTextFrom(treeModel->index(2, 0)) == "somepkg_c"); 0118 REQUIRE(getTextFrom(treeModel->index(0, 0, treeModel->index(2, 0))) == "component_a"); 0119 REQUIRE(getTextFrom(treeModel->index(1, 0, treeModel->index(2, 0))) == "component_b"); 0120 0121 REQUIRE(isChecked(treeModel->index(0, 0))); 0122 REQUIRE(!isChecked(treeModel->index(1, 0))); 0123 REQUIRE(isChecked(treeModel->index(2, 0))); 0124 REQUIRE(!isChecked(treeModel->index(0, 0, treeModel->index(2, 0)))); 0125 REQUIRE(isChecked(treeModel->index(1, 0, treeModel->index(2, 0)))); 0126 0127 auto testAndSetInputContents = [&dialog](const QString& inputFieldName, 0128 const QString& btnFieldName, 0129 QString& mockedRef, 0130 QString const& contents) { 0131 auto *input = dialog.findChild<QLineEdit *>(inputFieldName); 0132 auto *button = dialog.findChild<QPushButton *>(btnFieldName); 0133 REQUIRE(input != nullptr); 0134 REQUIRE(button != nullptr); 0135 0136 mockedRef = contents; 0137 button->click(); 0138 REQUIRE(input->text() == contents); 0139 0140 // No return value means input will not be changed 0141 mockedRef = ""; 0142 button->click(); 0143 REQUIRE(input->text() == contents); 0144 }; 0145 0146 auto *runCodeGenerationBtn = dialog.findChild<QPushButton *>("runCodeGenerationBtn"); 0147 REQUIRE(runCodeGenerationBtn != nullptr); 0148 0149 REQUIRE(dialogDetail.handleOutputDirEmptyCallCount == 0); 0150 REQUIRE(dialogDetail.handleCodeGenerationErrorCallCount == 0); 0151 REQUIRE(dialogDetail.codeGenerationIterationCallCount == 0); 0152 0153 runCodeGenerationBtn->click(); 0154 0155 REQUIRE(dialogDetail.handleOutputDirEmptyCallCount == 1); 0156 REQUIRE(dialogDetail.handleCodeGenerationErrorCallCount == 0); 0157 REQUIRE(dialogDetail.codeGenerationIterationCallCount == 0); 0158 0159 testAndSetInputContents("outputDirInput", 0160 "findOutputDirBtn", 0161 dialogDetail.mockedOutputDir, 0162 QString::fromStdString(outputDir.string())); 0163 { 0164 const std::string SCRIPT_CONTENTS = 0165 "\ndef buildPhysicalEntity(*args, **kwargs):" 0166 "\n bad code!" 0167 "\n"; 0168 (void) tmpDir.createTextFile("python/codegeneration/testingscript/codegenerator.py", SCRIPT_CONTENTS); 0169 runCodeGenerationBtn->click(); 0170 } 0171 REQUIRE(dialogDetail.handleOutputDirEmptyCallCount == 1); 0172 REQUIRE(dialogDetail.handleCodeGenerationErrorCallCount == 1); 0173 REQUIRE(dialogDetail.codeGenerationIterationCallCount == 0); 0174 0175 { 0176 const std::string SCRIPT_CONTENTS = 0177 "\ndef buildPhysicalEntity(*args, **kwargs):" 0178 "\n pass" 0179 "\n"; 0180 (void) tmpDir.createTextFile("python/codegeneration/testingscript/codegenerator.py", SCRIPT_CONTENTS); 0181 runCodeGenerationBtn->click(); 0182 } 0183 REQUIRE(dialogDetail.handleOutputDirEmptyCallCount == 1); 0184 REQUIRE(dialogDetail.handleCodeGenerationErrorCallCount == 1); 0185 REQUIRE(dialogDetail.codeGenerationIterationCallCount == 3); 0186 } 0187 0188 TEST_CASE_METHOD(QTApplicationFixture, "codegen find entities") 0189 { 0190 PyDefaultGilReleasedContext _pyDefaultGilReleasedContext; 0191 0192 auto tmp_dir = TmpDir{TMPDIR_NAME}; 0193 auto outputDir = tmp_dir.createDir("output"); 0194 0195 auto contentProvider = FakeContentProvider{}; 0196 auto dialogDetailPtr = std::make_unique<MockedCodeGenerationDialogDetails>(); 0197 auto& dialogDetail = *dialogDetailPtr; 0198 dialogDetail.mockedExecutablePath = QString::fromStdString(tmp_dir.path().string()); 0199 auto dialog = CodeGenerationDialog{contentProvider, std::move(dialogDetailPtr)}; 0200 auto *physicalEntitiesTree = dialog.findChild<QTreeView *>("physicalEntitiesTree"); 0201 auto *treeModel = qobject_cast<QStandardItemModel *>(physicalEntitiesTree->model()); 0202 0203 auto itemIsMarkedWithColor = [](QStandardItem *item, QString const& color) { 0204 return item->data(Qt::DisplayRole).toString().contains(color); 0205 }; 0206 0207 REQUIRE_FALSE(physicalEntitiesTree->isExpanded(treeModel->index(2, 0))); 0208 dialog.findChild<QLineEdit *>("searchValue")->setText("component"); 0209 REQUIRE(physicalEntitiesTree->isExpanded(treeModel->index(2, 0))); 0210 auto *pgkItem = treeModel->itemFromIndex(treeModel->index(2, 0)); 0211 REQUIRE(itemIsMarkedWithColor(pgkItem->child(0), "orange")); 0212 REQUIRE(itemIsMarkedWithColor(pgkItem->child(1), "yellow")); 0213 0214 dialog.findChild<QPushButton *>("searchGoToNextBtn")->click(); 0215 REQUIRE(itemIsMarkedWithColor(pgkItem->child(0), "yellow")); 0216 REQUIRE(itemIsMarkedWithColor(pgkItem->child(1), "orange")); 0217 0218 dialog.findChild<QPushButton *>("searchGoToPrevBtn")->click(); 0219 REQUIRE(itemIsMarkedWithColor(pgkItem->child(0), "orange")); 0220 REQUIRE(itemIsMarkedWithColor(pgkItem->child(1), "yellow")); 0221 0222 dialog.findChild<QLineEdit *>("searchValue")->setText("pkg"); 0223 REQUIRE_FALSE(itemIsMarkedWithColor(pgkItem->child(0), "yellow")); 0224 REQUIRE_FALSE(itemIsMarkedWithColor(pgkItem->child(0), "orange")); 0225 REQUIRE_FALSE(itemIsMarkedWithColor(pgkItem->child(1), "yellow")); 0226 REQUIRE_FALSE(itemIsMarkedWithColor(pgkItem->child(1), "orange")); 0227 REQUIRE(itemIsMarkedWithColor(pgkItem, "yellow")); 0228 dialog.findChild<QPushButton *>("searchGoToPrevBtn")->click(); 0229 REQUIRE(itemIsMarkedWithColor(pgkItem, "orange")); 0230 } 0231 0232 TEST_CASE_METHOD(QTApplicationFixture, "codegen selections") 0233 { 0234 PyDefaultGilReleasedContext _pyDefaultGilReleasedContext; 0235 0236 auto tmp_dir = TmpDir{TMPDIR_NAME}; 0237 auto outputDir = tmp_dir.createDir("output"); 0238 0239 auto contentProvider = FakeContentProvider{}; 0240 auto dialogDetailPtr = std::make_unique<MockedCodeGenerationDialogDetails>(); 0241 auto& dialogDetail = *dialogDetailPtr; 0242 dialogDetail.mockedExecutablePath = QString::fromStdString(tmp_dir.path().string()); 0243 auto dialog = CodeGenerationDialog{contentProvider, std::move(dialogDetailPtr)}; 0244 auto *physicalEntitiesTree = dialog.findChild<QTreeView *>("physicalEntitiesTree"); 0245 auto *treeModel = qobject_cast<QStandardItemModel *>(physicalEntitiesTree->model()); 0246 0247 auto requireState = [&treeModel](QModelIndex const& i, Qt::CheckState s) { 0248 REQUIRE(treeModel->itemFromIndex(i)->checkState() == s); 0249 }; 0250 auto setState = [&treeModel](QModelIndex const& i, Qt::CheckState s) { 0251 treeModel->itemFromIndex(i)->setCheckState(s); 0252 }; 0253 0254 dialog.show(); 0255 auto const& parent = treeModel->index(2, 0); 0256 physicalEntitiesTree->expand(parent); 0257 auto const& child1 = treeModel->index(0, 0, treeModel->index(2, 0)); 0258 auto const& child2 = treeModel->index(1, 0, treeModel->index(2, 0)); 0259 0260 // Puts the tree in a known state before starting the test 0261 setState(parent, Qt::Unchecked); 0262 setState(child1, Qt::Unchecked); 0263 setState(child2, Qt::Unchecked); 0264 0265 INFO("Selecting a parent item also updates the children"); 0266 INFO("parent Checked"); 0267 setState(parent, Qt::Checked); 0268 requireState(parent, Qt::Checked); 0269 requireState(child1, Qt::Checked); 0270 requireState(child2, Qt::Checked); 0271 0272 INFO("parent Unchecked"); 0273 setState(parent, Qt::Unchecked); 0274 requireState(parent, Qt::Unchecked); 0275 requireState(child1, Qt::Unchecked); 0276 requireState(child2, Qt::Unchecked); 0277 0278 INFO("Selecting a child item also updates the parent"); 0279 INFO("child1 Checked"); 0280 setState(child1, Qt::Checked); 0281 requireState(parent, Qt::PartiallyChecked); 0282 requireState(child1, Qt::Checked); 0283 requireState(child2, Qt::Unchecked); 0284 INFO("child2 Checked"); 0285 setState(child2, Qt::Checked); 0286 requireState(parent, Qt::Checked); 0287 requireState(child1, Qt::Checked); 0288 requireState(child2, Qt::Checked); 0289 0290 INFO("child1 Unchecked"); 0291 setState(child1, Qt::Unchecked); 0292 requireState(parent, Qt::PartiallyChecked); 0293 requireState(child1, Qt::Unchecked); 0294 requireState(child2, Qt::Checked); 0295 INFO("child2 Unchecked"); 0296 setState(child2, Qt::Unchecked); 0297 requireState(parent, Qt::Unchecked); 0298 requireState(child1, Qt::Unchecked); 0299 requireState(child2, Qt::Unchecked); 0300 }