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 }