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 }