File indexing completed on 2024-05-19 05:41:57

0001 // ct_lvtcgn_generatecode.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_generatecode.h>
0021 #include <ct_lvtcgn_testutils.h>
0022 
0023 #include <catch2-local-includes.h>
0024 #include <ct_lvttst_tmpdir.h>
0025 
0026 #pragma push_macro("slots")
0027 #undef slots
0028 #include <pybind11/embed.h>
0029 #include <pybind11/pybind11.h>
0030 #pragma pop_macro("slots")
0031 
0032 #include <fstream>
0033 #include <memory>
0034 #include <string>
0035 
0036 using namespace std::string_literals;
0037 static const std::string TMPDIR_NAME = "tmp_ct_lvtcgn_generatecode";
0038 
0039 using namespace Codethink::lvtcgn::mdl;
0040 
0041 namespace py = pybind11;
0042 struct PyDefaultGilReleasedContext {
0043     py::scoped_interpreter pyInterp;
0044     py::gil_scoped_release pyGilDefaultReleased;
0045 };
0046 
0047 TEST_CASE("Basic code generation")
0048 {
0049     PyDefaultGilReleasedContext _pyDefaultGilReleasedContext;
0050 
0051     auto tmp_dir = TmpDir{TMPDIR_NAME};
0052 
0053     const std::string SCRIPT_CONTENTS = R"(
0054 def beforeProcessEntities(output_dir, user_ctx):
0055     with open(output_dir + '/output.txt', 'a+') as f:
0056         f.write(f'BEFORE process entities called.\n')
0057     user_ctx['callcount'] = 0
0058 
0059 def buildPhysicalEntity(cgn, entity, output_dir, user_ctx):
0060     with open(output_dir + '/output.txt', 'a+') as f:
0061         f.write(f'({entity.name()}, {entity.type()});')
0062     user_ctx['callcount'] += 1
0063 
0064 def afterProcessEntities(output_dir, user_ctx):
0065     user_ctx['callcount'] += 1
0066     with open(output_dir + '/output.txt', 'a+') as f:
0067         f.write(f'\nAFTER process entities called. {user_ctx["callcount"]}')
0068 )";
0069     auto scriptPath = tmp_dir.createTextFile("some_script.py", SCRIPT_CONTENTS);
0070     auto outputDir = tmp_dir.createDir("out");
0071 
0072     auto contentProvider = FakeContentProvider{};
0073     auto result = CodeGeneration::generateCodeFromScript(scriptPath.string(), outputDir.string(), contentProvider);
0074     if (result.has_error()) {
0075         INFO("Error message: " + result.error().message);
0076     }
0077     REQUIRE(!result.has_error());
0078 
0079     auto resultStream = std::ifstream(outputDir / "output.txt");
0080     auto output = std::string((std::istreambuf_iterator<char>(resultStream)), (std::istreambuf_iterator<char>()));
0081     REQUIRE(output == R"(BEFORE process entities called.
0082 (somepkg_a, DiagramType.Package);(somepkg_c, DiagramType.Package);(component_b, DiagramType.Component);
0083 AFTER process entities called. 4)");
0084 }
0085 
0086 TEST_CASE("Code generation errors")
0087 {
0088     auto tmpDir = TmpDir{TMPDIR_NAME};
0089     auto contentProvider = FakeContentProvider{};
0090 
0091     // Provide a bad file
0092     {
0093         PyDefaultGilReleasedContext _pyDefaultGilReleasedContext;
0094         auto result = CodeGeneration::generateCodeFromScript("badfile.py", ".", contentProvider);
0095         REQUIRE(result.has_error());
0096         REQUIRE(result.error().message == "ModuleNotFoundError: No module named 'badfile'");
0097     }
0098 
0099     // Provide a good file, but no required function
0100     {
0101         PyDefaultGilReleasedContext _pyDefaultGilReleasedContext;
0102         const std::string SCRIPT_CONTENTS = R"(
0103 def f(x):
0104     pass
0105 )";
0106         auto scriptPath = tmpDir.createTextFile("some_script.py", SCRIPT_CONTENTS);
0107 
0108         auto result = CodeGeneration::generateCodeFromScript(scriptPath.string(), ".", contentProvider);
0109         REQUIRE(result.has_error());
0110         REQUIRE(result.error().message == "Expected function named buildPhysicalEntity");
0111     }
0112 
0113     // Python code invalid syntax
0114     {
0115         PyDefaultGilReleasedContext _pyDefaultGilReleasedContext;
0116         const std::string SCRIPT_CONTENTS = R"(
0117 def f(x):
0118     nonsense code
0119 )";
0120         auto scriptPath = tmpDir.createTextFile("some_script.py", SCRIPT_CONTENTS);
0121 
0122         auto result = CodeGeneration::generateCodeFromScript(scriptPath.string(), ".", contentProvider);
0123         REQUIRE(result.has_error());
0124         REQUIRE(result.error().message == "SyntaxError: invalid syntax (some_script.py, line 3)");
0125     }
0126 }
0127 
0128 TEST_CASE("Code generation python API")
0129 {
0130     PyDefaultGilReleasedContext _pyDefaultGilReleasedContext;
0131 
0132     auto tmpDir = TmpDir{TMPDIR_NAME};
0133 
0134     const std::string SCRIPT_CONTENTS = R"(
0135 def buildPhysicalEntity(cgn, entity, output_dir, user_ctx):
0136     with open(output_dir + '/output.txt', 'a+') as f:
0137         f.write(f'(')
0138 
0139         # TEST name()
0140         f.write(f'{entity.name()}, ')
0141 
0142         # TEST type()
0143         f.write(f'{entity.type()}, ')
0144 
0145         # TEST parent()
0146         if entity.parent():
0147             f.write(f'{entity.parent().name()}, ')
0148         else:
0149             f.write(f'<no parent>, ')
0150 
0151         # TEST forwardDependencies()
0152         f.write(f'deps = [')
0153         for dep in entity.forwardDependencies():
0154             f.write(f'{dep.name()}, ')
0155         f.write(f']')
0156 
0157         f.write(f')\n')
0158 )";
0159     auto scriptPath = tmpDir.createTextFile("some_script.py", SCRIPT_CONTENTS);
0160     auto outputDir = tmpDir.createDir("out");
0161 
0162     auto contentProvider = FakeContentProvider{};
0163     auto result = CodeGeneration::generateCodeFromScript(scriptPath.string(), outputDir.string(), contentProvider);
0164     if (result.has_error()) {
0165         INFO("Error message: " + result.error().message);
0166     }
0167     REQUIRE(!result.has_error());
0168 
0169     auto resultStream = std::ifstream(outputDir / "output.txt");
0170     auto output = std::string((std::istreambuf_iterator<char>(resultStream)), (std::istreambuf_iterator<char>()));
0171     REQUIRE(output == R"((somepkg_a, DiagramType.Package, <no parent>, deps = [])
0172 (somepkg_c, DiagramType.Package, <no parent>, deps = [])
0173 (component_b, DiagramType.Component, somepkg_c, deps = [component_a, ])
0174 )");
0175 }